fix(fallback): add insufficient_quota error classification

- Classify OpenAI insufficient_quota errors as rate_limit to trigger fallback
- Add quota pattern matching in JSON error body parsing
- Add quota string pattern matching for error messages
- Remove premature success recording - stream might still error during iteration

Note: Stream-time errors (errors during iteration) are not yet caught by
fallback retry logic. This is a known limitation when errors occur mid-stream
rather than at call time.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Artur Do Lago
2026-01-09 15:49:10 +01:00
parent 3ca5a72a73
commit eaa3df3fe7
2 changed files with 14 additions and 3 deletions
@@ -126,6 +126,9 @@ export namespace FallbackChain {
if (errorMessage.includes("rate") && errorMessage.includes("limit")) {
return "rate_limit"
}
if (errorMessage.includes("insufficient_quota") || errorMessage.includes("quota")) {
return "rate_limit"
}
if (errorMessage.includes("overloaded") || errorMessage.includes("capacity")) {
return "rate_limit"
}
@@ -142,6 +145,10 @@ export namespace FallbackChain {
if (json.error?.type === "too_many_requests" || json.error?.code?.includes("rate_limit")) {
return "rate_limit"
}
// OpenAI insufficient_quota errors should trigger fallback
if (json.error?.type === "insufficient_quota" || json.error?.code === "insufficient_quota") {
return "rate_limit"
}
if (json.error?.type === "server_error" || json.error?.type === "overloaded_error") {
return "unavailable"
}
@@ -160,6 +167,9 @@ export namespace FallbackChain {
if (lowerMessage.includes("too many requests") || lowerMessage.includes("429")) {
return "rate_limit"
}
if (lowerMessage.includes("insufficient_quota") || lowerMessage.includes("exceeded") && lowerMessage.includes("quota")) {
return "rate_limit"
}
if (lowerMessage.includes("overloaded") || lowerMessage.includes("capacity")) {
return "rate_limit"
}
+4 -3
View File
@@ -8,6 +8,7 @@ import { CircuitBreaker } from "./circuit-breaker"
import { FallbackChain } from "./fallback-chain"
import { ModelEquivalence } from "./equivalence"
import z from "zod"
import type { StreamTextResult, ToolSet } from "ai"
/**
* Fallback Orchestrator - Main entry point for LLM streaming with automatic fallback.
@@ -139,9 +140,9 @@ export namespace Fallback {
model: currentModel,
})
// Success - record it and emit event if fallback was used
CircuitBreaker.recordSuccess(currentModel.providerID)
// Don't record success immediately - the stream might still error during iteration
// The circuit breaker will learn from failures; success is the default state
// We only emit fallback notification here since we got a stream back
if (attempt > 0 && fallbackConfig.notifyOnFallback) {
Bus.publish(Event.FallbackUsed, {
sessionID: input.sessionID,