From 72fb0c6f5761c6625ea7443d0c66347ddb2acb29 Mon Sep 17 00:00:00 2001 From: LiWithDream Date: Sat, 13 Jun 2026 13:11:54 +0800 Subject: [PATCH] fix ChatGPT OAuth gpt-5.5 support --- packages/opencode/src/plugin/codex.ts | 58 ++++++++++--------- packages/opencode/src/provider/transform.ts | 36 ++++++++++-- packages/opencode/test/plugin/codex.test.ts | 51 ++++++++++++++++ .../opencode/test/provider/transform.test.ts | 48 +++++++++++++++ 4 files changed, 160 insertions(+), 33 deletions(-) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index a48e94c1..0c070287 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -14,6 +14,7 @@ const ISSUER = "https://auth.openai.com" const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses" const OAUTH_PORT = 1455 const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000 +const CHATGPT_OAUTH_MODELS = new Set(["gpt-5.2", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5"]) interface PkceCodes { verifier: string @@ -358,38 +359,41 @@ function waitForOAuthCallback(pkce: PkceCodes, state: string): Promise { return { + provider: { + id: "openai", + async models(provider, ctx) { + if (ctx.auth?.type !== "oauth") return provider.models + return Object.fromEntries( + Object.entries(provider.models) + .filter(([, model]) => model.api.id.includes("codex") || CHATGPT_OAUTH_MODELS.has(model.api.id)) + .map(([modelID, model]) => [ + modelID, + { + ...model, + cost: { + input: 0, + output: 0, + cache: { read: 0, write: 0 }, + }, + limit: + model.api.id === "gpt-5.5" + ? { + context: 400_000, + input: 272_000, + output: 128_000, + } + : model.limit, + }, + ]), + ) + }, + }, auth: { provider: "openai", - async loader(getAuth, provider) { + async loader(getAuth) { const auth = await getAuth() if (auth.type !== "oauth") return {} - // Filter models to only allowed Codex models for OAuth - const allowedModels = new Set([ - "gpt-5.1-codex", - "gpt-5.1-codex-max", - "gpt-5.1-codex-mini", - "gpt-5.2", - "gpt-5.2-codex", - "gpt-5.3-codex", - "gpt-5.4", - "gpt-5.4-mini", - ]) - for (const [modelId, model] of Object.entries(provider.models)) { - if (modelId.includes("codex")) continue - if (allowedModels.has(model.api.id)) continue - delete provider.models[modelId] - } - - // Zero out costs for Codex (included with ChatGPT subscription) - for (const model of Object.values(provider.models)) { - model.cost = { - input: 0, - output: 0, - cache: { read: 0, write: 0 }, - } - } - return { apiKey: OAUTH_DUMMY_KEY, async fetch(requestInput: RequestInfo | URL, init?: RequestInit) { diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 3821beeb..e476f08e 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -398,12 +398,18 @@ function limitImages(msgs: ModelMessage[]): ModelMessage[] { if (part.type !== "image") return part if (toDrop > 0) { toDrop-- - return { type: "text" as const, text: `[Image omitted: exceeds the configured limit of ${maxImages} prompt image(s).]` } + return { + type: "text" as const, + text: `[Image omitted: exceeds the configured limit of ${maxImages} prompt image(s).]`, + } } if (maxSize !== undefined) { const size = imageByteSize(String(part.image)) if (size !== undefined && size > maxSize) { - return { type: "text" as const, text: `[Image omitted: exceeds the configured ${maxSize}-byte prompt image size limit.]` } + return { + type: "text" as const, + text: `[Image omitted: exceeds the configured ${maxSize}-byte prompt image size limit.]`, + } } } return part @@ -991,6 +997,7 @@ export function options(input: { input.model.api.npm === "@ai-sdk/github-copilot" ) { result["reasoningSummary"] = "auto" + result["include"] = ["reasoning.encrypted_content"] } } @@ -999,6 +1006,7 @@ export function options(input: { if ( input.model.api.id.includes("gpt-5.") && !input.model.api.id.includes("codex") && + !input.model.api.id.includes("gpt-5.5") && !input.model.api.id.includes("-chat") && input.model.providerID !== "azure" ) { @@ -1159,9 +1167,7 @@ function flattenDiscriminatedUnion(schema: JSONSchema.BaseSchema | JSONSchema7): const propertyOwners: Record = {} for (const v of variants) { if (!v.properties) continue - const variantValue = discriminator - ? (v.properties as Record)[discriminator]?.const - : undefined + const variantValue = discriminator ? (v.properties as Record)[discriminator]?.const : undefined for (const [key, prop] of Object.entries(v.properties as Record)) { if (key === discriminator) continue if (!(key in properties)) properties[key] = prop @@ -1200,7 +1206,9 @@ function flattenDiscriminatedUnion(schema: JSONSchema.BaseSchema | JSONSchema7): properties[discriminator] = { type: "string", enum: enumValues, - description: baseDescription ? `${baseDescription}\n\nPer-${discriminator}: ${hints}.` : `Per-${discriminator}: ${hints}.`, + description: baseDescription + ? `${baseDescription}\n\nPer-${discriminator}: ${hints}.` + : `Per-${discriminator}: ${hints}.`, } } @@ -1212,6 +1220,18 @@ function flattenDiscriminatedUnion(schema: JSONSchema.BaseSchema | JSONSchema7): } as JSONSchema7 } +function flattenNestedDiscriminatedUnions(schema: JSONSchema7): JSONSchema7 { + const visit = (node: unknown): unknown => { + if (Array.isArray(node)) return node.map(visit) + if (node === null || typeof node !== "object") return node + + const flattened = flattenDiscriminatedUnion(node as JSONSchema7) as Record + return Object.fromEntries(Object.entries(flattened).map(([key, value]) => [key, visit(value)])) + } + + return visit(schema) as JSONSchema7 +} + export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JSONSchema7): JSONSchema7 { /* if (["openai", "azure"].includes(providerID)) { @@ -1239,6 +1259,10 @@ export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JS // and zod's runtime parse still enforces per-variant required fields strictly. schema = flattenDiscriminatedUnion(schema) + if (model.providerID === "openai" && model.api.id.includes("gpt-5.5")) { + schema = flattenNestedDiscriminatedUnions(schema) + } + // Convert integer enums to string enums for Google/Gemini if (model.providerID === "google" || model.api.id.includes("gemini")) { const isPlainObject = (node: unknown): node is Record => diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index 74d28ac9..8ab3c559 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test" +import type { Auth } from "@mimo-ai/sdk" +import type { Provider } from "@mimo-ai/sdk/v2" import { + CodexAuthPlugin, parseJwtClaims, extractAccountIdFromClaims, extractAccountId, @@ -13,6 +16,54 @@ function createTestJwt(payload: object): string { } describe("plugin.codex", () => { + test("keeps gpt-5.5 available for ChatGPT Plus/Pro OAuth", async () => { + const hooks = await CodexAuthPlugin({} as never) + const provider = { + models: { + "gpt-5.4": { + id: "gpt-5.4", + api: { id: "gpt-5.4" }, + cost: { input: 1, output: 2, cache: { read: 3, write: 4 } }, + limit: { context: 1, input: 1, output: 1 }, + }, + "gpt-5.5": { + id: "gpt-5.5", + api: { id: "gpt-5.5" }, + cost: { input: 1, output: 2, cache: { read: 3, write: 4 } }, + limit: { context: 1, input: 1, output: 1 }, + }, + "gpt-5.1": { + id: "gpt-5.1", + api: { id: "gpt-5.1" }, + cost: { input: 1, output: 2, cache: { read: 3, write: 4 } }, + limit: { context: 1, input: 1, output: 1 }, + }, + }, + } as unknown as Provider + + const models = await hooks.provider!.models!(provider, { auth: { type: "oauth" } as Auth }) + + expect(models["gpt-5.4"]).toBeDefined() + expect(models["gpt-5.5"]).toBeDefined() + expect(models["gpt-5.1"]).toBeUndefined() + expect(models["gpt-5.5"].cost).toEqual({ input: 0, output: 0, cache: { read: 0, write: 0 } }) + expect(models["gpt-5.5"].limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 }) + }) + + test("does not filter models for OpenAI API key auth", async () => { + const hooks = await CodexAuthPlugin({} as never) + const provider = { + models: { + "gpt-5.1": { id: "gpt-5.1", api: { id: "gpt-5.1" } }, + "gpt-5.5": { id: "gpt-5.5", api: { id: "gpt-5.5" } }, + }, + } as unknown as Provider + + const models = await hooks.provider!.models!(provider, { auth: { type: "api", key: "sk-test" } as Auth }) + + expect(models).toBe(provider.models) + }) + describe("parseJwtClaims", () => { test("parses valid JWT with claims", () => { const payload = { email: "test@example.com", chatgpt_account_id: "acc-123" } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 89231f27..bdbf8e2c 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -333,6 +333,15 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { expect(result.textVerbosity).toBe("low") }) + test("gpt-5.5 should include encrypted reasoning content without textVerbosity", () => { + const model = createGpt5Model("gpt-5.5") + const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) + expect(result.reasoningEffort).toBe("medium") + expect(result.reasoningSummary).toBe("auto") + expect(result.include).toEqual(["reasoning.encrypted_content"]) + expect(result.textVerbosity).toBeUndefined() + }) + test("gpt-5.2-chat-latest should NOT have textVerbosity set (only supports medium)", () => { const model = createGpt5Model("gpt-5.2-chat-latest") const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) @@ -3376,4 +3385,43 @@ describe("ProviderTransform.schema - openai discriminated-union flatten", () => expect(result.properties.a).toBeDefined() expect(result.anyOf).toBeUndefined() }) + + test("gpt-5.5 — flattens nested discriminated unions in tool parameters", () => { + const nested = { + type: "object", + properties: { + operation: { + type: "object", + anyOf: anyOfSchema.anyOf, + }, + }, + required: ["operation"], + additionalProperties: false, + } as any + + const result = ProviderTransform.schema({ providerID: "openai", api: { id: "gpt-5.5" } } as any, nested) as any + + expect(result.properties.operation.anyOf).toBeUndefined() + expect(result.properties.operation.type).toBe("object") + expect(result.properties.operation.properties.action.enum).toEqual(["create", "list", "rename"]) + expect(result.properties.operation.required).toEqual(["action"]) + }) + + test("non-gpt-5.5 — leaves nested discriminated unions untouched", () => { + const nested = { + type: "object", + properties: { + operation: { + type: "object", + anyOf: anyOfSchema.anyOf, + }, + }, + required: ["operation"], + additionalProperties: false, + } as any + + const result = ProviderTransform.schema({ providerID: "openai", api: { id: "gpt-5.4" } } as any, nested) as any + + expect(result.properties.operation.anyOf).toHaveLength(3) + }) })