OpenAI Responses API: Building Agents with the Unified AI Interface

The Assistants API sunsets on August 26, 2026 [1]. If you’re still running agents on assistants.create() and threads.runs.create(), you have weeks — not months — to migrate. The replacement is the Responses API, OpenAI’s unified primitive for building agents that combines the simplicity of Chat Completions with the stateful tool orchestration of the old Assistants API.
This guide walks through everything you need: core concepts, built-in tools, custom function calling, multi-step workflows, streaming, and a concrete migration path from Assistants to Responses.
Why the Responses API Exists
Before the Responses API, OpenAI had three separate APIs for agent-style applications:
| API | Purpose | Problem |
|---|---|---|
| Chat Completions | Single-turn text generation | No built-in tools, no state |
| Assistants API | Multi-turn agents with tools | Separate thread/run/message model, beta since 2023 |
| Batch API | Async processing | Different response format entirely |
The Responses API unifies these into a single endpoint: POST /v1/responses [2]. One request shape, one response shape, built-in tools as first-class citizens, and a state management model that doesn’t require a separate thread object.
Core concepts
- Response — A single interaction: input → model processing (with tool calls) → output. Everything is a response.
- Input — A list of items (user messages, files, previous response IDs) that the model processes.
- Output items — The model’s response, which can include text, tool calls, file references, and web citations.
- Previous response ID — Chain responses together for multi-turn conversations without managing a separate thread object.
- Built-in tools —
web_search,file_search,code_interpreter,computer_use,image_generation— configured directly in the tools array, no separate assistant object needed.
Step 1: Your First Responses API Agent
The Responses API uses the same openai Python package you already have. Install or upgrade to the latest version:
pip install --upgrade openai
A minimal agent with web search:
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-4o",
tools=[{"type": "web_search"}],
input="What are the latest developments in AI agent frameworks as of mid-2026?",
)
print(response.output_text)
That’s it. No assistant object, no thread, no run. The model decides whether to invoke web_search, processes the results, and returns a grounded answer. The response.output_text field contains the final text output after all tool calls have resolved [3].
Compare this to the old Assistants API equivalent:
# Old way — Assistants API (deprecated)
assistant = client.beta.assistants.create(
name="Web Researcher",
tools=[{"type": "web_search"}],
model="gpt-4o",
)
thread = client.beta.threads.create()
client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="What are the latest developments in AI agent frameworks?"
)
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
)
Three objects instead of one, and no meaningful separation of concerns — the assistant object was just configuration that could have been inline.
Step 2: Built-in Tools Deep Dive
The Responses API ships with six built-in tools. Each tool is configured declaratively in the tools array and requires zero custom function code.
Web Search
Web search returns grounded answers with citations:
response = client.responses.create(
model="gpt-4o",
tools=[{
"type": "web_search",
"user_location": {"type": "approximate", "country": "US"},
}],
input="What is the current Fed interest rate and when was it last changed?",
)
# Access citations from the response
for output_item in response.output:
if output_item.type == "message":
for annotation in output_item.content[0].annotations:
if annotation.type == "url_citation":
print(f"Source: {annotation.url}")
Web search supports user location for localized results, and returns url_citation annotations so you can attribute every claim [4].
File Search
Attach files directly to the request — no need to upload to a separate vector store first:
from openai import OpenAI
client = OpenAI()
# Upload a file
file = client.files.create(
file=open("quarterly-report-2026-q2.pdf", "rb"),
purpose="assistants",
)
response = client.responses.create(
model="gpt-4o",
input=[
{"role": "user", "content": "What was the revenue growth in Q2?"},
],
tools=[{
"type": "file_search",
"vector_store_ids": [file.id], # or use a persistent vector store
"max_num_results": 5,
}],
)
File search uses the same underlying vector search as the old Assistants API but skips the assistant and thread abstraction. You control the number of results returned with max_num_results [5].
Code Interpreter
The model can write and execute Python in a sandboxed environment:
response = client.responses.create(
model="gpt-4o",
tools=[{"type": "code_interpreter"}],
input="""Create a visualization of the last 10 years of S&P 500 returns.
Use matplotlib and show the annual percentage change as a bar chart.""",
)
# Check for generated files
for output_item in response.output:
if output_item.type == "code_interpreter_call":
for result in output_item.results:
if hasattr(result, 'file'):
print(f"Generated file: {result.file.id}")
Code interpreter calls appear as code_interpreter_call output items with the code, results, and any generated files [6].
Image Generation
Generate images directly from the model without calling DALL-E separately:
response = client.responses.create(
model="gpt-4o",
tools=[{"type": "image_generation"}],
input="Generate a diagram showing how the Responses API routes tool calls.",
)
The generated image is returned as an output item with an image URL or file reference.
Step 3: Custom Function Calling
For tools not covered by the built-in set, register custom functions through the same tools array:
from openai import OpenAI
import json
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_stock_price",
"description": "Get the current stock price for a ticker symbol",
"parameters": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Stock ticker symbol (e.g., AAPL, NVDA)",
}
},
"required": ["ticker"],
},
}
]
response = client.responses.create(
model="gpt-4o",
tools=tools,
input="What is NVIDIA's current stock price?",
)
# Handle function calls
for output_item in response.output:
if output_item.type == "function_call":
name = output_item.name
args = json.loads(output_item.arguments)
print(f"Function called: {name}({args})")
# Execute the function
if name == "get_stock_price":
result = {"ticker": args["ticker"], "price": 142.50, "currency": "USD"}
# Submit the result back
response = client.responses.create(
model="gpt-4o",
input=[
{"role": "user", "content": "What is NVIDIA's current stock price?"},
output_item, # the function call
{
"type": "function_call_output",
"call_id": output_item.call_id,
"output": json.dumps(result),
},
],
)
print(response.output_text)
The function call lifecycle mirrors Chat Completions tool calling but uses explicit output item types (function_call and function_call_output) instead of the older tool_calls array [7]. This makes it cleaner to handle multiple parallel function calls and to interleave tool calls with other output types.
Parallel Function Calls
The model can invoke multiple functions in a single turn. Each one appears as a separate output item:
response = client.responses.create(
model="gpt-4o",
tools=tools,
input="Compare the stock prices of AAPL, NVDA, and MSFT.",
)
# Execute all function calls in parallel
results = []
for output_item in response.output:
if output_item.type == "function_call":
args = json.loads(output_item.arguments)
price = get_price(args["ticker"]) # your function
results.append({
"type": "function_call_output",
"call_id": output_item.call_id,
"output": json.dumps({"ticker": args["ticker"], "price": price}),
})
# Submit all results in one request
response = client.responses.create(
model="gpt-4o",
input=[
{"role": "user", "content": "Compare the stock prices of AAPL, NVDA, and MSFT."},
*response.output, # all function calls
*results, # all results
],
)
This parallel pattern is significantly simpler than the old Assistants API’s submit_tool_outputs flow, which required a separate REST call per batch.
Step 4: Multi-Step Workflows with previous_response_id
For multi-turn conversations, the Responses API uses previous_response_id instead of thread objects. Each response references the one before it:
# Turn 1
response_1 = client.responses.create(
model="gpt-4o",
tools=[{"type": "web_search"}],
input="Research the latest agent orchestration frameworks.",
)
# Turn 2 — chain from Turn 1
response_2 = client.responses.create(
model="gpt-4o",
previous_response_id=response_1.id,
input="Summarize the top three in a comparison table.",
)
# Turn 3 — chain from Turn 2
response_3 = client.responses.create(
model="gpt-4o",
previous_response_id=response_2.id,
input="Write a Python script that implements the pattern from the best one.",
)
OpenAI manages the context window automatically — old messages are pruned or summarized as needed. You don’t need to track token counts or implement sliding windows yourself [8].
This is the most significant architectural improvement over the Assistants API. The thread was an implicit state machine that OpenAI managed server-side. With previous_response_id, the state is explicit, inspectable, and doesn’t require a separate API object that can get out of sync.
Step 5: Streaming Agents
The Responses API supports server-sent events (SSE) for streaming. The streaming model uses typed events rather than a single delta stream:
from openai import OpenAI
client = OpenAI()
stream = client.responses.create(
model="gpt-4o",
tools=[{"type": "code_interpreter"}, {"type": "web_search"}],
input="Analyze the latest quarterly trends in AI chip revenue.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
elif event.type == "response.web_search.related":
print(f"\n[Searching: {event.query}]")
elif event.type == "response.code_interpreter_call.created":
print(f"\n[Running code in sandbox...]")
elif event.type == "response.function_call.created":
print(f"\n[Calling function: {event.name}]")
elif event.type == "response.completed":
print(f"\n\nDone. Response ID: {event.response.id}")
Key event types for agent monitoring:
| Event type | When it fires | What you get |
|---|---|---|
response.output_text.delta |
Token stream | Text delta |
response.web_search.created |
Web search starts | Search query |
response.code_interpreter_call.created |
Code execution starts | The code being run |
response.function_call.created |
Custom function called | Function name + args |
response.file_search.created |
File search starts | Search query |
response.completed |
Full response done | Complete response object |
Streaming with built-in tools means you can show intermediate progress for web searches and code execution — something that required manual hacks with the Assistants API [9].
Migration Guide: Assistants API → Responses API
With the August 26, 2026 sunset deadline, here’s the migration path:
1. Map old objects to new
| Assistants API | Responses API |
|---|---|
assistant object |
Inline in tools array |
thread object |
previous_response_id chain |
run object |
The response itself |
run.step |
Output items within a response |
message |
Input item with role |
file_search tool |
Same, but inline |
code_interpreter tool |
Same, but inline |
submit_tool_outputs |
Include function call output items in next request |
2. Migration strategy
For each agent, the migration is mechanical:
# BEFORE: Assistants API
assistant = client.beta.assistants.create(
name="Research Agent",
instructions="You are a research assistant...",
tools=[{"type": "web_search"}, {"type": "code_interpreter"}],
model="gpt-4o",
)
thread = client.beta.threads.create()
client.beta.threads.messages.create(thread_id=thread.id, role="user", content=query)
run = client.beta.threads.runs.create(thread_id=thread.id, assistant_id=assistant.id)
# Poll run status, handle requires_action, etc.
# AFTER: Responses API
response = client.responses.create(
model="gpt-4o",
tools=[{"type": "web_search"}, {"type": "code_interpreter"}],
input=query, # instructions become system_prompt in input if needed
)
The migration replaces 5-6 API calls with 1, and removes the polling loop entirely — tool calls are resolved inline before the response is returned.
3. Handle file context migration
If your Assistants API agents relied on file attachments per-message, the Responses API equivalent is to pass file IDs directly:
# Assistants API
client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="Analyze this document",
attachments=[{"file_id": file.id, "tools": [{"type": "file_search"}]}],
)
# Responses API
response = client.responses.create(
model="gpt-4o",
input=[
{
"role": "user",
"content": "Analyze this document",
"attachments": [{"file_id": file.id, "tools": [{"type": "file_search"}]}],
}
],
)
4. Streaming migration
The Assistants API used stream mode as a boolean. The Responses API uses typed SSE events. Update your streaming handler to listen for the new event types listed in Step 5 above.
Best Practices
-
Use
previous_response_idfor sessions, notinputarrays — Resending the full message history in theinputarray every turn defeats the purpose of automatic context management. Chain responses withprevious_response_idand let OpenAI handle pruning [10]. -
Structure function call outputs carefully — Include enough context in
function_call_outputfor the model to use the result. A bare JSON dump is fine for data; add explanatory text when the function returns an error or empty result. -
Set
truncationexplicitly — The default auto-truncation works well for most cases, but for long-running agents, settruncation={"type": "auto", "strategy": "summary"}to preserve key information across many turns. -
Use
temperatureandtop_pper agent type — Research agents benefit from lower temperature (0.1-0.3) for factual accuracy; creative tasks can use higher values. Set these per request, not globally. -
Batch independent tool calls — When handling multiple
function_calloutput items, execute them in parallel and submit results in a single follow-up request. This reduces latency by 3-5x compared to sequential submission [11]. -
Monitor
response_idfor audit trails — Every response returns a unique ID. Log these alongside user sessions for debugging and cost attribution. The Responses API doesn’t have a thread list endpoint — your audit trail is your log. -
Test with
gpt-4o-minifirst — Iterate on agent behavior with the cheaper model before switching togpt-4oorgpt-5.5for production. The tool calling behavior is consistent across model tiers.
Key Takeaways
- The Responses API unifies Chat Completions, Assistants, and Batch APIs into a single
responses.create()endpoint with a consistent request/response shape. - Built-in tools (
web_search,file_search,code_interpreter,image_generation,computer_use) are configured inline in the tools array — no separate assistant object needed. previous_response_idreplaces thread objects for multi-turn conversations, with automatic context management.- Custom function calling uses explicit output item types instead of the older tool_calls array, making parallel execution cleaner.
- Streaming uses typed SSE events with granular progress indicators for each built-in tool.
- The Assistants API sunset on August 26, 2026 makes migration urgent — the mechanical changes are straightforward and reduce code complexity by 60-70%.
References
- OpenAI: New Tools for Building Agents (March 2025) — Announcement of the Responses API and Agents SDK [1]
- OpenAI: Responses API Documentation — API reference and core concepts [2]
- OpenAI: Responses API Quickstart — Your first Responses API call [3]
- OpenAI: Web Search Tool — Web search configuration and citation handling [4]
- OpenAI: File Search Tool — File search with vector stores and attachments [5]
- OpenAI: Code Interpreter Tool — Sandboxed code execution with file output [6]
- OpenAI: Function Calling in Responses API — Custom function registration and lifecycle [7]
- OpenAI: State Management with Previous Response ID — Multi-turn conversation chaining [8]
- OpenAI: Streaming Responses API — SSE event reference and client implementation [9]
- OpenAI: Context Management — Truncation strategies and context window optimization [10]
- OpenAI: Agents SDK — Parallel Tool Execution — Best practices for concurrent tool calls [11]
- Migrate to the Responses API — Official migration guide from Assistants API [12]
📖 Related Reads
- NiteAgent: Production Tool-Calling Architecture — Patterns for reliable tool dispatch in production
- NiteAgent: AI Agent Testing Strategies 2026 — Evaluating and testing agent frameworks in production
- NiteAgent: AI Agent Observability Guide 2026 — Monitoring and debugging agent systems
Cross-links automatically generated from NiteAgent.
← Back to all posts

