<?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>Oss-Contribution on Krishnatejaswi S</title>
    <link>https://KTS-o7.github.io/bear/tags/oss-contribution/</link>
    <description>Recent content in Oss-Contribution on Krishnatejaswi S</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</managingEditor>
    <webMaster>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</webMaster>
    <copyright>Krishnatejaswi S</copyright>
    <lastBuildDate>Thu, 16 Apr 2026 00:00:00 +0530</lastBuildDate>
    <atom:link href="https://KTS-o7.github.io/bear/tags/oss-contribution/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Fixing Silent $0 Streaming Costs in AgentBudget: An OSS Contribution</title>
      <link>https://KTS-o7.github.io/bear/notes/agentbudget_ulimit_for_ai_agents/</link>
      <pubDate>Thu, 16 Apr 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/notes/agentbudget_ulimit_for_ai_agents/</guid>
      <description>&lt;p&gt;AgentBudget patches the OpenAI and Anthropic SDKs to track the cost of every LLM call. Set a budget, get a hard limit. Simple premise.&lt;/p&gt;&#xA;&lt;p&gt;Except there was a gap. Every streaming call cost $0.00 in the ledger. Silently. No error, no warning &amp;ndash; just wrong numbers.&lt;/p&gt;&#xA;&lt;p&gt;I found the gap, fixed it, and got the PR merged. This is the story of how streaming cost tracking actually works.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-agentbudget-does&#34;&gt;What AgentBudget does&lt;/h2&gt;&#xA;&lt;p&gt;AgentBudget is an open-source SDK that puts a hard dollar limit on AI agent sessions. Two lines, no infrastructure:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>AgentBudget patches the OpenAI and Anthropic SDKs to track the cost of every LLM call. Set a budget, get a hard limit. Simple premise.</p>
<p>Except there was a gap. Every streaming call cost $0.00 in the ledger. Silently. No error, no warning &ndash; just wrong numbers.</p>
<p>I found the gap, fixed it, and got the PR merged. This is the story of how streaming cost tracking actually works.</p>
<h2 id="what-agentbudget-does">What AgentBudget does</h2>
<p>AgentBudget is an open-source SDK that puts a hard dollar limit on AI agent sessions. Two lines, no infrastructure:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">import</span> <span class="nn">agentbudget</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">agentbudget</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="s2">&#34;$5.00&#34;</span><span class="p">)</span></span></span></code></pre></div><p>It works by monkey-patching <code>Completions.create</code> (OpenAI) and <code>Messages.create</code> (Anthropic) at init time. Every call goes through a wrapper that looks up the model cost, accumulates spend, and raises <code>BudgetExhausted</code> when the limit is hit.</p>
<p>The non-streaming path worked fine. The streaming path did not.</p>
<h2 id="the-bug">The bug</h2>
<p>When <code>stream=True</code>, the patched method receives a <code>Stream</code> object back from the SDK &ndash; not a completed response with token counts. The original code had no handling for this case. It silently passed the stream through untracked.</p>
<p>Every streaming call registered as $0.00 in the ledger. Any agent using streaming could blow past its budget with zero resistance. The circuit breaker was blind to half its traffic.</p>
<h2 id="why-streaming-is-harder-to-cost">Why streaming is harder to cost</h2>
<p>Non-streaming is straightforward: the response comes back with a <code>usage</code> object containing <code>prompt_tokens</code> and <code>completion_tokens</code>. Multiply by the per-token rate, done.</p>
<p>Streaming returns a sequence of chunks. Token counts are not available upfront &ndash; they only appear after the stream is exhausted. For OpenAI specifically, usage data on the final chunk is opt-in: you need <code>stream_options={&quot;include_usage&quot;: True}</code> in the request, otherwise the final chunk carries no usage data and there is nothing to cost.</p>
<p>Anthropic handles it differently. Usage data is always present, split across two events: <code>message_start</code> carries input tokens, <code>message_delta</code> carries output tokens at the end.</p>
<h2 id="the-fix-transparent-wrapper-classes">The fix: transparent wrapper classes</h2>
<p>The solution is four wrapper classes that intercept the stream, yield every chunk unchanged, and record cost after the iterator is exhausted:</p>
<table>
  <thead>
      <tr>
          <th>Class</th>
          <th>Wraps</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>_OpenAIStreamWrapper</code></td>
          <td><code>openai.Stream[ChatCompletionChunk]</code></td>
      </tr>
      <tr>
          <td><code>_AsyncOpenAIStreamWrapper</code></td>
          <td><code>openai.AsyncStream[ChatCompletionChunk]</code></td>
      </tr>
      <tr>
          <td><code>_AnthropicStreamWrapper</code></td>
          <td><code>anthropic.Stream[RawMessageStreamEvent]</code></td>
      </tr>
      <tr>
          <td><code>_AsyncAnthropicStreamWrapper</code></td>
          <td><code>anthropic.AsyncStream[RawMessageStreamEvent]</code></td>
      </tr>
  </tbody>
