Два окна панели консоли pacod-gw, светлое и тёмное, наложенные друг на друга, с показателями запросов, ошибок, стоимости, токенов и задержки и графиком запросов по часам.
Панель консоли днём и ночью: запросы, ошибки, стоимость, токены и собственная задержка шлюза для области, выбранной вверху.

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 и строкой лога со стоимостью для каждого вызова.

Диаграмма платформы с выделенным pacod-gw и приглушёнными остальными проектами, с его связями для вызовов LLM, входа и бенчмарков.
Место pacod-gw. Каждый вызов LLM из pacod-ap, pacod-cms и pacod-kb проходит через него (зелёный), вход — через pacod-id (золотой), а pacod-benchmark его измеряет (коралловый).

Скриншоты

Журнал запросов pacod-gw с фильтрами сверху и строками вызовов, у каждого время, результат, модель, рабочее пространство, токены, стоимость и длительность.
Журнал запросов. Каждый вызов с результатом, обслужившей его моделью, токенами и стоимостью, а также долей времени, пришедшейся на сам шлюз.
Таблица моделей в консоли pacod-gw от Anthropic, DeepSeek, Gemini, GLM и OpenAI, все активны и с ценой.
Реестр моделей. Провайдер, тип, состояние и цена каждой модели; модель обслуживает вызовы только с ценой или явным «бесплатно».
Страница цен в консоли pacod-gw, таблица моделей Anthropic, DeepSeek, Gemini, GLM и OpenAI с тарифами на ввод, вывод и кэш в USD, версия 1, введены вручную и ещё не сверены с источником.
Действующие цены. Тарифы каждой модели за миллион токенов, чтение и запись кэша, где они есть, версия цены и её источник; новая цена становится новой версией, а прежние остаются у модели.
Экран кэша ответов pacod-gw с хранилищем, сохранёнными ответами, занятой памятью и экземплярами, которые его делят.
Кэш ответов. Что хранит каждый шлюз, его привязка на уровне платформы и хранилище за ним.
Тёмный экран бенчмарков консоли pacod-gw со списком опубликованного отчёта и формой загрузки другого.
Отчёты pacod-benchmark, опубликованные в консоли в разделе Platform → Testing → Benchmarks.

Зачем нужен шлюз

Каждый продукт, работающий с языковой моделью, в итоге заново реализует одно и то же: хранение ключей провайдеров, трансляцию между 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) хранится так же.

Один вызов, шаг за шагом

  1. Аутентификация. Ключ ищется по HMAC в снимке конфигурации в памяти.
  2. Чтение. Сканер находит модель и флаги потоковой передачи, не разбирая всё тело. Вызов, продолжающий сохранённый ответ, получает свои предыдущие ходы; плагины рабочего пространства видят диалог.
  3. Разрешение. Шаблон (@name или x-gw-template) или идентификатор разрешённой модели определяет модель и её параметры.
  4. Поиск в кэше. Если кэш ответов включён, в режиме managed сохранённый ответ на тот же запрос возвращается здесь, в диалекте вызывающей стороны, прежде чем вызов займёт слот или будет учтён в лимите.
  5. Допуск. Ограничение одновременных вызовов ключа и каждый применимый лимит должны быть соблюдены.
  6. Отправка. Сохранённые учётные данные провайдера заменяют ключ шлюза. В режиме managed ответ 429, 5xx или потерянное соединение повторяются или передаются следующей модели, прежде чем вызывающая сторона получит хоть один байт.
  7. Передача и расчёт. Ответ возвращается потоком, а отвод читает использование из копии; затем обновляются стоимость и счётчики, ответ сохраняется, если его берёт кэш, а событие использования уходит в спул на диске.

Ответ содержит 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.