<?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: Himanshu Sharma</title>
    <description>The latest articles on DEV Community by Himanshu Sharma (@h1manshu01).</description>
    <link>https://dev.to/h1manshu01</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%2F412166%2Fabbbdf27-b0f3-439c-be4f-1ac0c97c88a4.png</url>
      <title>DEV Community: Himanshu Sharma</title>
      <link>https://dev.to/h1manshu01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/h1manshu01"/>
    <language>en</language>
    <item>
      <title>When to Use an AI Agent Instead of a Simple LLM Call</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Tue, 13 Oct 2026 08:57:00 +0000</pubDate>
      <link>https://dev.to/h1manshu01/when-to-use-an-ai-agent-instead-of-a-simple-llm-call-djk</link>
      <guid>https://dev.to/h1manshu01/when-to-use-an-ai-agent-instead-of-a-simple-llm-call-djk</guid>
      <description>&lt;p&gt;The word “agent” is being attached to almost every AI feature right now. A prompt calls a model, the model returns JSON, and suddenly the architecture diagram has an agent in it.&lt;/p&gt;

&lt;p&gt;That label is not harmless. An agent brings a loop, tool access, state, new failure modes, more latency, and a larger surface to evaluate.&lt;/p&gt;

&lt;p&gt;A useful default is much simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the least autonomous architecture that can solve the problem reliably.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes that is one LLM call. Sometimes it is a workflow controlled by your code. An agent becomes useful only when the model must decide what to do next based on what it discovers.&lt;/p&gt;

&lt;p&gt;This article gives you a practical way to choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three architectures that are often confused
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. A direct LLM call
&lt;/h3&gt;

&lt;p&gt;Your application provides an input. The model returns an answer or a structured object. Your code validates the result and continues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input → Model → Validated output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classifying a support ticket&lt;/li&gt;
&lt;li&gt;Extracting fields from an invoice&lt;/li&gt;
&lt;li&gt;Rewriting text for a specific audience&lt;/li&gt;
&lt;li&gt;Generating a summary from supplied documents&lt;/li&gt;
&lt;li&gt;Producing a typed object from a prompt&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There may be retrieval or context preparation before the call. That still does not make the system an agent if the model is not controlling the execution path.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A deterministic workflow
&lt;/h3&gt;

&lt;p&gt;A workflow uses multiple steps, but your application decides the order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input → Classify → Retrieve → Draft → Validate → Output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A model can power one or more steps. The important point is that code owns the control flow.&lt;/p&gt;

&lt;p&gt;For example, a document processing system might:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract text from a file&lt;/li&gt;
&lt;li&gt;Classify the document&lt;/li&gt;
&lt;li&gt;Choose a schema with normal application logic&lt;/li&gt;
&lt;li&gt;Ask a model to extract structured data&lt;/li&gt;
&lt;li&gt;Validate the result&lt;/li&gt;
&lt;li&gt;Send invalid results to review&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is more capable than one model call, but it is still predictable. You can inspect every stage, retry a specific step, and write focused tests for each transition.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A tool using agent
&lt;/h3&gt;

&lt;p&gt;An agent receives a goal and controls at least part of the execution path. It chooses an action, observes the result, and then decides what to do next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Goal → Decide → Use tool → Observe → Decide again → Finish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://openai.com/business/guides-and-resources/a-practical-guide-to-building-ai-agents/" rel="noopener noreferrer"&gt;OpenAI describes an agent&lt;/a&gt; as a system that independently accomplishes tasks and uses an LLM to manage workflow execution. &lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Anthropic makes a similar distinction&lt;/a&gt; between workflows with predefined code paths and agents where the model directs its own process and tool use.&lt;/p&gt;

&lt;p&gt;The idea is closely related to the &lt;a href="https://arxiv.org/abs/2210.03629" rel="noopener noreferrer"&gt;ReAct pattern&lt;/a&gt;, which combines reasoning with actions and observations from an external environment.&lt;/p&gt;

&lt;p&gt;The key difference is control.&lt;/p&gt;

&lt;p&gt;A model inside a workflow follows the path your code selected. An agent helps select the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Direct call&lt;/th&gt;
&lt;th&gt;Fixed workflow&lt;/th&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who controls the next step?&lt;/td&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;Model within guardrails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of model turns&lt;/td&gt;
&lt;td&gt;Usually one&lt;/td&gt;
&lt;td&gt;Known in advance&lt;/td&gt;
&lt;td&gt;Varies by task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool choice&lt;/td&gt;
&lt;td&gt;None or fixed&lt;/td&gt;
&lt;td&gt;Chosen by code&lt;/td&gt;
&lt;td&gt;Chosen at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictability&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High to medium&lt;/td&gt;
&lt;td&gt;Medium to low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;Bounded&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;Bounded&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging&lt;/td&gt;
&lt;td&gt;One request and response&lt;/td&gt;
&lt;td&gt;Inspect each stage&lt;/td&gt;
&lt;td&gt;Inspect the full trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Transformation or judgment&lt;/td&gt;
&lt;td&gt;Known business process&lt;/td&gt;
&lt;td&gt;Open ended investigation and action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is not a maturity ladder. An agent is not the advanced version of every LLM feature. Each architecture solves a different control problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that usually decides it
&lt;/h2&gt;

&lt;p&gt;Ask this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can my application know the next step before it sees the current result?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is yes, keep the control flow in code.&lt;/p&gt;

&lt;p&gt;If the answer is no because the result determines which source to inspect, which tool to use, or whether the plan must change, an agent may be justified.&lt;/p&gt;

&lt;p&gt;Consider a production incident assistant.&lt;/p&gt;

&lt;p&gt;A fixed workflow can always fetch the same dashboard, read the latest deployment, summarize recent logs, and prepare a report. That works when the investigation procedure is stable.&lt;/p&gt;

&lt;p&gt;An agent is useful when the evidence changes the investigation. A database timeout might lead it to query slow operations. A deployment error might lead it to inspect a change set. A regional spike might lead it to compare infrastructure metrics. The next useful action is discovered during the task.&lt;/p&gt;

&lt;p&gt;That is agent shaped work.&lt;/p&gt;

&lt;h2&gt;
  
  
  When one LLM call is enough
&lt;/h2&gt;

&lt;p&gt;Use a direct call when the task has one clear transformation and the application already has the required context.&lt;/p&gt;

&lt;p&gt;Good examples include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Turn these notes into a concise update&lt;/li&gt;
&lt;li&gt;Map this ticket to one of eight categories&lt;/li&gt;
&lt;li&gt;Extract these fields using this schema&lt;/li&gt;
&lt;li&gt;Compare two pieces of text&lt;/li&gt;
&lt;li&gt;Draft a reply from the facts provided&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A direct call is easier to operate because it has a small failure surface. You can validate structured output, set a timeout, retry a safe request, and measure quality against a focused data set.&lt;/p&gt;

&lt;p&gt;Do not add an agent just because the prompt contains several instructions. A capable model can perform substantial reasoning inside one call. Internal reasoning is not the same as autonomous execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a workflow is the better design
&lt;/h2&gt;

&lt;p&gt;Use a workflow when the task has several steps but the allowed path is known.&lt;/p&gt;

&lt;p&gt;Imagine an internal request system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classify the request&lt;/li&gt;
&lt;li&gt;Retrieve the relevant policy&lt;/li&gt;
&lt;li&gt;Draft a response&lt;/li&gt;
&lt;li&gt;Check the response against required rules&lt;/li&gt;
&lt;li&gt;Route uncertain cases to a person&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model still provides valuable judgment. Your code keeps control of sequencing, permissions, retries, and approval.&lt;/p&gt;

&lt;p&gt;Workflows are especially strong when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The steps map to a business process&lt;/li&gt;
&lt;li&gt;Each stage has a clear input and output&lt;/li&gt;
&lt;li&gt;You need predictable latency or cost&lt;/li&gt;
&lt;li&gt;Different failures need different recovery logic&lt;/li&gt;
&lt;li&gt;Compliance requires an auditable path&lt;/li&gt;
&lt;li&gt;A write action must always pass through approval&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A workflow can also include model based routing. If the model chooses one of a small set of validated routes, the overall system can remain deterministic enough to test and operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an agent earns its complexity
&lt;/h2&gt;

&lt;p&gt;An agent becomes useful when several of these conditions are true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The task is open ended and has more than one valid path&lt;/li&gt;
&lt;li&gt;The model must choose among tools using intermediate evidence&lt;/li&gt;
&lt;li&gt;The number of steps cannot be known in advance&lt;/li&gt;
&lt;li&gt;The system must recover from failed actions or incomplete information&lt;/li&gt;
&lt;li&gt;Success can be checked with tests, rules, or other clear evidence&lt;/li&gt;
&lt;li&gt;The available tools and permissions can be tightly bounded&lt;/li&gt;
&lt;li&gt;The value of completing the task exceeds the extra cost and latency&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Strong examples include:&lt;/p&gt;

&lt;h3&gt;
  
  
  Investigating a software failure
&lt;/h3&gt;

&lt;p&gt;The agent can search logs, inspect recent changes, run safe diagnostics, compare symptoms, and revise its theory as evidence arrives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating a codebase toward a clear goal
&lt;/h3&gt;

&lt;p&gt;The agent can inspect the repository, edit files, run tests, study failures, and repeat until the acceptance criteria pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Research across changing sources
&lt;/h3&gt;

&lt;p&gt;The agent can search, open relevant sources, follow useful leads, reject weak evidence, and stop when the research question has enough support.&lt;/p&gt;

&lt;p&gt;In each case, a fixed sequence would either be too rigid or would require you to encode a large decision tree that the model is better placed to navigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of an agent is more than tokens
&lt;/h2&gt;

&lt;p&gt;Every additional turn can add:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Model latency&lt;/li&gt;
&lt;li&gt;Tool latency&lt;/li&gt;
&lt;li&gt;More tokens and provider cost&lt;/li&gt;
&lt;li&gt;Another opportunity to choose the wrong action&lt;/li&gt;
&lt;li&gt;More state to store and inspect&lt;/li&gt;
&lt;li&gt;A larger security boundary&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is also a simple reliability effect. If a ten step process had a 98 percent chance of success at every step, a simplified independent model would give the whole path only about an 82 percent chance of completing without an error.&lt;/p&gt;

&lt;p&gt;Real failures are not independent, so this is not a production formula. It is a reminder that small uncertainties compound across a long run.&lt;/p&gt;

&lt;p&gt;For agents, monitoring one final answer is not enough. You need the trace: model decisions, tool calls, results, guardrails, retries, and handoffs. OpenAI's &lt;a href="https://developers.openai.com/api/docs/guides/agent-evals" rel="noopener noreferrer"&gt;guidance on agent evaluations&lt;/a&gt; recommends evaluating the complete workflow so you can see whether the right tools were selected and whether instructions were followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Autonomy also changes the security model
&lt;/h2&gt;

&lt;p&gt;A direct call produces content. An agent may read private data or take actions in other systems.&lt;/p&gt;

&lt;p&gt;That makes prompt injection, excessive permissions, accidental writes, and data leakage architectural concerns rather than prompt quality concerns.&lt;/p&gt;

&lt;p&gt;OpenAI's &lt;a href="https://developers.openai.com/api/docs/guides/agent-builder-safety" rel="noopener noreferrer"&gt;agent safety guidance&lt;/a&gt; recommends structured data between steps, tool approvals, guardrails, and evaluation of traces. These controls matter because untrusted text can influence later tool calls.&lt;/p&gt;

&lt;p&gt;A useful permission model is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give each tool the narrowest capability it needs&lt;/li&gt;
&lt;li&gt;Separate read tools from write tools&lt;/li&gt;
&lt;li&gt;Validate every tool argument in application code&lt;/li&gt;
&lt;li&gt;Require approval before consequential actions&lt;/li&gt;
&lt;li&gt;Set limits on turns, cost, and elapsed time&lt;/li&gt;
&lt;li&gt;Define when the agent must stop and ask for help&lt;/li&gt;
&lt;li&gt;Keep an audit trail that explains what happened&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An agent should never receive broad access merely because it might be convenient.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an agent is unnecessary
&lt;/h2&gt;

