У вас уже есть серверная часть на клиентской библиотеке OpenAI. Новый поставщик предлагает OpenAI‑совместимый API и показывает знакомый пример: другой base_url, другой ключ, тот же вызов chat.completions.create(). На тесте «ответь одним словом» всё работает. Можно переносить боевой трафик?

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

Поэтому совместимость полезнее рассматривать не как свойство сервиса, а как набор контрактов между вашим приложением и API. До миграции нужно проверить семь вещей:

  1. какой HTTP‑маршрут формирует библиотека из base_url;

  2. как передаётся API‑ключ;

  3. какие идентификаторы моделей доступны на нужном маршруте API;

  4. совпадают ли значимые поля запроса и ответа;

  5. получает ли приложение события SSE по мере генерации, не дожидаясь всего ответа и не теряя фрагменты потока;

  6. проходит ли полный цикл вызова инструмента;

  7. можно ли программно различать ошибки и безопасно повторять запросы.

Эта карта относится и к единому API для ИИ‑моделей, и к шлюзу одного поставщика. Надпись «API совместим с OpenAI» описывает форму интерфейса, но не гарантирует, что он воспроизводит каждую возможность OpenAI API для каждой модели.

Шаг 0. Зафиксируйте исходный эталон

Допустим, в приложении есть метод generate_reply(). В фоновой задаче он ждёт обычный JSON‑ответ, в интерактивном режиме отдаёт текст по SSE, а при вопросе о заказе разрешает модели вызвать lookup_order. Через эти три режима и стоит проводить семь проверок: клиент один, но способы взаимодействия с ним различаются.

Сначала зафиксируйте то, что работает сейчас:

  • точную версию клиентской библиотеки и способ создания клиента;

  • маршрут API, идентификатор модели, тайм‑аут и настройки повторных попыток;

  • обязательные поля ответа, которые читает код;

  • формат сведений об ошибке, которые сохраняет приложение, и идентификатор запроса;

  • 5–10 обезличенных контрольных запросов из реальной нагрузки.

В наборе должны быть короткий ответ, длинная генерация, потоковая выдача, вызов инструмента и хотя бы один негативный сценарий — например, неизвестная модель. Разведите уровни наблюдения. На уровне HTTP сохраните код ответа, заголовки и момент прихода событий SSE; на уровне клиентской библиотеки — типы объектов или исключений, нужные поля, валидность аргументов инструмента и признак завершения потока. Сравнивать буквальный текст модели бессмысленно.

Именно этот исходный эталон отвечает на вопрос «что значит работает для нашего приложения». Без него сравнение быстро сводится к субъективному «новый маршрут API вроде отвечает», а несовместимость обнаруживается уже в обработчике ответа.

Шаг 1. Подмените базовый адрес в отдельной конфигурации

Не меняйте сразу действующую конфигурацию боевой среды. Создайте второй экземпляр клиента и направьте на него только тестовый прогон. Для официальной библиотеки OpenAI на Python минимальная конфигурация выглядит так:

import os
from openai import OpenAI

candidate = OpenAI(
    base_url=os.environ["SMARTAIPACK_BASE_URL"],
    api_key=os.environ["SMARTAIPACK_API_KEY"],
)

response = candidate.chat.completions.create(
    model=os.environ["SMARTAIPACK_MODEL"],
    messages=[{"role": "user", "content": "Верни JSON: {\"ok\": true}"}],
)

Зафиксируйте версию библиотеки рядом с результатом теста. Разные клиенты и обёртки по‑разному обрабатывают завершающий / и присоединяют маршрут API. Проверяйте итоговый URL запроса, а не только значение переменной.

Здесь можно подставить конкретный OpenAI‑совместимый API для языковых моделей. Например, SmartAIPack публикует единый базовый адрес https://route.smartaipack.ru/v1. Это отдельный российский B2B‑шлюз к ИИ‑моделям, а не домен OpenAI. Поэтому у переменных есть общий префикс SMARTAIPACK_*: он снижает риск перепутать ключи, но не заменяет проверку правильности привязки секрета к переменной среды.

В репозитории SmartAIPack этот адрес закреплён в инструкции для клиентов и тестах интерфейса, а автоматическая проверка перед выпуском обращается к публичному /v1/chat/completions. Для вашего приложения это всё равно лишь исходная точка. Сначала отправьте один и тот же контрольный запрос напрямую по HTTP, затем через зафиксированную версию библиотеки. Так вы отделите ошибку контракта HTTP от поведения библиотеки.

