Responses API Chat Completions OpenAI API Проектирование API

Responses API против Chat Completions: на чём строить

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

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

Если вы начинаете новую интеграцию сегодня, стройте её на Responses API (POST /v1/responses), а Chat Completions (POST /v1/chat/completions) оставьте коду, который уже существует и работает. Оба эндпоинта решают одну и ту же ключевую задачу — отправить промпт, получить ответ модели, — но моделируют разговор по-разному. Chat Completions трактует ход как плоский список messages и возвращает массив choices. Responses трактует ход как список типизированных элементов input и возвращает типизированный массив output, в котором бок о бок могут находиться сообщения, рассуждение и вызовы инструментов. Именно это структурное различие и есть вся суть, и оно определяет, какой эндпоинт подойдёт вашей нагрузке.

Эта статья сравнивает оба эндпоинта по структуре запроса, структуре вывода, состоянию, рассуждению и инструментам, а затем даёт правило принятия решения, которое можно применять без догадок. Она ничего не объявляет устаревшим: Chat Completions по-прежнему поддерживается и остаётся самой переносимой формой между провайдерами.

Структура запроса: messages или input

Chat Completions ожидает массив messages, где у каждого элемента есть role (system, user, assistant, tool) и content. Всё, что относится к ходу, — инструкции, история, результаты инструментов, — склеено в этот единственный список.

from openai import OpenAI
client = OpenAI(base_url="https://your-gateway.example.com/v1", api_key="AVE_GATEWAY_API_KEY")

completion = client.chat.completions.create(
    model="gpt-5.6",
    messages=[
        {"role": "system", "content": "You are a terse assistant."},
        {"role": "user", "content": "Name three uses for an API gateway."},
    ],
)
print(completion.choices[0].message.content)

Responses разделяет эти обязанности. Указания верхнего уровня переходят в instructions, а содержимое хода — в input, который принимает либо обычную строку, либо массив типизированных элементов.

response = client.responses.create(
    model="gpt-5.6",
    instructions="You are a terse assistant.",
    input="Name three uses for an API gateway.",
)
print(response.output_text)

Две вещи стоит заметить сразу. Во-первых, response.output_text — это удобный аксессор, который сам конкатенирует текстовый вывод, заменяя путь completion.choices[0].message.content. Во-вторых, instructions не «прилипают» между связанными ходами: если вы продолжаете разговор через previous_response_id, instructions предыдущего ответа не переносятся, поэтому вы отправляете их заново на каждом ходу.

Структура вывода: choices или элементы output

Chat Completions возвращает массив choices. С параметром n вы могли запросить несколько кандидатов-завершений; каждое было отдельным choice. Responses убирает n и возвращает одну генерацию, но эта генерация — массив output из элементов, у каждого свой type. Элементы, которые возвращает модель, — это:

  • message — текст ассистента (и любые отказы)
  • reasoning — сводки рассуждения или зашифрованные токены рассуждения
  • function_call — вызов инструмента, который модель хочет, чтобы вы выполнили

Обратите внимание, чего здесь нет: function_call_output — не элемент, возвращаемый моделью. Это элемент входа, который вы конструируете и отправляете обратно, чтобы передать модели результат инструмента, связанный по call_id. Разделение этих двух списков — элементы вывода, которые вы читаете, и элементы входа, которые вы пишете, — снимает частый источник путаницы, потому что вызов и его результат находятся по разные стороны обмена.

Трактовать каждую запись output как сообщение — самая частая ошибка миграции. Модель рассуждения может выдать элемент reasoning перед message, а ход с использованием инструмента может выдать function_call вообще без видимого пользователю текста. Обходите output по type, а не предполагайте, что индекс 0 — это ваш ответ:

for item in response.output:
    if item.type == "message":
        print(item.content[0].text)
    elif item.type == "function_call":
        print("tool wanted:", item.name, item.arguments)

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

Состояние: история на стороне клиента или потоки, хранимые сервером

С Chat Completions вы владеете разговором: вы пересылаете всю историю messages при каждом вызове, и запрос несёт всё, что нужно модели. Эта управляемая клиентом форма проста в осмыслении и легко маршрутизируется через любой прокси — во многом поэтому она стала лингва франка отрасли. Сохраняет ли OpenAI копию обмена — отдельный вопрос от самого эндпоинта: это определяется настройкой store и умолчаниями вашего аккаунта (которые могут включать хранение для логирования), а не тем, что Chat Completions якобы «без состояния» по своей природе. Если вам нужно гарантировать, что вверх по потоку ничего не сохраняется, отправляйте store: false явно, а не полагайтесь на то, что эндпоинт ничего не хранит по умолчанию.

Responses сохраняются по умолчанию. Каждый вызов возвращает id, и вы можете продолжить цепочку, передав previous_response_id в следующем запросе, вместо пересылки истории. Управлять состоянием можно тремя способами:

  1. previous_response_id, чтобы сцепиться с сохранённым ответом,
  2. вручную передать прежние элементы output обратно в input,
  3. Conversations API для более долгоживущих цепочек.

Хранение — удобство, а не обязанность. Задайте store: false, чтобы отказаться от него, — именно этого вы хотите для сценариев с нулевым удержанием данных. Одна деталь тарификации важна для планирования: даже когда вы сцепляете через previous_response_id, все предыдущие входные токены в цепочке тарифицируются как входные токены. Сцепление избавляет вас от повторной пересылки байтов по сети; оно не делает прежний контекст бесплатным.

Рассуждение: где Responses вырывается вперёд

