tweak: add new ContextOverflowError type (#12777)
This commit is contained in:
191
packages/opencode/src/provider/error.ts
Normal file
191
packages/opencode/src/provider/error.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { APICallError, ModelMessage } from "ai"
|
import type { ModelMessage } from "ai"
|
||||||
import { mergeDeep, unique } from "remeda"
|
import { mergeDeep, unique } from "remeda"
|
||||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||||
import type { JSONSchema } from "zod/v4/core"
|
import type { JSONSchema } from "zod/v4/core"
|
||||||
@@ -824,13 +824,4 @@ export namespace ProviderTransform {
|
|||||||
|
|
||||||
return schema as JSONSchema7
|
return schema as JSONSchema7
|
||||||
}
|
}
|
||||||
|
|
||||||
export function error(providerID: string, error: APICallError) {
|
|
||||||
let message = error.message
|
|
||||||
if (providerID.includes("github-copilot") && error.statusCode === 403) {
|
|
||||||
return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
|
|
||||||
}
|
|
||||||
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { LSP } from "../lsp"
|
|||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { fn } from "@/util/fn"
|
import { fn } from "@/util/fn"
|
||||||
import { Storage } from "@/storage/storage"
|
import { Storage } from "@/storage/storage"
|
||||||
import { ProviderTransform } from "@/provider/transform"
|
import { ProviderError } from "@/provider/error"
|
||||||
import { STATUS_CODES } from "http"
|
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { type SystemError } from "bun"
|
import { type SystemError } from "bun"
|
||||||
import type { Provider } from "@/provider/provider"
|
import type { Provider } from "@/provider/provider"
|
||||||
@@ -35,6 +34,10 @@ export namespace MessageV2 {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
export type APIError = z.infer<typeof APIError.Schema>
|
export type APIError = z.infer<typeof APIError.Schema>
|
||||||
|
export const ContextOverflowError = NamedError.create(
|
||||||
|
"ContextOverflowError",
|
||||||
|
z.object({ message: z.string(), responseBody: z.string().optional() }),
|
||||||
|
)
|
||||||
|
|
||||||
const PartBase = z.object({
|
const PartBase = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -361,6 +364,7 @@ export namespace MessageV2 {
|
|||||||
NamedError.Unknown.Schema,
|
NamedError.Unknown.Schema,
|
||||||
OutputLengthError.Schema,
|
OutputLengthError.Schema,
|
||||||
AbortedError.Schema,
|
AbortedError.Schema,
|
||||||
|
ContextOverflowError.Schema,
|
||||||
APIError.Schema,
|
APIError.Schema,
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -711,13 +715,6 @@ export namespace MessageV2 {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const 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
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromError(e: unknown, ctx: { providerID: string }) {
|
export function fromError(e: unknown, ctx: { providerID: string }) {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case e instanceof DOMException && e.name === "AbortError":
|
case e instanceof DOMException && e.name === "AbortError":
|
||||||
@@ -751,45 +748,28 @@ export namespace MessageV2 {
|
|||||||
{ cause: e },
|
{ cause: e },
|
||||||
).toObject()
|
).toObject()
|
||||||
case APICallError.isInstance(e):
|
case APICallError.isInstance(e):
|
||||||
const message = iife(() => {
|
const parsed = ProviderError.parseAPICallError({
|
||||||
let msg = e.message
|
providerID: ctx.providerID,
|
||||||
if (msg === "") {
|
error: e,
|
||||||
if (e.responseBody) return e.responseBody
|
})
|
||||||
if (e.statusCode) {
|
if (parsed.type === "context_overflow") {
|
||||||
const err = STATUS_CODES[e.statusCode]
|
return new MessageV2.ContextOverflowError(
|
||||||
if (err) return err
|
{
|
||||||
}
|
message: parsed.message,
|
||||||
return "Unknown error"
|
responseBody: parsed.responseBody,
|
||||||
}
|
},
|
||||||
const transformed = ProviderTransform.error(ctx.providerID, e)
|
{ cause: e },
|
||||||
if (transformed !== msg) {
|
).toObject()
|
||||||
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()
|
|
||||||
|
|
||||||
const metadata = e.url ? { url: e.url } : undefined
|
|
||||||
return new MessageV2.APIError(
|
return new MessageV2.APIError(
|
||||||
{
|
{
|
||||||
message,
|
message: parsed.message,
|
||||||
statusCode: e.statusCode,
|
statusCode: parsed.statusCode,
|
||||||
isRetryable: ctx.providerID.startsWith("openai") ? isOpenAiErrorRetryable(e) : e.isRetryable,
|
isRetryable: parsed.isRetryable,
|
||||||
responseHeaders: e.responseHeaders,
|
responseHeaders: parsed.responseHeaders,
|
||||||
responseBody: e.responseBody,
|
responseBody: parsed.responseBody,
|
||||||
metadata,
|
metadata: parsed.metadata,
|
||||||
},
|
},
|
||||||
{ cause: e },
|
{ cause: e },
|
||||||
).toObject()
|
).toObject()
|
||||||
@@ -797,72 +777,27 @@ export namespace MessageV2 {
|
|||||||
return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
|
return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
|
||||||
default:
|
default:
|
||||||
try {
|
try {
|
||||||
const json = iife(() => {
|
const parsed = ProviderError.parseStreamError(e)
|
||||||
if (typeof e === "string") {
|
if (parsed) {
|
||||||
try {
|
if (parsed.type === "context_overflow") {
|
||||||
return JSON.parse(e)
|
return new MessageV2.ContextOverflowError(
|
||||||
} catch {
|
{
|
||||||
return undefined
|
message: parsed.message,
|
||||||
}
|
responseBody: parsed.responseBody,
|
||||||
}
|
},
|
||||||
|
{ cause: e },
|
||||||
if (typeof e === "object" && e !== null) {
|
).toObject()
|
||||||
return e
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
if (json) {
|
|
||||||
const responseBody = JSON.stringify(json)
|
|
||||||
// Handle Responses API mid stream style errors
|
|
||||||
if (json?.type === "error") {
|
|
||||||
switch (json?.error?.code) {
|
|
||||||
case "context_length_exceeded":
|
|
||||||
return new MessageV2.APIError(
|
|
||||||
{
|
|
||||||
message: "Input exceeds context window of this model",
|
|
||||||
isRetryable: false,
|
|
||||||
responseBody,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cause: e,
|
|
||||||
},
|
|
||||||
).toObject()
|
|
||||||
case "insufficient_quota":
|
|
||||||
return new MessageV2.APIError(
|
|
||||||
{
|
|
||||||
message: "Quota exceeded. Check your plan and billing details.",
|
|
||||||
isRetryable: false,
|
|
||||||
responseBody,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cause: e,
|
|
||||||
},
|
|
||||||
).toObject()
|
|
||||||
case "usage_not_included":
|
|
||||||
return new MessageV2.APIError(
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
"To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
|
|
||||||
isRetryable: false,
|
|
||||||
responseBody,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cause: e,
|
|
||||||
},
|
|
||||||
).toObject()
|
|
||||||
case "invalid_prompt":
|
|
||||||
return new MessageV2.APIError(
|
|
||||||
{
|
|
||||||
message: json?.error?.message || "Invalid prompt.",
|
|
||||||
isRetryable: false,
|
|
||||||
responseBody,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cause: e,
|
|
||||||
},
|
|
||||||
).toObject()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return new MessageV2.APIError(
|
||||||
|
{
|
||||||
|
message: parsed.message,
|
||||||
|
isRetryable: parsed.isRetryable,
|
||||||
|
responseBody: parsed.responseBody,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cause: e,
|
||||||
|
},
|
||||||
|
).toObject()
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
|
return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ export namespace SessionRetry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function retryable(error: ReturnType<NamedError["toObject"]>) {
|
export function retryable(error: ReturnType<NamedError["toObject"]>) {
|
||||||
|
// DO NOT retry context overflow errors
|
||||||
|
if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
|
||||||
|
|
||||||
if (MessageV2.APIError.isInstance(error)) {
|
if (MessageV2.APIError.isInstance(error)) {
|
||||||
if (!error.data.isRetryable) return undefined
|
if (!error.data.isRetryable) return undefined
|
||||||
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
|
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { APICallError } from "ai"
|
||||||
import { MessageV2 } from "../../src/session/message-v2"
|
import { MessageV2 } from "../../src/session/message-v2"
|
||||||
import type { Provider } from "../../src/provider/provider"
|
import type { Provider } from "../../src/provider/provider"
|
||||||
|
|
||||||
@@ -786,12 +787,26 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("session.message-v2.fromError", () => {
|
describe("session.message-v2.fromError", () => {
|
||||||
|
test("serializes context_length_exceeded as ContextOverflowError", () => {
|
||||||
|
const input = {
|
||||||
|
type: "error",
|
||||||
|
error: {
|
||||||
|
code: "context_length_exceeded",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = MessageV2.fromError(input, { providerID: "test" })
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({
|
||||||
|
name: "ContextOverflowError",
|
||||||
|
data: {
|
||||||
|
message: "Input exceeds context window of this model",
|
||||||
|
responseBody: JSON.stringify(input),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("serializes response error codes", () => {
|
test("serializes response error codes", () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
|
||||||
code: "context_length_exceeded",
|
|
||||||
message: "Input exceeds context window of this model",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
code: "insufficient_quota",
|
code: "insufficient_quota",
|
||||||
message: "Quota exceeded. Check your plan and billing details.",
|
message: "Quota exceeded. Check your plan and billing details.",
|
||||||
@@ -827,6 +842,75 @@ describe("session.message-v2.fromError", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("maps github-copilot 403 to reauth guidance", () => {
|
||||||
|
const error = new APICallError({
|
||||||
|
message: "forbidden",
|
||||||
|
url: "https://api.githubcopilot.com/v1/chat/completions",
|
||||||
|
requestBodyValues: {},
|
||||||
|
statusCode: 403,
|
||||||
|
responseHeaders: { "content-type": "application/json" },
|
||||||
|
responseBody: '{"error":"forbidden"}',
|
||||||
|
isRetryable: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = MessageV2.fromError(error, { providerID: "github-copilot" })
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({
|
||||||
|
name: "APIError",
|
||||||
|
data: {
|
||||||
|
message:
|
||||||
|
"Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode.",
|
||||||
|
statusCode: 403,
|
||||||
|
isRetryable: false,
|
||||||
|
responseHeaders: { "content-type": "application/json" },
|
||||||
|
responseBody: '{"error":"forbidden"}',
|
||||||
|
metadata: {
|
||||||
|
url: "https://api.githubcopilot.com/v1/chat/completions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("detects context overflow from APICallError provider messages", () => {
|
||||||
|
const cases = [
|
||||||
|
"prompt is too long: 213462 tokens > 200000 maximum",
|
||||||
|
"Your input exceeds the context window of this model",
|
||||||
|
"The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
|
||||||
|
"Please reduce the length of the messages or completion",
|
||||||
|
"400 status code (no body)",
|
||||||
|
"413 status code (no body)",
|
||||||
|
]
|
||||||
|
|
||||||
|
cases.forEach((message) => {
|
||||||
|
const error = new APICallError({
|
||||||
|
message,
|
||||||
|
url: "https://example.com",
|
||||||
|
requestBodyValues: {},
|
||||||
|
statusCode: 400,
|
||||||
|
responseHeaders: { "content-type": "application/json" },
|
||||||
|
isRetryable: false,
|
||||||
|
})
|
||||||
|
const result = MessageV2.fromError(error, { providerID: "test" })
|
||||||
|
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not classify 429 no body as context overflow", () => {
|
||||||
|
const result = MessageV2.fromError(
|
||||||
|
new APICallError({
|
||||||
|
message: "429 status code (no body)",
|
||||||
|
url: "https://example.com",
|
||||||
|
requestBodyValues: {},
|
||||||
|
statusCode: 429,
|
||||||
|
responseHeaders: { "content-type": "application/json" },
|
||||||
|
isRetryable: false,
|
||||||
|
}),
|
||||||
|
{ providerID: "test" },
|
||||||
|
)
|
||||||
|
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(false)
|
||||||
|
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
test("serializes unknown inputs", () => {
|
test("serializes unknown inputs", () => {
|
||||||
const result = MessageV2.fromError(123, { providerID: "test" })
|
const result = MessageV2.fromError(123, { providerID: "test" })
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,14 @@ export type MessageAbortedError = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ContextOverflowError = {
|
||||||
|
name: "ContextOverflowError"
|
||||||
|
data: {
|
||||||
|
message: string
|
||||||
|
responseBody?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiError = {
|
export type ApiError = {
|
||||||
name: "APIError"
|
name: "APIError"
|
||||||
data: {
|
data: {
|
||||||
@@ -176,7 +184,13 @@ export type AssistantMessage = {
|
|||||||
created: number
|
created: number
|
||||||
completed?: number
|
completed?: number
|
||||||
}
|
}
|
||||||
error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError
|
error?:
|
||||||
|
| ProviderAuthError
|
||||||
|
| UnknownError
|
||||||
|
| MessageOutputLengthError
|
||||||
|
| MessageAbortedError
|
||||||
|
| ContextOverflowError
|
||||||
|
| ApiError
|
||||||
parentID: string
|
parentID: string
|
||||||
modelID: string
|
modelID: string
|
||||||
providerID: string
|
providerID: string
|
||||||
@@ -820,7 +834,13 @@ export type EventSessionError = {
|
|||||||
type: "session.error"
|
type: "session.error"
|
||||||
properties: {
|
properties: {
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError
|
error?:
|
||||||
|
| ProviderAuthError
|
||||||
|
| UnknownError
|
||||||
|
| MessageOutputLengthError
|
||||||
|
| MessageAbortedError
|
||||||
|
| ContextOverflowError
|
||||||
|
| ApiError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user