Шаг 2. Проверьте авторизацию отдельно

Ожидаемая схема — Authorization: Bearer …, но положительного запроса мало. Выполните три проверки: действующий ключ даёт ответ; отсутствующий заголовок отклоняется; заведомо неверное тестовое значение тоже отклоняется. Запишите код ответа HTTP и форму ошибки.

Ключ SmartAIPack создаётся для SmartAIPack и не является официальным ключом OpenAI. Не помещайте его в код, команду curl в задаче или браузерное приложение. Передавайте ключ через переменную среды или хранилище секретов. Не включайте его значение в диагностические сообщения своего приложения.

Шаг 3. Возьмите идентификатор модели из текущего каталога

Успешная авторизация не означает, что шлюз знает имя модели из вашей прежней конфигурации. В SmartAIPack доступные значения берутся в кабинете или через GET /v1/models. Это важнее маркетингового списка: каталог отражает идентификаторы моделей для клиентских запросов, а не названия из пресс‑релиза поставщика.

Проверьте три сочетания: известная модель на нужном маршруте API, неизвестная модель и, если каталог разводит протоколы, известная модель на неподдерживаемом для неё маршруте. Например, нельзя считать поддержку /v1/chat/completions доказательством поддержки /v1/responses.

Затем прогоните параметры, от которых зависит код: response_format, лимит токенов, seed, reasoning или другие используемые поля. Для каждого заранее задайте критерий проверки: JSON проходит схему, лимит отражается в признаке завершения или поле usage, детерминизм проверяется серией повторов, а режим рассуждений оставляет документированный признак. Если эффект нельзя отличить от случайного ответа, поддержка параметра не подтверждена. Молчаливое игнорирование опаснее честного 400: приложение продолжит работу, но изменит поведение.

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

Шаг 4. Разделите обычный ответ и потоковую выдачу

Начните с stream=false. На уровне HTTP зафиксируйте в отчёте о тесте статус и тело ответа. В объекте библиотеки сравните не все поля, а только те, которые читает generate_reply(): расположение текста, finish_reason или другой признак завершения, поле usage и идентификатор ответа. Затем намеренно вызовите ошибку проверки данных и посмотрите на тип и доступные поля исключения библиотеки.

После этого включайте stream=true. По документации OpenAI оба интерфейса используют SSE, но содержимое различается: Chat Completions присылает фрагменты с delta, а Responses — события с собственными значениями type. Клиент, написанный для одного формата, не обязан понимать другой только потому, что способ передачи совпадает.

Проверка потоковой выдачи должна отвечать на конкретные вопросы:

  • первый фрагмент приходит до завершения всей генерации;

  • фрагменты можно собрать в правильный текст без пропусков и дублей;

  • пустой финальный delta или завершающее событие не попадает в пользовательский текст;

  • разрыв соединения не считается успешным завершением;

  • промежуточный сервер, контроллер входящего трафика и серверная часть приложения не накапливают весь ответ перед отправкой клиенту.

В SmartAIPack этот участок передачи покрыт воспроизводимым тестом: предварительный маршрутизатор передаёт Content-Type: text/event-stream, тело SSE и статус вышестоящего сервиса, а в Nginx для маршрута отключено накопление ответа. Это подтверждает поведение конкретного участка шлюза. Ваш тест всё равно нужен: между ним и браузером могут находиться клиентская библиотека, серверная часть приложения, контроллер входящего трафика и CDN.

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

Шаг 5. Прогоните вызовы инструментов до финального ответа

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

Для контрольного инструмента lookup_order задайте короткую JSON Schema с additionalProperties: false. Затем выполните весь цикл:

  1. отправьте сообщения и описание инструмента;

  2. получите имя функции, идентификатор вызова и аргументы в JSON;

  3. проверьте аргументы по схеме и вызовите тестовую реализацию в приложении;

  4. добавьте исходное сообщение assistant с tool_calls и сообщение tool с тем же идентификатором вызова;

  5. отправьте историю повторно и получите финальный текст.

