Skip to content

fix(ai): groq quota header parsing accepts values it should reject — negative remaining publishes a spent window, partial reset durations publish a wrong resetAt #1793

Closed

Description

@dezren39

fix(ai): groq quota header parsing accepts values it should reject — a negative remaining publishes "-5 / 100 requests remaining" at 100% used, and a partially-parsed reset duration publishes a wrong resetAt

Summary

packages/ai/src/quota/providers/groq.ts is the only quota provider with its
own local numeric parsers (headerNumber, parseGroqResetSeconds), and both
accept malformed wire values that every sibling parser rejects — publishing
wrong quota windows instead of treating the data as absent (the provider's
own honest-absence path, no-quota-surface).

Verified 2026-11-05 on main aa2fe24fe3981756d37452191175e0416d3853ae
(laptop worktree of origin/main) with a probe driving the real exported
provider.fetchQuota / parseGroqResetSeconds
(~/git/fleet/probe-2026-11-05-groq-quota/probe.ts on the laptop).

Evidence

  1. Negative remaining counts are published as a spent window.
    headerNumber (groq.ts:54-59) returns any finite Number(raw) — no
    lower bound. The window guards (groq.ts:105, :117) check only
    limit > 0 and remaining !== null. Probe, headers
    x-ratelimit-limit-requests: 100, x-ratelimit-remaining-requests: -5:
    result ok: true with window
    requests_daily: { usedPercent: 100, valueLabel: "-5 / 100 requests remaining" }
    — the clamp at groq.ts:108 hides the impossibility in the percent while
    the label publishes the negative count verbatim. A negative remaining is
    not a count; the tokens window (groq.ts:115-125) has the identical flaw.

  2. Non-decimal spellings parse as counts. headerNumber uses bare
    Number(): probe x-ratelimit-limit-requests: 0x64 read as limit 100,
    and x-ratelimit-remaining-requests: 1e1 read as remaining 10 (window
    published at 90% used). This is the exact class closed fix(core): ModelRouteLimits.windows reads hex and exponent rate-limit counts as numbers — '0x0' benches a target like a spent window #711 fixed for
    core rate-limit headers (plain decimal only) and that
    script/check-numeric-boundaries exists to keep out. Note: the gate
    disposition for these two Number() sites (groq.ts:46, :57) is
    already tracked in open fix(ci): three gates red on main — PR #1722's quota subcommand landed 16 undispositioned sites, and turso bypasses the PR's own bounded reader #1724 — this issue is the runtime behaviour,
    not the gate bookkeeping.

  3. parseGroqResetSeconds partially consumes malformed durations and
    publishes the truncated value as resetAt.
    The regex
    (groq.ts:45) is unanchored and the loop sums whatever segments match;
    unconsumed input is never checked. Probe results from the real parser:

    • "1h30" → 3600 (the unitless 30 is dropped; the published
      resetAt is 30 minutes early — end-to-end probe: header
      x-ratelimit-reset-requests: 1h30 produced resetAfterSeconds: 3600)
    • "2m59.56" → 120 (the unitless 59.56 tail is dropped)
    • "59s trailing" → 59 (trailing garbage accepted)
    • "-30s" → 30 (the sign is silently discarded)
    • "1m1m" → 120 (duplicate units summed)

    The parser's own contract for unparseable input is null ("soon" →
    null, "" → null, groq.ts:40-42), which correctly yields
    resetAt: null; a partially-valid string is worse than an invalid one
    because it looks authoritative.

Checked and NOT claimed: blank header values are already absent —
headerNumber's if (!raw) falsy check returns null for "", and
Headers normalises a whitespace-only value to "". The #1741
blank-is-zero failure does not reproduce here.

Existing tests pin none of this: packages/ai/test/quota.test.ts
(groq describe, ~:192-200) covers only the happy-path durations
(2m59.56s, 7.66s, 14h12m), null, and "soon".

Why it matters

Groq's windows feed the same quota consumers as every other provider
(aggregator, dashboard, quota-aware placement). A garbage header from a
proxy or a future Groq spelling change currently degrades into a
confidently wrong answer — a pool shown 100% used with a negative
remaining count, or a reset time up to tens of minutes early — instead of
the provider's designed honest absence (no-quota-surface). Sibling
providers all route through the shared toNumber / clamp primitives in
quota/utils.ts; groq is the outlier.

