AI coding

When a Router Picks a Tool the Agent Cannot Call

Antonio Evans11 min read

A small router box passes one locked tool card to a larger agent box that stamps it with a red X while a receipt of token counts unrolls through three retry arrows.
Key points

Key points

  1. 1A misrouted tool call that fails on authority (the tool exists but the agent's credentials or scopes do not allow it) is far more expensive than a call that fails on fit, because retrying cannot fix it.
  2. 2The naive retry loop resends the same rejected request, and each round trip pays for the same prompt plus the same refused tool call, so cost scales with attempts while the success probability stays near zero.
  3. 3Server-side tool selection through a filtered catalog per agent removes unauthorized tools from the candidate set before the router ever scores them, which is cheaper than detecting and recovering from the rejection.
  4. 4Authorization failures surface as 401 or 403, or as a tool-not-found when the registry hides what an agent may not use, and a genuine permission denial has to be distinguished from an expired or revoked token.

Why the misroute you are not costing is the one that fails on authority

A router model does one job: given a user request and a catalog of tools, it picks the tool whose description best matches the intent. That is a fit decision. Function calling, as documented by OpenAI, is built around exactly this: you pass a list of tools, the model returns a tool call with a name and JSON arguments, and your application executes it. The model never executes the function itself, and it has no reliable view of whether the calling identity holds the scope that tool requires.

That gap is where authority failures live. Fit and authority are independent dimensions. A tool can be a perfect fit and completely out of scope. A 7B router asked to "issue a refund for order 4471" will happily select a issue_refund tool if that tool is in its list, even when the agent's OAuth grant carries read-only billing scope. The route was correct. The execution will be refused.

Research on small language models in tool systems shows this failure class is structural, not a prompt-tuning bug. A study presented at ACL 2026 found that small models hallucinate plausible but nonexistent tool names because the schemas they receive do not match naming conventions internalized during pretraining, and that adapting the schema to the model cut those schema misalignment errors by 80% (Lee et al., ACL 2026). The same mechanism applies to authority: a name like admin_refund or delete_account is strongly associated in pretraining with capability, so a small model gravitates toward it whether or not the current identity can call it.

The decision rule that follows is simple and worth stating plainly. If a failure is caused by fit, retrying with a better prompt or a larger model can help. If it is caused by authority, no amount of retrying or escalation will help until the permission graph changes. The rest of this article is about costing the second case and engineering it out.

The error path, step by step, with the token bill attached

Walk the failed call through its lifecycle. A planner or router classifies the request, picks a tool, and your orchestrator dispatches it. The executor checks the agent's credentials, calls the upstream API, and gets a 403 with an insufficient-scope error. That response goes back into the conversation as a tool result, and the model is asked to continue.

Each of those steps has a price. The cost is dominated by what gets resent, not by the size of the refusal. A typical agent context after a few turns includes a system prompt, a tool catalog, conversation history, and prior tool outputs. If that context is 6,000 tokens and the tool catalog is another 2,000, then every retry round trip sends roughly 8,000 input tokens, plus a few hundred output tokens for the reasoning and the refused tool call. At a low input price this looks trivial per call. It compounds quickly across thousands of requests.

Tool-call failures are also routine in production, not exceptional. One production write-up reports that individual tool calls fail 3 to 15 percent of the time in well-engineered systems due to timeouts, rate limits, and upstream interruptions, and that at 99 percent per-step reliability a 10-step workflow succeeds only about 90.4 percent of the time (Scalekit). Authority failures are worse than that baseline because they do not resolve on their own: the same credential will keep being refused.

Here is a worked example of the naive path. Suppose a support agent routes 20,000 tool requests a day, and 2 percent pick a tool the agent is not scoped for. That is 400 authority misroutes. Assume a naive retry loop that makes 3 attempts, escalating the final attempt to a larger, more expensive model. Assume 8,000 input tokens and 400 output tokens per attempt.

StepCalls per misrouteInput tokensOutput tokensNotes
First attempt (router tier)18,000400Rejected with 403, refused call is well-formed
Retry 1 (same tier)18,000400Identical request, identical refusal
Retry 2 (escalated)18,000400Larger model, still unauthorized
Total per misroute324,0001,200Zero progress toward success
Daily total at 400 misroutes1,200 calls9,600,000480,000All of it wasted

The escalation step is where the bill gets ugly. Research on agentic routing quantifies what the authors call token inflation, the ratio of true workflow cost to single-call cost. They measured inflation as high as 4.25 times for a 7B model on multi-hop question answering, and separately found that forwarding a failed reasoning chain to a stronger model reduces that model's accuracy by up to 34.8 percentage points (Fu et al., arXiv). The second finding matters here: an escalation that carries the failed attempt in context is not just expensive, it can be counterproductive, because the bigger model inherits the small model's framing of the problem.

Put a price on the daily total above. At an illustrative one dollar per million input tokens and three dollars per million output tokens, the 9.6 million wasted input tokens are about $9.60 and the 480,000 output tokens about $1.44, so roughly $11 a day, or about $330 a month, for zero value delivered. Replace those numbers with your own contracted rates; the multiplication is the point. If you run the retry loop four times a day across a bigger fleet, the wasted spend scales linearly with retry count while the failure rate does not move.

Separating a refusal from a token problem

Not every 401 or 403 means "you may not call this tool." The same status code can arise from three different causes, and treating them identically is how retry storms begin. The production guide from Scalekit is explicit that a 401 from an expired access token and a 401 from a revoked refresh token look identical at the execution layer but need completely different responses.

SignalLikely causeCorrect responseCorrect response after escalation?
401, token expiredShort-lived access token aged outRefresh token, retry onceNo, refresh is the fix
401, refresh revokedUser disconnected the app or consent withdrawnDo not retry; tell the userNo, only re-consent helps
403, insufficient scopeAgent grant lacks the required scopeStop; route to a different tool or escalate to a humanNo, a bigger model has the same grant
Tool-not-foundRegistry filtered the tool out for this agentDo not retry; treat as an authorization boundaryNo, the tool is not a candidate
200 with empty bodyCommon when a connector swallows an auth failureTreat as failure, not as valid empty stateNo, this is the silent-failure case

That empty-body row deserves stress. In the Scalekit example, a Salesforce query ran against a delegated OAuth token that a user had revoked, the connector returned an empty result set without surfacing the auth failure, and the agent treated empty context as valid state and continued routing tickets. No exception was raised. From every observability surface the team had, the run looked successful. Authority failures do not always announce themselves.

A policy or capability signal reads better than an HTTP status. The NHI Management Group's FAQ on diagnosing agent tool call failures lays out the diagnostic logic: if the inputs are malformed, it is a prompt or orchestration problem; if the inputs are valid but the provider returns an explicit error, expect authorization, quota, or a similar upstream control; if request and response both look clean but the answer is empty or wrong, suspect the tool's contract or data source. A valid, well-formed tool call that is refused is almost always authorization.

The fix: filter the catalog before the router sees it

The most expensive misroute is the one that fails on authority, and the cheapest way to prevent it is to make it impossible. Filter the tool catalog per agent, per task, before the router scores anything. An unauthorized tool should never be a candidate, so the router cannot pick it even by accident.

This is a server-side decision, not a prompt instruction. IBM's tool calling overview makes the control boundary explicit: the model proposes a tool call, and the application owns which tools are available, which arguments are allowed, whether the user has permission, and how errors and retries are handled. A practical comparison of function calling and structured outputs puts the same point in the loop: validate the structure, apply business rules and authorization, then execute and verify, and note that valid JSON alone does not prove success.

Here is the pattern in code. The filter runs where the tools are built, not where the prompt is written.

from dataclasses import dataclass

@dataclass(frozen=True)
class Tool:
    name: str
    required_scope: str

AUTH_ERRORS = {"insufficient_scope", "revoked", "unauthorized_agent"}

def filter_catalog(catalog: list[Tool], granted_scopes: set[str]) -> list[Tool]:
    """Only tools the agent can actually call reach the router."""
    return [t for t in catalog if t.required_scope in granted_scopes]

def handle_tool_result(result: dict, attempt: int) -> str:
    """Return 'retry', 'stop' or 'escalate_human'."""
    if result.get("ok"):
        return "done"
    code = result.get("error_code")
    if code in AUTH_ERRORS:
        # No retry, no bigger model. The grant will not change.
        return "escalate_human"
    if code == "rate_limited" and attempt < 3:
        return "retry"          # transient: back off and retry
    if code == "upstream_5xx" and attempt < 2:
        return "retry"          # transient: retry once
    return "stop"

The second function encodes the decision rule. Authorization errors map to escalation or a clear user-facing message, never to a retry. Rate limits and transient upstream failures map to bounded retries with backoff, because those are the failures where the same request can succeed. Keeping those classes apart is the difference between a retry policy and a retry storm; the Scalekit failure taxonomy splits identity and auth, connector and proxy, upstream API, and execution semantics precisely because each layer needs its own recovery path.

For multi-turn state, this filtering works best when the tool surface is explicit for the duration of a task. The Model Context Protocol models a task as a first-class authorization boundary: a task carries which agent executes, which user initiated it, which tools are in scope, and a lifetime, and permissions are deleted when the task completes (Auth0). MCP itself is versioned and negotiates per request, with servers rejecting unsupported requests with a versioned error rather than a generic failure (MCP versioning docs), which is a useful model for how an executor should report a scoped refusal.

If you are building a catalog that spans many models and modalities, the filter is only as good as the tool registry behind it. Quanta Inference gives you one OpenAI-compatible API across text, image, video, audio and embedding models, so the router tier and the executor tier can share a single key and a single billing surface while your permission filter stays in your own code.

Do not carry a failed chain into the escalation

The instinct when a small model picks an unauthorized tool is to escalate to a bigger model. That instinct is right about the model and wrong about the payload. Sending the whole failed chain to the stronger model gives it the small model's failed reasoning to build on, and the measured cost is large: the inflation-aware routing study found that forwarding a failed reasoning chain to a stronger model reduces that model's accuracy by up to 34.8 percentage points (Fu et al., arXiv). Their response was a fresh-escalation policy that discards the failed chain before routing to a stronger model.

Authority failures make this even cleaner. If the failure is a scope denial, escalating at all is a waste, because a bigger model with the same grant will produce the same well-formed call and receive the same 403. The correct move is to stop, surface a clear message, and either ask the user to grant the scope or route to a different path that the current identity is permitted to take. Reserve escalation for fit failures, where a stronger model genuinely changes the outcome, and send it a fresh prompt.

A four-stage router illustrates where the cost goes: measure token inflation across model tiers and task types, predict the retries a query will need from a pre-execution difficulty signal, score candidates by expected accuracy divided by predicted true cost, and escalate with a fresh prompt only when the chosen model inflates beyond prediction. Even in that harness, inflation for a 7B model on multi-hop QA reached 4.25 times while the frontier model sat at 1.31 times, which is the gap that authority-blind routing hides.

What to log so the next misroute is cheap

You cannot fix what you cannot see, and authority failures are quiet by default. The minimum viable log record for a tool call is the request, the permission context, and the response path, so an operator can tell whether the fault was prompt shape, policy, or tool chain without guessing (NHI Management Group).

Log these fields on every tool call:

  • The tool name the model selected, and whether it was in the filtered catalog for this agent.
  • The scope the tool requires and the scopes the agent's token actually carried.
  • The token ID, so an expired or revoked credential is distinguishable from a missing scope.
  • The call_id returned by the model, so the refusal is tied to a specific call in the trace, since every tool result should reference the originating call (OpenAI).
  • The result class: success, transient failure, authorization failure, or silent empty.

Then read two metrics weekly. Misroute rate on authority is the share of routed calls that fail because of a missing scope or a filtered tool; target it at zero, because the filter should prevent it. Retry spend on authority is the tokens spent on retries that were introduced by an authorization failure; it is pure waste. Both go to zero as the catalog filter tightens, and neither shows up in a latency dashboard.

For teams running many agents with different grants, the operational load is in keeping scope grants and tool catalogs in sync as agents gain and lose capabilities. That is the kind of plumbing Quanta Experts addresses by packaging a working agent that reads your context and hands back finished deliverables, rather than making every team rebuild the permission filter from scratch.

A checklist before the next release

Run this against a staging copy of the catalog before adding tools to production.

CheckPass conditionWhy it matters
Catalog is filtered per agentNo tool reaches the router without a matching granted scopeRemoves the authority misroute before scoring
Router prompt shows only filtered toolsThe list passed to the model equals the filtered listA prompt-only "do not call" instruction is not a boundary
Retry policy refuses on authority401, 403 and tool-not-found never trigger a model retryRetrying a scope denial cannot succeed
Refresh is distinguished from revokeExpired tokens refresh; revoked tokens stopPrevents retry storms from a single revoked grant
Escalation is freshFailed chains are discarded before a bigger model is calledA carried failed chain can lose up to 34.8 points
Empty responses are failuresA 200 with no body is logged and stoppedSilent auth failures look like success
Denials are logged with scope contextEach refusal records required and granted scopes plus the call idMakes the next misroute diagnosable in minutes

The order matters. Filtering first means the router never sees a candidate it cannot use, and every other check is a second line of defense. Retry discipline comes second because it controls cost. Logging comes third because it controls how fast you find the leaks you missed.

FAQ

FAQ

Can a router model see which tools an agent is authorized to use?

Not reliably. A router picks by fit from the list it is given, and it has no trustworthy view of the calling identity's scopes. Treat authority as your concern: filter the catalog per agent before the router scores it, so an unauthorized tool is never a candidate.

Why is a 403 from a tool different from a 429 from the same tool?

A 429 is transient and the same request can succeed after a backoff, so a bounded retry is correct. A 403 from insufficient scope is a statement about the credential, and the same request will be refused every time (Scalekit). Retrying it burns tokens without changing the outcome.

How much does the naive retry loop actually cost?

It depends on your context size and rates, but the waste is real and recurring. In the worked example above, 400 daily authority misroutes retried three times each burned about 9.6 million input tokens and 480,000 output tokens a day for no progress. Measure your own context length and multiply.

Should I escalate to a bigger model when a small model picks the wrong tool?

Only when the failure is about fit. If the tool was refused for lack of scope, a bigger model with the same grant gets the same refusal, so escalate to a human or ask the user for consent. When fit is the problem, escalate with a fresh prompt, because forwarding a failed chain can reduce the stronger model's accuracy by up to 34.8 percentage points (Fu et al., arXiv).

My connector returns an empty list instead of a 403. Is that a success?

No. An empty body from a connector that swallowed an auth failure is a silent failure, and the agent will treat empty context as valid state (Scalekit). Log it as a failure, stop the workflow, and inspect the token and scope that were presented. Name-based schema matching tools can help reconcile tool names and descriptions when the mismatch is in the schema rather than the credential (LLM-Matcher, ACM).

Sources

Sources

  1. Function calling | OpenAI API
  2. Don't Adapt Small Language Models for Tools
  3. Tool Call Failures in Production: Debug and Prevent Guide
  4. Inflation-Aware Routing for Agentic LLM Systems
  5. What are the signs that an agent tool call failed because of ...
  6. What Is Tool Calling? | IBM
  7. Function Calling vs Structured Outputs: 5 Practical Examples
  8. Intent, Not Just Permissions: Rethinking Authorization for AI ...
  9. Versioning
  10. LLM-Matcher: A Name-Based Schema Matching Tool ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles