SSD Nodes Learn 🎉 VPS от $4.99/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-07

Как развернуть LiteLLM как шлюз для LLM на своем сервере

Узнайте, как настроить LiteLLM для управления запросами к разным моделям через единый API. Инструкция по настройке виртуальных ключей, лимитов бюджета и отказоустойчивости.

Что делает self-hosted LLM gateway

LiteLLM — это LLM-шлюз с открытым исходным кодом, который вы размещаете самостоятельно: это единая HTTP-точка входа, к которой обращаются все ваши приложения, а она перенаправляет каждый запрос нужному провайдеру. LLM означает large language model (большая языковая модель). Шлюз поддерживает OpenAI chat completions API (application programming interface), поэтому любая клиентская библиотека, работающая с OpenAI, будет работать и с ним после двух изменений: базового URL и ключа.

Смысл заключается в этом единственном уровне косвенности. Вашим приложениям больше не нужно хранить учетные данные провайдеров. Замена модели превращается в изменение одной строки в конфигурационном файле на сервере вместо внесения правок в код пяти сервисов. А поскольку каждый вызов проходит через один процесс, у вас появляется место для контроля бюджета и ведения учета расходов.

Вот что вы получаете после запуска:

  • Единая точка входа. Приложения обращаются к https://gateway.example.com/v1 и запрашивают имя модели, которое вы придумали сами, например bulk или strong.
  • Виртуальные ключи. Каждое приложение получает собственный ключ с собственным списком разрешенных моделей и лимитом расходов. Вы можете отозвать один ключ, не затрагивая остальные.
  • Резервные варианты (fallbacks). Неудачный вызов или запрос, превышающий лимит токенов, автоматически повторяется с использованием другой модели.
  • Журнал записей. Каждый запрос записывается в строку с указанием стоимости, поэтому на вопрос «какое приложение потратило эти средства» всегда есть ответ.

Зачем запускать шлюз самостоятельно

Управляемый маршрутизатор выглядит так же, но при этом в каждом запросе участвует сторонний процесс. Самостоятельный запуск позволяет хранить ключи провайдера и текст промптов на сервере, который вы контролируете. У этого решения есть реальная цена: теперь вы отвечаете за компонент, от которого зависит каждое приложение. Последний раздел этого руководства посвящен именно этой стоимости, поскольку большинство инструкций упускают этот аспект из виду.

Что вам потребуется

  • VPS (виртуальный выделенный сервер) под управлением Ubuntu 24.04 с установленными Docker и плагином Compose.
  • Доменное имя, указывающее на этот сервер, если к шлюзу будут обращаться внешние машины по протоколу TLS (transport layer security).
  • Как минимум один API-ключ провайдера.

Шлюз не выполняет вычисления. Он пересылает запросы и передает потоковые ответы обратно, поэтому нагрузка на CPU зависит от объема запросов, а не от размера модели. Сервер с 1 vCPU без проблем справляется с несколькими внутренними приложениями. Растет только база данных, так как шлюз записывает одну строку расходов для каждого запроса.

Сначала создайте config.yaml

Файл конфигурации определяет, какие модели может запрашивать клиент. Важны четыре раздела верхнего уровня: model_list, litellm_settings, router_settings и general_settings.

model_list:
  - model_name: bulk
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: strong
    litellm_params:
      model: anthropic/claude-sonnet-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: strong
    litellm_params:
      model: openai/gpt-5.5
      api_key: os.environ/OPENAI_API_KEY

litellm_settings:
  num_retries: 2
  request_timeout: 120
  allowed_fails: 3
  cooldown_time: 30
  json_logs: true
  set_verbose: false

router_settings:
  fallbacks: [{"bulk": ["strong"]}]
  context_window_fallbacks: [{"bulk": ["strong"]}]

general_settings:
  background_health_checks: true
  health_check_interval: 300

model_name — это имя, которое отправляют ваши клиенты. litellm_params.model — это реальная модель, записанная как provider/model. Называйте модели в соответствии с их назначением, а не по имени поставщика. Приложение, которое запрашивает bulk, продолжит работать, даже если в следующем месяце вы решите, что bulk должна быть другой моделью.

api_key: os.environ/ANTHROPIC_API_KEY указывает LiteLLM считывать эту переменную во время выполнения. Сам ключ никогда не появляется в файле, что важно, так как config.yaml — это файл, который вы фиксируете в системе контроля версий.

