
pacod-gw стоит между вашими приложениями и LLM-провайдерами. Ядро на Rust проксирует или транслирует каждый вызов, не допускает ключи провайдеров в ваши приложения, следит за лимитами, рассчитывает стоимость каждого запроса и может ответить на повторный вызов из своего кэша. Данные об использовании идут в аналитику на ClickHouse, а управляется всё из консоли на SvelteKit.
AI-шлюз
- Лицензия: Apache-2.0
- Версия: 0.1.0 · Предварительная
- Языки: Rust, TypeScript, Python
- Технологии: Rust, PostgreSQL, ClickHouse, Valkey, SvelteKit, Caddy, Docker, Helm, Prometheus, OpenTelemetry
- Открыть на GitHub · Создать issue
Возможности
Proxy или managed
Проксируйте API провайдера байт в байт, включая сессии WebSocket, или доверьте шлюзу разрешение шаблонов, управление параметрами и трансляцию между диалектами.
Много диалектов, один контракт
Chat Completions и Responses от OpenAI, Messages от Anthropic, generateContent от Gemini и собственный интерфейс, а также эмбеддинги, изображения, синтез речи, транскрипция, rerank и Realtime.
Ключи остаются в шлюзе
Учётные данные провайдеров, запечатанные для инсталляции, организации или рабочего пространства, и ключи шлюза для каждого рабочего пространства. Приложения никогда не видят ключ провайдера.
Лимиты до вызова
Лимиты на токены, запросы и расходы для организации, рабочего пространства, ключа и конечного пользователя проверяются до того, как вызов покинет шлюз.
Стоимость каждого вызова
Версионированная цена для каждой модели, включая тарифы на кэш и рассуждения. Ответ managed может нести свою стоимость, а у каждого вызова она есть в журнале запросов.
Кэш ответов
Повторный вызов с температурой 0 получает ответ из кэша, по умолчанию отдельно для каждого API-ключа и в запечатанном виде в Valkey. Попадание ничего не стоит и не учитывается ни в одном лимите. В новой инсталляции выключен, пока его не включат.
Аналитика на ClickHouse
События поступают в отдельный сервис аналитики с отчётами и задачами по расписанию; для эксплуатации — Prometheus, Grafana и OpenTelemetry.
Маршрутизация и намерения
Схемы маршрутизации с правилами, переключение на резервную модель через границу диалектов и предохранитель, а также плагин намерений с движками LLM, Jev и cascade, который может заблокировать, направить или учесть вызов по его назначению.
Режим Lite
Та же плоскость данных в одном контейнере — без базы данных и консоли — с настройкой через один файл .env и строкой лога со стоимостью для каждого вызова.

Скриншоты





