- 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)
12 KiB
Read this in English.
Архитектура Cortiq Gateway
Документ описывает внутреннее устройство шлюза, поток запроса, абстракции и дорожную карту. Сопутствующие документы: PROTOCOLS.md (контракты протоколов), ROUTING.md (модель выбора моделей и стоимости).
1. Назначение и границы
Cortiq Gateway — это L7-прокси, который:
- принимает запрос в одном из стандартных протоколов (OpenAI, Anthropic, MCP);
- спрашивает cortiq-router, какой это тип задачи и насколько он сложный;
- выбирает конкретную модель из пула провайдеров (локальные + облачные);
- вызывает провайдера, транслируя протокол при необходимости;
- возвращает ответ клиенту (стриминг и без), добавляя метаданные роутинга;
- учитывает стоимость и (опц.) замыкает петлю обратной связи в роутер.
Что шлюз не делает: не хранит и не обучает модели (это роутер), не реализует сами 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. Трейты-расширения
// входящий протокол
#[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 детально
- Extract. Из
messagesберётся текст для классификации. Стратегия из конфига:last_user(по умолчанию) |concat_all|last_user_plus_system. Длинный текст усечётся доroute.max_chars(защита от стоимости/DoS). - Route.
router_client.route(text, profile)→{task_label, complexity}. Кэшируется по хэшу текста наroute.cache_ttl(мелкая экономия на повторах). - Select.
routing.select(label, tier)→ упорядоченныйVec<model_id>. Учитываются: доступность модели (health), еёcapabilities(нужны ли tools), и cost-политика (см. ROUTING.md). - Translate + Call. Канонический
ChatRequest→ провайдер. Стриминг проксируется чанк-в-чанк (SSE → SSE) без буферизации. - Failover. Если провайдер вернул 5xx/таймаут/перегрузку — следующий из списка. Circuit breaker помечает «больную» модель, чтобы не долбить её.
- 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 (скелет + спецификация в этом репозитории).