# OpenAI function calling: a Google search tool in 60 lines of Python

> Give an OpenAI model a Google search tool: the JSON schema, the Python tool-calling loop on the SerpCrypto SERP API, and the same schema for Claude. No KYC.

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

Describe a `google_search` function to the model, run the searches it asks for, send the results back. The schema is plain JSON Schema, so it carries over to Claude and other tool-calling models.

**Quick answer — How do I give an OpenAI model a web search tool?**

Declare a `google_search` function with a JSON Schema in `tools`, then loop: when the model returns `tool_calls`, run each search on `https://serpcrypto.com/api/v1/search` and append the results as `tool` messages until it answers. **SerpCrypto** charges 1 credit per successful search, from $0.30/1K searches, paid in crypto with no KYC.

## What does the tool schema look like?

A function tool has a name, a description and parameters in JSON Schema. The model sees only these three things, so the description must say what the tool returns and when to use it. Only `query` is required; country and language are optional and default to US English in the code.

*Tool definition (Chat Completions format)*

```json
{
  "type": "function",
  "function": {
    "name": "google_search",
    "description": "Search Google for current information. Returns the top results with title, link and snippet. Use it for facts newer than your training data.",
    "parameters": {
      "type": "object",
      "properties": {
        "query": { "type": "string", "description": "The search query, as a person would type it." },
        "gl": { "type": "string", "description": "Two-letter country code of the results, e.g. us, gb, de." },
        "hl": { "type": "string", "description": "Language code, e.g. en, de, pt-BR." }
      },
      "required": ["query"]
    }
  }
}
```

## How does the tool-calling loop work?

Send the question with `tools`. If the reply contains `tool_calls`, run each one, append the assistant message and one `tool` message per call with the matching `tool_call_id`, then call the model again. When a reply has no tool call, it is the final answer.

The loop below stops after three searches. A cap protects your credits if a model keeps searching, and three searches answer most factual questions.

*search_agent.py*

```python
"""Tool-calling loop: the model asks for searches, this code runs them on SerpCrypto."""
import json
import os

import requests
from openai import OpenAI

client = OpenAI()  # reads OPENAI_API_KEY
MODEL = os.environ["OPENAI_MODEL"]  # any OpenAI chat model with tool calling
API_URL = "https://serpcrypto.com/api/v1/search"

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "google_search",
            "description": "Search Google for current information. Returns the top results with title, link and snippet.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "The search query."},
                    "gl": {"type": "string", "description": "Two-letter country code, e.g. us."},
                    "hl": {"type": "string", "description": "Language code, e.g. en."},
                },
                "required": ["query"],
            },
        },
    }
]


def google_search(query, gl="us", hl="en"):
    r = requests.get(
        API_URL,
        params={"q": query, "gl": gl, "hl": hl},
        headers={"Authorization": "Bearer " + os.environ["SERPCRYPTO_API_KEY"]},
        timeout=90,
    )
    if r.status_code != 200:
        return f"Search failed with HTTP {r.status_code}."
    items = r.json().get("organic_results", [])[:5]
    return "\n\n".join(f"{x['title']}\n{x['link']}\n{x.get('snippet', '')}" for x in items) or "No results."


def ask(question, max_searches=3):
    messages = [{"role": "user", "content": question}]
    for _ in range(max_searches + 1):
        reply = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
        msg = reply.choices[0].message
        if not msg.tool_calls:
            return msg.content
        messages.append(msg)
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments)
            messages.append({"role": "tool", "tool_call_id": call.id, "content": google_search(**args)})
    return "Stopped: search limit reached."


print(ask("Which countries changed their data protection laws in 2026? Cite sources."))
```

## Does the same schema work with Claude and other models?

Yes, with a different wrapper. Claude's tool use takes the same JSON Schema under `input_schema`, next to `name` and `description`. Most other tool-calling APIs also accept JSON Schema for parameters; only the outer field names change. The search function itself stays the same, because it only calls the SerpCrypto endpoint.

*The same tool for Claude (Messages API)*

```json
{
  "name": "google_search",
  "description": "Search Google for current information. Returns the top results with title, link and snippet.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "description": "The search query." },
      "gl": { "type": "string", "description": "Two-letter country code, e.g. us." },
      "hl": { "type": "string", "description": "Language code, e.g. en." }
    },
    "required": ["query"]
  }
}
```

## What should the tool return to the model?

Short text with links. Five results with title, link and snippet are usually enough for the model to answer and to cite. Returning the full JSON works but costs more tokens and gives the model more noise to read.

Add the AI Overview text when present if you want the model to see Google's own summary, and keep the source links so the answer can cite pages rather than the overview.

## How do I keep search costs predictable?

Count tool calls: each one that succeeds is one credit. Cap searches per question, as the loop does, and log each call with its query so you can see what the model asks for. On the 100,000 searches pack the rate is $0.56/1K searches, and Prepaid search credits never expire: unused searches stay on your account until you use them.

Errors cost nothing: 1 credit = 1 successful search request, which returns one page of results on any supported engine. A request that fails is not charged.

## Function calling questions

### Can I use the Responses API instead of Chat Completions?

Yes. The Responses API takes function tools with the same JSON Schema, with `name`, `description` and `parameters` at the top level of each tool. The search function and the idea of the loop do not change.

### Why not use the model provider's built-in web search?

Built-in search tools choose their own sources and settings. A function on SerpCrypto gives you Google results for the country and language you pick, as JSON you can log, at a known price per search.

### Can the model choose the country?

Yes. `gl` is a parameter of the tool, so the model can set it from the question, for example `gb` for a question about the United Kingdom. Keep a default in your code for questions that name no country.

### Do I need a credit card to get the search API key?

No. SerpCrypto credits are paid in BTC, USDT, USDC, ETH, SOL, LTC, with no ID, no selfie and no documents. Account-number login; email is optional.

---

SerpCrypto: SerpCrypto is a SERP API for developers and AI agents that returns Google and Bing results as JSON, paid in crypto with no ID upload. Prepaid search credits from $0.30/1K searches never expire. No KYC — no ID, no selfie, no documents. Pay in crypto.
