Blog
 / 
September 29, 2026

How to Add Web Search to Agent Tool Calling With the You.com Web Search API

How to Add Web Search to Agent Tool Calling With the You.com Web Search API

TLDR: Agents search the web through a tool: you describe a search function in a JSON schema, the model decides when to call it, your code executes the call against the You.com Web Search API, and you return the results as the tool's output. This guide covers the schema, the executor, result formatting, where OpenAI and Anthropic's schemas actually diverge, and the failure modes that break agents in production.

Tool calling is how a language model reaches outside its training data. You register a function with a name, a description, and a parameter schema. The model emits a structured request when it needs the tool, and your code does the actual work. Web search is the most common tool in agent frameworks, and You.com exposes it as a single documented endpoint, which makes it a clean first tool for any agent you are building. The same pattern powers the host-specific guides in our series, from adding a web search tool to a LangChain agent to wiring search into Cursor through MCP.

What Does a Web Search Tool Definition Look Like?

A tool definition is a JSON schema the model reads before it answers. The description field does the most work: it tells the model when the tool is worth calling and when it is not.

{
  "type": "function",
  "function": {
    "name": "web_search",
    "description": "Search the live web for current information. Use this for facts, prices, versions, news, or anything that may have changed recently. Do not use it for questions you can answer confidently from memory.",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {
          "type": "string",
          "description": "One focused search query, the way a person would type it into a search box."
        },
        "count": {
          "type": "integer",
          "description": "Number of results to return, between 1 and 100. Use 5 to 10 for most agent lookups."
        }
      },
      "required": ["query"]
    }
  }
}

This nested shape, a name and schema packed under a function key, is OpenAI's Chat Completions format. OpenAI and Anthropic both read a name, a description, and a parameter schema, but neither the field names nor the nesting are identical between them, and OpenAI itself has two current shapes rather than one. The OpenAI function calling guide and the Anthropic tool use documentation describe the exact request formats each provider expects. Do not assume the schema above parses cleanly everywhere; the comparison below spells out where it does not.

How Do You Execute the Tool Call?

When the model wants to search, you receive a call request with the arguments, and your code performs the actual request against the Web Search API endpoint.

import os, json, urllib.request

