Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix(core): prevent Zen key on ChatGPT Codex requests
Console-provided OpenAI catalogs were left owned by the Zen integration after the OpenAI plugin rerouted them to the ChatGPT Codex endpoint. Rebind both credential ownership and source connection to the active ChatGPT OAuth connection.
  • Loading branch information
jhsu committed Sep 18, 2026
commit a2fa4a4a9e4cd06a76c2bb28b4de6927fe749647
29 changes: 28 additions & 1 deletion packages/core/src/plugin/provider/openai.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createServer } from "node:http"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Deferred, Effect } from "effect"
import { Deferred, Effect, Semaphore, Stream } from "effect"
import type { Scope } from "effect"
import { Credential } from "../../credential"
import { EventV2 } from "../../event"
import { InstallationVersion } from "../../installation/version"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
Expand Down Expand Up @@ -154,12 +155,32 @@ const headless = {
export const OpenAIPlugin = define({
id: "openai",
effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service
const loading = Semaphore.makeUnsafe(1)
let chatgpt = false

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 =
credential?.type === "oauth" &&
(credential.methodID === browserMethodID || credential.methodID === headlessMethodID)
})

yield* ctx.integration.transform((draft) => {
draft.method.update(browser)
draft.method.update(headless)
})
yield* load()
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
if (chatgpt) {
evt.provider.update(ProviderV2.ID.openai, (provider) => {
provider.integrationID = Integration.ID.make("openai")
})
}
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai") continue
Expand All @@ -185,6 +206,12 @@ export const OpenAIPlugin = define({
evt.language = evt.sdk.responses(evt.model.api.id)
}),
)
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")),
Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }),
)
}),
} satisfies PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>)

Expand Down
54 changes: 54 additions & 0 deletions packages/core/test/plugin/provider-openai.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test"
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 { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { LLM } from "@opencode-ai/llm"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"

Expand Down Expand Up @@ -76,6 +80,56 @@ describe("OpenAIPlugin", () => {
}),
)

it.effect("uses ChatGPT OAuth when Console also provides the OpenAI catalog", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const credentials = yield* Credential.Service
const integrations = yield* Integration.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openai, (provider) => {
provider.integrationID = Integration.ID.make("opencode")
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai",
url: "https://chatgpt.com/backend-api/codex",
}
})
catalog.model.update(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.6-luna"), () => {})
})
yield* credentials.create({
integrationID: Integration.ID.make("opencode"),
value: Credential.Key.make({ type: "key", key: "zen-key" }),
})
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,
}),
})
yield* addPlugin()

const provider = required(yield* catalog.provider.get(ProviderV2.ID.openai))
const integrationID = required(provider.integrationID)
expect(integrationID).toBe(Integration.ID.make("openai"))
const connection = required(yield* integrations.connection.active(integrationID))
const credential = required(yield* integrations.connection.resolve(connection))
const model = required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.6-luna")))
const resolved = yield* SessionRunnerModel.fromCatalogModel(model, 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.empty,
})
expect(headers.authorization).toBe("Bearer chatgpt-token")
}),
)

it.effect("ignores non-OpenAI SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
Expand Down
Loading