Единый шлюз к OpenAI, Claude и Gemini на своём сервере
Когда в проекте одновременно живут три провайдера, начинается зоопарк: у OpenAI один формат запроса, у Anthropic другой, у Google третий, ключи расползлись по десятку .env-файлов, а расходы видны только в трёх разных кабинетах. Шлюз к нейросетям на сервере убирает этот зоопарк: одно приложение принимает запросы в формате OpenAI, раскладывает их по провайдерам, считает деньги и уводит трафик на резерв, когда основной провайдер отвалился. Ниже — рабочий конфиг, замеры накладных расходов и честный список того, что такая схема ломает.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Что решает шлюз к нейросетям на сервере
Боль не в том, что моделей много, а в том, что у каждой свой протокол:
| Провайдер | Эндпоинт | Где ключ | Особенность тела |
|---|---|---|---|
| OpenAI | POST /v1/chat/completions | Authorization: Bearer sk-... | системный промпт — сообщение с role: system |
| Anthropic | POST /v1/messages | x-api-key + anthropic-version: 2023-06-01 | system вынесен в отдельное поле, max_tokens обязателен |
| Google Gemini | POST /v1beta/models/{model}:generateContent | ключ в query-строке ?key= | не messages, а contents[].parts[].text |
Три SDK, три схемы ошибок, три способа считать токены. Стоит захотеть «попробуем тут Gemini» — и в коде появляется третья ветка if provider ==.
LiteLLM Proxy решает это одним ходом: поднимает у вас OpenAI-совместимый HTTP-эндпоинт на порту 4000 и сам переводит запрос в нужный диалект. Приложение всегда шлёт POST /v1/chat/completions с полем model, а какой провайдер стоит за этим именем — вопрос конфига, а не кода.
Сразу разведём два разных «шлюза». Сетевой (WireGuard, 3proxy) работает на уровне маршрутизации и просто выпускает трафик через зарубежный IP. LiteLLM работает на уровне приложения: разбирает тело запроса, знает про токены и цены, умеет ретраи и лимиты. На практике его ставят на зарубежный сервер и получают обе функции одним объектом.
Один config.yaml на трёх провайдеров
Разворачивать удобнее в Docker — образ уже содержит миграции базы. Создайте /opt/litellm и три файла.
.env:
LITELLM_MASTER_KEY=sk-мастер-ключ-длинный-и-случайный
LITELLM_SALT_KEY=солёная-строка-которую-нельзя-менять
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
GEMINI_API_KEY=AIza...
DATABASE_URL=postgresql://litellm:пароль@db:5432/litellm
Мастер-ключ обязан начинаться с sk-, иначе прокси не стартует. LITELLM_SALT_KEY шифрует учётки провайдеров в базе: поменяете его после первого запуска — все сохранённые ключи превратятся в мусор, и восстановить их нечем. Запишите обе строки в менеджер паролей до старта.
config.yaml:
model_list:
- model_name: gpt-main
litellm_params:
model: openai/gpt-5
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-main
litellm_params:
model: anthropic/claude-sonnet-4-5
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: gemini-long
litellm_params:
model: gemini/gemini-2.5-pro
api_key: os.environ/GEMINI_API_KEY
litellm_settings:
drop_params: true
num_retries: 2
request_timeout: 120
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
store_model_in_db: true
proxy_batch_write_at: 60
Ключевая деталь — префикс провайдера в поле model. Без него шлюз падает с сообщением, которое видел каждый, кто ставил LiteLLM впервые:
litellm.BadRequestError: LLM Provider NOT provided. Pass in the LLM provider
you are trying to call. You passed model=claude-sonnet-4-5
Идентификаторы моделей меняются каждые несколько месяцев — подставляйте актуальные из документации провайдера, а model_name слева держите своим стабильным алиасом. Тогда смена поколения модели — одна строка в конфиге, а не правка во всех приложениях.
docker-compose.yml:
services:
litellm:
image: ghcr.io/berriai/litellm-database:main-stable
command: ["--config","/app/config.yaml","--port","4000","--num_workers","2"]
volumes: ["./config.yaml:/app/config.yaml:ro"]
env_file: .env
ports: ["127.0.0.1:4000:4000"]
depends_on: [db, redis]
restart: unless-stopped
db:
image: postgres:16-alpine
environment: {POSTGRES_USER: litellm, POSTGRES_PASSWORD: пароль, POSTGRES_DB: litellm}
volumes: ["./pgdata:/var/lib/postgresql/data"]
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
Обратите внимание на 127.0.0.1:4000:4000. Если написать просто "4000:4000", Docker опубликует порт на всех интерфейсах и пройдёт мимо ufw: демон пишет свои правила в цепочку DOCKER таблицы nat, которая обрабатывается раньше пользовательских. Проверяется за минуту: при публикации на 0.0.0.0 команда ufw deny 4000 порт не закроет, и мастер-ключ будет ждать первого сканера. Наружу пускайте только два порта: ufw allow 22/tcp && ufw allow 443/tcp && ufw enable.
Поднимаем и проверяем: docker compose up -d, затем curl -s localhost:4000/health/readiness. Ответ здорового шлюза:
{"status":"connected","db":"connected","cache":null,"litellm_version":"1.7x.x"}
"db":"connected" означает, что миграции прошли и учёт расходов заработает. Список моделей отдаёт /v1/models, панель живёт на /ui, вход по мастер-ключу. Наружу выставляйте не порт 4000, а Nginx с сертификатом и обязательно с proxy_buffering off; — иначе стриминг склеится в один кусок в конце, и пользователь будет смотреть на пустой экран все 20 секунд генерации.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть LiteLLMФолбэки: OpenAI отдал 429 — ответит Claude
Ради этого единый шлюз чаще всего и ставят. Добавьте в config.yaml секцию маршрутизации:
router_settings:
routing_strategy: usage-based-routing-v2
redis_host: redis
redis_port: 6379
num_retries: 2
timeout: 120
allowed_fails: 3
cooldown_time: 60
fallbacks:
- gpt-main: ["claude-main"]
- claude-main: ["gpt-main"]
context_window_fallbacks:
- gpt-main: ["gemini-long"]
Что происходит по шагам. Запрос уходит на gpt-main, прилетает 429 — шлюз делает два повтора с нарастающей паузой. Не помогло — срабатывает fallbacks, и тот же запрос уходит в claude-main без единой строчки в коде клиента. После трёх подряд неудач (allowed_fails: 3) деплой уходит в остывание на 60 секунд и на это время исключается из маршрутизации: трафик не долбится в заведомо мёртвого провайдера.
Отдельно стоит context_window_fallbacks — он ловит именно ContextWindowExceededError и перекидывает слишком длинный промпт на модель с большим окном. Логика правильная: не «упало, попробуем другого», а «не влезло, возьмём того, у кого окно шире».
Что фолбэк действительно сработал, видно по служебным заголовкам ответа:
curl -i -s localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-ваш-ключ" -H "Content-Type: application/json" \
-d '{"model":"gpt-main","messages":[{"role":"user","content":"ping"}]}' \
| grep -i '^x-litellm'
В выводе будут x-litellm-call-id, x-litellm-model-id и счётчики попыток. Если model-id не тот, что вы просили, — фолбэк отработал, и это повод заглянуть в логи основного провайдера.
Нюанс, на котором спотыкаются: стратегия usage-based-routing-v2 считает TPM/RPM в Redis. Без Redis и при --num_workers 2 каждый воркер ведёт счётчик в своей памяти, и реальный лимит выходит вдвое выше заявленного. Либо держите Redis, либо оставляйте один воркер.
Виртуальные ключи, лимиты и учёт денег
Второй смысл шлюза — никому не раздавать боевые ключи провайдеров. Вместо них выпускаются виртуальные, со своим бюджетом:
curl -s -X POST localhost:4000/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"models":["gpt-main","claude-main"],"max_budget":25,
"budget_duration":"30d","rpm_limit":60,
"metadata":{"owner":"analytics-bot"}}'
В ответ придёт {"key":"sk-...","expires":null}. Этот ключ видит только два разрешённых алиаса, не больше 60 запросов в минуту и не больше 25 долларов за 30 дней — дальше шлюз вернёт Budget has been exceeded. Ключ утёк — удаляете его через /key/delete, боевые ключи OpenAI и Anthropic при этом не трогали вообще.
Расходы пишутся в таблицу LiteLLM_SpendLogs, отдаются через /spend/logs и рисуются в /ui. Здесь же эксплуатационная грабля: каждая запись весит порядка 1–2 КБ, и на полумиллионе запросов база распухает примерно до гигабайта. На диске 40 ГБ терпимо, на 20 ГБ — уже повод чистить по расписанию:
DELETE FROM "LiteLLM_SpendLogs" WHERE "startTime" < NOW() - INTERVAL '30 days';
В свежих версиях есть и параметр автоочистки, но задание в cron надёжнее: оно не зависит от того, переименуют ли настройку в следующем релизе. Ещё про деньги: цены шлюз берёт из встроенного справочника model_prices_and_context_window.json, который обновляется вместе с образом. Закрепили тег полугодовой давности — отчёт считает по старым тарифам. Если цифры важны, обновляйте образ или задавайте input_cost_per_token и output_cost_per_token прямо в litellm_params. Про сами лимиты провайдеров есть отдельный разбор — ошибка 429 rate limit в LiteLLM.
Как перевести клиентов на свой эндпоинт
Раз эндпоинт OpenAI-совместимый, в приложениях меняются базовый URL и ключ — больше ничего.
- Python SDK:
OpenAI(base_url="https://gw.вашдомен.ru/v1", api_key="sk-виртуальный"). - Переменные окружения:
OPENAI_BASE_URLиOPENAI_API_KEY— этого хватает большинству CLI-инструментов и библиотек. - Open WebUI: тот же адрес как OpenAI API — и в списке моделей появляются разом
gpt-main,claude-main,gemini-long. - n8n: поле Base URL в OpenAI-креденшелах, дальше все ноды работают как раньше.
- LangChain, LlamaIndex: параметр
openai_api_base, никаких провайдер-специфичных классов. - Редакторы кода (Continue, Cline и аналоги): провайдер
openaiи вашapiBase.
Отдельно: LiteLLM умеет отдавать и нативный анропиковский /v1/messages, поэтому инструменты, которые ходят строго в формате Anthropic, тоже заворачиваются на шлюз переменной базового URL. Работает, но это менее хоженая тропа, чем OpenAI-совместимый путь, — проверьте на своей версии, прежде чем переводить туда рабочий процесс. Помимо чата проксируются /v1/embeddings (удобно, когда векторизация у одного провайдера, а генерация у другого) и транскрибация аудио; точный набор маршрутов зависит от версии.
Что шлюз ломает: честный список ограничений
Единый интерфейс — это всегда наименьший общий знаменатель, и за него платят.
Провайдер-специфичные параметры отваливаются. Anthropic не принимает frequency_penalty, presence_penalty и logit_bias. Без drop_params: true вы получите:
litellm.UnsupportedParamsError: anthropic does not support parameters:
['frequency_penalty'], for model=claude-sonnet-4-5. To drop these,
set litellm.drop_params=True
С drop_params: true запрос пройдёт, но параметр молча выбросят. Если вы им рулили разнообразием ответа, поведение изменится незаметно — «работает» и «работает как задумано» тут разные вещи.
Тонкие фичи живут хуже. Кеширование промптов у Anthropic, расширенные режимы рассуждения, загрузка PDF, structured output — всё это в OpenAI-формате выражается по-разному или не выражается вовсе. Строите продукт вокруг одной конкретной фичи одного провайдера — прямой SDK честнее.
Появляется единая точка отказа. Раньше падал один провайдер из трёх, теперь может упасть шлюз и забрать все три. Лечится буднично: restart: unless-stopped, внешняя проверка /health/liveliness (отвечает строкой I'm alive!) и алерт в Telegram.
Задержка растёт. На стенде 2 vCPU / 4 ГБ прогон hey -n 300 -c 10 через шлюз против прямого вызова дал примерно +9 мс к медиане и +24 мс к 95-му перцентилю — на фоне полутора-четырёх секунд генерации это шум. Но если клиент в Москве, а шлюз в Нью-Йорке, к каждому запросу добавляется ещё 110–130 мс сетевого RTT. Заметно только на времени до первого токена, дальше стриминг идёт ровно.
Часть возможностей платная. SSO, корпоративные политики и расширенная телеметрия относятся к Enterprise-редакции. Роутинг, ключи, бюджеты и учёт расходов доступны в открытой версии, но не рассчитывайте, что всё из документации включено бесплатно.
Релизы быстрые. Проект обновляется по нескольку раз в неделю. Не тяните main-latest на боевом сервере: закрепите конкретный стабильный тег и обновляйтесь осознанно, читая changelog. Сравнение с облачным вариантом — в статье LiteLLM против OpenRouter.
Какой сервер взять под шлюз в MAATRIX
Локация здесь не вопрос вкуса. Из России прямой запрос к api.openai.com штатно возвращает 403 с телом вида:
{"error":{"message":"Country, region, or territory not supported",
"type":"request_forbidden","code":"unsupported_country_region_territory"}}
Это отказ самого провайдера по географии IP, он не лечится сменой DNS. Поэтому шлюз ставят на VPS в США (Нью-Йорк): чистый американский адрес, до точек присутствия OpenAI, Anthropic и Google — единицы миллисекунд, все три API доступны с одного узла. Гибридная схема работает хорошо: приложение и база остаются на российском узле (152-ФЗ, минимальный пинг до пользователей), а за моделями оно ходит через американский шлюз. Если основная нагрузка из Европы и важно соседство с GDPR, вместо США логичнее узел в Лондоне или во Франции.
По ресурсам. Прокси не считает нейросети, он гоняет HTTP: процессор почти не при делах, память уходит на воркеры и Postgres. По docker stats контейнер LiteLLM в простое занимает около 420 МБ, при --num_workers 4 вместе с базой и Redis выходит 1,3–1,6 ГБ.
| Вариант | Конфигурация | Для чего годится |
|---|---|---|
| Минимум | 1 vCPU, 2 ГБ RAM, 20 ГБ NVMe | 1 воркер, без Redis, личные задачи и пара ботов, до ~10 запросов/мин |
| Комфорт | 2 vCPU, 4 ГБ RAM, 40 ГБ NVMe | 2–4 воркера, Redis и Postgres, команда до 15 человек, запас по логам |
| С нагрузкой | 4 vCPU, 8 ГБ RAM, 80 ГБ NVMe | продакшен, кеш ответов, длинная история расходов, сотни запросов/мин |
Честно про минимум: на 1 ГБ RAM связка запускается, но первый же всплеск трафика приводит к тому, что OOM-killer гасит контейнер базы, а /health/readiness начинает отдавать "db":"disconnected". Два гигабайта — реальный нижний порог, четыре — точка, после которой перестаёшь думать о памяти. Подробный разбор потребления — в статье сколько RAM нужно для LiteLLM Gateway.
Оплатить сервер можно картами российских банков, через СБП, криптовалютой или токеном MAAT — зарубежная карта для американской площадки не нужна. Сомневаетесь между минимумом и комфортом — отталкивайтесь от числа запросов в минуту и от того, нужен ли Redis: с ним берите сразу 4 ГБ. Смежные варианты размещения разобраны в материале про VPS в США для доступа к нейросетям и AI.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть LiteLLMОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Заменит ли шлюз VPN для доступа к ChatGPT в браузере?
Нет. LiteLLM работает только с API: принимает HTTP-запросы приложений и переводит их провайдерам. Веб-интерфейс ChatGPT через него не откроется — для браузера нужен сетевой шлюз на WireGuard.
Что будет, если упадёт Postgres?
Прокси продолжит отвечать, но перестанет проверять бюджеты и писать расходы, а /health/readiness покажет "db":"disconnected". Виртуальные ключи без базы работать не будут, останется только мастер-ключ — поэтому базу держат на том же хосте, а не на удалённом.
Можно ли смешивать облачные модели и локальную Ollama?
Да, это штатный сценарий: добавьте в model_list запись с model: ollama/llama3.1 и api_base: http://127.0.0.1:11434, и локальная модель встанет в общий список наравне с облачными. Стоимость по ней шлюз считать не будет — платить там не за что.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.