<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TokenLat</title>
    <description>The latest articles on DEV Community by TokenLat (@tokenlat).</description>
    <link>https://dev.to/tokenlat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4028280%2F94d16010-df94-45df-aee5-7d51647fcb74.png</url>
      <title>DEV Community: TokenLat</title>
      <link>https://dev.to/tokenlat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tokenlat"/>
    <language>en</language>
    <item>
      <title>Deterministic delivery is an architecture decision, not a model setting</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Fri, 06 Nov 2026 05:40:25 +0000</pubDate>
      <link>https://dev.to/tokenlat/deterministic-delivery-is-an-architecture-decision-not-a-model-setting-2091</link>
      <guid>https://dev.to/tokenlat/deterministic-delivery-is-an-architecture-decision-not-a-model-setting-2091</guid>
      <description>&lt;h2&gt;
  
  
  1. The argument is being filed under the wrong heading
&lt;/h2&gt;

&lt;p&gt;Most debates about agent reliability get filed as model problems. The proposed fix is a better model, a lower temperature, a longer system prompt, a different framework, or more evals. All of those can help. None of them change where the decision actually lives.&lt;/p&gt;

&lt;p&gt;Our position is that reliability in an agent system is not a property of the model. It is a property of the architecture around it: which decisions the model is allowed to make, which ones are made by ordinary code, and whether the system keeps enough evidence to tell the two apart afterwards.&lt;/p&gt;

&lt;p&gt;That is a technical position, not a product claim. The phrase we use for it is deterministic delivery, and it is worth stating precisely, because it is easy to oversell.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What the position actually says
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Deterministic delivery&lt;/strong&gt; is controllability, auditability and fallbacks at the architecture level. It is explicitly not a claim that the model will always be right. A model reading an ambiguous email can still get it wrong inside a perfectly built backbone.&lt;/p&gt;

&lt;p&gt;Three conditions have to hold before we'd use the phrase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every decision node leaves a replayable, traceable record: inputs, outputs, latency, and the acceptance criterion it was supposed to meet.&lt;/li&gt;
&lt;li&gt;Rule logic is deterministic by construction — same input, same state transition, same audit entry — while semantic nodes stay probabilistic and are measured rather than assumed.&lt;/li&gt;
&lt;li&gt;A failure can be located to a node, not just observed as a wrong final answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those is missing, adding tests or tuning prompts treats a symptom. The gap is architectural.&lt;/p&gt;

&lt;p&gt;This also fixes a common misreading. "Deterministic" here does not describe the model's output. It describes the parts of the system that are not allowed to improvise.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The split, and where the other side is right
&lt;/h2&gt;

&lt;p&gt;The alternative is a probabilistic black box: each step decides what happens next, state lives inside the prose, a retry is a model call that sounds like a retry, authorization is a line in a prompt asking nicely. When the output is wrong, you get a paragraph, not a location.&lt;/p&gt;

&lt;p&gt;We'd argue that's the wrong default for production. But it is not wrong everywhere, and a position that cannot say when its opposite is fine is a preference, not a position. For a one-off script, a throwaway prototype, or internal exploration where a wrong answer costs nothing and nobody has to explain it later, chaining model calls is the cheaper and more honest choice. A deterministic backbone earns its keep when failures must be attributed, outputs reach external users, an audit trail matters, or more than one person has to maintain the system past the demo.&lt;/p&gt;

&lt;p&gt;So the real disagreement is not about whether to use workflows, or how autonomous an agent should be. It is about a boundary: which decisions require judgement, and which decisions require a diff? Framed that way, the boundary is usually much narrower than the initial design assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Three constraints, one property
&lt;/h2&gt;

&lt;p&gt;The position above is not implemented by a single component. Three engineering constraints produce it, and deterministic delivery is the property that emerges when all three hold. They are not a feature list; each has a criterion you can check and a characteristic way of failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C1 — hybrid orchestration.&lt;/strong&gt; A deterministic backbone owns state, retries, authorization, audit trails and rollback; a model is invoked only at nodes requiring semantic judgement.&lt;br&gt;
&lt;em&gt;Criterion:&lt;/em&gt; count the nodes that could be written as an &lt;code&gt;if&lt;/code&gt;. If the answer is "almost none," there is no orchestration, only a chain of prompts with extra latency.&lt;br&gt;
&lt;em&gt;Failure mode:&lt;/em&gt; state hidden in generated text, so a retry silently multiplies the same call and nobody can reconstruct what ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C2 — structured prompt contract.&lt;/strong&gt; A versionable spec of role, hard constraints, I/O contract and fallback rules. Not a persona — persona in English means role-play, and role-play is the part that does not survive contact with production. What matters is the constraint block: &lt;code&gt;missing_context&lt;/code&gt; → ask for clarification, never infer; output must be a typed object; if a constraint cannot be satisfied, emit &lt;code&gt;fallback_reason&lt;/code&gt; and stop.&lt;br&gt;
&lt;em&gt;Criterion:&lt;/em&gt; you can diff the constraint set between two deploys and assert on it without running the whole agent.&lt;br&gt;
&lt;em&gt;Failure mode:&lt;/em&gt; the rule exists in a system prompt, but nothing records whether it was ever consulted on this input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C3 — white-box evaluation.&lt;/strong&gt; Capture per-node inputs, outputs, latency and acceptance criteria into replayable test sets, instead of judging the final answer by feel. Three sets, kept separate: positive (the happy path with grounded context), boundary (empty retrieval, contradictory sources, out-of-scope questions, malformed tool responses), and follow-up (multi-turn drift and corrections).&lt;br&gt;
&lt;em&gt;Criterion:&lt;/em&gt; a failure from last week can be replayed as a single node, with its original input and criterion.&lt;br&gt;
&lt;em&gt;Failure mode:&lt;/em&gt; evaluation that scores only the final answer, which tells you something is wrong and nothing about where.&lt;/p&gt;

&lt;p&gt;Note the ordering. C1 defines where decisions live, C2 makes the rules at those boundaries explicit and testable, C3 proves they fire. Skipping C1 and going straight to C3 is the most common mistake: you end up with a large test suite over a system whose nodes only exist in the model's output text.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. What this looks like in one trace
&lt;/h2&gt;

&lt;p&gt;Consider a support agent that tells a customer refunds apply to a product category no policy document covers. Nobody notices until a customer complains, because the only thing the system kept was the final answer. An illustrative trace of that node would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"step"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"answer_policy_question"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_chunks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"constraint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"missing_context -&amp;gt; ask_for_clarification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"constraint_evaluated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Refunds apply to this category"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1840&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read against the three constraints: C2's rule exists but was never evaluated; C1 has no record of the state transition that should have stopped the call; C3 has no boundary case asserting on &lt;code&gt;retrieved_chunks = 0&lt;/code&gt;. All three failed at once, and the trace is the only reason we can say so. The model produced a plausible continuation — that is what a language model does with no grounded context. Blaming hallucination ends the investigation; the trace starts it.&lt;/p&gt;

&lt;p&gt;One detail worth keeping: log the evaluation, not just the spec. Plenty of systems record the constraint and assume it ran. The gap between "we wrote the rule" and "the rule fired on this input" is where most of these incidents live.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Scope and limits
&lt;/h2&gt;

&lt;p&gt;A position is only useful if it says what it does not cover.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not fix semantic errors on genuinely ambiguous input, missing knowledge in the retrieval corpus, or requirements that were wrong to begin with. A perfect backbone around a bad question still produces a bad answer.&lt;/li&gt;
&lt;li&gt;It does not mean fewer model calls, cheaper inference, or no hallucinations. There is a cost consequence — explicit state makes retries visible and spend attributable to a step instead of to "the agent ran long" — but that is downstream of the architecture, not the goal.&lt;/li&gt;
&lt;li&gt;It does not remove the need for model quality. It changes what you can do when model quality is insufficient: add a boundary case, tighten a fallback, move a decision into code, and prove the change with a replay.&lt;/li&gt;
&lt;li&gt;It is not worth the overhead when failures are cheap, invisible and unattributable — the cases in §3 where the other side is right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does buy is a short, boring postmortem. Failures become addressable, "add a boundary case" becomes a normal engineering response instead of "tune the prompt and hope," and the model can be changed later without re-litigating the whole architecture. That is what keeps an agent maintainable past the demo — not a cleverer chain of prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What follows
&lt;/h2&gt;

&lt;p&gt;If this position holds, the first thing to build is not an eval suite. It is the definition of where the nodes are and who owns them, because every artefact above — the contract, the trace, the test sets — is indexed by that boundary. Get the boundary wrong and the rest becomes bookkeeping over a system you still cannot inspect.&lt;/p&gt;

&lt;p&gt;Two questions follow, and we'd ask them before discussing models at all: where is the line between rule logic and model judgement written down so it can be diffed, and can a wrong answer be replayed to the exact node with its input, its criterion and the fallback that did not fire? In your stack today, does that boundary live in the orchestrator's config — or in whatever the model happens to emit at runtime?&lt;/p&gt;

</description>
      <category>llm</category>
      <category>observability</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your AI bill grew 4x and nobody can tell you which step did it</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Wed, 04 Nov 2026 02:24:22 +0000</pubDate>
      <link>https://dev.to/tokenlat/your-ai-bill-grew-4x-and-nobody-can-tell-you-which-step-did-it-523</link>
      <guid>https://dev.to/tokenlat/your-ai-bill-grew-4x-and-nobody-can-tell-you-which-step-did-it-523</guid>
      <description>&lt;p&gt;A team I talked to last month had a 4x jump in inference spend over one quarter. Not a crisis — they had shipped an agent, usage grew, revenue grew. The problem was smaller and more annoying: when finance asked which part of the product had gotten more expensive, nobody could answer.&lt;/p&gt;

&lt;p&gt;Three engineers, three different guesses. Every dashboard was green. Every invoice was correct. And still, no one could name the step.&lt;/p&gt;

&lt;p&gt;That is the failure mode I keep running into. Not "the bill is too high." &lt;strong&gt;The bill is correct and unreadable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I want to argue this isn't a finance problem or a pricing problem. It's an instrumentation problem, and it has a specific cause: the unit of work changed, and the unit of attribution didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-token prices fell. The bill still went up.
&lt;/h2&gt;

&lt;p&gt;Every model you depend on has gotten cheaper per token over the last two years. And yet the bill went up. Both things are true, which should tell you that price is not the variable that matters.&lt;/p&gt;

&lt;p&gt;What changed is the number of calls behind a single unit of user-visible work.&lt;/p&gt;

&lt;p&gt;In the chat era, "summarize this customer's last quarter" was one request: a prompt, a completion, done. In the agent era, the same sentence is a plan call, a handful of retrieval or embedding calls, two or three tool calls against internal systems, a compression step so the tool output fits, a context rebuild, a generation, a validation pass, and possibly a second generation because validation failed.&lt;/p&gt;

&lt;p&gt;Twenty to forty calls for one thing the user perceives as a single action.&lt;/p&gt;

&lt;p&gt;And here's the part that makes the arithmetic non-linear: &lt;strong&gt;context is a multiplier, not an addend.&lt;/strong&gt; Call #30 doesn't carry the cost of call #30. It carries the accumulated results of calls #1 through #29. As a loop gets deeper, input tokens grow roughly with the square of the step count while useful output stays flat.&lt;/p&gt;

&lt;p&gt;So the shape of the bill changed. It used to be a small number of expensive things. Now it's a large number of individually cheap things, where cost concentrates in the plumbing between the things everyone is looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attribution problem
&lt;/h2&gt;

&lt;p&gt;Most teams instrument cost along axes that made sense for a chat product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;by API key&lt;/li&gt;
&lt;li&gt;by endpoint&lt;/li&gt;
&lt;li&gt;by team or environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These answer &lt;em&gt;who&lt;/em&gt;. They do not answer &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The conversation I described above usually goes like this. Someone pulls up spend by API key and says: platform team is 60% of the bill. Okay — but the platform team runs eleven features. Which one? And inside that feature, was it the retrieval step, the tool calls, or the retries? The attribution stops one layer above where the money is actually being spent.&lt;/p&gt;

&lt;p&gt;When the unit of work becomes a task chain, attribution has to move with it. In practice that means you need to answer three different questions at three different altitudes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Total.&lt;/strong&gt; How much this month, split by model and by outcome (200 vs. 429 vs. timeout — a status code is a cost signal, not just a health signal).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribution.&lt;/strong&gt; Which feature, which tenant, which task type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single step.&lt;/strong&gt; Inside this one trace, which of the 34 calls ate 60% of the cost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Almost every team I've talked to has level 1. Some have level 2. Very few have level 3 — and level 3 is the only one that changes behavior, because it's the only one that tells you what to &lt;em&gt;fix&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;There's a name for this in the vendor world now: an observability center with global, attribution, and per-step views. It's being sold as an enterprise feature. It shouldn't be a feature. It's the minimum for operating an agent at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the application layer can't answer this alone
&lt;/h2&gt;

&lt;p&gt;Here's the part that's genuinely structural, and it's the reason this doesn't get fixed by "just add better logging."&lt;/p&gt;

&lt;p&gt;Your application knows business semantics. It knows this is task &lt;code&gt;tsk_8f21&lt;/code&gt;, for tenant &lt;code&gt;acme&lt;/code&gt;, in the &lt;code&gt;support_draft&lt;/code&gt; feature, at step &lt;code&gt;retrieve_compress&lt;/code&gt;. What it does &lt;strong&gt;not&lt;/strong&gt; know is the cross-model reality of that step: how many tokens the provider actually billed, whether the prompt cache hit, whether a silent retry tripled the input, or whether the same step got routed to a different model than last week.&lt;/p&gt;

&lt;p&gt;The call layer knows all of that and knows none of the semantics. It sees &lt;code&gt;POST /v1/chat/completions&lt;/code&gt;, 18,432 prompt tokens, 412 completion tokens, cache miss, 340ms. It has no idea this is a support draft for a customer named Acme.&lt;/p&gt;

&lt;p&gt;So each side holds half the picture, and the split is exactly along the boundary where cost decisions get made. That's why "we'll join the logs later" fails: by the time you join them, the fields you need were never emitted.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous. Put the business coordinates on the request as it goes out, and let the layer that sees the tokens record them alongside the usage row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Business coordinates travel with the call, not in a side table.
&lt;/span&gt;&lt;span class="n"&gt;COORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Task-Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tsk_8f21&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Task-Step&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve_compress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Feature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support_draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_for_this_step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extra_headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COORDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# What comes back is not just a completion. It's a cost line item
# that already knows where it belongs.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One dict on the way out. Everything downstream — the usage table, the aggregation, the per-step breakdown — becomes possible; without it, nothing downstream can be reconstructed later.&lt;/p&gt;

&lt;p&gt;Then the query you actually want is boring SQL instead of an archaeology project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;task_step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;completion_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;model_usage&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'tsk_8f21'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;task_step&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six lines. If your current stack can't answer that query, you don't have a cost problem yet — you have a cost problem you can't see, which is worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three blind spots that show up in almost every audit
&lt;/h2&gt;

&lt;p&gt;When teams do get per-step visibility, the same three things light up. It's predictable enough that I now look for them first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Context rebuild.&lt;/strong&gt; The loop re-sends conversation history, tool schemas, and retrieved documents on every iteration. Nobody budgeted for this because nobody counted it as a feature. A single page snapshot can be 20k+ tokens; do that inside a five-step loop and it dominates your bill while your completion tokens look healthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Silent retry.&lt;/strong&gt; A retry that re-runs the whole step doesn't just cost 2x — it usually re-sends a &lt;em&gt;larger&lt;/em&gt; context (the failed attempt got appended) and quietly resets whatever spend cap you set at the task level. Your guardrail was on the task; the cost was incurred on the step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tool output inflation.&lt;/strong&gt; A tool returns 400KB of JSON. Two lines of it matter. It enters the context raw, gets re-sent on every subsequent call, and is paid for on each one. Trimming at the tool boundary is usually the single highest-leverage change available — higher than switching models.&lt;/p&gt;

&lt;p&gt;None of these are model-quality problems. All three are invisible without step-level attribution and obvious with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the cap where the cost is incurred
&lt;/h2&gt;

&lt;p&gt;One more thing that only becomes visible once you have step-level data: most teams define their spending limits in the wrong place.&lt;/p&gt;

&lt;p&gt;A task-level cap is the intuitive choice — "this job may cost at most X." But it answers the wrong question. It tells you a task blew its budget; it doesn't tell you which step did it, and by the time it fires the task has usually already failed in a way the user noticed. Meanwhile the pathological call keeps happening: the context rebuilt for the twelfth time, the tool output that inflated 40x, the retry chain that never terminated.&lt;/p&gt;

&lt;p&gt;A step-level cap fires on the specific call that went wrong, while the task can still degrade gracefully instead of dying.&lt;/p&gt;

&lt;p&gt;There's a second, quieter problem. Caps implemented in application code are per instance. Three workers, three counters, three times the budget — and the counter resets on every deploy. A cap enforced in the layer that every call already passes through is global by construction, and it survives a rolling restart.&lt;/p&gt;

&lt;p&gt;That's the same lesson as the attribution one, just from the other direction: if the layer that sees the tokens isn't the layer that enforces the policy, your guardrail is an estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-test
&lt;/h2&gt;

&lt;p&gt;I'll leave you with the question I now ask every team, because it separates the ones who can operate an agent from the ones who are still guessing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take the most expensive task your product ran last week. Can you name its three most expensive steps, and the reason each one cost what it did?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If yes, you have the instrumentation and the rest is tuning. If no, the bill isn't the problem — the missing step-level attribution is. And no amount of model shopping fixes a number you can't decompose.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>observability</category>
      <category>architecture</category>
    </item>
    <item>
      <title>We instrumented 11 production agents with real token accounting. In 9 of them, the most expensive step wasn't the model.</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Wed, 28 Oct 2026 01:06:28 +0000</pubDate>
      <link>https://dev.to/tokenlat/we-instrumented-11-production-agents-with-real-token-accounting-in-9-of-them-the-most-expensive-mgb</link>
      <guid>https://dev.to/tokenlat/we-instrumented-11-production-agents-with-real-token-accounting-in-9-of-them-the-most-expensive-mgb</guid>
      <description>&lt;p&gt;Last quarter we finally stopped trusting the LLM bill. We wired &lt;strong&gt;real per-step token attribution&lt;/strong&gt; into 11 production agents — not the default "total tokens" the provider hands you, but a tracer that tags every call, every tool, every cache lookup with the &lt;em&gt;trajectory&lt;/em&gt; it belonged to.&lt;/p&gt;

