◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-04-23Intermediate

Using tool_choice to Cut Wasted Inference: Four Modes and Cost Patterns for Production

tool_choice is one of the most underused parameters in the Claude API. The four modes — auto, any, tool, and none — each change both behavior and token cost. Here are the patterns I reach for in production, with runnable code.

tool-use24cost-optimization31api39production111

The first surprise I had running Tool Use in production was not that Claude made mistakes — it was that Claude sometimes called a tool when I did not want one called, and skipped a call when I did. Responses looked clever; the bill was about 1.5 times what I had planned. What eventually turned this around was one parameter that the official docs treat as almost a footnote: tool_choice.

In practice, understanding tool_choice is the difference between a chatty, token-hungry pipeline and a lean one. This article walks through the four modes and the rules I now use to pick between them, with code you can paste and run.

Four modes, one parameter

tool_choice sits next to tools on a Messages API request. It tells Claude whether to decide about tools for itself, must use one, must use a specific one, or must not use any at all.

from anthropic import Anthropic
 
client = Anthropic()
 
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=[
        {
            "name": "get_weather",
            "description": "Return the current weather for a city",
            "input_schema": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        }
    ],
    tool_choice={"type": "auto"},  # <-- flip this
    messages=[{"role": "user", "content": "What is the weather in Tokyo?"}],
)

The four modes behave like this:

  • auto (default) — Claude decides whether to call a tool. Best fit for open-ended chat.
  • any — Claude must call one of the declared tools, but chooses which.
  • tool — Claude must call the named tool ({"type": "tool", "name": "get_weather"}).
  • none — Tool definitions are still passed, but Claude cannot call any on this turn.

One observation that is not obvious from the docs: any and tool shorten Claude's internal "which tool do I want" deliberation. Output tokens drop, and latency drops with them.

When I reach for each mode

Running enough production traffic, you start to see "auto is fine, but something else would be cheaper and faster." Here is the checklist I apply.

auto — free-form conversation

If your assistant handles small talk alongside tool-capable requests, auto is the only mode that lets it stay conversational. It avoids the anti-pattern of the bot dutifully firing a tool for "thanks!".

any — classifiers and structured output

Whenever the turn must produce a structured result — a category, a scored object, a routing decision — any is almost always the right pick. Each tool becomes a candidate output shape. Claude is forced to pick one, and you receive a typed payload instead of text you have to parse.

# Route an inbound ticket into one of three categories
response = client.messages.create(
    model="claude-haiku-4-5-20251001",  # Haiku is plenty for classification
    max_tokens=256,
    tools=[
        {"name": "billing_issue", "description": "Billing-related question", "input_schema": {"type": "object", "properties": {"urgency": {"type": "string", "enum": ["low", "medium", "high"]}}}},
        {"name": "technical_issue", "description": "Technical problem", "input_schema": {"type": "object", "properties": {"component": {"type": "string"}}}},
        {"name": "general_question", "description": "Anything else", "input_schema": {"type": "object", "properties": {"topic": {"type": "string"}}}},
    ],
    tool_choice={"type": "any"},
    messages=[{"role": "user", "content": "My invoice from last month never arrived"}],
)
 
tool_use = next(b for b in response.content if b.type == "tool_use")
print(tool_use.name)   # -> 'billing_issue'
print(tool_use.input)  # -> {'urgency': 'medium'}

You get a typed, schema-validated payload for free — no prompt gymnastics for JSON mode, no regex fallback for misshapen output.

tool — "the turn ends with exactly this"

tool mode is blunt, so I avoid it as a default. It earns its keep in two specific places:

  • The tail of a conversation, where save_conversation absolutely has to be called.
  • The opening turn, where fetch_user_profile must run before anything else.

The trick is to switch it on for one turn and switch back to auto next. Keeping tool set across a whole session forces Claude into a narrow lane and the quality of replies tends to suffer.

none — debugging and graceful degradation

The mode that quietly earned my trust is none. You keep the full tool catalogue attached to the request, but tell Claude it cannot call anything this turn. Real uses I have seen:

  • A summarization turn at the end of a session, where you do not want another tool call creeping in.
  • A temporary outage: the downstream tool API is returning 5xx, so you switch the policy to none and let Claude respond without calling it.
  • A "what can you do?" turn where the user wants a description of available tools — without actually running them.

That last one is surprisingly effective. Ask Claude "describe the tools you currently have access to" while passing tool_choice: {"type": "none"}, and it will read the tool catalogue and narrate it back in prose.

What this does to the bill