Самая явная причина предпочесть Responses — модели рассуждения. Поскольку рассуждение — полноправный тип элемента output, API может сохранять состояние рассуждения между ходами, а не отбрасывать его между вызовами. Для конфигураций без состояния вы можете запросить зашифрованное рассуждение, отправив store: false вместе с include: ["reasoning.encrypted_content"]; API возвращает зашифрованные токены рассуждения, которые вы возвращаете на следующем ходу, а содержимое расшифровывается в памяти и затем отбрасывается. У Chat Completions нет эквивалентного типизированного слота для рассуждения, поэтому модель рассуждения теряет эту непрерывность между ходами.

Если ваш продукт зависит от многошагового рассуждения, циклов инструментов под управлением модели рассуждения или долгих агентных прогонов, Responses — эндпоинт, спроектированный именно для этого. Более подробный разбор задействованных параметров — в статье параметры API моделей рассуждения.

Инструменты и структурированный вывод

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

АспектChat CompletionsResponses
Эндпоинт/v1/chat/completions/v1/responses
Содержимое ходаmessages[]input (строка или элементы)
Системный промптсообщение systeminstructions
Текст результатаchoices[0].message.contentoutput_text
Структурированный выводresponse_formattext.format
Связка результата инструментаtool_call_id в сообщении toolfunction_call_output с call_id
Несколько кандидатовnубрано (одна генерация)
Нативные инструментыручная разводкаtools: [{ "type": "web_search" }]

Замену response_format на text.format легко упустить, и она порождает сбивающие с толку ошибки, если вы отправите не то поле. Для Structured Outputs задавайте strict: true явно в схеме, чтобы модель была ею связана. (Отдельный откат при пропуске strict — попытка строгого применения с деградацией до нестрогого режима при несовместимой схеме — относится к определениям функций/инструментов Responses, а не к структурированному выводу text.format; не путайте эти два случая.) Если вы полагаетесь на вывод по JSON Schema, прочтите структурированный вывод с JSON Schema, прежде чем переносить код валидации.

Чем цикл инструментов отличается на практике

Круг обмена при вызове инструмента — то место, где две формы ощущаются наиболее по-разному, поэтому их стоит увидеть рядом. В Chat Completions модель возвращает сообщение ассистента, чьи tool_calls вы выполняете, а затем добавляете сообщение с role: "tool" и соответствующим tool_call_id:

# Chat Completions: append a tool result as a message
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})

В Responses модель выдаёт элемент function_call, а вы возвращаете результат как элемент function_call_output, связанный через call_id, — а не как сообщение:

# Responses: append a typed function_call_output item
client.responses.create(
    model="gpt-5.6",
    previous_response_id=response.id,
    input=[{
        "type": "function_call_output",
        "call_id": call.call_id,
        "output": result,
    }],
)

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

Правило принятия решения, которое можно применить

Мучиться над этим не нужно. Пользуйтесь таблицей:

СитуацияЭндпоинт
Новая интеграция, особенно агенты или модели рассужденияResponses
Существующий рабочий код Chat Completions без новых требованийОставить Chat Completions
Вам нужна максимальная переносимость между многими провайдерами уже сегодняChat Completions (широко воспроизводимая форма)
Вам нужно управляемое сервером состояние разговора или нативные инструментыResponses
Вам нужна непрерывность зашифрованного рассуждения при нулевом удержанииResponses с store: false + зашифрованное рассуждение

Шлюз удешевляет этот выбор с обеих сторон. За одним base URL вы можете направлять трафик Chat Completions и трафик Responses к одним и тем же провайдерам, регистрировать провайдеров один раз и публиковать на открытой странице тарифов, что поддерживает и сколько стоит каждая модель. Если позже вы мигрируете эндпоинт, изменение на клиенте будет небольшим, а маршрутизация останется прежней. Пошаговый путь разобран в статье миграция с Chat Completions на Responses API.

Стриминг работает на обоих, но с разными событиями

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

Частые вопросы

Устарел ли Chat Completions теперь, когда есть Responses?

Нет. Chat Completions остаётся поддерживаемым эндпоинтом и по-прежнему самой широко воспроизводимой формой запроса среди не-OpenAI провайдеров. К отключению OpenAI пометила Assistants API, а не Chat Completions. Стройте новую агентную работу на Responses из-за его структурированных элементов вывода и поддержки рассуждения, а не потому, что Chat Completions куда-то исчезает.

Могу ли я получить несколько кандидатов-ответов из Responses API, как через n в Chat Completions?

Не одним параметром n — Responses возвращает одну генерацию за вызов. Если нужно несколько кандидатов, отправьте несколько запросов. Это настоящее изменение поведения, которое стоит учесть, если ваш код Chat Completions полагался на n > 1.

Снижает ли previous_response_id мой счёт за токены?

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

На какой эндпоинт стоит стандартизироваться мультипровайдерной конфигурации?

Если вы маршрутизируете ко многим провайдерам, Chat Completions сегодня по-прежнему самый переносимый общий знаменатель, тогда как Responses — более богатая нативная для OpenAI поверхность. Шлюз позволяет держать оба: выставлять каждому клиенту ту форму, на которой он уже говорит, и переводить по каждому каналу.

Прогоните оба через один рабочий процесс

Выберите один малорисковый рабочий процесс и прогоните его через свой шлюз дважды — один раз как вызов Chat Completions и один раз как вызов Responses, — а затем сравните форму output/choices, учёт токенов и задержку в журналах использования вашего шлюза. Одно такое сравнение бок о бок скажет вам о том, какой эндпоинт подходит вашему коду, больше, чем любая выжимка из спецификации.

Источники