Как мы собрали LLM-шлюз для России: готовый LiteLLM на data-plane, свой биллинг на Go и n8n
- вторник, 16 июня 2026 г. в 00:00:10

Год назад доступ к зарубежным LLM из России превратился в квест. OpenAI и Anthropic не принимают российские карты и блокируют запросы по гео. Обходные пути — VPN, иностранная карта, прокладка через знакомых за рубежом — годятся для пет-проекта, но не для продакшена, где нужен стабильный аккаунт, предсказуемый счёт и возможность объяснить бухгалтерии, за что платим.
Мы сделали apiglue — шлюз, который закрывает эту боль: один OpenAI-совместимый endpoint, за которым прячутся десятки провайдеров, оплата рублёвой картой и баланс в рублях. А заодно — управляемый хостинг n8n, чтобы не только дёргать модели через API, но и собирать на них автоматизации без своего сервера.
Ниже — как это устроено внутри и какие решения оказались неочевидными. Без маркетинга: где замерили — пишу замеры, где спроектировали на запас — так и говорю.
Первый инстинкт инженера — написать свой прокси. Маршрутизация, пулы ключей, ретраи, учёт токенов, стриминг — задача понятная. Мы даже начали: в репозитории до сих пор лежит gateway/ на Go с тестами под -race. Но довольно быстро стало ясно, что мы переписываем то, что уже есть в опенсорсе и обкатано на чужом проде.
Этим «уже есть» оказался LiteLLM — прокси на Python с поддержкой сотни с лишним провайдеров, виртуальными ключами, бюджетами, учётом расходов и метриками для Prometheus. Он умеет то, на что у нас ушли бы месяцы: единый формат запроса для OpenAI, Anthropic, Google, DeepSeek, OpenRouter и прочих, балансировку внутри пула ключей, cooldown при rate-limit и автоматический failover.
Отсюда выросла вся архитектура: LiteLLM остаётся как есть на data-plane, а control-plane на Go мы пишем сверху. Собственный gateway оставили как референс и запасной вариант, но горячий путь запросов через него не идёт.
Получилось два чётко разделённых слоя.
Гибридная архитектура: data-plane (LiteLLM) и control-plane (Go)

Data-plane (LiteLLM) обрабатывает весь трафик /v1/*: chat-completions, embeddings, стриминг. Внутри — пул провайдерских ключей, балансировка, failover, учёт расхода в долларах. Control-plane в этот путь не вмешивается, поэтому латентность шлюза не зависит от того, чем заняты кабинет и биллинг.
Control-plane (Go, в основном stdlib) — это всё остальное: регистрация и сессии, ключи пользователя, рублёвый баланс, пополнение через ЮKassa, кабинет, оркестрация n8n. LiteLLM он дёргает через admin-API: заводит виртуальные ключи (/key/generate), добавляет модели (/model/new), забирает расход (/spend/logs).
Почему это удобно: слои масштабируются независимо. LiteLLM-реплики делят состояние через Redis, control-plane stateless. И если завтра захочется заменить LiteLLM на что-то другое — контракт между слоями узкий и понятный.
Вторая половина продукта — управляемый хостинг n8n. Пользователь в кабинете нажимает «создать сервер», выбирает тариф (basic — от 1900 ₽/мес) и через несколько минут получает свой изолированный n8n на поддомене вида n8n-true-carp.nc.домен. Дальше — рекуррентное списание с того же рублёвого баланса.
Под капотом — Kubernetes. Каждый инстанс — это Deployment + PVC + Service + Ingress в собственном namespace, а раскладывает всё это наш Go-оркестратор через client-go. На k8s мы пошли осознанно: планировщик, рестарты упавших Pod'ов, масштабирование и сетевые политики кластер берёт на себя — нам не нужно городить это самим поверх голого демона.
Reconcile-петля вместо императивных вызовов. Оркестратор не делает apply прямо из обработчиков API. Control-plane пишет в Postgres желаемое состояние инстанса, а orchestrator в цикле сверяет его с фактическим состоянием кластера (через client-go, Server-Side Apply) и приводит одно к другому — создаёт манифесты, масштабирует Deployment в ноль и обратно, удаляет. По сути это маленький контроллер поверх собственного стейта в БД. Отсюда восстановляемость (оркестратор упал и поднялся — досверит из Postgres) и устойчивость к гонкам (несколько реплик под leader-lock не подерутся за один инстанс), а сам Kubernetes ещё и держит Pod'ы живыми, пока мы спим.
Изоляция. Каждому инстансу — свой namespace, NetworkPolicy с запретом на чужой трафик и свой PersistentVolumeClaim. Инстансы не видят друг друга по сети и не делят данные. Секрет N8N_ENCRYPTION_KEY шифруется AES-256-GCM и лежит в нашей БД только в виде шифротекста; в кластер он кладётся как Secret и расшифровывается лишь в момент применения манифеста, чтобы попасть в env Pod'а.
Шлюз доступа до онбординга. Свежий n8n до первого /setup открыт — кто первый зашёл, тот и завёл админа. Чтобы посторонний не «угнал» чужой инстанс по угаданному URL, перед инстансами стоит access-proxy с ext-authz (Ingress дергает его до того, как пустить запрос на Pod): до завершения setup он пускает только владельца. Владелец опознаётся по HMAC-токену в cookie, который проверяется субзапросом в control-plane. Встроить проверку в сам образ n8n нельзя — он неизменяемый, поэтому логика живёт в прокси.
Ближайшее — встроенная observability LLM-трафика на базе Langfuse: каждый пользователь видит свои запросы, токены, стоимость и латентность без единой строчки кода у себя, потому что трафик и так идёт через виртуальный ключ. Спека готова, реализация на очереди.
Вопросы по архитектуре и решениям — задавайте в комментариях, отвечаем.