AI agents

When to Let the Model Choose the Tool vs. When to Route in Code

Antonio Evans7 min read

A top-down diagram of an orchestration layer: on the left, a user request enters a decision diamond labeled 'Router in code?'.
Key points

Key points

  1. 1Route in code when the tool set is small and fixed, when a wrong tool call is expensive, or when arguments must be validated before execution.
  2. 2Let the model choose when the tool space is open-ended or when the right tool depends on the meaning of free text you cannot enumerate in advance.
  3. 3A model-chosen tool call costs an extra model turn and adds latency, so measure branch frequency and cost rather than guessing.
  4. 4Log which branch fired and what it cost, then move any model-chosen path with a stable pattern into a deterministic route.

Two layers, not one decision

Most teams treat function calling as a feature they turn on. It is really a decision about the orchestration layer: who picks the next action, your code or the model. Once you separate the two, the trade-offs become concrete.

In the model-chosen path, you hand the model a list of tool schemas and it returns a tool name plus arguments. The model does not run anything. It pauses, your code executes the function, returns the result, and the model continues. That is an inherently multi-turn, interactive flow, as the structured outputs versus function calling breakdown describes. Function calling relies on instruction tuning: the model is trained to recognize when it lacks information or should take an action.

In the code-routed path, your orchestrator inspects the request and calls a function directly. No model turn is spent on selection. Function calling is a specialization of JSON-schema constrained decoding where the schema names a tool to invoke, according to the ZeroEntropy explainer on constrained decoding. Same machinery, different framing. The point is that both paths produce a name and an argument object; the question is whether a model or a branch statement produced them.

Orchestration framing helps here. The best architecture uses the least flexible control mechanism that can reliably solve each part of the workflow, the orchestration guide at Promptessor argues. Routing can be deterministic, model-driven, rule-based, or hybrid. A single agent plus several tools is often simpler than a fleet, and the same logic applies to selection: do not pay for a model decision where an if statement is both cheaper and more reliable.

The costs you are actually trading

Four dimensions matter: latency, cost, reliability, and debuggability. Each moves in a predictable direction.

Latency. A model-chosen tool call requires at least one full generation round trip before any tool executes, plus a second generation to synthesize the result. A code route can dispatch in microseconds. If you need sub-100ms overhead, the model cannot be in the selection path. Constrained decoding itself adds only a small per-step overhead, typically a few percent for regex and JSON-schema constraints, per ZeroEntropy, so the selection turn, not the constraint machinery, is where your latency goes.

Cost. Every model-chosen call spends input tokens on tool schemas and output tokens on the name and arguments. With a large tool set, schema tokens grow with every call. A code route spends nothing on selection.

Reliability. Here the naive comparison misleads. Function calling guarantees syntactic validity of the argument JSON but not semantic correctness: the model might generate valid JSON with nonsensical parameter values, Blaxel's function calling overview notes. A code route has no selection error at all, but it can route the wrong way if your rules are incomplete. The honest comparison is between model selection error and rule coverage gaps.

Debuggability. This is the least discussed and most decisive factor. A code route leaves a stack trace. A model route leaves a prompt, a sampling temperature, and a tool name. When a call goes wrong, you need to know whether the failure was tool selection, argument extraction, tool design, missing permissions, or excessive context, and those should be tracked as separate metrics, Paragon's tool-calling benchmark writeup argues.

DimensionCode routeModel chooses
Selection latencyMicroseconds to a few millisecondsOne model round trip plus synthesis turn
Token costNone for selectionSchemas in, arguments out, every call
Failure modeWrong rule firesWrong tool, wrong arguments, or no call
Argument checkingBefore execution, in your codeAfter generation, back in your code
DebuggingStack trace, logsPrompt, sampling settings, tool name
Adapts to new phrasingOnly what you anticipatedYes, within the tool descriptions

A worked example with numbers

Suppose a support assistant handles 200,000 requests a day and must pick among 4 tools: look up order, start refund, update address, escalate to human. Assume one model-chosen call adds 600 input tokens of tool schemas and 40 output tokens of arguments, at a blended rate of $0.50 per million tokens (a rate chosen for arithmetic, not a quoted price). Selection cost per request is 640 tokens times $0.50 divided by 1,000,000, which is $0.00032. Across 200,000 requests that is about $64 a day on selection alone, before the synthesis turn.

Now add the reliability math. If the model picks the wrong tool on 2% of requests, that is 4,000 wrong calls a day. If 10% of those wrong calls touch money (a refund started instead of an order lookup), you have 400 financially consequential errors daily. A four-branch router written as validation-then-dispatch can drive that class of error to zero for the cases it covers.

The decision flips when the tool set grows and the intents are open-ended. If a research assistant has 40 tools registered and users ask in free text, a router cannot enumerate the branches. At that point the model's semantic matching is worth its cost, and the right move is to shrink the tool set it sees per request rather than to abandon model selection.

The decision rule

Apply these four tests in order. The first one that fires decides.

  1. Is the tool set small (roughly a handful) and fixed? Route in code. You can enumerate the branches and test them.
  2. Is a wrong tool call expensive? Route in code, or add a confirmation gate before the model's choice executes. A typed tool name and argument object describe an intended action, and runtime authorization is a separate decision, TrueFoundry's structured outputs guide stresses.
  3. Must arguments be validated before execution? Validate either way, but validation before dispatch is easier when your code produced the arguments. Tool calling is structured output with consequence, as the same guide puts it.
  4. Do you need sub-100ms selection overhead? Route in code. If none of these fire, let the model choose.

The VC-backed default of "always let the model pick" fails test 1 and test 2 simultaneously for most internal tools.

Validating arguments on both paths

