Skip to content
View as Markdown

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:

text
This API key is set to tool-capable models only, and this model cannot take tool calls.

The loop ​

  1. Send the conversation with a tools list.
  2. If the reply has tool_calls, run each function with its arguments.
  3. Append the assistant message and one tool message per call, then send again.
  4. Repeat until the model answers with plain text.
python
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),
        })
ts
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:

json
{
  "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 ​

FieldWhat it does
toolsThe 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_callsWhether 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 as function_call output items; send results as function_call_output input items with the same call_id.
  • Anthropic Messages: tools: [{"name": "get_weather", "description": "...", "input_schema": {...}}]. Calls come back as tool_use content blocks; send results in a user message with tool_result blocks. 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.

FreeTheAI documentation. Paid models and plans are coming soon; the free tier has daily limits.