Tool calling end to end
One round trip with tool_choice "auto": the model asks for a tool, your code runs it, and the result goes back as a tool message for a final answer. This recipe covers auto only — the model decides whether to call the tool; forced tool_choice is not covered.
Quick start
python
import json, os
from openai import OpenAI
client = OpenAI(base_url="https://api.bricksum.com/v1", api_key=os.environ["KILN_API_KEY"])
tools = [{"type": "function", "function": {
"name": "get_weather",
"description": "Current weather for a city", # required
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
}}]
messages = [{"role": "user", "content": "What's the weather in Seoul?"}]
first = client.chat.completions.create(model="qwen3-32b", messages=messages,
tools=tools, tool_choice="auto", max_tokens=800)
choice = first.choices[0]
# The model may answer without calling the tool, so this is a check, not an index.
if not choice.message.tool_calls:
print(f'no tool call, finish_reason: "{choice.finish_reason}"')
print("answer:", choice.message.content)
else:
# One reply can carry several tool calls; answer every one of them.
messages.append(choice.message)
for call in choice.message.tool_calls:
print("tool call:", call.function.name, call.function.arguments)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps({"temp_c": 27})})
final = client.chat.completions.create(model="qwen3-32b", messages=messages, max_tokens=800)
print("final:", final.choices[0].message.content)javascript
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.bricksum.com/v1", apiKey: process.env.KILN_API_KEY });
const tools: OpenAI.ChatCompletionTool[] = [{ type: "function", function: {
name: "get_weather",
description: "Current weather for a city", // required
parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
}}];
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "user", content: "What's the weather in Seoul?" },
];
const first = await client.chat.completions.create({ model: "qwen3-32b", messages, tools, tool_choice: "auto", max_tokens: 800 });
// The model may answer without calling the tool, so this is a check, not an index.
const calls = first.choices[0].message.tool_calls ?? [];
if (calls.length === 0) {
console.log('no tool call, finish_reason: "' + first.choices[0].finish_reason + '"');
console.log("answer:", first.choices[0].message.content);
} else {
// One reply can carry several tool calls; answer every one of them.
messages.push(first.choices[0].message);
for (const call of calls) {
if (call.type !== "function") continue;
console.log("tool call:", call.function.name, call.function.arguments);
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ temp_c: 27 }) });
}
const final = await client.chat.completions.create({ model: "qwen3-32b", messages, max_tokens: 800 });
console.log("final:", final.choices[0].message.content);
}Verify
Run the script. The first call's message.tool_calls holds get_weather, with a JSON city argument (possibly more than one entry — each gets its own tool message); the second call's message.content is the model's own wording, so it varies run to run, but it references the temperature you supplied (27).
expected output
tool call: get_weather {
"city": "Seoul"
}
final: The current temperature in Seoul is **27 °C**.Config reference
tool_choice="auto"— The model decides whether to call a tool. Forced choice — "required" or naming a function — is unsupported. On qwen3-32b (measured 2026-09-26), forced tool choice fails without a clear error: tool_choice "required" returns HTTP 500 with a plain-text "Internal Server Error" body, and naming a function returns 200 with empty arguments (finish_reason "stop"). Neither is a 4xx, so do not retry it — send tool_choice "auto" only.tools[].function.description=required— Every tool needs one. A tool without it is rejected with 400.parallel_tool_calls=not applied— Applied only on models whose Parallel tool calls cell on the Models page is yes; qwen3-32b is not one of them. Measured on gpt-oss-120b: accepted with 200, and the model called at most one tool per reply. On qwen3-32b the parallel_tool_calls setting is not applied, which is not the same as replies holding one call: even with parallel_tool_calls: false, one reply can carry several tool calls. Answer every entry in tool_calls with its own tool message.max_tokens=800— Reasoning tokens are billed inside this budget. A small value can end the turn with finish_reason "length" before the tool call is emitted.
Limitations
- Forced tool_choice ("required", or naming a specific function) is not supported. Only "auto" is covered by this recipe. On qwen3-32b (measured 2026-09-26), forced tool choice fails without a clear error: tool_choice "required" returns HTTP 500 with a plain-text "Internal Server Error" body, and naming a function returns 200 with empty arguments (finish_reason "stop"). Neither is a 4xx, so do not retry it — send tool_choice "auto" only.
- On qwen3-32b the parallel_tool_calls setting is not applied, which is not the same as replies holding one call: even with parallel_tool_calls: false, one reply can carry several tool calls. Answer every entry in tool_calls with its own tool message.
Troubleshooting
HTTP 500 Internal Server Error— The request set tool_choice to "required". On qwen3-32b that fails with a plain-text 500 instead of a 4xx (measured 2026-09-26). Send tool_choice "auto". Retrying the same request returns the same 500."arguments": ""— The request named a function in tool_choice. On qwen3-32b that returns 200 with empty arguments and finish_reason "stop" (measured 2026-09-26), so JSON.parse / json.loads on them fails. Send tool_choice "auto" and describe in the prompt which tool you expect; check tool_calls before parsing arguments."message": "missing or malformed API key"— KILN_API_KEY is unset, expired or was not exported into the shell running the script. Create a new key in Console → API keys and export it again."code": "model_not_found"— The model field is not an ID that GET /v1/models returns for your key. Check the Models page or GET /v1/models and set model to an ID listed there, e.g. "qwen3-32b".finish_reason: "length"— max_tokens was reached before the model finished reasoning about whether to call the tool. Raise max_tokens above the 800 the quick start sends — for example to 1600 — in the first create() call.
Note
Snippets on this page were run against the Kiln API on 2026-09-21 with openai 3.16.2 (Python 3.14.7), openai 7.20.0 (Node 24.12.0). Not verified: This run was on gpt-oss-120b; the page now names qwen3-32b, which it has not yet been run against, so the expected output below may differ. The no-tool-call branch in both quick-start tabs was added after this run and has been checked only against a local stub server, not against staging. The loop that answers every tool call was added later still (#760) and has not been run against the API.