From eaa3df3fe7290ec9a7fdf8965ad69167d09157f3 Mon Sep 17 00:00:00 2001 From: Artur Do Lago Date: Fri, 9 Jan 2026 15:49:10 +0100 Subject: [PATCH] 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 --- packages/opencode/src/provider/fallback-chain.ts | 10 ++++++++++ packages/opencode/src/provider/fallback.ts | 7 ++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/provider/fallback-chain.ts b/packages/opencode/src/provider/fallback-chain.ts index b2a912eef39..9b64fa35c88 100644 --- a/packages/opencode/src/provider/fallback-chain.ts +++ b/packages/opencode/src/provider/fallback-chain.ts @@ -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" } diff --git a/packages/opencode/src/provider/fallback.ts b/packages/opencode/src/provider/fallback.ts index 596c1fdbb2a..273c87ed006 100644 --- a/packages/opencode/src/provider/fallback.ts +++ b/packages/opencode/src/provider/fallback.ts @@ -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,