def execute_web_search(query: str, count: int = 5) -> list:
    url = "https://ydc-index.io/v1/search"
    payload = json.dumps({"query": query, "count": count}).encode()
    req = urllib.request.Request(
        url,
        data=payload,
        headers={
            "X-API-Key": os.environ["YDC_API_KEY"],
            "Content-Type": "application/json",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        # 401 means the key is missing or invalid. 429 means you hit the
        # rate limit. Neither is worth retrying with the same inputs
        # without a pause, so surface the status to the caller.
        return [{"error": f"search failed with HTTP {e.code}"}]
    except urllib.error.URLError as e:
        return [{"error": f"search unreachable: {e.reason}"}]

    results = data.get("results", {})
    web = results.get("web", []) or []
    return web

The executor owns three things the model cannot see: authentication, timeouts, and error translation. A search tool that raises an unhandled exception takes the whole agent turn down with it. Returning a structured error object instead lets the model tell the user the search failed and continue, which is the behavior you want in a long-running agent loop. The Web Search API guide documents the endpoint, the request body, and the filters the payload accepts.

You also do not have to hold a long-lived key at all. You.com's billing documentation describes machine payments for agents: a caller can invoke the Web Search API with no account and no stored key, settling each request in USDC from a funded wallet instead of drawing down a credit balance. For most integrations a stored YDC_API_KEY is simpler to operate and to rotate, but the no-account path is worth knowing about if you are shipping an autonomous agent that should not carry a long-lived secret at all.

How Do You Return Results to the Model?

Raw search responses are too large for most model contexts, so the return step is where most of the engineering happens. A practical format is a numbered list of compact result objects, trimmed to the fields the model needs.

def format_for_model(results: list, max_results: int = 5) -> str:
    if not results or "error" in results[0]:
        return json.dumps(results[0])  # let the model see the failure

    lines = []
    for i, r in enumerate(results[:max_results], start=1):
        title = (r.get("title") or "").strip()
        url = r.get("url") or ""
        snippet = (r.get("description") or "").strip()[:300]
        lines.append(f"[{i}] {title}\n{url}\n{snippet}")
    return "\n\n".join(lines)

Two rules keep this step safe. First, cap the number of results and the snippet length per result. Ten results with 300-character snippets is roughly 4,000 characters, which sits comfortably inside a tool response budget. Second, never drop the URL. Agents that cite their sources need the URL in the tool response, because the model cannot reconstruct it from a title. For a full walkthrough of the surrounding architecture, see the search API guide on this site.

Do OpenAI and Anthropic Expect the Same Tool Schema?

No, and the gaps are exactly the kind that pass a quick test against one provider and then break on the other. OpenAI's Chat Completions API nests the definition under a function key, and a call comes back inside message.tool_calls: each entry carries an id and a function object whose arguments field is a JSON-encoded string you parse yourself. OpenAI's newer Responses API, which the function calling guide now leads with and which the documentation states GPT-6 Astra and GPT-6.1 Sol require, flattens the definition, so type, name, description, and parameters sit at the same level with no function wrapper. A call arrives as a function_call item carrying its own call_id, and its arguments field is still a JSON string, not a parsed object. Anthropic drops the wrapper entirely and renames the parameter key: a tool is name, description, and input_schema. Claude's call arrives as a tool_use content block, and its input field is already a parsed object, the one case among the three that skips the decode step.

SurfaceDefinition shapeCall arrives asYou reply with
OpenAI Chat Completionsnested under function: name, description, parameterstool_calls[], each with id and a function.arguments JSON stringa tool role message keyed by tool_call_id
OpenAI Responses APIflat: type, name, description, parametersa function_call item with call_id and an arguments JSON stringa function_call_output item keyed by that same call_id
Anthropic Claudeflat: name, description, input_schema, no function wrappera tool_use content block with input already a parsed objecta tool_result content block keyed by tool_use_id

One more distinction matters for a web search tool specifically. Anthropic's tool use documentation also describes server tools, including a hosted web_search type that Claude calls and Anthropic executes on its own infrastructure, with no executor code from you and no You.com API in the loop at all. That is a different pattern from everything above it. If the goal is Claude searching through You.com rather than Anthropic's own web search, define a client tool with an input_schema the way the rest of this guide does, and do not reach for the server tool type by mistake.

When Should You Use Native Tool Calling Instead of MCP?

Native tool calling and the Model Context Protocol solve the same problem at different layers, and the choice is a real tradeoff rather than a fashion decision.

  • Native tool calling gives you full control of the schema, the executor, and the error handling. It is the right choice for a single application with a small, stable tool set, because there is no protocol layer to debug.
  • MCP exposes tools through a standard server, so any MCP client can use them without custom code. It is the right choice when the same tools must serve multiple hosts, or when you do not control the agent runtime. The MCP specification defines the protocol, and You.com's MCP Server documentation covers its hosted server, which our CrewAI web search tool guide walks through attaching to an agent end to end.

The decision framework in one line: if you own the loop, call the API directly. If hosts you do not own need your tools, serve them over MCP.

How Do You Test the Tool Before You Ship It?

A search tool fails quietly more often than it fails loudly, so test the pieces the model never sees rather than waiting to notice the agent got worse. Three things are worth an offline check before you ship: the formatter's output shape, its behavior on the error path, and whether the schema you send the model still matches the executor's actual signature. That last check is the automated version of the schema-drift failure mode below: a test that reads the executor's parameters and asserts every field the schema marks required is one the function actually accepts.

import inspect
import json
from unittest import TestCase, main as run


class FormatForModelTests(TestCase):
    def test_formats_results_in_order(self):
        fixture = [
            {"title": "You.com Web Search API", "url": "https://you.com/docs/guides/search",
             "description": "Structured web and news results."},
            {"title": "Model Context Protocol", "url": "https://modelcontextprotocol.io",
             "description": "An open standard for connecting AI applications to external systems."},
        ]
        out = format_for_model(fixture, max_results=5)
        self.assertTrue(out.startswith("[1] You.com Web Search API"))
        self.assertIn("[2] Model Context Protocol", out)

    def test_passes_through_the_error_object(self):
        out = format_for_model([{"error": "search failed with HTTP 429"}])
        self.assertEqual(out, json.dumps({"error": "search failed with HTTP 429"}))

    def test_schema_required_field_matches_executor_signature(self):
        schema_required = ["query"]
        params = inspect.signature(execute_web_search).parameters
        for field in schema_required:
            self.assertIn(field, params)


if __name__ == "__main__":
    run()

None of this calls the live API. The fixture in the first test is shaped like a real result object but is written by hand, the error test never leaves the process, and the signature check is pure introspection. That is enough to catch the two failure modes that actually ship: a formatter that silently changes shape when someone edits it, and a schema that drifts out of sync with the code behind it. Run it in the same suite as the rest of your agent's tests, not as a one-off script you remember to run occasionally.

What Breaks in Production?

Four failure modes account for most broken search tools, and each has a detectable signature.

Schema drift. You change a parameter name in your executor and forget the schema. The model keeps sending the old field, your executor ignores it, and search quality quietly degrades. Detection: log the arguments the model sends and the arguments your executor reads, and alert when they diverge, or catch it earlier with the signature test above.

Confident nonsense arguments. The model calls the tool with a malformed query, such as a full paragraph pasted into the query field. Detection: log query length. A median query length above roughly 200 characters means your description is not constraining the model, so tighten it.

Runaway tool loops. The model searches, gets weak results, and searches again with the same query. Detection: cap tool calls per turn in the loop, and log the query for every call. Repeated identical queries within one turn are the signature of a loop, and a hard cap of 5 to 10 calls per turn stops it.

Swallowed errors. An executor that catches exceptions and returns an empty list teaches the model that the tool "found nothing," which produces confident wrong answers instead of an honest failure. Detection: never return an empty list on error. Return the error object, and count error responses per day as a health metric.

Related Guides

FAQ

Do I need a framework to add tool calling? No. The pattern is three pieces: a JSON schema you send with the request, an executor that runs when the model calls the tool, and a result formatter. Frameworks package these pieces, but the raw provider APIs accept the same schema directly.

How many results should the search tool return? Start with five. More results give the model more chances to find the right source, but they also spend context and attention. Measure the answer quality at 3, 5, and 10 results on your own task set before you commit.

Should the tool description tell the model when not to search? Yes. A description that only says what the tool does invites the model to call it for every question. Add one sentence about when the tool is unnecessary, such as for stable facts the model already knows.

Do OpenAI and Anthropic use the same tool call schema? No. OpenAI's Chat Completions API nests the schema under a function key and returns arguments as a JSON string you decode yourself. Its newer Responses API flattens the shape but still returns arguments as a string. Anthropic drops the function wrapper, calls the schema input_schema, and hands back an already-parsed object. Convert at the edges rather than assuming one schema works everywhere.

    Share Article:

  1. LI Test

  2. LI Test

Related resources.

No items found.
No items found.