From 0b89a77b5ad9403fe7f6b58ac352f86774a5520b Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 13 Sep 2026 19:06:33 +0900 Subject: [PATCH 1/3] fix(catalog): bound custom native-id effort lists on gateways [skip ci] Intersect explicit custom reasoning lists with pinned native metadata whenever the model id itself is capability-backed, including YYLJ/gpt-6-astra. Desktop validates the model id, so none/minimal must not remain on those catalog rows. Full native identity still requires the canonical openai Codex-forward destination. Stored configuration and request-time clamps are unchanged. Refs #3775. Original report by @leonclab. #3804 already bounded the canonical forward case; this is the remaining catalog projection. --- .../content/docs/guides/codex-app-models.md | 12 +++-- .../docs/reference/configuration/providers.md | 11 ++-- src/codex/catalog/provider-fetch.ts | 18 +++++-- src/codex/catalog/sync.ts | 15 ++++-- structure/catalog.md | 21 ++++---- .../claude-models-discovery.test.ts | 6 +-- tests/codex-integration/codex-catalog.test.ts | 51 ++++++++++++++----- 7 files changed, 91 insertions(+), 43 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-app-models.md b/docs-site/src/content/docs/guides/codex-app-models.md index 8219dd2281..209469f41d 100644 --- a/docs-site/src/content/docs/guides/codex-app-models.md +++ b/docs-site/src/content/docs/guides/codex-app-models.md @@ -73,13 +73,15 @@ the resulting list; otherwise the native default is used when present, then the choice. Stored custom configuration is unchanged, and repeated syncs do not add `max` back to a narrow custom list. -This requires the exact provider, destination, and capability-backed model identity. An arbitrary -gateway such as `YYLJ/gpt-6-astra` does not inherit native capabilities from its name. Its explicit -custom ladder continues to override discovered provider metadata under the normal routed rules. +The same catalog bound applies when the custom model id has pinned native capability metadata, +including an arbitrary gateway such as `YYLJ/gpt-6-astra`. Desktop validates the model id, so +`none` and `minimal` are stripped from that catalog row. Full native identity still requires the +exact provider, destination, and capability-backed model identity; a gateway does not inherit +Responses Lite, multi-agent, or native windows from its name. Codex's native Astra `ultra` choice is retained: it is a client delegation mode converted to a supported wire effort, distinct from the [API model's effort list](https://developers.openai.com/api/docs/models/gpt-6-astra). -Catalog normalization does not rewrite existing thread settings or establish support for a -particular installed Desktop version. +Catalog normalization does not rewrite existing thread settings. Request-time native effort +clamps remain canonical-forward only. When the `codexAccountNamespaces` map is empty, account-qualified picker rows are off. If `codexAccountPickerEnabled` is omitted with a non-empty map, they are treated as enabled for diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index a80eab897f..752c13b395 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -231,11 +231,12 @@ provider request is sent. Changing away and back also ends that continuation. St use the new selection. Selection changes before the first provider send retain normal reselection. Custom-model `reasoningEfforts` normally override discovered provider metadata. The bounded -exception is an explicit Astra or Daybreak custom row on the canonical `openai` Codex-forward -destination: its advertised list is intersected with that model's pinned native capabilities. -An explicit empty list remains empty with no default; a nonempty incompatible list falls back -to the native default as a single choice. Defaults must belong to the final list. This changes -the catalog projection, not stored configuration or arbitrary gateway models sharing a GPT name. +exception is an explicit custom row whose model id has pinned native Codex capabilities, +including Astra or Daybreak on an arbitrary gateway: its advertised list is intersected with +that model's pinned native capabilities. Full native identity still requires the canonical +`openai` Codex-forward destination. An explicit empty list remains empty with no default; a +nonempty incompatible list falls back to the native default as a single choice. Defaults must +belong to the final list. This changes the catalog projection, not stored configuration. See [custom native catalog examples](/guides/codex-app-models/). ### Operator-pinned reasoning effort diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index f4560a03b8..f76e0b98fb 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -2321,7 +2321,7 @@ async function gatherRoutedModelsWithAuth( return models; } -/** Bound a proven Codex-forward custom row without changing its stored configuration. */ +/** Bound a custom row whose model id has pinned native Codex metadata, without changing stored configuration. */ function boundCustomNativeReasoning( model: CatalogModel, allowed: readonly string[], @@ -2625,8 +2625,8 @@ async function gatherRoutedModelsUncached( : {}), // Explicit custom-row ladder wins over the inherited provider row below: the merge only // gap-fills, so a stored `[]` (explicit "no reasoning") or a declared ladder is kept - // instead of being replaced by that row's metadata. Only proven native aliases are - // bounded against their own capability source after the merge. + // instead of being replaced by that row's metadata. Capability-backed native model ids + // are bounded against their own pinned ladder after the merge, including gateways. ...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}), ...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}), ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}), @@ -2679,8 +2679,16 @@ async function gatherRoutedModelsUncached( ...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}), ...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}), } : base; - const reasoningBounded = codexForwardNativeCapabilityAlias - ? boundCustomNativeReasoning(merged, nativeReasoningEfforts(cm.modelId), nativeAliasDefaultEffort) + // Catalog-advertised efforts are bounded whenever the model id is a pinned native + // slug. Desktop validates that id, so a gateway such as YYLJ/gpt-6-astra still cannot + // advertise none/minimal. Full native identity stays behind the alias predicate. + const nativeEffortSource = hasNativeOpenAiCapabilityMetadata(cm.modelId); + const reasoningBounded = nativeEffortSource + ? boundCustomNativeReasoning( + merged, + nativeReasoningEfforts(cm.modelId), + nativeAliasDefaultEffort ?? nativeDefaultReasoningEffort(cm.modelId), + ) : merged; // Vision-sidecar coverage only: when the enriched provider's shared predicate matches // noVisionModels or text-without-image modelInputModalities, advertise image input so the diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index edf41e74c9..d3380020b0 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -49,7 +49,7 @@ import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "../account-l import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing"; import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing"; -import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, RETIRED_NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata"; +import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, hasNativeOpenAiCapabilityMetadata, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, RETIRED_NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata"; import { bundledCatalogCacheState, loadBundledCodexCatalog, @@ -316,6 +316,13 @@ function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick