<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GWEN</title>
    <description>The latest articles on DEV Community by GWEN (@gwenj).</description>
    <link>https://dev.to/gwenj</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3988531%2F36f7e0d8-6aa0-4a21-ba14-62c292320d52.jpg</url>
      <title>DEV Community: GWEN</title>
      <link>https://dev.to/gwenj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gwenj"/>
    <language>en</language>
    <item>
      <title>RAG is Great, But Why Does My LLM Still "Forget" Key Context?</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Mon, 13 Jul 2026 10:08:02 +0000</pubDate>
      <link>https://dev.to/gwenj/rag-is-great-but-why-does-my-llm-still-forget-key-context-55n3</link>
      <guid>https://dev.to/gwenj/rag-is-great-but-why-does-my-llm-still-forget-key-context-55n3</guid>
      <description>&lt;p&gt;Hey everyone,You've meticulously set up your Retrieval Augmented Generation (RAG) system. You've got your chunking strategy, your embeddings are top-notch, and your vector database is humming along. You're feeding your LLM the most relevant context, confidently expecting a brilliant, informed answer.&lt;/p&gt;

&lt;p&gt;And then... crickets. Or worse, the LLM hallucinates, ignores the crucial piece of information it just retrieved, or provides a generic response that makes you wonder if it even read the context. It's like your LLM has selective amnesia, or maybe it just prefers making things up.&lt;/p&gt;

&lt;p&gt;This "context blindness" or "selective forgetting" in RAG systems is a real head-scratcher. It feels like we're constantly battling against this invisible force that makes our models ignore the very data we're providing to make them better.&lt;/p&gt;

&lt;p&gt;So, what's going on? Let's break down some common culprits I've encountered – and I'm keen to hear your war stories and insights too!&lt;/p&gt;

&lt;p&gt;The Usual Suspects Behind Context Blindness:&lt;br&gt;
Retrieval Recall Isn't What You Think:&lt;br&gt;
Sometimes, the problem starts even before the LLM sees the context. Is your retrieval really pulling the most relevant chunks? Are your chunking strategies optimal for your data? Is your embedding model capturing the nuances? Often, the issue isn't the LLM ignoring context, but rather it never receiving the right context in the first place.&lt;/p&gt;

&lt;p&gt;The "Too Long, Didn't Read" Syndrome (Context Window Overload):&lt;br&gt;
Even if you retrieve the perfect information, modern LLMs have finite context windows. If you're stuffing too much information in, even key details can get lost in the noise or simply pushed out. And let's be honest, just because an LLM has a large context window doesn't mean it effectively uses all of it. Key information can get diluted or ignored, especially in the middle of a lengthy prompt.&lt;/p&gt;

&lt;p&gt;Instruction Contradictions &amp;amp; Prompt Engineering Woes:&lt;br&gt;
Are your system prompts inadvertently conflicting with the retrieved context? Is your LLM getting mixed signals? Sometimes, our carefully crafted instructions might accidentally prime the model to prioritize certain types of information or generate responses in a way that sidelines the retrieved data. The subtle art of prompt engineering can quickly become a minefield.&lt;/p&gt;

&lt;p&gt;Noise, Noise, Everywhere (Irrelevant Information):&lt;br&gt;
Your retrieval might be good, but if it brings back a lot of tangential or irrelevant information alongside the golden nuggets, the LLM can get distracted. It's like trying to find a needle in a haystack, even if the needle is technically in the haystack. The signal-to-noise ratio matters, a lot.&lt;/p&gt;

&lt;p&gt;What's Your Biggest RAG "Oops" Moment?&lt;br&gt;
I'm genuinely curious: When it comes to RAG's context issues, where do you find your biggest headaches?&lt;/p&gt;

&lt;p&gt;Is it usually the retrieval phase (not finding the right info)?&lt;br&gt;
Is it the LLM's ability to process and use the context (even when it's given the right stuff)?&lt;br&gt;
Or is it more about prompting and managing the interaction between your instructions and the retrieved data?&lt;br&gt;
Perhaps it's the post-processing or generation phase, where the model just decides to go rogue?&lt;br&gt;
Strategies to Fight the Forgetfulness:&lt;br&gt;
Advanced Retrieval Techniques: Beyond basic similarity search, have you explored hybrid search, re-ranking models, or query expansion?&lt;br&gt;
Context Compression/Summarization: Can we distill the retrieved context before feeding it to the LLM, ensuring only the most vital parts make it into the prompt?&lt;br&gt;
Refined Prompting: More explicit instructions on how to use the retrieved context, and clear delineations between your instructions and the context itself.&lt;br&gt;
Iterative RAG &amp;amp; Self-Correction: Building systems where the LLM can reflect on its answer, identify gaps, and then perform another retrieval.&lt;br&gt;
Navigating the RAG Maze with Confidence&lt;br&gt;
Solving these RAG challenges isn't just about tweaking parameters; it's about deeply understanding the entire lifecycle of your LLM application, from retrieval to generation. Debugging these "silent failures" where the data seems to be there but isn't used effectively, can be incredibly time-consuming.&lt;/p&gt;

&lt;p&gt;That's where tools designed for robust LLM ops come into play. If you're constantly fighting these context battles and want to build more reliable, performant RAG applications, you need a way to test, observe, and iterate effectively.&lt;/p&gt;

&lt;p&gt;For those looking to get ahead of these issues and build more resilient RAG pipelines, check out Tokenbay. It's built to help you test, evaluate, and fine-tune your LLM applications, making those tricky context issues easier to spot and fix.&lt;/p&gt;

&lt;p&gt;Explore Tokenbay: &lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>Tool calling Returns HTTP 200, But I “Assumed” the Tool Ran — Have You Seen This?</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:21:11 +0000</pubDate>
      <link>https://dev.to/gwenj/tool-calling-returns-http-200-but-i-assumed-the-tool-ran-have-you-seen-this-50h9</link>
      <guid>https://dev.to/gwenj/tool-calling-returns-http-200-but-i-assumed-the-tool-ran-have-you-seen-this-50h9</guid>
      <description>&lt;p&gt;I’ve been building LLM apps and keep running into a really nasty failure mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request looks successful (HTTP 200 / response structure is “valid”)&lt;/li&gt;
&lt;li&gt;The model outputs &lt;code&gt;tool_calls&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;But the UI or the next assistant step behaves like the tool &lt;strong&gt;never actually ran&lt;/strong&gt; (missing info, the model “fills in the blanks,” or it just skips the tool-related part)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most annoying part is that this kind of failure is often &lt;strong&gt;silent&lt;/strong&gt;. If you only monitor “request success,” you’ll never see the real break point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I mean by “success” (and where it diverges)
&lt;/h2&gt;