На каждом шаге проверяйте форму данных, а не только код HTTP 200. Аргументы должны проходить вашу схему; неизвестная функция — отклоняться; идентификатор вызова — сохраняться во втором запросе. Обработчик параллельных вызовов сначала проверьте воспроизводимо: передайте ему искусственный ответ с двумя элементами tool_calls. Результаты должны сопоставляться по идентификаторам, а не по положению в массиве. Отдельный интеграционный прогон покажет, умеют ли выбранные модель и маршрут API начинать несколько вызовов. Один вызов сам по себе не доказывает отсутствия такой поддержки.

В SmartAIPack полный цикл закреплён автоматической проверкой перед выпуском: тест требует ровно один tool_call, возвращает результат инструмента и проверяет второй ответ модели. Это глубже поверхностной проверки работоспособности, но область остаётся ограниченной: автоматическая проверка охватывает заданные идентификатор модели, маршрут API и схему. Ваш lookup_order, версия библиотеки и обработчик истории в неё не входят.

После этого generate_reply() должен пройти в трёх режимах: обычный JSON, последовательные события SSE и полный цикл вызова инструмента. Только теперь можно переходить к ошибкам и повторным попыткам.

Шаг 6. Составьте карту ошибок до настройки повторных попыток

Не переносите обработчик ошибок со старого маршрута API вслепую. Вызовите как минимум пять контролируемых сбоев: отсутствующий API‑ключ, неизвестную модель, недопустимый параметр, превышение клиентского тайм‑аута и временную ошибку вышестоящего сервиса, если её можно безопасно смоделировать на испытательном стенде. В отчёте о тесте отмечайте наличие или отсутствие каждого элемента: статуса HTTP, тела JSON, заголовков, идентификатора запроса и исключения библиотеки. При тайм‑ауте или сетевом сбое ответа может не быть вовсе — это отдельный класс, а не ошибка HTTP с пустым телом.

В API OpenAI таким идентификатором служит x-request-id; официальная документация рекомендует сохранять его на стороне приложения для диагностики. Структурированная ошибка предварительного маршрутизатора SmartAIPack содержит code, message и request_id. Не считайте эти формы взаимозаменяемыми: приведите их к единому виду в своём адаптере ошибок, если приложение ожидает одну структуру данных.

Только после классификации включайте повторы. Ошибки проверки данных, неизвестная модель и недействительный API‑ключ требуют исправления запроса или конфигурации. Для временного ответа 429, 502, 503, 504 или сетевого сбоя повтор иногда уместен, но решение зависит от фактического кода и того, началась ли выдача ответа. Если есть Retry-After, соблюдайте его; иначе ограничьте попытки и применяйте экспоненциально растущую задержку со случайным разбросом. Не перезапускайте поток после отправки части текста: пользователь получит дубли. Внешняя функция без ключа идемпотентности при новом вызове может дважды изменить данные.

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

Матрица допуска вместо обещания совместимости

В рабочем документе добавьте столбцы «ожидалось», «фактически» и ссылку на обезличенный отчёт о тесте.

Контракт

Минимальный тест

Условие допуска

Базовый адрес

фактический путь запроса

один правильный /v1/...

Авторизация

действующий, отсутствующий и неверный ключ

ошибки различимы, ключ не раскрывается

Модели

каталог и неверный маршрут API

идентификатор и протокол подтверждены

Ответ

контрольный запрос с рабочими полями

ответ разбирается, usage доступно в нужной форме

Потоковая выдача

длинный ответ и разрыв

фрагменты приходят по мере генерации, обрыв виден

Инструменты

полный цикл

схема, идентификатор вызова и результат инструмента сохранены в истории приложения

Ошибки

ответы 4xx, тайм‑аут и временные 5xx

есть приведение к единому виду и ограниченные повторы

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

Даже при первом исходе не переключайте 100% трафика одним выпуском. Вынесите выбор профиля подключения в конфигурацию, направьте на новый маршрут малую долю обычных запросов и отдельно включите в автоматическую проверку редкие критичные ветки — потоковую выдачу и инструменты. Сравните долю ошибок, задержку, стоимость и правила бизнес‑логики.

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

Откройте smartaipack, сверьте актуальный базовый адрес и доступные протоколы, затем прогоните через изолированный клиент свой набор контрольных запросов. Решение о переносе принимает матрица тестов.

Комментарии (0)