fix: narrow api_error detection to exclude billing/auth patterns

This commit is contained in:
Ayush Ojha 2026-03-18 01:31:33 -07:00
parent 876566439e
commit 04755c2327
2 changed files with 52 additions and 11 deletions

View File

@ -860,4 +860,35 @@ describe("classifyFailoverReason", () => {
),
).toBe("timeout");
});
it("does not shadow billing errors that carry api_error type", () => {
// A provider may wrap a billing error in a JSON payload with "type":"api_error".
// The billing classifier must win over the broad api_error transient match.
expect(
classifyFailoverReason(
'{"type":"error","error":{"type":"api_error","message":"insufficient credits"}}',
),
).toBe("billing");
expect(
classifyFailoverReason(
'{"type":"error","error":{"type":"api_error","message":"Payment required"}}',
),
).toBe("billing");
});
it("does not shadow auth errors that carry api_error type", () => {
expect(
classifyFailoverReason(
'{"type":"error","error":{"type":"api_error","message":"invalid api key"}}',
),
).toBe("auth");
expect(
classifyFailoverReason(
'{"type":"error","error":{"type":"api_error","message":"unauthorized"}}',
),
).toBe("auth");
expect(
classifyFailoverReason(
'{"type":"error","error":{"type":"api_error","message":"permission_error"}}',
),
).toBe("auth_permanent");
});
});

View File

@ -857,8 +857,15 @@ function isJsonApiInternalServerError(raw: string): boolean {
// {"type":"error","error":{"type":"api_error","message":"Internal server error"}}
// Non-standard providers (e.g. MiniMax) may use different message text:
// {"type":"api_error","message":"unknown error, 520 (1000)"}
// Any api_error type indicates a provider-side failure regardless of message text.
return value.includes('"type":"api_error"');
if (!value.includes('"type":"api_error"')) {
return false;
}
// Billing and auth errors can also carry "type":"api_error". Exclude them so
// the more specific classifiers further down the chain handle them correctly.
if (isBillingErrorMessage(raw) || isAuthErrorMessage(raw) || isAuthPermanentErrorMessage(raw)) {
return false;
}
return true;
}
export function parseImageDimensionError(raw: string): {
@ -1011,24 +1018,27 @@ export function classifyFailoverReason(raw: string): FailoverReason | null {
// Treat remaining transient 5xx provider failures as retryable transport issues.
return "timeout";
}
if (isJsonApiInternalServerError(raw)) {
return "timeout";
}
if (isCloudCodeAssistFormatError(raw)) {
return "format";
}
// Billing and auth classifiers run before the broad isJsonApiInternalServerError
// check so that provider errors like {"type":"api_error","message":"insufficient
// balance"} are correctly classified as "billing"/"auth" rather than "timeout".
if (isBillingErrorMessage(raw)) {
return "billing";
}
if (isTimeoutErrorMessage(raw)) {
return "timeout";
}
if (isAuthPermanentErrorMessage(raw)) {
return "auth_permanent";
}
if (isAuthErrorMessage(raw)) {
return "auth";
}
if (isJsonApiInternalServerError(raw)) {
return "timeout";
}
if (isCloudCodeAssistFormatError(raw)) {
return "format";
}
if (isTimeoutErrorMessage(raw)) {
return "timeout";
}
return null;
}