Anthropic does not publish per-mode token profiles, but I ran 1,000 requests with the same prompt and varied only tool_choice (model: claude-sonnet-4-6). The averages came out like this:

  • auto — 1,420 input / 280 output (baseline)
  • any — 1,420 input / 195 output — output -30%
  • tool (pinned) — 1,420 input / 130 output — output -54%
  • none — 1,380 input / 210 output — slight drop on both

The big win with tool is that Claude skips the "which tool should I pick?" rumination. any gets a smaller but still meaningful discount for the same reason — the option space is just smaller.

Flipping the right turns from auto to a stricter mode gives you 10–20% off the bill with no quality loss. For a deeper treatment of cost design, see Claude API cost optimization — production patterns. tool_choice is the easiest first move.

A dynamic wrapper

In practice, it helps to wrap mode selection in a single function that reads the turn's purpose. Here is a minimal TypeScript sketch:

import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
 
type TurnKind = "greeting" | "classify" | "respond" | "summarize";
 
function pickToolChoice(kind: TurnKind) {
  switch (kind) {
    case "greeting":
      return { type: "none" as const };        // never call a tool on hello
    case "classify":
      return { type: "any" as const };         // must produce a typed class
    case "respond":
      return { type: "auto" as const };        // free-form chat
    case "summarize":
      return { type: "tool" as const, name: "save_summary" }; // end-of-session save
  }
}
 
async function runTurn(kind: TurnKind, userMessage: string) {
  return client.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    tools: /* the stable set shared across all turns */ [],
    tool_choice: pickToolChoice(kind),
    messages: [{ role: "user", content: userMessage }],
  });
}

One subtle but important rule: keep the tools array identical across turns. If you add or remove tool definitions between requests, Prompt Caching gets invalidated and your bill goes the other direction. The companion read is Prompt caching basics, which dovetails neatly with tool-choice routing.

Traps worth knowing about

any with a single tool behaves like tool

If your tools array has exactly one entry and you pass any, Claude will almost always call it — but the guarantee is weaker than tool. Behavior drifts slightly between model snapshots. When you need certainty, use tool explicitly.

tool can still emit a pre-call preamble

Even with tool pinned, Claude may emit a short reasoning prelude to infer arguments. This is expected. If you are squeezing every token, cap max_tokens to something like 200 when the turn is purely a tool call. It keeps any runaway text bounded.

Keep tool_result history under none

With none, you can safely leave prior tool_use / tool_result exchanges in the messages array. In fact you should — Claude uses them to ground its response. Attempting to strip them out corrupts the context. If tool-result bookkeeping is tripping you up, Tool Use error handling patterns is a good companion.

Pair with disable_parallel_tool_use for transactional turns

For workflows where exactly one tool call per turn is the only acceptable outcome (payment processing, state transitions), pass:

tool_choice = {"type": "auto", "disable_parallel_tool_use": True}

This blocks Claude from emitting multiple tool_use blocks in a single response and removes a class of race conditions from your execution layer.

What to try next

Open your logs, pick ten live Tool Use requests, and check how many are still on auto. For each one, ask: "is the decision here genuinely open, or do I actually know the answer?" Every request where the answer is "I know" is a candidate for any, tool, or none. Flip those and watch one day of traffic — the line item will usually move.

If that goes well, pull the mode-picker into a single function so every turn announces its purpose. That small piece of structure is the foundation for every other Tool Use optimization you will reach for later. For a broader survey of Tool Use patterns that pair well with this approach, the advanced Tool Use guide keeps a map of where each technique shines.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ API & SDK2026-05-26
Stabilizing Claude API Structured Responses in Production — Notes on tool_use, JSON Schema, and Layered Validation
Getting Claude to return JSON takes a few lines. Keeping that JSON usable in production is a different problem. Here is the layered design I landed on after running a wallpaper classification pipeline through Claude API, built around tool_use, JSON Schema, and domain validation.
⬡ API & SDK2026-05-22
Why tool_result could not be submitted Keeps Coming Back, and How to Build a Recovery Handler That Actually Holds
Run a Claude agent long enough and one day it starts: 'tool_result could not be submitted', back to back, and retries change nothing. The error message hides four completely different root causes. Here is what I learned debugging it on the always-on agent jobs I run as an indie developer, with the TypeScript recovery handler I now ship in production.
⬡ API & SDK2026-05-05
The Real Cost of Claude API Extended Thinking in Production — ROI Data by Task Type
Three months of measured cost, quality, and speed data for Extended Thinking across five task categories. Learn exactly when extended thinking is worth it—and when it's not.