Repository navigation
Description
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
-
Negative remaining counts are published as a spent window.
headerNumber(groq.ts:54-59) returns any finiteNumber(raw)— no
lower bound. The window guards (groq.ts:105, :117) check only
limit > 0andremaining !== null. Probe, headers
x-ratelimit-limit-requests: 100,x-ratelimit-remaining-requests: -5:
resultok: truewith 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. -
Non-decimal spellings parse as counts.
headerNumberuses bare
Number(): probex-ratelimit-limit-requests: 0x64read as limit 100,
andx-ratelimit-remaining-requests: 1e1read 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-boundariesexists to keep out. Note: the gate
disposition for these twoNumber()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. -
parseGroqResetSecondspartially consumes malformed durations and
publishes the truncated value asresetAt. 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 unitless30is dropped; the published
resetAtis 30 minutes early — end-to-end probe: header
x-ratelimit-reset-requests: 1h30producedresetAfterSeconds: 3600)"2m59.56"→120(the unitless59.56tail 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 sharedquota/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 tono-quota-surfacewhen no valid window remains). -
parseGroqResetSecondsreturnsnullunless the entire trimmed
input is a well-formed duration (each segment number+unit, no
sign, no duplicate-unit requirement beyond full consumption);
valid spellings2m59.56s,7.66s,14h12mkeep their current
values. - New tests in
packages/ai/test/quota.test.tspin: negative
remaining → no window; hex limit / exponent remaining → no window;
1h30,2m59.56,59s trailing,-30s→nullreset (and a
fetchQuota case showingresetAt: nullfor 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
dezren39 commentedon Nov 6, 2026
🤖 Agent note · filed
11 remaining items
dezren39 commentedon Nov 6, 2026
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.dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off to opencode claimant
opencode, lane row 3266)ghread this shift — no held/user-owned/do-not-* labels; fix route was decided in triage 10847 (strict parsing local to groq.ts). No PR exists yet for this task; the bound opencode session owns implementation.handed_offrecorded; task parked 1200s for the claimant; labels synced to parked form.dezren39 commentedon Nov 6, 2026
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.ts65 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).