Как я подключил 350+ AI-моделей через один API и перестал переписывать интеграции
Привет! Меня зовут Никита, я backend-разработчик, пишу на Go и PHP. Делаю свой продукт — AI-gateway, через который прошло 2M+ запросов к LLM за последние месяцы. В этой статье расскажу, зачем я его написал, покажу архитектуру и код, и честно объясню, где набил шишки.
Проблема, которую решаю: вы интегрировали OpenAI, потом понадобился Claude, потом заказчик хочет YandexGPT «потому что российское», а DeepSeek дешевле всех — и вот у вас четыре SDK, четыре формата ответов, четыре системы биллинга и ни одного нормального fallback'а.
Зоопарк LLM в 2026
Если вы уже в теме — смело пролистайте до «Проблема: каждый провайдер — свой мир»
Ещё в 2023 мир LLM был простым: GPT-4 для сложного, GPT-3.5 для дешёвого, done. Сегодня:
| Провайдер | Модели | Сильная сторона |
|---|---|---|
| OpenAI | GPT-5, GPT-4o, o3/o4-mini | Универсальность, экосистема |
| Anthropic | Claude Opus 4.6, Sonnet 4.6, Haiku 4.5 | Тексты, код, контекст 1M токенов |
| Gemini 3 Pro, Flash 2.5 | Мультимодальность, скорость | |
| Яндекс | YandexGPT Lite/Pro/Pro 32K | Русский язык, рос. юрисдикция |
| Сбер | GigaChat/GigaChat Pro | Русский язык, compliance |
| DeepSeek | V3.2, R1 | Reasoning, низкая цена |
| Alibaba | Qwen 3.6, Qwen3 235B | Бесплатные модели |
| xAI | Grok 4 | Без цензуры, свежие данные |
| Meta | Llama 4 | Open-source, self-hosted |
Таблица актуальна на апрель 2026
Плюс open-source модели на HuggingFace (500K+ моделей) и локальный запуск через Ollama (тема для отдельной статьи).
Три тренда, важных для архитектуры:
- Мультимодальность. GPT-5, Gemini, Claude умеют vision и image generation. API должен прозрачно передавать
image_urlв messages. - Reasoning. OpenAI o3, DeepSeek R1 — модели с цепочкой рассуждений. В 3-5 раз медленнее, но точнее для математики. Gateway должен знать их особенности.
- Параметры LLM. Кроме
temperatureестьtop_p,frequency_penalty,stop,response_format. Не все провайдеры поддерживают все.
Почему OpenAI API стал стандартом
Формат оказался настолько удачным, что его скопировали почти все: messages (массив {role, content}), model (строка), stream (boolean). Это стало lingua franca мира LLM. Любой существующий код работает без изменений — меняется только base_url.
Проблема: каждый провайдер — свой мир
| Провайдер | Auth | Формат messages | Streaming |
|---|---|---|---|
| OpenAI | Bearer token | {role, content} | SSE |
| Anthropic | x-api-key | system отдельно | SSE (другой формат) |
| YandexGPT | IAM + folder-id | Свой формат | gRPC stream |
| GigaChat | OAuth2 (TTL 30 мин) | Почти OpenAI | SSE (свой) |
| Gemini | API key | parts, contents | SSE (ещё один) |
Пять провайдеров — пять интеграций, пять обработчиков ошибок, пять форматов стриминга.
Решение: OpenAI-совместимый gateway на Go
Назвал его НейроГейт. Идея простая: один API, один формат, один ключ.
POST /v1/chat/ translate & (OpenAI, Claude,
completions route YandexGPT, GigaChat)
С точки зрения вашего кода — вы работаете с OpenAI SDK:
import openai
client = openai.OpenAI(
api_key="ng-proj-ваш-ключ",
base_url="https://api.neuralgate.ru/v1"
)
# Работает одинаково для любой модели
response = client.chat.completions.create(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": "Привет!"}],
stream=True
)
for chunk in response:
print(chunk.choices[0].delta.content, end="")
Go-версия — аналогично:
cfg := openai.DefaultConfig("ng-proj-ваш-ключ")
cfg.BaseURL = "https://api.neuralgate.ru/v1"
client := openai.NewClientWithConfig(cfg)
stream, err := client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
Model: "yandexgpt-pro",
Messages: []openai.ChatCompletionMessage{{Role: "user", Content: "Привет!"}},
})
Меняется одна строка — BaseURL. Всё остальное — ваш существующий код.
Стек
- Go — gateway на
:443, прямой TLS без Nginx, HTTP/2 native - PostgreSQL — организации, API-ключи, usage logs, модели, биллинг
- Redis (Sentinel) — rate limiting (sliding window), кэш токенов
- Prometheus + Grafana — метрики, алерты
Почему без Nginx? Nginx добавляет latency на каждом SSE-чанке. Для 500 параллельных стримов по 10-60 секунд — ощутимо. Go'шный crypto/tls справляется.
Архитектура роутинга
Auth
Ключ формата ng-proj-{32 hex}. Зарегистрирован как GitHub secret scanning prefix — если ваш ключ утечёт в публичный репо, GitHub автоматически уведомит нас.
Rate Limiting
Sliding window на Redis через ZRANGEBYSCORE + ZADD. RPM и TPM per key. Redis down — in-memory fallback:
func (rl *RateLimiter) Allow(ctx context.Context, key string, limit int, window time.Duration) bool {
allowed, err := rl.redisAllow(ctx, key, limit, window)
if err != nil {
// Redis down — лучше пропустить лишний запрос, чем заблокировать всех
rl.metrics.RedisFailover.Inc()
return rl.memoryAllow(key, limit, window)
}
return allowed
}
Translate
Каждый провайдер имеет свой Translator:
type Translator interface {
TranslateRequest(ctx context.Context, req *ChatCompletionRequest) (*http.Request, error)
TranslateResponse(resp *http.Response) (*ChatCompletionResponse, error)
TranslateStream(ctx context.Context, resp *http.Response) (<-chan ChatCompletionChunk, <-chan error)
}
YandexGPT: маппит model: "yandexgpt-pro" → modelUri: "gpt://folder-id/yandexgpt/latest", добавляет IAM-токен. GigaChat: OAuth2 с auto-refresh (TTL 30 мин).
SSE Streaming: где живут баги
SSE — HTTP-ответ с Content-Type: text/event-stream, который шлёт данные порциями:
data: {"choices":[{"delta":{"content":"Привет"}}]}
data: {"choices":[{"delta":{"content":"! Как"}}]}
data: [DONE]
Каждый стрим — горутина с обработкой отмены и ошибок:
func (g *Gateway) streamResponse(ctx context.Context, w http.ResponseWriter,
providerStream <-chan Chunk, errCh <-chan error, translator Translator) {
flusher := w.(http.Flusher)
for {
select {
case <-ctx.Done():
// Клиент дисконнектился
return
case err := <-errCh:
// Провайдер упал посреди стрима
fmt.Fprintf(w, "data: {\"error\":\"%s\"}\n\n", err.Error())
flusher.Flush()
return
case chunk, ok := <-providerStream:
if !ok {
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
return
}
translated := translator.TranslateChunk(chunk)
fmt.Fprintf(w, "data: %s\n\n", translated.JSON())
flusher.Flush()
}
}
}
Flush() после каждого чанка — критично. ctx.Done() — критично. Без них горутина живёт вечно при дисконнекте.
Грабли, на которые мы наступили
Каждая из этих проблем стоила часов дебага в продакшне. Делюсь, чтобы вы не наступали.
Грабля #1: YandexGPT шлёт невалидный JSON в середине стрима
Один из 500-600 стримов содержит обрезанный чанк: {"choices":[{"delta":{"conte и всё. Не HTTP-ошибка — просто битый JSON. Пики коррелируют с нагрузкой на стороне Яндекса.
Решение: трёхуровневый парсер: json.Decoder → ручное извлечение content → drop chunk + метрика stream_parse_errors в Grafana.
Урок: никогда не доверяй формату ответа LLM-провайдера на 100%. Defensive parsing обязательно.
Грабля #2: GigaChat OAuth token протухает посреди стрима
Access token TTL = 30 минут. Запрос на 29-й минуте, стрим 3 минуты — токен протух посередине. 80% ответа отправлено, повторить нельзя.
Решение: proactive refresh — фоновая горутина обновляет токен за 2 минуты до expiry.
Урок: OAuth2 с коротким TTL + long-running SSE = гарантированный баг.
Грабля #3: мультимодальный контент ломает парсер
Gemini и GPT-5 возвращают delta.content как массив вместо строки при генерации изображений. Translator дропал весь чанк. Изображения исчезали без ошибок в логах.
Решение: type switch — строка → старый путь, массив → итерируем по элементам.
Урок: мультимодальные модели нарушают «content = строка». Ловушка для любого gateway.
Грабля #4: race condition на балансе
Два параллельных запроса: оба читают 100₽, оба списывают 60₽. Итого: -120₽, баланс в минусе.
Решение: атомарный UPDATE ... WHERE balance >= $cost RETURNING balance. Плюс Redis mutex для длинных стримов.
Урок: read-then-write на балансе — классический race. Всегда атомарный UPDATE с WHERE.
Грабля #5: деньги зависли в резерве при disconnect
Reserve → Stream → Commit. Клиент закрыл вкладку на 40%. Провайдер потратил токены, Commit не вызван.
Решение: defer Release как safety net + sweep зависших резерваций старше 5 минут при старте.
Урок: двухфазные операции требуют recovery при всех вариантах отказа.
Грабля #6: revocation убивает активные стримы
При refresh JWT сразу отзывали старый токен. Но SSE-стрим на старом токене → 401 → обрыв.
Решение: не отзывать при refresh. Пусть старый живёт до expiry (7d TTL).
Урок: token revocation при refresh — антипаттерн для long-lived connections.
Грабля #7: LLM генерирует внутренние теги
System prompt: <!--buttons:[...]-->. Модель «заражается» и вставляет теги в ответ пользователю.
Решение: тройная защита — промпт + backend strip + frontend strip.
Урок: всё из system prompt LLM может скопировать в ответ. Sanitize output всегда.
Грабля #8: BLPOP блокирует shared Redis
BLPOP с таймаутом 120с блокирует соединение. Все остальные операции встают в очередь.
Решение: выделенное соединение per BLPOP.
Урок: blocking Redis commands — никогда на shared connection.
Грабля #9: Redis мьютекс зависает навечно
SETNX + EX 20. Crash — лок должен expire. Но иногда TTL = -1 (no expiry) при persistence restore.
Решение: проверять TTL при acquire, force delete если -1. Периодический sweep.
Урок: Redis TTL может стать -1. Не доверяйте EX — проверяйте TTL.
Грабля #10: TLS fingerprint выдаёт не-браузера
Go WebSocket к серверу — те же заголовки, но JA3 fingerprint ≠ браузерный. Сервер блокирует.
Решение: websocat (Rust CLI) как subprocess — browser-like TLS fingerprint.
Урок: TLS fingerprinting — реальность 2026. Нужны uTLS (Go) или websocat (Rust).
Circuit Breaker
Нюанс для стриминга: провайдер ответил 200 OK, начал стрим, умер на 40-м чанке. Считаем ошибкой для circuit breaker, только если получено менее 10% ожидаемых токенов.
Биллинг: pre-debit / post-adjust
// Фаза 1: резервируем по верхней границе
reservation, err := billing.Reserve(orgID, estimatedCost)
if err != nil {
return ErrInsufficientBalance
}
defer billing.Release(reservation) // safety net
// Фаза 2: после ответа — списываем по факту
actualCost := calcCost(usage.PromptTokens, usage.CompletionTokens, route)
billing.Commit(reservation, actualCost)
Атомарный UPDATE balance SET amount = amount - $cost WHERE amount >= $cost. defer Release — safety net при crash. Sweep зависших резерваций при старте.
Latency: куда уходят миллисекунды
| Этап | p50 | p99 |
|---|---|---|
| TLS termination | 0мс (keep-alive) | 15мс (новое) |
| Auth (Redis) | 2мс | 8мс |
| Rate limit | 3мс | 12мс |
| Route (memory) | <1мс | <1мс |
| Translation | 2мс | 5мс |
| Итого overhead | ~8мс | ~40мс |
| TLS к провайдеру | 30мс | 150мс |
| Итого до first byte | ~40мс | ~200мс |
Зачем, если есть OpenRouter?
- Нет YandexGPT и GigaChat. Compliance-требование для РФ, не каприз.
- Биллинг в рублях. Счёт от юрлица, акт, договор-оферта. Без проблем с 115-ФЗ.
- Данные в РФ. Gateway в Яндекс Cloud. Промпты не через зарубежный роутер.
- BYOK. Свой ключ OpenAI/Anthropic/Яндекс. Или Ollama как custom provider.
Есть ещё LiteLLM — open-source Python-прокси. Для self-hosted — достойный выбор. НейроГейт — managed + рубли + российские модели.
Compliance и данные
| Где сервер? | Яндекс Cloud, РФ |
| Логируются промпты? | Opt-in. По умолчанию — только метаданные |
| Только российские модели? | Да, ограничение в настройках org |
| 152-ФЗ | Данные в РФ, шифрование at rest |
| Юрлицо | ИП, договор-оферта, акты |
| Оплата | YooKassa, SBP, счёт для юрлиц |
| ФСТЭК | Пока нет. Работаем над этим |
Цифры
- 350+ модельных endpoint'ов от 15+ провайдеров (~50 уникальных архитектур)
- 45-65мс overhead gateway (p50/p99)
- 500 параллельных стримов per org
Попробовать
Бесплатный tier: Qwen, StepFun, Nemotron — 5 RPM. 100₽ welcome-бонус.
neuralgate.ru →Telegram: @neuralgate_dev
Дальше в серии: бенчмарк YandexGPT vs Claude vs GPT-5 vs DeepSeek — с ценами в рублях. И отдельная статья про Ollama: локальный запуск LLM, квантизация, выбор GPU.