Skip to content
View as Markdown

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)

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.