# Tool calling end to end > One round trip with tool_choice "auto": ask for a tool, run it, send the result back. Source: https://kiln.bricksum.com/docs/en/cookbook/tool-calling 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** ```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) ``` **TypeScript** ```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** ```text 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.