<?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: ImportStatic</title>
    <description>The latest articles on DEV Community by ImportStatic (@importstatic).</description>
    <link>https://dev.to/importstatic</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4166097%2F565b6edc-1683-4881-a4c8-a21ecb2fbdc9.png</url>
      <title>DEV Community: ImportStatic</title>
      <link>https://dev.to/importstatic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/importstatic"/>
    <language>en</language>
    <item>
      <title>PostgreSQL's recovery_prefetch prefetches pages the WAL already holds</title>
      <dc:creator>ImportStatic</dc:creator>
      <pubDate>Mon, 02 Nov 2026 01:57:22 +0000</pubDate>
      <link>https://dev.to/importstatic/postgresqls-recoveryprefetch-prefetches-pages-the-wal-already-holds-3fc6</link>
      <guid>https://dev.to/importstatic/postgresqls-recoveryprefetch-prefetches-pages-the-wal-already-holds-3fc6</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/databases/postgres-recovery-prefetch-full-page-writes" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://ardentperf.com/2026/10/20/postgres-recovery_prefetch-considered-harmful-and-causing-outages/" rel="noopener noreferrer"&gt;post on ardentperf&lt;/a&gt; from October 20, which reached the Hacker News front page, describes a synchronous standby on a 500 IOPS cloud disk that fell behind after a write burst and stalled writes on the primary for over 30 minutes. With &lt;code&gt;recovery_prefetch = off&lt;/code&gt; the long stall went away, and the author leans towards turning it off everywhere. We don't have a standby on metered storage, so we couldn't reproduce the outage. We did count what the prefetcher spends its reads on during replay on PostgreSQL 18.6, and the same counters are available on your own standby.&lt;/p&gt;

&lt;p&gt;Some of those reads are for pages that replay is about to rebuild from a full-page image in the WAL. The prefetcher leaves a block alone when the record that touches it carries an image, but it doesn't keep track of that block afterwards. When the next record touches the same page, it asks the operating system to read the page from disk. One heap page and two updates are enough to reproduce it, and the cause is a dozen lines of &lt;code&gt;xlogprefetcher.c&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the prefetcher does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/18/runtime-config-wal.html" rel="noopener noreferrer"&gt;&lt;code&gt;recovery_prefetch&lt;/code&gt;&lt;/a&gt; arrived in PostgreSQL 15. The docs describe it as trying "to prefetch blocks that are referenced in the WAL that are not yet in the buffer pool, during recovery". The default is &lt;code&gt;try&lt;/code&gt;, which turns it on wherever the OS can take read-ahead advice. On this Mac the PostgreSQL 18.6 build accepted &lt;code&gt;on&lt;/code&gt;, which a build without read-ahead support rejects.&lt;/p&gt;

&lt;p&gt;The prefetcher decodes WAL ahead of replay. In the &lt;a href="https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/access/transam/xlogprefetcher.c" rel="noopener noreferrer"&gt;REL_18_STABLE source&lt;/a&gt;, it allows &lt;code&gt;maintenance_io_concurrency&lt;/code&gt; reads in flight (16 by default) and looks up to four times that many block references ahead, within the &lt;code&gt;wal_decode_buffer_size&lt;/code&gt; window (512 kB). It only considers the main fork. For each block reference, the checks run in this order, and each outcome increments a column of &lt;code&gt;pg_stat_recovery_prefetch&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Counter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the record carries a full-page image of the block&lt;/td&gt;
&lt;td&gt;&lt;code&gt;skip_fpw&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the block will be zero-initialised&lt;/td&gt;
&lt;td&gt;&lt;code&gt;skip_init&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the relation or block doesn't exist yet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;skip_new&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the block is one of the last four blocks it considered&lt;/td&gt;
&lt;td&gt;&lt;code&gt;skip_rep&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the block is already in shared buffers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;otherwise: read-ahead advice for one 8 kB block&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prefetch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "last four" window is updated after the full-page-image check. A block skipped for its image returns early and never enters the window, so the window can't catch the next reference to it. Shared buffers won't help either, because when the prefetcher reaches that later record, replay often hasn't restored the image yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it with one page
&lt;/h2&gt;

&lt;p&gt;The test table has three 1 kB rows per 8 kB page (&lt;code&gt;fillfactor = 50&lt;/code&gt;, no indexes), so every update is a HOT update on the same page. After a &lt;code&gt;CHECKPOINT&lt;/code&gt;, a Go driver ran these statements, each as its own transaction, then stopped the server with &lt;code&gt;pg_ctl -m immediate&lt;/code&gt; and started it again so crash recovery replayed them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;ctid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'(0,1)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;ctid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'(1,1)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- pages 1 .. N, one row each&lt;/span&gt;
&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;ctid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'(2,1)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;ctid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'(0,2)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- page 0 again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pg_get_wal_block_info&lt;/code&gt; from &lt;a href="https://www.postgresql.org/docs/18/pgwalinspect.html" rel="noopener noreferrer"&gt;pg_walinspect&lt;/a&gt; lists the block references for &lt;code&gt;t&lt;/code&gt; in WAL order. With 20 pages between the two visits to page 0, it printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0+fpi 1+fpi 2+fpi 3+fpi 4+fpi 5+fpi 6+fpi 7+fpi 8+fpi 9+fpi 10+fpi 11+fpi 12+fpi 13+fpi 14+fpi 15+fpi 16+fpi 17+fpi 18+fpi 19+fpi 20+fpi 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first touch of each page after the checkpoint carries a full-page image, as &lt;a href="https://www.postgresql.org/docs/18/runtime-config-wal.html" rel="noopener noreferrer"&gt;&lt;code&gt;full_page_writes&lt;/code&gt;&lt;/a&gt; promises. The second touch of page 0 doesn't. After the crash, the counters were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; prefetch | hit | skip_init | skip_new | skip_fpw | skip_rep
----------+-----+-----------+----------+----------+----------
        1 |   0 |         0 |        0 |       22 |        0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The single prefetch was for page 0. Every other block was skipped for its image; the 22 are the 21 pages of &lt;code&gt;t&lt;/code&gt; plus one &lt;code&gt;pg_attribute&lt;/code&gt; page that a hint-bit write had imaged. Replay restores page 0 from the image in the first record, so the second record finds it in shared buffers and never uses the read the prefetcher asked for.&lt;/p&gt;

&lt;p&gt;With other gaps, and with &lt;code&gt;full_page_writes&lt;/code&gt; off:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pages between the two visits&lt;/th&gt;
&lt;th&gt;&lt;code&gt;full_page_writes&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;prefetch&lt;/th&gt;
&lt;th&gt;hit&lt;/th&gt;
&lt;th&gt;skip_fpw&lt;/th&gt;
&lt;th&gt;skip_rep&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With images on, page 0 is prefetched a second time even when the two records are back to back. With images off, the four-entry window catches the repeat at a gap of 2 (&lt;code&gt;skip_rep = 1&lt;/code&gt;) but not at 20. The short runs count a few more images than &lt;code&gt;t&lt;/code&gt; has because replay also covered catalog hint-bit images written after the test; the &lt;code&gt;prefetch&lt;/code&gt; column is the one to read.&lt;/p&gt;

&lt;p&gt;Stephen Frost raised this case &lt;a href="https://www.postgresql.org/message-id/20210214231815.GO27507@tamriel.snowman.net" rel="noopener noreferrer"&gt;during review in 2021&lt;/a&gt;, estimating thousands of unnecessary prefetch calls in a bad case with the default 512 kB decode buffer. He judged that the benefit outweighed the cost and that the redundancy could be fixed later. As of October 22, 2026, the REL_18_STABLE code still does the image check before it updates the window.&lt;/p&gt;

&lt;h2&gt;
  
  
  pgbench runs
&lt;/h2&gt;

&lt;p&gt;We ran the same crash-and-count loop on pgbench data (scale 50, &lt;code&gt;pgbench_accounts&lt;/code&gt; at 91,513 pages, 128 MB of shared buffers). "Bulk" updates 5% of the accounts in physical order in one statement. "Random" is 160,000 standard pgbench transactions from 8 clients. Each line is one run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;&lt;code&gt;full_page_writes&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;WAL&lt;/th&gt;
&lt;th&gt;prefetch&lt;/th&gt;
&lt;th&gt;hit&lt;/th&gt;
&lt;th&gt;skip_fpw&lt;/th&gt;
&lt;th&gt;skip_rep&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bulk&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;643 MB&lt;/td&gt;
&lt;td&gt;99,192&lt;/td&gt;
&lt;td&gt;28,363&lt;/td&gt;
&lt;td&gt;82,502&lt;/td&gt;
&lt;td&gt;777,355&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bulk&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;71 MB&lt;/td&gt;
&lt;td&gt;126,604&lt;/td&gt;
&lt;td&gt;24,845&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;836,061&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;random&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;684 MB&lt;/td&gt;
&lt;td&gt;143,991&lt;/td&gt;
&lt;td&gt;442,138&lt;/td&gt;
&lt;td&gt;79,855&lt;/td&gt;
&lt;td&gt;242,243&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;random&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;67 MB&lt;/td&gt;
&lt;td&gt;145,457&lt;/td&gt;
&lt;td&gt;458,868&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;290,126&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the random workload the prefetcher skipped 79,855 blocks for their images and still issued only 1,466 fewer prefetches than with images off. The random runs touched different rows, so the two lines aren't an exact pair, but they agree with the single-page test, where skipping a page's first reference mostly moves the prefetch to its next reference. In the bulk case images cut prefetches by about a fifth.&lt;/p&gt;

&lt;p&gt;Crash recovery took between 0.35 and 1.66 seconds in every run, on a laptop SSD with the files still in the OS page cache. Those times say nothing about a standby on a 500 IOPS volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking your own standby
&lt;/h2&gt;

&lt;p&gt;The counters are cumulative from the last reset, so reset them before a write burst and read them after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;pg_stat_reset_shared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'recovery_prefetch'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ... later&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skip_fpw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skip_rep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;io_depth&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_stat_recovery_prefetch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;prefetch&lt;/code&gt; is read-ahead advice for a single 8 kB block. On local NVMe that's cheap, and the default is fine. On a volume with an IOPS cap, a standby whose &lt;code&gt;prefetch&lt;/code&gt; count runs close to &lt;code&gt;skip_fpw&lt;/code&gt; while replay lag grows is the case to test. We'd turn it off there and watch replay lag through the next burst.&lt;/p&gt;

