2026-02-09 05:54:01 +00:00
import { APICallError } from "ai"
import { STATUS_CODES } from "http"
import { iife } from "@/util/iife"
2026-05-31 01:08:38 +00:00
import type { ProviderV2 } from "@opencode-ai/core/provider"
2026-06-05 20:05:51 +00:00
import { isContextOverflow } from "@opencode-ai/llm"
2026-02-09 05:54:01 +00:00
2026-05-27 02:22:24 +00:00
export class HeaderTimeoutError extends Error {
public override readonly name = "ProviderHeaderTimeoutError"
constructor ( public readonly ms : number ) {
super ( ` Provider response headers timed out after ${ ms } ms ` )
}
}
2026-05-28 06:16:18 +00:00
export class ResponseStreamError extends Error {
public override readonly name = "ProviderResponseStreamError"
constructor ( message : string , options? : ErrorOptions ) {
super ( message , options )
}
}
2026-04-16 05:02:50 +00:00
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
}
2026-02-09 05:54:01 +00:00
2026-04-16 05:02:50 +00:00
// Providers not reliably handled in this function:
// - z.ai: can accept overflow silently (needs token-count/context-window checks)
2026-05-31 01:08:38 +00:00
function message ( providerID : ProviderV2.ID , e : APICallError ) {
2026-04-16 05:02:50 +00:00
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
2026-02-09 05:54:01 +00:00
}
2026-04-16 05:02:50 +00:00
return "Unknown error"
}
2026-02-09 05:54:01 +00:00
2026-04-16 05:02:50 +00:00
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 } `
2026-02-09 05:54:01 +00:00
}
2026-04-16 05:02:50 +00:00
} catch { }
2026-02-09 05:54:01 +00:00
2026-04-16 05:02:50 +00:00
// 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."
2026-03-01 15:24:57 +00:00
}
2026-04-16 05:02:50 +00:00
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
}
2026-03-01 15:24:57 +00:00
2026-04-16 05:02:50 +00:00
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
2026-02-09 05:54:01 +00:00
}
2026-04-16 05:02:50 +00:00
return undefined
}
2026-02-09 05:54:01 +00:00
2026-04-16 05:02:50 +00:00
export type ParsedStreamError =
| {
type : "context_overflow"
message : string
responseBody : string
2026-02-09 05:54:01 +00:00
}
2026-04-16 05:02:50 +00:00
| {
type : "api_error"
message : string
2026-04-23 21:31:43 +00:00
isRetryable : boolean
2026-04-16 05:02:50 +00:00
responseBody : string
2026-02-09 05:54:01 +00:00
}
2026-04-16 05:02:50 +00:00
export function parseStreamError ( input : unknown ) : ParsedStreamError | undefined {
2026-04-23 21:31:43 +00:00
const raw = json ( input )
const body = typeof raw ? . message === "string" ? ( json ( raw . message ) ? ? raw ) : raw
2026-04-16 05:02:50 +00:00
if ( ! body ) return
2026-02-09 05:54:01 +00:00
2026-04-16 05:02:50 +00:00
const responseBody = JSON . stringify ( body )
if ( body . type !== "error" ) return
2026-02-09 05:54:01 +00:00
2026-04-16 05:02:50 +00:00
switch ( body ? . error ? . code ) {
case "context_length_exceeded" :
return {
type : "context_overflow" ,
message : "Input exceeds context window of this model" ,
responseBody ,
2026-02-09 05:54:01 +00:00
}
2026-04-16 05:02:50 +00:00
case "insufficient_quota" :
return {
type : "api_error" ,
message : "Quota exceeded. Check your plan and billing details." ,
isRetryable : false ,
responseBody ,
2026-02-09 05:54:01 +00:00
}
2026-04-16 05:02:50 +00:00
case "usage_not_included" :
2026-02-09 05:54:01 +00:00
return {
2026-04-16 05:02:50 +00:00
type : "api_error" ,
message : "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus." ,
isRetryable : false ,
responseBody ,
2026-02-09 05:54:01 +00:00
}
2026-04-16 05:02:50 +00:00
case "invalid_prompt" :
return {
type : "api_error" ,
message : typeof body ? . error ? . message === "string" ? body ? . error ? . message : "Invalid prompt." ,
isRetryable : false ,
responseBody ,
}
2026-05-05 15:39:25 +00:00
case "server_is_overloaded" :
2026-04-23 21:31:43 +00:00
case "server_error" :
return {
type : "api_error" ,
message : typeof body ? . error ? . message === "string" ? body ? . error ? . message : "Server error." ,
isRetryable : true ,
responseBody ,
}
2026-04-16 05:02:50 +00:00
}
}
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 >
2026-02-09 05:54:01 +00:00
}
2026-05-31 01:08:38 +00:00
export function parseAPICallError ( input : { providerID : ProviderV2.ID ; error : APICallError } ) : ParsedAPICallError {
2026-04-16 05:02:50 +00:00
const m = message ( input . providerID , input . error )
const body = json ( input . error . responseBody )
2026-06-05 20:05:51 +00:00
if ( isContextOverflow ( m ) || input . error . statusCode === 413 || body ? . error ? . code === "context_length_exceeded" ) {
2026-02-09 05:54:01 +00:00
return {
2026-04-16 05:02:50 +00:00
type : "context_overflow" ,
2026-02-09 05:54:01 +00:00
message : m ,
responseBody : input.error.responseBody ,
}
}
2026-04-16 05:02:50 +00:00
const metadata = input . error . url ? { url : input.error.url } : undefined
return {
type : "api_error" ,
message : m ,
statusCode : input.error.statusCode ,
2026-04-16 05:03:50 +00:00
isRetryable : input.providerID.startsWith ( "openai" ) ? isOpenAiErrorRetryable ( input . error ) : input . error . isRetryable ,
2026-04-16 05:02:50 +00:00
responseHeaders : input.error.responseHeaders ,
responseBody : input.error.responseBody ,
metadata ,
}
2026-02-09 05:54:01 +00:00
}
2026-04-27 18:33:33 +00:00
export * as ProviderError from "./error"