&lt;p&gt;A real, completed tool-calling chain should include (at minimum) these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model requests the tool&lt;/strong&gt; (&lt;code&gt;tool_calls&lt;/code&gt; are emitted)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your backend executes the tool&lt;/strong&gt; (the function actually runs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You inject the tool result back into the next LLM step&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The final assistant output is generated&lt;/strong&gt; (based on the tool result)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In my experience, “silent tool failures” usually mean one of steps &lt;strong&gt;2/3/4&lt;/strong&gt; quietly breaks, while everything still looks fine on the surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which step is most likely failing for you?
&lt;/h2&gt;

&lt;p&gt;I’m genuinely curious: in your setup, what usually breaks? Which one shows up most?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Argument parsing/validation failure&lt;/strong&gt;: the tool arguments aren’t what you expect, but your system still returns 200&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution failure / timeout&lt;/strong&gt;: the tool errors, but the error never makes it back as a proper tool result—so the model continues (or guesses)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Injection failure&lt;/strong&gt;: the tool result exists, but it never gets included in the next prompt (or gets truncated)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop control bug&lt;/strong&gt;: your state machine stops too early, so the agent never completes the “tool -&amp;gt; next -&amp;gt; final answer” loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re willing, share the most “hilarious” worst case you’ve seen. I’m trying to collect patterns and turn them into a solid troubleshooting checklist.&lt;/p&gt;




&lt;h2&gt;
  
  
  The lowest-cost way to detect it early (my rule now)
&lt;/h2&gt;

&lt;p&gt;My rule is: every tool call must produce logs with a stable &lt;code&gt;tool_call_id&lt;/code&gt;, and you should be able to see the lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;requested&lt;/strong&gt;: tool name + when the model asked for it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;executed&lt;/strong&gt;: server-side execution time + success/failure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;injected&lt;/strong&gt;: whether the tool result was successfully fed into the next LLM step (this is the one many people miss)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;completed&lt;/strong&gt;: whether the final assistant response was generated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your logs are missing &lt;strong&gt;executed&lt;/strong&gt; or &lt;strong&gt;injected&lt;/strong&gt;, “HTTP 200” is basically just a distraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you handle failure when the break happens?
&lt;/h2&gt;

&lt;p&gt;Let’s talk product strategy. When a tool chain breaks, what do you do?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retry the tool&lt;/strong&gt; (with safe limits to avoid infinite loops)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail fast and degrade gracefully&lt;/strong&gt; (tell the user you couldn’t fetch tool results instead of letting the model invent)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback to a no-tool answer&lt;/strong&gt; (make it clear the answer may be incomplete)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which strategy does your team lean toward? Do you have a standard playbook/checklist?&lt;/p&gt;




&lt;h2&gt;
  
  
  How we approached it (and the practical takeaway)
&lt;/h2&gt;

&lt;p&gt;The tricky part about tool calling incidents is that failures can be caused by subtle integration differences—different providers, different payload shapes, different streaming behaviors. That makes “request success” a misleading signal.&lt;/p&gt;

&lt;p&gt;What really matters is &lt;strong&gt;observability of the tool lifecycle&lt;/strong&gt;: can you reliably track whether tool execution and result injection actually happened?&lt;/p&gt;

&lt;p&gt;If you’re working on tool calling / agent orchestration and want to verify integration stability quickly, you can register and test with &lt;strong&gt;tokenbay&lt;/strong&gt; here:&lt;br&gt;&lt;br&gt;
&lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>api</category>
    </item>
    <item>
      <title>Beyond "Invalid JSON": Engineering Robust Structured Outputs from LLMs</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:26:01 +0000</pubDate>
      <link>https://dev.to/gwenj/beyond-invalid-json-engineering-robust-structured-outputs-from-llms-222p</link>
      <guid>https://dev.to/gwenj/beyond-invalid-json-engineering-robust-structured-outputs-from-llms-222p</guid>
      <description>&lt;p&gt;We’ve all been there: Your prompt explicitly says, &lt;em&gt;"Return ONLY a JSON object."&lt;/em&gt; But the LLM, in its infinite desire to be helpful, returns: &lt;em&gt;"Sure! Here is the data you requested:&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;json { ... }&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If your production parser expects a clean string, your app just crashed. While "JSON Mode" exists in most APIs, it’s not a magic bullet. It can still truncate, time out, or produce logically invalid data.&lt;/p&gt;

&lt;p&gt;Here is the engineering checklist for handling structured LLM outputs without losing your mind.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. JSON Mode is a Constraint, Not a Guarantee
&lt;/h2&gt;

&lt;p&gt;When you enable &lt;code&gt;response_format: { "type": "json_object" }&lt;/code&gt;, the model is constrained to output strings that &lt;em&gt;can&lt;/em&gt; be parsed as JSON. However:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It can still be empty:&lt;/strong&gt; If the model hits a safety filter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can be incomplete:&lt;/strong&gt; If it hits &lt;code&gt;max_tokens&lt;/code&gt; before closing the last bracket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The schema can be wrong:&lt;/strong&gt; It’s valid JSON, but the keys are missing or the types are wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Always treat the LLM output as "Untrusted Input."&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The Defensive Parsing Pattern
&lt;/h2&gt;

&lt;p&gt;Don't just &lt;code&gt;JSON.parse(response)&lt;/code&gt;. You need a multi-stage recovery logic. If the first attempt fails, try to "repair" the string before giving up.&lt;/p&gt;
&lt;h3&gt;
  
  
  The "Regex Rescue" (Node.js snippet)
&lt;/h3&gt;

&lt;p&gt;Sometimes models still wrap JSON in Markdown blocks despite your settings. A simple regex can save 20% of your failed requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;robustParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Direct try&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 2. Try to extract content between the first { and last }&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsonMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\{[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;innerError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Found JSON-like string but it's malformed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No JSON structure found in response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Dealing with Truncated JSON (The "Partial" Problem)
&lt;/h2&gt;

&lt;p&gt;In streaming mode, or when context limits are hit, you might receive &lt;code&gt;{"user": {"name": "John",&lt;/code&gt;. This is unparseable.&lt;/p&gt;

&lt;p&gt;If your UI needs to show data &lt;em&gt;while&lt;/em&gt; it's streaming, use a &lt;strong&gt;Partial JSON Parser&lt;/strong&gt; (like &lt;code&gt;partial-json-parser&lt;/code&gt;). It allows you to extract whatever keys have been completed so far, keeping the UI responsive without waiting for the closing &lt;code&gt;}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Schema Validation is Non-Negotiable
&lt;/h2&gt;

&lt;p&gt;A valid JSON is useless if &lt;code&gt;price&lt;/code&gt; is a string like &lt;code&gt;"100 USD"&lt;/code&gt; when your database expects an integer &lt;code&gt;100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Engineering Standard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Zod&lt;/strong&gt; or &lt;strong&gt;JSON Schema&lt;/strong&gt; to validate the object immediately after parsing.&lt;/li&gt;
&lt;li&gt;If validation fails, log the specific "Schema Drift" and trigger a retry or a fallback.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llm_parsing_failure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_555"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"schema_mismatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"missing_keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"raw_output_snippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. The "System Prompt" Trick for JSON
&lt;/h2&gt;

&lt;p&gt;To minimize parsing errors, stop using vague instructions. Be hyper-specific about the JSON structure in your System Prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&gt; &lt;em&gt;"Output a JSON object with user details."&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;Good:&lt;/strong&gt; &lt;em&gt;"Return a JSON object with exactly two keys: 'id' (integer) and 'status' (string: 'active'|'pending'). Do not include any text before or after the JSON."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring the "Parsing Health"
&lt;/h2&gt;

&lt;p&gt;If you don't monitor your parsing success rate, you're flying blind. Track these two metrics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hard Parse Failure Rate:&lt;/strong&gt; The % of responses that are not valid JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema Validation Failure Rate:&lt;/strong&gt; The % of valid JSONs that don't match your expected structure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the second one is high, your prompt is weak. If the first one is high, your provider or your &lt;code&gt;max_tokens&lt;/code&gt; setting is likely the culprit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;In a deterministic world, we expect 1+1=2. In the LLM world, 1+1 usually equals 2, but sometimes it equals &lt;code&gt;{"result": 2}&lt;/code&gt; and sometimes it equals &lt;em&gt;"The sum is two."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Engineering for LLMs is the art of wrapping non-deterministic "intelligence" in a deterministic "safety cage." Robust JSON handling is the bars of that cage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reliability architecture by:&lt;/strong&gt; &lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>api</category>
    </item>
    <item>
      <title>Why Your LLM App is Getting Slower (and More Expensive): The TTFT &amp; Context Crisis</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:11:51 +0000</pubDate>
      <link>https://dev.to/gwenj/why-your-llm-app-is-getting-slower-and-more-expensive-the-ttft-context-crisis-39m2</link>
      <guid>https://dev.to/gwenj/why-your-llm-app-is-getting-slower-and-more-expensive-the-ttft-context-crisis-39m2</guid>
      <description>&lt;p&gt;In the early stages of building an LLM app, everything feels fast. But as you add RAG (Retrieval-Augmented Generation), long conversation histories, and complex system prompts, two things happen: your &lt;strong&gt;TTFT (Time To First Token)&lt;/strong&gt; spikes, and your API bill explodes.&lt;/p&gt;

&lt;p&gt;If your users are waiting 5+ seconds to see the first word, you don't have a "slow model" problem—you have a &lt;strong&gt;Context Management&lt;/strong&gt; problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost of "Context Bloat"
&lt;/h2&gt;

&lt;p&gt;Every time you send a request, the LLM provider re-processes your entire prompt. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1,000 tokens&lt;/strong&gt; of system prompt? You pay for it every single turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5,000 tokens&lt;/strong&gt; of retrieved documents? You pay to re-index them every time the user asks a follow-up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When TTFT starts climbing, it's usually because the "Prefill" stage (the time the model spends reading your prompt) is overwhelmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 1: The "Hard Cut" vs. "Smart Summary"
&lt;/h2&gt;

&lt;p&gt;Most developers just use a sliding window for conversation history. This is lazy and dangerous. Instead, implement a &lt;strong&gt;Dual-Track Context&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Anchor:&lt;/strong&gt; Keep the System Prompt and the last 2 turns intact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Essence:&lt;/strong&gt; For older turns, don't send the full text. Summarize them into 1-2 sentences. &lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Implementation Tip (The Schema):
&lt;/h3&gt;

&lt;p&gt;Track the "Input Token Weight" in your logs to identify which features are bloating your requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"token_usage_audit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"system_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"history_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rag_context_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ttft_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;If &lt;code&gt;total_input_tokens&lt;/code&gt; correlates perfectly with &lt;code&gt;ttft_ms&lt;/code&gt;, you know exactly where to cut.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategy 2: Leverage Context Caching
&lt;/h2&gt;

&lt;p&gt;If you use long System Prompts or massive RAG datasets that don't change often, &lt;strong&gt;Context Caching&lt;/strong&gt; is your best friend. &lt;/p&gt;

&lt;p&gt;By caching the "prefix" of your prompt, the model doesn't have to re-read it. This can reduce TTFT by &lt;strong&gt;up to 80%&lt;/strong&gt; and cut costs significantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Rule of Thumb for Caching:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System Prompt &amp;gt; 1024 tokens?&lt;/strong&gt; Cache it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static RAG Knowledge Base?&lt;/strong&gt; Cache it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-specific Profile/History?&lt;/strong&gt; Cache it only if the session is active.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategy 3: Trim the RAG Fat
&lt;/h2&gt;

&lt;p&gt;More context $\neq$ Better answers. Sending 10 retrieved chunks to the LLM often leads to "Lost in the Middle" syndrome, where the model ignores the most relevant info.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Use a &lt;strong&gt;Reranker&lt;/strong&gt;. &lt;br&gt;
Instead of sending the top 10 chunks from your vector DB, get the top 20, run them through a cheap reranker, and only send the top 3 most relevant chunks to the expensive LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring the Metrics That Matter
&lt;/h2&gt;

&lt;p&gt;To keep your app lean, stop looking at "Average Latency" and start tracking these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TTFT (Time To First Token):&lt;/strong&gt; This is the ultimate UX metric. Keep it under 1s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TPS (Tokens Per Second):&lt;/strong&gt; This measures the model's "reading speed."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Hit Rate:&lt;/strong&gt; Are you actually reusing those expensive prefixes?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context/Output Ratio:&lt;/strong&gt; If you send 10k tokens to get 50 back, your prompt is likely inefficient.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thought: Less is More
&lt;/h2&gt;

&lt;p&gt;In LLM engineering, the most performant prompt is the shortest one that still gets the job done. Every token you remove is a millisecond saved and a fraction of a cent earned.&lt;/p&gt;

&lt;p&gt;Before you upgrade to a bigger model to fix "quality issues," try cleaning up your context. You'll be surprised how much "intelligence" was just hidden under the noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimization insights provided by:&lt;/strong&gt; &lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>api</category>
    </item>
    <item>
      <title>Tool Calling That “Works” But Never Executes (Silent Failure After HTTP 200)</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Tue, 07 Jul 2026 10:10:47 +0000</pubDate>
      <link>https://dev.to/gwenj/tool-calling-that-works-but-never-executes-silent-failure-after-http-200-2bc8</link>
      <guid>https://dev.to/gwenj/tool-calling-that-works-but-never-executes-silent-failure-after-http-200-2bc8</guid>
      <description>&lt;p&gt;Tool calling failures are the silent killers of LLM apps. Your API call returns &lt;strong&gt;HTTP 200&lt;/strong&gt;, the model outputs a &lt;strong&gt;tool call&lt;/strong&gt;, and everything looks “fine”… until users get an answer that’s missing the actual data—empty, guessed, stale, or half-formed.&lt;/p&gt;

&lt;p&gt;The annoying part: most teams only log the top-level completion. They don’t log the tool lifecycle. So you end up debugging uncertainty instead of root cause.&lt;/p&gt;

&lt;p&gt;In this post, I’ll show you a logging schema that answers one question with confidence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did the tool call get parsed, executed, and fed back into the model—before we rendered the final answer?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tool calling can fail while the request “succeeds”
&lt;/h2&gt;

&lt;p&gt;A logical “chat completion” can succeed while the &lt;em&gt;tool chain&lt;/em&gt; doesn’t. Common failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model outputs a tool call, but your server &lt;strong&gt;skips execution&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tool arguments parse fails, so you fall back to a non-tool path (but still return an answer)&lt;/li&gt;
&lt;li&gt;The tool executes, but you &lt;strong&gt;never send the tool result back&lt;/strong&gt; to the model&lt;/li&gt;
&lt;li&gt;You send it back, but the callback fails, times out, or throws, so the second model call doesn’t happen&lt;/li&gt;
&lt;li&gt;Streaming vs non-streaming changes the event ordering, so your state machine marks the flow as “done” too early&lt;/li&gt;
&lt;li&gt;Retries/fallback happen and hide the original tool failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your logs don’t cover the tool lifecycle, you can’t tell the difference between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“The model decided not to use tools”&lt;/li&gt;
&lt;li&gt;vs “It &lt;em&gt;tried&lt;/em&gt; to use tools, but your system dropped the execution”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The logging schema: make tool calling debuggable
&lt;/h2&gt;

&lt;p&gt;For every logical tool call (not just every LLM request), log enough to answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Was a tool call produced?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Were the tool arguments parsed successfully?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Did we execute the tool?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Did we send tool results back to the model?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Did we end with an answer that depended on the tool?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1) Tool call successfully executed and returned to the model
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llm_tool_call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_201"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_PROVIDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search_knowledge_base"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_arguments_parse_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parsed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_execution_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_result_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_result_size_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18231&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"callback_to_model_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callback_attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"final_assistant_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_ran_then_answered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"retry_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Tool call exists, but arguments fail to parse (execution skipped)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llm_tool_call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_202"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_def"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_PROVIDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get_customer_profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_arguments_parse_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_execution_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"skipped"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"tool_result_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_result_size_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"callback_to_model_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_sent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"final_assistant_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"answer_without_tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_arguments_parse_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid JSON in tool arguments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"retry_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backup-model"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Tool executed, but tool result callback to model failed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llm_tool_call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_203"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_xyz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_PROVIDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fetch_order_status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_arguments_parse_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parsed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_execution_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"tool_result_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_result_size_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4021&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"callback_to_model_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callback_attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"final_assistant_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_ran_but_no_followup_answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"callback_to_model_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Timeout while calling model follow-up"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"retry_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backup-model"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key point:&lt;/strong&gt; you’re not logging “the model output.” You’re logging &lt;em&gt;the tool chain state&lt;/em&gt;. That’s what turns “mystery UX” into a deterministic diagnosis.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two most common “it looks fine” illusions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Illusion A: tool call exists, but execution got skipped
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; answers are generic, missing facts, or reference “I don’t have enough data.”&lt;br&gt;
&lt;strong&gt;Log tell:&lt;/strong&gt; &lt;code&gt;tool_execution_status="skipped"&lt;/code&gt; while &lt;code&gt;tool_arguments_parse_status&lt;/code&gt; is failed/unknown or your state machine decided to bypass tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Illusion B: tool executed, but result never got fed back
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; model hallucinates the result or repeats the same question (“I need the tool output…”).&lt;br&gt;
&lt;strong&gt;Log tell:&lt;/strong&gt; &lt;code&gt;tool_execution_status="success"&lt;/code&gt; but &lt;code&gt;callback_to_model_status!="sent"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal wrapper logic (Node.js): enforce lifecycle ordering
&lt;/h2&gt;

&lt;p&gt;You don’t need a complex observability platform. You need a state machine that logs transitions.&lt;/p&gt;

&lt;p&gt;Below is a compact pattern you can adapt. It assumes you already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a function to parse tool arguments&lt;/li&gt;
&lt;li&gt;a tool executor&lt;/li&gt;
&lt;li&gt;a function to call the model again with tool results
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleToolCall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;executeTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;callbackToModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fallbackFrom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fallbackTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tool_call_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 1) Parse arguments&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;parseStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;parseError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;parseStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parsed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;parseStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;parseError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parseStatus&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parsed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;llm_tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_arguments_parse_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parseStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_execution_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;skipped&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_result_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_result_size_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_to_model_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;final_assistant_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;answer_without_tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_arguments_parse_failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parseError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;retry_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fallback_from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallbackFrom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fallback_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallbackTo&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parse_failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2) Execute tool&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;execStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;execError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;execStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;execStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;execError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;execStatus&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;llm_tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_arguments_parse_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parsed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_execution_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;execStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_result_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_result_size_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_to_model_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;final_assistant_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_failed_no_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_execution_failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;execError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;retry_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fallback_from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallbackFrom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fallback_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallbackTo&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_execution_failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Optional: compute hash/size for privacy-safe summaries&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultSizeBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3) Callback tool result to the model&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;callbackStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;callbackError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;callbackToModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;callbackStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;callbackStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;callbackError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;llm_tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tool_arguments_parse_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parsed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tool_execution_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tool_result_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256:…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tool_result_size_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resultSizeBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;callback_to_model_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callbackStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;callback_attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;final_assistant_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;callbackStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_ran_then_answered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_ran_but_no_followup_answer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callbackStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callback_to_model_failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callbackStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callbackError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retry_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fallback_from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallbackFrom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fallback_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallbackTo&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callbackStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callbackStatus&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you implement only one thing from this post: &lt;strong&gt;log tool lifecycle transitions&lt;/strong&gt; and &lt;strong&gt;make final assistant status depend on tool callback success&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring: alerts you actually care about
&lt;/h2&gt;

&lt;p&gt;Pick a few metrics that correlate directly with broken UX:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_call_present_rate&lt;/code&gt;&lt;/strong&gt; (baseline per route/feature)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_call_executed_rate&lt;/code&gt;&lt;/strong&gt; (if this drops, you skip execution)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_arguments_parse_failed_rate&lt;/code&gt;&lt;/strong&gt; (if this spikes, tool args schema drift)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_callback_failed_rate&lt;/code&gt;&lt;/strong&gt; (if this spikes, follow-up model call is broken)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;answer_without_tool_rate&lt;/code&gt;&lt;/strong&gt; (often the fastest UX damage indicator)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’re hunting for regressions that look like: &lt;em&gt;HTTP 200 is fine, but the tool chain isn’t.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Tool calling isn’t reliable just because you got a tool call out of the model.&lt;br&gt;&lt;br&gt;
It’s reliable only when you: &lt;strong&gt;parse → execute → callback → then render an answer that actually used the result.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your logs don’t tell that story, you’ll keep hearing “it worked but the answer was wrong.”&lt;/p&gt;

&lt;p&gt;tokenbay:  &lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>api</category>
    </item>
    <item>
      <title>Streaming Interrupted: How to Debug “Successful” LLM Streams (Before Support Tickets Start)</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Mon, 06 Jul 2026 10:25:59 +0000</pubDate>
      <link>https://dev.to/gwenj/streaming-interrupted-how-to-debug-successful-llm-streams-before-support-tickets-start-3fn6</link>
      <guid>https://dev.to/gwenj/streaming-interrupted-how-to-debug-successful-llm-streams-before-support-tickets-start-3fn6</guid>
      <description>&lt;p&gt;Streaming failures are the worst kind of incidents: your API call can look &lt;strong&gt;successful&lt;/strong&gt; while users still get a broken experience—cut-off answers, truncated JSON, missing tool outputs, or long “hangs” after the first tokens.&lt;/p&gt;

&lt;p&gt;The fastest way to stop guessing is to instrument streaming so you can answer one question with confidence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did the model stream actually finish, or did it stop “silently”?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this post, I’ll show you the logging shape I use to detect streaming interruptions, plus a practical checklist to find the root cause quickly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why streaming “success” is not success
&lt;/h2&gt;

&lt;p&gt;When you stream, you have more failure modes than plain REST calls. A request can return HTTP 200 and still be wrong in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the stream starts, but ends early&lt;/li&gt;
&lt;li&gt;chunks stop arriving (stall)&lt;/li&gt;
&lt;li&gt;the stream ends with fewer tokens than expected&lt;/li&gt;
&lt;li&gt;streaming completes but the output is malformed (e.g., incomplete JSON)&lt;/li&gt;
&lt;li&gt;the client disconnects mid-stream&lt;/li&gt;
&lt;li&gt;retries and fallbacks hide the original failure from user reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your logs only capture &lt;code&gt;status code&lt;/code&gt; and &lt;code&gt;latency&lt;/code&gt;, you’ll miss the real issue. You need stream lifecycle and what you actually received.&lt;/p&gt;




&lt;h2&gt;
  
  
  The logging schema that makes streaming debuggable
&lt;/h2&gt;

&lt;p&gt;For every &lt;strong&gt;logical LLM request&lt;/strong&gt;, log enough information to answer four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Was the stream finished?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How much data did we receive (chunks/tokens)?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why did it stop (stop vs interruption / completion reason)?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Did retry or fallback hide it?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Below is a minimal event model you can adapt.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Successful stream (completed normally)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llm_stream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tokenbay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chat_completion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"streaming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream_started_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1719999990000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream_finished_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1719999991842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream_duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"retry_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"chunks_received"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tokens_received"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"completion_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_disconnected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"error_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Interrupted stream (ended early / disconnected / incomplete)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llm_stream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_124"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tokenbay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chat_completion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"streaming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"interrupted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream_started_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1719999990000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream_finished_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1719999993020&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream_duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3020&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"retry_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4.1-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback_to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backup-model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"chunks_received"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tokens_received"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"completion_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_disconnected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"error_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stream_interrupted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Client disconnected"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What matters most:&lt;/strong&gt; the categories.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lifecycle (&lt;code&gt;started/finished/duration&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;received amount (&lt;code&gt;chunks/tokens&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;termination (&lt;code&gt;completion_reason&lt;/code&gt; or null)&lt;/li&gt;
&lt;li&gt;whether retries/fallback were involved (&lt;code&gt;retry_count&lt;/code&gt;, &lt;code&gt;fallback_from/to&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Normalize stream failures into a small set of &lt;code&gt;error_type&lt;/code&gt;s
&lt;/h2&gt;

&lt;p&gt;Raw streaming errors are inconsistent across SDKs and providers. Normalize them into stable categories so dashboards and alerts work.&lt;/p&gt;

&lt;p&gt;A practical set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client_disconnected&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;upstream_timeout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stream_interrupted&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;json_incomplete&lt;/code&gt; (incomplete structured output)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_tokens_reached&lt;/code&gt; (if you can detect it)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unknown_stream_failure&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if your exact cause varies, the &lt;strong&gt;category&lt;/strong&gt; stays consistent.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical wrapper: detect “no terminal finish” during streaming
&lt;/h2&gt;

&lt;p&gt;The core idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat a stream as “completed” only when you receive a terminal signal (or equivalent).&lt;br&gt;&lt;br&gt;
If the connection ends without a terminal finish, you’ve got an interruption.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s a working Node.js pattern using an OpenAI-compatible streaming interface. You will need to adapt two parts to your exact SDK payload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how you extract terminal signals (e.g., &lt;code&gt;finish_reason&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;how you count tokens (provider usage fields vs your own estimation)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;streamLoggedChatCompletion&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tokenbay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onToken&lt;/span&gt; &lt;span class="c1"&gt;// optional callback for UI&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;llm_stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat_completion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retry_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fallback_from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fallback_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;chunksReceived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Token counting:&lt;/span&gt;
  &lt;span class="c1"&gt;// - If your provider returns usage or token counts for streaming, use that.&lt;/span&gt;
  &lt;span class="c1"&gt;// - Otherwise you can estimate, but be explicit.&lt;/span&gt;
  &lt;span class="c1"&gt;// Here we keep it simple: count received text length as a placeholder.&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tokensReceived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;completionReason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;chunksReceived&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;onToken&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;tokensReceived&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// placeholder; replace with real token usage if available&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Terminal detection: adapt this to your SDK/provider payload.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;finish_reason&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;completionReason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;endedAt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;terminal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Connected but no terminal finish signal =&amp;gt; interruption/incomplete stream&lt;/span&gt;
      &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interrupted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stream_started_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stream_finished_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stream_duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;chunks_received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunksReceived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tokens_received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokensReceived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;completion_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;client_disconnected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stream_interrupted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stream ended without terminal finish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;interrupted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stream_started_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stream_finished_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stream_duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;chunks_received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunksReceived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tokens_received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokensReceived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;completion_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;completionReason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client_disconnected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;interrupted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;endedAt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientDisconnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disconnect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interrupted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stream_started_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stream_finished_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stream_duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;chunks_received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunksReceived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tokens_received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokensReceived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;completion_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client_disconnected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientDisconnected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientDisconnected&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;client_disconnected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stream_interrupted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two common gotchas:&lt;br&gt;
1) &lt;code&gt;tokensReceived&lt;/code&gt; above is a placeholder. Replace it with real token accounting if you can (usage fields, provider logs, or a token estimator you trust).&lt;br&gt;&lt;br&gt;
2) Terminal detection (&lt;code&gt;finish_reason&lt;/code&gt;) is provider/SDK-dependent. The pattern is right; the exact field name must match your runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Silent streaming failures you should watch for
&lt;/h2&gt;

&lt;p&gt;Once you have the events, you can catch regressions quickly. Here are patterns that consistently show up in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chunks/tokens suddenly drop for a specific model or route&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Compare &lt;code&gt;chunks_received&lt;/code&gt; / &lt;code&gt;tokens_received&lt;/code&gt; distributions by model and time window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Completion reason becomes null more often&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If &lt;code&gt;completion_reason&lt;/code&gt; is missing, you likely have a terminal detection mismatch, a new provider behavior, or a transport issue.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Interrupted rate spikes but HTTP errors don’t&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
That’s the definition of “silent streaming failure”: it looks healthy in REST metrics, but not in stream lifecycle metrics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retries increase while user experience seems OK&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Users might only see the final attempt. Your logs will show the hidden retry loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fallback becomes the default&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If &lt;code&gt;fallback_to&lt;/code&gt; is frequent after interruptions, the system is masking an upstream streaming stability issue.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The alert set that won’t annoy you
&lt;/h2&gt;

&lt;p&gt;If you only add a few alerts, add these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;interrupted_rate&lt;/strong&gt; &amp;gt; baseline (per model + feature/route)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;success stream_duration_ms p95&lt;/strong&gt; shifts upward&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chunks_received median&lt;/strong&gt; drops for a feature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;completion_reason_null_rate&lt;/strong&gt; exceeds threshold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is enough to catch most streaming breakages without drowning in noise.&lt;/p&gt;




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

&lt;p&gt;Streaming is not just “more responsive UI.” It changes the failure model.&lt;br&gt;&lt;br&gt;
A request can be “successful” while the stream is actually incomplete.&lt;/p&gt;

&lt;p&gt;If your logs can’t tell you whether the stream finished—and how much you received—you’re debugging uncertainty. You don’t need fancy observability. You need stream lifecycle fields.&lt;/p&gt;

&lt;p&gt;tokenbay: &lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>Cache Misses — Why Your AI Costs Won’t Drop (Even When Traffic Stays Flat)</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Fri, 03 Jul 2026 09:57:36 +0000</pubDate>
      <link>https://dev.to/gwenj/cache-misses-why-your-ai-costs-wont-drop-even-when-traffic-stays-flat-hd2</link>
      <guid>https://dev.to/gwenj/cache-misses-why-your-ai-costs-wont-drop-even-when-traffic-stays-flat-hd2</guid>
      <description>&lt;p&gt;Hey everyone—quick question.&lt;/p&gt;

&lt;p&gt;I’ve been seeing a pattern lately: teams invest in better models, tweak prompts, add tools… and yet their AI bill doesn’t drop. Sometimes it even creeps up, even when user traffic stays stable.&lt;/p&gt;

&lt;p&gt;That made me wonder whether the root cause is less about “model pricing” and more about how often you’re effectively reusing work.&lt;/p&gt;

&lt;p&gt;So I’m curious: how are you handling caching and reuse in your AI systems?&lt;/p&gt;

&lt;p&gt;When people say “we cache,” I often find they cache the obvious part (like embeddings or final responses), but the expensive part still gets recomputed. In practice, the cost might be leaking through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;repeated requests that look similar but aren’t token-identical&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tool call results that aren’t cached (or are cached with too-short TTLs)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;agent steps that re-run retrieval / planning even when the inputs haven’t changed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;context/history replay that defeats cache hits&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My working theory (and what I’ve tried)In systems with orchestration (multi-step, tool use, routing), cost is driven by the number of “unique execution paths”, not just the number of users. If caching doesn’t recognize execution equivalence, you end up paying for the same reasoning multiple times.&lt;/p&gt;

&lt;p&gt;For example, two requests might have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same user intent&lt;/li&gt;
&lt;li&gt;similar retrieved facts&lt;/li&gt;
&lt;li&gt;the same tool outputs
…but different message ordering, timestamps, or system prompt variants—so the cache key misses.
What I recommend checking first. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some questions:&lt;br&gt;&lt;br&gt;
Do you measure cache hit rate end-to-end? If yes, what are your biggest cost contributors that still don’t get cached?&lt;/p&gt;

&lt;p&gt;How do you define cache keys so they don’t miss due to tiny prompt differences?&lt;/p&gt;

&lt;p&gt;If you share your approach (even rules of thumb), I’d love to compare notes. I’m especially interested in what actually works in production, not just what sounds good in theory.&lt;/p&gt;

&lt;p&gt;If you’re curious about one tool I ran into while working through caching/reuse issues, here’s tokenbay for reference:&lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>Why AI API Gets Pricier？</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Thu, 02 Jul 2026 10:17:15 +0000</pubDate>
      <link>https://dev.to/gwenj/why-ai-api-gets-pricier-3o2l</link>
      <guid>https://dev.to/gwenj/why-ai-api-gets-pricier-3o2l</guid>
      <description>&lt;p&gt;I’ve been stuck on a pretty frustrating problem lately: why do AI API costs keep climbing the more we use it—and why does it feel like the bill has nothing to do with the “simple” product experience we’re shipping?&lt;/p&gt;

&lt;p&gt;At the beginning, it’s usually fine. You build a demo, fire off a few requests, try a handful of prompts, and the numbers look harmless. Then real users show up, features grow, and suddenly the cost curve goes vertical. Same app, same UI button—just a much more painful bill.&lt;/p&gt;

&lt;p&gt;What makes it worse is that, from the user’s side, the workflow still looks straightforward: they click a button, ask a question, or ask an agent to “complete a task.” But behind the scenes, one user interaction can trigger multiple model calls—retries, tool invocations, multi-step reasoning, chat history expansion, and sometimes agent “loops” that keep going longer than you intended. If you don’t design for that, the system can become a confident cost generator.&lt;/p&gt;

&lt;p&gt;So lately, I’m less interested in finding the “cheapest model” and more focused on a more fundamental engineering question: how do we make cost predictable and controllable per request?&lt;/p&gt;

&lt;p&gt;1) First: do you actually know your call graph?&lt;br&gt;
Before optimizing anything, you need visibility. Many teams only notice cost issues after it’s already unbearable.&lt;/p&gt;