&lt;p&gt;You probably do not need an agent when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One response can solve the task&lt;/li&gt;
&lt;li&gt;The sequence of steps is already known&lt;/li&gt;
&lt;li&gt;Tool choice can be expressed with ordinary conditions&lt;/li&gt;
&lt;li&gt;The result must arrive within a tight latency budget&lt;/li&gt;
&lt;li&gt;Cost must be predictable per request&lt;/li&gt;
&lt;li&gt;A wrong action would be difficult to reverse&lt;/li&gt;
&lt;li&gt;You cannot define what successful completion looks like&lt;/li&gt;
&lt;li&gt;You do not yet have evaluations for the simpler design&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A common warning sign is building an agent loop before establishing a reliable direct call. Start with the smallest working baseline. Measure where it fails. Add retrieval, a validation step, or a fixed workflow if that solves the real problem. Introduce autonomy only when the remaining failure is about choosing the path.&lt;/p&gt;

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

&lt;p&gt;Before building an agent, answer these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What exact outcome must the system produce?&lt;/li&gt;
&lt;li&gt;Can one model call produce it from the available context?&lt;/li&gt;
&lt;li&gt;If not, are the required steps known in advance?&lt;/li&gt;
&lt;li&gt;Does new evidence genuinely need to change the next action?&lt;/li&gt;
&lt;li&gt;Which tools are required, and can their permissions be narrowed?&lt;/li&gt;
&lt;li&gt;How will the system verify progress and completion?&lt;/li&gt;
&lt;li&gt;Which actions require human approval?&lt;/li&gt;
&lt;li&gt;What are the limits for turns, time, tokens, and cost?&lt;/li&gt;
&lt;li&gt;Can a failed action be retried or reversed safely?&lt;/li&gt;
&lt;li&gt;Do you have examples that test both success and failure paths?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result usually points to one of three choices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can one response solve the task?

Yes → Use a direct LLM call

No → Are the steps known in advance?

Yes → Use a deterministic workflow

No → Must intermediate results determine the next tool or action?

Yes → Consider an agent with bounded tools and clear stop rules

No → Simplify the problem before adding autonomy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The best AI architecture is not the one with the most autonomy. It is the one that places judgment in the right layer.&lt;/p&gt;

&lt;p&gt;Let the model generate, interpret, and reason where those abilities create value. Let normal code own the path when the path is already known. Use an agent when the work truly requires exploration, adaptation, and action across tools.&lt;/p&gt;

&lt;p&gt;Start with one call. Add a workflow when the process needs structure. Add an agent when the process itself must be discovered.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>Calendar Math vs. Duration Math: Lessons From an Open-Source Bug</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Sun, 11 Oct 2026 07:19:32 +0000</pubDate>
      <link>https://dev.to/h1manshu01/calendar-math-vs-duration-math-lessons-from-an-open-source-bug-1mpb</link>
      <guid>https://dev.to/h1manshu01/calendar-math-vs-duration-math-lessons-from-an-open-source-bug-1mpb</guid>
      <description>&lt;p&gt;“Repeat every year” sounds like a simple instruction—until the original date is February 29.&lt;/p&gt;

&lt;p&gt;While contributing to &lt;a href="https://github.com/super-productivity/super-productivity" rel="noopener noreferrer"&gt;Super Productivity&lt;/a&gt;, I worked on a recurrence bug involving a task that started on a leap day. In a non-leap year, the application could create an extra overdue task before the next occurrence was due.&lt;/p&gt;

&lt;p&gt;The bug demonstrated an important distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Duration arithmetic measures elapsed time. Calendar arithmetic models human schedules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those concepts overlap on ordinary dates, but they can produce different answers around leap years, month boundaries, daylight-saving transitions, and multi-year recurrence rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/super-productivity/super-productivity/issues/10679" rel="noopener noreferrer"&gt;reported task&lt;/a&gt; repeated yearly from February 29.&lt;/p&gt;

&lt;p&gt;Because February 29 does not exist every year, the application needed a policy for non-leap years. In this case, the intended behavior was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;February 29, 2024
February 28, 2025
February 28, 2026
February 28, 2027
February 29, 2028
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the failing path, setting the day to 29 on a February 2025 date overflowed to March 1. The calculation then stepped back one year while preserving March 1, making March 1, 2024 appear later than the already-created February 29, 2024 occurrence.&lt;/p&gt;

&lt;p&gt;That disagreement caused the application to materialize another overdue 2024 task.&lt;/p&gt;

&lt;p&gt;The visible problem was a duplicate task, but the underlying problem was more interesting: different parts of the scheduling logic disagreed about which calendar occurrence existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript normalizes out-of-range date components
&lt;/h2&gt;

&lt;p&gt;JavaScript’s &lt;code&gt;Date&lt;/code&gt; setters normalize out-of-range components instead of rejecting them.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2025&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// February 1, 2025&lt;/span&gt;

&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;29&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;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// March 1, 2025&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no February 29 in 2025, so JavaScript carries the extra day into March.&lt;/p&gt;

&lt;p&gt;This behavior is valid and sometimes useful. But it is not necessarily the recurrence policy your product intends.&lt;/p&gt;

&lt;p&gt;For a yearly task, several behaviors could be reasonable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move the occurrence to February 28.&lt;/li&gt;
&lt;li&gt;Move it to March 1.&lt;/li&gt;
&lt;li&gt;Skip non-leap years.&lt;/li&gt;
&lt;li&gt;Ask the user to select a policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important part is not choosing one universally correct answer. The important part is making the rule explicit and applying it consistently.&lt;/p&gt;

&lt;p&gt;Super Productivity’s expected rule was to clamp the date to the last valid day of the month: February 28.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calendar math and duration math answer different questions
&lt;/h2&gt;

&lt;p&gt;Consider the phrase “one year later.”&lt;/p&gt;

&lt;p&gt;It could mean an elapsed duration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ONE_YEAR_IN_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;later&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ONE_YEAR_IN_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that assumes every year represents the same duration. Leap years immediately break that assumption.&lt;/p&gt;

&lt;p&gt;It could instead mean a calendar operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep the original month and day.
Move to the target calendar year.
If the day does not exist, apply the recurrence policy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are different models.&lt;/p&gt;

&lt;p&gt;Duration math is appropriate for questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has a 30-minute timeout expired?&lt;/li&gt;
&lt;li&gt;How long did this request take?&lt;/li&gt;
&lt;li&gt;Has a 24-hour retention period passed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Calendar math is more appropriate for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repeat this task every year.&lt;/li&gt;
&lt;li&gt;Run payroll on the last day of each month.&lt;/li&gt;
&lt;li&gt;Renew this subscription every three months.&lt;/li&gt;
&lt;li&gt;Schedule an event every Monday.&lt;/li&gt;
&lt;li&gt;Trigger an anniversary reminder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A recurring task expresses human calendar intent. It is not merely a number of milliseconds after another timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the recurrence interval from the occurrence date
&lt;/h2&gt;

&lt;p&gt;The fix became clearer when I separated the problem into two questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Is this an eligible recurrence year?
&lt;/h3&gt;

&lt;p&gt;Suppose the task started in 2024 and repeats every two years.&lt;/p&gt;

&lt;p&gt;The eligible years should remain anchored to the original year:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2024, 2026, 2028, 2030...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yearsSinceStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidateYear&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startYear&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;isEligibleYear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;yearsSinceStart&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;yearsSinceStart&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;repeatEvery&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses calendar-year positions rather than asking whether a fixed duration has elapsed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What is the actual occurrence in that year?
&lt;/h3&gt;

&lt;p&gt;Once the year is eligible, the application needs to construct a valid date.&lt;/p&gt;

&lt;p&gt;A simplified model looks like this:&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;getOccurrence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetYear&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;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&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;requestedDay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&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;lastValidDay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLastDayOfMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetYear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;targetYear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestedDay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastValidDay&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;This is illustrative pseudocode rather than the project’s implementation.&lt;/p&gt;

&lt;p&gt;For a February 29 start date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Target year: 2025
Requested day: 29
Last valid day in February: 28
Actual occurrence: February 28, 2025
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application can then compare normalized calendar dates in the same timezone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;occurrenceDay&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;todayDay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is safer than comparing against an out-of-range component and relying on automatic normalization. In production code, &lt;code&gt;occurrenceDay&lt;/code&gt; and &lt;code&gt;todayDay&lt;/code&gt; should use the same calendar and timezone policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why multi-year repeats matter
&lt;/h2&gt;

&lt;p&gt;A fix that only handles yearly repetition can still introduce another bug.&lt;/p&gt;

&lt;p&gt;Consider a task that starts on February 29, 2024 and repeats every two years.&lt;/p&gt;

&lt;p&gt;The expected sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;February 29, 2024
February 28, 2026
February 29, 2028
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the algorithm advances from each normalized result, it may accidentally treat February 28 as the new permanent anchor. It can also drift into the wrong recurrence year if eligibility is based on fully elapsed durations rather than calendar-year intervals.&lt;/p&gt;

&lt;p&gt;The original rule therefore needs to remain the source of truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original month: February
Original day: 29
Original year: 2024
Repeat interval: 2 calendar years
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each occurrence should be projected from that rule. It should not be derived solely from the previous adjusted occurrence.&lt;/p&gt;

&lt;p&gt;This was one reason the change needed to cover both the forward and backward recurrence calculations. Fixing only the “next occurrence” path would still leave another part of the system interpreting the same rule differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the boundaries
&lt;/h2&gt;

&lt;p&gt;Date bugs often hide in the boundaries, so testing only an ordinary date such as June 15 would provide little confidence.&lt;/p&gt;

&lt;p&gt;The regression coverage included cases around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;February 28 in a non-leap year&lt;/li&gt;
&lt;li&gt;February 29 in a leap year&lt;/li&gt;
&lt;li&gt;March 1 after the adjusted occurrence&lt;/li&gt;
&lt;li&gt;An occurrence that had already been created&lt;/li&gt;
&lt;li&gt;Yearly repetition&lt;/li&gt;
&lt;li&gt;Two-year repetition&lt;/li&gt;
&lt;li&gt;Forward and backward recurrence calculations&lt;/li&gt;
&lt;li&gt;Different timezone configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One particularly important scenario was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start date: February 29
Non-leap-year occurrence: February 28
Current date: March 1
February 28 occurrence already created
Expected result: no additional occurrence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That test protects the user-visible behavior rather than only checking an internal date transformation.&lt;/p&gt;

&lt;p&gt;It asks the question that matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given the task’s history and today’s date, should another task be created?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer must be no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timezones are a separate source of risk
&lt;/h2&gt;

&lt;p&gt;Leap years were central to this bug, but timezone testing still mattered.&lt;/p&gt;

&lt;p&gt;A calendar date is not always equivalent to a fixed period from another calendar date. Around daylight-saving transitions, a local day may contain 23 or 25 hours.&lt;/p&gt;

&lt;p&gt;That means code like this can also be dangerous for calendar scheduling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextDay&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;It adds exactly 24 elapsed hours. It does not necessarily mean “the same local time tomorrow.”&lt;/p&gt;

&lt;p&gt;Testing the recurrence logic under more than one timezone helped confirm that the result represented the intended calendar date rather than an accidental property of one development environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Recurrence rules are domain logic
&lt;/h3&gt;

&lt;p&gt;“Every year” is not a low-level timestamp operation. It is a product rule that needs defined behavior for invalid dates.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Invalid dates should be handled intentionally
&lt;/h3&gt;

&lt;p&gt;JavaScript’s normalization rules are predictable, but predictable does not mean correct for every domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Preserve the original anchor
&lt;/h3&gt;

&lt;p&gt;Adjusted occurrences should not silently replace the original recurrence rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Forward and backward calculations must agree
&lt;/h3&gt;

&lt;p&gt;If one function searches for the next occurrence while another searches for the newest possible due occurrence, both must implement the same calendar policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Test the dates around the boundary
&lt;/h3&gt;

&lt;p&gt;For a February 29 rule, the most valuable tests are usually February 28, February 29, and March 1—not an arbitrary date in the middle of the year.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Test outcomes, not only transformations
&lt;/h3&gt;

&lt;p&gt;Checking that February 29 becomes February 28 is useful. Checking that no duplicate task is created after that occurrence already exists is better.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical rule
&lt;/h2&gt;

&lt;p&gt;First decide what the value represents. Use duration arithmetic for elapsed time—timeouts, performance measurements, and expiry windows. Use calendar arithmetic for human schedules—billing dates, anniversaries, weekday meetings, and recurring reminders.&lt;/p&gt;

&lt;p&gt;Then define what should happen when the requested calendar date does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This was a small open-source contribution, but it reinforced a broader engineering lesson: date bugs are often specification bugs hiding inside arithmetic.&lt;/p&gt;

