Add live web search to Mistral AI with function calling

Superhighway guides

Mistral's models support function calling through the mistralai Python SDK, so you can hand them tools and let the model decide when to call out for live data. This guide wires Superhighway's web search into a Mistral app using client.chat.complete(tools=[...]). The syntax is close to OpenAI's, but with a few Mistral-specific details: you call client.chat.complete() instead of client.chat.completions.create(), and tool-call arguments arrive as a JSON string you parse with json.loads. The model decides when a question needs fresh data, calls your web_search tool, and writes its answer from the live results.

1. Install

You need the Mistral SDK and requests to call Superhighway (no extra SDK required — Superhighway is a plain REST API):

pip install mistralai requests

2. Define the tool and run a basic search

Declare a web_search tool in the OpenAI-compatible function schema Mistral uses, then send a chat request with tools available and tool_choice="auto" so the model decides whether to call it.

import os, json, requests
from mistralai import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
SUPERHIGHWAY_KEY = os.environ["SUPERHIGHWAY_API_KEY"]

tools = [
    {
        "type": "function",
        "function": {
            "name": "web_search",
            "description": "Search the live web for current information. Use for recent events, releases, prices, or anything outside the model's training data.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search query"}
                },
                "required": ["query"]
            }
        }
    }
]

response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "What's the latest news about AI agents?"}],
    tools=tools,
    tool_choice="auto"
)

When the model wants live data, response.choices[0].message.tool_calls is populated. Each tool call carries tc.function.name and tc.function.arguments — and the arguments come back as a JSON string, so you parse them with json.loads.

3. Dispatch the tool to Superhighway

Write a handler that runs the search against Superhighway's /search endpoint and returns a compact text summary the model can read back:

def call_tool(tool_name, args):
    if tool_name == "web_search":
        r = requests.get(
            "https://superhighway.walls.sh/search",
            params={"q": args["query"]},
            headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"},
            timeout=15,
        )
        r.raise_for_status()
        results = r.json()["results"]
        return "\n".join(
            f"{x['title']}: {x['url']}\n{x['content']}" for x in results[:3]
        )
    return "Unknown tool"

4. Agentic while-loop

A single round-trip works for one search, but the model may want several before it can answer. Wrap the exchange in a while loop that runs until the response has no more tool_calls. Append the assistant message, then one "role": "tool" message per call — linked by tool_call_id:

def run_with_search(query):
    messages = [{"role": "user", "content": query}]

    while True:
        response = client.chat.complete(
            model="mistral-large-latest",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        message = response.choices[0].message

        if not message.tool_calls:
            return message.content

        messages.append(message)

        for tc in message.tool_calls:
            result = call_tool(tc.function.name, json.loads(tc.function.arguments))
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "name": tc.function.name,
                "content": result
            })

print(run_with_search("What's the latest news about AI agents?"))

The loop keeps the model in control: it searches as many times as it needs, and only when message.tool_calls is empty does it return the grounded answer.

5. Add news and scrape tools

The same pattern scales to the rest of the Superhighway suite. Add news_search and scrape_page to the tools array and route each call by name in call_tool:

tools = [
    {
        "type": "function",
        "function": {
            "name": "web_search",
            "description": "Search the live web for current information.",
            "parameters": {
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "news_search",
            "description": "Search recent news articles on a topic.",
            "parameters": {
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "scrape_page",
            "description": "Fetch the full text content of a URL.",
            "parameters": {
                "type": "object",
                "properties": {"url": {"type": "string"}},
                "required": ["url"]
            }
        }
    }
]

ENDPOINTS = {"web_search": "search", "news_search": "news", "scrape_page": "scrape"}

def call_tool(tool_name, args):
    endpoint = ENDPOINTS[tool_name]
    if tool_name == "scrape_page":
        params = {"url": args["url"]}
    else:
        params = {"q": args["query"]}
    r = requests.get(
        f"https://superhighway.walls.sh/{endpoint}",
        params=params,
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"},
        timeout=30,
    )
    r.raise_for_status()
    data = r.json()
    if tool_name == "scrape_page":
        return data.get("content", "")[:4000]
    return "\n".join(
        f"{x['title']}: {x['url']}\n{x['content']}" for x in data["results"][:3]
    )

The while-loop from section 4 doesn't change — it already dispatches by tc.function.name, so all three tools work with one handler.

6. Mistral vs OpenAI function calling

If you've used OpenAI's function calling, Mistral will feel familiar. The differences are small but matter:

DetailMistralOpenAI
Chat methodclient.chat.complete()client.chat.completions.create()
Tool schema{"type": "function", "function": {...}}Same
Tool choicetool_choice="auto"Same
Tool-call argumentsJSON string → json.loads()JSON string → json.loads()
Tool result message"role": "tool" + tool_call_id + nameSame

In practice the tools array and the tool-result message shape are identical — the main swap is the chat method name. Code written for one ports to the other with minimal changes.

7. Using the free API key (no credit card)

Superhighway's free tier needs no card. Grab a key and set it in your environment:

SUPERHIGHWAY_KEY = "YOUR_API_KEY"  # get free at https://superhighway.walls.sh/pricing

Or skip the key entirely with x402: your agent pays $0.002 per call in USDC on Base — no signup, no API key management. That's ideal for autonomous agents that provision their own access. See the x402 pay-per-call guide for the wallet setup.

Get your API key at /pricing (free tier: 1,000 calls/month). For the OpenAI equivalent, see the OpenAI function calling guide.