@@ -0,0 +1,96 @@
|
||||
import type { AssistantMessage } from "../types.ts";
|
||||
|
||||
function buildProviderErrorPattern(patterns: readonly string[]): RegExp {
|
||||
return new RegExp(patterns.join("|"), "i");
|
||||
}
|
||||
|
||||
const NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([
|
||||
// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's
|
||||
// Zen API. These are subscription/account limits, not transient throttles.
|
||||
"GoUsageLimitError",
|
||||
"FreeUsageLimitError",
|
||||
|
||||
// OpenCode Go subscription-limit text asks users to enable available-balance
|
||||
// usage after rolling/weekly/monthly limits are reached.
|
||||
"Monthly usage limit reached",
|
||||
"available balance",
|
||||
|
||||
// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's
|
||||
// quota/billing error code; the other strings cover common gateway wording.
|
||||
"insufficient_quota",
|
||||
"out of budget",
|
||||
"quota exceeded",
|
||||
"billing",
|
||||
]);
|
||||
|
||||
const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
|
||||
// Generic provider load, HTTP status, and server-side transient failures.
|
||||
"overloaded",
|
||||
"rate.?limit",
|
||||
"too many requests",
|
||||
"429",
|
||||
"500",
|
||||
"502",
|
||||
"503",
|
||||
"504",
|
||||
"service.?unavailable",
|
||||
"server.?error",
|
||||
"internal.?error",
|
||||
|
||||
// Wrapper/provider text for transient upstream failures, including OpenRouter
|
||||
// "Provider returned error" responses (#2264).
|
||||
"provider.?returned.?error",
|
||||
|
||||
// Network, proxy, and fetch transport failures. This includes OpenAI Codex
|
||||
// raw-fetch failures such as "upstream connect", "connection refused", and
|
||||
// "reset before headers" (#733), plus OpenRouter connection drops (#3317).
|
||||
"network.?error",
|
||||
"connection.?error",
|
||||
"connection.?refused",
|
||||
"connection.?lost",
|
||||
"other side closed",
|
||||
"fetch failed",
|
||||
"upstream.?connect",
|
||||
"reset before headers",
|
||||
"socket hang up",
|
||||
"timed? out",
|
||||
"timeout",
|
||||
"terminated",
|
||||
|
||||
// WebSocket transports can report close/error text instead of HTTP/fetch text.
|
||||
"websocket.?closed",
|
||||
"websocket.?error",
|
||||
|
||||
// Premature stream endings from SDKs and transports. Anthropic can throw
|
||||
// "stream ended without ..." and "Anthropic stream ended before message_stop"
|
||||
// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).
|
||||
"ended without",
|
||||
"stream ended before message_stop",
|
||||
"http2 request did not get a response",
|
||||
|
||||
// Provider-requested retry delay cap failures should flow through the outer
|
||||
// retry policy so callers can surface/abort the backoff (#1123).
|
||||
"retry delay",
|
||||
|
||||
// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock
|
||||
// stream exceptions (#6019).
|
||||
"you can retry your request",
|
||||
"try your request again",
|
||||
"please retry your request",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Classifies whether a failed assistant message looks like a transient provider
|
||||
* or transport error, so callers can decide if the last assistant turn should be
|
||||
* restarted.
|
||||
*
|
||||
* This does not implement retry policy. Callers should first handle context
|
||||
* overflow separately, then apply their own retry budget, backoff, and reporting
|
||||
* before restarting the assistant turn.
|
||||
*/
|
||||
export function isRetryableAssistantError(message: AssistantMessage): boolean {
|
||||
if (message.stopReason !== "error" || !message.errorMessage) return false;
|
||||
const errorMessage = message.errorMessage;
|
||||
if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;
|
||||
return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);
|
||||
}
|
||||
Reference in New Issue
Block a user