Tool calling

Describe your functions, and the model decides when to call them. You run the code; the model uses the result.

The model never executes anything. It returns a structured request to call a function, your code runs it, and you send the result back for the model to use in its answer.

The loop#

  • Send your message along with a list of tool definitions.
  • The model replies either with an answer, or with one or more tool calls and a finish_reason of tool_calls.
  • You run each call and append the result to the conversation.
  • You send the whole conversation again. Repeat until the model answers.

Defining a tool#

Chat Completions
{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city. Call this whenever the user asks about weather conditions.",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "City name, e.g. 'Hanoi' or 'Tokyo'"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "Temperature unit. Defaults to celsius."
        }
      },
      "required": ["city"]
    }
  }
}

The description is the prompt

The model chooses tools by reading their descriptions, so write them for a reader who cannot see your code. Say when to call it, not just what it does — "Call this whenever the user asks about weather" beats "weather function".

A complete round trip#

TypeScript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.XKIRO_API_KEY,
  baseURL: "https://api.xkiro.com/v1",
});

const tools = [
  {
    type: "function" as const,
    function: {
      name: "get_weather",
      description: "Get the current weather for a city.",
      parameters: {
        type: "object",
        properties: { city: { type: "string" } },
        required: ["city"],
      },
    },
  },
];

// Your real implementation goes here.
async function getWeather(city: string) {
  return { city, temperature: 31, condition: "humid" };
}

const messages: any[] = [
  { role: "user", content: "Should I take an umbrella in Hanoi today?" },
];

// Bound the loop. A model that keeps calling tools forever would otherwise
// spend your balance until something else breaks.
for (let step = 0; step < 5; step++) {
  const res = await client.chat.completions.create({
    model: "openai/gpt-5.6-sol",
    messages,
    tools,
  });

  const msg = res.choices[0].message;
  messages.push(msg);

  if (!msg.tool_calls?.length) {
    console.log(msg.content);
    break;
  }

  for (const call of msg.tool_calls) {
    // Arguments are a JSON *string* produced by a model, so parsing can fail.
    // Send the failure back as the tool result: the model can correct itself,
    // whereas a thrown exception ends the conversation.
    let result: unknown;
    try {
      const args = JSON.parse(call.function.arguments);
      result = await getWeather(args.city);
    } catch (err) {
      result = { error: `Invalid arguments: ${(err as Error).message}` };
    }

    messages.push({
      role: "tool",
      tool_call_id: call.id,
      content: JSON.stringify(result),
    });
  }
}

What a tool call looks like

Assistant message
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_a1b2c3",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Hanoi\"}"
      }
    }
  ]
}

arguments is a string, and the model wrote it

It is JSON-encoded text, not an object, and nothing guarantees it matches your schema. Always parse inside a try/catch and validate before using the values.

The Anthropic dialect#

Same concept, different field names: input_schema instead of parameters, and results travel as content blocks inside a user message.

Tool definition
{
  "name": "get_weather",
  "description": "Get the current weather for a city.",
  "input_schema": {
    "type": "object",
    "properties": { "city": { "type": "string" } },
    "required": ["city"]
  }
}
Returning a result
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01ABC",
      "content": "{\"temperature\":31,\"condition\":\"humid\"}"
    }
  ]
}

Forcing or preventing tool use#

  • tool_choice: "auto" — the model decides. The default, and usually right.
  • tool_choice: "none" — never call a tool this turn.
  • tool_choice: "required" — must call something.
  • tool_choice: { "type": "function", "function": { "name": "get_weather" } } — must call that one.

Forcing a tool turns off reasoning

Anthropic models cannot combine forced tool use with extended thinking. When you force a specific tool, xKiro keeps the tool constraint and drops thinking, because failing the request outright would be worse. See Reasoning.

Practical notes#

  • Names must be simple. Letters, digits, underscores and hyphens. Dots break some providers; xKiro rewrites them and maps the name back on the way out, but plain names avoid the problem entirely.
  • Fewer tools work better. Past roughly twenty, models start picking the wrong one. Group related actions behind one tool with a mode argument.
  • Return errors as data.A tool result saying "city not found" lets the model recover; a thrown exception ends the turn.
  • Tool definitions cost tokens on every call. They are part of the prompt each round, so a large schema is a recurring bill.