Большинство людей не начинают проект с мыслями о переносимости между поставщиками. Они берут ключ у того вендора, где проще зарегистрироваться, пишут интеграцию и выпускают продукт. Через несколько месяцев они читают бенчмарк, или открывают счёт, или упираются в лимит запросов, и хотят попробовать что-то другое. Вот тогда и появляется боль, потому что они заложили предположения о первом поставщике прямо в HTTP-слой.
Решение — не какая-то библиотека абстракции. Это формат передачи данных. API chat completions от OpenAI тихо стал протоколом по умолчанию для общения с LLM, и как только вы пишете код под него, смена поставщика — это в основном вопрос изменения двух строк.
API, который все скопировали
OpenAI определил простой контракт: вы отправляете POST JSON на /v1/chat/completions, а получаете обратно строку choices[0].message.content. Запрос выглядит так:
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
}'Почти каждая крупная модель теперь говорит на этом диалекте. DeepSeek, Qwen, ERNIE, Doubao и остальные предоставляют эндпоинт, который принимает то же тело запроса и возвращает ту же структуру. Это значит, что ваш клиентский код не заботит, кто находится на другом конце. Вы можете заменить строку model и эндпоинт, и больше ничего не меняется.
Вот тот же вызов, направленный на агрегатор, который предоставляет несколько из этих моделей под одним базовым URL:
curl https://api.token8341.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKENWORKS_API_KEY" \
-d '{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
}'Один клиент, много моделей
В Python вы обычно создаёте экземпляр OpenAI SDK один раз и передаёте имя модели с каждым запросом. Если ваш поставщик поддерживает совместимый API, вы задаёте base_url один раз, а всё остальное остаётся неизменным:
from openai import OpenAI
client = OpenAI(
base_url="https://api.token8341.com/v1",
api_key="sk-your-tokenworks-key",
)
def ask(model: str, prompt: str) -> str:
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
# Same function, three different model families.
print(ask("gpt-4o", "Summarize this log for me."))
print(ask("deepseek-chat", "Summarize this log for me."))
print(ask("qwen-max", "Summarize this log for me."))Функция не меняется. Меняется строка с названием модели. Вот и весь трюк. Стриминг работает так же:
stream = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Write a haiku about databases"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
print(delta, end="")Вызов функций, режим JSON и эмбеддинги — всё это работает через ту же совместимую поверхность, так что при переключении вы не ограничены обычным чатом.
Чего переключение вам не даёт
Хочу чётко обозначить то, что не меняется бесплатно. Транспорт переносим; модели — нет.
Разные модели по-разному реагируют на промпты. Системный промпт, настроенный под GPT-4o, может показать худшие результаты на модели рассуждений, которой нужна другая структура. Контекстные окна различаются, иногда сильно: одна модель может принимать 128k токенов, а другая — 32k. Токенизаторы тоже различаются, поэтому один и тот же документ стоит разного количества токенов на каждой модели. Максимальная длина вывода — ещё один параметр, который относится к модели, а не к протоколу.
Так что «переключение без изменения кода» — это на самом деле «переключение без переписывания вашего HTTP-клиента». Вам всё равно нужно провести прогон оценки, прежде чем направлять продакшен-трафик на новую модель. Хорошая новость в том, что совместимый API делает эту оценку дешёвой, потому что тестовый стенд — это просто цикл по именам моделей.
Практический шаблон
В своих побочных проектах я на самом деле держу небольшую таблицу конфигурации, а не изменения в коде:
MODELS = {
"fast": "deepseek-chat",
"smart": "qwen-max",
"frontier": "gpt-4o",
}
def run(task, prompt):
return ask(MODELS[task], prompt)Хотите попробовать более дешёвую модель для линии fast? Измените одну строку. Хотите переключиться на второго поставщика, когда первый упирается в лимит? Перехватите исключение и повторите с следующей строкой модели. Ничто из этого не затрагивает построение запроса.
Именно на этом шаблоне резервирования окупает себя мультимодельный эндпоинт. С одним базовым URL и одним ключом вы можете маршрутизировать, повторять и проводить A/B-тестирование между поставщиками — всё через те же две строки настройки клиента, которые вы написали в первый день.
Особенности ответов, за которыми стоит следить
Транспорт совместим, но некоторые различия на уровне ответов всё же просачиваются. Модели рассуждений иногда возвращают дополнительное поле вроде reasoning_content alongside обычного сообщения. Код, который читает choices[0].message.content, продолжает работать в любом случае, но вы можете захотеть отобразить этот текст рассуждений в отладочном представлении. Сообщения об ошибках тоже различаются: один поставщик возвращает полезную строку insufficient_quota, другой — голый 429 без тела. Если вы строите логику повторных попыток, рассматривайте любой 429 или 5xx как «отступить и попробовать снова», а не разбирайте текст, специфичный для вендора.
Ничто из этого не является причиной оставаться привязанным к одному поставщику. Это причина держать обработку ошибок универсальной, а выбор модели — переменной.
Именно такую позицию занимает SiCore TokenWorks: OpenAI-совместимый эндпоинт по адресу https://api.token8341.com/v1, где строка с названием модели — единственное, что меняется при переходе между GPT-4o, Claude, Gemini, DeepSeek, Qwen, ERNIE, Doubao, Spark и Pangu.