&lt;p&gt;What I found most useful is tracking at the “one user request” level:&lt;/p&gt;

&lt;p&gt;how many model calls happen per request&lt;br&gt;
input tokens and output tokens per call&lt;br&gt;
whether retries occur&lt;br&gt;
whether tool calls succeed or fail (and trigger fallback)&lt;br&gt;
agent steps / loop iterations&lt;br&gt;
If you can’t answer those questions from logs, cost optimization becomes guesswork.&lt;/p&gt;

&lt;p&gt;2) Next: add budget controls (a real “kill switch”)&lt;br&gt;
I increasingly believe agents need hard guardrails. Without limits, a weird edge case can burn money fast.&lt;/p&gt;

&lt;p&gt;Common controls include:&lt;/p&gt;

&lt;p&gt;max steps (stop after N reasoning steps)&lt;br&gt;
max tool calls&lt;br&gt;
token caps per request / per stage&lt;br&gt;
fallback behavior when thresholds are exceeded (e.g., degrade gracefully or ask the user to confirm)&lt;br&gt;
This isn’t just about saving money—it’s about making the system safe when things go wrong.&lt;/p&gt;

&lt;p&gt;3) Finally: make “failure → upgrade” meaningful&lt;br&gt;
A lot of people talk about “cheap model first, upgrade on failure.” That’s reasonable, but the part that’s often missing is: what counts as failure, and when do you decide to escalate?&lt;/p&gt;

