If you have built a research agent by hand, you know the shape of that code. One API client for search, a vector database for your documents, a container to run Python, plus a loop that decides which one to call next. That is a few hundred lines of glue before the agent does anything you actually care about.
The Responses API hosts three of those pieces for you: web search, file search, and Code Interpreter. You list them in the tools array and the model decides when to call them. No orchestration loop on your side, no vector DB to operate, no container to babysit.
This guide walks through each tool with runnable Python, shows how to pull citations out of the response, and covers when a hosted tool is the wrong call.
Prerequisites
- Python 3.9 or newer, plus the SDK:
pip install --upgrade openai OPENAI_API_KEYset in your environment- A tool-capable model. The examples use the model name from OpenAI's current docs, so swap in whatever your account can reach.
from openai import OpenAI
MODEL = "gpt-6-astra" # use the model name from your docs
client = OpenAI()
response = client.responses.create(
model=MODEL,
input="Say hello in one short sentence.",
)
print(response.output_text)
One migration note. The Assistants API was sunset on August 26, 2026 and no longer works. If your code still points at /v1/assistants or threads, the official migration guide covers moving Threads to Conversations and Assistants to prompts.
Web search
Register the tool and ask something time-sensitive.
response = client.responses.create(
model=MODEL,
tools=[{"type": "web_search"}],
input="What is the current recommended way to install Docker Engine on Ubuntu?",
)
print(response.output_text)
The response comes back as two output items. First a web_search_call with the call id and an action object whose type is search, open_page, or find_in_page. Then a message item holding the answer text and the annotations.
That second item is where the citations live. Each annotation is a url_citation with the source url, its title, and the start_index / end_index range of the answer text it supports.
for item in response.output:
if item.type != "message":
continue
for part in item.content:
if part.type != "output_text":
continue
print(part.text)
for note in part.annotations or []:
if note.type == "url_citation":
print(f"- {note.title}: {note.url} [{note.start_index}:{note.end_index}]")
OpenAI's terms require those inline citations to stay visible and clickable when you show searched content to users, so do not strip them out.
Three knobs worth knowing. search_context_size takes low, medium, or high and trades context tokens against answer quality. user_location biases results toward a country and city. external_web_access: false restricts the search to OpenAI's own index instead of pulling live pages, which helps when you want reproducible answers.
tools=[{
"type": "web_search",
"search_context_size": "low",
"user_location": {"type": "approximate", "country": "ID", "city": "Jakarta"},
}]
File search
File search is RAG as a service. You upload documents to a vector store, attach the store to the request, and the model retrieves chunks on its own.
# 1. upload the document
with open("handbook.pdf", "rb") as f:
uploaded = client.files.create(file=f, purpose="user_data")
# 2. create a vector store and attach the file
store = client.vector_stores.create(name="handbook")
client.vector_stores.files.create(vector_store_id=store.id, file_id=uploaded.id)
# 3. query it
response = client.responses.create(
model=MODEL,
input="What is the refund window for digital orders?",
tools=[{
"type": "file_search",
"vector_store_ids": [store.id],
"max_num_results": 5,
}],
include=["file_search_call.results"],
)
print(response.output_text)
Indexing runs asynchronously. In a real service, poll the vector store file status until it reports completed before you let users query it, or the first requests hit an empty store and return nothing useful.
Two retrieval controls matter: max_num_results caps how many chunks land in context, and filters narrows by file attributes you set at upload time.
tools=[{
"type": "file_search",
"vector_store_ids": [store.id],
"filters": {"type": "in", "key": "category", "value": ["policy", "billing"]},
}]
Answers come back with file_citation annotations carrying the file_id, filename, and an index pointer, so you can link a sentence to the document that backed it.
Code interpreter
Code Interpreter gives the model a sandboxed Python environment. It writes code, runs it, reads the output, and iterates when the first attempt fails.
response = client.responses.create(
model=MODEL,
tools=[{
"type": "code_interpreter",
"container": {"type": "auto", "memory_limit": "4g"},
}],
instructions="Run Python for every calculation and show the numbers you used.",
input="Build a 12-month compound growth table for a 2% monthly rate starting at 10,000, then save it as CSV.",
)
for item in response.output:
if item.type == "code_interpreter_call":
print(item.code)
for out in item.outputs or []:
print(out)
container: {"type": "auto"} creates a container on first use and reuses the active one from earlier code_interpreter_call items in the same context, which keeps files and variables around across turns. Memory tiers are 1g (the default), 4g, 16g, and 64g. For a shared environment across requests, create one explicitly with client.containers.create(name=..., memory_limit="4g") and pass the returned cntr_... id.
Generated files are referenced as sandbox:/mnt/data/... paths. You fetch the bytes through the container file content endpoint and serve them from your own route, because sandbox: means nothing to a browser.
Putting all three in one call
response = client.responses.create(
model=MODEL,
tools=[
{"type": "web_search"},
{"type": "file_search", "vector_store_ids": [store.id]},
{"type": "code_interpreter", "container": {"type": "auto"}},
],
input="Compare our internal refund policy with current industry benchmarks and chart the difference.",
)
The model picks the tools it needs and you read a single response object at the end. You can steer that choice with tool_choice, from auto (the default) to required, or restrict it to a named subset with allowed_tools.
When a hosted tool is not the right call
Each hosted tool is priced separately, and the numbers are published on the pricing page.
| Hosted tool | Price | Reach for it when |
|---|---|---|
| Web search | $10 per 1,000 calls plus search content tokens at model rates | You want zero search infrastructure and citations out of the box |
| File search | Storage at $0.10 per GB per day (first 1 GB free), $2.50 per 1,000 calls | Your corpus is small to medium and you would rather not run a vector DB |
| Code interpreter | $0.03 (1 GB) to $1.92 (64 GB) per container per 20-minute session | You need a throwaway Python sandbox without managing containers |
The trade-offs are mostly about control.
- Against a dedicated search API like Tavily, Brave, or Exa: the hosted tool is quicker to start and returns citations in the same response. A dedicated API wins when you need a specific index, source allowlists, or per-query cost you can predict. Worth knowing: for
gpt-4o-miniandgpt-4.1-minion the non-preview web search tool, search content tokens are billed as a fixed block of 8,000 input tokens per call. - Against self-hosted RAG on pgvector or Qdrant: hosted file search takes chunking, embedding, and retrieval code off your plate. Self-hosting wins when the corpus is large enough that daily storage bills pass your infrastructure cost, when you need custom reranking or chunking, or when the documents cannot leave your network.
- Against your own Docker sandbox: Code Interpreter needs no maintenance and no network policy work. Your own sandbox wins at high call volume, when you need languages or packages OpenAI does not ship, or when you need outbound network control.
Two defaults to watch. Responses are stored by default, so pass store=False if you do not want them retained. And set search_context_size to low when you only need a fact; the default pulls in more context, and you pay for every one of those tokens.
Next steps
Turn on streaming so users see what the agent is doing while it works, and surface the tool call items as they arrive. Add a remote MCP server to the same tools array to let external services join the loop without custom glue. And when the orchestration outgrows a single call, the Agents SDK builds on this same set of items and tools.
References
- OpenAI, Using tools in the Responses API: https://developers.openai.com/api/docs/guides/tools
- OpenAI, Web search, File search, and Code Interpreter guides: https://developers.openai.com/api/docs/guides/tools-web-search
- OpenAI, API pricing for built-in tools: https://developers.openai.com/api/docs/pricing