This article is published in English.
Understanding Items vs Messages in OpenAI's Responses API
Explains how OpenAI's Responses API reorganizes model outputs into items instead of messages, and why that shift matters for tool-calling and agentic workflows.
Chat Completions: Built Around Messages
Chat Completions has long been the go-to format for talking to LLMs. A typical call looks like this:
response = client.chat.completions.create(
model="...",
messages=[
{
"role": "user",
"content": "Explain RAG"
}
]
)
You send a list of messages, and the model hands back the next assistant message. Straightforward enough. The Responses API, on the other hand, looks a bit different from the start.
response = client.responses.create(
model="...",
input="Explain RAG"
)
On the surface, this might just seem like a different endpoint where input takes the place of messages. But the real distinction lies in the underlying abstraction each API uses. Chat Completions revolves around messages, whereas the Responses API is organized around items and responses. Once you bring tools and agents into the picture, that distinction starts to matter a lot more.
A conversation under Chat Completions is just an array of message objects.
messages = [
{
"role": "system",
"content": "Act as a helpful AI assistant."
},
{
"role": "user",
"content": "What is a vector database?"
}
]
Every message carries a role and some content.
The typical roles you'll see are:
systemuserassistant
You can picture the flow like this:
Messages (list)-> Model -> Assistant Message
The model takes in the conversation so far and produces the next assistant message. A minimal conversational loop looks like this:
# Human message appended to the messages list
messages.append({
"role": "user",
"content": user_input
})
response = client.chat.completions.create(
model="...",
messages=messages
)
# AI message appended to the messages list
messages.append(
response.choices[0].message
)
Your application is responsible for keeping track of message history, whether in memory, in a database, or however you choose. Each new user message gets appended to that list, the full list is sent to the model, and the model's reply is appended back in turn. This pattern maps naturally onto how chat interfaces work.
User Message (str)->
Message History (list[dict])->
Model (llm)->
Assistant Message (str)->
Message History (list[dict])
For plain text generation and typical chat-style use cases, this setup is perfectly adequate. But its limits start to show once an LLM-powered application needs to do more than converse.
LLM Applications = Not Just Chat Applications
Take a request like this:
Look up recent developments in AI, put together a summary of the ones that matter, and send that summary to my inbox.
Fulfilling that request means the model has to trigger a web search and hook into an email service.
Suddenly the execution path involves more than a simple user-message-then-assistant-message exchange.
User Request ->
Model ->
Web Search ->
Search Results ->
Summarize (Model)->
Send Mail ->
Final Response
More elaborate applications may rely on several different tools chained together.
At this point, the model is an active participant in a broader execution pipeline, not just a text generator. Some of what it outputs is never meant to reach the end user — it exists purely for internal processing. The model might invoke a tool, and the result of that tool call may need further handling, possibly triggering another tool call. Only once all of that resolves does the model produce a final answer, and even that answer might not take the shape of a text message.
The flow can no longer be reduced to:
Messages (list)-> Model -> Assistant Message
There are now intermediate outputs and application-level actions that need to persist as part of the context, and that's precisely where an abstraction built solely around messages starts to feel too narrow.
Every Model Output != Message
Once a model is wired up to tools, much of what it emits is a function call rather than conversational text.
Take this as an example:
Function Call
Name: get_weather
Arguments:
{
"city": "Bengaluru"
}
This isn't something meant for the user to read — it's an instruction addressed to your application. Your code runs the corresponding function and feeds the result back to the model, and that result, again, may or may not be user-facing.
In practice, a model can produce at least two distinct kinds of output during a run:
- Function Call
- Message
Lumping both of these together under the single label of "assistant message" doesn't reflect what's actually happening during execution. This mismatch is the core design problem that the Responses API sets out to fix.
Responses API: a Different Abstraction
Rather than being organized around message exchange, the Responses API is built around the concept of a response, which can bundle together multiple output items.
response = client.responses.create(
model="...",
input="Explain LangGraph"
)
print(response.output)
# response.output is a list of output items.
For a plain text prompt, that output might just be a single message. But for anything involving agents or tools, a response can include several different item types. A simplified view of that structure looks like this:
Response
| Reasoning Item
| Function Call Item
| Message Item
In this model, a message becomes just one of several possible output types, not the entirety of what a response represents.
Difference:
Difference:
Messages (list)-> Model -> Assistant Message
Chat Completions
Input -> Model -> Response
Response:
| Output Item
| Output Item
| Output Item
Responses API
With Chat Completions, a conversation is what gets modeled, whereas the Responses API frames model execution as a response made up of output items. For a simple text completion, this distinction barely matters. It becomes significant once tools, reasoning-capable models, or agentic workflows enter the picture.
Messages vs Items
The real difference between the two APIs shows up in how each one structures its response.
In Chat Completions, everything centers on the message.
print(response.choices[0].message.content)
# The generated text is inside the assistant message.
# response
# | choices
# | message
# | content
The Responses API organizes its output differently.
print(response.output)
# A simplified structure:
# response
# | output
# | reasoning
# | function_call
# | message
# The important difference is that output is not a list of messages,
# but output items.
A message is just one kind of item; a function call is another kind; and models that support reasoning may also emit reasoning items. This reshapes how you think about what a model actually returns.
Chat Completions
Model Output = Assistant Message
Responses API
Model Output = List of Output Items
- The structure which is useful for tool calling.
Tool Calls as Output Items
Take a simple function that reports the weather (the classic example used everywhere).
def get_weather(city: str):
return f"The weather in {city} is 28°C"
# The weather is ofcoure hardcoded.
You can expose this function to the model as a tool definition.
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string"
}
},
"required": ["city"]
}
}
]
That tool definition gets included with your request.
response = client.responses.create(
model="...",
tools=tools,
input="What is the weather in Bengaluru?"
)
From here, the model has two possible paths forward.
Option 1: Generate a message
Option 2: Call get_weather
Since the function's actual weather value isn't baked into its definition, the model needs live data, so it emits a function call item instead of answering directly.
Function Call Item
name: get_weather
arguments:
{
"city": "Bengaluru"
}
You can locate this function call by looping through the response's output items.
for item in response.output:
if item.type == "function_call":
print(item.name)
print(item.arguments)
Running that gives you:
get_weather
{"city":"Bengaluru"}
At this point the model hasn't produced a final answer yet, it has only asked for an action to be carried out; running the function itself is on your application.
result = get_weather("Bengaluru")
That result then needs to go back to the model.
Function Call Output
You represent a tool's result using an item of type function_call_output.
tool_output = {
"type": "function_call_output",
"call_id": item.call_id,
"output": result
}
The call_id field ties this output back to the specific function call that requested it.
Function Call
| call_id: call_123
Application Executes Tool
Function Call Output
| call_id: call_123
This matching becomes essential once several tools are invoked at once, for instance a query asking for the weather in two different cities simultaneously.
The Basic Tool Execution Loop
Applications built around tools typically follow a repeating cycle.
User Input ->
Model ->
Response Output Items ->
Check for Function Calls ->
Execute Functions ->
Create Function Call Outputs ->
Model ->
Final Response
Here's roughly what that looks like in code.
response = client.responses.create(
model="...",
input=user_input,
tools=tools
)
while True:
function_calls = [
item
for item in response.output
if item.type == "function_call"
]
if not function_calls:
break
tool_outputs = []
for call in function_calls:
result = execute_tool(
call.name,
call.arguments
)
tool_outputs.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": result
})
response = client.responses.create(
model="...",
previous_response_id=response.id,
input=tool_outputs,
tools=tools
)
This cycle keeps repeating as long as the model keeps returning function calls. Once it stops requesting tool calls, the response holds the model's final output.
Model ->
Function Call ->
Tool Result ->
Model ->
Function Call ->
Tool Result ->
Model ->
Message
This loop is the foundation underlying most tool-calling agent implementations.
Conclusion
The Responses API isn't simply a rebranded interface over Chat Completions. It offers a structure that more accurately reflects how contemporary LLM applications operate when tools, reasoning, and multiple output types are involved. Chat Completions remains a solid choice for straightforward conversational use cases. But once your workflow starts leaning agentic, the Responses API is the better fit. Messages handle conversation; items handle execution. That's the whole idea.