feat(observability): distinguish a rejected credential from an exhausted quota - #9550
Conversation
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-28 08:52:42 UTC
Review summary Nits — 7 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent did not find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9550 +/- ##
==========================================
- Coverage 89.60% 88.70% -0.90%
==========================================
Files 860 860
Lines 110573 110584 +11
Branches 26330 26337 +7
==========================================
- Hits 99078 98098 -980
- Misses 10231 11513 +1282
+ Partials 1264 973 -291
Flags with carried forward coverage won't be shown. Click here to find out more.
|
…ted quota A 401 and a 429 from an AI provider need opposite first responses -- rotate the credential, versus wait for the quota window -- but both surfaced identically as an opaque error string under a single reason-less counter. On the hosted box a burst of claude_code_error_429 read as a dead token and prompted a rotation that could not have helped. classifyProviderFailure maps a failure to credential_invalid / quota_exhausted / timeout / not_configured / other, matching on the structured error shapes this module itself throws rather than free-text provider prose. Anything unrecognised stays 'other': a confidently wrong label sends an operator to the wrong runbook. Exposed as a reason label on a NEW counter rather than a new label on loopover_ai_provider_failures_total, which shipped alert rules and dashboards already query, plus a reason field on the failure log. Two alert rules encode the split: a rejected credential is critical and its runbook points at the rotation path, an exhausted quota is a warning whose runbook says not to rotate. Closes #9549
…t the credential runbook
6e892da to
9244d4d
Compare
|
Conflicts resolved and both CI failures fixed; rebased onto current 1. Unregistered metric. 2. The runbook pointed at tooling that does not exist. The A correction I had to make to my own fix. My first rewrite said a Conflicts. 284 tests green across |
Summary
A provider
401and a provider429need opposite first responses, but every surface we expose rendered them identically —loopover_ai_provider_failures_totalhas no reason dimension, andselfhost_ai_provider_failedcarries only the raw error string. Decoding which one you're looking at meant grepping logs for a status code.That cost us real time: a burst of
claude_code_error_429on the hosted ORB box read as a dead token and prompted a credential rotation. The credential was valid the whole time — a 429 is a quota signal, and a new token on the same account behaves identically.classifyProviderFailuremaps a failure tocredential_invalid/quota_exhausted/timeout/not_configured/other. It matches on the structured error shapes this module itself throws (claude_code_error_<status>, the auth/timeout sentinels) rather than free-text provider prose, which is not a stable contract. The HTTP providers, which report auth failures as prose rather than anerror_NNNcode, get a separate narrower arm.Anything unrecognised stays
other— a confidently wrong label sends an operator to the wrong runbook, which is worse than an honest unknown.Two deliberate choices
A new counter, not a new label.
loopover_ai_provider_failures_totalis already referenced by shipped alert rules and dashboards. Adding areasonlabel would silently multiply it into one series per reason and change what those existing queries return.loopover_ai_provider_failure_reason_total{provider,reason}is purely additive and breaks nothing.Severity encodes the difference.
LoopoverAiProviderCredentialInvalidiscritical— it does not self-heal, and its runbook points straight at the rotation path from #9543 (noting no restart is needed).LoopoverAiProviderQuotaExhaustediswarningwith a longerfor:, since a short burst against a rate limit is normal and self-correcting, and its runbook opens by saying explicitly not to rotate.Validation
npm run typecheck— clean (0 errors)npx vitest run test/unit/selfhost-ai.test.ts— 216 tests passnpm run selfhost:validate-observability— dashboards and alert rules validTests cover every classification arm and both sides of the branches, including: each status code, the prose-based HTTP-provider arm, non-
Errorand nullish rejections, and theotherfallback. Three pin behaviour that matters specifically:quota_exhausted, not as a credential problem — the exact misreading this closes.otherrather than being mislabelled."failed after 401 bytes of output"classifies asother. The status pattern requires theerror_prefix, so a PR number or byte count can never masquerade as an HTTP status.Plus an assertion that the credential never leaks into the new metric's labels.
Closes #9549. Follow-up to #9543.