Acceptance criteria

  • headerNumber (or its replacement via the shared quota/utils.ts
    primitives) accepts only plain non-negative decimal counts: a
    negative, hex (0x…), or exponent (1e…) limit/remaining header
    reads as absent, so no window is minted from it (the result falls
    back to no-quota-surface when no valid window remains).
  • parseGroqResetSeconds returns null unless the entire trimmed
    input is a well-formed duration (each segment number+unit, no
    sign, no duplicate-unit requirement beyond full consumption);
    valid spellings 2m59.56s, 7.66s, 14h12m keep their current
    values.
  • New tests in packages/ai/test/quota.test.ts pin: negative
    remaining → no window; hex limit / exponent remaining → no window;
    1h30, 2m59.56, 59s trailing, -30s → null reset (and a
    fetchQuota case showing resetAt: null for a malformed reset
    header with otherwise-valid limit/remaining).
  • Existing groq/quota tests stay green; the fix(ci): three gates red on main — PR #1722's quota subcommand landed 16 undispositioned sites, and turso bypasses the PR's own bounded reader #1724 gate disposition
    for groq.ts:46/:57 is resolved by whichever route the fix takes
    (conversion through shared primitives or an allowlist entry that
    now tells the truth).

Filed by new-issue-creator-opencode; deduped against open/closed search
for "groq quota", "x-ratelimit", "parseGroqResetSeconds" and
"headerNumber" (2026-11-05): #1724 covers only the numeric-gate
disposition for these lines, #711/#672 are the core ModelRouteLimits
header path, #1741 is the shared toNumber — none covers this provider's
local parsers' runtime behaviour.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/aiPackage: ai (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 6, 2026
  2. dezren39 commented on Nov 6, 2026

    @dezren39
    Author

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-opencode)
    • Session / ID: cron job new-issue-creator-opencode, run 2026-11-06T01:35Z (scheduled 2026-11-05 19:35 CST)
    • Found via: filed this run
    • Task: Groq quota provider's local header/duration parsers accept negative, hex and exponent counts and partially consume malformed reset durations, publishing wrong windows/resetAt instead of the provider's no-quota-surface absence. Verified with a laptop probe driving the real provider.fetchQuota / parseGroqResetSeconds on origin/main aa2fe24 (~/git/fleet/probe-2026-11-05-groq-quota/).
    • Expectation: effort/s — tighten headerNumber + parseGroqResetSeconds in packages/ai/src/quota/providers/groq.ts (or route through the shared quota/utils.ts primitives) and pin the cases in packages/ai/test/quota.test.ts; existing groq/quota tests stay green.
    • Links: queue packet enqueued this run to repo-health-opencode-pilot; evidence body in the goal hidden_files/bodies/opencode-groq-quota-header-parsing.md
  3. added
    work/assignedAn agent holds this item via the queue (claimed task)
    queue/claimedA queue worker has claimed the task for this item
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  4. 11 remaining items

  5. dezren39 commented on Nov 6, 2026

    @dezren39
    Author

    agent-note:v1
    Agent: repo-worker-opencode (task 10849, lane opencode-a)
    state: handed_off

    Claimed by repo-worker-opencode; execution-lane assignment returned opencode. Handing off to the laptop opencode claimant, which polls for claimed, opencode-assigned, unbound tasks and launches a bound session to drive this task to its terminal state. No worktree created by this worker.

  6. dezren39 commented on Nov 6, 2026

    @dezren39
    Author

    🤖 Agent note · handed off to opencode claimant

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedA queue worker has claimed the task for this item
    and removed
    queue/claimedA queue worker has claimed the task for this item
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 6, 2026
  8. dezren39 commented on Nov 6, 2026

    @dezren39
    Author

    Fixed and merged via PR #1812 (merge commit 497c359). groq.ts header/duration parsing now goes through a strict plain-count parse (negative/hex/exponent headers read as absent; reset durations require full consumption with each unit at most once, malformed resets publish resetAt: null). Verified on the laptop: bun test test/quota.test.ts 65 pass including 5 new #1793 tests, mutation check 4 fail without the src change, packages/ai typecheck pass. #1724 disposition recorded on that issue (comment 6681142645).

  9. removed
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/aiPackage: ai (pkgs/ or apps/ in this repo)area/typescriptTypeScript code areaeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions