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
- You define functions in the
toolsparameter - The model decides if a function should be called
- The model returns a
tool_callsobject with function name + arguments - You execute the function and send the result back
- 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://dev-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:
| Value | Behaviour |
|---|---|
"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. Thejson_schemaobject is required; omitting it returns amissing_json_schemaerror.{"type": "json_object"}— valid JSON with no schema. Not available on Anthropic-backed models, which have no equivalent; those return a400naming the parameter rather than silently ignoring it. Usejson_schemainstead, 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://dev-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.