Архитектура
Два сервиса и одна база. Гейт принимает трафик и переводит форматы; панель владеет конфигурацией. Общаются они через 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, которым управляет клиент. Картинки для редактирования принимаются только инлайном.
Подробности форматов — в Схеме и миграциях.