Files
opencode/packages/opencode/src/provider/error.ts
2026-02-08 23:54:01 -06:00

192 lines
5.9 KiB
TypeScript

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
/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
/context[_ ]length[_ ]exceeded/i, // Generic fallback
/too many tokens/i, // Generic fallback
/token limit 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,
}
}
}