Две записи намеренно имеют одинаковое имя strong. Когда несколько развертываний имеют один и тот же model_name, маршрутизатор считает их взаимозаменяемыми и пробует другое, если первое дает сбой. Именно так strong продолжает работать, если у одного из поставщиков возникли временные проблемы.

num_retries: 2 повторяет попытку обращения к тому же развертыванию при возникновении ошибки, допускающей повтор. Резервный вариант (fallback) срабатывает только после того, как эти попытки исчерпаны. allowed_fails: 3 вместе с cooldown_time: 30 исключает развертывание из ротации на 30 секунд после 3 неудачных попыток, поэтому поставщик, возвращающий ошибки 500, перестает опрашиваться при каждом запросе.

fallbacks и context_window_fallbacks имеют разные триггеры, и второй из них — полезная функция, которую часто пропускают.

  • fallbacks срабатывает, когда основной вызов завершается неудачей.
  • context_window_fallbacks срабатывает, когда поставщик отклоняет запрос из-за того, что он превышает контекстное окно модели; таким образом, слишком большой промпт отправляется модели с достаточным объемом памяти вместо возврата ошибки вызывающей стороне.

Также существует content_policy_fallbacks для случаев, когда поставщик отказывает по причине нарушения политики контента. Настраивайте его только в том случае, если у вас есть подходящее место для перенаправления таких вызовов.

Развертывание LiteLLM на VPS с помощью Docker Compose

Создайте каталог, содержащий три файла: config.yaml, docker-compose.yml и .env. В официальном руководстве предлагается использовать тег latest. Зафиксируйте конкретный тег релиза, чтобы команда docker compose up -d в следующем месяце развернула тот же шлюз, что и сегодня, а откат к предыдущей версии выполнялся одной строкой.

services:
  litellm:
    image: ghcr.io/berriai/litellm:v1.95.0
    restart: unless-stopped
    command: ["--config", "/app/config.yaml", "--num_workers", "1"]
    ports:
      - "127.0.0.1:4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml:ro
    env_file: .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: litellm
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm"]
      interval: 5s
      timeout: 5s
      retries: 10
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Compose считывает .env здесь дважды. Первый раз — для подстановки ${POSTGRES_PASSWORD} непосредственно в файл compose, второй раз — через env_file, чтобы передать все переменные внутрь контейнера.

Версия v1.95.0 была актуальной в августе 2026 года. Проверьте страницу релизов проекта и зафиксируйте ту версию, которая является актуальной на момент вашего развертывания. Каждый релиз сопровождается цифровой подписью, поэтому вы можете проверить образ перед тем, как доверять ему:

cosign verify --key https://raw.githubusercontent.com/BerriAI/litellm/v1.95.0/cosign.pub ghcr.io/berriai/litellm:v1.95.0

Строка порта выглядит как 127.0.0.1:4000:4000, что публикует порт только на интерфейсе обратной петли (loopback). Напишите 4000:4000, и ваш шлюз станет доступен из всего Интернета, так как Docker добавляет свои правила в цепочку FORWARD в iptables, которые обрабатываются раньше правил ufw, поэтому ufw deny 4000 не сможет их заблокировать. Это самый распространенный способ, которым self-hosted шлюз оказывается открытым для доступа: см. как Docker публикует порт контейнера в обход ufw. Трафик извне должен поступать через обратный прокси-сервер.

Храните ключи провайдеров вне образа

Файл .env содержит все секретные данные. Он передается в виде переменных окружения во время выполнения, поэтому он никогда не встраивается в образ и не попадает в систему контроля версий.

LITELLM_MASTER_KEY=sk-REPLACE_ME
LITELLM_SALT_KEY=sk-REPLACE_ME_TOO
POSTGRES_PASSWORD=REPLACE_ME_AS_WELL
DATABASE_URL=postgresql://litellm:REPLACE_ME_AS_WELL@db:5432/litellm
STORE_MODEL_IN_DB=True
LITELLM_MODE=PRODUCTION
LITELLM_LOG=ERROR
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-proj-...

Сгенерируйте два ключа LiteLLM с использованием качественного источника случайных чисел, а затем ограничьте права доступа к файлу:

