From 51c5f614db36dfe63785c70715c81e5e81a77fe6 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 28 Apr 2026 21:20:46 +0800 Subject: [PATCH 1/3] fix(openai): use Codex GPT-5.5 OAuth limits --- packages/opencode/src/plugin/codex.ts | 10 ++++ packages/opencode/test/plugin/codex.test.ts | 55 +++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 5e643d8a0..fdf9d7812 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -449,6 +449,16 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { output: 0, cache: { read: 0, write: 0 }, } + + // GPT-5.5 has a smaller effective window when routed through Codex OAuth. + if (model.id.includes("gpt-5.5")) { + model.limit = { + context: 400_000, + // @ts-expect-error Provider SDK v1 model limits do not type input, but opencode uses it for compaction. + input: 272_000, + output: 128_000, + } + } } return { diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index eaf58ba47..6a110a3da 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import { + CodexAuthPlugin, parseJwtClaims, extractAccountIdFromClaims, extractAccountId, @@ -143,6 +144,60 @@ describe("plugin.codex", () => { }) }) + describe("CodexAuthPlugin", () => { + test("overrides GPT-5.5 limits for OAuth Codex plans", async () => { + const provider = { + models: { + "gpt-5.5": { + id: "gpt-5.5", + api: { id: "gpt-5.5" }, + cost: { + input: 2, + output: 8, + cache: { read: 1, write: 2 }, + }, + limit: { + context: 1_050_000, + input: 922_000, + output: 128_000, + }, + }, + }, + } + const hooks = await CodexAuthPlugin({ + client: {} as never, + project: {} as never, + directory: "", + worktree: "", + experimental_workspace: { + register() {}, + }, + } as never) + + await hooks.auth!.loader!( + async () => + ({ + type: "oauth", + access: "access", + refresh: "refresh", + expires: Date.now() + 60_000, + }) as never, + provider as never, + ) + + expect(provider.models["gpt-5.5"].limit).toEqual({ + context: 400_000, + input: 272_000, + output: 128_000, + }) + expect(provider.models["gpt-5.5"].cost).toEqual({ + input: 0, + output: 0, + cache: { read: 0, write: 0 }, + }) + }) + }) + describe("formatOAuthFailure", () => { test("includes safe JSON error fields and request metadata", async () => { const response = new Response( From 6a45ea1d1b8f400f325f83497e35525e7574d70e Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 28 Apr 2026 21:20:56 +0800 Subject: [PATCH 2/3] fix(provider): retry OpenAI server_error chunks --- packages/opencode/src/provider/error.ts | 12 ++++++++++-- packages/opencode/test/session/retry.test.ts | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 243e99579..9862b2deb 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -111,12 +111,13 @@ export type ParsedStreamError = | { type: "api_error" message: string - isRetryable: false + isRetryable: boolean responseBody: string } export function parseStreamError(input: unknown): ParsedStreamError | undefined { - const body = json(input) + const raw = json(input) + const body = typeof raw?.message === "string" ? (json(raw.message) ?? raw) : raw if (!body) return const responseBody = JSON.stringify(body) @@ -150,6 +151,13 @@ export function parseStreamError(input: unknown): ParsedStreamError | undefined isRetryable: false, responseBody, } + case "server_error": + return { + type: "api_error", + message: typeof body?.error?.message === "string" ? body?.error?.message : "Server error.", + isRetryable: true, + responseBody, + } } } diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 712bacfdb..4701d805a 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -294,4 +294,23 @@ describe("session.message-v2.fromError", () => { const result = MessageV2.fromError(error, { providerID: ProviderID.make("openai") }) as MessageV2.APIError expect(result.data.isRetryable).toBe(true) }) + + test("converts OpenAI server_error stream chunks to retryable APIError", () => { + const result = MessageV2.fromError( + { + message: JSON.stringify({ + type: "error", + error: { + code: "server_error", + message: "An error occurred while processing your request.", + }, + }), + }, + { providerID: ProviderID.make("openai") }, + ) + + expect(MessageV2.APIError.isInstance(result)).toBe(true) + expect((result as MessageV2.APIError).data.isRetryable).toBe(true) + expect(SessionRetry.retryable(result)).toBe("An error occurred while processing your request.") + }) }) From 32a79ecdfbd13c9451572f8a146228ae5e2b72d2 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 28 Apr 2026 22:45:01 +0800 Subject: [PATCH 3/3] fix(openai): address OAuth review feedback --- packages/opencode/src/plugin/codex.ts | 11 +++--- packages/opencode/src/provider/error.ts | 19 +++++++--- packages/opencode/test/plugin/codex.test.ts | 22 ++++++++++++ packages/opencode/test/session/retry.test.ts | 37 ++++++++++++++++++++ 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index fdf9d7812..baacb04c1 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -108,6 +108,10 @@ export function shouldKeepCodexOAuthModel(modelId: string, apiId: string): boole return major > 5 || (major === 5 && minor > 4) } +export function hasCodexOAuthGpt55Limit(apiId: string): boolean { + return /^gpt-5\.5(?:$|-)/.test(apiId) +} + function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string { const params = new URLSearchParams({ response_type: "code", @@ -450,14 +454,13 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { cache: { read: 0, write: 0 }, } - // GPT-5.5 has a smaller effective window when routed through Codex OAuth. - if (model.id.includes("gpt-5.5")) { - model.limit = { + if (hasCodexOAuthGpt55Limit(model.api.id)) { + const limit: typeof model.limit & { input: number } = { context: 400_000, - // @ts-expect-error Provider SDK v1 model limits do not type input, but opencode uses it for compaction. input: 272_000, output: 128_000, } + model.limit = limit } } diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 9862b2deb..68f9513fe 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -102,6 +102,10 @@ function json(input: unknown) { return undefined } +function isRecord(input: unknown): input is Record { + return typeof input === "object" && input !== null && !Array.isArray(input) +} + export type ParsedStreamError = | { type: "context_overflow" @@ -117,13 +121,18 @@ export type ParsedStreamError = 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 + if (!isRecord(raw)) return + + const inner = typeof raw.message === "string" ? json(raw.message) : undefined + // OpenAI stream errors can arrive wrapped in an Error-like object. Use the + // inner provider payload so responseBody matches the payload users need. + const body = isRecord(inner) && inner.type === "error" ? inner : raw const responseBody = JSON.stringify(body) if (body.type !== "error") return - switch (body?.error?.code) { + const error = isRecord(body.error) ? body.error : undefined + switch (error?.code) { case "context_length_exceeded": return { type: "context_overflow", @@ -147,14 +156,14 @@ export function parseStreamError(input: unknown): ParsedStreamError | undefined case "invalid_prompt": return { type: "api_error", - message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.", + message: typeof error.message === "string" ? error.message : "Invalid prompt.", isRetryable: false, responseBody, } case "server_error": return { type: "api_error", - message: typeof body?.error?.message === "string" ? body?.error?.message : "Server error.", + message: typeof error.message === "string" ? error.message : "Server error.", isRetryable: true, responseBody, } diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index 6a110a3da..4f83f92a2 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -5,6 +5,7 @@ import { extractAccountIdFromClaims, extractAccountId, formatOAuthFailure, + hasCodexOAuthGpt55Limit, shouldKeepCodexOAuthModel, type IdTokenClaims, } from "../../src/plugin/codex" @@ -144,6 +145,20 @@ describe("plugin.codex", () => { }) }) + describe("hasCodexOAuthGpt55Limit", () => { + test("matches GPT-5.5 API ids and explicit variants", () => { + expect(hasCodexOAuthGpt55Limit("gpt-5.5")).toBe(true) + expect(hasCodexOAuthGpt55Limit("gpt-5.5-codex")).toBe(true) + expect(hasCodexOAuthGpt55Limit("gpt-5.5-mini")).toBe(true) + }) + + test("does not match unrelated future models", () => { + expect(hasCodexOAuthGpt55Limit("gpt-5.50")).toBe(false) + expect(hasCodexOAuthGpt55Limit("chatgpt-5.5")).toBe(false) + expect(hasCodexOAuthGpt55Limit("gpt-5.6")).toBe(false) + }) + }) + describe("CodexAuthPlugin", () => { test("overrides GPT-5.5 limits for OAuth Codex plans", async () => { const provider = { @@ -164,6 +179,13 @@ describe("plugin.codex", () => { }, }, } + + expect(provider.models["gpt-5.5"].limit).toEqual({ + context: 1_050_000, + input: 922_000, + output: 128_000, + }) + const hooks = await CodexAuthPlugin({ client: {} as never, project: {} as never, diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 4701d805a..47f679296 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -313,4 +313,41 @@ describe("session.message-v2.fromError", () => { expect((result as MessageV2.APIError).data.isRetryable).toBe(true) expect(SessionRetry.retryable(result)).toBe("An error occurred while processing your request.") }) + + test("uses fallback message for OpenAI server_error stream chunks without message", () => { + const result = MessageV2.fromError( + { + message: JSON.stringify({ + type: "error", + error: { + code: "server_error", + }, + }), + }, + { providerID: ProviderID.make("openai") }, + ) + + expect(MessageV2.APIError.isInstance(result)).toBe(true) + expect((result as MessageV2.APIError).data.isRetryable).toBe(true) + expect((result as MessageV2.APIError).data.message).toBe("Server error.") + expect(SessionRetry.retryable(result)).toBe("Server error.") + }) + + test("does not convert unknown OpenAI stream error chunks to retryable APIError", () => { + const result = MessageV2.fromError( + { + message: JSON.stringify({ + type: "error", + error: { + code: "bad_request", + message: "Bad request", + }, + }), + }, + { providerID: ProviderID.make("openai") }, + ) + + expect(MessageV2.APIError.isInstance(result)).toBe(false) + expect(SessionRetry.retryable(result)).toBeUndefined() + }) })