diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 65ac72e05..3119c2bce 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -796,7 +796,76 @@ export namespace MessageV2 { case e instanceof Error: return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject() default: - return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }) + try { + const json = iife(() => { + if (typeof e === "string") { + try { + return JSON.parse(e) + } catch { + return undefined + } + } + + if (typeof e === "object" && e !== null) { + 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() + } + } + } + } catch {} + return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject() } } } diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 2f632ad1c..39c58bb6e 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -784,3 +784,57 @@ describe("session.message-v2.toModelMessage", () => { ]) }) }) + +describe("session.message-v2.fromError", () => { + test("serializes response error codes", () => { + const cases = [ + { + code: "context_length_exceeded", + message: "Input exceeds context window of this model", + }, + { + code: "insufficient_quota", + message: "Quota exceeded. Check your plan and billing details.", + }, + { + code: "usage_not_included", + message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.", + }, + { + code: "invalid_prompt", + message: "Invalid prompt from test", + }, + ] + + cases.forEach((item) => { + const input = { + type: "error", + error: { + code: item.code, + message: item.code === "invalid_prompt" ? item.message : undefined, + }, + } + const result = MessageV2.fromError(input, { providerID: "test" }) + + expect(result).toStrictEqual({ + name: "APIError", + data: { + message: item.message, + isRetryable: false, + responseBody: JSON.stringify(input), + }, + }) + }) + }) + + test("serializes unknown inputs", () => { + const result = MessageV2.fromError(123, { providerID: "test" }) + + expect(result).toStrictEqual({ + name: "UnknownError", + data: { + message: "123", + }, + }) + }) +})