printf 'sk-%s\n' "$(openssl rand -hex 32)"
chmod 600 .env

LITELLM_MASTER_KEY — это учетные данные администратора. Они используются для аутентификации в Management API и являются паролем для Admin UI по адресу /ui. Ни одно приложение не должно хранить их у себя.

LITELLM_SALT_KEY шифрует учетные данные провайдеров, хранящиеся в базе данных. Установите этот ключ один раз и не меняйте его. Если изменить его позже, ранее сохраненные учетные данные невозможно будет расшифровать: шлюз запустится в штатном режиме, но каждый вызов к провайдерам будет завершаться ошибкой аутентификации.

STORE_MODEL_IN_DB=True позволяет добавлять и редактировать модели через Admin UI, не изменяя config.yaml. Это удобно, но разделяет источник истины на две части. Определите, какой из них является приоритетным, и зафиксируйте это решение рядом с конфигурационным файлом.

Логика, согласно которой ключи не должны находиться в конфигурационном файле, применима и к инструментам, которые вы предоставляете агенту. В Хранение секретов провайдеров вне AI-агентов описан этот подход, а в env-файлы и секреты в Docker Compose — техническая реализация.

Запустите систему и отслеживайте первый процесс загрузки:

docker compose up -d
docker compose logs -f litellm

Проверка работоспособности

Существует две неавторизованные проверки и одна авторизованная; они завершаются ошибкой по разным причинам.

curl -s http://127.0.0.1:4000/health/liveliness
curl -s http://127.0.0.1:4000/health/readiness

/health/liveliness не требует аутентификации и отвечает "I'm alive!", пока процесс запущен. /health/readiness также не требует аутентификации. Он возвращает JSON-объект с полями "status": "healthy" и db или код 503, если база данных недоступна. Настройте мониторинг на readiness, так как liveliness остается в статусе green даже на шлюзе, который не может найти ни одного виртуального ключа.

Авторизованная проверка взаимодействует с провайдерами:

curl -s http://127.0.0.1:4000/health \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

Она отвечает массивами healthy_endpoints и unhealthy_endpoints. Если модель находится в unhealthy_endpoints с ошибкой аутентификации, это означает, что ключ провайдера в .env неверен или отсутствует — именно эту проблему сейчас нужно обнаружить. Поскольку background_health_checks: true задан, прокси самостоятельно выполняет эти проверки каждые health_check_interval секунд, а /health возвращает последний результат, поэтому опрос этого эндпоинта не отправляет тестовый запрос к вашим провайдерам каждый раз.

Виртуальные ключи и бюджеты для отдельных ключей

Каждое приложение получает собственный ключ, созданный на основе мастер-ключа.

curl -s http://127.0.0.1:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "key_alias": "nightly-summariser",
    "models": ["bulk"],
    "max_budget": 5,
    "budget_duration": "30d",
    "rpm_limit": 60,
    "tpm_limit": 200000
  }'

Ответ содержит поле key, начинающееся с sk-. Эта строка — то, что получает приложение, и это единственный объект, который ему доступен.

  • models — это список разрешенных запросов для данного ключа. Указанный выше ключ может запрашивать только bulk и ничего больше.
  • max_budget: 5 с budget_duration: "30d" ограничивает бюджет пятью долларами США за каждые 30 дней, после чего ключ перестает работать.
  • rpm_limit и tpm_limit устанавливают лимиты на количество запросов в минуту и токенов в минуту исключительно для этого ключа.
  • key_alias — это идентификатор, который вы увидите в журнале расходов спустя шесть недель. Всегда задавайте его.

Когда бюджет исчерпан, вызов завершается с ошибкой HTTP 401 и телом ответа следующего вида:

ExceededBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07

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

Просматривайте и изменяйте ключи через тот же API управления:

curl -s "http://127.0.0.1:4000/key/info?key=sk-..." \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

curl -s -X POST http://127.0.0.1:4000/key/update \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"key": "sk-...", "max_budget": 25}'

Бюджет, контролируемый на уровне шлюза, продолжает действовать, даже если сбой произошел в самом агенте. Именно поэтому он является основой для контроля расходов на AI-агентов на VPS.

Перенаправление массовых задач на бюджетную модель

Укажите клиенту адрес шлюза. Базовый URL, ключ, имя модели:

