| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- import { APICallError } from "ai"
- import { STATUS_CODES } from "http"
- import { iife } from "@/util/iife"
- export namespace ProviderError {
- // Adapted from overflow detection patterns in:
- // https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
- const OVERFLOW_PATTERNS = [
- /prompt is too long/i, // Anthropic
- /input is too long for requested model/i, // Amazon Bedrock
- /exceeds the context window/i, // OpenAI (Completions + Responses API message text)
- /input token count.*exceeds the maximum/i, // Google (Gemini)
- /maximum prompt length is \d+/i, // xAI (Grok)
- /reduce the length of the messages/i, // Groq
- /maximum context length is \d+ tokens/i, // OpenRouter, DeepSeek
- /exceeds the limit of \d+/i, // GitHub Copilot
- /exceeds the available context size/i, // llama.cpp server
- /greater than the context length/i, // LM Studio
- /context window exceeds limit/i, // MiniMax
- /exceeded model token limit/i, // Kimi For Coding, Moonshot
- /context[_ ]length[_ ]exceeded/i, // Generic fallback
- ]
- 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 isOverflow(message: string) {
- if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
- // Providers/status patterns handled outside of regex list:
- // - Cerebras: often returns "400 (no body)" / "413 (no body)"
- // - Mistral: often returns "400 (no body)" / "413 (no body)"
- return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
- }
- function error(providerID: string, error: APICallError) {
- if (providerID.includes("github-copilot") && error.statusCode === 403) {
- return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
- }
- return error.message
- }
- function message(providerID: string, 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"
- }
- const transformed = error(providerID, e)
- if (transformed !== msg) {
- return transformed
- }
- 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 {}
- 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: false
- responseBody: string
- }
- export function parseStreamError(input: unknown): ParsedStreamError | undefined {
- const body = json(input)
- 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,
- }
- }
- }
- export type ParsedAPICallError =
- | {
- type: "context_overflow"
- message: string
- responseBody?: string
- }
- | {
- type: "api_error"
- message: string
- statusCode?: number
- isRetryable: boolean
- responseHeaders?: Record<string, string>
- responseBody?: string
- metadata?: Record<string, string>
- }
- export function parseAPICallError(input: { providerID: string; error: APICallError }): ParsedAPICallError {
- const m = message(input.providerID, input.error)
- if (isOverflow(m)) {
- 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,
- }
- }
- }
|