&lt;p&gt;If your definition of failure is vague, you end up upgrading too often, or retrying forever in different ways. Then you’re not optimizing—you’re just paying for uncertainty.&lt;/p&gt;

&lt;p&gt;My takeaway&lt;br&gt;
To me, controlling AI API costs isn’t a one-time tuning job. It’s about building a smarter execution strategy: observable call counts, budget limits, and clear escalation rules.&lt;/p&gt;

&lt;p&gt;I’m currently working on related engineering problems at tokenbay, so I’ve been paying close attention to this direction. If you’re dealing with agent-based workflows and unexpected bills, I’d love to hear what you’re doing today.&lt;/p&gt;

&lt;p&gt;Here is the link that you can try：&lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When was the last time you measured “how many model calls happen per user action” in your system? Do you have guardrails, or is it mostly “let the agent figure it out and hope for the best”?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>llm</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Stop Overpaying for AI APIs</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Wed, 01 Jul 2026 07:59:42 +0000</pubDate>
      <link>https://dev.to/gwenj/stop-overpaying-for-ai-apis-i9</link>
      <guid>https://dev.to/gwenj/stop-overpaying-for-ai-apis-i9</guid>
      <description>&lt;p&gt;I don’t know if anyone else has the same feeling, but &lt;strong&gt;AI API costs can get out of hand really fast.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the beginning, it feels harmless. You build a small demo, send a few requests, test a few prompts, and the cost looks almost negligible. But once the project starts getting real users, or once you add more AI features, the bill grows much faster than expected.&lt;/p&gt;

