Комбо: отказоустойчивость и балансировка нагрузки
Combo — это одна виртуальная модель, за которой стоит упорядоченный список реальных целей
provider/model. Клиент запрашивает combo/<id>, opencodex выбирает цель, переписывает запрос на
конкретный provider/model и при сбое, допускающем повтор, может попробовать следующую цель.
Это полезно, если вам нужно одно из двух:
- Failover: предпочитать одну модель, но держать резервные варианты наготове.
- Балансировка нагрузки: распределять успешные запросы между моделями или провайдерами взвешенными партиями.
Combo работают поверх обычной маршрутизации провайдеров. Если синтаксис provider/model для вас
новый, сначала прочитайте Маршрутизацию моделей.
Быстрый старт за 60 секунд
Заголовок раздела «Быстрый старт за 60 секунд»Этот пример создаёт combo/main, где первым идёт Anthropic, а вторым OpenAI. Оба провайдера уже
должны существовать и быть включены.
ocx combo set main --targets anthropic/claude-opus-4-8,openai/gpt-5.6-solСтратегия по умолчанию — failover, поэтому обычный запрос уйдёт в
anthropic/claude-opus-4-8. Если эта попытка завершится retryable-сбоем, opencodex сможет
переключиться на openai/gpt-5.6-sol.
Используйте виртуальную модель везде, где вы обычно передаёте id модели:
{ "model": "combo/main", "input": "Explain why the sky looks blue."}Проверьте сохранённое определение:
ocx combo show mainКак работают имена combo
Заголовок раздела «Как работают имена combo»Идентификатор combo в ocx combo set <id> должен начинаться с буквы или цифры. Дальше он может
содержать буквы, цифры, ., _ или -, общей длиной до 64 символов. Канонический id модели
всегда имеет вид combo/<id>; например, id main превращается в combo/main.
Пространство имён combo/ резервируется, пока настроены combo. Провайдер с именем combo не
может его занять, и id combo не может дублировать имя уже настроенного провайдера.
Необязательный alias даёт combo другое публичное имя модели. Alias:
- использует тот же набор символов, что и id;
- может быть без слэша, например
daily-fast, либо содержать один/, напримерteam/daily-fast; - не может быть
comboи не может начинаться сcombo/; - не может дублировать alias другой combo; и
- обычно не может быть «голым» нативным именем семейства OpenAI, начинающимся с
gpt-,o1-,o3-,o4-илиcodex-; единственное исключение — явный режим совместимости DesktopnativeAlias: true.
Даже если alias задан, каноническая форма combo/<id> всё равно разрешается. Канонический поиск
выполняется раньше сопоставления alias, поэтому alias не может перехватить канонический id другой
combo.
Выбор стратегии
Заголовок раздела «Выбор стратегии»Failover: основной вариант и резерв в заданном порядке
Заголовок раздела «Failover: основной вариант и резерв в заданном порядке»failover выбирает первую подходящую цель в порядке конфигурации. Цель считается подходящей,
когда провайдер существует, включён, не находится в cooldown и может обработать специальные
ограничения запроса. Веса и stickyLimit на эту стратегию не влияют.
Если порядок такой:
anthropic/claude-opus-4-8openai/gpt-5.6-solgoogle/gemini-3-pro
то каждый запрос начинается с Anthropic. Retryable-сбой Anthropic переводит этот запрос на OpenAI; retryable-сбой OpenAI может перевести его на Google. Terminal-ошибка сразу останавливает обработку и не даёт пробовать оставшиеся цели.
Round-robin: сглаженные взвешенные партии
Заголовок раздела «Round-robin: сглаженные взвешенные партии»round-robin использует smooth weighted round-robin. Чем больше вес цели, тем большую долю она
получает со временем, но эта доля не отправляется одним длинным блоком. stickyLimit управляет
тем, сколько успешных запросов остаются на выбранной цели, прежде чем выполнится следующий
взвешенный выбор.
Создайте combo 2:1 с партиями по два успешных запроса:
ocx combo set balanced \ --targets anthropic/claude-opus-4-8:2,openai/gpt-5.6-sol:1 \ --strategy round-robin \ --sticky 2Если назвать цели A (вес 2) и B (вес 1), то первые шесть взвешенных выборов будут
A, B, A, A, B, A. Поскольку stickyLimit равен 2, каждый выбор удерживается на два успешных
запроса:
| Успешный запрос | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | — | — | — | — | — | — | — | — | — | — | — | — | — | — | | Цель | A | A | B | B | A | A | A | A | B | B | A | A |
В долгосрочном итоге доля всё равно будет 2:1. Retryable-сбой завершает текущую sticky-партию, переводит эту цель в cooldown и выбирает другую подходящую цель для того же запроса.
random: взвешенный выбор для каждого запроса
Заголовок раздела «random: взвешенный выбор для каждого запроса»random выбирает для каждого запроса одну подходящую цель с вероятностью, пропорциональной
weight. Каждый запрос — независимый выбор, поэтому трафик распределяется между целями без
детерминированной последовательности или закрепления, характерных для round-robin.
stickyLimit не влияет на эту стратегию.
least-used: в пользу цели с наименьшим числом успешных запросов
Заголовок раздела «least-used: в пользу цели с наименьшим числом успешных запросов»least-used направляет каждый запрос к подходящей цели, для которой этот процесс opencodex
зарегистрировал меньше всего успешных запросов. После перезапуска счётчики начинают с нуля, а при
равенстве сохраняется порядок конфигурации. Значения weight и stickyLimit не влияют на эту
стратегию.
reset-window: ближайший сброс квоты
Заголовок раздела «reset-window: ближайший сброс квоты»reset-window направляет каждый запрос к подходящей цели, у которой кэшированный снимок квоты
провайдера показывает ближайший предстоящий сброс окна (пятичасового, недельного, месячного или
пользовательского). Так расходуется квота провайдера, которая обновится первой. Цели без свежих
данных о квоте, а также цели с одинаковым временем сброса сохраняют порядок конфигурации. Значения
weight и stickyLimit не влияют на эту стратегию.
Для этого ранжирования и исключения провайдеров до отправки нужны свежие лимиты инференса моделей, применимые к единственному текущему API-ключу в целом. Сводки OAuth и текущего аккаунта, маршруты с передачей учётных данных вызывающей стороны, несколько ключей и снимки с изменившимися учётными данными или адресом назначения служат только для отображения при этом предварительном решении. То же относится к переопределению учётных данных заголовками Authorization, x-api-key или x-goog-api-key; окна только для поиска или MCP исключаются. Если ни у одной допустимой цели нет подходящего времени сброса, используется порядок конфигурации. При выборе аккаунта и повторных попытках по-прежнему действуют обычные ограничения.
Что происходит, когда цель сбоит
Заголовок раздела «Что происходит, когда цель сбоит»Сбои в combo делятся на hop-сбои и terminal-сбои.
| Результат | Поведение |
|---|---|
| HTTP 401, 403, 404, 408, 429, или любой 5xx | Перевести цель в cooldown и перейти к следующей подходящей цели. |
| HTTP 410 с явным признаком окончания срока службы модели, retirement, deprecated, sunset, decommissioned или недоступности | Перевести только эту цель в cooldown и перейти дальше. Несвязанные ответы 410 остаются terminal-ошибками. |
| Классифицированная ошибка аутентификации, подписки, квоты, rate-limit, перегрузки или upstream-server | Перевести цель в cooldown и переключиться, даже если одного статуса недостаточно. |
Отмена клиентом (499), origin_rejected, отказ из-за cyber-policy, переполнение контекста или иной некорректный запрос |
Остановиться и вернуть ошибку; другая цель не сделает такой запрос корректным. |
Структурированный HTTP 400, отклоняющий необязательный user, неподдерживаемое значение reasoning.effort/reasoning_effort или специфичный для модели отказ входного изображения (param: input) |
До начала вывода переходит к следующей допустимой цели без охлаждения; см. «Совместимость необязательных параметров» ниже. |
| Любая другая неклассифицированная ошибка | Остановиться и вернуть ошибку. |
Если cooldownMs не задан, цель после hop использует upstream fallback: 5 секунд для 429, ограничивающих частоту запросов, с кодом upstream 1302 или 1305, и 60 секунд в остальных случаях. Если он задан, cooldownMs применяется, когда нет пригодного сигнала upstream Retry-After или сигнала сброса Codex, включая такие 429, ограничивающие частоту запросов. Принимаются числовые секунды в Retry-After и значения HTTP-date; любой cooldown ограничен 10 минутами. Приоритет от сильного к слабому: явный Retry-After → заголовки сброса Codex (x-codex-primary-reset-at, x-codex-secondary-reset-at или x-codex-tertiary-reset-at) → cooldownMs этой combo (если задан) → 5-секундный fallback для rate-limit-кодов upstream 1302/1305 → стандартные 60 секунд. Корректный немедленный Retry-After: 0 сохраняется как немедленная директива upstream, а не заменяется настроенным cooldown.
Текущий запрос никогда не повторяет уже опробованную цель. Более поздние запросы пропускают её, пока не истечёт cooldown. HTTP-date в Retry-After, указывающий на уже прошедшее время, также сохраняется как немедленная директива upstream, как и Retry-After: 0. Задайте waitForCooldownMs, чтобы следующий запрос мог подождать cooldown цели, которая станет подходящей раньше всех, до этого ограничения при каждой попытке выбора, а затем выполнить один новый выбор. Поэтому запрос с несколькими hop в failover может ждать в общей сложности до hops × waitForCooldownMs. По умолчанию это 0: если все подходящие цели находятся в cooldown, запрос немедленно завершается HTTP 503; этот ответ combo_unavailable содержит заголовок Retry-After, равный оставшемуся cooldown цели с самым ранним окончанием, округлённый вверх до целых секунд, минимум до 1 секунды. К ожиданиям не добавляется джиттер, поэтому возможны синхронные пробуждения. Отмена запроса отменяет это ожидание и возвращает обычный ответ client_cancelled; после отмены резервная цель не запускается. Cooldown цели combo — состояние процесса, отдельное для каждой combo; он не связан с cooldown квоты Codex на уровне аккаунта, который используется нативной маршрутизацией аккаунта.
Для потоковых запросов одного HTTP-статуса upstream недостаточно для окончательного решения. OpenCodex буферизует только ограниченный префикс Responses SSE выбранной дочерней цели до начала вывода. Если повторяемый terminal response.failed приходит до текста, reasoning, вызова инструмента или другого события вывода, попытка отмечается как неудачная и combo может перейти к следующей подходящей цели. После начала вывода или достижения лимита буфера текущая цель считается выбранной; более поздний сбой потока не воспроизводится у другого провайдера. Это предотвращает дублирование текста и выполнения инструментов.
Effort по умолчанию
Заголовок раздела «Effort по умолчанию»defaultEffort заполняет отсутствующий reasoning.effort, если задан defaultEffort, отличный от null, и список поддерживаемых уровней цели известен и непуст. Поддерживаемое настроенное значение сохраняется; иначе выбирается максимальный поддерживаемый уровень не выше него, а если такого нет — минимальный поддерживаемый уровень. При неизвестном или пустом списке default не добавляется.
Подстановка сохраняет существующий effort и остальные поля reasoning. Нормализация возможностей ниже может отдельно удалить неподдерживаемые параметры effort/thinking. Значения default: low, medium, high, xhigh, max, ultra; отсутствие поля или null отключает подстановку.
Разные возможности reasoning в одном combo
Заголовок раздела «Разные возможности reasoning в одном combo»reasoningEffortMode по умолчанию равен "strict": публикуется пересечение списков effort всех целей, включая явно пустые списки. "adaptive" исключает пустые списки из пересечения, сохраняя выбор effort для смешанного combo. Неизвестные списки не ограничивают пересечение каталога в обоих режимах.
При отправке явно пустой список удаляет параметры effort и thinking в обоих режимах; неизвестный список — только в adaptive. reasoning.summary и остальные поля, не задающие effort, сохраняются. Для известных непустых списков разрешение effort не меняется. Неизвестные цели в strict и неизвестные объявления обычного native Chat сохраняют параметры вызывающей стороны. Подстановка значения по умолчанию не заменяет существующий effort, но нормализация возможностей может удалить неподдерживаемые параметры.
Шифрованные задачи подагентов v2
Заголовок раздела «Шифрованные задачи подагентов v2»Есть одно важное ограничение для подагентов Codex v2 (issue #92). Нативный родитель может отправить задачу новому воркеру только как ciphertext, выпущенный для нативного backend ChatGPT. Внешний провайдер не может прочитать эту нагрузку.
Для такого запроса combo фильтрует подходящие цели до канонических нативных маршрутов ChatGPT, в том числе после retryable-сбоя. Если в combo нет цели, способной расшифровать задачу, opencodex останавливается ещё до dispatch и возвращает HTTP 400:
{ "error": { "type": "invalid_request_error", "code": "unreadable_encrypted_agent_task" }}Это защищает задачу от отправки провайдеру, который всё равно не получил бы читаемых инструкций. Обычные незашифрованные задачи используют стандартную стратегию combo.
Есть четыре варианта восстановления:
- Выбрать для потомка нативную модель ChatGPT.
- Добавить в combo каноническую нативную цель ChatGPT.
- Использовать поверхность v1 для делегирования между разными провайдерами.
- Если вы управляете вызывающей стороной, повторно отправить задачу как plaintext в содержимом
v2
agent_message.
Подробности о режимах v1/base/v2 и полном потоке encrypted task см. в Поверхности подагентов.
Управление combo
Заголовок раздела «Управление combo»Дашборд
Заголовок раздела «Дашборд»Откройте локальный дашборд и выберите Models → Combos. Рабочая область умеет создавать, редактировать, переименовывать и удалять combo, а селектор целей исключает отключённые модели и вложенные combo.
У каждой цели также отображается актуальный значок квоты: Доступно, Квота исчерпана или Квота неизвестна.
Редактор блокирует сохранение и создание из-за квоты только тогда, когда для каждой пригодной к использованию цели есть действующее подтверждение сервера об исчерпании лимита инференса для настроенных учётных данных. Квоты аккаунта, модели, поиска и MCP, предназначенные только для отображения, а также отсутствующие или просроченные данные для принятия решения о маршрутизации не вызывают эту блокировку. Блокировка истекает при соответствующем сбросе квоты или окончании срока актуальности данных и проверяется повторно, когда страница становится активной или видимой; «Обновить» повторно загружает и данные combo, и квоты. Редактор дашборда пока не предоставляет cooldownMs и waitForCooldownMs; до появления соответствующего UI используйте файл конфигурации или Management API.
Основные команды:
ocx combo listocx combo show <id>ocx combo set <id> --targets provider/model[:weight],...ocx combo remove <id> --yesset также принимает --strategy, --sticky, --effort, --alias, --native-alias,
--display-name и --rename-from. Значение - у --effort, --alias или --display-name
очищает соответствующее поле. Для --native-alias нужны поддерживаемый сейчас bare native
alias и непустой display name. create и update — alias для set, а delete — alias для remove;
те же подкоманды доступны через ocx route combo.
Management API
Заголовок раздела «Management API»Headless-клиенты используют GET, PUT и DELETE на /api/combos. GET возвращает список
нормализованных определений combo, PUT создаёт или заменяет одну combo (и умеет переименовывать),
а DELETE принимает id в query-параметре. Аутентификация и детали контрактов запросов/ответов
описаны в справочнике Management API. Если в теле PUT не указано cooldownMs или waitForCooldownMs, API сохраняет уже записанное для этой combo значение; чтобы изменить его, передайте значение явно. Явно переданный cooldownMs (в том числе 60000) сохраняется как есть, поскольку он переопределяет fallback для ограничения частоты запросов. Сохранённый cooldownMs можно удалить только редактированием файла конфигурации; waitForCooldownMs возвращается к стандартному значению, если PUT явно передаёт 0, поскольку разреженный сериализатор опускает это значение по умолчанию. Пропуск каждого поля сохраняет соответствующее значение, а дашборд пока не позволяет настраивать эти параметры.
Полную сохранённую конфигурацию см. в Конфигурации.
Справочник конфигурации
Заголовок раздела «Справочник конфигурации»Combo хранятся в объекте верхнего уровня combos, по ключу id combo:
{ "combos": { "balanced": { "targets": [ { "provider": "anthropic", "model": "claude-opus-4-8", "weight": 2 }, { "provider": "openai", "model": "gpt-5.6-sol", "weight": 1 } ], "strategy": "round-robin", "stickyLimit": 2, "defaultEffort": "high", "alias": "team/balanced" } }}| Поле | Обязательно | По умолчанию | Правила |
|---|---|---|---|
targets |
Yes | — | Непустой упорядоченный массив настроенных целей { provider, model, weight? }. Дубли пар provider/model запрещены. |
targets[].weight |
No | 1 |
Целое число от 1 до 10 000. Используется стратегиями round-robin и random; игнорируется стратегиями failover, least-used и reset-window. |
strategy |
No | "failover" |
"failover", "round-robin", "random", "least-used" или "reset-window". |
stickyLimit |
No | 1 |
Целое число от 1 до 100 успешных запросов на один выбор round-robin. Применяется только к round-robin. |
cooldownMs |
No | не задано → fallback upstream (5 с для rate-limit 429 с кодами 1302/1305, иначе 60 с) |
Целое число от 1 до 600000. Если задано, применяется как cooldown каждой цели, когда нет пригодного upstream Retry-After или сигнала сброса Codex, включая rate-limit 429; если не задано, используется fallback upstream. |
waitForCooldownMs |
No | 0 |
Целое число от 0 до 600000. Максимальное время ожидания самой ранней подходящей цели в cooldown перед возвратом combo_unavailable; отмена запроса отменяет ожидание. |
defaultEffort |
No | null |
low, medium, high, xhigh, max или ultra; применяется только когда вызывающая сторона не указала effort, а цель объявляет поддержку. |
reasoningEffortMode |
Нет | "strict" |
strict или adaptive; задаёт пересечение возможностей и нормализацию параметров конкретной цели. |
alias |
No | none | Необязательный обрезанный публичный id модели; используйте правила alias выше. Пустое значение хранится как отсутствие alias. |
nativeAlias |
No | false |
Явно разрешает поддерживаемому сейчас bare native alias перехватить приоритет routing/catalog только для неквалифицированного id. Bare gpt-5.6-* использует учётные данные Codex Pool/Direct; маршруты с квалификатором аккаунта сохраняют свою идентичность, а provider-qualified openai-apikey/gpt-5.6-* использует API-ключ и никогда не переходит на native alias. |
displayName |
No | none | Метка только для отображения в catalog; обязательна при nativeAlias: true. |
Устранение неполадок
Заголовок раздела «Устранение неполадок»Почему combo/<id> возвращает 404?
Заголовок раздела «Почему combo/<id> возвращает 404?»Id combo неизвестен. Ответ — HTTP 404 с типом invalid_request_error. Запустите ocx combo list,
проверьте написание и регистр, а также убедитесь, что management-команда записывала в тот же
запущенный экземпляр opencodex, который принимает запросы к моделям.
Почему я получаю combo_unavailable?
Заголовок раздела «Почему я получаю combo_unavailable?»Сейчас ни одна цель не подходит: например, провайдер отключён, находится в cooldown, уже был
испробован для этого запроса или исключён из-за шифрованной задачи v2. Проверьте состояние
провайдеров цели и недавние upstream-ошибки. Для cooldown сначала следуйте значению Retry-After из ответа. Заголовки сброса Codex также имеют приоритет над cooldownMs, поэтому если ни один upstream-сигнал не пригоден, применяется заданный cooldownMs, а если он не задан — upstream fallback (5 секунд для rate-limit-кодов 1302/1305, иначе 60 секунд); любой cooldown ограничен 10 минутами.
Почему alias был отклонён?
Заголовок раздела «Почему alias был отклонён?»Сначала проверьте грамматику alias и резервные имена. Дублирующийся alias или некорректная форма отклоняются как HTTP 400. Alias со слэшем, у которого первый сегмент совпадает с namespace уже настроенного аккаунта Codex, отклоняется как HTTP 409; выберите другое пространство имён alias. CLI и дашборд показывают точное сообщение валидации от сервера.
Почему failover остановился после первой ошибки?
Заголовок раздела «Почему failover остановился после первой ошибки?»Ошибка была terminal, а не специфичной для конкретной цели. Исправьте некорректный ввод, сократите слишком большой контекст, обработайте отказ политики или исправьте отклонённое происхождение запроса. В таких случаях combo не выполняют hop.
Совместимость необязательных параметров
Заголовок раздела «Совместимость необязательных параметров»Исключение для завершающих ошибок 400: структурированный отказ от user, неподдерживаемое значение reasoning.effort/reasoning_effort или специфичный для модели отказ входного изображения (param: input) позволяет до начала вывода перейти к следующей допустимой цели без периода охлаждения. Отказ политики безопасности, отмена и уже начавшийся вывод по-прежнему запрещают повторное выполнение.

