<?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: synthorai</title>
    <description>The latest articles on DEV Community by synthorai (@synthorai).</description>
    <link>https://dev.to/synthorai</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%2F3954184%2Ff7a20b6f-3f1e-4eed-85a3-486012422cbd.png</url>
      <title>DEV Community: synthorai</title>
      <link>https://dev.to/synthorai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/synthorai"/>
    <language>en</language>
    <item>
      <title>Claude Fable 5 for Agents: Tool-Call Refusals, Cost vs GLM 5.2</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Tue, 07 Jul 2026 05:49:17 +0000</pubDate>
      <link>https://dev.to/synthorai/claude-fable-5-for-agents-tool-call-refusals-cost-vs-glm-52-41kb</link>
      <guid>https://dev.to/synthorai/claude-fable-5-for-agents-tool-call-refusals-cost-vs-glm-52-41kb</guid>
      <description>&lt;p&gt;Claude Fable 5 refused mid-tool-call on 11 of 44 coding-agent turns in our eval, on tasks as mundane as fixing a config default. The refusal arrives as &lt;code&gt;stop_reason: "refusal"&lt;/code&gt; partway through generating the tool arguments, the truncated arguments still parse as valid JSON, and an agent loop that executes tool calls without checking the stop reason will happily write a half-finished file to disk. That behavior, not the price tag, is the first thing to engineer around when you put Fable 5 in an agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Fable 5 returned &lt;code&gt;stop_reason: "refusal"&lt;/code&gt; mid-tool-call on mundane agent tasks (a config-default fix, a meeting-room booking); the truncated &lt;code&gt;write_file&lt;/code&gt; arguments still parsed, so a loop that doesn't check the stop reason executes half-written files.&lt;/li&gt;
&lt;li&gt;Fable 5's thinking is adaptive, with no off switch: &lt;code&gt;enabled&lt;/code&gt; and &lt;code&gt;disabled&lt;/code&gt; are both rejected; the control is &lt;code&gt;output_config.effort&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fable 5's cost premium is shape-dependent: a four-turn coding task ran $0.045 vs $0.003 on glm-5.2 (15x), but only 5x sonnet-5 on warm batch work.&lt;/li&gt;
&lt;li&gt;Fable 5 requires 30-day data retention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything below was measured on 2026-07-05 through the Synthorai gateway with a small scenario harness: five agent workload shapes (a tool-using coding loop, RAG question answering, tool-heavy orchestration, batch classification, and a 15-turn conversation), run against &lt;code&gt;claude-fable-5&lt;/code&gt;, &lt;code&gt;claude-opus-4-8&lt;/code&gt;, &lt;code&gt;claude-sonnet-5&lt;/code&gt;, and &lt;code&gt;glm-5.2&lt;/code&gt;, three runs per task where variance matters. The tasks are deliberately trivial; pass rates are a sanity gate, not a capability benchmark. Costs are the gateway's billed &lt;code&gt;usage.cost&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check stop_reason before executing tool calls
&lt;/h2&gt;

&lt;p&gt;This is the failure the docs don't warn you about, and it corrupts state. The agent reads &lt;code&gt;app.py&lt;/code&gt;, decides to write the fix, and starts emitting a &lt;code&gt;write_file&lt;/code&gt; call. Partway through the file content, the stream stops:&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;"stop_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refusal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write_file"&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;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEFAULTS = {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;timeout_s&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 30,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;input&lt;/code&gt; object is complete, parseable JSON. Nothing about it says "I stopped early." If your loop's contract is "got tool calls, run them," you just overwrote &lt;code&gt;app.py&lt;/code&gt; with a 38-character fragment that ends mid-dictionary and no longer parses as Python, and the next turn is a refusal too, so the loop ends with the workspace corrupted.&lt;/p&gt;

&lt;p&gt;Three things we can say from the data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It triggers on mundane work.&lt;/strong&gt; The tasks that drew refusals were fixing a &lt;code&gt;KeyError&lt;/code&gt; in a config lookup, implementing a slugify function, booking a meeting room, and creating a draft invoice. Nothing dual-use, nothing sensitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is repeatable, not random.&lt;/strong&gt; One coding task drew a refusal on all three runs, streaming and non-streaming alike. Other tasks never drew one. Across conditions, 58-75% of our trivial coding episodes passed on Fable 5 versus 100% for claude-opus-4-8, claude-sonnet-5, and glm-5.2, and every single failure traces to a refusal, not to wrong code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Once a refusal is in the conversation, the episode is done.&lt;/strong&gt; Follow-up turns returned &lt;code&gt;stop_reason: "refusal"&lt;/code&gt; with empty output. Retrying within the same context did not recover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trigger is not the task content, and the data is blunt about it. The task that refused every run was a nine-line &lt;code&gt;KeyError&lt;/code&gt; fix in a config dictionary, no credentials, no exploits. Meanwhile the batch scenario classified support tickets about cryptomining, leaked Stripe keys, and phishing pages without a single refusal, and the RAG scenario answered over docs full of AES-256-GCM secrets and breach-response procedures, also clean. Every refusal landed in the two multi-turn, tool-executing scenarios; the three single-shot scenarios never refused once, even carrying heavier content. The pattern is the agent-loop shape, not the words, which means sanitizing your inputs won't prevent it.&lt;/p&gt;

&lt;p&gt;The fix is one line before your tool-execution 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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refusal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# do NOT execute tool calls from this turn: arguments may be truncated
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AgentInterrupted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model refused; restart episode or escalate&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;p&gt;Anthropic documents the mechanics: a refusal that fires before any output returns an empty &lt;code&gt;content&lt;/code&gt; array and is not billed; a mid-stream refusal bills the already-streamed output, and the guidance is to discard the partial. The response also carries a &lt;code&gt;stop_details&lt;/code&gt; object with a category (such as &lt;code&gt;cyber&lt;/code&gt; or &lt;code&gt;bio&lt;/code&gt;, or null) so you can tell classifier blocks from ordinary declines. What the documentation doesn't spell out is the interaction with tool use we hit above: the refusal can land inside argument generation, and the partial arguments are indistinguishable from complete ones.&lt;/p&gt;

&lt;p&gt;There is also an official recovery path. On the Claude API, the beta &lt;code&gt;fallbacks&lt;/code&gt; parameter (&lt;code&gt;betas: ["server-side-fallback-2026-06-01"]&lt;/code&gt;, &lt;code&gt;fallbacks: [{"model": "claude-opus-4-8"}]&lt;/code&gt;) re-runs a declined request on a fallback model inside the same call, with the decline itself unbilled when it fired pre-output. It is not available on Amazon Bedrock, Vertex AI, or Microsoft Foundry, where the SDKs ship a client-side fallback middleware instead. Whichever path applies, the guard above still comes first: never execute tool calls from a turn whose stop reason is a refusal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What five agent shapes cost
&lt;/h2&gt;

&lt;p&gt;Median cost per completed unit (task, query, item, or conversation), same prompts, same day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;fable-5&lt;/th&gt;
&lt;th&gt;opus-4-8&lt;/th&gt;
&lt;th&gt;sonnet-5&lt;/th&gt;
&lt;th&gt;glm-5.2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coding loop (per task, 4 turns median)&lt;/td&gt;
&lt;td&gt;$0.045&lt;/td&gt;
&lt;td&gt;$0.012&lt;/td&gt;
&lt;td&gt;$0.0059&lt;/td&gt;
&lt;td&gt;$0.0031&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG answer (per query)&lt;/td&gt;
&lt;td&gt;$0.024&lt;/td&gt;
&lt;td&gt;$0.0075&lt;/td&gt;
&lt;td&gt;$0.0036&lt;/td&gt;
&lt;td&gt;$0.0031&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool orchestration (per task)&lt;/td&gt;
&lt;td&gt;$0.048&lt;/td&gt;
&lt;td&gt;$0.011&lt;/td&gt;
&lt;td&gt;$0.0045&lt;/td&gt;
&lt;td&gt;$0.0027&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch classification (per item, warm)&lt;/td&gt;
&lt;td&gt;$0.0024&lt;/td&gt;
&lt;td&gt;$0.0012&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00046&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.00057&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15-turn conversation (whole)&lt;/td&gt;
&lt;td&gt;$0.94&lt;/td&gt;
&lt;td&gt;$0.34&lt;/td&gt;
&lt;td&gt;$0.26&lt;/td&gt;
&lt;td&gt;$0.083&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two readings of that table matter more than any single cell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The cheapest model changes with the shape.&lt;/strong&gt; glm-5.2 wins the loops and the long conversation, but claude-sonnet-5 is the cheapest batch classifier in the set, under glm-5.2, because its &lt;a href="https://synthorai.io/blog/claude-sonnet-5-tokenizer/" rel="noopener noreferrer"&gt;introductory price&lt;/a&gt; rides on a 97% cache-read share once the scaffold prompt is warm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fable 5's premium is shape-dependent too&lt;/strong&gt;: 15x glm-5.2 on the coding loop, 11x on conversation, but only 5x sonnet-5 on warm batch items, where caching absorbs most of the prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of the cost picture is about controlling those numbers, and then about two things that quietly push them back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping an agent's bill down
&lt;/h2&gt;

&lt;p&gt;Caching is the single biggest lever, and on Fable 5 its &lt;a href="https://synthorai.io/blog/claude-fable-5-prompt-caching/" rel="noopener noreferrer"&gt;contract is unchanged&lt;/a&gt;. The agent data shows what it is worth: with &lt;code&gt;cache_control&lt;/code&gt; markers removed, the same coding task cost 2.0x more, and warm batch items 6.8x more. On opus-4-8 the same ablation reads 3.8x and 6.9x. In a loop, the sliding-marker pattern is not an optimization, it is the difference between a viable bill and not.&lt;/p&gt;

&lt;p&gt;Prompt order is the second lever, and it held up across every model we ran. Putting stable rules before per-query context (instead of after it) made RAG queries 26-37% cheaper on all four models; on the Claude line, the wrong order additionally pays the 1.25x cache-write premium on every call. The mechanics are in the &lt;a href="https://synthorai.io/blog/langchain-prompt-caching/" rel="noopener noreferrer"&gt;LangChain caching post&lt;/a&gt;; the numbers here just confirm they apply unchanged to Fable 5.&lt;/p&gt;

&lt;p&gt;Fable 5 also adds two levers of its own. The first is that &lt;strong&gt;the cache-eligibility floor dropped to 2,048 tokens&lt;/strong&gt;, half of Opus 4.8's 4,096. That reads like trivia until you remember where an agent's savings come from: the repeated scaffold (system prompt, tool definitions, the sliding conversation prefix) is what caches, and only if it clears the floor. A tool-heavy agent whose per-turn prefix sat between 2,048 and 4,096 tokens got no caching at all on Opus 4.8, and starts caching on Fable 5, turning a full-price prefix into a roughly 10%-of-price cache read on every subsequent turn. It cuts the other way too: a prefix padded to clear the old 4,096 floor may now be carrying dead weight. Read &lt;code&gt;cache_read_input_tokens&lt;/code&gt; off a live response rather than assuming, because on Fable 5 the discount begins earlier.&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;task budgets&lt;/strong&gt; (beta, header &lt;code&gt;task-budgets-2026-03-13&lt;/code&gt;), which address the exact problem this comparison keeps surfacing: a Fable 5 loop runs up a bill fast, and &lt;code&gt;max_tokens&lt;/code&gt; does not help. It is a hard per-response cap the model cannot see, so the model plans as if it has unlimited room, then gets cut off mid-thought. A task budget is different. You give the loop a token ceiling (minimum 20,000) that the model sees as a running countdown and paces itself against, wrapping up gracefully instead of being truncated. It counts what the model generates plus the tool results it reads on the turn, not the full history you resend each request. On a model whose coding-loop turn costs 15x glm-5.2, a budget the model self-moderates toward is the cheapest guardrail you can bolt on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two cost surprises the docs won't flag
&lt;/h2&gt;

&lt;p&gt;With the levers set, two smaller things still moved our bill in directions the documentation doesn't warn you about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Low" effort was not cheaper.&lt;/strong&gt; Fable 5's thinking depth is controlled by &lt;code&gt;output_config.effort&lt;/code&gt;, and the intuition is that &lt;code&gt;low&lt;/code&gt; costs less. It didn't. Setting &lt;code&gt;effort: "low"&lt;/code&gt; ran our coding loop at $0.0478 per task versus $0.0451 at the default, with more output tokens, not fewer. We saw the same pattern on GLM 5.2, where the &lt;a href="https://synthorai.io/blog/glm-52-coding-cost/" rel="noopener noreferrer"&gt;effort names don't track token counts&lt;/a&gt; either. On both model lines, measure the knob on your own workload before assuming "low" means "less." One reason the number is hard to predict: adaptive thinking's share of output tokens swung from 2% on the coding loop to 30% on RAG answers to 52% on batch classification, same model, same day. Budget output tokens per workload shape, not per model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never replay &lt;code&gt;reasoning_content&lt;/code&gt;.&lt;/strong&gt; On OpenAI-compatible models, the reasoning field is not conversation history. DeepSeek's API requires stripping it; on &lt;a href="https://synthorai.io/blog/glm-5-2-tool-calls/" rel="noopener noreferrer"&gt;GLM 5.2&lt;/a&gt; replaying it is legal but billed. Feeding it back into the message history inflated our GLM loop cost by about 28% until we stripped it. Anthropic's own thinking blocks are different: on the same model you must replay them unchanged, but a Fable 5 thinking block routed to a &lt;em&gt;different&lt;/em&gt; model (say, on a fallback to Opus) is dropped from the prompt automatically and not billed, so there's nothing to strip.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the request surface
&lt;/h2&gt;

&lt;p&gt;Fable 5 shares most of its request surface with Opus 4.7/4.8 and Sonnet 5. What's gone, per the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;thinking: {type: "enabled", budget_tokens: N}&lt;/code&gt; returns a 400. &lt;a href="https://platform.claude.com/docs/en/build-with-claude/extended-thinking" rel="noopener noreferrer"&gt;Extended thinking&lt;/a&gt; with a token budget, the mechanism from Claude 3.7 Sonnet through the 4.5 family, is retired across the 4.7+ line in favor of &lt;a href="https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking" rel="noopener noreferrer"&gt;adaptive thinking&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;thinking: {type: "disabled"}&lt;/code&gt; returns a 400, and this one is unique to Fable 5. Opus 4.7/4.8 and Sonnet 5 still let you switch thinking off; Fable 5 does not.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p&lt;/code&gt;, and &lt;code&gt;top_k&lt;/code&gt; are rejected at any non-default value.&lt;/li&gt;
&lt;li&gt;Assistant-message prefills (a trailing &lt;code&gt;assistant&lt;/code&gt; turn) return a 400.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;temperature&lt;/code&gt;/&lt;code&gt;top_p&lt;/code&gt;/&lt;code&gt;top_k&lt;/code&gt; and prefill removals are the two that most often bite a ported request; the thinking and retention changes are covered above and in the &lt;a href="https://synthorai.io/blog/fable-5-30-day-data-retention/" rel="noopener noreferrer"&gt;retention post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Fable 5 in an agent is an engineering problem before it is a budget problem. Handle &lt;code&gt;stop_reason: "refusal"&lt;/code&gt; before executing tool calls, or a truncated write will corrupt state on a task as boring as a config fix. Then treat the cost as something you shape: caching is the biggest lever, the eligibility floor is now 2,048 tokens so re-check your prefix, a task budget keeps a loop from running up the highest per-turn bill in this comparison, and &lt;code&gt;effort: "low"&lt;/code&gt; is not the discount its name implies. Budget by workload shape, too: the same model is 15x the cost of glm-5.2 on a coding loop and 5x sonnet-5 on warm batch work. None of this says use it or don't; it says the defaults are not neutral, and the bill and the failure modes both depend on how your agent is shaped.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Fable 5 refuse tool calls often?&lt;/strong&gt;&lt;br&gt;
It concentrated on specific tasks: one config-fix task refused on every run, others never did, and the same tasks reproduced under both streaming and non-streaming calls. So it is not a rare flake you can retry past. Rates on your workload will differ; the engineering answer is the same either way: check &lt;code&gt;stop_reason&lt;/code&gt; before executing tool calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I turn Fable 5's thinking off?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;thinking.type.disabled&lt;/code&gt; and &lt;code&gt;enabled&lt;/code&gt; are both rejected. Thinking is adaptive by default and &lt;code&gt;output_config.effort&lt;/code&gt; is the only control; in our loop, &lt;code&gt;low&lt;/code&gt; effort did not reduce cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Fable 5 ever the cheap option?&lt;/strong&gt;&lt;br&gt;
Not in this set. Its smallest premium is on warm cache-heavy batch work, about 5x sonnet-5, where a warm cache absorbs most of the prompt. On the loops and the long conversation it is the most expensive model we ran.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verification: all figures measured 2026-07-05 against &lt;code&gt;https://synthorai.io/&lt;/code&gt; (Anthropic-native &lt;code&gt;/v1/messages&lt;/code&gt; for the Claude line, &lt;code&gt;/v1/chat/completions&lt;/code&gt; for glm-5.2), 505 episodes and 1,022 calls across five scenario shapes, three runs per task where variance matters. Costs are the gateway-reported &lt;code&gt;usage.cost&lt;/code&gt;; medians shown. Tasks are deliberately simple, so pass rates are a sanity gate, not a capability benchmark; we don't publish capability claims we haven't measured. Refusal behavior reproduced in both streaming and non-streaming modes. Your numbers will vary with prompts, region, and load.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>glm52</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>LLM Prompt Caching #5: LangChain Setups That Actually Hit</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Sat, 04 Jul 2026 09:41:35 +0000</pubDate>
      <link>https://dev.to/synthorai/llm-prompt-caching-5-langchain-setups-that-actually-hit-186g</link>
      <guid>https://dev.to/synthorai/llm-prompt-caching-5-langchain-setups-that-actually-hit-186g</guid>
      <description>&lt;p&gt;Here is a LangChain system prompt that looks perfectly reasonable and caches nothing:&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;langchain_core.prompts&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&lt;/span&gt;&lt;span class="p"&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;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BIG_STABLE_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# the syntax every tutorial uses
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&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;{question}&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We ran this against &lt;code&gt;claude-sonnet-5&lt;/code&gt; twice with an identical 1,800-token system prompt and read the usage fields back. Both calls: cache writes 0, cache reads 0. Not a partial hit, not a fragmented cache. Nothing. The reason: Anthropic only caches what you mark with &lt;code&gt;cache_control&lt;/code&gt;, and a plain string in a &lt;code&gt;("system", ...)&lt;/code&gt; tuple has nowhere to put the marker. The most convenient syntax in LangChain is also the one that leaves the entire discount on the table, and no error tells you.&lt;/p&gt;

&lt;p&gt;This is part 5 of the caching series. &lt;a href="https://synthorai.io/blog/llm-prompt-caching-explained/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; covers how prefix caching works, &lt;a href="https://synthorai.io/blog/prompt-caching-tutorial-code-examples/" rel="noopener noreferrer"&gt;part 3&lt;/a&gt; does the raw-SDK tutorial. This part is about what changes when LangChain assembles your prompts for you. Everything below was measured on 2026-07-04 through the Synthorai gateway with &lt;code&gt;langchain-core&lt;/code&gt; 1.4.8, &lt;code&gt;langchain-anthropic&lt;/code&gt; 1.4.8, and &lt;code&gt;langchain-openai&lt;/code&gt; 1.3.3.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, which "caching" are you looking for?
&lt;/h2&gt;

