cd /news/large-language-models/forcing-a-specific-tool-call-in-the-… · home topics large-language-models article
[ARTICLE · art-94452] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=· neutral

Forcing a Specific Tool Call in the Claude API

An engineer detailed how the Claude API's tool_choice parameter can be used to force a specific tool call, enabling developers to obtain structured JSON output by declaring a single tool and setting tool_choice to that tool. The technique guarantees a tool_use response with no preamble text, which is useful for extracting structured data from unstructured input, though it does not ensure the values are present in the source document.

read5 min views1 publishedAug 12, 2026

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.

── more in #large-language-models 4 stories · sorted by recency
── more on @claude api 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/forcing-a-specific-t…] indexed:0 read:5min 2026-08-12 ·