&lt;p&gt;The number that broke our mental model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Across those 11 agents, the single most expensive step — measured &lt;strong&gt;per resolved task&lt;/strong&gt;, not per API call — was the model in exactly &lt;strong&gt;2 of them&lt;/strong&gt;. In the other &lt;strong&gt;9&lt;/strong&gt;, something we'd filed under "negligible overhead" was quietly eating more than the LLM.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the trap, in one picture. Same agent, same steps — two ways of ranking them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WHAT THE DASHBOARD SHOWS (per-call mean)        WHAT THE BILL ACTUALLY IS (per-trajectory total)
Step                  $/call     rank            Step                  $/task      rank
LLM completion        $0.0028     #1              PII / guardrail       $0.0042     #1
PII / guardrail       $0.00035    #4              Embedding loop        $0.0036     #2
Embedding (in loop)   $0.00002    #5              LLM completion        $0.0028     #3
Semantic-cache chk   $0.00090    #3              Semantic-cache chk   $0.0009     #4
Tool call             $0.00010    #2              Tool call             $0.0008     #5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Left column: what every dashboard reports. Right column: what one task actually costs. The model drops from #1 to #3. A step you pay two-hundredths of a cent per call becomes the #2 cost driver. &lt;strong&gt;The ranking is not the same list.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And across all 11 agents, the #1 cost step by trajectory-total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#1 cost driver (per trajectory), across 11 production agents
Embedding / retrieval loop   ██████████  5 / 11
PII / guardrail model        ██████      3 / 11
Model (LLM)                  ████        2 / 11
Cache-verify model           ██          1 / 11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine of eleven. The model isn't even in the lead most of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden axis: mean-vs-trajectory
&lt;/h2&gt;

&lt;p&gt;The reason dashboards mislead is structural. They report &lt;strong&gt;mean cost per call&lt;/strong&gt;, because that's what one request easily exposes. But an agent step isn't called once — it's called &lt;em&gt;inside loops, per chunk, per re-plan&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The real cost of a step is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;step_cost_per_task  =  calls_per_task  ×  cost_per_call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A step at &lt;code&gt;$0.00002/call&lt;/code&gt; looks free on the mean chart. Call it 180 times in a retrieval loop and it's &lt;code&gt;$0.0036/task&lt;/code&gt; — more than the LLM, which runs once. The "free" step wins by 28%, on exactly the hard tasks you care about.&lt;/p&gt;

&lt;p&gt;The invisible cost axis is &lt;strong&gt;trajectory-total vs call-mean&lt;/strong&gt;. Every dashboard I've seen only draws one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three cases we actually shipped
&lt;/h2&gt;

&lt;p&gt;These aren't hypotheticals — they're line items from the 11 agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 1 — The "free" embedding step that wasn't.&lt;/strong&gt;&lt;br&gt;
We had a retrieval step the finance team literally labeled &lt;em&gt;free&lt;/em&gt; in the cost model (embeddings at &lt;code&gt;$0.00002/call&lt;/code&gt;). It lives inside the agent's re-planning loop, which on hard tasks re-retrieves up to 180 times. &lt;code&gt;180 × $0.00002 = $0.0036/task&lt;/code&gt;. The LLM completion, called once, is &lt;code&gt;$0.0028&lt;/code&gt;. So the step we scored as zero out-costs the model by 28% — on every hard task, which is the only tasks that matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 2 — The cache hit that cost more than the miss.&lt;/strong&gt;&lt;br&gt;
Our semantic cache doesn't just return the cached answer on a hit. It runs a reranker plus a 70B "is this still relevant?" check, to avoid serving stale advice. That verification costs &lt;code&gt;$0.0009&lt;/code&gt;. A genuine miss regenerates from the 8B model for &lt;code&gt;$0.0006&lt;/code&gt;. So a hit — the thing caches exist to save you — costs &lt;strong&gt;50% more&lt;/strong&gt; than a miss, across the 40% of traffic that hits. We were paying a "savings" tax and calling it a win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 3 — The guardrail that 4×'d the bill.&lt;/strong&gt;&lt;br&gt;
A PII-scrubber runs a 7B model on every message chunk before the LLM sees it: 12 chunks × &lt;code&gt;$0.00035&lt;/code&gt; = &lt;code&gt;$0.0042/task&lt;/code&gt;. We added it for a compliance ask and never costed it per-trajectory. It became the #1 line item on 3 of the 11 agents. Removing it wasn't on the table — but routing it to a 0.5B model dropped it to &lt;code&gt;$0.0003&lt;/code&gt;, a 14× cut, with no detectable quality change on our eval set.&lt;/p&gt;
&lt;h2&gt;
  
  
  How you should actually measure
&lt;/h2&gt;

&lt;p&gt;The fix is almost never "pick a cheaper model." It's accounting per trajectory so the loop multiplier can't hide, then routing each step to the right-sized model. We sit in front of 25+ models behind one OpenAI-compatible gateway; the interesting savings came from moving the scrubber and the cache-verify down, not from swapping the completion.&lt;/p&gt;

&lt;p&gt;Three changes that took an afternoon:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Attribute every token to a &lt;code&gt;trajectory_id&lt;/code&gt;, not a request.&lt;/strong&gt; If you can't say what one task cost, you don't know your cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rank by total, sample real tasks.&lt;/strong&gt; Pull N resolved tasks, sum per step, sort by total. The mean chart will lie to you; the total won't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put a &lt;code&gt;cost_per_resolved_task&lt;/code&gt; SLO in place.&lt;/strong&gt; Alert on the trajectory, not the call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal version of what we run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cost_attribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trajectory&lt;/span&gt;          &lt;span class="c1"&gt;# not 'request' and not 'call'&lt;/span&gt;
  &lt;span class="na"&gt;trace_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trajectory_id&lt;/span&gt;   &lt;span class="c1"&gt;# tag every step, tool, cache lookup&lt;/span&gt;
  &lt;span class="na"&gt;rank_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cost&lt;/span&gt;       &lt;span class="c1"&gt;# never mean_cost&lt;/span&gt;
  &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cost_per_resolved_task&lt;/span&gt;
    &lt;span class="na"&gt;threshold_usd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.02&lt;/span&gt;
  &lt;span class="na"&gt;exclude_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;         &lt;span class="c1"&gt;# never exclude the 'cheap' steps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the five lines that surface the buried step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rank_steps_by_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;per_step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;per_step&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_usd&lt;/span&gt;          &lt;span class="c1"&gt;# already trajectory-attributed
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# prints the REAL ranking — usually not what your dashboard shows
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;rank_steps_by_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The question I'll leave you with
&lt;/h2&gt;

&lt;p&gt;The dashboards aren't lying. They're reporting the axis that's easiest to measure. We spent a year optimizing model choice while a loop multiplier and a "free" safety step ran away from us — invisible the whole time, because nothing on the screen was drawing the trajectory-total axis.&lt;/p&gt;

&lt;p&gt;So: when you last looked at your agent's cost, were you ranking by what &lt;strong&gt;one call&lt;/strong&gt; costs — or by what &lt;strong&gt;one task&lt;/strong&gt; actually costs? Because those are not the same list.&lt;/p&gt;

&lt;p&gt;What's the step you've been treating as free?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>Your retry loop is your most expensive feature</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:16:29 +0000</pubDate>
      <link>https://dev.to/tokenlat/your-retry-loop-is-your-most-expensive-feature-59dk</link>
      <guid>https://dev.to/tokenlat/your-retry-loop-is-your-most-expensive-feature-59dk</guid>
      <description>&lt;p&gt;A team I worked with tracked cost per task down to the token. They capped context. They set a budget alert. And then they added &lt;code&gt;retries: 3&lt;/code&gt; to the model client and never put it in the budget.&lt;/p&gt;

&lt;p&gt;That retry path only fires when the model is slow or down — i.e. at peak load. And every retry replays the &lt;em&gt;entire&lt;/em&gt; context. So the one part of the system nobody accounts for is the part that costs the most, exactly when you can afford it least.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that surprises people
&lt;/h2&gt;

&lt;p&gt;Take a support agent for an ASEAN e-commerce shop. A hard ticket pulls a 50k-token context (order history, policy docs, prior turns) and emits ~1k tokens.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One attempt ≈ 51k tokens.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;retries: 3&lt;/code&gt; and the task still failing, you pay for &lt;strong&gt;4 attempts&lt;/strong&gt;: 51k × 4 ≈ &lt;strong&gt;204k tokens&lt;/strong&gt; for a task you budgeted at 51k.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a &lt;strong&gt;4× tax&lt;/strong&gt; — and it isn't spread evenly. The tasks that fail are the expensive ones: long context, heavy tool chains, messy inputs. Retries concentrate exactly where cost is highest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;retry_policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;naive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;on_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;same&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;model"&lt;/span&gt;
    &lt;span class="na"&gt;hidden_cost_multiplier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;        &lt;span class="c1"&gt;# 1 attempt + 3 full-context replays&lt;/span&gt;
    &lt;span class="na"&gt;shows_up_in_dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;cost_aware&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;on_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;route&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;next&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;equivalent&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;model"&lt;/span&gt;
    &lt;span class="na"&gt;carries_context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;hidden_cost_multiplier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;        &lt;span class="c1"&gt;# context travels, never replayed&lt;/span&gt;
    &lt;span class="na"&gt;shows_up_in_dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why the bill stays invisible
&lt;/h2&gt;