curl -s http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-<the virtual key>" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "bulk",
    "messages": [{"role": "user", "content": "Say hello in five words."}]
  }'

Любая клиентская библиотека OpenAI работает аналогично: установите base_url в https://gateway.example.com/v1, а api_key — в виртуальный ключ.

Политика маршрутизации из config.yaml теперь применяется автоматически, без участия вызывающей стороны. Запрос к bulk направляется на бюджетную модель. Если этот вызов завершается ошибкой после всех попыток повтора, запрос перенаправляется на strong. Если промпт слишком длинный для bulk, context_window_fallbacks отправляет его на strong вместо возврата ошибки. Массовые задачи, такие как классификация или обработка очереди суммаризации, по умолчанию выполняются дешево, и только сложные запросы требуют больших затрат.

Здесь шлюз также доказывает свою эффективность при работе с агентами, использующими инструменты. MCP (model context protocol) сервер на том же VPS и управляющий им агент могут обращаться к одной точке входа, что позволяет менять модель без переразвертывания любого из компонентов.

Как узнать, что произошел переход на резервный вариант?

Это режим сбоя, который стоит денег, так как внешне всё выглядит исправно. Успешный переход на резерв возвращает HTTP 200 с обычным телом ответа. Ваша дешевая модель может быть недоступна целый день, каждый запрос будет незаметно обрабатываться дорогой моделью, и первым доказательством этого станет счет на оплату.

Доказательства существуют в заголовках ответа. Запросите их:

curl -s -D - -o /dev/null http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-<the virtual key>" \
  -H 'Content-Type: application/json' \
  -d '{"model":"bulk","messages":[{"role":"user","content":"ping"}]}' \
  | grep -i '^x-litellm'
  • x-litellm-model-group — это то, что запрашивал клиент. x-litellm-model-id — это развертывание, которое ответило. Если они не совпадают, значит, произошел переход на резерв.
  • x-litellm-attempted-fallbacks и x-litellm-attempted-retries подсчитывают их. При корректном вызове оба значения равны 0.
  • x-litellm-response-cost — это стоимость одного вызова в долларах США.
  • x-litellm-call-id — это идентификатор, который вы используете для поиска того же вызова в своих логах.

Записывайте x-litellm-attempted-fallbacks для каждого запроса и настройте оповещение, когда значение перестает быть равным 0. Это число — разница между работающей политикой маршрутизации и политикой, которая незаметно превратилась в «всегда использовать дорогую модель».

Полноценный вариант решения — трассировка, которая заслуживает отдельной настройки: self-hosted Langfuse для трассировки вызовов агентов. LiteLLM поставляет callback, поэтому его подключение занимает две строки плюс учетные данные.

litellm_settings:
  success_callback: ["langfuse"]
  failure_callback: ["langfuse"]
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://langfuse.example.com

Установите failure_callback, а также success_callback. Если пропустить этот шаг, вы сохраните только те трассировки, в которых не возникло ошибок. Помимо всего этого, LiteLLM записывает строку расходов по каждому запросу в Postgres, а панель администратора по адресу /ui считывает эту таблицу. Она растет вместе с трафиком, поэтому следите за ней на дисках малого объема.

Размещение шлюза за обратным прокси-сервером

Никакие внешние запросы не должны достигать порта 4000 напрямую. Выполняйте TLS-терминацию в nginx или Caddy и перенаправляйте трафик на адрес loopback.

location / {
    proxy_pass http://127.0.0.1:4000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_read_timeout 600s;
}

Две из этих строк часто забывают добавить. Параметр proxy_buffering off важен, так как потоковая передача завершения (streaming completion) представляет собой последовательность событий, отправляемых сервером (server-sent events). При включенной буферизации nginx удерживает фрагменты данных до завершения ответа, из-за чего клиент «замирает» в ожидании, а затем получает всё содержимое сразу. Параметр proxy_read_timeout 600s важен, так как длительная генерация ответа превышает стандартный таймаут nginx в 60 секунд. В этом случае клиент получает ошибку 504, а в журнале ошибок фиксируется upstream timed out (110: Connection timed out) while reading response header from upstream.

Для настройки сертификата кратчайшим путём является Certbot с Let's Encrypt для nginx. Если сервер уже обслуживает несколько контейнеров, Traefik перед несколькими приложениями Compose позволит управлять маршрутизацией и сертификатами в одном месте.

