<?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: Cursuri AI</title>
    <description>The latest articles on DEV Community by Cursuri AI (cursuri-ai).</description>
    <link>https://dev.to/cursuri-ai</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%2Forganization%2Fprofile_image%2F12719%2Ffd877e1e-b068-40d1-90c2-438ed313f3e4.png</url>
      <title>DEV Community: Cursuri AI</title>
      <link>https://dev.to/cursuri-ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cursuri-ai"/>
    <language>en</language>
    <item>
      <title>The Batch API Halves Your Claude Bill — Unless Your Prompt Cache Was Already Doing the Work</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Fri, 02 Oct 2026 11:38:58 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/the-batch-api-halves-your-claude-bill-unless-your-prompt-cache-was-already-doing-the-work-1l4a</link>
      <guid>https://dev.to/cursuri-ai/the-batch-api-halves-your-claude-bill-unless-your-prompt-cache-was-already-doing-the-work-1l4a</guid>
      <description>&lt;p&gt;The Message Batches API has the simplest pitch in Anthropic's pricing table: send work that doesn't need an answer right now, get the results back later (most batches finish within an hour, the hard limit is 24), and pay half of every token. For plenty of workloads it is the easiest cost cut you will ever ship.&lt;/p&gt;

&lt;p&gt;For one very common workload it isn't a cut at all. Picture a job that sends a large shared prefix (instructions, a label taxonomy, few-shot examples, a policy document), a small per-item payload, and asks for a short answer. If you already run that job synchronously with prompt caching, most of your bill is cache reads. Batch halves those too. But inside a batch, cache hits are best-effort, and Anthropic's documentation puts typical hit rates anywhere between 30% and 98%. Every miss is billed as a cache write, which costs 25 times a cache read on Opus 5.5. Move that job to batch at the wrong hit rate and it costs several times what it cost synchronously.&lt;/p&gt;

&lt;p&gt;What follows applies the cost-versus-reliability reasoning from &lt;a href="https://cursuri-ai.ro/en/courses/advanced-llm-integration-in-production" rel="noopener noreferrer"&gt;the production LLM integration course at Cursuri-AI.ro&lt;/a&gt; to this one API: the arithmetic on current list prices, a function that makes the call, the documented recipe for raising batch hit rates, and the operational details that decide whether you lose data (idempotent &lt;code&gt;custom_id&lt;/code&gt;s, unordered results, the four result types, re-submitting only what failed, the 24-hour and 29-day clocks).&lt;/p&gt;

&lt;p&gt;Prompt caching itself (prefix matching, breakpoints, silent invalidators) is assumed knowledge here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "50% off" actually applies to
&lt;/h2&gt;

&lt;p&gt;The discount covers all usage: input, output and, because the caching multipliers stack with the Batch API discount, cache writes and cache reads as well. For the three current models, in USD per million tokens, synchronous → batch:&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&lt;/th&gt;
&lt;th&gt;5-min cache write&lt;/th&gt;
&lt;th&gt;1-hour cache write&lt;/th&gt;
&lt;th&gt;Cache read&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;Claude Fable 5.1&lt;/td&gt;
&lt;td&gt;$10 → $5&lt;/td&gt;
&lt;td&gt;$12.50 → $6.25&lt;/td&gt;
&lt;td&gt;$20 → $10&lt;/td&gt;
&lt;td&gt;$0.25 → $0.125&lt;/td&gt;
&lt;td&gt;$50 → $25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5.5&lt;/td&gt;
&lt;td&gt;$4 → $2&lt;/td&gt;
&lt;td&gt;$5 → $2.50&lt;/td&gt;
&lt;td&gt;$8 → $4&lt;/td&gt;
&lt;td&gt;$0.20 → $0.10&lt;/td&gt;
&lt;td&gt;$20 → $10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;
&lt;td&gt;$2 → $1&lt;/td&gt;
&lt;td&gt;$2.50 → $1.25&lt;/td&gt;
&lt;td&gt;$4 → $2&lt;/td&gt;
&lt;td&gt;$0.20 → $0.10&lt;/td&gt;
&lt;td&gt;$10 → $5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things in that table matter more than the headline.&lt;/p&gt;

&lt;p&gt;First, the &lt;em&gt;batch&lt;/em&gt; input price is far above the &lt;em&gt;synchronous&lt;/em&gt; cache-read price: ten times on Opus 5.5 ($2 vs $0.20), five times on Sonnet 5.5, twenty times on Fable 5.1. Halving a token you were already reading from cache at a few percent of list price saves almost nothing in absolute terms. Halving it and then losing the cache hit costs you a great deal.&lt;/p&gt;

&lt;p&gt;Second, the read multiplier differs across the lineup. Fable 5.1 reads cache at 0.025× its input price, Opus 5.5 at 0.05×, Sonnet 5.5 at the standard 0.1×. The cheaper the read, the less of your bill is left for the batch discount to work on, and the more a miss hurts by comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: 10,000 classifications against a 50K prefix
&lt;/h2&gt;

