From 2bdb897531ac1f4d14a9a0e25f973abf6c17eaaf Mon Sep 17 00:00:00 2001 From: Hand_Song <269829845+songwunil@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:38:06 +0900 Subject: [PATCH 1/4] fix(catalog): prioritize model names in picker labels --- .../content/docs/guides/codex-integration.md | 4 ++++ src/codex/catalog/effort.ts | 15 ++++++++++----- src/codex/catalog/parsing.ts | 3 ++- tests/codex-catalog.test.ts | 19 +++++++++++++------ tests/slug-codec.test.ts | 4 +++- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index bef498ce4..27f0cdd05 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -185,6 +185,10 @@ start and on `ocx sync`, opencodex: 5. **Re-ranks** so featured models sort first (see below), then writes the merged catalog back. Routed catalog entries also get their GPT-5 identity rewritten to the real upstream model name. +Their default picker label is the final segment of the native model id, so provider namespaces do +not hide the model name in narrow pickers. The full `provider/model` route remains in the catalog +slug and description. Configure a custom display name when the route itself matters visually (for +example, `Claude Opus 5 (TeamClaude)`); custom names continue to take precedence. Reasoning controls come from provider/model metadata across Codex's `low | medium | high | xhigh | max | ultra` ladder; unsupported values are mapped or clamped before the upstream request. diff --git a/src/codex/catalog/effort.ts b/src/codex/catalog/effort.ts index 13fd33bba..f007a4831 100644 --- a/src/codex/catalog/effort.ts +++ b/src/codex/catalog/effort.ts @@ -115,13 +115,18 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel) // bare combo alias from a genuine native model row. if (model.provider === COMBO_NAMESPACE) entry.owned_by = model.owned_by ?? COMBO_NAMESPACE; // displayName is DISPLAY-ONLY: it relabels the picker row but never touches the routing - // slug, alias, or provider. deriveEntry already stamped the slug as display_name; a - // configured displayName overrides just the label. The `/` separator is rejected at every - // input boundary (CLI `ocx models add`, management API), so the catalog trusts its source. - // Combos carry no displayName, and natives never reach here (no CatalogModel), so genuine - // upstream marketing names and combo alias labels are preserved untouched. + // slug, alias, or provider. A configured displayName wins. Otherwise physical routed rows + // use the final segment of their native id, keeping provider/vendor namespaces out of the + // narrow picker label while the full route remains in slug + description. Aliased combos + // keep their public alias. Natives never reach here (no CatalogModel), so genuine upstream + // marketing names are preserved untouched. const displayName = typeof model.displayName === "string" ? model.displayName.trim() : ""; if (displayName) entry.display_name = displayName; + else if (!model.alias) { + const nativeId = model.id.trim(); + const finalSegment = nativeId.slice(nativeId.lastIndexOf("/") + 1).trim(); + if (finalSegment) entry.display_name = finalSegment; + } if (typeof model.contextWindow === "number" && model.contextWindow > 0) { entry.context_window = model.contextWindow; entry.max_context_window = model.contextWindow; diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 432ace821..1fbd80709 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -92,7 +92,8 @@ export interface CatalogModel { /** * Display-only Codex catalog `display_name` override. Relabels the picker row ONLY — it never * affects the routing slug, alias-collision order, native marketing-name precedence, or provider - * behavior. When unset, the entry falls back to its Codex-facing slug (the historical behavior). + * behavior. When unset, routed entries use the final segment of the native model id so the + * provider namespace does not crowd the picker; aliased combo rows keep their public alias. * Native upstream entries (e.g. gpt-5.6-sol → "GPT-5.6-Sol") come from the pinned snapshot path * which carries no CatalogModel, so a configured displayName can never override a native name. */ diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index 84f85cb37..f8364aaa2 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -736,22 +736,29 @@ describe("configured CatalogModel displayName -> catalog display_name", () => { expect(catalogModelSlug(model)).toBe("deepseek/deepseek-v4"); }); - test("absent displayName leaves display_name as the slug (unchanged behavior)", () => { + test("absent displayName uses the native model id without the provider namespace", () => { const entries = buildCatalogEntries(nativeTemplate(), [], [ { provider: "anthropic", id: "claude-sonnet-4-6", owned_by: "anthropic" }, ]); const row = entries.find(e => e.slug === "anthropic/claude-sonnet-4-6"); - expect(row?.display_name).toBe("anthropic/claude-sonnet-4-6"); + expect(row?.display_name).toBe("claude-sonnet-4-6"); expect(row?.slug).toBe("anthropic/claude-sonnet-4-6"); }); - test("empty/whitespace displayName is ignored and falls back to the slug", () => { + test("namespaced native ids use only their final segment as the default label", () => { const entries = buildCatalogEntries(nativeTemplate(), [], [ - { provider: "deepseek", id: "deepseek-v4", displayName: " ", owned_by: "deepseek" }, + { provider: "openrouter", id: "google/gemini-3.6-flash", displayName: " ", owned_by: "openrouter" }, ]); - const row = entries.find(e => e.slug === "deepseek/deepseek-v4"); - expect(row?.display_name).toBe("deepseek/deepseek-v4"); + const row = entries.find(e => e.slug === "openrouter/google-gemini-3.6-flash"); + expect(row?.display_name).toBe("gemini-3.6-flash"); + expect(row?.slug).toBe("openrouter/google-gemini-3.6-flash"); + }); + + test("an aliased combo keeps its public alias when no displayName is configured", () => { + const withAlias = { provider: "combo", id: "x", alias: "fast-chat", owned_by: "combo" }; + const entries = buildCatalogEntries(nativeTemplate(), [], [withAlias], undefined, false, "default", new Set(["fast-chat"])); + expect(entries.find(e => e.slug === "fast-chat")?.display_name).toBe("fast-chat"); }); test("displayName never affects the routing slug, alias, or provider", () => { diff --git a/tests/slug-codec.test.ts b/tests/slug-codec.test.ts index 6360ecd42..9a7d8ba01 100644 --- a/tests/slug-codec.test.ts +++ b/tests/slug-codec.test.ts @@ -158,7 +158,9 @@ describe("catalog emission (Codex-facing)", () => { const routed = entries.find(e => typeof e.slug === "string" && e.slug.startsWith("zenmux/")); expect(routed?.slug).toBe("zenmux/moonshotai-kimi-k3-free"); expect((routed?.slug as string).split("/")).toHaveLength(2); - expect(routed?.display_name).toBe("zenmux/moonshotai-kimi-k3-free"); + // The picker label is display-only and keeps the model name visible; routing still uses the + // exactly-one-slash Codex slug asserted above. + expect(routed?.display_name).toBe("kimi-k3-free"); // Identity text uses the NATIVE model name, not the encoded alias. expect(String(routed?.base_instructions)).toContain("moonshotai/kimi-k3-free"); }); From 5f9bb33d133d9b5674a43b3f7a30931fcfd01331 Mon Sep 17 00:00:00 2001 From: Hand_Song <269829845+songwunil@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:51:01 +0900 Subject: [PATCH 2/4] docs(codex): localize picker label behavior --- docs-site/src/content/docs/ja/guides/codex-integration.md | 6 +++++- docs-site/src/content/docs/ko/guides/codex-integration.md | 6 +++++- docs-site/src/content/docs/ru/guides/codex-integration.md | 6 +++++- .../src/content/docs/zh-cn/guides/codex-integration.md | 5 ++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index 4530561a6..8934c4656 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -147,7 +147,11 @@ Codex はディスクのカタログ(デフォルト `$CODEX_HOME/opencodex-cata 4. `config.disabledModels` と各プロバイダーの空でない `selectedModels` 許可リストを**適用**します。 5. フィーチャー済みモデルが先に並ぶよう**再整列**した後(下記参照)、マージされたカタログを書き戻します。 -ルーティングされたカタログ項目の GPT-5 アイデンティティ文言も実際の上流モデル名に合わせます。推論選択肢は +ルーティングされたカタログ項目の GPT-5 アイデンティティ文言も実際の上流モデル名に合わせます。 +デフォルトのピッカーラベルにはネイティブモデル ID の最後のセグメントが使われるため、幅の狭いピッカーでも +プロバイダーの名前空間がモデル名を隠しません。完全な `provider/model` 経路はカタログのスラッグと説明に残ります。 +経路自体を見分ける必要がある場合(例: `Claude Opus 5 (TeamClaude)`)はカスタム表示名を設定してください。 +カスタム表示名は引き続きデフォルトラベルより優先されます。推論選択肢は プロバイダーとモデルメタデータに応じて Codex の `low | medium | high | xhigh | max | ultra` 段階を使い、 上流がサポートしない値はリクエスト送信前にマッピングまたはサポート範囲に下げます。 diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index 3dd928e49..f4cabe570 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -145,7 +145,11 @@ Codex는 디스크의 카탈로그(기본값 `$CODEX_HOME/opencodex-catalog.json 4. `config.disabledModels`와 각 프로바이더의 비어 있지 않은 `selectedModels` 허용 목록을 **적용**합니다. 5. featured 모델이 먼저 정렬되도록 **재정렬**한 뒤(아래 참고), 병합된 카탈로그를 다시 작성합니다. -라우팅된 카탈로그 항목의 GPT-5 정체성 문구도 실제 업스트림 모델 이름에 맞게 바꿉니다. reasoning 선택지는 +라우팅된 카탈로그 항목의 GPT-5 정체성 문구도 실제 업스트림 모델 이름에 맞게 바꿉니다. 기본 선택기 레이블은 +네이티브 모델 ID의 마지막 부분을 사용하므로 폭이 좁은 선택기에서도 프로바이더 네임스페이스가 모델 이름을 +가리지 않습니다. 전체 `provider/model` 경로는 카탈로그 slug와 설명에 그대로 남습니다. 경로 자체를 눈에 띄게 +구분해야 한다면(예: `Claude Opus 5 (TeamClaude)`) 사용자 지정 표시 이름을 설정하세요. 사용자 지정 이름은 +계속 기본 레이블보다 우선합니다. reasoning 선택지는 프로바이더와 모델 메타데이터에 따라 Codex의 `low | medium | high | xhigh | max | ultra` 단계를 사용하며, 업스트림이 지원하지 않는 값은 요청을 보내기 전에 매핑하거나 지원 범위로 낮춥니다. diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index e77970afd..01c69037b 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -166,7 +166,11 @@ Codex показывает модели из каталога на диске ( объединённый каталог обратно. У маршрутизируемых записей каталога идентичность GPT-5 также переписывается на настоящее имя -вышестоящей модели. Элементы управления рассуждениями берутся из метаданных провайдера и модели +вышестоящей модели. По умолчанию в селекторе показывается последний сегмент нативного идентификатора +модели, поэтому пространство имён провайдера не скрывает имя модели в узком селекторе. Полный маршрут +`provider/model` сохраняется в slug и описании каталога. Если маршрут важно различать визуально +(например, `Claude Opus 5 (TeamClaude)`), задайте пользовательское отображаемое имя; оно по-прежнему +имеет приоритет над меткой по умолчанию. Элементы управления рассуждениями берутся из метаданных провайдера и модели по шкале Codex `low | medium | high | xhigh | max | ultra`; неподдерживаемые значения сопоставляются или ограничиваются перед запросом к вышестоящему провайдеру. diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index 53fceb9c9..26e768d91 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -138,7 +138,10 @@ Codex 显示的模型来自一个磁盘上的目录(默认为 `$CODEX_HOME/ope 4. **应用过滤**:`config.disabledModels`,以及每个提供商非空的 `selectedModels` allowlist。 5. **重新排序**,使置顶模型排在最前(见下文),然后将合并后的目录写回。 -路由目录条目还会把 GPT-5 身份文案改为真实的上游模型名称。reasoning 选项会依据提供商和模型元数据, +路由目录条目还会把 GPT-5 身份文案改为真实的上游模型名称。 +默认的选择器标签使用原生模型 ID 的最后一段,因此在较窄的选择器中,提供商命名空间不会遮住模型名称。 +完整的 `provider/model` 路由仍保留在目录 slug 和说明中。如果需要直观看出路由本身(例如 +`Claude Opus 5 (TeamClaude)`),请配置自定义显示名称;自定义名称仍然优先于默认标签。reasoning 选项会依据提供商和模型元数据, 使用 Codex 的 `low | medium | high | xhigh | max | ultra` 档位;上游不支持的值会在发送请求前完成 映射或下调。 From 4cd6e64629f698120a6d93310f5bd9506913062d Mon Sep 17 00:00:00 2001 From: Hand_Song <269829845+songwunil@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:15:15 +0900 Subject: [PATCH 3/4] fix(catalog): disambiguate picker label collisions --- .../content/docs/guides/codex-integration.md | 10 +-- .../docs/ja/guides/codex-integration.md | 6 +- .../docs/ko/guides/codex-integration.md | 7 ++- .../docs/ru/guides/codex-integration.md | 8 ++- .../docs/zh-cn/guides/codex-integration.md | 5 +- src/codex/catalog/effort.ts | 15 +++-- src/codex/catalog/parsing.ts | 3 +- src/codex/catalog/sync.ts | 63 +++++++++++++++++-- tests/codex-catalog.test.ts | 36 +++++++++++ 9 files changed, 127 insertions(+), 26 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 27f0cdd05..e5dfdf09f 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -185,10 +185,12 @@ start and on `ocx sync`, opencodex: 5. **Re-ranks** so featured models sort first (see below), then writes the merged catalog back. Routed catalog entries also get their GPT-5 identity rewritten to the real upstream model name. -Their default picker label is the final segment of the native model id, so provider namespaces do -not hide the model name in narrow pickers. The full `provider/model` route remains in the catalog -slug and description. Configure a custom display name when the route itself matters visually (for -example, `Claude Opus 5 (TeamClaude)`); custom names continue to take precedence. +When unique, their default picker label is the final segment of the native model id, so provider +namespaces do not hide the model name in narrow pickers. Basename collisions retain enough native +route context to distinguish the rows; if the same native id comes from multiple providers, the +provider is shown too. The full catalog slug remains in the description. Configure a custom display +name when the route itself matters visually (for example, `Claude Opus 5 (TeamClaude)`); custom +names continue to take precedence. Reasoning controls come from provider/model metadata across Codex's `low | medium | high | xhigh | max | ultra` ladder; unsupported values are mapped or clamped before the upstream request. diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index 8934c4656..6d1a8ea85 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -148,8 +148,10 @@ Codex はディスクのカタログ(デフォルト `$CODEX_HOME/opencodex-cata 5. フィーチャー済みモデルが先に並ぶよう**再整列**した後(下記参照)、マージされたカタログを書き戻します。 ルーティングされたカタログ項目の GPT-5 アイデンティティ文言も実際の上流モデル名に合わせます。 -デフォルトのピッカーラベルにはネイティブモデル ID の最後のセグメントが使われるため、幅の狭いピッカーでも -プロバイダーの名前空間がモデル名を隠しません。完全な `provider/model` 経路はカタログのスラッグと説明に残ります。 +一意であれば、デフォルトのピッカーラベルにはネイティブモデル ID の最後のセグメントが使われるため、幅の狭い +ピッカーでもプロバイダーの名前空間がモデル名を隠しません。同じ basename が衝突する場合は行を区別できるだけの +ネイティブ経路を残し、同じネイティブ ID が複数のプロバイダーにある場合はプロバイダー名も表示します。完全な +カタログスラッグは説明に残ります。 経路自体を見分ける必要がある場合(例: `Claude Opus 5 (TeamClaude)`)はカスタム表示名を設定してください。 カスタム表示名は引き続きデフォルトラベルより優先されます。推論選択肢は プロバイダーとモデルメタデータに応じて Codex の `low | medium | high | xhigh | max | ultra` 段階を使い、 diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index f4cabe570..11ae5409f 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -145,9 +145,10 @@ Codex는 디스크의 카탈로그(기본값 `$CODEX_HOME/opencodex-catalog.json 4. `config.disabledModels`와 각 프로바이더의 비어 있지 않은 `selectedModels` 허용 목록을 **적용**합니다. 5. featured 모델이 먼저 정렬되도록 **재정렬**한 뒤(아래 참고), 병합된 카탈로그를 다시 작성합니다. -라우팅된 카탈로그 항목의 GPT-5 정체성 문구도 실제 업스트림 모델 이름에 맞게 바꿉니다. 기본 선택기 레이블은 -네이티브 모델 ID의 마지막 부분을 사용하므로 폭이 좁은 선택기에서도 프로바이더 네임스페이스가 모델 이름을 -가리지 않습니다. 전체 `provider/model` 경로는 카탈로그 slug와 설명에 그대로 남습니다. 경로 자체를 눈에 띄게 +라우팅된 카탈로그 항목의 GPT-5 정체성 문구도 실제 업스트림 모델 이름에 맞게 바꿉니다. 이름이 고유하면 기본 선택기 +레이블은 네이티브 모델 ID의 마지막 부분만 사용하므로 폭이 좁은 선택기에서도 프로바이더 네임스페이스가 모델 이름을 +가리지 않습니다. 같은 basename이 겹치면 행을 구분하는 데 필요한 네이티브 경로를 남기고, 같은 네이티브 ID가 여러 +프로바이더에 있으면 프로바이더 이름도 표시합니다. 전체 카탈로그 slug는 설명에 그대로 남습니다. 경로 자체를 눈에 띄게 구분해야 한다면(예: `Claude Opus 5 (TeamClaude)`) 사용자 지정 표시 이름을 설정하세요. 사용자 지정 이름은 계속 기본 레이블보다 우선합니다. reasoning 선택지는 프로바이더와 모델 메타데이터에 따라 Codex의 `low | medium | high | xhigh | max | ultra` 단계를 사용하며, diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 01c69037b..4e875a36e 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -166,9 +166,11 @@ Codex показывает модели из каталога на диске ( объединённый каталог обратно. У маршрутизируемых записей каталога идентичность GPT-5 также переписывается на настоящее имя -вышестоящей модели. По умолчанию в селекторе показывается последний сегмент нативного идентификатора -модели, поэтому пространство имён провайдера не скрывает имя модели в узком селекторе. Полный маршрут -`provider/model` сохраняется в slug и описании каталога. Если маршрут важно различать визуально +вышестоящей модели. Если имя уникально, по умолчанию в селекторе показывается последний сегмент +нативного идентификатора модели, поэтому пространство имён провайдера не скрывает имя модели в узком +селекторе. При совпадении basename сохраняется достаточная часть нативного маршрута, а если один и тот +же нативный ID доступен у нескольких провайдеров, показывается и провайдер. Полный slug каталога +сохраняется в описании. Если маршрут важно различать визуально (например, `Claude Opus 5 (TeamClaude)`), задайте пользовательское отображаемое имя; оно по-прежнему имеет приоритет над меткой по умолчанию. Элементы управления рассуждениями берутся из метаданных провайдера и модели по шкале Codex `low | medium | high | xhigh | max | ultra`; неподдерживаемые значения diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index 26e768d91..706e04e27 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -139,8 +139,9 @@ Codex 显示的模型来自一个磁盘上的目录(默认为 `$CODEX_HOME/ope 5. **重新排序**,使置顶模型排在最前(见下文),然后将合并后的目录写回。 路由目录条目还会把 GPT-5 身份文案改为真实的上游模型名称。 -默认的选择器标签使用原生模型 ID 的最后一段,因此在较窄的选择器中,提供商命名空间不会遮住模型名称。 -完整的 `provider/model` 路由仍保留在目录 slug 和说明中。如果需要直观看出路由本身(例如 +名称唯一时,默认的选择器标签使用原生模型 ID 的最后一段,因此在较窄的选择器中,提供商命名空间不会遮住模型名称。 +若 basename 冲突,则保留足以区分各行的原生路由信息;若多个提供商暴露同一个原生 ID,还会显示提供商名称。 +完整的目录 slug 仍保留在说明中。如果需要直观看出路由本身(例如 `Claude Opus 5 (TeamClaude)`),请配置自定义显示名称;自定义名称仍然优先于默认标签。reasoning 选项会依据提供商和模型元数据, 使用 Codex 的 `low | medium | high | xhigh | max | ultra` 档位;上游不支持的值会在发送请求前完成 映射或下调。 diff --git a/src/codex/catalog/effort.ts b/src/codex/catalog/effort.ts index f007a4831..9e30813a1 100644 --- a/src/codex/catalog/effort.ts +++ b/src/codex/catalog/effort.ts @@ -109,7 +109,11 @@ export function catalogEntryEfforts(entry: RawEntry): string[] { export const ROUTED_REASONING_LEVELS = [...CODEX_REASONING_LEVELS]; -export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void { +export function applyCatalogModelMetadata( + entry: RawEntry, + model?: CatalogModel, + fallbackDisplayName?: string, +): void { if (!model) return; // This marker survives strict catalog normalization and lets sync distinguish a stale // bare combo alias from a genuine native model row. @@ -117,15 +121,16 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel) // displayName is DISPLAY-ONLY: it relabels the picker row but never touches the routing // slug, alias, or provider. A configured displayName wins. Otherwise physical routed rows // use the final segment of their native id, keeping provider/vendor namespaces out of the - // narrow picker label while the full route remains in slug + description. Aliased combos - // keep their public alias. Natives never reach here (no CatalogModel), so genuine upstream - // marketing names are preserved untouched. + // narrow picker label. buildCatalogEntries supplies a longer fallback only when that basename + // would collide with another visible row. Aliased combos keep their public alias. Natives never + // reach here (no CatalogModel), so genuine upstream marketing names are preserved untouched. const displayName = typeof model.displayName === "string" ? model.displayName.trim() : ""; if (displayName) entry.display_name = displayName; else if (!model.alias) { const nativeId = model.id.trim(); const finalSegment = nativeId.slice(nativeId.lastIndexOf("/") + 1).trim(); - if (finalSegment) entry.display_name = finalSegment; + const fallback = fallbackDisplayName?.trim() || finalSegment; + if (fallback) entry.display_name = fallback; } if (typeof model.contextWindow === "number" && model.contextWindow > 0) { entry.context_window = model.contextWindow; diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 1fbd80709..f83176e84 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -93,7 +93,8 @@ export interface CatalogModel { * Display-only Codex catalog `display_name` override. Relabels the picker row ONLY — it never * affects the routing slug, alias-collision order, native marketing-name precedence, or provider * behavior. When unset, routed entries use the final segment of the native model id so the - * provider namespace does not crowd the picker; aliased combo rows keep their public alias. + * provider namespace does not crowd the picker; basename collisions retain enough route context + * to stay distinguishable, and aliased combo rows keep their public alias. * Native upstream entries (e.g. gpt-5.6-sol → "GPT-5.6-Sol") come from the pinned snapshot path * which carries no CatalogModel, so a configured displayName can never override a native name. */ diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index d7724439f..43084877b 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -145,6 +145,7 @@ export function deriveEntry( priority: number, model?: CatalogModel, exactComboSlugs: ReadonlySet = new Set(), + fallbackDisplayName?: string, ): RawEntry { const preserveExact = isExactComboCatalogModel(model, exactComboSlugs); const isRouted = model !== undefined; @@ -181,7 +182,7 @@ export function deriveEntry( applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact); normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true); if (model) applyJawcodeCatalogMetadata(e, model.provider, model.id, model.contextCap); - applyCatalogModelMetadata(e, model); + applyCatalogModelMetadata(e, model, fallbackDisplayName); } else { applyNativeOpenAiContextOverride(e); if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e); @@ -218,7 +219,7 @@ export function deriveEntry( if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry); } if (model && isRouted) applyJawcodeCatalogMetadata(entry, model.provider, model.id, model.contextCap); - applyCatalogModelMetadata(entry, model); + applyCatalogModelMetadata(entry, model, fallbackDisplayName); if (!isRouted) applyNativeOpenAiContextOverride(entry); return ensureStrictCatalogFields(normalizeServiceTiers(entry), { preserveExactInputModalities: preserveExact, @@ -226,6 +227,50 @@ export function deriveEntry( }); } +function configuredDisplayName(model: CatalogModel): string { + return typeof model.displayName === "string" ? model.displayName.trim() : ""; +} + +function defaultPickerLabel(model: CatalogModel): string { + const nativeId = model.id.trim(); + return nativeId.slice(nativeId.lastIndexOf("/") + 1).trim(); +} + +/** + * Keep the common picker path compact, but retain only as much route context as needed when two + * emitted rows would otherwise have the same label. Explicit display names and combo aliases are + * author-owned and never rewritten. If distinct native ids share a basename, the native id is + * enough; if the same native id is exposed by multiple providers, append the provider as well. + */ +function routedFallbackDisplayNames(models: readonly CatalogModel[]): Map { + const intendedLabel = (model: CatalogModel): string => + configuredDisplayName(model) || model.alias?.trim() || defaultPickerLabel(model); + const labelCounts = new Map(); + const nativeIdCounts = new Map(); + for (const model of models) { + const label = intendedLabel(model); + if (!label) continue; + labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1); + const nativeKey = `${label}\u0000${model.id.trim()}`; + nativeIdCounts.set(nativeKey, (nativeIdCounts.get(nativeKey) ?? 0) + 1); + } + + const out = new Map(); + for (const model of models) { + if (configuredDisplayName(model) || model.alias) continue; + const label = defaultPickerLabel(model); + if (!label) continue; + if ((labelCounts.get(label) ?? 0) <= 1) { + out.set(model, label); + continue; + } + const nativeId = model.id.trim(); + const sameNativeIdCount = nativeIdCounts.get(`${label}\u0000${nativeId}`) ?? 0; + out.set(model, sameNativeIdCount > 1 ? `${nativeId} (${model.provider})` : nativeId); + } + return out; +} + export function buildCatalogEntries( template: RawEntry | null, gptSlugs: string[], @@ -250,22 +295,28 @@ export function buildCatalogEntries( if (rank.has(slug)) e.priority = rank.get(slug)!; out.push(e); } - for (const m of goModels) { - if (collisionSkipped.has(m)) continue; + const routedModels = goModels.filter(m => { + if (collisionSkipped.has(m)) return false; const slug = catalogModelSlug(m); if (m.provider !== COMBO_NAMESPACE && comboPublicSlugs.has(slug)) { warnComboMasqueradeCollisionOnce(slug); - continue; + return false; } + return true; + }); + const fallbackDisplayNames = routedFallbackDisplayNames(routedModels); + for (const m of routedModels) { + const slug = catalogModelSlug(m); // Provider rows use the one-slash slug codec; combo aliases intentionally override that // public slug and may be bare. const e = deriveEntry( template, slug, - `Routed via opencodex → ${m.provider} (${m.owned_by ?? m.provider}).`, + `Routed via opencodex → ${slug} (${m.owned_by ?? m.provider}).`, 5, m, exactComboSlugs, + fallbackDisplayNames.get(m), ); // Featured picks may be stored raw (legacy) or encoded — honor both. const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`); diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index f8364aaa2..acda7ef2d 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -755,6 +755,42 @@ describe("configured CatalogModel displayName -> catalog display_name", () => { expect(row?.slug).toBe("openrouter/google-gemini-3.6-flash"); }); + test("basename collisions retain only the route context needed to distinguish rows", () => { + const entries = buildCatalogEntries(nativeTemplate(), [], [ + { provider: "openrouter", id: "reka/reka-edge", owned_by: "openrouter" }, + { provider: "openrouter", id: "rekaai/reka-edge", owned_by: "openrouter" }, + ]); + const reka = entries.find(e => e.slug === "openrouter/reka-reka-edge"); + const rekaAi = entries.find(e => e.slug === "openrouter/rekaai-reka-edge"); + + expect(reka?.display_name).toBe("reka/reka-edge"); + expect(rekaAi?.display_name).toBe("rekaai/reka-edge"); + expect(reka?.description).toContain("openrouter/reka-reka-edge"); + expect(rekaAi?.description).toContain("openrouter/rekaai-reka-edge"); + }); + + test("the same native id exposed by multiple providers also includes the provider", () => { + const entries = buildCatalogEntries(nativeTemplate(), [], [ + { provider: "openrouter", id: "moonshotai/kimi-k3", owned_by: "openrouter" }, + { provider: "nvidia", id: "moonshotai/kimi-k3", owned_by: "nvidia" }, + ]); + + expect(entries.find(e => e.slug === "openrouter/moonshotai-kimi-k3")?.display_name) + .toBe("moonshotai/kimi-k3 (openrouter)"); + expect(entries.find(e => e.slug === "nvidia/moonshotai-kimi-k3")?.display_name) + .toBe("moonshotai/kimi-k3 (nvidia)"); + }); + + test("explicit display names keep precedence when they collide with a default label", () => { + const entries = buildCatalogEntries(nativeTemplate(), [], [ + { provider: "openrouter", id: "reka/reka-edge", displayName: "reka-edge", owned_by: "openrouter" }, + { provider: "openrouter", id: "rekaai/reka-edge", owned_by: "openrouter" }, + ]); + + expect(entries.find(e => e.slug === "openrouter/reka-reka-edge")?.display_name).toBe("reka-edge"); + expect(entries.find(e => e.slug === "openrouter/rekaai-reka-edge")?.display_name).toBe("rekaai/reka-edge"); + }); + test("an aliased combo keeps its public alias when no displayName is configured", () => { const withAlias = { provider: "combo", id: "x", alias: "fast-chat", owned_by: "combo" }; const entries = buildCatalogEntries(nativeTemplate(), [], [withAlias], undefined, false, "default", new Set(["fast-chat"])); From 9ea993b0ea58377dd4646a1d3b978a196cb49ad1 Mon Sep 17 00:00:00 2001 From: Hand_Song <269829845+songwunil@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:20:00 +0900 Subject: [PATCH 4/4] docs(codex): clarify catalog versus runtime identity --- docs-site/src/content/docs/guides/codex-integration.md | 4 +++- docs-site/src/content/docs/ja/guides/codex-integration.md | 4 +++- docs-site/src/content/docs/ko/guides/codex-integration.md | 4 +++- docs-site/src/content/docs/ru/guides/codex-integration.md | 6 ++++-- .../src/content/docs/zh-cn/guides/codex-integration.md | 3 ++- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index e5dfdf09f..84dee9b8a 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -184,7 +184,9 @@ start and on `ocx sync`, opencodex: 4. **Filters** `config.disabledModels` and each provider's non-empty `selectedModels` allowlist. 5. **Re-ranks** so featured models sort first (see below), then writes the merged catalog back. -Routed catalog entries also get their GPT-5 identity rewritten to the real upstream model name. +The cloned catalog `base_instructions` identity line is rewritten to the real upstream model name +as static catalog metadata. Runtime request identity is handled separately: routed adapters replace +Codex's live GPT-5 identity line with a model-agnostic coding-agent introduction. When unique, their default picker label is the final segment of the native model id, so provider namespaces do not hide the model name in narrow pickers. Basename collisions retain enough native route context to distinguish the rows; if the same native id comes from multiple providers, the diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index 6d1a8ea85..5002c66dc 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -147,7 +147,9 @@ Codex はディスクのカタログ(デフォルト `$CODEX_HOME/opencodex-cata 4. `config.disabledModels` と各プロバイダーの空でない `selectedModels` 許可リストを**適用**します。 5. フィーチャー済みモデルが先に並ぶよう**再整列**した後(下記参照)、マージされたカタログを書き戻します。 -ルーティングされたカタログ項目の GPT-5 アイデンティティ文言も実際の上流モデル名に合わせます。 +複製されたカタログの `base_instructions` にあるアイデンティティ行は、静的なカタログメタデータとして実際の +上流モデル名に合わせて書き換えられます。実行時リクエストのアイデンティティは別に処理され、ルーティング +アダプターが Codex のライブ GPT-5 アイデンティティ行をモデル非依存のコーディングエージェント文言に置き換えます。 一意であれば、デフォルトのピッカーラベルにはネイティブモデル ID の最後のセグメントが使われるため、幅の狭い ピッカーでもプロバイダーの名前空間がモデル名を隠しません。同じ basename が衝突する場合は行を区別できるだけの ネイティブ経路を残し、同じネイティブ ID が複数のプロバイダーにある場合はプロバイダー名も表示します。完全な diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index 11ae5409f..4d1061e36 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -145,7 +145,9 @@ Codex는 디스크의 카탈로그(기본값 `$CODEX_HOME/opencodex-catalog.json 4. `config.disabledModels`와 각 프로바이더의 비어 있지 않은 `selectedModels` 허용 목록을 **적용**합니다. 5. featured 모델이 먼저 정렬되도록 **재정렬**한 뒤(아래 참고), 병합된 카탈로그를 다시 작성합니다. -라우팅된 카탈로그 항목의 GPT-5 정체성 문구도 실제 업스트림 모델 이름에 맞게 바꿉니다. 이름이 고유하면 기본 선택기 +복제된 카탈로그의 `base_instructions` 정체성 문구는 정적 카탈로그 메타데이터에서만 실제 업스트림 모델 이름으로 +바뀝니다. 런타임 요청의 정체성은 별도로 처리되며, 라우팅 어댑터가 Codex의 실시간 GPT-5 정체성 문구를 모델과 무관한 +코딩 에이전트 문구로 바꿉니다. 이름이 고유하면 기본 선택기 레이블은 네이티브 모델 ID의 마지막 부분만 사용하므로 폭이 좁은 선택기에서도 프로바이더 네임스페이스가 모델 이름을 가리지 않습니다. 같은 basename이 겹치면 행을 구분하는 데 필요한 네이티브 경로를 남기고, 같은 네이티브 ID가 여러 프로바이더에 있으면 프로바이더 이름도 표시합니다. 전체 카탈로그 slug는 설명에 그대로 남습니다. 경로 자체를 눈에 띄게 diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 4e875a36e..1eedc25ec 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -165,8 +165,10 @@ Codex показывает модели из каталога на диске ( 5. **Переранжирует** так, чтобы избранные модели шли первыми (см. ниже), и записывает объединённый каталог обратно. -У маршрутизируемых записей каталога идентичность GPT-5 также переписывается на настоящее имя -вышестоящей модели. Если имя уникально, по умолчанию в селекторе показывается последний сегмент +В клонированном `base_instructions` строка идентичности переписывается на настоящее имя вышестоящей +модели только как статические метаданные каталога. Идентичность рабочего запроса обрабатывается +отдельно: маршрутизируемые адаптеры заменяют живую строку Codex о GPT-5 нейтральным описанием +агента программирования. Если имя уникально, по умолчанию в селекторе показывается последний сегмент нативного идентификатора модели, поэтому пространство имён провайдера не скрывает имя модели в узком селекторе. При совпадении basename сохраняется достаточная часть нативного маршрута, а если один и тот же нативный ID доступен у нескольких провайдеров, показывается и провайдер. Полный slug каталога diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index 706e04e27..43c9e6b58 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -138,7 +138,8 @@ Codex 显示的模型来自一个磁盘上的目录(默认为 `$CODEX_HOME/ope 4. **应用过滤**:`config.disabledModels`,以及每个提供商非空的 `selectedModels` allowlist。 5. **重新排序**,使置顶模型排在最前(见下文),然后将合并后的目录写回。 -路由目录条目还会把 GPT-5 身份文案改为真实的上游模型名称。 +克隆目录中的 `base_instructions` 身份行只会作为静态目录元数据改写为真实的上游模型名称。 +运行时请求的身份信息单独处理:路由适配器会把 Codex 实时发送的 GPT-5 身份行替换为与模型无关的编码代理说明。 名称唯一时,默认的选择器标签使用原生模型 ID 的最后一段,因此在较窄的选择器中,提供商命名空间不会遮住模型名称。 若 basename 冲突,则保留足以区分各行的原生路由信息;若多个提供商暴露同一个原生 ID,还会显示提供商名称。 完整的目录 slug 仍保留在说明中。如果需要直观看出路由本身(例如