Whichever path you choose, the argument object needs a schema and a validator. Pydantic is the common choice: you define a model, generate a JSON schema for the tool definition, then validate the returned arguments against the same model. Jason Liu's Pydantic and LLM tutorial shows both halves, including Packages.model_validate_json on the tool-call arguments and Packages.model_json_schema() inside the tool definition. The separation of data from its description makes prompts easier to organize.

from pydantic import BaseModel, field_validator

class RefundRequest(BaseModel):
    order_id: str
    amount_cents: int

    @field_validator("amount_cents")
    @classmethod
    def cap_refund(cls, v: int) -> int:
        if v <= 0 or v > 50_000:
            raise ValueError("refund outside allowed range")
        return v

# same class feeds the tool schema and validates the model's arguments
args = RefundRequest.model_validate_json(tool_call.function.arguments)

That validator is your router's second safety net. It runs identically whether the arguments came from a model or from your own parser, and it enforces the domain rule (a refund cap) that no JSON schema keyword can express on its own.

Do not rely on the schema alone for correctness. Strict schema output narrows the interface but only within its supported contract; shape is not truth, and a schema-valid customer ID, amount, or date can still violate domain rules, per TrueFoundry. Keep schemas minimal with required fields, strict types, and enums to prevent ambiguous outputs, and log raw and parsed outputs with validation failure metrics so violations surface early, MLflow's structured outputs guide recommends.

Making model selection more reliable when you keep it

If the model must choose, improve the choice rather than the model. Tool descriptions carry more weight than most teams expect: models rely on the description to match user intent to the right function, and clear, specific descriptions reduce routing errors, Blaxel notes. Ambiguous queries sometimes trigger the wrong function or no function at all.

Three levers matter, in this order:

  • Model choice. Swapping the underlying LLM produced larger and more consistent swings than any prompt, schema, or routing change tested, per Paragon's evals. It is the lever most teams cannot pull freely, which is why the next two matter.
  • Description depth and prompt wording. These moved the numbers mostly on multi-tool tasks and were close to flat on single-tool tasks, same source. If you have one tool, stop rewriting the prompt.
  • Dynamic filtering. Send only the tools plausible for the current request. Fewer choices means fewer wrong choices and fewer schema tokens.

Then instrument. Distributed tracing that captures sessions, traces, and spans for each tool call, including inputs, outputs, latency, and errors, is the minimum for finding out why a selection went wrong, Maxim's tool-calling deep dive describes this pattern. Evaluate on the full trajectory, not just the final answer.

One more trap: silent truncation. If generation hits the token limit mid-payload, the JSON is cut off and most providers guarantee schema conformance only for complete generations. Always check the finish reason before parsing, per Tianpan's failure taxonomy, which also notes that complex schemas break constrained decoding in framework-specific ways.

Migrating a model-chosen path into code

The rule is to measure first and promote second. Log four fields for every request: the branch taken, the tool name the model chose, the validated arguments, and the wall-clock cost of selection plus execution. After two weeks you will see a distribution, not a guess.

Promote a path to code when all three hold: the same tool is chosen for more than 90% of requests in a recognizable intent class, the arguments follow a stable template that your code can construct from parsed input, and the path has produced at least one misroute you can explain. Keep it model-chosen while any of these fail.

A hybrid is often best. Route the top three intents in code, pass everything else to a model picker with a narrowed tool set, and require a confirmation gate on any tool that moves money or touches external systems. This keeps the deterministic path fast and the open-ended path possible.

When your tools span text, image, audio, and embedding models, a single API key for all of them removes a layer of provider-specific routing from the orchestration problem, which is what Quanta Inference provides. For teams that want the orchestration and the deliverables handled rather than assembled, Quanta Experts is the natural next step.

FAQ

FAQ

Does function calling mean my code never validates arguments?

No. Function calling guarantees the argument JSON is syntactically valid but not semantically correct; the model can emit valid JSON with nonsensical values. Parse the arguments against your own schema and run domain validators before executing anything, exactly as you would for a code route.

Can I get sub-100ms tool selection with a model in the loop?

Rarely, if the model is choosing. Selection costs a full generation round trip before any tool runs, plus a turn to synthesize the result. Code routes dispatch in microseconds. If your latency budget is tight, route the common cases in code and reserve model selection for the open-ended tail.

How many tools is too many to hand to a model?

There is no fixed cutoff, but schema tokens and selection error grow with the list. The practical fix is dynamic filtering: send only the tools plausible for the current request so the model sees a smaller menu. Measure tool correctness and task completion separately to catch the point where the menu is hurting you.

Why did my structured output pass validation and still break downstream?

Because shape is not truth. A schema-valid customer ID, amount, or date can still violate a business rule, and truncation at the token limit can produce a payload that looks complete but is not. Check the finish reason and add a domain validation layer after generation.

When should I move a model-chosen path into a deterministic route?

When it shows a stable pattern: one tool chosen for most requests in a recognizable intent class, argument templates your code can construct, and at least one explained misroute. Log branch, tool name, arguments, and cost first; you need the distribution before you can justify the migration.

Sources

Sources

  1. Structured Outputs vs. Function Calling: Which Should Your Agent ...
  2. Constrained decoding: forcing LLM output to a grammar - ZeroEntropy
  3. AI Agent Orchestration: Patterns, Workflows & Best Practices
  4. What Is LLM Function Calling?
  5. How to Optimize Tool Calling for AI Agents - Paragon
  6. LLM Structured Outputs with JSON Schema
  7. How to Use Pydantic for LLMs: Schema, Validation & Prompts
  8. Enforce, Validate, Observe: 3 LLM Structured Output ...
  9. Understanding Tool Calling Mechanisms in AI Agents
  10. Structured Outputs Are Not a Solved Problem: JSON Mode ...
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