Files
cortiq-gateway/docs/ROUTING.ru.md
T
infosave2007 95e560dc72 feat: per-key spend accounting and per-task complexity pinning
API keys page: the table now answers "which department spends what" — per-key
requests, tokens in/out and cost for a selectable window (24h/7d/30d) plus a
cumulative all-time cost column, joined from stats by the key's account.

The stats log stays bounded while the accounting survives: once stats.jsonl
exceeds max_file_mb (default 16, 0 disables), records older than the
retention window fold into cumulative per-account totals in a small sidecar
file (tmp+rename on both) and leave the log. alltime = folded + in-file, a
record is never counted twice, verified end-to-end on synthetic data
(1.2 MB → 301 KB, totals identical before/after compaction and restarts).

Routing: a task rule can now PIN the complexity tier —
`[routing.tasks.legal] models=[...] tier="high"` (old plain-array rules parse
unchanged). The pinned tier replaces the router's verdict before the
escalation check and reports itself as route_source "router+task-rule@high".
The Routing page grows a per-task «Complexity» selector (Auto/low/medium/
high); verified live: a code prompt with a pinned high went
router+task-rule@high → the expensive model.

13 new strings in all 7 languages; docs/ROUTING(.ru) updated.
2026-08-08 08:42:11 +03:00

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.legal]
models = ["strong-model"]
tier   = "high"      # юридические тексты — всегда high, что бы ни решил роутер

Ответы с закреплённым уровнем сообщают route_source: "router+task-rule@high". На вкладке Роутинг это селектор «Сложность» у каждой задачи (Авто / low / medium / high).

Исключение — высокая сложность эскалирует. Когда роутер оценил запрос как high (и цепочка high настроена), цепочка уровня идёт первой, а правило задачи становится резервом. Иначе правило вида code → дешёвая-модель приковало бы к дешёвой модели даже самые сложные запросы, и система уровней не имела бы права голоса. Для low/medium правило задачи ведёт, как и раньше. Список меток приходит с роутера (GET /v1/taxonomies) — вкладка Роутинг строит редактор по живой таксономии аккаунта и показывает долю каждой задачи в трафике. Когда цепочку возглавило правило задачи, ответ сообщает route_source: "router+task-rule".