You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(observability): distinguish a rejected credential from an exhausted 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
Copy file name to clipboardExpand all lines: prometheus/rules/alerts.yml
+31Lines changed: 31 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -604,6 +604,37 @@ groups:
604
604
description: "Provider {{ $labels.provider }} has failed repeatedly and its circuit breaker is skipping calls fast during its cooldown (sustained 5m)."
605
605
runbook: "Check that provider's credentials/reachability (CLI auth for claude-code/codex, or the configured API key/base URL for HTTP providers) via loopover_ai_provider_failures_total{provider=\"...\"} and recent selfhost_ai_provider_failed logs."
606
606
607
+
# The next two rules exist to separate the only two provider failures whose FIRST RESPONSE differs
608
+
# (#9549). Both previously surfaced as an opaque `claude_code_error_NNN` under the same counter, so a
609
+
# quota exhaustion looked exactly like a dead credential -- and rotating a perfectly good token is the
610
+
# natural, wasted next move. Distinct severities encode the difference: one needs a human NOW, the
611
+
# other resolves itself when the window rolls over.
612
+
- alert: LoopoverAiProviderCredentialInvalid
613
+
# A 401/403 from a provider: the credential is rejected outright. This does NOT self-heal -- every
614
+
# review degrades to the fallback provider (or fails) until someone rotates it. Any occurrence in
615
+
# 15m is actionable; `for: 5m` only filters a single blip during an in-flight rotation.
summary: "loopover AI provider {{ $labels.provider }} credential is being rejected"
622
+
description: "Provider {{ $labels.provider }} returned {{ $value | printf \"%.0f\" }} auth failure(s) in 15m (sustained 5m). The credential is invalid or expired — this will not recover on its own."
623
+
runbook: "Rotate the credential: `./scripts/rotate-secret.sh claude_code_oauth_token` (or the loopover_admin_rotate_secret MCP tool). No restart is needed — the token is re-read per AI call. Confirm recovery via loopover_ai_provider_failure_reason_total{reason=\"credential_invalid\"} going flat."
624
+
625
+
- alert: LoopoverAiProviderQuotaExhausted
626
+
# A 429: the credential is fine, the plan's limit is spent. Rotating it changes NOTHING, so this is a
627
+
# warning and the runbook says so explicitly. Sustained 15m rather than any-occurrence, because a
628
+
# short burst against a rate limit is normal and self-correcting.
summary: "loopover AI provider {{ $labels.provider }} is out of quota"
635
+
description: "Provider {{ $labels.provider }} returned {{ $value | printf \"%.0f\" }} quota/rate-limit rejection(s) in 15m, sustained for 15m. The credential is valid; the plan's limit is spent."
636
+
runbook: "Do NOT rotate the credential — a 429 is a quota signal and a new token on the same account behaves identically. Wait for the window to roll over, raise the plan/limit, or configure a fallback in AI_PROVIDER so reviews degrade instead of failing."
0 commit comments