Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,15 @@ 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
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.

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

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

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

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

Expand Down
12 changes: 10 additions & 2 deletions docs-site/src/content/docs/ru/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,16 @@ Codex показывает модели из каталога на диске (
5. **Переранжирует** так, чтобы избранные модели шли первыми (см. ниже), и записывает
объединённый каталог обратно.

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

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

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

Expand Down
22 changes: 16 additions & 6 deletions src/codex/catalog/effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,29 @@ 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.
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. 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disambiguate basename-only picker labels

When a provider exposes two namespaced native IDs with the same last segment (for example OpenRouter metadata includes both reka/reka-edge and rekaai/reka-edge), this fallback gives both catalog rows the same display_name while buildCatalogEntries also gives them the same provider-only description. In Codex pickers that show the label/description, users can no longer tell which upstream route they are selecting; keep enough of model.id to disambiguate basename collisions or fall back to the encoded slug for those cases.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4cd6e64. Default labels stay basename-only when unique; basename collisions retain the native id, and the same native id from multiple providers also includes the provider. Explicit display names and combo aliases still win. Added regression coverage for all three cases.

const fallback = fallbackDisplayName?.trim() || finalSegment;
if (fallback) entry.display_name = fallback;
}
if (typeof model.contextWindow === "number" && model.contextWindow > 0) {
entry.context_window = model.contextWindow;
entry.max_context_window = model.contextWindow;
Expand Down
4 changes: 3 additions & 1 deletion src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ 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; 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.
*/
Expand Down
63 changes: 57 additions & 6 deletions src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export function deriveEntry(
priority: number,
model?: CatalogModel,
exactComboSlugs: ReadonlySet<string> = new Set(),
fallbackDisplayName?: string,
): RawEntry {
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
const isRouted = model !== undefined;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -218,14 +219,58 @@ 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,
isRouted,
});
}

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<CatalogModel, string> {
const intendedLabel = (model: CatalogModel): string =>
configuredDisplayName(model) || model.alias?.trim() || defaultPickerLabel(model);
const labelCounts = new Map<string, number>();
const nativeIdCounts = new Map<string, number>();
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<CatalogModel, string>();
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[],
Expand All @@ -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}`);
Expand Down
Loading
Loading