</table>
<p>Each wrapper is fully transparent to the caller. The existing code pattern &ndash; <code>for chunk in stream:</code> or <code>async for chunk in stream:</code> or <code>with client.stream(...) as stream:</code> &ndash; continues to work without any changes.</p>
<p>The wrapper accumulates token counts as chunks arrive, then records the cost in the session ledger once the stream ends.</p>
<p>For OpenAI:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">_OpenAIStreamWrapper</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">stream</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">model</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span> <span class="o">=</span> <span class="n">stream</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_session</span> <span class="o">=</span> <span class="n">session</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_model</span> <span class="o">=</span> <span class="n">model</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">def</span> <span class="fm">__iter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="c1"># Track usage from the final chunk</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">if</span> <span class="n">chunk</span><span class="o">.</span><span class="n">usage</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">                <span class="n">cost</span> <span class="o">=</span> <span class="n">price</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_model</span><span class="p">,</span> <span class="n">chunk</span><span class="o">.</span><span class="n">usage</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">_session</span><span class="o">.</span><span class="n">record</span><span class="p">(</span><span class="n">cost</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="k">yield</span> <span class="n">chunk</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1"># Full protocol: __enter__, __exit__, close()</span></span></span></code></pre></div><p>For Anthropic, the accumulation spans two event types:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">_AnthropicStreamWrapper</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="fm">__iter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">input_tokens</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">output_tokens</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">for</span> <span class="n">event</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="k">if</span> <span class="n">event</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s2">&#34;message_start&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                <span class="n">input_tokens</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">usage</span><span class="o">.</span><span class="n">input_tokens</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">elif</span> <span class="n">event</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s2">&#34;message_delta&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                <span class="n">output_tokens</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">usage</span><span class="o">.</span><span class="n">output_tokens</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">yield</span> <span class="n">event</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">cost</span> <span class="o">=</span> <span class="n">price</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_model</span><span class="p">,</span> <span class="n">input_tokens</span><span class="p">,</span> <span class="n">output_tokens</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_session</span><span class="o">.</span><span class="n">record</span><span class="p">(</span><span class="n">cost</span><span class="p">)</span></span></span></code></pre></div><p>The detection of whether a response is a stream happens via <code>isinstance</code> checks guarded by <code>ImportError</code>, so the wrapper degrades gracefully if only one SDK is installed.</p>
<h2 id="implementing-the-full-protocol">Implementing the full protocol</h2>
<p>The tricky part is not the cost tracking logic &ndash; it is making the wrapper implement the full stream protocol correctly.</p>
<p>Python streams support multiple usage patterns: iterator (<code>for chunk in stream</code>), context manager (<code>with stream</code>), async iterator, async context manager, and explicit <code>close()</code>/<code>aclose()</code>. Missing any of these means certain callers will break with an <code>AttributeError</code> or a resource leak.</p>
<p>The wrapper needs to implement all of them:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Sync: iterator + context manager + close</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="fm">__iter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="o">...</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="fm">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="fm">__exit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">def</span> <span class="nf">close</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># Async: async iterator + async context manager + aclose</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="fm">__aiter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="o">...</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="fm">__aenter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="fm">__aexit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">):</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">aclose</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">aclose</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="o">.</span><span class="n">aclose</span><span class="p">()</span></span></span></code></pre></div><p>Getting this wrong is invisible in unit tests with mocks but breaks immediately against real SDK objects.</p>
<h2 id="tests">Tests</h2>
<p>The PR added 19 tests covering:</p>
<ul>
<li>Cost recorded correctly after stream exhaustion</li>
<li>Chunks with no usage data skipped without error</li>
<li>Passthrough identity &ndash; every chunk the caller sees is unchanged</li>
<li><code>BudgetExhausted</code> propagates correctly mid-stream</li>
<li>Both <code>for chunk in stream:</code> and <code>with client.stream() as stream:</code> patterns</li>
<li><code>close()</code> and <code>aclose()</code> delegation to the underlying stream</li>
<li>Drop-in mode (<code>agentbudget.init()</code>) wrapping end-to-end</li>
<li>No-session noop &ndash; if no budget session is active, streaming is fully transparent</li>
</ul>
<p>All 129 pre-existing tests continued to pass. 148 total passing at merge.</p>
<h2 id="the-known-limitation">The known limitation</h2>
<p>OpenAI streaming requires the caller to pass <code>stream_options={&quot;include_usage&quot;: True}</code>. Without it, the final chunk carries no usage data, and the cost is silently $0.</p>
<p>This is OpenAI&rsquo;s API behaviour &ndash; usage data on streams is opt-in. The limitation is documented in the module docstring. A future improvement could auto-inject the option at the patch layer, but that requires modifying the outbound request, which is a bigger change than this PR intended to be.</p>
<p>Anthropic has no equivalent limitation &ndash; usage is always present.</p>
<h2 id="what-i-learned">What I learned</h2>
<p><strong>Transparent wrappers need the full protocol.</strong> It is tempting to implement just <code>__iter__</code> and call it done. But real callers use context managers, explicit close, and async variants. Implementing the minimum breaks production code in ways that are hard to debug.</p>
<p><strong>Streaming costs are provider-specific.</strong> OpenAI and Anthropic have different conventions for where usage data appears in the stream. Any SDK that claims to support both needs to handle both separately.</p>
<p><strong>Test against real SDK objects, not just mocks.</strong> The <code>isinstance</code> detection and chunk structure only behave correctly against live SDK classes. Mock-based tests passed for the wrong reasons until I verified against an actual installed SDK.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://github.com/AgentBudget/agentbudget/pull/6">PR #6 &ndash; feat: track costs for streaming OpenAI and Anthropic responses</a></li>
<li><a href="https://github.com/AgentBudget/agentbudget">AgentBudget on GitHub</a></li>
<li><a href="https://pypi.org/project/agentbudget/">PyPI</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Fixing Parallel Tool Call Streaming in Bifrost: An OSS Bug Hunt</title>
      <link>https://KTS-o7.github.io/bear/notes/bifrost_parallel_tool_call_fix/</link>
      <pubDate>Mon, 02 Mar 2026 00:00:00 +0000</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/notes/bifrost_parallel_tool_call_fix/</guid>
      <description>&lt;p&gt;An AI agent calls three tools in parallel. The LLM streams back interleaved deltas — a few bytes of &lt;code&gt;search_documents&lt;/code&gt; arguments, then a few bytes of &lt;code&gt;get_user&lt;/code&gt;, then more &lt;code&gt;search_documents&lt;/code&gt;. On the other end, the gateway reassembles these fragments into complete tool calls.&lt;/p&gt;&#xA;&lt;p&gt;Except it doesn&amp;rsquo;t. It concatenates them all into one call: &lt;code&gt;search_documentsget_usersearch_documents&lt;/code&gt;. The agent hallucinates, retries, and loops. I stared at the telemetry for twenty minutes before I realized the bug wasn&amp;rsquo;t in the agent — it was in the gateway&amp;rsquo;s streaming accumulator.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>An AI agent calls three tools in parallel. The LLM streams back interleaved deltas — a few bytes of <code>search_documents</code> arguments, then a few bytes of <code>get_user</code>, then more <code>search_documents</code>. On the other end, the gateway reassembles these fragments into complete tool calls.</p>
