Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 44 additions & 2 deletions src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick<Ocx
return slug;
}

/**
* Cria uma entrada nativa ou roteada a partir do snapshot upstream, de um clone
* do template ou de campos mínimos. Aplica os metadados e limites pertinentes
* sem alterar o template nem herdar sua marca de nome ou histórico de prioridade.
*/
export function deriveEntry(
template: RawEntry | null,
slug: string,
Expand All @@ -332,6 +337,7 @@ export function deriveEntry(
}
if (template || codexForwardNativeCapabilityAlias) {
const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
delete e.opencodex_native_display_name;
// A cached template may carry display-order history; each new row owns its natural rank.
delete e[SPAWN_PRIORITY_FIELD];
e.slug = slug;
Expand Down Expand Up @@ -773,6 +779,20 @@ function recoverableNativeSlug(entry: RawEntry): string | null {
: null;
}

/** Undo our display overlay before native metadata normalization and template reuse. */
function restoreNativeDisplayName(entry: RawEntry): RawEntry {
const saved = entry.opencodex_native_display_name;
delete entry.opencodex_native_display_name;
if (saved && typeof saved === "object" && !Array.isArray(saved)) {
const label = saved as Record<string, unknown>;
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[],
Expand Down Expand Up @@ -862,6 +882,8 @@ export interface ObservedCatalogMergeInput {
readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
readonly policy: ObservedCatalogMergePolicy;
readonly openaiContextCap?: NativeContextLimitsInput;
/** Exact display-only labels for bare native OpenAI models. */
readonly nativeDisplayNames?: Readonly<Record<string, string>>;
}

/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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],
Expand Down
7 changes: 7 additions & 0 deletions src/codex/convergence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OcxConfig>,
source: Extract<CatalogSourceForGather, { kind: "available" }>,
Expand Down Expand Up @@ -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],
Expand Down
75 changes: 75 additions & 0 deletions tests/codex-integration/codex-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading