If your LLM wrapper retries every 429, it is wrong at least once a month. Each provider uses the status for different things, and only the body tells you which.
OpenAI: read error.code
rate_limit_exceeded is a per-minute limit and clears in seconds. insufficient_quota arrives as the same 429 RateLimitError, but means the project is out of credit. Retrying it just loops.
Anthropic: 429 is yours, 529 is theirs
rate_limit_error (429) means your organization’s limits; honour retry-after. overloaded_error (529) means the provider is short on capacity, so retry or fall back to another model.
Gemini: RESOURCE_EXHAUSTED covers both windows
Per-minute and per-day quotas share the same RESOURCE_EXHAUSTED status. If backoff stops working after a few minutes, you have probably hit the daily cap.
One classifier for all three
export function classify(provider, status, body) { if (provider === "openai" && body?.error?.code === "insufficient_quota") return "billing"; if (provider === "anthropic" && status === 529) return "provider-overload"; if (status === 429) return "rate-limit"; if (status >= 500) return "provider-error"; return "fix-request"; } // billing → page a human · rate-limit → backoff · provider-overload → retry, then fall back
Track the three classes as separate metrics. A rising billing count is a finance problem; a rising overload count is a reason to add a second provider.