<p>Except it doesn&rsquo;t. It concatenates them all into one call: <code>search_documentsget_usersearch_documents</code>. The agent hallucinates, retries, and loops. I stared at the telemetry for twenty minutes before I realized the bug wasn&rsquo;t in the agent — it was in the gateway&rsquo;s streaming accumulator.</p>
<p>This is the story of how I found and fixed a parallel tool call merging bug in <a href="https://github.com/maximhq/bifrost">Bifrost</a>, a 2.6k-star open-source AI gateway.</p>
<h2 id="what-is-bifrost">What is Bifrost?</h2>
<p>Bifrost is a high-performance AI gateway written in Go. It sits between your application and LLM providers (OpenAI, Anthropic, Bedrock, Vertex, etc.), providing a unified OpenAI-compatible API with automatic failover, load balancing, semantic caching, and streaming support. Think of it as an intelligent reverse proxy for AI — one API, fifteen providers, ~11µs overhead at 5k RPS.</p>
<p>The relevant piece here is the <strong>streaming accumulator</strong>. When you make a streaming chat completion request, the LLM sends back a sequence of Server-Sent Events (SSE), each containing a small delta — a few tokens of text, or a fragment of a tool call&rsquo;s arguments. The accumulator&rsquo;s job is to reassemble these deltas into complete messages.</p>
<h2 id="the-bug-recency-based-routing">The bug: recency-based routing</h2>
<p>Here&rsquo;s the scenario. You ask Claude or GPT to call two tools in parallel:</p>