&lt;p&gt;Long prompts, chat history, retries, background tasks, embeddings, summarization, classification, agent workflows… everything adds up.&lt;/p&gt;

&lt;p&gt;The annoying part is that the product may still look simple from the outside. A user clicks one button, asks one question, or uploads one file. But behind the scenes, that single action might trigger several model calls. And if you are using a powerful model for every single step, the cost becomes painful very quickly.&lt;/p&gt;

&lt;p&gt;I’ve been thinking about this a lot recently because, honestly, using the best model for everything is probably not sustainable for many projects.&lt;/p&gt;

&lt;p&gt;So I started looking into &lt;strong&gt;some practical ways to reduce AI API costs without completely ruining the user experience.&lt;/strong&gt; Here are a few things I found useful.&lt;br&gt;
**&lt;br&gt;
The first one is simple: don’t use the most expensive model for every task.**&lt;/p&gt;

&lt;p&gt;Not every AI task needs the strongest reasoning model. Some tasks are just classification, rewriting, formatting, extracting information, or generating short summaries. Using a premium model for all of these is kind of like hiring a senior engineer to rename files. Sure, it works. But it’s a waste.&lt;/p&gt;

&lt;p&gt;A better approach is to match the model to the task. Use stronger models for complex reasoning, planning, coding, or high-value user interactions. For simpler tasks, cheaper and faster models are often good enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second thing is prompt length.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one is easy to ignore. I used to keep adding more instructions, more examples, more context, and more chat history into the prompt, thinking it would make the output better. Sometimes it does. But sometimes half of that prompt is no longer useful.&lt;/p&gt;

