tool_choice
decides whether the model may call a tool, must call some tool, must call one particular tool, or may not call any. The interesting part is not the parameter — it is what each setting removes from the response.
tool_choice
is a top-level object with a type
discriminator. All four forms:
"tool_choice": {"type": "auto"} // default
"tool_choice": {"type": "any"} // some tool, model picks
"tool_choice": {"type": "tool", "name": "get_deploy_status"} // this exact tool
"tool_choice": {"type": "none"} // no tools this turn
auto
is what you get when the parameter is omitted and tools
is present. none
is what you get when there are no tools at all, and setting it explicitly while leaving the tools
array in place is the way to keep the tool definitions in the cached prefix while forbidding calls for one turn.
With auto
, the model chooses. The response may contain only text, with stop_reason: "end_turn"
:
{
"content": [{"type": "text", "text": "Deploy ids look like dpl_XXXX — which one?"}],
"stop_reason": "end_turn"
}
Or text plus one or more tool_use
blocks, with stop_reason: "tool_use"
. Both are normal and your code has to handle both.
With any
or tool
, the model is required to call. stop_reason
is "tool_use"
on every response, and — this is the part worth internalising — the preamble text disappears. A forced response is the tool call and nothing else:
// tool_choice: {"type": "tool", "name": "get_deploy_status"}
{
"content": [
{"type": "tool_use", "id": "toolu_01B7…", "name": "get_deploy_status",
"input": {"deploy_id": "dpl_8f21"}}
],
"stop_reason": "tool_use"
}
There is no text block to render, and if your UI expected one it now shows a blank turn. That is the trade: you gain a guaranteed shape and lose the model’s ability to say “I need more information first”.
The difference between any
and tool
is only who picks. With any
and three tools declared, you will get exactly one of the three and you will not know which until you read name
. With tool
you know before you send the request.
The most common reason to force a specific tool has nothing to do with tools. A tool is a JSON Schema the model must fill in, so declaring one tool and forcing it is a way to get a guaranteed object back:
{
"model": "claude-opus-4-6",
"max_tokens": 1024,
"tools": [{
"name": "record_incident",
"description": "Record the structured fields of an incident report.",
"input_schema": {
"type": "object",
"properties": {
"severity": {"type": "string", "enum": ["sev1", "sev2", "sev3"]},
"component": {"type": "string"},
"customer_impact": {"type": "boolean"}
},
"required": ["severity", "component", "customer_impact"]
}
}],
"tool_choice": {"type": "tool", "name": "record_incident"},
"messages": [{"role": "user", "content": "<incident channel transcript>"}]
}
You never execute record_incident
. You read content[0].input
and that is your parsed object — no fenced code blocks to strip, no leading “Here is the JSON” to discard. The enum
constrains severity
to three values in a way a prompt instruction does not. This pattern predates structured outputs and remains useful because it works on every model that supports tools; see getting JSON out via tool_choice for the full technique.
There is one thing to check that the schema does not guarantee. A forced call means the model produced a value for each required field, not that the value was in the document. Asked to extract a severity from a transcript that never states one, the model must still emit one of the three enum members. The schema constrains the shape and nothing else, so a nullable field or an explicit "unknown"
member is usually worth adding — it gives the model somewhere honest to put the answer.
This is the failure that costs people real money, and it comes from treating tool_choice
as configuration rather than as a per-request decision.
Force a tool, get the call, execute it, append the tool_result
, and send the conversation back with the same request-building code — which still sets tool_choice: {"type": "tool"}
. The model is again required to call, so it calls again. You execute again, append again, and send again. Nothing errors. Every response is a valid 200 with stop_reason: "tool_use"
, and the loop runs until your iteration cap catches it or, if there is no cap, until someone notices the spend.
// wrong: the same tool_choice on every iteration
while (msg.stop_reason === "tool_use") {
msg = await client.messages.create({
model, max_tokens, tools, messages,
tool_choice: { type: "tool", name: "record_incident" }, // never relaxes
});
}
// right: force the first call only, then let the model finish
let toolChoice = { type: "tool", name: "record_incident" };
while (true) {
const msg = await client.messages.create({
model, max_tokens, tools, messages, tool_choice: toolChoice,
});
toolChoice = { type: "auto" }; // relax after the first turn
if (msg.stop_reason !== "tool_use") break;
// … execute, append assistant turn and tool_result …
}
The same trap exists in a milder form with {"type": "any"}
, which is worse to diagnose because the model varies which tool it calls and the transcript looks like exploration rather than a stuck loop.
The general rule: tool_choice
belongs to a turn, not to a conversation. If the reason you forced the call was to guarantee the first action, relax it the moment that action has happened. And if you are using the forced-tool pattern purely for extraction — one tool, no execution, read input
and stop — then there is no second turn at all, and the loop is the bug rather than the design.
auto
.auto
: the common shape is to force the initial call and then let the model decide whether it needs more.tool_choice
is not part of the cached prefix in the way the tools
array is, so you can vary it per request without paying to re-cache the tool definitions.Any tool_choice
value can carry disable_parallel_tool_use
, which caps the response at one tool_use
block:
"tool_choice": {"type": "auto", "disable_parallel_tool_use": true}
By default the model may emit several calls in one turn, which is faster when the calls are independent and wrong when they are not — a tool that mutates state usually wants to see the result of call one before deciding on call two. Setting the flag serialises the loop at the cost of a round trip per call.