DEV Community

Multigrid
Multigrid

Posted on • Originally published at multigrid.ai

Forcing a Specific Tool Call in the Claude API

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.

The four variants

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
Enter fullscreen mode Exit fullscreen mode

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.

What each one does to the response

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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.

Forcing a tool as an extraction technique

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>"}]
}
Enter fullscreen mode Exit fullscreen mode

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.

The loop that never ends

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 …
}
Enter fullscreen mode Exit fullscreen mode

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.

Constraints that come with forcing

  • You give up the escape hatch. Forced to call, a model with insufficient information will call anyway, guessing or leaving fields empty rather than asking. If a clarifying question is an acceptable outcome, use auto.
  • It interacts with thinking. On models with extended or adaptive thinking, forcing a tool call is restricted — on some platforms it requires thinking to be explicitly disabled, and the combination otherwise errors. The mechanism is that a forced first block conflicts with a thinking block being the first block. Check the current rule in Anthropic’s tool use documentation for the model you are on.
  • It is a per-request setting, not a per-conversation one. Force the first turn and leave later turns on auto: the common shape is to force the initial call and then let the model decide whether it needs more.
  • Changing it does not invalidate the prompt cache. 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.

Turning off parallel calls

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}
Enter fullscreen mode Exit fullscreen mode

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.

Related

Top comments (0)