&lt;p&gt;Most cost dashboards report &lt;em&gt;average cost per successful request&lt;/em&gt;. A task that fails three times and finally succeeds doesn't appear as "4× cost" — it appears as one success with a latency blip. The retry is a hidden fourth axis of spend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; triples → someone notices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; triples → almost nobody measures it, because it's buried in "failed then retried" traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So teams tune the happy path and ignore the one code path that runs under stress and multiplies the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is routing, not retrying
&lt;/h2&gt;

&lt;p&gt;You don't make retries cheaper by retrying harder. You stop replaying context: when a model is unavailable, advance the task to the &lt;em&gt;next equivalent model&lt;/em&gt; that can take the &lt;strong&gt;same handoff context&lt;/strong&gt;. The context travels with the task; you don't re-pay for it as a "retry."&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;25+ models behind one OpenAI-compatible endpoint&lt;/strong&gt;, "equivalent" stops being theoretical — there's a tier you can actually route to without rewriting prompts or adapters. For teams under data-residency rules (PDPA-aligned, SG-hosted), this also matters: re-running context on a different provider just to "retry" raises compliance questions, not just cost.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  subgraph Naive[Naive retry: context replayed]
    N1[Task · 50k ctx] --&amp;gt; N2[Attempt 1: 50k in]
    N2 --&amp;gt;|fail| N3[Replay 50k in]
    N3 --&amp;gt;|fail| N4[Replay 50k in]
    N4 --&amp;gt;|fail| N5[Replay 50k in]
    N1 -.paid 4x.-.-&amp;gt; N5
  end
  subgraph Smart[Cost-aware: context travels]
    S1[Task · 50k ctx] --&amp;gt; S2[Model A call]
    S2 --&amp;gt;|unavailable| S3[Model B · same ctx]
    S3 --&amp;gt;|unavailable| S4[Model C · same ctx]
    S1 -.ctx paid once.-.-&amp;gt; S4
  end&lt;/code&gt;&lt;/pre&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_registry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# cost-aware: context is paid ONCE, then travels with the task
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;model_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ordered_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handoff_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# no replay
&lt;/span&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;Unavailable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                                   &lt;span class="c1"&gt;# next equivalent, same context
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;NoCapableModel&lt;/span&gt;

&lt;span class="c1"&gt;# for contrast — the naive version re-sends the FULL context every attempt
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;naive_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# full context RE-SENT each time
&lt;/span&gt;        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Unavailable&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;RetryExhausted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router doesn't retry the &lt;em&gt;model&lt;/em&gt;. It advances the &lt;em&gt;task&lt;/em&gt; to the next model that can take its context. Same uptime, a fraction of the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I'll leave you with
&lt;/h2&gt;

&lt;p&gt;Before you ship &lt;code&gt;retries: 3&lt;/code&gt;: for the tasks that fail, how many tokens does the retry actually replay — and is that number in any dashboard you look at?&lt;/p&gt;

&lt;p&gt;No tool to sell, no link to click. Just this: most LLM systems that survive a model outage are just retry loops with better logging — and the loop is the most expensive thing they do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>Your failover is a retry loop pretending to be resilience</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Tue, 13 Oct 2026 05:32:26 +0000</pubDate>
      <link>https://dev.to/tokenlat/your-failover-is-a-retry-loop-pretending-to-be-resilience-1f8m</link>
      <guid>https://dev.to/tokenlat/your-failover-is-a-retry-loop-pretending-to-be-resilience-1f8m</guid>
      <description>&lt;p&gt;A team I worked with had a clean SLA: agent responses in under 8 seconds, 99.5% of the time. Then the frontier model they routed everything through started rate-limiting during peak hours. Their "failover" was three retries with exponential backoff.&lt;/p&gt;

&lt;p&gt;The SLA held on paper. In practice, users got a 24-second response that sometimes failed anyway. The retries weren't failover — they were the &lt;em&gt;same&lt;/em&gt; request, replayed, hoping the wall had moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counterintuitive part
&lt;/h2&gt;

&lt;p&gt;Retry and failover look identical in a dashboard ("request failed → tried again"). They are not the same operation.&lt;/p&gt;

&lt;p&gt;A retry assumes the &lt;em&gt;system&lt;/em&gt; was momentarily unavailable and the &lt;em&gt;task&lt;/em&gt; is intact. That's true for a database connection. It's false for an agent mid-task: the moment you re-send the whole conversation, you've thrown away the in-flight state — the half-formed tool call, the retrieved doc, the branch the agent had already chosen. You pay for the first attempt's tokens &lt;em&gt;and&lt;/em&gt; the replay's tokens. Latency triples. And if the model is down (not just slow), every retry hits the same wall.&lt;/p&gt;

&lt;p&gt;So the failure mode isn't "generation failed." It's &lt;strong&gt;state continuity failed.&lt;/strong&gt; The task had accumulated context, and retry discards it.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is a contract problem, not a wrapper problem
&lt;/h2&gt;

&lt;p&gt;You don't fix this by adding &lt;code&gt;retries=3&lt;/code&gt; to your client. You fix it by defining, per sub-task, what "equivalent" means — and making the fallback &lt;em&gt;consume the same handoff context&lt;/em&gt; rather than "start over."&lt;/p&gt;

&lt;p&gt;That requires three things most single-model setups don't have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An abstraction so the fallback speaks the same interface&lt;/strong&gt; — if the backup model is OpenAI-compatible, swapping it in doesn't mean rewriting prompts or adapters. The task never learns a model went down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A ranked fallback registry, not a single backup&lt;/strong&gt; — one standby model is a single point of failure with a different name. A registry orders equivalents by capability and cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The routing layer carries state&lt;/strong&gt; — the switch happens &lt;em&gt;inside&lt;/em&gt; the task, not above it. The user sees one continuous attempt.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;failover_policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontier-model&lt;/span&gt;
  &lt;span class="na"&gt;fallback_tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mid-tier-instruct&lt;/span&gt;
      &lt;span class="na"&gt;equivalence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handles&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;same&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tool-calling&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;schema"&lt;/span&gt;
      &lt;span class="na"&gt;carries_state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small-instruct&lt;/span&gt;
      &lt;span class="na"&gt;equivalence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;degrades&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;summary,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;preserves&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;context"&lt;/span&gt;
      &lt;span class="na"&gt;carries_state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;on_primary_unavailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;route_to_first_capable&lt;/span&gt;   &lt;span class="c1"&gt;# not: retry_primary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a catalog of &lt;strong&gt;25+ models&lt;/strong&gt; behind one interface, "equivalent" stops being theoretical — there's a梯队 you can actually fail over to without dropping the task.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant T as Task (in-flight)
    participant R as Router
    participant P as Primary (frontier)
    participant F as Fallback (mid-tier)
    T-&amp;gt;&amp;gt;R: needs next step
    R-&amp;gt;&amp;gt;P: call (carries handoff context)
    P--&amp;gt;&amp;gt;R: 429 / timeout
    R-&amp;gt;&amp;gt;F: call (same handoff context)
    F--&amp;gt;&amp;gt;T: continuation (state preserved)
    Note over T,F: user sees ONE attempt, not a replay&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  The SLA only means something if the task survives
&lt;/h2&gt;

&lt;p&gt;"99.9% availability" is meaningless if failover loses the task. A retry that returns after 3x the latency and a fresh start isn't a success — the user waited, paid, and got a do-over. The SLA number is green; the experience is red.&lt;/p&gt;

&lt;p&gt;Real failover measures &lt;em&gt;task completion&lt;/em&gt;, not &lt;em&gt;request return&lt;/em&gt;. The fallback has to be able to pick up where the primary left off — which is exactly why the interface abstraction matters. Without it, you're not failing over, you're restarting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_registry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;model_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ordered_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handoff_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# state travels with the task
&lt;/span&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;Unavailable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                                   &lt;span class="c1"&gt;# next equivalent, same context
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;NoCapableModel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router doesn't retry the &lt;em&gt;model&lt;/em&gt;. It advances the &lt;em&gt;task&lt;/em&gt; to the next model that can take its context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I'll leave you with
&lt;/h2&gt;

&lt;p&gt;Before you add a &lt;code&gt;retry_on_failure&lt;/code&gt; wrapper: for the step you're protecting, what does "equivalent" mean — and does your fallback carry its state, or just rerun it?&lt;/p&gt;

&lt;p&gt;No tool to sell, no link to click. Just this: most "resilient" LLM systems are retry loops with better logging. Resilience is a contract the fallback has to honor.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>architecture</category>
      <category>performance</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>200 agents, 2,011,438 tool calls: who's paying for your AI?</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:16:07 +0000</pubDate>
      <link>https://dev.to/tokenlat/200-agents-2011438-tool-calls-whos-paying-for-your-ai-14o6</link>
      <guid>https://dev.to/tokenlat/200-agents-2011438-tool-calls-whos-paying-for-your-ai-14o6</guid>
      <description>&lt;p&gt;A team I worked with shipped 200 internal agents last quarter. The monthly bill landed at &lt;strong&gt;2,011,438 tool calls&lt;/strong&gt;. Finance asked the usual thing: &lt;em&gt;"which model is eating our budget?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The intuitive answer — "the frontier model, obviously" — was wrong. And &lt;em&gt;why&lt;/em&gt; it was wrong says something useful about how agentic systems actually spend money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counterintuitive part
&lt;/h2&gt;

