Responses API Миграция OpenAI API Chat Completions

Миграция с Chat Completions на Responses API

Пошаговое руководство по переносу кода OpenAI Chat Completions на Responses API: элементы входа, разбор вывода, состояние, инструменты, стриминг и план отката.

AveMujica API 7 мин чтения

Миграция с Chat Completions на Responses API — это в основном работа по переименованию и изменению формы: POST /v1/chat/completions превращается в POST /v1/responses, массив messages становится input, системная подсказка становится instructions, а вы читаете response.output_text вместо completion.choices[0].message.content. Спотыкаются же люди на тех частях, где меняется поведение, а не название, — на элементах вывода, состоянии, привязке инструментов и полях структурированного вывода. Это руководство ведёт вас через миграцию в том порядке, в котором вам действительно стоит её выполнять, с планом отката, чтобы можно было переносить по одной конечной точке за раз, а не переписывать всё разом.

Вы не обязаны мигрировать. Chat Completions по-прежнему поддерживается. Мигрируйте, когда вам нужна непрерывность рассуждений, управляемое сервером состояние диалога, встроенные инструменты или более чистый типизированный вывод, который выгоден агентным циклам. Если вы ещё не определились, сначала прочитайте Responses API против Chat Completions.

Шаг 0: установите базовую линию, с которой можно сравнивать

Прежде чем трогать код, зафиксируйте, как выглядит «правильно» на вашей текущей конечной точке. Отправьте несколько репрезентативных подсказок через Chat Completions и запишите текст ответа, расход токенов и задержку. Пропустите их через свой шлюз, чтобы трассы попали в его журналы использования. Позже вы прогоните те же самые подсказки через Responses и сравните. Миграции, пропускающие этот шаг, превращаются в гадание, как только вывод начинает отличаться.

Шаг 1: переименуйте конечную точку и перенесите системную подсказку

Наименьшая работающая миграция — это три правки. Направьте SDK на метод Responses, перенесите любое сообщение system в instructions и сверните единственный ход пользователя в input.

До:

completion = client.chat.completions.create(
    model="gpt-5.6",
    messages=[
        {"role": "system", "content": "You are a release-notes summarizer."},
        {"role": "user", "content": "Summarize the changelog below:\n" + changelog},
    ],
)
text = completion.choices[0].message.content

После:

response = client.responses.create(
    model="gpt-5.6",
    instructions="You are a release-notes summarizer.",
    input="Summarize the changelog below:\n" + changelog,
)
text = response.output_text

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

Шаг 2: разбирайте элементы вывода, а не choices[0]

Это тот шаг, который меняет поведение. Chat Completions давал вам choices, и индекс 0 почти всегда был вашим ответом. Responses возвращает массив output из типизированных элементов — message, reasoning и function_call, — и ответ не гарантированно идёт первым. Модель рассуждений может выдать элемент reasoning перед message, а ход инструмента может выдать function_call вообще без текста. (function_call_output, с помощью которого вы возвращаете результат инструмента, — это создаваемый вами элемент входа, а не один из возвращаемых элементов; не смешивайте эти две стороны.)

Удобный аксессор response.output_text обрабатывает распространённый случай «просто дай мне текст». Когда нужен больший контроль, итерируйте по type:

answer, tool_calls = None, []
for item in response.output:
    if item.type == "message":
        answer = item.content[0].text
    elif item.type == "function_call":
        tool_calls.append(item)

Заодно обновите учёт расхода: prompt_tokens/completion_tokens становятся input_tokens/output_tokens в объекте usage. Любой код биллинга или дашборда, читающий число токенов, должен во время поэтапного развёртывания обрабатывать оба имени полей.

Шаг 3: решите, как вы управляете состоянием диалога

Chat Completions не имеет состояния — вы пересылаете полную историю при каждом вызове. Responses по умолчанию сохраняются и возвращают id, от которого можно строить цепочку. Выберите одну из трёх стратегий и применяйте её последовательно:

СтратегияКакКогда использовать
previous_response_idОтправлять id предыдущего ответа в следующем вызовеВы хотите, чтобы историю хранил сервер
Ручные элементыПередавать прежние элементы output обратно в inputВы хотите полный контроль над тем, что пересылается
Без состоянияstore: false и пересылать историю самостоятельноНулевое хранение, или вы уже управляете хранилищем

Два факта, которые стоит учесть при планировании. Во-первых, instructions не переносятся через previous_response_id, поэтому пересылайте их каждый ход. Во-вторых, построение цепочки не снижает ваш счёт: все предыдущие входные токены в цепочке тарифицируются как входные токены. Цепочка экономит трафик и шаблонный код, но не деньги. Для реального снижения стоимости контекста сочетайте миграцию с кэшированием подсказок.

Шаг 4: перенесите вызовы инструментов и структурированный вывод

Вызов функций переживает миграцию, но проводка меняется. В Chat Completions вы добавляли сообщение role: "tool" с tool_call_id. В Responses вы отправляете элемент function_call_output, связанный через call_id:

client.responses.create(
    model="gpt-5.6",
    previous_response_id=response.id,
    input=[{
        "type": "function_call_output",
        "call_id": call.call_id,
        "output": tool_result,
    }],
)