&lt;p&gt;The setting has &lt;code&gt;sighup&lt;/code&gt; context: &lt;code&gt;ALTER SYSTEM SET recovery_prefetch = off&lt;/code&gt; and &lt;code&gt;SELECT pg_reload_conf()&lt;/code&gt; apply it without a restart, which we confirmed in the server log. Changing &lt;code&gt;wal_decode_buffer_size&lt;/code&gt; needs a restart; &lt;code&gt;maintenance_io_concurrency&lt;/code&gt; doesn't, and 0 disables prefetching too.&lt;/p&gt;

&lt;p&gt;If you're planning the move to PostgreSQL 19, check this along with the rest of &lt;a href="https://importstatic.com/databases/postgresql-19-rc1-upgrade-checks" rel="noopener noreferrer"&gt;the upgrade checks&lt;/a&gt;: we only read the REL_18_STABLE prefetcher for this article.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/databases/postgres-recovery-prefetch-full-page-writes" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;. This article was drafted with AI assistance; every claim was checked against the sources linked in it and the code samples were compiled and run.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
      <category>backend</category>
    </item>
    <item>
      <title>anthropic-sdk-go 1.81 traces your Claude calls and forwards your OTel baggage</title>
      <dc:creator>ImportStatic</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:45:38 +0000</pubDate>
      <link>https://dev.to/importstatic/anthropic-sdk-go-181-traces-your-claude-calls-and-forwards-your-otel-baggage-2nn0</link>
      <guid>https://dev.to/importstatic/anthropic-sdk-go-181-traces-your-claude-calls-and-forwards-your-otel-baggage-2nn0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/ai/anthropic-sdk-go-opentelemetry-baggage" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your Go service registers a global OpenTelemetry tracer provider and propagator, upgrading to &lt;a href="https://github.com/anthropics/anthropic-sdk-go/releases/tag/v1.81.0" rel="noopener noreferrer"&gt;anthropic-sdk-go v1.81.0&lt;/a&gt; (released October 14) changes two things with no code change: every Claude call becomes a span, and every request to the API carries the headers your propagator writes. In most Go services those are &lt;code&gt;traceparent&lt;/code&gt; and &lt;code&gt;baggage&lt;/code&gt;, since the OpenTelemetry Go getting-started guide registers both. Whatever your services keep in baggage now goes to Anthropic.&lt;/p&gt;

&lt;p&gt;To see what goes over the wire, we sent the same request from v1.80.0 and v1.81.0 to an &lt;code&gt;httptest&lt;/code&gt; server that returns canned Messages API responses. The request ran inside a handler span whose context held one baggage member, &lt;code&gt;user.email=jane@example.com&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v1.80.0:  traceparent=""  baggage=""
          spans: "POST /tickets/summary"

v1.81.0:  traceparent="00-9cdfdc78baf5c4413a4de1c242b1b145-f6538546ab8336d9-01"
          baggage="user.email=jane@example.com"
          spans: "anthropic.messages.create" (client), "POST /tickets/summary"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key was used and nothing reached Anthropic, so this shows what the SDK sends and nothing about how the real API handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v1.81.0 changes
&lt;/h2&gt;

&lt;p&gt;According to the &lt;a href="https://github.com/anthropics/anthropic-sdk-go/releases/tag/v1.81.0" rel="noopener noreferrer"&gt;release note&lt;/a&gt;, if the app registers a global tracer provider, "the SDK will create spans with it after upgrading", and "requests will also get trace context headers from the global propagator". The &lt;a href="https://platform.claude.com/docs/en/release-notes/overview" rel="noopener noreferrer"&gt;Claude Platform release notes&lt;/a&gt; for the same day list this under a beta of OpenTelemetry export, which also lets the API send its own server-side spans to your collector once an admin adds a destination in the Console.&lt;/p&gt;

&lt;p&gt;The SDK side needs no setup with Anthropic. It reads &lt;code&gt;otel.GetTracerProvider()&lt;/code&gt; and &lt;code&gt;otel.GetTextMapPropagator()&lt;/code&gt;, so any service that calls &lt;code&gt;otel.SetTracerProvider&lt;/code&gt; at startup is opted in.&lt;/p&gt;

&lt;p&gt;Services that pass a provider explicitly to their instrumentation and never set the global one are only partly opted out. In our run of that setup (handler span from a non-global provider, global propagator registered), the SDK recorded no span of its own, but the request still went out with &lt;code&gt;traceparent&lt;/code&gt;, carrying the handler span's ID, and the baggage. A comment in &lt;code&gt;StartAPICallSpan&lt;/code&gt; covers this case: a non-recording span is still used when its trace context "goes on" in the headers. A process sends nothing only if it has neither a span in the context nor a registered propagator.&lt;/p&gt;

&lt;p&gt;The feature is in beta. The &lt;a href="https://platform.claude.com/docs/en/manage-claude/opentelemetry/sdk" rel="noopener noreferrer"&gt;SDK tracing guide&lt;/a&gt; warns that span names, attributes, options and environment variables can change between versions and tells you to pin the SDK version, which matters if you build dashboards on these attribute names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baggage header
&lt;/h2&gt;

&lt;p&gt;The SDK guide mentions it once: "If that propagator also writes baggage, the SDK sends your baggage too." OpenTelemetry's &lt;a href="https://opentelemetry.io/docs/concepts/signals/baggage/" rel="noopener noreferrer"&gt;baggage page&lt;/a&gt; warns that "sensitive Baggage items can be shared with unintended resources, like third-party APIs", and api.anthropic.com is now one of them.&lt;/p&gt;

&lt;p&gt;Before this release, a Claude request only carried that header if you'd wrapped the SDK's HTTP client in &lt;code&gt;otelhttp&lt;/code&gt; or set it yourself. With v1.81.0, any service that registered the composite propagator from the &lt;a href="https://opentelemetry.io/docs/languages/go/getting-started/" rel="noopener noreferrer"&gt;getting-started guide&lt;/a&gt; sends it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTextMapPropagator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propagation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCompositeTextMapPropagator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;propagation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TraceContext&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;propagation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Baggage&lt;/span&gt;&lt;span class="p"&gt;{}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To know what leaves, check baggage extracted from incoming requests as well as what your own code sets. Upstream services add members too, so grepping for &lt;code&gt;baggage.NewMember&lt;/code&gt; only finds part of it.&lt;/p&gt;

