From b9ecf83ded81decc5f7e0465a28cedf6d78de128 Mon Sep 17 00:00:00 2001 From: mberman84 <748450+mberman84@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:04:10 -0700 Subject: [PATCH] fix(server): managed ChatGPT models newer than Codex CLI show reasoning effort Models listed by the ChatGPT catalog but absent from the managed Codex CLI's native catalog (e.g. gpt-6.1-sol) got null capabilities, hiding the reasoning-effort selector. Map the catalog's advertised reasoning levels when native capabilities are missing. Fixes #26707 Co-Authored-By: Claude Opus 5.5 --- .../src/provider/CodexChatGptModels.test.ts | 35 +++++++++++++++++-- .../server/src/provider/CodexChatGptModels.ts | 22 +++++++++++- apps/server/src/provider/CodexProvider.ts | 6 +++- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/CodexChatGptModels.test.ts b/apps/server/src/provider/CodexChatGptModels.test.ts index 9533a3c64d00a..dd533c32928bf 100644 --- a/apps/server/src/provider/CodexChatGptModels.test.ts +++ b/apps/server/src/provider/CodexChatGptModels.test.ts @@ -19,9 +19,24 @@ it.effect( models: request.headers.authorization === "Bearer account-a" ? [ - { slug: "second", display_name: "Second from OpenAI", visibility: "list" }, + { + slug: "second", + display_name: "Second from OpenAI", + visibility: "list", + default_reasoning_level: "low", + supported_reasoning_levels: [ + { effort: "low", description: "Fast" }, + { effort: "ultra", description: "Deepest" }, + { effort: "", description: "Malformed" }, + ], + }, { slug: "hidden", display_name: "Hidden", visibility: "hidden" }, - { slug: "first", display_name: "First from OpenAI", visibility: "list" }, + { + slug: "first", + display_name: "First from OpenAI", + visibility: "list", + supported_reasoning_levels: [{ effort: "low", description: "Fast" }], + }, ] : [{ slug: "account-b-only", display_name: "B", visibility: "list" }], }), @@ -49,7 +64,20 @@ it.effect( ], ); assert.deepEqual(a[1]!.capabilities, native[0]!.capabilities); - assert.isNull(a[0]!.capabilities); + assert.deepEqual(a[0]!.capabilities, { + optionDescriptors: [ + { + id: "reasoningEffort", + label: "Reasoning", + type: "select", + options: [ + { id: "low", label: "Low", isDefault: true }, + { id: "ultra", label: "Ultra" }, + ], + currentValue: "low", + }, + ], + }); const b = yield* chatGptModels("account-b", native).pipe( Effect.provideService(HttpClient.HttpClient, http), ); @@ -57,6 +85,7 @@ it.effect( b.map((model) => model.slug), ["account-b-only"], ); + assert.isNull(b[0]!.capabilities); assert.deepEqual(requests, ["Bearer account-a", "Bearer account-b"]); }), ); diff --git a/apps/server/src/provider/CodexChatGptModels.ts b/apps/server/src/provider/CodexChatGptModels.ts index f3490b6cf737e..20148c04b56fb 100644 --- a/apps/server/src/provider/CodexChatGptModels.ts +++ b/apps/server/src/provider/CodexChatGptModels.ts @@ -2,6 +2,7 @@ import type { ServerProviderModel } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import { HttpClient, HttpClientRequest } from "effect/http"; +import { mapCodexModelCapabilities } from "./CodexProvider.ts"; export class ChatGptCatalogError extends Schema.TaggedError()( "ChatGptCatalogError", @@ -14,10 +15,28 @@ const Catalog = Schema.Struct({ slug: Schema.NonEmptyString, display_name: Schema.NonEmptyString, visibility: Schema.String, + default_reasoning_level: Schema.optionalKey(Schema.String), + supported_reasoning_levels: Schema.optionalKey( + Schema.Array(Schema.Struct({ effort: Schema.String, description: Schema.String })), + ), }), ), }); +// Models newer than the managed Codex CLI are missing from its native catalog, +// so fall back to the reasoning levels OpenAI advertises for them. +function catalogCapabilities(model: (typeof Catalog.Type)["models"][number]) { + const supportedReasoningEfforts = (model.supported_reasoning_levels ?? []) + .filter(({ effort }) => effort.length > 0) + .map(({ effort, description }) => ({ reasoningEffort: effort, description })); + if (supportedReasoningEfforts.length === 0) return null; + return mapCodexModelCapabilities({ + model: model.slug, + defaultReasoningEffort: model.default_reasoning_level ?? "", + supportedReasoningEfforts, + }); +} + export const chatGptModels = Effect.fn("chatGptModels")(function* ( accessToken: string, nativeModels: ReadonlyArray, @@ -37,7 +56,8 @@ export const chatGptModels = Effect.fn("chatGptModels")(function* ( ({ ...nativeModels.find((native) => native.slug === model.slug), capabilities: - nativeModels.find((native) => native.slug === model.slug)?.capabilities ?? null, + nativeModels.find((native) => native.slug === model.slug)?.capabilities ?? + catalogCapabilities(model), slug: model.slug, name: model.display_name, isCustom: false, diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index 2b2d5501881b4..e84f0c2fd6269 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -166,7 +166,11 @@ export function codexDefaultReasoningEffortOverride(model: string): "medium" | u } export function mapCodexModelCapabilities( - model: CodexSchema.V2ModelListResponse__Model, + model: Pick< + CodexSchema.V2ModelListResponse__Model, + "model" | "defaultReasoningEffort" | "supportedReasoningEfforts" + > & + Partial, ): ModelCapabilities { const reasoningOptions = model.supportedReasoningEfforts.map(({ reasoningEffort }) => reasoningEffort ===