&lt;p&gt;And every extra token costs money.&lt;/p&gt;

&lt;p&gt;So now I think prompt cleanup should be part of the development process. Remove repeated instructions, summarize old conversation history, and only send the context that is actually needed for the current task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The third one is caching.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your users often ask similar questions, or if your app repeatedly generates similar outputs, you probably don’t need to call the model every single time. Cached responses or cached intermediate results can save a surprising amount of money.&lt;/p&gt;

&lt;p&gt;Of course, caching doesn’t work for every use case. But for FAQs, document analysis, repeated summaries, product descriptions, or internal tools, it can be very effective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fourth thing is monitoring.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This sounds obvious, but many teams don’t really know where their AI costs are coming from. Which feature uses the most tokens? Which user or project has abnormal usage? Which calls are unnecessary? Which prompts are too long?&lt;/p&gt;

&lt;p&gt;Without this visibility, cost optimization is mostly guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fifth thing is setting limits.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I know limits are not exciting, but they are necessary. Rate limits, user quotas, project budgets, and maximum output lengths can prevent small mistakes from becoming expensive problems. A broken loop or an overly aggressive agent can burn through a budget much faster than expected.&lt;br&gt;
**&lt;br&gt;
The last idea is fallback.**&lt;/p&gt;

&lt;p&gt;Instead of always starting with the most expensive model, maybe we can start with a cheaper model first. If the result is not good enough, then escalate to a stronger one. For many workflows, this kind of step-by-step strategy makes more sense than throwing the best model at every request.&lt;/p&gt;

