Skip to content

Commit bf7a643

Browse files
authored
Merge pull request #763 from songwunil/fix/routed-picker-labels
fix(catalog): prioritize model names in picker labels
2 parents 78416d9 + 9ea993b commit bf7a643

10 files changed

Lines changed: 170 additions & 26 deletions

File tree

docs-site/src/content/docs/guides/codex-integration.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,15 @@ start and on `ocx sync`, opencodex:
184184
4. **Filters** `config.disabledModels` and each provider's non-empty `selectedModels` allowlist.
185185
5. **Re-ranks** so featured models sort first (see below), then writes the merged catalog back.
186186

187-
Routed catalog entries also get their GPT-5 identity rewritten to the real upstream model name.
187+
The cloned catalog `base_instructions` identity line is rewritten to the real upstream model name
188+
as static catalog metadata. Runtime request identity is handled separately: routed adapters replace
189+
Codex's live GPT-5 identity line with a model-agnostic coding-agent introduction.
190+
When unique, their default picker label is the final segment of the native model id, so provider
191+
namespaces do not hide the model name in narrow pickers. Basename collisions retain enough native
192+
route context to distinguish the rows; if the same native id comes from multiple providers, the
193+
provider is shown too. The full catalog slug remains in the description. Configure a custom display
194+
name when the route itself matters visually (for example, `Claude Opus 5 (TeamClaude)`); custom
195+
names continue to take precedence.
188196
Reasoning controls come from provider/model metadata across Codex's `low | medium | high | xhigh |
189197
max | ultra` ladder; unsupported values are mapped or clamped before the upstream request.
190198

docs-site/src/content/docs/ja/guides/codex-integration.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,15 @@ Codex はディスクのカタログ(デフォルト `$CODEX_HOME/opencodex-cata
147147
4. `config.disabledModels` と各プロバイダーの空でない `selectedModels` 許可リストを**適用**します。
148148
5. フィーチャー済みモデルが先に並ぶよう**再整列**した後(下記参照)、マージされたカタログを書き戻します。
149149

150-
ルーティングされたカタログ項目の GPT-5 アイデンティティ文言も実際の上流モデル名に合わせます。推論選択肢は
150+
複製されたカタログの `base_instructions` にあるアイデンティティ行は、静的なカタログメタデータとして実際の
151+
上流モデル名に合わせて書き換えられます。実行時リクエストのアイデンティティは別に処理され、ルーティング
152+
アダプターが Codex のライブ GPT-5 アイデンティティ行をモデル非依存のコーディングエージェント文言に置き換えます。
153+
一意であれば、デフォルトのピッカーラベルにはネイティブモデル ID の最後のセグメントが使われるため、幅の狭い
154+
ピッカーでもプロバイダーの名前空間がモデル名を隠しません。同じ basename が衝突する場合は行を区別できるだけの
155+
ネイティブ経路を残し、同じネイティブ ID が複数のプロバイダーにある場合はプロバイダー名も表示します。完全な
156+
カタログスラッグは説明に残ります。
157+
経路自体を見分ける必要がある場合(例: `Claude Opus 5 (TeamClaude)`)はカスタム表示名を設定してください。
158+
カスタム表示名は引き続きデフォルトラベルより優先されます。推論選択肢は
151159
プロバイダーとモデルメタデータに応じて Codex の `low | medium | high | xhigh | max | ultra` 段階を使い、
152160
上流がサポートしない値はリクエスト送信前にマッピングまたはサポート範囲に下げます。
153161

docs-site/src/content/docs/ko/guides/codex-integration.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,14 @@ Codex는 디스크의 카탈로그(기본값 `$CODEX_HOME/opencodex-catalog.json
145145
4. `config.disabledModels`와 각 프로바이더의 비어 있지 않은 `selectedModels` 허용 목록을 **적용**합니다.
146146
5. featured 모델이 먼저 정렬되도록 **재정렬**한 뒤(아래 참고), 병합된 카탈로그를 다시 작성합니다.
147147

148-
라우팅된 카탈로그 항목의 GPT-5 정체성 문구도 실제 업스트림 모델 이름에 맞게 바꿉니다. reasoning 선택지는
148+
복제된 카탈로그의 `base_instructions` 정체성 문구는 정적 카탈로그 메타데이터에서만 실제 업스트림 모델 이름으로
149+
바뀝니다. 런타임 요청의 정체성은 별도로 처리되며, 라우팅 어댑터가 Codex의 실시간 GPT-5 정체성 문구를 모델과 무관한
150+
코딩 에이전트 문구로 바꿉니다. 이름이 고유하면 기본 선택기
151+
레이블은 네이티브 모델 ID의 마지막 부분만 사용하므로 폭이 좁은 선택기에서도 프로바이더 네임스페이스가 모델 이름을
152+
가리지 않습니다. 같은 basename이 겹치면 행을 구분하는 데 필요한 네이티브 경로를 남기고, 같은 네이티브 ID가 여러
153+
프로바이더에 있으면 프로바이더 이름도 표시합니다. 전체 카탈로그 slug는 설명에 그대로 남습니다. 경로 자체를 눈에 띄게
154+
구분해야 한다면(예: `Claude Opus 5 (TeamClaude)`) 사용자 지정 표시 이름을 설정하세요. 사용자 지정 이름은
155+
계속 기본 레이블보다 우선합니다. reasoning 선택지는
149156
프로바이더와 모델 메타데이터에 따라 Codex의 `low | medium | high | xhigh | max | ultra` 단계를 사용하며,
150157
업스트림이 지원하지 않는 값은 요청을 보내기 전에 매핑하거나 지원 범위로 낮춥니다.
151158

docs-site/src/content/docs/ru/guides/codex-integration.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,16 @@ Codex показывает модели из каталога на диске (
165165
5. **Переранжирует** так, чтобы избранные модели шли первыми (см. ниже), и записывает
166166
объединённый каталог обратно.
167167

168-
У маршрутизируемых записей каталога идентичность GPT-5 также переписывается на настоящее имя
169-
вышестоящей модели. Элементы управления рассуждениями берутся из метаданных провайдера и модели
168+
В клонированном `base_instructions` строка идентичности переписывается на настоящее имя вышестоящей
169+
модели только как статические метаданные каталога. Идентичность рабочего запроса обрабатывается
170+
отдельно: маршрутизируемые адаптеры заменяют живую строку Codex о GPT-5 нейтральным описанием
171+
агента программирования. Если имя уникально, по умолчанию в селекторе показывается последний сегмент
172+
нативного идентификатора модели, поэтому пространство имён провайдера не скрывает имя модели в узком
173+
селекторе. При совпадении basename сохраняется достаточная часть нативного маршрута, а если один и тот
174+
же нативный ID доступен у нескольких провайдеров, показывается и провайдер. Полный slug каталога
175+
сохраняется в описании. Если маршрут важно различать визуально
176+
(например, `Claude Opus 5 (TeamClaude)`), задайте пользовательское отображаемое имя; оно по-прежнему
177+
имеет приоритет над меткой по умолчанию. Элементы управления рассуждениями берутся из метаданных провайдера и модели
170178
по шкале Codex `low | medium | high | xhigh | max | ultra`; неподдерживаемые значения
171179
сопоставляются или ограничиваются перед запросом к вышестоящему провайдеру.
172180

docs-site/src/content/docs/zh-cn/guides/codex-integration.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,12 @@ Codex 显示的模型来自一个磁盘上的目录(默认为 `$CODEX_HOME/ope
138138
4. **应用过滤**`config.disabledModels`,以及每个提供商非空的 `selectedModels` allowlist。
139139
5. **重新排序**,使置顶模型排在最前(见下文),然后将合并后的目录写回。
140140

141-
路由目录条目还会把 GPT-5 身份文案改为真实的上游模型名称。reasoning 选项会依据提供商和模型元数据,
141+
克隆目录中的 `base_instructions` 身份行只会作为静态目录元数据改写为真实的上游模型名称。
142+
运行时请求的身份信息单独处理:路由适配器会把 Codex 实时发送的 GPT-5 身份行替换为与模型无关的编码代理说明。
143+
名称唯一时,默认的选择器标签使用原生模型 ID 的最后一段,因此在较窄的选择器中,提供商命名空间不会遮住模型名称。
144+
若 basename 冲突,则保留足以区分各行的原生路由信息;若多个提供商暴露同一个原生 ID,还会显示提供商名称。
145+
完整的目录 slug 仍保留在说明中。如果需要直观看出路由本身(例如
146+
`Claude Opus 5 (TeamClaude)`),请配置自定义显示名称;自定义名称仍然优先于默认标签。reasoning 选项会依据提供商和模型元数据,
142147
使用 Codex 的 `low | medium | high | xhigh | max | ultra` 档位;上游不支持的值会在发送请求前完成
143148
映射或下调。
144149

src/codex/catalog/effort.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,29 @@ export function catalogEntryEfforts(entry: RawEntry): string[] {
109109

110110
export const ROUTED_REASONING_LEVELS = [...CODEX_REASONING_LEVELS];
111111

112-
export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void {
112+
export function applyCatalogModelMetadata(
113+
entry: RawEntry,
114+
model?: CatalogModel,
115+
fallbackDisplayName?: string,
116+
): void {
113117
if (!model) return;
114118
// This marker survives strict catalog normalization and lets sync distinguish a stale
115119
// bare combo alias from a genuine native model row.
116120
if (model.provider === COMBO_NAMESPACE) entry.owned_by = model.owned_by ?? COMBO_NAMESPACE;
117121
// displayName is DISPLAY-ONLY: it relabels the picker row but never touches the routing
118-
// slug, alias, or provider. deriveEntry already stamped the slug as display_name; a
119-
// configured displayName overrides just the label. The `/` separator is rejected at every
120-
// input boundary (CLI `ocx models add`, management API), so the catalog trusts its source.
121-
// Combos carry no displayName, and natives never reach here (no CatalogModel), so genuine
122-
// upstream marketing names and combo alias labels are preserved untouched.
122+
// slug, alias, or provider. A configured displayName wins. Otherwise physical routed rows
123+
// use the final segment of their native id, keeping provider/vendor namespaces out of the
124+
// narrow picker label. buildCatalogEntries supplies a longer fallback only when that basename
125+
// would collide with another visible row. Aliased combos keep their public alias. Natives never
126+
// reach here (no CatalogModel), so genuine upstream marketing names are preserved untouched.
123127
const displayName = typeof model.displayName === "string" ? model.displayName.trim() : "";
124128
if (displayName) entry.display_name = displayName;
129+
else if (!model.alias) {
130+
const nativeId = model.id.trim();
131+
const finalSegment = nativeId.slice(nativeId.lastIndexOf("/") + 1).trim();
132+
const fallback = fallbackDisplayName?.trim() || finalSegment;
133+
if (fallback) entry.display_name = fallback;
134+
}
125135
if (typeof model.contextWindow === "number" && model.contextWindow > 0) {
126136
entry.context_window = model.contextWindow;
127137
entry.max_context_window = model.contextWindow;

src/codex/catalog/parsing.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ export interface CatalogModel {
9292
/**
9393
* Display-only Codex catalog `display_name` override. Relabels the picker row ONLY — it never
9494
* affects the routing slug, alias-collision order, native marketing-name precedence, or provider
95-
* behavior. When unset, the entry falls back to its Codex-facing slug (the historical behavior).
95+
* behavior. When unset, routed entries use the final segment of the native model id so the
96+
* provider namespace does not crowd the picker; basename collisions retain enough route context
97+
* to stay distinguishable, and aliased combo rows keep their public alias.
9698
* Native upstream entries (e.g. gpt-5.6-sol → "GPT-5.6-Sol") come from the pinned snapshot path
9799
* which carries no CatalogModel, so a configured displayName can never override a native name.
98100
*/

src/codex/catalog/sync.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export function deriveEntry(
166166
priority: number,
167167
model?: CatalogModel,
168168
exactComboSlugs: ReadonlySet<string> = new Set(),
169+
fallbackDisplayName?: string,
169170
): RawEntry {
170171
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
171172
const isRouted = model !== undefined;
@@ -202,7 +203,7 @@ export function deriveEntry(
202203
applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
203204
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
204205
if (model) applyJawcodeCatalogMetadata(e, model.provider, model.id, model.contextCap);
205-
applyCatalogModelMetadata(e, model);
206+
applyCatalogModelMetadata(e, model, fallbackDisplayName);
206207
} else {
207208
applyNativeOpenAiContextOverride(e);
208209
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
@@ -239,14 +240,58 @@ export function deriveEntry(
239240
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
240241
}
241242
if (model && isRouted) applyJawcodeCatalogMetadata(entry, model.provider, model.id, model.contextCap);
242-
applyCatalogModelMetadata(entry, model);
243+
applyCatalogModelMetadata(entry, model, fallbackDisplayName);
243244
if (!isRouted) applyNativeOpenAiContextOverride(entry);
244245
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
245246
preserveExactInputModalities: preserveExact,
246247
isRouted,
247248
});
248249
}
249250

251+
function configuredDisplayName(model: CatalogModel): string {
252+
return typeof model.displayName === "string" ? model.displayName.trim() : "";
253+
}
254+
255+
function defaultPickerLabel(model: CatalogModel): string {
256+
const nativeId = model.id.trim();
257+
return nativeId.slice(nativeId.lastIndexOf("/") + 1).trim();
258+
}
259+
260+
/**
261+
* Keep the common picker path compact, but retain only as much route context as needed when two
262+
* emitted rows would otherwise have the same label. Explicit display names and combo aliases are
263+
* author-owned and never rewritten. If distinct native ids share a basename, the native id is
264+
* enough; if the same native id is exposed by multiple providers, append the provider as well.
265+
*/
266+
function routedFallbackDisplayNames(models: readonly CatalogModel[]): Map<CatalogModel, string> {
267+
const intendedLabel = (model: CatalogModel): string =>
268+
configuredDisplayName(model) || model.alias?.trim() || defaultPickerLabel(model);
269+
const labelCounts = new Map<string, number>();
270+
const nativeIdCounts = new Map<string, number>();
271+
for (const model of models) {
272+
const label = intendedLabel(model);
273+
if (!label) continue;
274+
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
275+
const nativeKey = `${label}\u0000${model.id.trim()}`;
276+
nativeIdCounts.set(nativeKey, (nativeIdCounts.get(nativeKey) ?? 0) + 1);
277+
}
278+
279+
const out = new Map<CatalogModel, string>();
280+
for (const model of models) {
281+
if (configuredDisplayName(model) || model.alias) continue;
282+
const label = defaultPickerLabel(model);
283+
if (!label) continue;
284+
if ((labelCounts.get(label) ?? 0) <= 1) {
285+
out.set(model, label);
286+
continue;
287+
}
288+
const nativeId = model.id.trim();
289+
const sameNativeIdCount = nativeIdCounts.get(`${label}\u0000${nativeId}`) ?? 0;
290+
out.set(model, sameNativeIdCount > 1 ? `${nativeId} (${model.provider})` : nativeId);
291+
}
292+
return out;
293+
}
294+
250295
export function buildCatalogEntries(
251296
template: RawEntry | null,
252297
gptSlugs: string[],
@@ -271,22 +316,28 @@ export function buildCatalogEntries(
271316
if (rank.has(slug)) e.priority = rank.get(slug)!;
272317
out.push(e);
273318
}
274-
for (const m of goModels) {
275-
if (collisionSkipped.has(m)) continue;
319+
const routedModels = goModels.filter(m => {
320+
if (collisionSkipped.has(m)) return false;
276321
const slug = catalogModelSlug(m);
277322
if (m.provider !== COMBO_NAMESPACE && comboPublicSlugs.has(slug)) {
278323
warnComboMasqueradeCollisionOnce(slug);
279-
continue;
324+
return false;
280325
}
326+
return true;
327+
});
328+
const fallbackDisplayNames = routedFallbackDisplayNames(routedModels);
329+
for (const m of routedModels) {
330+
const slug = catalogModelSlug(m);
281331
// Provider rows use the one-slash slug codec; combo aliases intentionally override that
282332
// public slug and may be bare.
283333
const e = deriveEntry(
284334
template,
285335
slug,
286-
`Routed via opencodex → ${m.provider} (${m.owned_by ?? m.provider}).`,
336+
`Routed via opencodex → ${slug} (${m.owned_by ?? m.provider}).`,
287337
5,
288338
m,
289339
exactComboSlugs,
340+
fallbackDisplayNames.get(m),
290341
);
291342
// Featured picks may be stored raw (legacy) or encoded — honor both.
292343
const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);

0 commit comments

Comments
 (0)