Зачем нужен шлюз
Каждый продукт, работающий с языковой моделью, в итоге заново реализует одно и то же: хранение ключей провайдеров, трансляцию между API, подсчёт токенов, соблюдение лимитов и объяснение счёта. pacod-gw забирает всё это из приложения. Приложение обращается к одной конечной точке с ключом шлюза; шлюз решает, какая модель обслужит вызов, сколько он может стоить и кто за него платит.
Первая цель конкретна: заменить LiteLLM-прокси перед приложением, изменив только его базовый URL и ключ.
Два режима, одно ядро
Ядро — это один бинарный файл на Rust с двумя слушателями: плоскостью данных для приложений и API управления для консоли и скриптов. Режим выбирается префиксом URL и разрешается для каждого ключа.
- Режим proxy (
/proxy/{provider}/…) передаёт собственный API провайдера байт в байт, включая сессии WebSocket, и добавляет аутентификацию, лимиты и учёт. Шаблон при этом всё равно может задать модель и её параметры. - Режим managed обслуживает
/v1/chat/completionsи/v1/responsesот OpenAI,/v1/messagesот Anthropic,generateContentот Gemini и собственный/gw/v1/chat. Помимо чата он обслуживает эмбеддинги, синтез речи, транскрипцию и перевод аудио, генерацию и редактирование изображений, rerank в формате Cohere (у Cohere, Jina, Voyage и vLLM), Realtime-сессии OpenAI через WebSocket, а также API Files и Batches от OpenAI и Anthropic.
В режиме managed маршрут определяют диалект вызывающей стороны и провайдер модели:
| Вызывающая сторона говорит | Модель находится у | Что происходит |
|---|---|---|
| Диалект OpenAI | OpenAI-совместимого провайдера (OpenAI, DeepSeek, GLM, Kimi, Ollama, …) | Байты вызывающей стороны правятся точечно; стоимость добавляется в usage. |
| Диалект Anthropic | Anthropic | Диалог доходит до Anthropic в том виде, в каком был отправлен; стоимость возвращается в заголовке. |
| Responses API от OpenAI | OpenAI или xAI | Байты вызывающей стороны правятся точечно, всегда с store: false; стоимость добавляется в usage. |
| Любая другая пара и собственный интерфейс | любого провайдера | Трансляция через нейтральную модель и обратно, поэтому вызов может переключиться на резервную модель и через границу диалектов. |
Сохранённые ответы. Responses API от OpenAI умеет хранить ответ, чтобы следующий вызов лишь
ссылался на него. Шлюз никогда не позволяет провайдеру хранить ответы: учётные данные, общие для
нескольких рабочих пространств, позволили бы каждому читать ответы других. Если рабочее
пространство включает responses.store, шлюз сам хранит его ответы в PostgreSQL, по умолчанию 30
дней, и при следующем вызове заново отправляет предыдущие ходы. Так диалог может переходить между
провайдерами: ход, на который ответил Claude, можно продолжить у OpenAI и обратно. Conversations
API от OpenAI (/v1/conversations) хранится так же.
Один вызов, шаг за шагом
- Аутентификация. Ключ ищется по HMAC в снимке конфигурации в памяти.
- Чтение. Сканер находит модель и флаги потоковой передачи, не разбирая всё тело. Вызов, продолжающий сохранённый ответ, получает свои предыдущие ходы; плагины рабочего пространства видят диалог.
- Разрешение. Шаблон (
@nameилиx-gw-template) или идентификатор разрешённой модели определяет модель и её параметры. - Поиск в кэше. Если кэш ответов включён, в режиме managed сохранённый ответ на тот же запрос возвращается здесь, в диалекте вызывающей стороны, прежде чем вызов займёт слот или будет учтён в лимите.
- Допуск. Ограничение одновременных вызовов ключа и каждый применимый лимит должны быть соблюдены.
- Отправка. Сохранённые учётные данные провайдера заменяют ключ шлюза. В режиме managed ответ 429, 5xx или потерянное соединение повторяются или передаются следующей модели, прежде чем вызывающая сторона получит хоть один байт.
- Передача и расчёт. Ответ возвращается потоком, а отвод читает использование из копии; затем обновляются стоимость и счётчики, ответ сохраняется, если его берёт кэш, а событие использования уходит в спул на диске.
Ответ содержит x-gw-request-id, x-gw-delay-ms (время, добавленное самим шлюзом),
x-gw-model-served, x-gw-route, когда модель выбрала схема маршрутизации, x-gw-cache, когда
кэш рассматривал вызов, и, для вызовов managed без потоковой передачи, x-gw-cost.
Конфигурация хранится в PostgreSQL, но путь запроса её не ждёт: изменение подменяет снимок примерно за две секунды, а если база данных недоступна, трафик продолжает идти на последнем снимке. Исключений немного, и они названы: вызов, ссылающийся на сохранённый ответ или загруженный файл, читает его запись, сохранённый ответ записывается до того, как будет отдан, а кэш ждёт Valkey по умолчанию 100 мс и обходится без него, если тот не отвечает.
Организации, рабочие пространства и ключи
Организация — это клиент или крупное подразделение. Рабочее пространство — одно приложение, проект, среда или команда; ему принадлежат его ключи, шаблоны, лимиты, настройки и использование. Два правила покрывают любое ограничение: списки разрешений пересекаются, а числовые пределы берут минимум.
Ключи провайдеров принадлежат инсталляции, организации или рабочему пространству, и вызов использует ближайший: собственный ключ рабочего пространства, затем ключ его организации, затем общий ключ инсталляции. Ключ, который шлюз уже хранит, можно передать вниз организации или рабочему пространству, не вводя его заново; секрет никогда не покидает шлюз.
Роли описаны в одном файле, authz/gw.yaml, из которого генерируются проверки и ядра, и консоли:
| Роль | Что охватывает |
|---|---|
| Администратор платформы | Провайдеры, учётные данные, модели, цены, организации, люди |
| Администратор организации | Её настройки, рабочие пространства, участники и выданные модели |
| Администратор рабочего пространства | Шаблоны, ключи, лимиты, настройки и режим аудита одного рабочего пространства |
| Аудитор | Читает конфигурацию, использование и записи режима аудита |
| Наблюдатель | Читает конфигурацию и использование; без секретов и записей режима аудита |
Вызовы несут атрибуцию — end_user, session_id, trace_id, объявленные теги, agent_id,
task, complexity, — которую используют отчёты, лимиты и маршрутизация. Пользователи входят в
консоль с локальными учётными записями или, при PGW_AUTH=oidc, через pacod-id: шлюз регистрирует
там свои организации и рабочие пространства и следует его ленте ролей, а завершённая сессия
отклоняется в течение двух секунд.
Лимиты и стоимость
Лимит применяется к организации, рабочему пространству, ключу или конечному пользователю. Он
считает токены, запросы или расходы за минуту, час, день, неделю, месяц или за всё время и либо
отклоняет вызовы (429, 402), либо только считает и сообщает. Приложения могут узнать состояние
квоты пользователя через GET /gw/v1/quota. Счётчики объединяются между инстансами через
PostgreSQL примерно раз в секунду, поэтому лимит может быть превышен примерно на секунду трафика.
Каждый вызов оценивается при завершении по версионированной цене. Токены тарифицируются за миллион, с отдельными тарифами на чтение и запись кэша, рассуждения, аудио- и графические токены, где они есть в цене, а также пакетные варианты, ступени по размеру входа и внепиковые окна. Символы синтеза речи, изображения, секунды аудио, поисковые единицы, вызовы инструментов на стороне провайдера и запросы имеют поштучные тарифы. Цены по умолчанию нет: модель можно активировать только с ценой или с явной пометкой «бесплатно».
Обнаружение моделей по умолчанию раз в день запрашивает у каждого провайдера список его моделей и предлагает те, которых в шлюзе нет; модель, добавленная из предложения, остаётся черновиком, пока у неё нет цены. Трекер цен по расписанию читает прайс-лист LiteLLM или страницу цен провайдера и применяет изменения в пределах заданной границы либо предлагает их на утверждение. Пока у него нет источников, консоль предлагает список LiteLLM для OpenAI, Anthropic, Gemini, DeepSeek и z.ai.
Кэш ответов
Приложения часто отправляют один и тот же запрос дважды: набор тестов, классификация с
температурой 0, страница, которая задаёт модели один и тот же вопрос для каждого посетителя. Кэш
ответов отвечает на такой вызов ответом, который модель уже дала, не обращаясь к провайдеру. Это
плагин response_cache, установленный по умолчанию: выключен в новой инсталляции, пока
администратор его не включит, и включён в стеке разработки и в демо.
«Тот же запрос» означает, что той же модели отправлено то же тело — после применения шаблона и приведения тела к форме модели. Потоковый и цельный ответ делят один сохранённый ответ, и он возвращается в собственном диалекте вызывающей стороны. Попадание ничего не стоит: нет вызова к провайдеру, стоимость 0, слот ключа не занимается, лимиты не учитывают его. Его событие использования фиксирует сэкономленное, а отчёт показывает попадания, промахи и экономию во времени, по моделям и по рабочим пространствам.
| Настройка | По умолчанию | Что решает |
|---|---|---|
mode |
off (новая инсталляция) |
enforce отвечает из кэша; shadow только отмечает would_hit, чтобы сначала увидеть долю попаданий |
calls |
deterministic |
Вызовы с температурой 0 или all |
scope |
key |
Кто делит ответы: один API-ключ, один конечный пользователь ключа или всё рабочее пространство (workspace) |
ttl_seconds |
3600 | Сколько хранится ответ, не дольше недели |
max_answer_bytes |
1 МиБ | Ответы крупнее не сохраняются |
Платформа задаёт значение по умолчанию, которое организация или рабочее пространство может
заменить; организация может зафиксировать свой выбор. Вызывающая сторона решает для каждого вызова
через x-gw-cache: off, refresh или on, а ответ сообщает hit, miss, would_hit, refresh
или bypass в том же заголовке, с Age при попадании. Кэшируются только вызовы managed: не режим
proxy, эмбеддинги, синтез речи, подсчёт токенов, пакеты и не собственные вызовы шлюза. Отказы,
оборванные потоки и ответы, в которых использовались собственные инструменты провайдера, например
веб-поиск, никогда не сохраняются.
Ответы хранятся в Valkey (gw-valkey), общем для всех шлюзов и только в памяти. Каждый ответ
запечатывается с AES-256-GCM под ключом, выведенным из ключа шифрования ключей для его рабочего
пространства, и под именем, которое является хешем запроса с ключом: тот, кто читает Valkey, не
узнаёт ни вопроса, ни ответа, а ответ, который не открывается, считается промахом. Ответы никогда
не переходят между рабочими пространствами. Администратор очищает рабочее пространство,
организацию или всю платформу разом, а правило аудита forbid выключает кэш везде, где действует.
Маршрутизация и намерения
Шаблон может содержать пул моделей с уровнями, весами и приоритетами. Без дополнительной настройки
обслуживает первая модель, а следующая подхватывает при 429, 5xx или потере соединения. Схема
маршрутизации добавляет правила по тому, что сообщает вызывающая сторона (complexity, task,
теги, размер входа или выражение CEL), и алгоритмы: по приоритету, взвешенный, round robin,
наименьшая задержка и самый дешёвый, с необязательной привязкой к сессии, чтобы кэши промптов
оставались «тёплыми». Предохранитель (circuit breaker) отодвигает сбоящую модель, а каждый ответ
называет в x-gw-route правило, по которому он был направлен.
Плагин намерений отвечает на вопрос «для чего этот вызов?» по каталогу, который пишет организация или рабочее пространство, с помощью LLM, движка Jev или каскада из обоих. Его вердикт может заблокировать вызов, направить его, учесть в лимитах по намерению или пометить для проверки; сначала он может работать в теневом режиме и никогда не меняет диалог.
Новый каталог начинается со встроенного шаблона starter с намерениями универсального ассистента
и правилами, которые помечают и блокируют. Новая привязка начинается с шаблона, который шлюз
заполняет для своей области: теневой режим, этот каталог и самая дешёвая выданная модель, которая
отвечает по JSON-схеме, или Jev, если такой нет. Панели пробы и оценки в консоли запускают движки
на примерах текстов, сначала на демонстрационных данных, подготовленных для каталога, чтобы
сравнить движки до того, как привязка начнёт применять решения. См.
руководство по маршрутизации и
концепцию плагинов.
Наблюдаемость и аудит
События использования приходят в сервис аналитики пакетами из спула каждого инстанса; доставка выполняется как минимум один раз, а ClickHouse удаляет дубликаты. Сервис строит отчёты по использованию, ошибкам, задержке шлюза, рейтингам, конечным пользователям и кэшу ответов, с экспортом в CSV, и выполняет задачи по расписанию. Консоль обращается к нему только через ядро, которое проверяет роль вызывающего и принудительно задаёт область видимости.
Prometheus, дашборды Grafana, правила Alertmanager и трейсы OpenTelemetry покрывают эксплуатацию; консоль показывает текущие показатели, состояние провайдеров и открытые оповещения.
Режим аудита сохраняет полные вызовы для отладки: запрос, каждую попытку к провайдеру и ответы. По умолчанию он выключен, организация может его потребовать или запретить, учётные данные удаляются до сохранения, а записи по умолчанию истекают через 14 дней. Realtime-сессии и вызовы, связанные с файлами и пакетами, записываются там, где этого просит и рабочее пространство. То, что шлюз передаёт не читая, — сессию WebSocket в режиме proxy, пакет, созданный в режиме proxy, — записать нельзя, и рабочее пространство может отклонить такой вызов, вместо того чтобы обслужить его без записи.
Модель безопасности
- Ключи провайдеров запечатываются по отдельности с AES-256-GCM под ключом шифрования ключей, который берётся из окружения или файла, но никогда из базы данных. API показывает только отпечаток.
- Ключи шлюза (
pgw_…) содержат 32 случайных байта, хранятся как HMAC-SHA256 с pepper и показываются один раз. - Разделение арендаторов. Рабочее пространство определяется ключом, а не запросом; ядро ограничивает отчёты тем, что разрешают роли вызывающего, а ответы из кэша никогда не переходят между рабочими пространствами.
- API управления слушает только в
pacod-admin-net, внутренней сети без выхода наружу, к которой подключаются лишь его серверные клиенты: консоль, сервис аналитики и тестер, а в полном стеке и продукты, которые обращаются к нему или к pacod-id. Контейнер только в общей сети до него не дотягивается. Консоль отправляет Content-Security-Policy с каждой страницей. - Роль в базе данных. Ядро владеет собственной базой данных и не является суперпользователем PostgreSQL; база данных pacod-id для него закрыта.
- Исходящие соединения к провайдерам и источникам цен проверяются после разрешения имён: частные и link-local адреса отклоняются, если оператор явно их не открыл.
- Промпты не попадают в логи, метрики и трейсы, которые содержат только метаданные. Тела хранятся только там, где это кто-то выбрал: в режиме аудита, в сохранённых ответах и в запечатанных ответах кэша.
Модель угроз перечисляет и то, что остаётся открытым: порт метрик не требует аутентификации и несёт метки арендаторов, тот, кто владеет ключом шифрования ключей и может писать в Valkey, мог бы подложить ответы в кэш, а у репозитория нет CI, поэтому проверки зависимостей выполняются, только когда их кто-то запускает.
Развёртывание и эксплуатация
Ядро работает в роли all или разделяется на инстансы gateway и один инстанс admin. Инстансы
делят один PostgreSQL и, для кэша ответов, один Valkey; больше ничего.
| Схема | Файлы |
|---|---|
| Одно ядро | deploy/docker-compose.yml с docker-compose.standalone.yml |
| Несколько шлюзов на одном хосте | плюс docker-compose.scale.yml |
| Шлюзы на других машинах | docker-compose.hub.yml централизованно, deploy/node/ на каждой машине |
| Kubernetes | deploy/helm/pacod-gw, со своими PostgreSQL, ClickHouse и Valkey |
| Минимальный шлюз | deploy/lite/docker-compose.yml |
Как и все проекты pacod, шлюз подключается к общей Docker-сети pacod-net, где у каждого сервиса
своё имя: ядро называется pacod-gw, остальные сервисы — gw-*. Слушатели, к которым обращаются
только серверы, находятся во второй, внутренней сети pacod-admin-net. docker-compose.yml
содержит собственные сервисы шлюза, в том числе ClickHouse и Valkey; docker-compose.standalone.yml
добавляет его копии общих компонентов: PostgreSQL, pacod-id, Caddy и мониторинг.
pacod-demo запускает шлюз вместе с остальными проектами
pacod на demo.pacod.io: он загружает только первый файл и запускает каждый
общий компонент один раз.
Вызовы продолжают идти, когда недоступны PostgreSQL, ClickHouse, сервис аналитики или Valkey: ядро работает на последней конфигурации, копит события в спуле и считает каждый поиск в кэше промахом. Caddy завершает TLS и направляет каждый вызов на готовый шлюз с наименьшим числом вызовов в работе. Ранбук описывает секреты, резервное копирование и восстановление, ротацию ключей, обновления и устранение неполадок.
pacod-gw-lite сохраняет трансляцию, потоковую передачу, повторы и переключение на резервные
модели, с лимитами на ключ в памяти и одной строкой лога на каждый вызов с его стоимостью. Его кэш
ответов по умолчанию выключен и живёт в его собственной памяти (PGW_CACHE_MODE); файлам и пакетам
нужен каталог для их записи (PGW_OBJECTS_DIR). Его клиенты делят ключи провайдеров и не отделены
друг от друга на стороне провайдера. В общей сети минимальный шлюз тоже называется pacod-gw,
поэтому на одном хосте работает либо он, либо полный шлюз
(подробнее).
Что проверено
- OpenAI, Anthropic, Gemini, DeepSeek и GLM проверены живыми вызовами; Kimi, Ollama, xAI и Vertex AI — только по их документации и на mock-провайдере.
- Первое развёртывание по ранбуку пройдено с чистого клона на одной машине (Docker Desktop на macOS, 2026-10-05), включая резервное копирование и восстановление. Схема с тремя шлюзами приняла 40 000 нагрузочных вызовов через балансировщик без единого сбоя.
- Страница Testing в консоли запускает тест-кейсы и нагрузочные тесты против работающей инсталляции и показывает отчёты pacod-benchmark, который измеряет, что добавляет шлюз по сравнению с базовой линией напрямую к mock-провайдеру. Прогон, чьи нагрузочные агенты работали в собственном стеке шлюза, помечается как поведение, а не ёмкость.
- Проектная цель — 10 000 одновременных потоков в режиме proxy и 5 000 в режиме managed на инстанс (2 CPU, 1 GB) — пока не подтверждена нагрузочным тестом.
- Пока не опробованы: сертификаты Let's Encrypt, хост на Linux, шлюзы на физически разных машинах с TLS между ними, обновление между релизами и Helm-чарт в реальном кластере. Минимальный шлюз ещё не запускался на сервере.
- Не входят в первый релиз: управление MCP и инструментами и контентные ограничители.
Страница об архитектуре показывает, как устроен код и где он отличается от концепции дизайна.
Быстрый старт
Полный стек с mock-провайдером
make test-services
make verify
make dev-up
make smoke-mock
open http://localhost:13000
Минимальный шлюз
make lite-dev-up
make lite-smoke
Работает с
- pacod-id: С PGW_AUTH=oidc пользователи входят в консоль через pacod-id; шлюз регистрирует там свои организации и рабочие пространства и следует его ленте ролей.
- pacod-ap: Каждый вызов LLM от агента проходит через шлюз, который управляет моделями, ценами, ключами и учётом использования.
- pacod-cms: AI-перевод в студии обращается к шлюзу, поэтому CMS никогда не хранит ключ провайдера.
- pacod-kb: Необязательный проход очистки через LLM в базе знаний и необязательное второе мнение её проверки содержимого маршрутизируются, тарифицируются и атрибутируются шлюзом.
- pacod-demo: Запускает шлюз рядом с остальными продуктами, с включённым кэшем ответов и его mock-провайдером, пока не задан ключ провайдера.
- pacod-benchmark: Измеряет, что добавляет шлюз, его же нагрузочными агентами и относительно базовой линии напрямую к mock-провайдеру, и публикует свои отчёты в консоли в разделе Platform → Testing → Benchmarks.