&lt;p&gt;The difficult question was not how to manipulate a JavaScript &lt;code&gt;Date&lt;/code&gt;. It was deciding what “repeat every year” meant and ensuring every code path implemented that meaning consistently.&lt;/p&gt;

&lt;p&gt;The proposed fix is available in &lt;a href="https://github.com/super-productivity/super-productivity/pull/10724" rel="noopener noreferrer"&gt;Super Productivity PR #10724&lt;/a&gt;, created for &lt;a href="https://github.com/super-productivity/super-productivity/issues/10679" rel="noopener noreferrer"&gt;issue #10679&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At the time of writing, the pull request is still open.&lt;/p&gt;

&lt;p&gt;If there is one rule worth remembering, it is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use duration arithmetic for elapsed time. Use calendar arithmetic for human schedules.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>javascript</category>
      <category>testing</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>One call, from fetch to a validated object — streaming structured output without the plumbing</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:35:27 +0000</pubDate>
      <link>https://dev.to/h1manshu01/one-call-from-fetch-to-a-validated-object-streaming-structured-output-without-the-plumbing-2jko</link>
      <guid>https://dev.to/h1manshu01/one-call-from-fetch-to-a-validated-object-streaming-structured-output-without-the-plumbing-2jko</guid>
      <description>&lt;p&gt;You asked the model for a JSON object and set &lt;code&gt;stream: true&lt;/code&gt;, because you want the UI (or the next step) to start the moment the first field lands instead of blocking on the last token. Then you're here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&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;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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;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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&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;payload&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[DONE]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;json&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;payload&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="p"&gt;.&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="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// provider-specific&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&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="c1"&gt;// 💥 throws on every partial&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Schema&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;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;json&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 💥 and throws here if the model added a trailing comma&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate problems are tangled together in that loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The transport.&lt;/strong&gt; SSE framing by hand — splitting on newlines, handling a frame split across two network chunks, skipping &lt;code&gt;[DONE]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The partial parse.&lt;/strong&gt; &lt;code&gt;JSON.parse(json)&lt;/code&gt; throws on every chunk until the object is complete, so you get nothing to render in the meantime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The validation.&lt;/strong&gt; At the end you still have to validate — and models emit trailing commas, wrap JSON in Markdown code fences, and send &lt;code&gt;"true"&lt;/code&gt; where you wanted &lt;code&gt;true&lt;/code&gt;, so a strict &lt;code&gt;.parse()&lt;/code&gt; rejects output that's &lt;em&gt;almost&lt;/em&gt; right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of those is a small, well-defined job. I built one tiny zero-dependency package for each — and then one more that wires them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  One call
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;structured&lt;/span&gt; &lt;span class="p"&gt;}&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;trickle-structured&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&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;zod&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;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o&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&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;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;json_object&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="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;partial&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// {}, { id: 42 }, { id: 42, name: "Ad" }, … typed as DeepPartial&amp;lt;User&amp;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;user&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;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// User — validated, and repaired if the model slipped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. No reader loop, no &lt;code&gt;try/catch&lt;/code&gt; around &lt;code&gt;JSON.parse&lt;/code&gt;, no provider-specific envelope code, no separate validation step.&lt;/p&gt;

&lt;h2&gt;
  
  
  One stream, two surfaces
&lt;/h2&gt;

&lt;p&gt;The return value is both an &lt;strong&gt;async iterable&lt;/strong&gt; of partial snapshots and a holder for the &lt;strong&gt;final promise&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&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="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// iterate for live partials…&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;partial&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="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// …and/or await the validated result&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// User&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repairs&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;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// what coerce-json had to fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Each &lt;code&gt;partial&lt;/code&gt; is a &lt;code&gt;DeepPartial&amp;lt;User&amp;gt;&lt;/code&gt; — every field optional at every depth, because the object is still being built. Perfect for rendering skeletons that fill in.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.value&lt;/code&gt; resolves once the stream ends, with the &lt;strong&gt;validated and coerced&lt;/strong&gt; result. If the output can't satisfy the schema even after repair, it rejects with a typed &lt;code&gt;CoercionError&lt;/code&gt; carrying everything that was attempted.&lt;/li&gt;
&lt;li&gt;The underlying fetch runs &lt;strong&gt;exactly once&lt;/strong&gt;, no matter which surface you touch. Awaiting &lt;code&gt;.value&lt;/code&gt; without iterating still drains the stream; iterating without awaiting &lt;code&gt;.value&lt;/code&gt; never leaves a dangling rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It composes three tiny packages
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;trickle-structured&lt;/code&gt; is the capstone of a small stack. The pipeline is literally the dependency list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch → SSE (sse-wire) → parse partial JSON (trickle-json) → validate + repair (coerce-json)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;sse-wire&lt;/a&gt;&lt;/strong&gt; — fetch-based SSE: the POST, custom headers, and &lt;code&gt;AbortSignal&lt;/code&gt; that the browser's &lt;code&gt;EventSource&lt;/code&gt; can't do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;trickle-json&lt;/a&gt;&lt;/strong&gt; — an incremental partial-JSON parser that returns the best valid value on every chunk, in O(n) total regardless of how the bytes are split.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;coerce-json&lt;/a&gt;&lt;/strong&gt; — repairs and coerces almost-valid model JSON to fit your schema (strips fences, fixes &lt;code&gt;"7"&lt;/code&gt;→&lt;code&gt;7&lt;/code&gt;), and logs every change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is useful on its own; &lt;code&gt;trickle-structured&lt;/code&gt; is the one import when you want all three wired correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Providers are handled
&lt;/h2&gt;

&lt;p&gt;OpenAI and Anthropic wrap model output in different SSE envelopes. &lt;code&gt;trickle-structured&lt;/code&gt; decodes both, and &lt;strong&gt;auto-detects&lt;/strong&gt; from the first event when you don't say which:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                          &lt;span class="c1"&gt;// sniffs the first event&lt;/span&gt;
&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// or be explicit&lt;/span&gt;
&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;text&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="c1"&gt;// or bring your own&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tool calls too
&lt;/h2&gt;

&lt;p&gt;More and more, models return structured data as a &lt;strong&gt;tool/function call&lt;/strong&gt;, where the object lands in the call's &lt;code&gt;arguments&lt;/code&gt; rather than in message content. Same pipeline, one flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WeatherArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c&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;f&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="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="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WeatherArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&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="na"&gt;mode&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&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&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_weather&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&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;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// { city: "Paris", units: "c" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using React?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;trickle-structured&lt;/code&gt; is deliberately framework-agnostic — a plain async iterable plus a promise. For React, &lt;a href="https://www.npmjs.com/package/trickle-react" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-react&lt;/code&gt;&lt;/a&gt; wraps the same pipeline in a &lt;code&gt;useStreamingObject&lt;/code&gt; hook with per-field status and lifecycle-aware abort. Same stack, different top layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;trickle-structured
npm &lt;span class="nb"&gt;install &lt;/span&gt;zod   &lt;span class="c"&gt;# optional peer, only if you pass a Zod schema&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/trickle-structured" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/trickle-structured&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/H1manshu01/trickle-structured" rel="noopener noreferrer"&gt;https://github.com/H1manshu01/trickle-structured&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The orchestration layer is ~1.2 kB min+brotli (its three dependencies are separate, deduped packages). ESM + CJS + &lt;code&gt;.d.ts&lt;/code&gt;, Node ≥ 18 and modern browsers, published with provenance. Tested with a 200-run byte-split fuzz that proves the final value is identical no matter how the stream is chunked.&lt;/p&gt;

&lt;p&gt;If it mis-parses something, open an issue with the stream and the schema — a ⭐ is appreciated if it saves you a reader loop and a pile of &lt;code&gt;try/catch&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>llm</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Fit any chat history into the context window, bring your own tokenizer</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Fri, 09 Oct 2026 05:58:21 +0000</pubDate>
      <link>https://dev.to/h1manshu01/fit-any-chat-history-into-the-context-window-bring-your-own-tokenizer-5184</link>
      <guid>https://dev.to/h1manshu01/fit-any-chat-history-into-the-context-window-bring-your-own-tokenizer-5184</guid>
      <description>&lt;p&gt;You ship a chat feature. It works. Then a conversation gets long enough, a couple&lt;br&gt;
of chunky tool results land in the middle, and the next request crosses the&lt;br&gt;
model's context window. You get one of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;400 This model's maximum context length is 128000 tokens, however you requested 131402...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you write a trimmer. Keep the system prompt, keep the last few turns, drop the&lt;br&gt;
oldest stuff until it fits. Easy — except the first version drops the system&lt;br&gt;
prompt by accident, the second one orphans a &lt;code&gt;tool&lt;/code&gt; result from the assistant&lt;br&gt;
call that produced it (which the provider rejects), and the third one is off by&lt;br&gt;
enough tokens that you still get the 400 sometimes. Everyone writes this. Almost&lt;br&gt;
everyone writes it slightly wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trimming shape is always the same
&lt;/h2&gt;

&lt;p&gt;Whatever the app, fitting a history into the window is the same move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the &lt;strong&gt;system prompt&lt;/strong&gt; (it defines the behavior).&lt;/li&gt;
&lt;li&gt;Keep the &lt;strong&gt;most recent turns&lt;/strong&gt; (they are the live conversation).&lt;/li&gt;
&lt;li&gt;Keep anything you &lt;strong&gt;pinned&lt;/strong&gt; (a few-shot example, the current user turn).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop or fold&lt;/strong&gt; the rest until the total is under the budget.&lt;/li&gt;
&lt;li&gt;Never separate a &lt;strong&gt;tool result&lt;/strong&gt; from the &lt;strong&gt;tool call&lt;/strong&gt; it answers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get it wrong and you ship one of three failures: a hard &lt;strong&gt;400&lt;/strong&gt; from the&lt;br&gt;
provider, a conversation that is &lt;strong&gt;silently truncated&lt;/strong&gt; at the wrong place, or a&lt;br&gt;
request that &lt;strong&gt;quietly drops the system prompt&lt;/strong&gt; and changes how the model&lt;br&gt;
behaves. The shape is simple; the edges are where it breaks.&lt;/p&gt;
&lt;h2&gt;
  
  
  context-budgeter
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/context-budgeter" rel="noopener noreferrer"&gt;&lt;code&gt;context-budgeter&lt;/code&gt;&lt;/a&gt; is that shape&lt;br&gt;
done once and correctly. You bring a token counter; it brings the strategies and&lt;br&gt;
a hard guarantee the result is within budget with your pinned messages intact — or&lt;br&gt;
it throws, rather than handing you something over budget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="p"&gt;}&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;context-budgeter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limitFor&lt;/span&gt; &lt;span class="p"&gt;}&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;context-budgeter/models&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="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;dropped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fits&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                          &lt;span class="c1"&gt;// bring your own; a real tokenizer is more accurate&lt;/span&gt;
  &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;limitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reserve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="c1"&gt;// hold back for the reply&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keep-system&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;middle-out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// try in order until it fits&lt;/span&gt;
  &lt;span class="na"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                        &lt;span class="c1"&gt;// always keep the 4 most recent&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fit&lt;/code&gt; is synchronous and deterministic. &lt;code&gt;messages&lt;/code&gt; comes back in original order&lt;br&gt;
with the system prompt still first and the last four turns kept; &lt;code&gt;dropped&lt;/code&gt; is what&lt;br&gt;
was removed; &lt;code&gt;fits&lt;/code&gt; tells you whether it all fit. The input is never mutated, and&lt;br&gt;
every passthrough field on your messages (&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;tool_calls&lt;/code&gt;, &lt;code&gt;tool_call_id&lt;/code&gt;,&lt;br&gt;
anything) is carried through untouched.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bring your own tokenizer — on purpose
&lt;/h2&gt;

&lt;p&gt;There is no universal tokenizer. OpenAI, Anthropic, and Llama all count&lt;br&gt;
differently, real BPE tokenizers are large, and sometimes a provider's&lt;br&gt;
token-count API is the only exact source. So &lt;code&gt;context-budgeter&lt;/code&gt; bundles &lt;strong&gt;no&lt;/strong&gt;&lt;br&gt;
tokenizer and takes a &lt;code&gt;count&lt;/code&gt; function instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;encode&lt;/span&gt; &lt;span class="p"&gt;}&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;gpt-tokenizer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="p"&gt;}&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;context-budgeter&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;// exact, for OpenAI-family models&lt;/span&gt;
&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reserve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gpt-tokenizer&lt;/code&gt; and &lt;code&gt;js-tiktoken&lt;/code&gt; are &lt;strong&gt;counters you pair with&lt;/strong&gt; this library, not&lt;br&gt;
competitors. If you just want to prototype, the optional &lt;code&gt;context-budgeter/models&lt;/code&gt;&lt;br&gt;
module ships a rough &lt;code&gt;estimate&lt;/code&gt; (~4 chars/token) and a small &lt;code&gt;limitFor(model)&lt;/code&gt;&lt;br&gt;
table — both approximate, both deliberately kept out of the core so it stays&lt;br&gt;
tiny (the core is ~971 B min+brotli).&lt;/p&gt;
&lt;h2&gt;
  
  
  Strategies, as a list tried in order
&lt;/h2&gt;

&lt;p&gt;A strategy decides the &lt;strong&gt;order droppable messages are removed in&lt;/strong&gt;. Pass one, or&lt;br&gt;
an array that is applied in sequence until the history fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"drop-oldest"&lt;/code&gt; (default) — classic sliding window, drops from the front.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"middle-out"&lt;/code&gt; — drops from the &lt;strong&gt;center&lt;/strong&gt; outward, so you keep both the oldest
framing and the newest turns and sacrifice the middle.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"keep-system"&lt;/code&gt; — force-pins every system message for the run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"summarize"&lt;/code&gt; — folds the dropped span into a summary (async; see below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;["keep-system", "middle-out", "summarize"]&lt;/code&gt; reads exactly as it should: always&lt;br&gt;
keep system, prefer to drop the middle, and if it still doesn't fit, summarize&lt;br&gt;
what was dropped.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pinning, and the tool-call edge nobody remembers
&lt;/h2&gt;

&lt;p&gt;Pins combine — a message survives if any &lt;code&gt;keep&lt;/code&gt; rule matches it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;system&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="c1"&gt;// keep every system message (default)&lt;/span&gt;
    &lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                              &lt;span class="c1"&gt;// keep a leading few-shot pair&lt;/span&gt;
    &lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                               &lt;span class="c1"&gt;// keep the 4 most recent&lt;/span&gt;
    &lt;span class="na"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;important&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// keep anything your predicate matches&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;And the edge that trips hand-rolled trimmers: a &lt;code&gt;tool&lt;/code&gt; result is grouped with the&lt;br&gt;
assistant message that called it and they &lt;strong&gt;drop together&lt;/strong&gt;. A tool result with no&lt;br&gt;
preceding call is a request the provider rejects — &lt;code&gt;context-budgeter&lt;/code&gt; never&lt;br&gt;
produces one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Summarize instead of discard (async)
&lt;/h2&gt;

&lt;p&gt;When you would rather fold the dropped span into a summary than lose it, set the&lt;br&gt;
&lt;code&gt;"summarize"&lt;/code&gt; strategy and a hook, and call &lt;code&gt;fitAsync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fitAsync&lt;/span&gt; &lt;span class="p"&gt;}&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;context-budgeter&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;r&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;fitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drop-oldest&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;summarize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// drop first; summarize what dropped&lt;/span&gt;
  &lt;span class="na"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;myLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dropped&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 summary is inserted where the dropped span used to be, so order reads&lt;br&gt;
naturally. (&lt;code&gt;fit&lt;/code&gt; throws if a summarize hook would run — a nudge to reach for the&lt;br&gt;
async entry point deliberately.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The guarantees are the point
&lt;/h2&gt;

&lt;p&gt;Covered by 15 tests, including a &lt;code&gt;fast-check&lt;/code&gt; property test over hundreds of&lt;br&gt;
random inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The result &lt;strong&gt;never exceeds the budget&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pinned messages are &lt;strong&gt;never dropped&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If the pinned set &lt;strong&gt;alone&lt;/strong&gt; is over budget, &lt;code&gt;fit&lt;/code&gt; throws a &lt;code&gt;RangeError&lt;/code&gt; — it
never silently returns an over-budget result.&lt;/li&gt;
&lt;li&gt;Order and passthrough fields are preserved; the &lt;strong&gt;input is never mutated&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tool-call and tool-result messages &lt;strong&gt;drop together&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Deterministic and synchronous — the only async is your own &lt;code&gt;summarize&lt;/code&gt; hook.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part of a small line of LLM dev tools
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;context-budgeter&lt;/code&gt; is one of a set of zero-dependency tools, each useful on its&lt;br&gt;
own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/context-budgeter" rel="noopener noreferrer"&gt;&lt;code&gt;context-budgeter&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — fit a
chat history into the context window. &lt;em&gt;(this one)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;&lt;code&gt;sse-wire&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — fetch-based SSE
client: POST, headers, abort, opt-in reconnection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/retry-wire" rel="noopener noreferrer"&gt;&lt;code&gt;retry-wire&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — provider-aware
retry + throttle for the request (429 / 529 / &lt;code&gt;Retry-After&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-json&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — assemble
streamed token deltas into the best valid partial value on every chunk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;&lt;code&gt;coerce-json&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — repair and
coerce that value to your Zod / JSON Schema, logging every fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/trickle-react" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-react&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — React
bindings for the streaming parse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/expect-llm" rel="noopener noreferrer"&gt;&lt;code&gt;expect-llm&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — assertions for
LLM output in tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;context-budgeter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/context-budgeter" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/context-budgeter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/H1manshu01/context-budgeter" rel="noopener noreferrer"&gt;https://github.com/H1manshu01/context-budgeter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies, ESM + CJS, full types, core ~971 B min+brotli,
published with provenance. Runs on Node 18+ and in the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it trims something it shouldn't — or keeps something it shouldn't, or your&lt;br&gt;
budget math disagrees with your tokenizer — open an issue with the messages and&lt;br&gt;
the &lt;code&gt;count&lt;/code&gt; you used. The guarantees are the whole point, so I want to know. A&lt;br&gt;
star helps if it saves you a hand-rolled trimmer.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Make your LLM calls survive 429s and overloads, with any client</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Fri, 09 Oct 2026 05:24:59 +0000</pubDate>
      <link>https://dev.to/h1manshu01/make-your-llm-calls-survive-429s-and-overloads-with-any-client-2lc0</link>
      <guid>https://dev.to/h1manshu01/make-your-llm-calls-survive-429s-and-overloads-with-any-client-2lc0</guid>
      <description>&lt;p&gt;You ship an LLM feature. It works. Then, in production, you start seeing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429 Too Many Requests
529 overloaded_error
503 Service Unavailable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you reach for a retry library — &lt;code&gt;p-retry&lt;/code&gt;, &lt;code&gt;async-retry&lt;/code&gt;, a hand-rolled&lt;br&gt;
backoff loop — and wrap the call. And it mostly works, until you notice it's&lt;br&gt;
retrying a &lt;code&gt;400 Bad Request&lt;/code&gt; (pointless), ignoring the &lt;code&gt;Retry-After: 2&lt;/code&gt; the&lt;br&gt;
server politely sent (so you wait the wrong amount), and hammering a &lt;code&gt;429&lt;/code&gt; that&lt;br&gt;
you could have &lt;em&gt;paced under&lt;/em&gt; in the first place.&lt;/p&gt;

&lt;p&gt;The problem is that an LLM endpoint doesn't fail like a generic HTTP service, and&lt;br&gt;
generic retry libraries don't know the difference.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's actually different about LLM failures
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;429&lt;/strong&gt; might be a quota limit &lt;em&gt;or&lt;/em&gt; a momentary burst. Either way the server
usually tells you how long to wait, in a &lt;code&gt;Retry-After&lt;/code&gt; (seconds or an HTTP
date) or a &lt;code&gt;retry-after-ms&lt;/code&gt; header. A blind exponential backoff ignores it and
guesses.&lt;/li&gt;
&lt;li&gt;Anthropic adds a &lt;strong&gt;529 &lt;code&gt;overloaded_error&lt;/code&gt;&lt;/strong&gt; — "the service is saturated, try
again." That's retryable, and it's a &lt;em&gt;different thing&lt;/em&gt; from a quota &lt;code&gt;429&lt;/code&gt;. Most
retry predicates don't even have 529 in their list.&lt;/li&gt;
&lt;li&gt;Providers report your &lt;strong&gt;remaining budget&lt;/strong&gt; in &lt;code&gt;x-ratelimit-remaining-requests&lt;/code&gt;
/ &lt;code&gt;-tokens&lt;/code&gt; (OpenAI) and &lt;code&gt;anthropic-ratelimit-*&lt;/code&gt; headers. You can pace against
that instead of waiting to be told "no."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The official SDKs know all of this. The &lt;code&gt;openai&lt;/code&gt; and &lt;code&gt;@anthropic-ai/sdk&lt;/code&gt; clients&lt;br&gt;
retry 429/5xx and honor &lt;code&gt;retry-after&lt;/code&gt; out of the box. The catch: &lt;strong&gt;that logic&lt;br&gt;
lives inside the client.&lt;/strong&gt; The moment you call through a gateway, a proxy, your&lt;br&gt;
own &lt;code&gt;fetch&lt;/code&gt;, a second provider, or a streaming client, you leave it behind and&lt;br&gt;
you're back to a generic loop.&lt;/p&gt;
&lt;h2&gt;
  
  
  retry-wire
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/retry-wire" rel="noopener noreferrer"&gt;&lt;code&gt;retry-wire&lt;/code&gt;&lt;/a&gt; is that provider-aware&lt;br&gt;
retry logic, unbundled — a zero-dependency policy you wrap around &lt;strong&gt;any&lt;/strong&gt; &lt;code&gt;fetch&lt;/code&gt;&lt;br&gt;
or &lt;strong&gt;any&lt;/strong&gt; async call. Two functions, same knowledge inside both.&lt;/p&gt;

&lt;p&gt;A drop-in &lt;code&gt;fetch&lt;/code&gt; wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;retryFetch&lt;/span&gt; &lt;span class="p"&gt;}&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;retry-wire&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;rfetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retryFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&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="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rfetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rfetch&lt;/code&gt; has the exact &lt;code&gt;fetch&lt;/code&gt; signature, so it drops in anywhere a &lt;code&gt;fetch&lt;/code&gt; is&lt;br&gt;
accepted. It retries 429 / 529 / 5xx / 408 / 409 and network errors, honors&lt;br&gt;
&lt;code&gt;Retry-After&lt;/code&gt;, drains the response body before retrying so nothing leaks, and&lt;br&gt;
returns non-retryable responses (200, 400, …) untouched — just like &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or wrap an SDK method — anything that returns a promise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withResilience&lt;/span&gt; &lt;span class="p"&gt;}&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;retry-wire&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;out&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;withResilience&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// treats 529 overloaded as retryable, distinct from 429&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;full&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`retry &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;It reads the thrown error's status and headers (matching how the SDK error&lt;br&gt;
objects are shaped), forwards an &lt;code&gt;AbortSignal&lt;/code&gt; to your call, and never retries an&lt;br&gt;
abort or a non-retryable error.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rule packs: the provider knowledge, as data
&lt;/h2&gt;

&lt;p&gt;What makes it provider-aware is a &lt;strong&gt;rule pack&lt;/strong&gt; — plain data, not hardcoded&lt;br&gt;
branching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RulePack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;isRetryable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// is this outcome worth retrying?&lt;/span&gt;
  &lt;span class="nf"&gt;retryAfterMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the server's advised wait&lt;/span&gt;
  &lt;span class="nx"&gt;readLimits&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;RateLimitInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// remaining request/token budget&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;genericPack&lt;/code&gt;, &lt;code&gt;openaiPack&lt;/code&gt;, and &lt;code&gt;anthropicPack&lt;/code&gt; ship built in. &lt;code&gt;anthropicPack&lt;/code&gt;&lt;br&gt;
is the one that knows &lt;code&gt;529&lt;/code&gt;; the OpenAI and Anthropic packs also parse the&lt;br&gt;
provider's rate-limit headers. For a gateway, Bedrock, Vertex, or anything with&lt;br&gt;
its own rules, &lt;code&gt;defineRulePack&lt;/code&gt; gives you a typed custom pack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineRulePack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseRetryAfter&lt;/span&gt; &lt;span class="p"&gt;}&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;retry-wire&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;bedrockPack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineRulePack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;isRetryable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retryAfterMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&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;
  
  
  Don't just react to 429 — avoid it
&lt;/h2&gt;

