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_reasonoftool_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#
{
"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#
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
{
"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.
{
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}{
"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.