&lt;p&gt;Take a hypothetical but typical backfill on Claude Opus 5.5 (Anthropic's recommended starting model for most workloads):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared prefix:&lt;/strong&gt; 50,000 tokens of instructions, label definitions and examples, identical on every request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per item:&lt;/strong&gt; 500 tokens of fresh input (the ticket, the document, the record).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output:&lt;/strong&gt; 150 tokens. On Opus 5.5 adaptive thinking is always on, and thinking tokens are billed as output, so treat this as "measured output including thinking at low effort", not as the length of the JSON you get back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run synchronously with a steady request rate, the prefix is written once and then read on nearly every call. Per request: $0.010 for the cached prefix, $0.002 for the fresh input, $0.003 for the output, so $0.015. Here is the same job on batch, for all 10,000 items:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup (Opus 5.5, 10,000 requests)&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Synchronous, prompt caching, ≈100% hits&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$150&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch, no prompt caching&lt;/td&gt;
&lt;td&gt;$1,025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch, caching, 98% hit rate&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch, caching, 95% hit rate&lt;/td&gt;
&lt;td&gt;$135&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch, caching, 90% hit rate&lt;/td&gt;
&lt;td&gt;$195&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch, caching, 60% hit rate&lt;/td&gt;
&lt;td&gt;$555&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch, caching, 30% hit rate&lt;/td&gt;
&lt;td&gt;$915&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These figures use 5-minute writes for the misses. With 1-hour writes a miss costs more ($4 instead of $2.50 per million batch tokens), so 90% becomes $270 and 60% becomes $855.&lt;/p&gt;

&lt;p&gt;Read the table from the bottom. Batch without caching costs almost seven times the cached synchronous job; batch with caching at the low end of the documented range, six times. Batch wins only near the top of that range, and the gap between winning and losing badly is a few points of hit rate you don't directly control. You may well land high, and the recipe below pushes you that way, but you won't know until you measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The break-even, for any workload
&lt;/h2&gt;

&lt;p&gt;The comparison generalizes. Let &lt;code&gt;P&lt;/code&gt; be the shared prefix in tokens, &lt;code&gt;r&lt;/code&gt; the synchronous cache-read price, &lt;code&gt;w&lt;/code&gt; the synchronous cache-write price for the TTL you use in the batch, and &lt;code&gt;U&lt;/code&gt; the uncacheable part of each request, priced the same way (fresh tokens × input price + output tokens × output price). If your synchronous job already hits the cache on essentially every request, batch is cheaper only when its hit rate &lt;code&gt;h&lt;/code&gt; satisfies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h &amp;gt; (w - 2r - U/P) / (w - r)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the example shape (50K prefix, 500 fresh tokens, 150 output tokens):&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;Break-even hit rate, 5-min writes&lt;/th&gt;
&lt;th&gt;Break-even hit rate, 1-hour writes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5.1&lt;/td&gt;
&lt;td&gt;95.9%&lt;/td&gt;
&lt;td&gt;97.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5.5&lt;/td&gt;
&lt;td&gt;93.8%&lt;/td&gt;
&lt;td&gt;96.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;
&lt;td&gt;89.1%&lt;/td&gt;
&lt;td&gt;93.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The threshold moves fast with output size, because output is the part caching can't touch and batch always halves:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Opus 5.5, 50K prefix, 5-min writes&lt;/th&gt;
&lt;th&gt;Break-even hit rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;150 output tokens&lt;/td&gt;
&lt;td&gt;93.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 output tokens&lt;/td&gt;
&lt;td&gt;86.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000 output tokens&lt;/td&gt;
&lt;td&gt;61.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,000 output tokens&lt;/td&gt;
&lt;td&gt;28.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;above ~11,400 output tokens&lt;/td&gt;
&lt;td&gt;none: batch always wins&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And it moves the other way with prefix size: with 150 output tokens on Opus 5.5, a 2K prefix needs about 44%, a 5K prefix 75%, a 20K prefix about 91%, a 200K prefix about 95%.&lt;/p&gt;

&lt;p&gt;Batch's advantage grows with everything that can't be cached (long answers, heavy thinking, large per-item inputs) and shrinks as the shared prefix grows. Short answers over a huge shared context are exactly where synchronous caching is hardest to beat. Long generations over a modest prompt are where batch is a clean 50% cut.&lt;/p&gt;

&lt;p&gt;Here is the decision as code. Prices are synchronous list prices; the function applies the discount itself:&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;# Synchronous list prices, USD per million tokens (Anthropic pricing page, 2 Oct 2026)
&lt;/span&gt;&lt;span class="n"&gt;PRICES&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;claude-fable-5-1&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;10.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_1h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;20.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;50.00&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-5-5&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;5.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_1h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;8.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;20.00&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-sonnet-5-5&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_1h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;10.00&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;BATCH_DISCOUNT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;  &lt;span class="c1"&gt;# applies to every column above, cache reads and writes included
&lt;/span&gt;&lt;span class="n"&gt;MIN_CACHEABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;   &lt;span class="c1"&gt;# tokens, for all three models above
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cost_per_request&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;hit_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Expected USD per request. A miss on a cache_control prefix is billed as a cache write.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRICES&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;write&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_1h&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;ttl&lt;/span&gt; &lt;span class="o"&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="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;prefix_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hit_rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&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="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;hit_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;
    &lt;span class="n"&gt;usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;prefix_rate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&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&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;output&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&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;1_000_000&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;usd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;BATCH_DISCOUNT&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;usd&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;breakeven_hit_rate&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;sync_hit_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&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;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&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;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Lowest batch cache hit rate at which batch beats your current synchronous job.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRICES&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;write&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_1h&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;ttl&lt;/span&gt; &lt;span class="o"&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="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write_5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;sync&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cost_per_request&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;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sync_hit_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;uncacheable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&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&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;output&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&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;1_000_000&lt;/span&gt;
    &lt;span class="n"&gt;needed_prefix_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;BATCH_DISCOUNT&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;uncacheable&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_000_000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;needed_prefix_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&lt;/span&gt;&lt;span class="sh"&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;should_batch&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;sync_hit_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&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;measured_batch_hit_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&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;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&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;span class="n"&gt;hard_deadline_under_24h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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;zdr_or_hipaa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&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;zdr_or_hipaa&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Batch is not ZDR- or HIPAA-eligible: inputs and outputs are stored up to 29 days.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hard_deadline_under_24h&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A batch can end with requests expired, unprocessed, after 24 hours.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MIN_CACHEABLE&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;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nothing here can be cached, so batch is a flat 50% cut.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;breakeven_hit_rate&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;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sync_hit_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&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;need&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Uncacheable tokens dominate: batch wins even with zero cache hits.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;measured_batch_hit_rate&lt;/span&gt; &lt;span class="ow"&gt;is&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="bp"&gt;None&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;Run a pilot batch first: you need a cache hit rate above &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="si"&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;measured_batch_hit_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;need&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;True&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;Measured &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;measured_batch_hit_rate&lt;/span&gt;&lt;span class="si"&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; beats break-even &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="si"&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&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="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;Measured &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;measured_batch_hit_rate&lt;/span&gt;&lt;span class="si"&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is below break-even &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="si"&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: stay synchronous.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sync_hit_rate&lt;/code&gt; is there because your synchronous job may not be the ideal baseline either. A job that fans out hundreds of parallel requests can't read a cache entry until the first response that wrote it has started, so it pays for many writes at the start; plug in what your logs show. And price isn't the only input. Batch traffic has its own rate limits and doesn't count against your Messages API limits, so a backfill on batch doesn't compete with production traffic. If your synchronous backfill is throttling user-facing requests, that can justify batch at break-even.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raising the batch hit rate: the documented recipe
&lt;/h2&gt;

&lt;p&gt;The prompt-caching docs describe a specific pattern for batches. Each step below closes one way a batch request can miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put an explicit breakpoint at the end of the shared prefix.&lt;/strong&gt; Don't rely on automatic caching here. It places the breakpoint on the last cacheable block, which in a batch is the per-item content: every request writes an entry no other request will ever read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make every request render the same prefix, byte for byte.&lt;/strong&gt; Same &lt;code&gt;tools&lt;/code&gt;, same &lt;code&gt;system&lt;/code&gt;, and also the same thinking configuration, &lt;code&gt;output_config.effort&lt;/code&gt; and &lt;code&gt;output_config.format&lt;/code&gt;. Those settings are rendered into the prompt, so a request at a different effort or with a different schema has a different prefix, even when the text is identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the 1-hour TTL.&lt;/strong&gt; The docs note that batch requests commonly take between 5 minutes and an hour to complete, which is longer than the default 5-minute cache lifetime. The 1-hour write costs more, and the break-even function accounts for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prime the cache with a one-request batch.&lt;/strong&gt; Submit a batch containing a single request that carries the shared prefix and the 1-hour cache block. When it ends, submit the rest. The primer can be one of your real items, so nothing is wasted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay above the minimum.&lt;/strong&gt; The minimum cacheable prompt length is 512 tokens on Fable 5.1, Opus 5.5 and Sonnet 5.5. Below it nothing is cached and no error is returned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay in one workspace.&lt;/strong&gt; Both batches and prompt caches are isolated per workspace on the Claude API, so a primer written from another workspace's key primes nothing for you.&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;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&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="nc"&gt;Anthropic&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-opus-5-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# INSTRUCTIONS, TAXONOMY_AND_EXAMPLES and LABEL_SCHEMA are your own constants.
&lt;/span&gt;
&lt;span class="n"&gt;SHARED_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;INSTRUCTIONS&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;TAXONOMY_AND_EXAMPLES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;# the large shared prefix
&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;span class="c1"&gt;# explicit breakpoint
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# Effort and output format are rendered into the prompt: keep them identical everywhere.
&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_CONFIG&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;effort&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;low&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;format&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;json_schema&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;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LABEL_SCHEMA&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;params_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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;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;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;max_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;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# thinking counts toward max_tokens
&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;SHARED_SYSTEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OUTPUT_CONFIG&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="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;item_text&lt;/span&gt;&lt;span class="p"&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;submit_primed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;items: (custom_id, text). One request writes the 1-hour cache, then the rest follow.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;first_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&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;primer&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batches&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;requests&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;custom_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;first_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;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;params_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_text&lt;/span&gt;&lt;span class="p"&gt;)}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primer&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="c1"&gt;# defined in the polling section below
&lt;/span&gt;    &lt;span class="n"&gt;main&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batches&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;requests&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;custom_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;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;params_for&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;for&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;primer&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="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then measure. Every succeeded result carries the full &lt;code&gt;usage&lt;/code&gt; object, so your real hit rate is one division away: &lt;code&gt;cache_read_input_tokens / (cache_read_input_tokens + cache_creation_input_tokens)&lt;/code&gt;, summed across results. That token-weighted ratio is the &lt;code&gt;h&lt;/code&gt; the break-even formula wants. Run a pilot of a few thousand items, compute it, feed it to &lt;code&gt;should_batch()&lt;/code&gt;, and only then move the full job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it in production without losing results
&lt;/h2&gt;

&lt;p&gt;The economics decide whether to batch. The operational details decide whether every item comes back exactly once. A batch job has more ways to lose data quietly than a synchronous loop: results arrive out of order, failures are reported per request inside a "successful" batch, and everything has an expiry date.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;custom_id&lt;/code&gt; is your idempotency key
&lt;/h3&gt;

&lt;p&gt;Each request carries a &lt;code&gt;custom_id&lt;/code&gt; of 1 to 64 characters matching &lt;code&gt;^[a-zA-Z0-9_-]{1,64}$&lt;/code&gt;. Results can come back in any order, so the &lt;code&gt;custom_id&lt;/code&gt; is the only reliable link between a result and its input. Never zip results against your input list.&lt;/p&gt;

&lt;p&gt;The API requires uniqueness only &lt;em&gt;within&lt;/em&gt; a batch. Nothing stops you from submitting the same item in two batches and paying for it twice. That makes the &lt;code&gt;custom_id&lt;/code&gt; your job, and the useful version is deterministic: derive it from the record's identity and the prompt version, so the same item under the same prompt always maps to the same ID, and a new prompt produces new IDs. Then keep a ledger keyed by it. The same discipline applies to any &lt;a href="https://cursuri-ai.ro/en/courses/data-engineering-for-ai" rel="noopener noreferrer"&gt;idempotent data pipeline&lt;/a&gt;: the key, not the run, defines what has been processed.&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;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="n"&gt;PROMPT_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labels-v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# bump on any change to model, prompt, schema or effort
&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;batch_ledger.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    CREATE TABLE IF NOT EXISTS ledger (
        custom_id   TEXT PRIMARY KEY,
        record_id   TEXT NOT NULL,
        status      TEXT NOT NULL DEFAULT &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,  -- pending|submitting|submitted|succeeded|retry|failed
        batch_id    TEXT,
        attempts    INTEGER NOT NULL DEFAULT 0,
        error       TEXT,
        result_json TEXT
    )
&lt;/span&gt;&lt;span class="sh"&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;make_custom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PROMPT_VERSION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&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;prompt_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record_id&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="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&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;r_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&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="c1"&gt;# 42 chars, only [a-z0-9_]
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT OR IGNORE INTO ledger (custom_id, record_id) VALUES (?, ?)&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="nf"&gt;make_custom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rid&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;rid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record_ids&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;INSERT OR IGNORE&lt;/code&gt; makes enqueueing safe to re-run: items already in the ledger keep their status.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chunk by count and by bytes
&lt;/h3&gt;

&lt;p&gt;A single batch is capped at 100,000 requests or 256 MB, whichever comes first; an oversized request body can come back as a 413 &lt;code&gt;request_too_large&lt;/code&gt;. There is also a per-organization cap on batch requests waiting in the processing queue, which depends on your usage tier (200,000 at the Start tier, 500,000 at Scale), so a large backfill gets fed in waves anyway.&lt;/p&gt;

&lt;p&gt;There is a second reason to keep batches smaller than the cap. Results become available only when every request in the batch has finished, or after 24 hours. Ten batches of 10,000 give you results as each finishes. One batch of 100,000 gives you nothing until the slowest request is done.&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;MAX_REQUESTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;          &lt;span class="c1"&gt;# hard cap is 100,000; smaller batches return sooner
&lt;/span&gt;&lt;span class="n"&gt;MAX_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200_000_000&lt;/span&gt;        &lt;span class="c1"&gt;# stay well under the 256 MB cap
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&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;req&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;n&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&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;batch&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&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;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_REQUESTS&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
            &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&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="n"&gt;batch&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;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Validate one request synchronously first
&lt;/h3&gt;

&lt;p&gt;Validation of each request's &lt;code&gt;params&lt;/code&gt; happens asynchronously, and validation errors come back only when the whole batch has ended. A typo in a parameter name turns into 10,000 &lt;code&gt;errored&lt;/code&gt; results an hour later. Send one request with exactly the batch shape to the ordinary Messages API before submitting. While you're at it, remember the three parameters a batch rejects: &lt;code&gt;stream: true&lt;/code&gt;, &lt;code&gt;speed&lt;/code&gt; (fast mode) and &lt;code&gt;max_tokens: 0&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Submit with an intent marker
&lt;/h3&gt;

&lt;p&gt;There is a crash window between "the batch was created" and "the ledger knows its ID". Mark the rows &lt;code&gt;submitting&lt;/code&gt; before the call and &lt;code&gt;submitted&lt;/code&gt; after it. If a restart finds rows stuck in &lt;code&gt;submitting&lt;/code&gt;, check &lt;code&gt;client.messages.batches.list()&lt;/code&gt; for recent batches before re-submitting. If you do end up submitting twice, the deterministic &lt;code&gt;custom_id&lt;/code&gt; means the second result overwrites the first rather than creating a duplicate: it costs money, not correctness.&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;MAX_ATTEMPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;build_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;rows: (custom_id, record_id). build_params(record_id) returns the Messages params.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;cid&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;cid&lt;/span&gt;&lt;span class="p"&gt;,&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;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE ledger SET status=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;submitting&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; WHERE custom_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;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;batch&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batches&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;requests&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;custom_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;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rid&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;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE ledger SET status=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;submitted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, batch_id=?, attempts=attempts+1 WHERE custom_id=?&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;batch&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="n"&gt;cid&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;cid&lt;/span&gt;&lt;span class="p"&gt;,&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;rows&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&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;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Poll gently, and don't expect a progress bar
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;processing_status&lt;/code&gt; moves from &lt;code&gt;in_progress&lt;/code&gt; to &lt;code&gt;ended&lt;/code&gt; (via &lt;code&gt;canceling&lt;/code&gt; if you cancel). The &lt;code&gt;request_counts&lt;/code&gt; object looks like a progress meter but isn't one: &lt;code&gt;succeeded&lt;/code&gt;, &lt;code&gt;errored&lt;/code&gt;, &lt;code&gt;canceled&lt;/code&gt; and &lt;code&gt;expired&lt;/code&gt; all stay at zero until the entire batch has ended, and only &lt;code&gt;processing&lt;/code&gt; is meaningful before then. Poll with backoff: the Batches API has its own requests-per-minute limit, and a tight polling loop spends it.&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;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic.types.messages&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MessageBatch&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_sleep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;300.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MessageBatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;
    &lt;span class="k"&gt;while&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;batch&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&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;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ended&lt;/span&gt;&lt;span class="sh"&gt;"&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;batch&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="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&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;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_sleep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;expires_at&lt;/code&gt; on the batch object tells you when the 24-hour clock runs out. Any request not processed by then comes back as &lt;code&gt;expired&lt;/code&gt;. The docs warn that processing can slow down with demand and with your own volume, and that more requests may expire as a result. Treat expiry as a normal outcome your retry path handles, not as an incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four result types, and "succeeded" isn't the same as usable
&lt;/h3&gt;

&lt;p&gt;Each result has one of four types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result type&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Billed?&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;succeeded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A message was created&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;stop_reason&lt;/code&gt; before trusting it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;errored&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No message: invalid request or server-side error&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Retry server errors; fix &lt;code&gt;invalid_request_error&lt;/code&gt; first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;canceled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You canceled before it was sent to the model&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Re-submit if still needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The 24-hour limit hit before it was sent to the model&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Re-submit, ideally in a smaller batch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The column that trips people up is the first one. A &lt;code&gt;succeeded&lt;/code&gt; result can still carry &lt;code&gt;stop_reason: "max_tokens"&lt;/code&gt; (truncated; remember that thinking counts toward the budget), &lt;code&gt;"refusal"&lt;/code&gt;, or &lt;code&gt;"pause_turn"&lt;/code&gt; (a server-tool loop that paused and needs continuing). None of those is a usable answer. Route them to review, not into your table of answers.&lt;/p&gt;

&lt;p&gt;Stream the results instead of downloading them whole (the SDK's &lt;code&gt;results()&lt;/code&gt; iterator reads the &lt;code&gt;.jsonl&lt;/code&gt; line by line) and write each one to durable storage as it arrives:&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;RETRYABLE&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;api_error&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;overloaded_error&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;rate_limit_error&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;timeout_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;USABLE_STOPS&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;end_turn&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;stop_sequence&lt;/span&gt;&lt;span class="sh"&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;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stats&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;succeeded&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errored&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canceled&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&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;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;start&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;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&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="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;succeeded&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
            &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&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;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="n"&gt;cache_read_input_tokens&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&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;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="n"&gt;cache_creation_input_tokens&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&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;stop_reason&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;USABLE_STOPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;succeeded&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump_json&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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&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;stop_reason:&lt;/span&gt;&lt;span class="si"&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;stop_reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errored&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RETRYABLE&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                   &lt;span class="c1"&gt;# canceled or expired: never reached the model
&lt;/span&gt;            &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE ledger SET status=?, error=?, result_json=? WHERE custom_id=?&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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;custom_id&lt;/span&gt;&lt;span class="p"&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;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;500&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="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&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;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hit_rate&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;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&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;total&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stats["hit_rate"]&lt;/code&gt; is the measured value to feed back into &lt;code&gt;should_batch()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Re-submit only what failed
&lt;/h3&gt;

&lt;p&gt;With the ledger in place, the whole job is a loop: pick rows that are &lt;code&gt;pending&lt;/code&gt; or &lt;code&gt;retry&lt;/code&gt; and under the attempt cap, chunk them, submit, wait, collect, repeat until nothing is left. Re-using the same &lt;code&gt;custom_id&lt;/code&gt;s in a new batch is fine, since uniqueness is only required within one batch, and it keeps every attempt for an item under a single 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;build_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;while&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT custom_id, record_id FROM ledger &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE status IN (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&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="s"&gt;retry&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) AND attempts &amp;lt; ?&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;MAX_ATTEMPTS&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&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;batch_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build_params&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;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&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;batch_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&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;batch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chunks()&lt;/code&gt; sizes ledger rows here, which is fine for small items; for large ones, size by the serialized params. Rows that end as &lt;code&gt;failed&lt;/code&gt; (validation errors, truncations, refusals) or hit the attempt cap go to a person. Everything else converges on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  The clocks: 24 hours, 29 days
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;24 hours&lt;/strong&gt; from creation is the processing limit. You can't modify a batch once it's submitted. To change it, cancel it (status goes to &lt;code&gt;canceling&lt;/code&gt;, and requests already in flight may still complete) and submit a new one. A canceled batch still ends with partial results for whatever was processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;29 days&lt;/strong&gt; from &lt;code&gt;created_at&lt;/code&gt; (not &lt;code&gt;ended_at&lt;/code&gt;) is how long results stay downloadable. After that the batch object is still visible, with &lt;code&gt;archived_at&lt;/code&gt; set, but its results are gone. That's why &lt;code&gt;collect()&lt;/code&gt; writes to your own storage instead of treating the API as the system of record. Once results are stored, you can delete the batch with &lt;code&gt;client.messages.batches.delete(batch_id)&lt;/code&gt;; an in-progress batch has to be canceled first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more note from the docs: because of high throughput and concurrent processing, a batch can go slightly over your workspace's configured spend limit, so don't treat that limit as an exact cap for batch jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where batch is the right tool
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Offline evals.&lt;/strong&gt; An eval suite is the textbook batch workload. Nobody is waiting on the answer, it reruns on every prompt or model change, and it shouldn't use up the rate limits your product needs. Score the results from the ledger after the batch ends. If the suite itself is the weak part, start with &lt;a href="https://cursuri-ai.ro/en/courses/llm-evaluation-and-testing" rel="noopener noreferrer"&gt;a proper offline eval harness&lt;/a&gt; before optimizing what it costs to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classification and extraction backfills.&lt;/strong&gt; A new field to fill on a million old records, a taxonomy applied to an archive. Short outputs over a big shared prefix: this is the case to measure before you move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-processing after a prompt change.&lt;/strong&gt; Because &lt;code&gt;PROMPT_VERSION&lt;/code&gt; is part of the &lt;code&gt;custom_id&lt;/code&gt;, version 4 of the prompt creates new IDs next to version 3's. Run the new version as a batch, then diff the two result sets by &lt;code&gt;record_id&lt;/code&gt; to see exactly which answers changed before switching production over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Very long generations.&lt;/strong&gt; With the &lt;code&gt;output-300k-2026-03-24&lt;/code&gt; beta header, batch requests on Opus 5.5, Sonnet 5.5 and several earlier models can set &lt;code&gt;max_tokens&lt;/code&gt; up to 300,000, against 128K on the synchronous API. It's batch-only, available on the Claude API and Claude Platform on AWS, and a single generation that long can take over an hour, so plan around the 24-hour window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server tools at scale.&lt;/strong&gt; Web search, web fetch and code execution work inside batches, and with no open connection to keep alive, the batch loop runs more iterations per turn before returning &lt;code&gt;pause_turn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And where it isn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anything a user is waiting for, or that needs streaming (&lt;code&gt;stream: true&lt;/code&gt; is rejected).&lt;/li&gt;
&lt;li&gt;Fast mode (&lt;code&gt;speed&lt;/code&gt; is rejected; fast mode tunes synchronous latency).&lt;/li&gt;
&lt;li&gt;Cache pre-warming (&lt;code&gt;max_tokens: 0&lt;/code&gt; is rejected inside a batch).&lt;/li&gt;
&lt;li&gt;Zero-data-retention or HIPAA workloads. Batch processing stores inputs and outputs for up to 29 days and isn't eligible for either arrangement. Under HIPAA readiness the API rejects it with a 400. Under ZDR it isn't blocked, but using it takes that data outside your ZDR arrangement.&lt;/li&gt;
&lt;li&gt;Work with a hard deadline shorter than a day. "Most batches finish within an hour" describes the typical case. It isn't a guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Compute the break-even hit rate for your actual prefix, fresh-input and output sizes, including thinking tokens in output&lt;/li&gt;
&lt;li&gt;[ ] Pilot a few thousand items and measure the hit rate from &lt;code&gt;usage&lt;/code&gt; before moving the full job&lt;/li&gt;
&lt;li&gt;[ ] Explicit breakpoint at the end of the shared prefix, 1-hour TTL, a primer batch first&lt;/li&gt;
&lt;li&gt;[ ] Identical &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt;, effort, thinking configuration and output format on every request&lt;/li&gt;
&lt;li&gt;[ ] Deterministic &lt;code&gt;custom_id&lt;/code&gt; from record ID plus prompt version; a ledger keyed by it&lt;/li&gt;
&lt;li&gt;[ ] Chunk by count and bytes; smaller batches if you want results sooner&lt;/li&gt;
&lt;li&gt;[ ] One synchronous dry run of the request shape before submitting&lt;/li&gt;
&lt;li&gt;[ ] Check &lt;code&gt;stop_reason&lt;/code&gt; on every &lt;code&gt;succeeded&lt;/code&gt; result&lt;/li&gt;
&lt;li&gt;[ ] Retry server errors, &lt;code&gt;expired&lt;/code&gt; and &lt;code&gt;canceled&lt;/code&gt;; fix &lt;code&gt;invalid_request_error&lt;/code&gt;; cap attempts&lt;/li&gt;
&lt;li&gt;[ ] Persist results as you stream them; they stop being downloadable 29 days after creation&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Does the batch discount apply to cached tokens?&lt;/strong&gt;&lt;br&gt;
Yes. Caching multipliers stack with the Batch API discount, so cache reads and writes are half price too. What batch can't promise is the hit: cache hits inside a batch are best-effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are partial results visible while a batch is running?&lt;/strong&gt;&lt;br&gt;
No. Results become available when every request has finished, or after 24 hours, and the per-type counts stay at zero until then. If you need earlier partial results, submit several smaller batches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do batch requests count against Messages API rate limits?&lt;/strong&gt;&lt;br&gt;
No. The Batches API has its own limits, on HTTP requests per minute and on requests waiting in the processing queue, and batch usage doesn't affect your Messages API limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to go deeper?
&lt;/h2&gt;

&lt;p&gt;A 50% discount looks like an easy call until you put it next to the caching bill you already have. Then it's a calculation, and sometimes the answer is to stay synchronous. Working out cost per request and per customer before choosing an architecture is a recurring thread in &lt;a href="https://cursuri-ai.ro/en/courses/build-and-ship-a-production-ai-saas" rel="noopener noreferrer"&gt;Build and Ship a Production AI SaaS&lt;/a&gt;, which follows an LLM product from unit economics to paying users, and it pairs well with the caching and batching material in the LLM integration track linked at the top.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published by &lt;a href="https://cursuri-ai.ro/en/courses/advanced-llm-integration-in-production" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, Eastern Europe's AI education platform, with hands-on courses on LLM integration, evaluation and AI engineering.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prices, limits and behavior were checked against Anthropic's documentation on 2 October 2026: &lt;a href="https://platform.claude.com/docs/en/build-with-claude/batch-processing" rel="noopener noreferrer"&gt;batch processing&lt;/a&gt;, &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;prompt caching&lt;/a&gt;, &lt;a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener noreferrer"&gt;rate limits&lt;/a&gt; and &lt;a href="https://platform.claude.com/docs/en/manage-claude/api-and-data-retention" rel="noopener noreferrer"&gt;API and data retention&lt;/a&gt;. Prices and limits change, so re-check them before you commit a budget.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Claude Opus 5.5 Migration: Four Errors You'll See and Three You Won't</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:37:27 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/claude-opus-55-migration-four-errors-youll-see-and-three-you-wont-202p</link>
      <guid>https://dev.to/cursuri-ai/claude-opus-55-migration-four-errors-youll-see-and-three-you-wont-202p</guid>
      <description>&lt;p&gt;Anthropic released Claude Opus 5.5 on 22 September 2026. It costs less than Opus 5 ($4/$20 per million input/output tokens, down from $5/$25), cache reads dropped from $0.50 to $0.20, and in Anthropic's own benchmark table it beats Claude Fable 5.1 on agentic coding at 40% of Fable's list price. Most teams running Opus 5 will move, and most will start by changing one 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="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-opus-5-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# was "claude-opus-5"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is the whole migration only if your integration never disabled thinking, never forced a tool, never used the older computer use tool, and never edits its own history. Four request shapes that Opus 5 accepted now come back as a 400. Those are the easy ones: they fail loudly, in staging, with a readable message. The three changes that don't throw are the ones that reach production — a default that moved, an agent UI that goes quiet, and an unattended agent that stops halfway.&lt;/p&gt;

&lt;p&gt;Below: each exact error message (so you can grep your logs for it), the fix, and the three silent changes with the code that catches them. It is the model-upgrade drill we run in the &lt;a href="https://cursuri-ai.ro/en/courses/advanced-llm-integration-in-production" rel="noopener noreferrer"&gt;production LLM integration track at Cursuri-AI.ro&lt;/a&gt;, cut down to what this release actually changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, in one table
&lt;/h2&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;Claude Opus 5&lt;/th&gt;
&lt;th&gt;Claude Opus 5.5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-opus-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-opus-5-5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input / output (per MTok)&lt;/td&gt;
&lt;td&gt;$5 / $25&lt;/td&gt;
&lt;td&gt;$4 / $20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache read (per MTok)&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;$0.20 (0.05× input)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-minute cache write (per MTok)&lt;/td&gt;
&lt;td&gt;$6.25&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch API (per MTok)&lt;/td&gt;
&lt;td&gt;$2.50 / $12.50&lt;/td&gt;
&lt;td&gt;$2 / $10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast mode (Claude API only, research preview)&lt;/td&gt;
&lt;td&gt;$10 / $50&lt;/td&gt;
&lt;td&gt;$8 / $40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default &lt;code&gt;effort&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;medium&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;thinking: {"type": "disabled"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accepted at &lt;code&gt;high&lt;/code&gt; or below&lt;/td&gt;
&lt;td&gt;400 at every level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tool_choice&lt;/code&gt; of type &lt;code&gt;any&lt;/code&gt; / &lt;code&gt;tool&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Accepted&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;computer_20251124&lt;/code&gt; on Claude API / Google Cloud&lt;/td&gt;
&lt;td&gt;Accepted with beta header&lt;/td&gt;
&lt;td&gt;400 (toolset only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text between tool calls&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;text&lt;/code&gt; blocks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;thinking&lt;/code&gt; blocks, empty by default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The context window (1M tokens) and max output (128K; up to 300K on the Batch API with the &lt;code&gt;output-300k-2026-03-24&lt;/code&gt; beta header) are unchanged. The knowledge cutoff is June 2026, and Anthropic commits to not retiring the model before 22 September 2027. The numbers come from the &lt;a href="https://platform.claude.com/docs/en/models/opus-5-5/overview" rel="noopener noreferrer"&gt;Opus 5.5 model page&lt;/a&gt;, &lt;a href="https://platform.claude.com/docs/en/models/opus-5-5/whats-new-opus-5-5" rel="noopener noreferrer"&gt;what's new in Opus 5.5&lt;/a&gt; and the &lt;a href="https://platform.claude.com/docs/en/build-with-claude/fast-mode" rel="noopener noreferrer"&gt;fast mode docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four errors you'll see
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;"thinking.type.disabled" is not supported for this model&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"thinking.type.disabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Opus 5, thinking was on by default, but you could switch it off at &lt;code&gt;high&lt;/code&gt; effort or below — and latency-sensitive integrations often did. On Opus 5.5 adaptive thinking is always on, and &lt;code&gt;disabled&lt;/code&gt; returns a 400 at every effort level. (Manual &lt;code&gt;{"type": "enabled", "budget_tokens": N}&lt;/code&gt; budgets were already rejected on Opus 5, with the same message naming &lt;code&gt;"thinking.type.enabled"&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;The replacement for "off" is a lower effort:&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;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-opus-5-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output_config&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;effort&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;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# was: thinking={"type": "disabled"}
&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;prompt&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;The error is the easy part. The follow-on bug is in the code that reads the response. With thinking disabled, &lt;code&gt;resp.content[0]&lt;/code&gt; was always your text. Now a response can begin with one or more &lt;code&gt;thinking&lt;/code&gt; blocks, and at the default &lt;code&gt;display: "omitted"&lt;/code&gt; their text is an empty string, so they don't even look like content when you print them. &lt;code&gt;resp.content[0].text&lt;/code&gt; starts raising &lt;code&gt;AttributeError&lt;/code&gt; on some turns and not others, depending on whether the model decided to think. Select blocks by type, always:&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;text_of&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more consequences of "always on". Thinking tokens are billed as output tokens even when their text isn't returned to you, so effort now moves your bill whether or not you ever look at the reasoning. And &lt;code&gt;max_tokens&lt;/code&gt; covers thinking &lt;em&gt;plus&lt;/em&gt; the reply: a limit sized for Opus 5 with thinking off can now end replies early with &lt;code&gt;stop_reason: "max_tokens"&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;tool_choice: type "tool" and "any" are not supported for this model&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool_choice: type "tool" and "any" are not supported for this model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forced tool use is gone. &lt;code&gt;{"type": "any"}&lt;/code&gt; and &lt;code&gt;{"type": "tool", "name": ...}&lt;/code&gt; are rejected; &lt;code&gt;auto&lt;/code&gt; (the default) and &lt;code&gt;none&lt;/code&gt; still work. The same validation runs on the token-counting endpoint, so if a cost estimator counts tokens for the same request shape, it breaks too — in a different service, usually owned by a different person.&lt;/p&gt;

&lt;p&gt;Teams forced tools for two different jobs, and each job has its own replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job one: "give me JSON."&lt;/strong&gt; Forcing a single &lt;code&gt;extract&lt;/code&gt; tool was the classic way to get schema-shaped output. That job now belongs to structured outputs, which are generally available and need no beta header:&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;json&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;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-opus-5-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4_096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output_config&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;effort&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;low&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;format&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;json_schema&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;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;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;object&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;properties&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;vendor&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;string&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;total_cents&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;integer&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;due_date&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;string&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;format&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;date&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;required&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;vendor&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;total_cents&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;due_date&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;additionalProperties&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;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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extract the invoice fields:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice_text&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="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&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="nf"&gt;text_of&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON arrives in a &lt;code&gt;text&lt;/code&gt; block, which is one more reason &lt;code&gt;text_of()&lt;/code&gt; exists. Check the &lt;a href="https://platform.claude.com/docs/en/build-with-claude/structured-outputs" rel="noopener noreferrer"&gt;schema limitations&lt;/a&gt; before you port: numeric bounds (&lt;code&gt;minimum&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt;), string lengths and recursive schemas aren't supported, so keep validating those in code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job two: "call this tool now."&lt;/strong&gt; Where you forced a specific tool in an agent step, the migration guide's answer is to keep &lt;code&gt;tool_choice&lt;/code&gt; on &lt;code&gt;auto&lt;/code&gt;, add &lt;code&gt;strict: true&lt;/code&gt; so every call matches the tool's schema, and say in the prompt when the tool applies:&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;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-opus-5-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16_000&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="p"&gt;[{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&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="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;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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Look up order 4417 with get_order before you answer.&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;called_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you lose is the guarantee. Log &lt;code&gt;called_tool&lt;/code&gt; as a metric: a rising share of turns where the model answered in text instead of calling the tool is a prompt problem you want to see on a dashboard, not in a support ticket.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;'claude-opus-5-5' does not support tool types: computer_20251124.&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'claude-opus-5-5' does not support tool types: computer_20251124.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message goes on to list the tool types the model does accept. On the Claude API and Google Cloud, Opus 5.5 takes computer use only as the &lt;code&gt;computer_toolset_20260801&lt;/code&gt; toolset. Amazon Bedrock still accepts the old tool, which is a trap of its own: the same code passes on Bedrock and fails on the Claude API.&lt;/p&gt;

&lt;p&gt;The request change is small. Drop the beta header; the toolset entry takes no name and no display size:&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;# Before (Opus 5)
&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;beta&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-opus-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;betas&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;computer-use-2025-11-24&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="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;computer_20251124&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;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;computer&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;display_width_px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;display_height_px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;768&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="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After (Opus 5.5)
&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;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-opus-5-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&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="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;computer_toolset_20260801&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop change is bigger. With the toolset, the action is the &lt;code&gt;tool_use&lt;/code&gt; block's &lt;code&gt;name&lt;/code&gt;, not &lt;code&gt;input.action&lt;/code&gt;; one turn can carry several of those blocks; and every result has to echo &lt;code&gt;toolset_name&lt;/code&gt;. A dispatcher written as &lt;code&gt;match block.input["action"]&lt;/code&gt; needs a rewrite, not a rename. The &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool#migrate-from-computer-20251124" rel="noopener noreferrer"&gt;computer use migration notes&lt;/a&gt; list every change.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;Invalid `signature` in `thinking` block&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messages.{i}.content.{j}: Invalid `signature` in `thinking` block. The block is bound to a different conversation. Remove the block, or set `thinking.block_binding.prefix_mismatch_behavior` to "drop_block".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opus 5.5 inherits Fable 5.1's binding: a thinking block stays valid only while the &lt;code&gt;system&lt;/code&gt; prompt, the &lt;code&gt;tools&lt;/code&gt; and every message before it are unchanged. For accounts created on or after 31 August 2026, editing any of those and replaying the block returns this 400; older accounts get the check only when they opt in. Rebuilding the system prompt per request, trimming old turns on the client, or injecting a reminder and deleting it on the next call all trigger it, and retrying the same request body won't clear it.&lt;/p&gt;

&lt;p&gt;The fix is structural: keep the history append-only, change instructions with mid-conversation system messages instead of edits, and leave trimming to server-side compaction or context editing. It is the same discipline as &lt;a href="https://cursuri-ai.ro/en/courses/context-engineering-and-memory-for-ai-agents" rel="noopener noreferrer"&gt;keeping agent context append-only&lt;/a&gt; for caching reasons, except that on Opus 5.5 it is also a correctness rule.&lt;/p&gt;

&lt;p&gt;What is new with Opus 5.5 is &lt;em&gt;which models can read whose reasoning&lt;/em&gt;, and that part never throws:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Conversation moves…&lt;/th&gt;
&lt;th&gt;Reasoning carried over?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5 (or earlier Opus, Sonnet, Haiku) → Opus 5.5&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5 → Fable 5.1 or Mythos 5.1 (Claude API)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5 → any other model (Sonnet 5, Haiku 4.5, Opus 5, Opus 4.8…)&lt;/td&gt;
&lt;td&gt;No, blocks silently dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable or Mythos → Opus 5.5&lt;/td&gt;
&lt;td&gt;No, blocks silently dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A block the target model can't read is removed before the model sees it: the request succeeds, and the dropped blocks aren't billed. With the &lt;code&gt;thinking-binding-controls-2026-08-01&lt;/code&gt; beta header, the drop is reported in a top-level &lt;code&gt;input_transformations&lt;/code&gt; array. Turn that on in any service that routes between models, or you will never know it happened.&lt;/p&gt;

&lt;p&gt;The design consequence for routers: &lt;strong&gt;escalate up, don't hand down.&lt;/strong&gt; Start a hard task on Opus 5.5 and escalate to Fable 5.1 when it stalls, and the escalated model keeps everything Opus 5.5 reasoned so far. Route the other way — Fable plans, Opus executes in the same conversation — and every hand-off starts cold. The same applies to refusal fallbacks and to mid-conversation downgrades that save money. If your system is built on &lt;a href="https://cursuri-ai.ro/en/courses/ai-agents-architecture-and-automation" rel="noopener noreferrer"&gt;routing and escalation between agents&lt;/a&gt;, this table belongs in its design doc.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three changes you won't see
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The default effort dropped a level, and the benchmarks didn't
&lt;/h3&gt;

&lt;p&gt;A request that omits &lt;code&gt;effort&lt;/code&gt; ran at &lt;code&gt;high&lt;/code&gt; on Opus 5. On Opus 5.5 it runs at &lt;code&gt;medium&lt;/code&gt;. No error, no warning, different behavior.&lt;/p&gt;

&lt;p&gt;Anthropic's position is that this is fine: in its testing, Opus 5.5 at &lt;code&gt;medium&lt;/code&gt; matches or beats Opus 5 at &lt;code&gt;high&lt;/code&gt; on coding and knowledge-work evaluations, and on several coding evaluations &lt;code&gt;low&lt;/code&gt; comes close at much lower cost. That may well hold for your workload. But read the &lt;a href="https://www.anthropic.com/news/claude-opus-5-5" rel="noopener noreferrer"&gt;announcement's&lt;/a&gt; fine print before you rely on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The benchmark table is run at &lt;strong&gt;max&lt;/strong&gt; effort: "unless otherwise noted, all Claude Opus 5.5 results use adaptive thinking at max effort."&lt;/li&gt;
&lt;li&gt;The cost claim, 40% less than Opus 5 on typical workloads, is "at default settings", which means &lt;strong&gt;medium&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both statements are true. They describe different configurations. The model that scores 66.4% on Terminal-Bench 4.0 and the model that costs 40% less are two settings of the same weights, and you choose one per request.&lt;/p&gt;

&lt;p&gt;Two more effort facts change the arithmetic. At any given level, Opus 5.5 tends to think &lt;em&gt;more&lt;/em&gt; per turn than Opus 5, most of all at &lt;code&gt;xhigh&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt;, so carrying your Opus 5 setting over means longer turns and more output tokens, not the same bill. And level names don't map across models: Opus 5.5's &lt;code&gt;medium&lt;/code&gt; is not Opus 5's &lt;code&gt;medium&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So don't carry settings over. Re-run the sweep. Each case is a prompt plus a check function, the same shape as any eval:&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;time&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;client&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;USD_IN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;USD_OUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;   &lt;span class="c1"&gt;# claude-opus-5-5 list prices
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_case&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&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;monotonic&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;client&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="c1"&gt;# stream long turns
&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-opus-5-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;output_config&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;effort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;effort&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;case&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&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="k"&gt;as&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;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&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="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;passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nf"&gt;text_of&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seconds&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&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;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;USD_IN&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;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;USD_OUT&lt;/span&gt;&lt;span class="p"&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;sweep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;levels&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;low&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;medium&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;high&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;xhigh&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="bp"&gt;None&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;effort&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;run_case&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effort&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;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;n&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;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;effort&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  pass &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&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;passed&lt;/span&gt;&lt;span class="sh"&gt;'&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="o"&gt;%&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&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;seconds&lt;/span&gt;&lt;span class="sh"&gt;'&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s  &lt;/span&gt;&lt;span class="sh"&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;avg $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&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;usd&lt;/span&gt;&lt;span class="sh"&gt;'&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the lowest level that holds your pass rate, set it explicitly in code (never rely on a default that has changed once and can change again), and leave &lt;code&gt;max_tokens&lt;/code&gt; room for thinking; Anthropic reports that 128,000, the model's maximum, worked well for long agentic coding turns. If one conversation needs different levels at different points, use a per-message effort change (beta header &lt;code&gt;mid-conversation-output-config-2026-07-01&lt;/code&gt;), because changing the top-level value between requests invalidates the prompt cache. The sweep above is deliberately tiny. The version that earns its keep is &lt;a href="https://cursuri-ai.ro/en/courses/llm-evaluation-and-testing" rel="noopener noreferrer"&gt;a real eval harness&lt;/a&gt; with your own traffic as the dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Your agent's progress messages went quiet
&lt;/h3&gt;

&lt;p&gt;On Opus 5, the short notes the model writes between tool calls ("Found the failing test, checking the auth module next") came back as &lt;code&gt;text&lt;/code&gt; blocks. Plenty of agent UIs stream exactly those as a status line.&lt;/p&gt;

&lt;p&gt;On Opus 5.5, as on Fable 5.1, those notes arrive as &lt;strong&gt;progress-update &lt;code&gt;thinking&lt;/code&gt; blocks&lt;/strong&gt;, at most one before each tool call. At the default &lt;code&gt;display: "omitted"&lt;/code&gt; their text is an empty string. Nothing fails. Your UI just shows a spinner for three minutes where it used to narrate.&lt;/p&gt;

&lt;p&gt;To get the notes back, request &lt;code&gt;display: "updates"&lt;/code&gt; (beta), which keeps reasoning hidden and returns progress updates as text, and render any non-empty thinking delta:&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;with&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;beta&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;thinking&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;adaptive&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;display&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;updates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;betas&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;thinking-display-updates-2026-08-18&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content_block_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thinking_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;         &lt;span class="c1"&gt;# non-empty text = a progress update
&lt;/span&gt;            &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&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;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;final&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;# unchanged, see error 4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details. Send the blocks back unchanged with the rest of the assistant turn. Expect fewer updates at higher effort and in long tool chains, and a pause of several seconds before each one. And when a response stops on &lt;code&gt;max_tokens&lt;/code&gt; right after a tool call, the last progress block reads &lt;code&gt;This part of the response was interrupted before it finished.&lt;/code&gt;: show it rather than swallow it. &lt;code&gt;display: "summarized"&lt;/code&gt; also returns the updates, but mixed with reasoning summaries you can't tell apart.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Your unattended agent stops halfway
&lt;/h3&gt;

&lt;p&gt;On long, multi-part tasks Opus 5.5 reports progress readily, and some of those reports end the turn with text instead of a tool call: &lt;code&gt;stop_reason: "end_turn"&lt;/code&gt;, a tidy summary, and "next, I'll migrate the remaining two endpoints." A loop that treats &lt;code&gt;end_turn&lt;/code&gt; as "task complete" exits right there. In an interactive product that is a feature. In a nightly job it is a half-done migration that looks finished in the logs.&lt;/p&gt;

&lt;p&gt;Anthropic's &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5-5" rel="noopener noreferrer"&gt;prompting guide for Opus 5.5&lt;/a&gt; says to treat a text-only end of turn as a report, not as proof of completion: keep the task's parts in a checklist (a to-do tool, a file), and if items remain open with no blocker stated, nudge — two or three times at most:&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;MAX_NUDGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_unattended&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checklist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&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;task&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="n"&gt;nudges&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;while&lt;/span&gt; &lt;span class="bp"&gt;True&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;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;output_config&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;effort&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;medium&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&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;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="k"&gt;if&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;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;tool_use&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="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="nf"&gt;run_tools&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="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;open_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checklist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;# your state, not the model's summary
&lt;/span&gt;        &lt;span class="k"&gt;if&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;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;end_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;open_items&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;nudges&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_NUDGES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;nudges&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;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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your task list still has open items: &lt;/span&gt;&lt;span class="si"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open_items&lt;/span&gt;&lt;span class="p"&gt;)&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Continue with them. If one is blocked, say what is blocking it.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)})&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;   &lt;span class="c1"&gt;# finished, refused, out of tokens, or stuck: a human looks next
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checklist is the important part: completion comes from your state, not from the tone of the model's last message. And cap the nudges. An agent that is genuinely stuck should stop and be reviewed, not loop politely forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more non-error: refusals have new categories
&lt;/h2&gt;

&lt;p&gt;Opus 5.5 runs a biology classifier in addition to the cybersecurity one, plus a new &lt;code&gt;reasoning_extraction&lt;/code&gt; category. A decline is an HTTP 200 with &lt;code&gt;stop_reason: "refusal"&lt;/code&gt; and a &lt;code&gt;stop_details.category&lt;/code&gt;, so code that only checks the status code logs it as a success with an empty answer. Two practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompts that tell the model to write out its reasoning in the response, a common workaround from the thinking-disabled days, can be declined as &lt;code&gt;reasoning_extraction&lt;/code&gt;. Remove those instructions, and read &lt;code&gt;display: "summarized"&lt;/code&gt; thinking if you need the reasoning.&lt;/li&gt;
&lt;li&gt;Server-side fallback (&lt;code&gt;fallbacks: "default"&lt;/code&gt;, beta header &lt;code&gt;server-side-fallback-2026-07-01&lt;/code&gt;) retries a declined request on a recommended model, except for &lt;code&gt;reasoning_extraction&lt;/code&gt;, which comes straight back to you. And a fallback to another model runs without Opus 5.5's reasoning; see the table under error 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A refusal that arrives before any output isn't billed, but it still counts against your rate limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The money, honestly
&lt;/h2&gt;

