Содержание

Архитектура

Два сервиса и одна база. Гейт принимает трафик и переводит форматы; панель владеет конфигурацией. Общаются они через Postgres и больше никак — между ними нет ни HTTP, ни очереди.

   клиенты                    gateway (Go)                  апстримы
┌──────────────┐        ┌────────────────────┐        ┌──────────────────┐
│ OpenAI SDK   │───────▶│  /v1/chat/…        │───────▶│ OpenAI           │
│ Anthropic SDK│        │  /v1/messages      │        │ Anthropic        │
└──────────────┘        │  … 18 типов        │        │ Google / Vertex  │
                        └─────────┬──────────┘        │ Ollama / vLLM    │
                     конфиг в памяти,                 └──────────────────┘
                     LISTEN/NOTIFY │  логи (пакетно, вне горячего пути)
                                   ▼
                        ┌────────────────────┐        ┌──────────────────┐
                        │     PostgreSQL     │◀───────│  admin (Python)  │
                        └────────────────────┘        │  FastAPI + HTMX  │
                                                      └──────────────────┘

Решение первое: единая схема посередине

Каждый формат конвертируется только во внутреннюю схему и обратно, никогда напрямую в чужой.

Это N+M конвертеров вместо N×M. С двумя входящими форматами и четырьмя провайдерами наивный подход дал бы 16 путей трансляции; здесь их 6, и новый провайдер стоит одного конвертера, а не четырёх. Wire-типы каждого формата используются дважды: провайдером на исходящем вызове и входящим API на приёме.

Цена решения — всё, что не выражается в единой схеме, приходится либо добавлять в неё, либо честно отвергать. Отсюда 501 вместо выдуманных ответов в границах версии.

Решение второе: конфиг в памяти гейта

Гейт читает конфигурацию из Postgres напрямую и держит её в памяти. Изменение в панели прилетает по LISTEN/NOTIFY за миллисекунды.

Следствия, на которые стоит рассчитывать:

  • На горячем пути нет ни сети, ни базы. Запрос не ждёт Postgres.
  • Лежащая панель не роняет гейт — трафик идёт на последнем известном конфиге. Обратное тоже верно: лежащий гейт не мешает править настройки.
  • Логи пишутся пакетно, вне горячего пути. Запрос не ждёт записи; при переполнении очереди строки теряются, и счётчик потерь виден в /healthz.
  • Правка в базе руками работает — триггеры разбудят гейт так же, как правка из панели. Но схема принадлежит панели, см. Схема и миграции.

Резервный путь на случай потерянного уведомления — периодическое обновление конфига, по умолчанию раз в минуту (config_refresh_interval).

Кто чем владеет

Панель Гейт
Схема БД владеет, мигрирует только читает
Провайдеры, ключи, цены, лимиты пишет читает
Логи запросов читает пишет
Расход по ключу читает, сбрасывает увеличивает
Секреты провайдеров шифрует расшифровывает

Одно исключение из «панель пишет, гейт читает»: гейт увеличивает spent_usd и last_used_at у ключа пользователя и сбрасывает бюджеты по расписанию. Это именно тот учёт, который должен идти на стороне трафика.

Границы доверия

  • Ключ клиента проверяется по SHA-256, кэшируется в памяти гейта на 30 секунд и никогда не уходит в апстрим.
  • Секреты провайдеров лежат зашифрованными AES-256-GCM под общим ZBGATE_SECRET_KEY; дамп базы без ключа их не выдаёт.
  • Гейт не ходит по URL из запроса — это был бы SSRF, которым управляет клиент. Картинки для редактирования принимаются только инлайном.

Подробности форматов — в Схеме и миграциях.