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

202 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
*Read this in [English](ROUTING.md).*
# Маршрутизация и стоимость
Как Cortiq Gateway превращает решение роутера (`task_label` + `complexity.tier`) в
выбор конкретной модели из вашего пула — и как сделать это **cost-aware**:
дешёвое локально, дорогое в облаке.
---
## 1. Базовая модель: полоса сложности → пул моделей
cortiq-router возвращает полосу сложности (`low` / `medium` / `high`, настраивается
в роутере). Таблица `[routing]` сопоставляет полосе **упорядоченный список** моделей —
это одновременно приоритет и цепочка fallback.
```toml
[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, и пусть шлюз сам решает, что куда.
```toml
# Локальная модель на 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):
```toml
[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. Что слать роутеру
на классификацию, настраивается:
```toml
[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`, запрос не падает (мягкая деградация).
```toml
[breaker]
threshold = 5
cooldown = "30s"
```
---
## 7. Петля обратной связи (опционально, v0.4)
Если включено, шлюз после успешного выполнения может отправить в роутер
`/v1/feedback` сигнал (например, по результату валидации ответа агентом) — роутер
дообучит маску задачи. Так маршрутизация со временем точнее ложится на ваш пул.
```toml
[feedback]
enabled = false
# источник истины задаёт интегратор (например, заголовок X-Cortiq-Correct-Label)
```
## Распределение по задачам
Роутер классифицирует каждый запрос дважды: **тип задачи** (что это за работа)
и **уровень сложности** (насколько она трудна). Уровни выбирают, *сколько
модели* нужно; правила по задачам — *какая* модель лучше в этом деле:
```toml
[routing.tasks]
translation = ["local-qwen"] # переводы дёшевы — оставляем локально
code = ["claude-sonnet", "gpt"] # код — сильнейшему кодеру
```
Модели подходящего правила идут **первыми** в цепочке кандидатов; цепочка по
сложности следует за ними как резерв — правило по задаче не ослабляет failover.
Задачи без правила маршрутизируются только по сложности.
**Закрепление сложности за задачей.** Правило задачи принимает и полную форму
с фиксированным уровнем — вердикт роутера для этой задачи заменяется до всех
остальных шагов:
```toml
[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"`.