&lt;p&gt;Most token spend in an agent loop isn't in the hard reasoning step you're picturing. It's in the boring middle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the 60-line retrieval that reformats a doc the model already saw,&lt;/li&gt;
&lt;li&gt;the JSON reshaping between two tools,&lt;/li&gt;
&lt;li&gt;the "which tool next?" classifier that fires before every action,&lt;/li&gt;
&lt;li&gt;the summary pass that condenses a transcript nobody reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those need a frontier model. They need &lt;em&gt;a&lt;/em&gt; model — a cheap one, routed to that specific sub-task. But the agent was wired with one "default" model for the whole loop, so every sub-task paid frontier prices.&lt;/p&gt;

&lt;p&gt;We measured it: roughly &lt;strong&gt;80% of the bill was sub-tasks that cleared the quality bar at ~1/20th the cost on a smaller model.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  This is a routing problem, not a prompting problem
&lt;/h2&gt;

&lt;p&gt;You don't fix this with a cleverer system prompt. You fix it by making the &lt;em&gt;sub-task boundary&lt;/em&gt; visible to a router.&lt;/p&gt;

&lt;p&gt;Instead of &lt;code&gt;agent.llm = frontier_model&lt;/code&gt;, each sub-task declares what it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;subtasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;retrieve_context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;min_capability&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;embedding-lookup&lt;/span&gt;
    &lt;span class="na"&gt;route_to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small-retriever&lt;/span&gt;      &lt;span class="c1"&gt;# cheap, no reasoning needed&lt;/span&gt;
  &lt;span class="na"&gt;classify_next_tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;min_capability&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;light-classification&lt;/span&gt;
    &lt;span class="na"&gt;route_to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mini-classifier&lt;/span&gt;      &lt;span class="c1"&gt;# tiny, fast&lt;/span&gt;
  &lt;span class="na"&gt;synthesize_answer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;min_capability&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontier-reasoning&lt;/span&gt;
    &lt;span class="na"&gt;route_to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontier-model&lt;/span&gt;       &lt;span class="c1"&gt;# only here do we pay up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router picks the cheapest model that clears &lt;code&gt;min_capability&lt;/code&gt;. The agent's behavior doesn't change. The bill does.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Sub-task emitted] --&amp;gt; R{Router}
    R --&amp;gt;|embedding-lookup| S[small-retriever]
    R --&amp;gt;|light-classification| M[mini-classifier]
    R --&amp;gt;|frontier-reasoning| F[frontier-model]
    S --&amp;gt; L[per-step ledger]
    M --&amp;gt; L
    F --&amp;gt; L
    L --&amp;gt; C{PDPA-aligned?}
    C --&amp;gt;|yes| OK[serve in SG-hosted region]
    C --&amp;gt;|no| B[route to compliant region]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Per-step cost attribution is the missing half
&lt;/h2&gt;

&lt;p&gt;Routing only helps if you can &lt;em&gt;see&lt;/em&gt; it. Most teams have one line item: "LLM." That hides the problem — finance sees a number, not a decision.&lt;/p&gt;

&lt;p&gt;Attach cost to every sub-task, not every request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtask&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cheapest_that_clears&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_capability&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;estimate_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# per-step, not per-request
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the bill reads by &lt;em&gt;capability tier&lt;/em&gt;: "retrieval $X, classification $Y, synthesis $Z." When synthesis balloons, you know which sub-task to attack — not which model to blame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and compliance converge on the same layer
&lt;/h2&gt;

&lt;p&gt;Here's what surprised the team: the router was also where data-residency rules had to live. Some sub-tasks touched regulated customer data, so they had to stay in a &lt;strong&gt;PDPA-aligned, SG-hosted&lt;/strong&gt; region even when a cheaper model sat elsewhere.&lt;/p&gt;

&lt;p&gt;So the same routing layer that cut the bill also enforced jurisdiction. Cost optimization and compliance stopped being separate meetings — they became one config block.&lt;/p&gt;

&lt;p&gt;That's the real unlock: a routing layer isn't infrastructure you tolerate. It's where your cost &lt;em&gt;and&lt;/em&gt; your constraints get expressed as code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I'll leave you with
&lt;/h2&gt;

&lt;p&gt;If you mapped every sub-task in your agent loop to the cheapest model that clears its quality bar, how much of your current bill would survive?&lt;/p&gt;

&lt;p&gt;No tool to sell, no link to click. Just this: most agent bills are really routing bills in disguise.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>Retrieval is a routing problem. Your RAG stack just hides it.</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Tue, 29 Sep 2026 05:46:44 +0000</pubDate>
      <link>https://dev.to/tokenlat/retrieval-is-a-routing-problem-your-rag-stack-just-hides-it-n1l</link>
      <guid>https://dev.to/tokenlat/retrieval-is-a-routing-problem-your-rag-stack-just-hides-it-n1l</guid>
      <description>&lt;p&gt;Most RAG failures aren't model failures. They're routing failures wearing a retrieval costume.&lt;/p&gt;

&lt;p&gt;We spend enormous effort choosing the right model for a RAG pipeline — the right context window, the right reasoning strength, the right price tier — and then we feed &lt;em&gt;every&lt;/em&gt; query through the exact same retrieval path. Same vector index. Same &lt;code&gt;top_k&lt;/code&gt;. Same chunking. Same reranker. A yes/no factual lookup and a ten-document synthesis get treated as the same shape of problem.&lt;/p&gt;

&lt;p&gt;That isn't a retrieval step. That's a routing decision we forgot to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrieval step decides your model for you
&lt;/h2&gt;

&lt;p&gt;Retrieval is upstream of model selection. The chunks you pull determine what context the model sees, and the &lt;em&gt;shape&lt;/em&gt; of that context determines which model is even appropriate. If retrieval drowns a simple lookup in noise, no model choice saves it. If retrieval starves a synthesis of breadth, the strongest model just hallucinates confidently.&lt;/p&gt;

&lt;p&gt;Yet we treat retrieval as fixed preprocessing — a hardcoded preamble the model has to swallow whole. The leverage is right there, and we've bolted it shut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three intents, three retrieval strategies
&lt;/h2&gt;

&lt;p&gt;The same &lt;code&gt;top_k=5&lt;/code&gt; vector search serves three very different jobs badly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lookup&lt;/strong&gt; — "What's our refund policy for cross-border orders?" Needs precision. Narrow, high-recall-on-target, maybe keyword + metadata filter, low tolerance for irrelevant context. Top-1 to top-3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthesis&lt;/strong&gt; — "Summarize the Q3 incident reports." Needs breadth. Multi-source, high-recall, dedup, section-aware assembly. Starved by a small k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparison&lt;/strong&gt; — "How does our p99 latency compare to the regional average?" Often isn't a vector problem at all. Structured extraction, maybe a SQL or semi-structured path, not free-text chunks the model has to reverse-engineer into numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feed all three through one undifferentiated retrieval call and you get the worst of each: lookups drowned, syntheses starved, comparisons given prose it can't reason over.&lt;/p&gt;

&lt;p&gt;Concretely, a retrieval plan keyed by intent — not hardcoded — looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# retrieval plan, selected by classified intent&lt;/span&gt;
&lt;span class="na"&gt;strategies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lookup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policy_dense&lt;/span&gt;
    &lt;span class="na"&gt;k&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_type:policy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AND&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;region:{user_region}"&lt;/span&gt;
    &lt;span class="na"&gt;reranker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
  &lt;span class="na"&gt;synthesis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;incidents_dense&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;incidents_faq&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;k&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;now()&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;90d"&lt;/span&gt;
    &lt;span class="na"&gt;reranker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cross_encoder&lt;/span&gt;
    &lt;span class="na"&gt;fusion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rrf&lt;/span&gt;
  &lt;span class="na"&gt;comparison&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sql_extract&lt;/span&gt;        &lt;span class="c1"&gt;# not a vector path at all&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metrics_db&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;structured_rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The defensible lever: make retrieval a routable step
&lt;/h2&gt;

&lt;p&gt;The fix is to pull retrieval &lt;em&gt;up&lt;/em&gt; into the routing layer. Before the query hits the index, classify intent and select a retrieval strategy — which index, what k, what filter expression, which reranker, what fusion method. This is the same shape as model routing: you're deciding which backend serves this step.&lt;/p&gt;

&lt;p&gt;And here's the constraint that shapes the whole architecture: &lt;strong&gt;context is a budget, not a free resource.&lt;/strong&gt; More retrieved tokens are not better — irrelevant context dilutes attention (the same dilution problem that makes an unmeasured step expensive). So retrieval routing is simultaneously a quality router &lt;em&gt;and&lt;/em&gt; a cost router. With 25+ models behind an OpenAI-compatible interface, the router can pair "cheap retrieval + cheap model for lookup" against "broad retrieval + stronger model for synthesis" — but only if retrieval is itself a routable step, not a hardcoded preamble.&lt;/p&gt;

&lt;p&gt;Compliance adds a second axis. For PDPA-aligned workloads, the source index and the model that reads it may need to live in the same region — SG-hosted, not shipped offshore. Retrieval routing then has a compliance gate &lt;em&gt;before&lt;/em&gt; a quality gate: you can't route a strategy that would move regulated data out of jurisdiction, no matter how good its recall looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern, concretely
&lt;/h2&gt;

