Resilience Guide (Русский)
Необязательный глобальный период ожидания провайдера (оконный шлюз)
Заголовок раздела «Необязательный глобальный период ожидания провайдера (оконный шлюз)»Четвёртый, необязательный уровень (PROVIDER_COOLDOWN_ENABLED, по умолчанию отключён) хранит
между запросами в open-sse/services/providerCooldownTracker.ts сведения о провайдерах, у которых происходят сбои; эти сведения используются при
определении целей комбинированной маршрутизации, чтобы последовательные комбинированные запросы не пытались снова обращаться к провайдеру, у которого только что
произошёл сбой. Записи уровня провайдера учитывают оконный шлюз PROVIDER_PROFILES:
| Профиль | срабатывает после (providerFailureThreshold) |
в течение (providerFailureWindowMs) |
период ожидания (providerCooldownMs) |
|---|---|---|---|
| OAuth | 10 |
15min |
5min |
| API-ключ | 15 |
30min |
10min |
Пока порог не достигнут, провайдер не считается находящимся в периоде ожидания; успешный запрос очищает
окно. Для записей уровня подключения (provider:connectionId) вместо этого сохраняется
экспоненциальная задержка minRetryCooldownMs → maxRetryCooldownMs. Переопределения:
OMNIROUTE_PROVIDER_BREAKER_{OAUTH,API_KEY}_{FAILURE_THRESHOLD,FAILURE_WINDOW_MS,COOLDOWN_MS}.
Защита от регрессий: tests/unit/provider-cooldown-window-gate.test.ts.
2. Период ожидания подключения
Заголовок раздела «2. Период ожидания подключения»Область действия: одно подключение / одна учётная запись / один ключ провайдера.
Назначение: пропускать один неработающий ключ, пока другие подключения того же провайдера продолжают обслуживать запросы.
Реализация:
- Пометка как недоступного:
src/sse/services/auth.ts::markAccountUnavailable() - Выбор:
getProviderCredentials*в том же файле - Расчёт периода ожидания:
open-sse/services/accountFallback.ts::checkFallbackError() - Настройки:
src/lib/resilience/settings.ts
Поля для каждого подключения:
rateLimitedUntil— временная метка окончания периода ожиданияtestStatus: "unavailable"lastError,lastErrorType,errorCodebackoffLevel— счётчик экспоненциальной задержки
Периоды ожидания по умолчанию:
- Базовый для OAuth: 5 с
- Базовый для API-ключа: 3 с
- Для ответа 429 при использовании API-ключа: предпочтительно используются переданные вышестоящим сервисом заголовки
Retry-After/сброса или текст с разбираемым временем сброса - Задержка:
baseCooldownMs * 2 ** failureIndex
Защита от лавины запросов: не позволяет одновременным сбоям чрезмерно продлевать период ожидания или дважды увеличивать backoffLevel.
Терминальные состояния (НЕ периоды ожидания):
banned— устанавливается при обнаружении ключевого слова блокировки / блокировки учётной записи (см. BAN_DETECTION), а также после трёх последовательных отказов вышестоящего сервиса на отдельные запросы (request_rejected, например Anthropic OAuth 403 “Запрос не разрешён” —open-sse/services/requestRejectedStreak.ts); одиночный отказ лишь переводит подключение в режим ожиданияexpired(переходит в терминальное состояние после ограниченного числа повторных попыток —EXPIRED_RETRY_MAX = 3с экспоненциальной задержкой, — поэтому временные ошибки OAuth могут устраниться самостоятельно до окончательной деактивации учётной записи)credits_exhausted
Эти состояния сохраняются до изменения учётных данных или их сброса оператором. Не перезаписывайте терминальные состояния временным состоянием ожидания.
Отложенное восстановление: когда время rateLimitedUntil проходит, подключение снова становится доступным для выбора. После успешного использования clearAccountError() очищает все поля ошибок.
Ограничение использования Claude OAuth: низкоприоритетный режим + сброс лимита сеанса
Заголовок раздела «Ограничение использования Claude OAuth: низкоприоритетный режим + сброс лимита сеанса»Область действия: одно подключение по подписке Claude (OAuth). Обе функции включаются отдельно для каждого
подключения (Изменить подключение → раздел Claude → lowPriorityMode / autoLimitReset в
providerSpecificData, обе по умолчанию отключены) и соответствуют командам Claude Code /low-priority и
/limit-reset (сетевой контракт зафиксирован на основе Claude Code 2.1.263).
Реализация:
- Конечный автомат + классификация ответов:
open-sse/services/claudeLowPriority.ts - Клиент проверки статуса/запроса сброса:
open-sse/services/claudeLimitReset.ts - Перехватчик исполнителя (добавление заголовка + повторная попытка с той же учётной записью):
open-sse/executors/base.ts::execute() - Сохранение настроек явного включения:
src/lib/providers/requestDefaults.ts::normalizeProviderSpecificData()
Условие срабатывания: достижение 5-часового лимита использования — ответ 429, заголовки которого содержат
anthropic-ratelimit-unified-status: rejected и, если учётная запись соответствует требованиям,
anthropic-ratelimit-unified-slow-offer: treatment. До первого ответа 429 при достижении лимита ничего не отправляется;
пакетный ответ 429 без унифицированных заголовков обрабатывается обычным механизмом периода ожидания.
Низкоприоритетный режим (lowPriorityMode):
- При ответе 429 из-за достижения лимита исполнитель принимает предложение и немедленно повторяет запрос с той же
учётной записью, добавляя
anthropic-usage-limit: slow; режим остаётся активным до объявленного времениanthropic-ratelimit-unified-reset(+60 с дополнительного времени), и каждый запрос в этом временном окне содержит этот заголовок. Перехваченный ответ 429 никогда не достигаетhandleChatCore, поэтому подключение не переводится в режим ожидания и не заменяется другим. anthropic-ratelimit-unified-slow-statusв последующих ответах:active/not_neededсохраняют режим; приslot_busy(429) или529ожидается указанное сервером времяanthropic-ratelimit-unified-slow-retry-after(по умолчанию 20 с, ограничение 5–600 с, случайное отклонение ±30%), после чего запрос повторяется с учётом ограниченияanthropic-ratelimit-unified-slow-max-wait(по умолчанию 20 мин, ограничение 1 мин–6 ч) — по истечении этого срока режим завершается, а повторное принятие блокируется на 10 минут. Время ожидания также ограничивается оставшимся временем собственного тайм-аута начала запроса к вышестоящему сервису (resolveFetchStartTimeout, по умолчанию 10 мин) за вычетом 5 с: без этого ограничения максимальное время ожидания по умолчанию, равное 20 минутам, пережило бы сам запрос, а ожидание было бы прервано на полпути, что привело бы кTimeoutErrorвместо штатного завершения поmax_waitс последующим периодом блокировки.weekly_limit/budget_exhausted/off/ineligible, переход к новому 5-часовому окну илиineligible+anthropic-ratelimit-unified-overage-in-use: true(что завершает режим какextra_usageпри любом статусе, поскольку платное сверхлимитное использование теперь покрывает ограничение) завершают режим; после этого ответ передаётся обычному механизму периода ожидания. Состояниеbudget_exhaustedзапоминается до объявленного сброса бюджета (≤ 8 дней).- Проверка лимита выполняется после внутренних повторных попыток исполнителя в рамках одной попытки, вызванных ответом 400 (редактирование контекста, ограничение параметров мышления/усилий, автоматическое обучение параметров), поэтому ответ 429 из-за лимита, появившийся только при одной из этих повторных попыток, всё равно перехватывается и не попадает в механизм периода ожидания.
- Состояние хранится в памяти отдельно для каждого подключения (после перезапуска для повторного принятия потребуется один дополнительный ответ 429 из-за лимита).
Сброс лимита сеанса (autoLimitReset, при включении обеих функций выполняется до перехода в низкоприоритетный режим):
GET https://api.anthropic.com/api/oauth/usage?at_wall=1&skip_spend=1→ блокjuniper_tide; когдаarm: "reset"иavailable: true, выполняетсяPOST https://api.anthropic.com/api/organizations/{orgUUID}/reset_rate_limitsс{ "program": "juniper_tide" }(UUID организации берётся изproviderSpecificData.organizationUUID, с резервной загрузкой при инициализации).- При
result: reset|not_limitedзапрос повторяется на полной скорости (без заголовка низкой скорости). Дляalready_used/not_offeredзапоминаетсяnext_available_at(по умолчанию одна неделя); при любой ошибке применяется 15-минутная задержка. Сброс доступен один раз в неделю и по-прежнему учитывается в недельном лимите.
Защита от регрессий: tests/unit/claude-low-priority-mode.test.ts,
tests/unit/claude-limit-reset.test.ts, tests/unit/claude-low-priority-executor.test.ts.
Привязка сеанса (#7274)
Заголовок раздела «Привязка сеанса (#7274)»Область действия: один клиентский сеанс (заголовок X-Session-Id / x-codex-session-id / x-omniroute-session), привязанный к одному подключению для любого провайдера.
Назначение: удерживать многоходового агента (Claude Code, aider, пользовательские агенты) на одной и той же учетной записи между запросами, уменьшая потерю контекста при переключении между учетными записями и количество повторных ошибок 429 при холодном запуске у провайдеров, хранящих состояние сеанса на уровне учетной записи.
Реализация:
- Определение TTL:
src/sse/services/sessionAffinityPin.ts::resolveSessionAffinityTtlMs() - Выбор/создание привязки:
src/sse/services/sessionAffinityPin.ts::selectSessionAffinityConnection() - Извлечение заголовка (универсальное, для любого провайдера):
src/sse/services/auth.ts::extractSessionAffinityKey() - Таблица сохраняемых привязок:
sessionAccountAffinity(src/lib/db/sessionAccountAffinity.ts) - Настройка:
sessionAffinityTtlMs(глобальный TTL в мс,0отключает) —src/lib/db/settings.ts. Переименована из предназначенной только для Codex настройкиcodexSessionAffinityTtlMsмиграцией124_generic_session_affinity_ttl.sql, которая переносит ранее настроенный TTL Codex в качестве нового значения по умолчанию.
До #7274 resolveSessionAffinityTtlMs() немедленно возвращала 0 для всех провайдеров, кроме codex, поэтому настройка TTL (и заголовки сеанса) не действовала нигде больше, хотя механизм привязки и извлечение заголовков уже не зависели от провайдера. Исправление удалило этот досрочный выход; теперь TTL применяется одинаково ко всем провайдерам, если его глобальное значение задано выше 0.
Три заголовка привязки сеанса никогда не пересылаются вышестоящему сервису — исполнители формируют собственные вышестоящие заголовки с нуля, а не передают клиентские заголовки напрямую, поэтому они остаются исключительно внутренними идентификаторами корреляции.
Эксклюзивные аренды подключений управляемых сеансов
Заголовок раздела «Эксклюзивные аренды подключений управляемых сеансов»Область действия: один активный управляемый HTTP-клиент/сеанс владеет одним подходящим подключением OmniRoute.
Назначение: обеспечить устойчивое эксклюзивное владение подключением для клиентов, которым требуется жесткая маршрутизационная граница между запросами. Это отличается от привязки сеанса, которая является мягким предпочтением непрерывности: эксклюзивная аренда сохраняет состояние жизненного цикла в SQLite, обеспечивает глобальную уникальность активного владельца и активного подключения и отклоняет устаревшее поколение до передачи запроса провайдеру.
Функция включается отдельно для каждого ключа API. Управляемый ключ должен иметь область действия lease:exclusive и явно заданный непустой список allowedConnections. Конечную точку управления жизненным циклом может использовать любой HTTP-клиент; имя клиента, user-agent, провайдер, метод OAuth или модель не требуются. Аренда закрепляет подключение, а не модель, поэтому при смене модели привязка сохраняется, пока подключение остается подходящим по обычным критериям. Стандартные правила модели, квоты, работоспособности, периода ожидания и списка разрешений остаются приоритетными и могут перевести то же поколение на другое свободное подходящее подключение.
Управление жизненным циклом выполняется через POST /api/v1/session-leases с JSON-действиями acquire, renew и release. Управляемые запросы логического вывода передают непрозрачное значение X-OmniRoute-Lease-Owner и точное значение X-OmniRoute-Lease-Generation. Идентификатор владельца состоит из префикса vlo_, за которым следуют 43 символа base64url; хранится только его SHA-256-хеш. Каждая окончательная проверка перед отправкой также связывает идентификатор аутентифицированного ключа API и идентификатор активного подключения. Управляющие заголовки аренды удаляются из журналов, сохраняемых снимков запросов и заголовков вышестоящих исполнителей.
Если обычная маршрутизация находит подходящие управляемые подключения-кандидаты, но каждый свободный кандидат занят чужой активной арендой, OmniRoute возвращает HTTP 429, код недоступности емкости аренды, состояние ожидания доступной емкости и ограниченное значение Retry-After, вычисленное на основе ближайшего соответствующего срока истечения. Обычное отсутствие подходящих подключений не считается конфликтом аренд и сохраняет существующую семантику ошибок маршрутизации.
Связанные механизмы остаются отдельными:
- Занятость сеансов OAuth — это локальное для процесса мягкое распределение учетных записей OAuth.
- Семафоры учетных записей предоставляют разрешения на параллельное выполнение запросов и освобождаются после завершения запроса.
- Эксклюзивные аренды подключений управляемых сеансов обеспечивают устойчивое владение на протяжении жизненного цикла с проверкой поколения.
3. Блокировка модели
Заголовок раздела «3. Блокировка модели»Область действия: тройка «провайдер + подключение + модель».
Область действия ключа в зависимости от статуса: статус ошибки определяет, для какого ключа будет создана блокировка
(resolveLockoutScope() в open-sse/services/accountFallback/exactModelLock.ts):
429/403/402— сигнал о квоте или правах доступа — блокируется семейство квоты: для codex — вся областьcodex/spark(все моделиgpt-5*данного подключения), для других провайдеров —getQuotaScopedModelForProvider().404блокирует только конкретную модель (getModelLockKey()сужает область дляnot_found).- Любой другой статус — транспортные/серверные ошибки
5xxи собственный синтезированный OmniRoute статус502, возникающий при проверке качества, — блокирует только точную тройку «провайдер/подключение/модель». Неудачный поток для одной модели не свидетельствует о состоянии квоты аккаунта; до введения этого правила один пустой ответ отcodex/gpt-5.6-lunaисключал все моделиgpt-5*данного подключения из маршрутизации на 2–30 мин (с увеличением интервала), хотя его квота оставалась неизрасходованной. - Явно заданный вызывающей стороной параметр
scopeвсегда имеет приоритет (Antigravity передаёт"exact").
Назначение: не допускать отключения всего подключения, когда недоступна или ограничена по квоте только одна модель.
Примеры:
- Провайдеры с квотами для отдельных моделей, возвращающие 429
- Локальные провайдеры, возвращающие 404 для одной отсутствующей модели
- Ошибки прав доступа к режиму/модели, специфичные для провайдера (например, режимы Grok)
Реализация: open-sse/services/accountFallback.ts — lockModel(), clearModelLock(), getAllModelLockouts().
Панель периодов блокировки моделей (v3.8.0)
Заголовок раздела «Панель периодов блокировки моделей (v3.8.0)»Интерфейс: Настройки → Периоды блокировки моделей (src/app/(dashboard)/dashboard/settings/components/ModelCooldownsCard.tsx)
Отображает активные блокировки со следующими данными: провайдер, подключение, модель, причина, expiresAt. Операторы могут вручную повторно включить модель из карточки.
REST API:
GET /api/resilience/model-cooldowns— получить список активных блокировокDELETE /api/resilience/model-cooldowns— вручную повторно включить модель. Тело:{provider, connection, model}. Авторизация: управление.
Интерфейс настроек блокировки + восстановление за счёт уменьшения при успехе (v3.8.23)
Заголовок раздела «Интерфейс настроек блокировки + восстановление за счёт уменьшения при успехе (v3.8.23)»Блокировка модели перестала быть всегда включённым жёстко заданным поведением и стала полностью настраиваемой, включаемой по желанию функцией с собственной карточкой настроек и самовосстанавливающимся механизмом восстановления.
Карточка настроек: Настройки → Блокировка модели
(src/app/(dashboard)/dashboard/settings/components/ModelLockoutCard.tsx).
Она отличается от приведённой выше доступной только для чтения карточки ModelCooldownsCard (которая лишь
отображает активные блокировки): новая карточка задаёт параметры. Значения по умолчанию
находятся в DEFAULT_MODEL_LOCKOUT_SETTINGS
(src/lib/resilience/modelLockoutSettings.ts):
| Параметр | Значение по умолчанию | Значение |
|---|---|---|
enabled |
false |
Главный переключатель — блокировка модели по умолчанию отключена. |
errorCodes |
[403, 404, 429, 502, 503, 504] |
Статусы вышестоящего сервиса, считающиеся ошибкой конкретной модели. |
baseCooldownMs |
120_000 (120 с) |
Начальная продолжительность блокировки после первой ошибки. |
maxCooldownMs |
1_800_000 (30 мин) |
Максимальный увеличенный период блокировки. |
maxBackoffSteps |
10 |
Максимальное число шагов увеличения экспоненциальной задержки. |
useExponentialBackoff |
true |
Увеличивать ли период блокировки экспоненциально при повторных ошибках. |
Настройки сохраняются через обычное хранилище настроек и проверяются схемой
настроек устойчивости; карточка ограничивает значения baseCooldownMs/maxCooldownMs
(при этом maxCooldownMs ≥ baseCooldownMs) и maxBackoffSteps.
Восстановление за счёт уменьшения при успехе: восстановление происходит не только по истечении таймера. Успешный
ответ постепенно уменьшает счётчик ошибок модели, поэтому модель, восстановившаяся
в течение текущего интервала, перестаёт увеличивать период блокировки (и блокировка снимается) до истечения таймера. При успешном
целевом объекте комбинации open-sse/services/combo.ts вызывает decayModelFailureCount()
(open-sse/services/accountFallback.ts), который уменьшает вдвое сохранённое значение
failureCount (Math.floor(failureCount / 2)); когда оно достигает 0, запись о блокировке
полностью удаляется. Парная функция recordModelLockoutFailure()
увеличивает счётчик (и период блокировки) при ошибках, возникающих в пределах
окна эскалации. Это уменьшение при успехе дополняет обычное истечение таймера —
повторно включить модель может любой из этих механизмов.
Состояние: блокировки хранятся в памяти (отдельные для каждого процесса объекты Map с
записями ModelLockoutEntry, использующие ключ provider:connectionId:model, а для блокировок с точной областью —
provider:connectionId:exact:model) и не сохраняются в
БД — при перезапуске они теряются. Настройки сохраняются, а активное
состояние блокировок является временным.
4. Управление параллелизмом Quota-Share (v3.8.36)
Заголовок раздела «4. Управление параллелизмом Quota-Share (v3.8.36)»Аккаунты с подпиской (GLM, MiniMax и т. д.) часто допускают лишь ~1–3 параллельных
запроса; превышение этого количества приводит к ошибкам 429 и периодам ожидания. Особенно остро это проявляется
в комбинациях quota-share (qtSd/…), где несколько ключей API используют один общий вышестоящий
аккаунт. Три уровня защиты предотвращают перегрузку общего аккаунта.
Ограничение параллелизма для каждого подключения (max_concurrent)
Заголовок раздела «Ограничение параллелизма для каждого подключения (max_concurrent)»Для каждого подключения провайдера можно задать верхний предел max_concurrent
(provider_connections.max_concurrent, настраивается в модальном окне подключения / через API / в БД).
Оставьте поле пустым, чтобы не ограничивать параллелизм. Это единственный параметр, управляющий описанным
ниже уровнем сериализации, — задайте в нём фактический уровень параллелизма аккаунта (например, GLM ~1, MiniMax ~2).
Сериализация запросов quota-share
Заголовок раздела «Сериализация запросов quota-share»Когда диспетчеризация quota-share направляет запрос к подключению с положительным значением
max_concurrent, параллельные запросы к этому аккаунту сериализуются посредством
семафора для конкретного подключения (ключ qsconn:<connectionId>): лишние запросы ожидают в
очереди, а не перегружают аккаунт. Механизм работает по принципу fail-open — при переполнении
очереди или истечении времени ожидания запрос выполняется без получения слота, а не отклоняется, если
его можно диспетчеризовать. Переключатель находится в разделе Настройки → Устойчивость → Параллелизм
quota-share для каждого подключения (resilienceSettings.quotaShareConcurrencyLimit.enabled, по умолчанию
включено). Если ограничение max_concurrent не задано, поведение не меняется.
Сам шлюз маршрутизации quota-share (
selectQuotaShareTarget, DRR + P2C) работает по принципу fail-open и только снижает приоритет подключения, достигшего своего лимита, — при пуле из одного подключения он не может жёстко ограничивать нагрузку, поэтому именно этот семафор фактически сдерживает поток запросов.
Повторные попытки с учётом периода ожидания комбинации
Заголовок раздела «Повторные попытки с учётом периода ожидания комбинации»Для каждой стратегии комбинации (если функция включена) запрос, который привёл бы к окончательной ошибке 429
из-за КОРОТКОГО временного периода ожидания, дожидается его окончания и диспетчеризуется повторно вместо
возврата ошибки 429. Это распространяется на окна TPM/RPM класса Gemini (~60 с согласно retry-after)
в многомодельных комбинациях, например когда обе цели комбинации из 2 моделей достигают ограничения
частоты запросов для конкретной модели. Ограничивается параметрами comboCooldownWait (enabled, maxWaitMs, maxAttempts,
budgetMs) в разделе Настройки → Устойчивость. Ожидание никогда не применяется для quota_exhausted
(блокировка до полуночи), а также причин, связанных с аутентификацией или отсутствием ресурса.
5. Управление допуском в очередь запросов (v3.8.49 · issue #6593)
Заголовок раздела «5. Управление допуском в очередь запросов (v3.8.49 · issue #6593)»Область действия: локальная очередь ограничения частоты запросов для каждой пары «провайдер+подключение» (open-sse/services/rateLimitManager.ts,
работающая на базе Bottleneck), расположенная на один уровень ниже трёх описанных выше механизмов.
maxWaitMs ограничивает ожидание в очереди; executionMaxWaitMs ограничивает выполнение.
Эти параметры намеренно разделены, и ни один из них не влияет на другой.
resilienceSettings.requestQueue.maxWaitMs — это бюджет ожидания в очереди:
он охватывает ожидание свободного слота провайдера и последующее нахождение в состоянии QUEUED, а его таймер
сбрасывается в тот момент, когда задание покидает состояние QUEUED и начинает выполняться
(rateLimitManager.ts, wrappedFn). Запрос, превысивший этот бюджет, никогда
не отправляется вышестоящему сервису. Значение по умолчанию — 30000ms, оно задаётся через DEFAULT_REQUEST_QUEUE_MAX_WAIT_MS
в src/lib/resilience/settings.ts и зафиксировано тестом
tests/unit/ratelimit-admission-control-6593.test.ts, поэтому при его изменении
тест завершится с ошибкой, а этот абзац не останется незаметно устаревшим.
resilienceSettings.requestQueue.executionMaxWaitMs — это значение, которое Bottleneck
получает в качестве параметра задания expiration; его таймер запускается только после диспетчеризации. Это
страховочный механизм для исполнителей, у которых нет собственного тайм-аута вышестоящего сервиса, и его значение
увеличивается до собственного тайм-аута исполнителя с момента начала fetch, если тот больше, поэтому он
не может прервать исправно выполняющийся запрос. Значение по умолчанию — 600000ms (10 мин).
Передача бюджета очереди в expiration раньше приводила к прерыванию неинкрементальных
шлюзов прямо во время выполнения — они обоснованно могут работать несколько минут до получения первых байтов, —
поэтому истечение срока выполнения возвращается как code: "RATE_LIMIT_EXECUTION_TIMEOUT" (HTTP 504), тогда как превышение бюджета очереди возвращает
код тайм-аута очереди. Любой из параметров можно переопределить через RATE_LIMIT_MAX_WAIT_MS /
RATE_LIMIT_EXECUTION_MAX_WAIT_MS (переменные окружения) или на панели управления
(Настройки → Отказоустойчивость). При нормализации оба значения ограничиваются диапазоном 1ms–24h.
Приоритет для обоих параметров: переменная окружения задаёт только значение по умолчанию. Значение,
сохранённое в resilienceSettings.requestQueue (через панель управления / API patch и хранящееся
в key_value), имеет приоритет над ним, а заданные для отдельного подключения
rateLimitOverrides.maxWaitMs / .executionMaxWaitMs имеют приоритет и над сохранённым значением. Поэтому задание
переменной окружения в развёртывании, где значение уже сохранено,
ничего не изменит — вместо этого очистите или обновите сохранённую настройку.
Время нахождения в очереди ограничивается maxWaitMs; описанный ниже maxQueueDepth ограничивает количество
запросов, которые могут одновременно находиться в очереди.
maxQueueDepth — необязательное ограничение допуска (новое). resilienceSettings.requestQueue.maxQueueDepth
ограничивает количество запросов, которые могут одновременно находиться в очереди (ещё не будучи диспетчеризованными)
для одной пары «провайдер+подключение». Если в очереди уже находится maxQueueDepth
запросов, новый запрос немедленно отклоняется типизированной ошибкой
code: "RATE_LIMIT_QUEUE_FULL" до того, как попадёт в limiter.schedule(),
поэтому отклонение обходится дёшево и происходит до любой последующей
работы по сжатию / переводу промпта для этого запроса. Значение по умолчанию 0 =
отключено, что сохраняет существующее поведение с неограниченной очередью; допустимый диапазон — 0–100000.
Переопределить значение можно через RATE_LIMIT_MAX_QUEUE_DEPTH (переменную окружения) или
resilienceSettings.requestQueue.maxQueueDepth (панель управления/API patch).
Сама проверка допуска представляет собой чистую функцию
(open-sse/services/rateLimitManager/admission.ts::checkQueueAdmission), поэтому
её можно модульно тестировать без реального ограничителя Bottleneck.
В RFC, с которого начался #6593, также предлагался флаг
bypassCompressionOnRateLimit. В этом репозитории конвейерopen-sse/services/compression/выполняет сжатие промпта/контекста исходящего запроса к LLM (chatCore.ts, в районе блокаresolveCompressionSettings/selectCompressionStrategy), а не HTTP-сжатие тел синтезированных ответов 429 — соответствующего пути выполнения для буквального флага обхода не существует. Кроме того, этот шаг сжатия промпта сейчас выполняется доwithRateLimit()в конвейере обработки запроса, поэтому изменение порядка для его пропуска при отклонении из-за заполненной очереди — это отдельное и более масштабное изменение, выходящее за рамки этой задачи; оно намеренно не было реализовано здесь и оставлено для последующей работы, если выигрыш в использовании CPU оправдает риск изменения порядка выполнения.
6. Сторожевой механизм пропускной способности медленного потока (#9709)
Заголовок раздела «6. Сторожевой механизм пропускной способности медленного потока (#9709)»Необязательный защитный механизм resilienceSettings.streamRecovery.throughputWatchdog обнаруживает
вышестоящий источник, который продолжает отправлять фрагменты, но формирует вывод ассистента со скоростью
ниже настроенной скорости полезного вывода. Он намеренно отделён от тайм-аута бездействия:
сигналы активности и метаданные не сбрасывают ни один из таймеров и не считаются прогрессом. Он также
отличается от жёсткого предельного срока попытки (#9153), который остаётся абсолютным ограничением
безопасности независимо от качества вывода.
Прежде чем сторожевой механизм сможет прервать операцию, должны завершиться период прогрева и
полное скользящее окно. Он подсчитывает текстовые дельты из событий вывода Chat Completions и Responses API
(используя консервативную аппроксимацию количества байтов UTF-8), игнорирует пустые события и события,
содержащие только данные об использовании, а также приостанавливает оценку, пока выполняются события
вызова инструментов или рассуждений. По умолчанию он отключён; включить его можно с помощью
STREAM_THROUGHPUT_WATCHDOG_ENABLED=true. Окно, период прогрева, минимальная скорость и минимальный
измеримый объём вывода ограничиваются стандартным слоем нормализации настроек устойчивости.
Когда механизм включён, его прерывание применяется только к активной попытке обращения к вышестоящему источнику. До отправки клиенту каких-либо байтов существующий путь раннего восстановления в рамках той же учётной записи может повторно открыть попытку. После фиксации поток никогда не воспроизводится повторно вслепую; дополнить его суффиксом может только существующий контракт безопасного продолжения в середине потока. Финализация по-прежнему выполняется однократно, поэтому учёт использования и освобождение семафора не дублируются.
7. Переназначение статуса вышестоящего источника (ошибки квоты с неверным статусом)
Заголовок раздела «7. Переназначение статуса вышестоящего источника (ошибки квоты с неверным статусом)»Область действия: один вышестоящий шлюз, который сообщает о временном исчерпании квоты с неверным HTTP-статусом.
Цель: исправить вводящий в заблуждение статус ДО классификации, чтобы последующие потребители (механизм резервного переключения, агрегация комбинаций, ответ клиенту) видели истинную, допускающую повторную попытку природу сбоя.
Некоторые шлюзы сигнализируют о ВРЕМЕННОМ исчерпании квоты с помощью HTTP-статуса,
не допускающего повторную попытку. agentrouter.org возвращает 403 (иногда 400) с текстом
на китайском языке (用户额度不足 / 额度不足) вместо стандартного 429. Такие клиенты, как Claude
Code, считают 403 постоянной ошибкой и прерывают сеанс, а без исправления
механизм резервного переключения классифицировал бы её как AUTH_ERROR, а не как событие
квоты.
Реализация:
- Реестр и сопоставитель:
open-sse/config/upstreamStatusRestatement.ts— список правил для каждого провайдера ({id, fromStatuses, toStatus, textMarkers, excludeMarkers, defaultRetryAfterMs}), сопоставляемых посредствомapplyStatusRestatement(). - Место вызова: блок
providerFailure:вopen-sse/handlers/chatCore.ts(примерно строка 3654), непосредственно после того, какparseUpstreamError()разбирает ответ вышестоящего источника с ошибочным HTTP-статусом (!providerResponse.ok), и до выполнения любой классификации, чтобы каждый последующий потребитель видел исправленный статус. Ошибки, встроенные в поток SSE со статусом200, обрабатываются по отдельному, более позднему пути разбора потока и на данный момент не охватываются этим перехватчиком — это известное ограничение, которое пока несущественно для неверного статуса agentrouter (он проявляется как ошибочный HTTP-статус). - Допустимость повторной попытки:
429входит вRETRY_AFTER_ELIGIBLE_STATUSES(open-sse/services/combo/unavailableRetryGate.ts), поэтому ошибка с переназначенным статусом получает фактическое окно повторной попытки, а не выдаётся как безнадёжная ошибка403. - Синтетическое значение
60sдляdefaultRetryAfterMs(upstreamStatusRestatement.ts) определяет только то, что ответ с переназначенным статусом сообщает клиенту; само по себе оно не является длительностью внутреннего периода ожидания или блокировки соединения — она отдельно определяется механизмом, который фактически обрабатывает ошибку с переназначенным статусом (нарастающей задержкой механизма Connection Cooldown, §2, с базовым значением3sдля провайдеров с API-ключами; либо механизмом Model Lockout, §3, для провайдеров с квотами на отдельные модели, таких как agentrouter). Маршрутизатор может получить возможность повторить попытку раньше, чем истечёт объявленное клиенту окно в 60 секунд, — это намеренный запас, а не ошибка.
Постоянным ошибкам (无权访问模型 от agentrouter — нет доступа к этой модели)
статус НЕ переназначается: excludeMarkers отменяет правило даже при совпадении textMarkers,
поэтому ошибка сохраняет исходный статус и для неё не выполняются бесконечные повторные попытки.
Соответствующее правило классификации провайдера
(agentrouter-model-access-denied в open-sse/config/providerErrorRules.ts:
reason: "auth_error", scope: "model", объявленный базовый период ожидания 6h)
проверяется функцией checkFallbackError (open-sse/services/accountFallback.ts)
до раннего возврата универсальной категории API-ключей FORBIDDEN при условии,
что honorsRuleLockScope(provider) допускает это (#10334 — в настоящее время исключительно
для agentrouter посредством списка разрешённых HONORS_RULE_LOCK_SCOPE_PROVIDERS в
providerErrorRules.ts). Объявленный правилом 6-часовой период ожидания передаётся как
fallbackResult.baseCooldownMs, но всё равно направляется в существующий путь блокировки
по квоте отдельной модели (lockModelIfPerModelQuota() /
recordModelLockoutFailure(), не изменённый в #10334, кроме источника периода ожидания):
его значение уменьшается до заданного оператором mlSettings.maxCooldownMs
(по умолчанию 1_800_000ms / 30 мин), как и для любой другой блокировки модели, а
сохранённая причина блокировки остаётся прежней, жёстко заданной строкой "forbidden",
а не значением правила "auth_error" — сквозным образом учитывается только длительность
периода ожидания, но не строка причины. Само соединение остаётся активным;
другие модели в том же соединении не затрагиваются.
Переформулированные ошибки квоты (额度不足) в рабочей среде соответствуют правилу провайдера
(agentrouter-user-quota-exhausted: reason: "quota_exhausted", scope: "connection", без собственного явно заданного времени восстановления — применяется
масштабируемая задержка по умолчанию из слоя персистентности). Начиная с #10334, scope в
ProviderErrorRuleMatch ДЕЙСТВИТЕЛЬНО используется по всей цепочке, но только для провайдеров из
списка разрешённых HONORS_RULE_LOCK_SCOPE_PROVIDERS (providerErrorRules.ts —
на данный момент только "agentrouter", с проверкой через honorsRuleLockScope()). Для всех
остальных провайдеров scope остаётся информационным, как и до #10334.
checkFallbackError предоставляет область действия совпавшего правила как
fallbackResult.ruleScope; isAgentrouterConnectionQuotaScope()
(src/sse/services/auth.ts) — это общая защитная проверка, подтверждающая, что
ruleScope действительно можно безопасно учитывать как относящийся ко всему соединению
самовосстанавливающийся сигнал (область действия "connection", причина quota_exhausted, никогда не
permanent, никогда не creditsExhausted — защита от возможного будущего правила, сочетающего область действия
"connection" с постоянным состоянием учётной записи). Её вызывают два потребителя:
- Персистентность (
markAccountUnavailable(),src/sse/services/auth.ts): вместо перехода в ветвь блокировки для каждой модели транзитного провайдера (у agentrouterpassthroughModels: true→hasPerModelQuota()возвращаетtrue) применяется временная задержка восстановления соединения —testStatus: "unavailable"+rateLimitedUntil, но никогда терминальный статус (credits_exhausted/banned/expired) — благодаря этому соединение самовосстанавливается после истечения задержки, а не требует ручного сброса учётных данных. Пропускается для соединений сdisableCooling: true(#2997): при таком отказе вместо этого выполняется переход к блокировке для каждой модели (это документированный компромисс — см. комментарий в коде над ветвью). - Комбинированная маршрутизация в рамках того же запроса (
applyComboTargetExhaustion(),open-sse/services/combo/targetExhaustion.ts): та же защитная проверка добавляет соединение во внутрипроцессный наборexhaustedConnectionsс ключом${provider}:${connectionId}. Это приводит к пропуску только оставшейся цели В РАМКАХ ТОГО ЖЕ ЗАПРОСА, которая сама уже содержит именно этотconnectionIdв собственном объекте цели (getExhaustedTargetSkipReason(),open-sse/services/combo/comboPredicates.ts,if (provider && connectionId)перед поиском вexhaustedConnections) — обычная комбинация на основе списка моделей, в которой у соседних целей нет собственных закреплённыхconnectionId, а идентификатор определяется для каждой отправки из заголовка ответаX-OmniRoute-Selected-Connection-Id, никогда не даёт совпадения с этим ключом. В этом распространённом случае реальную защиту от повторного использования оставшимся этапом только что исчерпавшей квоту учётной записи обеспечивает НЕ этот Set, а описанный выше слой персистентности (rateLimitedUntilсоединения теперь находится в будущем) в сочетании с тем, что та же защитная проверка подавляетtransientRateLimitedProvidersдля этой ошибки (см. раздел «Двухэтапная схема» и комментарий в коде ветвиisAgentrouterConnectionQuotaScopeвtargetExhaustion.ts): поскольку этот Set остаётся непомеченным, принудительное разрешениеallowRateLimitedConnectionизcombo.ts(open-sse/services/combo.ts:1005-1013,:2734-2738) НЕ срабатывает для оставшихся этапов провайдера, поэтому фильтр выбора учётных данных поrateLimitedUntil(src/sse/services/auth.ts:1238) учитывается как обычно, а оставшийся этап либо выбирает другое, всё ещё доступное соединение agentrouter, либо завершается ошибкой из-за отсутствия доступных учётных данных — он не возвращается принудительно к соединению, для которого эта ветвь только что установила задержку восстановления.
Двухэтапная схема: переформулирование статуса, затем классификация
Заголовок раздела «Двухэтапная схема: переформулирование статуса, затем классификация»Переформулирование статуса (upstreamStatusRestatement.ts) и правила
классификации провайдеров (open-sse/config/providerErrorRules.ts,
providerRuleRegistry) — это отдельные реестры, оба использующие идентификатор провайдера
и текстовые маркеры в качестве ключей, но они применяются в разных местах и служат разным
целям: переформулирование на раннем этапе в chatCore.ts заменяет HTTP-статус;
правила классификации выбирают резервный reason и scope блокировки
(model / provider / connection) внутри checkFallbackError()
(open-sse/services/accountFallback.ts).
Правила классификации видят полный текст ошибки (необходимый для сопоставления с
маркерами тела ответа, такими как 额度不足) только для провайдеров из списка разрешённых
FULL_TEXT_RULE_PROVIDERS в providerErrorRules.ts — в настоящее время только
"agentrouter". Для любого другого провайдера из встроенного каталога
checkFallbackError передаёт в getProviderErrorRuleMatch только
структурированную ошибку ({code, type}), которой достаточно для правил на основе
заголовков, статусов или кодов, но которая не видит маркеры в тексте тела ответа.
Вспомогательная функция resolveRuleMatchBody() выполняет этот выбор: полный текст ошибки
для провайдеров из списка разрешённых и структурированную ошибку для остальных. Добавление
встроенного провайдера в FULL_TEXT_RULE_PROVIDERS является явным согласием
на уровне отдельного провайдера — этот механизм существует, чтобы стандартный путь для каждого
провайдера, отсутствующего в списке, оставался побайтово неизменным.
scope правила (model / provider / connection) требует отдельного явного согласия,
независимого от FULL_TEXT_RULE_PROVIDERS: checkFallbackError лишь предоставляет его как
fallbackResult.ruleScope, а последующие потребители учитывают его не просто как
информационную метку только для провайдеров из списка разрешённых
HONORS_RULE_LOCK_SCOPE_PROVIDERS в том же файле (с проверкой через honorsRuleLockScope() — на данный момент только "agentrouter"). Описание фактического
поведения при совпадении с scope: "connection" после добавления провайдера в этот список
см. выше в разделе «Переформулированные ошибки квоты».
#11104 — правила, объявленные оператором, обходят оба списка разрешённых. Оператор может
объявить правило для конкретного провайдера во время выполнения через settings.providerErrorRules
(open-sse/config/providerErrorRules.ts::setOperatorProviderErrorRules)
без редактирования этого файла. Ограничение операторского правила посредством
FULL_TEXT_RULE_PROVIDERS/HONORS_RULE_LOCK_SCOPE_PROVIDERS — списков разрешённых,
предназначенных для защиты стандартного поведения встроенных правил каталога, —
сделало бы механизм настроек неработоспособным для всех провайдеров, кроме уже
перечисленных там, поскольку само объявление правила уже является явным
согласием оператора. resolveRuleMatchBody() и honorsRuleLockScope() сначала
проверяют hasOperatorRuleForProvider(): провайдер с операторским правилом получает
необработанный текст ошибки, а объявленный для него scope соблюдается независимо
от того, присутствует ли он также в одном из этих списков разрешённых.
Известный пробел — providerRuleRegistry никогда не проверяется для HTTP 400.
Ветка BAD_REQUEST в checkFallbackError классифицирует статус 400 исключительно
посредством собственных массивов шаблонов (MODEL_ACCESS_DENIED_PATTERNS,
CONTEXT_OVERFLOW_PATTERNS и т. д. в accountFallback.ts) и возвращает результат
до достижения расположенной выше ветки configuredRule/getProviderErrorRuleMatch.
Встроенное правило каталога (или операторское правило) с status: 400
синтаксически допустимо, но никогда не сработает. Сейчас ни одно существующее правило
не предназначено для 400, поэтому в рабочей среде это ни на что не влияет, — однако
будущее правило для 400 потребует сначала изменить эту ветку, что представляет собой
более масштабное изменение, чем добавление правила (оно переклассифицирует 400 для
каждого провайдера, уже полагающегося на поведение массивов шаблонов), и выходит за
рамки добавления правила для одного провайдера.
Добавление нового шлюза, неверно указывающего квоту
Заголовок раздела «Добавление нового шлюза, неверно указывающего квоту»- Зарегистрируйте один массив правил в
statusRestatementRegistry(open-sse/config/upstreamStatusRestatement.ts). ЗначенияtextMarkersдолжны быть специфичны для провайдера; никогда не используйте повторно общие английские фразы, которые пересекаются сCREDITS_EXHAUSTED_SIGNALS(open-sse/services/accountFallback.ts). - При необходимости зарегистрируйте правила классификации в
open-sse/config/providerErrorRules.ts(providerRuleRegistry), чтобы выбрать правильную область блокировки (connectionдля квоты уровня аккаунта,modelдля ошибок отдельных моделей). В рабочей среде этот шаг действует только для провайдеров, правилам которых необходим полный текст ошибки (маркеры в теле): добавьте идентификатор провайдера вFULL_TEXT_RULE_PROVIDERSв том же файле — иначеcheckFallbackErrorпередаёт правилу только структурированную ошибку{code, type}, и правило, проверяющее текст тела, никогда не совпадёт с реальным трафиком. Правила, сопоставляемые исключительно поstatus/headers(например, правила Opencode или Minimax), не требуют такого явного включения. Отдельно, если правило объявляетscope: "connection"и требуется фактическая пауза для всего подключения вместе с пропуском комбинации в рамках того же запроса (а не просто информационная метка), добавьте идентификатор провайдера вHONORS_RULE_LOCK_SCOPE_PROVIDERSв том же файле — именно это разрешает обработку в стилеisAgentrouterConnectionQuotaScope()вmarkAccountUnavailable()(src/sse/services/auth.ts) иapplyComboTargetExhaustion()(open-sse/services/combo/targetExhaustion.ts); без этогоscopeпо-прежнему передаётся черезfallbackResult.ruleScope, но никак не используется. - Добавьте модульные тесты по образцу
tests/unit/upstream-status-restatement.test.tsиtests/unit/agentrouter-error-rules.test.ts(включая проверки not-permanent / not-creditsExhausted и — если провайдеру нужен список разрешённых — тест, подтверждающий, чтоresolveRuleMatchBody()возвращает полный текст только для этого провайдера).
Изменения в chatCore.ts, classifyError или combo не требуются.
Блокировка по исходящему IP (#10880)
Заголовок раздела «Блокировка по исходящему IP (#10880)»Провайдеры из EGRESS_BUCKETED_LOCK_PROVIDERS (семейство opencode) считаются
вышестоящими сервисами с группировкой по IP (бесплатный уровень opencode группирует
по IP, а не по аккаунтам — см. #9611): статус 429, классифицированный как
quota_exhausted или rate_limit_exceeded, приостанавливает каждое подключение
семейства из списка разрешённых, чей последний известный исходящий IP совпадает с IP
подключения, завершившегося ошибкой, прежде чем ротация сможет их попробовать
— это позволяет избежать N-1 заведомо неудачных вызовов вышестоящего сервиса
(по аналогии с #10460/#10525).
rate_limit_exceeded включён намеренно: на пути markAccountUnavailable
специфичные для opencode правила никогда не совпадают (заголовки/тело не передаются в
checkFallbackError, а opencode отсутствует в FULL_TEXT_RULE_PROVIDERS), поэтому 429,
в теле которого содержится текст о квоте подписки (“monthly usage limit
reached”), классифицируется как quota_exhausted резервной проверкой текста квоты
(buildSubscriptionQuotaFallback, accountFallback.ts; пауза на 1 ч) до того, как
будет достигнуто правило status_429, — тогда как 429 без текста о квоте (обычное
ограничение частоты запросов) классифицируется правилом status_429 как
rate_limit_exceeded и всё равно приостанавливает семейство IP. Для провайдера из
списка разрешённых ограничение частоты запросов, сгруппированное по IP, является тем
же сигналом, что и исчерпанная квота. Реальные ограничения:
- Максимально возможный результат: блокировка определяет последний известный
egress_ipсоединения изproxy_logs(окно 24 ч, синхронно, без кеша). При холодном кеше (исходящий IP никогда не проверялся) или отсутствии строки проблемное соединение всё равно переводится этой веткой в режим ожидания (с записью, как и сейчас), но связанные соединения не блокируются. - Никогда не является окончательным состоянием: режим ожидания представляет
собой возобновляемое окно квоты (
testStatus: "unavailable"); постоянное состояние никогда не определяется на основании сигнала уровня IP. Соединения сdisableCoolingполностью пропускают эту ветку. - Для семейства из списка разрешённых меняется гранулярность блокировки: это
изменение области действия, а не просто оптимизация связанных соединений.
opencode — провайдер
passthroughModels, поэтому до появления этой ветки ошибка 429 приводила к блокировке на уровне МОДЕЛИ; теперь она переводит соединение в режим ожидания — в том числе когда оператор использует только одно соединение без каких-либо связанных соединений. Именно эта гранулярность уже объявлена корректной в таблице правил opencode (scope: "connection",providerErrorRules.ts), но до сих пор не применялась, поскольку opencode отсутствует вHONORS_RULE_LOCK_SCOPE_PROVIDERS. Ветка самостоятельно записывает режим ожидания иbackoffLevelпроблемного соединения, повторяя поведение ветки agentrouter с областью действия на уровне соединения, после чего выполняет возврат — блокировка на уровне модели и расположенный ниже общий путь никогда не достигаются. - Комбинированный режим включён: как и ветка agentrouter, эта область
действия намеренно игнорирует понижение
persistUnavailableState/isCombo, которое комбинированный вызывающий код применяет к ошибке 429. Блокировка на уровне модели — не более слабая форма этой области действия, а неверная единица: она ничего не сообщает об исчерпанном IP, поэтому ротация в комбинированном режиме продолжала бы расходовать по одному гарантированно неуспешному вызову на каждое связанное соединение. - Безопасность связанных соединений: связанное соединение, уже находящееся в окончательном состоянии (banned/credits_exhausted) или в более длительном режиме ожидания, никогда не перезаписывается.
- Исключительный список разрешённых: расширение
EGRESS_BUCKETED_LOCK_PROVIDERS— явное решение владельца; без общей автоматической интеграции (шаблон #10334/#10419). Запрос связанных соединений использует тот же список разрешённых, а не повторяет его как SQL-литерал, поэтому для его расширения по-прежнему достаточно изменить одну строку. - Ротация исходящего IP в обоих направлениях: окно поиска (24 ч) значительно шире TTL кеша исходящего IP (5 мин), поэтому «последний известный IP» — это история, а не текущее состояние. Если прокси соединения сменился в пределах этого окна, блокировка может не охватить действительно общий IP (записан новый, ещё не исчерпанный IP) — и, симметрично, может перевести в режим ожидания связанное соединение, которое уже сменило исчерпанный IP. Во втором случае связанное соединение теряет одно окно режима ожидания; оба варианта приняты как ограничения подхода best-effort, основанного на исторических данных.
- Стоимость: два ограниченных сканирования
proxy_logs(с фильтрацией по окну черезidx_pl_timestamp), выполняемые только с частотой возникновения ошибок 429. Новый индекс не добавляется (миграция 134, YAGNI). Измерено на копии БД умеренного размера с реальным трафиком; экземпляр с высокой пропускной способностью будет хранить пропорционально больше строк в том же окне.
Другие механизмы отказоустойчивости
Заголовок раздела «Другие механизмы отказоустойчивости»- 19 стратегий маршрутизации (приоритетная, взвешенная, циклическая, с ретрансляцией контекста, с заполнением по порядку, p2c, случайная, по наименьшему использованию, оптимизированная по стоимости, с учётом сброса, по окну сброса, по резерву, строго случайная, автоматическая, lkgp, оптимизированная по контексту, оптимизированная по кэшу, объединённая, конвейерная) — см. AUTO-COMBO.md.
- Маршрутизация с учётом сброса (v3.8.0) — приоритизирует подключения по времени сброса квоты.
- Деградация фонового режима — режим Responses API
background: trueпереключается на синхронный с предупреждением. - Динамическое определение лимита инструментов — снижает приоритет провайдеров при достижении ограничений на количество инструментов.
- Аварийное переключение — управляется переменной
OMNIROUTE_EMERGENCY_FALLBACK; операторы могут переопределить его на странице Feature Flags без перезапуска.
Отладка
Заголовок раздела «Отладка»- Взвешенная комбинация возвращает
503 all_targets_cooling_down(задан заголовокRetry-After, аdiagnostics.excludedсодержит все цели с причинамиmodel_lockout/circuit_open/provider_cooldown/unavailable) → пул настроен и подключён, но каждая цель исключена таймером отказоустойчивости; предупреждение[COMBO] Weighted selection: every target excluded before dispatch — …указывает причины и оставшееся количество секунд. Ответ404 no_executable_targetsот той же комбинации означает, что таймеры отказоустойчивости не были задействованы (нечего запускать либо все учётные записи не прошли проверку доступности). Реализовано вopen-sse/services/combo/pinRecovery.tsна основе исключений, собранных вtargetResolution.ts. - Все ключи провайдера пропущены → проверьте как состояние автоматического выключателя, так и
rateLimitedUntil/testStatusкаждого подключения. - Провайдер навсегда исключён после окончания окна сброса → код считывает необработанное значение
stateвместо использованияgetStatus()/canExecute(). - Один ключ не работает, остальные должны работать → отдавайте предпочтение периоду ожидания подключения, а не автоматическому выключателю.
- Не работает только одна модель → отдавайте предпочтение блокировке модели, а не периоду ожидания подключения.
- Состояние должно восстанавливаться автоматически, но этого не происходит → проверьте наличие временной метки в будущем и путь чтения, обновляющий состояние с истёкшим сроком действия. Постоянные статусы требуют изменений вручную.
TLS-фингерпринтинг и скрытность
Заголовок раздела «TLS-фингерпринтинг и скрытность»Специфичные для провайдеров механизмы скрытности (JA3/JA4, CCH, обфускация) описаны отдельно — см. docs/security/STEALTH_GUIDE.md (в git; не включается при сборке в /docs).
Тестирование отказоустойчивости (этап 8 · блок C)
Заголовок раздела «Тестирование отказоустойчивости (этап 8 · блок C)»Помимо модульных тестов логики отказоустойчивости, три теста проверяют среду выполнения в реальных условиях нагрузки и сбоев (все они интеграционные/ночные и не блокируют PR):
| Тест | Что проверяется | Запуск |
|---|---|---|
| Хаос-тест | Имитационный вышестоящий узел вносит реальные задержки, сбросы, тайм-ауты и ошибки 503; проверяется, что автоматический выключатель размыкается/восстанавливается, а checkFallbackError классифицирует 503 как восстанавливаемый сбой с переключением. |
RUN_CHAOS_INT=1 npm run test:chaos |
| Рост кучи | ~500 потоков на каждый createSSEStream при использовании --expose-gc; тест завершается ошибкой, если куча превышает предельный размер (защита от OOM #3069). |
npm run test:heap |
| Нагрузочный тест k6 | Продолжительная нагрузка на /api/monitoring/health; пороговые значения p95 и ошибок. |
k6 run tests/load/k6-soak.js (ночной) |
Оркестрация выполняется посредством .github/workflows/nightly-resilience.yml (cron + dispatch). В
стандартном test:integration тесты хаоса и кучи автоматически пропускаются (без RUN_CHAOS_INT/--expose-gc).
См. также
Заголовок раздела «См. также»- Руководство по архитектуре — Архитектура системы и внутреннее устройство
- Руководство пользователя — Провайдеры, комбинации, интеграция с CLI
- Механизм автоматических комбинаций — Оценка по 16 факторам, наборы режимов
HagiCode
HagiCode — агентная среда разработки со структурированными процессами, параллельным выполнением несколькими агентами и интерфейсами Hero Dungeon.
Превращайте идеи в полезное ПО с более умным, быстрым и увлекательным агентным рабочим процессом.

- SmartСтруктурированные процессы превращают намерение в исполнимый путь от идеи до готового изменения.
- EfficientМультиагентные процессы параллельно продвигают исследование, реализацию и проверку.
- FunHero Dungeon делает длительную совместную разработку наглядной и увлекательной.