From 671e164f8d27248a37f45df6c19d4ef7a27bf6d9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:43:38 -0500 Subject: [PATCH] refactor(core): contain Codex in OpenAI plugin (#39734) --- packages/core/src/model-resolver.ts | 26 --- .../core/src/plugin/provider/openai-codex.ts | 42 ---- packages/core/src/plugin/provider/openai.ts | 64 ++++-- packages/core/test/model-resolver.test.ts | 70 +----- .../core/test/plugin/provider-openai.test.ts | 207 ++++++------------ 5 files changed, 112 insertions(+), 297 deletions(-) delete mode 100644 packages/core/src/plugin/provider/openai-codex.ts diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index 1d3396736b0..a3bfd29e41e 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -18,7 +18,6 @@ import { Credential } from "./credential" import { Integration } from "./integration" import { Capabilities, ID, Info, Ref, VariantID } from "./model" import { Npm } from "@opencode-ai/util/npm" -import { OpenAICodex } from "./plugin/provider/openai-codex" import { Provider } from "./provider" export class VariantUnavailableError extends Schema.TaggedErrorClass()( @@ -153,12 +152,7 @@ export const fromCatalogModel = ( const packageName = Provider.packageName(resolved.package) const key = apiKey(resolved, credential) - if (OpenAICodex.isChatGPT(credential) && !Provider.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { - return Effect.succeed(codexModel(resolved, credential, key)) - } - if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { - if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key)) return Effect.succeed( withDefaults(resolved, OpenAIResponses.route) .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) @@ -227,10 +221,6 @@ export const fromCatalogModel = ( }) } -const isNativeOpenAI = (packageName: string | undefined) => - packageName === "@opencode-ai/ai/providers/openai" || - packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true - const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => { if (!credential) return {} if (credential.type === "key") return { apiKey: credential.key } @@ -252,22 +242,6 @@ const withoutNativeAuthSettings = (settings: Record) => { return rest } -const codexModel = ( - model: Info, - credential: Credential.Value | undefined, - key: ReturnType | undefined, -) => { - const account = OpenAICodex.accountID(credential) - return withDefaults(model, OpenAIResponses.route) - .with({ - endpoint: { baseURL: OpenAICodex.baseURL }, - auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen( - account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }), - ), - }) - .model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) -} - const unsupported = (model: Info) => new UnsupportedPackageError({ providerID: model.providerID, diff --git a/packages/core/src/plugin/provider/openai-codex.ts b/packages/core/src/plugin/provider/openai-codex.ts deleted file mode 100644 index eb734b29f3d..00000000000 --- a/packages/core/src/plugin/provider/openai-codex.ts +++ /dev/null @@ -1,42 +0,0 @@ -export * as OpenAICodex from "./openai-codex" - -// TEMPORARY SEAM (#34765): plugins have no hook into LLM route construction, so -// Codex routing lives in ModelResolver and catalog filtering. -// in OpenAIPlugin, sharing this module. Once the native provider packages land -// (#33689/#33925/#34462) this should collapse into the native OpenAI provider. -// The eligibility rules mirror V1's CodexAuthPlugin allowlist; models.dev has no -// plan-eligibility data for OpenAI today, but models other vendors' subscriptions -// as dedicated providers (e.g. zai-coding-plan) - a future openai-chatgpt-plan -// provider entry could replace the hardcoded rules with catalog data. - -/** ChatGPT-plan requests must target the codex backend instead of the public API. */ -export const baseURL = "https://chatgpt.com/backend-api/codex" - -const methodIDs: readonly string[] = ["chatgpt-browser", "chatgpt-headless"] - -/** Structural credential shape so both core and plugin-facing credential types fit. */ -type CredentialLike = { - readonly type: string - readonly methodID?: string - readonly metadata?: Record | undefined -} - -export const isChatGPT = (credential: CredentialLike | undefined) => - credential?.type === "oauth" && credential.methodID !== undefined && methodIDs.includes(credential.methodID) - -export const accountID = (credential: CredentialLike | undefined) => { - if (!isChatGPT(credential)) return undefined - const value = credential?.metadata?.accountID - return typeof value === "string" ? value : undefined -} - -const allowed = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"]) -const disallowed = new Set(["gpt-5.5-pro", "gpt-5.6"]) - -/** Which API model ids a ChatGPT subscription may call through the codex backend. */ -export const eligible = (apiID: string) => { - if (allowed.has(apiID)) return true - if (disallowed.has(apiID)) return false - const match = apiID.match(/^gpt-(\d+\.\d+)/) - return match ? Number.parseFloat(match[1]) > 5.4 : false -} diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index e133d9edc22..ef0011c259b 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -10,14 +10,16 @@ import { Model } from "../../model" import { OauthCallbackPage } from "../../oauth/page" import { Provider } from "../../provider" import type { PluginInternal } from "../internal" -import { OpenAICodex } from "./openai-codex" const clientID = "app_EMoamEEZ73f0CkXaXp7hrann" const issuer = "https://auth.openai.com" const callbackPort = 1455 const pollingSafetyMargin = 3000 +const codexBaseURL = "https://chatgpt.com/backend-api/codex" const browserMethodID = Integration.MethodID.make("chatgpt-browser") const headlessMethodID = Integration.MethodID.make("chatgpt-headless") +const codexAllowed = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"]) +const codexDisallowed = new Set(["gpt-5.5-pro", "gpt-5.6"]) type Pkce = { verifier: string @@ -164,14 +166,18 @@ export const OpenAIPlugin = define({ effect: Effect.fn(function* (ctx) { const bus = yield* Bus.Service const loading = Semaphore.makeUnsafe(1) - let chatgpt = false + let chatgpt: Credential.OAuth | undefined const load = Effect.fn("OpenAIPlugin.load")(function* () { const connection = yield* ctx.integration.connection.active("openai") const credential = connection ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined))) : undefined - chatgpt = OpenAICodex.isChatGPT(credential) + chatgpt = + credential?.type === "oauth" && + (credential.methodID === browserMethodID || credential.methodID === headlessMethodID) + ? credential + : undefined }) yield* ctx.integration.transform((draft) => { @@ -183,16 +189,19 @@ export const OpenAIPlugin = define({ for (const item of evt.provider.list()) { if (!Provider.isAISDK(item.provider.package)) continue if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai") continue - if (!item.models.has(Model.ID.make("gpt-5-chat-latest"))) continue - evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => { - // OpenAIPlugin sends OpenAI models through Responses; this alias is a - // chat-completions-only model, so hide it only from OpenAI's catalog. - model.enabled = false + evt.provider.update(item.provider.id, (provider) => { + provider.package = "@opencode-ai/ai/providers/openai" }) } if (!chatgpt) return const item = evt.provider.get(Provider.ID.openai) if (!item) return + item.provider.settings = Provider.mergeOverlay(item.provider.settings, { baseURL: codexBaseURL }) + const account = chatgpt.metadata?.accountID + item.provider.headers = Provider.mergeHeaders( + item.provider.headers, + typeof account === "string" ? { "chatgpt-account-id": account } : undefined, + ) for (const model of item.models.values()) { // ChatGPT-plan tokens only authorize codex-eligible models, and the // subscription covers usage, so hide the rest and zero the cost. @@ -201,14 +210,36 @@ export const OpenAIPlugin = define({ draft.enabled = false return } - if (!OpenAICodex.eligible(draft.modelID ?? draft.id)) { + const apiID = draft.modelID ?? draft.id + const match = apiID.match(/^gpt-(\d+\.\d+)/) + if ( + !codexAllowed.has(apiID) && + (codexDisallowed.has(apiID) || !match || Number.parseFloat(match[1]) <= 5.4) + ) { draft.enabled = false return } draft.cost = [] + if (draft.id.includes("gpt-5.5")) { + draft.limit = { context: 400_000, input: 272_000, output: 128_000 } + } + if (draft.id.includes("gpt-5.6")) { + draft.limit = { context: 500_000, input: 372_000, output: 128_000 } + } }) } }) + yield* ctx.session.hook("request", (evt) => + Effect.sync(() => { + if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return + const url = new URL(evt.url) + if (url.origin === "https://api.openai.com") { + evt.url = `${codexBaseURL}${url.pathname.replace(/^\/v1/, "")}${url.search}` + } + evt.headers.originator = "opencode" + evt.headers["session-id"] = evt.sessionID + }), + ) const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( @@ -216,21 +247,6 @@ export const OpenAIPlugin = define({ Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), ) - yield* ctx.aisdk.hook( - "sdk", - Effect.fn(function* (evt) { - if (evt.package !== "@ai-sdk/openai") return - const mod = yield* Effect.promise(() => import("@ai-sdk/openai")) - evt.sdk = mod.createOpenAI(evt.options) - }), - ) - yield* ctx.aisdk.hook( - "language", - Effect.fn(function* (evt) { - if (evt.model.providerID !== Provider.ID.openai) return - evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id) - }), - ) }), } satisfies PluginInternal.InternalPlugin) diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index ea31e93c575..b329288da2d 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -307,12 +307,12 @@ describe("ModelResolver", () => { }), ) - it.effect("routes ChatGPT OAuth credentials to the codex backend", () => + it.effect("applies plugin-projected OpenAI endpoint and headers", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(Provider.aisdk("@ai-sdk/openai"), { - settings: { baseURL: "https://openai.example/v1" }, - headers: {}, + model("@opencode-ai/ai/providers/openai", { + settings: { baseURL: "https://chatgpt.com/backend-api/codex" }, + headers: { "chatgpt-account-id": "acct_123" }, body: {}, }), Credential.OAuth.make({ @@ -337,37 +337,8 @@ describe("ModelResolver", () => { id: "openai-responses", endpoint: { baseURL: "https://chatgpt.com/backend-api/codex" }, }) + expect(resolved.route.defaults.headers).toMatchObject({ "chatgpt-account-id": "acct_123" }) expect(headers.authorization).toBe("Bearer chatgpt-token") - expect(headers["chatgpt-account-id"]).toBe("acct_123") - }), - ) - - it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () => - Effect.gen(function* () { - const resolved = yield* ModelResolver.fromCatalogModel( - model("@opencode-ai/ai/providers/openai", { - settings: { baseURL: "https://openai.example/v1" }, - }), - Credential.OAuth.make({ - type: "oauth", - methodID: Integration.MethodID.make("chatgpt-browser"), - access: "chatgpt-token", - refresh: "refresh", - expires: Date.now() + 60_000, - metadata: { accountID: "acct_123" }, - }), - ) - const headers = yield* resolved.route.auth.apply({ - request: LLM.request({ model: resolved, prompt: "Hello" }), - method: "POST", - url: "https://chatgpt.com/backend-api/codex/responses", - body: "{}", - headers: Headers.empty, - }) - - expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex") - expect(headers.authorization).toBe("Bearer chatgpt-token") - expect(headers["chatgpt-account-id"]).toBe("acct_123") }), ) @@ -407,37 +378,6 @@ describe("ModelResolver", () => { }), ) - it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () => - Effect.gen(function* () { - const resolved = yield* ModelResolver.fromCatalogModel( - model(Provider.aisdk("@ai-sdk/openai"), { - settings: { baseURL: "https://openai.example/v1" }, - headers: {}, - body: {}, - }), - Credential.OAuth.make({ - type: "oauth", - methodID: Integration.MethodID.make("chatgpt-headless"), - access: "chatgpt-token", - refresh: "refresh", - expires: Date.now() + 60_000, - }), - ) - const request = LLM.request({ model: resolved, prompt: "Hello" }) - const headers = yield* resolved.route.auth.apply({ - request, - method: "POST", - url: "https://chatgpt.com/backend-api/codex/responses", - body: "{}", - headers: Headers.empty, - }) - - expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex") - expect(headers.authorization).toBe("Bearer chatgpt-token") - expect(headers["chatgpt-account-id"]).toBeUndefined() - }), - ) - it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index 75b6385c021..37b71d3a212 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -1,7 +1,7 @@ -import { AISDK } from "@opencode-ai/core/aisdk" import { Money } from "@opencode-ai/schema/money" +import { Agent } from "@opencode-ai/schema/agent" +import { Session } from "@opencode-ai/schema/session" import { describe, expect } from "bun:test" -import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" @@ -9,6 +9,7 @@ import { Integration } from "@opencode-ai/core/integration" import { Model } from "@opencode-ai/core/model" import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" +import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai" import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" @@ -18,7 +19,6 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* Plugin.Service - const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) const integrations = yield* Integration.Service yield* OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations)) @@ -29,19 +29,6 @@ function required(value: T | undefined): T { return value } -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - describe("OpenAIPlugin", () => { it.effect("registers browser and headless ChatGPT OAuth methods", () => Effect.gen(function* () { @@ -61,104 +48,6 @@ describe("OpenAIPlugin", () => { }), ) - it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () => - Effect.gen(function* () { - const plugin = yield* Plugin.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.make("custom-openai"), Model.ID.make("gpt-5")), - modelID: Model.ID.make("gpt-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/openai", - options: { name: "custom-openai", apiKey: "test" }, - }) - expect(result.sdk?.responses("gpt-5").provider).toBe("custom-openai.responses") - }), - ) - - it.effect("ignores non-OpenAI SDK packages", () => - Effect.gen(function* () { - const plugin = yield* Plugin.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.openai, Model.ID.make("gpt-5")), - modelID: Model.ID.make("gpt-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/openai-compatible", - options: { name: "openai" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("uses the Responses API for language models", () => - Effect.gen(function* () { - const plugin = yield* Plugin.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.openai, Model.ID.make("alias")), - modelID: Model.ID.make("gpt-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }) - expect(calls).toEqual(["responses:gpt-5"]) - expect(result.language).toBeDefined() - }), - ) - - it.effect("ignores non-OpenAI providers", () => - Effect.gen(function* () { - const plugin = yield* Plugin.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.anthropic, Model.ID.make("gpt-5")), - modelID: Model.ID.make("gpt-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }) - expect(calls).toEqual([]) - expect(result.language).toBeUndefined() - }), - ) - - it.effect("disables gpt-5-chat-latest during catalog transforms", () => - Effect.gen(function* () { - const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => { - const item = Provider.Info.make({ - ...Provider.Info.empty(Provider.ID.openai), - package: Provider.aisdk("@ai-sdk/openai"), - }) - catalog.provider.update(item.id, (draft) => { - draft.package = item.package - }) - catalog.model.update(item.id, Model.ID.make("gpt-5"), () => {}) - catalog.model.update(item.id, Model.ID.make("gpt-5-chat-latest"), () => {}) - }) - yield* addPlugin() - expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5"))).enabled).toBe(true) - expect( - required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5-chat-latest"))).enabled, - ).toBe(false) - }), - ) - it.effect("filters the OpenAI catalog to codex-eligible models under a ChatGPT connection", () => Effect.gen(function* () { const catalog = yield* Catalog.Service @@ -172,6 +61,7 @@ describe("OpenAIPlugin", () => { draft.package = item.package }) catalog.model.update(item.id, Model.ID.make("gpt-5.5"), (model) => { + model.limit = { context: 1_050_000, input: 922_000, output: 128_000 } model.cost = [ { input: Money.USDPerMillionTokens.make(1), @@ -189,7 +79,9 @@ describe("OpenAIPlugin", () => { model.body = { reasoning: { mode: "pro" } } }) catalog.model.update(item.id, Model.ID.make("gpt-5.6"), () => {}) - catalog.model.update(item.id, Model.ID.make("gpt-5.6-sol"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.6-sol"), (model) => { + model.limit = { context: 1_050_000, input: 922_000, output: 128_000 } + }) catalog.model.update(item.id, Model.ID.make("gpt-4.1"), () => {}) }) yield* credentials.create({ @@ -205,8 +97,47 @@ describe("OpenAIPlugin", () => { }) yield* addPlugin() + const request = yield* (yield* PluginHooks.Service).trigger("session", "request", { + sessionID: Session.ID.make("ses_test"), + agent: Agent.ID.make("build"), + model: Model.Ref.make({ providerID: Provider.ID.openai, id: Model.ID.make("gpt-5.5") }), + url: "https://api.openai.com/v1/responses", + method: "POST", + headers: {}, + body: "{}", + }) + const custom = yield* (yield* PluginHooks.Service).trigger("session", "request", { + sessionID: Session.ID.make("ses_test"), + agent: Agent.ID.make("build"), + model: Model.Ref.make({ providerID: Provider.ID.make("custom-openai"), id: Model.ID.make("gpt-5.5") }), + url: "https://custom.example/v1/responses", + method: "POST", + headers: {}, + body: "{}", + }) + const proxy = yield* (yield* PluginHooks.Service).trigger("session", "request", { + sessionID: Session.ID.make("ses_test"), + agent: Agent.ID.make("build"), + model: Model.Ref.make({ providerID: Provider.ID.openai, id: Model.ID.make("gpt-5.5") }), + url: "https://proxy.example/v1/responses?region=us", + method: "POST", + headers: {}, + body: "{}", + }) + + const provider = required(yield* catalog.provider.get(Provider.ID.openai)) + expect(provider.package).toBe("@opencode-ai/ai/providers/openai") + expect(provider.settings).toMatchObject({ baseURL: "https://chatgpt.com/backend-api/codex" }) + expect(provider.headers).toMatchObject({ "chatgpt-account-id": "acct_123" }) + expect(request.url).toBe("https://chatgpt.com/backend-api/codex/responses") + expect(request.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" }) + expect(custom.headers).toEqual({}) + expect(proxy.url).toBe("https://proxy.example/v1/responses?region=us") + expect(proxy.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" }) const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))) + expect(eligible.package).toBe("@opencode-ai/ai/providers/openai") expect(eligible.cost).toEqual([]) + expect(eligible.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 }) expect(eligible.enabled).toBe(true) expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5-pro"))).enabled).toBe( false, @@ -215,9 +146,9 @@ describe("OpenAIPlugin", () => { false, ) expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6"))).enabled).toBe(false) - expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6-sol"))).enabled).toBe( - true, - ) + const gpt56 = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6-sol"))) + expect(gpt56.enabled).toBe(true) + expect(gpt56.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 }) expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(false) }), ) @@ -234,7 +165,9 @@ describe("OpenAIPlugin", () => { catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, Model.ID.make("gpt-5.5"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.5"), (model) => { + model.limit = { context: 1_050_000, input: 922_000, output: 128_000 } + }) catalog.model.update(item.id, Model.ID.make("gpt-4.1"), () => {}) }) yield* credentials.create({ @@ -243,29 +176,23 @@ describe("OpenAIPlugin", () => { }) yield* addPlugin() - expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))).enabled).toBe(true) + const request = yield* (yield* PluginHooks.Service).trigger("session", "request", { + sessionID: Session.ID.make("ses_test"), + agent: Agent.ID.make("build"), + model: Model.Ref.make({ providerID: Provider.ID.openai, id: Model.ID.make("gpt-5.5") }), + url: "https://api.openai.com/v1/responses", + method: "POST", + headers: {}, + body: "{}", + }) + + const model = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))) + expect(model.package).toBe("@opencode-ai/ai/providers/openai") + expect(model.enabled).toBe(true) + expect(model.limit).toEqual({ context: 1_050_000, input: 922_000, output: 128_000 }) + expect(request.headers).toEqual({}) expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(true) }), ) - it.effect("does not disable gpt-5-chat-latest for non-OpenAI providers", () => - Effect.gen(function* () { - const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => { - const item = Provider.Info.make({ - ...Provider.Info.empty(Provider.ID.make("custom-openai")), - package: Provider.aisdk("test-provider"), - }) - catalog.provider.update(item.id, (draft) => { - draft.package = item.package - }) - catalog.model.update(item.id, Model.ID.make("gpt-5-chat-latest"), () => {}) - }) - yield* addPlugin() - expect( - required(yield* catalog.model.get(Provider.ID.make("custom-openai"), Model.ID.make("gpt-5-chat-latest"))) - .enabled, - ).toBe(true) - }), - ) })