&lt;p&gt;Retrying reacts to a rate limit. Throttling &lt;em&gt;avoids&lt;/em&gt; it: pace your own calls&lt;br&gt;
under the tier before the provider has to push back. It's opt-in, built on token&lt;br&gt;
buckets over requests- and tokens-per-minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rfetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retryFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&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="na"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rpm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tpm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;estimateTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;countTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="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;A call that would outrun the budget waits on an abortable timer until a bucket&lt;br&gt;
token is available — so you glide under the limit instead of bouncing off it.&lt;/p&gt;
&lt;h2&gt;
  
  
  It composes with sse-wire
&lt;/h2&gt;

&lt;p&gt;If you stream completions, you've probably met&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;&lt;code&gt;sse-wire&lt;/code&gt;&lt;/a&gt; — a &lt;code&gt;fetch&lt;/code&gt;-based SSE client&lt;br&gt;
that reconnects a stream only when the connection actually drops. The two solve&lt;br&gt;
different halves of the same problem: &lt;code&gt;sse-wire&lt;/code&gt; reconnects the &lt;em&gt;stream&lt;/em&gt;;&lt;br&gt;
&lt;code&gt;retry-wire&lt;/code&gt; resilience-wraps the &lt;em&gt;request that opens the stream&lt;/em&gt;, so a 429 or&lt;br&gt;
529 on the way in is retried before a byte streams.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;retryFetch&lt;/span&gt; &lt;span class="p"&gt;}&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;retry-wire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sse&lt;/span&gt; &lt;span class="p"&gt;}&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;sse-wire&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;rfetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retryFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&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="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="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="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&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="nx"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rfetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// open the stream through the resilient fetch&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[DONE]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;retry-wire&lt;/code&gt; is part of a small line of zero-dependency LLM dev tools, each&lt;br&gt;
useful on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/retry-wire" rel="noopener noreferrer"&gt;&lt;code&gt;retry-wire&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — provider-aware
retry + throttle for the request. &lt;em&gt;(this one)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;&lt;code&gt;sse-wire&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — fetch-based SSE
client: POST, headers, abort, opt-in reconnection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-json&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — assemble
streamed token deltas into the best valid partial value on every chunk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;&lt;code&gt;coerce-json&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — repair and
coerce that value to your Zod / JSON Schema, logging every fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/trickle-react" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-react&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — React
bindings for the streaming parse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/expect-llm" rel="noopener noreferrer"&gt;&lt;code&gt;expect-llm&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — assertions for
LLM output in tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not just use the SDK's retries?
&lt;/h2&gt;

&lt;p&gt;Because they only cover calls made &lt;em&gt;through&lt;/em&gt; that SDK. One app that talks to&lt;br&gt;
OpenAI and Anthropic has two retry implementations with different knobs; add a&lt;br&gt;
gateway or a raw &lt;code&gt;fetch&lt;/code&gt; and you have a third, generic one. &lt;code&gt;retry-wire&lt;/code&gt; is one&lt;br&gt;
policy — &lt;code&gt;openai&lt;/code&gt; / &lt;code&gt;anthropic&lt;/code&gt; / &lt;code&gt;generic&lt;/code&gt; packs plus custom — across every&lt;br&gt;
transport: SDKs, gateways, raw &lt;code&gt;fetch&lt;/code&gt;, and streaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;retry-wire
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/retry-wire" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/retry-wire&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/H1manshu01/retry-wire" rel="noopener noreferrer"&gt;https://github.com/H1manshu01/retry-wire&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies, ESM + CJS, full types, ~1.68 kB min+brotli,
published with provenance. Runs on Node ≥ 18, the browser, and Bun.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it retries something it shouldn't — or doesn't retry something it should, or&lt;br&gt;
misreads a &lt;code&gt;Retry-After&lt;/code&gt; — open an issue with the status/headers you saw. The&lt;br&gt;
provider-aware part is the whole point, so I want to know. A star helps if it&lt;br&gt;
saves you a hand-rolled backoff loop.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Assert LLM output in the test runner you already use</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Fri, 09 Oct 2026 05:02:42 +0000</pubDate>
      <link>https://dev.to/h1manshu01/assert-llm-output-in-the-test-runner-you-already-use-120p</link>
      <guid>https://dev.to/h1manshu01/assert-llm-output-in-the-test-runner-you-already-use-120p</guid>
      <description>&lt;p&gt;You have an LLM call in your product, and a test that exercises it. Now you have&lt;br&gt;
to assert on the output. If it is like most LLM output, it comes back looking&lt;br&gt;
something like this:&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="nl"&gt;"total"&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;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&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;JSON, probably valid, maybe wrapped in a markdown fence, with fields you need and&lt;br&gt;
phrases you would rather not see. So you write the assertion by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&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;unknown&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;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/``&lt;/span&gt;&lt;span class="err"&gt;`
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="err"&gt;\&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="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;```/g, ""));
} catch {
  throw new Error("not valid JSON"); // the real error is now gone
}
const result = Invoice.safeParse(parsed);
expect(result.success).toBe(true);   // and when it fails? "expected false to be true"
expect(raw.includes("total")).toBe(true);
expect(raw.includes("as an AI")).toBe(false);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line of that is a small papercut. The &lt;code&gt;try/catch&lt;/code&gt; swallows the parser's&lt;br&gt;
message. &lt;code&gt;expect(result.success).toBe(true)&lt;/code&gt; throws away every per-field reason&lt;br&gt;
Zod just computed. The &lt;code&gt;includes&lt;/code&gt; checks can't tell you &lt;em&gt;which&lt;/em&gt; string was&lt;br&gt;
missing. And you will write it again, slightly differently, in the next test.&lt;/p&gt;

&lt;p&gt;The usual escape hatch is to adopt an eval framework — promptfoo, DeepEval,&lt;br&gt;
Braintrust, Evalite. Those are great tools for a real eval suite. But for&lt;br&gt;
"assert this one response in my unit test," they are a CLI, a config file, maybe&lt;br&gt;
an account, and a second mental model that lives outside the &lt;code&gt;vitest&lt;/code&gt;/&lt;code&gt;jest&lt;/code&gt; run&lt;br&gt;
you already have.&lt;/p&gt;

&lt;p&gt;There is a middle option.&lt;/p&gt;
&lt;h2&gt;
  
  
  expect-llm
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/expect-llm" rel="noopener noreferrer"&gt;&lt;code&gt;expect-llm&lt;/code&gt;&lt;/a&gt; is a set of&lt;br&gt;
&lt;code&gt;expect(...)&lt;/code&gt; matchers for the things LLM output actually gets wrong. Register&lt;br&gt;
them once, then assert inline, in the runner you already run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&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;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&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;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;llmMatchers&lt;/span&gt; &lt;span class="p"&gt;}&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;expect-llm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;llmMatchers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// once, in a setup file&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeValidJSON&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;allowFences&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatchSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// out may be an object OR a JSON string&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContainAll&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;total&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;currency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContainNone&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TODO&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;as an AI&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeOneOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund&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;escalate&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;deny&lt;/span&gt;&lt;span class="dl"&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 whole papercut block from the top collapses into that. And when something&lt;br&gt;
fails, the message tells you what — the missing items, the mismatched field, the&lt;br&gt;
parser's actual error — instead of &lt;code&gt;expected false to be true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;zero runtime dependencies&lt;/strong&gt;, ships ESM + CJS + types, runs on Node &amp;gt;= 18,&lt;br&gt;
and is roughly 1.41 kB min+brotli. &lt;code&gt;.not&lt;/code&gt; works on every matcher.&lt;/p&gt;
&lt;h2&gt;
  
  
  The matchers
&lt;/h2&gt;

&lt;p&gt;Eight matchers. Seven are deterministic; one is the opt-in judge.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;toBeValidJSON(options?)&lt;/code&gt;&lt;/strong&gt; — the value is a parseable JSON string. Pass
&lt;code&gt;{ allowFences: true }&lt;/code&gt; to strip a surrounding markdown code fence first, which
is exactly what models wrap JSON in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;toContainAll&lt;/code&gt; / &lt;code&gt;toContainNone&lt;/code&gt; / &lt;code&gt;toContainAny&lt;/code&gt;&lt;/strong&gt; — substring checks over
the (stringified) value. &lt;code&gt;toContainNone(["as an AI", "TODO"])&lt;/code&gt; is the banned-
phrase guard you keep writing by hand; the failure names the phrase that leaked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;toBeOneOf(allowed)&lt;/code&gt;&lt;/strong&gt; — the value deep-equals one of a closed set. For a
routing or classification decision: &lt;code&gt;toBeOneOf(["refund", "escalate", "deny"])&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;toMatchStructure(reference)&lt;/code&gt;&lt;/strong&gt; — same keys and value &lt;em&gt;types&lt;/em&gt; as a reference,
recursively, ignoring the values and array length. A shape snapshot that
survives a prompt refactor changing the numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;toMatchSchema(schema)&lt;/code&gt;&lt;/strong&gt; — validate against a Zod schema or any
&lt;a href="https://standardschema.dev/" rel="noopener noreferrer"&gt;Standard Schema&lt;/a&gt;. A string value is JSON-parsed
first, fences tolerated — so you assert on the raw output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;toSatisfy(rubric, judge)&lt;/code&gt;&lt;/strong&gt; — the opt-in judge, below.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Assert on raw output, fences and all
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;toBeValidJSON&lt;/code&gt; and &lt;code&gt;toMatchSchema&lt;/code&gt; tolerate a code fence and parse a&lt;br&gt;
string for you, you assert on exactly what the model returned — no pre-cleaning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;```json&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;total&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:42,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;currency&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;```&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeValidJSON&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;allowFences&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="c1"&gt;// passes&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatchSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// parses, unfences, then validates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Shape, not values
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;toMatchStructure&lt;/code&gt; is the one I reach for most. It checks the &lt;em&gt;shape&lt;/em&gt; of a&lt;br&gt;
response — keys and types — so a test does not break every time the model picks a&lt;br&gt;
different number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;active&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;expect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p&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;q&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;active&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatchStructure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// passes: same keys, same types&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;active&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatchStructure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fails: id is a string, not a number&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deterministic by default; the judge is opt-in
&lt;/h2&gt;

&lt;p&gt;Those seven matchers are pure, synchronous, and &lt;strong&gt;make no network call&lt;/strong&gt; — a&lt;br&gt;
guard test fails the build if any of them touches &lt;code&gt;fetch&lt;/code&gt;. Use them for anything&lt;br&gt;
objectively checkable, which is most of what breaks: valid JSON, schema shape,&lt;br&gt;
required and forbidden content, a closed decision set, a stable structure.&lt;/p&gt;

