fix(model-fallback): add HTTP statusCode check for GLM rate limit fallback

isRetryableModelError() now checks the HTTP status code (429/503/529)
in addition to existing message pattern matching. This ensures rate
limit errors trigger model fallback regardless of error message format
or language (e.g., Chinese GLM errors).

Changes:
- ErrorInfo interface extended with statusCode?: number
- isRetryableModelError() checks statusCode after STOP patterns, before
  message pattern fallback
- extractErrorStatusCode() added to error-classifier.ts (supports
  statusCode, status, code, response.status fields)
- GLM-specific STOP patterns added: daily call limit, in arrears,
  fair use policy, recharge and try — these prevent quota/billing 429s
  from being treated as transient rate limits
- statusCode propagated through tryFallbackRetry and manager.ts

400 intentionally excluded from statusCode check (permanent client error).
This commit is contained in:
cailgarrisk-collab
2026-05-03 11:59:13 +02:00
parent 9ba3b574a7
commit 61d2f1195b
5 changed files with 187 additions and 4 deletions
@@ -65,6 +65,33 @@ export function extractErrorMessage(error: unknown): string | undefined {
}
}
export function extractErrorStatusCode(error: unknown): number | undefined {
if (!isRecord(error)) return undefined
for (const key of ["statusCode", "status", "code"]) {
const val = (error as Record<string, unknown>)[key]
if (typeof val === "number" && val >= 100 && val < 600) return val
}
const statusVal = (error as Record<string, unknown>)["status"]
if (typeof statusVal === "string") {
const parsed = parseInt(statusVal, 10)
if (parsed >= 100 && parsed < 600) return parsed
}
const responseRaw = (error as Record<string, unknown>)["response"]
if (isRecord(responseRaw)) {
const respStatus = responseRaw["status"]
if (typeof respStatus === "number" && respStatus >= 100 && respStatus < 600) return respStatus
if (typeof respStatus === "string") {
const parsed = parseInt(respStatus, 10)
if (parsed >= 100 && parsed < 600) return parsed
}
}
return undefined
}
interface EventPropertiesLike {
[key: string]: unknown
}
@@ -19,7 +19,7 @@ function canonicalizeModelID(modelID: string): string {
export async function tryFallbackRetry(args: {
task: BackgroundTask
errorInfo: { name?: string; message?: string }
errorInfo: { name?: string; message?: string; statusCode?: number }
source: string
concurrencyManager: ConcurrencyManager
client: OpencodeClient
+6 -2
View File
@@ -46,6 +46,7 @@ import {
isAbortedSessionError,
extractErrorName,
extractErrorMessage,
extractErrorStatusCode,
getSessionErrorMessage,
isRecord,
} from "./error-classifier"
@@ -749,6 +750,7 @@ The fallback retry session is now created and can be inspected directly.
const errorInfo = {
name: extractErrorName(error),
message: extractErrorMessage(error),
statusCode: extractErrorStatusCode(error),
}
if (await this.tryFallbackRetry(existingTask, errorInfo, "promptAsync.launch")) {
return
@@ -1079,6 +1081,7 @@ The fallback retry session is now created and can be inspected directly.
const errorInfo = {
name: extractErrorName(error),
message: extractErrorMessage(error),
statusCode: extractErrorStatusCode(error),
}
if (await this.tryFallbackRetry(existingTask, errorInfo, "promptAsync.resume")) {
return
@@ -1199,6 +1202,7 @@ The fallback retry session is now created and can be inspected directly.
const errorInfo = {
name: extractErrorName(assistantError),
message: extractErrorMessage(assistantError),
statusCode: extractErrorStatusCode(assistantError),
}
void this.tryFallbackRetry(task, errorInfo, "message.updated").catch((error) => {
log("[background-agent] Error handling message.updated fallback retry:", {
@@ -1444,7 +1448,7 @@ The fallback retry session is now created and can be inspected directly.
private async handleSessionErrorEvent(args: {
task: BackgroundTask
errorInfo: { name?: string; message?: string }
errorInfo: { name?: string; message?: string; statusCode?: number }
errorName: string | undefined
errorMessage: string | undefined
}): Promise<void> {
@@ -1532,7 +1536,7 @@ The fallback retry session is now created and can be inspected directly.
private tryFallbackRetry(
task: BackgroundTask,
errorInfo: { name?: string; message?: string },
errorInfo: { name?: string; message?: string; statusCode?: number },
source: string,
): Promise<boolean> {
const previousSessionID = task.sessionId