Skip to content

Commit 99ea135

Browse files
authored
tweak: add new ContextOverflowError type (#12777)
1 parent d40dffb commit 99ea135

6 files changed

Lines changed: 349 additions & 125 deletions

File tree

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { APICallError } from "ai"
2+
import { STATUS_CODES } from "http"
3+
import { iife } from "@/util/iife"
4+
5+
export namespace ProviderError {
6+
// Adapted from overflow detection patterns in:
7+
// https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
8+
const OVERFLOW_PATTERNS = [
9+
/prompt is too long/i, // Anthropic
10+
/input is too long for requested model/i, // Amazon Bedrock
11+
/exceeds the context window/i, // OpenAI (Completions + Responses API message text)
12+
/input token count.*exceeds the maximum/i, // Google (Gemini)
13+
/maximum prompt length is \d+/i, // xAI (Grok)
14+
/reduce the length of the messages/i, // Groq
15+
/maximum context length is \d+ tokens/i, // OpenRouter
16+
/exceeds the limit of \d+/i, // GitHub Copilot
17+
/exceeds the available context size/i, // llama.cpp server
18+
/greater than the context length/i, // LM Studio
19+
/context window exceeds limit/i, // MiniMax
20+
/exceeded model token limit/i, // Kimi For Coding
21+
/context[_ ]length[_ ]exceeded/i, // Generic fallback
22+
/too many tokens/i, // Generic fallback
23+
/token limit exceeded/i, // Generic fallback
24+
]
25+
26+
function isOpenAiErrorRetryable(e: APICallError) {
27+
const status = e.statusCode
28+
if (!status) return e.isRetryable
29+
// openai sometimes returns 404 for models that are actually available
30+
return status === 404 || e.isRetryable
31+
}
32+
33+
// Providers not reliably handled in this function:
34+
// - z.ai: can accept overflow silently (needs token-count/context-window checks)
35+
function isOverflow(message: string) {
36+
if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
37+
38+
// Providers/status patterns handled outside of regex list:
39+
// - Cerebras: often returns "400 (no body)" / "413 (no body)"
40+
// - Mistral: often returns "400 (no body)" / "413 (no body)"
41+
return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
42+
}
43+
44+
function error(providerID: string, error: APICallError) {
45+
if (providerID.includes("github-copilot") && error.statusCode === 403) {
46+
return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
47+
}
48+
49+
return error.message
50+
}
51+
52+
function message(providerID: string, e: APICallError) {
53+
return iife(() => {
54+
const msg = e.message
55+
if (msg === "") {
56+
if (e.responseBody) return e.responseBody
57+
if (e.statusCode) {
58+
const err = STATUS_CODES[e.statusCode]
59+
if (err) return err
60+
}
61+
return "Unknown error"
62+
}
63+
64+
const transformed = error(providerID, e)
65+
if (transformed !== msg) {
66+
return transformed
67+
}
68+
if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
69+
return msg
70+
}
71+
72+
try {
73+
const body = JSON.parse(e.responseBody)
74+
// try to extract common error message fields
75+
const errMsg = body.message || body.error || body.error?.message
76+
if (errMsg && typeof errMsg === "string") {
77+
return `${msg}: ${errMsg}`
78+
}
79+
} catch {}
80+
81+
return `${msg}: ${e.responseBody}`
82+
}).trim()
83+
}
84+
85+
function json(input: unknown) {
86+
if (typeof input === "string") {
87+
try {
88+
const result = JSON.parse(input)
89+
if (result && typeof result === "object") return result
90+
return undefined
91+
} catch {
92+
return undefined
93+
}
94+
}
95+
if (typeof input === "object" && input !== null) {
96+
return input
97+
}
98+
return undefined
99+
}
100+
101+
export type ParsedStreamError =
102+
| {
103+
type: "context_overflow"
104+
message: string
105+
responseBody: string
106+
}
107+
| {
108+
type: "api_error"
109+
message: string
110+
isRetryable: false
111+
responseBody: string
112+
}
113+
114+
export function parseStreamError(input: unknown): ParsedStreamError | undefined {
115+
const body = json(input)
116+
if (!body) return
117+
118+
const responseBody = JSON.stringify(body)
119+
if (body.type !== "error") return
120+
121+
switch (body?.error?.code) {
122+
case "context_length_exceeded":
123+
return {
124+
type: "context_overflow",
125+
message: "Input exceeds context window of this model",
126+
responseBody,
127+
}
128+
case "insufficient_quota":
129+
return {
130+
type: "api_error",
131+
message: "Quota exceeded. Check your plan and billing details.",
132+
isRetryable: false,
133+
responseBody,
134+
}
135+
case "usage_not_included":
136+
return {
137+
type: "api_error",
138+
message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
139+
isRetryable: false,
140+
responseBody,
141+
}
142+
case "invalid_prompt":
143+
return {
144+
type: "api_error",
145+
message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
146+
isRetryable: false,
147+
responseBody,
148+
}
149+
}
150+
}
151+
152+
export type ParsedAPICallError =
153+
| {
154+
type: "context_overflow"
155+
message: string
156+
responseBody?: string
157+
}
158+
| {
159+
type: "api_error"
160+
message: string
161+
statusCode?: number
162+
isRetryable: boolean
163+
responseHeaders?: Record<string, string>
164+
responseBody?: string
165+
metadata?: Record<string, string>
166+
}
167+
168+
export function parseAPICallError(input: { providerID: string; error: APICallError }): ParsedAPICallError {
169+
const m = message(input.providerID, input.error)
170+
if (isOverflow(m)) {
171+
return {
172+
type: "context_overflow",
173+
message: m,
174+
responseBody: input.error.responseBody,
175+
}
176+
}
177+
178+
const metadata = input.error.url ? { url: input.error.url } : undefined
179+
return {
180+
type: "api_error",
181+
message: m,
182+
statusCode: input.error.statusCode,
183+
isRetryable: input.providerID.startsWith("openai")
184+
? isOpenAiErrorRetryable(input.error)
185+
: input.error.isRetryable,
186+
responseHeaders: input.error.responseHeaders,
187+
responseBody: input.error.responseBody,
188+
metadata,
189+
}
190+
}
191+
}

packages/opencode/src/provider/transform.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { APICallError, ModelMessage } from "ai"
1+
import type { ModelMessage } from "ai"
22
import { mergeDeep, unique } from "remeda"
33
import type { JSONSchema7 } from "@ai-sdk/provider"
44
import type { JSONSchema } from "zod/v4/core"
@@ -824,13 +824,4 @@ export namespace ProviderTransform {
824824

825825
return schema as JSONSchema7
826826
}
827-
828-
export function error(providerID: string, error: APICallError) {
829-
let message = error.message
830-
if (providerID.includes("github-copilot") && error.statusCode === 403) {
831-
return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
832-
}
833-
834-
return message
835-
}
836827
}

0 commit comments

Comments
 (0)