# OpenAI function calling: give the model a search_web tool

> Web search for OpenAI function calling: a search_web JSON schema, the Responses API loop in Python, and the same tool in the Agents SDK. No KYC, crypto.

- URL: https://llmwebsearch.com/docs/openai/
- Last updated: 2026-10-22
- Publisher: LLMWebSearch (https://llmwebsearch.com)

Describe a `search_web` function to the model, run the call it asks for against our API, and send the sources back. The model then answers with citations. This page shows the JSON schema, the Responses API loop and the shorter OpenAI Agents SDK version.

**Quick answer — How do I add web search to an OpenAI model with function calling?**

Declare a `search_web` function with a JSON schema, and when the model calls it, POST the query to **LLMWebSearch**'s `https://llmwebsearch.com/api/v1/search`. Return the numbered sources, with the top 3 pages as Markdown, as a `function_call_output`; the model answers and cites them. A search costs 1 credit, from $1.20/1K searches as of October 2026. No KYC; pay in crypto.

## What you need

| Attribute | Value |
| --- | --- |
| Packages | `openai` and `requests` (or `openai-agents` for the Agents SDK version) |
| Keys | `OPENAI_API_KEY` for the model, `LLMWEBSEARCH_API_KEY` for search |
| Model | Any model that supports function calling, set in `OPENAI_MODEL` |
| API used | Responses API: `client.responses.create` with `tools` and `input` |
| Billing on our side | One credit pays for one search: up to 20 ranked results, with the pages of the first 3 read and returned as Markdown. Reading 4 to 10 pages makes it 2 credits. A call that ends in an error costs nothing |

## What does the tool schema look like?

A function tool has a `name`, a `description` and JSON Schema `parameters`. Keep the description concrete: what the tool returns, and that sources should be cited by number. Two parameters are enough for most agents: the `query`, and how many top results to `read` in full.

The model never sees your API key. It only proposes arguments; your code decides how to call the API and what to send back.

## How does the call loop work?

Send the user's message with the tools. If the response holds items of type `function_call`, parse each one's JSON `arguments`, run the search, and append a `function_call_output` with the same `call_id`. Then call the model again with the longer input list. When a response contains no function call, its text is the answer.

The loop below stops after five rounds, a guard against a model that keeps searching. OpenAI's function-calling guide describes the same `function_call` and `function_call_output` items, read on 22 October 2026.

*search_tool_loop.py (Responses API)*

```python
import json
import os

import requests
from openai import OpenAI

client = OpenAI()
MODEL = os.environ["OPENAI_MODEL"]  # any model that supports function calling
SEARCH = "https://llmwebsearch.com/api/v1/search"

tools = [
    {
        "type": "function",
        "name": "search_web",
        "description": "Search the live web. Returns numbered sources with URLs; the first `read` include page text. Cite sources as [1], [2].",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "What to search for, as a person would type it."},
                "read": {"type": "integer", "minimum": 0, "maximum": 10, "description": "How many top results to read in full."},
            },
            "required": ["query"],
            "additionalProperties": False,
        },
    }
]


def search_web(query, read=3):
    resp = requests.post(
        SEARCH,
        headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
        json={"q": query, "max_results": 5, "read": read, "max_chars": 5000},
        timeout=60,
    )
    if resp.status_code != 200:
        return "Search failed: " + resp.json().get("error", {}).get("code", str(resp.status_code))
    parts = []
    for i, r in enumerate(resp.json()["results"], start=1):
        text = r["content"] if r["content_status"] == "ok" else "(snippet only) " + r["snippet"]
        parts.append(f"[{i}] {r['title']} - {r['url']}\n{text}")
    return "\n\n".join(parts)


input_list = [{"role": "user", "content": "How should I size a Postgres connection pool?"}]
for _ in range(5):
    response = client.responses.create(model=MODEL, tools=tools, input=input_list)
    calls = [item for item in response.output if item.type == "function_call"]
    if not calls:
        print(response.output_text)
        break
    input_list += response.output
    for item in calls:
        args = json.loads(item.arguments)
        output = search_web(args["query"], int(args.get("read", 3)))
        input_list.append({"type": "function_call_output", "call_id": item.call_id, "output": output})

```

## Is there a shorter version with the Agents SDK?

Yes. The OpenAI Agents SDK turns a Python function into a tool with a decorator and runs the call loop for you. Its tools documentation, read on 22 October 2026, shows the `@tool` decorator from `agents.decorators`, with the description taken from the docstring and each argument's description from its `Args` section. The same page also mentions the `@function_tool` form.

`Runner.run_sync` runs the agent until it produces a final answer, calling `search_web` as often as the model asks.

*search_agent.py (OpenAI Agents SDK)*

```python
import os

import requests
from agents import Agent, Runner
from agents.decorators import tool


@tool
def search_web(query: str, read: int = 3) -> str:
    """Search the live web and return numbered sources with their URLs.

    Args:
        query: What to search for, as a person would type it.
        read: How many top results to read in full, from 0 to 10.
    """
    resp = requests.post(
        "https://llmwebsearch.com/api/v1/search",
        headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
        json={"q": query, "max_results": 5, "read": max(0, min(read, 10)), "max_chars": 5000},
        timeout=60,
    )
    resp.raise_for_status()
    parts = []
    for i, r in enumerate(resp.json()["results"], start=1):
        text = r["content"] if r["content_status"] == "ok" else "(snippet only) " + r["snippet"]
        parts.append(f"[{i}] {r['title']} - {r['url']}\n{text}")
    return "\n\n".join(parts)


agent = Agent(
    name="Researcher",
    instructions="Answer from search_web results only and cite them as [1], [2].",
    tools=[search_web],
)
result = Runner.run_sync(agent, "How should I size a Postgres connection pool?")
print(result.final_output)

```

## Why use an API tool instead of OpenAI's built-in web search?

Mostly for control over what the model reads and what it costs. Our tool hands your code the raw sources, with URLs and page text, so you decide what enters the context, can log it and can store it. We do not train any model on your queries or on what the API returns, and we add no rule of our own against storing results; the rights of the sites whose pages you read still apply.

The price is also fixed per search instead of depending on how much retrieved content the model reads. The [built-in tools comparison](https://llmwebsearch.com/compare/built-in-web-search-tools/) puts the per-call prices of OpenAI, Anthropic, Google and others side by side, with dates.

## How do I stop the model from searching too much?

Say it in the instructions: search once, read the sources, search again only if they disagree or miss the point. Cap the number of rounds in your own loop, as the script above does with five.

Keep `max_chars` modest. Three pages of 5,000 characters is about 4,000 tokens per search, and each round adds them to the input of the next model call.

## OpenAI function calling: common questions

### Does this work with the Chat Completions API?

Yes, with the older `tools` format of Chat Completions, where the function sits inside a `function` object and results go back as `tool` messages. The search function itself does not change.

### Can the model read a page it finds in a source?

Add a second function, `read_urls`, that posts URLs to `https://llmwebsearch.com/api/v1/extract`. Reading pages whose URLs you already have costs 1 credit for each started group of 3 pages returned with content, up to 20 URLs per call; refused or failed URLs are not counted.

### Which model should I use?

Any model that supports function calling. Try two or three on your own questions: the answer depends on the sources as much as on the model, and a smaller model costs less per round.

## Sources

1. [Function calling](https://developers.openai.com/api/docs/guides/function-calling) — OpenAI API docs (read 2026-10-22)
2. [Tools (OpenAI Agents SDK)](https://openai.github.io/openai-agents-python/tools/) — OpenAI Agents SDK docs (read 2026-10-22)

---

LLMWebSearch: LLMWebSearch is a web search API for AI agents that returns ranked results with the content of the top pages in Markdown, paid in crypto with no ID upload. Prepaid credits from $1.20/1K searches never expire. No KYC — no ID, no selfie, no documents. Pay in crypto.
