<?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: Angelina Gupta</title>
    <description>The latest articles on DEV Community by Angelina Gupta (@angelina_gupta).</description>
    <link>https://dev.to/angelina_gupta</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%2F4144400%2F3c1ce4a4-9d59-42fa-ab10-f2fa00c24c7e.png</url>
      <title>DEV Community: Angelina Gupta</title>
      <link>https://dev.to/angelina_gupta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/angelina_gupta"/>
    <language>en</language>
    <item>
      <title>My AI feature was failing 26% of the time. Nothing looked broken.</title>
      <dc:creator>Angelina Gupta</dc:creator>
      <pubDate>Sat, 26 Sep 2026 13:44:43 +0000</pubDate>
      <link>https://dev.to/angelina_gupta/my-ai-feature-was-failing-26-of-the-time-nothing-looked-broken-3c5f</link>
      <guid>https://dev.to/angelina_gupta/my-ai-feature-was-failing-26-of-the-time-nothing-looked-broken-3c5f</guid>
      <description>&lt;p&gt;I built TaskFlow, a project management app with an AI feature called Quick Add. You type &lt;em&gt;"assign the API docs to Priya by Friday"&lt;/em&gt; and it creates a task with a title, an assignee, and a due date.&lt;/p&gt;

&lt;p&gt;For twelve days in August, it quietly stopped doing that for about a quarter of requests. Users got a task titled &lt;em&gt;"assign the API docs to Priya by Friday"&lt;/em&gt; — no assignee, no date. No error. No crash. Just a worse product.&lt;/p&gt;

&lt;p&gt;My test suite caught it on day one. I didn't look until day twelve. This post is about both of those things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LLM features need their own tests
&lt;/h2&gt;

&lt;p&gt;A normal unit test checks that a function returned. It can't tell you whether the model got the date right. LLM output looks fluent even when it's wrong, so "it didn't crash" means very little.&lt;/p&gt;

&lt;p&gt;So I built an eval harness: &lt;strong&gt;71 test cases across 4 prompt suites&lt;/strong&gt; — quick-add (50), extract-tasks (9), decompose (6), and today (6). Each case is an input plus the fields the model should return.&lt;/p&gt;

&lt;p&gt;A few decisions that turned out to matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It imports the real production prompts&lt;/strong&gt;, not copies. If a prompt changes, the tests test the change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring is deterministic.&lt;/strong&gt; No LLM grading another LLM — dates and assignees either match or they don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clock is pinned&lt;/strong&gt;, so "Friday" means the same date on every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It mirrors production's token limits.&lt;/strong&gt; This one ends up being the whole story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs in GitHub Actions&lt;/strong&gt;: a small subset on every pull request, the full suite nightly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It separates a wrong answer from a failed request.&lt;/strong&gt; Rate-limit errors get retried, so a 429 doesn't show up as a quality failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;Groq retired the model I was using, &lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt;, on August 16. I switched to &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; on August 24.&lt;/p&gt;

&lt;p&gt;The nightly suite went red that same night. It stayed red for twelve consecutive nights before I looked properly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04yl4du1mn4vflezb7gx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04yl4du1mn4vflezb7gx.png" alt="GitHub Actions run history showing the nightly AI evals workflow failing on every scheduled run from August 25 to September 3" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things kept it hidden:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The fallback worked too well.&lt;/strong&gt; When Quick Add couldn't parse the model's output, it used the raw text as the task title. That's the right design — the app stays usable — but it meant there was no error to notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The error looked like someone else's problem.&lt;/strong&gt; 13 of the 15 failures were &lt;code&gt;400 Failed to validate JSON&lt;/code&gt;. That reads like an infrastructure issue, so my first instinct was to blame the provider.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Finding the cause
&lt;/h2&gt;

&lt;p&gt;The error message was a dead end. The distribution wasn't.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Suite&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;&lt;code&gt;max_tokens&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;quick-add&lt;/td&gt;
&lt;td&gt;35/50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;300&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extract-tasks&lt;/td&gt;
&lt;td&gt;9/9&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decompose&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;td&gt;1800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;today&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every failure was in the one suite with the smallest token limit.&lt;/p&gt;

&lt;p&gt;The token data confirmed it. Of the quick-add cases that returned, the five largest completions were &lt;strong&gt;283, 284, 288, 298 and 298&lt;/strong&gt; tokens — jammed right up against the 300 limit. The old model's largest was 64.&lt;/p&gt;

&lt;p&gt;The new model is a &lt;strong&gt;reasoning model&lt;/strong&gt;. It spends tokens thinking before it writes any JSON. With a 300-token cap, it was running out mid-object and returning truncated JSON, which the provider then rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — and what it uncovered
&lt;/h2&gt;

&lt;p&gt;The first fix was one number: &lt;code&gt;max_tokens&lt;/code&gt; from 300 to 900, in two places that must always match — the production controller and the eval harness.&lt;/p&gt;

&lt;p&gt;That exposed three more bugs the 400s had been hiding. Once the model could finish its answers, three date cases returned &lt;code&gt;due: null&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"by end of week"&lt;/li&gt;
&lt;li&gt;"by July 20"&lt;/li&gt;
&lt;li&gt;"in two weeks"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt gave the model a 10-day calendar and told it never to compute weekdays itself. So anything outside that window came back empty. The proof: "in 3 days" and "by July 9" already passed, because both fall inside it.&lt;/p&gt;

&lt;p&gt;I split the rule. Weekday phrases still resolve from the calendar; absolute dates and offsets are computed forward from today. The prompt now says the calendar is &lt;em&gt;"a 10-day window, not a limit on what you may answer."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Priority had a similar gap — it only recognised words like "asap" or "critical", so "this is blocking the release" came back as unstated. It now judges described impact, not tone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick-add went from 35/50 to 50/50. The full suite went from 78.9% to 98.6%.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands today
&lt;/h2&gt;

&lt;p&gt;As of the latest nightly, it's &lt;strong&gt;69/71 — 97.2%&lt;/strong&gt;. Two cases fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One is a genuine quality miss.&lt;/strong&gt; The "today" planner includes a task it should filter out. I haven't fixed it because it's a ranking judgment, not a rule I can state cleanly — and I'd rather keep one honest failure than overfit the prompt to a single test case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other is the same bug coming back.&lt;/strong&gt; Same truncated-JSON signature, now in the "today" suite at its 900-token cap. My fix was too narrow: I raised the limit where it was failing, instead of asking which other endpoints had limits sized for a non-reasoning model. One endpoint is still at 400 tokens with no eval coverage at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell other developers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Graceful fallbacks hide bugs.&lt;/strong&gt; Make your fallback path loud — log it, count it, alert on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the distribution, not the error.&lt;/strong&gt; The error text pointed at the provider. The pattern across suites pointed at the real cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you fix a limit, audit every place with the same assumption.&lt;/strong&gt; A model change breaks assumptions everywhere at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A red test nobody reads isn't a test.&lt;/strong&gt; The harness did its job on night one. The gap was mine.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;TaskFlow is live at &lt;a href="https://taskflow-dpsa.vercel.app/" rel="noopener noreferrer"&gt;taskflow-dpsa.vercel.app&lt;/a&gt;. Code: &lt;a href="https://github.com/angelina10504/taskflow" rel="noopener noreferrer"&gt;github.com/angelina10504/taskflow&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