Шлюз теперь является единой точкой отказа

Будьте честны в отношении того, что вы построили. Каждое ваше приложение теперь зависит от одного контейнера на одном VPS. Пока он не работает, ничто не может обратиться к какой-либо модели, включая провайдеров, которые полностью исправны. Из этого следуют четыре вывода.

  • Неверная конфигурация выводит из строя всё сразу. restart: unless-stopped перезапускает процесс при сбое, и он снова и снова перезапускает контейнер, который не может прочитать config.yaml. Читайте docker compose logs litellm после каждого изменения конфигурации и вносите изменения тогда, когда у вас есть время проконтролировать их.
  • Postgres находится на пути прохождения запроса. Поиск виртуальных ключей и запись расходов используют его. /health/readiness, возвращающий 503, — это предупреждение о том, что шлюз запущен, но не может выполнять ни то, ни другое.
  • Масштабируйтесь путём добавления экземпляров, а не увеличения одного. Рекомендация самого проекта — один воркер на экземпляр (--num_workers 1) при условии, что несколько экземпляров используют одну базу данных. Два небольших шлюза за балансировщиком нагрузки исключают проблему единственного контейнера. Они не исключают проблему базы данных.
  • Делайте резервные копии того, что нельзя восстановить. Это config.yaml и .env вместе с pg_dump базы данных. Потеря LITELLM_SALT_KEY делает зашифрованные учетные данные провайдеров внутри этого дампа бесполезными, поэтому файл окружения и дамп должны быть частью одного задания резервного копирования: резервное копирование restic во внешнее хранилище.

Обновление заключается в редактировании тега образа и выполнении docker compose up -d. LiteLLM по умолчанию выполняет prisma migrate deploy при запуске, поэтому новый контейнер выполняет миграцию схемы базы данных при первой загрузке. Сделайте дамп до того, как измените тег, так как возврат старого образа не отменяет миграцию, которая уже была выполнена.

FAQ

Добавляет ли LiteLLM заметную задержку к каждому вызову?

Проект заявляет о задержке 8 мс на 95-м перцентиле при нагрузке 1000 запросов в секунду, согласно README по состоянию на август 2026 года. Рассматривайте это как маркетинговую цифру вендора. Реальная задержка зависит от сетевого расстояния между вашими приложениями и шлюзом, так как вы добавляете один дополнительный round trip к каждому вызову. Запускайте шлюз в том же регионе, где находятся вызывающие его приложения, а затем измерьте собственные накладные расходы с помощью заголовка x-litellm-overhead-duration-ms в реальном ответе.

Почему потоковая передача (streaming) перестала работать после установки nginx перед сервисом?

Потому что nginx по умолчанию буферизует ответы от upstream, а потоковая генерация (streaming completion) представляет собой серию server-sent events. При включенном proxy_buffering nginx собирает фрагменты и отдает их только после завершения ответа, поэтому клиент ожидает в тишине, а затем получает весь ответ целиком. Установите proxy_buffering off; в блоке location. Увеличьте proxy_read_timeout в том же блоке, иначе длительная генерация превысит стандартный таймаут nginx в 60 секунд, и клиент получит ошибку 504.

Что происходит, когда у виртуального ключа заканчивается бюджет?

Вызов завершается с ошибкой HTTP 401 и телом ответа вида ExceededBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07. Ошибка 401 — это ловушка: клиентские библиотеки интерпретируют её как сбой аутентификации, поэтому пользователи начинают проверять валидность ключа вместо того, чтобы прочитать сообщение. Логируйте тело ответа вместе с кодом состояния. Проверьте реальный статус ключа с помощью /key/info?key=sk-... относительно мастер-ключа и увеличьте лимит через /key/update, если бюджет был задан слишком низким.

Может ли шлюз направлять запросы как к локальной модели, так и к облачным?

Да, это просто еще одна запись в model_list. Используйте префикс ollama_chat/ вместе с api_base, например, model: ollama_chat/llama3.1 наряду с api_base: http://ollama:11434. Внутри контейнера localhost означает сам этот контейнер, поэтому используйте имя сервиса из Compose или адрес хоста в сети Docker, но никогда не используйте 127.0.0.1. Развертывание локальной модели — это отдельная задача: см. самостоятельный хостинг LLM с помощью Ollama на VPS.