&lt;p&gt;Two unrelated features share the word, and the LangChain docs page you land on when you search is usually the wrong one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Response caching (&lt;a href="https://reference.langchain.com/python/langchain-core/caches/InMemoryCache" rel="noopener noreferrer"&gt;LangChain's &lt;code&gt;InMemoryCache&lt;/code&gt;&lt;/a&gt;)&lt;/th&gt;
&lt;th&gt;Prompt caching (this series)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it stores&lt;/td&gt;
&lt;td&gt;The whole completion, in your app&lt;/td&gt;
&lt;td&gt;The prompt prefix's KV state, on the provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When it saves money&lt;/td&gt;
&lt;td&gt;The exact same request repeats&lt;/td&gt;
&lt;td&gt;Different requests share a prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;set_llm_cache(InMemoryCache())&lt;/code&gt;, SQLite, Redis&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cache_control&lt;/code&gt; markers or automatic prefix matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent loops, RAG, chat&lt;/td&gt;
&lt;td&gt;Almost useless (every request differs)&lt;/td&gt;
&lt;td&gt;The main lever, since system + tools repeat every turn&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And "exact same request" means exact: the built-ins key on the pair (serialized prompt, model-config string). Measured: an identical repeat returned in 0 ms with no API call; adding one space to the prompt missed; the same prompt with &lt;code&gt;max_tokens&lt;/code&gt; changed by one also missed. (The cached replay also returns the original call's usage numbers, so naive token accounting double-counts.) Semantic caches exist as third-party integrations; the built-ins are exact-match only.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;set_llm_cache&lt;/code&gt; is fine for deduplicating identical calls in tests; the 2,000-token system prompt you re-send on every agent turn is prompt caching's job, and it needs the prompt assembled the right way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: content blocks, not strings
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cache_control&lt;/code&gt; travels inside a content block, so the system message has to be a &lt;code&gt;SystemMessage&lt;/code&gt; with block content rather than a bare string:&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;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.messages&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SystemMessage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.prompts&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatAnthropic&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://synthorai.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# any Anthropic-compatible endpoint
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;SystemMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&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;type&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;text&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BIG_STABLE_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# a bare string has nowhere to put this
&lt;/span&gt;    &lt;span class="p"&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;human&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;{question}&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;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same 1,800-token system prompt, measured through the same gateway:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;String-tuple syntax&lt;/th&gt;
&lt;th&gt;Content-block syntax&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st (cold)&lt;/td&gt;
&lt;td&gt;write 0 / read 0&lt;/td&gt;
&lt;td&gt;write 1,875 / read 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2nd, different question&lt;/td&gt;
&lt;td&gt;write 0 / read 0&lt;/td&gt;
&lt;td&gt;write 0 / &lt;strong&gt;read 1,875&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A warm read bills at roughly 10% of the input price, so on Claude this one structural change is the difference between paying full price forever and a 90% discount on the stable half of every call. The economics are in &lt;a href="https://synthorai.io/blog/llm-prompt-caching-explained/" rel="noopener noreferrer"&gt;part 1&lt;/a&gt;; the marker mechanics match the raw SDK usage in the &lt;a href="https://docs.langchain.com/oss/python/integrations/chat/anthropic" rel="noopener noreferrer"&gt;LangChain Anthropic integration docs&lt;/a&gt; and &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Anthropic's prompt caching guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where your template variables go decides your hit rate
&lt;/h2&gt;

&lt;p&gt;LangChain templates make it effortless to interpolate variables anywhere, which is exactly the hazard. The cache key is the byte-exact prefix. We put a date inside the cached block and measured:&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="nc"&gt;SystemMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&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;type&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;text&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&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="s"&gt;Today is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;BIG_STABLE_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# variable INSIDE the block
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;ephemeral&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;day A, question 1&lt;/td&gt;
&lt;td&gt;write 1,865 (cold for this value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;day A, question 2&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;read 1,865&lt;/strong&gt; (same value, hit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;day B, question 1&lt;/td&gt;
&lt;td&gt;write 1,865 (new value, cold again)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cache did not break. It got keyed on the variable. A value that repeats, like a date, costs one cache write per value and hits after that. A value that is unique per call, like a timestamp or a request ID, makes every call a cold write and the hit rate exactly zero.&lt;/p&gt;

&lt;p&gt;The expensive real-world version of this mistake is RAG. The template many chains reach for puts the retrieved context at the top of the system prompt, before the static instructions. We measured both orders with an 800-token retrieved context that changes per query and a marked system block:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Order inside the prompt&lt;/th&gt;
&lt;th&gt;Call 1&lt;/th&gt;
&lt;th&gt;Call 2 (new query, new context)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context first, then rules&lt;/td&gt;
&lt;td&gt;write 3,133&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;write 3,133 again, read 0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rules first (marked), context in the human turn&lt;/td&gt;
&lt;td&gt;write 1,852&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;read 1,852&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The wrong row is not merely "no discount": every call pays the cache-write premium, about 1.25× the normal input price, on the full 3,133 tokens, and nothing is ever read back. &lt;strong&gt;A mis-ordered RAG prompt with caching enabled costs more than not caching at all.&lt;/strong&gt; The fixed content sits after the variable content, so it might as well not exist.&lt;/p&gt;

&lt;p&gt;The rule that falls out of the measurements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static text first, inside the marked block.&lt;/strong&gt; System rules, tool definitions, few-shot examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that varies goes after the marker&lt;/strong&gt;, ideally in the human turn: retrieved context, dates, user questions.&lt;/li&gt;
&lt;li&gt;A variable inside the block is acceptable only if it repeats often enough to amortize its own cache write.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tool definitions get cached too
&lt;/h2&gt;

&lt;p&gt;An agent re-sends its tool schemas on every call, and in Anthropic's request layout the tools sit &lt;strong&gt;before&lt;/strong&gt; the system prompt. Since a marker means "cache everything from the start of the request up to here," that raises two practical questions. Does a marker on the system block also cover the tools in front of it? And does LangChain's &lt;code&gt;bind_tools&lt;/code&gt; turn your tools into the exact same bytes on every call, because if the serialization wobbles, the prefix changes and every call misses.&lt;/p&gt;

&lt;p&gt;Measured answers to both. With the same marked system prompt, the warm cache read was 1,861 tokens without tools and &lt;strong&gt;2,389 tokens with two tools bound&lt;/strong&gt;: the extra 528 tokens are the tool schemas coming back out of the cache. And that 2,389 repeated exactly across three consecutive calls, which means &lt;code&gt;bind_tools&lt;/code&gt; serializes the same way every time; the framework does not leak noise into the prefix. So to be explicit: &lt;strong&gt;as long as the system block carries the marker, the tools themselves need no &lt;code&gt;cache_control&lt;/code&gt;&lt;/strong&gt;; that one marker behind them does all the work.&lt;/p&gt;

&lt;p&gt;The opposite arrangement exists for one specific shape: the tools are the biggest stable thing you send and the system prompt is thin or absent. Then the request still needs a marker somewhere, and it can live on a tool. This only works with the raw Anthropic-format dict, because a &lt;code&gt;@tool&lt;/code&gt;-decorated function has no field to carry it; &lt;code&gt;bind_tools&lt;/code&gt; passes the dict through untouched:&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;# variant: NO marked system block anywhere; the tool carries the request's only marker
&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind_tools&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;get_weather&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;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LONG_TOOL_DESCRIPTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# passes through bind_tools verbatim
&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured: write 3,002 cold, read 3,002 warm, with no marked system message in the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-turn: move the marker to the last message
&lt;/h2&gt;

&lt;p&gt;A conversation looks like another ordering problem, but it is the opposite case: the order is already perfect, because history only ever appends, so the whole transcript is stable prefix. The problem here is coverage. A marker on the system block caches the system block and nothing after it: as the history grows, the warm read stays flat at the system size while every accumulated turn bills as ordinary input.&lt;/p&gt;

&lt;p&gt;The pattern that fixes it is the same one the raw SDK uses: put the marker on the &lt;strong&gt;latest&lt;/strong&gt; message, so the breakpoint slides forward and the whole conversation-so-far becomes the cached prefix:&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;marked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HumanMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&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;type&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;text&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;ephemeral&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="c1"&gt;# each turn: history stays plain, only the newest human message carries the marker
&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_question&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured across two turns: turn 1 wrote 1,864; turn 2 &lt;strong&gt;read 1,864 and wrote only the 15-token delta&lt;/strong&gt; (the previous answer plus the new question), the prior prefix billing at the ≈10% read rate. That is the shape an agent loop wants, and LangChain expresses it with an ordinary message list. Anthropic allows up to four markers per request, so the sliding marker composes with a fixed marker on the system block or on the tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the meters, and know their names
&lt;/h2&gt;

&lt;p&gt;LangChain standardizes usage into &lt;code&gt;usage_metadata&lt;/code&gt;, and here is the gotcha we hit: with &lt;code&gt;langchain-anthropic&lt;/code&gt; 1.4.8, on every response in our runs, the standard &lt;code&gt;input_token_details.cache_creation&lt;/code&gt; field &lt;strong&gt;stayed 0 even when a cache write happened&lt;/strong&gt;. The real write count lands in a nonstandard key:&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="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&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;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;det&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage_metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_token_details&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                  &lt;span class="c1"&gt;# correct on hits (1875 above)
&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_creation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;              &lt;span class="c1"&gt;# 0 even on a cold write; do not alert on this
&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral_5m_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# the actual write count (1875)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider reported the write correctly (&lt;code&gt;cache_creation_input_tokens: 1875&lt;/code&gt; in the raw response, visible via &lt;code&gt;r.response_metadata["usage"]&lt;/code&gt;); the standardized mapping just files it under the TTL-bucket key. A cost dashboard watching &lt;code&gt;cache_creation&lt;/code&gt; will tell you caching is free while the write premium quietly accrues. Trust the raw usage object or know the bucket keys. This is the same class of problem as gateways mis-reporting cache fields, which we audit in &lt;a href="https://synthorai.io/blog/llm-gateway-cache-audit/" rel="noopener noreferrer"&gt;Does Your LLM Gateway Lie About Cache?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implicit caches: mis-ordering fails silently, so watch it hardest here
&lt;/h2&gt;

&lt;p&gt;Claude's cache is explicit. GPT and most open-weight providers cache automatically on prefix match, no markers, and through LangChain the same chain works after one constructor change:&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="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatOpenAI&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glm-5.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://synthorai.io/v1&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;p&gt;Plain string system prompt, no markers: GLM 5.2's second call read 1,088 tokens of the roughly 1,850-token prefix. (Not all of it: automatic caches match in coarse block increments rather than byte-for-byte to the end; OpenAI, for instance, documents 128-token granularity.) So far, free money. But the mis-ordering hazard from the RAG table above applies with full force here, and with a nastier failure mode. We reran the same order experiment on the automatic path, new retrieved context on every call:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Order (no markers, automatic cache)&lt;/th&gt;
&lt;th&gt;Call 1&lt;/th&gt;
&lt;th&gt;Call 2 (new query, new context)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context first, then rules&lt;/td&gt;
&lt;td&gt;read 0&lt;/td&gt;
&lt;td&gt;read 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rules first, context in the human turn&lt;/td&gt;
&lt;td&gt;read 0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;read 1,088&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The wrong order is an unconditional zero: the changing context sits at the front, no two calls share a prefix, and the discount never arrives. On the explicit path the same mistake at least shows up in the bill as a cache-write premium on every call; on the implicit path there is no premium, no error, and no signal. The prompt just quietly never qualifies, while you assume "automatic" means "working." And since there is no marker to place, &lt;strong&gt;prompt order is the only knob the implicit path gives you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So verify from the meters, in production rather than once in a test: &lt;code&gt;input_token_details.cache_read&lt;/code&gt; (LangChain) or &lt;code&gt;prompt_tokens_details.cached_tokens&lt;/code&gt; (raw). &lt;a href="https://developers.openai.com/api/docs/guides/prompt-caching" rel="noopener noreferrer"&gt;OpenAI's automatic caching&lt;/a&gt; additionally documents a 1,024-token minimum prefix, and per-provider TTL and eligibility differ, which is &lt;a href="https://synthorai.io/blog/provider-caching-comparison/" rel="noopener noreferrer"&gt;part 2's&lt;/a&gt; territory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On Claude, a &lt;code&gt;("system", "...")&lt;/code&gt; string tuple has nowhere to carry &lt;code&gt;cache_control&lt;/code&gt;: nothing gets cached, nothing warns you. Cacheable system prompts go in a &lt;code&gt;SystemMessage&lt;/code&gt; with content blocks and the marker.&lt;/li&gt;
&lt;li&gt;The cache key is the byte-exact prefix: static content first, variables after the marker or in the human turn. RAG context before the rules does not just miss; it pays the write premium every call.&lt;/li&gt;
&lt;li&gt;A variable inside the cached block creates one cache entry per value: repeating values amortize, per-call-unique values (timestamps, request IDs) never hit.&lt;/li&gt;
&lt;li&gt;Tools sit before the system prompt in the prefix, so the system marker caches bound tools too (&lt;code&gt;bind_tools&lt;/code&gt; serializes deterministically). If tools are your biggest stable block, the marker can go on an Anthropic-format tool dict instead.&lt;/li&gt;
&lt;li&gt;In conversations, a marker fixed on the system block leaves the growing history at full price; put it on the latest message so each turn reads the prior prefix and writes only the delta.&lt;/li&gt;
&lt;li&gt;Do not monitor &lt;code&gt;input_token_details.cache_creation&lt;/code&gt;: it stays 0 even on writes, so a dashboard concludes caching is free while write premiums accrue. The real count is in &lt;code&gt;ephemeral_5m_input_tokens&lt;/code&gt;, or read the raw &lt;code&gt;response_metadata["usage"]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On automatic-cache models (GPT, GLM, DeepSeek), prompt order is the only knob and mis-ordering fails silently: no premium, no error, just a discount that never arrives. Verify hits from the usage fields.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;set_llm_cache&lt;/code&gt; stores whole responses keyed on the exact prompt and model config; it only pays off when identical requests repeat, never on an agent loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The habits are small: a content block instead of a string, static before variable, a marker that slides with the conversation, one usage field read correctly. The measured difference was a 90% discount on every stable token versus nothing, and in the mis-ordered RAG case, versus paying extra. LangChain does not get in the way of prompt caching; it just makes the wrong prompt shape as easy to write as the right one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;Measured on 2026-07-04 against &lt;code&gt;https://synthorai.io/&lt;/code&gt; with &lt;code&gt;langchain-core&lt;/code&gt; 1.4.8, &lt;code&gt;langchain-anthropic&lt;/code&gt; 1.4.8, &lt;code&gt;langchain-openai&lt;/code&gt; 1.3.3, models &lt;code&gt;claude-sonnet-5&lt;/code&gt; and &lt;code&gt;glm-5.2&lt;/code&gt;, a roughly 1,800-token English system prefix, small samples, and a 1–2 second gap between consecutive calls so cache writes have time to land. Each experiment used a fresh randomized prefix to guarantee a cold cache, which is why the baseline token counts differ slightly between tables (1,852 to 1,875). Library field mappings and provider cache behavior change between versions; re-measure against your own stack before depending on the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Anthropic: Prompt caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/prompt-caching" rel="noopener noreferrer"&gt;OpenAI: Prompt caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.langchain.com/oss/python/integrations/chat/anthropic" rel="noopener noreferrer"&gt;LangChain: ChatAnthropic integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://reference.langchain.com/python/langchain-core/caches/InMemoryCache" rel="noopener noreferrer"&gt;LangChain: &lt;code&gt;InMemoryCache&lt;/code&gt; (response caching)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>caching</category>
      <category>langchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Claude Sonnet 5's New Tokenizer: 41% More Tokens per Prompt</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Thu, 02 Jul 2026 09:25:46 +0000</pubDate>
      <link>https://dev.to/synthorai/claude-sonnet-5s-new-tokenizer-41-more-tokens-per-prompt-3lm6</link>
      <guid>https://dev.to/synthorai/claude-sonnet-5s-new-tokenizer-41-more-tokens-per-prompt-3lm6</guid>
      <description>&lt;p&gt;&lt;code&gt;claude-sonnet-5&lt;/code&gt; is live on the Synthorai gateway, and right now it is cheap: &lt;strong&gt;$2 / $10 per million input / output tokens&lt;/strong&gt;, which is 2.5× under Opus 4.8 and below Sonnet 4.6. Enjoy it while it lasts. That is &lt;a href="https://www.anthropic.com/news/claude-sonnet-5" rel="noopener noreferrer"&gt;introductory pricing through August 31, 2026&lt;/a&gt;; on September 1 the rate returns to &lt;strong&gt;$3 / $15&lt;/strong&gt;, the same sticker as Sonnet 4.6.&lt;/p&gt;

&lt;p&gt;If you cache against the Claude line, the caching and TTL contract is a drop-in carry-over. Cost is where you have to look twice, and the reason is how Sonnet 5 counts tokens. It ships with a new tokenizer that turns the same English text into about &lt;strong&gt;41% more input tokens&lt;/strong&gt; than Sonnet 4.6, and token count is what you pay on and are limited by. The sticker price is only half the bill.&lt;/p&gt;

&lt;p&gt;Here is what that token change touches, before any code change or quality question enters the picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost per prompt.&lt;/strong&gt; At the standard rate, the same English prompt costs about 41% more than on Sonnet 4.6, since identical text is billed as more tokens at the same per-token price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every token-based estimate.&lt;/strong&gt; A per-call budget, or a local-tokenizer count, sized against 4.6 runs about 40% low on Sonnet 5. Meter the live &lt;code&gt;usage&lt;/code&gt;, not a local guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context-window headroom.&lt;/strong&gt; The same document eats about 41% more of the window, so long-context and RAG calls fit less real text per request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits.&lt;/strong&gt; A tokens-per-minute cap drains about 41% faster for the same workload, trimming throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache eligibility (a small upside).&lt;/strong&gt; The 1,024-token minimum is easier to clear, so a prefix that sat just under it on 4.6 may become cacheable on Sonnet 5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of the post puts measured numbers on each: price, the caching economics, and the token-count shift.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prices, caching, TTL, and token counts measured against &lt;code&gt;https://synthorai.io/&lt;/code&gt; (Anthropic-native &lt;code&gt;/v1/messages&lt;/code&gt;) on 2026-07-01. Per-token prices are derived from the &lt;code&gt;usage&lt;/code&gt; cost on live calls; the intro/standard rates and the August 31 expiry are from Anthropic's announcement. Reproduce against your own prompt before quoting.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;



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

&lt;span class="n"&gt;anth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SYNTHORAI_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://synthorai.io/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# SDK appends /v1/messages
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# the only line that changes
&lt;/span&gt;    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&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;type&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;text&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;ephemeral&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;messages&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}],&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="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# cache_creation_input_tokens, cache_read_input_tokens, cost
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap the &lt;code&gt;model&lt;/code&gt; field and nothing in your caching path moves. The mechanics behind &lt;code&gt;cache_control&lt;/code&gt; are in &lt;a href="https://synthorai.io/blog/prompt-caching-tutorial-code-examples/" rel="noopener noreferrer"&gt;the caching tutorial&lt;/a&gt;; the architecture of why the cache exists is in &lt;a href="https://synthorai.io/blog/llm-prompt-caching-explained/" rel="noopener noreferrer"&gt;Part 1 of the series&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Price: cheap now, back to Sonnet 4.6's rate in September
&lt;/h2&gt;

&lt;p&gt;Per-token pricing on the gateway, derived from the &lt;code&gt;usage&lt;/code&gt; cost on plain (uncached) calls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input ($/M)&lt;/th&gt;
&lt;th&gt;Output ($/M)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude-sonnet-5&lt;/code&gt; (intro, through Aug 31)&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude-sonnet-5&lt;/code&gt; (standard, from Sep 1)&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;$25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The intro rate is a real discount, and against &lt;strong&gt;Opus 4.8&lt;/strong&gt; it is the durable part of the story: even at the standard $3 / $15, Sonnet 5 stays cheaper than Opus, and the two share a tokenizer (more on that below), so the comparison is clean at both prices.&lt;/p&gt;

&lt;p&gt;Against &lt;strong&gt;Sonnet 4.6&lt;/strong&gt; the discount is temporary. On September 1 the sticker price is identical, so any "Sonnet 5 is cheaper than 4.6" plan built on today's number expires with the promo. And as the next section shows, at equal sticker price Sonnet 5 is actually the pricier of the two for the same text.&lt;/p&gt;

&lt;p&gt;We don't publish capability benchmarks we haven't run; whether Sonnet 5's quality justifies its cost over 4.6 is your eval, not ours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching and TTL: a drop-in
&lt;/h2&gt;

&lt;p&gt;The caching contract is identical to the rest of the Claude line. We ran a cold write / warm read sequence with a stable 2.2K-token prefix, varying the user message each call so no response-level cache could contaminate the result. Cost per warm turn, &lt;strong&gt;at the current intro price&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Cold turn (cache write)&lt;/th&gt;
&lt;th&gt;Warm turn (cache read)&lt;/th&gt;
&lt;th&gt;Cold → warm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude-sonnet-5&lt;/code&gt; (intro)&lt;/td&gt;
&lt;td&gt;$0.0069&lt;/td&gt;
&lt;td&gt;$0.0017&lt;/td&gt;
&lt;td&gt;4.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.0079&lt;/td&gt;
&lt;td&gt;$0.0024&lt;/td&gt;
&lt;td&gt;3.3×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.0172&lt;/td&gt;
&lt;td&gt;$0.0043&lt;/td&gt;
&lt;td&gt;4.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The invariants hold as they do across the Opus line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read discount ≈ 90%.&lt;/strong&gt; A warm cache read costs about 10% of the input price, matching Anthropic's documented "up to 90%" cached-read savings. Break-even is one hit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1-hour TTL works the same.&lt;/strong&gt; &lt;code&gt;cache_control: {"type": "ephemeral", "ttl": "1h"}&lt;/code&gt; is accepted on Sonnet 5, and the &lt;code&gt;usage&lt;/code&gt; object splits the buckets as before: &lt;code&gt;cache_creation.ephemeral_5m_input_tokens&lt;/code&gt; vs &lt;code&gt;ephemeral_1h_input_tokens&lt;/code&gt;. The 1-hour write premium is about 2× no-cache (vs about 1.25× for the 5-minute write); reads stay ≈10% regardless of TTL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat on the table: those warm-turn dollars are at the intro rate. From September 1, multiply the Sonnet 5 figures by 1.5× ($2 → $3 input, $10 → $15 output). A warm Sonnet 5 turn that costs $0.0017 today is about $0.0026 in September, still under Opus 4.8's $0.0043, but no longer under Sonnet 4.6.&lt;/p&gt;




&lt;h2&gt;
  
  
  The token-count catch
&lt;/h2&gt;

&lt;p&gt;Here is what makes the September reset bite twice. The &lt;strong&gt;same system text reports about 41% more input tokens on Sonnet 5 than on Sonnet 4.6.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input tokens (identical text)&lt;/th&gt;
&lt;th&gt;Input cost at standard price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,594&lt;/td&gt;
&lt;td&gt;$0.0048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,245&lt;/td&gt;
&lt;td&gt;$0.0067&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,245&lt;/td&gt;
&lt;td&gt;$0.0112&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sonnet 5 tokenizes the same English prompt as 2,245 tokens, the identical count Opus 4.8 reports, and well above Sonnet 4.6's 1,594. Sonnet 5 shipped with the newer tokenizer the Opus line adopted at 4.7.&lt;/p&gt;

&lt;p&gt;Put the price and the token count together and the picture is clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;During the intro period&lt;/strong&gt;, the 41% token bump is offset by the 33% lower rate ($2 vs $3), so the same uncached prompt costs about what it did on 4.6, and warm turns run cheaper thanks to the discounted output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From September 1&lt;/strong&gt;, the rate matches 4.6 but the token count does not. The same English prompt costs &lt;strong&gt;about 41% more on Sonnet 5 than on Sonnet 4.6&lt;/strong&gt; ($0.0067 vs $0.0048 for this prefix), because identical text is simply counted as more tokens at the same per-token price.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Against Opus 4.8 there is no such catch: the tokenizer is the same (2,245 = 2,245), so Sonnet 5 is cleanly cheaper at both the intro rate (2.5×) and the standard rate (1.67×).&lt;/p&gt;

&lt;p&gt;So budget the September bill, not the July one: the per-token rate rises 1.5× on September 1, and the higher token count is already baked in today. And read &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; / &lt;code&gt;cache_read_input_tokens&lt;/code&gt; from the live response rather than a local tokenizer that may still be on the old vocabulary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sonnet 5 vs Opus 4.8: the durable win
&lt;/h2&gt;

&lt;p&gt;This is the comparison the launch changes for keeps. Sonnet 5 and Opus 4.8 share a tokenizer, so on any prompt the token counts are identical and the cost difference is purely the rate: &lt;strong&gt;2.5× cheaper at the intro price, 1.67× cheaper at the standard price&lt;/strong&gt;, on cold turns, warm turns, input, and output alike. A warm cached turn is $0.0017 vs $0.0043 today; even in September it is roughly $0.0026 vs $0.0043.&lt;/p&gt;

&lt;p&gt;For a high-volume caching agent loop where the prefix repeats every turn, that gap compounds. The decision is the usual one: run your own eval, and if Sonnet 5 clears your quality bar, the gateway math favors it durably, not just until August. If it doesn't, Opus 4.8 is one &lt;code&gt;model&lt;/code&gt; field away with the same caching code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Caching code carries over verbatim.&lt;/strong&gt; &lt;code&gt;cache_control&lt;/code&gt; markers, breakpoint count, &lt;code&gt;ttl: "1h"&lt;/code&gt;, &lt;code&gt;usage&lt;/code&gt; field names are all identical to the Opus line.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;TTL choices carry over.&lt;/strong&gt; 5m for live/session workloads, 1h for bursty or agent-with-pauses work.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Discount economics carry over.&lt;/strong&gt; ≈90% read, ≈1.25× write (5m), ≈2× write (1h).&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Mark September 1 on the budget.&lt;/strong&gt; The intro rate ends Aug 31; Sonnet 5 goes to $3 / $15. Model the 1.5× step-up before it lands.&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Re-measure token counts (from 4.6 or earlier).&lt;/strong&gt; Same text, about 41% more tokens on Sonnet 5. At standard pricing that makes the same prompt pricier than 4.6, not cheaper.&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Trust the live &lt;code&gt;usage&lt;/code&gt; object.&lt;/strong&gt; Read &lt;code&gt;*_input_tokens&lt;/code&gt; and &lt;code&gt;cost&lt;/code&gt; from the response, not a cached estimate from the old generation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Sonnet 5 is a strong deal on a clock. Against Opus 4.8 it is a durable 1.67–2.5× cheaper with a drop-in caching path, which makes it the obvious first thing to eval for any Opus workload that isn't quality-critical. Against Sonnet 4.6 the win is only the introductory discount: on September 1 the price matches 4.6, and the new tokenizer means the same prompt actually costs more. Take the discount, but size your budget on the September numbers and confirm your token counts against the live &lt;code&gt;usage&lt;/code&gt; object before you promise finance anything.&lt;/p&gt;

&lt;p&gt;For the full caching playbook, see the four-part series starting with &lt;a href="https://synthorai.io/blog/llm-prompt-caching-explained/" rel="noopener noreferrer"&gt;How KV Cache &amp;amp; TTL Work&lt;/a&gt; and the &lt;a href="https://synthorai.io/blog/prompt-caching-tutorial-code-examples/" rel="noopener noreferrer"&gt;working Python tutorial&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Sonnet 5 cheaper than Sonnet 4.6?&lt;/strong&gt;&lt;br&gt;
Only during the introductory period. Through August 31, 2026 it is $2 / $10 vs 4.6's $3 / $15. From September 1 it is $3 / $15, the same rate. And because the same text counts as about 41% more tokens on Sonnet 5, at the standard price the same prompt costs more than on 4.6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When does the intro price end?&lt;/strong&gt;&lt;br&gt;
August 31, 2026, per &lt;a href="https://www.anthropic.com/news/claude-sonnet-5" rel="noopener noreferrer"&gt;Anthropic's announcement&lt;/a&gt;. On September 1 the rate becomes $3 per million input and $15 per million output tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much cheaper is Sonnet 5 than Opus 4.8?&lt;/strong&gt;&lt;br&gt;
2.5× at the intro rate, 1.67× at the standard rate, on both input and output. They share a tokenizer, so token counts match and the difference is purely the rate, at both prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to change my &lt;code&gt;cache_control&lt;/code&gt; code?&lt;/strong&gt;&lt;br&gt;
No. Marker syntax, breakpoint limit, and TTL options are identical to the Opus line. Change the &lt;code&gt;model&lt;/code&gt; field and nothing else. Warm reads are ≈10% of the input price; the 1-hour write is ≈2× no-cache, the 5-minute write ≈1.25×.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Sonnet 5 a drop-in replacement for Opus 4.8?&lt;/strong&gt;&lt;br&gt;
On the caching, TTL, and cost surface, migration is trivial and it is cheaper at both prices. On quality, run your own eval; we don't publish capability benchmarks we haven't run. For model-quality claims, see Anthropic's model card.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verification: price, caching, TTL, and token-count figures measured against &lt;code&gt;https://synthorai.io/&lt;/code&gt; on 2026-07-01 using the Anthropic-native &lt;code&gt;/v1/messages&lt;/code&gt; path, single tenant. Per-token prices are derived from &lt;code&gt;usage&lt;/code&gt; cost on plain calls; cost-per-turn is a small-sample median with a 2.2K-token cached prefix and reflects the current intro rate. Intro pricing and the August 31, 2026 expiry are from &lt;a href="https://www.anthropic.com/news/claude-sonnet-5" rel="noopener noreferrer"&gt;Anthropic's Sonnet 5 announcement&lt;/a&gt;; discount/premium ratios cross-checked against &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Anthropic Prompt Caching docs&lt;/a&gt;. Your numbers will vary with prompt, region, and load.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>caching</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>GLM 5.2 Tool Calls in Agent Loops: What 'OpenAI-Compatible' Hides</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Tue, 30 Jun 2026 12:06:27 +0000</pubDate>
      <link>https://dev.to/synthorai/glm-52-tool-calls-in-agent-loops-what-openai-compatible-hides-4m3o</link>
      <guid>https://dev.to/synthorai/glm-52-tool-calls-in-agent-loops-what-openai-compatible-hides-4m3o</guid>
      <description>&lt;p&gt;Point an existing OpenAI-style agent loop at GLM 5.2 and most of it just works: you send &lt;code&gt;tools&lt;/code&gt;, you get back &lt;code&gt;tool_calls&lt;/code&gt;, you run them, you send the results. Then it does something the SDK examples never show. The assistant returns a line of text in the &lt;em&gt;same&lt;/em&gt; turn as the tool calls:&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;"choices"&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;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I'll look up both pieces of information for you at the same time!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"function"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get_weather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&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="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;city&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Paris&lt;/span&gt;&lt;span class="se"&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"function"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&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="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;city&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Tokyo&lt;/span&gt;&lt;span class="se"&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="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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two conventions dominate, and it helps to keep both in view. In OpenAI's, you send function schemas, get &lt;code&gt;tool_calls&lt;/code&gt; back, and answer with a &lt;code&gt;tool&lt;/code&gt; message per call, keyed by &lt;code&gt;tool_call_id&lt;/code&gt;:&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="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&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="c1"&gt;# assistant.tool_calls → [{"id": "call_…", "function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}}]