&lt;p&gt;For the subjective checks — "is this a polite refusal?" — there is &lt;code&gt;toSatisfy&lt;/code&gt;,&lt;br&gt;
and it is deliberately &lt;strong&gt;bring your own model&lt;/strong&gt;. &lt;code&gt;expect-llm&lt;/code&gt; ships no SDK, no&lt;br&gt;
key handling, and no default provider. You pass the model call in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Judge&lt;/span&gt; &lt;span class="p"&gt;}&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;expect-llm&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;judge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Judge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rubric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;verdict&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;myModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Does this satisfy "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rubric&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"?\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\nAnswer yes or no.`&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/yes/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verdict&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;verdict&lt;/span&gt; &lt;span class="p"&gt;};&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toSatisfy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is a concise, polite refusal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things fall out of that design. One: the deterministic matchers never cost a&lt;br&gt;
token, so your CI stays fast and reproducible and the model call happens only&lt;br&gt;
where you opt in. Two: the judge is a plain function, so a stub judge lets you&lt;br&gt;
test the &lt;em&gt;wiring&lt;/em&gt; in CI without a network call, and the judge's &lt;code&gt;reason&lt;/code&gt; shows up&lt;br&gt;
in the failure message when the assertion fails.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vitest or Jest — same matchers
&lt;/h2&gt;

&lt;p&gt;Register once in a setup file. The entry point is the only thing that differs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vitest — vitest.setup.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&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;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;llmMatchers&lt;/span&gt; &lt;span class="p"&gt;}&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;expect-llm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;llmMatchers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Jest — jest.setup.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;llmMatchers&lt;/span&gt; &lt;span class="p"&gt;}&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;expect-llm/jest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;llmMatchers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Vitest entry augments Vitest's &lt;code&gt;expect&lt;/code&gt; types; the Jest entry augments&lt;br&gt;
&lt;code&gt;jest.Matchers&lt;/code&gt;. Same matcher set, typed on both.&lt;/p&gt;
&lt;h2&gt;
  
  
  It pairs with coerce-json and zod
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;expect-llm&lt;/code&gt; is the assertion end of a small line of zero-dependency LLM&lt;br&gt;
dev-tools I maintain. In a structured-output flow it sits right after&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;&lt;code&gt;coerce-json&lt;/code&gt;&lt;/a&gt;, which repairs and&lt;br&gt;
coerces almost-valid model output to fit a schema. Coerce first, then assert the&lt;br&gt;
result — &lt;code&gt;toMatchSchema&lt;/code&gt; takes the same Zod schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;coerce&lt;/span&gt; &lt;span class="p"&gt;}&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;coerce-json/zod&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawModelOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatchSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContainAll&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;total&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;currency&lt;/span&gt;&lt;span class="dl"&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 full suite, each piece zero-dependency and useful on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch -&amp;gt; SSE (sse-wire) -&amp;gt; parse partial JSON (trickle-json) -&amp;gt; coerce to schema (coerce-json) -&amp;gt; assert (expect-llm)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;sse-wire&lt;/a&gt;&lt;/strong&gt; — fetch-based SSE client for LLM streams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;trickle-json&lt;/a&gt;&lt;/strong&gt; — incremental partial-JSON parser for those streams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;coerce-json&lt;/a&gt;&lt;/strong&gt; — repair and coerce to your schema, logging every fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/expect-llm" rel="noopener noreferrer"&gt;expect-llm&lt;/a&gt;&lt;/strong&gt; — assert the result in Vitest or Jest. (this one)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; expect-llm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/expect-llm" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/expect-llm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/H1manshu01/expect-llm" rel="noopener noreferrer"&gt;https://github.com/H1manshu01/expect-llm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies, ESM + CJS, full types, published with provenance.
&lt;code&gt;vitest&lt;/code&gt;, &lt;code&gt;jest&lt;/code&gt;, and &lt;code&gt;zod&lt;/code&gt; are optional peers — bring only the ones you use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a matcher's failure message is ever less helpful than the hand-rolled&lt;br&gt;
assertion it replaced, open an issue — readable failures are the whole point. A&lt;br&gt;
star is appreciated if it saves you a pile of &lt;code&gt;try/catch&lt;/code&gt; boilerplate.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stream an LLM object into React, field by field — in one hook</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:05:53 +0000</pubDate>
      <link>https://dev.to/h1manshu01/stream-an-llm-object-into-react-field-by-field-in-one-hook-43jp</link>
      <guid>https://dev.to/h1manshu01/stream-an-llm-object-into-react-field-by-field-in-one-hook-43jp</guid>
      <description>&lt;p&gt;You asked the model for a JSON object and set &lt;code&gt;stream: true&lt;/code&gt; so the UI can fill&lt;br&gt;
in as it arrives. Now you're in the usual mess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;ctrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&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="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 💥 throws on every partial chunk&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="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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JSON.parse&lt;/code&gt; throws until the very last token, you can't tell which fields are&lt;br&gt;
done, and if you're not careful you'll tear or leak the stream. Let's fix it.&lt;/p&gt;
&lt;h2&gt;
  
  
  One hook
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&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;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sse&lt;/span&gt; &lt;span class="p"&gt;}&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;sse-wire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useStreamingObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;openAIContent&lt;/span&gt; &lt;span class="p"&gt;}&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;trickle-react&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;Profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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;ProfileCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStreamingObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;openAIContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Skeleton&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;data-loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;data-loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bio&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bio&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Badge&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;complete&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Badge&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;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;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;data&lt;/code&gt;&lt;/strong&gt; is a &lt;code&gt;DeepPartial&amp;lt;Profile&amp;gt;&lt;/code&gt; that updates on every chunk — never throws.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;status.city&lt;/code&gt;&lt;/strong&gt; turns &lt;code&gt;true&lt;/code&gt; once &lt;code&gt;city&lt;/code&gt; has fully arrived (not just appeared).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;done&lt;/code&gt;&lt;/strong&gt; flips at the end; the stream aborts automatically on unmount or when &lt;code&gt;deps&lt;/code&gt; change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood it uses &lt;code&gt;useSyncExternalStore&lt;/code&gt;, so renders are tearing-free and&lt;br&gt;
React-18 concurrent-safe, and the partial JSON parsing is handled by&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-json&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  It's the top of a small stack
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;trickle-react&lt;/code&gt; is the adoption layer over three other tiny, zero-dependency&lt;br&gt;
packages I built — each solves one step of the streaming structured-output&lt;br&gt;
pipeline, and each is useful on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch → SSE (sse-wire) → parse partial JSON (trickle-json) → repair/coerce (coerce-json) → render (trickle-react)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;sse-wire&lt;/a&gt;&lt;/strong&gt; — fetch-based SSE: the POST + headers &lt;code&gt;EventSource&lt;/code&gt; can't do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;trickle-json&lt;/a&gt;&lt;/strong&gt; — incremental partial-JSON parser; best valid value on every chunk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;coerce-json&lt;/a&gt;&lt;/strong&gt; — repairs &amp;amp; coerces the model's almost-valid JSON to fit your schema, logging every fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't have to adopt the whole stack — &lt;code&gt;trickle-react&lt;/code&gt; only needs&lt;br&gt;
&lt;code&gt;trickle-json&lt;/code&gt;. Bring &lt;code&gt;sse-wire&lt;/code&gt; for the stream and &lt;code&gt;coerce-json&lt;/code&gt;/&lt;code&gt;zod&lt;/code&gt; for&lt;br&gt;
validation when you want them.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's more than one object
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// rows as they stream&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useStreamingList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Suggestion&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suggestions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// function-call arguments as they fill in&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useToolCalls&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;trickle-react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/trickle-react" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/trickle-react&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/H1manshu01/trickle-react" rel="noopener noreferrer"&gt;https://github.com/H1manshu01/trickle-react&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;react&lt;/code&gt; is a peer; &lt;code&gt;zod&lt;/code&gt; and &lt;code&gt;coerce-json&lt;/code&gt; are optional. ~1.5 kB, ESM + CJS,&lt;br&gt;
fully typed, published with provenance. If it mis-renders something, open an&lt;br&gt;
issue with the stream and the schema — ⭐ appreciated if it saves you a pile of&lt;br&gt;
&lt;code&gt;useEffect&lt;/code&gt; plumbing.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>EventSource can't POST. Your LLM stream needs it to.</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Thu, 08 Oct 2026 15:30:44 +0000</pubDate>
      <link>https://dev.to/h1manshu01/eventsource-cant-post-your-llm-stream-needs-it-to-3gll</link>
      <guid>https://dev.to/h1manshu01/eventsource-cant-post-your-llm-stream-needs-it-to-3gll</guid>
      <description>&lt;p&gt;You want to stream a chat completion. The response is a &lt;code&gt;text/event-stream&lt;/code&gt;, so&lt;br&gt;
the browser's built-in &lt;code&gt;EventSource&lt;/code&gt; seems like the obvious tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;es&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;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except that line can't work, and not for a small reason. &lt;code&gt;EventSource&lt;/code&gt; can only&lt;br&gt;
issue a &lt;strong&gt;GET&lt;/strong&gt;, and it &lt;strong&gt;can't set a single header&lt;/strong&gt;. The chat-completions&lt;br&gt;
endpoint needs the exact opposite: a &lt;code&gt;POST&lt;/code&gt;, an &lt;code&gt;Authorization: Bearer …&lt;/code&gt;&lt;br&gt;
header, and a JSON body with your messages. The one tool the platform gives you&lt;br&gt;
for SSE is structurally incapable of making an LLM request.&lt;/p&gt;

&lt;p&gt;So you drop to &lt;code&gt;fetch&lt;/code&gt; — and now you own the parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&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;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="c1"&gt;// now split on \n\n... or was it \r\n\r\n? what about a \r\n split&lt;/span&gt;
  &lt;span class="c1"&gt;// across two chunks? and a multibyte character split mid-sequence?&lt;/span&gt;
  &lt;span class="c1"&gt;// and when I break early, did I cancel the reader, or leak the connection?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those comments is a real bug people ship. This is the part of every&lt;br&gt;
streaming integration nobody writes a blog post about. Let's fix it properly.&lt;/p&gt;
&lt;h2&gt;
  
  
  The usual options (and where they hurt)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Native &lt;code&gt;EventSource&lt;/code&gt;.&lt;/strong&gt; GET only, no headers — a non-starter for LLM APIs.&lt;br&gt;
And when it &lt;em&gt;does&lt;/em&gt; work, it reconnects on &lt;strong&gt;any&lt;/strong&gt; stream end, including a clean&lt;br&gt;
one. For an LLM response that's wrong: a clean end means the model &lt;em&gt;finished&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;@microsoft/fetch-event-source&lt;/code&gt;.&lt;/strong&gt; The well-known "fetch SSE that can POST."&lt;br&gt;
Genuinely good — but it's a callback API (&lt;code&gt;onmessage&lt;/code&gt;, &lt;code&gt;onclose&lt;/code&gt;), and you drive&lt;br&gt;
the reconnection policy and the &lt;code&gt;Last-Event-ID&lt;/code&gt; bookkeeping yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Polyfills — &lt;code&gt;eventsource&lt;/code&gt;, &lt;code&gt;launchdarkly-eventsource&lt;/code&gt;.&lt;/strong&gt; These bring the&lt;br&gt;
&lt;code&gt;EventSource&lt;/code&gt; &lt;em&gt;interface&lt;/em&gt; to Node. That's the catch: you inherit the callback&lt;br&gt;
model, &lt;code&gt;.close()&lt;/code&gt; cancellation, and always-on reconnection — the semantics built&lt;br&gt;
for a persistent stream that &lt;em&gt;should&lt;/em&gt; reconnect, not a one-shot completion that&lt;br&gt;
ends when the answer is done.&lt;/p&gt;

&lt;p&gt;The common thread: either you can't POST, or you get a callback API with&lt;br&gt;
reconnection tuned for the wrong shape of stream.&lt;/p&gt;
&lt;h2&gt;
  
  
  sse-wire
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;&lt;code&gt;sse-wire&lt;/code&gt;&lt;/a&gt; is a zero-dependency,&lt;br&gt;
&lt;code&gt;fetch&lt;/code&gt;-based SSE client you consume with &lt;code&gt;for await&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sse&lt;/span&gt; &lt;span class="p"&gt;}&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;sse-wire&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;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="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="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&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="nx"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt; &lt;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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[DONE]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// provider sentinel — not JSON&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;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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// { event?, data, id? }&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&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;That's the whole surface for the common case. Three things make it pull its&lt;br&gt;
weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It POSTs with headers.&lt;/strong&gt; It's &lt;code&gt;fetch&lt;/code&gt; underneath, so method, body, and
headers are first-class. It auto-sets &lt;code&gt;accept: text/event-stream&lt;/code&gt; if you
didn't, and passes your &lt;code&gt;signal&lt;/code&gt; straight through — so the &lt;code&gt;AbortController&lt;/code&gt;
you already have cancels the request &lt;em&gt;and&lt;/em&gt; the stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's an async generator.&lt;/strong&gt; &lt;code&gt;for await (const event of sse(...))&lt;/code&gt;. &lt;code&gt;break&lt;/code&gt;
when you hit &lt;code&gt;[DONE]&lt;/code&gt;, wrap it in &lt;code&gt;try/finally&lt;/code&gt;, compose it like any other
iterable. No listener wiring, no &lt;code&gt;.close()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A clean end means done.&lt;/strong&gt; When the stream closes normally, the loop ends —
because the model finished. It does &lt;strong&gt;not&lt;/strong&gt; silently reconnect and re-request a
completed generation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Cancellation that doesn't leak
&lt;/h3&gt;

&lt;p&gt;Abort, and &lt;code&gt;sse-wire&lt;/code&gt; throws the standard &lt;code&gt;AbortError&lt;/code&gt;, cancels the in-flight&lt;br&gt;
&lt;code&gt;fetch&lt;/code&gt;, and cancels the stream reader. Same if you just &lt;code&gt;break&lt;/code&gt; out of the loop&lt;br&gt;
early — the reader is cancelled in a &lt;code&gt;finally&lt;/code&gt;, so the connection is never left&lt;br&gt;
dangling. It's covered by tests that assert the reader's &lt;code&gt;cancel()&lt;/code&gt; actually ran&lt;br&gt;
on abort &lt;em&gt;and&lt;/em&gt; on early break.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// or AbortSignal.timeout(5_000)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reconnection, only when it's real
&lt;/h3&gt;

&lt;p&gt;Reconnection is &lt;strong&gt;opt-in&lt;/strong&gt; and fires on &lt;strong&gt;transport errors only&lt;/strong&gt; — a dropped&lt;br&gt;
connection mid-answer, never a clean end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&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="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reconnect&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;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;When the connection actually drops, it waits an equal-jitter exponential backoff&lt;br&gt;
(a server &lt;code&gt;retry:&lt;/code&gt; directive becomes the base delay), re-issues the request with&lt;br&gt;
&lt;code&gt;Last-Event-ID&lt;/code&gt; set to the last event it saw, and keeps yielding into the same&lt;br&gt;
loop. Your consumer never notices the seam.&lt;/p&gt;
&lt;h3&gt;
  
  
  The parser is the hard-won part
&lt;/h3&gt;

&lt;p&gt;The wire format is fiddly, and &lt;code&gt;sse-wire&lt;/code&gt; handles it so you don't: CR / LF /&lt;br&gt;
CRLF terminators (including a &lt;strong&gt;CRLF split across two chunks&lt;/strong&gt;), multi-line&lt;br&gt;
&lt;code&gt;data:&lt;/code&gt; joined with &lt;code&gt;\n&lt;/code&gt;, comments, a leading BOM, a NUL-in-&lt;code&gt;id&lt;/code&gt;, and a final&lt;br&gt;
line with no newline. It buffers across chunk &lt;strong&gt;and&lt;/strong&gt; UTF-8 boundaries, so a&lt;br&gt;
multibyte character split mid-sequence decodes correctly. There's a property&lt;br&gt;
test that splits the same payload at &lt;strong&gt;every single byte offset&lt;/strong&gt; and asserts the&lt;br&gt;
events come out identical to a one-shot parse.&lt;/p&gt;

&lt;p&gt;You can use that parser directly on any byte/text stream, not just a fetch body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseSSE&lt;/span&gt; &lt;span class="p"&gt;}&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;sse-wire&lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;parseSSE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;someByteStream&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// { event?, data, id? }&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  It's the front of a pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sse-wire&lt;/code&gt; is the &lt;strong&gt;transport&lt;/strong&gt; step of a streaming structured-output flow — and&lt;br&gt;
it sits first. Its two siblings pick up from the events it yields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch → SSE (sse-wire) → parse partial JSON (trickle-json) → repair/coerce to schema (coerce-json) → validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;&lt;code&gt;sse-wire&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — POST the request,
stream the events. &lt;em&gt;(this one)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-json&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — assemble the
streamed token deltas into the best valid partial value available on every
chunk, without throwing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;&lt;code&gt;coerce-json&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — repair and
coerce that value to your Zod / JSON Schema, logging every fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's all three together — stream a completion, assemble the JSON as it arrives,&lt;br&gt;
and coerce the result to a schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sse&lt;/span&gt; &lt;span class="p"&gt;}&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;sse-wire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StreamingJsonParser&lt;/span&gt; &lt;span class="p"&gt;}&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;trickle-json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;coerce&lt;/span&gt; &lt;span class="p"&gt;}&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;coerce-json/zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&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;zod&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;Answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sentiment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;positive&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;neutral&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;negative&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;default&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;parser&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;StreamingJsonParser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;snapshot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;renderPreview&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// progressive UI, every chunk&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="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[DONE]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;Answer&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;else&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;could not fully repair:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sse-wire&lt;/code&gt; owns the transport; &lt;code&gt;trickle-json&lt;/code&gt; gives you the best value on every&lt;br&gt;
chunk without throwing; &lt;code&gt;coerce-json&lt;/code&gt; makes it fit your schema and hands you the&lt;br&gt;
receipts. Each is zero-dependency and works on its own — adopt only the piece you&lt;br&gt;
need.&lt;/p&gt;

&lt;p&gt;"But doesn't provider structured output / the official SDK already do this?" The&lt;br&gt;
SDKs help, but they couple the stream to their request lifecycle and their&lt;br&gt;
provider's shapes. &lt;code&gt;sse-wire&lt;/code&gt; is a pure, framework-agnostic &lt;code&gt;fetch&lt;/code&gt; SSE client:&lt;br&gt;
drop it into any pipeline, point it at any &lt;code&gt;text/event-stream&lt;/code&gt; (LLM or not), mock&lt;br&gt;
it in a test, and hand clean events to whatever's next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;sse-wire
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/sse-wire" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/sse-wire&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/H1manshu01/sse-wire" rel="noopener noreferrer"&gt;https://github.com/H1manshu01/sse-wire&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies, ESM + CJS, full types, ~1.4 kB min+brotli,
published with provenance. Runs on Node ≥ 18, the browser, and Bun.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it mishandles some stream it shouldn't — a terminator edge case, a chunk&lt;br&gt;
boundary, an abort that leaks — open an issue with a repro. The parser-parity and&lt;br&gt;
no-leaked-reader guarantees are the whole point, so I want to know. ⭐ appreciated&lt;br&gt;
if it saves you a hand-rolled fetch loop.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your LLM returned almost-valid JSON. Stop hand-patching it.</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Thu, 08 Oct 2026 15:09:22 +0000</pubDate>
      <link>https://dev.to/h1manshu01/your-llm-returned-almost-valid-json-stop-hand-patching-it-2fma</link>
      <guid>https://dev.to/h1manshu01/your-llm-returned-almost-valid-json-stop-hand-patching-it-2fma</guid>
      <description>&lt;p&gt;You asked the model for &lt;code&gt;{ id: number, active: boolean }&lt;/code&gt;. Here's what came&lt;br&gt;
back:&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="nl"&gt;"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;"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;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&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;Every field is &lt;em&gt;almost&lt;/em&gt; right. &lt;code&gt;id&lt;/code&gt; is a string. &lt;code&gt;active&lt;/code&gt; is a string. There's&lt;br&gt;
a &lt;code&gt;role&lt;/code&gt; you never asked for. And the whole thing is wrapped in a markdown code&lt;br&gt;
fence. So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;User&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;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;raw&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 💥&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;throws twice — once on the fence, and again on the types. So you start writing&lt;br&gt;
the patches by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unfenced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/``&lt;/span&gt;&lt;span class="err"&gt;`
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="err"&gt;\&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="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;```/g, "");
const obj = JSON.parse(unfenced);
obj.id = Number(obj.id);
obj.active = obj.active === "true"; // and "yes"? and "on"? and "1"?
// ...and now do it again for the next schema
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part of every LLM integration nobody writes a blog post about. Let's&lt;br&gt;
fix it properly.&lt;/p&gt;
&lt;h2&gt;
  
  
  The usual fixes (and where they hurt)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;z.coerce&lt;/code&gt;.&lt;/strong&gt; Zod can coerce primitives — &lt;code&gt;z.coerce.number()&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;z.coerce.boolean()&lt;/code&gt;. But you opt in field by field, it can't touch the fence or&lt;br&gt;
