Когда почти каждая операция в продукте проходит через языковую модель — генерация коммерческих предложений, скоринг, саммари звонков — зависимость от единственного провайдера становится системным риском. Groq может лечь с ошибкой 503, OpenAI упереться в рейт-лимит, а флагманская модель по ошибке конфигурации начать обрабатывать задачи, для которых достаточно самой дешёвой. Команда wiin.agency решила эту проблему роутером примерно на 500 строк кода на NestJS — без сторонних фреймворков-оркестраторов.

Основа архитектуры — то, что большинство современных провайдеров реализуют OpenAI-совместимый API. Это позволяет использовать один официальный openai SDK для всех пяти провайдеров, подменяя только параметр baseURL при инициализации клиента. Groq, Mistral, DeepSeek, xAI и сам OpenAI работают через один и тот же вызов client.chat.completions.create(). Если ключ для провайдера не задан, клиент инициализируется как null и провайдер просто выпадает из цепочки — без ошибок в рантайме.

РольМодельЗа что отвечает
qualityopenai/gpt-oss-120bReasoning, сложный текст, ~500 tok/s
largellama-3.3-70b-versatileМультиязычность, контекст 131k токенов
structuredqwen/qwen3-32bJSON-mode и structured output
fastllama-3.1-8b-instant$0,08 за миллион токенов, 560 tok/s

Вызывающий код не знает ничего о конкретных моделях. Он указывает класс задачи: QUALITY для русскоязычных текстов и КП, BALANCED для скоринга и саммари, FAST для парсинга интентов и извлечения JSON, TRANSCRIBE для аудио. Каждому классу соответствует упорядоченная цепочка «провайдер + модель». В большинстве цепочек первым стоит Groq — четыре разные модели под разные роли: gpt-oss-120b для reasoning и сложных текстов, llama-3.3-70b-versatile для мультиязычных задач с большим контекстом, qwen/qwen3-32b для JSON-mode, llama-3.1-8b-instant для дешёвых и быстрых операций по $0,08 за миллион токенов. Mistral выступает кросс-провайдерным fallback, за ним — OpenAI, DeepSeek и xAI.

Запросы делятся на классы задач: QUALITY, BALANCED, FAST, TRANSCRIBE — каждый класс имеет свою цепочку провайдеров.

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

Ряд нюансов потребовал отдельной обработки. Reasoning-модели серии gpt-5 и gpt-oss-* не принимают стандартные параметры max_tokens и temperature — вместо них используются max_completion_tokens и reasoning_effort. Reasoning-токены списываются из того же completion-бюджета, поэтому лимит выставляется с двукратным запасом. Qwen 3 добавляет в ответ блоки <think>…</think> с внутренними рассуждениями — они вырезаются регуляркой. Если модель оборачивает JSON в текстовую преамбулу («Конечно, вот ваш JSON: {…}»), отдельная функция stripMarkdown извлекает первый валидный объект.

Для транскрипции аудио роутер проверяет качество результата: Whisper на тишине или шуме склонен к галлюцинациям — повторяет одну фразу десятки раз. Текст короче 50 символов или с долей уникальных слов ниже 15% считается невалидным и роутер переходит к следующему провайдеру в цепочке TRANSCRIBE.

Каждый успешный запрос записывается в таблицу LLMCallLog: операция, стратегия, провайдер, модель, количество входных и выходных токенов, стоимость в центах, длительность. Те же данные идут в Prometheus. Это позволяет видеть реальную стоимость каждой фичи и отслеживать, какой провайдер фактически обрабатывает трафик, а не просто числится в цепочке.

Авторы честно перечисляют готовые альтернативы. OpenRouter — hosted-решение с одним ключом и встроенным fallback, нулевой инфраструктурой. LiteLLM — де-факто стандарт отрасли: SDK плюс прокси-гейтвей, поддержка более 100 провайдеров, кэш, бюджеты, observability. Portkey — аналогичный ИИ-gateway с guardrails. Vercel ИИ SDK — если стек на TypeScript. Существуют и семантические роутеры вроде Not Diamond и Martian, которые выбирают модель под конкретный запрос динамически, а не по статичной цепочке.

Собственное решение авторы объясняют не принципиальностью: нужен был тонкий слой без дополнительного прокси-хопа в критическом пути, с учётом стоимости прямо в доменной БД и со специфической логикой — валидацией Whisper-галлюцинаций и обработкой reasoning-моделей под конкретные промпты. Минимальная версия роутера без БД и фреймворка опубликована на GitHub по адресу github.com/ИИ-sales-agency/wiin-examples.