fix(llm): classify pi-ai failures by the leading HTTP status
pi-ai's SDK-backed providers render an HTTP failure as `<status> <body>`. A gateway 500 that embeds its upstream's 401 in the body matched the AUTH rule before the 5xx rule, so Chat reported an invalid API key and the retry policy gave up. When a message leads with a 4xx/5xx status, the status rules now read only that status; other wordings still scan the whole message. Refs #8182
This commit is contained in:
1 parent
5a18be1c9a
commit
d1711894c8
2 files changed
+26
-5
No files matched your search
@@ -39,15 +39,24 @@ export function mapUsage(usage: PiUsage): TokenUsage {
|
||||
// wrapper a bare `terminated`, so we are left pattern-matching terse words here.
|
||||
// If pi-ai ever forwards the original Error (or a fetch/dispatcher hook that lets
|
||||
// us capture the cause ourselves), classify on `code`/`cause` instead of text.
|
||||
//
|
||||
// pi-ai's SDK-backed providers render an HTTP failure as `<status> <body>`
|
||||
// (Anthropic and OpenAI SDK `APIError`, pi-messages `<status> <statusText>: …`).
|
||||
// The body can quote another hop's status, such as a gateway 500 that embeds its
|
||||
// upstream's 401, so when a message leads with a status the status rules read
|
||||
// only that status; other wordings scan the whole message.
|
||||
const LEADING_HTTP_STATUS = /^[45]\d\d\b/
|
||||
|
||||
function classifyPiAiError(message: string): BuiltinLlmFailureCode {
|
||||
if (/\b(?:401|403)\b/.test(message)) return 'AUTH'
|
||||
const statusSource = LEADING_HTTP_STATUS.exec(message)?.[0] ?? message
|
||||
if (/\b(?:401|403)\b/.test(statusSource)) return 'AUTH'
|
||||
if (isQuotaExceededError(message)) return QUOTA_EXCEEDED_CODE
|
||||
if (/\b429\b|rate.?limit/i.test(message)) return 'RATE_LIMIT'
|
||||
if (/\b429\b/.test(statusSource) || /rate.?limit/i.test(message)) return 'RATE_LIMIT'
|
||||
// A rejected request body (gateway or provider size cap): resending the
|
||||
// same request cannot succeed, so it is invalid, not transient.
|
||||
if (/\b413\b|failed to buffer the request body:\s*length limit exceeded|payload too large|request body too large/i.test(message)) return 'INVALID_REQUEST'
|
||||
if (/\b400\b|invalid.?request/i.test(message)) return 'INVALID_REQUEST'
|
||||
if (/\b5\d\d\b/.test(message)) return 'SERVER'
|
||||
if (/\b413\b/.test(statusSource) || /failed to buffer the request body:\s*length limit exceeded|payload too large|request body too large/i.test(message)) return 'INVALID_REQUEST'
|
||||
if (/\b400\b/.test(statusSource) || /invalid.?request/i.test(message)) return 'INVALID_REQUEST'
|
||||
if (/\b5\d\d\b/.test(statusSource)) return 'SERVER'
|
||||
if (/\btime(?:d)?\s*out\b|timeout/i.test(message)) return 'TIMEOUT'
|
||||
// A stream truncated before the provider's terminal event: each pi-ai provider
|
||||
// throws its own wording when the wire closes mid-response without a terminal
|
||||
|
||||
@@ -939,6 +939,18 @@ describe('mapStopReason / mapUsage', () => {
|
||||
}))).toMatchObject({ kind: 'error', failure: { code: 'PI_AI_ERROR' } })
|
||||
})
|
||||
|
||||
it.each([
|
||||
// A gateway 500 that relays its upstream's 401 in the body, as the Anthropic SDK renders it.
|
||||
['500 {"error":{"origin":"upstream","message":"upstream returned an error response: User not found.","type":"upstream_error","code":"upstream_response_error","upstream":{"provider":"custom","status":401,"body":{"error":{"type":"<nil>","message":"User not found."},"type":"error"}}}}', 'SERVER'],
|
||||
['502 {"error":{"message":"upstream status 400"}}', 'SERVER'],
|
||||
['404 {"error":{"message":"upstream status 403"}}', 'PI_AI_ERROR'],
|
||||
['401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}', 'AUTH'],
|
||||
['429 Too Many Requests: slow down', 'RATE_LIMIT'],
|
||||
] as const)('classifies %s by its leading response status', (errorMessage, code) => {
|
||||
expect(mapStopReason(assistant({ stopReason: 'error', errorMessage })))
|
||||
.toMatchObject({ kind: 'error', failure: { code } })
|
||||
})
|
||||
|
||||
it.each([
|
||||
'other side closed',
|
||||
'HTTP2 request did not get a response',
|
||||
|
||||
Reference in new issue
Block a user