the prose, and &lt;code&gt;z.coerce.boolean()&lt;/code&gt; just calls &lt;code&gt;Boolean(x)&lt;/code&gt; — so &lt;code&gt;"false"&lt;/code&gt;&lt;br&gt;
becomes &lt;code&gt;true&lt;/code&gt;. Worse: when it does coerce, it does it &lt;strong&gt;silently&lt;/strong&gt;. You can't&lt;br&gt;
log what changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;jsonrepair&lt;/code&gt; / &lt;code&gt;json-repair&lt;/code&gt;.&lt;/strong&gt; Great at fixing broken &lt;em&gt;syntax&lt;/em&gt; — unclosed&lt;br&gt;
braces, missing quotes, trailing commas. But they're &lt;strong&gt;schema-blind&lt;/strong&gt;. Feed one&lt;br&gt;
&lt;code&gt;{"id":"42"}&lt;/code&gt; and it returns &lt;code&gt;{"id":"42"}&lt;/code&gt;, because that's already valid JSON.&lt;br&gt;
The string-vs-number mismatch is exactly the problem, and it can't see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;instructor-js&lt;/code&gt; and re-prompting.&lt;/strong&gt; Validate, and on failure send the error&lt;br&gt;
back to the model to try again. It works — but you've turned a &lt;code&gt;"42"&lt;/code&gt; → &lt;code&gt;42&lt;/code&gt;&lt;br&gt;
cast, which is a few microseconds of local work, into another network round&lt;br&gt;
trip, more tokens, and a non-deterministic retry.&lt;/p&gt;

&lt;p&gt;The common thread: each tool owns one slice. Cast primitives, &lt;em&gt;or&lt;/em&gt; fix syntax,&lt;br&gt;
&lt;em&gt;or&lt;/em&gt; re-prompt. None of them read the schema you already have, coerce the value&lt;br&gt;
to fit it, and &lt;strong&gt;tell you what they changed&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  coerce-json
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;&lt;code&gt;coerce-json&lt;/code&gt;&lt;/a&gt; is a&lt;br&gt;
zero-dependency library that takes almost-valid model output and makes it fit&lt;br&gt;
your schema — reporting every fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;coerce&lt;/span&gt; &lt;span class="p"&gt;}&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;coerce-json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&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;zod&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;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;```

json&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;{"id":"42","active":"true"}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;

```&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// value   → { id: 42, active: true, role: "user" }&lt;/span&gt;
&lt;span class="c1"&gt;// ok      → true&lt;/span&gt;
&lt;span class="c1"&gt;// changes → strip-fence, string-&amp;gt;number @ id, string-&amp;gt;boolean @ active, fill-default @ role&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things make that line pull its weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's schema-aware.&lt;/strong&gt; It reads your Zod schema (or a JSON Schema, or its own
core spec) and coerces &lt;em&gt;toward it&lt;/em&gt; — the fence comes off, &lt;code&gt;"42"&lt;/code&gt; becomes a
number because &lt;code&gt;id&lt;/code&gt; wants a number, the missing &lt;code&gt;role&lt;/code&gt; gets its documented
default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It reports every fix.&lt;/strong&gt; &lt;code&gt;changes&lt;/code&gt; is an ordered, auditable log. A coercion
is never a silent black box — you can log it, warn on it, or gate on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never fabricates.&lt;/strong&gt; The only values it adds are &lt;strong&gt;documented schema
defaults&lt;/strong&gt;, and each one is logged. A missing optional field is left absent,
not guessed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How much does that actually buy you? I built a corpus of 37 representative LLM&lt;br&gt;
mistakes — numbers as strings, &lt;code&gt;"yes"&lt;/code&gt; booleans, fenced and prose-wrapped&lt;br&gt;
objects, enum casing, missing defaults. Under plain Zod, &lt;strong&gt;13.5%&lt;/strong&gt; of them&lt;br&gt;
validate. Run them through &lt;code&gt;coerce&lt;/code&gt; with defaults, and &lt;strong&gt;86.5%&lt;/strong&gt; validate. Flip&lt;br&gt;
on &lt;code&gt;fuzzy: true&lt;/code&gt; and it's &lt;strong&gt;100%&lt;/strong&gt;. (It's a hand-built, illustrative corpus, not&lt;br&gt;
a scientific sample — the harness and the full breakdown are in&lt;br&gt;
&lt;a href="https://github.com/H1manshu01/coerce-json/blob/main/BENCHMARKS.md" rel="noopener noreferrer"&gt;BENCHMARKS.md&lt;/a&gt;,&lt;br&gt;
so re-run it before quoting the numbers.)&lt;/p&gt;
&lt;h3&gt;
  
  
  It handles what models actually emit
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// prose-wrapped output&lt;/span&gt;
&lt;span class="nf"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sure! Here it is: {"a":"1"}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="c1"&gt;// → { a: 1 }   (extract-json, then string-&amp;gt;number)&lt;/span&gt;

&lt;span class="c1"&gt;// booleans the way models spell them&lt;/span&gt;
&lt;span class="nf"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"active":"yes"}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt; &lt;span class="c1"&gt;// → { active: true }&lt;/span&gt;

&lt;span class="c1"&gt;// enum casing, always safe&lt;/span&gt;
&lt;span class="nf"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"status":"ACTIVE"}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&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;inactive&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;// → { status: "active" }   (case-insensitive, on by default)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Lossier guesses — enum &lt;em&gt;near-misses&lt;/em&gt; (&lt;code&gt;"activ"&lt;/code&gt; → &lt;code&gt;"active"&lt;/code&gt;) and key re-casing&lt;br&gt;
(&lt;code&gt;first_name&lt;/code&gt; → &lt;code&gt;firstName&lt;/code&gt;) — are &lt;strong&gt;opt-in&lt;/strong&gt; behind &lt;code&gt;{ fuzzy: true }&lt;/code&gt;, because&lt;br&gt;
they can change meaning. And an ambiguous fuzzy match is refused, not guessed.&lt;/p&gt;
&lt;h3&gt;
  
  
  JSON Schema, too