Отправка результата без соответствующего call_id — одна из самых частых ошибок миграции, поэтому проверяйте это ассертами в тестах. Структурированный вывод тоже переезжает: response_format становится text.format, а в схеме нужно явно задать strict: true — убедитесь, что ваша JSON Schema по-прежнему проходит проверку по строгому подмножеству. (Не переносите сюда поведение определений инструментов, где пропуск strict допускает деградацию к нестрогому режиму; для структурированного вывода задавайте режим явно.) Подробности — в структурированном выводе с JSON Schema, а переносимые определения — в переносимом вызове инструментов между провайдерами.

Шаг 5: обновите потребителей стриминга

Если вы стримите, транспорт остаётся Server-Sent Events, но события меняются. Chat Completions стримил объекты chat.completion.chunk с choices[].delta и заканчивал data: [DONE]. Responses стримит семантические события, такие как response.created, response.output_text.delta и response.completed. Семантические события проще маршрутизировать, потому что каждое называет само себя, но ваш существующий парсер [DONE]-и-delta не поймёт их без изменений. Шаблоны разбора для обоих — в стриминге ответов LLM через SSE.

Шаг 6: разворачивайте за флагом, держите откат

Не переключайте все вызовы разом. Поместите выбор конечной точки за флаг функции или маршрутизируйте его по модели на своём шлюзе, зарегистрируйте за ним вышестоящие узлы и отправьте небольшую долю трафика в Responses. Сравните зафиксированную на Шаге 0 базовую линию с новыми трассами: те же ответы в пределах допуска, разумное число токенов и приемлемая задержка. Если что-то деградировало, откат — это переключение флага обратно на Chat Completions, а не повторное развёртывание, потому что различие на стороне клиента изолировано. Когда доля выглядит здоровой, расширяйте её.

Что миграция не меняет

Полезно знать, где можно расслабиться. Несколько вещей переносятся без изменений, так что переписывать их не стоит:

  • Аутентификация. Тот же API-ключ и заголовок Authorization: Bearer работают; меняется только путь.
  • Идентификаторы моделей. Модель вроде gpt-5.6 называется одинаково на обеих конечных точках — вы меняете обёртку запроса, а не модель.
  • Содержимое подсказок. Ваши фактические инструкции и пользовательский текст идентичны; они просто переезжают из записей messages в instructions и input.
  • Температура и ручки сэмплирования. Обычные параметры генерации сохраняют свои имена и значения.
  • Транспорт для стриминга. Обе конечные точки стримят через Server-Sent Events, так что ваш SSE-ридер остаётся; меняются лишь формы событий, которые он разбирает.

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

Как читать распространённые ошибки миграции

Большинство багов миграции дают горстку узнаваемых симптомов. Пустой или отсутствующий текст обычно означает, что вы прочитали output[0] и попали на элемент reasoning или function_call вместо message — итерируйте по type или используйте output_text. Связанный со схемой 400 на структурированном выводе обычно означает, что вы оставили response_format на месте, вместо того чтобы перейти на text.format. Цикл инструментов, который зависает или падает, обычно означает, что function_call_output ушёл без соответствующего call_id. А диалог, который «забывает» свою роль или тон между ходами, обычно означает, что вы понадеялись на сохранение instructions через previous_response_id — оно не сохраняется, так что пересылайте их. Держа этот короткий список рядом со своим diff, вы превращаете сбивающие с толку сбои в быстрые справки.

Чек-лист миграции

  • Конечная точка изменена с /v1/chat/completions на /v1/responses.
  • Сообщение system перенесено в instructions и пересылается на каждом ходе цепочки.
  • Вывод читается через output_text или итерацией элементов output по type.
  • Учёт токенов обрабатывает input_tokens/output_tokens.
  • Выбрана стратегия состояния: previous_response_id, ручные элементы или store: false.
  • Результаты инструментов отправляются как function_call_output с соответствующим call_id.
  • response_format заменён на text.format; схема перепроверена.
  • Потребители стриминга обновлены под семантические события.
  • Развёртывание закрыто флагом с откатом одним переключением.

Часто задаваемые вопросы

Обязан ли я мигрировать до какого-либо дедлайна?

Отключение Chat Completions не запланировано, поэтому для него нет принудительного дедлайна. Конечная точка, которую OpenAI пометила как устаревшую, — это Assistants API (объявлена устаревшей 2025-08-26, с отключением, анонсированным на 2026-08-26), а это другая поверхность. Мигрируйте на Responses ради его возможностей, а не из-за спешки.

После миграции мой ассистент иногда возвращает пустой текст. Почему?

Вы почти наверняка читаете output[0] и попадаете на элемент reasoning или function_call вместо message. Итерируйте output по type или используйте response.output_text, который за вас пропускает нетекстовые элементы.

Могу ли я и дальше пересылать полную историю вместо использования previous_response_id?

Да. Установите store: false и передавайте свою историю как элементы input ровно так же, как вы делали это с messages. Это оставляет конечную точку без состояния и является правильным выбором для требований нулевого хранения.

Сначала проверьте на одной конечной точке

Возьмите одну некритичную конечную точку, примените только Шаги 1–2 (переименование плюс разбор вывода) и прогоните через неё базовые подсказки Шага 0. Если текст совпадает, а число токенов выглядит верно в журналах использования вашего шлюза, вы доказали механику миграции на собственном трафике, прежде чем трогать состояние, инструменты или стриминг.

Ссылки