Fix cooldown fallback switching across model/runtime fallback hooks

This commit is contained in:
Ravi Tharuma
2026-03-04 18:35:09 +01:00
committed by YeonGyu-Kim
parent eb43bd7c43
commit 512bc05404
11 changed files with 803 additions and 38 deletions
+28
View File
@@ -36,6 +36,20 @@ describe("model-error-classifier", () => {
expect(result).toBe(true)
})
test("treats cooling-down auto-retry messages as retryable", () => {
//#given
const error = {
message:
"All credentials for model claude-opus-4-6-thinking are cooling down [retrying in ~5 days attempt #1]",
}
//#when
const result = shouldRetryError(error)
//#then
expect(result).toBe(true)
})
test("selectFallbackProvider prefers first connected provider in preference order", () => {
//#given
writeFileSync(
@@ -73,4 +87,18 @@ describe("model-error-classifier", () => {
//#then
expect(provider).toBe("anthropic")
})
test("selectFallbackProvider maps opencode fallback to quotio when quotio is connected", () => {
//#given
writeFileSync(
join(TEST_CACHE_DIR, "connected-providers.json"),
JSON.stringify({ connected: ["quotio"], updatedAt: new Date().toISOString() }, null, 2),
)
//#when
const provider = selectFallbackProvider(["opencode"], "quotio")
//#then
expect(provider).toBe("quotio")
})
})
+33
View File
@@ -36,6 +36,11 @@ const RETRYABLE_MESSAGE_PATTERNS = [
"rate_limit",
"rate limit",
"quota",
"quota will reset after",
"usage limit has been reached",
"all credentials for model",
"cooling down",
"exhausted your capacity",
"not found",
"unavailable",
"insufficient",
@@ -55,6 +60,23 @@ const RETRYABLE_MESSAGE_PATTERNS = [
"504",
]
const AUTO_RETRY_GATE_PATTERNS = [
"rate limit",
"quota",
"usage limit",
"limit reached",
"cooling down",
"credentials for model",
"exhausted your capacity",
]
function hasProviderAutoRetrySignal(message: string): boolean {
if (!message.includes("retrying in")) {
return false
}
return AUTO_RETRY_GATE_PATTERNS.some((pattern) => message.includes(pattern))
}
export interface ErrorInfo {
name?: string
message?: string
@@ -79,6 +101,9 @@ export function isRetryableModelError(error: ErrorInfo): boolean {
// Check message patterns for unknown errors
const msg = error.message?.toLowerCase() ?? ""
if (hasProviderAutoRetrySignal(msg)) {
return true
}
return RETRYABLE_MESSAGE_PATTERNS.some((pattern) => msg.includes(pattern))
}
@@ -124,6 +149,14 @@ export function selectFallbackProvider(
const connectedProviders = readConnectedProvidersCache()
if (connectedProviders) {
const connectedSet = new Set(connectedProviders.map(p => p.toLowerCase()))
if (connectedSet.has("quotio")) {
const hasQuotio = providers.some((p) => p.toLowerCase() === "quotio")
const hasOpencode = providers.some((p) => p.toLowerCase() === "opencode")
if (hasQuotio || hasOpencode) {
return "quotio"
}
}
for (const provider of providers) {
if (connectedSet.has(provider.toLowerCase())) {
return provider