&lt;/h3&gt;

&lt;p&gt;Not a Zod shop? Same API, with an optional Ajv hook for authoritative&lt;br&gt;
validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;coerceWithAjv&lt;/span&gt; &lt;span class="p"&gt;}&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;coerce-json/json-schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"id":"5","active":"yes"}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&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;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;additionalProperties&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="c1"&gt;// → { id: 5, active: true, role: "user" }, ok: true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part I care about: trust
&lt;/h2&gt;

&lt;p&gt;If a library silently rewrites your data, you can't trust it in a pipeline. So&lt;br&gt;
&lt;code&gt;coerce-json&lt;/code&gt; holds four invariants, checked by property tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never fabricates&lt;/strong&gt; beyond documented defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every mutation is logged&lt;/strong&gt; — &lt;code&gt;changes&lt;/code&gt; is empty &lt;em&gt;if and only if&lt;/em&gt; the output
equals the input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent&lt;/strong&gt; — coercing an already-valid value is a no-op, and re-coercing
any output changes nothing (it reaches a fixpoint).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prototype-pollution safe&lt;/strong&gt; — &lt;code&gt;__proto__&lt;/code&gt; / &lt;code&gt;constructor&lt;/code&gt; / &lt;code&gt;prototype&lt;/code&gt; keys
are dropped and logged.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  It's the second half of a pipeline
&lt;/h2&gt;

&lt;p&gt;This is a companion to &lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-json&lt;/code&gt;&lt;/a&gt;,&lt;br&gt;
my incremental partial-JSON parser for LLM streams. Together they're the&lt;br&gt;
backbone of a streaming structured-output flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch → SSE → parse partial JSON (trickle-json) → repair/coerce to schema (coerce-json) → validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;trickle-json&lt;/code&gt; gives you the best value available on every streamed chunk&lt;br&gt;
without throwing; &lt;code&gt;coerce-json&lt;/code&gt; makes that value fit your schema and hands you&lt;br&gt;
the receipts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StreamingJsonParser&lt;/span&gt; &lt;span class="p"&gt;}&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;trickle-json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;coerce&lt;/span&gt; &lt;span class="p"&gt;}&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;coerce-json/zod&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;parser&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;StreamingJsonParser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;snapshot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;renderPreview&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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;Answer&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;else&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;could not fully repair:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"But provider structured outputs already fix this!" — they help, a lot. But they&lt;br&gt;
don't cover local and open models, older endpoints, streamed partials, or&lt;br&gt;
anything wrapped in prose or a fence. That's exactly &lt;code&gt;coerce-json&lt;/code&gt;'s niche — and&lt;br&gt;
when the output &lt;em&gt;is&lt;/em&gt; already clean, it's a near no-op, so it's safe to leave in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;coerce-json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/coerce-json" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/coerce-json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/H1manshu01/coerce-json" rel="noopener noreferrer"&gt;https://github.com/H1manshu01/coerce-json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies, ESM + CJS, full types, published with provenance.
&lt;code&gt;zod&lt;/code&gt; and &lt;code&gt;ajv&lt;/code&gt; are optional peers — bring them only if you use them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it mangles some input it shouldn't, open an issue with the string and the&lt;br&gt;
schema — the change log and the "never fabricate" guarantee are the whole point,&lt;br&gt;
so I want to know. ⭐ appreciated if it saves you a pile of hand-written casts.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>JSON.parse throws on every token your LLM streams. Here's a cleaner fix.</title>
      <dc:creator>Himanshu Sharma</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:29:25 +0000</pubDate>
      <link>https://dev.to/h1manshu01/jsonparse-throws-on-every-token-your-llm-streams-heres-a-cleaner-fix-4go8</link>
      <guid>https://dev.to/h1manshu01/jsonparse-throws-on-every-token-your-llm-streams-heres-a-cleaner-fix-4go8</guid>
      <description>&lt;p&gt;If you've ever streamed structured output from an LLM, you've met this bug:&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="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;part&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;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;part&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="p"&gt;?.&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="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 💥 SyntaxError on literally every chunk&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;A model emits JSON one token at a time. Until the &lt;strong&gt;very last&lt;/strong&gt; token arrives, the buffer is syntactically broken — &lt;code&gt;{"city":"Par&lt;/code&gt; isn't valid JSON — so &lt;code&gt;JSON.parse&lt;/code&gt; throws on every intermediate chunk. You want to show the UI filling in as it streams; the parser wants a complete document.&lt;/p&gt;

&lt;p&gt;Let's look at the usual workarounds, why they fall short, and a small library I built to do it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual fixes (and where they hurt)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. "Just parse at the end."&lt;/strong&gt; Accumulate everything, parse once when the stream finishes. Correct — but you've thrown away the entire point of streaming. The user stares at a spinner while a 2 KB object generates token by token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Split on &lt;code&gt;}&lt;/code&gt;.&lt;/strong&gt; Tempting for a streamed array, but it breaks the instant an element contains a nested object or a &lt;code&gt;}&lt;/code&gt; inside a string. You end up re-implementing a JSON tokenizer badly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Repair-then-parse&lt;/strong&gt; (&lt;code&gt;jsonrepair&lt;/code&gt; and friends). Close the open braces/quotes, then &lt;code&gt;JSON.parse&lt;/code&gt;. This mostly works, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;re-parses the whole buffer on every chunk&lt;/strong&gt; — that's O(n²) over a stream. On a long response it gets genuinely slow.&lt;/li&gt;
&lt;li&gt;Repair &lt;strong&gt;guesses&lt;/strong&gt;. It can briefly surface values that aren't actually in the final object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. The incumbent, &lt;code&gt;partial-json&lt;/code&gt;.&lt;/strong&gt; It's the de-facto choice (~35M downloads/month) — but it hasn't shipped a release since May 2024 and is still on &lt;code&gt;0.1.x&lt;/code&gt;. And the better ideas (progressive snapshots, path subscriptions, schema typing, provider adapters) are scattered across a handful of tiny single-purpose packages.&lt;/p&gt;

&lt;p&gt;These aren't hypothetical rough edges. Two real bugs from the Vercel AI SDK tracker show exactly what goes wrong when a parseability check meets a half-streamed buffer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/vercel/ai/issues/12052" rel="noopener noreferrer"&gt;#12052&lt;/a&gt; — a parseability check passes on a &lt;strong&gt;truncated&lt;/strong&gt; tool-call argument, emitting a tool call whose input is still a half-string (&lt;code&gt;Unterminated string in JSON at position 3337&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vercel/ai/issues/6687" rel="noopener noreferrer"&gt;#6687&lt;/a&gt; — an empty arguments string &lt;code&gt;""&lt;/code&gt; is treated as unparseable, so a zero-arg tool call never fires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread: deciding "is this done?" by asking "does it parse yet?" is fragile. You want a parser that always returns the best valid value so far and &lt;em&gt;never throws&lt;/em&gt; — so you can call it on every chunk without a &lt;code&gt;try/catch&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  trickle-json
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;&lt;code&gt;trickle-json&lt;/code&gt;&lt;/a&gt; is a zero-dependency, incremental partial-JSON parser built for LLM streams. The core idea:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parsePartial&lt;/span&gt; &lt;span class="p"&gt;}&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;trickle-json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;parsePartial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"user":{"name":"Ja&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → { user: { name: "Ja" } }&lt;/span&gt;
&lt;span class="nf"&gt;parsePartial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[1,2,3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// → [1, 2, 3]&lt;/span&gt;
&lt;span class="nf"&gt;parsePartial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;// → undefined  (never throws)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns the &lt;strong&gt;most-complete valid value available right now&lt;/strong&gt;. A truncated string comes back as the partial string; a member whose value hasn't arrived yet is dropped rather than guessed; empty input is &lt;code&gt;undefined&lt;/code&gt;, not an exception.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming, with progressive snapshots
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StreamingJsonParser&lt;/span&gt; &lt;span class="p"&gt;}&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;trickle-json&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;parser&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;StreamingJsonParser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;snapshot&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;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// best-effort value after each chunk&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;part&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="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;part&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="p"&gt;?.&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="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;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&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;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  React to one field, or to a list filling in
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fires as a single field grows&lt;/span&gt;
&lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&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;choices[0].message.content&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;appendToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// wildcards + per-element "append" events for streaming lists&lt;/span&gt;
&lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&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;items[*].id&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;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;segments&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="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;append&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;items&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;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;addRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Provider adapters (SSE + tool calls)
&lt;/h3&gt;

&lt;p&gt;Parse a raw &lt;code&gt;text/event-stream&lt;/code&gt; body directly, including tool/function-call arguments as they stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openAISSEToChunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;streamOpenAIToolCalls&lt;/span&gt; &lt;span class="p"&gt;}&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;trickle-json/openai&lt;/span&gt;&lt;span class="dl"&gt;"&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;calls&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;streamOpenAIToolCalls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;openAISSEToChunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// calls[0].name, calls[0].arguments — arguments fill in as they arrive&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a matching &lt;code&gt;trickle-json/anthropic&lt;/code&gt; (text + &lt;code&gt;input_json_delta&lt;/code&gt; tool-use), and a &lt;code&gt;trickle-json/zod&lt;/code&gt; entry point for typed partial snapshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm proud of: it's O(n), not O(n²)
&lt;/h2&gt;

&lt;p&gt;The naive approach re-parses the whole accumulated buffer on every chunk. &lt;code&gt;trickle-json&lt;/code&gt; uses a &lt;strong&gt;resumable, character-at-a-time parser&lt;/strong&gt; that keeps its state across chunks and mutates a live value tree in place — so feeding N characters total costs O(N), no matter how they're split.&lt;/p&gt;

&lt;p&gt;Streaming a growing document in small chunks (reproduce with &lt;code&gt;npm run bench:stream&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;records&lt;/th&gt;
&lt;th&gt;trickle-json (incremental)&lt;/th&gt;
&lt;th&gt;re-parse each chunk&lt;/th&gt;
&lt;th&gt;jsonriver&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.0 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;182 ms&lt;/td&gt;
&lt;td&gt;2.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.9 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,927 ms&lt;/td&gt;
&lt;td&gt;5.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.7 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12,347 ms&lt;/td&gt;
&lt;td&gt;9.0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 2,000 records the incremental engine is &lt;strong&gt;~2,150× faster&lt;/strong&gt; than re-parsing the buffer on every chunk — the quadratic cost simply disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correct, and tested like it
&lt;/h2&gt;

&lt;p&gt;Partial parsing is only useful if it's &lt;em&gt;right&lt;/em&gt;. The streaming engine is verified equivalent to the one-shot parser on &lt;strong&gt;every prefix&lt;/strong&gt; of a broad corpus plus 200 randomly generated documents, with a never-throw guarantee checked against an adversarial/garbage corpus. Against the incumbents on a prefix-by-prefix sweep (&lt;code&gt;npm run bench&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;parser&lt;/th&gt;
&lt;th&gt;throws on truncation&lt;/th&gt;
&lt;th&gt;no invented data&lt;/th&gt;
&lt;th&gt;empty input&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;trickle-json&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;99.3%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;partial-json&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;99.4%&lt;/td&gt;
&lt;td&gt;throws&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;best-effort-json-parser&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;""&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;trickle-json&lt;/code&gt; is the only one that never throws on truncation &lt;em&gt;and&lt;/em&gt; returns &lt;code&gt;undefined&lt;/code&gt; (not an exception) on empty input — so it's safe to call on every chunk.&lt;/p&gt;

&lt;p&gt;(One honest caveat shared by every snapshot parser: a number truncated mid-exponent, like &lt;code&gt;1.5e-1&lt;/code&gt; as a prefix of &lt;code&gt;1.5e-10&lt;/code&gt;, transiently reads as &lt;code&gt;0.15&lt;/code&gt;. It resolves as soon as the next digit arrives.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;trickle-json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/trickle-json" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/trickle-json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/H1manshu01/trickle-json" rel="noopener noreferrer"&gt;https://github.com/H1manshu01/trickle-json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies, ~2.6 kB min+gzip, ESM + CJS, full types, published with provenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building streaming AI UIs, give it a spin — and if it breaks on some input, open an issue with the string; the never-throw guarantee is the whole point, so I want to know. ⭐ appreciated if it saves you a &lt;code&gt;try/catch&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
