import { APICallError } from "ai" import { STATUS_CODES } from "http" import { iife } from "@/util/iife" import type { ProviderV2 } from "@opencode-ai/core/provider" import { isContextOverflow } from "@opencode-ai/ai" export class HeaderTimeoutError extends Error { public override readonly name = "ProviderHeaderTimeoutError" constructor(public readonly ms: number) { super(`Provider response headers timed out after ${ms}ms`) } } export class ResponseStreamError extends Error { public override readonly name = "ProviderResponseStreamError" constructor(message: string, options?: ErrorOptions) { super(message, options) } } function isOpenAiErrorRetryable(e: APICallError) { const status = e.statusCode if (!status) return e.isRetryable // openai sometimes returns 404 for models that are actually available return status === 404 || e.isRetryable } // Providers not reliably handled in this function: // - z.ai: can accept overflow silently (needs token-count/context-window checks) function message(providerID: ProviderV2.ID, e: APICallError) { return iife(() => { const msg = e.message if (msg === "") { if (e.responseBody) return e.responseBody if (e.statusCode) { const err = STATUS_CODES[e.statusCode] if (err) return err } return "Unknown error" } if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) { return msg } try { const body = JSON.parse(e.responseBody) // try to extract common error message fields const errMsg = body.message || body.error || body.error?.message if (errMsg && typeof errMsg === "string") { return `${msg}: ${errMsg}` } } catch {} // If responseBody is HTML (e.g. from a gateway or proxy error page), // provide a human-readable message instead of dumping raw markup if (/^\s*` to re-authenticate." } if (e.statusCode === 403) { return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings." } return msg } return `${msg}: ${e.responseBody}` }).trim() } function json(input: unknown) { if (typeof input === "string") { try { const result = JSON.parse(input) if (result && typeof result === "object") return result return undefined } catch { return undefined } } if (typeof input === "object" && input !== null) { return input } return undefined } export type ParsedStreamError = | { type: "context_overflow" message: string responseBody: string } | { type: "api_error" message: string isRetryable: boolean responseBody: string } export function parseStreamError(input: unknown): ParsedStreamError | undefined { const raw = json(input) const body = typeof raw?.message === "string" ? (json(raw.message) ?? raw) : raw if (!body) return const responseBody = JSON.stringify(body) if (body.type !== "error") return switch (body?.error?.code) { case "context_length_exceeded": return { type: "context_overflow", message: "Input exceeds context window of this model", responseBody, } case "insufficient_quota": return { type: "api_error", message: "Quota exceeded. Check your plan and billing details.", isRetryable: false, responseBody, } case "usage_not_included": return { type: "api_error", message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.", isRetryable: false, responseBody, } case "invalid_prompt": return { type: "api_error", message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.", isRetryable: false, responseBody, } case "server_is_overloaded": case "server_error": return { type: "api_error", message: typeof body?.error?.message === "string" ? body?.error?.message : "Server error.", isRetryable: true, responseBody, } } } export type ParsedAPICallError = | { type: "context_overflow" message: string responseBody?: string } | { type: "api_error" message: string statusCode?: number isRetryable: boolean responseHeaders?: Record responseBody?: string metadata?: Record } export function parseAPICallError(input: { providerID: ProviderV2.ID; error: APICallError }): ParsedAPICallError { const m = message(input.providerID, input.error) const body = json(input.error.responseBody) if (isContextOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") { return { type: "context_overflow", message: m, responseBody: input.error.responseBody, } } const metadata = input.error.url ? { url: input.error.url } : undefined return { type: "api_error", message: m, statusCode: input.error.statusCode, isRetryable: input.providerID.startsWith("openai") ? isOpenAiErrorRetryable(input.error) : input.error.isRetryable, responseHeaders: input.error.responseHeaders, responseBody: input.error.responseBody, metadata, } } export * as ProviderError from "./error"