Keystone 1.0: LLM-шлюз, который не хардкодит модели, держит свои GPU и не теряет ключи


Имя модели в коде устаревает в день выхода следующей модели. Если у вас десять сервисов, вы перепишете его десять раз и десять раз разнесёте по конфигам ключи вендоров.
Keystone. The Model Switchboard. (KTMS) убирает это из кода. Сервис говорит, насколько сложна задача, а шлюз решает, какую модель позвать, с каким ключом и не отдать ли запрос вашей собственной GPU. В простое он занимает 7 МиБ памяти, а с версии 1.0 его API в пределах 1.x не ломается.
Вместе со шлюзом 1.0 вышли веб-консоль ktms-admin и учёт расходов light-tokenmeter, все три на Go и под Apache 2.0. Возможности ниже описаны по актуальной версии 1.3.
Что такое Keystone за минуту
Keystone — self-hosted шлюз между вашими сервисами и LLM-вендорами. Сервисы обращаются к одной точке, а KTMS:
хранит ключи вендоров в Postgres, зашифрованные AES-256-GCM, и не показывает их клиентам;
выбирает модель по правилам, а не по имени в коде;
переключается на следующую модель, если вендор лежит или ограничивает запросы;
ограничивает запросы, токены и деньги на ключ, пользователя и команду;
отправляет событие о каждом вызове в сервис учёта.
ваши сервисы ──gRPC / HTTP /v1──▶ Keystone ──▶ OpenAI / Anthropic / Gemini / DeepSeek / Bedrock / ...
│ │ └──▶ ваши GPU-ноды (Ollama, vLLM, TGI, llama.cpp)
Postgres ┘ └ Redis (лимиты, кэш) └──▶ light-tokenmeter (учёт)

Сервис не пишет gpt-4o. Он просит сложность: LOW, MEDIUM или HIGH, и добавляет произвольные атрибуты (language, task, user_id). Упорядоченный список правил превращает это в цепочку моделей с фолбэками:
grpcurl -plaintext -H "authorization: Bearer $ADMIN_KEY" -d '{"rules":[
{"name":"spanish-high", "priority":30, "effort":"EFFORT_HIGH",
"attributes":{"language":"es"}, "use":["gemini-2.5-pro","gpt-4o"], "enabled":true},
{"name":"low-default", "priority":20, "effort":"EFFORT_LOW",
"use":["deepseek-chat","gpt-4o-mini"], "enabled":true},
{"name":"anything-else", "priority":1, "use":["gpt-4o-mini"], "enabled":true}
]}' localhost:9090 ktms.v1.AdminService/ReplaceRules
Правила меняются на лету, без релиза сервисов. Вышла новая модель — правите правило, а не сорок микросервисов.
Подключение без переписывания кода
Есть типизированный gRPC (ktms.v1) и OpenAI-совместимый HTTP. Существующий код на OpenAI SDK переключается сменой base_url:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="ktms_<ваш ключ>")
reply = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Say hi in three words."}],
)
print(reply.choices[0].message.content, reply.model)
В ответе заголовки X-KTMS-Vendor, X-KTMS-Rule и X-KTMS-Placement показывают, кто и по какому правилу ответил.
Что умеет (на версии 1.3)
Маршрутизация: effort LOW/MEDIUM/HIGH и атрибуты, правила с приоритетами, которые меняются на лету, цепочки фолбэков,
model: "auto"с судьёй (250 мс, откат на MEDIUM), канарейки с привязкой к пользователю, shadow-трафик с оценкой, сухой прогонktms route explain.Надёжность: circuit breaker, пауза модели по
Retry-After, одна попытка — один вызов, работа при отказе Postgres и Redis.Деньги и лимиты: RPM/TPM и allowlist моделей на ключ, дневные и месячные лимиты расходов на ключ, пользователя, команду и организацию, цены prompt cache, Batch API вендоров.
Свои GPU: агент ноды для Ollama, vLLM, TGI и llama.cpp, допуск по реальной очереди, placement «только свои ноды», уровни мощности и расписание нагрузки, локальный журнал
ktms-node stats.Безопасность: ключи вендоров в Postgres под AES-256-GCM и их ротация, маскирование PII, детекция prompt injection, латентные guardrails и семантический кэш (экспериментально).
API: gRPC
ktms.v1и OpenAI-совместимый HTTP, стриминг, tool calling, JSON Schema без потерь, embeddings, TTS, картинки, Judge.Управление: CLI
ktmsс контекстами иktms init, конфигурация в YAML (plan/apply), MCP-серверktms mcpдля ИИ-ассистентов, плагин для Claude Code, scopeadmin:read, веб-консоль ktms-admin.Эксплуатация: метрики
ktms_*с готовыми алертами, Helm-чарт в OCI, подписанные образы с SBOM и SLSA, тест обновления на каждом PR.
Сколько это стоит по ресурсам
Keystone — один Go-процесс без интерпретатора. На стенде из 11 шлюзов (Google Cloud c3-standard-4, 4 vCPU, mock-апстрим, два прогона):
~7 МиБ памяти в простое и ~20 МиБ при 200 RPS (Postgres и Redis не считаются);
~0,64 мс добавленной задержки на p50 и ~1,1 мс на p99;
не меньше 4000 RPS на 4 vCPU без ошибок;
~170 мс от
docker startдо первого ответа;образ ~21 МБ на скачивание.
Мы не самые быстрые: agentgateway, Apache APISIX и Kong добавляют меньше задержки и тратят меньше CPU на запрос. Зато по сочетанию памяти, старта и возможностей Keystone в верхней группе.