&lt;p&gt;A retrieval router takes &lt;code&gt;(query, metadata)&lt;/code&gt; and emits a retrieval plan: &lt;code&gt;{ index, k, filter, reranker, fusion }&lt;/code&gt;. The model router then consumes the &lt;em&gt;projected context shape&lt;/em&gt; as an input — not just the query text. Routing becomes two-stage: &lt;strong&gt;retrieve-shape → model-shape.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the failover lesson carries straight over from model routing: if the primary retrieval backend degrades, the router should have a fallback &lt;em&gt;retrieval path&lt;/em&gt; — keyword, SQL, a secondary index — not just a blind retry of the same call. A retried vector search that's already degraded returns the same degraded result, just billed twice.&lt;/p&gt;

&lt;p&gt;The two-stage flow, side by side with the naive path:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  Q[Query] --&amp;gt; C{Intent classifier}
  C --&amp;gt;|lookup| L[policy index · k=3 · filtered]
  C --&amp;gt;|synthesis| S[incidents · k=12 · RRF + rerank]
  C --&amp;gt;|comparison| P[SQL extract · structured rows]
  L --&amp;gt; M[Model router]
  S --&amp;gt; M
  P --&amp;gt; M
  M --&amp;gt; A[Answer]
  subgraph Naive[Naive RAG]
    NQ[Query] --&amp;gt; NV[one vector index · top_k=5]
    NV --&amp;gt; NM[Model]
  end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;And the function that produces the plan — note the compliance gate sits &lt;em&gt;before&lt;/em&gt; the quality gate, and the failover is a different path, not a retry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_retrieval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classify_intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# lookup | synthesis | comparison
&lt;/span&gt;    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STRATEGY_TABLE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# {index, k, filter, reranker, ...}
&lt;/span&gt;
    &lt;span class="c1"&gt;# compliance gate BEFORE quality gate
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;in_jurisdiction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fall_back_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region_local_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;degraded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="c1"&gt;# primary backend soft-failed
&lt;/span&gt;        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FAILOVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# different path, not a retry
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;   &lt;span class="c1"&gt;# model router consumes context.shape, not just the query
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The question I keep landing on
&lt;/h2&gt;

&lt;p&gt;Do you route retrieval by query intent, or does the same &lt;code&gt;top_k&lt;/code&gt; serve every question in your stack? If you split retrieval strategies, where does that decision live — before the query reaches the index, or after the model has already committed to an answer?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>Your agent's cost problem isn't the model. It's the steps you never measured.</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Tue, 22 Sep 2026 02:47:08 +0000</pubDate>
      <link>https://dev.to/tokenlat/your-agents-cost-problem-isnt-the-model-its-the-steps-you-never-measured-38ag</link>
      <guid>https://dev.to/tokenlat/your-agents-cost-problem-isnt-the-model-its-the-steps-you-never-measured-38ag</guid>
      <description>&lt;p&gt;A few weeks back I watched an agentic pipeline burn a month of budget in three days. The instinct everyone reached for was the obvious one: "the frontier model is too expensive, switch to a smaller one." That's the wrong diagnosis, and it's worth writing down why.&lt;/p&gt;

&lt;p&gt;When we finally tagged every token to the &lt;code&gt;(step, tool, model)&lt;/code&gt; tuple that spent it, the picture was uncomfortable. The expensive model wasn't the problem. The problem was that &lt;em&gt;every&lt;/em&gt; step — the trivial classifier, the summary, the hard reasoning call — silently defaulted to it, and nobody could see which step was responsible. We'd been arguing about which model to buy when we should have been arguing about which step was allowed to call it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost in an agent isn't a number. It's a tree.
&lt;/h2&gt;

&lt;p&gt;In a single LLM call, cost is one line item. In an agentic loop it's a tree: each step calls a model, maybe a tool, maybe a retry, maybe a sub-step that calls again. The bill at the end is the sum over that whole tree.&lt;/p&gt;

&lt;p&gt;If you only ever see the total, you are optimizing blind. "Make it cheaper" with only a total in front of you means guessing. And the guess is almost always "use a smaller model everywhere," which throws away quality on the steps that actually needed the frontier model while leaving the real waste untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent default that inflates everything
&lt;/h2&gt;

&lt;p&gt;Most agent frameworks let you set "the model" once, at the top. So a routing decision that should have been per-step gets frozen into a single global choice. A classification step that a small model handles fine? Frontier. A summarization of text the model just produced? Frontier. The one genuinely hard reasoning call? Also frontier — but now it's lost in the noise.&lt;/p&gt;

&lt;p&gt;The waste here isn't that frontier models are pricey. It's that ~80% of those calls never needed them, and without per-step attribution you can't even prove which 80%. You're paying a premium on steps whose output quality wouldn't have moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-step attribution is boring infrastructure, not a feature
&lt;/h2&gt;

&lt;p&gt;There's no clever trick. You log, for every call: step id, tool, model, input/output tokens, latency, retry count. That's it. Once that data exists, the waste shows up in minutes — usually a handful of steps dominate the bill, and they're rarely the ones you'd have guessed.&lt;/p&gt;

&lt;p&gt;I've lost count of how many times the "expensive" step turned out to be a retry loop quietly re-running a hard call five times because the tool returned a shape the parser rejected, not because the model was wrong. That's not a model-cost problem. That's an un-instrumented step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failover is where the blind spots explode
&lt;/h2&gt;

&lt;p&gt;Here's the part that actually blows budgets. A model errors, or times out, or returns garbage. The naive fix re-runs the &lt;em&gt;whole trajectory&lt;/em&gt; — or at best the whole step — against the same model. Cost spikes exactly when you can least afford it, on the run that was already failing.&lt;/p&gt;

&lt;p&gt;A planned failover routes that &lt;em&gt;single step&lt;/em&gt; to a fallback at the step boundary: same tool contract, different model, the rest of the run continues. The gap between "retry the universe" and "swap the model for this one call" is most of the cost story nobody talks about, because you only see it when things break — and by then you're not measuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lever lives at the routing layer, not in prompts
&lt;/h2&gt;

&lt;p&gt;The point that took me too long to internalize: you cannot prompt your way to per-step cost governance. The layer that decides which model serves each step is the &lt;em&gt;only&lt;/em&gt; place that can both measure the spend and act on it. Put the attribution and the failover decision anywhere else and you're asking a component that doesn't see the calls to fix calls it can't see.&lt;/p&gt;

&lt;p&gt;This is also where the compliance constraint and the cost lever happen to land on the same component. In Southeast Asia, PDPA-aligned handling means prompts and outputs can't wander out of region — so the routing layer has to be SG-hosted (Tencent Cloud) and keep data resident. That same in-region gateway is exactly where per-step attribution naturally lives: the router is already on the path of every token, so measuring them costs nothing extra. A gateway that exposes 25+ models behind one OpenAI-compatible endpoint isn't just a convenience for failover — it's the only place the measurement and the swap can share state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I keep coming back to
&lt;/h2&gt;

&lt;p&gt;When your agent's bill doubles, can you point to the three steps that caused it — or do you just know the total went up? If you can't attribute per step, the fix was never a cheaper model. It was instrumenting the routing you already had.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
    <item>
      <title>Language is a routing problem, not a translation problem</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:54:48 +0000</pubDate>
      <link>https://dev.to/tokenlat/language-is-a-routing-problem-not-a-translation-problem-1227</link>
      <guid>https://dev.to/tokenlat/language-is-a-routing-problem-not-a-translation-problem-1227</guid>
      <description>&lt;p&gt;Most teams approach a multilingual support bot as a translation problem. Wire up a translation layer, send everything to one big model, done. We did roughly that the first time, and it mostly worked — until we looked at the bill and at which customers were actually being served well.&lt;/p&gt;

&lt;p&gt;This is the field-note version of what we learned building support bots for businesses serving Southeast Asia: the "multilingual" part is not one problem, it's three, and conflating them is why these deployments quietly cost 3x and quietly fail the Tamil- or Malay-speaking customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three problems hiding under "multilingual"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Language detection under code-switching.&lt;/strong&gt;&lt;br&gt;
A real SEA customer doesn't pick a language. They write &lt;em&gt;"boleh tolong check my order ah?"&lt;/em&gt; — Malay and English in one breath. A monolingual pipeline treats that as an error condition. The first routing decision isn't "which model," it's "is this even one language," and most systems answer wrong. Miss it and you've already degraded the interaction before the model sees it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Language is not model size.&lt;/strong&gt;&lt;br&gt;
This is the one nobody warns you about. English is relatively simple — SVO, minimal inflection — and a small model holds quality fine. Arabic, Malay, and Tamil carry morphological complexity that needs a larger model to stay accurate. Routing every language to the same frontier model leaves money and latency on the table for English while still under-serving the harder languages. We saw a 3.5B-class model hold English quality while a 7B-class tier was needed for the others. One published pipeline routes low-resource languages through translate-to-English before classification and lifts low-tier accuracy from ~0.46 to ~0.68 Macro-F1 — same idea: not every language deserves the same path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Per-step routing inside a single turn.&lt;/strong&gt;&lt;br&gt;
"Check my order" is classify-and-lookup. "Explain why my transfer failed and draft a reply" is synthesis. Same customer, same language, two completely different model tiers — and most bots send both to the expensive one because the router only sees "it's a support message."&lt;/p&gt;

&lt;h2&gt;
  
  
  Then PDPA decides your topology
&lt;/h2&gt;

&lt;p&gt;Singapore's PDPA (and Malaysia's, moving alongside it) wants clarity on where data is processed and stored, PII redaction, and no-train terms with the underlying providers. For a support bot handling customer PII, that means inference runs in-region.&lt;/p&gt;

