| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- import { APICallError } from "ai"
- import { STATUS_CODES } from "http"
- import { iife } from "@/util/iife"
- import type { ProviderV2 } from "@kirincode-ai/core/provider"
- import { isContextOverflow } from "@kirincode-ai/llm"
- 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*<!doctype|^\s*<html/i.test(e.responseBody)) {
- if (e.statusCode === 401) {
- return "Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running `opencode auth login <your provider URL>` 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<string, string>
- responseBody?: string
- metadata?: Record<string, string>
- }
- 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"
|