&lt;p&gt;At identical token counts the list-price drop is real. One agent turn with a 60K-token cached prefix, 3K fresh input tokens and 2K output tokens:&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;Opus 5&lt;/th&gt;
&lt;th&gt;Opus 5.5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;60K cache read&lt;/td&gt;
&lt;td&gt;$0.030&lt;/td&gt;
&lt;td&gt;$0.012&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3K input&lt;/td&gt;
&lt;td&gt;$0.015&lt;/td&gt;
&lt;td&gt;$0.012&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2K output&lt;/td&gt;
&lt;td&gt;$0.050&lt;/td&gt;
&lt;td&gt;$0.040&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per turn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.095&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.064 (−33%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Token counts won't stay identical, though, and they move in opposite directions: more thinking per turn at a given effort pushes output up, while a lower default effort and (according to Anthropic and its early testers) fewer steps per task push it down. The honest answer to "will my bill drop?" is "re-baseline and see", which the sweep above already does. Two other levers: the Batch API halves everything ($2/$10) for work that can wait, and fast mode ($8/$40, up to 2.5× more output tokens per second, Claude API only, research preview) is for when latency is worth paying double. Requests at different speeds don't share cached prefixes, so don't flip fast mode on and off within a conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opus 5.5 or Fable 5.1?
&lt;/h2&gt;

&lt;p&gt;Vendor-reported results, at max effort, from Anthropic's announcement:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Opus 5.5&lt;/th&gt;
&lt;th&gt;Fable 5.1&lt;/th&gt;
&lt;th&gt;Opus 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench 4.0&lt;/td&gt;
&lt;td&gt;66.4%&lt;/td&gt;
&lt;td&gt;55.8%&lt;/td&gt;
&lt;td&gt;52.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CursorBench 4.0&lt;/td&gt;
&lt;td&gt;57.8%&lt;/td&gt;
&lt;td&gt;51.8%&lt;/td&gt;
&lt;td&gt;46.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPval-AA v2.1 (Elo)&lt;/td&gt;
&lt;td&gt;1846&lt;/td&gt;
&lt;td&gt;1735&lt;/td&gt;
&lt;td&gt;1708&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSWorld 2.0 (partial)&lt;/td&gt;
&lt;td&gt;81.8%&lt;/td&gt;
&lt;td&gt;80.7%&lt;/td&gt;
&lt;td&gt;74.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same table puts GPT-6 Astra ahead of Opus 5.5 on AutomationBench (41.4% vs 40.0%) and Terminal-Bench-Science (64.6% vs 58.7%), a useful reminder that "the leading model" depends on which row you read.&lt;/p&gt;

&lt;p&gt;At $4/$20 against Fable 5.1's $10/$50, the default move for most workloads is Opus 5.5, with Fable 5.1 kept as the escalation tier rather than retired. Thanks to the compatibility table above, that direction — Opus 5.5 first, Fable 5.1 on escalation — is also the one that doesn't throw away reasoning. One caveat for cache-heavy traffic: Fable 5.1's cache read is $0.25 per MTok, close to Opus 5.5's $0.20, so for workloads dominated by cached context the saving comes mostly from output and fresh input, and it is smaller than the list prices suggest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Model ID → &lt;code&gt;claude-opus-5-5&lt;/code&gt; (on Bedrock: &lt;code&gt;anthropic.claude-opus-5-5&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Remove &lt;code&gt;thinking: {"type": "disabled"}&lt;/code&gt;; set &lt;code&gt;output_config.effort&lt;/code&gt; explicitly&lt;/li&gt;
&lt;li&gt;[ ] Read content by block &lt;code&gt;type&lt;/code&gt;, never &lt;code&gt;content[0].text&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Raise &lt;code&gt;max_tokens&lt;/code&gt; to leave room for thinking&lt;/li&gt;
&lt;li&gt;[ ] Replace forced &lt;code&gt;tool_choice&lt;/code&gt;: structured outputs for JSON jobs, &lt;code&gt;auto&lt;/code&gt; + &lt;code&gt;strict: true&lt;/code&gt; + a prompt instruction for tool jobs, including in token-counting calls&lt;/li&gt;
&lt;li&gt;[ ] Computer use on the Claude API or Google Cloud → &lt;code&gt;computer_toolset_20260801&lt;/code&gt;, and rewrite the dispatcher&lt;/li&gt;
&lt;li&gt;[ ] No edits to &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt; or earlier turns mid-conversation; report &lt;code&gt;input_transformations&lt;/code&gt; wherever you route between models&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;display: "updates"&lt;/code&gt; if your UI shows progress between tool calls&lt;/li&gt;
&lt;li&gt;[ ] Unattended loops: completion comes from a checklist, with capped nudges on a text-only &lt;code&gt;end_turn&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Handle &lt;code&gt;stop_reason: "refusal"&lt;/code&gt;; remove "write out your reasoning" instructions&lt;/li&gt;
&lt;li&gt;[ ] Re-run the effort sweep and re-baseline cost and latency before moving production traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team works in Claude Code, the bundled Claude API skill automates the mechanical half: &lt;code&gt;/claude-api migrate this project to claude-opus-5-5&lt;/code&gt; swaps model IDs, fixes the breaking parameters, calibrates effort and hands back a list of things to verify by hand. It can't know that your UI depends on text between tool calls, or that your nightly job trusts &lt;code&gt;end_turn&lt;/code&gt;; those are the items only your tests will catch. Structured &lt;a href="https://cursuri-ai.ro/en/courses/claude-code-mastery-agentic-coding" rel="noopener noreferrer"&gt;Claude Code workflows&lt;/a&gt; — plan, migrate, verify against a test suite — are what turn that checklist into a one-afternoon job.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is Claude Opus 5.5 cheaper than Opus 5?&lt;/strong&gt;&lt;br&gt;
On list price, yes: $4/$20 against $5/$25 per million tokens, and cache reads fall from $0.50 to $0.20. Anthropic says typical workloads cost about 40% less at default settings, but the default effort is also lower, so compare the two at the effort level you will actually run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I still turn thinking off on Opus 5.5?&lt;/strong&gt;&lt;br&gt;
No. Thinking is always on, and &lt;code&gt;disabled&lt;/code&gt; returns a 400. Use &lt;code&gt;effort: "low"&lt;/code&gt; for latency-sensitive paths, and keep &lt;code&gt;display: "omitted"&lt;/code&gt; (the default) if you only want thinking text out of your responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will conversations started on Opus 5 continue on Opus 5.5?&lt;/strong&gt;&lt;br&gt;
Yes. Opus 5.5 reads thinking blocks from Opus 5 and earlier Opus, Sonnet and Haiku models, so in-flight conversations keep their reasoning. The reverse doesn't hold: hand an Opus 5.5 conversation back to Opus 5 and its reasoning is dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does anything differ on Bedrock, Google Cloud or Microsoft Foundry?&lt;/strong&gt;&lt;br&gt;
The thinking, tool-choice and history changes apply on every platform. Computer use is the exception: the old &lt;code&gt;computer_20251124&lt;/code&gt; tool is rejected on the Claude API and Google Cloud but keeps working on Amazon Bedrock; for Foundry, check the compatibility table in the computer use docs. Fast mode exists only on the Claude API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I get guaranteed JSON now that I can't force a tool?&lt;/strong&gt;&lt;br&gt;
Use structured outputs: &lt;code&gt;output_config.format&lt;/code&gt; with a JSON schema. They are generally available and return the JSON in a text block.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;On paper, Opus 5.5 is an easy upgrade: cheaper, faster and, by Anthropic's numbers, stronger on agentic work. The four 400s make sure you fix the obvious parts, so spend your testing time on the other three. Set effort explicitly and sweep it again, render progress from thinking blocks, and take task completion from your own state rather than from the model's last sentence. None of those three will ever show up as an error in your logs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and teach production AI systems at &lt;a href="https://cursuri-ai.ro/en/courses" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, Eastern Europe's AI education platform — hands-on courses on LLM integration, agent architecture, evaluation, and shipping model upgrades without surprises.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Model in Your Config Has an Expiry Date — a Runbook for LLM Deprecations</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Mon, 31 Aug 2026 22:05:44 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/the-model-in-your-config-has-an-expiry-date-a-runbook-for-llm-deprecations-e1p</link>
      <guid>https://dev.to/cursuri-ai/the-model-in-your-config-has-an-expiry-date-a-runbook-for-llm-deprecations-e1p</guid>
      <description>&lt;p&gt;There is a category of production incident that has no bug, no bad deploy, and no root cause inside your codebase. Your code is byte-identical to the version that worked yesterday. Your tests pass. Your dependencies are pinned. And a feature is returning 404s.&lt;/p&gt;

&lt;p&gt;The model string in your config refers to something that no longer exists.&lt;/p&gt;

&lt;p&gt;This is the strangest failure mode in the whole LLM stack, because it is the only one that arrives entirely on somebody else's schedule — and the only one you were told about, in writing, months in advance, in an email nobody on the on-call rotation read.&lt;/p&gt;

&lt;p&gt;I teach AI engineering at &lt;a href="https://cursuri-ai.ro" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, and model retirement is the incident I see teams handle worst, not because it's hard, but because it doesn't feel like engineering work until the day it does. Here's the whole thing: what actually breaks, what the provider tells you and when, and the runbook that turns a surprise outage into a boring Tuesday ticket.&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%2Fcursuri-ai.ro%2Fimages%2Fblog%2Fmodel-deprecation-runbook-en.svg" 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%2Fcursuri-ai.ro%2Fimages%2Fblog%2Fmodel-deprecation-runbook-en.svg" alt="Model deprecation runbook — lifecycle states, the 60-day window, and the five-step migration" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The vocabulary you're missing two-thirds of
&lt;/h2&gt;

&lt;p&gt;Most teams have two mental states for a model: "it works" and "it's old." Anthropic's &lt;a href="https://platform.claude.com/docs/en/about-claude/model-deprecations" rel="noopener noreferrer"&gt;model deprecations page&lt;/a&gt; defines four, and the gap between them is where the incident lives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Active&lt;/strong&gt; — fully supported, recommended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy&lt;/strong&gt; — no longer receiving updates, may be deprecated in future.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deprecated&lt;/strong&gt; — still functional, no longer recommended, &lt;strong&gt;has an assigned retirement date&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retired&lt;/strong&gt; — gone. Requests fail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important one is &lt;em&gt;deprecated&lt;/em&gt;. A deprecated model works perfectly. Latency is normal, quality is normal, your dashboards are green. Nothing in the response payload tells you a clock is running. The only signal is a documentation page and an email to the account owner — who is usually in finance.&lt;/p&gt;

&lt;p&gt;And "retired" means what it says. The model ID stops resolving, and the API answers the way it answers any unknown model ID: HTTP &lt;strong&gt;404&lt;/strong&gt;, &lt;code&gt;not_found_error&lt;/code&gt;, whose documented cause is "invalid endpoint or model ID." Your retry logic will not help you, because 404 is not retryable. If your error handling catches one broad exception class and retries everything, you'll spend the outage generating three times the traffic and no useful log lines.&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;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&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="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&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;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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CHAT_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;1024&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;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&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;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# This is not a blip. This is a retired or misspelled model ID.
&lt;/span&gt;    &lt;span class="c1"&gt;# Page someone. Do not retry.
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;span class="k"&gt;except&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;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;except&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;APIConnectionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That first &lt;code&gt;except&lt;/code&gt; block is maybe ninety seconds of work, and it's the difference between an alert that says &lt;em&gt;"the model is gone"&lt;/em&gt; and an alert that says &lt;em&gt;"elevated error rate, service degraded."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The dates are published before the deprecation, and almost nobody reads them
&lt;/h2&gt;

&lt;p&gt;Here is the part that genuinely surprises people, and it's the single most useful fact in this article.&lt;/p&gt;

&lt;p&gt;Anthropic publishes &lt;strong&gt;tentative retirement dates for models that are currently active&lt;/strong&gt;. Not deprecated — active. As of the end of August 2026, the model status table reads, in part:&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;State&lt;/th&gt;
&lt;th&gt;Earliest retirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-4-5-20250929&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Not sooner than September 29, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Not sooner than October 15, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-5-20251101&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Not sooner than November 24, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Not sooner than February 5, 2027&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude-opus-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Not sooner than July 24, 2027&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;Active&lt;/td&gt;
&lt;td&gt;Not sooner than June 9, 2027&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the first row again. If you are running &lt;code&gt;claude-sonnet-4-5-20250929&lt;/code&gt; in production today, its published floor is roughly four weeks out. That is not a retirement announcement — "not sooner than" is a floor, not a date, and no deprecation has been announced for it. But it is a planning input you already have, for free, and it costs you nothing to put in a calendar.&lt;/p&gt;

&lt;p&gt;The counterpart is the models that already went: &lt;code&gt;claude-opus-4-1-20250805&lt;/code&gt; was deprecated on June 5, 2026 and &lt;strong&gt;retired on August 5, 2026&lt;/strong&gt; — three weeks before this article. &lt;code&gt;claude-opus-4-20250514&lt;/code&gt; and &lt;code&gt;claude-sonnet-4-20250514&lt;/code&gt; were deprecated April 14, 2026 and retired June 15, 2026.&lt;/p&gt;

&lt;p&gt;Do the arithmetic on those pairs: 61 days, and 62 days. Anthropic commits to "at least 60 days' notice before model retirement for publicly released models," and in practice the notice window is &lt;em&gt;exactly that&lt;/em&gt;. Sixty days is enough time to run a migration. It is not enough time to build an eval suite, get budget approval, and negotiate with a team that owns the prompt. That work has to already exist when the email lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aliases are a strategy, not a shield
&lt;/h2&gt;

&lt;p&gt;Anthropic exposes two shapes of model ID: aliases like &lt;code&gt;claude-opus-5&lt;/code&gt; or &lt;code&gt;claude-sonnet-4-6&lt;/code&gt;, and dated snapshots like &lt;code&gt;claude-sonnet-4-5-20250929&lt;/code&gt;. Note that the current generation are &lt;em&gt;complete as aliases&lt;/em&gt; — &lt;code&gt;claude-opus-5&lt;/code&gt; is the whole ID, and appending a date suffix to it produces a 404 as surely as a retired model does.&lt;/p&gt;

&lt;p&gt;Neither shape saves you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A dated snapshot&lt;/strong&gt; is reproducible and will never change behavior under you — and it is precisely what appears in the retirement table, by name, with a date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An alias&lt;/strong&gt; may keep resolving across point releases, but the alias is scoped to a model family. When the family goes, the alias goes. And an alias that silently starts pointing somewhere new is its own kind of incident: your outputs change, your evals shift, and nothing in your diff explains it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real answer isn't picking one. It's picking one &lt;strong&gt;per route, deliberately, and writing down why&lt;/strong&gt;. A billing-adjacent classifier that must produce identical output for audit reasons wants a snapshot and a calendar entry. A chat surface where quality improvements are welcome wants an alias and an eval suite that runs on a schedule. What you must not have is thirty model strings scattered across a codebase, each chosen by whoever wrote that file, with no owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is never just the model string
&lt;/h2&gt;

&lt;p&gt;This is the trap that turns a one-line change into a two-week migration, and it's why "we'll swap it when we have to" fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request parameters retire too.&lt;/strong&gt; On Claude Opus 4.7 and later, &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p&lt;/code&gt; and &lt;code&gt;top_k&lt;/code&gt; return a &lt;strong&gt;400 error&lt;/strong&gt; when set to a non-default value — the recommended replacement is to omit them and steer behavior through prompting. And it's not only the API: the Python SDK v1.0 and later &lt;em&gt;removes&lt;/em&gt; those parameters from the request types, so passing them raises a &lt;code&gt;TypeError&lt;/code&gt; before a request is ever made.&lt;/p&gt;

&lt;p&gt;The same applies across the current generation: the fixed thinking budget (&lt;code&gt;thinking: {type: "enabled", budget_tokens: N}&lt;/code&gt;) is gone in favor of adaptive thinking plus an effort level, and assistant-message prefill — the classic trick for forcing a response format — returns a 400 on the whole 4.6-and-later family. If your "quick model swap" touches code written against a 2025-era API, you are not changing a string. You are rewriting the request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Written for an older model. Every line here is now a 400 or a TypeError.
&lt;/span&gt;&lt;span class="n"&gt;response&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;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-3-5-sonnet-20241022&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# retired October 28, 2025
&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;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;# 400 on Opus 4.7+ (non-default)
&lt;/span&gt;    &lt;span class="n"&gt;thinking&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;enabled&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;budget_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;8000&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# removed
&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="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;prompt&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;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="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="c1"&gt;# prefill: 400
&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="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The current shape.
&lt;/span&gt;&lt;span class="n"&gt;response&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;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-opus-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;thinking&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;adaptive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;output_config&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;effort&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;high&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;format&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;json_schema&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;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INVOICE_SCHEMA&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;prompt&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;&lt;strong&gt;And prompts are part of the migration.&lt;/strong&gt; This is the least mechanical and most expensive piece. Prompts tuned against an older model are frequently over-prescriptive for a newer one — step-by-step scaffolding that raised quality two generations ago can now suppress it. A migration that swaps the ID, passes your smoke test, and ships is a migration that quietly degraded output quality in a way you'll attribute to something else three weeks later. This is exactly the failure mode that makes an &lt;a href="https://cursuri-ai.ro/en/courses/llm-evaluation-and-testing" rel="noopener noreferrer"&gt;evaluation suite&lt;/a&gt; the load-bearing part of the runbook rather than the optional part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The runbook
&lt;/h2&gt;

&lt;p&gt;Five steps. The first three are cheap and you should do them this week, whether or not anything of yours is deprecated.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Inventory: find every model string you own
&lt;/h3&gt;

&lt;p&gt;You cannot plan a migration you can't enumerate. Two passes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Every model ID that lives in code, config, notebooks, IaC, or a CI secret file.&lt;/span&gt;
rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--hidden&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="s1"&gt;'!node_modules'&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="s1"&gt;'!.git'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="s1"&gt;'claude-(fable|mythos|opus|sonnet|haiku)-[0-9a-z.\-]*|claude-[0-9]'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the pass that matters more, because it catches the jobs nobody remembers: in the Claude Console, open &lt;strong&gt;Usage → Export&lt;/strong&gt;, and read the CSV. It breaks usage down &lt;strong&gt;by API key and by model&lt;/strong&gt;, which means it shows you the nightly batch job in a repo you don't own, running under a key that was issued in 2025. That report is the actual inventory. The grep is just the part you control.&lt;/p&gt;

&lt;p&gt;The output of this step is a single table — route, model ID, owner, alias-or-snapshot, why. Keep it in the repo, not in a wiki nobody opens.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Centralize the strings
&lt;/h3&gt;

&lt;p&gt;Every model ID resolves through one module. Not because indirection is beautiful, but because a migration should be one reviewable diff, and because it gives you a place to hang the metadata:&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;# models.py — the only file in the repo that contains a model ID.
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModelChoice&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="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# who approves a change
&lt;/span&gt;    &lt;span class="n"&gt;pinned_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# why this shape, not the other one
&lt;/span&gt;    &lt;span class="n"&gt;review_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;      &lt;span class="c1"&gt;# ISO date, from the published retirement floor
&lt;/span&gt;
&lt;span class="n"&gt;CHAT&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModelChoice&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-5&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;platform&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;quality-led surface, alias by choice&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;2027-07-24&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CLASSIFIER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModelChoice&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-haiku-4-5&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;platform&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;high volume, latency-sensitive&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;2026-10-15&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;&lt;code&gt;review_by&lt;/code&gt; is not a guess. It is the published "not sooner than" date, copied in. Now your calendar and your code agree.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A canary that fails loudly and cheaply
&lt;/h3&gt;

&lt;p&gt;A retirement should be caught by a scheduled job, not by a user. This is about eight lines and costs a fraction of a cent per run:&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;# canary.py — run daily in CI. Alerts on the one error that is never transient.
&lt;/span&gt;&lt;span class="kn"&gt;import&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;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&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;CLASSIFIER&lt;/span&gt;

&lt;span class="n"&gt;client&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="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;=&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;choice&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;CHAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CLASSIFIER&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;client&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;choice&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="n"&gt;max_tokens&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping&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="k"&gt;except&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;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dead&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;choice&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;dead&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;choice&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MODEL GONE: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (owner: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&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="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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&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="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the inventory half, &lt;code&gt;client.models.list()&lt;/code&gt; gives you what your key can currently reach, with &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;display_name&lt;/code&gt;, &lt;code&gt;max_input_tokens&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt; and a capabilities tree — useful for catching the day a model stops appearing, and for asserting that a replacement actually supports the features your route depends on before you switch to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The eval gate, built before you need it
&lt;/h3&gt;

&lt;p&gt;Sixty days is plenty of time to change a string and not nearly enough to answer &lt;em&gt;"is the new one better on our task?"&lt;/em&gt; if you're starting from zero. The gate needs three things and they take an afternoon each:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A frozen set of real inputs.&lt;/strong&gt; Fifty to two hundred requests sampled from production, with the messy ones deliberately over-represented. Not synthetic examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A grader that isn't a vibe.&lt;/strong&gt; Exact match where you can get it, schema validation where the output is structured, an LLM judge with a written rubric where you can't. The judge is fine — an unwritten rubric is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cost and latency baseline.&lt;/strong&gt; Because the honest outcome of a migration is sometimes "quality held, p95 doubled," and you want to know that before your users do.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run it against the old model and the new one on the same inputs, same day. The comparison is the deliverable, and it's the artifact that lets you say "yes, ship it" in an afternoon instead of a fortnight. If you're standing this up for the first time, this is the same machinery you need for every other change you'll ever make to an LLM feature — &lt;a href="https://cursuri-ai.ro/en/courses/llm-evaluation-and-testing" rel="noopener noreferrer"&gt;building it properly once&lt;/a&gt; pays for itself on the second migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Migrate the request, then the prompt, then the model
&lt;/h3&gt;

&lt;p&gt;Order matters, because it isolates variables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Modernize the request shape on the model you're already running&lt;/strong&gt; — remove sampling parameters, replace fixed thinking budgets with adaptive thinking plus an effort level, replace prefill with structured outputs. Ship it. Nothing about behavior should change, and if something does, you've learned it in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the model ID&lt;/strong&gt; behind a flag, run the eval gate, compare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-tune the prompt for the new model&lt;/strong&gt; — usually by &lt;em&gt;deleting&lt;/em&gt; scaffolding rather than adding it — and re-run the gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roll out by percentage&lt;/strong&gt;, keeping the old model reachable until its retirement date, not until your rollout finishes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Doing all four at once produces a result you can't attribute. Doing them in order takes the same total effort and tells you which change did what.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the provider actually owes you, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;Worth being clear-eyed here, because the answer is better than the ecosystem average and still not a guarantee.&lt;/p&gt;

&lt;p&gt;You get: at least 60 days' notice before retirement of a publicly released model, email to affected accounts with active deployments, published tentative floors for active models, a recommended replacement for every deprecated model, and a self-service usage audit. Anthropic has also published &lt;a href="https://www.anthropic.com/research/deprecation-commitments" rel="noopener noreferrer"&gt;commitments on model deprecation and preservation&lt;/a&gt;, including long-term preservation of model weights, and states openly that retirement exists to free capacity — with real downsides for people who depended on a specific model's behavior.&lt;/p&gt;

&lt;p&gt;You don't get: a promise that the replacement behaves like the old one on &lt;em&gt;your&lt;/em&gt; task. That has never been promised by anyone, and it's the only part that requires actual work from you.&lt;/p&gt;

&lt;p&gt;One more thing worth knowing if you're multi-cloud: those dates apply to Anthropic-operated platforms — the Claude API, Claude Platform on AWS, and Microsoft Foundry. Amazon Bedrock and Google Cloud set their own retirement schedules, so a model's status and dates can differ there. If your failover path routes to a different platform, that path has its own calendar, and it is not this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Print this. It's the whole article.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every model ID in the codebase resolves through one module.&lt;/li&gt;
&lt;li&gt;[ ] That module records, per route: owner, alias-or-snapshot, and the reason.&lt;/li&gt;
&lt;li&gt;[ ] The published "not sooner than" date for each model is in the code &lt;em&gt;and&lt;/em&gt; in a shared calendar.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;NotFoundError&lt;/code&gt; / HTTP 404 is caught separately and pages a human. It is never retried.&lt;/li&gt;
&lt;li&gt;[ ] A daily canary calls every configured model with &lt;code&gt;max_tokens=1&lt;/code&gt; and fails the build when one is gone.&lt;/li&gt;
&lt;li&gt;[ ] The Console usage export has been read at least once this quarter, by a human, looking for keys and models nobody claims.&lt;/li&gt;
&lt;li&gt;[ ] An eval suite with real inputs and a written rubric exists &lt;em&gt;today&lt;/em&gt;, not on announcement day.&lt;/li&gt;
&lt;li&gt;[ ] The request shape is modernized independently of the model swap.&lt;/li&gt;
&lt;li&gt;[ ] Prompt re-tuning is scheduled as part of every migration, not treated as optional.&lt;/li&gt;
&lt;li&gt;[ ] Anyone routing through Bedrock or Vertex has checked those platforms' separate schedules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is clever. All of it is the difference between a migration you schedule and an outage you explain.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and teach this stack at &lt;a href="https://cursuri-ai.ro/en/courses" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt; — including &lt;a href="https://cursuri-ai.ro/en/courses/advanced-llm-integration-in-production" rel="noopener noreferrer"&gt;taking LLM features to production&lt;/a&gt; and &lt;a href="https://cursuri-ai.ro/en/courses/build-and-ship-a-production-ai-saas" rel="noopener noreferrer"&gt;shipping a full AI product end to end&lt;/a&gt;. If your team has a model string you can't account for, the inventory step is genuinely the highest-value hour you'll spend this month.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your LLM Stream Works on localhost and Dies in Production — Fixing SSE End to End</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Sat, 22 Aug 2026 11:39:18 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/your-llm-stream-works-on-localhost-and-dies-in-production-fixing-sse-end-to-end-1748</link>
      <guid>https://dev.to/cursuri-ai/your-llm-stream-works-on-localhost-and-dies-in-production-fixing-sse-end-to-end-1748</guid>
      <description>&lt;p&gt;Streaming an LLM response looks like the easiest feature in your product. The SDK gives you an iterator, you print tokens, it works on your laptop in under a minute.&lt;/p&gt;

&lt;p&gt;Then you deploy it, and one of these happens: the response arrives all at once after 40 seconds instead of token by token. Or the connection drops mid-answer with no error anywhere in your logs. Or a user closes the tab and you keep paying for 60,000 tokens nobody will ever read.&lt;/p&gt;

&lt;p&gt;None of those are model problems. They're the six things that break in the space between &lt;code&gt;api.anthropic.com&lt;/code&gt; and a browser tab — a proxy, a load balancer, an HTTP client, and a JavaScript API that each have opinions about long-lived responses.&lt;/p&gt;

&lt;p&gt;I teach AI engineering at &lt;a href="https://cursuri-ai.ro" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, an AI education platform in Eastern Europe, and this is the failure surface I see most often in production reviews — because every layer of it looks fine in isolation. Here's the whole path, one break at a time.&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%2Fcursuri-ai.ro%2Fimages%2Fblog%2Fstreaming-llm-sse-production-en.svg" 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%2Fcursuri-ai.ro%2Fimages%2Fblog%2Fstreaming-llm-sse-production-en.svg" alt="Streaming LLM responses in production — the six breaks between the model and the browser" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First: streaming is not a UX nicety anymore
&lt;/h2&gt;

&lt;p&gt;It's worth being clear about why you're doing this, because it changes how you treat failures.&lt;/p&gt;

&lt;p&gt;The perceived-latency argument is the famous one, and it's real. But on current models, streaming is also a &lt;em&gt;correctness&lt;/em&gt; requirement for a growing share of requests. Claude Opus 5, Sonnet 5, and the 4.6/4.7/4.8 family support up to 128K output tokens, and Anthropic's SDKs will refuse a non-streaming request they estimate will exceed the connection's tolerance — the Python SDK raises a &lt;code&gt;ValueError&lt;/code&gt; rather than let you build something that hangs and drops. The default client timeout is 10 minutes (note the units differ by SDK: seconds in Python and Ruby, &lt;strong&gt;milliseconds&lt;/strong&gt; in TypeScript), and with thinking on by default on Opus 5, a hard task can spend minutes generating before the first visible character.&lt;/p&gt;

&lt;p&gt;So: any request with a large &lt;code&gt;max_tokens&lt;/code&gt;, a long input, or a reasoning-heavy prompt is a streaming request. Not for the animation — for the connection.&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;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&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="nc"&gt;Anthropic&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;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;thinking&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;adaptive&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;display&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;summarized&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyze this incident report...&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="k"&gt;as&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;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&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;text_stream&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;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&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;flush&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;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing in that snippet is not cosmetic. On Opus 5, Opus 4.8/4.7, Fable 5, and Sonnet 5, the thinking &lt;code&gt;display&lt;/code&gt; default is &lt;code&gt;"omitted"&lt;/code&gt; — the thinking block opens, emits a single &lt;code&gt;signature_delta&lt;/code&gt;, and closes, with &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;thinking_delta&lt;/code&gt; events. If you stream reasoning to users and leave the default in place, your UI shows a long dead pause and then a wall of text. &lt;code&gt;display: "summarized"&lt;/code&gt; is what gives you something to render during the think. It costs nothing extra: thinking happens and is billed identically under every display setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #1: your proxy is holding the tokens hostage
&lt;/h2&gt;

&lt;p&gt;This is the number-one "streaming doesn't work in production" bug, and the tell is unmistakable: locally you see tokens appear one by one; deployed, the whole response lands at once at the end.&lt;/p&gt;

&lt;p&gt;Nothing in your app is wrong. nginx is buffering. From the nginx documentation, &lt;code&gt;proxy_buffering&lt;/code&gt; defaults to &lt;strong&gt;on&lt;/strong&gt;, and when buffering is enabled nginx reads the response from the upstream into its own buffers before passing it along. When it's off, "the response is passed to a client synchronously, immediately as it is received."&lt;/p&gt;

&lt;p&gt;There are two ways to fix it, and you want the second one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/chat/stream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;300s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# default is 60s&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works, but it puts a per-route infrastructure rule in a file your application team doesn't own. nginx also honors a response header: "Buffering can also be enabled or disabled by passing &lt;code&gt;yes&lt;/code&gt; or &lt;code&gt;no&lt;/code&gt; in the &lt;code&gt;X-Accel-Buffering&lt;/code&gt; response header field." So the streaming endpoint can turn buffering off for itself:&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;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StreamingResponse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sse_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generator&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;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/event-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;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;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;no-cache&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;Connection&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;keep-alive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Accel-Buffering&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;no&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 important one
&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;The header travels with the endpoint. Add a route, get correct behavior. No config drift, no "works in staging" incident six months later when someone rebuilds the ingress.&lt;/p&gt;

&lt;p&gt;While you're there, note &lt;code&gt;proxy_read_timeout&lt;/code&gt; — it defaults to &lt;strong&gt;60s&lt;/strong&gt; in nginx. A model thinking hard for 90 seconds before its first token will be cut off by your own proxy, and the error you'll see is a generic upstream timeout with no mention of streaming at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #2: the load balancer kills you during the pause
&lt;/h2&gt;

&lt;p&gt;Same class of bug, one layer out, and it bites hardest exactly on the requests you care most about.&lt;/p&gt;

&lt;p&gt;An AWS Application Load Balancer's &lt;code&gt;idle_timeout.timeout_seconds&lt;/code&gt; attribute has a &lt;strong&gt;valid range of 1–4000 seconds and a default of 60&lt;/strong&gt; (see &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_LoadBalancerAttribute.html" rel="noopener noreferrer"&gt;&lt;code&gt;LoadBalancerAttribute&lt;/code&gt;&lt;/a&gt; in the ELB API reference). "Idle" means no bytes in either direction. An LLM that thinks for 75 seconds before emitting its first token produces exactly that: a silent, live, perfectly healthy TCP connection that your load balancer decides is dead.&lt;/p&gt;

&lt;p&gt;You can raise the timeout, and you probably should. But raising it alone is a fragile fix, because it only pushes the cliff further out. The robust fix is to make sure the connection is never actually idle — which SSE has a purpose-built mechanism for.&lt;/p&gt;

&lt;p&gt;The SSE wire format treats a line beginning with a colon as a comment. MDN puts it plainly: "A colon as the first character of a line is in essence a comment, and is ignored," and "The comment line can be used to prevent connections from timing out; a server can send a comment periodically to keep the connection alive."&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;asyncio&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sse_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;heartbeat&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&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="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: keepalive&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# ignored by every SSE client
&lt;/span&gt;
    &lt;span class="c1"&gt;# ... producer task pushes real events onto the same queue ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen seconds is a good default — comfortably under a 60-second idle timeout, cheap enough that nobody notices. The bytes are discarded by the client and keep every hop on the path convinced the connection is alive.&lt;/p&gt;

&lt;p&gt;Anthropic does the same thing on its side, by the way: "Event streams may also include any number of &lt;code&gt;ping&lt;/code&gt; events." Your parser needs to expect them and ignore them, which leads directly to the next break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #3: the errors arrive with HTTP 200
&lt;/h2&gt;

&lt;p&gt;Here's the part that catches teams who have otherwise done everything right.&lt;/p&gt;

&lt;p&gt;Once the stream has started, the HTTP status code is already sent. It's 200. It will stay 200 no matter what happens next. Failures after that point arrive &lt;strong&gt;as events inside the body&lt;/strong&gt;, and if your parser only handles the happy path, they vanish silently.&lt;/p&gt;

&lt;p&gt;The Messages API documents this directly: "The API may occasionally send errors in the event stream. For example, during periods of high usage, you may receive an &lt;code&gt;overloaded_error&lt;/code&gt;, which would normally correspond to an HTTP 529 in a non-streaming context":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using the SDK helpers this is handled for you. If you're parsing raw SSE — which you are, at the browser end, and often at the server end too — you need to branch on it explicitly.&lt;/p&gt;

&lt;p&gt;Two more things belong in the same parser:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unknown event types must not throw.&lt;/strong&gt; The docs are explicit: "In accordance with the versioning policy, new event types may be added, and your code should handle unknown event types gracefully." A parser with an &lt;code&gt;else: raise&lt;/code&gt; in it is a scheduled outage with an unknown date. Log and skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;stop_reason&lt;/code&gt; is not always &lt;code&gt;end_turn&lt;/code&gt;.&lt;/strong&gt; On Opus 4.7 and later, safety classifiers can decline a request with HTTP 200 and &lt;code&gt;stop_reason: "refusal"&lt;/code&gt;, with a category in &lt;code&gt;stop_details&lt;/code&gt;. Check &lt;code&gt;stop_reason&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; you read &lt;code&gt;content&lt;/code&gt;. (&lt;code&gt;stop_details&lt;/code&gt; is populated &lt;strong&gt;only&lt;/strong&gt; for refusals and is &lt;code&gt;null&lt;/code&gt; for every other stop reason, so guard before reading it.) On Opus 5 and Fable 5 you can also opt into server-side fallbacks — &lt;code&gt;betas: ["server-side-fallback-2026-07-01"]&lt;/code&gt; with &lt;code&gt;fallbacks: "default"&lt;/code&gt; — which routes by refusal category. If you do, expect a &lt;code&gt;fallback&lt;/code&gt; content block in the stream at each model boundary: a &lt;code&gt;content_block_start&lt;/code&gt; / &lt;code&gt;content_block_stop&lt;/code&gt; pair with no deltas between them. A parser that assumes every block has deltas will choke on it.&lt;/p&gt;

&lt;p&gt;Here's the shape of a parser that survives all of it:&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;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw_events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&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;type&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;t&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content_block_delta&lt;/span&gt;&lt;span class="sh"&gt;"&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;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&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;d&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&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;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;d&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;d&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thinking_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thinking&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;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;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thinking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
        &lt;span class="c1"&gt;# input_json_delta / signature_delta: accumulate, don't render
&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# NOTE: usage counts in message_delta are CUMULATIVE, not incremental
&lt;/span&gt;        &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&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;usage&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="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&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;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;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_tokens&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&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;stop_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop_reason&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;t&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&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;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&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="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping&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_block_start&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_block_stop&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;message_start&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;message_stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&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;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown stream event, ignoring&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&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;event_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment about cumulative usage is worth internalizing — the docs flag it with a warning box. Adding up &lt;code&gt;message_delta&lt;/code&gt; usage across events gives you a token count that grows quadratically and a cost dashboard that is confidently wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #4: the user left and you're still paying
&lt;/h2&gt;

&lt;p&gt;A user asks a question, gets three sentences in, sees the answer isn't what they wanted, and closes the tab.&lt;/p&gt;

&lt;p&gt;What happens to the 60,000-token generation you started? In a lot of production apps: it runs to completion, bills in full, and writes its result to a database row nobody will read. Multiply by your bounce rate.&lt;/p&gt;

&lt;p&gt;Cancellation has to be propagated explicitly, at every hop. Server side, Starlette (and therefore FastAPI) exposes the disconnect signal:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;client&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&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;stream&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;text&lt;/span&gt; &lt;span class="ow"&gt;in&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;text_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_disconnected&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;          &lt;span class="c1"&gt;# exiting the with-block closes the upstream connection
&lt;/span&gt;            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exiting the context manager is what actually matters — it closes the HTTP connection to Anthropic, and generation stops. A &lt;code&gt;break&lt;/code&gt; without the context manager, or a background task that owns the stream and outlives the request, keeps burning tokens.&lt;/p&gt;

&lt;p&gt;Browser side, &lt;code&gt;fetch&lt;/code&gt; cancellation goes through &lt;code&gt;AbortController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;stopButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&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="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/chat/stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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;Aborting closes the TCP connection, which is what your &lt;code&gt;is_disconnected()&lt;/code&gt; check observes. The chain only works if every link is present.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #5: &lt;code&gt;EventSource&lt;/code&gt; can't do what your chat app needs
&lt;/h2&gt;

&lt;p&gt;The browser's native SSE client is &lt;code&gt;EventSource&lt;/code&gt;, and reaching for it is the obvious move. It's also, for most LLM chat UIs, the wrong one.&lt;/p&gt;

&lt;p&gt;The constructor takes a URL and an options object whose only meaningful member is &lt;code&gt;withCredentials&lt;/code&gt;. There is no place to put an HTTP method, a request body, or headers. That rules out sending a prompt as a POST body and rules out an &lt;code&gt;Authorization&lt;/code&gt; header. And its built-in auto-reconnect — "By default, if the connection between the client and server closes, the connection is restarted" — is actively hostile for LLM generation: a dropped connection silently fires a &lt;em&gt;brand-new&lt;/em&gt; generation, at full cost, with no memory of the tokens already delivered. One flaky connection becomes a duplicate bill.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;fetch&lt;/code&gt; with a &lt;code&gt;ReadableStream&lt;/code&gt; reader instead. You get POST, headers, &lt;code&gt;AbortController&lt;/code&gt;, and — crucially — no reconnect you didn't ask for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                       &lt;span class="c1"&gt;// keep the incomplete tail&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataLines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// ':' comment lines fall out here&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trimStart&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;dataLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// heartbeat frame&lt;/span&gt;
    &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that cause real bugs. First, &lt;strong&gt;TCP does not deliver your frames whole&lt;/strong&gt; — &lt;code&gt;reader.read()&lt;/code&gt; will hand you half an event, and the &lt;code&gt;buf.split("\n\n")&lt;/code&gt; / &lt;code&gt;buf.pop()&lt;/code&gt; pattern is what keeps you from parsing a truncated JSON object. Second, multiple consecutive &lt;code&gt;data:&lt;/code&gt; lines in one frame are concatenated with newlines between them, per the spec; joining them with &lt;code&gt;""&lt;/code&gt; corrupts any payload containing a newline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #6: partial output is a state you have to design for
&lt;/h2&gt;

&lt;p&gt;Every stream can end three ways: complete, cancelled, failed mid-flight. Most codebases only persist the first.&lt;/p&gt;

&lt;p&gt;That produces two ugly symptoms. A user refreshes after a network blip and their half-written answer is simply gone — the connection is stateless, so the tokens are too. And your cost tracking under-reports, because you only record usage on clean completions, while every abandoned generation was billed in full.&lt;/p&gt;

&lt;p&gt;The fix is not resumable streams — that's a hard, mostly unnecessary feature. It's persisting the assistant message &lt;em&gt;as it's produced&lt;/em&gt;, with a status:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Insert the message row with &lt;code&gt;status = "streaming"&lt;/code&gt; before the first token.&lt;/li&gt;
&lt;li&gt;Flush accumulated text to it periodically (every ~50 tokens or every second — not every token, unless you enjoy write amplification).&lt;/li&gt;
&lt;li&gt;On &lt;code&gt;message_stop&lt;/code&gt;, set &lt;code&gt;status = "complete"&lt;/code&gt; and write the final usage from &lt;code&gt;get_final_message()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On disconnect or error, set &lt;code&gt;status = "partial"&lt;/code&gt; or &lt;code&gt;"failed"&lt;/code&gt; and record whatever usage you observed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now a refresh renders the partial answer with an honest "generation was interrupted" marker, and your cost dashboard counts abandoned generations — which is the number that tells you whether Break #4 is costing you real money. Wiring persistence, usage accounting, and streaming state together is exactly the kind of plumbing we build end to end in our &lt;a href="https://cursuri-ai.ro/en/courses/build-and-ship-a-production-ai-saas" rel="noopener noreferrer"&gt;course on shipping a production AI SaaS&lt;/a&gt;, because it's where "demo works" and "product works" actually diverge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Everything above, compressed into things you can go verify this afternoon:&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;Check&lt;/th&gt;
&lt;th&gt;Where&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;
&lt;code&gt;proxy_buffering off&lt;/code&gt; or &lt;code&gt;X-Accel-Buffering: no&lt;/code&gt; on streaming routes&lt;/td&gt;
&lt;td&gt;nginx / app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Proxy read timeout and LB idle timeout raised above your p99 time-to-first-token&lt;/td&gt;
&lt;td&gt;infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Heartbeat comment frames every ~15s&lt;/td&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Parser handles &lt;code&gt;ping&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, and unknown event types without throwing&lt;/td&gt;
&lt;td&gt;app + client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stop_reason&lt;/code&gt; checked before reading &lt;code&gt;content&lt;/code&gt;; &lt;code&gt;stop_details&lt;/code&gt; guarded&lt;/td&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;message_delta&lt;/code&gt; usage treated as cumulative, not incremental&lt;/td&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;thinking.display: "summarized"&lt;/code&gt; if you render reasoning&lt;/td&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Client disconnect propagated to the upstream stream (context manager exits)&lt;/td&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AbortController&lt;/code&gt; wired to a visible stop control&lt;/td&gt;
&lt;td&gt;client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Partial messages persisted with a status; abandoned usage recorded&lt;/td&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want to know whether any of this is actually costing you, the honest test is a load test with a 30% mid-stream abandonment rate, run against staging, with token usage measured at both ends. The gap between what your dashboard reports and what your Anthropic console reports is the size of the problem.&lt;/p&gt;

&lt;p&gt;Streaming is where a lot of AI products quietly lose both money and trust — and it's a plumbing problem, not a prompting one. If you want the full path from a first API call to an application that holds up under real traffic, that's the arc of our &lt;a href="https://cursuri-ai.ro/en/courses/advanced-llm-integration-in-production" rel="noopener noreferrer"&gt;Advanced LLM Integration in Production&lt;/a&gt; course; the failure-mode discipline behind it — knowing whether a change actually helped — comes from &lt;a href="https://cursuri-ai.ro/en/courses/llm-evaluation-and-testing" rel="noopener noreferrer"&gt;LLM Evaluation and Testing&lt;/a&gt;. And if your next feature is voice, every one of these six breaks gets harder, which is its own topic in &lt;a href="https://cursuri-ai.ro/en/courses/voice-ai-and-realtime-multimodal-agents" rel="noopener noreferrer"&gt;Voice AI and Realtime Multimodal Agents&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://platform.claude.com/docs/en/build-with-claude/streaming" rel="noopener noreferrer"&gt;Streaming Messages — Claude API docs&lt;/a&gt; · &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" rel="noopener noreferrer"&gt;Using server-sent events — MDN&lt;/a&gt; · &lt;a href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html" rel="noopener noreferrer"&gt;&lt;code&gt;ngx_http_proxy_module&lt;/code&gt; — nginx&lt;/a&gt; · &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_LoadBalancerAttribute.html" rel="noopener noreferrer"&gt;&lt;code&gt;LoadBalancerAttribute&lt;/code&gt; — AWS ELB API Reference&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Anthropic Filed to Go Public. Here's What Actually Changes for Developers Building on Claude</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Fri, 14 Aug 2026 11:40:32 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/anthropic-filed-to-go-public-heres-what-actually-changes-for-developers-building-on-claude-592</link>
      <guid>https://dev.to/cursuri-ai/anthropic-filed-to-go-public-heres-what-actually-changes-for-developers-building-on-claude-592</guid>
      <description>&lt;p&gt;On &lt;strong&gt;June 1, 2026&lt;/strong&gt;, Anthropic PBC &lt;a href="https://www.anthropic.com/news/confidential-draft-s1-sec" rel="noopener noreferrer"&gt;confidentially submitted a draft registration statement on Form S-1 to the SEC&lt;/a&gt; for a proposed IPO of its common stock. That's the whole confirmed story. The number of shares hasn't been set, the price hasn't been set, no exchange has been named, no ticker has been named, and the announcement says plainly that any offering "will depend on market conditions and other factors."&lt;/p&gt;

&lt;p&gt;Everything else you've read this summer — October listing, Nasdaq, $60B raise, which banks are on the cover — is press speculation dressed up in specific numbers. Some of it will turn out right. None of it is confirmed by the company, and none of it should be in your planning documents yet.&lt;/p&gt;

&lt;p&gt;I teach AI engineering at &lt;a href="https://cursuri-ai.ro" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, an AI education platform in Eastern Europe, and I don't have a stock tip for you. What I do have is the version of this story that matters if you have production traffic going to &lt;code&gt;api.anthropic.com&lt;/code&gt;: an IPO changes the incentives of the company on the other end of your API key, and there are four consequences worth engineering around &lt;em&gt;before&lt;/em&gt; the S-1 goes public.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the numbers that are actually confirmed
&lt;/h2&gt;

&lt;p&gt;Three figures anchor everything else, and all three come from Anthropic or from reporting on its own disclosures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Figure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run-rate revenue&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Surpassed $47B&lt;/strong&gt;, up from ~$9B at the end of 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Series H&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$65B raised&lt;/strong&gt; at a &lt;strong&gt;$965B post-money&lt;/strong&gt; valuation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S-1 status&lt;/td&gt;
&lt;td&gt;Confidential draft submitted June 1, 2026 — SEC review ongoing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A roughly 5x run-rate increase in under two quarters is the part worth sitting with. That is not a company that needs to go public to survive. It's a company converting enterprise API and coding-agent demand into revenue faster than almost any software business in history, and choosing to put that on a public balance sheet.&lt;/p&gt;

&lt;p&gt;It also isn't happening in isolation. SpaceX — which now owns xAI outright and rebranded the combined entity SpaceXAI — &lt;a href="https://www.cnbc.com/2026/06/12/spacex-ipo-spcx-live-updates.html" rel="noopener noreferrer"&gt;listed on Nasdaq under SPCX on June 12, 2026&lt;/a&gt;, closing its first day at $160.95, up 19%. Frontier AI is moving from "priced by a handful of private rounds" to "priced by the public market every trading day." That shift is the actual subject of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consequence #1: your vendor gets a quarterly clock
&lt;/h2&gt;

&lt;p&gt;Private Anthropic optimized on a multi-year horizon and answered to a small set of investors who had already agreed to the thesis. Public Anthropic answers, every ninety days, to a market that will form opinions about gross margin, revenue concentration, and compute spend.&lt;/p&gt;

&lt;p&gt;I want to be careful here, because the lazy version of this argument is "public companies get greedy and raise prices." That's not a forecast I can support, and the recent evidence points the other way: Claude Opus 5 shipped in July 2026 at the same $5/$25 per million tokens that Opus 4.8 cost, and Sonnet 5 sits at $2/$10. Inference prices per unit of capability have been falling, not rising.&lt;/p&gt;

&lt;p&gt;The realistic pressure isn't the sticker price. It's everything around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free and low-tier generosity&lt;/strong&gt; is the easiest margin lever to pull, and the least visible in a headline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits and capacity allocation&lt;/strong&gt; start to follow revenue, not goodwill. Enterprise contracts get the GPUs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deprecation timelines tighten&lt;/strong&gt;, because retiring an old model is pure margin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those show up as a price increase. All of them show up in your error rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consequence #2: model retirement becomes an operational risk, not a footnote
&lt;/h2&gt;

&lt;p&gt;This is the one I'd act on this week, and I say that as someone who has eaten the failure.&lt;/p&gt;

&lt;p&gt;We had an integration pinned to a hardcoded Anthropic model ID. That model was retired. The result wasn't a graceful warning in a dashboard — it was a live 404 in production, in a customer-facing feature, discovered by users. The lesson generalizes past any one vendor: &lt;strong&gt;every hardcoded model ID is a scheduled outage with an unknown date.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every frontier lab now runs a formal retirement process — xAI's docs currently carry a dedicated "Model Retirement" migration page, and Anthropic publishes deprecation notices. A public company has a stronger incentive to run that process on a tighter schedule. Your codebase should treat model identity as configuration, not as a string literal:&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;# config, not code — one place to change, one place to audit
&lt;/span&gt;&lt;span class="n"&gt;MODELS&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;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_DEFAULT&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_HARD&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;claude-opus-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cheap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_CHEAP&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;claude-haiku-4-5-20251001&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&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;MODELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TASK_TIER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&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;client&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;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test for whether you're exposed is simple: &lt;strong&gt;can you change which model serves a workload without a deploy?&lt;/strong&gt; If the answer is no, that's the highest-leverage refactor on your list, and it's worth doing before you need it at 2am. Getting from "API calls sprinkled across services" to "an application where a model swap is a config change" is the arc we walk through in our &lt;a href="https://cursuri-ai.ro/courses/construire-aplicatii-ai-python-sdk" rel="noopener noreferrer"&gt;course on building AI applications with the Python SDK&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consequence #3: the S-1 will be the best vendor due-diligence document ever published
&lt;/h2&gt;

&lt;p&gt;This is the genuinely good news, and almost nobody is talking about it.&lt;/p&gt;

&lt;p&gt;Frontier labs are financial black boxes. We infer their economics from leaks, from investor decks that reach journalists, and from third-party trackers that disagree with each other by tens of billions. A public S-1 ends that for one of them. When Anthropic's full prospectus lands, you will be able to read, under penalty of securities law:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gross margin on inference.&lt;/strong&gt; The single number that tells you how much room exists under current API prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revenue concentration.&lt;/strong&gt; How much of that $47B comes from the top handful of customers. If one hyperscaler or one coding-agent partner is a huge slice, that's a dependency in &lt;em&gt;your&lt;/em&gt; supply chain too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute commitments.&lt;/strong&gt; Multi-year obligations to chip and cloud suppliers — the fixed costs that determine how price-flexible the company can be in a downturn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The risk factors section.&lt;/strong&gt; Labs are required to enumerate, in writing, what could go wrong: litigation, regulation, model liability, safety incidents, key-personnel loss. It's the most candid document a lab will ever publish about itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're the person at your company who signs off on an AI vendor, put a reminder in your calendar for the day the public S-1 drops. Read the risk factors and the concentration disclosures first. That's a better afternoon of vendor research than any analyst report you'll pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consequence #4: "which model" stops being a taste question
&lt;/h2&gt;

&lt;p&gt;A public market prices frontier labs against each other continuously, and that competition lands in your API bill and your eval scores. The current spread is already wide enough that model choice is a real engineering decision rather than a preference:&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 / output per 1M tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5&lt;/td&gt;
&lt;td&gt;$10 / $50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;$5 / $25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;$2 / $10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok 4.6&lt;/td&gt;
&lt;td&gt;$2 / $6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An 8x spread on output tokens between the top and bottom of that table means the same agent architecture can cost wildly different amounts depending on routing. And routing decisions made on vibes are how teams end up paying flagship prices for classification work.&lt;/p&gt;

&lt;p&gt;The prerequisite for routing well is an eval harness that can answer "did quality hold when I moved this workload down a tier?" with data instead of a hunch. Without it, every model launch is a risk you absorb. With it, every launch is a shopping opportunity. That harness is the core of our &lt;a href="https://cursuri-ai.ro/courses/ai-evals-llm-productie" rel="noopener noreferrer"&gt;LLM evals in production course&lt;/a&gt;, and the comparative side — what each frontier family is actually good at — is what we break down in our &lt;a href="https://cursuri-ai.ro/courses/comparatie-modele-ai" rel="noopener noreferrer"&gt;AI model comparison course&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually do this quarter
&lt;/h2&gt;

&lt;p&gt;Five items, in the order I'd tackle them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep for hardcoded model IDs.&lt;/strong&gt; Move every one to configuration. This is a half-day of work that prevents a class of outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build or fix the eval gate.&lt;/strong&gt; You cannot safely change models, effort levels, or providers without one. Everything else on this list depends on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument cost per task finished&lt;/strong&gt; — not cost per token. Token price is a distraction; what you care about is what it costs to complete one unit of real work, including retries and reasoning tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read your contract's deprecation terms.&lt;/strong&gt; How much notice are you actually owed before a model you depend on goes away? If you're on a standard developer plan, the answer is probably "whatever the public policy says," which can change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove that failover works.&lt;/strong&gt; Not "we have a second provider configured" — actually run a game day where the primary returns 429s and 500s and see what your users experience.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is IPO-specific advice. It's just ordinary production hygiene that an IPO makes newly urgent, because the difference between a well-run integration and a fragile one only becomes visible when the vendor's incentives shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Anthropic filing to go public is, on balance, good for developers. A company with $47B in run-rate revenue and public-market accountability is a more predictable dependency than a private lab whose economics you have to guess at. You get disclosure, you get a documented risk profile, and you get a competitor that has to keep winning on capability because the scoreboard is now public.&lt;/p&gt;

&lt;p&gt;What you also get is a vendor with a quarterly clock, which means the slack in free tiers, generous rate limits, and leisurely deprecation windows is likely to tighten before it loosens. The teams that come out ahead won't be the ones who predicted the ticker. They'll be the ones who spent this quarter making a model swap a config change instead of an incident.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want structured, hands-on training on any of this — evals, model selection, production LLM applications — that's what we build at &lt;a href="https://cursuri-ai.ro/trasee" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://www.anthropic.com/news/confidential-draft-s1-sec" rel="noopener noreferrer"&gt;Anthropic — confidential draft S-1 submission&lt;/a&gt; · &lt;a href="https://techcrunch.com/2026/06/01/anthropic-files-to-go-public/" rel="noopener noreferrer"&gt;TechCrunch — Anthropic files to go public&lt;/a&gt; · &lt;a href="https://www.cnbc.com/2026/06/12/spacex-ipo-spcx-live-updates.html" rel="noopener noreferrer"&gt;CNBC — SpaceX IPO, SPCX first-day close&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The EU AI Act's Transparency Rules Went Live — Here's What You Actually Have to Ship</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Sun, 09 Aug 2026 21:42:39 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/the-eu-ai-acts-transparency-rules-went-live-heres-what-you-actually-have-to-ship-3i2d</link>
      <guid>https://dev.to/cursuri-ai/the-eu-ai-acts-transparency-rules-went-live-heres-what-you-actually-have-to-ship-3i2d</guid>
      <description>&lt;p&gt;Most AI regulation, for most developers, has been something that happens to other people. Risk classifications, conformity assessments, notified bodies — the kind of thing where you nod, assume legal will handle it, and go back to your streaming handler.&lt;/p&gt;

&lt;p&gt;Article 50 is different, and it went live on &lt;strong&gt;2 August 2026&lt;/strong&gt;. It's the part of the EU AI Act that doesn't care whether your system is "high risk." It cares about one thing: &lt;strong&gt;can the person on the other end tell that this is AI?&lt;/strong&gt; If the answer is no, you owe them a disclosure — and the disclosure is a product decision, a UI decision, and in one case a file-format decision. All three land on engineering.&lt;/p&gt;

&lt;p&gt;The Commission adopted its guidelines on Article 50 on &lt;strong&gt;20 July 2026&lt;/strong&gt;, and the AI Office published a &lt;strong&gt;Code of Practice on Transparency of AI-Generated Content&lt;/strong&gt; on &lt;strong&gt;10 June 2026&lt;/strong&gt;. Between those two documents and the article text itself, the shape of what you have to build is now reasonably clear. This is that shape, translated into work items.&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%2Fcursuri-ai.ro%2Fimages%2Fblog%2Feu-ai-act-article-50-en.svg" 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%2Fcursuri-ai.ro%2Fimages%2Fblog%2Feu-ai-act-article-50-en.svg" alt="The four Article 50 transparency obligations mapped to what each one requires in a product" width="1200" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First: does this apply to you?
&lt;/h2&gt;

&lt;p&gt;Two questions, and you're probably in scope on both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are you a provider or a deployer?&lt;/strong&gt; The Act splits duties. A &lt;strong&gt;provider&lt;/strong&gt; develops an AI system (or has one developed) and places it on the market under its own name — if you built the chatbot or the image generator, that's you. A &lt;strong&gt;deployer&lt;/strong&gt; uses an AI system under its own authority — if you dropped someone else's model into your support widget, that's you. Article 50 assigns two obligations to providers and two to deployers, and plenty of teams are both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the EU reach you?&lt;/strong&gt; The Act's territorial hooks (Article 2) are not "are you an EU company." They're closer to "is the system placed on the EU market, or is its output used in the EU." A US startup with EU users is in scope. This is the GDPR pattern, and it caught a lot of people by surprise the first time.&lt;/p&gt;

&lt;p&gt;Not in scope: things that don't interact with people or generate content for them. The Commission's guidelines explicitly put spam filters and automated translation tools &lt;em&gt;outside&lt;/em&gt; Article 50(1), and voice assistants and chatbots &lt;em&gt;inside&lt;/em&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obligation 1 — tell people they're talking to an AI
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Providers shall ensure that AI systems intended to interact directly with natural persons are designed and developed in such a way that the natural persons concerned are informed that they are interacting with an AI system, unless this is obvious from the point of view of a natural person who is reasonably well-informed, observant and circumspect. — Article 50(1)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the one that hits the most products, and it's the easiest to get wrong in a way that looks compliant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "obvious" carve-out is narrower than you want it to be.&lt;/strong&gt; The test isn't "our users are technical." It's a reasonably well-informed, observant and circumspect person &lt;em&gt;in the circumstances and context of use&lt;/em&gt;. A widget labelled "AI Assistant" in a developer tool is plausibly obvious. The same engine answering an inbound phone call in a warm, human-sounding voice is not — and voice is exactly where the gap is widest right now, because &lt;a href="https://cursuri-ai.ro/en/courses/voice-ai-and-realtime-multimodal-agents" rel="noopener noreferrer"&gt;realtime voice agents&lt;/a&gt; are good enough that the "obvious" defence has quietly stopped being true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the disclosure has to live.&lt;/strong&gt; Article 50(5) settles the argument you're about to have with someone in a planning meeting: the information must be provided &lt;strong&gt;at the latest at the time of the first interaction or exposure&lt;/strong&gt;, and it must be &lt;strong&gt;clear and distinguishable&lt;/strong&gt;. That rules out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;burying it in the Terms of Service&lt;/li&gt;
&lt;li&gt;a footnote in the privacy policy&lt;/li&gt;
&lt;li&gt;a tooltip behind a hover on a mobile UI&lt;/li&gt;
&lt;li&gt;disclosing on turn three, after the user has already asked something personal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Article 50(5) also requires conformity with applicable accessibility requirements — which in practice means your disclosure has to survive a screen reader, not just a design review.&lt;/p&gt;

&lt;p&gt;What that looks like in a chat surface is boring, and boring is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Rendered before the first assistant message, not after it. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ai-disclosure"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"note"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;You're chatting with an AI assistant.&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
  Answers are generated automatically.
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/support/human"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Talk to a person&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For voice, the equivalent is a spoken line in the &lt;strong&gt;first&lt;/strong&gt; turn — before you collect anything, and in the language of the call. For an API you sell to other developers, the honest move is to pass the obligation downstream explicitly: document it, and give integrators a disclosure string they can render, because when they ship your model to end users under their own brand, the deployer duties become theirs and the design duty stays yours.&lt;/p&gt;

&lt;p&gt;One more thing worth building while you're in there: a &lt;strong&gt;handoff path to a human&lt;/strong&gt;. Article 50 doesn't mandate it. But the disclosure lands very differently when it's followed by an escape hatch, and support teams that ship &lt;a href="https://cursuri-ai.ro/en/courses/ai-for-customer-support-and-service" rel="noopener noreferrer"&gt;AI assistants without one&lt;/a&gt; tend to discover the reason the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obligation 2 — mark synthetic output so machines can detect it
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Providers of AI systems […] generating synthetic audio, image, video or text content, shall ensure the outputs […] are marked in a machine-readable format and detectable as artificially generated or manipulated. — Article 50(2)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the genuinely hard engineering item, and it's the one with a different deadline (more on that below).&lt;/p&gt;

&lt;p&gt;Two properties are required, and they're not the same thing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Machine-readable marking&lt;/strong&gt; — metadata that a downstream system can parse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detectability&lt;/strong&gt; — the output can be recognised as artificially generated or manipulated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Act asks for solutions that are "effective, interoperable, robust and reliable as far as this is technically feasible" — a standard that explicitly bends to the state of the art. It also carves out &lt;strong&gt;assistive editing functions&lt;/strong&gt; and systems that &lt;strong&gt;do not substantially alter the input data&lt;/strong&gt;. Your auto-crop and your denoise filter are not in scope. Your "generate a product photo from this prompt" endpoint is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the Code of Practice points at.&lt;/strong&gt; The AI Office's Code, published 10 June 2026, describes a layered approach rather than a single mechanism: signed, timestamped provenance metadata — where &lt;strong&gt;C2PA&lt;/strong&gt; is the standard identified as meeting those criteria — &lt;em&gt;plus&lt;/em&gt; an imperceptible watermark embedded in the content itself, robust enough to survive ordinary transformations like compression, cropping, scaling and format conversion. The Code is voluntary and, at the time of writing, going through an adequacy assessment by the Commission and the AI Board. Adhering to it is a route to demonstrating compliance; not adhering means you have to show equivalently adequate means of your own.&lt;/p&gt;

&lt;p&gt;In practice, for images and video, that means attaching &lt;strong&gt;C2PA Content Credentials&lt;/strong&gt; at generation time. The Content Authenticity Initiative ships open-source tooling for this — &lt;a href="https://github.com/contentauth/c2pa-rs" rel="noopener noreferrer"&gt;&lt;code&gt;c2pa-rs&lt;/code&gt;&lt;/a&gt; with Python, JS, C++, Swift and Android bindings, plus a &lt;code&gt;c2patool&lt;/code&gt; CLI — so this is a library integration, not a research project.&lt;/p&gt;

&lt;p&gt;The assertion that carries "this was AI-generated" is the IPTC digital source type, referenced inside a &lt;code&gt;c2pa.actions&lt;/code&gt; assertion:&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;"assertions"&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;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c2pa.actions"&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"&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;"actions"&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;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c2pa.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"digitalSourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"&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;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;&lt;code&gt;trainedAlgorithmicMedia&lt;/code&gt; is the IPTC code for content created by a generative model. There are neighbouring codes for composites and for algorithmically edited media — pick the one that actually describes what your pipeline did, because "created" on a system that only retouched is its own kind of wrong. Verify the current assertion shape against the C2PA spec before you ship; the standard is still moving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three things nobody tells you in the compliance deck:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metadata gets stripped.&lt;/strong&gt; Plenty of platforms re-encode uploads and discard provenance metadata on the way in. Signing at generation is necessary; assuming it survives the internet is not. This is precisely why the Code pairs metadata with a watermark instead of trusting either alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text is the weak link.&lt;/strong&gt; Machine-readable marking of &lt;em&gt;text&lt;/em&gt; has no equivalent of C2PA that works after a copy-paste. Statistical watermarking of token distributions exists, degrades under paraphrase, and doesn't survive a user retyping the paragraph. The Act's "as far as technically feasible" language is doing real work here — but "hard" is not "exempt," and documenting your reasoning is part of the deliverable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign server-side.&lt;/strong&gt; Any marking applied in the browser is marking a determined user can skip. The signature belongs on the generation path, before the bytes reach a client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're generating &lt;a href="https://cursuri-ai.ro/en/courses/ai-image-generation" rel="noopener noreferrer"&gt;images&lt;/a&gt; or video in a product today, this obligation is now a line item in your media pipeline, not a policy question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obligation 3 — emotion recognition and biometric categorisation
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Deployers of an emotion recognition system or a biometric categorisation system shall inform the natural persons exposed thereto of the operation of the system, and shall process the personal data in accordance with [the GDPR and related instruments]. — Article 50(3)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Shorter, and mostly a matter of knowing that it applies to you. If your product infers emotional state from voice, face or text, or sorts people into categories from biometric data, you inform the people exposed to it — and you're squarely in GDPR territory on top, usually with special-category data.&lt;/p&gt;

&lt;p&gt;Before you scope the disclosure: check Article 5 first. Some emotion recognition — in the &lt;strong&gt;workplace&lt;/strong&gt; and in &lt;strong&gt;education&lt;/strong&gt; — is &lt;em&gt;prohibited&lt;/em&gt; outright, not merely subject to transparency, and has been since February 2025. Article 50 is the wrong chapter to be reading if that's your use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obligation 4 — deepfakes and public-interest text
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Deployers of an AI system that generates or manipulates image, audio or video content constituting a deep fake, shall disclose that the content has been artificially generated or manipulated. — Article 50(4)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note the split from Obligation 2: marking the file is the &lt;strong&gt;provider's&lt;/strong&gt; duty; disclosing to the audience is the &lt;strong&gt;deployer's&lt;/strong&gt;. If you use a third-party model to produce a synthetic spokesperson for a campaign, the vendor's C2PA manifest doesn't discharge your obligation. You still have to tell the audience.&lt;/p&gt;

&lt;p&gt;Two carve-outs matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Artistic and satirical works.&lt;/strong&gt; Where the content is part of an evidently artistic, creative, satirical or fictional work, the disclosure shrinks to revealing the existence of generated content &lt;strong&gt;in an appropriate manner that does not hamper the display or enjoyment of the work&lt;/strong&gt;. A film doesn't need a permanent banner across the frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text on matters of public interest.&lt;/strong&gt; AI-generated or manipulated &lt;em&gt;text&lt;/em&gt; published to inform the public on matters of public interest must be disclosed — &lt;strong&gt;unless&lt;/strong&gt; it has undergone human review or editorial control and a natural or legal person holds editorial responsibility. This is the clause every content-heavy site should read twice. An unreviewed AI-written news summary needs a label. The same article with a named editor who checked it and owns it does not. If your publishing workflow can't currently prove which of those two happened, that's the actual gap — and it's a workflow problem before it's a legal one, which is why &lt;a href="https://cursuri-ai.ro/en/courses/ai-for-content-creation-and-copywriting" rel="noopener noreferrer"&gt;content operations built on AI&lt;/a&gt; now need an audit trail as much as a style guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deadlines, which are not all the same
&lt;/h2&gt;

&lt;p&gt;The Digital Omnibus on AI — published in the Official Journal on 24 July 2026 and in force since 27 July — shifted several AI Act dates. Article 50 came out of it mostly intact:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Applies from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Article 50(1) AI-interaction disclosure&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 August 2026&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Article 50(3) emotion recognition / biometric categorisation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 August 2026&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Article 50(4) deepfake and public-interest text disclosure&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 August 2026&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Article 50(2) marking, systems placed on the market &lt;strong&gt;on or after&lt;/strong&gt; 2 Aug 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 August 2026&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Article 50(2) marking, systems placed on the market &lt;strong&gt;before&lt;/strong&gt; 2 Aug 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 December 2026&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annex III high-risk obligations (Chapter III)&lt;/td&gt;
&lt;td&gt;deferred to &lt;strong&gt;2 December 2027&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-risk AI embedded in regulated products (Annex I)&lt;/td&gt;
&lt;td&gt;deferred to &lt;strong&gt;2 August 2028&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last pair is the source of most of the confusion in the room right now: the &lt;em&gt;high-risk&lt;/em&gt; regime got a long deferral, and a lot of teams heard "the AI Act got pushed back" and stopped reading. Article 50 did not get pushed back. The only grace period is the marking obligation for generative systems that were already on the market, and it expires on 2 December 2026.&lt;/p&gt;

&lt;p&gt;Enforcement sits with national market surveillance authorities, the AI Office and — for EU institutions — the European Data Protection Supervisor. Breaching Article 50 carries fines of up to &lt;strong&gt;€15 million or 3% of worldwide annual turnover&lt;/strong&gt;, whichever is higher.&lt;/p&gt;

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

&lt;p&gt;Pin this to the epic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Inventory every surface where an AI system talks to a person — chat, voice, email autoresponders, in-app agents. Each one gets a first-interaction disclosure or a written argument for why it's obvious.&lt;/li&gt;
&lt;li&gt;[ ] Disclosure rendered &lt;strong&gt;before&lt;/strong&gt; the first AI output, accessible, in the user's language, not in the ToS.&lt;/li&gt;
&lt;li&gt;[ ] Every generative endpoint identified as provider-side or deployer-side. Write it down; vendor contracts should say the same thing.&lt;/li&gt;
&lt;li&gt;[ ] C2PA Content Credentials signed &lt;strong&gt;server-side&lt;/strong&gt; on image/audio/video generation, with the correct IPTC &lt;code&gt;digitalSourceType&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Watermarking assessed for each modality; where it isn't feasible, the reasoning is documented rather than assumed.&lt;/li&gt;
&lt;li&gt;[ ] Deepfake disclosure at the &lt;strong&gt;publication&lt;/strong&gt; surface, not just in the file metadata.&lt;/li&gt;
&lt;li&gt;[ ] Editorial-review provenance recorded for AI-assisted public-interest text — who reviewed, when, who owns it.&lt;/li&gt;
&lt;li&gt;[ ] Emotion recognition / biometric categorisation checked against &lt;strong&gt;Article 5 prohibitions&lt;/strong&gt; before anything else.&lt;/li&gt;
&lt;li&gt;[ ] Disclosure copy and placement covered by a test, so the next redesign doesn't silently delete it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The thing that makes Article 50 unusual, as regulation goes, is how little of it is paperwork. There's no conformity assessment here, no technical documentation dossier, no notified body. There's a banner that has to render before the first message, a signature that has to happen on the generation path, a label that has to reach the audience, and a record of who reviewed what. Four engineering tickets, roughly, and none of them are hard.&lt;/p&gt;

&lt;p&gt;They're just easy to defer — and the deferral is what gets expensive. Article 50 is now enforceable, the guidelines are published, the Code of Practice exists, and the tooling for the hard part is open source. Compliance here is mostly a question of whether someone put it in the sprint.&lt;/p&gt;

&lt;p&gt;The teams that will have the least trouble with this aren't the ones with the biggest legal department. They're the ones that were already willing to tell users, plainly, what the machine was doing. Turns out that was always the good product decision — it just became the required one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by the team behind &lt;a href="https://cursuri-ai.ro/en/" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, an AI education platform with hands-on English-language courses on &lt;a href="https://cursuri-ai.ro/en/courses/ai-data-privacy-and-eu-ai-act-compliance" rel="noopener noreferrer"&gt;AI, data privacy and EU AI Act compliance&lt;/a&gt;, production LLM integration, and shipping AI products.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources &amp;amp; further reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EU AI Act — &lt;a href="https://artificialintelligenceact.eu/article/50/" rel="noopener noreferrer"&gt;Article 50: Transparency Obligations for Providers and Deployers of Certain AI Systems&lt;/a&gt; (full text, exemptions, codes of practice)&lt;/li&gt;
&lt;li&gt;European Commission — &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/guidelines-transparency-ai-generated-content" rel="noopener noreferrer"&gt;Guidelines on transparency obligations for providers and deployers of certain AI systems&lt;/a&gt; (adopted 20 July 2026)&lt;/li&gt;
&lt;li&gt;European Commission — &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/code-practice-ai-generated-content" rel="noopener noreferrer"&gt;Code of Practice on Transparency of AI-generated Content&lt;/a&gt; (published 10 June 2026)&lt;/li&gt;
&lt;li&gt;Content Authenticity Initiative — &lt;a href="https://opensource.contentauthenticity.org/docs/introduction/" rel="noopener noreferrer"&gt;open-source C2PA SDKs and &lt;code&gt;c2patool&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;IPTC — &lt;a href="https://cv.iptc.org/newscodes/digitalsourcetype/" rel="noopener noreferrer"&gt;Digital Source Type NewsCodes vocabulary&lt;/a&gt; (&lt;code&gt;trainedAlgorithmicMedia&lt;/code&gt; and related values)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article is educational content written by engineers, not legal advice. Article 50 interacts with the GDPR, the DSA, national implementing rules and sector regulation, and the Digital Omnibus changed several dates in 2026 — verify against current official sources and your own counsel before shipping.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Claude Opus 5 Is Out — Here Are the API Changes That Will Actually Break Your Code</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Tue, 28 Jul 2026 22:38:55 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/claude-opus-5-is-out-here-are-the-api-changes-that-will-actually-break-your-code-312k</link>
      <guid>https://dev.to/cursuri-ai/claude-opus-5-is-out-here-are-the-api-changes-that-will-actually-break-your-code-312k</guid>
      <description>&lt;p&gt;Anthropic shipped &lt;strong&gt;Claude Opus 5&lt;/strong&gt; on July 24, 2026, and if you build on the Claude API there's a version of this launch story you can safely skip: the benchmark charts. The version you can't skip is the API contract, because for the first time in a while an Opus release changes how existing requests behave — and one parameter combination that was perfectly valid on Opus 4.8 now returns a &lt;strong&gt;400 error&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I write and teach about AI engineering at &lt;a href="https://cursuri-ai.ro" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, Eastern Europe's AI education platform, and this post is the writeup I wish every model launch came with: not "how smart is it," but "what do I need to change in my code, in what order, and what silently behaves differently if I change nothing." Everything below comes from Anthropic's official "What's new in Claude Opus 5" documentation — no leaked numbers, no vibes.&lt;/p&gt;

&lt;p&gt;Quick disclaimer: this space moves monthly and this is a launch-window snapshot. Verify against the &lt;a href="https://platform.claude.com/docs/en/about-claude/models/whats-new-opus-5" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; before you wire anything to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec sheet, in one table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Claude Opus 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model ID&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;claude-opus-5&lt;/code&gt; (&lt;code&gt;anthropic.claude-opus-5&lt;/code&gt; on Bedrock)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1M tokens — both default and maximum&lt;/strong&gt; (no smaller variant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max output&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;128k tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thinking&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On by default&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;$5 / $25 per million input/output tokens — unchanged from Opus 4.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast mode&lt;/td&gt;
&lt;td&gt;$10 / $50, research preview, &lt;strong&gt;Claude API only&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of these are bigger deals than they look. The 1M context window isn't an opt-in variant — it's just what the model has, and the docs specifically claim consistent instruction following, tool calling, and reasoning throughout the window. And 128k max output makes single-request long deliverables (big refactors, full reports) realistic — with a caveat about &lt;code&gt;max_tokens&lt;/code&gt; we'll get to, because it's now doing more work than it used to.&lt;/p&gt;

&lt;p&gt;Opus 4.8 stays available on every platform, so nothing forces a same-day migration. But the changes below are the kind you want to understand &lt;em&gt;before&lt;/em&gt; your first "why is this request failing" incident, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change #1: thinking is on by default
&lt;/h2&gt;

&lt;p&gt;On Opus 4.8, a request without a &lt;code&gt;thinking&lt;/code&gt; field ran &lt;strong&gt;without&lt;/strong&gt; extended thinking. You opted in with &lt;code&gt;thinking: {"type": "adaptive"}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Opus 5, that same field-less request now runs &lt;strong&gt;with thinking on&lt;/strong&gt;. The model decides when and how much to think on each turn, and the &lt;code&gt;effort&lt;/code&gt; parameter is the knob that controls thinking depth. If your code already sends &lt;code&gt;thinking: {"type": "adaptive"}&lt;/code&gt;, you're fine — that value remains valid and is equivalent to the new default.&lt;/p&gt;

&lt;p&gt;Why this can bite you even though it sounds like a free upgrade:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;max_tokens&lt;/code&gt; is a hard limit on &lt;em&gt;total&lt;/em&gt; output — thinking plus response text.&lt;/strong&gt; A workload that ran happily with &lt;code&gt;max_tokens: 2000&lt;/code&gt; on Opus 4.8 (no thinking, short answers) can now spend a chunk of that budget on reasoning before it writes a single visible token. The official guidance is explicit: revisit &lt;code&gt;max_tokens&lt;/code&gt; for every workload that previously ran without thinking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change #2: the actual breaking change — disabled thinking + high effort = 400
&lt;/h2&gt;

&lt;p&gt;Here's the one to grep your codebase for today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;thinking: {"type": "disabled"}&lt;/code&gt; is accepted &lt;strong&gt;only when effort is &lt;code&gt;high&lt;/code&gt; or below&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;thinking: {"type": "disabled"}&lt;/code&gt; combined with effort &lt;code&gt;xhigh&lt;/code&gt; or &lt;code&gt;max&lt;/code&gt; returns a &lt;strong&gt;400 error&lt;/strong&gt;, on every request.&lt;/li&gt;
&lt;li&gt;This is generally available behavior (not beta) from Opus 5 onward — and it's a breaking change from Opus 4.8, where disabling thinking was independent of the effort level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you currently run thinking-disabled at high effort levels, you have exactly two exits: keep thinking disabled and drop effort to &lt;code&gt;high&lt;/code&gt; or below, or keep your effort level and &lt;strong&gt;delete the &lt;code&gt;thinking&lt;/code&gt; field entirely&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There's a second, subtler reason to just let thinking stay on. The docs note that with thinking disabled, Opus 5 can occasionally write a tool call &lt;strong&gt;into its text output&lt;/strong&gt; instead of emitting a proper &lt;code&gt;tool_use&lt;/code&gt; block, or leak internal XML tags into the visible response. If you parse tool calls in production, that's not a cosmetic footnote — it's a failure mode your handler needs to survive. Anthropic's recommendation is to keep thinking enabled and control cost with lower effort levels instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change #3: &lt;code&gt;effort&lt;/code&gt; is now the control lever that matters
&lt;/h2&gt;

&lt;p&gt;With thinking on by default, &lt;code&gt;effort&lt;/code&gt; becomes the central parameter of your integration. The full ladder on Opus 5: &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt; (the default), &lt;code&gt;xhigh&lt;/code&gt;, and &lt;code&gt;max&lt;/code&gt;. The docs make a claim worth taking seriously: Opus 5 converts additional effort into better results &lt;em&gt;more reliably than any earlier Opus model&lt;/em&gt; — which means the level you pick carries more weight than it did on 4.8. At the other end, &lt;code&gt;low&lt;/code&gt; and &lt;code&gt;medium&lt;/code&gt; are explicitly called out for producing strong quality at a fraction of the tokens and latency.&lt;/p&gt;

&lt;p&gt;A request with everything turned up looks like this:&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;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&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="nc"&gt;Anthropic&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;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output_config&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;effort&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;max&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="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="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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three deliberate details in that snippet: there's &lt;strong&gt;no &lt;code&gt;thinking&lt;/code&gt; field&lt;/strong&gt; (the default is what you want), &lt;code&gt;max_tokens&lt;/code&gt; is large (at &lt;code&gt;xhigh&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; the model needs room to think and act across tool calls), and it's &lt;strong&gt;streamed&lt;/strong&gt; — at 64k-token budgets, non-streaming requests can hit the time limit.&lt;/p&gt;

&lt;p&gt;The sane strategy is the documented one: start at the default &lt;code&gt;high&lt;/code&gt;, then adjust based on &lt;strong&gt;your own evals&lt;/strong&gt;, not vibes. Step down where quality holds — you pocket the tokens and latency. Step up to &lt;code&gt;xhigh&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; for the genuinely hard work. If you don't have an eval harness that can answer "did quality hold when I dropped effort?", that's the single highest-leverage thing to build this quarter — it's the discipline we treat as foundational in our &lt;a href="https://cursuri-ai.ro/courses/ai-evals-llm-productie" rel="noopener noreferrer"&gt;course on LLM evals in production&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change #4: the model &lt;em&gt;behaves&lt;/em&gt; differently, even if you change nothing
&lt;/h2&gt;

&lt;p&gt;The docs have a refreshingly honest section on differences you'll notice without touching your code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default responses run longer&lt;/strong&gt; — both user-facing answers and written deliverables. If your product has strict length constraints, enforce them in the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In agentic sessions, the model narrates its progress more often.&lt;/strong&gt; Good for UX transparency; tune it down where you want silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In multi-agent frameworks, it delegates to subagents more readily&lt;/strong&gt; — budget accordingly, subagents are tokens too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It verifies its own work without being told to.&lt;/strong&gt; This is the one that demands action: verification instructions inherited from older prompts — "include a final verification step," "use a subagent to verify" — should be &lt;strong&gt;removed&lt;/strong&gt;, because on Opus 5 they cause over-verification. You pay twice, in tokens and latency, for work the model already does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a pattern every migration teaches: prompts get calibrated against the previous model's weaknesses, and those calibrations become friction on the next model. A model migration without a prompt audit is half a migration. That workflow — prompts, tools, verification, on real repos — is exactly what we drill in our &lt;a href="https://cursuri-ai.ro/courses/claude-code-mastery-coding-agentic" rel="noopener noreferrer"&gt;Claude Code and agentic coding course&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smaller changes you'll actually be happy about
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt caching minimum drops to 512 tokens&lt;/strong&gt;, from 1,024 on Opus 4.8. System prompts that were too short to cache start caching with zero code changes. If you run compact prompts at volume, this shows up on your invoice by itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid-conversation tool changes (beta)&lt;/strong&gt;: you can add or remove tools between turns &lt;strong&gt;while preserving the prompt cache&lt;/strong&gt;, with the &lt;code&gt;mid-conversation-tool-changes-2026-07-01&lt;/code&gt; beta header. For phase-based agents (explore → edit → verify, different tools each), this removes a whole architectural compromise — no more front-loading every tool "just in case."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;"default"&lt;/code&gt; mode for fallbacks (beta)&lt;/strong&gt;: the &lt;code&gt;fallbacks&lt;/code&gt; parameter can now apply Anthropic's recommended fallback models by refusal category, instead of a hand-maintained model list. Use the &lt;code&gt;server-side-fallback-2026-07-01&lt;/code&gt; header (the older &lt;code&gt;2026-06-01&lt;/code&gt; header only accepts explicit lists).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast mode exists but is API-only&lt;/strong&gt;: $10/$50 per million tokens, research preview, not currently on Bedrock, Google Cloud, or Microsoft Foundry. If you route through a cloud provider and were counting on it, adjust your plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The migration checklist, in the order things break
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep for the fatal combination&lt;/strong&gt;: &lt;code&gt;thinking: {"type": "disabled"}&lt;/code&gt; anywhere near effort &lt;code&gt;xhigh&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; → that's a 400 now. Decide per integration: drop the &lt;code&gt;thinking&lt;/code&gt; field, or lower effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revisit &lt;code&gt;max_tokens&lt;/code&gt;&lt;/strong&gt; on every request that ran without thinking on 4.8 — the budget now covers reasoning + response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the model ID&lt;/strong&gt; to &lt;code&gt;claude-opus-5&lt;/code&gt; — from a config variable, not a hardcoded string. (Model retirements turn hardcoded IDs into production 404s; ask me how I know.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit your prompts for verification instructions&lt;/strong&gt; and remove them — over-verification is pure waste on Opus 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn on streaming&lt;/strong&gt; for anything with a large &lt;code&gt;max_tokens&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you must run thinking-disabled&lt;/strong&gt;, make your parser survive tool calls in text and stray XML tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run your evals before shifting traffic.&lt;/strong&gt; "Better on average" is not "better on your tasks."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your architecture makes step 3 scary — model IDs scattered across services, no config layer, no eval gate — that's a structural problem worth fixing once, properly. Going from "API calls sprinkled through the codebase" to "an application where a model swap is a config change" is the arc of our &lt;a href="https://cursuri-ai.ro/courses/construire-aplicatii-ai-python-sdk" rel="noopener noreferrer"&gt;building AI applications with Python course&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Opus 5 doesn't ask you to learn a new API — it asks you to &lt;strong&gt;re-check the assumptions&lt;/strong&gt; your existing code was built on. Thinking is on unless you say otherwise, &lt;code&gt;effort&lt;/code&gt; is the lever that matters, &lt;code&gt;max_tokens&lt;/code&gt; now pays for cognition too, and one previously-valid parameter combo is a hard error. In exchange you get a 1M-token window as the default, 128k output, cheaper caching, and betas that clean up real architectural pain.&lt;/p&gt;

&lt;p&gt;Teams that treat this as a checklist migration — rather than a find-and-replace on the model name — will come out with integrations that are cheaper and more reliable than what they had. And that discipline, built once, pays out again on every launch that follows.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want structured, hands-on training on any of this — evals, agentic coding, production LLM apps — that's what we build at &lt;a href="https://cursuri-ai.ro" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>javascript</category>
      <category>opus</category>
    </item>
    <item>
      <title>Multi-Agent AI Systems: When One Agent Isn't Enough — and When Two Is Already Too Many</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Wed, 22 Jul 2026 20:54:34 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/multi-agent-ai-systems-when-one-agent-isnt-enough-and-when-two-is-already-too-many-38kj</link>
      <guid>https://dev.to/cursuri-ai/multi-agent-ai-systems-when-one-agent-isnt-enough-and-when-two-is-already-too-many-38kj</guid>
      <description>&lt;p&gt;Somewhere around the third time your agent's context window filled up with search results it no longer needed, you had the thought every agent builder eventually has: &lt;em&gt;what if I split this into multiple agents?&lt;/em&gt; One to research, one to write, one to review. Maybe a "manager" agent on top. It sounds like an org chart, and org charts feel like architecture.&lt;/p&gt;

&lt;p&gt;Sometimes that instinct is exactly right — Anthropic's research system, built as an orchestrator with parallel subagents, outperformed its best single-agent setup by a wide margin on internal evals. And sometimes it's exactly wrong — Cognition (the team behind Devin) published a piece bluntly titled &lt;a href="https://cognition.ai/blog/dont-build-multi-agents" rel="noopener noreferrer"&gt;"Don't Build Multi-Agents"&lt;/a&gt;, arguing that for their domain, splitting the work is how you manufacture inconsistency. Both teams are right, because they're solving different problems.&lt;/p&gt;

&lt;p&gt;This article is the decision framework between those two positions: what multi-agent architectures actually buy you, what they cost, the orchestration pattern that works in production, and the failure modes that don't make it into the launch posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a multi-agent system actually is
&lt;/h2&gt;

&lt;p&gt;Strip the buzz and it's simple: a &lt;strong&gt;multi-agent system&lt;/strong&gt; is one where an LLM-driven agent can delegate work to &lt;em&gt;other&lt;/em&gt; LLM-driven agents, each running in its &lt;strong&gt;own context window&lt;/strong&gt;, and use their results. The common production shape is &lt;strong&gt;orchestrator–worker&lt;/strong&gt;: a lead agent owns the goal, decomposes it into subtasks, spawns subagents to execute them (often in parallel), and synthesizes what comes back.&lt;/p&gt;

&lt;p&gt;That "own context window" clause is the entire point. It's not about the anthropomorphic org chart — it's about &lt;strong&gt;context isolation&lt;/strong&gt;. Everything else follows from it.&lt;/p&gt;

&lt;p&gt;Notice what this is &lt;em&gt;not&lt;/em&gt;: a pipeline of prompts you call in sequence from your own code is not a multi-agent system — it's just a program (and often, that's exactly what you should build instead). The multi-agent label earns its keep when the &lt;em&gt;decomposition itself is dynamic&lt;/em&gt; — when an agent decides at runtime what to delegate, to whom, and with what instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things fan-out actually buys you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Context isolation.&lt;/strong&gt; A subagent that greps a codebase or reads twenty web pages burns thousands of tokens on intermediate noise — raw file contents, search results, dead ends. In a single-agent design, all of that lands in the one context window you have, where it dilutes attention for every subsequent step. In an orchestrator–worker design, the noise lives and &lt;em&gt;dies&lt;/em&gt; in the subagent's window; the orchestrator receives a compressed conclusion. You've effectively multiplied your usable context by the number of workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Parallelism.&lt;/strong&gt; Independent subtasks — "evaluate these five libraries," "check each of these twelve services for the deprecated call" — can run concurrently instead of serially. For research-shaped and audit-shaped work, this is a wall-clock improvement measured in multiples, not percent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Separation of concerns.&lt;/strong&gt; Each subagent gets a narrow prompt, a curated toolset, and one job. A focused agent with six relevant tools reliably beats a generalist juggling twenty — the same reason you curate tool sets aggressively in &lt;a href="https://cursuri-ai.ro/en/courses/context-engineering-and-memory-for-ai-agents" rel="noopener noreferrer"&gt;context engineering for agents&lt;/a&gt;, applied at the architecture level.&lt;/p&gt;

&lt;p&gt;Anthropic's engineering write-up on &lt;a href="https://www.anthropic.com/engineering/built-multi-agent-research-system" rel="noopener noreferrer"&gt;their multi-agent research system&lt;/a&gt; put numbers on this: the orchestrator-with-parallel-subagents architecture outperformed their single-agent baseline by &lt;strong&gt;90.2%&lt;/strong&gt; on an internal research eval, and they found that token usage — how much &lt;em&gt;relevant&lt;/em&gt; thinking-and-reading the system could do — explained most of the performance variance. Fan-out is, at bottom, a way to spend more tokens productively on one problem than a single context window physically allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs you
&lt;/h2&gt;

&lt;p&gt;The same write-up is refreshingly honest about the bill: in their data, a single agent used roughly &lt;strong&gt;4× more tokens than a chat interaction, and multi-agent systems roughly 15×&lt;/strong&gt;. That's the entry fee. Multi-agent architectures only make economic sense when the task's value clears it.&lt;/p&gt;

&lt;p&gt;The subtler cost is &lt;strong&gt;coordination&lt;/strong&gt;. The orchestrator communicates with subagents through &lt;em&gt;task descriptions&lt;/em&gt; — and every ambiguity in those descriptions becomes a subagent doing confidently the wrong thing in a context you can't see. Early versions of Anthropic's system had subagents duplicating each other's work, wandering off-scope, and searching for things other subagents had already found — not because the model was weak, but because the delegation instructions were vague. The fix was unglamorous prompt engineering: each delegation needs an explicit objective, output format, tool guidance, and &lt;em&gt;boundaries&lt;/em&gt; where the subtask ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case against — and it's a strong one
&lt;/h2&gt;

&lt;p&gt;Cognition's counter-argument deserves to be taken seriously, because it identifies the precise condition under which multi-agent designs fail: &lt;strong&gt;when subtasks are not actually independent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Their core principles: share full context and trace history, and remember that &lt;em&gt;actions carry implicit decisions&lt;/em&gt;. When two subagents work in parallel from partial views of the task, each makes small implicit decisions — naming, structure, interpretation of an ambiguous requirement. When their outputs meet, those decisions conflict. For coding work, where part A and part B must compile, link, and agree on conventions, parallel agents with partial context produce exactly the inconsistency you'd expect from two contractors who never spoke to each other.&lt;/p&gt;

&lt;p&gt;So the honest synthesis of the two positions isn't "Anthropic vs. Cognition" — it's a property of the &lt;em&gt;task&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-heavy, decomposable, breadth-first&lt;/strong&gt; work (research, audits, evaluation sweeps, codebase exploration) parallelizes beautifully. Subagent outputs are &lt;em&gt;findings&lt;/em&gt; that an orchestrator can merge, and conflicts between them are cheap to resolve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-heavy, interdependent, convention-laden&lt;/strong&gt; work (feature implementation, refactoring, anything where outputs must cohere) punishes fan-out. Here a single agent with a well-managed context — compaction, external memory, just-in-time retrieval — is the stronger architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful mnemonic: &lt;strong&gt;fan out to read, stay single to write.&lt;/strong&gt; It's not absolute (isolated write tasks in separate worktrees parallelize fine), but it's the right default, and knowing when to break it is precisely the judgment that &lt;a href="https://cursuri-ai.ro/en/courses/ai-agents-architecture-and-automation" rel="noopener noreferrer"&gt;a serious course on AI agent architecture&lt;/a&gt; spends real time building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The orchestration patterns that survive production
&lt;/h2&gt;

&lt;p&gt;If your task passes the test above, these are the shapes that hold up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orchestrator–worker (the default).&lt;/strong&gt; One lead agent decomposes, delegates, synthesizes. Workers don't talk to each other — results flow through the orchestrator. This keeps the communication topology a star, not a mesh, which is the difference between debuggable and not. Resist the peer-to-peer "agents chatting with agents" fantasy; in production it mostly produces token-expensive misunderstandings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipeline with isolated stages.&lt;/strong&gt; Each item flows through fixed stages (find → verify → summarize), each stage its own agent call with its own clean context. No barrier between items — item A can be in stage 3 while item B is in stage 1. Deterministic control flow in &lt;em&gt;your&lt;/em&gt; code, LLM judgment inside each stage. This hybrid — code decides the structure, agents fill the slots — is the most underrated pattern in the space, and it's where most "do we even need a framework?" questions dissolve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adversarial verification.&lt;/strong&gt; Spawn N independent agents to &lt;em&gt;refute&lt;/em&gt; a finding rather than confirm it, and keep only what survives. This is fan-out used not for speed but for &lt;strong&gt;confidence&lt;/strong&gt; — independent contexts mean independent mistakes, which is exactly what majority voting needs to work. It's the cheapest defense against the plausible-but-wrong output that a single agent will happily double down on.&lt;/p&gt;

&lt;p&gt;Three implementation rules that pay for themselves regardless of pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Delegate like you're writing a ticket for a contractor.&lt;/strong&gt; Objective, output format, tools to use, scope boundaries. If a competent human would need to ask a clarifying question, your subagent will instead guess — silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return data, not prose.&lt;/strong&gt; Subagents should report structured results (schema-validated if your stack allows it), because the orchestrator is a program consuming output, not a reader enjoying it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match effort to value.&lt;/strong&gt; Simple lookups get one worker with a small budget; open-ended research earns a fleet. An orchestrator that spawns ten subagents for a question one search would answer is the multi-agent version of the microservices monorepo with three users.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The failure modes nobody demos
&lt;/h2&gt;

&lt;p&gt;Every one of these is a thing that happens &lt;em&gt;after&lt;/em&gt; the demo works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicated work.&lt;/strong&gt; Two subagents independently discover the same fact at full token price, because the orchestrator's task descriptions didn't partition the space. Partition explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compounding errors.&lt;/strong&gt; In a pipeline, a stage-one hallucination becomes stage-two's trusted input. Multi-agent systems don't just propagate errors — they &lt;em&gt;launder&lt;/em&gt; them: by the final synthesis, the wrong fact arrives with the confidence of having been "processed" three times. This is why verification stages belong &lt;em&gt;early&lt;/em&gt;, not only at the end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The telephone game.&lt;/strong&gt; Every orchestrator-mediated hop compresses information. Three hops in, "the test fails intermittently on CI under load" has become "the tests are broken." Keep hierarchies shallow — one level of delegation covers almost every real use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lost partial work.&lt;/strong&gt; Ten parallel subagents, one crashes at minute eight. If your harness can't resume without re-running the other nine, you'll pay the 15× token bill more than once. Checkpointing and resumability are boring, and they are the difference between a system and a demo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the meta-failure that enables all of the above: &lt;strong&gt;not measuring.&lt;/strong&gt; A multi-agent system has more knobs than anything else you'll build with LLMs — decomposition granularity, worker count, delegation prompts, synthesis strategy — and every knob is a chance to regress silently. The teams that ship these systems treat &lt;a href="https://cursuri-ai.ro/en/courses/llm-evaluation-and-testing" rel="noopener noreferrer"&gt;evaluation as first-class infrastructure&lt;/a&gt;: a representative task set, end-to-end outcome scoring (with an LLM judge where rubrics allow), and a re-run on every architectural change. "It seemed better on the three queries I tried" is how 15× token bills get approved for 1× results.&lt;/p&gt;

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

&lt;p&gt;Before you split one agent into several, put the task through this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;If yes →&lt;/th&gt;
&lt;th&gt;If no →&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can subtasks run with &lt;strong&gt;genuinely independent context&lt;/strong&gt;?&lt;/td&gt;
&lt;td&gt;fan-out is viable&lt;/td&gt;
&lt;td&gt;stay single-agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the work &lt;strong&gt;read/analyze-heavy&lt;/strong&gt; rather than write/produce-heavy?&lt;/td&gt;
&lt;td&gt;fan-out helps&lt;/td&gt;
&lt;td&gt;single agent + context engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the task &lt;strong&gt;value clear a ~15× token cost&lt;/strong&gt;?&lt;/td&gt;
&lt;td&gt;proceed&lt;/td&gt;
&lt;td&gt;simplify the architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Would a fixed pipeline in code + single agent calls do it?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;build that instead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;orchestrator–worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you have evals to catch coordination regressions?&lt;/td&gt;
&lt;td&gt;ship&lt;/td&gt;
&lt;td&gt;build evals first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note how often the honest answer is the boring one. A single agent with disciplined context management — compaction, external memory, sub-tasking only when context demands it — is the right architecture for most of what gets built today, and wiring either variant into a real product with retries, budgets, and observability is &lt;a href="https://cursuri-ai.ro/en/courses/advanced-llm-integration-in-production" rel="noopener noreferrer"&gt;its own production discipline&lt;/a&gt; on top of the architecture choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Multi-agent systems aren't the next tier above single agents — they're a specialized tool that trades tokens and coordination complexity for context capacity and parallelism. That trade is spectacular for breadth-first, read-heavy, decomposable work, and actively harmful for interdependent, convention-heavy production of artifacts. Fan out to read; stay single to write; let deterministic code own the control flow either way; and put an eval behind every knob.&lt;/p&gt;

&lt;p&gt;The teams that get this right aren't the ones with the most agents. They're the ones who can articulate, for their specific task, what a second context window &lt;em&gt;buys&lt;/em&gt; — and who kept the architecture exactly one notch more complex than that answer requires.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and teach this material at &lt;a href="https://cursuri-ai.ro/en/" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, an AI education platform with hands-on English-language tracks on AI agent architecture, context engineering, LLM evaluation, and production integration.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources &amp;amp; further reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic — &lt;a href="https://www.anthropic.com/engineering/built-multi-agent-research-system" rel="noopener noreferrer"&gt;How we built our multi-agent research system&lt;/a&gt; (orchestrator–worker pattern, 90.2% eval result, token-cost data, delegation lessons)&lt;/li&gt;
&lt;li&gt;Anthropic — &lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Building effective agents&lt;/a&gt; (workflows vs. agents, orchestrator–workers, "simplest thing that works")&lt;/li&gt;
&lt;li&gt;Cognition — &lt;a href="https://cognition.ai/blog/dont-build-multi-agents" rel="noopener noreferrer"&gt;Don't Build Multi-Agents&lt;/a&gt; (context-sharing principles, the case for single-threaded agents)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article is educational content. Architectures, model capabilities, and costs evolve quickly — validate against your own workloads and current documentation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Fine-Tuning vs RAG vs Prompting: How to Actually Decide in 2026</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Wed, 15 Jul 2026 11:49:01 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/fine-tuning-vs-rag-vs-prompting-how-to-actually-decide-in-2026-11gj</link>
      <guid>https://dev.to/cursuri-ai/fine-tuning-vs-rag-vs-prompting-how-to-actually-decide-in-2026-11gj</guid>
      <description>&lt;p&gt;There's a predictable arc to most LLM projects. Something doesn't work, someone says "we should fine-tune it," a month disappears into dataset wrangling and GPU bills, and the model comes back... about as wrong as before — because the actual problem was that it never had the right facts in front of it. Fine-tuning was never going to fix that.&lt;/p&gt;

&lt;p&gt;The three techniques — &lt;strong&gt;prompting&lt;/strong&gt;, &lt;strong&gt;retrieval-augmented generation (RAG)&lt;/strong&gt;, and &lt;strong&gt;fine-tuning&lt;/strong&gt; — are not a ladder you climb from cheap to fancy. They solve &lt;em&gt;different problems&lt;/em&gt;, and choosing the wrong one is expensive in exactly the way that's hard to notice: it looks like progress while it burns weeks.&lt;/p&gt;

&lt;p&gt;This is a decision framework. Not "here's what each one is" — you can get that anywhere — but the concrete questions that tell you which one your problem actually needs, and the failure signatures that mean you picked wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one distinction that resolves most arguments
&lt;/h2&gt;

&lt;p&gt;Before any framework, internalize this split, because it settles 80% of the "should we fine-tune?" debates on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG changes what the model &lt;em&gt;knows&lt;/em&gt;.&lt;/strong&gt; It injects facts into the context at inference time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning changes how the model &lt;em&gt;behaves&lt;/em&gt;.&lt;/strong&gt; It adjusts the weights to shift style, format, structure, and task-specific skill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompting changes what the model is &lt;em&gt;told to do&lt;/em&gt; right now&lt;/strong&gt;, using the knowledge and behavior it already has.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the first question is never "which technique is best?" It's "&lt;strong&gt;is my problem a knowledge gap or a behavior gap?&lt;/strong&gt;"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model gives outdated, made-up, or "I don't have access to that" answers about &lt;em&gt;your&lt;/em&gt; data → &lt;strong&gt;knowledge gap → RAG.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The model knows the facts but won't reliably produce the &lt;em&gt;format, tone, or task structure&lt;/em&gt; you need → &lt;strong&gt;behavior gap → fine-tuning (maybe).&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You haven't seriously tried telling it clearly what to do yet → &lt;strong&gt;prompting, first, always.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get this wrong and no amount of engineering saves you. Fine-tuning a model to "know your docs" is the classic error: you can bake a &lt;em&gt;few&lt;/em&gt; facts into weights, but they go stale the moment your docs change, you can't cite sources, and you've spent training compute to build a worse version of a lookup. Knowledge that changes belongs in retrieval, not in weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Always start with prompting (yes, even now)
&lt;/h2&gt;

&lt;p&gt;Prompting is not the beginner tier you graduate from. In 2026, with frontier models, a well-constructed prompt plus a few good examples solves a startling share of problems that teams &lt;em&gt;assume&lt;/em&gt; need training. It's the fastest, cheapest, most inspectable option, and it should be your baseline before you're allowed to say the word "fine-tune."&lt;/p&gt;

&lt;p&gt;Reach for prompting when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're still discovering what "good output" even looks like. Prompts are editable in seconds; datasets are not.&lt;/li&gt;
&lt;li&gt;The task is reasoning, transformation, or generation the model already broadly knows how to do.&lt;/li&gt;
&lt;li&gt;You need to ship this week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The techniques that make prompting punch above its weight are unglamorous but real: precise role and task framing, &lt;strong&gt;few-shot examples&lt;/strong&gt; that demonstrate the exact output shape, chain-of-thought for multi-step reasoning, and rigid output contracts (structured/JSON) so downstream code can trust the result. Most "the model can't do this" conclusions are actually "we asked badly" conclusions. Squeezing the ceiling out of prompting before spending on anything heavier is a discipline in itself — it's the whole point of a &lt;a href="https://cursuri-ai.ro/courses/prompt-engineering-masterclass" rel="noopener noreferrer"&gt;prompt engineering masterclass&lt;/a&gt;, and the ROI of getting it right first is enormous because everything downstream inherits a better baseline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The prompting ceiling — how you know you've hit it:&lt;/strong&gt; you've iterated seriously, added good examples, and the model &lt;em&gt;still&lt;/em&gt; fails — and the failure is either (a) it doesn't know facts it couldn't possibly know, or (b) it can't hold a consistent behavior across inputs no matter how you phrase the instruction. (a) points to RAG. (b) &lt;em&gt;might&lt;/em&gt; point to fine-tuning. Not before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reach for RAG when the problem is knowledge
&lt;/h2&gt;

&lt;p&gt;RAG is the answer whenever the model needs to work with information it wasn't trained on: your internal documentation, a product catalog, last week's tickets, a knowledge base that updates daily, anything private or fresh.&lt;/p&gt;

&lt;p&gt;Choose RAG when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answers must be &lt;strong&gt;grounded in a specific corpus&lt;/strong&gt; and you need to &lt;strong&gt;cite sources&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The knowledge &lt;strong&gt;changes&lt;/strong&gt; — pricing, policies, docs, inventory. You update an index, not a model.&lt;/li&gt;
&lt;li&gt;Hallucination on facts is unacceptable and you need an audit trail of &lt;em&gt;where&lt;/em&gt; an answer came from.&lt;/li&gt;
&lt;li&gt;The knowledge base is large, or partly access-controlled per user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason RAG beats fine-tuning for knowledge isn't subtle: updating a document store is trivial and instant; updating weights is a training run. RAG gives you freshness, provenance, and per-user access control for free — none of which fine-tuning can offer. When your facts have a shelf life, retrieval is the &lt;em&gt;only&lt;/em&gt; correct architecture, and building it well (chunking, hybrid search, re-ranking) is where the real engineering lives — the substance of a dedicated &lt;a href="https://cursuri-ai.ro/courses/rag-retrieval-augmented-generation" rel="noopener noreferrer"&gt;course on RAG and retrieval-augmented generation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG's own ceiling:&lt;/strong&gt; retrieval fixes &lt;em&gt;what the model knows&lt;/em&gt;, not &lt;em&gt;how it behaves&lt;/em&gt;. If your RAG answers are factually correct but come out in the wrong format, wrong tone, or don't follow your house style no matter how you prompt — that residual behavior gap is exactly where fine-tuning finally earns its place, &lt;em&gt;on top of&lt;/em&gt; RAG, not instead of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine-tune when the problem is behavior — and only then
&lt;/h2&gt;

&lt;p&gt;Fine-tuning is the right tool, but for a narrower set of problems than its reputation suggests. It shines at teaching &lt;em&gt;consistent behavior&lt;/em&gt; that's hard to specify in a prompt: a very specific output structure, a domain's tone and terminology, a classification or extraction task where you have lots of labeled examples, or a skill the base model does clumsily.&lt;/p&gt;

&lt;p&gt;Legitimately reach for fine-tuning when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need &lt;strong&gt;consistent style, format, or structure&lt;/strong&gt; at a level prompting can't hold across the full input distribution.&lt;/li&gt;
&lt;li&gt;You have a &lt;strong&gt;narrow, high-volume, well-defined task&lt;/strong&gt; (classification, extraction, a specific transformation) and enough quality labeled data.&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;bake in a behavior&lt;/strong&gt; so you can drop it from the prompt — shorter prompts, lower per-call cost, faster responses at scale.&lt;/li&gt;
&lt;li&gt;Latency or cost at scale matters and a smaller fine-tuned model can match a bigger prompted one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things make modern fine-tuning far less scary than its reputation. First, &lt;strong&gt;you almost never do full fine-tuning&lt;/strong&gt; — parameter-efficient methods like &lt;strong&gt;LoRA/QLoRA&lt;/strong&gt; train a tiny set of adapter weights, cutting the compute and memory cost by orders of magnitude while getting most of the benefit. Second, the bottleneck is &lt;strong&gt;data quality, not model choice&lt;/strong&gt;: a few hundred to a few thousand &lt;em&gt;clean, consistent, representative&lt;/em&gt; examples beat a huge noisy pile every time. The hard part of fine-tuning was never running the training job — it's building the dataset, choosing PEFT trade-offs, and evaluating the result without fooling yourself, which is precisely the ground a &lt;a href="https://cursuri-ai.ro/courses/fine-tuning-modele-ai" rel="noopener noreferrer"&gt;fine-tuning course&lt;/a&gt; has to cover to be worth anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When fine-tuning is the &lt;em&gt;wrong&lt;/em&gt; answer — the red flags:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"We'll fine-tune it on our docs so it knows them." → No. That's RAG. Fine-tuned facts go stale and can't be cited.&lt;/li&gt;
&lt;li&gt;"We haven't really tried prompting." → Do that first; you may not need to train at all.&lt;/li&gt;
&lt;li&gt;"The requirements change weekly." → Fine-tuning bakes behavior in; if the target moves, you're re-training constantly. Keep it in the prompt until it stabilizes.&lt;/li&gt;
&lt;li&gt;"We have 40 examples." → Usually not enough for reliable behavior change; strong prompting with those 40 as few-shot examples will likely beat it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The combinations are the real answer
&lt;/h2&gt;

&lt;p&gt;Framing these as rivals is the beginner mistake. In production, the strongest systems &lt;strong&gt;combine&lt;/strong&gt; them, because they operate on different axes — knowledge, behavior, and instruction — and stack cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG + prompting&lt;/strong&gt; is the workhorse for most knowledge-grounded assistants: retrieve the right context, then a well-engineered prompt instructs the model to answer &lt;em&gt;only&lt;/em&gt; from it and cite sources. No training required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning + RAG&lt;/strong&gt; is the high end: fine-tune for the domain's &lt;em&gt;behavior&lt;/em&gt; (tone, format, task skill), and use RAG for the &lt;em&gt;facts&lt;/em&gt;. The model behaves exactly right &lt;em&gt;and&lt;/em&gt; stays current — behavior in the weights, knowledge in the index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning + prompting&lt;/strong&gt; collapses a long, brittle instruction into learned behavior, so your prompts get short and your inference gets cheaper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Orchestrating these — deciding which layer owns which responsibility, and routing a request through retrieval, tools, and the model in the right order — is its own engineering discipline, and it's the core of a &lt;a href="https://cursuri-ai.ro/courses/advanced-llm-integration" rel="noopener noreferrer"&gt;course on advanced LLM integration&lt;/a&gt;. The mental model to keep: &lt;strong&gt;knowledge → retrieval, behavior → weights, instruction → prompt.&lt;/strong&gt; Put each requirement on the axis it actually lives on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision, in one pass
&lt;/h2&gt;

&lt;p&gt;Run your problem through this, in order. Stop at the first that fits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Have you genuinely exhausted prompting&lt;/strong&gt; — clear instructions, good few-shot examples, structured output? If not → &lt;strong&gt;prompt.&lt;/strong&gt; (This is where most projects should still be.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the failure a knowledge gap&lt;/strong&gt; — missing, stale, or private facts; needs citations? → &lt;strong&gt;RAG.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the failure a behavior gap&lt;/strong&gt; — format/tone/task consistency the prompt can't hold, &lt;em&gt;and&lt;/em&gt; you have quality labeled data &lt;em&gt;and&lt;/em&gt; the target is stable? → &lt;strong&gt;fine-tune&lt;/strong&gt; (LoRA first).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it both?&lt;/strong&gt; → &lt;strong&gt;RAG for the facts, fine-tuning for the behavior.&lt;/strong&gt; In that order.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And underneath all of it: &lt;strong&gt;you cannot make this decision without evaluation.&lt;/strong&gt; "It seems better" is not data. Before you choose, build a small eval set — representative inputs with known-good outputs — so you can measure whether prompting already clears the bar, whether RAG actually retrieves the right context, and whether a fine-tune moved the metric or just moved the failures around. Teams that skip this pick techniques by vibes and discover the mistake in production; teams that treat &lt;a href="https://cursuri-ai.ro/courses/ai-evals-llm-productie" rel="noopener noreferrer"&gt;evals as first-class&lt;/a&gt; make the cheap correct choice on purpose. The eval set is what turns this framework from an opinion into a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The reason so many LLM projects stall isn't a shortage of technique — it's reaching for the wrong one and mistaking motion for progress. Fine-tuning a model to "learn facts," RAG-ing a problem that was really a bad prompt, or grinding on prompts when the model fundamentally lacks the data: each fails in a way that looks like effort.&lt;/p&gt;

&lt;p&gt;Anchor on the split and you'll rarely go wrong. &lt;strong&gt;Knowledge that changes → RAG. Behavior you can't prompt into place → fine-tuning. Everything else → prompt, and prompt well.&lt;/strong&gt; Start cheap, measure honestly, and add complexity only when an eval — not a hunch — tells you the current layer has topped out. The best architecture isn't the most sophisticated one; it's the one that puts each requirement on the axis where it actually belongs.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources &amp;amp; further reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lewis et al. — &lt;a href="https://arxiv.org/abs/2005.11401" rel="noopener noreferrer"&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hu et al. — &lt;a href="https://arxiv.org/abs/2106.09685" rel="noopener noreferrer"&gt;LoRA: Low-Rank Adaptation of Large Language Models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dettmers et al. — &lt;a href="https://arxiv.org/abs/2305.14314" rel="noopener noreferrer"&gt;QLoRA: Efficient Finetuning of Quantized LLMs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article is educational content. Models, tooling, and cost trade-offs evolve quickly; validate any approach against your own data and current provider documentation before committing to it in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Vector Embeddings Explained: Build Semantic Search in Python</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Mon, 13 Jul 2026 11:25:24 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/vector-embeddings-explained-build-semantic-search-in-python-2og1</link>
      <guid>https://dev.to/cursuri-ai/vector-embeddings-explained-build-semantic-search-in-python-2og1</guid>
      <description>&lt;p&gt;Search for "reset my password" in a keyword-based system and a help article titled "How to recover your account credentials" won't match — not one word overlaps. Yet any human knows they mean the same thing. Closing that gap between &lt;em&gt;characters&lt;/em&gt; and &lt;em&gt;meaning&lt;/em&gt; is what &lt;strong&gt;vector embeddings&lt;/strong&gt; do, and they're the quiet engine behind semantic search, RAG, recommendation systems, and most of the "AI that understands you" experiences shipped since 2023.&lt;/p&gt;

&lt;p&gt;This is a practical guide. We'll cover what an embedding actually is, why cosine similarity is the operation you'll use constantly, and then build a real, working semantic search engine in Python — first with pure NumPy so you see the mechanics, then with the tools you'd actually reach for in production. By the end you'll have code that runs and a mental model that transfers to every embedding-powered feature you build next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an embedding actually is
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;embedding&lt;/strong&gt; is a list of numbers — a vector — that represents a piece of content as a point in high-dimensional space. An embedding model (a neural network trained on enormous text corpora) reads your text and outputs, say, 384 or 1,536 floating-point numbers. The magic isn't the numbers themselves; it's the property the training instills: &lt;strong&gt;texts with similar meaning land close together in that space, and unrelated texts land far apart.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole idea. "How do I reset my password?" and "I forgot my login credentials" produce vectors that sit near each other. "What's the weather in Cluj?" produces a vector off in a completely different region. The model has effectively turned &lt;em&gt;meaning&lt;/em&gt; into &lt;em&gt;geometry&lt;/em&gt; — and geometry is something a computer can measure with plain arithmetic.&lt;/p&gt;

&lt;p&gt;A few properties worth internalizing before we write code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dimensionality is fixed per model.&lt;/strong&gt; A given model always outputs the same length (e.g. 384 for &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;, 1,536 for OpenAI's &lt;code&gt;text-embedding-3-small&lt;/code&gt;). You can't mix vectors from different models — they live in different spaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The individual numbers are not interpretable.&lt;/strong&gt; Dimension 200 doesn't mean "formality" or "topic." Meaning is distributed across all dimensions at once. Don't try to read them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distance is the entire point.&lt;/strong&gt; You almost never care about a vector's absolute position — only how close it is to &lt;em&gt;other&lt;/em&gt; vectors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cosine similarity: the one operation you'll use everywhere
&lt;/h2&gt;

&lt;p&gt;To ask "how similar are these two texts?", you compare their vectors. The near-universal choice for text embeddings is &lt;strong&gt;cosine similarity&lt;/strong&gt;: it measures the angle between two vectors, ignoring their magnitude.&lt;/p&gt;

&lt;p&gt;Why the angle and not, say, straight-line (Euclidean) distance? Because for text embeddings, &lt;em&gt;direction&lt;/em&gt; encodes meaning while &lt;em&gt;length&lt;/em&gt; often encodes uninteresting things like text length or token count. Two documents about the same topic point the same way even if one is a sentence and the other a paragraph. Cosine similarity captures exactly that.&lt;/p&gt;

&lt;p&gt;The formula is just the dot product of the two vectors divided by the product of their magnitudes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cos(θ) = (A · B) / (‖A‖ · ‖B‖)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns a value from &lt;strong&gt;-1 to 1&lt;/strong&gt;, though for most modern text embeddings you'll see results land in roughly the 0-to-1 range:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~1.0&lt;/strong&gt; — nearly identical meaning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~0.5&lt;/strong&gt; — loosely related&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~0.0&lt;/strong&gt; — unrelated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single number, computed against a corpus of stored vectors, &lt;em&gt;is&lt;/em&gt; semantic search. Everything else is optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a semantic search engine from scratch
&lt;/h2&gt;

&lt;p&gt;Let's make it concrete. We'll use &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;&lt;code&gt;sentence-transformers&lt;/code&gt;&lt;/a&gt;, which runs a capable embedding model locally — no API key, no network calls, so you can run this offline right now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;sentence-transformers numpy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1 — Embed a corpus
&lt;/h3&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;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="c1"&gt;# A small, fast, widely used model. 384-dimensional output.
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;documents&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;How to reset your account password&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;Refund policy for annual subscriptions&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;Setting up two-factor authentication&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;Our office hours and contact details&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;How to recover a locked account&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;# Encode the whole corpus once. Shape: (5, 384)
&lt;/span&gt;&lt;span class="n"&gt;doc_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&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;doc_embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (5, 384)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;doc_embeddings&lt;/code&gt; array is your search index. In a real app you compute it &lt;strong&gt;once&lt;/strong&gt;, when a document is created or updated, and store it — never on every query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Cosine similarity in NumPy
&lt;/h3&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;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Cosine similarity between vector `a` and each row of matrix `b`.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;a_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;b_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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;keepdims&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;b_norm&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;a_norm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten lines, no dependencies beyond NumPy. This is the actual core of semantic search — the rest is plumbing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Search
&lt;/h3&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;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;query_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_embeddings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&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="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# top-k, highest first
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;documents&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="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&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="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="n"&gt;ranked&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;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I forgot my login credentials&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="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;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&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;Run it, and you'll get something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.62  How to reset your account password
0.55  How to recover a locked account
0.31  Setting up two-factor authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what happened: the query &lt;strong&gt;"I forgot my login credentials"&lt;/strong&gt; shares &lt;em&gt;zero words&lt;/em&gt; with "How to reset your account password," yet it ranked first. A keyword search would have returned nothing. That's the payoff — you matched on meaning, not on string overlap. This shift from lexical to semantic matching is the foundation every retrieval-augmented system builds on, and it's the starting point of a structured &lt;a href="https://cursuri-ai.ro/courses/rag-retrieval-augmented-generation" rel="noopener noreferrer"&gt;course on RAG and retrieval-augmented generation&lt;/a&gt; that goes from this toy index to production retrieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  From toy to production: what changes
&lt;/h2&gt;

&lt;p&gt;The NumPy version is perfect for learning and fine for a few thousand documents. Past that, three things force an upgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  You need a vector database
&lt;/h3&gt;

&lt;p&gt;Computing cosine similarity against &lt;em&gt;every&lt;/em&gt; stored vector on every query is &lt;code&gt;O(n)&lt;/code&gt; — fine at 5 documents, painful at 5 million. Vector databases solve this with &lt;strong&gt;Approximate Nearest Neighbor (ANN)&lt;/strong&gt; indexes (HNSW is the common one) that trade a sliver of accuracy for enormous speed, returning near-neighbors in milliseconds over huge corpora.&lt;/p&gt;

&lt;p&gt;You have good open-source options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pgvector&lt;/strong&gt; — a Postgres extension. If your data already lives in Postgres, this is often the pragmatic choice: vectors and relational data in one place, one backup story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chroma / Qdrant / Weaviate / Milvus&lt;/strong&gt; — purpose-built vector stores with richer filtering and scaling stories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAISS&lt;/strong&gt; — a library (not a server) from Meta for fast similarity search when you want to manage the index yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal Chroma example shows how little the mental model changes:&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;chromadb&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;collection&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="nf"&gt;create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&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;d&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="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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;))])&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_texts&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;I forgot my login credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&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;Chroma embeds the text for you and handles the index. Same concept, production ergonomics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chunking matters more than the model
&lt;/h3&gt;

&lt;p&gt;You rarely embed whole documents. A 30-page PDF becomes one vector that's an average of everything and a good match for nothing. In practice you &lt;strong&gt;chunk&lt;/strong&gt; documents into passages (a few hundred tokens, often with slight overlap) and embed each chunk. Get chunking wrong and even a great embedding model returns mush — which is one of the most common reasons retrieval systems quietly underperform. Chunking strategy, overlap, and metadata are exactly the unglamorous details that separate a demo from a dependable system, and they're covered in depth in a &lt;a href="https://cursuri-ai.ro/courses/advanced-llm-integration" rel="noopener noreferrer"&gt;course on advanced LLM integration for production apps&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing and swapping embedding models
&lt;/h3&gt;

&lt;p&gt;Embedding models differ in dimensionality, speed, cost, and language coverage — and critically, &lt;strong&gt;you must embed your corpus and your queries with the same model.&lt;/strong&gt; Change the model and you re-embed everything. For multilingual apps (Romanian included), pick a model with strong multilingual training rather than an English-first one, or your non-English recall will suffer silently. Public benchmarks like the &lt;strong&gt;MTEB&lt;/strong&gt; leaderboard on Hugging Face are the sane starting point for comparing models on retrieval quality rather than vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid search: when semantic alone isn't enough
&lt;/h2&gt;

&lt;p&gt;Here's a lesson that surprises people: pure semantic search is &lt;em&gt;worse&lt;/em&gt; than keyword search for certain queries. Ask for an exact product code, a specific error like &lt;code&gt;ERR_CONN_REFUSED&lt;/code&gt;, a person's name, or an acronym, and embeddings can betray you — they match on &lt;em&gt;meaning&lt;/em&gt;, and a precise identifier has little semantic meaning to spread around. The embedding for &lt;code&gt;ERR_CONN_REFUSED&lt;/code&gt; sits near "connection problems" generally, so a document about a &lt;em&gt;different&lt;/em&gt; connection error can outrank the exact match.&lt;/p&gt;

&lt;p&gt;The production answer is &lt;strong&gt;hybrid search&lt;/strong&gt;: run both a keyword search (classic lexical scoring like BM25) and a semantic search, then combine the rankings. Keyword search nails exact terms, identifiers, and rare words; semantic search nails paraphrase and intent. Together they cover each other's blind spots.&lt;/p&gt;

&lt;p&gt;The standard way to merge the two result lists is &lt;strong&gt;Reciprocal Rank Fusion (RRF)&lt;/strong&gt; — a simple, robust formula that scores each document by its &lt;em&gt;rank&lt;/em&gt; in each list rather than by raw scores that live on incompatible scales:&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;reciprocal_rank_fusion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rankings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fuse multiple ranked lists of doc IDs into one score per doc.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&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;ranking&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rankings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;# e.g. [keyword_results, semantic_results]
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranking&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&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;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because RRF only looks at positions, you don't have to normalize BM25 scores against cosine similarities — a notorious apples-to-oranges trap. Most serious vector databases (Weaviate, Qdrant, and others) now offer hybrid search with RRF built in, precisely because "just embeddings" quietly underperforms on real, messy query logs. If you take one production lesson from this article, make it this: &lt;strong&gt;measure your recall on real queries, and reach for hybrid the moment exact-match queries show up.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where embeddings show up beyond search
&lt;/h2&gt;

&lt;p&gt;Semantic search is the gateway, but the same vectors power a lot more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG (retrieval-augmented generation)&lt;/strong&gt; — retrieve relevant chunks by embedding similarity, then feed them to an LLM as grounding context. Embeddings are the &lt;em&gt;retrieval&lt;/em&gt; half; without good retrieval, generation hallucinates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication &amp;amp; clustering&lt;/strong&gt; — near-duplicate detection and topic clustering fall out of distances almost for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendations&lt;/strong&gt; — "items similar to this one" is a nearest-neighbor query in embedding space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classification&lt;/strong&gt; — embed labeled examples, then classify new items by nearest neighbors, often without training a dedicated model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The through-line: any time you need "similar in meaning" rather than "matches exactly," embeddings are the tool. Building these features end to end — API to product, with the retrieval and orchestration wired up properly — is the spine of a hands-on &lt;a href="https://cursuri-ai.ro/courses/construire-aplicatii-ai-python-sdk" rel="noopener noreferrer"&gt;course on building AI applications in Python with the OpenAI and Anthropic SDKs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost you hours
&lt;/h2&gt;

&lt;p&gt;A few traps that catch almost everyone the first time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Re-embedding the corpus on every query.&lt;/strong&gt; Embed documents once, store the vectors, embed only the incoming query at search time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixing models.&lt;/strong&gt; Query vectors and document vectors must come from the &lt;em&gt;same&lt;/em&gt; embedding model. A silent mismatch produces garbage rankings with no error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to normalize.&lt;/strong&gt; If you compute raw dot products instead of cosine similarity (and your vectors aren't already unit-normalized), longer texts get an unfair boost. Normalize, or use a library that does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding documents that are too large.&lt;/strong&gt; One vector per giant document averages meaning into uselessness. Chunk first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting a single similarity threshold forever.&lt;/strong&gt; The "good enough" cutoff depends on your model and data. Measure it on real queries; don't hardcode 0.8 because a blog post said so.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Vector embeddings are one of those ideas that feels like magic until you see the mechanics — and then it's just geometry. Text becomes a point in space, meaning becomes distance, and search becomes "find the nearest points." You built exactly that in a few lines of Python, from raw NumPy cosine similarity to a Chroma-backed index, and the same core idea scales from a toy corpus to millions of documents behind an ANN index.&lt;/p&gt;

&lt;p&gt;Start where we started: embed a handful of your own documents, run a query that shares no keywords with the right answer, and watch it surface anyway. Once that clicks, RAG, recommendations, and semantic features stop looking like separate topics and start looking like one tool applied five ways. If you want the structured path from here — retrieval, chunking, evaluation, and production wiring — &lt;a href="https://cursuri-ai.ro/courses/rag-retrieval-augmented-generation" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt; builds it step by step.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources &amp;amp; further reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sentence-Transformers — &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;Official documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hugging Face — &lt;a href="https://huggingface.co/spaces/mteb/leaderboard" rel="noopener noreferrer"&gt;MTEB: Massive Text Embedding Benchmark leaderboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;pgvector — &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;Open-source vector similarity search for Postgres&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chroma — &lt;a href="https://www.trychroma.com/" rel="noopener noreferrer"&gt;Open-source embedding database&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article is educational content. Model names, dimensions, and library APIs evolve; verify current details in the official documentation before building production systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Context Engineering for AI Agents: Beyond Prompt Engineering</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Thu, 09 Jul 2026 13:08:24 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/context-engineering-for-ai-agents-beyond-prompt-engineering-14ep</link>
      <guid>https://dev.to/cursuri-ai/context-engineering-for-ai-agents-beyond-prompt-engineering-14ep</guid>
      <description>&lt;p&gt;You wrote a great prompt. It worked beautifully in the playground — one question, one clean answer. Then you wired the same model into an agent that runs twenty steps, calls six tools, and reads back their output, and somewhere around step twelve it started forgetting the goal, calling the wrong tool, or confidently acting on something it misread three steps ago. The prompt didn't get worse. The &lt;em&gt;context&lt;/em&gt; did.&lt;/p&gt;

&lt;p&gt;This is the gap that context engineering fills. Prompt engineering is about writing one good instruction. Context engineering is about managing the entire set of tokens a model sees at inference time — across a long, multi-step run — so the signal stays high and the model keeps making good decisions. Anthropic frames it as &lt;a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" rel="noopener noreferrer"&gt;the natural progression of prompt engineering&lt;/a&gt;, and if you're building anything agentic in 2026, it's the discipline that separates a demo from a system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What context engineering actually is
&lt;/h2&gt;

&lt;p&gt;Start with a precise definition. &lt;strong&gt;Context&lt;/strong&gt; is the full set of tokens you include when you sample from a large language model. Not just your prompt — the system instructions, the tool definitions, the examples, the running message history, the retrieved documents, the tool results fed back in. Everything in the window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context engineering&lt;/strong&gt; is the set of strategies for curating and maintaining the &lt;em&gt;optimal&lt;/em&gt; set of those tokens during inference. The goal, in one line: find the smallest set of high-signal tokens that reliably produces the outcome you want.&lt;/p&gt;

&lt;p&gt;The reason this is a distinct discipline from prompt engineering is the shape of the problem. A prompt is something you write once and it stays put. Context in an agent is &lt;em&gt;dynamic&lt;/em&gt; — it grows on every turn as the model reads files, calls tools, and accumulates history. You're not authoring a static string anymore; you're managing a budget that fills up on its own, and deciding continuously what earns a place in it and what gets thrown out. That's an engineering problem, and it's &lt;a href="https://cursuri-ai.ro/courses/prompt-engineering-masterclass" rel="noopener noreferrer"&gt;the foundation the whole prompt-to-production journey builds on&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just add more context" is the wrong instinct
&lt;/h2&gt;

&lt;p&gt;The intuitive move, when an agent makes a mistake, is to give it more: more instructions, more examples, more history, more retrieved documents. Sometimes that helps. Very often it makes things worse, and here's why.&lt;/p&gt;

&lt;p&gt;A model's effective attention is a finite resource. Every token you add competes with every other token for the model's limited ability to attend to what matters. Past a certain point, adding context doesn't add capability — it dilutes it. The relevant fact is now buried among ten irrelevant ones, and the model attends to the wrong thing.&lt;/p&gt;

&lt;p&gt;This shows up empirically. The "lost in the middle" effect — &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;documented by Liu et al.&lt;/a&gt; — found that models attend most reliably to information at the &lt;em&gt;start&lt;/em&gt; and &lt;em&gt;end&lt;/em&gt; of a long context, and least reliably to what's stuck in the middle. As context grows, retrieval of any single fact inside it gets less reliable, a degradation sometimes called &lt;em&gt;context rot&lt;/em&gt;. A 200K-token window does not mean you should put 200K tokens in it. Capacity is not the same as attention.&lt;/p&gt;

&lt;p&gt;So the mental model to internalize: &lt;strong&gt;context is a budget, not a bucket.&lt;/strong&gt; You're not trying to fill it. You're trying to spend it on the highest-signal tokens available and refuse everything else. Every technique below is a way to enforce that discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things competing for your window
&lt;/h2&gt;

&lt;p&gt;Before the techniques, know your spenders. In a running agent, four categories of tokens fight for the same finite budget:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The system prompt&lt;/strong&gt; — your instructions, role, constraints. Usually small, high-value, and stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool definitions&lt;/strong&gt; — the schemas describing every tool the agent can call. These are sneakily expensive: each tool's description sits in context on &lt;em&gt;every&lt;/em&gt; turn, whether or not it's used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message history&lt;/strong&gt; — the accumulating transcript of the conversation and the agent's own steps. This is the one that grows without bound and quietly eats the window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieved / external data&lt;/strong&gt; — documents, search results, file contents, database rows pulled in to ground the model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The overall guidance from Anthropic is worth memorizing: keep each of these &lt;strong&gt;informative yet tight&lt;/strong&gt;. Not empty — an under-specified system prompt or a missing tool leaves the model guessing. But not bloated either. The art is the calibration, and it's different for each category. Let's work through the techniques that manage them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 1: Compaction — summarize the history before it drowns you
&lt;/h2&gt;

&lt;p&gt;Message history is the runaway spender. A long agent run accumulates hundreds of turns of "called tool, got 4KB of JSON back, reasoned about it, called the next tool." Most of those raw tool outputs are dead weight three steps later — you needed the &lt;em&gt;conclusion&lt;/em&gt;, not the 4KB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compaction&lt;/strong&gt; is the fix: periodically replace a chunk of verbose history with a tight summary that preserves the decisions, the key facts, and the current state, while dropping the raw noise. When the agent has finished investigating something, you don't need the full transcript of the investigation in context — you need "here's what I found and what it means for the task."&lt;/p&gt;

&lt;p&gt;Practical rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compact at natural boundaries&lt;/strong&gt;, not mid-reasoning — after a sub-task completes, when a phase ends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve the load-bearing details&lt;/strong&gt;: open questions, decisions made, constraints discovered, current state. Drop the intermediate chatter and the raw dumps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the goal pinned.&lt;/strong&gt; The single most common long-run failure is the agent losing the plot on the original objective. The goal should survive every compaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Done well, compaction is what lets an agent run for hundreds of steps without either overflowing its window or forgetting why it started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 2: External memory — let the agent write things down
&lt;/h2&gt;

&lt;p&gt;The window is not the only place to keep information. The most effective long-running agents treat the context window as &lt;em&gt;working memory&lt;/em&gt; and offload durable state to &lt;strong&gt;external memory&lt;/strong&gt; — a file, a scratchpad, a structured store the agent reads from and writes to deliberately.&lt;/p&gt;

&lt;p&gt;Instead of carrying every fact in-context forever, the agent writes a note ("the auth module uses JWT with 15-minute expiry; the bug is in the refresh path") to a persistent store, and pulls it back only when relevant. The context window stays lean; the knowledge doesn't get lost. This is exactly how a human engineer works — you don't hold the entire codebase in your head, you keep notes and open the file when you need it.&lt;/p&gt;

&lt;p&gt;This pattern — persistent, deliberately-managed memory outside the window — is &lt;a href="https://cursuri-ai.ro/courses/context-engineering-memorie-agenti" rel="noopener noreferrer"&gt;the core of building agents that hold state over long horizons&lt;/a&gt;, and it's what turns a stateless model into something that can work a problem across a session without drowning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 3: Sub-agents — isolate context so the main thread stays clean
&lt;/h2&gt;

&lt;p&gt;Here's a structural technique that most people never turn on. When a sub-task is going to burn a lot of tokens — "find every call site of this function across the repo," "research these five libraries" — don't do it in the main agent's context. Delegate it to a &lt;strong&gt;sub-agent&lt;/strong&gt;: a separate agent instance with its own isolated window that does the noisy work and reports back a clean result.&lt;/p&gt;

&lt;p&gt;The win is context hygiene. The thousands of tokens of file contents and search output that the investigation churns through stay in the &lt;em&gt;sub-agent's&lt;/em&gt; context and die with it. The main agent gets back a two-paragraph summary, and its own window stays focused on the actual task instead of silting up with intermediate noise. As a bonus, independent sub-tasks run in parallel instead of serially.&lt;/p&gt;

&lt;p&gt;The rule of thumb: delegate when work is &lt;strong&gt;independent, parallelizable, or context-heavy&lt;/strong&gt;; keep it inline when it's sequential and cheap. Knowing when to fan out to sub-agents and how to orchestrate them without stepping on each other is &lt;a href="https://cursuri-ai.ro/courses/ai-agents-automatizare" rel="noopener noreferrer"&gt;a central skill in building AI agents and automation&lt;/a&gt;, and it's one of the highest-leverage context moves you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 4: Just-in-time retrieval — pull data when needed, not upfront
&lt;/h2&gt;

&lt;p&gt;There are two ways to get external data into an agent. The naive way is to &lt;strong&gt;preload&lt;/strong&gt;: at the start, dump everything the agent might conceivably need into context — the whole document, all the schemas, every config file. The problem is obvious once you see it: you're spending your budget on maybes, and most of it goes unused while crowding out what matters.&lt;/p&gt;

&lt;p&gt;The better pattern is &lt;strong&gt;just-in-time retrieval&lt;/strong&gt;: give the agent the &lt;em&gt;ability&lt;/em&gt; to fetch data (a search tool, a file reader, a database query) and let it pull exactly what it needs, exactly when it needs it. Instead of "here are all 40 files," it's "here's a tool to read a file — go get the one you need." The agent loads the relevant chunk into context at the moment of use, acts on it, and (with compaction) lets it fall away afterward.&lt;/p&gt;

&lt;p&gt;This mirrors how retrieval-augmented systems already work, and getting the retrieval layer right — what to fetch, how to rank it, how much to bring back — is &lt;a href="https://cursuri-ai.ro/courses/advanced-llm-integration" rel="noopener noreferrer"&gt;where advanced LLM integration earns its keep&lt;/a&gt;. Preloading feels safer; just-in-time is what actually scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 5: Tool curation — the failure mode hiding in plain sight
&lt;/h2&gt;

&lt;p&gt;Remember that tool definitions sit in context on every turn. That makes a bloated tool set a double tax: it burns tokens continuously, &lt;em&gt;and&lt;/em&gt; it degrades decisions. Anthropic calls out one of the most common failure modes directly — tool sets that cover too much functionality or create ambiguous, overlapping choices about which tool to use.&lt;/p&gt;

&lt;p&gt;The tell is a sharp one: &lt;strong&gt;if a human engineer can't say for certain which tool should be used in a given situation, an AI agent can't either.&lt;/strong&gt; Fifteen tools with fuzzy, overlapping responsibilities will produce worse behavior than six sharp, non-overlapping ones — and cost more tokens doing it.&lt;/p&gt;

&lt;p&gt;So curate the toolbox like you'd curate an API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fewer, sharper tools.&lt;/strong&gt; Each with a clear, distinct job and an unambiguous "use this when…"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No overlap.&lt;/strong&gt; Two tools that could both plausibly handle the same request is a decision point where the agent will sometimes pick wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prune ruthlessly.&lt;/strong&gt; A tool that's rarely the right choice is paying rent in your context on every single turn. Cut it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool curation is the least glamorous technique here and often the highest-ROI. It's pure subtraction, and subtraction is exactly what context engineering rewards.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you know it's working: measure it
&lt;/h2&gt;

&lt;p&gt;Every technique above is a change to your context, and changes to context are exactly the kind of thing that &lt;em&gt;feels&lt;/em&gt; better while being worse — or vice versa. If your only signal is "I ran a few queries and it seemed fine," you're tuning blind, and you'll ship a regression the day you compact one turn too aggressively and the agent starts forgetting a constraint.&lt;/p&gt;

&lt;p&gt;The fix is the same as anywhere in production ML: an evaluation set. Assemble representative tasks with known good outcomes, and re-run them every time you change how context is managed. Then you can say "compaction at phase boundaries held task success at 0.9 while cutting average tokens 40%" instead of "I think it's better now." Treating &lt;a href="https://cursuri-ai.ro/courses/ai-evals-llm-productie" rel="noopener noreferrer"&gt;evaluation as first-class rather than an afterthought&lt;/a&gt; is what turns context engineering from a craft into engineering — a number that moves when you change something, not a vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;Context engineering isn't a framework you install; it's a posture you adopt toward the model's window. The whole discipline collapses to one principle applied relentlessly: &lt;strong&gt;spend the finite budget on the smallest set of high-signal tokens that does the job.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In practice, for a real agent, that means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compact&lt;/strong&gt; the history at natural boundaries so a long run doesn't drown in its own transcript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offload&lt;/strong&gt; durable state to external memory instead of carrying it forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegate&lt;/strong&gt; noisy, independent work to sub-agents so the main window stays clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve just-in-time&lt;/strong&gt; instead of preloading everything you might need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curate the tools&lt;/strong&gt; hard — fewer, sharper, no overlap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure&lt;/strong&gt; with evals so every change is verified, not hoped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these are exotic, and that's the point. The model is already capable. What makes the capability &lt;em&gt;hold up&lt;/em&gt; over a twenty-step run isn't a cleverer prompt — it's disciplined management of everything the model reads along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Prompt engineering taught us to write one good instruction. Context engineering is what you need the moment that instruction has to survive a long, tool-using, self-accumulating agent run — which is to say, the moment you build anything real. The failure you saw at step twelve was never the model getting dumber. It was the context getting noisier, and no one curating it.&lt;/p&gt;

&lt;p&gt;Adopt the budget mindset, apply the five techniques, and put an eval set behind every change. Do that, and the same model that fell apart at step twelve will run to step fifty and still know exactly what it's doing — because you engineered what it was looking at the whole way.&lt;/p&gt;

&lt;p&gt;The courses linked throughout are part of &lt;a href="https://cursuri-ai.ro/courses/context-engineering-memorie-agenti" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, an AI-learning platform with hands-on, current tracks on context engineering, AI agents, LLM integration, and evaluating AI systems in production.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources &amp;amp; further reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic — &lt;a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" rel="noopener noreferrer"&gt;Effective context engineering for AI agents&lt;/a&gt; (definition, the four context components, tool-set failure modes, compaction and memory)&lt;/li&gt;
&lt;li&gt;Anthropic — &lt;a href="https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents" rel="noopener noreferrer"&gt;Effective harnesses for long-running agents&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Liu et al. — &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;Lost in the Middle: How Language Models Use Long Contexts&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article is educational content. Model behavior, context limits, and tooling evolve quickly; validate approaches against your own workloads and current official documentation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Run LLMs Locally with Ollama in 2026: The Practical Developer Guide</title>
      <dc:creator>galian</dc:creator>
      <pubDate>Sun, 05 Jul 2026 21:11:24 +0000</pubDate>
      <link>https://dev.to/cursuri-ai/run-llms-locally-with-ollama-in-2026-the-practical-developer-guide-48n</link>
      <guid>https://dev.to/cursuri-ai/run-llms-locally-with-ollama-in-2026-the-practical-developer-guide-48n</guid>
      <description>&lt;p&gt;For years, "run the model locally" was the option you mentioned and then didn't take: the models were too weak, the tooling too fiddly, and the cloud APIs too convenient. In 2026 that calculus has genuinely shifted. Open-weight models in the 12–35B range now handle real coding and agent workloads, Apple Silicon got a dedicated inference engine, and Ollama quietly became a drop-in backend for the same tools you already use against cloud APIs — including Claude Code.&lt;/p&gt;

&lt;p&gt;I teach practical AI engineering at &lt;a href="https://cursuri-ai.ro" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt;, Eastern Europe's AI education platform, and local inference has gone from a curiosity module to one of the questions companies ask us most — usually spelled "how do we use LLMs without sending our data anywhere?" This guide is the answer I give developers: what changed, what hardware you actually need, which models are worth pulling, and how to plug it all into a real workflow.&lt;/p&gt;

&lt;p&gt;As always with this space: versions and model rankings move monthly. Everything below is verified against Ollama's official blog and docs as of early July 2026 — re-check before you build a budget or an architecture on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local, and why now
&lt;/h2&gt;

&lt;p&gt;Three arguments have survived contact with production; the rest is mostly vibes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy and data residency.&lt;/strong&gt; With a local model, prompts and outputs never leave your machine (or your VPC, if you self-host on a server). For anyone dealing with client data, medical text, legal documents, or GDPR-sensitive workloads, this eliminates the entire "what does the provider do with my data" conversation instead of managing it through contracts. This is the single biggest adoption driver we see in Europe, and it's the backbone of our &lt;a href="https://cursuri-ai.ro/courses/llm-locale-ollama-privacy-self-hosting" rel="noopener noreferrer"&gt;course on local LLMs, self-hosting, and privacy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost shape.&lt;/strong&gt; Cloud APIs bill per token; local inference bills you once, in hardware you may already own. For high-volume, latency-tolerant workloads — batch classification, summarization pipelines, internal tooling — a mid-range GPU that's already on someone's desk can absorb work that would otherwise be a real monthly line item. (For low-volume or frontier-quality work, cloud still wins. More on that below.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No external dependency.&lt;/strong&gt; A local model doesn't get deprecated, rate-limited, price-changed, or suspended out from under you. After the model-availability surprises of the last year, "at least one workload runs on weights we control" has become a reasonable line item in a resilience plan, not paranoia.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed in Ollama in 2026
&lt;/h2&gt;

&lt;p&gt;If you last touched Ollama when it was "a nice wrapper around llama.cpp," the 2026 releases are the reason to look again. All of this is from &lt;a href="https://ollama.com/blog" rel="noopener noreferrer"&gt;Ollama's official blog&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic API compatibility (January, v0.14.0).&lt;/strong&gt; Ollama now exposes a native Anthropic-style &lt;code&gt;/v1/messages&lt;/code&gt; endpoint. This is the sleeper feature of the year: Anthropic-native tools — most notably Claude Code — can talk to a local model directly, with no proxy or translation layer. There's a matching OpenAI-compatible endpoint too, so Codex and OpenAI-SDK apps work the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ollama launch&lt;/code&gt; (January).&lt;/strong&gt; A single command that configures and starts a coding agent against a local model — &lt;code&gt;ollama launch claude&lt;/code&gt; sets up Claude Code, prompts you to pick a model, and you're in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experimental image generation (January).&lt;/strong&gt; Early days, but the scope of "local model" is no longer text-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MLX engine on Apple Silicon (March preview → June release).&lt;/strong&gt; Ollama moved its Mac inference path to Apple's MLX framework, which exploits unified memory. Ollama's own framing for the June release: its highest performance on Apple Silicon yet — faster output with reduced memory usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama 0.30 and 0.31 (June).&lt;/strong&gt; Version 0.30 brought improved performance and broader GGUF model compatibility through llama.cpp; 0.31 made Gemma 4 significantly faster on Apple Silicon via multi-token prediction (MTP), enabled by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The theme is clear: Ollama is positioning itself less as a hobbyist toy and more as the standard local backend for agentic tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started in five minutes
&lt;/h2&gt;

&lt;p&gt;Install (macOS and Windows have installers at &lt;a href="https://ollama.com/download" rel="noopener noreferrer"&gt;ollama.com/download&lt;/a&gt;; on Linux):&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;-fsSL&lt;/span&gt; https://ollama.com/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull and run a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma4
ollama run gemma4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a working local chat. Ollama also starts a local server on port &lt;code&gt;11434&lt;/code&gt;, which is where the interesting part begins — every API-based tool you have can point at it.&lt;/p&gt;

&lt;p&gt;Useful daily commands: &lt;code&gt;ollama ls&lt;/code&gt; (installed models), &lt;code&gt;ollama ps&lt;/code&gt; (what's loaded and where — CPU vs GPU), &lt;code&gt;ollama rm &amp;lt;model&amp;gt;&lt;/code&gt; (free disk space; models are multi-gigabyte).&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware: the honest sizing guide
&lt;/h2&gt;

&lt;p&gt;The rule of thumb that matters: a model quantized to 4 bits needs very roughly &lt;strong&gt;0.5–0.7 GB of memory per billion parameters&lt;/strong&gt;, plus overhead for context. Everything else follows from that.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your hardware&lt;/th&gt;
&lt;th&gt;What runs comfortably&lt;/th&gt;
&lt;th&gt;Experience&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8 GB RAM, no GPU&lt;/td&gt;
&lt;td&gt;3–8B models, quantized&lt;/td&gt;
&lt;td&gt;Fine for chat, drafting, classification. Slow but usable on CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 GB RAM (Apple Silicon)&lt;/td&gt;
&lt;td&gt;8–14B models&lt;/td&gt;
&lt;td&gt;Good daily-driver territory; MLX made this tier notably faster in 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24 GB+ (M-series Pro/Max or a 24 GB GPU)&lt;/td&gt;
&lt;td&gt;27–35B models&lt;/td&gt;
&lt;td&gt;Where local coding models get genuinely useful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48 GB+ unified memory / multi-GPU&lt;/td&gt;
&lt;td&gt;Large MoE models&lt;/td&gt;
&lt;td&gt;Server-class local inference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two nuances that save people disappointment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quantization is why any of this works.&lt;/strong&gt; Models ship in compressed 4–8 bit variants (the GGUF ecosystem) that trade a small quality loss for a 2–4× memory reduction. Ollama's default tags are already quantized — you rarely need to think about it, but it explains why a "27B model" fits in 24 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixture-of-experts (MoE) models need memory for their &lt;em&gt;total&lt;/em&gt; parameters but compute like their &lt;em&gt;active&lt;/em&gt; subset.&lt;/strong&gt; NVIDIA's Nemotron-3-Super, for example, is a 120B model with 12B active parameters: it runs faster than its size suggests, but you still need the RAM to hold it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Context length eats memory too — an agent session with 32K+ tokens of context adds real overhead on top of the weights. If you're sizing for coding agents, budget for that, not just the model file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mid-2026 open-weight lineup worth knowing
&lt;/h2&gt;

&lt;p&gt;Rankings churn monthly, so treat this as a map, not a leaderboard. From &lt;a href="https://ollama.com/search" rel="noopener noreferrer"&gt;Ollama's model library&lt;/a&gt;, the families that matter right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4&lt;/strong&gt; (12B–31B) — Google's open family, currently the most-pulled model on Ollama. Multimodal, tuned for reasoning and agentic work, and the main beneficiary of the MLX/MTP speedups on Macs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3.5 / Qwen3.6&lt;/strong&gt; (0.8B–122B) — the ecosystem's Swiss army knife. Qwen3.5 spans everything from edge-tiny to server-large; Qwen3.6 (27B–35B) focuses on agentic coding. &lt;strong&gt;qwen3-coder&lt;/strong&gt; is Ollama's own recommendation for coding-agent use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GLM-5 family&lt;/strong&gt; — flagship-class open weights (GLM-5 is 744B total / 40B active); strong at coding and long-horizon tasks. Too big for most desktops locally, but available as &lt;code&gt;:cloud&lt;/code&gt; variants (see below) and self-hostable on serious hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nemotron-3-Super&lt;/strong&gt; (120B MoE, 12B active) — NVIDIA's entry, aimed at multi-agent applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MiniMax-M3&lt;/strong&gt; — notable for a 1M-token context window, if your workload is long-document analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialists&lt;/strong&gt;: GLM-OCR for document understanding, TranslateGemma (4B–27B, 55 languages) for translation, LFM2 (24B) for on-device deployment, Ornith (9B–35B) for agentic coding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sensible defaults: on a 16 GB Mac, start with &lt;code&gt;gemma4:12b&lt;/code&gt;. On 24 GB+, try &lt;code&gt;qwen3-coder&lt;/code&gt; for code and &lt;code&gt;gemma4:27b&lt;/code&gt; for general work. Then run &lt;em&gt;your&lt;/em&gt; tasks on them — a model's rank on someone's benchmark tells you little about your use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that changes your workflow: Ollama as a drop-in API
&lt;/h2&gt;

&lt;p&gt;Ollama's server speaks both major API dialects, which means "switch to a local model" is now a base-URL change, not a rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI-compatible&lt;/strong&gt; (&lt;code&gt;/v1&lt;/code&gt;) — any OpenAI-SDK app works:&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;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;http://localhost:11434/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;ollama&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="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;gemma4&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain GGUF quantization in one paragraph.&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="nf"&gt;print&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;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Anthropic-compatible&lt;/strong&gt; (&lt;code&gt;/v1/messages&lt;/code&gt;) — and this is the one with teeth, because it means &lt;strong&gt;Claude Code runs against local models&lt;/strong&gt;. Per &lt;a href="https://docs.ollama.com/api/anthropic-compatibility" rel="noopener noreferrer"&gt;Ollama's official docs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ollama       &lt;span class="c"&gt;# accepted but not validated&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:11434

claude &lt;span class="nt"&gt;--model&lt;/span&gt; qwen3-coder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or let Ollama do the wiring for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Honest caveats before you get excited:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama recommends &lt;strong&gt;at least 32K tokens of context&lt;/strong&gt; for Claude Code, and its model suggestions for coding are &lt;code&gt;qwen3-coder&lt;/code&gt; locally (30B — you want 24 GB+ of VRAM/unified memory) or &lt;code&gt;glm-4.7:cloud&lt;/code&gt; / &lt;code&gt;minimax-m2.1:cloud&lt;/code&gt; via Ollama's cloud, which keeps the same API surface but runs the weights remotely.&lt;/li&gt;
&lt;li&gt;The compatibility layer doesn't cover everything: &lt;strong&gt;no prompt caching, no token-counting endpoint, no forced tool selection, no batches API, no PDF inputs&lt;/strong&gt; (images must be base64). If your workflow leans on those, you'll feel it.&lt;/li&gt;
&lt;li&gt;A 30B local model is not Opus, and it isn't trying to be. It's "capable pair of hands on an airplane / on confidential code," not "frontier reasoning."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern that actually works in practice is routing: local models for the private, high-volume, or offline work; frontier cloud models for the hard reasoning. Deciding which tier a task belongs to — and building the escalation path — is an architecture skill, and it's exactly the kind of decision we drill in our &lt;a href="https://cursuri-ai.ro/courses/ai-system-architecture" rel="noopener noreferrer"&gt;AI system architecture course&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When local is the wrong choice
&lt;/h2&gt;

&lt;p&gt;Being a fan of local inference means knowing where it loses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontier-quality reasoning.&lt;/strong&gt; For the hardest tasks, top cloud models remain clearly ahead of anything you can run on a workstation. If wrong answers are expensive, don't fight this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-volume workloads.&lt;/strong&gt; If you make a few thousand API calls a month, per-token billing is cheaper than any GPU. Local pays off at volume, at privacy constraints, or at both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ops you don't want.&lt;/strong&gt; A self-hosted model is a service you now run: updates, monitoring, capacity. &lt;code&gt;ollama run&lt;/code&gt; on a laptop is trivial; a team-wide inference server is real infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal breadth and long-tail capabilities.&lt;/strong&gt; Cloud APIs still bundle more (native PDF understanding, larger tool ecosystems, batch APIs) than the local stack replicates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more thing people conflate: running a model locally is different from &lt;em&gt;customizing&lt;/em&gt; one. If your actual goal is a model that speaks your domain language or follows your house style, that's a fine-tuning question — LoRA adapters on an open-weight base, then serving the result through Ollama. That pipeline (when to fine-tune vs when to just engineer the prompt) is its own discipline, covered in our &lt;a href="https://cursuri-ai.ro/courses/fine-tuning-modele-ai" rel="noopener noreferrer"&gt;fine-tuning course&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Ollama free?
&lt;/h3&gt;

&lt;p&gt;The tool itself is open source and free. The models each carry their own licenses — Gemma, Qwen, GLM and friends have different terms, some with restrictions on commercial use. Check the license tab on the model's Ollama page before you ship a product on it. Ollama's optional cloud models are a paid service.&lt;/p&gt;

&lt;h3&gt;
  
  
  What hardware do I need to run LLMs locally in 2026?
&lt;/h3&gt;

&lt;p&gt;As a rule of thumb at 4-bit quantization: 8 GB of RAM runs 3–8B models, 16 GB runs 8–14B comfortably (especially on Apple Silicon with the MLX engine), and 24 GB+ opens up the 27–35B class where local coding models get genuinely useful. More context = more memory on top of the weights.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a local model replace GPT or Claude?
&lt;/h3&gt;

&lt;p&gt;For a growing set of tasks — summarization, classification, drafting, routine coding on mid-size codebases — yes, credibly. For frontier reasoning and the highest-stakes accuracy, no. Production teams typically route: local for private/high-volume work, cloud for the hard 10%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I really use Claude Code with Ollama?
&lt;/h3&gt;

&lt;p&gt;Yes. Since Ollama v0.14.0 (January 2026) there's native Anthropic Messages API compatibility: set &lt;code&gt;ANTHROPIC_BASE_URL=http://localhost:11434&lt;/code&gt;, run &lt;code&gt;claude --model qwen3-coder&lt;/code&gt; — or just &lt;code&gt;ollama launch claude&lt;/code&gt;. Expect a capable assistant, not Opus-level reasoning, and note that prompt caching and a few other API features aren't supported through the compatibility layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ollama vs llama.cpp vs vLLM — which should I use?
&lt;/h3&gt;

&lt;p&gt;Ollama for developer experience: one command, model management, dual API compatibility. llama.cpp (which powers Ollama's GGUF path) for maximum control and minimal footprint. vLLM for high-throughput multi-user serving on server GPUs. Most developers should start with Ollama and only move down the stack when they hit a concrete limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skill underneath the tool
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable part: pulling a model is the easy 5%. The value shows up when you can answer the questions around it — which model for which task, how to measure whether the local model is &lt;em&gt;good enough&lt;/em&gt; for your workload instead of guessing, how to build the routing and fallback so privacy-sensitive work stays local while hard problems escalate to a frontier model. That's engineering judgment, not tooling trivia.&lt;/p&gt;

&lt;p&gt;That judgment is what we teach at &lt;a href="https://cursuri-ai.ro" rel="noopener noreferrer"&gt;our AI education platform&lt;/a&gt; — hands-on courses built around real repositories and an interactive AI instructor, covering the full local-to-cloud spectrum: self-hosting and privacy, fine-tuning, architecture, and the agentic workflow on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In 2026, local LLMs crossed the line from hobby to infrastructure option. Ollama's dual API compatibility means your existing tools — including Claude Code — can run against open weights with a base-URL change; the MLX engine made a 16 GB MacBook a legitimate inference machine; and the open-weight lineup in the 12–35B range is good enough for a real slice of production work.&lt;/p&gt;

&lt;p&gt;The play isn't "cancel your API keys." It's knowing which slice of your workload belongs on weights you control — then running it there deliberately, measured, with an escalation path for everything else. Start with &lt;code&gt;ollama run gemma4&lt;/code&gt; tonight; you're one evening away from having an informed opinion.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by the team at &lt;a href="https://cursuri-ai.ro" rel="noopener noreferrer"&gt;Cursuri-AI.ro&lt;/a&gt; — practical, hands-on AI engineering courses for developers and professionals across Eastern Europe, from local LLMs and self-hosting to agentic coding, evals, and AI system architecture.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://ollama.com/blog" rel="noopener noreferrer"&gt;Ollama Blog&lt;/a&gt; · &lt;a href="https://docs.ollama.com/api/anthropic-compatibility" rel="noopener noreferrer"&gt;Anthropic API compatibility — Ollama Docs&lt;/a&gt; · &lt;a href="https://ollama.com/blog/claude" rel="noopener noreferrer"&gt;Claude Code with Anthropic API compatibility — Ollama&lt;/a&gt; · &lt;a href="https://ollama.com/search" rel="noopener noreferrer"&gt;Ollama Model Library&lt;/a&gt; · &lt;a href="https://ollama.com/download" rel="noopener noreferrer"&gt;Download Ollama&lt;/a&gt;&lt;/p&gt;

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