Просмотр исходного кода

refactor(core): keep Codex routing in plugin

Aiden Cline 2 недель назад
Родитель
Сommit
aef37e4514

+ 0 - 25
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<VariantUnavailableError>()(
@@ -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<string, unknown>) => {
   return rest
 }
 
-const codexModel = (
-  model: Info,
-  credential: Credential.Value | undefined,
-  key: ReturnType<typeof Auth.value> | 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,

+ 7 - 2
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.

+ 3 - 34
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(

+ 29 - 11
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",
+      )
     }),
   )