Compare commits

...

1 Commits

Author SHA1 Message Date
rekram1-node 5a02790945 fix(ai): retry xAI capacity stream errors 2026-08-20 14:28:07 +00:00
4 changed files with 60 additions and 7 deletions
+8 -2
View File
@@ -1073,7 +1073,13 @@ const providerErrorMessage = (event: Event, fallback: string): string => {
}
export const providerFailure = (id: string, event: Event, fallback: string) => {
const code = event.code || event.error?.code || event.response?.error?.code || undefined
const code =
event.code ||
event.error?.code ||
event.error?.type ||
event.response?.error?.code ||
event.response?.error?.type ||
undefined
const message = providerErrorMessage(event, fallback)
const status =
typeof event.status === "number"
@@ -1084,7 +1090,7 @@ export const providerFailure = (id: string, event: Event, fallback: string) => {
return new AIError({
module: id,
method: "stream",
reason: classifyProviderFailure({ message, code, status }),
reason: classifyProviderFailure({ message, code, status, stream: true }),
})
}
+14
View File
@@ -74,9 +74,11 @@ const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error"
const RATE_LIMIT_TEXT = /rate increased too quickly|rate[-_\s]?limit|too[_\s]?many[_\s]?requests/i
const QUOTA_TEXT = /insufficient[-_\s]?quota|quota[-_\s]?exceeded/i
const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i
const TRANSIENT_TEXT = /\btry again (?:later|in\b)|\b(?:currently|temporarily) at capacity\b|\bretry your request\b/i
export interface ProviderFailure {
readonly message: string
readonly stream?: boolean | undefined
readonly status?: number | undefined
readonly code?: string | undefined
readonly retryAfterMs?: number | undefined
@@ -133,6 +135,12 @@ export function classifyProviderFailure(input: ProviderFailure): AIError["reason
status: input.status,
retryAfterMs: input.retryAfterMs,
})
if (TRANSIENT_TEXT.test(text))
return new ProviderInternalReason({
...common,
status: input.status,
retryAfterMs: input.retryAfterMs,
})
if (input.status === 429) {
return new RateLimitReason({
...common,
@@ -149,6 +157,12 @@ export function classifyProviderFailure(input: ProviderFailure): AIError["reason
if (codes.some((code) => INVALID_REQUEST_CODES.has(code))) return new InvalidRequestReason(common)
if (input.status === 400 || input.status === 404 || input.status === 413 || input.status === 422)
return new InvalidRequestReason(common)
if (input.stream)
return new ProviderInternalReason({
...common,
status: input.status,
retryAfterMs: input.retryAfterMs,
})
return new UnknownProviderReason({ ...common, status: input.status })
}
+20
View File
@@ -75,6 +75,26 @@ describe("provider error classification", () => {
).toEqual(["ProviderInternal", "ProviderInternal", "ProviderInternal"])
})
test("classifies transient gateway message fallbacks as provider internal", () => {
expect(
[
"The model is currently at capacity due to high demand.",
"The service is temporarily at capacity.",
"Please try again in a few minutes.",
"Please retry your request shortly.",
].map((message) => classifyProviderFailure({ message })._tag),
).toEqual(["ProviderInternal", "ProviderInternal", "ProviderInternal", "ProviderInternal"])
})
test("classifies provider stream errors as provider internal", () => {
expect(
[
"The model is currently at capacity due to high demand. Please try again in a few minutes, or use a higher service tier for priority processing: https://docs.x.ai/developers/advanced-api-usage/priority-processing",
"The model is temporarily unavailable.",
].map((message) => classifyProviderFailure({ message, stream: true })._tag),
).toEqual(["ProviderInternal", "ProviderInternal"])
})
test("classifies transient client statuses as provider internal", () => {
expect([408, 409].map((status) => classifyProviderFailure({ message: `HTTP ${status}`, status })._tag)).toEqual([
"ProviderInternal",
@@ -809,7 +809,7 @@ describe("OpenAI Responses route", () => {
"ProviderInternal",
"RateLimit",
"ProviderInternal",
"UnknownProvider",
"ProviderInternal",
])
}),
)
@@ -2661,6 +2661,19 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("classifies code-less capacity errors as transient", () =>
Effect.gen(function* () {
const message =
"The model is currently at capacity due to high demand. Please try again in a few minutes, or use a higher service tier for priority processing: https://docs.x.ai/developers/advanced-api-usage/priority-processing"
const error = yield* LLMClient.generate(request).pipe(
Effect.provide(fixedResponse(sseEvents({ type: "error", message }))),
Effect.flip,
)
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message })
}),
)
it.effect("falls back to error code when no message is present", () =>
Effect.gen(function* () {
const error = yield* LLMClient.generate(request).pipe(
@@ -2801,7 +2814,7 @@ describe("OpenAI Responses route", () => {
Effect.flip,
)
expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "Something went wrong" })
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "Something went wrong" })
}),
)
@@ -2812,7 +2825,7 @@ describe("OpenAI Responses route", () => {
Effect.flip,
)
expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "OpenAI Responses stream error" })
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "OpenAI Responses stream error" })
}),
)
@@ -2823,7 +2836,7 @@ describe("OpenAI Responses route", () => {
Effect.flip,
)
expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "OpenAI Responses stream error" })
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "OpenAI Responses stream error" })
}),
)
@@ -2834,7 +2847,7 @@ describe("OpenAI Responses route", () => {
Effect.flip,
)
expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "OpenAI Responses response failed" })
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "OpenAI Responses response failed" })
}),
)