OpenAI-совместимость на практике: что меняется в вашем коде
Смена адреса в клиенте занимает минуту. Остальное время уходит на проверки.

Заменить base_url на чужой эндпоинт можно за минуту. Дальше начинается работа, потому что код, который писали под OpenAI, держится на десятке мелких допущений. Часть из них ломается молча.

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

Что значит «совместимый API» на деле

Совместимость здесь про контракт HTTP. Те же адреса вида /v1/chat/completions, тот же формат JSON в запросе и ответе, тот же заголовок авторизации. Библиотека openai из Python или Node смотрит на base_url, и ей всё равно, кто на другом конце, если ответ укладывается в ожидания.

Это не значит, что модели ведут себя как GPT. Формат совпадает, содержание ответа, скорость, поддержка расширений и поведение на ошибках отличаются. Код, который шлёт сообщение и печатает текст, переедет за вечер. Код с инструментами и стримингом придётся проверять отдельно.

Первое место правки: клиент и адрес

Держите адрес и ключ в переменных окружения, а не в конструкторе. Тогда переключение между провайдерами — это правка одного файла .env и перезапуск процесса, а не поиск строк по всему репозиторию в ночь перед релизом.

client = OpenAI(base_url="https://api.example.ru/v1", api_key=os.environ["LLM_API_KEY"])
resp = client.chat.completions.create(model="deepseek-chat", messages=messages, stream=True)

Обратите внимание на суффикс /v1. У части шлюзов он входит в адрес, у части нет. Лишний или пропущенный сегмент даёт 404 с невнятным телом ответа. Проверяется одним curl до того, как вы тронете код приложения.

Имя модели ищут дольше всего

У каждого провайдера свои имена. Строка вида gpt-4o живёт в коде, в конфиге деплоя, в промптах и в тестах, и это четыре разных места. Хуже, когда имя собирается из переменной и конкатенации: поиск по репозиторию не находит ничего, а запрос уходит с несуществующей моделью.

Приём простой. Заведите модуль, который отдаёт имя по роли: быстрая модель для ответов клиенту, сильная для разбора документов, дешёвая для черновиков. Остальной код спрашивает роль и не знает, какое имя за ней стоит. При следующем переезде вы меняете один файл.

Отдельно проверьте промпты. В системной инструкции часто остаётся фраза «ты GPT-4» или упоминание конкретной модели. На новой модели такой текст иногда тянет за собой стиль, который вам не нужен.

Расширения протокола: стриминг и инструменты

Стриминг

При stream=True сервер присылает куски ответа, клиент склеивает их в поток. Различия начинаются в мелочах: приходит ли первый чанк с пустым содержимым, как выглядит финальный пакет, есть ли в нём причина остановки, повторяются ли служебные поля.

Код, который накапливает текст через delta.content и не проверяет на None, падает на первом же служебном чанке. На коротком ответе это может не проявиться вообще: поток заканчивается раньше, чем успевает прийти проблемный пакет.

Прогоните длинный ответ, на полторы-две тысячи токенов. Обрыв потока на середине и потерянные абзацы видны только там.

Инструменты и JSON

Function calling, строгий JSON по схеме, режим размышлений — это расширения. Они описаны в документации OpenAI, но не обязательны для совместимого шлюза. Часть провайдеров их поддерживает, часть нет, часть поддерживает с ограничениями: без параллельных вызовов, без принудительного выбора функции, без строгой схемы.

Если продукт живёт на инструментах, проверяйте это до переезда. Тест простой: два инструмента, запрос, который требует вызвать второй, и запрос, который не требует инструментов вовсе. Второй важнее. Модель должна ответить текстом, а не выдумать вызов.

С JSON та же история. Даже когда параметр формата принимается, никто не обещает валидный JSON в каждом ответе. Валидацию на своей стороне не убирают, и это правило не зависит от провайдера.

Чего обычно нет

  • Ответов в стиле Responses API: это отдельный формат, а не chat completions.
  • Векторных представлений текста: embeddings часто не входят в набор.
  • Ассистентов с файлами и хранилищами на стороне провайдера.
  • Картинки тем же ключом, которым работает текст.

Вывод практический. Если в проекте только chat completions и стриминг, переезд почти безболезненный. Если навешаны ассистенты и файлы, сначала посмотрите, что из этого действительно используется в проде, а что осталось от прототипа.

Ошибки и повторы

Коды ошибок совместимы, поведение отличается. Ответ 429 приходит чаще, а в теле может лежать другая структура с деталями. SDK умеет повторять запрос, но по умолчанию делает это не всегда так, как нужно вашему сценарию.

Задайте таймаут и число повторов явно, обработку 429 вынесите в одно место. Если каждый вызов тащит свой цикл повторов, при инциденте вы получите лавину запросов и потраченные токены вместо понятной ошибки.

Тесты перед переключением трафика

Соберите 20-30 реальных запросов из логов: короткий вопрос, длинный контекст, инструкция с форматом, запрос с инструментом, провокация на выдумку. Прогоните их на старой и новой модели, сохраните пары ответов.

Смотрите не на «лучше или хуже», а на поломки: изменился формат, потерялась инструкция, появились лишние вступления, ответ обрезался на длине. Общую оценку можно снять вторым прогоном, где модель-судья читает пару и отвечает одним словом, подходит или нет. Критерии пишите свои, иначе получите вежливое «в целом неплохо».

Один сценарий стоит проверить руками целиком, от запроса до того, что видит пользователь. Автотесты ловят формат, но не ловят тон и лишние вступления.

Переключение без простоя

В коде живут два клиента: старый и новый. Флаг в конфиге решает, какой работает, доля трафика растёт постепенно. Логи с обеих сторон пишутся в одно место, чтобы сравнение шло по факту.

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

Лимит расходов на новом ключе ставьте сразу. Незнакомая модель на незнакомой нагрузке — обычная причина счёта, который не сходится с планом. Как выбрать порог, разбирали в статье про лимиты расходов на API.

Чек-лист на первый вечер

  1. Адрес и ключ в переменных окружения, суффикс /v1 проверен curl-ом.
  2. Имена моделей собраны в один модуль, поиск по старым именам не находит ничего.
  3. Стриминг проверен на длинном ответе, служебные чанки не ломают парсер.
  4. Инструменты и JSON проверены отдельно, валидация на своей стороне осталась.
  5. Запросы из логов прогнаны на двух моделях, пары сохранены.
  6. Флаг переключения и мгновенный откат на месте.
  7. Лимит расходов и уведомление включены.

Начинать стоит с одного сценария, который не жалко сломать. Опыт на нём дешевле, чем переключение всей системы по инструкции из документации.

Где мы в этой схеме

Китай-API — российский шлюз к моделям DeepSeek, Qwen, GLM и Kimi. Один ключ, OpenAI-совместимый адрес, оплата в рублях, для юрлица счёт и закрывающие документы. Про смену адреса в SDK есть отдельный разбор — OpenAI SDK и китайские модели, про перенос проекта целиком — миграция с OpenAI на DeepSeek.

Продукт в подготовке: запуск скоро, приём заявок на ранний доступ открыт. Если хотите прогнать свои запросы на шлюзе до общего запуска, оставьте заявку на ранний доступ.