Что вошло в релиз
Что появилось на пути от 0.13 к 1.0.
Организации и команды. Ключи группируются в организации и команды. У команды свой список разрешённых моделей и месячный бюджет на все её ключи. ListModels показывает команде только её модели.
Надёжность вызовов вендоров. Встроенные ретраи SDK отключены, поэтому одна попытка роутера — ровно один вызов вендора. Шлюз читает Retry-After (и аналоги у Gemini) и ставит модель на паузу для конкретного ключа вендора, а цепочка идёт дальше. Плюс circuit breaker на пару (вендор, модель).
Поведение при отказах зависимостей. Описано и проверено для каждого случая. При отказе Postgres ключи, проверенные за последние 5 минут, продолжают работать, а маршрутизация идёт из последнего снимка конфигурации. При отказе Redis лимиты не блокируют трафик (для денег режим выбирается отдельно). События учёта копятся в очереди и доставляются ровно один раз.
Структурированный вывод без потерь. JSON Schema передаётся вендорам вместе с enum, format, nullable и additionalProperties. Если вендор не умеет структурированный вывод, шлюз сам проверяет ответ, один раз повторяет запрос и переходит к следующей модели.
Локальные ноды по реальной очереди. Агент ноды передаёт глубину очереди движка (vLLM, TGI, llama.cpp), и шлюз не отправляет запрос на ноду, где он будет ждать дольше допустимого.
Ротация ключей шифрования. Безопасная процедура: новые записи шифруются новой версией ключа, RotateEncryption перешифровывает ключи вендоров и токены нод в одной транзакции. Последовательность прогоняется на Postgres в CI.
Подписанные релизы. Образы и checksums.txt подписаны cosign в режиме keyless, у образов есть SBOM и SLSA-provenance, у архивов SPDX SBOM. govulncheck запускается на каждом pull request.
Лицензия Apache 2.0 вместо MIT: явная патентная лицензия для тех, кто встраивает шлюз в свои продукты.
Недавняя компрометация релиза одной популярной Python-библиотеки напомнила, что шлюз хранит ключи ко всем вендорам сразу. Подробный разбор того инцидента и что из него следует — в отдельной статье.
Два соседних проекта
ktms-admin — веб-консоль: модели, правила, ключи, учётные данные вендоров и песочница для запроса через ваши правила. Вход по паролю, через Google или любой OpenID Connect, GitHub или ваш access-прокси (oauth2-proxy, Authelia, Authentik, Pomerium, Cloudflare Access) со списком разрешённых адресов. Один бинарник, интерфейс встроен, сборки фронтенда нет.
light-tokenmeter — отвечает на вопрос «кто тратит токены и стоит ли оно того». Принимает событие на каждый вызов и отдаёт сводки по проектам, пользователям, действиям, вендорам и моделям. Консоль по этим сводкам помечает группы как greedy или worthy. Один бинарник, одна таблица Postgres, ClickHouse по желанию.

Как попробовать
git clone https://github.com/ai-process/ktms.git && cd ktms
OPENAI_API_KEY=sk-... docker compose up -d --build
curl -s http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer ktms_dev_default_key" -H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"Say hi in three words."}]}'
Compose поднимает Postgres, Redis и шлюз с готовыми моделями, правилами и ключом для разработки.
Что значит 1.0
До 1.0 мы ничего не обещали. С 1.0 в пределах 1.x несовместимо не меняется:
Поверхность | Что сохраняется в 1.x |
|---|---|
gRPC | RPC, поля, enum и номера полей. |
HTTP | Пути, поля запросов и ответов, имена заголовков |
Переменные окружения | Имена, значения по умолчанию и смысл |
База данных | Любой 1.x сам обновляет базу предыдущего, без ручного SQL |
Метрики | Имена, метки, совместимость в обе стороны |
Коды и причины ошибок | Смысл сохраняется, новые причины могут появиться |
Что помечено экспериментальным и может поменяться в минорном релизе: выбор effort судьёй, shadow-трафик, семантический кэш и латентные guardrails, JudgeStream и EvaluateRollouts, формат YAML-конфигурации и команда ktms до версии формата v1. Полный список — в README, а в proto такие поля отмечены комментарием Experimental:.
Обещание проверяется автоматически. На каждом pull request и перед каждым релизом тест обновления поднимает базу предыдущего релиза и самого старого релиза той же мажорной версии, мигрирует её новой сборкой и снова запускает старую версию на мигрированной базе. Так проверяется и обновление, и rolling-деплой. Релиз публикуется только после этого теста и buf breaking.
Что дальше
После 1.0 в планах: маршрутизация чувствительных данных только на свои ноды, hedged-запросы, корректирующий повтор для JSON, Grafana-дашборд и рецепты в документации. Ничто из этого не ломает API 1.x
Репозиторий: github.com/ai-process/ktms
Документация: ai-process.dev
Консоль: github.com/ai-process/ktms-admin
Учёт расходов: github.com/ai-process/light-tokenmeter
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.