diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index d4bfc49a6f..4c1c9c478d 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -370,6 +370,13 @@ Vercel AI Gateway は、1 つのモデルを複数の基盤となる推論プロ 表示名には `modelDisplayNames` を使用します。優先順位は、運用者が設定した `modelDisplayNames`、プロバイダーカタログのメタデータ、通常の `provider/model` 表示の順です。キーはこのプロバイダー内の正確なネイティブモデル ID です。例えば `xai/grok-4.6` のキーは `grok-4.6` です。ラベルは表示専用で、正確なルーティング ID や上流モデル ID を変更しません。`config.json` の既存プロバイダー設定にこのフィールドだけを追加し、他のすべてのフィールドを残してください。`PUT /api/providers/:provider/model-display-names` に `{ "modelId": "grok-4.6", "displayName": "Grok 4.6" }` を送ると保存され、`displayName: null` を送るとその名前だけがリセットされます。 +ローカル Codex カタログでサポートされるプレフィックスなしのネイティブ GPT 行にも、 +`providers.openai.modelDisplayNames` で正確な表示名を指定できます。例えば `"gpt-6-astra": "GPT 6 Astra"` です。 +起動時の同期とローカルカタログの収束処理は、どちらもこれらの名前を再適用します。名前の設定を削除すると、行の現在の表示名が +適用済みの上書きとまだ一致する場合にのみ、元のネイティブ名が復元されます。その後に外部で変更された表示名は保持されます。 +モデル ID、メタデータ(機能を含む)、順序、ルーティングされたコンボのエイリアス、アカウント修飾付きの行は変更されません。 +このローカルカタログの上書きは、HTTP のモデル一覧や仮想 `*-pro` 行の表示名には適用されません。 + プレビュー GPT-5.6 フォールバック エントリは同じメカニズムを使用します。 OpenAI API キー プリセットは、ベース ID と Pro ID にコンテキスト `922000` と最大入力 `922000` をシードします。 OpenRouter は、コンテキスト `922000` を持つ `openai/gpt-5.6-sol`、`openai/gpt-5.6-terra`、および `openai/gpt-5.6-luna` をシードします。プール/ダイレクトは `922000` をアドバタイズします。同期されたカタログは、`xhigh` を区別しつつ、`max` をアドバタイズします。 ```json diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index c9c4de5ede..4efd783623 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -377,6 +377,13 @@ Vercel AI Gateway는 하나의 모델을 여러 기반 추론 공급자에 걸 표시 이름은 `modelDisplayNames`로 설정합니다. 우선순위는 운영자가 설정한 `modelDisplayNames`, 공급자 카탈로그 메타데이터, 일반 `provider/model` 표시 순서입니다. 키는 이 공급자 안의 정확한 네이티브 모델 id입니다. 예를 들어 `xai/grok-4.6`의 키는 `grok-4.6`입니다. 이름은 표시 전용이며 정확한 라우팅 id나 업스트림 모델 id를 바꾸지 않습니다. `config.json`의 기존 공급자 설정에 이 필드만 추가하고 다른 모든 필드는 유지하세요. `PUT /api/providers/:provider/model-display-names`에 `{ "modelId": "grok-4.6", "displayName": "Grok 4.6" }`를 보내 저장하고, `displayName: null`을 보내 해당 이름만 초기화합니다. +로컬 Codex 카탈로그에서 지원되는 접두사 없는 네이티브 GPT 항목에도 +`providers.openai.modelDisplayNames`로 정확한 표시 이름을 지정할 수 있습니다. 예를 들어 `"gpt-6-astra": "GPT 6 Astra"`를 사용합니다. +시작 시 동기화와 로컬 카탈로그 수렴은 모두 이 이름을 다시 적용합니다. 이름 설정을 삭제하면 항목의 현재 표시 이름이 +적용된 재정의와 여전히 일치할 때만 원래 네이티브 이름을 복원합니다. 이후 외부에서 변경된 표시 이름은 유지합니다. +모델 ID, 메타데이터(기능 포함), 정렬 순서, 라우팅된 콤보 별칭 및 계정 선택자가 붙은 항목은 바뀌지 않습니다. +이 로컬 카탈로그 재정의는 HTTP 모델 목록이나 가상 `*-pro` 항목의 이름을 바꾸지 않습니다. + 프리뷰 GPT-5.6 폴백 항목도 같은 메커니즘을 사용합니다. OpenAI API 키 프리셋은 base와 Pro id에 컨텍스트 `922000`, 최대 입력 `922000`을 채웁니다. OpenRouter는 `openai/gpt-5.6-sol`, `openai/gpt-5.6-terra`, `openai/gpt-5.6-luna`에 컨텍스트 `922000`을 채웁니다. Pool/Direct는 `922000`을 노출하고, 동기화된 카탈로그는 `xhigh`를 구분한 채 `max`를 노출합니다. ```json diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 85a9ba6903..7b83f4a6b2 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -230,6 +230,14 @@ all other provider settings. The example includes the surrounding required field } ``` +Supported bare native GPT rows in the local Codex catalog also accept exact labels in +`providers.openai.modelDisplayNames`, for example `"gpt-6-astra": "GPT 6 Astra"`. +Both startup synchronization and local catalog convergence reapply these labels. Removing a label +restores the original native name only when the row's display name still matches the applied +override. A newer external display name is preserved instead. Model IDs, metadata (including capabilities), ordering, +routed combo aliases, and account-qualified rows remain unchanged. This local catalog override does +not relabel the HTTP model listings or virtual `*-pro` rows. + The effective label order is operator `modelDisplayNames`, then provider catalog metadata, then the normal `provider/model` fallback. The routed selector remains `xai/grok-4.6`, while the upstream wire model remains `grok-4.6`. Labels are display only. They do not change authentication, adapter diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index b7339cd4c2..7f3b0d343a 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -371,6 +371,13 @@ Vercel AI Gateway 可以在多个底层推理提供者之间路由一个模型 请使用 `modelDisplayNames` 设置显示名称。优先顺序是操作者设置的 `modelDisplayNames`、提供者目录元数据,然后是普通的 `provider/model` 显示。键是此提供者内精确的原生模型 id,例如 `xai/grok-4.6` 的键是 `grok-4.6`。名称只改变显示,不会改变精确路由 id 或上游模型 id。请只把此字段加入 `config.json` 中现有的提供者设置,并保留所有其他字段。向 `PUT /api/providers/:provider/model-display-names` 发送 `{ "modelId": "grok-4.6", "displayName": "Grok 4.6" }` 可保存名称,发送 `displayName: null` 只重置该名称。 +本地 Codex 目录中受支持的不带前缀的原生 GPT 条目也可以通过 +`providers.openai.modelDisplayNames` 设置精确的显示名称, 例如 `"gpt-6-astra": "GPT 6 Astra"`。 +启动时同步和本地目录收敛都会重新应用这些名称。删除名称设置时, 只有条目的当前显示名称仍与已应用的覆盖值一致, +才会恢复原始原生名称。之后由外部更改的显示名称会被保留。 +模型 ID、能力、排序、路由组合别名和带账户限定的条目均保持不变。 +此本地目录覆盖不会重命名 HTTP 模型列表中的条目或虚拟 `*-pro` 条目。 + 预览版 GPT-5.6 回退条目使用相同机制。OpenAI API key 预设会为基础和 Pro id 设定 `922000` 上下文和 `922000` 最大输入;OpenRouter 会为 `openai/gpt-5.6-sol`、`openai/gpt-5.6-terra` 和 `openai/gpt-5.6-luna` 设定 `922000` 上下文。Pool/Direct 会声明 `922000`;同步后的目录会声明 `max`,同时保留 `xhigh` 的独立性。 ```json diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index 22d235dbcc..972b6d74c6 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -307,6 +307,11 @@ function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick; + if (recoverableNativeSlug(entry) === label.slug + && typeof label.original === "string" && entry.display_name === label.applied) { + entry.display_name = label.original; + } + } + return entry; +} + /** Append missing supported native rows from trusted catalog sources only. */ export function mergeCatalogModelsWithNativeRecovery( primaryCatalogModels: readonly RawEntry[], @@ -862,6 +882,8 @@ export interface ObservedCatalogMergeInput { readonly suppressedBareNativeSlugs?: ReadonlySet; readonly policy: ObservedCatalogMergePolicy; readonly openaiContextCap?: NativeContextLimitsInput; + /** Exact display-only labels for bare native OpenAI models. */ + readonly nativeDisplayNames?: Readonly>; } /** @@ -896,12 +918,14 @@ export function mergeCatalogEntriesFromObservedState({ suppressedBareNativeSlugs = new Set(), policy, openaiContextCap, + nativeDisplayNames, }: ObservedCatalogMergeInput): RawEntry[] { // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at // the observed-core boundary so callers can safely retain evidence objects or repeat the merge. - const detachedCatalogModels = catalogModels.map(entry => structuredClone(entry) as RawEntry); + const detachedCatalogModels = catalogModels + .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry)); const detachedBaselineCatalogModels = baselineCatalogModels - .map(entry => structuredClone(entry) as RawEntry); + .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry)); const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry); // Track this invocation's generated custom rows, not ownership markers read from disk. // Their builder already finalized exact native ladders and ordinary routed mock tiers. @@ -1256,6 +1280,17 @@ export function mergeCatalogEntriesFromObservedState({ ); applyFullModelPickerOrder(versionedEntries, modelPickerOrder); for (const entry of versionedEntries) { + // Templates and account clones must not inherit the native row's overlay marker. + delete entry.opencodex_native_display_name; + const slug = recoverableNativeSlug(entry); + if (slug !== null) { + const label = nativeDisplayNames && Object.hasOwn(nativeDisplayNames, slug) + ? nativeDisplayNames[slug]?.trim() : undefined; + if (label && label !== entry.display_name) { + entry.opencodex_native_display_name = { slug, original: entry.display_name, applied: label }; + entry.display_name = label; + } + } const kind = entry.opencodex_catalog_kind; if (trustedAccountBoundNativeCatalogSlug(entry) === undefined && kind !== CODEX_CUSTOM_MODEL_CATALOG_KIND @@ -1659,6 +1694,12 @@ export function finalizeAutoReviewModelOverride( return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels); } +/** + * Mescla o catálogo retido com os modelos visíveis e as configurações atuais, + * incluindo os nomes nativos. Tenta preservar o backup original e usa a permissão + * de escrita para publicar o resultado apenas se os bytes mudarem, retornando + * a contagem de entradas roteadas e por conta, o caminho e o estado da gravação. + */ function writeRetainedCatalogSync({ config, goModels, @@ -1880,6 +1921,7 @@ function writeRetainedCatalogSync({ accountBoundEntries, suppressedBareNativeSlugs, openaiContextCap, + nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames, policy: { ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs], diff --git a/src/codex/convergence.ts b/src/codex/convergence.ts index 8b30bb9eb2..2b8a8512c6 100644 --- a/src/codex/convergence.ts +++ b/src/codex/convergence.ts @@ -226,6 +226,12 @@ function bindGatherPaths( }; } +/** + * Prepara um candidato de catálogo para convergência sem gravá-lo em disco. + * Clona a fonte e mescla as observações nativas, os modelos roteados e por conta, + * aplicando a configuração, inclusive nomes nativos, e os limites de raciocínio + * observados no runtime antes de retornar o catálogo resultante. + */ function prepareCatalog( config: Readonly, source: Extract, @@ -366,6 +372,7 @@ function prepareCatalog( accountBoundEntries, suppressedBareNativeSlugs, openaiContextCap, + nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames, policy: { ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs], diff --git a/tests/codex-integration/codex-catalog.test.ts b/tests/codex-integration/codex-catalog.test.ts index 5820edc6dd..49e5c655e0 100644 --- a/tests/codex-integration/codex-catalog.test.ts +++ b/tests/codex-integration/codex-catalog.test.ts @@ -3046,6 +3046,81 @@ function mergeObservedForTest( } describe("Codex catalog routed normalization", () => { + test("reapplies native display names after repeated catalog merges without changing model metadata", () => { + const input = { + catalogModels: [{ ...nativeTemplate(), slug: "gpt-5.6-sol" }], + routedEntries: [], + }; + const original = mergeObservedForTest(input); + const labels = { "gpt-5.6-sol": "GPT 5.6 Sol" }; + const renamed = mergeObservedForTest({ ...input, nativeDisplayNames: labels }); + const row = renamed.find(entry => entry.slug === "gpt-5.6-sol")!; + expect(row.display_name).toBe("GPT 5.6 Sol"); + expect({ ...row, display_name: undefined, opencodex_native_display_name: undefined }).toEqual({ + ...original.find(entry => entry.slug === "gpt-5.6-sol"), display_name: undefined, + }); + const regenerated = mergeObservedForTest({ + ...input, catalogModels: renamed, nativeDisplayNames: labels, + }); + expect(regenerated.find(entry => entry.slug === "gpt-5.6-sol")?.display_name).toBe("GPT 5.6 Sol"); + const changed = mergeObservedForTest({ + ...input, catalogModels: regenerated, + nativeDisplayNames: { "gpt-5.6-sol": " Sol 5.6 " }, + }); + expect(changed.find(entry => entry.slug === "gpt-5.6-sol")?.display_name).toBe("Sol 5.6"); + expect(JSON.stringify(regenerated)).toBe(JSON.stringify(renamed)); + for (const nativeDisplayNames of [undefined, {}, { "gpt-5.6-sol": " " }]) { + const restored = mergeObservedForTest({ ...input, catalogModels: changed, nativeDisplayNames }); + expect(restored).toEqual(original); + } + }); + + test("native display names preserve external label changes when clearing the overlay", () => { + const renamed = mergeObservedForTest({ + catalogModels: [{ ...nativeTemplate(), slug: "gpt-5.6-sol" }], routedEntries: [], + nativeDisplayNames: { "gpt-5.6-sol": "Custom Sol" }, + }); + renamed.find(entry => entry.slug === "gpt-5.6-sol")!.display_name = "Updated upstream Sol"; + const restored = mergeObservedForTest({ catalogModels: renamed, routedEntries: [] }); + const row = restored.find(entry => entry.slug === "gpt-5.6-sol")!; + expect(row.display_name).toBe("Updated upstream Sol"); + expect(row.opencodex_native_display_name).toBeUndefined(); + }); + + test("native display names preserve pinned metadata upgrades and restore pinned names", () => { + for (const slug of ["gpt-5.6-sol", "gpt-6-astra"]) { + const input = { catalogModels: [{ ...nativeTemplate(), slug, display_name: slug }], routedEntries: [] }; + const original = mergeObservedForTest(input); + const renamed = mergeObservedForTest({ ...input, nativeDisplayNames: { [slug]: "Custom name" } }); + expect(renamed.find(entry => entry.slug === slug)?.display_name).toBe("Custom name"); + expect(mergeObservedForTest({ catalogModels: renamed, routedEntries: [] })).toEqual(original); + } + }); + + test("native display names do not leak overlay markers through catalog templates", () => { + const template = { + ...nativeTemplate(), + opencodex_native_display_name: { slug: "gpt-5.6-sol", original: "Sol", applied: "Custom" }, + }; + const entries = buildCatalogEntries(template, ["gpt-5.5"], [{ provider: "local", id: "qwen3-coder" }]); + expect(entries.length).toBeGreaterThanOrEqual(2); + for (const entry of entries) expect(entry.opencodex_native_display_name).toBeUndefined(); + expect(template.opencodex_native_display_name).toBeDefined(); + }); + + test("native display names do not relabel a routed combo occupying a native slug", () => { + const routed = { + ...nativeTemplate(), slug: "gpt-5.6-sol", display_name: "My combo", + owned_by: "combo", description: "Routed via opencodex → combo (combo).", + opencodex_catalog_kind: CODEX_NATIVE_ALIAS_CATALOG_KIND, + }; + const rows = mergeObservedForTest({ + catalogModels: [], routedEntries: [routed], + nativeDisplayNames: { "gpt-5.6-sol": "GPT 5.6 Sol" }, + }); + expect(rows.find(entry => entry.slug === "gpt-5.6-sol")?.display_name).toBe("My combo"); + }); + test("pending re-registration cannot recover ON rows from a degraded old catalog", () => { const old = { ...nativeTemplate(), slug: "vendor/model-0", owned_by: "vendor", opencodex_catalog_kind: CODEX_PROVIDER_MODEL_CATALOG_KIND }; const input = {