&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;tool&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;tool_call_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;call_…&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;content&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;18C, clear&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;p&gt;Anthropic's is shaped differently: tools carry an &lt;code&gt;input_schema&lt;/code&gt;, the model emits &lt;code&gt;tool_use&lt;/code&gt; blocks, and you answer with a &lt;code&gt;tool_result&lt;/code&gt; block:&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="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&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="c1"&gt;# resp.content → [{"type": "tool_use", "id": "toolu_…", "name": "get_weather", "input": {"city": "Paris"}}]
&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;assistant&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;tool_result&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;tool_use_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;toolu_…&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;content&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;18C, clear&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;p&gt;GLM 5.2 speaks the OpenAI dialect.&lt;/p&gt;

&lt;p&gt;In OpenAI's contract, &lt;code&gt;message.content&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; when &lt;code&gt;finish_reason&lt;/code&gt; is &lt;code&gt;tool_calls&lt;/code&gt;. Plenty of agent loops lean on that: they branch on "content or tool calls," log &lt;code&gt;content&lt;/code&gt; as the final answer, or assert it's empty. GLM hands you both at once, and that assumption is the first thing to break.&lt;/p&gt;

&lt;p&gt;The behavior here was captured from live tool-calling requests to &lt;code&gt;glm-5.2&lt;/code&gt;, with &lt;code&gt;gpt-5.5&lt;/code&gt; and &lt;code&gt;claude-opus-4-8&lt;/code&gt; run on the same task as reference points. The short version: GLM 5.2 uses the OpenAI API surface, but on a couple of axes it behaves more like Claude than like GPT, and an OpenAI-trained loop is the one that trips.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same turn, three ways
&lt;/h2&gt;

