The single per-task «Complexity» pin was the wrong shape — what you actually
decide is WHICH model serves a task at WHICH complexity. A task rule is now a
per-tier map, edited in the console as rows of “model ▾ + complexity ▾” pairs
(plus ×/add) inside each task:
[routing.tasks.code]
low = ["cheap-coder"]
high = ["best-coder"]
any = ["generic-coder"] # leads at every tier after the tier pick
Chain order: the task's tier-specific list → (at high: the global high chain,
so escalation still beats any-tier pins) → the task's any-tier list → the
global tier chain → default as the last resort; deduplicated. Plain-array
rules keep meaning "any tier" unchanged, and the short-lived {models, tier}
form from the previous commit is normalized into the map on load.
Verified live: legacy config migrated visibly in the editor, pairs survive
save+reload, a math prompt at low ignored the math@high pair, per-tier leads
confirmed by unit tests (19 passing).
11 KiB
Read this in English.
Маршрутизация и стоимость
Как Cortiq Gateway превращает решение роутера (task_label + complexity.tier) в
выбор конкретной модели из вашего пула — и как сделать это cost-aware:
дешёвое локально, дорогое в облаке.
1. Базовая модель: полоса сложности → пул моделей
cortiq-router возвращает полосу сложности (low / medium / high, настраивается
в роутере). Таблица [routing] сопоставляет полосе упорядоченный список моделей —
это одновременно приоритет и цепочка fallback.
[routing]
low = ["local-qwen"] # простое → только локально (бесплатно)
medium = ["gpt-4o-mini", "local-qwen"] # средне → дешёвый хост, откат на локальное
high = ["claude-opus", "gpt-4o-mini"] # сложно → дорогая модель, откат на средн.
default = "gpt-4o-mini" # если роутер недоступен
Алгоритм выбора:
1. tier := router.complexity.tier (или default-полоса, если роутер недоступен)
2. candidates := routing[tier] (упорядоченный список model_id)
3. для каждого model_id по порядку:
если модель healthy И поддерживает нужные возможности (tools/vision):
выбрать её → выход
4. если все недоступны: следующая полоса вниз/вверх по политике, либо ошибка 503
2. Универсальный пул: локальные дешёвые + хостовые дорогие
Главный сценарий для разработчика: смешать бесплатную локальную модель и платный облачный API, и пусть шлюз сам решает, что куда.
# Локальная модель на vLLM / llama.cpp / Ollama (OpenAI-совместимый endpoint)
[[models]]
id = "local-qwen"
provider = "openai"
base_url = "http://localhost:8000/v1"
model = "qwen2.5-7b-instruct"
cost_tier = "cheap"
price_in = 0.0 # локально → 0
price_out = 0.0
caps = ["tools"] # что умеет (для проверки на этапе выбора)
# Дешёвый облачный
[[models]]
id = "gpt-4o-mini"
provider = "openai"
base_url = "https://api.openai.com/v1"
model = "gpt-4o-mini"
api_key_env = "OPENAI_API_KEY"
cost_tier = "mid"
price_in = 0.15 # $/1M входных токенов
price_out = 0.60
# Дорогой облачный для сложного
[[models]]
id = "claude-opus"
provider = "anthropic"
base_url = "https://api.anthropic.com"
model = "claude-opus-4"
api_key_env = "ANTHROPIC_API_KEY"
cost_tier = "expensive"
price_in = 15.0
price_out = 75.0
caps = ["tools", "vision"]
Поскольку local-qwen и gpt-4o-mini оба provider = "openai", шлюзу не важно,
где модель физически — локальные серверы уже говорят на OpenAI API. Это и делает
прокси универсальным.
3. Cost-aware режим (опционально)
Вместо жёсткой таблицы можно включить выбор по бюджету/цене (v0.2):
[routing.policy]
mode = "cost_aware" # fixed_table | cost_aware
# при cost_aware шлюз для каждой полосы берёт самую дешёвую модель,
# удовлетворяющую минимальному «классу качества» полосы:
min_class = { low = "cheap", medium = "mid", high = "expensive" }
# верхняя граница цены за запрос (страховка от дорогих вызовов)
max_cost_usd_per_request = 0.50
Логика: среди моделей не ниже min_class[tier] и не дороже лимита выбирается самая
дешёвая доступная. Это даёт «минимально достаточную» модель под сложность задачи.
4. Pinning и passthrough
- Passthrough.
model = "local-qwen"(реальный id) — без обращения к роутеру, прямой вызов. Полезно, когда клиент сам знает, что хочет. - Pinning профиля.
model = "cortiq-auto:cost-saver"— роутинг с конкретным policy-профилем роутера (реже эскалирует → дешевле). - Pinning версии роутера (v0.4). Чтобы поведение не «плыло» из-за самообучения
роутера, можно зафиксировать
model_version— агент получает воспроизводимый роутинг.
5. Извлечение текста для маршрутизации (мультитёрн)
Агентский запрос — это не одна строка, а system + история + tools. Что слать роутеру на классификацию, настраивается:
[route]
text_strategy = "last_user" # last_user | concat_all | last_user_plus_system
max_chars = 4000 # усечение длинного контекста (стоимость/DoS)
cache_ttl = "60s" # кэш решения по хэшу текста
profile = "balanced" # дефолтный policy-профиль
| Стратегия | Что классифицируем | Когда |
|---|---|---|
last_user |
последнее сообщение пользователя | по умолчанию; дёшево и точно для большинства агентов |
last_user_plus_system |
system + последнее user | когда роль задаётся системным промптом |
concat_all |
весь диалог (усечённый) | мультитёрн со смыслом, размазанным по истории |
6. Failover и circuit breaker
- Failover. При 5xx/таймауте/
overloadedот провайдера — следующая модель списка. - Circuit breaker. После
breaker.thresholdошибок подряд модель помечается «открытой» наbreaker.cooldown, запросы её минуют. Полупроброс — один пробный запрос после кулдауна. - Router down. Берётся
routing.default, запрос не падает (мягкая деградация).
[breaker]
threshold = 5
cooldown = "30s"
7. Петля обратной связи (опционально, v0.4)
Если включено, шлюз после успешного выполнения может отправить в роутер
/v1/feedback сигнал (например, по результату валидации ответа агентом) — роутер
дообучит маску задачи. Так маршрутизация со временем точнее ложится на ваш пул.
[feedback]
enabled = false
# источник истины задаёт интегратор (например, заголовок X-Cortiq-Correct-Label)
Распределение по задачам
Роутер классифицирует каждый запрос дважды: тип задачи (что это за работа) и уровень сложности (насколько она трудна). Уровни выбирают, сколько модели нужно; правила по задачам — какая модель лучше в этом деле:
[routing.tasks]
translation = ["local-qwen"] # переводы дёшевы — оставляем локально
code = ["claude-sonnet", "gpt"] # код — сильнейшему кодеру
Модели подходящего правила идут первыми в цепочке кандидатов; цепочка по сложности следует за ними как резерв — правило по задаче не ослабляет failover. Задачи без правила маршрутизируются только по сложности.
Модели по сложности внутри задачи. Правило задачи принимает и карту по уровням — «какая модель обслуживает задачу на какой сложности»:
[routing.tasks.code]
low = ["cheap-coder"] # рутинный код — дёшево
high = ["best-coder"] # сложный код — сильнейшему кодеру
any = ["generic-coder"] # ведёт на всех уровнях после уровневого выбора
Список конкретного уровня ведёт на своём уровне; any (и старая форма-массив)
ведёт на всех; глобальная цепочка уровня и default всегда остаются резервом —
правило задачи не ослабляет failover. На вкладке Роутинг это редактируется
строками пар «модель + сложность» у каждой задачи.
Исключение — высокая сложность эскалирует. Когда роутер оценил запрос как
high (и цепочка high настроена), цепочка уровня идёт первой, а правило
задачи становится резервом. Иначе правило вида code → дешёвая-модель
приковало бы к дешёвой модели даже самые сложные запросы, и система уровней
не имела бы права голоса. Для low/medium правило задачи ведёт, как и раньше. Список меток приходит
с роутера (GET /v1/taxonomies) — вкладка Роутинг строит редактор по живой
таксономии аккаунта и показывает долю каждой задачи в трафике. Когда цепочку
возглавило правило задачи, ответ сообщает route_source: "router+task-rule".