<pre tabindex="0"><code>User: &#34;Search for compliance circulars about RBI and also get the user&#39;s profile&#34;</code></pre><p>The model decides to call both <code>search_circulars</code> and <code>get_user_profile</code> simultaneously. During streaming, the provider sends interleaved deltas:</p>





<pre tabindex="0"><code>Delta 1: tool_call[0] name=&#34;search&#34; (index=0, id=call_abc)
Delta 2: tool_call[1] name=&#34;get_us&#34; (index=1, id=call_xyz)
Delta 3: tool_call[0] args=&#39;{&#34;query&#34;: &#34;RBI&#39;   (index=0)
Delta 4: tool_call[1] args=&#39;{&#34;user_id&#34;: &#34;u&#39;   (index=1)
Delta 5: tool_call[0] args=&#39; circulars&#34;}&#39;      (index=0)
Delta 6: tool_call[1] args=&#39;123&#34;}&#39;             (index=1)</code></pre><p>Each delta carries an <code>index</code> (and optionally an <code>id</code>) that identifies which tool call it belongs to. The expected behavior: two distinct tool calls with their own names and arguments.</p>
<p>The actual behavior in Bifrost&rsquo;s accumulator before the fix:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Old logic (simplified)</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">Function</span><span class="p">.</span><span class="nx">Name</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="c1">// New tool call — create it</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nx">existingToolCalls</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">existingToolCalls</span><span class="p">,</span><span class="w"> </span><span class="nx">newToolCall</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="c1">// Argument delta — append to the LAST tool call</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nx">toolCallToModify</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">existingToolCalls</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="nx">existingToolCalls</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">    </span><span class="nx">toolCallToModify</span><span class="p">.</span><span class="nx">Function</span><span class="p">.</span><span class="nx">Arguments</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">Function</span><span class="p">.</span><span class="nx">Arguments</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>The routing logic was based on <strong>recency</strong>: if a delta has a <code>Name</code>, it&rsquo;s a new tool call; otherwise, append arguments to the most recent one. This works perfectly when tool calls arrive sequentially. It breaks completely when they&rsquo;re interleaved.</p>
<p>With the deltas above:</p>
<ul>
<li>Delta 3 (index=0 args) arrives → appended to the <strong>last</strong> tool call, which is <code>get_user_profile</code> (index=1). Wrong target.</li>
<li>Delta 5 (index=0 args) arrives → again appended to <code>get_user_profile</code>. Now <code>get_user_profile</code> has arguments from two different calls mashed together.</li>
</ul>
<p>The result: concatenated tool names, merged arguments, and an agent stuck in a retry loop trying to invoke <code>search_circularsget_user_profile</code> — a tool that doesn&rsquo;t exist.</p>
<h2 id="finding-the-bug">Finding the bug</h2>
<p>I hit this in production. Our compliance platform uses an AI agent that calls multiple tools in parallel — searching circulars, fetching regulatory data, looking up user context. The agent kept failing with tool resolution errors, and the telemetry showed impossible tool names:</p>





