Docker / publish (push) Canceled after 0s
Release / crates-io (push) Canceled after 0s
Release / release (macos-14, aarch64-apple-darwin) (push) Canceled after 0s
Release / release (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Canceled after 0s
Release / release (windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
188 lines
9.8 KiB
Markdown
188 lines
9.8 KiB
Markdown
*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.
|
||
Задачи без правила маршрутизируются только по сложности.
|
||
|
||
**Исключение — высокая сложность эскалирует.** Когда роутер оценил запрос как
|
||
`high` (и цепочка `high` настроена), цепочка уровня идёт первой, а правило
|
||
задачи становится резервом. Иначе правило вида `code → дешёвая-модель`
|
||
приковало бы к дешёвой модели даже самые сложные запросы, и система уровней
|
||
не имела бы права голоса. Для low/medium правило задачи ведёт, как и раньше. Список меток приходит
|
||
с роутера (`GET /v1/taxonomies`) — вкладка Роутинг строит редактор по живой
|
||
таксономии аккаунта и показывает долю каждой задачи в трафике. Когда цепочку
|
||
возглавило правило задачи, ответ сообщает `route_source: "router+task-rule"`.
|