Если вы занимаетесь бэкендом для мультимодельной интеграции, вы наверняка переживали такой коллапс: бизнес-код забит if (vendor === 'openai') ... else if (vendor === 'claude') ..., интерфейс потокового диалога написан на восемьсот строк логики адаптации, а при добавлении нового вендора приходится трогать весь ключевой путь. Проблема не в вас — просто API больших моделей от разных компаний изначально не задумывались унифицированными.
Суть модельного шлюза — это слой трансляции протоколов: свести разнородные запросы/ответы N вендоров к 1 набору внутреннего стандартного протокола. Эта статья разбирает, как именно должен быть устроен слой трансляции, в трёх последовательных частях: принципы, практика, сравнение.
I. Четыре типа неоднородности: почему прямое подключение к нескольким вендорам неизбежно выходит из-под контроля
Сначала посмотрим на реальные различия, каждое с цифрами.
Способ аутентификации: OpenAI использует Authorization: Bearer sk-xxx, длина поля заголовка около 51 символа; Claude использует x-api-key; Gemini засовывает ключ в URL в параметр запроса ?key=; Qwen и ERNIE используют Authorization, но с разными алгоритмами подписи, у ERNIE access_token по умолчанию действителен 30 дней, по истечении возвращается код ошибки 110. Четыре вендора — четыре разных подхода.
Формат SSE-чанков потоковой передачи: чанк OpenAI — это data: {"choices":[{"delta":{"content":"x"}}]}, маркер окончания — data: [DONE]; тип события Claude — event: content_block_delta вместе с data: {"delta":{"text":"x"}}, имя поля меняется с content на text; Gemini кладёт инкремент в candidates[0].content.parts[0].text, вложенность на 3 уровня глубже. Чтобы бизнес-слой разобрал одно и то же предложение, приходится писать четыре набора парсеров.
Система кодов ошибок: это самое мерзкое. OpenAI использует HTTP-статус-коды + error.type, ограничение скорости — 429; Claude при ограничении скорости возвращает 429, но type в теле — rate_limit_error; Qwen при ограничении скорости может вернуть статус 200, но в теле с code: "Throttling"; ERNIE использует error_code: 17 для обозначения превышения квоты. Одно и то же событие — четыре разных выражения.
Метрика подсчёта токенов: у OpenAI usage.total_tokens = prompt + completion, и китайский, и английский режутся по BPE; Claude отдельно возвращает input_tokens и output_tokens, причём гранулярность разбиения китайских символов тоньше, для одного и того же фрагмента из 500 китайских иероглифов количество токенов у двух вендоров может отличаться на 20%–35%. При оплате по объёму это различие напрямую превращается в погрешность в счёте.
При прямом подключении к четырём вендорам объём кода бизнес-слоя разрастается в 3–4 раза по сравнению с одним вендором, а стоимость поддержки растёт линейно с числом вендоров.
II. Проектирование слоя трансляции: единый формат, нормализация потока, маппинг кодов ошибок, агрегация использования
Слой трансляции должен делать четыре вещи, ни одну нельзя опустить.
Единый формат запроса/ответа: внутри определяется набор стандартной схемы, поля выравниваются по стилю OpenAI (потому что у него самая широкая экосистема). Внешний запрос сначала преобразуется во внутренний формат, затем адаптер преобразует его в формат каждого вендора. Паттерн адаптера: по одному классу на вендора, реализующему два метода — buildRequest() и parseResponse().
Нормализация потоковых чанков: суть в том, чтобы превратить SSE-чанки всех вендоров в единый {delta: string, finish_reason: string|null}. Делается это через конечный автомат, который по типу вендора диспетчеризует в разные парсеры и выдаёт единую структуру. Ключевой момент — обработка границ: разделитель SSE \n\n, префикс data:, склейка многострочных data — всё это должен съедать парсер. Моя личная боль: события ping и message_stop у Claude — если их явно не игнорировать, пустые delta уйдут на фронтенд и вызовут мерцание UI.
Таблица маппинга кодов ошибок: поддерживается таблица vendorCode -> unifiedCode, сводящая всё к 6 категориям: AUTH_FAILED, RATE_LIMITED, QUOTA_EXCEEDED, INVALID_REQUEST, UPSTREAM_ERROR, TIMEOUT. Что бы ни вернул апстрим, слой трансляции ищет по таблице и преобразует в единый код, а бизнес-слой знает только эти 6.
Агрегация использования: после каждого вызова метрики токенов всех вендоров единообразно пересчитываются во внутренний стандартный токен, при записи в БД проставляются vendor, model, request_id для последующей сверки и анализа затрат. Здесь нужно учитывать различие метрик: output_tokens у Claude и completion_tokens у OpenAI нельзя просто складывать, их нужно приводить к одной базе по коэффициенту пересчёта.
В слоистой архитектуре я обычно выделяю четыре слоя: слой доступа (аутентификация + ограничение скорости), слой маршрутизации (выбор модели), слой трансляции (преобразование протокола), слой адаптеров (вызов SDK вендора). Слой трансляции — чистые функции, не трогает IO, удобен для юнит-тестов.
III. Практический код: меняем одну строку base_url и подключаемся к агрегирующему шлюзу
Если не хотите писать слой трансляции с нуля, использовать готовую платформу агрегации AI API — более прагматичный выбор. В нашем проекте использовался token8341, он совместим с OpenAI SDK, достаточно изменить одну строку base_url, чтобы переключиться, — работу слоя трансляции он делает за вас.
from openai import OpenAI
client = OpenAI(
api_key="your-key",
base_url="https://api.token8341.com/v1" # меняем только эту строку
)
resp = client.chat.completions.create(
model="deepseek-v3", # также можно заменить на gpt-4o / qwen-max / claude-4-sonnet
messages=[{"role": "user", "content": "объясни SSE"}],
stream=True
)
for chunk in resp:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="")В этом коде нормализация SSE-чанков, маппинг кодов ошибок и агрегация использования уже выполнены на стороне шлюза, бизнес-слой получает стандартный формат OpenAI. Маршрутизация между моделями тоже не нужна — шлюз сам выбирает оптимальную модель под задачу: например, длинные китайские тексты идут через Qwen, задачи рассуждения — через DeepSeek, сценарии, чувствительные к стоимости, — через Doubao.
Предупреждение о подводных камнях: после смены base_url не передавайте напрямую параметры, специфичные для вендора. Например, logit_bias у OpenAI, top_k у Claude — шлюз не обязательно поддерживает их все, при передаче они могут быть молча проигнорированы, и вы будете думать, что они работают, а на самом деле нет. Перед релизом прогоните скрипт нагрузочного тестирования на совместимость параметров.
IV. Сравнение: маршрутизация между моделями vs привязка к одной модели
Приведу набор наших измеренных цифр. Привязка к одной модели (прямое подключение к OpenAI): время интеграции около 2 человеко-дней, для переключения при сбое нужно менять код и перевыпускать, среднее время восстановления 15 минут, возможностей оптимизации затрат нет. Маршрутизация между моделями (через шлюз): время интеграции около 0.5 человеко-дня, переключение при сбое выполняется автоматически, время восстановления менее 3 секунд, затраты можно снизить примерно на 30%–40% за счёт распределения по задачам.
По сложности поддержки: код адаптации при прямом подключении к 4 вендорам — около 2000 строк, после перехода на шлюз код адаптации бизнес-слоя сокращается до менее чем 200 строк, уменьшение на 90%. По задержке: шлюз добавляет один переход, P99 увеличивается примерно на 20–40 мс — по сравнению с сэкономленными затратами на поддержку и гибкостью переключения эти накладные расходы приемлемы.
Конечно, шлюз — не серебряная пуля. Если ваш бизнес использует только одну модель и никогда её не меняет, прямое подключение проще. Но как только речь заходит о нескольких моделях, нескольких вендорах, оптимизации затрат — слой трансляции становится обязательной инфраструктурой. Модельный шлюз, API больших моделей, агрегация AI API — это по сути три названия одной и той же проблемы одного уровня.
И последнее напоминание: покрытие юнит-тестами слоя трансляции должно быть максимальным, особенно граничные случаи потоковых чанков — это зона повышенного риска онлайн-инцидентов.
Автор: Лю Чжиюань
Дата публикации: 28 октября 2026 г.