<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Ai on WebDevStation</title>
    <link>https://webdevstation.com/tags/ai/</link>
    <description>5 articles tagged Ai — tutorials, code examples and notes from building real systems, newest first.</description>
    <generator>Hugo</generator>
    <language>en</language>
    <managingEditor>Alex</managingEditor>
    <webMaster>Alex</webMaster>
    <copyright>© 2026 WebDevStation</copyright>
    <lastBuildDate>Tue, 01 Sep 2026 09:20:00 +0200</lastBuildDate>
    <atom:link href="https://webdevstation.com/tags/ai/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How to Test Go Code That Calls an LLM</title>
      <link>https://webdevstation.com/posts/testing-go-code-that-calls-an-llm/</link>
      <pubDate>Tue, 01 Sep 2026 09:20:00 +0200</pubDate>
      <author>Alex</author>
      <guid isPermaLink="true">https://webdevstation.com/posts/testing-go-code-that-calls-an-llm/</guid>
      <description>Non-determinism does not have to mean untestable. Wrapping the model behind an interface, faking it in unit tests, golden files for prompt assembly, and the small…</description>
      <content:encoded><![CDATA[<p>The first question everyone asks about a feature backed by a language model is &ldquo;how do you even test that?&rdquo; — usually with a shrug, as though non-determinism were a get-out-of-jail card for the whole test suite. It is not. Almost all of the code you write around a model is perfectly deterministic, and the small part that is not can be pinned down with a different kind of test. Here is how I split it.</p>
<h2 id="three-things-tested-three-ways">Three Things, Tested Three Ways</h2>
<p>The confusion comes from treating &ldquo;the LLM feature&rdquo; as one indivisible thing. It is three:</p>
<table>
	<thead>
			<tr>
					<th>What</th>
					<th>Deterministic?</th>
					<th>How to test it</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Prompt assembly, parsing, tool handlers, the loop</td>
					<td>Yes, completely</td>
					<td>Ordinary unit tests</td>
			</tr>
			<tr>
					<td>Wiring to the real API — auth, streaming, errors</td>
					<td>Yes enough</td>
					<td>A few integration tests, run on demand</td>
			</tr>
			<tr>
					<td>Output quality — is the answer any good?</td>
					<td>No</td>
					<td>Evals, scored not asserted</td>
			</tr>
	</tbody>
</table>
<p>The first row is 90% of your code and needs no model at all. Get that boundary right and the rest is manageable.</p>
<h2 id="put-an-interface-in-front-of-the-model">Put an Interface in Front of the Model</h2>
<p>Do not scatter <code>client.Messages.New</code> through your handlers. Define the narrowest interface your code actually needs and depend on that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Completer turns a prompt into text. It is deliberately narrow — narrower</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// than the SDK — so tests can implement it in five lines.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Completer</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Request</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">System</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Messages</span> []<span style="color:#a6e22e">Message</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Tools</span>    []<span style="color:#a6e22e">Tool</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Response</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Text</span>       <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ToolCalls</span>  []<span style="color:#a6e22e">ToolCall</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">StopReason</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Usage</span>      <span style="color:#a6e22e">Usage</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is just the dependency-inversion habit that makes any external service testable, and it earns its keep faster here than almost anywhere else. Two things fall out of it:</p>
<p><strong>Your business logic never imports the SDK.</strong> It imports your <code>Request</code> and <code>Response</code> types. When the SDK&rsquo;s union types change shape, one adapter file changes.</p>
<p><strong>The fake is trivial.</strong> No HTTP, no fixtures, no mocking library:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">fakeCompleter</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">responses</span> []<span style="color:#a6e22e">Response</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">err</span>       <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">calls</span>     []<span style="color:#a6e22e">Request</span> <span style="color:#75715e">// recorded for assertions</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">fakeCompleter</span>) <span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">_</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">calls</span> = append(<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">calls</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Response</span>{}, <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">responses</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Response</span>{}, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;fakeCompleter: no responses left&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">responses</span>[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">responses</span> = <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">responses</span>[<span style="color:#ae81ff">1</span>:]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Returning a queue rather than a single value is what lets you test multi-turn loops: the first call returns a tool request, the second returns the final answer.</p>
<h2 id="test-the-loop-not-the-model">Test the Loop, Not the Model</h2>
<p>An agent loop has plenty of logic worth testing, none of which needs a model. Does it stop when it should? Does it return a result for every tool call? Does it hand a tool failure back rather than aborting?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestLoopReturnsResultForFailedTool</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fake</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fakeCompleter</span>{<span style="color:#a6e22e">responses</span>: []<span style="color:#a6e22e">Response</span>{
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">StopReason</span>: <span style="color:#e6db74">&#34;tool_use&#34;</span>, <span style="color:#a6e22e">ToolCalls</span>: []<span style="color:#a6e22e">ToolCall</span>{
</span></span><span style="display:flex;"><span>            {<span style="color:#a6e22e">ID</span>: <span style="color:#e6db74">&#34;t1&#34;</span>, <span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;lookup&#34;</span>, <span style="color:#a6e22e">Input</span>: []byte(<span style="color:#e6db74">`{&#34;id&#34;:&#34;missing&#34;}`</span>)},
</span></span><span style="display:flex;"><span>        }},
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">StopReason</span>: <span style="color:#e6db74">&#34;end_turn&#34;</span>, <span style="color:#a6e22e">Text</span>: <span style="color:#e6db74">&#34;I couldn&#39;t find that record.&#34;</span>},
</span></span><span style="display:flex;"><span>    }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">agent</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">fake</span>, <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">ToolFunc</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;lookup&#34;</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, []<span style="color:#66d9ef">byte</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;not found&#34;</span>)
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">agent</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#e6db74">&#34;look up record missing&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;Run() error = %v, want nil&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">got</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;I couldn&#39;t find that record.&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;got %q&#34;</span>, <span style="color:#a6e22e">got</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// The failure must reach the model as a tool result, not abort the loop.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">second</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fake</span>.<span style="color:#a6e22e">calls</span>[<span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">findToolResult</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">second</span>, <span style="color:#e6db74">&#34;t1&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">IsError</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;tool failure was not marked as an error result&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That test catches a real bug — a loop that drops the failed call instead of reporting it — and runs in microseconds with no API key.</p>
<p>The same approach covers the rest of the loop&rsquo;s contract — <a href="/posts/tool-use-in-go-agent-loop/">the invariants from the agent-loop article</a>: that it stops at <code>MaxIterations</code>, that every <code>tool_use</code> block gets exactly one result, that results come back in one message. Table-driven tests fit this beautifully, since each case is just a different queue of canned responses.</p>
<h2 id="golden-files-for-prompt-assembly">Golden Files for Prompt Assembly</h2>
<p>Prompt building is string manipulation, and it drifts. Someone adds a field, reorders a section, changes a heading — and unlike code, a prompt regression produces no compile error and no failing assertion, just slightly worse output that nobody attributes to the change.</p>
<p>Golden files make the diff visible in review:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">update</span> = <span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">Bool</span>(<span style="color:#e6db74">&#34;update&#34;</span>, <span style="color:#66d9ef">false</span>, <span style="color:#e6db74">&#34;update golden files&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestBuildSystemPrompt</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tests</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">name</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">cfg</span>    <span style="color:#a6e22e">Config</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">golden</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    }{
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">&#34;default&#34;</span>, <span style="color:#a6e22e">Config</span>{}, <span style="color:#e6db74">&#34;system_default.txt&#34;</span>},
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">&#34;with_tools&#34;</span>, <span style="color:#a6e22e">Config</span>{<span style="color:#a6e22e">Tools</span>: <span style="color:#a6e22e">allTools</span>}, <span style="color:#e6db74">&#34;system_with_tools.txt&#34;</span>},
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">&#34;terse_mode&#34;</span>, <span style="color:#a6e22e">Config</span>{<span style="color:#a6e22e">Terse</span>: <span style="color:#66d9ef">true</span>}, <span style="color:#e6db74">&#34;system_terse.txt&#34;</span>},
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">tt</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">tests</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">tt</span>.<span style="color:#a6e22e">name</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">got</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BuildSystemPrompt</span>(<span style="color:#a6e22e">tt</span>.<span style="color:#a6e22e">cfg</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">path</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">filepath</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#e6db74">&#34;testdata&#34;</span>, <span style="color:#a6e22e">tt</span>.<span style="color:#a6e22e">golden</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">update</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#a6e22e">path</span>, []byte(<span style="color:#a6e22e">got</span>), <span style="color:#ae81ff">0</span><span style="color:#a6e22e">o644</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">want</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ReadFile</span>(<span style="color:#a6e22e">path</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">diff</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmp</span>.<span style="color:#a6e22e">Diff</span>(string(<span style="color:#a6e22e">want</span>), <span style="color:#a6e22e">got</span>); <span style="color:#a6e22e">diff</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;prompt changed (-want +got):\n%s&#34;</span>, <span style="color:#a6e22e">diff</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Run <code>go test ./... -update</code> to accept a change deliberately. The point is not that the golden file is correct — it is that changing it requires saying so out loud, in a diff a reviewer can read.</p>
<p>This also catches the caching bugs from the <a href="/posts/prompt-caching-llm-cost/">prompt caching article</a> before they cost you anything. A golden test on the system prompt fails the moment somebody interpolates <code>time.Now()</code> into it, because the output differs on every run.</p>
<p>Which suggests a second, blunter test worth having:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestPromptAssemblyIsDeterministic</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Go randomises map iteration order, so a prompt built by ranging over a</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// map differs run to run — and silently never caches.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">first</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BuildSystemPrompt</span>(<span style="color:#a6e22e">cfg</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#ae81ff">20</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">got</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BuildSystemPrompt</span>(<span style="color:#a6e22e">cfg</span>); <span style="color:#a6e22e">got</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">first</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;prompt is not deterministic on run %d&#34;</span>, <span style="color:#a6e22e">i</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Twenty iterations is enough to make a randomised map order fail essentially every time.</p>
<h2 id="integration-tests-behind-a-build-tag">Integration Tests, Behind a Build Tag</h2>
<p>You do need a handful of tests that touch the real API — enough to catch an SDK upgrade that changed a union type, a model id that no longer resolves, or streaming that broke. But they cost money and need a key, so they must not run on every <code>go test ./...</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">//go:build integration</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">llm_test</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestStreamingReturnsCompleteMessage</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;ANTHROPIC_API_KEY&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Skip</span>(<span style="color:#e6db74">&#34;no API key; skipping integration test&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ... real call, assert on shape rather than content</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go test ./...                    <span style="color:#75715e"># fast, free, no key</span>
</span></span><span style="display:flex;"><span>go test -tags<span style="color:#f92672">=</span>integration ./...  <span style="color:#75715e"># the real thing, on demand</span>
</span></span></code></pre></div><p>Assert on <strong>shape</strong>, never on wording. <code>resp.Text</code> being non-empty, <code>StopReason</code> being <code>end_turn</code>, <code>Usage.OutputTokens</code> being greater than zero, a streamed message accumulating to the same content as a non-streamed one. Those hold across model versions. &ldquo;The answer contains the word Paris&rdquo; does not, and a flaky test that fails once a month teaches your team to ignore failures.</p>
<p>There is a middle option that gets you a long way for free: point the SDK at a local <code>httptest.Server</code> via the <code>ANTHROPIC_BASE_URL</code> environment variable and serve canned JSON. That exercises the real SDK — its parsing, its retry behaviour, its streaming decoder — without a key or a bill. It is the closest analogue to what <a href="/posts/how-to-test-database-interactions-go/">go-sqlmock</a> does for the database layer: a real driver, a fake server.</p>
<p>It is also the only sane way to test the paths you cannot easily provoke on purpose:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Rate limiting: does the caller back off, or hammer?</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">mux</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/v1/messages&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;Retry-After&#34;</span>, <span style="color:#e6db74">&#34;1&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusTooManyRequests</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprint</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">`{&#34;type&#34;:&#34;error&#34;,&#34;error&#34;:{&#34;type&#34;:&#34;rate_limit_error&#34;,&#34;message&#34;:&#34;...&#34;}}`</span>)
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>Do the same for a 500, a connection dropped mid-stream, and a malformed body. Those are the failures that actually page you, and they are trivial to simulate and nearly impossible to trigger on demand against the real API.</p>
<h2 id="the-refusal-case-nobody-tests">The Refusal Case Nobody Tests</h2>
<p>Worth its own paragraph because it is so easy to miss: a safety refusal comes back as <strong>HTTP 200</strong> with <code>StopReason</code> set to <code>refusal</code>. Your error handling never fires. Code that goes from <code>if err != nil</code> straight to reading the first content block treats it as an empty answer.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestRefusalIsNotTreatedAsEmptyAnswer</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fake</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fakeCompleter</span>{<span style="color:#a6e22e">responses</span>: []<span style="color:#a6e22e">Response</span>{
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">StopReason</span>: <span style="color:#e6db74">&#34;refusal&#34;</span>, <span style="color:#a6e22e">Text</span>: <span style="color:#e6db74">&#34;&#34;</span>},
</span></span><span style="display:flex;"><span>    }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">fake</span>, <span style="color:#66d9ef">nil</span>).<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#e6db74">&#34;...&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">Is</span>(<span style="color:#a6e22e">err</span>, <span style="color:#a6e22e">ErrDeclined</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;got %v, want ErrDeclined&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>One line in the fake, and you have covered a branch most production code does not have at all. The sentinel-error pattern behind <code>ErrDeclined</code> is the one from <a href="/posts/error-handling-in-go/">error handling in Go</a>.</p>
<h2 id="evals-for-quality-not-correctness">Evals: For Quality, Not Correctness</h2>
<p>Everything above tests whether your <em>code</em> is right. None of it tells you whether the answers are any good. That needs a different instrument, and the mistake is trying to force it into <code>go test</code>.</p>
<p>An eval is a fixed set of inputs, run against the real model, scored rather than asserted:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">//go:build eval</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestClassificationAccuracy</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cases</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">loadEvalCases</span>(<span style="color:#a6e22e">t</span>, <span style="color:#e6db74">&#34;testdata/eval/classification.jsonl&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">correct</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">cases</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">classify</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">realClient</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Input</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">got</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Want</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">correct</span><span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Logf</span>(<span style="color:#e6db74">&#34;MISS: input=%q got=%q want=%q&#34;</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Input</span>, <span style="color:#a6e22e">got</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Want</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">accuracy</span> <span style="color:#f92672">:=</span> float64(<span style="color:#a6e22e">correct</span>) <span style="color:#f92672">/</span> float64(len(<span style="color:#a6e22e">cases</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Logf</span>(<span style="color:#e6db74">&#34;accuracy: %.1f%% (%d/%d)&#34;</span>, <span style="color:#a6e22e">accuracy</span><span style="color:#f92672">*</span><span style="color:#ae81ff">100</span>, <span style="color:#a6e22e">correct</span>, len(<span style="color:#a6e22e">cases</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// A floor, not an equality check. Below this, something regressed.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">accuracy</span> &lt; <span style="color:#ae81ff">0.90</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;accuracy %.1f%% below the 90%% floor&#34;</span>, <span style="color:#a6e22e">accuracy</span><span style="color:#f92672">*</span><span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Two things make this useful rather than annoying. <strong>The threshold is a floor</strong>, not a target — you are detecting regression, not demanding perfection. And <strong>the misses get logged</strong>, because the list of what it got wrong is the actual output; the pass/fail is almost incidental.</p>
<p>Run evals when you change a prompt, a model, or an effort setting — not on every commit. They cost money and take minutes.</p>
<h2 id="what-not-to-do">What Not to Do</h2>
<p><strong>Do not assert on model wording.</strong> <code>strings.Contains(resp, &quot;Paris&quot;)</code> passes today and fails after a model update that phrases it differently. It is not testing your code.</p>
<p><strong>Do not set temperature to zero and call it deterministic.</strong> Sampling parameters are not even accepted on current models, and identical output was never guaranteed regardless.</p>
<p><strong>Do not mock the SDK&rsquo;s types.</strong> Mocking <code>anthropic.Message</code> and its union blocks is a lot of work to test the adapter you wrote to avoid exactly that. Fake your own interface instead.</p>
<p><strong>Do not let integration tests run by default.</strong> A test suite that needs an API key is a test suite that new contributors cannot run, and CI cost that grows with every push.</p>
<p><strong>Do not skip testing the error paths</strong> because they are &ldquo;just the SDK&rsquo;s job&rdquo;. Rate limits, refusals and mid-stream disconnects are the failures you will actually see in production, and they are the cheapest things in this article to cover.</p>
<h2 id="checklist">Checklist</h2>
<ul>
<li>A narrow interface between your logic and the SDK; business code never imports the SDK.</li>
<li>A fake with a queue of canned responses for multi-turn loops.</li>
<li>Unit tests for the loop&rsquo;s contract: one result per tool call, errors handed back, iteration cap honoured.</li>
<li>Golden files for prompt assembly, plus a determinism test.</li>
<li>Integration tests behind <code>//go:build integration</code>, asserting on shape not wording.</li>
<li>An <code>httptest.Server</code> for 429s, 500s and truncated streams.</li>
<li>A test for the refusal path.</li>
<li>Evals behind their own tag, scored against a floor, misses logged.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>&ldquo;You cannot test LLM code&rdquo; conflates the model with the code around it. The code around it — the prompt builder, the parser, the loop, the tool handlers, the error branches — is ordinary Go, and it becomes easy to test the moment there is an interface between it and the SDK. Push non-determinism out to the edges, cover the edges with evals scored against a floor, and the rest of your suite stays fast, free and green.</p>]]></content:encoded>
      <category>AI Engineering</category>
      <category>Go Programming</category>
      <category>Testing</category>
    </item>
    <item>
      <title>Prompt Caching: The Cheapest Win in Your LLM Bill</title>
      <link>https://webdevstation.com/posts/prompt-caching-llm-cost/</link>
      <pubDate>Mon, 31 Aug 2026 11:15:00 +0200</pubDate>
      <author>Alex</author>
      <guid isPermaLink="true">https://webdevstation.com/posts/prompt-caching-llm-cost/</guid>
      <description>Prompt caching is a prefix match, and one stray timestamp can silently disable it. How cache breakpoints, TTLs and the usage fields actually work — and how to build…</description>
      <content:encoded><![CDATA[<p>I have written on this blog about caching database reads with <a href="/posts/ristretto-the-most-performant-concurrent-cache-library-for-go/">Ristretto</a> and about teaching <a href="/posts/how-to-make-nginx-cookie-aware/">Nginx to cache by cookie</a>. Prompt caching belongs in the same family, with one difference that makes it far more interesting: the thing you are caching costs real money per byte, and when the cache stops working, nothing breaks. No error, no alert, no failed request. Just a bigger invoice next month.</p>
<h2 id="one-invariant-everything-follows-from-it">One Invariant, Everything Follows From It</h2>
<p><strong>Prompt caching is a prefix match. Any change anywhere in the prefix invalidates everything after it.</strong></p>
<p>That is the whole model. The cache key is derived from the exact bytes of your rendered prompt up to each breakpoint. One byte different at position N — a timestamp, a reordered JSON key, an extra tool in the list — and every cached position at or after N is gone.</p>
<p>The render order matters and is fixed:</p>
<pre tabindex="0"><code>tools  →  system  →  messages
</code></pre><p>Tools render first, at position zero. That has a consequence people discover the expensive way: <strong>change the tool list and you have invalidated everything</strong>, system prompt and entire conversation included. More on that below.</p>
<h2 id="what-it-costs">What It Costs</h2>
<p>Two numbers govern whether caching pays:</p>
<ul>
<li>A cache <strong>read</strong> costs about <strong>0.1×</strong> the base input price.</li>
<li>A cache <strong>write</strong> costs <strong>1.25×</strong> for the 5-minute TTL, <strong>2×</strong> for the 1-hour TTL.</li>
</ul>
<p>So with the default 5-minute TTL, two requests already break even: <code>1.25 + 0.1 = 1.35</code> against <code>2.0</code> uncached. By the third request you are well ahead. With the 1-hour TTL you need three requests to break even, because the write costs double.</p>
<p>Which is why the TTL question is <em>not</em> &ldquo;how long do I want this cached&rdquo; but &ldquo;how far apart do requests sharing this prefix start&rdquo;:</p>
<table>
	<thead>
			<tr>
					<th>Start-to-start gap</th>
					<th>Use</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Under 5 minutes</td>
					<td>5-minute TTL. Every read refreshes the timer, so continuous traffic keeps it warm indefinitely and it is strictly cheaper.</td>
			</tr>
			<tr>
					<td>5–60 minutes</td>
					<td>1-hour TTL. This is the only window where the doubled write price earns its keep.</td>
			</tr>
			<tr>
					<td>Over an hour</td>
					<td>Neither, directly. Re-warm on a schedule or accept the cold miss.</td>
			</tr>
	</tbody>
</table>
<p>The subtlety in row one: a read refreshes the entry at no extra cost, and the lifetime is measured from the <em>start</em> of the request. A four-minute generation leaves about one minute for the next request to begin before a five-minute entry expires. For a chat endpoint under steady load, the 5-minute TTL is the right answer and the 1-hour TTL just doubles your write bill.</p>
<h2 id="making-it-work-in-go">Making It Work in Go</h2>
<p>The syntax is a <code>CacheControl</code> on the last block of whatever you want cached. Because tools render before system, a marker on the final system block caches <strong>both</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">params</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageNewParams</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Model</span>:     <span style="color:#e6db74">&#34;claude-opus-5&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">16000</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Tools</span>:     <span style="color:#a6e22e">tools</span>, <span style="color:#75715e">// deterministic order — see below</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">System</span>: []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">TextBlockParam</span>{{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Text</span>:         <span style="color:#a6e22e">systemPrompt</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">CacheControl</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewCacheControlEphemeralParam</span>(), <span style="color:#75715e">// 5-minute default</span>
</span></span><span style="display:flex;"><span>    }},
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Messages</span>: <span style="color:#a6e22e">messages</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For the 1-hour TTL:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">CacheControl</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">CacheControlEphemeralParam</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">TTL</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">CacheControlEphemeralTTLTTL1h</span>,
</span></span><span style="display:flex;"><span>},
</span></span></code></pre></div><p>There is also a top-level <code>CacheControl</code> on <code>MessageNewParams</code> that automatically places a breakpoint on the last cacheable block and moves it forward as the conversation grows. For multi-turn chat that is the right default — no marker bookkeeping, and the growing history caches incrementally.</p>
<p><strong>The robust combination for anything agentic:</strong> one explicit breakpoint at the end of the static system prefix, so the expensive shared part has a guaranteed read point no matter what happens later in <code>messages</code>, plus top-level automatic caching for the growing tail.</p>
<p>You get four breakpoints per request, so there is no need to be frugal — place them at genuine stability boundaries.</p>
<h2 id="where-automatic-caching-is-the-wrong-tool">Where Automatic Caching Is the Wrong Tool</h2>
<p>Automatic placement puts the breakpoint at the very end of your prompt. When the prompt <em>ends</em> with something unique per request — a retrieved document, the user&rsquo;s actual question — that is a pure surcharge: every request writes a new cache entry that nothing will ever read.</p>
<p>The signature is unmistakable once you know it: <code>cache_creation_input_tokens</code> is non-zero on every single request, while <code>cache_read_input_tokens</code> never covers the shared prefix.</p>
<p>The fix is an explicit marker at the end of the <strong>shared</strong> portion:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">Messages</span>: []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageParam</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewUserMessage</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">TextBlockParam</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Text</span>:         <span style="color:#a6e22e">sharedContext</span>, <span style="color:#75715e">// few-shot examples, retrieved docs</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">CacheControl</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewCacheControlEphemeralParam</span>(),
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewTextBlock</span>(<span style="color:#a6e22e">userQuestion</span>), <span style="color:#75715e">// no marker — differs every time</span>
</span></span><span style="display:flex;"><span>    ),
</span></span><span style="display:flex;"><span>},
</span></span></code></pre></div><p>Same rule, restated: put the breakpoint where the prompt <em>stops</em> being shared, not where the prompt ends.</p>
<h2 id="the-minimum-prefix-which-is-not-monotonic">The Minimum Prefix, Which Is Not Monotonic</h2>
<p>A prompt shorter than the model&rsquo;s minimum will not cache — no error, no warning, <code>cache_creation_input_tokens</code> simply comes back zero. And the minimum does not move in the direction you would guess as models get newer:</p>
<table>
	<thead>
			<tr>
					<th>Model</th>
					<th style="text-align: right">Minimum</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Claude Opus 5, Fable 5</td>
					<td style="text-align: right">512 tokens</td>
			</tr>
			<tr>
					<td>Opus 4.8, Sonnet 5, Sonnet 4.6</td>
					<td style="text-align: right">1024 tokens</td>
			</tr>
			<tr>
					<td>Opus 4.7</td>
					<td style="text-align: right">2048 tokens</td>
			</tr>
			<tr>
					<td>Opus 4.6, Haiku 4.5</td>
					<td style="text-align: right">4096 tokens</td>
			</tr>
	</tbody>
</table>
<p>A 3,000-token system prompt caches on Opus 5 and Opus 4.8, and silently does not on Opus 4.6 or Haiku 4.5. If you switched models and your cache hit rate fell off a cliff, this is the first thing to check — and it cuts the other way too: moving to Opus 5 halves the Opus 4.8 minimum, so prompts that were previously too short start caching with no code change at all.</p>
<h2 id="silent-invalidators">Silent Invalidators</h2>
<p>This is the part worth committing to memory, because every one of these is code that looks perfectly reasonable in review.</p>
<table>
	<thead>
			<tr>
					<th>Pattern</th>
					<th>Why it kills the cache</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>time.Now()</code> in the system prompt</td>
					<td>The prefix differs on every single request</td>
			</tr>
			<tr>
					<td>A request ID or UUID early in the content</td>
					<td>Same — every request is unique</td>
			</tr>
			<tr>
					<td><code>json.Marshal</code> of a <code>map</code> in the prompt</td>
					<td>Go randomises map iteration order; the bytes differ run to run</td>
			</tr>
			<tr>
					<td>Ranging over a map to build tool definitions</td>
					<td>Same problem, at position zero, which is the worst place for it</td>
			</tr>
			<tr>
					<td>User or session ID interpolated into the system prompt</td>
					<td>A per-user prefix; nothing shares anything</td>
			</tr>
			<tr>
					<td><code>if flag { system += ... }</code></td>
					<td>Every flag combination is a distinct prefix</td>
			</tr>
			<tr>
					<td>A tool set that varies per user or per mode</td>
					<td>Tools render first — nothing caches across users</td>
			</tr>
	</tbody>
</table>
<p>The Go-specific ones deserve emphasis. Map iteration order in Go is deliberately randomised, so this is not a cache bug that appears under load — it appears on <strong>every request</strong>, and it is invisible because the rendered prompt is semantically identical each time:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Bad: iteration order is randomised, so the bytes differ every run.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">name</span>, <span style="color:#a6e22e">def</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">tools</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span> = append(<span style="color:#a6e22e">tools</span>, <span style="color:#a6e22e">def</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Good: deterministic.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">names</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">slices</span>.<span style="color:#a6e22e">Sorted</span>(<span style="color:#a6e22e">maps</span>.<span style="color:#a6e22e">Keys</span>(<span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">tools</span>))
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">name</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">names</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span> = append(<span style="color:#a6e22e">tools</span>, <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">tools</span>[<span style="color:#a6e22e">name</span>])
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Sorting keys is a one-line fix that most Go LLM code needs and almost none has.</p>
<h2 id="injecting-dynamic-context-without-breaking-everything">Injecting Dynamic Context Without Breaking Everything</h2>
<p>The usual reason a system prompt has a timestamp in it is that the model genuinely needs to know the date, or the user&rsquo;s plan tier, or the current mode. The instinct is to template it into the system prompt. Don&rsquo;t — that is the front of the prefix, and it invalidates everything behind it.</p>
<p>Put dynamic context <strong>after</strong> the cached history instead. On the newest models there is a first-class channel for this: a <code>system</code>-role message appended to <code>messages</code>, rather than an edit to the top-level <code>system</code> field.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// The top-level system prompt stays byte-identical and stays cached.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// The operator instruction goes after the history, invalidating nothing before it.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">messages</span> = append(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">userTurn</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">messages</span> = append(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageParam</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Role</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageParamRoleSystem</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Content</span>: []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ContentBlockParamUnion</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewTextBlock</span>(<span style="color:#e6db74">&#34;Terse mode enabled — keep responses under 40 words.&#34;</span>),
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>A message at turn five invalidates nothing before turn five. That is the whole trick.</p>
<p>Two constraints: it must follow a user message and be either the last entry or followed by an assistant turn — it cannot be <code>messages[0]</code>, so use the top-level <code>system</code> for the initial prompt. And support is model-dependent; unsupported models return a 400 saying the <code>system</code> role is not supported, so catch that and fall back to putting the instruction in a user turn.</p>
<h2 id="three-rules-that-beat-marker-placement">Three Rules That Beat Marker Placement</h2>
<p>Fix these before you fiddle with breakpoints.</p>
<p><strong>Freeze the system prompt.</strong> No dates, no user names, no modes. It is the front of the prefix and everything downstream depends on it not moving.</p>
<p><strong>Never change tools or model mid-conversation.</strong> Tools render at position zero, so adding, removing or reordering one invalidates the entire cache. Caches are also model-scoped, so switching models mid-conversation starts from cold. If you need &ldquo;modes&rdquo;, do not swap the tool set — pass the mode as message content.</p>
<p><strong>Forked calls must reuse the parent&rsquo;s exact prefix.</strong> Summarisation passes, sub-agents and side computations usually build their own request. If that fork rebuilds <code>system</code>, <code>tools</code> or <code>model</code> with any difference at all, it misses the parent&rsquo;s cache completely. Copy them verbatim and append the fork-specific content at the end.</p>
<p>That last one is also the argument against a &ldquo;cheap model for the easy stuff&rdquo; cascade, at least as a first move. Caches are per model, so routing between two models forfeits cache reuse across them. Measure the capable model at lower effort before you build the cascade — it is often cheaper <em>and</em> simpler, and it keeps one cache namespace.</p>
<h2 id="verifying-it-forever">Verifying It, Forever</h2>
<p>Every response carries the accounting (the same <code>Usage</code> struct I said to log from day one in <a href="/posts/calling-claude-from-go/">calling an LLM from Go</a>):</p>
<table>
	<thead>
			<tr>
					<th>Field</th>
					<th>Meaning</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>CacheCreationInputTokens</code></td>
					<td>Written to cache this request (you paid ~1.25×)</td>
			</tr>
			<tr>
					<td><code>CacheReadInputTokens</code></td>
					<td>Served from cache (you paid ~0.1×)</td>
			</tr>
			<tr>
					<td><code>InputTokens</code></td>
					<td>Full price, uncached</td>
			</tr>
	</tbody>
</table>
<p><code>InputTokens</code> is the <strong>uncached remainder only</strong> — not the prompt size. Total prompt = all three added together. If an agent ran for an hour and <code>InputTokens</code> reads 4K, the rest came from cache; check the sum, not the one field.</p>
<p>In a healthy multi-turn loop you should see, on each request:</p>
<ul>
<li><code>CacheReadInputTokens</code> — the whole prior prefix, growing turn over turn.</li>
<li><code>CacheCreationInputTokens</code> — roughly the last assistant turn plus the new input. Small.</li>
<li><code>InputTokens</code> — just the tail past the last breakpoint.</li>
</ul>
<p>If <code>CacheCreationInputTokens</code> is instead close to the full conversation size every time, the prefix is being rewritten upstream of your breakpoint. Go find the timestamp.</p>
<p><strong>And then keep checking.</strong> The expensive failure here is never the bad first implementation — it is the regression. Caching works the day you write it, then six weeks later somebody adds a dynamic field to the system prompt or a tool list that stopped being sorted, and every request misses. Nothing fails. Nothing pages. You find out from finance.</p>
<p>So make it a standing assertion, not a one-time look:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestSystemPromptStaysCacheable</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Two identical requests: the second must read from cache.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">first</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">callModel</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">fixture</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">first</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">CacheCreationInputTokens</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;nothing was cached: prompt may be under the model minimum&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">second</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">callModel</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">fixture</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">second</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">CacheReadInputTokens</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;cache miss on an identical prompt — a silent invalidator crept in&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That test costs a few cents to run and catches a regression that otherwise runs for months. Put it behind a build tag so it only runs when you mean it — the same treatment I gave integration tests in <a href="/posts/how-to-test-database-interactions-go/">how to test database interactions in Golang</a>. Better still, put a monitor on the ratio of <code>CacheReadInputTokens</code> to total input tokens and alert when it drops.</p>
<h2 id="checklist">Checklist</h2>
<ul>
<li>Frozen system prompt: no dates, IDs, names or conditional sections.</li>
<li>Deterministic tool list, sorted by name, identical across users.</li>
<li>One explicit breakpoint at the end of the static prefix; automatic caching for the tail.</li>
<li>Breakpoint at the end of the <em>shared</em> portion, not the end of the prompt.</li>
<li>5-minute TTL under continuous traffic; 1-hour only for 5–60 minute gaps.</li>
<li>Prompt above the model&rsquo;s minimum, which changes between models.</li>
<li>Dynamic context appended after the history, never templated into the system prompt.</li>
<li>Forks copy the parent&rsquo;s <code>system</code>, <code>tools</code> and <code>model</code> verbatim.</li>
<li>A test or monitor on the usage fields, checked on every change to prompt assembly.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Prompt caching is the rare optimisation with no quality tradeoff: identical output, roughly a tenth of the input cost, and lower latency as a bonus. It is also unusually fragile, because it hinges on byte-exact prefixes and fails completely silently. Treat the prompt-building path the way you would treat a cache key anywhere else in your system — deterministic, stable, and covered by a test — and it mostly takes care of itself.</p>]]></content:encoded>
      <category>AI Engineering</category>
      <category>Performance Optimization</category>
      <category>Backend Development</category>
    </item>
    <item>
      <title>Tool Use in Go: Building an Agent Loop You Can Actually Debug</title>
      <link>https://webdevstation.com/posts/tool-use-in-go-agent-loop/</link>
      <pubDate>Sat, 29 Aug 2026 10:10:00 +0200</pubDate>
      <author>Alex</author>
      <guid isPermaLink="true">https://webdevstation.com/posts/tool-use-in-go-agent-loop/</guid>
      <description>How LLM tool use really works in Go: the agentic loop, the SDK&#39;s tool runner, parallel tool calls, bounded concurrency, returning errors as tool results, and the…</description>
      <content:encoded><![CDATA[<p>&ldquo;Agent&rdquo; is doing a lot of work as a word right now. Strip the marketing off and what is underneath is a <code>for</code> loop: you send a message, the model asks you to run something, you run it, you send the result back, repeat until it stops asking. That is genuinely all it is — and once you have written the loop yourself, most of the mystique evaporates and what is left is a set of very ordinary Go problems.</p>
<h2 id="the-loop-in-full">The Loop, In Full</h2>
<p>Here is a complete manual loop. It is worth reading once even if you end up using the SDK&rsquo;s runner, because everything that goes wrong later is easier to diagnose when you know this shape. It assumes you already have a client and know how to read a response — if not, start with <a href="/posts/calling-claude-from-go/">calling an LLM from Go</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;encoding/json&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;log&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;github.com/anthropics/anthropic-sdk-go&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewClient</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">addTool</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ToolParam</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>:        <span style="color:#e6db74">&#34;add&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Description</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;Add two integers&#34;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">InputSchema</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ToolInputSchemaParam</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Properties</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;a&#34;</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>{<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;integer&#34;</span>},
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;b&#34;</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>{<span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;integer&#34;</span>},
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span> <span style="color:#f92672">:=</span> []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ToolUnionParam</span>{{<span style="color:#a6e22e">OfTool</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">addTool</span>}}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">messages</span> <span style="color:#f92672">:=</span> []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageParam</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewUserMessage</span>(<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewTextBlock</span>(<span style="color:#e6db74">&#34;What is 2 + 3?&#34;</span>)),
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageNewParams</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Model</span>:     <span style="color:#e6db74">&#34;claude-opus-5&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">16000</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Messages</span>:  <span style="color:#a6e22e">messages</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Tools</span>:     <span style="color:#a6e22e">tools</span>,
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Append the assistant turn BEFORE handling the tool calls.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">messages</span> = append(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">ToParam</span>())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">toolResults</span> []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ContentBlockParamUnion</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">block</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Content</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">variant</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">AsAny</span>().(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">TextBlock</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">variant</span>.<span style="color:#a6e22e">Text</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ToolUseBlock</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">in</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">A</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;a&#34;`</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">B</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;b&#34;`</span>
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// block.Input is raw JSON — parse it, never string-match it.</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Unmarshal</span>([]byte(<span style="color:#a6e22e">variant</span>.<span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">Input</span>.<span style="color:#a6e22e">Raw</span>()), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">in</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%d&#34;</span>, <span style="color:#a6e22e">in</span>.<span style="color:#a6e22e">A</span><span style="color:#f92672">+</span><span style="color:#a6e22e">in</span>.<span style="color:#a6e22e">B</span>)
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">toolResults</span> = append(<span style="color:#a6e22e">toolResults</span>,
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">result</span>, <span style="color:#66d9ef">false</span>))
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StopReason</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">StopReasonToolUse</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// All results from this turn go back in ONE user message.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">messages</span> = append(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewUserMessage</span>(<span style="color:#a6e22e">toolResults</span><span style="color:#f92672">...</span>))
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Five things in there are load-bearing, and four of them are easy to get subtly wrong.</p>
<p><strong><code>resp.ToParam()</code> converts the response into a history entry.</strong> You must append the assistant&rsquo;s turn — including its tool-call blocks — before you send the results, or the next request has results referring to a call that does not exist in the conversation.</p>
<p><strong>Parse the tool input; never pattern-match the raw string.</strong> <code>variant.JSON.Input.Raw()</code> gives you the JSON to unmarshal. Current models vary their JSON string escaping (Unicode escapes, escaped forward slashes), so anything doing <code>strings.Contains</code> on the serialised input is a bug waiting for a release.</p>
<p><strong>All tool results go back in a single user message.</strong> <code>anthropic.NewUserMessage</code> is variadic for exactly this reason. Splitting results across several messages technically works, and it quietly teaches the model to stop issuing parallel calls — which halves your throughput for no visible reason.</p>
<p><strong><code>StopReason</code> is the exit condition</strong>, not &ldquo;did I see any tool blocks&rdquo;. Check it after you have appended the results, not before.</p>
<p><strong>Every tool call needs a result.</strong> If the model asked for three tools and you return two results, the next request is malformed. Including for the one that failed — which brings us to the most useful trick in this whole article.</p>
<h2 id="errors-are-results-not-exceptions">Errors Are Results, Not Exceptions</h2>
<p>The instinct when a tool fails is to abort the loop. Usually that is wrong. Hand the failure back to the model as a tool result flagged as an error, and it will very often recover on its own — retry with a corrected argument, try a different tool, or tell the user what went wrong:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">out</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">variant</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// isError = true. The model sees the failure and can adapt.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">toolResults</span> = append(<span style="color:#a6e22e">toolResults</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>(), <span style="color:#66d9ef">true</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">toolResults</span> = append(<span style="color:#a6e22e">toolResults</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">out</span>, <span style="color:#66d9ef">false</span>))
</span></span></code></pre></div><p>That third parameter is <code>isError</code>. Getting this right turns a class of hard failures into self-correcting ones.</p>
<p>One caveat worth stating plainly: the error text goes into the model&rsquo;s context, so do not put a raw database error with connection strings and internal hostnames in there. Return the error you would show a careful external user. This is the same discipline as deciding what a sentinel error exposes at your HTTP boundary, which I covered in <a href="/posts/error-handling-in-go/">error handling in Go</a>.</p>
<h2 id="let-the-sdk-drive">Let the SDK Drive</h2>
<p>Once you understand the loop, you mostly do not want to maintain it. The Go SDK&rsquo;s tool runner handles the iteration, and generates the JSON schema from your struct tags:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;github.com/anthropics/anthropic-sdk-go/toolrunner&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GetWeatherInput</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">City</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;city&#34; jsonschema:&#34;required,description=The city name&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">weatherTool</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">toolrunner</span>.<span style="color:#a6e22e">NewBetaToolFromJSONSchema</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;get_weather&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Get current weather for a city&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">in</span> <span style="color:#a6e22e">GetWeatherInput</span>) (<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaToolResultBlockParamContentUnion</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaToolResultBlockParamContentUnion</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">OfText</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaTextBlockParam</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Text</span>: <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;The weather in %s is sunny, 22°C&#34;</span>, <span style="color:#a6e22e">in</span>.<span style="color:#a6e22e">City</span>),
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        }, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">runner</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Beta</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">NewToolRunner</span>(
</span></span><span style="display:flex;"><span>    []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaTool</span>{<span style="color:#a6e22e">weatherTool</span>},
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaToolRunnerParams</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">BetaMessageNewParams</span>: <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaMessageNewParams</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Model</span>:     <span style="color:#e6db74">&#34;claude-opus-5&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">16000</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Messages</span>: []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">BetaMessageParam</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewBetaUserMessage</span>(<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewBetaTextBlock</span>(<span style="color:#e6db74">&#34;What&#39;s the weather in Kyiv?&#34;</span>)),
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">MaxIterations</span>: <span style="color:#ae81ff">5</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">message</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">runner</span>.<span style="color:#a6e22e">RunToCompletion</span>(<span style="color:#a6e22e">ctx</span>)
</span></span></code></pre></div><p>Note the namespace: this lives under <code>client.Beta.Messages</code>, and the types are the <code>Beta*</code> variants — <code>BetaTextBlock</code>, not <code>TextBlock</code>. Mixing the two is the most common compile error here.</p>
<p><code>MaxIterations</code> is not optional decoration. Without a ceiling, a model that gets into a retry rut can loop until your context deadline, and you pay for every turn. Set it to the smallest number that lets legitimate work finish.</p>
<p>If you need to inspect or gate each step — approvals, audit logging, a check before a destructive tool runs — you do not have to drop back to a manual loop. The runner exposes <code>NextMessage()</code> and an <code>All()</code> iterator so you can step it and look at each message, and its <code>Params</code> field lets you adjust the next request. Reach for the manual loop only when you want control the runner genuinely does not expose.</p>
<h2 id="running-tools-concurrently--with-a-limit">Running Tools Concurrently — With a Limit</h2>
<p>When the model asks for four tools in one turn, running them sequentially wastes the whole point. But the naive concurrent version is the same mistake Go developers make everywhere else:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Don&#39;t. One turn can ask for many tools; this has no ceiling.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">call</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">calls</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">call</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Use a bounded group. The results still have to come back in one message, in a fixed order, so index into a preallocated slice rather than appending from goroutines:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;golang.org/x/sync/errgroup&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">results</span> <span style="color:#f92672">:=</span> make([]<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ContentBlockParamUnion</span>, len(<span style="color:#a6e22e">calls</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">g</span>, <span style="color:#a6e22e">gctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">errgroup</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">g</span>.<span style="color:#a6e22e">SetLimit</span>(<span style="color:#ae81ff">4</span>) <span style="color:#75715e">// whatever your slowest downstream can absorb</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">call</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">calls</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">g</span>.<span style="color:#a6e22e">Go</span>(<span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">out</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">gctx</span>, <span style="color:#a6e22e">call</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Not a group error: hand it to the model instead.</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">call</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>(), <span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewToolResultBlock</span>(<span style="color:#a6e22e">call</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">out</span>, <span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">g</span>.<span style="color:#a6e22e">Wait</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">messages</span> = append(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewUserMessage</span>(<span style="color:#a6e22e">results</span><span style="color:#f92672">...</span>))
</span></span></code></pre></div><p>Each goroutine writes one distinct slice element, so no mutex is needed — different elements are different memory. Appending to a shared slice from several goroutines is a different story, and so is writing to a shared map; <a href="/posts/concurrent-map-writing-and-reading-in-go/">concurrent map writing and reading in Go</a> has the failure mode in detail.</p>
<p>Notice that a tool failure returns <code>nil</code> from <code>g.Go</code>. Returning the error would cancel <code>gctx</code> and kill the sibling tool calls, when what you actually want is to report that one failure to the model and let the others finish. The full set of tradeoffs around <code>SetLimit</code> and error propagation is in <a href="/posts/worker-pools-in-go-with-errgroup/">worker pools in Go with errgroup</a>.</p>
<h2 id="designing-the-tools-themselves">Designing the Tools Themselves</h2>
<p>The loop is the easy part. Tool <em>design</em> is where agents get good or stay bad.</p>
<p><strong>The description is the API documentation, and its reader is the model.</strong> A tool called <code>search</code> described as &ldquo;searches&rdquo; will be called wrongly and often. Say what it searches, what it returns, and when <em>not</em> to use it. Most &ldquo;the agent keeps doing the wrong thing&rdquo; problems are description problems.</p>
<p><strong>Fewer, broader tools beat many narrow ones.</strong> Twenty tools that each wrap one endpoint force the model to plan a long chain and give it twenty chances to pick wrong. One <code>query_orders</code> tool with a few well-named parameters usually outperforms <code>get_order</code>, <code>list_orders_by_user</code>, <code>list_orders_by_date</code> and <code>count_orders</code>.</p>
<p><strong>Constrain the schema.</strong> Enums, required fields and explicit types are enforced before your handler runs. Every constraint you express in the schema is a class of invalid call you never have to validate by hand.</p>
<p><strong>Make results terse.</strong> Tool results occupy context on every subsequent turn of the loop. Returning a 400-row JSON dump costs you tokens on turn two, turn three and turn four. Return the fields the model needs to decide what to do next, and nothing else.</p>
<p><strong>Be deliberate about side effects.</strong> The model will call your tools in orders you did not anticipate. Anything that writes, sends, charges or deletes wants an approval gate — step the runner and confirm — or, at minimum, idempotency so a double call is harmless.</p>
<h2 id="guardrails-that-actually-matter-in-production">Guardrails That Actually Matter in Production</h2>
<p>A tool loop has a cost profile unlike a normal handler: every iteration resends the whole conversation. The bill grows quadratically with the number of turns if you are not careful, and three things keep it honest.</p>
<p><strong>Cap the iterations.</strong> <code>MaxIterations</code> on the runner, or a counter in your manual loop. Non-negotiable.</p>
<p><strong>Bound the wall clock.</strong> A deadline on the context that covers the whole loop, not each request:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#ae81ff">5</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span></code></pre></div><p><strong>Cache the prefix.</strong> Every turn resends the system prompt and the full history. Without prompt caching you pay full price for all of it, every iteration — this is where agent loops get expensive, and it is the one lever with no quality tradeoff at all. It gets <a href="/posts/prompt-caching-llm-cost/">its own article</a>.</p>
<p>Then there is the interaction between concurrency and rate limits. A pool of four tool calls, times however many concurrent user requests, times however many turns each — an agent loop is a very effective way to discover your own rate limits. The token-bucket approach from <a href="/posts/rate-limiting-go-apis/">rate limiting Go APIs</a> works just as well pointed at your own outbound calls as at inbound traffic.</p>
<h2 id="observability-or-you-are-flying-blind">Observability, Or You Are Flying Blind</h2>
<p>When a loop misbehaves, you need to see what the model actually saw. Log per iteration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Info</span>(<span style="color:#e6db74">&#34;agent turn&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;iteration&#34;</span>, <span style="color:#a6e22e">i</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;stop_reason&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StopReason</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;tools_called&#34;</span>, <span style="color:#a6e22e">toolNames</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;input_tokens&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">InputTokens</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;output_tokens&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">OutputTokens</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;cache_read&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">CacheReadInputTokens</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>With a request-scoped logger carrying the conversation id (<a href="/posts/structured-logging-in-go-with-slog/">the pattern from the slog post</a>), you can pull the entire trajectory of one run out of your logs — which is the difference between &ldquo;the agent is flaky&rdquo; and &ldquo;on turn three it called <code>search</code> with an empty query because the description was ambiguous&rdquo;.</p>
<h2 id="pitfalls">Pitfalls</h2>
<table>
	<thead>
			<tr>
					<th>Pitfall</th>
					<th>Fix</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Results returned for only some tool calls</td>
					<td>Return one result per <code>tool_use</code> block, failures included</td>
			</tr>
			<tr>
					<td>Tool results split across several user messages</td>
					<td>One user message, all results, variadic <code>NewUserMessage</code></td>
			</tr>
			<tr>
					<td>Assistant turn not appended before results</td>
					<td><code>messages = append(messages, resp.ToParam())</code> first</td>
			</tr>
			<tr>
					<td>String-matching the raw tool input</td>
					<td><code>json.Unmarshal(variant.JSON.Input.Raw())</code></td>
			</tr>
			<tr>
					<td>Mixing <code>TextBlock</code> and <code>BetaTextBlock</code></td>
					<td>Pick a namespace; the runner is <code>Beta.*</code> throughout</td>
			</tr>
			<tr>
					<td>Loop runs until the deadline</td>
					<td><code>MaxIterations</code>, plus a context timeout for the whole loop</td>
			</tr>
			<tr>
					<td>Tool error cancels its siblings</td>
					<td>Return <code>nil</code> from <code>g.Go</code>; hand the error to the model</td>
			</tr>
			<tr>
					<td>Cost grows faster than expected</td>
					<td>Cache the prefix; keep tool results terse</td>
			</tr>
	</tbody>
</table>
<h2 id="conclusion">Conclusion</h2>
<p>The loop is twenty lines and you should write it once by hand, then let the runner own it. After that, the work that actually improves an agent is not loop code at all: sharper tool descriptions, tighter schemas, terser results, a hard iteration cap, and enough logging to reconstruct a bad run. The interesting engineering is in the tools, not the loop around them.</p>]]></content:encoded>
      <category>AI Engineering</category>
      <category>Go Programming</category>
      <category>Backend Development</category>
    </item>
    <item>
      <title>Calling an LLM from Go: Streaming, Timeouts and the Parts That Bite</title>
      <link>https://webdevstation.com/posts/calling-claude-from-go/</link>
      <pubDate>Thu, 27 Aug 2026 09:40:00 +0200</pubDate>
      <author>Alex</author>
      <guid isPermaLink="true">https://webdevstation.com/posts/calling-claude-from-go/</guid>
      <description>A practical guide to wiring Claude into a Go service with the official SDK: content blocks, adaptive thinking, streaming, context deadlines, typed errors and the…</description>
      <content:encoded><![CDATA[<p>Most of the LLM tutorials I read are Python notebooks. That is fine for a prototype, but the moment the thing has to live inside a service — with timeouts, cancellation, retries and a bill attached — Go&rsquo;s constraints start to matter, and so do the details the notebooks skip. This is the write-up I wanted when I put my first Claude call into a Go HTTP handler.</p>
<h2 id="getting-a-client">Getting a Client</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go get github.com/anthropics/anthropic-sdk-go
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;github.com/anthropics/anthropic-sdk-go&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;github.com/anthropics/anthropic-sdk-go/option&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Reads ANTHROPIC_API_KEY from the environment.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewClient</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Or pass it explicitly, if you load config yourself.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewClient</span>(<span style="color:#a6e22e">option</span>.<span style="color:#a6e22e">WithAPIKey</span>(<span style="color:#a6e22e">key</span>))
</span></span></code></pre></div><p>Build the client <strong>once</strong>, at startup, and pass it around. It is safe for concurrent use and holds a connection pool — constructing one per request throws away keep-alive and gives you a fresh TLS handshake every time.</p>
<h2 id="the-first-call">The First Call</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageNewParams</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Model</span>:     <span style="color:#e6db74">&#34;claude-opus-5&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">16000</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Messages</span>: []<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageParam</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewUserMessage</span>(<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">NewTextBlock</span>(<span style="color:#e6db74">&#34;Summarise this changelog in three bullets.&#34;</span>)),
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;summarise changelog: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Two things about that snippet are worth slowing down on.</p>
<p><strong>The model is a plain string.</strong> The SDK ships typed constants (<code>anthropic.ModelClaudeOpus4_8</code> and friends), but <code>anthropic.Model</code> is an alias for <code>string</code>, so a model that has no constant yet is passed as its id. Either form compiles; check the SDK release notes before assuming a constant exists for the model you want.</p>
<p><strong><code>MaxTokens</code> is a ceiling, not a target.</strong> It is the point at which generation is cut off mid-sentence, and the model is not told about it. Setting it to <code>500</code> because you want a short answer does not produce a short answer — it produces a truncated one. Ask for brevity in the prompt and leave the ceiling generous. For non-streaming requests, something around <code>16000</code> keeps you clear of both truncation and the SDK&rsquo;s HTTP timeout.</p>
<h2 id="content-is-a-list-of-blocks-not-a-string">Content Is a List of Blocks, Not a String</h2>
<p>This is where the first hour usually goes. A response is not <code>resp.Text</code>. It is <code>resp.Content</code>, a slice of union values that can hold text, thinking, tool calls and more:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">block</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Content</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">variant</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">AsAny</span>().(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">TextBlock</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">variant</span>.<span style="color:#a6e22e">Text</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ThinkingBlock</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Reasoning, when you have asked for it to be shown.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Debug</span>(<span style="color:#e6db74">&#34;model reasoning&#34;</span>, <span style="color:#e6db74">&#34;text&#34;</span>, <span style="color:#a6e22e">variant</span>.<span style="color:#a6e22e">Thinking</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>block.AsAny()</code> is the accessor that gets you a concrete type to switch on. Reaching for <code>resp.Content[0].Text</code> and hoping works right up until the day a thinking block or a tool call lands in position zero, at which point you silently return an empty string. Write the type switch once, in a helper, and use it everywhere:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// firstText returns the first text block in a response, or &#34;&#34; if there is none.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">firstText</span>(<span style="color:#a6e22e">msg</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">Message</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">block</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">Content</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">block</span>.<span style="color:#a6e22e">AsAny</span>().(<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">TextBlock</span>); <span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Text</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="thinking-and-why-you-probably-want-it-on">Thinking, and Why You Probably Want It On</h2>
<p>Current Claude models can reason before answering. The recommended mode is <strong>adaptive</strong> — you do not budget tokens for it, the model decides how much thinking a given request deserves:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">adaptive</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ThinkingConfigAdaptiveParam</span>{}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">params</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageNewParams</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Model</span>:     <span style="color:#e6db74">&#34;claude-opus-5&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">16000</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Thinking</span>:  <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ThinkingConfigParamUnion</span>{<span style="color:#a6e22e">OfAdaptive</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">adaptive</span>},
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Messages</span>:  <span style="color:#a6e22e">messages</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>There is no <code>ThinkingConfigParamOfAdaptive</code> helper — you construct the union literal and take the address of the variant, as above. That trips people up because almost every other option in the SDK <em>does</em> have a constructor function.</p>
<p>A word of warning if you are carrying settings over from older code: the fixed thinking budget (<code>ThinkingConfigParamOfEnabled(N)</code>) is gone on current models and returns a 400 rather than being ignored. If you want to spend <em>less</em>, the lever is effort, not a token budget — and effort lives inside <code>output_config</code>, not at the top level of the request.</p>
<p>The counter-intuitive part: on the newest models, turning thinking <strong>off</strong> is not reliably a cost saving. Lower effort with thinking on generally beats thinking off, and disabling it has failure modes of its own. Leave it on and turn effort down.</p>
<h2 id="streaming">Streaming</h2>
<p>Anything a user waits for should stream. It is not just perceived speed — a long non-streaming request is also the easiest way to hit an HTTP timeout.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">stream</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">NewStreaming</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">MessageNewParams</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Model</span>:     <span style="color:#e6db74">&#34;claude-opus-5&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">64000</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Messages</span>:  <span style="color:#a6e22e">messages</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Next</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">event</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Current</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">ev</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">AsAny</span>().(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ContentBlockDeltaEvent</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">delta</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ev</span>.<span style="color:#a6e22e">Delta</span>.<span style="color:#a6e22e">AsAny</span>().(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">TextDelta</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Print</span>(<span style="color:#a6e22e">delta</span>.<span style="color:#a6e22e">Text</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Err</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;stream response: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Two nested type switches is not the prettiest Go you will write, but the shape is stable: outer switch on the event, inner switch on the delta.</p>
<p><strong>Always check <code>stream.Err()</code>.</strong> <code>stream.Next()</code> returning <code>false</code> means &ldquo;no more events&rdquo; — it does not tell you whether that was a clean finish or a dropped connection. A loop that ignores <code>Err()</code> will happily serve a truncated answer as if it were complete.</p>
<p>If you want the whole message <em>and</em> the incremental deltas, accumulate as you go. There is no <code>GetFinalMessage()</code> on the Go stream:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">stream</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">NewStreaming</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">params</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">message</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">Message</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Next</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">message</span>.<span style="color:#a6e22e">Accumulate</span>(<span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Current</span>())
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ... also forward the delta to the user here</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Err</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e">// message.Content is now the complete response.</span>
</span></span></code></pre></div><p>Raising <code>MaxTokens</code> to <code>64000</code> in the streaming example is deliberate. Timeouts stop being the binding constraint once you stream, so you can give the model room.</p>
<h3 id="streaming-to-a-browser">Streaming to a Browser</h3>
<p>Server-sent events are the path of least resistance, and Go&rsquo;s <code>http.ResponseController</code> makes the flushing straightforward:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">stream</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;Content-Type&#34;</span>, <span style="color:#e6db74">&#34;text/event-stream&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;Cache-Control&#34;</span>, <span style="color:#e6db74">&#34;no-cache&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;X-Accel-Buffering&#34;</span>, <span style="color:#e6db74">&#34;no&#34;</span>) <span style="color:#75715e">// stop nginx buffering the stream</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">rc</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">NewResponseController</span>(<span style="color:#a6e22e">w</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">stream</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">NewStreaming</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">params</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Next</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ev</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Current</span>().<span style="color:#a6e22e">AsAny</span>().(<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">ContentBlockDeltaEvent</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">delta</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ev</span>.<span style="color:#a6e22e">Delta</span>.<span style="color:#a6e22e">AsAny</span>().(<span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">TextDelta</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// SSE data frames must not contain raw newlines.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">payload</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">delta</span>.<span style="color:#a6e22e">Text</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;data: %s\n\n&#34;</span>, <span style="color:#a6e22e">payload</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#75715e">// client hung up</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rc</span>.<span style="color:#a6e22e">Flush</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Err</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;llm stream failed&#34;</span>, <span style="color:#e6db74">&#34;err&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprint</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;event: error\ndata: {}\n\n&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">rc</span>.<span style="color:#a6e22e">Flush</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprint</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;event: done\ndata: {}\n\n&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">rc</span>.<span style="color:#a6e22e">Flush</span>()
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Three details that cost me an afternoon each:</p>
<ul>
<li><strong><code>X-Accel-Buffering: no</code>.</strong> Without it nginx buffers your stream and delivers the whole thing at the end, which looks exactly like streaming being broken. (Nginx has strong opinions about proxied responses generally — I ran into a related set of them in <a href="/posts/how-to-make-nginx-cookie-aware/">making Nginx cache cookie aware</a>.)</li>
<li><strong>JSON-encode the delta.</strong> A model can and will emit a newline mid-sentence, and a bare newline terminates an SSE frame.</li>
<li><strong>Pass <code>r.Context()</code>, not <code>context.Background()</code>.</strong> When the user closes the tab, the request context cancels, the SDK aborts the HTTP call, and you stop paying for tokens nobody will read.</li>
</ul>
<h2 id="deadlines-and-cancellation">Deadlines and Cancellation</h2>
<p>Context is not decoration here. It is the only thing standing between a slow model call and a goroutine that lives forever:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#ae81ff">90</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">params</span>)
</span></span></code></pre></div><p>Set the deadline against how long the <em>work</em> should take, not a habit. A classification call has no business taking 90 seconds; a long agentic turn on a hard problem might legitimately run for several minutes. If you have not internalised how deadlines propagate through a call chain, <a href="/posts/understanding-golang-context/">understanding Golang context</a> covers the machinery.</p>
<p>The corollary at shutdown: an in-flight model call is exactly the kind of long request that a naive <code>SIGTERM</code> handler will sever. Drain it properly — see <a href="/posts/graceful-shutdown-in-go-web-services/">graceful shutdown in Go web services</a>.</p>
<h2 id="errors-worth-distinguishing">Errors Worth Distinguishing</h2>
<p>The SDK returns typed errors. Use <code>errors.As</code> to get at the status code rather than matching on message strings:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Messages</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">params</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">apiErr</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">Error</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">As</span>(<span style="color:#a6e22e">err</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">apiErr</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">apiErr</span>.<span style="color:#a6e22e">StatusCode</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusTooManyRequests</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;rate limited: %w&#34;</span>, <span style="color:#a6e22e">ErrRetryable</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusBadRequest</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Your request is malformed. Retrying will not help.</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;bad request to model: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;model call failed (%d): %w&#34;</span>, <span style="color:#a6e22e">apiErr</span>.<span style="color:#a6e22e">StatusCode</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Not an API error: context cancellation, DNS, TLS, connection reset.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;model call failed: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Wrapping with <code>%w</code> at each layer is what lets the HTTP boundary decide the status code without every layer needing to know about HTTP — the same pattern I laid out in <a href="/posts/error-handling-in-go/">error handling in Go</a>.</p>
<p><strong>The SDK already retries for you.</strong> By default it retries a couple of times on <code>408</code>, <code>409</code>, <code>429</code>, <code>5xx</code> and connection errors, with backoff. Two consequences people miss:</p>
<ol>
<li><strong>Do not add your own retry loop on top</strong> without lowering the SDK&rsquo;s. Three of yours around two of its is nine attempts and a long tail of latency.</li>
<li><strong>Wall-clock can reach <code>timeout × (attempts + 1)</code>.</strong> Your context deadline is the real budget — set it deliberately, because the retry behaviour will happily use all of it.</li>
</ol>
<h2 id="a-refusal-is-not-an-error">A Refusal Is Not an Error</h2>
<p>This one surprises people. If a safety classifier declines a request, you get <strong>HTTP 200</strong> — a perfectly successful response whose <code>StopReason</code> says the model declined:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StopReason</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">anthropic</span>.<span style="color:#a6e22e">StopReasonRefusal</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Warn</span>(<span style="color:#e6db74">&#34;model declined&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;category&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StopDetails</span>.<span style="color:#a6e22e">Category</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;explanation&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">StopDetails</span>.<span style="color:#a6e22e">Explanation</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">ErrDeclined</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>err</code> is nil. <code>resp.Content</code> may hold nothing useful. Code that goes straight from <code>if err != nil</code> to reading <code>Content[0]</code> treats this as an empty answer and moves on. Check <code>StopReason</code> before you read content — and note the other values you care about: <code>max_tokens</code> means you were truncated, and <code>tool_use</code> means the model is waiting on you (which is <a href="/posts/tool-use-in-go-agent-loop/">a whole article of its own</a>).</p>
<h2 id="watch-the-usage-numbers">Watch the Usage Numbers</h2>
<p>Every response carries a token accounting:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Info</span>(<span style="color:#e6db74">&#34;model call&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;input_tokens&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">InputTokens</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;output_tokens&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">OutputTokens</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;cache_read&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">CacheReadInputTokens</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;cache_write&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>.<span style="color:#a6e22e">CacheCreationInputTokens</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>Log these from day one. They are the only ground truth about what a feature costs, and <code>CacheReadInputTokens</code> in particular is how you find out that your prompt caching silently stopped working three deploys ago — the failure mode there is not an error, just a bigger invoice. That is <a href="/posts/prompt-caching-llm-cost/">its own post</a>, because it is the single biggest lever on what an LLM feature costs to run.</p>
<p>A structured logger pays for itself here: these are five numeric fields per call that you will want to aggregate later, which is exactly the case for <a href="/posts/structured-logging-in-go-with-slog/">structured logging with log/slog</a>.</p>
<h2 id="checklist">Checklist</h2>
<ul>
<li>One client, built at startup, shared across handlers.</li>
<li>Iterate <code>resp.Content</code> with a type switch; never index blindly into it.</li>
<li>Adaptive thinking on; tune cost with effort, not by disabling it.</li>
<li>Stream anything a human waits for, and always check <code>stream.Err()</code>.</li>
<li><code>r.Context()</code> all the way down, with a deliberate deadline.</li>
<li><code>errors.As</code> into <code>*anthropic.Error</code> for status-code branching.</li>
<li>Do not stack your retries on the SDK&rsquo;s.</li>
<li>Check <code>StopReason</code> before reading content — a refusal returns 200.</li>
<li>Log the usage fields from the first commit.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>The API surface is small — one endpoint, one loop, a handful of block types. What makes an LLM call different from any other HTTP call in your service is that it is slow, occasionally non-deterministic, priced per token, and able to succeed while declining to do what you asked. Go gives you good tools for exactly those problems, provided you use the context properly and read the response as the structured thing it is rather than the string you wish it were.</p>]]></content:encoded>
      <category>AI Engineering</category>
      <category>Go Programming</category>
      <category>Backend Development</category>
    </item>
    <item>
      <title>The Perfect Harmony: Enhancing Your Reading Experience with Music and BookTuning</title>
      <link>https://webdevstation.com/posts/enhancing-reading-experience-with-music-and-booktuning/</link>
      <pubDate>Sun, 08 Jun 2025 21:21:21 +0200</pubDate>
      <author>Alex</author>
      <guid isPermaLink="true">https://webdevstation.com/posts/enhancing-reading-experience-with-music-and-booktuning/</guid>
      <description>Discover how the right music can transform your reading experience and how BookTuning&#39;s AI-powered platform creates personalized playlists perfectly matched to any…</description>
      <content:encoded><![CDATA[<p>I read a lot, and for years I treated music as something that either helped or ruined the session with no pattern I could name. It turns out there is a pattern — and once you know it, you can pick a soundtrack that makes a book land harder instead of fighting it for attention.</p>
<h2 id="the-science-behind-music-and-reading">The Science Behind Music and Reading</h2>
<p>Reading with the right musical accompaniment can create a unique synergy. When chosen thoughtfully, background music can:</p>
<ul>
<li>Create an immersive atmosphere that complements your book&rsquo;s setting</li>
<li>Block out distracting environmental noises</li>
<li>Establish a consistent rhythm that helps maintain focus</li>
<li>Enhance emotional connections to characters and plot developments</li>
</ul>
<p>The key lies in finding music that complements rather than competes with your reading material. This is where specialized tools like BookTuning enter the picture.</p>
<h2 id="booktuning-an-ai-powered-reading-companion">BookTuning: An AI-Powered Reading Companion</h2>
<p><a href="https://booktun.ing">BookTuning</a> approaches the music-reading relationship with technological sophistication. This innovative platform uses artificial intelligence to create personalized music playlists specifically designed to enhance your reading experience.</p>
<h3 id="how-it-works">How It Works</h3>
<p>BookTuning&rsquo;s approach is three-pronged:</p>
<ol>
<li>
<p><strong>Mood-Matched Music</strong>: Their AI analyzes the mood and atmosphere of your chosen book to find musical accompaniment that enhances those specific elements.</p>
</li>
<li>
<p><strong>Genre-Specific Selection</strong>: The platform considers your book&rsquo;s genre—whether fantasy, romance, thriller, or sci-fi—and curates tracks that complement the thematic elements unique to that genre.</p>
</li>
<li>
<p><strong>AI-Generated Descriptions</strong>: Each playlist includes a personalized explanation of why the selected music pairs well with your book, adding a thoughtful layer to the experience.</p>
</li>
</ol>
<h3 id="customization-options">Customization Options</h3>
<p>What impressed me most about BookTuning was the level of personalization available. Users can:</p>
<ul>
<li>Adjust how diverse and unique they want their song selection to be</li>
<li>Opt to discover new tracks rather than familiar ones</li>
<li>Optimize music selection based on their specific reading environment</li>
</ul>
<p>The playlists seamlessly integrate with Spotify, making the transition from selection to listening effortless.</p>
<h2 id="the-personal-experience">The Personal Experience</h2>
<p>As someone who has long struggled with finding the right musical backdrop for different types of books, BookTuning addresses a genuine need. Historical fiction suddenly becomes more immersive with period-appropriate instrumentals, while science fiction takes on new dimensions with ambient electronic compositions.</p>
<p>The platform&rsquo;s ability to match subtle emotional undertones in literature with corresponding musical elements demonstrates a sophisticated understanding of both mediums. During my testing, the AI consistently provided thoughtful pairings that enhanced rather than distracted from the reading experience.</p>
<h2 id="is-booktuning-worth-it">Is BookTuning Worth It?</h2>
<p>For readers who already enjoy pairing music with books but find themselves spending too much time curating playlists, BookTuning offers a valuable service. It removes the friction between wanting that perfect soundtrack and actually getting to your reading.</p>
<p>The AI-powered approach proves particularly helpful when exploring unfamiliar genres or books with complex emotional landscapes. Rather than interrupting your reading flow to adjust your playlist, BookTuning creates a seamless audio environment tailored to your literary journey.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The partnership between literature and music is deeply personal, yet BookTuning has found a way to enhance this relationship through thoughtful technology. By creating customized soundscapes that complement rather than compete with your reading material, it offers a new dimension to how we experience books.</p>
<p>Whether you&rsquo;re a longtime practitioner of reading with musical accompaniment or curious to try this approach for the first time, BookTuning provides an accessible entry point that respects both the power of music and the sanctity of the reading experience.</p>
<p>If you enjoy this kind of small-tool write-up, I also collected the fastest ways I have found to <a href="/posts/aneasywaytogenerateqrcodefast/">generate QR codes</a>.</p>
<hr>
<p><em>Have you tried reading with musical accompaniment? What have been your experiences with tools like BookTuning? Share your thoughts in the comments below!</em></p>]]></content:encoded>
      <category>Tools</category>
    </item>
  </channel>
</rss>
