Browse Source

fix(core): support OpenAI pro mode (#36896)

Aiden Cline 1 month ago
parent
commit
f4f761246a

+ 23 - 6
packages/core/src/aisdk.ts

@@ -103,11 +103,12 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
 }
 
 function prepareOptions(model: ModelV2.Info, pkg: string) {
+  const projected = mapBodyToProviderOptions(model)
   const options: Record<string, any> = {
     name: model.providerID,
     ...(model.settings ?? {}),
     headers: model.headers,
-    body: model.body,
+    body: projected.body,
   }
 
   const customFetch = options.fetch
@@ -300,8 +301,9 @@ export const locationLayer = Layer.effect(
 export const defaultLayer = locationLayer
 
 function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
-  const settings = requestSettings(info.settings)
-  const optionKey = providerOptionKey(ProviderV2.packageName(info.package), info.providerID)
+  const packageName = ProviderV2.packageName(info.package)
+  const projected = mapBodyToProviderOptions(info)
+  const optionKey = providerOptionKey(packageName, info.providerID)
   const route: AnyRoute = {
     id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
     provider: ProviderID.make(info.providerID),
@@ -317,11 +319,14 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
     defaults: {
       headers: info.headers,
       http:
-        info.body === undefined && info.headers === undefined
+        projected.body === undefined && info.headers === undefined
           ? undefined
-          : { body: info.body === undefined ? undefined : { ...info.body }, headers: info.headers },
+          : {
+              body: projected.body === undefined ? undefined : { ...projected.body },
+              headers: info.headers,
+            },
       limits: { context: info.limit.context, output: info.limit.output },
-      providerOptions: settings === undefined ? undefined : { [optionKey]: settings },
+      providerOptions: projected.settings === undefined ? undefined : { [optionKey]: projected.settings },
     },
     body: {
       schema: Schema.Unknown,
@@ -356,6 +361,18 @@ function requestSettings(settings: Readonly<Record<string, unknown>> | undefined
   return Object.keys(result).length === 0 ? undefined : result
 }
 
+function mapBodyToProviderOptions(model: ModelV2.Info) {
+  const settings = requestSettings(model.settings)
+  if (!Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning))
+    return { settings, body: model.body }
+  const body = { ...model.body }
+  delete body.reasoning
+  return {
+    settings: ProviderV2.mergeOverlay(settings, { reasoningMode: "pro" }),
+    body: Object.keys(body).length === 0 ? undefined : body,
+  }
+}
+
 function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
   return {
     prompt: prompt(request),

+ 1 - 1
packages/core/src/plugin/provider/openai-codex.ts

@@ -31,7 +31,7 @@ export const accountID = (credential: CredentialLike | 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"])
+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) => {

+ 4 - 0
packages/core/src/plugin/provider/openai.ts

@@ -194,6 +194,10 @@ export const OpenAIPlugin = define({
         // ChatGPT-plan tokens only authorize codex-eligible models, and the
         // subscription covers usage, so hide the rest and zero the cost.
         evt.model.update(item.provider.id, model.id, (draft) => {
+          if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(draft.body?.reasoning)) {
+            draft.enabled = false
+            return
+          }
           if (!OpenAICodex.eligible(draft.modelID ?? draft.id)) {
             draft.enabled = false
             return

+ 22 - 0
packages/core/test/aisdk.test.ts

@@ -68,6 +68,28 @@ it.effect("projects request settings, headers, and body overlays", () =>
   }),
 )
 
+it.effect("maps pro reasoning bodies to AI SDK provider options", () =>
+  Effect.gen(function* () {
+    const aisdk = yield* AISDK.Service
+    let body: unknown
+    yield* aisdk.hook.sdk((event) => {
+      body = event.options.body
+      event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
+    })
+
+    const resolved = yield* aisdk.model({
+      ...model("@ai-sdk/openai"),
+      body: { reasoning: { mode: "pro" } },
+    })
+    const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
+      LLM.request({ model: resolved, prompt: "Hello" }),
+    )
+
+    expect(body).toBeUndefined()
+    expect(prepared.body.providerOptions).toEqual({ openai: { reasoningMode: "pro" } })
+  }),
+)
+
 it.effect("projects replay metadata onto AI SDK prompt parts", () =>
   Effect.gen(function* () {
     const aisdk = yield* AISDK.Service

+ 13 - 0
packages/core/test/plugin/provider-openai.test.ts

@@ -198,6 +198,12 @@ describe("OpenAIPlugin", () => {
           ]
         })
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5-pro"), () => {})
+        catalog.model.update(item.id, ModelV2.ID.make("gpt-5.4-pro"), (model) => {
+          model.modelID = ModelV2.ID.make("gpt-5.4")
+          model.body = { reasoning: { mode: "pro" } }
+        })
+        catalog.model.update(item.id, ModelV2.ID.make("gpt-5.6"), () => {})
+        catalog.model.update(item.id, ModelV2.ID.make("gpt-5.6-sol"), () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {})
       })
       yield* credentials.create({
@@ -223,6 +229,13 @@ describe("OpenAIPlugin", () => {
       expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.5-pro"))).enabled).toBe(
         false,
       )
+      expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.4-pro"))).enabled).toBe(
+        false,
+      )
+      expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.6"))).enabled).toBe(false)
+      expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.6-sol"))).enabled).toBe(
+        true,
+      )
       expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-4.1"))).enabled).toBe(false)
     }),
   )