From a9bad753d48ca30627b759cb6db60d89289f2bf0 Mon Sep 17 00:00:00 2001 From: dev-mirzabicer Date: Fri, 6 Mar 2026 00:29:29 +0300 Subject: [PATCH] feat(opencode): add gpt-5.4 model support --- packages/opencode/src/plugin/codex.ts | 1 + packages/opencode/src/provider/models.ts | 58 ++++++++++++++-- packages/opencode/src/provider/transform.ts | 5 +- packages/opencode/test/plugin/codex.test.ts | 68 +++++++++++++++++++ .../opencode/test/provider/provider.test.ts | 30 ++++++++ .../opencode/test/provider/transform.test.ts | 25 +++++++ 6 files changed, 181 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 56931b2ed62..1e5c3e5c7d8 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -362,6 +362,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { "gpt-5.1-codex-mini", "gpt-5.2", "gpt-5.2-codex", + "gpt-5.4", "gpt-5.3-codex", "gpt-5.1-codex", ]) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index bae33178467..a492fd1e4c8 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -81,21 +81,71 @@ export namespace ModelsDev { export type Provider = z.infer + const gpt54: Model = { + id: "gpt-5.4", + name: "GPT-5.4", + family: "gpt", + release_date: "2026-03-05", + attachment: true, + reasoning: true, + temperature: false, + tool_call: true, + cost: { + input: 2.5, + output: 15, + cache_read: 0.25, + }, + limit: { + context: 1_050_000, + input: 922_000, + output: 128_000, + }, + modalities: { + input: ["text", "image"], + output: ["text"], + }, + options: {}, + } + + const gpt54op: Model = { + ...gpt54, + modalities: { + input: ["text", "image", "pdf"], + output: ["text"], + }, + provider: { + npm: "@ai-sdk/openai", + }, + } + + function add(data: Record, id: string, model: Model) { + const provider = data[id] + if (!provider) return + if (provider.models[model.id]) return + provider.models[model.id] = model + } + + function patch(data: Record) { + add(data, "openai", gpt54) + add(data, "opencode", gpt54op) + return data + } + function url() { return Flag.OPENCODE_MODELS_URL || "https://models.dev" } export const Data = lazy(async () => { const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {}) - if (result) return result + if (result) return patch(result as Record) // @ts-ignore const snapshot = await import("./models-snapshot") .then((m) => m.snapshot as Record) .catch(() => undefined) - if (snapshot) return snapshot - if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {} + if (snapshot) return patch(snapshot as Record) + if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return patch({}) const json = await fetch(`${url()}/api.json`).then((x) => x.text()) - return JSON.parse(json) + return patch(JSON.parse(json) as Record) }) export async function get() { diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 6980be05188..0993dafbe43 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -438,7 +438,7 @@ export namespace ProviderTransform { } } const copilotEfforts = iife(() => { - if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3")) + if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3") || id.includes("5.4")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"] return WIDELY_SUPPORTED_EFFORTS }) @@ -488,7 +488,8 @@ export namespace ProviderTransform { if (id === "gpt-5-pro") return {} const openaiEfforts = iife(() => { if (id.includes("codex")) { - if (id.includes("5.2") || id.includes("5.3")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"] + if (id.includes("5.2") || id.includes("5.3") || id.includes("5.4")) + return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"] return WIDELY_SUPPORTED_EFFORTS } const arr = [...WIDELY_SUPPORTED_EFFORTS] diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index 74d28ac9dcc..df80eaa8d07 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -1,10 +1,13 @@ import { describe, expect, test } from "bun:test" +import type { PluginInput } from "@opencode-ai/plugin" import { + CodexAuthPlugin, parseJwtClaims, extractAccountIdFromClaims, extractAccountId, type IdTokenClaims, } from "../../src/plugin/codex" +import type { Provider as ProviderNS } from "../../src/provider/provider" function createTestJwt(payload: object): string { const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url") @@ -12,6 +15,35 @@ function createTestJwt(payload: object): string { return `${header}.${body}.sig` } +function createModel(id: string) { + return { + id, + providerID: "openai", + api: { + id, + url: "https://api.openai.com", + npm: "@ai-sdk/openai", + }, + name: id, + capabilities: { + temperature: false, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 400_000, input: 272_000, output: 128_000 }, + status: "active", + options: {}, + headers: {}, + release_date: "2026-03-05", + variants: {}, + } +} + describe("plugin.codex", () => { describe("parseJwtClaims", () => { test("parses valid JWT with claims", () => { @@ -120,4 +152,40 @@ describe("plugin.codex", () => { ).toBe("acc-123") }) }) + + describe("auth loader", () => { + test("keeps gpt-5.4 in oauth model allowlist", async () => { + const hooks = await CodexAuthPlugin({ + client: { + auth: { + set: async () => {}, + }, + }, + } as unknown as PluginInput) + + if (!hooks.auth?.loader) { + throw new Error("Missing auth loader") + } + + const provider = { + models: { + "gpt-5.4": createModel("gpt-5.4"), + "gpt-4.1": createModel("gpt-4.1"), + }, + } as unknown as ProviderNS.Info + + await hooks.auth.loader( + async () => ({ + type: "oauth", + access: "token", + refresh: "refresh", + expires: Date.now() + 60_000, + }), + provider, + ) + + expect(provider.models["gpt-5.4"]).toBeDefined() + expect(provider.models["gpt-4.1"]).toBeUndefined() + }) + }) }) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 11c943db6f8..438f974a79d 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -110,6 +110,36 @@ test("enabled_providers restricts to only listed providers", async () => { }) }) +test("gpt-5.4 is backfilled for openai and opencode", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("OPENAI_API_KEY", "test-openai-key") + Env.set("OPENCODE_API_KEY", "test-opencode-key") + }, + fn: async () => { + const providers = await Provider.list() + const openai = providers["openai"]?.models["gpt-5.4"] + const opencode = providers["opencode"]?.models["gpt-5.4"] + expect(openai).toBeDefined() + expect(opencode).toBeDefined() + expect(openai?.limit.context).toBe(1_050_000) + expect(openai?.limit.input).toBe(922_000) + expect(opencode?.api.npm).toBe("@ai-sdk/openai") + }, + }) +}) + test("model whitelist filters models for provider", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 2329846351c..e01fc6b4ab4 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -138,6 +138,12 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { expect(result.textVerbosity).toBe("low") }) + test("gpt-5.4 should have textVerbosity set to low", () => { + const model = createGpt5Model("gpt-5.4") + const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) + expect(result.textVerbosity).toBe("low") + }) + test("gpt-5.1 should have textVerbosity set to low", () => { const model = createGpt5Model("gpt-5.1") const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) @@ -1989,6 +1995,25 @@ describe("ProviderTransform.variants", () => { }) }) + test("gpt-5.4 includes xhigh", () => { + const model = createMockModel({ + id: "gpt-5.4", + providerID: "github-copilot", + api: { + id: "gpt-5.4", + url: "https://api.githubcopilot.com", + npm: "@ai-sdk/github-copilot", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"]) + expect(result.xhigh).toEqual({ + reasoningEffort: "xhigh", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + }) + }) + test("gpt-5.2-codex includes xhigh", () => { const model = createMockModel({ id: "gpt-5.2-codex",