Tool calls
Tool calls (function calling) let a model ask your code to run a function and use the result. FreeTheAI supports them on Chat Completions, Responses, and Anthropic Messages, streamed or not, in each format's standard shape. Coding agents such as Claude Code, Codex, Cline, and OpenCode rely on them.
Which models can call tools
fta/zai/glm-5.3 and fta/kimi/k3 handle tool calls well and were tested with Claude Code and Codex. The fta/dog/ and fta/lion/ models cannot make tool calls.
For agents, turn on Tool-capable models only on your key (key settings). /v1/models then lists only models that can call tools, and any other model is refused with:
This API key is set to tool-capable models only, and this model cannot take tool calls.The loop
- Send the conversation with a
toolslist. - If the reply has
tool_calls, run each function with its arguments. - Append the assistant message and one
toolmessage per call, then send again. - Repeat until the model answers with plain text.
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.freetheai.org/v1", api_key=os.environ["FTAI_KEY"])
def get_weather(city: str) -> dict:
# Replace with a real lookup.
return {"city": city, "temperature_c": 18, "conditions": "cloudy"}
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "City name"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "Should I take an umbrella in Lisbon today?"}]
while True:
reply = client.chat.completions.create(
model="fta/zai/glm-5.3", messages=messages, tools=tools,
)
message = reply.choices[0].message
if not message.tool_calls:
print(message.content)
break
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
args = json.loads(call.function.arguments or "{}")
result = get_weather(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})import OpenAI from 'openai'
import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions'
const client = new OpenAI({ baseURL: 'https://api.freetheai.org/v1', apiKey: process.env.FTAI_KEY })
function getWeather(city: string) {
// Replace with a real lookup.
return { city, temperature_c: 18, conditions: 'cloudy' }
}
const tools: ChatCompletionTool[] = [{
type: 'function',
function: {
name: 'get_weather',
description: 'Get the current weather for a city.',
parameters: {
type: 'object',
properties: { city: { type: 'string', description: 'City name' } },
required: ['city'],
},
},
}]
const messages: ChatCompletionMessageParam[] = [
{ role: 'user', content: 'Should I take an umbrella in Lisbon today?' },
]
while (true) {
const reply = await client.chat.completions.create({ model: 'fta/zai/glm-5.3', messages, tools })
const message = reply.choices[0].message
if (!message.tool_calls?.length) {
console.log(message.content)
break
}
messages.push(message)
for (const call of message.tool_calls) {
if (call.type !== 'function') continue
const args = JSON.parse(call.function.arguments || '{}')
messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(getWeather(args.city)) })
}
}The assistant message with a tool call looks like this:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Lisbon\"}" }
}
]
}finish_reason is tool_calls when the model stops to call a tool.
Options
| Field | What it does |
|---|---|
tools | The functions the model may call, each with a JSON Schema for its parameters. |
tool_choice | "auto" (default), "none", "required", or {"type": "function", "function": {"name": "get_weather"}} to force one. |
parallel_tool_calls | Whether the model may ask for several calls in one turn, where the model supports it. Handle every entry in tool_calls either way. |
Arguments arrive as a JSON string. Parse them defensively: a model can produce invalid or incomplete JSON, so catch the error and send it back as the tool result.
Streaming tool calls
When streaming, tool calls arrive in pieces in delta.tool_calls, keyed by index. Join the function.arguments pieces for each index before parsing. The OpenAI SDKs' helpers (for example client.chat.completions.stream(...) in Python) do this for you.
Responses and Anthropic Messages
The other two formats use their own standard tool shapes:
- Responses:
tools: [{"type": "function", "name": "get_weather", "parameters": {...}}]. Calls come back asfunction_calloutput items; send results asfunction_call_outputinput items with the samecall_id. - Anthropic Messages:
tools: [{"name": "get_weather", "description": "...", "input_schema": {...}}]. Calls come back astool_usecontent blocks; send results in ausermessage withtool_resultblocks. See the Anthropic SDK example.
Web search as a tool
The fta/dog/ and fta/lion/ models can search the web on their own when you ask for it with web_search_options (Chat Completions) or a web_search tool type (Responses and Messages). For other models, call /v1/search yourself, for example from a tool. See Web search.