&lt;p&gt;Same prompt, same two tools, three models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GLM (&lt;code&gt;glm-5.2&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;OpenAI (&lt;code&gt;gpt-5.5&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Anthropic (&lt;code&gt;claude-opus-4-8&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API surface&lt;/td&gt;
&lt;td&gt;OpenAI chat-completions&lt;/td&gt;
&lt;td&gt;OpenAI chat-completions&lt;/td&gt;
&lt;td&gt;Anthropic messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text in the tool-call turn&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;content&lt;/code&gt; preamble (non-null)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;content&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;text&lt;/code&gt; block before &lt;code&gt;tool_use&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning on that turn&lt;/td&gt;
&lt;td&gt;exposed: &lt;code&gt;reasoning_content&lt;/code&gt; + &lt;code&gt;reasoning_tokens&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;hidden; only &lt;code&gt;reasoning_tokens&lt;/code&gt; in &lt;code&gt;usage&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;only as a &lt;code&gt;thinking&lt;/code&gt; block, if you enable it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel tool calls&lt;/td&gt;
&lt;td&gt;yes, with &lt;code&gt;index&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes, multiple &lt;code&gt;tool_use&lt;/code&gt; blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Done signal&lt;/td&gt;
&lt;td&gt;&lt;code&gt;finish_reason: "tool_calls"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;finish_reason: "tool_calls"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stop_reason: "tool_use"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-call id prefix&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call_…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call_…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;toolu_…&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rows are where loops break: text in the tool-call turn, and reasoning showing up on that turn. The rest is reassuringly boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text rides with the tool call
&lt;/h2&gt;

&lt;p&gt;GLM 5.2 routinely emits a short assistant &lt;code&gt;content&lt;/code&gt; preamble alongside &lt;code&gt;tool_calls&lt;/code&gt;, with &lt;code&gt;finish_reason: "tool_calls"&lt;/code&gt;. It is not an error and it is not occasional.&lt;/p&gt;

&lt;p&gt;Here is the same turn from all three, trimmed to the part that differs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// OpenAI gpt-5.5: content is null on a tool-call turn&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"message"&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;"content"&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;"tool_calls"&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="p"&gt;{&lt;/span&gt;&lt;span class="c"&gt;/* get_weather */&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="c"&gt;/* get_time */&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// GLM glm-5.2: content carries a preamble&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"message"&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;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I'll look up both pieces of information for you at the same time!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&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="p"&gt;{&lt;/span&gt;&lt;span class="c"&gt;/* get_weather */&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="c"&gt;/* get_time */&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// Anthropic claude-opus-4-8: a text block sits before the tool_use blocks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"content"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I'll get both pieces of information for you."&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;/* get_weather */&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;/* get_time */&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="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;OpenAI leaves &lt;code&gt;content&lt;/code&gt; null; GLM fills it; Anthropic has always put a &lt;code&gt;text&lt;/code&gt; block there. So GLM takes OpenAI's wire format with Anthropic's habit of narrating before it acts, and a loop written against OpenAI is the one caught off guard. The fix is small but you have to make it deliberately. Stop treating a tool-call turn as content-free:&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glm-5.2&lt;/span&gt;&lt;span class="sh"&gt;"&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;msgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;

&lt;span class="c1"&gt;# GLM may return assistant text in the same turn as the tool calls.
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preamble: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# keep or drop, but don't assume it's empty
&lt;/span&gt;
&lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&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;call&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;tool&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;tool_call_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your loop renders &lt;code&gt;content&lt;/code&gt; to the user as the assistant's reply, you will now show a "let me check that" line before every tool call. Decide whether you want it. The point is that the decision is yours, not something the model's silence makes for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  It thinks out loud
&lt;/h2&gt;

&lt;p&gt;GLM 5.2 is a reasoning model, and that does not pause for tool use. A tool-call turn carries reasoning along with it, and GLM 5.2 exposes it as text. In a non-streaming response the token accounting makes it explicit:&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="nl"&gt;"usage"&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;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens_details"&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;"reasoning_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&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;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;292&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;Almost half the completion was reasoning, on a request whose visible output is two short function calls. This is the row where all three models diverge. GLM 5.2 gives you the reasoning as &lt;code&gt;reasoning_content&lt;/code&gt; plus a token count. OpenAI bills &lt;code&gt;reasoning_tokens&lt;/code&gt; in &lt;code&gt;usage&lt;/code&gt; but never shows the text. Anthropic shows it only as &lt;code&gt;thinking&lt;/code&gt; blocks, and only when you turn extended thinking on. GLM 5.2 is the most exposed of the three by default.&lt;/p&gt;

&lt;p&gt;Two consequences. First, cost: you pay for those reasoning tokens on tool-call turns, and an agent loop is many turns. Reasoning effort is the dial that moves the number, which we covered in &lt;a href="https://dev.to/blog/glm-52-coding-cost/"&gt;GLM 5.2: Reasoning Effort Is the Cost Lever&lt;/a&gt;. Count reasoning tokens on every turn, not just the final answer.&lt;/p&gt;

&lt;p&gt;Second, streaming order. When you stream the request, GLM sends the reasoning first, then the preamble text, then the tool calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reasoning_content  (many deltas)
content            (a few deltas)
tool_calls         (id + name, then arguments)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A parser written against vanilla OpenAI chat completions does not know the &lt;code&gt;reasoning_content&lt;/code&gt; field and will quietly ignore that opening burst. Usually fine. It stops being fine if your UI shows a "thinking…" state keyed on the first content delta, because the first thing on the wire is reasoning, not content, and the indicator never flips.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a GLM 5.2 tool-call turn costs
&lt;/h2&gt;

&lt;p&gt;Behavior is half the story; the bill is the other half, and an agent loop runs the same turn many times over. With a fixed prefix (a roughly 2,000-token system prompt plus the tool definitions) and the user message varied each call, measured over ten warm turns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;per warm tool-call turn&lt;/th&gt;
&lt;th&gt;GLM &lt;code&gt;glm-5.2&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;OpenAI &lt;code&gt;gpt-5.5&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Anthropic &lt;code&gt;claude-opus-4-8&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$0.0009&lt;/td&gt;
&lt;td&gt;$0.0042&lt;/td&gt;
&lt;td&gt;$0.0051&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency (median)&lt;/td&gt;
&lt;td&gt;6.6s&lt;/td&gt;
&lt;td&gt;1.9s&lt;/td&gt;
&lt;td&gt;3.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt cached&lt;/td&gt;
&lt;td&gt;≈96%&lt;/td&gt;
&lt;td&gt;≈81%&lt;/td&gt;
&lt;td&gt;≈97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning tokens&lt;/td&gt;
&lt;td&gt;≈27&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold → warm cost&lt;/td&gt;
&lt;td&gt;3.4×&lt;/td&gt;
&lt;td&gt;2.8×&lt;/td&gt;
&lt;td&gt;4.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GLM 5.2 is the cheap one: roughly 4.5× cheaper than GPT-5.5 and 5.4× cheaper than Opus per warm turn. It is also the slow one, two to three and a half times their latency, because it spends reasoning tokens on every turn while the other two spent none on this task. That is the trade: GLM buys cost with latency, and reasoning effort is the dial that moves it.&lt;/p&gt;

&lt;p&gt;Caching is what makes any of these affordable in a loop. The system prompt and tool definitions are most of every prompt and identical each turn, so once the prefix is cached the turn gets 2.8× to 4.9× cheaper. Two things decide whether you see it. GLM and OpenAI cache the prefix automatically; Anthropic only caches what you mark with &lt;code&gt;cache_control&lt;/code&gt;. And GLM's cache warms up a beat late, so a three-step task can pay full price while a thirty-step one runs cached. The mechanics are in &lt;a href="https://dev.to/blog/prompt-caching-open-weight-llms/"&gt;Open-Weight LLM Caching&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for GLM 5.2, and how to run it well
&lt;/h2&gt;

&lt;p&gt;Put the pieces together. GLM 5.2 is the cheap model in that table and the slow one, and it reasons on every turn. That profile points at where it earns its place.&lt;/p&gt;

&lt;p&gt;Where it fits: long, multi-step agent loops where cost dominates and a few seconds per turn is acceptable. Background coding agents, CI and batch automation, jobs that run unattended. The reasoning that makes it slow is also why it holds up on real coding and planning rather than trivial routing. Caching compounds the case past the warm-up: a thirty-step task amortizes the prefix and runs cheap, while a three-step one can pay full price and eat the latency for nothing. So reach for GLM 5.2 on the long jobs, and keep a faster model for the interactive, single-shot calls where six seconds a turn is felt.&lt;/p&gt;

&lt;p&gt;How to run GLM 5.2 well. Five habits make a loop GLM-ready without leaving the OpenAI API surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat a tool-call turn as possibly carrying &lt;code&gt;content&lt;/code&gt;. Do not assert it is empty.&lt;/li&gt;
&lt;li&gt;Expect &lt;code&gt;reasoning_content&lt;/code&gt; on the wire and &lt;code&gt;reasoning_tokens&lt;/code&gt; in &lt;code&gt;usage&lt;/code&gt;; budget for both, and use the reasoning-effort dial to trade quality for cost.&lt;/li&gt;
&lt;li&gt;In streaming, do not key UI state on the first content delta, since reasoning arrives first.&lt;/li&gt;
&lt;li&gt;Echo &lt;code&gt;tool_call_id&lt;/code&gt; verbatim; treat it as opaque, never parse or regenerate it.&lt;/li&gt;
&lt;li&gt;Accumulate streaming &lt;code&gt;arguments&lt;/code&gt; by &lt;code&gt;index&lt;/code&gt; until the call closes; do not assume a chunk count.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things you do not have to defend against: GLM emits parallel tool calls with an &lt;code&gt;index&lt;/code&gt; like the others, and the round-trip closes normally. Append the assistant turn, append one &lt;code&gt;tool&lt;/code&gt; message per call with its result, and it finishes with &lt;code&gt;finish_reason: "stop"&lt;/code&gt;. Keep the cacheable prefix byte-stable across turns while you are at it; the system prompt and tool definitions are most of every prompt, and a stable prefix is what lets GLM's cache carry the cost once it warms.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It is the gap between "the request succeeds" and "the agent loop is correct," and on GLM that gap is mostly two assumptions wide: that a tool-call turn is silent, and that it isn't thinking. Drop those two, keep the prefix stable, and one loop carries GLM, GPT, and Claude alike, with GLM doing it for a fraction of the cost wherever latency is not the thing you are optimizing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;The cost, latency, and cache figures above were measured on 2026-06-30 over ten warm tool-call turns per model, with &lt;code&gt;glm-5.2&lt;/code&gt;, &lt;code&gt;gpt-5.5&lt;/code&gt;, and &lt;code&gt;claude-opus-4-8&lt;/code&gt;. Cost is taken from reported usage; latency is wall-clock median and shifts with load and reasoning effort. Model behavior and prices drift, so treat the figures as indicative and re-measure against your own traffic before depending on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.z.ai/" rel="noopener noreferrer"&gt;z.ai (Zhipu) API and pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/function-calling" rel="noopener noreferrer"&gt;OpenAI function calling guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use" rel="noopener noreferrer"&gt;Anthropic tool use (Messages API)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>What a Simple Transcription Test Can and Can't Tell You</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Fri, 26 Jun 2026 10:52:00 +0000</pubDate>
      <link>https://dev.to/synthorai/what-a-simple-transcription-test-can-and-cant-tell-you-cdp</link>
      <guid>https://dev.to/synthorai/what-a-simple-transcription-test-can-and-cant-tell-you-cdp</guid>
      <description>&lt;p&gt;Synthorai now transcribes audio, with thirteen models behind one endpoint in two families.&lt;/p&gt;

&lt;p&gt;That one endpoint hides a lot of work, because natively these models barely resemble each other. &lt;code&gt;whisper-1&lt;/code&gt; takes a multipart file upload and returns &lt;code&gt;{text}&lt;/code&gt;. &lt;code&gt;gpt-4o-transcribe&lt;/code&gt; uses the same upload but adds token usage. Gemini is not a transcription API at all: you base64-encode the audio into a JSON &lt;code&gt;generateContent&lt;/code&gt; request and dig the transcript out of &lt;code&gt;candidates[0].content.parts[].text&lt;/code&gt;. ByteDance's &lt;code&gt;seed-asr&lt;/code&gt; speaks the BytePlus AUC protocol, and Google's &lt;code&gt;chirp&lt;/code&gt; models are Cloud Speech-to-Text recognizers reached with OAuth.&lt;/p&gt;

&lt;p&gt;Different endpoints, different auth, different response shapes, one more integration each. Through the gateway it is one OpenAI-compatible call: swap &lt;code&gt;gpt-4o-mini-transcribe&lt;/code&gt; for &lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; or &lt;code&gt;seed-asr-bigmodel&lt;/code&gt;, and nothing else in your code changes.&lt;/p&gt;

&lt;p&gt;The call is the OpenAI-compatible transcription endpoint, so it is a drop-in if you already use Whisper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://synthorai.io/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$SYNTHORAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@meeting.mp3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemini-2.5-flash-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://synthorai.io/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-syn-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meeting.mp3&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;rb&lt;/span&gt;&lt;span class="sh"&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&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;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transcriptions&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash-lite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&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="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transcript comes back in &lt;code&gt;text&lt;/code&gt;, and the billed cost is in the &lt;code&gt;x-total-cost-usd&lt;/code&gt; response header.&lt;/p&gt;

&lt;p&gt;We put all thirteen through the same simple test, and what that test is shapes every number below.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this test is, and isn't
&lt;/h2&gt;

&lt;p&gt;We generated everyday passages with no proper nouns (a morning, the weather, a trip to the market) with a standard text-to-speech voice in each of the world's five most-spoken languages, then transcribed each clip through all thirteen models. Each clip runs about 12 to 15 seconds, roughly 40 words of normal-paced speech with no long silences, encoded as 16 kHz mono 16-bit PCM WAV (256 kbps, about 2 MB a minute). The text is the ground truth and the durations are exact.&lt;/p&gt;

&lt;p&gt;This is a deliberately easy case: clean, scripted, single-speaker audio with no accents, noise, or jargon. That makes it good for the things that do not depend on how hard the audio is. It measures cost, latency, which languages a model accepts at all, and whether it can stream, and those are stable facts.&lt;/p&gt;

&lt;p&gt;It is not a quality benchmark. Real recordings with accents, background noise, domain vocabulary, overlapping speakers, and an hour of runtime separate these models in ways clean speech never will, and nothing here predicts that. Read the accuracy numbers as a floor check, not a ranking, and treat the cost, coverage, and streaming results as the baseline you can actually rely on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two model types, three request modes
&lt;/h2&gt;

&lt;p&gt;The thirteen models come in two kinds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native multimodal models&lt;/strong&gt; (six, Google's Gemini family: &lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt;, &lt;code&gt;gemini-3.1-flash-lite-preview&lt;/code&gt;, &lt;code&gt;gemini-2.5-flash&lt;/code&gt;, &lt;code&gt;gemini-3-flash-preview&lt;/code&gt;, &lt;code&gt;gemini-3.5-flash&lt;/code&gt;, &lt;code&gt;gemini-2.5-pro&lt;/code&gt;). General audio-and-text models that transcribe as a side effect of being multimodal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated ASR models&lt;/strong&gt; (seven: OpenAI's &lt;code&gt;whisper-1&lt;/code&gt;, &lt;code&gt;gpt-4o-transcribe&lt;/code&gt;, &lt;code&gt;gpt-4o-mini-transcribe&lt;/code&gt;; ByteDance's &lt;code&gt;seed-asr-bigmodel&lt;/code&gt;; Alibaba's &lt;code&gt;qwen3-asr-flash&lt;/code&gt;; Google's &lt;code&gt;chirp-2&lt;/code&gt; and &lt;code&gt;chirp-3&lt;/code&gt;). Purpose-built for speech.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And three ways to send the audio:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File in, batch out&lt;/strong&gt;: upload a complete recording, get the full transcript in one response. Every model supports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File in, streamed text out&lt;/strong&gt;: the same upload, but the transcript streams back over SSE as it is produced. Some models support this; others are batch-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio stream in, text stream out&lt;/strong&gt;: real-time recognition of a live mic or call. In development, not yet available, so everything below is the first two modes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How transcription is billed
&lt;/h2&gt;

&lt;p&gt;Two billing shapes. &lt;strong&gt;Per audio-minute&lt;/strong&gt; (&lt;code&gt;whisper-1&lt;/code&gt;, &lt;code&gt;seed-asr&lt;/code&gt;, &lt;code&gt;qwen3-asr-flash&lt;/code&gt;, the Chirp models): you pay for the wall-clock length of the recording, whatever is in it. &lt;strong&gt;Per token&lt;/strong&gt; (the &lt;code&gt;gpt-4o&lt;/code&gt; and Gemini models): audio tokenizes at a flat rate, and you pay for those input tokens plus the transcript output tokens, so silence is cheaper than dense speech.&lt;/p&gt;

&lt;p&gt;The per-token shape has a trap: the listed input rate is for text, but audio bills higher (&lt;code&gt;gpt-4o-mini-transcribe&lt;/code&gt; lists $1.25/M input but bills audio at $3/M). Estimate from the text rate and you undershoot. The gateway returns the real charge in an &lt;code&gt;x-total-cost-usd&lt;/code&gt; header, so read that rather than guessing from a price page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;This is the part the test pins down cleanly, and it varies the most. Cost per minute, from the billed header:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Cost / min&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Streams&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;multimodal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0006&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;≈4s&lt;/td&gt;
&lt;td&gt;chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-lite-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;multimodal&lt;/td&gt;
&lt;td&gt;$0.0016&lt;/td&gt;
&lt;td&gt;≈3s&lt;/td&gt;
&lt;td&gt;chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seed-asr-bigmodel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dedicated&lt;/td&gt;
&lt;td&gt;$0.0020&lt;/td&gt;
&lt;td&gt;≈10s&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-asr-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dedicated&lt;/td&gt;
&lt;td&gt;$0.0021&lt;/td&gt;
&lt;td&gt;≈3s&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;multimodal&lt;/td&gt;
&lt;td&gt;$0.0026&lt;/td&gt;
&lt;td&gt;≈2s&lt;/td&gt;
&lt;td&gt;chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-mini-transcribe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dedicated&lt;/td&gt;
&lt;td&gt;$0.0031&lt;/td&gt;
&lt;td&gt;≈3s&lt;/td&gt;
&lt;td&gt;token-by-token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3-flash-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;multimodal&lt;/td&gt;
&lt;td&gt;$0.0035&lt;/td&gt;
&lt;td&gt;≈4s&lt;/td&gt;
&lt;td&gt;chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;whisper-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dedicated&lt;/td&gt;
&lt;td&gt;$0.0060&lt;/td&gt;
&lt;td&gt;≈4s&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-transcribe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dedicated&lt;/td&gt;
&lt;td&gt;$0.0062&lt;/td&gt;
&lt;td&gt;≈2s&lt;/td&gt;
&lt;td&gt;token-by-token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-pro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;multimodal&lt;/td&gt;
&lt;td&gt;$0.0082&lt;/td&gt;
&lt;td&gt;≈5s&lt;/td&gt;
&lt;td&gt;chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chirp-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dedicated&lt;/td&gt;
&lt;td&gt;$0.0164&lt;/td&gt;
&lt;td&gt;≈3s&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chirp-3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dedicated&lt;/td&gt;
&lt;td&gt;$0.0164&lt;/td&gt;
&lt;td&gt;≈4s&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;gemini-3.5-flash&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;multimodal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0178&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;≈5s&lt;/td&gt;
&lt;td&gt;chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The spread is about 30x, from &lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; at $0.0006 a minute to &lt;code&gt;gemini-3.5-flash&lt;/code&gt; at $0.0178. Two things are worth noticing, both about price rather than quality. The single cheapest model is a Gemini flash-lite, three times cheaper than the cheapest dedicated ASR. And within the Gemini family the price had no relationship to accuracy on this test, so a bigger, pricier model is not automatically the safer choice; it is a reason to benchmark the cheap one on your own audio before paying for the large one.&lt;/p&gt;

&lt;p&gt;How these numbers move with your own files depends on the billing shape. The per-minute models (&lt;code&gt;whisper-1&lt;/code&gt;, &lt;code&gt;seed-asr&lt;/code&gt;, &lt;code&gt;qwen3-asr-flash&lt;/code&gt;, the Chirps) bill by duration alone, so the rate is portable: ten minutes of audio costs ten times the per-minute figure, whatever the format or content.&lt;/p&gt;

&lt;p&gt;The per-token models (the &lt;code&gt;gpt-4o&lt;/code&gt; and Gemini rows) scale their input cost with duration, not file size, because the provider resamples the audio before tokenizing. A heavy 320 kbps MP3 and our lean 16 kHz WAV of the same words tokenize to about the same cost, so compressing your files saves storage, not transcription spend. What does move a per-token bill is how much is actually spoken: our clips are normal-paced with no dead air, so audio that is denser or quieter than that bills a little more or less on the output tokens. The &lt;code&gt;x-total-cost-usd&lt;/code&gt; header is the ground truth in every case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accuracy and language coverage
&lt;/h2&gt;

&lt;p&gt;On English, Spanish, and French, every model that accepts the language scored about 0% error. That is the floor, and everyone clears it. Mandarin and Hindi are where even this easy test starts to show cracks, but read that as a hint about where to point your own testing, not a verdict:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Mandarin (CER)&lt;/th&gt;
&lt;th&gt;Hindi (WER)&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-lite-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seed-asr-bigmodel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;English + Chinese only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-asr-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-mini-transcribe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3-flash-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;whisper-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-transcribe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-pro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chirp-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chirp-3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;all five&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hard fact here is coverage, not accuracy. &lt;code&gt;seed-asr&lt;/code&gt; returns a useless transcript for Hindi, Spanish, and French: it is an English-and-Chinese model, so it is only an option if your audio is one of those two languages. Everything else handled all five.&lt;/p&gt;

&lt;p&gt;The Hindi spread and the Mandarin slips (&lt;code&gt;chirp-2&lt;/code&gt;, one Gemini) say those models are worth testing on your harder languages before you trust them, not that one is better than another. The absolute numbers are inflated by the synthetic voice and the scoring and move from run to run. The honest read is that on clean speech in major languages, accuracy is not where these models separate, so it is not where this test can tell you to choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming output
&lt;/h2&gt;

&lt;p&gt;Whether a model can stream its transcript is a capability, not a quality call, and it splits the lineup. The per-minute models (&lt;code&gt;whisper-1&lt;/code&gt;, &lt;code&gt;seed-asr&lt;/code&gt;, &lt;code&gt;qwen3-asr-flash&lt;/code&gt;, and both Chirps) are batch-only; the gateway returns a 400 if you ask them to stream. The &lt;code&gt;gpt-4o&lt;/code&gt; models stream token by token: &lt;code&gt;gpt-4o-transcribe&lt;/code&gt; returns its first words in about a second and fills in the rest, which is what a live-feel UI needs. The Gemini models technically stream, but in three to six large blocks, with the first arriving about when the whole transcript is done, so it buys almost nothing. Cost is unchanged from batch. To stream, add &lt;code&gt;stream=true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-N&lt;/span&gt; https://synthorai.io/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$SYNTHORAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@meeting.mp3 &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gpt-4o-transcribe &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="c"&gt;# data: {"type":"transcript.text.delta","delta":"When"}&lt;/span&gt;
&lt;span class="c"&gt;# data: {"type":"transcript.text.delta","delta":" you"} ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caching repeated audio
&lt;/h2&gt;

&lt;p&gt;Caching is where the two billing shapes split one more time. The per-minute models cannot cache: we sent the same clip to &lt;code&gt;whisper-1&lt;/code&gt; five times and paid an identical $0.015478 every time, because the bill is just duration. The token-billed Gemini models can. Send the same file repeatedly and Gemini's implicit cache reuses the audio tokens: on a 155-second clip sent five times, &lt;code&gt;gemini-2.5-flash&lt;/code&gt; dropped from $0.0054 to $0.0026 on two of the repeats, about 51% off, and &lt;code&gt;gemini-2.5-pro&lt;/code&gt; fell about 39%.&lt;/p&gt;

&lt;p&gt;Two caveats keep it from being a sure thing. It is best-effort, so some repeats hit the cache and some pay full price; and the audio has to clear Gemini's token floor, roughly a minute or more, which the short clips elsewhere in this test never do. The &lt;code&gt;gpt-4o&lt;/code&gt; models list no cache rate and showed only ordinary run-to-run variation. So if your workload re-transcribes the same files, caching is a real discount on the token-billed models and nothing on the per-minute ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check first, and what to test yourself
&lt;/h2&gt;

&lt;p&gt;This test cannot tell you which model is most accurate on your recordings. It can tell you what to filter on before you run your own evaluation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Languages.&lt;/strong&gt; Check that the model accepts every language you need. &lt;code&gt;seed-asr&lt;/code&gt; is English and Chinese only; the other twelve handled all five we tried. This is a hard gate, not a preference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming.&lt;/strong&gt; If you need a live transcript, only the &lt;code&gt;gpt-4o&lt;/code&gt; models stream token by token; the per-minute models are batch-only and Gemini's streaming is coarse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; The spread is about 30x. &lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; is the cheapest and still multilingual; the Chirps and the largest Gemini are the most expensive. A bigger model in the same family did not earn its premium on the easy clips, so do not assume you need it without checking. If you re-transcribe the same files often, the token-billed Gemini models can also cache the audio, as above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once a few models clear those, the question that is left, how accurate each one is on your own audio with its accents, noise, and vocabulary, is the one you have to answer yourself. No clean-speech benchmark substitutes for running the survivors on real recordings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;On clean, scripted speech in major languages, all thirteen models are about equally accurate, which is the most useful thing this test says: accuracy is not the axis to choose on. What it does pin down, and what genuinely varies, is the baseline: cost spans about 30x, one model covers only two languages, and several cannot stream. Use those to narrow the field, not to declare a winner, then run the two or three survivors on your own audio. That last step is the one no simple test can do for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/speech-to-text" rel="noopener noreferrer"&gt;OpenAI: Speech to text guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.com/api/pricing/" rel="noopener noreferrer"&gt;OpenAI: API pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/speech-to-text/v2/docs/chirp_2-model" rel="noopener noreferrer"&gt;Google Cloud: Chirp speech models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.byteplus.com/en/product/modelark" rel="noopener noreferrer"&gt;BytePlus: Seed-ASR (ByteDance) overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Costs and latencies measured on Synthorai on 2026-06-25 across thirteen models and five languages (English, Mandarin, Hindi, Spanish, French), via the &lt;code&gt;x-total-cost-usd&lt;/code&gt; header and SSE timing. The audio was text-to-speech generated and deliberately easy, so the accuracy figures are a floor check rather than a quality benchmark; real-world speech with accents and noise would separate these models differently. Latency varies run to run. Listing prices are this platform's rates as of that date. Verify current pricing before relying on it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>GLM 5.2: Reasoning Effort Is the Cost Lever</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Wed, 24 Jun 2026 15:19:00 +0000</pubDate>
      <link>https://dev.to/synthorai/glm-52-reasoning-effort-is-the-cost-lever-5ddm</link>
      <guid>https://dev.to/synthorai/glm-52-reasoning-effort-is-the-cost-lever-5ddm</guid>
      <description>&lt;p&gt;GLM 5.2 is now on Synthorai at about a sixth of frontier per-token prices, and the open-weight, frontier-benchmark headline is real. But the per-token price is the wrong number to anchor on. What a coding task actually costs on GLM 5.2 swings by more than an order of magnitude depending on a single knob, reasoning effort, and the default leaves that knob in the worst position. Set it well and GLM 5.2 is correct and cheaper than frontier on both easy and hard work. Leave it on the default and the same answer costs twenty times more and takes minutes. We measured it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What GLM 5.2 is
&lt;/h2&gt;

&lt;p&gt;GLM 5.2 is Zhipu's open-weight frontier model, released 2026-06-13: a mixture-of-experts network (~744B total, ~40B active), a usable 1M-token context, and an MIT license you can self-host. It targets coding and agentic work, with strong published benchmarks (SWE-bench Pro 62.1, Terminal-Bench 2.1 81.0, AIME 2026 99.2, GPQA Diamond 91.2). On Synthorai it's &lt;code&gt;glm-5.2&lt;/code&gt;, priced at $1.40 per million input tokens and $4.40 per million output.&lt;/p&gt;

&lt;p&gt;The detail that drives everything below: it is a reasoning model, and how much it reasons is something you set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it sits on price
&lt;/h2&gt;

&lt;p&gt;On per-token listing price, GLM 5.2 sits well below the Western frontier and among the cheaper Chinese models. Synthorai's rates for a representative set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input ($/M)&lt;/th&gt;
&lt;th&gt;Output ($/M)&lt;/th&gt;
&lt;th&gt;Cache read ($/M)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-pro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.44&lt;/td&gt;
&lt;td&gt;0.87&lt;/td&gt;
&lt;td&gt;0.0036&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;td&gt;3.01&lt;/td&gt;
&lt;td&gt;0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;glm-5.2&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.26&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.20&lt;/td&gt;
&lt;td&gt;6.00&lt;/td&gt;
&lt;td&gt;0.36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-pro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.00&lt;/td&gt;
&lt;td&gt;12.00&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-5.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.00&lt;/td&gt;
&lt;td&gt;30.00&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Its $4.40 output rate is about a seventh of &lt;code&gt;gpt-5.5&lt;/code&gt; and a sixth of &lt;code&gt;claude-opus-4-8&lt;/code&gt;, though &lt;code&gt;deepseek-v4-pro&lt;/code&gt; and &lt;code&gt;kimi-k2.5&lt;/code&gt; undercut it. So GLM 5.2 is frontier-class capability at roughly Chinese-model prices, not the absolute floor. There is no separate cache-write charge: a cache write bills at the input rate, and only the cache read is discounted to the rate above. The discount varies by vendor, with GLM 5.2's cache read about a fifth of its input rate and the frontier models (&lt;code&gt;gpt-5.5&lt;/code&gt;, &lt;code&gt;claude-opus-4-8&lt;/code&gt;, &lt;code&gt;gemini-3.1-pro&lt;/code&gt;) discounting reads to roughly a tenth.&lt;/p&gt;

&lt;p&gt;It is also a step up from its own predecessors. The previous GLM generation was extraordinarily cheap; the GLM 5 line raised prices, and GLM 5.2 lands at about 3x the input rate of GLM-4.6 (Zhipu's official rates):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GLM model&lt;/th&gt;
&lt;th&gt;Released&lt;/th&gt;
&lt;th&gt;Input ($/M)&lt;/th&gt;
&lt;th&gt;Output ($/M)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GLM-4.5&lt;/td&gt;
&lt;td&gt;2025-07&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;2.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-4.6&lt;/td&gt;
&lt;td&gt;2025-09&lt;/td&gt;
&lt;td&gt;0.43&lt;/td&gt;
&lt;td&gt;1.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;3.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GLM-5.2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2026-06&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.40&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That buys the 1M context and the frontier benchmarks. But the per-token rate is only the headline. What you actually pay per task is set by the reasoning effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reasoning-effort dial
&lt;/h2&gt;

&lt;p&gt;GLM 5.2's reasoning is a dial, not a switch. You can turn it off (&lt;code&gt;enable_thinking: false&lt;/code&gt;), set &lt;code&gt;reasoning_effort&lt;/code&gt; to low, medium, or high, or leave it on the default, which runs reasoning unbounded. That setting changes cost and latency by far more than the price does. We ran one easy and one hard coding task across the settings, checking every answer against a reference on hundreds of randomized cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  An easy task: reasoning just adds cost
&lt;/h3&gt;

&lt;p&gt;Weighted interval scheduling, a moderate dynamic-programming problem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Reasoning tokens&lt;/th&gt;
&lt;th&gt;Answer tokens&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;glm-5.2&lt;/code&gt;, thinking off&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0008&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;≈5s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;glm-5.2&lt;/code&gt;, &lt;code&gt;reasoning_effort: low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1,563&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;$0.0076&lt;/td&gt;
&lt;td&gt;39s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;glm-5.2&lt;/code&gt;, unbounded default&lt;/td&gt;
&lt;td&gt;≈6,290&lt;/td&gt;
&lt;td&gt;≈150&lt;/td&gt;
&lt;td&gt;$0.0285&lt;/td&gt;
&lt;td&gt;137s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gpt-5.5&lt;/code&gt; (reference)&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;141&lt;/td&gt;
&lt;td&gt;$0.0064&lt;/td&gt;
&lt;td&gt;4.8s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude-opus-4-8&lt;/code&gt; (reference)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;$0.0057&lt;/td&gt;
&lt;td&gt;3.3s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out. Thinking off is correct and the cheapest thing on the board, about 8x under the frontier models, and every step up the dial just adds cost for the same answer. And the bill tracks the reasoning, not the answer: the code GLM returns is roughly 150 tokens every time, while the reasoning in front of it grows from nothing to about 6,300, billed at the same $4.40/M output rate. The unbounded default spends that reasoning to reach the same answer thinking off produced with none, and the gap is the entire cost difference. The frontier models answer here with little or no reported reasoning: &lt;code&gt;gpt-5.5&lt;/code&gt; spends 59 reasoning tokens, and &lt;code&gt;claude-opus-4-8&lt;/code&gt;'s usage reports none.&lt;/p&gt;

&lt;h3&gt;
  
  
  A hard task: reasoning earns its keep, the default does not
&lt;/h3&gt;

&lt;p&gt;Wildcard string matching (&lt;code&gt;?&lt;/code&gt; and &lt;code&gt;*&lt;/code&gt;), the classic problem that is easy to get subtly wrong. Here thinking off broke. It returned a memoized recursion:&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;is_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;memo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;memo&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;memo&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&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;j&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&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;*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks right, and the memo even suggests some care. But the &lt;code&gt;*&lt;/code&gt; branch recurses &lt;code&gt;match(i + 1, j)&lt;/code&gt; without bounding &lt;code&gt;i&lt;/code&gt;. Once the string is consumed and the pattern still has a &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;i&lt;/code&gt; climbs forever and the stack overflows. Fast, cheap, and wrong.&lt;/p&gt;

&lt;p&gt;Turn the dial up and it returns the correct iterative two-pointer algorithm, which backtracks to the last &lt;code&gt;*&lt;/code&gt; instead of recursing:&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;is_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;star_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;s_idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;p_idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p_idx&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;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p_idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s_idx&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
            &lt;span class="n"&gt;s_idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;p_idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;p_idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p_idx&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;*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;star_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_idx&lt;/span&gt;
            &lt;span class="n"&gt;match_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s_idx&lt;/span&gt;
            &lt;span class="n"&gt;p_idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;star_idx&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;p_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;star_idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;match_idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;s_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match_idx&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;p_idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p_idx&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;*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;p_idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p_idx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full dial on this task:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GLM 5.2 setting&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;thinking off&lt;/td&gt;
&lt;td&gt;$0.0007&lt;/td&gt;
&lt;td&gt;6s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no (stack overflow)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reasoning_effort: high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0031&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;13s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reasoning_effort: medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.0032&lt;/td&gt;
&lt;td&gt;16s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reasoning_effort: low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.0068&lt;/td&gt;
&lt;td&gt;40s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unbounded default&lt;/td&gt;
&lt;td&gt;$0.062&lt;/td&gt;
&lt;td&gt;405s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gpt-5.5&lt;/code&gt; (reference)&lt;/td&gt;
&lt;td&gt;$0.0064&lt;/td&gt;
&lt;td&gt;5.4s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude-opus-4-8&lt;/code&gt; (reference)&lt;/td&gt;
&lt;td&gt;$0.0069&lt;/td&gt;
&lt;td&gt;4.6s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every explicit effort level solved it. &lt;code&gt;reasoning_effort: high&lt;/code&gt; did it for $0.0031 in 13 seconds, about twenty times cheaper and thirty times faster than the unbounded default for the same answer, and it undercuts the frontier models on cost, just a few seconds slower. One quirk worth knowing: GLM's &lt;code&gt;low&lt;/code&gt; produced more reasoning than &lt;code&gt;high&lt;/code&gt;, consistently across both tasks, so the names don't track token count. Medium and high were the cheap, fast settings.&lt;/p&gt;

&lt;p&gt;The unbounded default is the one setting to avoid. It is the worst of both worlds: it buys reasoning the task may not need and takes minutes to do it, reaching the same answer &lt;code&gt;reasoning_effort: high&lt;/code&gt; gave for twenty times the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;The lever is the reasoning effort, and the right setting belongs to the task, not the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple or high-volume work&lt;/strong&gt; where correctness is easy: thinking off (&lt;code&gt;enable_thinking: false&lt;/code&gt;). Correct and about 8x under frontier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harder problems&lt;/strong&gt; where thinking off fails: &lt;code&gt;reasoning_effort: medium&lt;/code&gt; or &lt;code&gt;high&lt;/code&gt;. Correct, around $0.003 a task, under frontier on cost and only a few seconds slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never the unbounded default.&lt;/strong&gt; Leaving reasoning on with no effort cap is how a $0.003 answer becomes a $0.06, seven-minute one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot tell in advance whether a task needs reasoning, &lt;code&gt;reasoning_effort: high&lt;/code&gt; is a safe default: it was cheap, it solved both tasks, and it never ran away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching helps the input, not the reasoning
&lt;/h2&gt;

&lt;p&gt;GLM 5.2 supports caching on the gateway, and it helps where you'd expect. We sent a 1,494-token shared prefix (a code module to review) with several different questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Prompt tokens&lt;/th&gt;
&lt;th&gt;Cached&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;new question, prefix not yet cached&lt;/td&gt;
&lt;td&gt;1,493&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;$0.0026&lt;/td&gt;
&lt;td&gt;6.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new question, prefix cached&lt;/td&gt;
&lt;td&gt;1,494&lt;/td&gt;
&lt;td&gt;1,472&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;$0.0009&lt;/td&gt;
&lt;td&gt;5.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exact repeat (semantic hit)&lt;/td&gt;
&lt;td&gt;1,494&lt;/td&gt;
&lt;td&gt;1,494&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;$0.0009&lt;/td&gt;
&lt;td&gt;1.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once a large prefix has been seen, it caches. The cached input tokens bill at roughly a fifth of the normal input rate, which cut an otherwise identical request from $0.0026 to $0.0009, about 64%. An exact repeat is served straight from the semantic cache: the same answer at the same cost as the cached call, but back in about a second instead of five.&lt;/p&gt;

&lt;p&gt;The catch is the same one the dial taught: caching discounts the input, and the moment reasoning is on, the cost and latency live in the reasoning output, which is not cached. So caching is a real win for thinking-off, high-context work (the same system prompt or codebase on every call), and a small one once reasoning is on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it on Synthorai
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;glm-5.2&lt;/code&gt; is live on the gateway. Three practical notes from our testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set the reasoning effort explicitly.&lt;/strong&gt; Use &lt;code&gt;enable_thinking: false&lt;/code&gt; for simple work and &lt;code&gt;reasoning_effort: medium&lt;/code&gt; or &lt;code&gt;high&lt;/code&gt; for harder problems. The one thing to avoid is leaving reasoning on with no effort cap (the unbounded default), which is the $0.06, seven-minute trap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream when reasoning is on.&lt;/strong&gt; Reasoning responses can run for minutes, and a non-streaming request sits on a silent connection long enough that your client will likely time out before the answer arrives. Use &lt;code&gt;stream: true&lt;/code&gt; and you get incremental output and the full result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse your context.&lt;/strong&gt; If you send the same large system prompt or codebase on every call, prefix caching cuts the input cost, and pairing it with thinking off makes the whole request cheap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pricing is $1.40 / $4.40 per million tokens, and the gateway returns a &lt;code&gt;cost&lt;/code&gt; field per call so you can see exactly what each request cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;GLM 5.2 is a genuinely cheap, capable coding model, and configured well it beats frontier prices on both easy and hard work. The catch is the configuration. Its reasoning is a dial, and the default leaves it unbounded, which is how a task that should cost $0.003 becomes a $0.06, seven-minute call. Set &lt;code&gt;enable_thinking: false&lt;/code&gt; for simple work and &lt;code&gt;reasoning_effort: medium&lt;/code&gt; or &lt;code&gt;high&lt;/code&gt; for the rest, and GLM 5.2 is cheap and correct across the board. Leave reasoning on its default, and it is the slowest, priciest option you could have picked.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://venturebeat.com/technology/z-ais-open-weights-glm-5-2-beats-gpt-5-5-on-multiple-long-horizon-coding-benchmarks-for-1-6th-the-cost" rel="noopener noreferrer"&gt;VentureBeat: Z.ai's open-weight GLM-5.2 beats GPT-5.5 on long-horizon coding for 1/6 the cost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.eigent.ai/blog/glm-5-2" rel="noopener noreferrer"&gt;eigent.ai: GLM-5.2 specs and overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudprice.net/models/zhipu-glm-5-2" rel="noopener noreferrer"&gt;CloudPrice: GLM-5.2 pricing and specs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.z.ai/guides/overview/pricing" rel="noopener noreferrer"&gt;Z.ai: official GLM API pricing (GLM-4.5 / 4.6 / 5 generations)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Synthorai listing prices above are this platform's rates as of 2026-06-24; GLM generational rates are Zhipu's official list.)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Costs measured on Synthorai on 2026-06-24 (&lt;code&gt;glm-5.2&lt;/code&gt; at $1.40 / $4.40 per M tokens); verify current pricing before relying on it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>What Actually Drives Your Image-Generation Bill</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Fri, 19 Jun 2026 18:14:35 +0000</pubDate>
      <link>https://dev.to/synthorai/what-actually-drives-your-image-generation-bill-2bc2</link>
      <guid>https://dev.to/synthorai/what-actually-drives-your-image-generation-bill-2bc2</guid>
      <description>&lt;p&gt;We added image generation to a gateway built for text LLMs and measured what drives the cost across four variables: model, resolution, image count, and quality. The largest lever is quality, a parameter most image APIs expose and most callers leave on default. Resolution, prompt caching, and batching matter far less than people expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  How image models differ
&lt;/h2&gt;

&lt;p&gt;Image models aren't drop-in swaps for one another. They diverge on several axes, and only one of them (billing shape) is about price. The active catalog at a glance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family&lt;/th&gt;
&lt;th&gt;Billing&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;quality&lt;/code&gt; knob&lt;/th&gt;
&lt;th&gt;Batch &lt;code&gt;n&amp;gt;1&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gpt-image&lt;/code&gt; (OpenAI)&lt;/td&gt;
&lt;td&gt;per-token&lt;/td&gt;
&lt;td&gt;✓ &lt;code&gt;low&lt;/code&gt;/&lt;code&gt;med&lt;/code&gt;/&lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;up to ≈2K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gemini-image&lt;/code&gt; (Google)&lt;/td&gt;
&lt;td&gt;per-token&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗ 1/call&lt;/td&gt;
&lt;td&gt;1K (&lt;code&gt;gemini-3&lt;/code&gt;: to 4K)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;qwen-image&lt;/code&gt; / &lt;code&gt;wan2.7&lt;/code&gt; (Alibaba)&lt;/td&gt;
&lt;td&gt;flat/image&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;512²–2048²&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;seedream&lt;/code&gt; (BytePlus)&lt;/td&gt;
&lt;td&gt;flat/image&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗ 1/call&lt;/td&gt;
&lt;td&gt;≥1920² (4.5/5.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The axes that bite if you assume one model behaves like another:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Billing shape.&lt;/strong&gt; Per-token (&lt;code&gt;gpt-image&lt;/code&gt;, &lt;code&gt;gemini&lt;/code&gt;) or flat-per-image (&lt;code&gt;qwen&lt;/code&gt;, &lt;code&gt;wan&lt;/code&gt;, &lt;code&gt;seedream&lt;/code&gt;). This is the axis that decides your bill, and it's the subject of the next section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;quality&lt;/code&gt; knob.&lt;/strong&gt; Only &lt;code&gt;gpt-image&lt;/code&gt; has it (&lt;code&gt;low&lt;/code&gt;/&lt;code&gt;medium&lt;/code&gt;/&lt;code&gt;high&lt;/code&gt;). Gemini changes fidelity by model tier (&lt;code&gt;flash&lt;/code&gt; to &lt;code&gt;pro&lt;/code&gt;) or &lt;code&gt;image_size&lt;/code&gt;; flat models have no such dial. That one knob swings the bill about 36×, so it's the main cost lever, covered below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch (&lt;code&gt;n&amp;gt;1&lt;/code&gt;) isn't universal.&lt;/strong&gt; &lt;code&gt;gpt-image&lt;/code&gt;, &lt;code&gt;qwen&lt;/code&gt;, and &lt;code&gt;wan&lt;/code&gt; return several images per call. Every Gemini and Seedream image model is one-image-per-call: &lt;code&gt;n=2&lt;/code&gt; returns a &lt;code&gt;400&lt;/code&gt;, so you issue N requests and orchestrate the batch yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution limits cut both ways.&lt;/strong&gt; &lt;code&gt;gemini-2.5-flash-image&lt;/code&gt; caps at 1K (1 MP), while &lt;code&gt;gemini-3&lt;/code&gt; reaches 2K/4K (and its bill roughly doubles from 1K to 4K). Seedream 4.5/5.0 enforce a floor of about 1920² and reject anything smaller. &lt;code&gt;qwen-image&lt;/code&gt; lives in a 512²–2048² band. Higher resolution isn't always available, and dropping resolution to save money isn't always allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control knobs and image-to-image differ.&lt;/strong&gt; Only some models accept &lt;code&gt;seed&lt;/code&gt;, &lt;code&gt;negative_prompt&lt;/code&gt;, or &lt;code&gt;guidance_scale&lt;/code&gt;, and the reference-image limit for editing runs from 3 (&lt;code&gt;gemini-2.5&lt;/code&gt;) to 16 (&lt;code&gt;gpt-image&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;quality&lt;/code&gt; knob has one non-obvious property. For &lt;code&gt;gpt-image&lt;/code&gt;, an output token is a billing unit, not a measure of the file you get. OpenAI assigns the count from a published per-(&lt;code&gt;quality&lt;/code&gt; × &lt;code&gt;size&lt;/code&gt;) rate table (272 / 1,056 / 4,160 tokens for low / medium / high at 1024² on gpt-image-1), so the count is set by &lt;code&gt;quality&lt;/code&gt;, not derived from the bytes returned. We checked: the same prompt at 1024² across all three tiers produced identical 1024×1024 PNGs of roughly the same file size (about 0.9 MB), yet billed 196, 1,756, and 7,024 tokens. Same resolution, same byte size, 36× the cost. You pay for rendering effort, not pixels, which is why you read &lt;code&gt;usage&lt;/code&gt; rather than eyeball the output.&lt;/p&gt;

&lt;p&gt;One capability none of these models has is prompt caching, usually the first cost-saving idea people reach for. Image generation is stateless: there's no conversation or KV state to reuse, the &lt;code&gt;usage&lt;/code&gt; object carries no cache fields, and (as we measure below) batching doesn't share the prompt either. Caching is a chat feature, not an image one, which rules out a common assumption about cutting image cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  We measured it
&lt;/h2&gt;

&lt;p&gt;Same e-commerce-style product prompt, real generations through the gateway, with cost computed from the returned &lt;code&gt;usage&lt;/code&gt; against each model's published rates. Five findings, each from a separate sweep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The image is the cost, not the prompt.&lt;/strong&gt; In text-to-image (a prompt in, an image out), the bill is 97–100% output tokens: a 1024² &lt;code&gt;gpt-image-2&lt;/code&gt; generation is 21 input and 196 output tokens (about $0.0001 plus $0.0059), and &lt;code&gt;gemini-2.5-flash-image&lt;/code&gt; takes 10 input. The prompt you write is a rounding error, but only because it's text. Feed an image instead (image-to-image, like "make this mug blue") and the input tokenizes large:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;t2i input&lt;/th&gt;
&lt;th&gt;i2i input (1 ref)&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gpt-image-2&lt;/code&gt; (low)&lt;/td&gt;
&lt;td&gt;21 tok&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,043 tok&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;196 tok&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 tok&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,297 tok&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,290 tok&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The input jumps 50–130×, and it scales linearly: each extra reference adds about 1,025 tokens on &lt;code&gt;gpt-image-2&lt;/code&gt; (1, 2, and 3 references measured at 1,043, 2,068, and 3,093). At low quality those input tokens outnumber the generated output five-to-one. The principle holds either way: an image is the cost, whether you generate it or supply it, and the prompt never is. The rest of this article stays in text-to-image; the fuller image-to-image economics are their own follow-up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Model choice is a 6× lever.&lt;/strong&gt; Identical 1024² request, default quality:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Billing&lt;/th&gt;
&lt;th&gt;Cost / image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-image-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;token · &lt;code&gt;quality&lt;/code&gt; knob&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0060&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpt-image-1-mini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;token · &lt;code&gt;quality&lt;/code&gt; knob&lt;/td&gt;
&lt;td&gt;$0.0085&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seedream-4-0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-request flat&lt;/td&gt;
&lt;td&gt;$0.030&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen-image-2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-request flat&lt;/td&gt;
&lt;td&gt;$0.035&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;token · no &lt;code&gt;quality&lt;/code&gt; knob&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0387&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 6.4× spread between the cheapest and priciest path, driven entirely by how many output tokens each model emits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Resolution barely moves it.&lt;/strong&gt; Sweeping &lt;code&gt;gpt-image-2&lt;/code&gt; from 1024² to 2048², per-image cost stayed roughly flat ($0.0060 to $0.0121); output tokens aren't proportional to pixels. &lt;code&gt;gemini-2.5-flash-image&lt;/code&gt; returned the same 1,290 tokens whatever size we requested, because it's 1K-only and &lt;code&gt;size&lt;/code&gt; only changes the aspect ratio. (The &lt;code&gt;gemini-3&lt;/code&gt; image tiers do honor &lt;code&gt;image_size&lt;/code&gt;, roughly doubling cost from 1K to 4K, but &lt;code&gt;2.5-flash-image&lt;/code&gt;, the model we cost here, does not.) Per-image flat models are resolution-independent by definition. So far the per-token model looks hard to beat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Quality is the crossover.&lt;/strong&gt; Sweep &lt;code&gt;gpt-image-2&lt;/code&gt; across quality tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;quality&lt;/th&gt;
&lt;th&gt;1024²&lt;/th&gt;
&lt;th&gt;2048²&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;$0.0060 (196 tok)&lt;/td&gt;
&lt;td&gt;$0.0121 (397 tok)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;$0.053 (1,756 tok)&lt;/td&gt;
&lt;td&gt;$0.107 (3,568 tok)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.211&lt;/strong&gt; (7,024 tok)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.428&lt;/strong&gt; (14,272 tok)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Output tokens scale about 9× from low to medium and about 36× from low to high. At low quality the per-token model is the cheapest option; at medium or high it passes the flat per-image price ($0.03–0.035). The crossover sits where the arithmetic puts it, around 1,000 output tokens ($0.03 ÷ $30/M): &lt;code&gt;low&lt;/code&gt; is under it, &lt;code&gt;medium&lt;/code&gt; is over. This also corrects an earlier conclusion of ours. "Per-token is always cheapest" was an artifact of testing at default low quality.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwz4bwjtobd90c078hh5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwz4bwjtobd90c078hh5.jpg" alt="The same prompt rendered by gpt-image-2 at low, medium and high quality: three equally sharp 1024² product photos labelled 196 / 1,756 / 7,024 output tokens and $0.006 / $0.053 / $0.215." width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same prompt, &lt;code&gt;gpt-image-2&lt;/code&gt;, 1024². &lt;code&gt;low&lt;/code&gt; / &lt;code&gt;medium&lt;/code&gt; / &lt;code&gt;high&lt;/code&gt; bill 196 / 1,756 / 7,024 output tokens, or $0.006 / $0.053 / $0.215: a 36× spread at identical resolution. For a clean product shot like this the three are hard to tell apart, so the cheapest tier is often enough. Set &lt;code&gt;quality&lt;/code&gt; to the job instead of defaulting to &lt;code&gt;high&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. You can't share a prompt across images.&lt;/strong&gt; Generating &lt;code&gt;n&lt;/code&gt; images in one call doesn't amortize the prompt. &lt;code&gt;gpt-image-2&lt;/code&gt; bills it &lt;code&gt;N&lt;/code&gt; times: input tokens went from 28 to 112 at &lt;code&gt;n=4&lt;/code&gt;, and a long brand prompt went from 499 to 1,996. Per-image cost was identical at &lt;code&gt;n=1&lt;/code&gt; and &lt;code&gt;n=4&lt;/code&gt;. With no caching either, there's no prompt-cost-sharing mechanism for image generation. You pay per output image, and the prompt is re-billed each time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;For text-to-image, it comes down to quality, not the things people assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low / draft / thumbnail quality:&lt;/strong&gt; a token-with-quality model (&lt;code&gt;gpt-image&lt;/code&gt;, about $0.006–0.012). Cheapest at any resolution up to about 2K.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium / high quality:&lt;/strong&gt; per-request flat (&lt;code&gt;seedream&lt;/code&gt; / &lt;code&gt;qwen&lt;/code&gt;, $0.03–0.035). The per-token bill runs away ($0.05–0.43 in our sweep), and flat is both cheaper and quality-independent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gemini&lt;/code&gt; (about $0.039 at default 1K) is rarely the cost-optimal pick.&lt;/strong&gt; It's undercut by &lt;code&gt;gpt-image&lt;/code&gt; at low quality and by per-request flat at medium and high. It has no &lt;code&gt;quality&lt;/code&gt; dial; you'd choose its Pro tier or a higher &lt;code&gt;image_size&lt;/code&gt; for output quality, not for price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution moves cost about 2× within a quality tier,&lt;/strong&gt; not enough to flip the choice. Quality flips it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;n&amp;gt;1&lt;/code&gt;, caching, and batching never reduce per-image cost.&lt;/strong&gt; There's nothing to share.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image-to-image: default to flat per-image.&lt;/strong&gt; A reference image is input, and only per-token models surcharge it (about 1,025 tokens each); flat models include it for free. For editing, &lt;code&gt;seedream&lt;/code&gt; / &lt;code&gt;qwen&lt;/code&gt; usually win. &lt;code&gt;gpt-image&lt;/code&gt; stays cheaper only for low-quality edits with a few references (around 5 crosses the flat price), and loses once quality or reference count climbs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;E-commerce is the clearest example. Say you generate product photos by sending the same long brand prompt for every item in the catalog, and you assume caching that repeated prompt will save money. That fails for two reasons: the prompt was never the cost (the image is), and there's no caching for generation anyway. Since real product imagery is medium quality or higher, the right choice is a flat per-image model, which is both cheaper and more predictable regardless of how repetitive your prompts are.&lt;/p&gt;

&lt;p&gt;The capability gates from the opening section can still override the choice: one-image-per-call models, resolution floors and ceilings, data-residency limits, and which knobs (&lt;code&gt;seed&lt;/code&gt;, &lt;code&gt;negative_prompt&lt;/code&gt;, &lt;code&gt;guidance_scale&lt;/code&gt;) a model exposes. Pick on cost, then confirm the capability fits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why you can trust these numbers
&lt;/h2&gt;

&lt;p&gt;These figures come from real &lt;code&gt;usage&lt;/code&gt; against each vendor's list rates, not estimates. Image billing on our gateway is sessionless: it settles only on a &lt;code&gt;2xx&lt;/code&gt; (a failed generation is never charged), pre-checks the worst-case cost before any spend, and bills a missing-&lt;code&gt;usage&lt;/code&gt; response at the ceiling rather than silently $0. The principle is the same one we apply everywhere: trust the cost, not a figure the vendor hands you. It's the method we used to &lt;a href="https://dev.to/blog/llm-gateway-cache-audit/"&gt;audit whether a gateway lies about cache&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Image generation looks like just another endpoint, but the billing unit changed. For text-to-image the lever isn't the prompt (no caching, no batch sharing) or the resolution. It's quality: &lt;code&gt;gpt-image&lt;/code&gt; is cheapest at low, per-image flat (&lt;code&gt;seedream&lt;/code&gt; / &lt;code&gt;qwen&lt;/code&gt;) wins at medium and high, with the crossover near 1,000 output tokens. Set quality deliberately, match the model to it, and check the cost. When you move from generating to editing, feeding a reference image, re-run the math, because the input image becomes the cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does prompt caching reduce image-generation cost?&lt;/strong&gt;&lt;br&gt;
No. Generation is stateless: the &lt;code&gt;usage&lt;/code&gt; object has no cache fields, and batching re-bills the prompt per image. The cost is the output image, not the text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-token or per-image, which is cheaper?&lt;/strong&gt;&lt;br&gt;
It depends on quality. For low or draft quality, a &lt;code&gt;quality&lt;/code&gt;-knob model like &lt;code&gt;gpt-image&lt;/code&gt; (about $0.006–0.012). For medium or high, per-image flat like &lt;code&gt;seedream&lt;/code&gt;/&lt;code&gt;qwen&lt;/code&gt; ($0.03–0.035), because the per-token bill runs away. For image-to-image the answer tilts further to flat: they include reference images for free, while per-token surcharges about 1,025 tokens each.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/image-generation" rel="noopener noreferrer"&gt;OpenAI: Image generation API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=43787769" rel="noopener noreferrer"&gt;OpenAI: gpt-image per-token pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/gemini-api/docs/pricing" rel="noopener noreferrer"&gt;Google: Gemini API pricing (image output tokens)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/prompt-caching" rel="noopener noreferrer"&gt;OpenAI: Prompt caching (why it does not apply to image generation)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;All checked 2026-06-19. Not financial advice; verify current pricing before relying on it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Prompt Caching for Open-Weight LLMs: Provider Roulette</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Mon, 15 Jun 2026 11:52:43 +0000</pubDate>
      <link>https://dev.to/synthorai/prompt-caching-for-open-weight-llms-provider-roulette-4faf</link>
      <guid>https://dev.to/synthorai/prompt-caching-for-open-weight-llms-provider-roulette-4faf</guid>
      <description>&lt;p&gt;With a closed model, prompt caching is one documented contract. Claude has &lt;code&gt;cache_control&lt;/code&gt; breakpoints; OpenAI and Gemini cache automatically above a token floor; the discounts are published and stable. You read one page and you're done.&lt;/p&gt;

&lt;p&gt;Open weights break that assumption. The same Qwen or Llama checkpoint is served by a dozen hosts, and &lt;strong&gt;caching is not a property of the model — it's a property of where the model runs.&lt;/strong&gt; To show how far that goes, here's one measured request — an identical ~4.7K-token prompt sent to the same Qwen model through a multi-provider router six times, no upstream pinned:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Upstream the router picked&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Cached tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Upstream A&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0141&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Upstream B&lt;/td&gt;
&lt;td&gt;$0.000709&lt;/td&gt;
&lt;td&gt;0 (cold)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3–6&lt;/td&gt;
&lt;td&gt;Upstream B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.000286&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4,224 (warm)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same model, same router, same prompt: the bill ranged from &lt;strong&gt;$0.0141 to $0.000286 — a 49× spread&lt;/strong&gt; — purely on which upstream the router chose and whether that upstream had the prefix warm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt caching for open-weight models is a routing outcome, not a model feature.&lt;/strong&gt; It's implemented — free and automatic — in the inference engine, then preserved or destroyed by every layer above it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five layers: one provides caching, three can break it.&lt;/strong&gt; The model (sets cacheability, serves no cache) → the inference engine (caching, free) → the compute host (productizes it, unevenly) → the gateway (multi-cluster routing) → the router (scatters across vendors with disjoint caches).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measured.&lt;/strong&gt; An identical request, scattered by a router, cost &lt;strong&gt;49× more&lt;/strong&gt; on one pick than another; on one model, one host delivered &lt;strong&gt;59.6% off&lt;/strong&gt; and another &lt;strong&gt;0%&lt;/strong&gt;; published cache discounts span &lt;strong&gt;0% to ~98%&lt;/strong&gt; across models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What to do.&lt;/strong&gt; Pin your route so repeated prefixes hit the same warm cache; audit by the &lt;strong&gt;cost&lt;/strong&gt; delta, not the &lt;code&gt;cached_tokens&lt;/code&gt; field (which often reads 0 on a real hit); weigh latency separately — warm prefills run 2–10× faster even at ~0% cost discount.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Live figures were measured on 2026-06-14 against a multi-provider router and our own gateway, with a fixed ~4.7K-token English prompt, small &lt;code&gt;max_tokens&lt;/code&gt;, sequential runs. Documented pricing was checked against primary provider docs the same day and cross-verified adversarially. &lt;strong&gt;Ratios&lt;/strong&gt; (percent discount, latency change) are the portable part; absolute dollars depend on the venue, your prompt, and load. Reproduce before quoting.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The cache types you'll actually meet
&lt;/h2&gt;

&lt;p&gt;Before the stack, the vocabulary. Across open-weight hosts there are four distinct cache shapes, and they bill differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Automatic prefix caching (no markers).&lt;/strong&gt; The dominant pattern. The server hashes your prompt prefix, reuses the KV state if it matches an earlier request, and applies the discount on its own — no &lt;code&gt;cache_control&lt;/code&gt;, no code change, often impossible to disable. DeepSeek, Zhipu GLM, and most open-weight hosts do this. Writes are free; the cache lives anywhere from VRAM (minutes) to disk (DeepSeek keeps prefixes "a few hours to a few days").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Explicit breakpoint caching (&lt;code&gt;cache_control&lt;/code&gt;).&lt;/strong&gt; The Anthropic shape, which a few open-weight hosts also offer. Alibaba's Model Studio takes &lt;code&gt;"cache_control": {"type": "ephemeral"}&lt;/code&gt; on a Qwen message block; some serving platforms expose an equivalent marker. You mark the boundary, pay a &lt;strong&gt;write surcharge&lt;/strong&gt;, and get a &lt;strong&gt;deeper read discount&lt;/strong&gt; in return.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Rented cache objects (with a storage fee).&lt;/strong&gt; The one to watch. Moonshot's legacy &lt;code&gt;moonshot-v1&lt;/code&gt; family makes you &lt;code&gt;POST /v1/caching&lt;/code&gt; to create a cache, then bills a write fee, a &lt;strong&gt;per-token-per-minute storage fee&lt;/strong&gt;, and a per-call hit fee. Google's &lt;em&gt;explicit&lt;/em&gt; Gemini caching is the same idea — input cost &lt;strong&gt;plus storage&lt;/strong&gt; at roughly $1.00–$4.50 per 1M-tokens per hour. The cache is a resource you rent and must garbage-collect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Self-host KV reuse (free).&lt;/strong&gt; Run the weights yourself and the inference engine caches for free and automatically. No write fee, no read rate, no storage rental — a hit just skips prefill.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cache type&lt;/th&gt;
&lt;th&gt;Markers?&lt;/th&gt;
&lt;th&gt;Write fee&lt;/th&gt;
&lt;th&gt;Storage fee&lt;/th&gt;
&lt;th&gt;Where you meet it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Automatic prefix&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Most open-weight hosts; DeepSeek, GLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit breakpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cache_control&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Surcharge&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Qwen (explicit mode); some platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rented cache object&lt;/td&gt;
&lt;td&gt;Create/TTL/delete&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moonshot &lt;code&gt;moonshot-v1&lt;/code&gt;, Gemini explicit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-host KV reuse&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;vLLM, SGLang, TensorRT-LLM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen on Model Studio offers &lt;strong&gt;both&lt;/strong&gt; automatic and explicit modes, with a real tradeoff: implicit bills a hit at &lt;strong&gt;20% of input&lt;/strong&gt; with free writes; explicit bills a hit at &lt;strong&gt;10% of input&lt;/strong&gt; but charges &lt;strong&gt;125% on the write&lt;/strong&gt; and bounds the entry to a 5-minute TTL. Deeper discount, but you pay to populate and pay again each time it expires.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where caching lives in the stack
&lt;/h2&gt;

&lt;p&gt;Here is the key idea. Prompt caching for open weights is &lt;strong&gt;solved at exactly one layer and endangered at every layer above it.&lt;/strong&gt; Walk the stack from the weights up, and at each layer ask: does this layer &lt;em&gt;provide&lt;/em&gt; caching, or merely &lt;em&gt;forward&lt;/em&gt; it — and can it &lt;em&gt;break&lt;/em&gt; what the layer below already did?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  request
     |
     v
  +--------------------------------------------------+
  | L5  router             scatters across vendors   |  can break it
  | L4  gateway            multi-cluster routing     |  can break it
  | L3  compute host       uneven delivery           |  can break it
  |==================================================|
  | L2  inference engine   CACHING LIVES HERE, free  |  &amp;lt;-- the cache is born here
  |==================================================|
  | L1  model              cacheability: MLA / GQA   |  sets the ceiling
  +--------------------------------------------------+

  A cache hit is born at L2 and must survive L3-L5 routing to reach you;
  every layer above L2 is a chance to land where your prefix isn't.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 1 — The model: cacheability, not a cache
&lt;/h3&gt;

&lt;p&gt;This is the layer most people &lt;em&gt;think&lt;/em&gt; caching lives in — "DeepSeek has caching" — so it's the first one to get precise about. A checkpoint is a bag of weights; it runs the same attention whether or not a KV cache exists. It ships no cache, no discount, no TTL, no &lt;code&gt;cache_control&lt;/code&gt; marker — those are serving-layer features. In that strict sense the weights provide no caching &lt;em&gt;product&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But the weights are not neutral, and DeepSeek is the perfect example of why. &lt;strong&gt;The model's attention architecture decides how big the KV cache is, and therefore how cheap caching can ever be:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DeepSeek's &lt;strong&gt;Multi-head Latent Attention (MLA)&lt;/strong&gt; compresses the KV cache into a low-rank latent — to roughly 4–14% of a standard multi-head cache. That compression is exactly what lets DeepSeek's API persist prefixes to disk and price a cache read at ~2% of input. The architecture is the &lt;em&gt;enabler&lt;/em&gt;; the disk cache is a &lt;em&gt;product built on top of it&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grouped-Query Attention (GQA)&lt;/strong&gt; — used by Llama, Qwen, Mistral, and DeepSeek — shares KV heads to shrink the cache by the group factor (≈8× on Llama-3).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So Layer 1's contribution is &lt;strong&gt;cacheability, not a cache&lt;/strong&gt;: the architecture sets the ceiling on how cheap every layer above can make caching, but the weights never serve a cached token themselves. And "DeepSeek has caching" quietly merges two different things wearing the same name — the &lt;em&gt;weights&lt;/em&gt; (this layer, which give you MLA) and DeepSeek's &lt;em&gt;API and serving stack&lt;/em&gt; (Layers 2–3, which give you the disk cache, the discount, and the usage fields). Download the open weights and run them yourself and you keep MLA's small KV cache, but the disk-cache &lt;em&gt;product&lt;/em&gt; stays on DeepSeek's servers — you inherit whatever Layer 2 you deploy in its place. So the operational move still holds: stop asking whether a &lt;em&gt;model&lt;/em&gt; caches and start asking where it's &lt;em&gt;served&lt;/em&gt; — just don't mistake that for the architecture not mattering. It sets the ceiling; the path sets what you actually get.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2 — The inference engine: where caching is built, and free
&lt;/h3&gt;

&lt;p&gt;One layer up, caching is not just present — it's &lt;strong&gt;solved, and free.&lt;/strong&gt; Modern inference engines cache prefixes automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;vLLM&lt;/strong&gt; — Automatic Prefix Caching: hashes each KV block, reuses any block whose prefix hash it has seen, LRU-evicts. On by default in V1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SGLang&lt;/strong&gt; — RadixAttention: stores the KV cache in a radix tree so any shared prefix is reused, with cache-aware scheduling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TensorRT-LLM&lt;/strong&gt; — block reuse (&lt;code&gt;enable_block_reuse&lt;/code&gt;, default on), with optional offload of KV blocks to host memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Projects like LMCache extend this further — offloading KV to CPU/disk and &lt;em&gt;sharing it across instances&lt;/em&gt;, which is the seed of solving the routing problem we're about to hit. The point: if you self-host, you are done. Caching is automatic, costs nothing beyond the GPUs you already run, evicts by LRU, and &lt;strong&gt;you own it&lt;/strong&gt; — a hit simply skips prefill, lowering TTFT and raising throughput. There is no &lt;code&gt;cached_tokens&lt;/code&gt; billing field because nothing is billed; the payoff shows up in your own latency metrics. For a closed model you rent caching; for an open one you can own it outright. The catch is the inverse of the hosted world: the cache is ephemeral (VRAM, LRU), so it survives only while the prefix stays hot — which is precisely what the layers above must preserve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3 — The compute host: productizing it, unevenly
&lt;/h3&gt;

&lt;p&gt;Commercial inference hosts wrap Layer 2 and run &lt;strong&gt;fleets of replicas&lt;/strong&gt;. They inherit free automatic caching — the question is whether they implement it &lt;em&gt;well&lt;/em&gt;, and the answer is mixed on two axes.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;exposure and price vary wildly&lt;/strong&gt;. Among the major open-weight hosts: one applies a flat 50% to cached input and lets cached tokens skip rate limits; another defaults to 50% off on serverless; a third prices cached input per model (e.g. a Qwen tier at ~80% off) and exposes a cache-key hint to improve affinity; a fourth makes caching always-on and undiscloseable on dedicated endpoints. Same underlying engine, four pricing philosophies.&lt;/p&gt;

&lt;p&gt;Second — and this is the first place caching &lt;em&gt;breaks&lt;/em&gt; — the &lt;strong&gt;multi-replica problem&lt;/strong&gt;. Your warm prefix lives in the VRAM of the one replica that served the cold request. The host's own load balancer may send your next request to a different replica with a cold cache. We saw exactly this: pinning the same Qwen model to one upstream at a time and running cold→warm:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pinned upstream&lt;/th&gt;
&lt;th&gt;Cold&lt;/th&gt;
&lt;th&gt;Warm&lt;/th&gt;
&lt;th&gt;Discount&lt;/th&gt;
&lt;th&gt;&lt;code&gt;cached_tokens&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Provider A&lt;/td&gt;
&lt;td&gt;$0.000709&lt;/td&gt;
&lt;td&gt;$0.000286&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;59.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4,224 ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider B&lt;/td&gt;
&lt;td&gt;$0.000662&lt;/td&gt;
&lt;td&gt;$0.000662&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Provider A cached cleanly and reported it. Provider B — which &lt;em&gt;advertises&lt;/em&gt; a cache-read price for this model — returned &lt;strong&gt;no discount across a cold call and two warm calls&lt;/strong&gt; in our test. Whether that's eligibility, replica fan-out, or a longer warm-up than two requests, the measured result on this path was zero. The capability is solved at Layer 2; whether you actually receive it is a Layer-3 execution detail, and it differs by host.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4 — The gateway: the multi-cluster problem
&lt;/h3&gt;

&lt;p&gt;A gateway sits in front of one or more upstreams and multiplies the replica problem into a &lt;strong&gt;cluster problem&lt;/strong&gt;. If it round-robins requests across clusters or providers without &lt;strong&gt;cache affinity&lt;/strong&gt;, the warm cache becomes structurally unreachable — every request lands somewhere the prefix isn't. A cache-aware gateway must route by prefix hash so identical prefixes stick to the same upstream, the same way Layer 2 sticks them to the same KV blocks.&lt;/p&gt;

&lt;p&gt;We ran a cold→warm battery across open-weight models on a third-party gateway, reading the per-request &lt;code&gt;cost&lt;/code&gt; directly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Cold&lt;/th&gt;
&lt;th&gt;Warm&lt;/th&gt;
&lt;th&gt;Discount&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-pro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.00189&lt;/td&gt;
&lt;td&gt;$0.0000155&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6.0s → 1.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.000564&lt;/td&gt;
&lt;td&gt;$0.0000116&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;97.9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.9s → 1.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.000561&lt;/td&gt;
&lt;td&gt;$0.0000853&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;84.8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10.2s → 1.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.00242&lt;/td&gt;
&lt;td&gt;$0.000469&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.2s → 1.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.00350&lt;/td&gt;
&lt;td&gt;$0.00336&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.2s → 1.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3.5-plus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.00114&lt;/td&gt;
&lt;td&gt;$0.00114&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.8s → 1.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DeepSeek-V4 hit 97–99% (affinity working end to end); &lt;code&gt;qwen3.5-plus&lt;/code&gt; and &lt;code&gt;qwen3-max&lt;/code&gt; returned ~0% on the warm call despite carrying a cache-read price in the catalog. Two more gateway lessons hide in this table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The usage field lies; the cost doesn't.&lt;/strong&gt; &lt;code&gt;cached_tokens&lt;/code&gt; read &lt;strong&gt;0&lt;/strong&gt; on &lt;em&gt;every&lt;/em&gt; call here, including the 99% cost drops. Many OpenAI-compatible gateways don't populate the cached-token field for upstreams that cache automatically. Audit by the &lt;code&gt;cost&lt;/code&gt; delta between a cold and warm call, not by the token field — the same lesson as auditing a &lt;a href="https://dev.to/blog/llm-gateway-cache-audit/"&gt;gateway's cache claims&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency improves even when cost doesn't.&lt;/strong&gt; Every warm call was 2–10× faster — &lt;code&gt;qwen3.5-flash&lt;/code&gt; went 10.2s→1.0s — including the ~0%-discount ones. A hit skips prefill regardless of how the host prices it, so caching can pay off in TTFT on a gateway that gives you nothing on the bill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A gateway that doesn't preserve affinity hands you a cache you can't reach; one that doesn't surface cache cost hands you one you can't verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 5 — The router: random distribution across providers
&lt;/h3&gt;

&lt;p&gt;At the top, a multi-provider router load-balances one model ID across &lt;em&gt;different companies'&lt;/em&gt; clusters — each with a &lt;strong&gt;separate cache&lt;/strong&gt;. Now even perfect affinity within a provider can't save you: if call 1 goes to one vendor and call 2 to another, there is no shared cache to hit. This is the scatter from the top of this post, and it compounds Layer 4 — not just multiple clusters, but multiple vendors with disjoint cache state and disjoint prices (the priciest pick billed 20× the cheapest upstream's base rate). The cache only engaged once routing happened to stick to one provider.&lt;/p&gt;

&lt;p&gt;The fix is to collapse the randomness — make routing deterministic so repeated prefixes land on the same warm cache:&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;# Pin the upstream; otherwise load-balancing scatters you across disjoint caches.
# (field names follow a common multi-provider router's API)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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;ROUTER_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&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;span class="n"&gt;json&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;model&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;qwen/qwen3.5-35b-a3b&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;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;              &lt;span class="c1"&gt;# return cost + cached_tokens
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&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="c1"&gt;# the part that makes caching work
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your-chosen-upstream&amp;gt;&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;allow_fallbacks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To its credit, the router &lt;em&gt;did&lt;/em&gt; report &lt;code&gt;cached_tokens&lt;/code&gt; (4,224 on the hit) and a per-request &lt;code&gt;cost&lt;/code&gt;, so you can verify both — better than the Layer-4 gateway that read 0. But the routing is yours to constrain. &lt;strong&gt;Caching is a routing problem dressed up as a pricing feature:&lt;/strong&gt; the cache is free at Layer 2, and Layers 3, 4, and 5 are three escalating ways to route yourself away from it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How deep is the discount? It's all over the map
&lt;/h2&gt;

&lt;p&gt;When the routing &lt;em&gt;does&lt;/em&gt; line up, how much do you save? For closed models the cache-read discount clusters near 90%. For open weights the published cache-read price ranges from a token gesture to near-total, even within one vendor's lineup. First-party published rates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model (first-party / mode)&lt;/th&gt;
&lt;th&gt;Input $/M&lt;/th&gt;
&lt;th&gt;Cache read $/M&lt;/th&gt;
&lt;th&gt;Discount&lt;/th&gt;
&lt;th&gt;Layer-2 type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-v4-flash&lt;/td&gt;
&lt;td&gt;0.14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0028&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~98%&lt;/td&gt;
&lt;td&gt;auto disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-v4-pro&lt;/td&gt;
&lt;td&gt;1.74&lt;/td&gt;
&lt;td&gt;0.145&lt;/td&gt;
&lt;td&gt;~92%&lt;/td&gt;
&lt;td&gt;auto disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen (explicit mode)&lt;/td&gt;
&lt;td&gt;base&lt;/td&gt;
&lt;td&gt;0.10× base&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;explicit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K2.6&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;0.16&lt;/td&gt;
&lt;td&gt;~83%&lt;/td&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;auto implicit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen (implicit mode)&lt;/td&gt;
&lt;td&gt;base&lt;/td&gt;
&lt;td&gt;0.20× base&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DeepSeek's automatic disk cache is the deepest in the field — &lt;code&gt;deepseek-v4-flash&lt;/code&gt; reads cached input at &lt;strong&gt;$0.0028/M against a $0.14/M miss, a 1:50 ratio&lt;/strong&gt;, which our Layer-4 test reproduced at 97.9%. &lt;strong&gt;Third-party hosts of these same open weights price cached input independently&lt;/strong&gt; — some apply a flat ~50%, others vary per model from ~50% to ~90% — so the discount you get is a function of which host you land on, not just the model. Same feature name, a 48-point spread.&lt;/p&gt;

&lt;p&gt;Because the discount is a venue property, one model carries different cache economics everywhere it's served. &lt;code&gt;deepseek-v4-pro&lt;/code&gt;, four ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where (layer)&lt;/th&gt;
&lt;th&gt;Cache-read discount&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First-party API (L3)&lt;/td&gt;
&lt;td&gt;~92% ($1.74 → $0.145)&lt;/td&gt;
&lt;td&gt;documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party host A (L3)&lt;/td&gt;
&lt;td&gt;~89% ($1.74 → $0.20)&lt;/td&gt;
&lt;td&gt;documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party host B (L3)&lt;/td&gt;
&lt;td&gt;~92% ($1.6 → $0.135)&lt;/td&gt;
&lt;td&gt;documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party gateway (L4)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;measured (cold→warm)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"DeepSeek-V4-Pro supports caching" is true and nearly useless; the operational question is "supports caching &lt;em&gt;where, at what rate, reported how&lt;/em&gt;."&lt;/p&gt;




&lt;h2&gt;
  
  
  A decision checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;The model sets the ceiling, not the cache&lt;/strong&gt; (Layer 1). Its attention architecture (MLA, GQA) decides how cheap caching &lt;em&gt;can&lt;/em&gt; be, but it never serves a cached token — so still ask where it's served and what that host's stack does.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Self-hosting? You already have it free&lt;/strong&gt; (Layer 2). Confirm automatic prefix caching is on (it is by default in vLLM/SGLang) and watch your prefix hit rate.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;On a compute host, verify delivery, not the price column&lt;/strong&gt; (Layer 3). A cache-read price is a claim; measure a cold→warm cost delta. Use a cache-key affinity hint where the host offers one.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Through a gateway, demand cache-affinity routing and cost reporting&lt;/strong&gt; (Layer 4). If identical prefixes don't stick to one upstream, or &lt;code&gt;cost&lt;/code&gt; doesn't drop on a warm call, the cache is unreachable or unverifiable.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;On a router, pin the upstream&lt;/strong&gt; (Layer 5). Constrain routing (e.g. a provider-order field with fallbacks off), or you forfeit hits to load-balancing across disjoint caches — and risk a 20–50× pricier upstream.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Weigh latency separately from cost.&lt;/strong&gt; Warm prefills are 2–10× faster even when the dollar discount is ~0.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Watch for storage-fee cache types.&lt;/strong&gt; Rented caches (Moonshot &lt;code&gt;moonshot-v1&lt;/code&gt;, Gemini explicit) bill per-token-time for an idle cache; automatic prefix caches don't.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;For closed models, "does it cache?" has one answer. For open weights the capability was solved years ago at the inference-engine layer — vLLM and SGLang cache every prefix, for free, automatically. Everything above that layer is plumbing that either preserves the hit or scatters you away from it: a compute host's replica balancer, a gateway's cluster routing, a router's random spread across vendors. The model's architecture sets the ceiling on how cheap caching can be — MLA and GQA are real, model-level wins — but the path your request takes decides what you actually get. Treat cache behavior as a &lt;strong&gt;routing property&lt;/strong&gt; — measure it in cost terms on the exact path you'll run, pin the route so the cache you warmed is the one you hit, and remember that the deepest discount in the world is worth nothing if request two lands somewhere request one never touched.&lt;/p&gt;

&lt;p&gt;For the mechanics of &lt;em&gt;why&lt;/em&gt; a KV cache exists and how TTLs work, start with &lt;a href="https://dev.to/blog/llm-prompt-caching-explained/"&gt;How KV Cache &amp;amp; TTL Work&lt;/a&gt;; to audit a gateway's cache claims, see &lt;a href="https://dev.to/blog/llm-gateway-cache-audit/"&gt;Does Your LLM Gateway Lie About Cache?&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do open-weight models support prompt caching?&lt;/strong&gt;&lt;br&gt;
The weights set how cheap caching can be — attention architectures like MLA and GQA shrink the KV cache — but the &lt;em&gt;cache itself&lt;/em&gt;, the discount, and the API come from the serving stack. Caching is implemented in the inference engine (vLLM, SGLang, TensorRT-LLM), inherited by compute hosts, and forwarded (or scattered) by gateways and routers. Ship the same checkpoint to three hosts and you can get free automatic caching, none, or explicit-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did the same model cost 49× more on one call than another?&lt;/strong&gt;&lt;br&gt;
On a multi-provider router, an un-pinned request is load-balanced across different vendors' clusters with different base prices and disjoint cache state. One call hit a pricey provider cold; another hit a cheap one warm. Pin the upstream (constrain provider order, fallbacks off) to control both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If I self-host, do I need to pay for caching?&lt;/strong&gt;&lt;br&gt;
No. Automatic prefix caching in vLLM, SGLang, and TensorRT-LLM is on by default and free — a hit just skips prefill. You pay only for the GPUs you already run, and the cache is yours, evicted by LRU when VRAM is needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API says &lt;code&gt;cached_tokens: 0&lt;/code&gt; but my bill dropped — did caching work?&lt;/strong&gt;&lt;br&gt;
Probably yes. Many gateways don't populate &lt;code&gt;cached_tokens&lt;/code&gt; for upstreams that cache automatically. Trust the &lt;code&gt;cost&lt;/code&gt; field: a large drop between a cold and an identical warm call means the cache hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which open-weight model has the deepest cache discount?&lt;/strong&gt;&lt;br&gt;
DeepSeek's automatic disk cache: &lt;code&gt;deepseek-v4-flash&lt;/code&gt; reads cached input at ~$0.0028/M against $0.14/M uncached (~98% off), reproduced at 97.9–99.2% across the V4 line in our cold→warm tests. Many third-party hosts apply a flat ~50% instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a catch with caches that charge a storage fee?&lt;/strong&gt;&lt;br&gt;
Yes — Moonshot's &lt;code&gt;moonshot-v1&lt;/code&gt; explicit cache and Gemini's explicit cache bill per-token-time to keep the cache alive (Gemini ~$1–4.50 / 1M-tokens / hour). An idle cache you forgot to delete keeps charging. Automatic prefix caches have no storage fee.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verification: live cost/latency figures measured 2026-06-14 against a multi-provider router and our own gateway, using a fixed ~4.7K-token prompt, small &lt;code&gt;max_tokens&lt;/code&gt;, sequential cold→warm runs; discounts computed from the returned per-request &lt;code&gt;cost&lt;/code&gt;. Documented pricing and cache mechanics checked against primary provider docs the same day and cross-verified adversarially; a few vendor figures (notably Moonshot's explicit-cache fees) move frequently — confirm current values before quoting. Your numbers will vary with provider, prompt, region, and load.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api-docs.deepseek.com/quick_start/pricing" rel="noopener noreferrer"&gt;DeepSeek — Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api-docs.deepseek.com/guides/kv_cache" rel="noopener noreferrer"&gt;DeepSeek — KV cache / Context Caching guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2412.19437" rel="noopener noreferrer"&gt;DeepSeek-V3 Technical Report — MLA (KV-cache compression)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2305.13245" rel="noopener noreferrer"&gt;GQA: Training Generalized Multi-Query Transformer Models (Ainslie et al.)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.alibabacloud.com/help/en/model-studio/" rel="noopener noreferrer"&gt;Alibaba Cloud Model Studio — context cache &amp;amp; pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.moonshot.ai/docs/guide/use-context-caching" rel="noopener noreferrer"&gt;Moonshot AI — Context Caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.z.ai/" rel="noopener noreferrer"&gt;Zhipu / Z.AI — pricing &amp;amp; caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.vllm.ai/en/latest/features/automatic_prefix_caching.html" rel="noopener noreferrer"&gt;vLLM — Automatic Prefix Caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.sglang.ai/" rel="noopener noreferrer"&gt;SGLang — RadixAttention / cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/LMCache/LMCache" rel="noopener noreferrer"&gt;LMCache — KV cache offloading &amp;amp; sharing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/gemini-api/docs/caching" rel="noopener noreferrer"&gt;Google — Gemini context caching&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;All checked 2026-06-14. Not financial advice; verify current pricing before relying on it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>llm</category>
    </item>
    <item>
      <title>Claude Fable 5's 30-Day Retention: ZDR, HIPAA, COPPA</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Fri, 12 Jun 2026 06:54:46 +0000</pubDate>
      <link>https://dev.to/synthorai/claude-fable-5s-30-day-retention-zdr-hipaa-coppa-3d5d</link>
      <guid>https://dev.to/synthorai/claude-fable-5s-30-day-retention-zdr-hipaa-coppa-3d5d</guid>
      <description>&lt;p&gt;If your organization runs Claude under a zero-data-retention (ZDR) agreement, your first request to &lt;code&gt;claude-fable-5&lt;/code&gt; didn't return a completion. It returned &lt;code&gt;400 invalid_request_error&lt;/code&gt;. That's not an outage — it's policy. Fable 5 is the first generally available Claude model that &lt;strong&gt;cannot be used without 30-day data retention&lt;/strong&gt;, and the requirement follows the model onto every platform: the Claude API, AWS Bedrock, Google Vertex AI, and Microsoft Foundry each gate it behind an explicit retention opt-in.&lt;/p&gt;

&lt;p&gt;For teams that treated "we don't retain your data" as a settled property of their LLM stack, this is an architectural event. This post covers what the policy says, why the window exists, how each cloud implements it, and what it changes for consumer products and sensitive-data industries.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Policy details were checked against Anthropic's, AWS's, Google's, and Microsoft's published documentation on 2026-06-12. Policies change; verify against the linked primary sources and your own contracts. This is an engineering overview, not legal advice.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What the policy actually says
&lt;/h2&gt;

&lt;p&gt;Anthropic designates Claude Fable 5 and Claude Mythos 5 as &lt;a href="https://support.claude.com/en/articles/15425695" rel="noopener noreferrer"&gt;Covered Models&lt;/a&gt;. Per the &lt;a href="https://platform.claude.com/docs/en/manage-claude/api-and-data-retention" rel="noopener noreferrer"&gt;API data retention docs&lt;/a&gt; and the &lt;a href="https://support.claude.com/en/articles/15425996-data-retention-practices-for-mythos-class-models" rel="noopener noreferrer"&gt;Mythos-class retention practices article&lt;/a&gt; (effective 2026-06-09):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompts and completions are retained for 30 days&lt;/strong&gt;, then automatically deleted — unless flagged for an active safety investigation or required by law.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no opt-out.&lt;/strong&gt; Retention is a condition of using the model. A request from an organization whose retention configuration doesn't meet the requirement returns &lt;code&gt;400 invalid_request_error&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access is narrow by design.&lt;/strong&gt; Automated safety systems screen the data; only a small group of approved personnel can review flagged conversations, they cannot export, copy, or download it, and every access lands in tamper-proof logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing ZDR agreements do not carry over&lt;/strong&gt; to Covered Model traffic — including through cloud platforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consumer plans (Claude Free/Pro/Max) are unaffected — they already operate under their own retention terms. This policy targets the commercial API surface, exactly where "we never retain" promises tend to live.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a 30-day window exists
&lt;/h2&gt;

&lt;p&gt;The rationale in the &lt;a href="https://support.claude.com/en/articles/15425695" rel="noopener noreferrer"&gt;Covered Models article&lt;/a&gt; is specific: these models have substantially advanced capabilities in software engineering, agentic workflows, and cybersecurity, and &lt;strong&gt;"some forms of misuse only become detectable across many requests."&lt;/strong&gt; The cited examples — best-of-N jailbreaking, state-sponsored espionage — are attack patterns where each prompt looks benign and only the sequence is diagnostic. You can't detect a sequence you've deleted.&lt;/p&gt;

&lt;p&gt;Two things the window is &lt;strong&gt;not&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not training data.&lt;/strong&gt; Anthropic states retained data is never used for training without express permission. The purpose is abuse detection, full stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not new in kind — new in enforceability.&lt;/strong&gt; A ~30-day abuse-monitoring window has been the industry default for years: &lt;a href="https://openai.com/enterprise-privacy/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt; keeps API abuse logs up to 30 days (ZDR by approval); &lt;a href="https://learn.microsoft.com/en-us/answers/questions/2156579/azure-openai-data-management-and-abuse-monitoring" rel="noopener noreferrer"&gt;Azure OpenAI&lt;/a&gt; stores prompts up to 30 days unless approved for modified abuse monitoring. What changed is that the window became &lt;strong&gt;non-negotiable for one model class&lt;/strong&gt; — previously every provider offered a zero-retention escape hatch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One pre-existing caveat that surprises people: even under ZDR, Anthropic retains safety classifier results, and content flagged for Usage Policy violations can be kept &lt;strong&gt;up to 2 years&lt;/strong&gt;. Zero data retention has never meant zero data — it means zero retention of unflagged content in the normal path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Same requirement, three clouds, three mechanisms
&lt;/h2&gt;

&lt;p&gt;The retention applies wherever the model runs, but each platform wires the opt-in differently — and the differences decide who processes your data and where your controls live.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Opt-in mechanism&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Without opt-in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude API&lt;/td&gt;
&lt;td&gt;30-day retention in Privacy controls&lt;/td&gt;
&lt;td&gt;Organization or workspace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;400 invalid_request_error&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Bedrock&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data_retention_mode: provider_data_share&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Account or project&lt;/td&gt;
&lt;td&gt;Model listed &lt;code&gt;unavailable&lt;/code&gt;; requests blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Vertex AI&lt;/td&gt;
&lt;td&gt;Anthropic data sharing + Model Garden terms&lt;/td&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;Requests blocked until enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Foundry&lt;/td&gt;
&lt;td&gt;Anthropic's terms accepted at deployment&lt;/td&gt;
&lt;td&gt;Subscription/deployment&lt;/td&gt;
&lt;td&gt;Not covered by Azure's ZDR program at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;AWS Bedrock&lt;/strong&gt; is the most explicit. &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/data-retention.html" rel="noopener noreferrer"&gt;Data retention is a configurable mode&lt;/a&gt; (&lt;code&gt;default&lt;/code&gt; / &lt;code&gt;provider_data_share&lt;/code&gt; / &lt;code&gt;none&lt;/code&gt;), resolved project → account → model default. Fable 5 declares &lt;code&gt;allowed_modes: ["provider_data_share"]&lt;/code&gt;: prompts and completions are shared with Anthropic and retained up to 30 days. Under any other mode:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anthropic.claude-fable-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This model is not available under data retention mode 'default'."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data_retention"&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowed_modes"&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="s2"&gt;"provider_data_share"&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="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;Nothing changed for pre-Fable-5 models, and an SCP on the &lt;code&gt;bedrock:DataRetentionMode&lt;/code&gt; condition key can enforce your posture org-wide — nobody quietly flips the account to try the new model. Note: with cross-region inference, the retained copy lives in the &lt;em&gt;destination&lt;/em&gt; region, which matters if you carry residency commitments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Vertex AI&lt;/strong&gt; gates the model behind a project-level Anthropic data-sharing setting (&lt;code&gt;setPublisherModelConfig&lt;/code&gt; with &lt;code&gt;dataSharingEnabledProvider: "anthropic"&lt;/code&gt;) plus terms acceptance in Model Garden, per &lt;a href="https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/partner-models/claude/fable-5" rel="noopener noreferrer"&gt;Google's Fable 5 documentation&lt;/a&gt;. General data handling follows &lt;a href="https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance" rel="noopener noreferrer"&gt;Vertex AI's data-governance policy&lt;/a&gt;; for residency-sensitive workloads, Vertex's regional and multi-region endpoints control where inference runs — which now includes where the retained copy lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft Foundry&lt;/strong&gt; is structurally different. Microsoft's &lt;a href="https://learn.microsoft.com/en-us/azure/foundry/responsible-ai/claude-models/data-privacy" rel="noopener noreferrer"&gt;data and privacy documentation&lt;/a&gt; is explicit that Claude models are third-party marketplace services: you accept Anthropic's terms at deployment, and &lt;strong&gt;Anthropic — not Microsoft — is the data processor&lt;/strong&gt;. Azure OpenAI's ZDR and modified-abuse-monitoring programs don't extend to Claude deployments. Organizations with ZDR postures elsewhere typically isolate Covered Model use in a dedicated subscription, making the retention boundary structural rather than procedural.&lt;/p&gt;

&lt;p&gt;The pattern across all three: &lt;strong&gt;retention class became a first-class, machine-readable model attribute&lt;/strong&gt; — a mode, a flag, a terms gate — rather than a paragraph in a contract. Your infrastructure can now enforce your data posture, and it should.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it means for enterprise deployments
&lt;/h2&gt;

&lt;p&gt;With no ZDR agreement, nothing changes mechanically — you were already in a 30-day-style posture, possibly without realizing it. The work is making it &lt;em&gt;explicit&lt;/em&gt; in your vendor documentation.&lt;/p&gt;

&lt;p&gt;With a ZDR agreement, you have a three-way choice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Skip Covered Models.&lt;/strong&gt; ZDR stays uniform; you give up the model. Viable if your workloads don't need it — see our &lt;a href="https://dev.to/blog/claude-fable-5-prompt-caching/"&gt;measured Fable 5 evaluation&lt;/a&gt; for what it costs and where it differs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split by workspace or project.&lt;/strong&gt; Every platform supports a scoped opt-in: a designated Claude API workspace (Console → Settings → Workspaces → Privacy controls), a Bedrock project with &lt;code&gt;provider_data_share&lt;/code&gt;, a separate Vertex project or Azure subscription. Route only retention-tolerant workloads there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept retention org-wide.&lt;/strong&gt; Simplest to operate, but it silently downgrades the guarantee for &lt;em&gt;every&lt;/em&gt; workload — including the ones whose sensitivity justified ZDR. That's a decision for your data-protection owner, not a config change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Regardless of provider: &lt;strong&gt;your own logging is a second retention surface.&lt;/strong&gt; If your gateway or observability stack logs full prompts, you're running a longer window than your provider, under your own roof. Provider guarantees are only as meaningful as the layer in front of them — the same audit logic we applied to &lt;a href="https://dev.to/blog/llm-gateway-cache-audit/"&gt;cache claims&lt;/a&gt; applies here.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it means for consumer-facing products
&lt;/h2&gt;

&lt;p&gt;If you serve consumers and route their content through a Covered Model, the change propagates into your own legal surface — ZDR agreement or not. Three concrete consequences:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Your privacy notice probably needs an update.&lt;/strong&gt; Most regimes require disclosing retention, not just collection: GDPR Article 13(2)(a) requires the storage period (or criteria) at collection time; California's CPRA requires the notice at collection to state retention per category of personal information. If your notice says — or implies — that conversation data isn't retained anywhere, a processor holding a 30-day copy makes it wrong. Update the notice, the records of processing, and the DPA inventory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You cannot offer users an opt-out you don't have.&lt;/strong&gt; The retention has no exception mechanism, so there is no toggle you can build that exempts a user's prompts &lt;em&gt;while still using that model&lt;/em&gt;. The lever you actually hold is &lt;strong&gt;routing&lt;/strong&gt;: a consent-aware gateway sends users who decline data sharing to ZDR-eligible models and everyone else to the Covered Model — a legal constraint turned into an ordinary routing rule. Far better than a preference checkbox that does nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deletion requests need accurate plumbing.&lt;/strong&gt; Erasure obligations (GDPR Art. 17, CPRA deletion, and their equivalents) extend to processors. A bounded window that auto-deletes within 30 days is generally a defensible processor posture — but your DSAR playbook should say that, not promise immediate downstream deletion you can't execute.&lt;/p&gt;

&lt;p&gt;The global dimension compounds this: the same disclosure-and-processor logic appears in the UK GDPR, Brazil's LGPD, and the spreading family of US state privacy laws. For users in China, PIPL adds two sharper edges — providing personal information to another processor generally requires separate consent, and routing Chinese users' content to an overseas LLM endpoint is a cross-border transfer needing a recognized mechanism (security assessment, standard contract, or certification). A model upgrade that changes who retains what, where, for how long is exactly the change these frameworks expect you to re-paper.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sensitive-data industries: where 30 days bites hardest
&lt;/h2&gt;

&lt;p&gt;For most products the provider's window is a documentation problem. For industries whose data is itself regulated, it's an architecture problem: the retained copy is regulated data at rest at a vendor, and sector rules govern exactly that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Healthcare (HIPAA)
&lt;/h3&gt;

&lt;p&gt;HIPAA doesn't require zero retention — it requires that any vendor holding protected health information does so &lt;strong&gt;under a Business Associate Agreement (BAA)&lt;/strong&gt; with appropriate safeguards. The 30-day copy of your prompts is PHI at rest at a business associate; the question is whether your BAA covers it. The two major API vendors structure this differently, and the difference now matters: &lt;a href="https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#hipaa-readiness" rel="noopener noreferrer"&gt;Anthropic's HIPAA-ready API access&lt;/a&gt; explicitly &lt;em&gt;doesn't&lt;/em&gt; require ZDR — it's built on retention-with-safeguards (encryption, access controls, audit logging, enforced feature restrictions). &lt;a href="https://help.openai.com/en/articles/8660679-how-can-i-get-a-business-associate-agreement-baa-with-openai" rel="noopener noreferrer"&gt;OpenAI's API BAA&lt;/a&gt; covers endpoints eligible for zero data retention — and a BAA scoped to ZDR endpoints structurally cannot cover a model that mandates retention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A model's retention class is now a BAA-eligibility question.&lt;/strong&gt; Confirm in writing that your BAA covers the specific model before routing PHI to it — and remember the chain shifts on clouds: on Bedrock the platform is your business associate; on Foundry, Anthropic processes the data directly. One sharp edge: PHI must never appear in JSON schema definitions for structured outputs — cached schemas don't get the same protections as message content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Children's products (COPPA)
&lt;/h3&gt;

&lt;p&gt;The timing is awkward: the FTC's &lt;a href="https://www.ftc.gov/news-events/news/press-releases/2025/01/ftc-finalizes-changes-childrens-privacy-rule-limiting-companies-ability-monetize-kids-data" rel="noopener noreferrer"&gt;amended COPPA Rule&lt;/a&gt; took effect June 23, 2025, with compliance on most provisions due April 22, 2026 — the first model with mandatory provider-side retention arrived just as operators finished implementing the new retention obligations. Two of those interact directly with the 30-day window: a &lt;strong&gt;written, public data retention policy&lt;/strong&gt; is now mandatory (§312.10) — what children's data is collected, why, and when it's deleted — and &lt;strong&gt;indefinite retention is prohibited&lt;/strong&gt;, with retention limited to what's reasonably necessary for the collected purpose.&lt;/p&gt;

&lt;p&gt;A bounded 30-day window with automatic deletion is the &lt;em&gt;compatible&lt;/em&gt; shape — but the provider retains for &lt;em&gt;its&lt;/em&gt; trust-and-safety purpose, not the purpose you collected the child's data for, and your notice must describe the processor relationship accurately. For child-directed products that adopted ZDR specifically to minimize the data trail, the routing answer applies with higher stakes: children's traffic stays on ZDR-eligible models, or the Covered Model window goes into your §312.10 policy first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same pattern, other sectors
&lt;/h3&gt;

&lt;p&gt;Once you see the structure — &lt;em&gt;regulated data, retained copy at a vendor, sector rule governing retention&lt;/em&gt; — it recurs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Biometrics (Illinois BIPA):&lt;/strong&gt; operators need a written, publicly available retention schedule and destruction guidelines for biometric data. A provider's 30-day copy of prompts containing biometric identifiers belongs in that schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments (PCI DSS / GLBA):&lt;/strong&gt; PCI DSS prohibits storing sensitive authentication data after authorization — anywhere. Card data pasted into a prompt becomes card data retained at a provider for 30 days. The clean answer is upstream redaction, not downstream paperwork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Education (FERPA):&lt;/strong&gt; vendors handling student records under the school-official exception must remain under the school's &lt;em&gt;direct control&lt;/em&gt;. A safety-retention copy the school cannot access or delete early sits uneasily with that standard — a question for counsel before EdTech traffic hits a Covered Model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial services — the inversion (SEC/FINRA):&lt;/strong&gt; broker-dealers must &lt;em&gt;retain&lt;/em&gt; business communications under books-and-records rules. For them the provider's window isn't the problem; capturing their own compliant copy is. Same retention question, opposite sign.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread: &lt;strong&gt;sector rules regulate retention in both directions&lt;/strong&gt;, and a provider-side window you don't control must be mapped into whichever direction your sector points.&lt;/p&gt;




&lt;h2&gt;
  
  
  A decision checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Inventory which models your traffic actually touches.&lt;/strong&gt; Retention class is now a per-model attribute, not a per-provider one.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;If you have ZDR: decide deliberately&lt;/strong&gt; — skip Covered Models, split by workspace/project/subscription, or accept retention org-wide. Don't let it happen implicitly.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Enforce the posture in infrastructure&lt;/strong&gt; — Bedrock SCPs, workspace privacy controls, separate cloud projects — not in a wiki page.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;B2C: update privacy notices and DSAR playbooks&lt;/strong&gt;; route non-consenting users to ZDR-eligible models instead of building opt-outs that can't work.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Regulated data: confirm coverage per model, in writing&lt;/strong&gt; — BAA for PHI, §312.10 policy for children's data, retention schedules for biometrics — before routing that data to a retention-required model.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Audit your own logging.&lt;/strong&gt; A provider's 30-day window is irrelevant if your gateway logs prompts indefinitely.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;The 30-day window attached to Fable 5 is not a data grab — it's bounded, purpose-limited abuse monitoring, consistent with what most of the industry already does by default, made mandatory for one model class because cross-request misuse detection doesn't work on deleted data. For most teams the engineering impact is zero and the governance impact is a paragraph in a vendor review.&lt;/p&gt;

&lt;p&gt;But for organizations whose compliance position assumed zero retention — ZDR-scoped BAAs, privacy notices that say nothing persists, children's products built on data minimization — Fable 5 is the moment that assumption stopped being uniform across models. The fix isn't avoiding the model. It's making retention class an explicit, per-model input to routing decisions, the same way you already treat price and context window.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use Claude Fable 5 under a zero-data-retention agreement?&lt;/strong&gt;&lt;br&gt;
No. Fable 5 and Mythos 5 are Covered Models requiring 30-day retention; ZDR organizations get a &lt;code&gt;400 invalid_request_error&lt;/code&gt; unless they enable 30-day retention for a workspace and route Fable 5 traffic through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does going through AWS Bedrock, Vertex AI, or Microsoft Foundry avoid the requirement?&lt;/strong&gt;&lt;br&gt;
No. Each platform gates the model behind its own retention opt-in: &lt;code&gt;provider_data_share&lt;/code&gt; on Bedrock, Anthropic data sharing plus Model Garden terms on Vertex, Anthropic's terms at deployment on Foundry (where Anthropic, not Microsoft, is the data processor). Existing ZDR arrangements don't carry over on any of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can my end users opt out of the retention?&lt;/strong&gt;&lt;br&gt;
No — there is no opt-out mechanism. The lever you hold is routing: send users who decline data sharing to ZDR-eligible models. Don't ship a preference toggle that doesn't change anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the retained data used to train models?&lt;/strong&gt;&lt;br&gt;
Anthropic states retained data is never used for training without express permission. The purpose is trust-and-safety review: automated screening, with flagged conversations reviewable only by approved personnel who cannot export the data, under tamper-proof access logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the 30-day retention change how prompt caching works?&lt;/strong&gt;&lt;br&gt;
No. Cache entries follow their own short TTLs (5 minutes or 1 hour) and the caching contract on Fable 5 is unchanged — see our &lt;a href="https://dev.to/blog/claude-fable-5-prompt-caching/"&gt;measured evaluation&lt;/a&gt;. The 30-day window is a separate, parallel retention for safety review.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/manage-claude/api-and-data-retention" rel="noopener noreferrer"&gt;Anthropic — API and data retention&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.claude.com/en/articles/15425695" rel="noopener noreferrer"&gt;Anthropic — Covered Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.claude.com/en/articles/15425996-data-retention-practices-for-mythos-class-models" rel="noopener noreferrer"&gt;Anthropic — Data retention practices for Mythos-class models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/data-retention.html" rel="noopener noreferrer"&gt;AWS — Amazon Bedrock data retention&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/partner-models/claude/fable-5" rel="noopener noreferrer"&gt;Google Cloud — Claude Fable 5 (partner models)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance" rel="noopener noreferrer"&gt;Google Cloud — Vertex AI data governance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/foundry/responsible-ai/claude-models/data-privacy" rel="noopener noreferrer"&gt;Microsoft — Claude in Foundry: data, privacy, and security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.com/enterprise-privacy/" rel="noopener noreferrer"&gt;OpenAI — Enterprise privacy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.openai.com/en/articles/8660679-how-can-i-get-a-business-associate-agreement-baa-with-openai" rel="noopener noreferrer"&gt;OpenAI — BAA for API services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ftc.gov/news-events/news/press-releases/2025/01/ftc-finalizes-changes-childrens-privacy-rule-limiting-companies-ability-monetize-kids-data" rel="noopener noreferrer"&gt;FTC — COPPA Rule amendments (press release)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.federalregister.gov/documents/2025/04/22/2025-05904/childrens-online-privacy-protection-rule" rel="noopener noreferrer"&gt;Federal Register — Children's Online Privacy Protection Rule&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;All checked 2026-06-12. Policies change — verify against current documents and your own contracts. Not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>fable5</category>
    </item>
    <item>
      <title>Claude Fable 5: Caching, Tokenizer &amp; Cost vs Opus 4.6</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Thu, 11 Jun 2026 15:49:46 +0000</pubDate>
      <link>https://dev.to/synthorai/claude-fable-5-caching-tokenizer-cost-vs-opus-46-43ce</link>
      <guid>https://dev.to/synthorai/claude-fable-5-caching-tokenizer-cost-vs-opus-46-43ce</guid>
      <description>&lt;p&gt;&lt;code&gt;claude-fable-5&lt;/code&gt; is now available on the Synthorai gateway. If you cache against the Claude line, the good news is that the caching and TTL contract is a carry-over: same &lt;code&gt;cache_control&lt;/code&gt; markers, same 5-minute and 1-hour TTLs, same write premiums, same deep read discount. Your caching code moves over by changing one string.&lt;/p&gt;

&lt;p&gt;The thing to budget for isn't the cache mechanics — it's the bill. Fable 5 lists at &lt;strong&gt;2x the Opus token price&lt;/strong&gt;, and it tokenizes the same English text into &lt;strong&gt;~45% more tokens than Opus 4.6&lt;/strong&gt; (it's on the post-4.6 tokenizer, identical to Opus 4.8). Those two multipliers stack. This post measures all of it so you don't have to.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All numbers below were measured against &lt;code&gt;https://synthorai.io/&lt;/code&gt; (Anthropic-native &lt;code&gt;/v1/messages&lt;/code&gt;) on 2026-06-10 with a stable ~6.6–9.6K-token English system prompt, &lt;code&gt;max_tokens&lt;/code&gt; small, single sequential run. Cost figures are read from the gateway &lt;code&gt;usage.cost&lt;/code&gt; field; &lt;strong&gt;ratios&lt;/strong&gt; (token counts, write premium, read discount, cross-model cost) are the portable part — absolute dollars scale with your prompt. Reproduce against your own prompt before quoting them.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;



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

&lt;span class="n"&gt;anth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SYNTHORAI_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://synthorai.io/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# SDK appends /v1/messages
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-fable-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# the only line that changes
&lt;/span&gt;    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&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;type&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;text&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;ephemeral&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;messages&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}],&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="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# input_tokens, cache_creation_input_tokens, cache_read_input_tokens, cost
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;claude-opus-4-6&lt;/code&gt; → &lt;code&gt;claude-fable-5&lt;/code&gt; and nothing in your caching path needs to move. Fable 5 is an Anthropic-native model with a 1M-token context window. One behavioral note: it is a reasoning model and &lt;strong&gt;emits thinking tokens by default&lt;/strong&gt; — even a trivial "reply OK" returned &lt;code&gt;output_tokens_details.thinking_tokens &amp;gt; 0&lt;/code&gt; in our runs, where Opus 4.6/4.8 returned zero. Budget output tokens accordingly. The mechanics behind &lt;code&gt;cache_control&lt;/code&gt; are covered in &lt;a href="https://dev.to/blog/prompt-caching-tutorial-code-examples/"&gt;the caching tutorial&lt;/a&gt;; the architecture of &lt;em&gt;why&lt;/em&gt; the cache exists is in &lt;a href="https://dev.to/blog/llm-prompt-caching-explained/"&gt;Part 1 of the series&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The headline: Fable 5 is on the new tokenizer
&lt;/h2&gt;

&lt;p&gt;The token count for the Opus line jumped at the 4.7 generation: the same English text that counted as ~6.6K tokens on 4.6 counts as ~9.6K on 4.8. &lt;strong&gt;Fable 5 lands on the new side&lt;/strong&gt; — identical text reports the exact same token count as Opus 4.8.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input tokens (identical text)&lt;/th&gt;
&lt;th&gt;Tokenizer generation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6,614&lt;/td&gt;
&lt;td&gt;pre-4.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9,619&lt;/td&gt;
&lt;td&gt;post-4.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-fable-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9,619&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;post-4.7 (identical to 4.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same system prompt is &lt;strong&gt;~45% more tokens on Fable 5 than on Opus 4.6&lt;/strong&gt; (9,619 / 6,614 = 1.45). This is the single most important number to internalize before you migrate, because every downstream figure — cost, the 1,024-token cache-eligibility floor, your per-call budget — is computed in tokens.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We're describing a measured observation — identical text, identical token count on Fable 5 and Opus 4.8, ~45% above Opus 4.6 — most consistent with the tokenizer/vocabulary update that shipped at the 4.7 generation. If you're coming from 4.6 or earlier, re-measure; if you're coming from 4.7/4.8, expect parity.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Caching behavior: the contract is unchanged
&lt;/h2&gt;

&lt;p&gt;We ran the same no-cache / cold-write / warm-read sequence on each model. The discount structure is identical end to end — Fable 5 honors &lt;code&gt;cache_control&lt;/code&gt; and reports the same usage fields (&lt;code&gt;cache_creation_input_tokens&lt;/code&gt;, &lt;code&gt;cache_read_input_tokens&lt;/code&gt;, and the &lt;code&gt;ephemeral_5m&lt;/code&gt; / &lt;code&gt;ephemeral_1h&lt;/code&gt; buckets).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;5m cache write&lt;/th&gt;
&lt;th&gt;1h cache write&lt;/th&gt;
&lt;th&gt;Warm read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.25x&lt;/td&gt;
&lt;td&gt;2.00x&lt;/td&gt;
&lt;td&gt;~9% of no-cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.25x&lt;/td&gt;
&lt;td&gt;2.00x&lt;/td&gt;
&lt;td&gt;~6% of no-cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-fable-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.24x&lt;/td&gt;
&lt;td&gt;1.99x&lt;/td&gt;
&lt;td&gt;~6% of no-cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two invariants hold across all three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write premium ≈ 1.25x (5m), ≈ 2x (1h).&lt;/strong&gt; The first (cold) call costs ~1.25x the no-cache price to populate a 5-minute entry, or ~2x for a 1-hour entry. Break-even is one hit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read discount ≈ 90%+.&lt;/strong&gt; A warm cache read on Fable 5 cost ~6% of the no-cache call — a ~94% discount, in line with (slightly better than) Anthropic's documented ~90% cached-read economics. Reads stay deeply discounted regardless of TTL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The percentages are flat across the line. As with the Opus 4.7 → 4.8 step, the higher &lt;em&gt;absolute&lt;/em&gt; bill on Fable 5 is a price-and-token story, not a cache-economics story — covered next.&lt;/p&gt;




&lt;h2&gt;
  
  
  TTL behavior: both windows honored
&lt;/h2&gt;

&lt;p&gt;Fable 5 supports the same two TTLs as the rest of the line: a 5-minute sliding default and an opt-in 1-hour window. We isolated each TTL with a unique prefix per call (so no stale entry could contaminate the result) and confirmed the usage object reports the correct bucket — &lt;code&gt;cache_creation.ephemeral_5m_input_tokens&lt;/code&gt; or &lt;code&gt;ephemeral_1h_input_tokens&lt;/code&gt;.&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;# 1-hour TTL — same marker syntax on Fable 5 as on the Opus line
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;ephemeral&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;ttl&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;1h&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;p&gt;The 1-hour write costs ~2x no-cache (vs ~1.25x for the 5-minute write), and reads stay at the deep discount regardless of TTL — identical to Opus 4.6/4.8. If you picked &lt;code&gt;5m&lt;/code&gt; for live chat and &lt;code&gt;1h&lt;/code&gt; for agents with human-in-the-loop pauses on Opus, keep those choices on Fable 5.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cost story: 2x price x 1.45x tokens
&lt;/h2&gt;

&lt;p&gt;Here is where Fable 5 actually differs. Two things push the bill up, and they multiply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. List price is 2x the Opus tier.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input ($/M)&lt;/th&gt;
&lt;th&gt;Output ($/M)&lt;/th&gt;
&lt;th&gt;Cache read ($/M)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude-opus-4-6&lt;/code&gt; / &lt;code&gt;4-8&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-fable-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. The same text is ~45% more tokens than on 4.6&lt;/strong&gt; (the tokenizer shift above).&lt;/p&gt;

&lt;p&gt;Multiply them and the same English prompt costs materially more. Measured against the identical system prompt on each model (gateway &lt;code&gt;usage.cost&lt;/code&gt;, same single run):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Token ratio&lt;/th&gt;
&lt;th&gt;Price ratio&lt;/th&gt;
&lt;th&gt;Same-prompt cost ratio (measured)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5 vs &lt;strong&gt;Opus 4.8&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;td&gt;2.0x&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.0x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5 vs &lt;strong&gt;Opus 4.6&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;1.45x&lt;/td&gt;
&lt;td&gt;2.0x&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.9x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So against Opus 4.8 (same tokenizer), Fable 5 is a clean &lt;strong&gt;2x&lt;/strong&gt; — pure price premium. Against Opus 4.6, the tokenizer change compounds the price change into roughly &lt;strong&gt;2.9x&lt;/strong&gt; the cost for the same prompt. Your cache &lt;em&gt;discount&lt;/em&gt; is unchanged, but the absolute base it applies to is ~2.9x larger than it was on 4.6. If you sized a per-call budget against 4.6, re-do it.&lt;/p&gt;

&lt;p&gt;A practical consequence: &lt;strong&gt;re-check the 1,024-token cache-eligibility floor.&lt;/strong&gt; Anthropic only caches prefixes at or above a minimum size. A prompt that sat just under the floor on 4.6 (in old-tokenizer tokens) may clear it on Fable 5 (~45% more tokens) — and vice versa for size estimates built on the old count. Always read &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; / &lt;code&gt;cache_read_input_tokens&lt;/code&gt; from the live response rather than estimating from a local tokenizer that may not match.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration checklist (Opus → Fable 5)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Caching code carries over verbatim.&lt;/strong&gt; &lt;code&gt;cache_control&lt;/code&gt; markers, breakpoint count (up to 4), &lt;code&gt;ttl: "1h"&lt;/code&gt;, usage-field names — all identical.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;TTL choices carry over.&lt;/strong&gt; 5m for live/session workloads, 1h for bursty/agent-with-pauses.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Discount economics carry over.&lt;/strong&gt; ~90%+ read, ~1.25x write (5m), ~2x write (1h).&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Re-budget on absolute cost.&lt;/strong&gt; Fable 5 is ~2x Opus per token, and ~2.9x the same-prompt cost vs Opus 4.6. The discount percentage is unchanged; the base it applies to is not.&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Re-measure token counts&lt;/strong&gt; if coming from 4.6 or earlier (expect ~45% more for the same text). From 4.7/4.8, expect parity.&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Account for default thinking tokens.&lt;/strong&gt; Fable 5 emits reasoning tokens by default — they bill at the output rate ($50/M). Cap or disable thinking if you don't need it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;For a team already caching against Claude, &lt;code&gt;claude-fable-5&lt;/code&gt; is an easy &lt;em&gt;integration&lt;/em&gt;: the entire caching and TTL surface is stable, so there's nothing to relearn and no code to rewrite. It is not an easy &lt;em&gt;budget&lt;/em&gt; swap from Opus 4.6 — between the 2x token price and the ~45% tokenizer inflation, the same prompt runs ~2.9x the cost. Confirm your numbers against the live &lt;code&gt;usage&lt;/code&gt; object, decide whether you need the default thinking tokens, and size the cache breakpoints against the new token counts.&lt;/p&gt;

&lt;p&gt;For the full caching playbook — prompt structure, hit-rate debugging, TTL-aware patterns — see the four-part series starting with &lt;a href="https://dev.to/blog/llm-prompt-caching-explained/"&gt;How KV Cache &amp;amp; TTL Work&lt;/a&gt; and the &lt;a href="https://dev.to/blog/prompt-caching-tutorial-code-examples/"&gt;working Python tutorial&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need to change my &lt;code&gt;cache_control&lt;/code&gt; code to use Fable 5?&lt;/strong&gt;&lt;br&gt;
No. The marker syntax, breakpoint limit, and TTL options are identical to the Opus line. Change the &lt;code&gt;model&lt;/code&gt; field and nothing else in the caching path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did the cache read discount change on Fable 5?&lt;/strong&gt;&lt;br&gt;
No. A warm read is a small single-digit fraction of the no-cache input price (~90%+ off) — we measured ~94% on Fable 5, consistent with Anthropic's documented cached-read economics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Fable 5 support the 1-hour TTL?&lt;/strong&gt;&lt;br&gt;
Yes. &lt;code&gt;{"type": "ephemeral", "ttl": "1h"}&lt;/code&gt; works exactly as on Opus. The 1-hour write costs ~2x no-cache; the 5-minute write ~1.25x. Reads stay deeply discounted on both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is the same prompt so much more expensive on Fable 5 than on Opus 4.6?&lt;/strong&gt;&lt;br&gt;
Two stacked multipliers: Fable 5 lists at 2x the per-token price, and the same English text counts as ~45% more tokens (it uses the post-4.6 tokenizer). Together that's ~2.9x the cost for an identical prompt. The cache &lt;em&gt;discount&lt;/em&gt; is unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Fable 5 a drop-in replacement for Opus 4.8?&lt;/strong&gt;&lt;br&gt;
On the caching/TTL surface and token counts, yes — token counts are identical, so the only delta is the 2x price and Fable 5's default thinking tokens. We don't publish capability benchmarks we haven't run; for quality and reasoning claims, see Anthropic's model card.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verification: all token-count, cost, write-premium, and read-discount figures measured against &lt;code&gt;https://synthorai.io/&lt;/code&gt; on 2026-06-10 using the official &lt;code&gt;anthropic&lt;/code&gt; SDK, single tenant, single sequential run. Cost is read from the gateway &lt;code&gt;usage.cost&lt;/code&gt; field; cross-model and premium/discount ratios are computed from those measured costs and are independent of any account-level promotion. Discount/premium ratios cross-checked against &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Anthropic Prompt Caching docs&lt;/a&gt;. Warm-read latency (TTFT) was dominated by network jitter in our run and is omitted as unreliable. Your numbers will vary with prompt, region, and load.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>llm</category>
      <category>claude</category>
    </item>
    <item>
      <title>Provider Drift: How Default Routing Inflates LLM Cost 3.9 — A Measurement</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Fri, 05 Jun 2026 09:34:34 +0000</pubDate>
      <link>https://dev.to/synthorai/provider-drift-how-default-routing-inflates-llm-cost-39x-a-measurement-2017</link>
      <guid>https://dev.to/synthorai/provider-drift-how-default-routing-inflates-llm-cost-39x-a-measurement-2017</guid>
      <description>&lt;p&gt;You turned on prompt caching, the hit counter ticks now and then, but your bill barely moved. Before blaming your prompt structure, look at something the dashboard hides: which upstream actually served each request.&lt;/p&gt;

&lt;p&gt;Multi-provider gateways spread a single model across several upstream providers and pick one per request. Prompt caches are per-provider (often per-node inside a provider). So when your second identical request lands on a different upstream than the first, it is a cache miss, even though your prompt did not change one byte. This is &lt;strong&gt;provider drift&lt;/strong&gt;, and on a pay-per-token model it quietly multiplies your cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two conditions that trigger it
&lt;/h2&gt;

&lt;p&gt;This is not a misconfiguration you opted into. It is what you get out of the box:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Default auto routing.&lt;/strong&gt; The request is sent to the model without pinning an upstream, so the gateway chooses one per call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default provider sort = "default (balanced)".&lt;/strong&gt; The gateway load-balances across eligible upstreams rather than sticking to one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are the factory defaults. You do not have to touch anything to get drift; you have to touch settings to avoid it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 20 identical requests look like
&lt;/h2&gt;

&lt;p&gt;We sent the &lt;strong&gt;same&lt;/strong&gt; ~8K-token prefix 20 times in a row to one popular multi-provider gateway, on the defaults above, asking for the upstream's own reported provider and cache fields each time. For a disk-cached model in the DeepSeek family:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;9 distinct upstreams&lt;/strong&gt; served the 20 calls: &lt;code&gt;N***a&lt;/code&gt;, &lt;code&gt;S***w&lt;/code&gt;, &lt;code&gt;M***h&lt;/code&gt;, &lt;code&gt;D***a&lt;/code&gt;, &lt;code&gt;A***L&lt;/code&gt;, &lt;code&gt;P***l&lt;/code&gt;, &lt;code&gt;S***e&lt;/code&gt;, &lt;code&gt;V***e&lt;/code&gt;, &lt;code&gt;A***d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache hit rate: 4/20 (20%).&lt;/strong&gt; You only hit on the calls that happened to land on an upstream that had already cached your prefix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the same 20 calls against a &lt;strong&gt;single-backend&lt;/strong&gt; gateway (one model, one upstream, no balancing) and the hit rate is &lt;strong&gt;19/20 (95%)&lt;/strong&gt; on the identical workload. Same model, same prompt, same number of calls. The only variable is whether routing drifts.&lt;/p&gt;

&lt;p&gt;For contrast, on the very same multi-provider gateway a GPT-class model was routed to &lt;strong&gt;one&lt;/strong&gt; upstream (&lt;code&gt;A***e&lt;/code&gt;) for all 20 calls and hit &lt;strong&gt;19/20&lt;/strong&gt;. Drift is not uniform; it bites whichever model the gateway happens to spread, and on this run that was the DeepSeek-family model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion A: the cost you expected vs the cost you paid
&lt;/h2&gt;

&lt;p&gt;Per-call cost on the drifting model split cleanly by cache outcome:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;call type&lt;/th&gt;
&lt;th&gt;median cost / call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cache hit&lt;/td&gt;
&lt;td&gt;~$0.00015&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache miss&lt;/td&gt;
&lt;td&gt;~$0.00062&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A miss costs about &lt;strong&gt;4x a hit&lt;/strong&gt; on this model (on raw input tokens the published gap is wider still, roughly 50x). Now total it across the 20 calls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scenario&lt;/th&gt;
&lt;th&gt;hit rate&lt;/th&gt;
&lt;th&gt;cost for 20 identical calls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;expected&lt;/strong&gt; (cache reachable)&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0026&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;actual&lt;/strong&gt; (default drift)&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0102&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same model, same prompt, same 20 requests. Provider drift made the run cost &lt;strong&gt;~3.9x more&lt;/strong&gt;. The caching was "on" the whole time; the routing layer simply billed most of your tokens at the miss rate. Scale that to a production endpoint replaying a large stable prefix all day and the gap is the bulk of your input spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion B: no cache also means no latency win
&lt;/h2&gt;

&lt;p&gt;Caching is not only a cost lever. A warm prefill returns the first token sooner. When drift denies you the cache, you forfeit that speedup too. We measured time-to-first-token (TTFT) on repeated identical calls:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPT-class model (routed to one consistent upstream, cache reachable):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;call&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st (cold, miss)&lt;/td&gt;
&lt;td&gt;~1760 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;subsequent (warm, hit)&lt;/td&gt;
&lt;td&gt;~1130 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Caching buys roughly a &lt;strong&gt;36% faster first token&lt;/strong&gt;, and it is steady: every warm call lands in a tight band.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek-family model (default drift, cache rarely reachable):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache hits across a 10-call repeat: &lt;strong&gt;0&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;TTFT swung from &lt;strong&gt;~1000 ms to ~4500 ms&lt;/strong&gt; call to call, with occasional empty responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because almost every request is a fresh upstream, you stay at cold-prefill latency and inherit the variance of whichever provider answered. The GPT model got a 36% TTFT improvement from a reachable cache; the drifting model got none, plus a 4.5x spread between its fastest and slowest call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit your own setup in five minutes
&lt;/h2&gt;

&lt;p&gt;Do not trust these numbers, or anyone's. Send the same long prefix several times and watch two fields. No domains hardcoded; point it at your own gateway with env vars.&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;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GW_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GW_BASE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;SYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[probe &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&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;You are a support assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&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="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GW_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&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="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYS&lt;/span&gt;&lt;span class="p"&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&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="s"&gt;q&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&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;span class="n"&gt;extra_body&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;usage&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;det&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens_details&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&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;det&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seen&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;d&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;   &lt;span class="c1"&gt;# populated when exposed
&lt;/span&gt;    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&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="s"&gt;hit rate &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/20; upstreams seen: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;)&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;p&gt;More than one upstream for the same model means drift. A hit rate well below your prompt stability means it is taxing you. The fuller method is in &lt;a href="https://dev.to/blog/llm-gateway-cache-audit/"&gt;Does Your LLM Gateway Lie About Cache?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for
&lt;/h2&gt;

&lt;p&gt;The cure for drift is structural: route a given model to a consistent backend so a warm cache is actually reachable on the next request, instead of load-balancing each call onto a fresh upstream that has never seen your prefix. When you evaluate a gateway, send the same prefix 20 times and count the upstreams. One is what you want. Nine is a tax.&lt;/p&gt;

&lt;p&gt;A fair caveat: prompt caching is best-effort everywhere, and on disk-cached models the hit rate still softens over long idle gaps even with a single backend. Eliminating drift does not hand you an infinite cache. It removes the largest and most wasteful source of misses, the one you never agreed to and cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;"Supports prompt caching" and "your cache is reachable" are different claims. A gateway that scatters one model across a rotating cast of upstreams can report cache support truthfully while delivering a 20% hit rate, a ~4x bill, and first-token latency that swings 4.5x. The number to watch is not whether caching is advertised. It is your measured hit rate and how many upstreams your identical requests touch. Run the probe and let the data settle it.&lt;/p&gt;

&lt;p&gt;For the broader audit method see &lt;a href="https://dev.to/blog/llm-gateway-cache-audit/"&gt;Does Your LLM Gateway Lie About Cache?&lt;/a&gt;; for why caches exist at all, see &lt;a href="https://dev.to/blog/llm-prompt-caching-explained/"&gt;How KV Cache &amp;amp; TTL Work&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is this a misconfiguration on my side?&lt;/strong&gt;&lt;br&gt;
No. It happens on the factory defaults: auto routing with the provider sort left at "default (balanced)." Avoiding drift requires actively pinning an upstream, not the other way around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does pinning one upstream fix it?&lt;/strong&gt;&lt;br&gt;
It removes cross-provider drift, but a single upstream often runs multiple replicas without prefix affinity, so hits can still flip-flop. Measure after pinning rather than assuming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did the GPT-class model not drift?&lt;/strong&gt;&lt;br&gt;
On this run the gateway happened to route it to a single upstream. Drift is per-model and depends on how many eligible upstreams the gateway balances across; it is not uniform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the cost gap really ~4x?&lt;/strong&gt;&lt;br&gt;
On the per-call totals we measured, a miss was ~4x a hit; on raw input-token pricing for this model class the published hit-vs-miss gap is closer to 50x. Either way, turning expected hits into misses is the expensive part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What single metric should I monitor?&lt;/strong&gt;&lt;br&gt;
Cache hit rate per model over time, alongside the count of distinct upstreams per model. If hit rate falls or upstream count rises, your effective token cost just went up.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>openrouter</category>
    </item>
    <item>
      <title>Does Your LLM Gateway Lie About Cache? A 5-Min Audit</title>
      <dc:creator>synthorai</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:30:00 +0000</pubDate>
      <link>https://dev.to/synthorai/does-your-llm-gateway-lie-about-cache-a-5-min-audit-k3l</link>
      <guid>https://dev.to/synthorai/does-your-llm-gateway-lie-about-cache-a-5-min-audit-k3l</guid>
      <description>&lt;p&gt;A gateway sits between your code and the model provider. You read &lt;code&gt;cached_tokens&lt;/code&gt; back from the response, you see a smaller number, and you trust the dollars saved are real. But you never see the upstream call. The gateway could report a cache hit and still bill the full input rate. It could fail to cache at all behind a perfectly clean response. It could strip usage metadata on streaming, the path most of your production traffic runs on, so you can't tell either way.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. A &lt;a href="https://news.ycombinator.com/item?id=48319827" rel="noopener noreferrer"&gt;Hacker News PSA&lt;/a&gt; reported that routing DeepSeek V4 through a popular gateway returned &lt;strong&gt;2–3× fewer cached tokens&lt;/strong&gt; than calling DeepSeek directly; one commenter posted bills showing the caching stats weren't reported through the gateway at all. The gateway's team replied that they couldn't reproduce it and were investigating. That disagreement is the whole point. When two parties can't agree on whether your cache is working, the only tiebreaker is a measurement you ran yourself.&lt;/p&gt;

&lt;p&gt;Usually this isn't malice. It's a translation gap or an unfinished code path. The effect on your invoice is the same either way. This post is one runnable script that audits both styles of prompt caching, automatic (DeepSeek) and marker-based (Claude), against any gateway, including this one. It prints a side-by-side scorecard in under five minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Four ways a gateway can lie about cache
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What's actually happening&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Silent no-cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A clean response, no error&lt;/td&gt;
&lt;td&gt;Nothing was cached; you pay full price every call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache theater&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cached_tokens&lt;/code&gt; &amp;gt; 0 in the response&lt;/td&gt;
&lt;td&gt;…but the billed cost is the full input rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Markup creep&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A plausible cost number&lt;/td&gt;
&lt;td&gt;The gateway's markup quietly eats the discount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metadata blackout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean text output&lt;/td&gt;
&lt;td&gt;Usage fields stripped (esp. on streaming), so you can't audit it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dangerous ones are the first two: the response &lt;em&gt;looks&lt;/em&gt; like caching is working. You find out at the end of the month.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two cache mechanisms, one audit
&lt;/h2&gt;

&lt;p&gt;Providers expose caching in two shapes, and a real gateway has to pass both through faithfully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic&lt;/strong&gt; (DeepSeek, GPT, Gemini, Qwen): the provider caches any sufficiently long prefix on its own. No markers. Hits appear in &lt;code&gt;usage.prompt_tokens_details.cached_tokens&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marker-based&lt;/strong&gt; (Anthropic Claude): you tag cacheable spans with &lt;code&gt;cache_control&lt;/code&gt;. Hits appear as &lt;code&gt;cache_read_input_tokens&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script hides that difference behind a thin &lt;code&gt;Lane&lt;/code&gt; adapter, then runs all five checks against both. Here is the whole thing: two lanes and one &lt;code&gt;audit()&lt;/code&gt; that performs every check.&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;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;

&lt;span class="n"&gt;KEY&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GATEWAY_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;oai&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://synthorai.io/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# auto lane
&lt;/span&gt;&lt;span class="n"&gt;anth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://synthorai.io/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# marker lane
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AutoLane&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# DeepSeek / GPT / Gemini / Qwen: provider caches automatically
&lt;/span&gt;    &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;self&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&lt;/span&gt;
    &lt;span class="k"&gt;def&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;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oai&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;self&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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;stream_options&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;include_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;q&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;ev&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&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;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens_details&lt;/span&gt;
                    &lt;span class="n"&gt;cached&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cached_tokens&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oai&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;self&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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&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="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;}]).&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens_details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cached_tokens&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens_details&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MarkerLane&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;# Anthropic Claude: explicit cache_control markers
&lt;/span&gt;    &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;self&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&lt;/span&gt;
    &lt;span class="k"&gt;def&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;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;block&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;type&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;text&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&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;type&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;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&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;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;anth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&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;self&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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;block&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="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;q&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;s&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
                &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_final_message&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&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;prompt_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&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;self&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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;block&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="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;}]).&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_creation_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&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;prompt_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lane&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;long_prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;SYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[audit &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;long_prompt&lt;/span&gt;    &lt;span class="c1"&gt;# unique =&amp;gt; guaranteed cold start
&lt;/span&gt;    &lt;span class="n"&gt;r&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;lane&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lane&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# CHECK 1: cache engages. Cold misses; a repeat should hit. A cache can
&lt;/span&gt;    &lt;span class="c1"&gt;# take a moment to become readable, so poll the warm read (sleep 1s between
&lt;/span&gt;    &lt;span class="c1"&gt;# attempts) before concluding "no cache".
&lt;/span&gt;    &lt;span class="n"&gt;cold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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;SYS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;warm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cold&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;warm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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;SYS&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="s"&gt;warm &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="c1"&gt;# CHECK 2: cost reflects the discount (catches "cache theater").
&lt;/span&gt;    &lt;span class="n"&gt;disc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;disc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;disc&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;disc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# CHECK 3: token accounting. cached fits inside the prompt total.
&lt;/span&gt;    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# CHECK 4: streaming preserves usage metadata (cache count AND cost).
&lt;/span&gt;    &lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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;SYS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# CHECK 5: negative control. a unique prefix must always miss.
&lt;/span&gt;    &lt;span class="n"&gt;n1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[uniq &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;long_prompt&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;n2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[uniq &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;long_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;n2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;

&lt;span class="c1"&gt;# Any long, STABLE text works as the cacheable prefix: a system prompt, tool
# schemas, or a retrieved document. It only needs to clear the provider's
# minimum cacheable size (see Check 1). Load yours however you like.
&lt;/span&gt;&lt;span class="n"&gt;LONG_SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_prompt.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# ~8K+ tokens
&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AutoLane&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-v4-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;MarkerLane&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lane&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LONG_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the post walks each check: the lines that implement it, what both lanes returned, and how to read the result.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 1: does the cache engage?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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;SYS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;warm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cold&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                       &lt;span class="c1"&gt;# poll: a cache may take a beat to be readable
&lt;/span&gt;    &lt;span class="n"&gt;warm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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;SYS&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="s"&gt;warm &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;cold cached&lt;/th&gt;
&lt;th&gt;warm cached&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7,552 / 7,870 (96%)&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;12,446 / 12,454 (99.9%)&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A cold call on a unique prefix must cache nothing; a repeat must hit. The single most common false alarm is &lt;strong&gt;declaring "no cache" after one warm call&lt;/strong&gt;, because caches don't always become readable instantly. The loop polls a few times with a 1-second pause, which removes the flakiness. If you still get &lt;code&gt;0&lt;/code&gt; after several warm calls on a prompt above the size floor (~1,024 tokens for most providers; DeepSeek matches at a finer 64), the cache genuinely isn't engaging.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 2: does the cost reflect the discount?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;disc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;disc&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;disc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;cold cost&lt;/th&gt;
&lt;th&gt;warm cost&lt;/th&gt;
&lt;th&gt;discount&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.00107&lt;/td&gt;
&lt;td&gt;$0.00030&lt;/td&gt;
&lt;td&gt;72.3%&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$0.07112&lt;/td&gt;
&lt;td&gt;$0.00672&lt;/td&gt;
&lt;td&gt;90.6%&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the check that catches &lt;strong&gt;cache theater&lt;/strong&gt;. The warm call's cost must actually drop. DeepSeek's per-call total fell ~72% (the cached input is discounted more steeply; output and the uncached remainder dilute the headline). Claude's cached &lt;em&gt;read&lt;/em&gt; is ~90% off. The failure signal is unmistakable: &lt;code&gt;cached_tokens &amp;gt; 0&lt;/code&gt; with &lt;strong&gt;identical&lt;/strong&gt; cold and warm cost means the gateway is reporting a hit it isn't pricing. You're paying full freight for a cache that "works" on paper.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 3: do the token counts add up?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_total&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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;cached&lt;/th&gt;
&lt;th&gt;prompt total&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7,552&lt;/td&gt;
&lt;td&gt;7,870&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12,446&lt;/td&gt;
&lt;td&gt;12,454&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;cached&lt;/code&gt; has to sit inside the prompt total, with the remainder billed as uncached input. Both reconcile. If &lt;code&gt;cached_tokens&lt;/code&gt; exceeds &lt;code&gt;prompt_tokens&lt;/code&gt;, or the uncached remainder is implausibly large for a stable prefix, the gateway is mis-accounting: re-tokenizing or double-counting somewhere in the translation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 4: does streaming preserve the metadata?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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;SYS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_cost&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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;stream cached&lt;/th&gt;
&lt;th&gt;stream cost&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;preserved&lt;/td&gt;
&lt;td&gt;preserved&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;preserved&lt;/td&gt;
&lt;td&gt;preserved&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most production chat streams, so this is the path that matters most. On both lanes the cache hit signal and the cost both survive the stream. &lt;code&gt;cached_tokens&lt;/code&gt; and &lt;code&gt;cost&lt;/code&gt; come through in the final usage chunk, so your highest-volume path stays auditable. The failure mode to watch for is a gateway that drops usage on streaming: a clean token output with no &lt;code&gt;cached_tokens&lt;/code&gt; or &lt;code&gt;cost&lt;/code&gt; means you're flying blind on the path you run most. (Pass &lt;code&gt;stream_options={"include_usage": True}&lt;/code&gt; so the usage chunk is emitted at all.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 5: the negative control
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;n1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[uniq &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;long_prompt&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;n2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lane&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[uniq &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;long_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;n2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;unique-prefix A&lt;/th&gt;
&lt;th&gt;unique-prefix B&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cached 0&lt;/td&gt;
&lt;td&gt;cached 0&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cached 0&lt;/td&gt;
&lt;td&gt;cached 0&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Send a unique prefix every call; it must never hit. Both lanes correctly reported &lt;code&gt;cached=0&lt;/code&gt; at full cost for distinct prefixes. A "hit" here would make the cache reporting a false positive you could never trust. The clean negative control is what makes the &lt;em&gt;positive&lt;/em&gt; results in Checks 1–2 meaningful in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading your scorecard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Healthy result&lt;/th&gt;
&lt;th&gt;Red flag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. cache engages&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; cold, &lt;code&gt;&amp;gt;0&lt;/code&gt; warm (after polling)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; after several warm calls, above the size floor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. cost reflects discount&lt;/td&gt;
&lt;td&gt;warm cost ≪ cold cost&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cached &amp;gt; 0&lt;/code&gt; but costs equal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. token accounting&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cached ≤ prompt_total&lt;/code&gt;, reconciles&lt;/td&gt;
&lt;td&gt;counts don't add up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. streaming metadata&lt;/td&gt;
&lt;td&gt;cache + cost survive the stream&lt;/td&gt;
&lt;td&gt;usage missing on streamed calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. negative control&lt;/td&gt;
&lt;td&gt;unique prefix always misses&lt;/td&gt;
&lt;td&gt;a distinct prefix "hits"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two that cost money silently are &lt;strong&gt;2&lt;/strong&gt; (full price for a reported hit) and &lt;strong&gt;1&lt;/strong&gt; (no caching behind a clean response). Run both on every model you bill against.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Caching is the highest-leverage cost lever in an LLM app, which is exactly why "the cache is working" deserves a test, not an assumption. Wire Check 1 + Check 2 into CI against each model you bill against, alert if the discount drifts below your expected band, and you'll catch a silent regression the day a gateway or upstream provider changes behavior, instead of at the end of the billing cycle. And whatever your audit does, &lt;strong&gt;poll the warm read&lt;/strong&gt; before you call a cache broken.&lt;/p&gt;

&lt;p&gt;For the mechanics behind these numbers (prefill, KV cache, TTLs) start with &lt;a href="https://dev.to/blog/llm-prompt-caching-explained/"&gt;How KV Cache &amp;amp; TTL Work&lt;/a&gt;. For working caching patterns per provider, see the &lt;a href="https://dev.to/blog/prompt-caching-tutorial-code-examples/"&gt;tutorial&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My Check 1 shows &lt;code&gt;0&lt;/code&gt; on the warm call. Is my gateway lying?&lt;/strong&gt;&lt;br&gt;
Check three things first. (1) Does your prompt clear the provider's minimum cacheable size (~1,024 tokens for most; DeepSeek matches at finer 64-token granularity)? (2) Did you &lt;strong&gt;poll&lt;/strong&gt; the warm read a few times? Caches don't always become readable on the very next call. (3) Is the prefix byte-identical between calls, with no timestamps or per-request IDs at the front? Only after all three should you suspect the gateway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does "cache theater" cost me in practice?&lt;/strong&gt;&lt;br&gt;
You pay the full input rate on every call while believing you pay a fraction. On a high-volume endpoint with a large stable prefix, that's your bill being several times what you modeled. Check 2 is the one to alert on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is DeepSeek's discount lower than Claude's here?&lt;/strong&gt;&lt;br&gt;
Different things are being measured. Claude's ~90% is the &lt;em&gt;read&lt;/em&gt; discount on cached input. DeepSeek's ~72% is the &lt;em&gt;per-call total&lt;/em&gt; reduction, where output and the uncached remainder are billed at full rate and dilute the headline. Compare like with like for your own prompt shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work for GPT, Gemini, Qwen too?&lt;/strong&gt;&lt;br&gt;
Yes. They're all automatic, so they use the &lt;code&gt;AutoLane&lt;/code&gt; unchanged with a different &lt;code&gt;model&lt;/code&gt;. Only Claude needs the &lt;code&gt;MarkerLane&lt;/code&gt;. Same five checks either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should this live in CI?&lt;/strong&gt;&lt;br&gt;
Yes. Run Check 1 + Check 2 against every model you bill against, on a schedule, and alert when the observed discount drifts outside your expected band. A standing audit turns a silent regression into a notification.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