&lt;p&gt;To me, reducing AI API costs is not just about finding the cheapest provider. It’s more about using models in a smarter way.&lt;/p&gt;

&lt;p&gt;Maybe the future of AI apps won’t be “one best model for everything.” It will probably be a mix of different models, routing rules, budgets, caching, and monitoring.&lt;/p&gt;

&lt;p&gt;I’m currently working on related engineering at TokenBay, so I’ve been keeping a close eye on this trend. If you’re interested, you can also try TokenBay—using one API for multiple models is another way to save money.&lt;/p&gt;

&lt;p&gt;Link：&lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m curious how other developers are dealing with this.&lt;/p&gt;

&lt;p&gt;Have you also felt that AI API costs are getting harder to control? Are you still using one powerful model for everything, or have you started routing different tasks to different models?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>Will OpenAI-compatible APIs Become the Standard for AI App Development?</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Tue, 30 Jun 2026 08:50:54 +0000</pubDate>
      <link>https://dev.to/gwenj/will-openai-compatible-apis-become-the-standard-for-ai-app-development-fj1</link>
      <guid>https://dev.to/gwenj/will-openai-compatible-apis-become-the-standard-for-ai-app-development-fj1</guid>
      <description>&lt;p&gt;Over the past year, I’ve noticed a pretty clear trend: many AI app developers say they are integrating “different models,” but from an engineering perspective, what they really want is for those models to behave like the same API.&lt;/p&gt;

&lt;p&gt;The OpenAI-style Chat Completions API has already become a kind of default interface in many projects. Whether the underlying model comes from OpenAI, Claude, Gemini, DeepSeek, or other closed-source or open-source models, the ideal experience for developers is simple: don’t make me rewrite the SDK, don’t make me redesign the message format, and don’t force me to change a bunch of business logic just to switch models.&lt;/p&gt;

&lt;p&gt;This is not because developers are lazy. It’s because AI application engineering is already complicated enough.&lt;/p&gt;

&lt;p&gt;A serious AI product usually needs to handle much more than the model call itself: prompt management, context length, token costs, retry logic, streaming responses, logs, user quotas, safety filters, evaluation, and monitoring. If every new model requires a different request format, response format, error handling logic, and streaming implementation, the team can quickly get buried in glue code.&lt;/p&gt;

&lt;p&gt;So in my view, the popularity of OpenAI-compatible APIs is not necessarily because OpenAI will always be the strongest model provider. It’s because developers need a stable abstraction layer.&lt;/p&gt;

&lt;p&gt;This is similar to what happened in other parts of software infrastructure. Not everyone uses AWS, but many cloud tools and interface designs have been influenced by AWS. Not every database is MySQL, but SQL has remained a common way to express data queries. AI model APIs may follow a similar path: the underlying models stay diverse, while the upper-level interface gradually becomes more standardized.&lt;/p&gt;

&lt;p&gt;For developers, this is a good thing.&lt;/p&gt;

&lt;p&gt;First, it lowers the cost of experimentation. If you use one model for customer support today and want to switch to another model for summarization tomorrow, compatibility makes that migration much easier.&lt;/p&gt;

&lt;p&gt;Second, it reduces vendor lock-in. AI models are evolving incredibly fast. The best model today may not be the most cost-effective choice three months from now. If your application is tightly coupled to one provider’s API, switching later can become painful.&lt;/p&gt;

&lt;p&gt;Third, it makes multi-model architecture more realistic. In one product, complex reasoning can use a stronger model, simple classification can use a cheaper model, and coding tasks can use a model that performs better on code. But this only works well if these models can be called and managed through a relatively unified interface. Otherwise, engineering complexity can quickly get out of control.&lt;/p&gt;

&lt;p&gt;Of course, OpenAI-compatible APIs won’t solve everything. Different models still have different capabilities, context handling, tool-calling behavior, multimodal support, and structured output quality. A unified interface does not mean unified performance. Developers still need proper evaluation, fallback strategies, and prompt adjustments.&lt;/p&gt;

&lt;p&gt;But from an engineering perspective, I believe “OpenAI-compatible” may become an important standard in AI infrastructure, at least for quite some time.&lt;/p&gt;

&lt;p&gt;I’m currently working on related engineering problems at TokenBay, so I’ve been paying close attention to this trend: do developers prefer each model to keep its own native API, or do they prefer a more unified interface on top, with the freedom to switch models underneath?&lt;/p&gt;

&lt;p&gt;Here is the link, love to hear any ideas for TokenBay:&lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re building AI applications, I’d love to hear your thoughts:&lt;/p&gt;

&lt;p&gt;Do you think OpenAI-compatible APIs will become the de facto standard for AI development? Or as models become more complex, will each model provider eventually move toward completely different API designs?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>How to switch AI models without rewriting your app</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Mon, 29 Jun 2026 08:47:08 +0000</pubDate>
      <link>https://dev.to/gwenj/how-to-switch-ai-models-without-rewriting-your-app-1dc6</link>
      <guid>https://dev.to/gwenj/how-to-switch-ai-models-without-rewriting-your-app-1dc6</guid>
      <description>&lt;p&gt;Most AI apps start with one model provider.&lt;/p&gt;

&lt;p&gt;That is usually the right choice. For a first version, you want one SDK, one API key, one billing page, and one model name. Simple is good when you are trying to ship.&lt;/p&gt;

&lt;p&gt;But once the product grows, the model decision gets more complicated.&lt;/p&gt;

&lt;p&gt;You may want to test another model because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one model is better at reasoning&lt;/li&gt;
&lt;li&gt;another model is faster for chat&lt;/li&gt;
&lt;li&gt;another one is cheaper for background jobs&lt;/li&gt;
&lt;li&gt;another model handles long context better&lt;/li&gt;
&lt;li&gt;you want a fallback when one provider is slow or unavailable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The annoying part is that switching models is often not just changing a string.&lt;/p&gt;