&lt;p&gt;We tested three ways to keep &lt;code&gt;traceparent&lt;/code&gt; and drop &lt;code&gt;baggage&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;traceparent&lt;/code&gt; sent&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;baggage&lt;/code&gt; sent&lt;/th&gt;
&lt;th&gt;Side effects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Register only &lt;code&gt;propagation.TraceContext{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Global: every other client in the process stops sending baggage too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;option.WithOpenTelemetryPropagation(false)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;API-side spans can't join your trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDK middleware that deletes the header&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Only this client is affected; no help if the client uses &lt;code&gt;otelhttp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The SDK injects trace headers before it runs your middleware chain, so a middleware can remove them. For a service whose other outbound calls need baggage, this is the one we'd use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&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;next&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MiddlewareNext&lt;/span&gt;&lt;span class="p"&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;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"baggage"&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;next&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="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing else in the process reads baggage, registering only &lt;code&gt;TraceContext{}&lt;/code&gt; is simpler, and it's the setup the SDK guide recommends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the span records
&lt;/h2&gt;

&lt;p&gt;In the default &lt;code&gt;metadata_only&lt;/code&gt; mode, a non-streaming call produced one &lt;code&gt;CLIENT&lt;/code&gt; span named &lt;code&gt;anthropic.messages.create&lt;/code&gt; under the handler span. Its attributes, with &lt;code&gt;server.port&lt;/code&gt;, &lt;code&gt;url.path&lt;/code&gt; and &lt;code&gt;url.scheme&lt;/code&gt; left out and the port removed from &lt;code&gt;url.full&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;anthropic.message.stop_reason=end_turn
anthropic.request.id=req_test
gen_ai.operation.name=chat
gen_ai.provider.name=anthropic
gen_ai.request.max_tokens=256
gen_ai.request.model=claude-sonnet-5-5
gen_ai.response.id=msg_01
gen_ai.response.model=claude-sonnet-5-5
gen_ai.usage.input_tokens=12
gen_ai.usage.output_tokens=3
http.request.method=POST
http.response.status_code=200
http.route=/v1/messages
server.address=127.0.0.1
url.full=http://127.0.0.1/v1/messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No prompt text is recorded in this mode. Setting &lt;code&gt;ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content&lt;/code&gt; added two attributes with the prompt and the reply; our test prompt included the email address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gen_ai.input.messages=[{"role":"user","parts":[{"type":"text","content":"Summarise ticket 44...
gen_ai.output.messages=[{"role":"assistant","parts":[{"type":"text","content":"Hi"}]}]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The v1.81.0 source (&lt;code&gt;internal/tracing/tracing.go&lt;/code&gt;) reads that variable on every call rather than once when the client is built, and an option passed to &lt;code&gt;anthropic.NewClient&lt;/code&gt; overrides it. If prompts must never reach your tracing backend, pass &lt;code&gt;option.WithOpenTelemetryTracesContentMode(option.OpenTelemetryContentModeMetadataOnly)&lt;/code&gt; explicitly so an env var in a deployment manifest can't change it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries, otelhttp and streams
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Retries.&lt;/strong&gt; With the stand-in answering the first request with a 529 &lt;code&gt;overloaded_error&lt;/code&gt;, the SDK retried using the same &lt;code&gt;traceparent&lt;/code&gt; for both attempts and recorded one span with &lt;code&gt;http.request.resend_count=1&lt;/code&gt; and status &lt;code&gt;Ok&lt;/code&gt;. The &lt;a href="https://platform.claude.com/docs/en/api/opentelemetry/spans" rel="noopener noreferrer"&gt;span reference&lt;/a&gt; documents this: one span covers the whole call, retries included, so failed attempts show up in that attribute and not in the span count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;otelhttp.&lt;/strong&gt; With the SDK's HTTP client wrapped in &lt;code&gt;otelhttp.NewTransport&lt;/code&gt;, we got the &lt;code&gt;anthropic.messages.create&lt;/code&gt; span with an &lt;code&gt;HTTP POST&lt;/code&gt; span under it, no duplicate &lt;code&gt;anthropic.*&lt;/code&gt; span, and a single &lt;code&gt;traceparent&lt;/code&gt; value at the server, carrying the &lt;code&gt;otelhttp&lt;/code&gt; span's ID. The SDK guide suggests &lt;code&gt;WithOpenTelemetryPropagation(false)&lt;/code&gt; in this setup. In our Go run the headers were identical with and without it, because &lt;code&gt;otelhttp&lt;/code&gt; writes them last.&lt;/p&gt;

&lt;p&gt;For the same reason the baggage middleware above has no effect here: &lt;code&gt;otelhttp&lt;/code&gt; adds the header again after it runs. This setup already sent baggage before v1.81.0, and registering only &lt;code&gt;TraceContext{}&lt;/code&gt; is the way to stop it. The &lt;code&gt;HTTP POST&lt;/code&gt; span repeats the request timing; drop &lt;code&gt;otelhttp&lt;/code&gt; from this client if you don't want it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streams.&lt;/strong&gt; &lt;code&gt;Messages.NewStreaming&lt;/code&gt; returns a stream whose span ends only when you've read it to the end or closed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read all events:         span ended, status Ok, gen_ai.response.time_to_first_chunk=0.0023
read 1 event, Close():   span ended, status Unset, no output token count
read 1 event, no Close:  no span exported
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your loop can stop before the last event, &lt;code&gt;defer stream.Close()&lt;/code&gt;, or that call's span is never exported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting tokens from these spans
&lt;/h2&gt;

&lt;p&gt;With an API-side destination configured as well, each request produces two spans named &lt;code&gt;anthropic.messages.create&lt;/code&gt;, yours (&lt;code&gt;CLIENT&lt;/code&gt;) and the API's (&lt;code&gt;SERVER&lt;/code&gt;), and both carry &lt;code&gt;gen_ai.usage.*&lt;/code&gt;. The &lt;a href="https://platform.claude.com/docs/en/manage-claude/opentelemetry" rel="noopener noreferrer"&gt;OpenTelemetry overview&lt;/a&gt; says to sum only one source per request, preferring the API spans when both exist; summing both doubles your token counts. The API also honours the sampled flag in &lt;code&gt;traceparent&lt;/code&gt;, so per the &lt;a href="https://platform.claude.com/docs/en/manage-claude/opentelemetry/api" rel="noopener noreferrer"&gt;API guide&lt;/a&gt; a trace your sampler drops gets no API spans either.&lt;/p&gt;

&lt;p&gt;Span delivery is best effort, and the guide points to the Usage API for spend tracking. Our &lt;a href="https://importstatic.com/ai/llm-api-spend-limit-errors" rel="noopener noreferrer"&gt;article on LLM API spend limits&lt;/a&gt; covers what the vendors enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it off
&lt;/h2&gt;

&lt;p&gt;Pass &lt;code&gt;option.WithoutOpenTelemetry()&lt;/code&gt; to disable it for one client, or set &lt;code&gt;ANTHROPIC_OPEN_TELEMETRY=false&lt;/code&gt; for the whole process. With either, the server got no trace headers and only the handler span was recorded. Either is reasonable if another library already traces your Claude calls, or until you've checked what's in your baggage.&lt;/p&gt;

&lt;p&gt;If you're upgrading across several SDK versions at once, the Go type changes from earlier releases still apply; we covered the ones that bit Haiku 5.5 migrations in &lt;a href="https://importstatic.com/ai/claude-haiku-5-5-migration-go" rel="noopener noreferrer"&gt;Content[0].Text coming back empty&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/ai/anthropic-sdk-go-opentelemetry-baggage" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;. This article was drafted with AI assistance; every claim was checked against the sources linked in it and the code samples were compiled and run.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Every OpenAI model shutdown date until January 2027, and what replaces it</title>
      <dc:creator>ImportStatic</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:35:59 +0000</pubDate>
      <link>https://dev.to/importstatic/every-openai-model-shutdown-date-until-january-2027-and-what-replaces-it-1dfk</link>
      <guid>https://dev.to/importstatic/every-openai-model-shutdown-date-until-january-2027-and-what-replaces-it-1dfk</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/ai/openai-model-shutdown-dates" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your code calls &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;, &lt;code&gt;gpt-4&lt;/code&gt;, &lt;code&gt;gpt-4-turbo&lt;/code&gt;, &lt;code&gt;gpt-4.1-nano&lt;/code&gt;, &lt;code&gt;o1&lt;/code&gt;, &lt;code&gt;o1-pro&lt;/code&gt;, &lt;code&gt;o3-mini&lt;/code&gt;, &lt;code&gt;o4-mini&lt;/code&gt; or &lt;code&gt;gpt-image-1&lt;/code&gt;, it has until October 23, 2026, eight days from today. On October 14 OpenAI also scheduled the GPT-4o, GPT-4o mini, GPT-4.1 and GPT-4.1 mini snapshots for shutdown on January 14, 2027, with &lt;code&gt;gpt-6-luna&lt;/code&gt; as the replacement for all of them.&lt;/p&gt;

&lt;p&gt;Everything below comes from OpenAI's &lt;a href="https://developers.openai.com/api/docs/deprecations" rel="noopener noreferrer"&gt;deprecations page&lt;/a&gt; as of October 15, 2026. At shutdown, it says, "the model or endpoint will no longer be accessible". The page doesn't name the error you'll get, so plan for requests failing outright rather than being routed to another model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shutdown dates and replacements
&lt;/h2&gt;

&lt;p&gt;Chat, reasoning and image models, ordered by shutdown date:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shuts down&lt;/th&gt;
&lt;th&gt;Model IDs&lt;/th&gt;
&lt;th&gt;Replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-23&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-3.5-turbo&lt;/code&gt;, &lt;code&gt;gpt-3.5-turbo-0125&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-terra&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-23&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-4&lt;/code&gt;, &lt;code&gt;gpt-4-0613&lt;/code&gt;, &lt;code&gt;gpt-4-1106-preview&lt;/code&gt;, &lt;code&gt;gpt-4-turbo&lt;/code&gt;, &lt;code&gt;gpt-4-turbo-2024-04-09&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-23&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-4o-2024-05-13&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-23&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;o1&lt;/code&gt;, &lt;code&gt;o1-2024-12-17&lt;/code&gt;, &lt;code&gt;o3-mini&lt;/code&gt;, &lt;code&gt;o3-mini-2025-01-31&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-23&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;o1-pro&lt;/code&gt;, &lt;code&gt;o1-pro-2025-03-19&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-5.6-sol&lt;/code&gt; with &lt;code&gt;reasoning.mode: pro&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-23&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;o4-mini&lt;/code&gt;, &lt;code&gt;o4-mini-2025-04-16&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-terra&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-23&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-4.1-nano&lt;/code&gt;, &lt;code&gt;gpt-4.1-nano-2025-04-14&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-luna&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-23&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-image-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-image-2.5-sunburst&lt;/code&gt; or &lt;code&gt;gpt-image-2.5-flare&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-12-01&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-image-1-mini&lt;/code&gt;, &lt;code&gt;gpt-image-1.5&lt;/code&gt;, &lt;code&gt;chatgpt-image-latest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-image-2.5-sunburst&lt;/code&gt; or &lt;code&gt;gpt-image-2.5-flare&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-12-11&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-5-2025-08-07&lt;/code&gt;, &lt;code&gt;o3-2025-04-16&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-12-11&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-5-pro-2025-10-06&lt;/code&gt;, &lt;code&gt;o3-pro-2025-06-10&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-5.6-sol&lt;/code&gt; with &lt;code&gt;reasoning.mode: pro&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-12-11&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5-mini-2025-08-07&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-terra&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-12-11&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5-nano-2025-08-07&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-5.6-luna&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2027-01-14&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-4o-2024-08-06&lt;/code&gt;, &lt;code&gt;gpt-4o-2024-11-20&lt;/code&gt;, &lt;code&gt;gpt-4o-mini-2024-07-18&lt;/code&gt;, &lt;code&gt;gpt-4.1-2025-04-14&lt;/code&gt;, &lt;code&gt;gpt-4.1-mini-2025-04-14&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-6-luna&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The October 23 entry also lists completions-endpoint variants (&lt;code&gt;gpt-3.5-turbo-completions&lt;/code&gt;, &lt;code&gt;gpt-4-completions&lt;/code&gt; and others) and the fine-tuned snapshots &lt;code&gt;ft-gpt-3.5-turbo&lt;/code&gt;, &lt;code&gt;ft-gpt-4&lt;/code&gt;, &lt;code&gt;ft-gpt-4.1-nano-2025-04-14&lt;/code&gt;, &lt;code&gt;ft-o4-mini-2025-04-16&lt;/code&gt;, &lt;code&gt;ft-babbage-002&lt;/code&gt; and &lt;code&gt;ft-davinci-002&lt;/code&gt;. The audio and realtime models (&lt;code&gt;gpt-realtime&lt;/code&gt;, &lt;code&gt;gpt-audio&lt;/code&gt;, &lt;code&gt;gpt-4o-realtime&lt;/code&gt;, &lt;code&gt;gpt-4o-mini-audio&lt;/code&gt; and five more) follow on January 20, 2027, and &lt;code&gt;tts-1&lt;/code&gt; and &lt;code&gt;tts-1-hd&lt;/code&gt; on January 6, 2027.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aliases without a date
&lt;/h3&gt;

&lt;p&gt;For October 23 the page lists both the alias and the snapshot (&lt;code&gt;o4-mini&lt;/code&gt; and &lt;code&gt;o4-mini-2025-04-16&lt;/code&gt;). The December 11 and January 14 entries list only dated snapshots, and nothing on the page says what &lt;code&gt;gpt-4o-mini&lt;/code&gt;, &lt;code&gt;gpt-4o&lt;/code&gt;, &lt;code&gt;gpt-4.1&lt;/code&gt; or &lt;code&gt;gpt-5&lt;/code&gt; will resolve to once their snapshots are gone. openai-go v3.78.0 has made its own call for December and marks &lt;code&gt;gpt-5&lt;/code&gt;, &lt;code&gt;gpt-5-mini&lt;/code&gt;, &lt;code&gt;gpt-5-nano&lt;/code&gt; and &lt;code&gt;o3&lt;/code&gt; as deprecated with a December 11 shutdown. We'd treat every alias as going away with its snapshot and move off it before then.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine-tuned GPT-4o mini and GPT-4.1 models
&lt;/h2&gt;

&lt;p&gt;The January entry says "Fine-tuned versions of these models follow the same shutdown schedule as their underlying base models", so an &lt;code&gt;ft:gpt-4o-mini-2024-07-18:...&lt;/code&gt; model stops answering on January 14.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.openai.com/api/docs/models/gpt-6-luna" rel="noopener noreferrer"&gt;gpt-6-luna model page&lt;/a&gt; lists fine-tuning as unsupported, and the &lt;a href="https://developers.openai.com/api/docs/guides/fine-tuning" rel="noopener noreferrer"&gt;fine-tuning guide&lt;/a&gt; says OpenAI "is winding down the fine-tuning platform". According to the deprecations page, existing customers can't create new fine-tuning jobs after January 6, 2027. The only base models the guide still lists for supervised fine-tuning are the three GPT-4.1 snapshots, all of which are in the table above. A product that depends on a fine-tune has three months to replace it with a prompt (with examples) on &lt;code&gt;gpt-6-luna&lt;/code&gt;, checked against the fine-tune's outputs on its own evaluation set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the model IDs in your code
&lt;/h2&gt;

&lt;p&gt;openai-go marks deprecated models with Go's standard &lt;code&gt;// Deprecated:&lt;/code&gt; comment, which staticcheck's SA1019 check reports. Our test service names models in the ways real code does (an SDK constant, a string constant, a map of fallbacks, an environment default and a YAML config), and staticcheck 2026.2.2 reports one of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ go run honnef.co/go/tools/cmd/staticcheck@latest ./app/
app/main.go:18:15: github.com/openai/openai-go/v3.ChatModelO4Mini is deprecated: Announced shutdown date: 2026-10-23. See https://developers.openai.com/api/docs/deprecations for details and recommended replacements.  (SA1019)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest go unreported for two reasons. &lt;code&gt;ChatModel&lt;/code&gt; is declared as &lt;code&gt;type ChatModel = string&lt;/code&gt;, so a model written as a string literal is just a string, with nothing for SA1019 to attach to. And &lt;a href="https://github.com/openai/openai-go/releases" rel="noopener noreferrer"&gt;openai-go v3.78.0&lt;/a&gt; came out on October 13, a day before the January deprecation was announced, so &lt;code&gt;ChatModelGPT4oMini&lt;/code&gt; and &lt;code&gt;ChatModelGPT4_1Mini&lt;/code&gt; have no deprecation marker yet. A later release will probably add one, but that won't help with string literals.&lt;/p&gt;

&lt;p&gt;To catch those, we wrote a scanner that walks a tree, matches whole model IDs in &lt;code&gt;.go&lt;/code&gt;, &lt;code&gt;.py&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.yaml&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt;, &lt;code&gt;.toml&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; files, and resolves &lt;code&gt;openai.ChatModel...&lt;/code&gt; constants by parsing &lt;code&gt;shared/shared.go&lt;/code&gt; from the SDK's module directory. Its core is the shutdown table and a pattern that won't match &lt;code&gt;gpt-4&lt;/code&gt; inside &lt;code&gt;gpt-4o&lt;/code&gt; or &lt;code&gt;gpt-4.1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-10-23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-5.6-terra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-3.5-turbo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-3.5-turbo-0125"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o4-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o4-mini-2025-04-16"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-10-23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-5.6-sol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4-0613"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4-1106-preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4-turbo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"gpt-4-turbo-2024-04-09"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o-2024-05-13"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o1-2024-12-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o3-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o3-mini-2025-01-31"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-10-23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-5.6-luna"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4.1-nano"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4.1-nano-2025-04-14"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ... the rest of the table above ...&lt;/span&gt;
    &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2027-01-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-6-luna"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o-2024-08-06"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o-2024-11-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o-mini-2024-07-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"gpt-4.1-2025-04-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4.1-mini-2025-04-14"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2027-01-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-6-luna"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// idPattern matches a whole model ID: the characters around it must not be&lt;/span&gt;
&lt;span class="c"&gt;// part of an ID, so "gpt-4" doesn't match inside "gpt-4o" or "gpt-4.1".&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;idPattern&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Regexp&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="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&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;id&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&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;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&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;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QuoteMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortFunc&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="k"&gt;func&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&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;len&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="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;len&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="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`(?:^|[^\w.-])(`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&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="s"&gt;"|"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;`)(?:$|[^\w.-])`&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;On the same service it finds nine references and exits with status 1, so it can gate CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ go run ./scan -sdk $(go list -m -f '{{.Dir}}' github.com/openai/openai-go/v3) ./app
app/config.yaml:2: literal gpt-4o-mini shuts down 2027-01-14 -&amp;gt; gpt-6-luna (alias; the page lists the snapshot)
app/config.yaml:3: literal gpt-4.1-2025-04-14 shuts down 2027-01-14 -&amp;gt; gpt-6-luna
app/config.yaml:4: literal gpt-image-1 shuts down 2026-10-23 -&amp;gt; gpt-image-2.5-sunburst or -flare
app/main.go:13: literal gpt-4o-mini-2024-07-18 shuts down 2027-01-14 -&amp;gt; gpt-6-luna
app/main.go:16: literal gpt-4.1-nano shuts down 2026-10-23 -&amp;gt; gpt-5.6-luna
app/main.go:17: constant ChatModelGPT4_1Mini = gpt-4.1-mini shuts down 2027-01-14 -&amp;gt; gpt-6-luna (alias; the page lists the snapshot)
app/main.go:18: constant ChatModelO4Mini = o4-mini shuts down 2026-10-23 -&amp;gt; gpt-5.6-terra
app/main.go:25: literal gpt-4o shuts down 2027-01-14 -&amp;gt; gpt-6-luna (alias; the page lists the snapshot)
app/main.go:30: constant ChatModelGPT4oMini = gpt-4o-mini shuts down 2027-01-14 -&amp;gt; gpt-6-luna (alias; the page lists the snapshot)
9 references
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;text-embedding-3-small&lt;/code&gt; on the next line of the YAML isn't on any list and isn't reported. A bare &lt;code&gt;o1&lt;/code&gt; in an unrelated YAML key would be a false positive. The scanner also can't see a model name that only exists in a deployed environment variable or a database row, so those need checking separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving a tool-calling gpt-4o-mini call to gpt-6-luna
&lt;/h2&gt;

&lt;p&gt;A Chat Completions call that uses tools needs more than a new model string. The &lt;a href="https://developers.openai.com/api/docs/guides/latest-model" rel="noopener noreferrer"&gt;latest-model guide&lt;/a&gt; says "GPT-6 Sol and GPT-6 Luna support function calling in Chat Completions only with &lt;code&gt;reasoning_effort: "none"&lt;/code&gt;", and Luna's default effort is &lt;code&gt;medium&lt;/code&gt;. The same guide says to remove &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p&lt;/code&gt; and &lt;code&gt;top_logprobs&lt;/code&gt; (and &lt;code&gt;logprobs&lt;/code&gt; on Chat Completions) whenever effort isn't &lt;code&gt;none&lt;/code&gt;. It doesn't say whether they're accepted at &lt;code&gt;none&lt;/code&gt;, so we dropped &lt;code&gt;temperature&lt;/code&gt; as well.&lt;/p&gt;

&lt;p&gt;We built the old and new requests with openai-go and sent them to a local &lt;code&gt;httptest&lt;/code&gt; stub that records the body; nothing went to OpenAI, since there's no API key on this machine. The new params look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatCompletionNewParams&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;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatModelGPT6Luna&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="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatCompletionMessageParamUnion&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Weather in Oslo?"&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="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatCompletionToolUnionParam&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;weatherTool&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c"&gt;// Function calling on Chat Completions needs "none" for gpt-6-luna.&lt;/span&gt;
    &lt;span class="n"&gt;ReasoningEffort&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasoningEffortNone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MaxCompletionTokens&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&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 stub recorded these bodies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before (messages and tools omitted):
{
  "max_tokens": 500,
  "model": "gpt-4o-mini",
  "temperature": 0.2
}
after (messages and tools omitted):
{
  "max_completion_tokens": 500,
  "model": "gpt-6-luna",
  "reasoning_effort": "none"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK's own field comment calls &lt;code&gt;max_tokens&lt;/code&gt; deprecated in favour of &lt;code&gt;max_completion_tokens&lt;/code&gt;, which also counts reasoning tokens. That comment isn't a &lt;code&gt;// Deprecated:&lt;/code&gt; marker, so staticcheck didn't flag &lt;code&gt;MaxTokens&lt;/code&gt; either. To have Luna reason and call tools in the same request, the guide says to move the call to the Responses API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price and context window
&lt;/h2&gt;

&lt;p&gt;Per million tokens, &lt;code&gt;gpt-6-luna&lt;/code&gt; lists $0.10 input and $0.50 output, against $0.15 and $0.60 for &lt;a href="https://developers.openai.com/api/docs/models/gpt-4o-mini" rel="noopener noreferrer"&gt;GPT-4o mini&lt;/a&gt;, and its context window is 1,050,000 tokens against 128,000, so cost is no reason to delay the move. For more on Luna's typed-answer endpoint, see our piece on the &lt;a href="https://importstatic.com/ai/openai-decisions-api-go" rel="noopener noreferrer"&gt;OpenAI Decisions API from Go&lt;/a&gt;. If you also call Claude, our &lt;a href="https://importstatic.com/ai/claude-sonnet-4-5-deprecation-migration" rel="noopener noreferrer"&gt;Claude Sonnet 4.5 deprecation&lt;/a&gt; piece runs the same staticcheck test on anthropic-sdk-go, where Sonnet 4.5 retires on November 30.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/ai/openai-model-shutdown-dates" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;. This article was drafted with AI assistance; every claim was checked against the sources linked in it and the code samples were compiled and run.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How much MCP token overhead you pay depends on the harness</title>
      <dc:creator>ImportStatic</dc:creator>
      <pubDate>Sat, 17 Oct 2026 06:21:35 +0000</pubDate>
      <link>https://dev.to/importstatic/how-much-mcp-token-overhead-you-pay-depends-on-the-harness-1ko3</link>
      <guid>https://dev.to/importstatic/how-much-mcp-token-overhead-you-pay-depends-on-the-harness-1ko3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/ai/mcp-tool-definition-token-overhead" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The issue-writing tool in GitHub's MCP server costs 807 tokens to describe to a model. The help page for &lt;code&gt;gh issue create&lt;/code&gt; costs 869. So much for the idea that an MCP tool is inherently heavier than a CLI command.&lt;/p&gt;

&lt;p&gt;That comparison comes from a small Go program we wrote after reading &lt;a href="https://news.ycombinator.com/item?id=49906637" rel="noopener noreferrer"&gt;a Hacker News discussion&lt;/a&gt; of a post about an agent harness adding MCP support. One side said MCP eats context on every turn whether you use the tools or not. The other said it depends on the harness. That's an argument a measurement can settle. Both sides turn out to be right, and the MCP token overhead is easy to measure: GitHub's server with its default settings is 11,207 tokens of tool definitions, and whether those tokens reach the model is now a harness setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we counted, and with which tokenizer
&lt;/h2&gt;

&lt;p&gt;The program starts a server over stdio with the &lt;a href="https://importstatic.com/ai/build-mcp-server-go" rel="noopener noreferrer"&gt;official Go SDK&lt;/a&gt;, lists its tools, and serializes each one the way a model API wants it: name, description and input schema. Then it counts tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetBpeLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tiktoken_loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOfflineLoader&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetEncoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o200k_base"&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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 go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;session&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;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;functionTool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InputSchema&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputSchema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;defs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;countJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;countJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputSchema&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 caveats before the numbers. The tokenizer is &lt;code&gt;o200k_base&lt;/code&gt;, which OpenAI's &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken/model.py" rel="noopener noreferrer"&gt;tiktoken table&lt;/a&gt; maps to the gpt-4o, gpt-4.1, gpt-5 and o-series names. Claude models have their own tokenizer, and the way to count with it is Anthropic's &lt;a href="https://platform.claude.com/docs/en/build-with-claude/token-counting" rel="noopener noreferrer"&gt;token counting endpoint&lt;/a&gt;. It's free and accepts a &lt;code&gt;tools&lt;/code&gt; array, but it needs an API key and this machine doesn't have one. So these are OpenAI-encoding counts of compact JSON. Second, a provider is free to render tool definitions into the prompt in its own format, so what the model sees can differ from the JSON you send. Treat the figures as close, not exact.&lt;/p&gt;

&lt;p&gt;All four servers were built from source at their latest tagged versions as of October 6, 2026, and started with dummy credentials. &lt;code&gt;tools/list&lt;/code&gt; doesn't need real ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP token overhead of four real servers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Definitions&lt;/th&gt;
&lt;th&gt;Per tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gopls v0.23.0 (&lt;code&gt;gopls mcp&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;878 tokens&lt;/td&gt;
&lt;td&gt;109&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terraform-mcp-server v1.3.0&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;1,718 tokens&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;github-mcp-server v1.14.0, defaults&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;11,207 tokens&lt;/td&gt;
&lt;td&gt;243&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp-grafana v1.6.3&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;20,493 tokens&lt;/td&gt;
&lt;td&gt;253&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;github-mcp-server v1.14.0, &lt;code&gt;--toolsets all&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;23,206 tokens&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small servers are cheap. gopls gives a model eight tools for less than a page of text. The trouble starts when a server mirrors a whole product API: GitHub with every toolset plus Grafana is 43,699 tokens before the user has typed anything.&lt;/p&gt;

&lt;p&gt;That lines up with what Anthropic says. Its &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool" rel="noopener noreferrer"&gt;tool search documentation&lt;/a&gt; gives the example of a five-server setup (GitHub, Slack, Sentry, Grafana and Splunk) that "can consume ~55k tokens in definitions before Claude does any work." Different tokenizer, different server versions, same order of magnitude.&lt;/p&gt;

&lt;p&gt;And yes, without deferral you pay it on every request. Model APIs are stateless. Anthropic's pricing notes say tool use is billed on input tokens "including in the &lt;code&gt;tools&lt;/code&gt; parameter", and that the parameter's tokens are the "tool names, descriptions, and schemas". On top of that sits a fixed tool-use system prompt, which Anthropic lists as 286 tokens for Claude Opus 5.5 and Sonnet 5.5. Next to 11,207, that part is noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most of it is JSON Schema, not prose
&lt;/h2&gt;

&lt;p&gt;Here's the full output for the GitHub server with default settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server            github-mcp-server v1.14.0
tools             46
definitions       11207 tokens (243 per tool)
  descriptions    1368 tokens
  input schemas   9287 tokens
output schemas    0 tokens on 0 tools (not included above)
annotations       964 tokens (not included above)
instructions      377 tokens
names only        185 tokens
names + 1st line  1190 tokens
largest tools:
  issue_write                                807 tokens (description 14, schema 783)
  pull_request_read                          560 tokens (description 12, schema 537)
  list_issues                                549 tokens (description 33, schema 505)
smallest tool:
  get_me                                      54 tokens (description 35, schema 9)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool descriptions, the part people imagine when they say "tool descriptions waste tokens", are 12% of the total. The input schemas are 83%.&lt;/p&gt;

&lt;p&gt;Look at &lt;code&gt;issue_write&lt;/code&gt;. Its description is one sentence, 14 tokens. Its schema is 783, because it has seventeen properties and several of them carry a paragraph of usage rules. This is one property out of the seventeen, copied from the server's response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"parent_owner"&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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Repository owner of the parent issue. Must be provided with parent_repo. Omit both to use owner and repo. Only used when method is 'create' and parent_issue_number is provided."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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;Grafana's server leans the other way, with 5,777 tokens of descriptions against 13,601 of schema, but schemas still dominate.&lt;/p&gt;

&lt;p&gt;If you write a server, that's where to look. Merging "create" and "update" into one tool with a &lt;code&gt;method&lt;/code&gt; argument, as GitHub does here, saves a tool slot and then spends the saving on parameters that only apply to one of the two methods. I wouldn't call that wrong. It's a trade, and it's worth knowing which side of it your tokens are on.&lt;/p&gt;

&lt;p&gt;One more line in that output deserves a look: &lt;code&gt;instructions&lt;/code&gt;. Servers can return free-text instructions when a client connects. GitHub's are 377 tokens with default toolsets and 1,559 with all of them. Terraform's are 924, more than half the size of its nine tool definitions. If you audit a server, count those too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CLI isn't smaller, it's just lazy
&lt;/h2&gt;

&lt;p&gt;The usual counter-proposal is to skip MCP and let the agent run &lt;code&gt;gh&lt;/code&gt;. We built gh v2.102.0 from source and counted its help output with the same tokenizer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the agent reads&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gh --help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;610&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gh issue --help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;346&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gh issue create --help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;869&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gh issue edit --help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,068&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP &lt;code&gt;issue_write&lt;/code&gt; (create and update)&lt;/td&gt;
&lt;td&gt;807&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An agent that starts cold and walks down to &lt;code&gt;gh issue create&lt;/code&gt; reads 1,825 tokens of help. The two help pages that cover what &lt;code&gt;issue_write&lt;/code&gt; does add up to 1,937. Operation for operation, the MCP definition is the more compact of the two.&lt;/p&gt;

&lt;p&gt;What the CLI has going for it is timing. Nothing is loaded until the agent asks, and an agent that never files an issue never reads the page. A model may also know &lt;code&gt;gh&lt;/code&gt; well enough from training to skip the help entirely; we can't measure that here, and it won't be true for your internal tools.&lt;/p&gt;

&lt;p&gt;So the real difference was never the format. It's eager versus lazy loading. And that is exactly the part harnesses have changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  With tool search, the bill drops to a list of names
&lt;/h2&gt;

&lt;p&gt;Anthropic's API has a &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool" rel="noopener noreferrer"&gt;tool search tool&lt;/a&gt;. You mark tools with &lt;code&gt;defer_loading: true&lt;/code&gt;, and the docs are specific about what that does: it "controls what enters the context window, not what you send in the request." The definitions still travel in the &lt;code&gt;tools&lt;/code&gt; array, but the model only sees the ones a search returns, up to five per search by default. Anthropic reports this "typically reduces" definition tokens "by over 85 percent". For MCP servers attached through its connector, you set the flag once for the whole server.&lt;/p&gt;

&lt;p&gt;OpenAI's Responses API has &lt;a href="https://developers.openai.com/api/docs/guides/tools-tool-search" rel="noopener noreferrer"&gt;the same idea&lt;/a&gt;: a &lt;code&gt;tool_search&lt;/code&gt; tool, supported on &lt;code&gt;gpt-5.4&lt;/code&gt; and later, plus &lt;code&gt;defer_loading&lt;/code&gt; on functions or MCP servers. For a deferred server the model initially sees, in the guide's words, "only the namespace or server name and description". OpenAI recommends keeping each namespace under ten functions.&lt;/p&gt;

&lt;p&gt;Claude Code &lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;turns tool search on by default&lt;/a&gt;. Its docs list the setups that go without: a custom &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;, &lt;code&gt;ENABLE_TOOL_SEARCH=false&lt;/code&gt;, and older models on Google Cloud. With it on, the docs describe Claude Code listing a server's tool names to the model, which then goes through a &lt;code&gt;ToolSearch&lt;/code&gt; call to use them.&lt;/p&gt;

&lt;p&gt;How small is a list of names? For GitHub's 46 default tools our program counted 185 tokens, against 11,207 for the definitions. Each harness formats its own list, so that's a floor and not a prediction. Then add what a search pulls in: five average GitHub tools is about 1,200 tokens, and only on the turns that need them.&lt;/p&gt;

&lt;p&gt;That's the answer to the thread. In a harness that defers, MCP's standing cost is a few hundred tokens. In one that doesn't, it's the whole table above on every call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do if your harness loads everything
&lt;/h2&gt;

&lt;p&gt;Find out first. If you call a model API yourself and pass the tools from &lt;code&gt;tools/list&lt;/code&gt; straight through, you're loading everything. (The &lt;a href="https://importstatic.com/ai/mcp-stateless-spec-migration" rel="noopener noreferrer"&gt;stateless revision of the spec&lt;/a&gt; changes how a client fetches that list, not how big it is.)&lt;/p&gt;

&lt;p&gt;Then trim at the server. GitHub's README says it plainly: "Enabling only the toolsets that you need can help the LLM with tool choice and reduce the context size." We measured what the flags buy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;github-mcp-server v1.14.0 flags&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Definitions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--toolsets all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;23,206 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(defaults)&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;11,207 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--read-only&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;6,492 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--toolsets issues&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;3,281 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--tools issue_read,issue_write,list_issues&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1,693 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An agent that triages issues doesn't need 91 tools, and a server started with &lt;code&gt;--tools&lt;/code&gt; costs it 7% of the full set.&lt;/p&gt;

&lt;p&gt;If you can use tool search, use it, and keep the handful of tools you call constantly non-deferred. Anthropic suggests three to five. Its own threshold for bothering is ten or more tools, or more than 10k tokens of definitions, which the GitHub server crosses on defaults.&lt;/p&gt;

&lt;p&gt;And measure your own setup instead of trusting anyone's table, including ours. With an Anthropic key, send your real &lt;code&gt;tools&lt;/code&gt; array to &lt;code&gt;count_tokens&lt;/code&gt; for the model you run; the docs note that Claude Opus 4.7 and later produce roughly 30 percent more tokens for the same text than earlier Claude models did, so an old count is stale. Without a key, a short Go program and a published tokenizer will tell you which three tools are eating your context.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/ai/mcp-tool-definition-token-overhead" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;. This article was drafted with AI assistance; every claim was checked against the sources linked in it and the code samples were compiled and run.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Go 1.27's JSON v2 is already under your encoding/json</title>
      <dc:creator>ImportStatic</dc:creator>
      <pubDate>Mon, 12 Oct 2026 02:12:26 +0000</pubDate>
      <link>https://dev.to/importstatic/go-127s-json-v2-is-already-under-your-encodingjson-281p</link>
      <guid>https://dev.to/importstatic/go-127s-json-v2-is-already-under-your-encodingjson-281p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/go/go-json-v2-migration" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Go 1.27 put &lt;code&gt;encoding/json/v2&lt;/code&gt; in the standard library, and the part most people will miss is that you are already running it. Since Go 1.27, the old &lt;code&gt;encoding/json&lt;/code&gt; package is implemented on top of the v2 engine with a set of compatibility options switched on. Your output should not change, but your error strings can, and the moment you change an import to &lt;code&gt;encoding/json/v2&lt;/code&gt; the defaults flip: nil slices become &lt;code&gt;[]&lt;/code&gt;, field names match case-sensitively, duplicate keys are rejected and &lt;code&gt;time.Duration&lt;/code&gt; stops marshaling at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Go 1.27 changed even if you never import v2
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://go.dev/doc/go1.27" rel="noopener noreferrer"&gt;Go 1.27 release notes&lt;/a&gt; describe two new packages and one swapped engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;encoding/json/v2&lt;/code&gt; is the new semantic API: &lt;code&gt;Marshal&lt;/code&gt;, &lt;code&gt;Unmarshal&lt;/code&gt;, &lt;code&gt;MarshalWrite&lt;/code&gt;, &lt;code&gt;UnmarshalRead&lt;/code&gt;, &lt;code&gt;MarshalEncode&lt;/code&gt; and &lt;code&gt;UnmarshalDecode&lt;/code&gt;, each taking variadic &lt;code&gt;Options&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;encoding/json/jsontext&lt;/code&gt; is the lower-level syntactic layer, with an &lt;code&gt;Encoder&lt;/code&gt; and &lt;code&gt;Decoder&lt;/code&gt; that work on tokens and raw values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The notes then say that &lt;code&gt;encoding/json&lt;/code&gt; "is now backed by the v2 implementation. Marshaling and unmarshaling behavior is preserved, but the exact text of error messages may differ." The v1 API stays supported and nobody is required to migrate. If something does break after the upgrade, building with &lt;code&gt;GOEXPERIMENT=nojsonv2&lt;/code&gt; restores the original code, but the release notes say that opt-out "is expected to be removed in a future release."&lt;/p&gt;

&lt;p&gt;So the first migration step costs nothing: upgrade to 1.27 and run your tests. The one category to look for is tests that compare &lt;code&gt;err.Error()&lt;/code&gt; against a fixed string. Those can fail without any behaviour change, and the fix is to assert on error types such as &lt;code&gt;*json.SyntaxError&lt;/code&gt; or &lt;code&gt;*json.UnmarshalTypeError&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go JSON v2 vs v1, side by side
&lt;/h2&gt;

&lt;p&gt;The package documentation for &lt;code&gt;encoding/json&lt;/code&gt; has a "Migrating to v2" section listing every behaviour difference and the option that controls it. Reading a list is one thing; seeing your own structs change is another. Here is the struct we used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"name"`&lt;/span&gt;
    &lt;span class="n"&gt;Tags&lt;/span&gt;    &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"tags"`&lt;/span&gt;
    &lt;span class="n"&gt;Admin&lt;/span&gt;   &lt;span class="kt"&gt;bool&lt;/span&gt;     &lt;span class="s"&gt;`json:"admin,omitempty"`&lt;/span&gt;
    &lt;span class="n"&gt;Retries&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;      &lt;span class="s"&gt;`json:"retries,omitempty"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The program marshals and unmarshals the same values with &lt;code&gt;jsonv1 "encoding/json"&lt;/code&gt; and &lt;code&gt;"encoding/json/v2"&lt;/code&gt; and prints both. This is the real output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;== nil slice and omitempty on bool/int ==
v1   {"name":"ana","tags":null}
v2   {"name":"ana","tags":[],"admin":false,"retries":0}
== case-insensitive field names ==
&lt;/span&gt;&lt;span class="gp"&gt;v1   "bob" err=&amp;lt;nil&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;v2   "" err=&amp;lt;nil&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;== duplicate names ==
&lt;/span&gt;&lt;span class="gp"&gt;v1   "mallory" err=&amp;lt;nil&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;v2   "alice" err=jsontext: duplicate object member name "name"
== invalid UTF-8 ==
&lt;/span&gt;&lt;span class="gp"&gt;v1   "caf�" err=&amp;lt;nil&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;v2   "" err=jsontext: invalid UTF-8 within "/name" after offset 12
== time.Duration ==
v1   {"name":"backup","timeout":90000000000}
v2   error: json: unable to marshal from Go time.Duration within "/timeout": no default representation
v2+  {"name":"backup","timeout":90000000000}
== HTML escaping ==
&lt;/span&gt;&lt;span class="gp"&gt;v1   {"q":"&amp;lt;a&amp;amp;b&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"}
&lt;/span&gt;&lt;span class="gp"&gt;v2   {"q":"&amp;lt;a&amp;amp;b&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="gp"&gt;v2+  {"q":"&amp;lt;a&amp;amp;b&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Going through them in order of how likely they are to hurt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nil slices and maps.&lt;/strong&gt; v1 writes &lt;code&gt;null&lt;/code&gt; for a nil slice or map; v2 writes &lt;code&gt;[]&lt;/code&gt; and &lt;code&gt;{}&lt;/code&gt;. That is friendlier to JavaScript clients and a silent contract change for anyone who checks &lt;code&gt;=== null&lt;/code&gt;. &lt;code&gt;json.FormatNilSliceAsNull(true)&lt;/code&gt; and &lt;code&gt;json.FormatNilMapAsNull(true)&lt;/code&gt; bring the old output back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;omitempty means something else.&lt;/strong&gt; The first line shows &lt;code&gt;"admin":false,"retries":0&lt;/code&gt; reappearing. In v1, &lt;code&gt;omitempty&lt;/code&gt; drops false, 0, nil pointers and nil interfaces. In v2 it drops a field only if it would encode as JSON &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;""&lt;/code&gt;, &lt;code&gt;{}&lt;/code&gt; or &lt;code&gt;[]&lt;/code&gt;. The documentation says the two agree for strings, slices, maps and arrays, and that existing uses on a bool, number, pointer or interface "should migrate to specifying &lt;code&gt;omitzero&lt;/code&gt; instead", which works the same in both versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case-insensitive field matching is gone.&lt;/strong&gt; v1 happily put &lt;code&gt;{"NAME":"bob"}&lt;/code&gt; into the &lt;code&gt;Name&lt;/code&gt; field. v2 matches names exactly, so the field stays empty and, notice, no error is returned. If your clients send inconsistent casing, you will find out from missing data, not from logs. &lt;code&gt;json.MatchCaseInsensitiveNames(true)&lt;/code&gt; restores the loose match for a whole call; the &lt;code&gt;case:ignore&lt;/code&gt; tag option does it per field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicate keys are an error.&lt;/strong&gt; v1 let the last &lt;code&gt;"name"&lt;/code&gt; win, which is how &lt;code&gt;{"name":"alice","name":"mallory"}&lt;/code&gt; became mallory. The v2 documentation's security section explains why this matters when two services parse the same request differently. v2 rejects it unless you pass &lt;code&gt;jsontext.AllowDuplicateNames(true)&lt;/code&gt;. One detail from our run: after the error, the destination already held &lt;code&gt;"alice"&lt;/code&gt;. Treat the struct as garbage once &lt;code&gt;Unmarshal&lt;/code&gt; returns an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invalid UTF-8 is an error.&lt;/strong&gt; v1 replaced the bad byte with U+FFFD; v2 refuses, unless &lt;code&gt;jsontext.AllowInvalidUTF8(true)&lt;/code&gt; is set. If you ingest data from old Latin-1 systems, this is the one that will show up in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;time.Duration has no representation.&lt;/strong&gt; In v2, a &lt;code&gt;time.Duration&lt;/code&gt; "has no default representation and results in a SemanticError". That is the &lt;code&gt;v2 error&lt;/code&gt; line. The &lt;code&gt;v2+&lt;/code&gt; line passes &lt;code&gt;jsonv1.FormatDurationAsNano(true)&lt;/code&gt;, an option that lives in the v1 package, and gets the old nanosecond integer back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML escaping is off.&lt;/strong&gt; v2 uses minimal escaping, so &lt;code&gt;&amp;lt;a&amp;amp;b&amp;gt;&lt;/code&gt; goes out as is. If you embed JSON in HTML &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; blocks, keep &lt;code&gt;jsontext.EscapeForHTML(true)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The documentation lists a few more that our structs did not hit: Go arrays must now be unmarshaled from a JSON array of exactly the same length, byte arrays (not slices) become Base64 strings, maps are no longer sorted when marshaled unless you pass &lt;code&gt;json.Deterministic(true)&lt;/code&gt;, the &lt;code&gt;string&lt;/code&gt; tag option only applies to values that encode as numbers, unmarshaling &lt;code&gt;null&lt;/code&gt; always zeroes the target, and malformed struct tags are reported as errors at runtime instead of being ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tags that give the same JSON under v1 and v2
&lt;/h2&gt;

&lt;p&gt;You do not have to pick a side per call site. Most differences can be pinned on the type, so the same struct encodes identically whichever package touches it. This version of the struct passes a test that marshals with both and compares bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Portable keeps the same JSON under v1 and v2.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Portable&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"name,case:ignore"`&lt;/span&gt;
    &lt;span class="n"&gt;Tags&lt;/span&gt;    &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"tags,omitempty"`&lt;/span&gt;
    &lt;span class="n"&gt;Admin&lt;/span&gt;   &lt;span class="kt"&gt;bool&lt;/span&gt;     &lt;span class="s"&gt;`json:"admin,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;Retries&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;      &lt;span class="s"&gt;`json:"retries,omitzero"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestPortableSameBytes&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;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Portable&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"ana"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"bo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;Admin&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Retries&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;jsonv1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="p"&gt;)&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;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"v1 %s != v2 %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="p"&gt;)&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;Logf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b2&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;=== RUN   TestPortableSameBytes
    main_test.go:31: {"name":"ana"}
    main_test.go:31: {"name":"bo","tags":["x"],"admin":true,"retries":3}
--- PASS: TestPortableSameBytes (0.00s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;omitempty&lt;/code&gt; on the slice makes nil and empty both disappear, which sidesteps the &lt;code&gt;null&lt;/code&gt; versus &lt;code&gt;[]&lt;/code&gt; question. &lt;code&gt;omitzero&lt;/code&gt; replaces &lt;code&gt;omitempty&lt;/code&gt; on the bool and int. &lt;code&gt;case:ignore&lt;/code&gt; keeps accepting &lt;code&gt;"NAME"&lt;/code&gt; under v2, and a second test confirmed it decodes into &lt;code&gt;Name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While you are in there, v2 has an option v1 never had. &lt;code&gt;json.RejectUnknownMembers(true)&lt;/code&gt; turns a typo or an unexpected field into an error that wraps &lt;code&gt;json.ErrUnknownName&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;json: cannot unmarshal JSON string into Go main.Portable: unknown object member name "role"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is worth having on any request body that reaches an authorization check.&lt;/p&gt;

&lt;p&gt;If you used v2 under &lt;code&gt;GOEXPERIMENT=jsonv2&lt;/code&gt; on Go 1.25 or 1.26, check your tags again. The release notes list changes made before the package became final: the &lt;code&gt;format&lt;/code&gt; and &lt;code&gt;unknown&lt;/code&gt; tag options were removed, the &lt;code&gt;DiscardUnknownMembers&lt;/code&gt; option and &lt;code&gt;SkipFunc&lt;/code&gt; were removed, and the &lt;code&gt;inline&lt;/code&gt; tag option was renamed &lt;code&gt;embed&lt;/code&gt;. Blog posts written during the experiment still show &lt;code&gt;format:&lt;/code&gt; tags that will not work on 1.27.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating one option at a time with DefaultOptionsV1
&lt;/h2&gt;

&lt;p&gt;For a large codebase, the safe path is the one the official &lt;a href="https://go.dev/doc/jsonv2-migration" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; calls option-by-option. Change call sites to the v2 functions but pass &lt;code&gt;jsonv1.DefaultOptionsV1()&lt;/code&gt;, which the guide calls "a trivial and safe change". Then switch individual behaviours to v2 by adding options after it, since later options override earlier ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonv1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultOptionsV1&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"v2v1"&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;err&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;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonv1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultOptionsV1&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FormatNilSliceAsNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mix"&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;err&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 console"&gt;&lt;code&gt;&lt;span class="go"&gt;v2v1 {"name":"ana","tags":null}
mix  {"name":"ana","tags":[]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call is byte-for-byte v1. The second is v1 except for nil slices. Each option you flip is a small, reviewable change, and when the list of overrides covers everything you can drop &lt;code&gt;DefaultOptionsV1()&lt;/code&gt; entirely.&lt;/p&gt;

&lt;p&gt;For servers where you cannot predict every payload from tests, the guide points to &lt;a href="https://pkg.go.dev/github.com/go-json-experiment/jsonsplit" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/go-json-experiment/jsonsplit&lt;/code&gt;&lt;/a&gt;. It has call modes such as &lt;code&gt;CallBothButReturnV1&lt;/code&gt;, which runs both implementations, returns the v1 result and reports any difference, so production traffic tells you which options you need before you switch to &lt;code&gt;CallBothButReturnV2&lt;/code&gt; or &lt;code&gt;OnlyCallV2&lt;/code&gt;. The guide warns that running both "will approximately double the cost of marshaling". Use it for the migration window, not permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we measured: faster unmarshal, slower marshal
&lt;/h2&gt;

&lt;p&gt;The release notes say "marshal performance is broadly at parity with the previous implementation, while unmarshal performance is significantly faster." We checked that on one payload: a 500-element array of a struct with an int64, a string, a float, a three-element string slice, a two-entry &lt;code&gt;map[string]string&lt;/code&gt; and a nested struct, about 79 KB of JSON. The benchmark calls plain &lt;code&gt;encoding/json&lt;/code&gt;, run twelve times on the default 1.27.1 build and six times with &lt;code&gt;GOEXPERIMENT=nojsonv2&lt;/code&gt;, on an Apple M4 Pro:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;encoding/json on Go 1.27.1&lt;/th&gt;
&lt;th&gt;Unmarshal&lt;/th&gt;
&lt;th&gt;Allocs&lt;/th&gt;
&lt;th&gt;Marshal&lt;/th&gt;
&lt;th&gt;Allocs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v2 engine (default)&lt;/td&gt;
&lt;td&gt;~695 µs&lt;/td&gt;
&lt;td&gt;5,011&lt;/td&gt;
&lt;td&gt;~336 µs&lt;/td&gt;
&lt;td&gt;2,003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;old engine (&lt;code&gt;nojsonv2&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;~920 µs&lt;/td&gt;
&lt;td&gt;7,018&lt;/td&gt;
&lt;td&gt;~241 µs&lt;/td&gt;
&lt;td&gt;2,502&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Times are rounded medians; the unmarshal runs were noisy (537 to 878 µs on the new engine), while the allocation counts were identical on every run. Unmarshal came out about 25% faster with 29% fewer allocations, which matches the release notes. Marshal took about 39% longer on this payload, which does not match "broadly at parity". One payload on one machine is not a verdict, but it is enough to say: if marshaling is on your hot path, benchmark it on 1.27 with and without &lt;code&gt;nojsonv2&lt;/code&gt; before you roll out.&lt;/p&gt;

&lt;p&gt;If you are upgrading for the other Go 1.27 changes as well, the new &lt;a href="https://importstatic.com/go/go-synctest-testing-concurrent-code" rel="noopener noreferrer"&gt;&lt;code&gt;synctest.Sleep&lt;/code&gt; and in-memory test server&lt;/a&gt; are worth a look in the same pass.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://importstatic.com/go/go-json-v2-migration" rel="noopener noreferrer"&gt;ImportStatic&lt;/a&gt;. This article was drafted with AI assistance; every claim was checked against the sources linked in it and the code samples were compiled and run.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>json</category>
      <category>programming</category>
      <category>backend</category>
    </item>
    <item>
      <title>Claude Haiku 5.5 pricing jumps fivefold at 100,001 prompt tokens</title>
      <dc:creator>ImportStatic</dc:creator>
      <pubDate>Thu, 08 Oct 2026 01:03:42 +0000</pubDate>
      <link>https://dev.to/importstatic/claude-haiku-55-pricing-jumps-fivefold-at-100001-prompt-tokens-53ok</link>
      <guid>https://dev.to/importstatic/claude-haiku-55-pricing-jumps-fivefold-at-100001-prompt-tokens-53ok</guid>
      <description>&lt;p&gt;A request to Claude Haiku 5.5 with a 100,000-token prompt and 2,000 tokens of output costs $0.011. Add one token to the prompt and the same request costs $0.055. Nothing else changed; you crossed a line in the price list.&lt;/p&gt;

&lt;p&gt;Anthropic released Haiku 5.5 on October 7, 2026, and as of October 8 it's available on the Claude API, Amazon Bedrock, Google Cloud, Microsoft Foundry and Claude Platform on AWS under the ID &lt;code&gt;claude-haiku-5-5&lt;/code&gt; (&lt;code&gt;anthropic.claude-haiku-5-5&lt;/code&gt; on Bedrock). The headline is the price: $0.10 per million input tokens against $1 for Haiku 4.5. The catch is that the cheap rate is for "prompts up to 100,000 tokens", and the model takes prompts ten times that size. A &lt;a href="https://news.ycombinator.com/item?id=49996437" rel="noopener noreferrer"&gt;Hacker News discussion of the release&lt;/a&gt; went back and forth on whether agent workloads can stay under the line and didn't settle it. So we did the arithmetic with a small Go program. We had no API key in this run, so nothing below was sent to the model: the prices come from Anthropic's documentation and the costs are computed from them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Haiku 5.5 pricing is two complete price lists
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; gives Haiku 5.5 two rows, one "for prompts up to 100,000 tokens" and one "for prompts over 100,000 tokens". Per million 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;Up to 100,000&lt;/th&gt;
&lt;th&gt;Over 100,000&lt;/th&gt;
&lt;th&gt;Haiku 4.5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache read&lt;/td&gt;
&lt;td&gt;$0.01&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache write, 5 minutes&lt;/td&gt;
&lt;td&gt;$0.125&lt;/td&gt;
&lt;td&gt;$0.625&lt;/td&gt;
&lt;td&gt;$1.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache write, 1 hour&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch input / output&lt;/td&gt;
&lt;td&gt;$0.05 / $0.25&lt;/td&gt;
&lt;td&gt;$0.25 / $1.25&lt;/td&gt;
&lt;td&gt;$0.50 / $2.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every rate in the second column is five times the first. That includes output, which is the part people miss: the length of what you send decides the price of what comes back.&lt;/p&gt;

&lt;p&gt;This isn't a marginal bracket like income tax, where only the excess pays the higher rate. The row is chosen per prompt and applies to the whole request. It's also unique in the current lineup. For the other 1M-context models the same page says "A 900k-token request is billed at the same per-token rate as a 9k-token request", and it names Haiku 5.5 as the exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the docs say counts as the prompt, and what they leave out
&lt;/h2&gt;

&lt;p&gt;"Prompt" is doing a lot of work in that table, and the documentation doesn't define it as a formula over the &lt;code&gt;usage&lt;/code&gt; fields. Here's what it does say.&lt;/p&gt;

&lt;p&gt;With prompt caching on, the input is split across &lt;code&gt;input_tokens&lt;/code&gt;, &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; and &lt;code&gt;cache_read_input_tokens&lt;/code&gt;. The &lt;a href="https://platform.claude.com/docs/en/build-with-claude/context-windows" rel="noopener noreferrer"&gt;context windows page&lt;/a&gt; is blunt about cached tokens: "prompt caching changes what you pay for those tokens, not whether they count." So we treat the prompt as the sum of all three, which is the reading where a 100,000-token cached prefix plus a 50-token question lands in the expensive tier. That's our reading, not a quoted rule. If your bill depends on it, check a real invoice against a request you know the size of.&lt;/p&gt;

&lt;p&gt;Two more things move the line.&lt;/p&gt;

&lt;p&gt;Haiku 5.5 uses the newer tokenizer, and the migration guide says the same text "produces approximately 30% more tokens" than on Haiku 4.5. If you know your prompt sizes in Haiku 4.5 tokens, the threshold isn't 100,000 for you. It's about 76,900.&lt;/p&gt;

&lt;p&gt;And the pre-flight check is approximate. The &lt;a href="https://platform.claude.com/docs/en/build-with-claude/token-counting" rel="noopener noreferrer"&gt;token counting endpoint&lt;/a&gt; is free and takes the same body as a message request, but the docs call its result an estimate that "might differ by a small amount". Don't route on &lt;code&gt;&amp;lt;= 100000&lt;/code&gt;. Leave headroom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.anthropic.com/v1/messages/count_tokens &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"anthropic-version: 2023-06-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "claude-haiku-5-5", "messages": [{"role": "user", "content": "..."}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the documented request with the model swapped in; we didn't send it. Pass &lt;code&gt;claude-haiku-5-5&lt;/code&gt; and not your old model, or you'll get the old tokenizer's count.&lt;/p&gt;

&lt;h2&gt;
  
  
  One token over, priced in Go
&lt;/h2&gt;

&lt;p&gt;The calculator is one function. It mirrors the &lt;code&gt;usage&lt;/code&gt; object, picks a tier from the prompt length and applies that tier to everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Usage&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;InputTokens&lt;/span&gt;              &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="s"&gt;`json:"input_tokens"`&lt;/span&gt;
    &lt;span class="n"&gt;CacheCreationInputTokens&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="s"&gt;`json:"cache_creation_input_tokens"`&lt;/span&gt;
    &lt;span class="n"&gt;CacheReadInputTokens&lt;/span&gt;     &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="s"&gt;`json:"cache_read_input_tokens"`&lt;/span&gt;
    &lt;span class="n"&gt;OutputTokens&lt;/span&gt;             &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="s"&gt;`json:"output_tokens"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Rates are US dollars per million tokens.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Rates&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheWrite5m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheRead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Haiku55Short&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Rates&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheWrite5m&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.125&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheRead&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;Haiku55Long&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Rates&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheWrite5m&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.625&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheRead&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.50&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;Haiku45&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Rates&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheWrite5m&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheRead&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Haiku55Threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="n"&gt;_000&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&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;PromptTokens&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;int&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;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputTokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheCreationInputTokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheReadInputTokens&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&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;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;Rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;float64&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="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheCreationInputTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheWrite5m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheReadInputTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheRead&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1e6&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Haiku55Cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;Usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dollars&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;bool&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;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PromptTokens&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;Haiku55Threshold&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;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Haiku55Long&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&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;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Haiku55Short&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feed it two requests that differ by one token, then one 150,000-token prompt against the same material in two halves (1,000 output tokens each):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt  100000 + 2000 out  $0.011000  long=false
prompt  100001 + 2000 out  $0.055001  long=true

one request  $0.0775
two requests $0.0160
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second pair is the practical one. Summarising a long document in two 75,000-token calls costs a fifth of doing it in one, and you can add a third call to merge the halves and still be far ahead. For extraction and classification over big inputs, which is what this model is sold for, chunking just became a pricing decision as well as a quality one.&lt;/p&gt;

&lt;h2&gt;
  
  
  An agent loop crosses the line on turn 17
&lt;/h2&gt;

&lt;p&gt;Is 100,000 tokens a lot? For a single classification call, it's enormous. For a tool-calling agent that appends every result to the conversation, it isn't.&lt;/p&gt;

&lt;p&gt;We modelled a loop with prompt caching on: an 8,000-token prefix of system prompt and tool definitions, and each turn adding a 5,000-token tool result and a 500-token reply. Those sizes are made up to be plausible. This is a model of the billing, and your agent will have different numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;turn 16: prompt  95500  $0.00184  long=false
turn 17: prompt 101000  $0.00946  long=true
append only          $0.3267  (first long-tier turn: 17)
compact before 100k  $0.0623  (first long-tier turn: 0, compactions: 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn 17 costs five times turn 16 for almost the same work, and every later turn stays in the expensive tier because the history only grows. Over 40 turns the append-only loop costs $0.33. The second line replaces the history with a 3,000-token summary whenever the next prompt would cross 100,000, pays for writing that summary, and comes to $0.06. Whether a summary is good enough for your task is a separate question that no price table answers.&lt;/p&gt;

&lt;p&gt;There's a trap in the obvious fix. The API's &lt;a href="https://platform.claude.com/docs/en/build-with-claude/compaction-threshold" rel="noopener noreferrer"&gt;threshold compaction&lt;/a&gt;, in beta and listed as supporting Haiku 5.5, summarises old turns on the server once the input reaches a trigger. The default trigger is 150,000 input tokens. On every other model that's a sensible default. On this one it means compaction switches on 50,000 tokens after the price went up. Set it yourself; the minimum is 50,000:&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-haiku-5-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"context_management"&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;"edits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"compact_20260112"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80000&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="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&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;That body is adapted from the documented example and goes with the &lt;code&gt;anthropic-beta: compact-2026-01-12&lt;/code&gt; header. We didn't send it either.&lt;/p&gt;

&lt;p&gt;Don't trim the history by hand without reading the &lt;a href="https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; first. Haiku 5.5 thinks by default, and a request that sends a thinking block back after earlier messages changed can come back as a 400 error (the guide says which accounts get the check). Its instruction is to keep conversations append-only, which is exactly what a hand-rolled "drop the oldest tool results" loop doesn't do.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's still cheaper than Haiku 4.5 on both sides
&lt;/h2&gt;

&lt;p&gt;None of this makes Haiku 5.5 a bad deal. The long tier is half of Haiku 4.5's per-token price, and a quarter of Sonnet 5.5's $2 input rate.&lt;/p&gt;

&lt;p&gt;The tokenizer eats into that, though. Pricing the same text on both models, with Haiku 5.5's counts inflated by the documented 30%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;threshold in Haiku 4.5 tokens: 76923
4.5:  10000 in  $0.0150 | 5.5:  13000 in  $0.0019 long=false | saving 87%
4.5:  50000 in  $0.0550 | 5.5:  65000 in  $0.0072 long=false | saving 87%
4.5:  76000 in  $0.0810 | 5.5:  98800 in  $0.0105 long=false | saving 87%
4.5:  78000 in  $0.0830 | 5.5: 101400 in  $0.0539 long=true  | saving 35%
4.5: 150000 in  $0.1550 | 5.5: 195000 in  $0.1008 long=true  | saving 35%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So: 87% cheaper for the same text under the line, 35% cheaper above it. Anthropic's announcement says prompts up to 100,000 tokens "make up around 90% of requests to our previous Haiku model". That's a share of requests, and long requests carry more tokens each, so the share of your spend that lands in the long tier can be a good deal higher than one in ten.&lt;/p&gt;

&lt;p&gt;The switch isn't only a price change. Manual &lt;code&gt;budget_tokens&lt;/code&gt; thinking, non-default &lt;code&gt;temperature&lt;/code&gt;, and assistant prefill all return 400 errors on Haiku 5.5, much like the changes we listed for &lt;a href="https://dev.to/ai/claude-sonnet-5-5-api-migration"&gt;Claude Sonnet 5.5&lt;/a&gt;. And if your long prompts are long because of tool definitions, &lt;a href="https://dev.to/ai/mcp-tool-definition-token-overhead"&gt;measure those first&lt;/a&gt;; they're in every request.&lt;/p&gt;

&lt;p&gt;Our advice is short. Put a budget of 90,000 counted tokens on anything you send to Haiku 5.5, set the compaction trigger below it, and log &lt;code&gt;usage&lt;/code&gt; per request so you can see how many calls go over. If a workload can't fit, split it. If it can't be split, run it anyway, and know that it's the 35% discount you're getting.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
