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); }