Web Search
From Custom Functions to Built-In Tools
Unit 8 covered function calling as a general mechanism: you define a schema, your code implements the function, and the model requests calls to it. Starting with this lesson, the course covers a different category of tool — built-in tools, where the platform itself provides both the schema and the implementation. A web search tool is the clearest example: rather than writing your own function that calls a search API, parses results, and returns them to the model, you simply tell the request to enable web search, and the platform handles searching the live web and feeding the results into the model's reasoning, all within a single API call.
This distinction matters practically. A custom function (Unit 8) requires you to build, host, and maintain the actual implementation — your own weather lookup, your own database query. A built-in tool requires no implementation at all on your part; you are enabling a capability the platform already operates, in exchange for less control over exactly how it works internally.
Enabling Web Search
response = client.responses.create(
model="gpt-5.6-terra",
input="What were the major headlines in AI research this week?",
tools=[{"type": "web_search"}],
)
print(response.output_text)
Note: The exact tool type identifier (
web_searchhere), its available configuration options, and which models support it are details that can change across SDK versions and platform updates. Confirm the current tool name and supported options against your installed SDK version's documentation before relying on these specifics in production code.
Compare this to Unit 8's function-calling schemas: there is no parameters object to define, no function to implement, and no dispatch loop to write. Enabling {"type": "web_search"} in tools is sufficient for the model to decide, on its own, whether a given request would benefit from a live web search, perform that search internally, and incorporate the results directly into its response — all within the single client.responses.create() call, without the multi-round request/execute/respond loop Unit 8, Lesson 3 covered for custom functions.
Why This Exists: The Knowledge Cutoff Problem
Every model has a training cutoff — a point beyond which it has no information, because it was never trained on anything published after that date. Unit 1 introduced this limitation; web search is the platform's built-in answer to it for a specific class of request: anything that depends on current events, recent publications, live prices, or any other fact that changes after a model's training data was collected. Without web search, a model asked about "this week's headlines" has no honest way to answer beyond acknowledging it cannot know — with web search enabled, the same request can be answered with actual current information, retrieved at request time rather than baked into the model's training.
Inspecting What the Model Actually Searched For
A response that used web search includes structured output items describing the search itself, not just the final synthesized text — useful for transparency, debugging, and, in some applications, for showing users what sources informed an answer.
response = client.responses.create(
model="gpt-5.6-terra",
input="What is the current status of the Artemis lunar program?",
tools=[{"type": "web_search"}],
)
for item in response.output:
if item.type == "web_search_call":
print(f"Search performed: {item.action}")
elif item.type == "message":
for content_item in item.content:
if hasattr(content_item, "annotations"):
for annotation in content_item.annotations:
print(f"Source cited: {annotation.url}")
print(response.output_text)
Note: The exact structure of
web_search_callitems and citation annotations (their field names and how URLs and titles are represented) can vary by SDK version. Confirm the current output shape against your installed SDK version's documentation before building parsing logic around these specifics.
This structure mirrors the function_call / function_call_output pattern from Unit 8 in spirit — the response contains distinct output items for the tool invocation itself versus the final message — but the crucial difference is that the platform already executed the search and folded the results into the same response; there is no follow-up round trip required from your code, unlike the explicit execute-and-send-back step Unit 8, Lesson 3 required for custom functions. Iterating over response.output and checking item.type remains the same defensive pattern Unit 8 established, since a response might contain zero, one, or multiple web_search_call items depending on whether and how many times the model decided a search was warranted.
Citations and Why They Matter
When web search informs an answer, the response typically includes citation information — which source URLs the answer draws on — attached as annotations on the output text. This is worth treating as more than incidental metadata: presenting an answer that draws on live web content without surfacing where that content came from removes the user's ability to independently verify a claim, which matters more for web-search-informed answers than for answers drawn from the model's general training, precisely because web content varies enormously in reliability.
def extract_citations(response) -> list[dict]:
citations = []
for item in response.output:
if item.type != "message":
continue
for content_item in item.content:
for annotation in getattr(content_item, "annotations", []):
if getattr(annotation, "type", None) == "url_citation":
citations.append({"url": annotation.url, "title": getattr(annotation, "title", None)})
return citations
citations = extract_citations(response)
for citation in citations:
print(f"- {citation['title']}: {citation['url']}")
Extracting and displaying citations like this — rather than only showing the synthesized final text — is a straightforward but important step for any user-facing application built on web search: it lets a user judge the reliability of an answer's sources themselves, and it is a direct, practical way to reduce the risk of an unverified or low-quality source shaping a user's understanding of a current event without their awareness.
When Web Search Is and Isn't the Right Tool
Web search is well suited to genuinely time-sensitive or current-events questions — "what happened in the news today," "what is the latest version of a specific piece of software," "what is a company's current stock price." It is a poor fit for questions that are better served by a custom function against your own data (Unit 8's territory entirely — a customer's order status is not something a public web search will ever find), and it is unnecessary overhead for a question the model can already answer reliably from stable, well-established knowledge that doesn't change over time (a mathematical fact, a well-documented historical event, the syntax of a stable programming language feature). Providing {"type": "web_search"} alongside custom function tools, as the next section covers, lets the model choose the right source for each specific request rather than forcing every request through the same channel.
Combining Web Search With Custom Function Tools
Web search can be provided in the same tools list alongside the custom function tools Unit 8 covered, letting the model choose between live web information and your own application-specific functions based on what a given request actually needs.
def get_account_balance(account_id: str) -> dict:
return {"account_id": account_id, "balance": 542.10}
tools = [
{"type": "web_search"},
{
"type": "function",
"name": "get_account_balance",
"description": "Get the current balance for a specific internal account ID.",
"parameters": {
"type": "object",
"properties": {"account_id": {"type": "string"}},
"required": ["account_id"],
"additionalProperties": False,
},
},
]
response = client.responses.create(
model="gpt-5.6-terra",
input="What's the current exchange rate from USD to EUR, and what's the balance on account ACC-99?",
tools=tools,
)
Given this combined question, the model should recognize that the exchange rate calls for a web search (since exchange rates change constantly and are exactly the kind of live, external fact web search exists for) while the account balance calls for the custom get_account_balance function (since that information exists only in your own systems, and no public web search will ever find it) — correctly routing each half of the question to the appropriate tool without either being hardcoded. The dispatch loop from Unit 8, Lesson 3 and Lesson 4 still applies unchanged to the function_call items in the response; web_search_call items require no such handling on your part, since the platform has already resolved them by the time the response arrives.
Cost and Latency Considerations
Enabling web search adds real cost and latency beyond a standard text request — the platform performs an actual search and processes the returned results before generating a final answer, which takes measurably longer and typically costs more than a request that doesn't invoke any tool. This has a direct practical implication mirroring Unit 5's general reasoning-effort and model-tier guidance: enabling web search for every request regardless of whether it's actually needed adds unnecessary cost and latency to requests that never benefited from it in the first place. A well-designed system prompt or a lightweight upstream classification step that decides whether a given request is plausibly time-sensitive — before deciding whether to enable web search for it at all — is a reasonable way to avoid paying this cost on every single request indiscriminately.
Forcing or Restricting Tool Use
Beyond simply listing web_search as an available tool and letting the model decide whether to use it, some requests call for stronger control over that decision — either forcing a search to happen, or preventing tool use entirely for a specific request.
# Force the model to use a tool (any available tool) rather than answer directly
response = client.responses.create(
model="gpt-5.6-terra",
input="What's the latest version of the openai Python package?",
tools=[{"type": "web_search"}],
tool_choice="required",
)
# Prevent any tool use, even though tools are available — useful for
# comparing a model's own knowledge against a search-informed answer
response_no_tools = client.responses.create(
model="gpt-5.6-terra",
input="What's the latest version of the openai Python package?",
tools=[{"type": "web_search"}],
tool_choice="none",
)
Note: The exact accepted values for
tool_choice("required","none","auto", or a specific tool name) and their precise behavior can vary by SDK version. Confirm current options against your installed SDK version's documentation.
Setting tool_choice="required" is useful for a request where you know in advance that an unaided answer would be unreliable — a package version number is a fast-changing fact the model's training data will not reflect accurately by the time the request is made, so forcing a search removes any chance the model answers confidently from stale training data instead. Setting tool_choice="none" is useful in the opposite situation: deliberately comparing the model's own unaided knowledge against a search-informed answer, which can be a useful diagnostic during development for understanding how much a given feature actually benefits from web search versus how well the model would perform without it.
Common Mistakes
Enabling web search for every request regardless of whether the question is time-sensitive, incurring unnecessary cost and latency on questions the model could have answered reliably and instantly from its own training.
Displaying a web-search-informed answer without surfacing its citations, removing the user's ability to judge the reliability of the underlying sources for a claim about current events.
Assuming every response with web search enabled actually performed a search, rather than checking for the presence of web_search_call items, since the model may reasonably decide a search isn't warranted for a given input even when the tool is available.
Treating web search results as infallible, when web content varies enormously in reliability and a search-informed answer still deserves the same critical evaluation as any other unverified source.
Best Practices
Enable web search selectively, based on whether a request is plausibly time-sensitive, rather than unconditionally on every request, to avoid unnecessary cost and latency.
Always surface citation information in a user-facing feature that uses web search, giving users the ability to independently verify claims drawn from live web content.
Combine web search with custom function tools when an application needs both live public information and access to private, application-specific data, letting the model route each part of a request to the appropriate source.
Treat web-search-informed content with the same critical evaluation as any other unverified source, rather than assuming a citation automatically confers reliability.