<pre tabindex="0"><code>search_circularsearch_circularsearch_circular</code></pre><p>At first I suspected the agent&rsquo;s tool-calling logic. Then I looked at the raw SSE stream from the provider — the deltas were correct. Each one had the right <code>index</code> and <code>id</code>. The corruption was happening between the provider response and the final accumulated message.</p>
<p>I traced it to <code>accumulateToolCallsInMessage</code> in <code>framework/streaming/accumulator.go</code>. The function had no concept of tool call identity. It used the presence of a <code>Name</code> field as a heuristic for &ldquo;new vs. continuation,&rdquo; which is fragile and incorrect when deltas arrive out of order.</p>
<h2 id="the-fix">The fix</h2>
<p>The fix is conceptually simple: <strong>route deltas by identity, not by recency.</strong></p>
<p>Each tool call delta carries two potential identifiers:</p>
<ol>
<li><code>tool_call.id</code> — a string like <code>call_abc123</code> (set by OpenAI/Anthropic)</li>
<li><code>tool_call.index</code> — an integer position (0, 1, 2, &hellip;)</li>
</ol>
<p>The new logic builds lookup maps on existing tool calls and uses them to route each incoming delta to the correct target:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Build lookup maps for identity-based routing</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">idToIndex</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">existingToolCalls</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">indexToIndex</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">uint16</span><span class="p">]</span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">existingToolCalls</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">toolCall</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">existingToolCalls</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">toolCall</span><span class="p">.</span><span class="nx">ID</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">*</span><span class="nx">toolCall</span><span class="p">.</span><span class="nx">ID</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">        </span><span class="nx">idToIndex</span><span class="p">[</span><span class="o">*</span><span class="nx">toolCall</span><span class="p">.</span><span class="nx">ID</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">i</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">    </span><span class="nx">indexToIndex</span><span class="p">[</span><span class="nx">toolCall</span><span class="p">.</span><span class="nx">Index</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">i</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>For each incoming delta, routing follows a priority chain:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">deltaToolCall</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">deltaToolCalls</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="nx">index</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="c1">// Priority 1: match by tool_call.id</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">ID</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">*</span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">ID</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">existingIndex</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">idToIndex</span><span class="p">[</span><span class="o">*</span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">ID</span><span class="p">];</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">            </span><span class="nx">index</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">existingIndex</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="c1">// Priority 2: match by tool_call.index</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">index</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">existingIndex</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">indexToIndex</span><span class="p">[</span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">Index</span><span class="p">];</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">            </span><span class="nx">index</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">existingIndex</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="c1">// Priority 3: no match → create new tool call (if name present)</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">index</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">Function</span><span class="p">.</span><span class="nx">Name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">            </span><span class="c1">// Can&#39;t create a tool call without a name</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">            </span><span class="k">continue</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">        </span><span class="nx">newToolCall</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">schemas</span><span class="p">.</span><span class="nx">ChatAssistantMessageToolCall</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">            </span><span class="nx">Index</span><span class="p">:</span><span class="w">    </span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">Index</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">            </span><span class="nx">ID</span><span class="p">:</span><span class="w">       </span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">            </span><span class="nx">Function</span><span class="p">:</span><span class="w"> </span><span class="cm">/* ... */</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">        </span><span class="nx">existingToolCalls</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">existingToolCalls</span><span class="p">,</span><span class="w"> </span><span class="nx">newToolCall</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">        </span><span class="c1">// Update maps for future deltas</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">        </span><span class="nx">indexToIndex</span><span class="p">[</span><span class="nx">newToolCall</span><span class="p">.</span><span class="nx">Index</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">existingToolCalls</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">newToolCall</span><span class="p">.</span><span class="nx">ID</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">            </span><span class="nx">idToIndex</span><span class="p">[</span><span class="o">*</span><span class="nx">newToolCall</span><span class="p">.</span><span class="nx">ID</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">existingToolCalls</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">        </span><span class="k">continue</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w">    </span><span class="c1">// Append arguments to the correct tool call</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">    </span><span class="nx">existingToolCalls</span><span class="p">[</span><span class="nx">index</span><span class="p">].</span><span class="nx">Function</span><span class="p">.</span><span class="nx">Arguments</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nx">deltaToolCall</span><span class="p">.</span><span class="nx">Function</span><span class="p">.</span><span class="nx">Arguments</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>The key insight is that the maps are updated as new tool calls are created, so subsequent deltas for the same call always find their target — even if they arrive many chunks later.</p>
<h2 id="the-same-bug-twice">The same bug, twice</h2>
<p>During code review, a reviewer (<a href="https://github.com/Pratham-Mishra04">Pratham-Mishra04</a>) pointed out that the same recency-based routing existed in a second code path: the <strong>Responses API streaming</strong> handler in <code>responses.go</code>.</p>
<p>The Responses API is OpenAI&rsquo;s newer streaming format (the <code>/v1/responses</code> endpoint). It uses <code>ItemID</code> instead of <code>tool_call.index</code>, but the accumulation logic had the same flaw — <code>FunctionCallArgumentsDelta</code> events were always appended to the last message:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Old responses.go logic</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">Delta</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">messages</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="c1">// Always targets the last message — wrong for parallel calls</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nx">a</span><span class="p">.</span><span class="nf">appendFunctionArgumentsDeltaToResponsesMessage</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">        </span><span class="o">&amp;</span><span class="nx">messages</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="nx">messages</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span><span class="w"> </span><span class="o">*</span><span class="nx">resp</span><span class="p">.</span><span class="nx">Delta</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>The fix follows the same pattern: search for a message matching <code>resp.ItemID</code>, and only fall back to the last message when no <code>ItemID</code> is present. If an <code>ItemID</code> exists but no message matches, create a stub message to hold the delta rather than merging it into the wrong call:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">Delta</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">messages</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="nx">targetIdx</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">messages</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">resp</span><span class="p">.</span><span class="nx">ItemID</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">        </span><span class="nx">targetIdx</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">messages</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">--</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="nx">messages</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">ID</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">*</span><span class="nx">messages</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">ID</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">*</span><span class="nx">resp</span><span class="p">.</span><span class="nx">ItemID</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">                </span><span class="nx">targetIdx</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">i</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">                </span><span class="k">break</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">targetIdx</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">            </span><span class="c1">// Create stub to avoid merging into wrong message</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">            </span><span class="nx">id</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">*</span><span class="nx">resp</span><span class="p">.</span><span class="nx">ItemID</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">            </span><span class="nx">messages</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">messages</span><span class="p">,</span><span class="w"> </span><span class="nx">schemas</span><span class="p">.</span><span class="nx">ResponsesMessage</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">id</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">            </span><span class="nx">targetIdx</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">messages</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nx">a</span><span class="p">.</span><span class="nf">appendFunctionArgumentsDeltaToResponsesMessage</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">        </span><span class="o">&amp;</span><span class="nx">messages</span><span class="p">[</span><span class="nx">targetIdx</span><span class="p">],</span><span class="w"> </span><span class="o">*</span><span class="nx">resp</span><span class="p">.</span><span class="nx">Delta</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="testing-parallel-accumulation">Testing parallel accumulation</h2>
<p>The regression test simulates the exact interleaving pattern that triggers the bug:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">TestAccumulateToolCallsInterleavedParallel</span><span class="p">(</span><span class="nx">t</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="c1">// ...</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nx">toolCallID0</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;call_0&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nx">toolCallID1</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;call_1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nx">toolNameAdd</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;add&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nx">toolNameMultiply</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;multiply&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="c1">// Simulate interleaved deltas</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nf">accumulate</span><span class="p">(</span><span class="nf">makeDelta</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">toolCallID0</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">toolNameAdd</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">))</span><span class="w">       </span><span class="c1">// create call 0</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nf">accumulate</span><span class="p">(</span><span class="nf">makeDelta</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">toolCallID1</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">toolNameMultiply</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">))</span><span class="w">  </span><span class="c1">// create call 1</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nf">accumulate</span><span class="p">(</span><span class="nf">makeDelta</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="s">`{&#34;a&#34;: 1`</span><span class="p">))</span><span class="w">                  </span><span class="c1">// args for call 0</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nf">accumulate</span><span class="p">(</span><span class="nf">makeDelta</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="s">`{&#34;a&#34;: 2`</span><span class="p">))</span><span class="w">                  </span><span class="c1">// args for call 1</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nf">accumulate</span><span class="p">(</span><span class="nf">makeDelta</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="s">`, &#34;b&#34;: 3}`</span><span class="p">))</span><span class="w">                </span><span class="c1">// more args for call 0</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nf">accumulate</span><span class="p">(</span><span class="nf">makeDelta</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="s">`, &#34;b&#34;: 4}`</span><span class="p">))</span><span class="w">                </span><span class="c1">// more args for call 1</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="c1">// Assert: two distinct tool calls with correct arguments</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="nf">assert</span><span class="p">(</span><span class="nx">addCall</span><span class="p">.</span><span class="nx">Arguments</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">`{&#34;a&#34;: 1, &#34;b&#34;: 3}`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nf">assert</span><span class="p">(</span><span class="nx">multiplyCall</span><span class="p">.</span><span class="nx">Arguments</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">`{&#34;a&#34;: 2, &#34;b&#34;: 4}`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Without the fix, <code>add</code> would end up with <code>multiply</code>&rsquo;s arguments concatenated in. With the fix, each call accumulates only its own deltas.</p>
<p>A second test covers the Responses API path with <code>ItemID</code>-based routing, using the same interleaving pattern but with <code>OutputItemAdded</code> + <code>FunctionCallArgumentsDelta</code> events.</p>
<h2 id="the-pr-lifecycle">The PR lifecycle</h2>
<p>I filed <a href="https://github.com/maximhq/bifrost/issues/1829">Issue #1829</a> with reproduction details from production telemetry, then opened <a href="https://github.com/maximhq/bifrost/pull/1830">PR #1830</a> with the fix and tests.</p>
<p>The review process was smooth:</p>
<ol>
<li>First commit: fix the Chat Completions accumulator + regression test</li>
<li>Reviewer feedback: same bug exists in Responses API — please fix there too + add changelog entries</li>
<li>Second commit: fix Responses API routing + regression test + changelogs</li>
<li>Merged by <a href="https://github.com/akshaydeo">akshaydeo</a> (Bifrost maintainer)</li>
</ol>
<p>Total: 3 commits, ~280 lines of Go across 5 files. Same-day turnaround from issue to merge.</p>
<h2 id="what-i-learned">What I learned</h2>
<h3 id="1-recency-is-not-identity">1. Recency is not identity</h3>
<p>The original code used position (last element) as a proxy for identity. This is a common pattern in sequential streaming — and it works, until the stream isn&rsquo;t sequential. The fix is to use the actual identity field that the protocol already provides.</p>
<p>This is a general principle: <strong>if the protocol gives you an ID field, use it.</strong> Don&rsquo;t rely on ordering assumptions that hold today but break when concurrency enters.</p>
<h3 id="2-the-same-bug-pattern-repeats-across-api-surfaces">2. The same bug pattern repeats across API surfaces</h3>
<p>Bifrost supports two streaming formats: Chat Completions (the classic <code>tool_calls</code> array) and Responses API (the newer <code>ItemID</code>-based format). Both had the same routing flaw because the accumulation logic was duplicated rather than shared. The reviewer catching this in a different file was the most valuable part of the review.</p>
<h3 id="3-oss-contributions-are-about-context-not-complexity">3. OSS contributions are about context, not complexity</h3>
<p>The fix itself is straightforward — build a map, use it for lookup. The hard part was:</p>
<ul>
<li><strong>Finding</strong> the bug in a codebase I&rsquo;d never seen before</li>
<li><strong>Understanding</strong> why the existing approach was wrong</li>
<li><strong>Testing</strong> with realistic interleaved deltas</li>
<li><strong>Responding</strong> to review feedback about a parallel code path I hadn&rsquo;t initially noticed</li>
</ul>
<p>The code change was ~100 lines. The context-building was hours.</p>
<h3 id="4-production-telemetry-is-the-best-bug-report">4. Production telemetry is the best bug report</h3>
<p>The issue report included the exact malformed tool name from production: <code>search_circularsearch_circularsearch_circular</code>. That single string made the bug immediately reproducible and the fix trivially verifiable. If you&rsquo;re filing a bug, include the malformed output — it&rsquo;s worth more than a paragraph of description.</p>
<h2 id="links">Links</h2>
<ul>
<li><strong>PR</strong>: <a href="https://github.com/maximhq/bifrost/pull/1830">fix: preserve parallel tool call deltas — #1830</a></li>
<li><strong>Issue</strong>: <a href="https://github.com/maximhq/bifrost/issues/1829">Streaming tool call deltas merge in parallel tool calling — #1829</a></li>
<li><strong>Project</strong>: <a href="https://github.com/maximhq/bifrost">Bifrost AI Gateway</a> — 2.6k ⭐, fastest enterprise AI gateway</li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
