Function Calling

Function calling lets the model invoke functions you define. The model decides when to call a function and generates the arguments — your code executes it and returns the result.

How it works

  1. You define functions in the tools parameter
  2. The model decides if a function should be called
  3. The model returns a tool_calls object with function name + arguments
  4. You execute the function and send the result back
  5. The model uses the result to form its final response

Example: Weather lookup

import json
from openai import OpenAI
 
client = OpenAI(api_key="sk-...", base_url="https://backend.sovereigneg.com/v1")
 
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name, e.g. Cairo, Riyadh"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit"
                    }
                },
                "required": ["city"]
            }
        }
    }
]
 
response = client.chat.completions.create(
    model="gpt-oss-20b",
    messages=[{"role": "user", "content": "What's the weather in Cairo?"}],
    tools=tools,
    tool_choice="auto"
)
 
message = response.choices[0].message
 
if message.tool_calls:
    for call in message.tool_calls:
        name = call.function.name
        args = json.loads(call.function.arguments)
        print(f"Model wants to call: {name}({args})")
 
        # Execute your function
        result = get_weather(**args)  # your implementation
 
        # Send result back
        followup = client.chat.completions.create(
            model="gpt-oss-20b",
            messages=[
                {"role": "user", "content": "What's the weather in Cairo?"},
                message,
                {
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": json.dumps(result)
                }
            ]
        )
        print(followup.choices[0].message.content)

Tool response format

When the model decides to call a function:

{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\": \"Cairo\", \"unit\": \"celsius\"}"
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

Multiple tools

You can define multiple functions. The model picks the right one based on the user's request:

tools = [
    {"type": "function", "function": {"name": "search_docs", ...}},
    {"type": "function", "function": {"name": "create_ticket", ...}},
    {"type": "function", "function": {"name": "get_user_info", ...}},
]

Controlling which tool is called

tool_choice decides whether — and which — tool the model may call. Four forms are accepted:

ValueBehaviour
"auto"The model decides whether to call a tool. Default when tools is present.
"none"Never call a tool. The model answers with text only, even though tools was sent.
"required"Must call at least one tool. Use when a plain-text answer is not acceptable.
{"type": "function", "function": {"name": "get_weather"}}Must call exactly this tool.
response = client.chat.completions.create(
    model="gpt-oss-20b",
    messages=[{"role": "user", "content": "What's the weather in Cairo?"}],
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "get_weather"}},
)

Forcing a specific tool is the reliable way to get structured data out of a model that would otherwise reply in prose — you define the shape you want as the tool's parameters schema and read it back from arguments.

Parallel tool calls

A model can request several tools in a single turn. tool_calls is an array, so always iterate it rather than reading tool_calls[0]:

message = response.choices[0].message
 
for call in message.tool_calls or []:
    result = dispatch(call.function.name, json.loads(call.function.arguments))
    followup_messages.append({
        "role": "tool",
        "tool_call_id": call.id,   # ← must match the call you are answering
        "content": json.dumps(result),
    })

Send one role: "tool" message per call, each carrying the matching tool_call_id, and append them all before your next request. Skipping one leaves the conversation in a state most models reject.

Set parallel_tool_calls: false if you want at most one call per turn.

Streaming tool calls

With stream: true, tool calls arrive in fragments that you must accumulate. The name and id come first; arguments then dribbles in as partial JSON, so any individual chunk is usually invalid JSON on its own:

data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_abc","type":"function",
                                           "function":{"name":"get_weather","arguments":""}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"ci"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ty\":\"Ca"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"iro\"}"}}]}}]}
data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}
data: [DONE]

Accumulate by index — that is what ties fragments to the call they belong to, and it is how parallel calls stay separated:

calls: dict[int, dict] = {}
 
for chunk in client.chat.completions.create(..., tools=tools, stream=True):
    for fragment in chunk.choices[0].delta.tool_calls or []:
        slot = calls.setdefault(fragment.index, {"id": "", "name": "", "arguments": ""})
        if fragment.id:
            slot["id"] = fragment.id
        if fragment.function.name:
            slot["name"] = fragment.function.name
        slot["arguments"] += fragment.function.arguments or ""
 
# Only now is each slot["arguments"] complete, parseable JSON.
for slot in calls.values():
    args = json.loads(slot["arguments"])

Only call json.loads once the stream ends. Parsing a fragment as it arrives is the single most common streaming tool-call bug.

Structured output with response_format

response_format constrains the model's text reply — a separate mechanism from tools, useful when you want JSON back but have no function to call:

response = client.chat.completions.create(
    model="gpt-oss-20b",
    messages=[{"role": "user", "content": "List 3 cities in Egypt."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "cities",
            "schema": {
                "type": "object",
                "properties": {"cities": {"type": "array", "items": {"type": "string"}}},
                "required": ["cities"],
            },
        },
    },
)

Supported values:

  • {"type": "text"} — the default, ordinary prose.
  • {"type": "json_schema", "json_schema": {...}} — the reply conforms to your schema. The json_schema object is required; omitting it returns a missing_json_schema error.
  • {"type": "json_object"} — valid JSON with no schema. Not available on Anthropic-backed models, which have no equivalent; those return a 400 naming the parameter rather than silently ignoring it. Use json_schema instead, or pick a model from an OpenAI-compatible provider.

Support varies by model. When you need a guarantee on a specific model, forcing a tool via tool_choice is the more portable route.

Supported models

Function calling requires a model that supports tools. Check the Model Library for models with a Tools badge — not every model supports tool calls.

Pick any live model with a Tools badge from the Model Library. Model IDs change — use GET /v1/models rather than hardcoding names from examples.

cURL quick-test

Confirm tool-calling is wired end-to-end without leaving your shell:

curl https://backend.sovereigneg.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<your-model-id>",
    "messages": [{"role":"user","content":"What is the weather in Cairo? Use the tool."}],
    "tools": [{
      "type":"function",
      "function":{
        "name":"get_weather",
        "description":"Get the current weather for a city",
        "parameters":{
          "type":"object",
          "properties":{"city":{"type":"string"}},
          "required":["city"]
        }
      }
    }],
    "tool_choice":"auto"
  }' | jq '.choices[0].message.tool_calls'
# expect: a list with one entry whose function.arguments contains {"city":"Cairo"}.

If you get tool_calls: null and a free-form text answer, the model may not support tool calling — try another model from the catalog or contact support.