Add live web search to any OpenAI app with function calling

Superhighway guides

Most developers talk to OpenAI the same way: client.chat.completions.create() with a tools=[] list of functions the model can call. This guide shows how to plug Superhighway's web search in as one of those tools — no new libraries, no agent framework, just the openai SDK you already have. The model decides when a question needs fresh information, calls your web_search function, and writes its answer from the live results.

1. Install

pip install openai requests

2. Define the tool and run a basic search

Declare a web_search tool in the OpenAI tool schema, send a message with it available, and handle the case where the model decides to call it. You execute the call against Superhighway's /search endpoint, hand the JSON back as a tool message, and ask the model again for its grounded answer.

import os, json, requests
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
SUPERHIGHWAY_KEY = os.environ["SUPERHIGHWAY_API_KEY"]

tools = [
    {
        "type": "function",
        "function": {
            "name": "web_search",
            "description": "Search the live web for up-to-date information. Use for current events, recent releases, prices, or anything that may not be in the model's training data.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query"
                    },
                    "count": {
                        "type": "integer",
                        "description": "Number of results to return (default 5, max 10)"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

def web_search(query: str, count: int = 5) -> dict:
    r = requests.get(
        "https://superhighway.walls.sh/search",
        params={"q": query, "count": count},
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"},
        timeout=15,
    )
    r.raise_for_status()
    return r.json()

messages = [
    {"role": "user", "content": "What are the latest Python web frameworks gaining traction in 2025?"}
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

# Handle tool call
if response.choices[0].finish_reason == "tool_calls":
    tool_call = response.choices[0].message.tool_calls[0]
    args = json.loads(tool_call.function.arguments)

    result = web_search(**args)

    messages.append(response.choices[0].message)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools,
    )

print(response.choices[0].message.content)

The model returns finish_reason == "tool_calls" when it wants live data. You run the search, append both the assistant's tool-call message and a matching tool message (linked by tool_call_id), then call the API again to get the final grounded reply.

3. Handle repeated tool calls in a loop

A single round-trip works for one search, but the model may want to search several times — or call other tools — before it has enough to answer. Wrap the exchange in a while loop so it runs until the model stops requesting tools:

def run_with_search(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        choice = response.choices[0]

        if choice.finish_reason != "tool_calls":
            return choice.message.content

        messages.append(choice.message)

        for tool_call in choice.message.tool_calls:
            args = json.loads(tool_call.function.arguments)
            result = web_search(**args)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result),
            })

print(run_with_search("Summarize the top 3 news stories in AI this week."))

Looping over choice.message.tool_calls handles the case where the model requests multiple searches in parallel — append one tool message per call, all linked by their tool_call_id, before looping back to the API.

4. Add more Superhighway tools

The same pattern scales to the rest of the Superhighway suite. Add a news tool and a scrape tool to your tools list:

news_tool = {
    "type": "function",
    "function": {
        "name": "news_search",
        "description": "Search for recent news articles on a topic.",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string"}
            },
            "required": ["query"]
        }
    }
}

scrape_tool = {
    "type": "function",
    "function": {
        "name": "scrape_page",
        "description": "Fetch the full content of a specific URL as clean text.",
        "parameters": {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "The URL to fetch"}
            },
            "required": ["url"]
        }
    }
}

Add both to the tools list (tools = [web_search_tool, news_tool, scrape_tool]), then dispatch by function name inside the loop. A lookup table keeps the handler clean:

HEADERS = {"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}

TOOL_FUNCTIONS = {
    "web_search": lambda args: requests.get(
        "https://superhighway.walls.sh/search", params={"q": args["query"], "count": args.get("count", 5)},
        headers=HEADERS, timeout=15).json(),
    "news_search": lambda args: requests.get(
        "https://superhighway.walls.sh/news", params={"q": args["query"]},
        headers=HEADERS, timeout=15).json(),
    "scrape_page": lambda args: requests.get(
        "https://superhighway.walls.sh/scrape", params={"url": args["url"]},
        headers=HEADERS, timeout=15).json(),
}

for tool_call in choice.message.tool_calls:
    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)
    result = TOOL_FUNCTIONS[name](args)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

5. Function calling vs Agents SDK vs MCP

ApproachBest forWhat you need
Function calling (this guide)Any app already using chat.completionsopenai SDK + Superhighway API key
OpenAI Agents SDKAgent loop with persistence, handoffsopenai-agents library
MCP serverClaude, Cursor, Windsurf clientsnpx

Get your API key at /pricing (free tier: 1,000 calls/month). For the full OpenAI Agents SDK integration with agent loops and handoffs, see the OpenAI Agents SDK guide.