feat: экспоненциальный backoff (providers[].backoff / upstreams[].backoff)
Повторяющиеся сбои апстрима (5xx/429/402 и сетевые ошибки) увязываются экспоненциальным откатом: первая ошибка — отдых 1s, далее 2s, 4s, … до потолка из конфига (ISO-8601: P1M, PT15M…); успешный запрос сбрасывает счётчик. Приоритет: upstreams[].backoff (свой счётчик на модель) над providers[].backoff (общий счётчик на все модели провайдера). - Backoff.kt: BackoffGuard (Mutex/@Volatile, kotlinx-datetime) + BackoffRegistry (скоупы провайдер/модель); - pickFreeUpstream пропускает апстримы в откате (лог «~Ns — пропускаю»); - все апстримы модели в откате → 503 «all upstreams cooling, retry in Ns» + заголовок Retry-After; - некорректный ISO-8601 в backoff — warn + игнор поля; - BackoffTest (8 тестов) + обновлённые тесты pickFreeUpstream; - CONFIG.md/README/TESTING.md. Проверено: jvmTest, compileKotlinLinuxX64, linuxX64Test.
This commit is contained in:
@@ -46,6 +46,10 @@
|
||||
модели (фейловер); если свободных не осталось — отдаём `503` от прокси.
|
||||
`402` (insufficient balance / исчерпан лимит токенов) — ошибка аккаунта
|
||||
провайдера, но фейловер имеет смысл: следующий апстрим может быть платёжеспособен.
|
||||
Повторяющиеся сбои уводятся из ротации экспоненциальным **backoff** (поле
|
||||
`backoff`, ниже): упавший апстрим «отдыхает» 1s, 2s, 4s, … до заданного потолка,
|
||||
и прокси его не трогает. Если **все** апстримы модели в откате — `503` с
|
||||
заголовком `Retry-After`.
|
||||
|
||||
## Формат файла
|
||||
|
||||
@@ -79,6 +83,8 @@ providers:
|
||||
patch: # уровень провайдера: ко всем его запросам
|
||||
provider:
|
||||
allow_fallbacks: false
|
||||
backoff: P1M # опционально; потолок экспоненциального backoff
|
||||
# на весь провайдер (ISO-8601: P1M, PT15M, P1D…)
|
||||
|
||||
- id: local-llama
|
||||
url: "http://10.0.0.5:8080/v1"
|
||||
@@ -95,6 +101,8 @@ upstreams:
|
||||
patch: # уровень апстрима
|
||||
provider:
|
||||
ignore: [deepseek]
|
||||
backoff: PT15M # опционально; собственный backoff этой модели
|
||||
# (приоритет над backoff провайдера)
|
||||
|
||||
- id: local-qwen
|
||||
provider: local-llama
|
||||
@@ -258,6 +266,58 @@ providers:
|
||||
# reasoning_empty_ok: true # опционально; дефолт false
|
||||
```
|
||||
|
||||
### Экспоненциальный backoff (`backoff`)
|
||||
|
||||
Если апстрим регулярно ошибается (`5xx`/`429`/`402` или сетевые ошибки),
|
||||
прокси не бьёт по нему на каждом запросе, а отправляет в **откат**
|
||||
(cooldown) — это паттерн экспоненциального бэкоффа / circuit breaker:
|
||||
чем дольше сервис молчит, тем дольше мы к нему не ходим. Пока апстрим в
|
||||
откате, роутер пропускает его и берёт следующий по списку модели.
|
||||
|
||||
| Поле | Тип / дефолт | Значение |
|
||||
|---|---|---|
|
||||
| `providers[].backoff` | ISO-8601-длительность / отсутствует | Потолок отката **на весь провайдер**: счётчик общий для всех его моделей |
|
||||
| `upstreams[].backoff` | ISO-8601-длительность / отсутствует | Потолок отката **на конкретную модель**: счётчик индивидуальный |
|
||||
|
||||
**Приоритет — модели.** Если `upstreams[].backoff` задан, у этой модели
|
||||
собственный счётчик и потолок (провайдерский `backoff` на неё не действует).
|
||||
Если у модели не задан, но задан у провайдера — счётчик общий на провайдера:
|
||||
сбой на одной модели охлаждает и все остальные модели этого провайдера.
|
||||
Если не задан нигде — backoff для этого апстрима выключен (остаются только
|
||||
конкурентность и фейловер).
|
||||
|
||||
Поведение:
|
||||
|
||||
- **первая** ошибка → отдых 1s; каждая следующая **удваивает** интервал
|
||||
(2s, 4s, 8s, …) до заданного потолка (cap);
|
||||
- **успешный** запрос сбрасывает счётчик (откат и удвоение начинаются заново);
|
||||
- апстрим в откате пропускается при выборе (лог:
|
||||
`upstream=<id> в backoff-откате (~Ns) — пропускаю`);
|
||||
- если **все** апстримы модели в откате — прокси отдаёт `503`
|
||||
(`all upstreams cooling, retry in Ns`) с заголовком `Retry-After: N`
|
||||
(секунд до выхода первого апстрима из отката).
|
||||
|
||||
Значение — ISO-8601-длительность (формат, в котором сериализуется
|
||||
`kotlin.time.Duration`): `PT30M` (30 минут), `PT1H15M`, `P1D` (сутки),
|
||||
`P1M` (месяц). Некорректное значение — предупреждение в лог и поле
|
||||
просто игнорируется.
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
- id: routerai
|
||||
url: "https://routerai.ru/api/v1"
|
||||
backoff: P1M # потолок на весь провайдер
|
||||
|
||||
upstreams:
|
||||
- id: routerai-gpt4o
|
||||
provider: routerai
|
||||
model: gpt-4o
|
||||
backoff: PT15M # у модели своё: потолок 15m, провайдерский P1M не действует
|
||||
```
|
||||
|
||||
При старте выводится, что настроено:
|
||||
`[llm-proxy] backoff: upstreams=routerai-gpt4o=PT15M providers=routerai=P1M`.
|
||||
|
||||
### Пример сборки тела (многослойный `patch`)
|
||||
|
||||
Берём модель `my-gpt` (из примера выше), маршрут уходит на апстрим
|
||||
@@ -342,6 +402,7 @@ data class ProviderConf(
|
||||
val think_tags: String? = null,
|
||||
val reasoning_field: String? = null,
|
||||
val reasoning_empty_ok: Boolean = false,
|
||||
val backoff: Duration? = null, // потолок backoff на провайдера (ISO-8601)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -351,6 +412,8 @@ data class UpstreamConf(
|
||||
val model: String, // реальное имя модели у провайдера
|
||||
val max_concurrency: Int? = null,// опционально; null/0 = безлимит
|
||||
val patch: JsonObject? = null, // к запросам этой апстрим-модели
|
||||
val think_tags: String? = null, // переопределение think-режима модели
|
||||
val backoff: Duration? = null, // потолок backoff на модель (приоритет над провайдерским)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -511,6 +574,12 @@ fun release(u: UpstreamConf) = active.getValue(u.id).decrementAndGet()
|
||||
освобождён слот `active[id]=N/limit`.
|
||||
- **Ошибка апстрима** (не `5xx`/`429`/`402`, а сетевая/таймаут): как сейчас — лог с
|
||||
сообщением.
|
||||
- **Backoff**: при фейловере строка дополняется интервалом отдыха
|
||||
(`(backoff: отдых PT…)`); при пропуске апстрима в откате —
|
||||
`upstream=<id> в backoff-откате (~Ns) — пропускаю`; при старте — список
|
||||
настроенных backoff (`backoff: upstreams=… providers=…`).
|
||||
- **Все в откате** — `503 all upstreams cooling, retry in Ns` для
|
||||
`model=<витрина>` с заголовком `Retry-After: N`.
|
||||
|
||||
Формат строки лога — один префикс `[llm-proxy]`, как сейчас, чтобы не ломать
|
||||
существующий парсинг логов (если он есть).
|
||||
|
||||
Reference in New Issue
Block a user