Web Search

Ma Mahalakshmi V Updated 16 Sep 2026
10 min read ·Lesson 37 of 224

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.

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_search here), 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_call items 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.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Web Search and get answers drawn from it.

Signed-in readers only.