&lt;p&gt;Here's the part we didn't expect: the residency requirement and the latency requirement turn out to be the &lt;em&gt;same&lt;/em&gt; decision. Keep inference in-region and you've satisfied the residency half of PDPA while staying under the ~300ms threshold for natural conversation. Push it overseas and you're violating one and degrading the other. "Where does the model run" stops being a cost footnote and becomes the architecture. We run inference in-region (SG-hosted, on Tencent Cloud, PDPA-aligned) partly because the regulation demands it and partly because it's the cleanest way the latency math works — the compliance team and the performance team ended up arguing for the identical topology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this only works if the models are a dial
&lt;/h2&gt;

&lt;p&gt;None of the above is a single-model story. The language-tier decision — 3.5B for English, 7B for the harder languages, a frontier model for the synthesis step — only pays off if switching tiers is a config change, not a migration. We sit 25+ models behind one OpenAI-compatible endpoint, so "route Malay to a bigger tier" is a line in a routing table, not a procurement cycle. That unglamorous fact is the reason routing works at all: the models are a dial, not a commitment. And per-language routing is also where most of the cost saving actually comes from — you stop paying frontier rates for English classification.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part we're still getting wrong
&lt;/h2&gt;

&lt;p&gt;We measure routing quality with one global score, but the failure modes are per-tier. English can be perfect while a low-resource language drifts for a week and nobody notices until a customer complains in Tamil. We're moving to Macro-F1 &lt;em&gt;per language tier&lt;/em&gt; as the monitoring signal. Curious whether others running multilingual routing monitor drift per-language or still trust a single aggregate — that's the open question we haven't settled.&lt;/p&gt;

&lt;p&gt;The whole point: a multilingual bot isn't a translation layer bolted onto one model. It's a router whose first dimension is language, and language is messier, more tiered, and more regulated than the translation mental model admits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We're writing these up as we run them — more at tokenlat.com.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>llm</category>
    </item>
    <item>
      <title>Stop asking "is this task hard?" Route by what the token is for.</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:03:38 +0000</pubDate>
      <link>https://dev.to/tokenlat/stop-asking-is-this-task-hard-route-by-what-the-token-is-for-nei</link>
      <guid>https://dev.to/tokenlat/stop-asking-is-this-task-hard-route-by-what-the-token-is-for-nei</guid>
      <description>&lt;p&gt;Last week I made the case for measuring whether your routing actually works — comparing output distributions per route, computing cost per &lt;em&gt;successful&lt;/em&gt; task instead of per token, versioning the harness. If you did that, you've already beaten most teams.&lt;/p&gt;

&lt;p&gt;But even teams that route correctly are usually routing at the wrong granularity. The decision they make is "is this request hard?" — and then the whole request goes to frontier or to cheap. The request is the wrong unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The expensive part was never the model
&lt;/h2&gt;

&lt;p&gt;A recent write-up described cutting token use roughly 42x on a code-editing workload. The tempting read is "they found a smaller model." They didn't. The saving came from deleting the agent's explore-reread-redecide loop — the tokens spent &lt;em&gt;figuring out what to do&lt;/em&gt; rather than &lt;em&gt;doing it&lt;/em&gt;. The model barely changed. What changed was which job the tokens were performing.&lt;/p&gt;

&lt;p&gt;That's the tell. When a workload gets dramatically cheaper without changing models, the lever wasn't model choice. It was that someone stopped spending tokens on a job that didn't need a frontier model at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most "hard" requests are mostly mechanical
&lt;/h2&gt;

&lt;p&gt;Look at almost any production request. A single task that feels hard — summarize this thread, draft a reply, extract structured fields from a doc — is usually a thin slice of genuine reasoning wrapped in a lot of mechanical work: pull the right context, classify it, format the output, validate the schema, retry on parse failure.&lt;/p&gt;

&lt;p&gt;Route the whole request to a frontier model and you pay frontier prices for the 80% that was extraction and formatting. Route the whole request to a cheap model and you gamble the 20% that actually needed the frontier.&lt;/p&gt;

&lt;p&gt;Per-request routing forces you to answer a question you can't reliably answer up front: "how hard is this?" Difficulty is a property you discover, not one you know at the door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route by token-purpose, not by difficulty
&lt;/h2&gt;

&lt;p&gt;The more useful axis is &lt;em&gt;what the token is for&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instruction and system tokens.&lt;/strong&gt; Stable, repeated, cacheable. They barely benefit from a frontier model — and they're the easiest to keep on the cheap route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured extraction, classification, formatting.&lt;/strong&gt; Routine by definition. These are where most of your volume lives, and where cheaper models — typically 70%+ cheaper, often up to 90%+ on China models — are indistinguishable in output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First pass vs. escalation.&lt;/strong&gt; You can't know a request is hard before you generate. A gate that decides &lt;em&gt;after&lt;/em&gt; a cheap first pass fails — and only escalates the part that failed — spends frontier tokens only where they earn it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The genuinely ambiguous slice.&lt;/strong&gt; Keep frontier here. But make it the slice, not the request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why you can't do this by hand
&lt;/h2&gt;

&lt;p&gt;You can't easily split a prompt into "tokens that are formatting" and "tokens that are reasoning" before you send it. That's the job of the call boundary. A routing gateway sits there, sees each request, and assigns the job — so the 80% mechanical portion never reaches a frontier model, and the thin hard portion does.&lt;/p&gt;

&lt;p&gt;And you can only trust it if you can &lt;em&gt;see&lt;/em&gt; it. This is where last week's point lands: route by job, then measure cost per successful task including retries and escalations, with request-level tracing. A gateway that hides the blend is just a black box you're told is cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic shape
&lt;/h2&gt;

&lt;p&gt;Take a support pipeline: classify the ticket, draft a reply, validate against a template. At frontier prices the extraction and formatting alone might run six figures a year. Split by job — cheap model for classify and format, frontier only for the draft's hard cases, escalation after a failed first pass — and the bill typically drops 70%+. On China models the same split can reach 90%+ cheaper. The saving only counts, of course, if you measured cost per &lt;em&gt;successful&lt;/em&gt; task and the retries didn't eat it back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this matters most
&lt;/h2&gt;

&lt;p&gt;For teams in Southeast Asia — Malaysia, Singapore, Indonesia — building on tight infrastructure budgets and answering to data-sovereignty rules that US-default tooling tends to ignore, the unit of routing is also a unit of control. A gateway that is SG-hosted (Tencent Cloud) and PDPA-aligned, exposing one OpenAI-compatible endpoint over 25+ models, lets a KL-based team route by job without shipping data out of the region or stitching five providers together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit I'd steal
&lt;/h2&gt;

&lt;p&gt;Next time you reach for "is this task hard?", stop. Ask "what job is this token doing?" Route the mechanical jobs to cheap models, keep frontier for the slice that earns it, escalate &lt;em&gt;after&lt;/em&gt; the cheap pass fails — and trace every request so you can prove the blend worked. Routing by difficulty guesses at the door. Routing by token-purpose decides after it reads the work.&lt;/p&gt;

&lt;p&gt;If you're wiring this up, &lt;a href="https://tokenlat.com" rel="noopener noreferrer"&gt;TokenLat&lt;/a&gt; exposes a single OpenAI-compatible endpoint over 25+ models with request-level tracing, so the blend you route is the blend you can actually prove.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>productivity</category>
    </item>
    <item>
      <title>You routed 80% to cheaper models. Now measure whether it worked.</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:11:43 +0000</pubDate>
      <link>https://dev.to/tokenlat/you-routed-80-to-cheaper-models-now-measure-whether-it-worked-4pf5</link>
      <guid>https://dev.to/tokenlat/you-routed-80-to-cheaper-models-now-measure-whether-it-worked-4pf5</guid>
      <description>&lt;p&gt;Last week I argued the obvious part: most production LLM traffic — extraction, classification, short rewrites — rarely needs the frontier model, and routing it to cheaper models (Chinese open-weight models are typically 70%+ cheaper, often up to 90%+ on China models) turns a flat bill into a blended one.&lt;/p&gt;

&lt;p&gt;The easy part is done. The hard part is the question nobody answers: &lt;strong&gt;did the routing actually work, or did you just make your bills cheaper and your outputs silently worse?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Success rate is a half-truth
&lt;/h2&gt;

&lt;p&gt;The first metric teams reach for is "did the call succeed." 200 OK. JSON parsed. Task completed. Green checkmark.&lt;/p&gt;

&lt;p&gt;That metric can't answer the question. A cheap-model call can return 200, parse cleanly, and still be wrong in a way that only surfaces three steps later. The call that &lt;em&gt;looks&lt;/em&gt; fine and is quietly wrong is the dangerous failure — and it's exactly the failure routing introduces, because you changed which model handles the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure distributions, not pass rates
&lt;/h2&gt;

&lt;p&gt;For a sample of real traffic on each route, compare the &lt;em&gt;distribution&lt;/em&gt; of outputs against a frontier baseline — not a single "did it pass" flag.&lt;/p&gt;

&lt;p&gt;A median output length collapsing from ~20k tokens to 0 is a different signal than an empty result. A confidence score shifting by a point is noise; a whole cluster of outputs landing in a range the frontier never produced is a behavior change. You only see this if you measure the blend, not the pass rate. Route cheap, but verify the cheap route is still producing output you'd have trusted before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost per successful task, not cost per token
&lt;/h2&gt;

