Add live web search to any OpenAI app with function calling
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
| Approach | Best for | What you need |
|---|---|---|
| Function calling (this guide) | Any app already using chat.completions | openai SDK + Superhighway API key |
| OpenAI Agents SDK | Agent loop with persistence, handoffs | openai-agents library |
| MCP server | Claude, Cursor, Windsurf clients | npx |
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.