&lt;p&gt;It can mean adding another SDK, another API key, another request format, another dashboard, and another set of provider-specific edge cases.&lt;/p&gt;

&lt;p&gt;That gets messy quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before: direct OpenAI integration
&lt;/h2&gt;

&lt;p&gt;A first version might look like this:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a short onboarding message for a developer tool.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is clean and totally fine.&lt;/p&gt;

&lt;p&gt;But if you later want to compare Claude, Gemini, DeepSeek, or another model family, you may not want to rewrite your AI integration around each provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  After: use an OpenAI-compatible gateway
&lt;/h2&gt;

&lt;p&gt;One practical option is to use an OpenAI-compatible API gateway.&lt;/p&gt;

&lt;p&gt;Your app keeps using the OpenAI SDK style, but the gateway lets you route requests to different model families through one endpoint.&lt;/p&gt;

&lt;p&gt;I work on the TokenBay team, so the example below uses TokenBay. The general idea applies to any OpenAI-compatible gateway.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.tokenbay.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_TOKENBAY_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a short onboarding message for a developer tool.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main change is just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;
&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.tokenbay.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_TOKENBAY_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the useful part.&lt;/p&gt;

&lt;p&gt;You keep the familiar OpenAI client shape, but you are no longer wiring every provider separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try another model
&lt;/h2&gt;

&lt;p&gt;Once your app uses an OpenAI-compatible endpoint, testing another supported model can be as simple as changing the model name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4.6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a short onboarding message for a developer tool.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a short onboarding message for a developer tool.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not that every model behaves the same.&lt;/p&gt;

&lt;p&gt;They do not.&lt;/p&gt;

&lt;p&gt;The point is that your business logic should not need to change every time you want to compare models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the model in config
&lt;/h2&gt;

&lt;p&gt;For a real app, I would keep the base URL, API key, and model name in environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash
&lt;span class="nv"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.tokenbay.com/v1
&lt;span class="nv"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_TOKENBAY_API_KEY
&lt;span class="nv"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gpt-5.4-mini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then your application code can stay stable while you test different models.&lt;/p&gt;

&lt;p&gt;Change config, redeploy, compare results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Very boring. Very useful.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When this pattern helps
&lt;/h2&gt;

&lt;p&gt;This setup is useful if you are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;building an AI SaaS product&lt;/li&gt;
&lt;li&gt;comparing cost and quality across models&lt;/li&gt;
&lt;li&gt;using different models for chat, reasoning, extraction, or fallback&lt;/li&gt;
&lt;li&gt;trying to avoid provider-specific code too early&lt;/li&gt;
&lt;li&gt;managing multiple projects or API keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not magically solve model selection. You still need to test output quality, latency, pricing, context length, and reliability.&lt;/p&gt;

&lt;p&gt;But it does make the integration layer much simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  When direct integration may be better
&lt;/h2&gt;

&lt;p&gt;A gateway is not always the right choice.&lt;/p&gt;

&lt;p&gt;Direct provider integration may be better if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you need provider-specific beta features immediately&lt;/li&gt;
&lt;li&gt;you already have enterprise contracts&lt;/li&gt;
&lt;li&gt;your compliance process requires direct vendor relationships&lt;/li&gt;
&lt;li&gt;your app only uses one model and probably always will&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a fair tradeoff.&lt;/p&gt;

&lt;p&gt;The point is not &lt;strong&gt;"always use a gateway."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The point is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are going to test multiple models anyway, your app should not need a rewrite every time.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TokenBay example
&lt;/h2&gt;

&lt;p&gt;TokenBay is an OpenAI-compatible API gateway for accessing models such as GPT, Claude, Gemini, DeepSeek, and others through one endpoint and one API key.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pay-as-you-go billing&lt;/li&gt;
&lt;li&gt;API key management&lt;/li&gt;
&lt;li&gt;usage logs&lt;/li&gt;
&lt;li&gt;per-key limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to test this pattern, you can try TokenBay here:&lt;/p&gt;

&lt;p&gt;[Try TokenBay]&lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Current launch offer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15% off most models&lt;/li&gt;
&lt;li&gt;500 free credits&lt;/li&gt;
&lt;li&gt;invite a friend and get 200 credits each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would love feedback from builders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you prefer direct provider APIs or one OpenAI-compatible endpoint?&lt;/li&gt;
&lt;li&gt;How do you currently compare model cost and quality?&lt;/li&gt;
&lt;li&gt;What would make you trust or not trust an AI model gateway?&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>I got tired of managing separate APIs for GPT, Claude, Gemini, DeepSeek, and Qwen</title>
      <dc:creator>GWEN</dc:creator>
      <pubDate>Fri, 26 Jun 2026 08:29:32 +0000</pubDate>
      <link>https://dev.to/gwenj/i-got-tired-of-managing-separate-apis-for-gpt-claude-gemini-deepseek-and-qwen-52d6</link>
      <guid>https://dev.to/gwenj/i-got-tired-of-managing-separate-apis-for-gpt-claude-gemini-deepseek-and-qwen-52d6</guid>
      <description>&lt;p&gt;I’ve been building with LLM APIs for a while, and one thing that keeps getting annoying is not the models themselves — it’s managing all the different providers.&lt;/p&gt;

&lt;p&gt;OpenAI for one use case, Claude for another, Gemini for long-context tasks, DeepSeek or Qwen for cost-sensitive workflows… and suddenly you’re dealing with different API keys, dashboards, pricing pages, rate limits, billing systems, and slightly different integration patterns.&lt;/p&gt;

&lt;p&gt;At some point, the “AI part” becomes less of the problem. The infrastructure around it starts eating time.&lt;/p&gt;

&lt;p&gt;That’s why I build TokenBay, a unified API platform that lets you access multiple AI models through one API key:&lt;/p&gt;

&lt;p&gt;TokenBay:&lt;br&gt;
&lt;a href="https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content" rel="noopener noreferrer"&gt;https://www.tokenbay.com/?utm_source=devto&amp;amp;utm_medium=community_content&amp;amp;utm_campaign=week1_free_content&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea is simple: instead of wiring your app to each model provider separately, you use one OpenAI-compatible API layer and switch between models depending on the task.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use stronger models for reasoning-heavy tasks&lt;/li&gt;
&lt;li&gt;use cheaper models for summaries, classification, or simple chat&lt;/li&gt;
&lt;li&gt;test GPT, Claude, Gemini, DeepSeek, Qwen, GLM, etc. without rebuilding your integration every time&lt;/li&gt;
&lt;li&gt;manage credits and usage in one place instead of jumping across dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don’t think everyone needs a unified API gateway. If your app only uses one model provider, direct API access is probably cleaner.&lt;/p&gt;

&lt;p&gt;But once you start comparing multiple models, optimizing cost, or building fallback into production workflows, having one API layer starts to make a lot more sense.&lt;/p&gt;

&lt;p&gt;There are also some launch benefits available right now:&lt;/p&gt;

&lt;p&gt;15% off most models&lt;br&gt;
500 free credits&lt;br&gt;
Invite a friend → both get 200 credits&lt;br&gt;
I’m curious how other builders are handling this.&lt;/p&gt;

&lt;p&gt;Are you still integrating directly with each provider, or are you using a unified API gateway for multiple LLMs?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>api</category>
    </item>
  </channel>
</rss>
