fix: accept null error fields and narrow message retry heuristics

This commit is contained in:
Christiaan Arnoldus
2026-07-30 18:16:09 +02:00
parent 31d844a708
commit 411f28f118
2 changed files with 46 additions and 4 deletions
@@ -9,8 +9,8 @@ export type Frame = {
} & Record<string, unknown>
const payload = z.looseObject({
code: z.union([z.string(), z.number()]).optional(),
message: z.string().optional(),
code: z.union([z.string(), z.number()]).nullish(),
message: z.string().nullish(),
})
// OpenAI Responses API terminal frame forwarded by @ai-sdk/openai >= 3.0.82
@@ -29,7 +29,7 @@ const wrapper = z.looseObject({
// fields so gateway wrappers keep the specific inner code
const bare = z.looseObject({
type: z.undefined().optional(),
code: z.union([z.string(), z.number()]),
code: z.union([z.string(), z.number(), z.null()]),
message: z.string(),
})
@@ -49,6 +49,9 @@ export function frame(body: unknown): Frame {
}
const RETRYABLE = /rate.?limit|too.?many.?requests|rate increased too quickly|exhausted|overload|server|unavailable|timeout/i
// Session.retryable only matched these phrases against free-form message
// text; the wider pattern above is for structured code/type fields only
const RETRYABLE_TEXT = /rate increased too quickly|rate limit|too many requests/i
// Must stay at least as permissive as the Session.retryable heuristics that
// applied when these frames still surfaced as NamedError.Unknown, or
@@ -63,7 +66,7 @@ function retryable(error: Frame["error"], message: string) {
}
const type = error?.type
if (typeof type === "string" && RETRYABLE.test(type)) return true
return RETRYABLE.test(message)
return RETRYABLE_TEXT.test(message)
}
/**
@@ -187,6 +187,45 @@ describe("responses api terminal frames", () => {
expect(result.data.isRetryable).toBe(true)
})
test("normalizes error objects with explicit null fields", () => {
const payload = {
error: {
message: "The server had an error while processing your request",
type: "server_error",
param: null,
code: null,
},
}
const result = MessageV2.fromError(payload, { providerID: ProviderV2.ID.make("openai") })
expect(MessageV2.APIError.isInstance(result)).toBe(true)
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
expect(result.data.message).toBe(payload.error.message)
expect(result.data.isRetryable).toBe(true)
})
test("normalizes response.failed frames with a null error code", () => {
const payload = {
type: "response.failed",
response: { error: { code: null, message: "mid-stream failure" }, incomplete_details: null },
}
const result = MessageV2.fromError(payload, { providerID: ProviderV2.ID.make("openai") })
expect(MessageV2.APIError.isInstance(result)).toBe(true)
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
expect(result.data.message).toBe("mid-stream failure")
expect(result.data.isRetryable).toBe(false)
})
test("does not retry terminal errors that merely mention availability", () => {
const payload = { code: "invalid_request", message: "The model is unavailable in your region" }
const result = MessageV2.fromError(payload, { providerID: ProviderV2.ID.make("openai") })
expect(MessageV2.APIError.isInstance(result)).toBe(true)
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
expect(result.data.isRetryable).toBe(false)
})
test("ignores response.failed frames without an error payload", () => {
const payload = { type: "response.failed", response: { error: null, incomplete_details: null } }
const result = MessageV2.fromError(payload, { providerID: ProviderV2.ID.make("openai") })