У вас уже есть серверная часть на клиентской библиотеке OpenAI. Новый поставщик предлагает OpenAI‑совместимый API и показывает знакомый пример: другой base_url, другой ключ, тот же вызов chat.completions.create(). На тесте «ответь одним словом» всё работает. Можно переносить боевой трафик?
Пока нет. Такой запрос подтверждает, что клиент подключился, прошёл авторизацию и получил один успешный ответ. Он ничего не говорит о потоковой выдаче, вызовах инструментов, структуре ошибок и даже о том, как выбранная модель обработает параметры рабочего запроса.
Поэтому совместимость полезнее рассматривать не как свойство сервиса, а как набор контрактов между вашим приложением и API. До миграции нужно проверить семь вещей:
какой HTTP‑маршрут формирует библиотека из
base_url;как передаётся API‑ключ;
какие идентификаторы моделей доступны на нужном маршруте API;
совпадают ли значимые поля запроса и ответа;
получает ли приложение события SSE по мере генерации, не дожидаясь всего ответа и не теряя фрагменты потока;
проходит ли полный цикл вызова инструмента;
можно ли программно различать ошибки и безопасно повторять запросы.
Эта карта относится и к единому 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. Затем выполните весь цикл:
отправьте сообщения и описание инструмента;
получите имя функции, идентификатор вызова и аргументы в JSON;
проверьте аргументы по схеме и вызовите тестовую реализацию в приложении;
добавьте исходное сообщение
assistantсtool_callsи сообщениеtoolс тем же идентификатором вызова;отправьте историю повторно и получите финальный текст.
На каждом шаге проверяйте форму данных, а не только код 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, соблюдайте его; иначе ограничьте попытки и применяйте экспоненциально растущую задержку со случайным разбросом. Не перезапускайте поток после отправки части текста: пользователь получит дубли. Внешняя функция без ключа идемпотентности при новом вызове может дважды изменить данные.
Отдельно проверьте, не сложились ли повторные попытки библиотеки, вашего приложения и шлюза. Три независимых уровня по две попытки способны превратить один пользовательский запрос в несколько обращений к модели и непредсказуемую задержку.
Матрица допуска вместо обещания совместимости
В рабочем документе добавьте столбцы «ожидалось», «фактически» и ссылку на обезличенный отчёт о тесте.
Контракт |
Минимальный тест |
Условие допуска |
|---|---|---|
Базовый адрес |
фактический путь запроса |
один правильный |
Авторизация |
действующий, отсутствующий и неверный ключ |
ошибки различимы, ключ не раскрывается |
Модели |
каталог и неверный маршрут API |
идентификатор и протокол подтверждены |
Ответ |
контрольный запрос с рабочими полями |
ответ разбирается, |
Потоковая выдача |
длинный ответ и разрыв |
фрагменты приходят по мере генерации, обрыв виден |
Инструменты |
полный цикл |
схема, идентификатор вызова и результат инструмента сохранены в истории приложения |
Ошибки |
ответы 4xx, тайм‑аут и временные 5xx |
есть приведение к единому виду и ограниченные повторы |
Возможны три исхода: все используемые контракты совпали и достаточно переключаемого профиля подключения; нужен тонкий адаптер для ошибок, событий или названий моделей; критичный сценарий не поддержан и перенос трафика блокируется.
Даже при первом исходе не переключайте 100% трафика одним выпуском. Вынесите выбор профиля подключения в конфигурацию, направьте на новый маршрут малую долю обычных запросов и отдельно включите в автоматическую проверку редкие критичные ветки — потоковую выдачу и инструменты. Сравните долю ошибок, задержку, стоимость и правила бизнес‑логики.
Откат должен целиком возвращать прежний рабочий профиль: базовый адрес, идентификатор секрета, идентификатор модели, параметры и нужную версию адаптера. Тогда смена маршрута не требует отката приложения и не оставляет половину новой конфигурации со старым адресом.
Откройте smartaipack, сверьте актуальный базовый адрес и доступные протоколы, затем прогоните через изолированный клиент свой набор контрольных запросов. Решение о переносе принимает матрица тестов.