&lt;p&gt;The second trap is counting tokens. "I saved 70% per call" is true and irrelevant if the cheap call fails 15% of the time and each failure triggers a retry or an escalation to frontier anyway.&lt;/p&gt;

&lt;p&gt;The number that matters is cost per &lt;em&gt;successful&lt;/em&gt; task: total spend across the retry and escalation path divided by tasks that actually completed correctly. A cheap-first policy with a clean fallback can still win — but you have to measure the whole path, not the first hop. If your "savings" vanish once you include the retries, you haven't optimized, you've deferred the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The router is part of the benchmark
&lt;/h2&gt;

&lt;p&gt;Here's the one people miss: when you change the routing policy and re-run your eval, you're not measuring the model. You're measuring the &lt;em&gt;router plus&lt;/em&gt; the model. Version the harness — the prompts, the retry logic, the memory, the tool schemas — the same way you version the model. A benchmark number with an unversioned harness is a number you can't reproduce and can't trust.&lt;/p&gt;

&lt;p&gt;And don't trust a single run. "All 20 passed" tells you the mean passed; it tells you nothing about the tail. Variance in latency, in failure modes, in output quality lives in the tail, and the tail is where production breaks. One trial is a point estimate, not a confidence interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this bites harder on a tight budget
&lt;/h2&gt;

&lt;p&gt;This is where it lands hardest for teams in Southeast Asia — Malaysia, Singapore, Indonesia — building on tight infrastructure budgets and answering to data-sovereignty rules (Malaysia's PDPA, for instance) that US-default tooling tends to ignore. When every dollar of inference is scrutinized, "we saved 70%" on a slide means nothing if nobody measured whether the work still got done. A routing gateway that is SG-hosted (Tencent Cloud) and PDPA-aligned lets a KL-based team route cheap-when-possible and frontier-when-needed — but the saving only counts if you can prove the cheap route is trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit I'd steal
&lt;/h2&gt;

&lt;p&gt;Stop celebrating "everything succeeded." Start measuring the blend. Pick a sample, compare distributions per route, compute cost per &lt;em&gt;successful&lt;/em&gt; task including retries, and version the harness so the number means something next month. Routing without measurement is just hope with extra steps.&lt;/p&gt;

&lt;p&gt;If you're wiring this up, &lt;a href="https://tokenlat.com" rel="noopener noreferrer"&gt;TokenLat&lt;/a&gt; exposes a single OpenAI-compatible endpoint over 25+ models with request-level tracing, so the blend you measure is the blend you actually shipped.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>api</category>
      <category>llm</category>
    </item>
    <item>
      <title>Most of Your LLM Spend Is Wasted on Calls That Don't Need a Frontier Model</title>
      <dc:creator>TokenLat</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:31:22 +0000</pubDate>
      <link>https://dev.to/tokenlat/most-of-your-llm-spend-is-wasted-on-calls-that-dont-need-a-frontier-model-39o</link>
      <guid>https://dev.to/tokenlat/most-of-your-llm-spend-is-wasted-on-calls-that-dont-need-a-frontier-model-39o</guid>
      <description>&lt;p&gt;If your LLM bill looks like a flat line of frontier-model calls, you're probably overpaying by 70% or more for work that a cheaper model would do just as well.&lt;/p&gt;

&lt;p&gt;I'm not talking about a toy benchmark. I mean the actual shape of production traffic: extraction, classification, short rewrites, JSON shaping, routing decisions, draft summaries. The kinds of calls that make up the bulk of a real pipeline — and almost none of which need the most expensive model on the menu.&lt;/p&gt;

&lt;p&gt;This post is a practical routing playbook, grounded in what Chinese open-weight models are actually capable of in 2026, and why the "one model to rule them all" habit is the most expensive default in modern AI infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: every call goes to the most expensive model
&lt;/h2&gt;

&lt;p&gt;Here's a pipeline I audited recently. ~12,000 LLM calls/day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;61% were structured extraction (pull fields from an email, a ticket, a PDF)&lt;/li&gt;
&lt;li&gt;22% were classification (intent, sentiment, spam/not-spam)&lt;/li&gt;
&lt;li&gt;11% were short rewrites / tone adjustments&lt;/li&gt;
&lt;li&gt;6% were genuinely hard: open-ended reasoning, ambiguous multi-step planning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every single one was sent to the top-tier model. The 6% that justified it. And the 94% that absolutely did not.&lt;/p&gt;

&lt;p&gt;The instinct is understandable. Frontier models are the safe choice. If you route to a cheaper model and it fails, that's on you. So everyone defaults to the best — and quietly pays a premium on traffic that would never have noticed the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Chinese models actually do now
&lt;/h2&gt;

&lt;p&gt;The "Chinese models are cheap but worse" framing is two years out of date. In 2026 the top open-weight models from China — DeepSeek, Kimi, GLM, Qwen — routinely match or beat frontier models on the majority of practical, production-shaped tasks.&lt;/p&gt;

&lt;p&gt;Two data points that reframed how I think about this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The MoE argument from the model itself.&lt;/strong&gt; A recent write-up on Kimi K3's Mixture-of-Experts architecture noted that of 896 experts, only ~1.8% activate per token. The model is &lt;em&gt;already routing internally&lt;/em&gt; — deciding, token by token, which sliver of capacity a given input needs. If the model architects route by difficulty at the parameter level, it's a little odd that we don't route by difficulty at the request level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The benchmark reality.&lt;/strong&gt; On standard reasoning and coding evals, the gap between frontier and top Chinese models has collapsed on everything except the hardest 10–20% of tasks. For extraction, classification, and most summarization, the delta is within noise.&lt;/p&gt;

&lt;p&gt;That last 10–20% is real and worth paying for. The other 80% is where the money leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The playbook: route by scenario, not by model name
&lt;/h2&gt;

&lt;p&gt;The fix isn't "use a cheap model for everything." It's a tiered policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudo-policy: route by task difficulty, not model brand
&lt;/span&gt;&lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extraction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cheap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# structured field pulling — deterministic enough
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cheap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# intent/sentiment/spam — high volume, low variance
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cheap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# tone/format — cheap models are fine
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cheap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# unless source is very long/technical
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# open-ended, ambiguous, multi-step
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# high-stakes, low-tolerance
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# The 80/20 default: assume cheap until a signal says otherwise
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;select_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cheap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cheap_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;# e.g. a top Chinese open-weight model
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;frontier_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design choice: &lt;strong&gt;default to cheap, escalate on signal.&lt;/strong&gt; Not the other way around. A confidence threshold or a fast pre-classifier decides whether a call deserves the expensive model.&lt;/p&gt;

&lt;p&gt;A gateway that exposes 25+ models behind one OpenAI-compatible endpoint makes this trivial — you write the policy once, the gateway routes, and you stop hand-wiring API keys for every provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually saves
&lt;/h2&gt;

&lt;p&gt;For most teams the math is not subtle. Routing ~80% of traffic to cheaper models — with Chinese open-weight models typically &lt;strong&gt;70%+ cheaper&lt;/strong&gt; than frontier list prices, and on many Chinese models &lt;strong&gt;up to 90%+ cheaper&lt;/strong&gt; — turns a flat frontier bill into a blended one.&lt;/p&gt;

&lt;p&gt;The number that matters isn't "how much did the model cost per token." It's "how much of my traffic never needed the expensive model in the first place." That's the lever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't route blind — watch the distribution
&lt;/h2&gt;

&lt;p&gt;One trap I've seen: teams set up routing, see "everything succeeds," and declare victory. But success rate lies. The dangerous failure is the call that &lt;em&gt;looks&lt;/em&gt; fine and is quietly wrong.&lt;/p&gt;

&lt;p&gt;If you route 80% to cheap models, the metric that matters isn't "did the call return 200." It's "does the cheap-model output still look like the data we trusted before." Watch the distribution of outputs per route. A median collapsing from ~20k to 0 is a different signal than an empty result — and it only shows up if you're measuring the blend, not just the pass rate. Route cheap, but verify the cheap route is actually producing trustworthy output. Routing without measurement is just hope with extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more outside the US
&lt;/h2&gt;

&lt;p&gt;There's a reason this playbook resonates harder in emerging markets. In Southeast Asia — Malaysia, Singapore, Indonesia — teams build on tight infrastructure budgets and answer to data-sovereignty rules (Malaysia's PDPA, for instance) that US-default tooling tends to ignore.&lt;/p&gt;

&lt;p&gt;A routing gateway that is &lt;strong&gt;SG-hosted (Tencent Cloud)&lt;/strong&gt; and &lt;strong&gt;PDPA-aligned&lt;/strong&gt; lets a KL-based team get frontier-when-needed and cheap-when-possible, without shipping customer data through jurisdictions that complicate compliance. Cost-sensitive markets are exactly where the 80/20 split stops being an optimization and starts being the difference between a project that ships and one that doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule I'd steal from this
&lt;/h2&gt;

&lt;p&gt;Stop treating the model selector like a constant. Treat it like a router. Most of your calls are mechanical — route them cheap. Keep frontier for the slice that earns it. The models to do both are already here, and most of them don't cost what you're paying.&lt;/p&gt;

&lt;p&gt;If you're wiring this up, &lt;a href="https://tokenlat.com" rel="noopener noreferrer"&gt;TokenLat&lt;/a&gt; exposes a single OpenAI-compatible endpoint over 25+ models — route by scenario, measure the blend, and tune the thresholds as signal accumulates.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
