Files
cortiq-gateway/docs/ARCHITECTURE.ru.md
T
infosave2007 cbc385f6c7 feat: universal LLM gateway with embedded multilingual admin console
- OpenAI-compatible gateway with intelligent routing via cortiq-router (allaigate)
- Embedded build-free admin SPA at /admin: 7 languages (en/ru/de/fr/es/zh/tr),
  light/dark themes, hot config reload (no restart)
- Admin API, per-request statistics + Prometheus /metrics, secret store, playground
- English documentation (primary) with Russian translations (*.ru.md)
2026-06-30 10:51:37 +03:00

203 lines
12 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](ARCHITECTURE.md).*
# Архитектура Cortiq Gateway
Документ описывает внутреннее устройство шлюза, поток запроса, абстракции и
дорожную карту. Сопутствующие документы: [PROTOCOLS.md](PROTOCOLS.ru.md) (контракты
протоколов), [ROUTING.md](ROUTING.ru.md) (модель выбора моделей и стоимости).
---
## 1. Назначение и границы
**Cortiq Gateway** — это L7-прокси, который:
1. принимает запрос в одном из **стандартных протоколов** (OpenAI, Anthropic, MCP);
2. спрашивает **cortiq-router**, какой это тип задачи и насколько он сложный;
3. выбирает конкретную модель из **пула провайдеров** (локальные + облачные);
4. вызывает провайдера, **транслируя протокол** при необходимости;
5. возвращает ответ клиенту (стриминг и без), добавляя метаданные роутинга;
6. учитывает стоимость и (опц.) замыкает петлю обратной связи в роутер.
Что шлюз **не** делает: не хранит и не обучает модели (это роутер), не реализует
сами LLM (это провайдеры). Шлюз — тонкий, без состояния (кроме кэшей идемпотентности
и rate-limit), горизонтально масштабируется.
---
## 2. Компонентная схема
```
┌───────────────────────────────────────────────┐
│ Cortiq Gateway │
│ │
client (any protocol) │ ┌──────────────┐ ┌───────────────────┐ │
───────────────────────▶│──▶│ Inbound │──▶│ Core pipeline │ │
│ │ Protocol │ │ │ │
│ │ Adapters │ │ 1. extract text │ │
│ │ (openai/ │ │ 2. route() │────┼──▶ cortiq-router
│ │ anthropic/ │ │ 3. select model │ │ /v1/route
│ │ mcp) │ │ 4. translate │ │
│ └──────────────┘ │ 5. call provider │────┼──▶ Provider pool
│ │ 6. stream back │ │ (openai/anthropic/
│ ┌──────────────┐ │ 7. usage/cost │◀───┼── ollama/http)
│ │ Cross-cutting│ │ 8. feedback (opt)│────┼──▶ cortiq-router
│ │ auth · rate │ └───────────────────┘ │ /v1/feedback
│ │ idem · trace │ │
│ └──────────────┘ │
└───────────────────────────────────────────────┘
```
Модули кода (`src/`):
| Модуль | Ответственность |
|---|---|
| `main.rs` | загрузка конфига, сборка роутов по включённым протоколам, запуск сервера |
| `config.rs` | парсинг TOML: пул моделей, таблица роутинга, протоколы, ключи |
| `router_client.rs` | клиент к `cortiq-router` (`/v1/route`, `/v1/feedback`) |
| `registry.rs` | реестр моделей пула + резолв `id → провайдер+endpoint` |
| `routing.rs` | `(label, tier) → упорядоченный список моделей`; fallback/default |
| `pipeline.rs` | ядро: extract → route → select → translate → call → stream |
| `providers/mod.rs` | трейт `Provider` (унифицированный вызов LLM) |
| `providers/openai.rs` | OpenAI-совместимый адаптер (covers vLLM/llama.cpp/ollama/openrouter) |
| `providers/anthropic.rs` | адаптер Anthropic Messages |
| `protocols/mod.rs` | трейт входящего адаптера + общие типы канонической модели сообщения |
| `protocols/openai_chat.rs` | вход `/v1/chat/completions` ↔ каноническая модель |
| `protocols/anthropic_messages.rs` | вход `/v1/messages` ↔ каноническая модель |
| `protocols/mcp.rs` | MCP-сервер (JSON-RPC), маршрутизация как инструмент |
| `auth.rs` | виртуальные ключи, rate-limit, квоты |
| `telemetry.rs` | метрики Prometheus, OpenTelemetry-спаны, request-id |
| `error.rs` | единый тип ошибки + рендер в OpenAI-конверт |
---
## 3. Каноническая модель (внутренний нейтральный формат)
Чтобы N входящих протоколов × M провайдеров не превратились в N×M переводов,
внутри всё приводится к **одной канонической модели** запроса/ответа.
```
ChatRequest {
routing: RoutingDirective, // auto | pinned(model_id) | profile
messages: Vec<Message>, // role + content (+ tool_calls)
tools: Vec<Tool>,
params: GenParams, // temperature, max_tokens, top_p, stop, ...
stream: bool,
meta: RequestMeta, // idempotency key, traceparent, account
}
ChatResponse {
id, model_used, choices, usage, finish_reason, cortiq: RouteInfo
}
```
- Входящий адаптер: `protocol-specific → ChatRequest` и `ChatResponse → protocol-specific`.
- Исходящий провайдер: `ChatRequest → provider wire` и `provider wire → ChatResponse`.
Добавить новый протокол = один входящий адаптер. Добавить нового провайдера =
один исходящий адаптер. Никакого комбинаторного взрыва.
---
## 4. Трейты-расширения
```rust
// входящий протокол
#[async_trait]
trait InboundProtocol {
fn routes(&self) -> Router; // axum-роуты этого протокола
fn name(&self) -> &'static str; // "openai_chat", ...
}
// исходящий провайдер
#[async_trait]
trait Provider: Send + Sync {
fn id(&self) -> &str;
async fn chat(&self, req: ChatRequest) -> Result<ChatResponse>;
async fn chat_stream(&self, req: ChatRequest) -> Result<ChatStream>; // SSE-поток
async fn embed(&self, req: EmbedRequest) -> Result<EmbedResponse>;
fn capabilities(&self) -> Caps; // tools? streaming? vision?
}
```
Реестр (`registry.rs`) держит `HashMap<model_id, Arc<dyn Provider>>`, построенный
из `[[models]]`. Таблица роутинга (`routing.rs`) оперирует `model_id`.
---
## 5. Поток `route → select → call` детально
1. **Extract.** Из `messages` берётся текст для классификации. Стратегия из конфига:
`last_user` (по умолчанию) | `concat_all` | `last_user_plus_system`. Длинный
текст усечётся до `route.max_chars` (защита от стоимости/DoS).
2. **Route.** `router_client.route(text, profile)` → `{task_label, complexity}`.
Кэшируется по хэшу текста на `route.cache_ttl` (мелкая экономия на повторах).
3. **Select.** `routing.select(label, tier)` → упорядоченный `Vec<model_id>`.
Учитываются: доступность модели (health), её `capabilities` (нужны ли tools),
и cost-политика (см. [ROUTING.md](ROUTING.ru.md)).
4. **Translate + Call.** Канонический `ChatRequest` → провайдер. Стриминг проксируется
чанк-в-чанк (SSE → SSE) без буферизации.
5. **Failover.** Если провайдер вернул 5xx/таймаут/перегрузку — следующий из списка.
Circuit breaker помечает «больную» модель, чтобы не долбить её.
6. **Degrade.** Если cortiq-router недоступен — берётся `routing.default` (мягкая
деградация, запрос не падает).
---
## 6. Надёжность
| Механизм | Поведение |
|---|---|
| Таймауты | отдельные на router-вызов, connect и стрим провайдера |
| Failover | по упорядоченному списку моделей полосы |
| Circuit breaker | временно исключает модель после серии ошибок |
| Router down | `routing.default` |
| Все провайдеры полосы down | OpenAI-ошибка `503 upstream_unavailable`, `retriable=true` |
| Идемпотентность | повтор с тем же `Idempotency-Key` отдаёт сохранённый ответ |
---
## 7. Безопасность и мультиарендность
- **Изоляция ключей.** Агенты держат только виртуальный ключ шлюза; ключи провайдеров
(`*_API_KEY`) живут в окружении шлюза и наружу не уходят.
- **Аккаунты.** Каждый `[[api_keys]]` → аккаунт с rate-limit, квотой, (опц.) своим
подмножеством разрешённых моделей и своей таблицей роутинга.
- **PII.** Редакция выполняется в роутере перед эскалацией к оракулу; шлюз дополнительно
может включить логирование без тел сообщений (`log.bodies = false` по умолчанию).
- **Аудит.** Каждый запрос: `request_id`, аккаунт, выбранная модель, токены, стоимость,
путь решения — без текста промпта по умолчанию.
---
## 8. Наблюдаемость
`GET /metrics` (Prometheus), `GET /healthz`, `GET /readyz`. Метрики:
```
gw_requests_total{protocol,account}
gw_route_calls_total # обращения к cortiq-router
gw_model_selected_total{model_id,tier}
gw_provider_calls_total{provider,model_id,outcome}
gw_failovers_total
gw_tokens_total{model_id,direction}
gw_cost_usd_total{account,model_id}
gw_latency_seconds{stage} # route | provider | total (histogram)
```
OpenTelemetry-спаны: `inbound → route → provider`, с пробросом `traceparent`.
---
## 9. Дорожная карта
| Фаза | Содержимое |
|---|---|
| **v0.1** | OpenAI Chat/Completions/Embeddings/Models, провайдер `openai`, роутинг + fallback, auth, метрики, стриминг |
| **v0.2** | Провайдер `anthropic` + входящий Anthropic Messages, cost-aware выбор, идемпотентность, circuit breaker |
| **v0.3** | MCP-сервер, провайдер `ollama` нативный, OpenTelemetry, кэш роутинга |
| **v0.4** | Per-account таблицы роутинга, feedback-loop в роутер, нагрузочные тесты, Helm-чарт |
| **v1.0** | Стабилизация контракта, OpenAPI-спека, официальные примеры под crewAI/LangChain/Hermes |
Текущая фаза — **Phase 0** (скелет + спецификация в этом репозитории).