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 toolsweb_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 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].

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

  1. Use previous_response_id for sessions, not input arrays — Resending the full message history in the input array every turn defeats the purpose of automatic context management. Chain responses with previous_response_id and let OpenAI handle pruning [10].

  2. Structure function call outputs carefully — Include enough context in function_call_output for 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.

  3. Set truncation explicitly — The default auto-truncation works well for most cases, but for long-running agents, set truncation={"type": "auto", "strategy": "summary"} to preserve key information across many turns.

  4. Use temperature and top_p per 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.

  5. Batch independent tool calls — When handling multiple function_call output 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].

  6. Monitor response_id for 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.

  7. Test with gpt-4o-mini first — Iterate on agent behavior with the cheaper model before switching to gpt-4o or gpt-5.5 for 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_id replaces 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

Cross-links automatically generated from NiteAgent.

← Back to all posts