From aef37e4514ec987bb4e285727427edbd6ae0a084 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 28 Jul 2026 04:38:46 +0000 Subject: [PATCH] refactor(core): keep Codex routing in plugin --- packages/core/src/model-resolver.ts | 25 ------------ packages/core/src/plugin/provider/openai.ts | 9 ++++- packages/core/test/model-resolver.test.ts | 37 ++--------------- .../core/test/plugin/provider-openai.test.ts | 40 ++++++++++++++----- 4 files changed, 39 insertions(+), 72 deletions(-) diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index 4b69d1c500..604f0ac912 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -17,7 +17,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()( @@ -152,12 +151,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) }) @@ -223,10 +217,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 } @@ -248,21 +238,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({ - 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.ts b/packages/core/src/plugin/provider/openai.ts index 0dc25c64e2..d2a15a6a12 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -164,14 +164,14 @@ 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" && OpenAICodex.isChatGPT(credential) ? credential : undefined }) yield* ctx.integration.transform((draft) => { @@ -194,6 +194,11 @@ export const OpenAIPlugin = define({ const item = evt.provider.get(Provider.ID.openai) if (!item) return item.provider.settings = Provider.mergeOverlay(item.provider.settings, { baseURL: OpenAICodex.baseURL }) + const account = OpenAICodex.accountID(chatgpt) + item.provider.headers = Provider.mergeHeaders( + item.provider.headers, + account === undefined ? undefined : { "chatgpt-account-id": account }, + ) 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. diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 47428dbd00..44d35e688c 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -307,7 +307,7 @@ describe("ModelResolver", () => { }), ) - it.effect("keeps an explicit endpoint for ChatGPT OAuth credentials", () => + it.effect("does not reinterpret an explicit endpoint based on the OAuth method", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( model(Provider.aisdk("@ai-sdk/openai"), { @@ -338,7 +338,7 @@ describe("ModelResolver", () => { endpoint: { baseURL: "https://openai.example/v1" }, }) expect(headers.authorization).toBe("Bearer chatgpt-token") - expect(headers["chatgpt-account-id"]).toBe("acct_123") + expect(headers["chatgpt-account-id"]).toBeUndefined() }), ) @@ -367,7 +367,7 @@ describe("ModelResolver", () => { expect(resolved.route.endpoint.baseURL).toBe("https://openai.example/v1") expect(headers.authorization).toBe("Bearer chatgpt-token") - expect(headers["chatgpt-account-id"]).toBe("acct_123") + expect(headers["chatgpt-account-id"]).toBeUndefined() }), ) @@ -407,37 +407,6 @@ describe("ModelResolver", () => { }), ) - it.effect("keeps an explicit endpoint for ChatGPT OAuth credentials without an account id", () => - 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://openai.example/v1/responses", - body: "{}", - headers: Headers.empty, - }) - - expect(resolved.route.endpoint.baseURL).toBe("https://openai.example/v1") - 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 d8cbf0925a..60917c7d89 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -1,12 +1,15 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { Money } from "@opencode-ai/schema/money" import { describe, expect } from "bun:test" +import { LLM } from "@opencode-ai/ai" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" +import { Headers } from "effect/unstable/http" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" import { Model } from "@opencode-ai/core/model" +import { ModelResolver } from "@opencode-ai/core/model-resolver" import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai" @@ -192,16 +195,17 @@ describe("OpenAIPlugin", () => { catalog.model.update(item.id, Model.ID.make("gpt-5.6-sol"), () => {}) catalog.model.update(item.id, Model.ID.make("gpt-4.1"), () => {}) }) + const credential = 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" }, + }) yield* credentials.create({ integrationID: Integration.ID.make("openai"), - value: 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" }, - }), + value: credential, }) yield* addPlugin() @@ -209,6 +213,18 @@ describe("OpenAIPlugin", () => { expect(eligible.cost).toEqual([]) expect(eligible.enabled).toBe(true) expect(eligible.settings?.baseURL).toBe("https://chatgpt.com/backend-api/codex") + expect(eligible.headers?.["chatgpt-account-id"]).toBe("acct_123") + const resolved = yield* ModelResolver.fromCatalogModel(eligible, credential) + 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.fromInput(resolved.route.defaults.headers), + }) + 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") expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5-pro"))).enabled).toBe( false, ) @@ -226,9 +242,11 @@ describe("OpenAIPlugin", () => { provider.settings = Provider.mergeOverlay(provider.settings, { baseURL: "https://proxy.example/v1" }) }) }) - expect( - required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))).settings?.baseURL, - ).toBe("https://proxy.example/v1") + const configured = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))) + expect(configured.settings?.baseURL).toBe("https://proxy.example/v1") + expect((yield* ModelResolver.fromCatalogModel(configured, credential)).route.endpoint.baseURL).toBe( + "https://proxy.example/v1", + ) }), )