Skip to content
Closed
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
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/fr/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,9 @@ pas les copies externes ; révoquez-la séparément sur le hub si nécessaire.
## Le sélecteur /model (« Depuis la passerelle »)

Claude Code 2.1.129+ découvre les modèles de passerelle via `GET /v1/models?limit=1000` et les répertorie dans
le sélecteur natif `/model` intitulé « Depuis la passerelle ». Comme ce sélecteur n'accepte que les identifiants commençant
le sélecteur natif `/model`. Une ligne sans `description` affiche « From gateway » ; opencodex en envoie une pour
chaque ligne du CLI Claude Code (`Routed by OpenCodex to <provider>/<model>` ; lignes natives : `Routed by OpenCodex to native <model>` ; les lignes Fast ajoutent ` · Fast` et les lignes 1M gardent la description de base), que Claude Code 2.1.257+ affiche
à la place. Comme ce sélecteur n'accepte que les identifiants commençant
Comment thread
coderabbitai[bot] marked this conversation as resolved.
par `claude` ou `anthropic`, opencodex expose les modèles routés sous forme d'alias stables et réversibles :

| Surface | Format | Exemple |
Expand Down
6 changes: 4 additions & 2 deletions docs-site/src/content/docs/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,10 @@ arbitrary external copies; revoke separately on the hub if desired.
## The /model picker ("From gateway")

Claude Code 2.1.129+ discovers gateway models via `GET /v1/models?limit=1000` and lists them in
the native `/model` picker labeled "From gateway". Because the picker only accepts ids beginning
with `claude` or `anthropic`, opencodex exposes routed models as stable, reversible aliases:
the native `/model` picker. A row without a `description` reads "From gateway"; opencodex sends one
for every Claude Code CLI row (`Routed by OpenCodex to <provider>/<model>`; native rows use `Routed by OpenCodex to native <model>`, Fast rows append ` · Fast`, and 1M rows keep the base description), which Claude Code
2.1.257+ shows in its place. Because the picker only accepts ids beginning with `claude` or
`anthropic`, opencodex exposes routed models as stable, reversible aliases:

| Surface | Format | Example |
| --- | --- | --- |
Expand Down
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/ja/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ Claude Code CLI 互換性は英語版ドキュメントを参照してくださ
## /model ピッカー("From gateway")

Claude Code 2.1.129 以降は `GET /v1/models?limit=1000` でゲートウェイモデルを探し、デフォルトの `/model`
ピッカーの "From gateway" 項目に表示します。ピッカーは `claude` または `anthropic` で始まる ID のみ
ピッカーに表示します。`description` のない行は "From gateway" と表示されます。opencodex は Claude Code CLI
向けの各行に `description`(`Routed by OpenCodex to <provider>/<model>`、ネイティブ行は `Routed by OpenCodex to native <model>`、Fast 行は末尾に ` · Fast`、1M 行は元の説明のまま)を送り、Claude Code 2.1.257 以降は
その内容を代わりに表示します。ピッカーは `claude` または `anthropic` で始まる ID のみ
受け付けるため、opencodex はルーティングモデルを安定で元に戻せるエイリアスとして公開します。

| 画面 | 形式 | 例 |
Expand Down
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/ko/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ import/export는 로컬 설정만 다뤄요. 허브 프로필을 바꾸지 않
지정하거나 `/model`에 라우팅 id를 직접 입력하세요 (Claude Code는 문자열을 그대로 통과시킵니다).

Claude Code 2.1.129 이상은 `GET /v1/models?limit=1000`에서 게이트웨이 모델을 찾아 기본 `/model`
선택기의 "From gateway" 항목에 표시해요. 선택기는 `claude` 또는 `anthropic`으로 시작하는 ID만
선택기에 표시해요. `description`이 없는 항목은 "From gateway"로 보이는데, opencodex는 Claude Code CLI용
항목마다 `description`(`Routed by OpenCodex to <provider>/<model>`, 네이티브 항목은 `Routed by OpenCodex to native <model>`, Fast 항목은 끝에 ` · Fast`, 1M 항목은 기본 설명 그대로)을 보내고 Claude Code 2.1.257 이상은
그 내용을 대신 보여줘요. 선택기는 `claude` 또는 `anthropic`으로 시작하는 ID만
받으므로, opencodex는 라우팅 모델을 안정적이고 되돌릴 수 있는 별칭으로 노출해요.

| 화면 | 형식 | 예시 |
Expand Down
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/ru/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ Claude Desktop: работающий процесс может хранить п
## Селектор /model («From gateway»)

Claude Code 2.1.129+ обнаруживает модели шлюза через `GET /v1/models?limit=1000` и показывает их
в нативном селекторе `/model` в разделе «From gateway». Поскольку селектор принимает только id,
в нативном селекторе `/model`. Строка без `description` подписана «From gateway»; opencodex отправляет
её для каждой строки Claude Code CLI (`Routed by OpenCodex to <provider>/<model>`; для нативных строк — `Routed by OpenCodex to native <model>`, строки Fast добавляют ` · Fast`, строки 1M сохраняют базовое описание), и Claude Code 2.1.257+
показывает этот текст вместо подписи. Поскольку селектор принимает только id,
начинающиеся с `claude` или `anthropic`, opencodex публикует маршрутизируемые модели как
стабильные обратимые алиасы:

Expand Down
6 changes: 4 additions & 2 deletions docs-site/src/content/docs/tr/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,10 @@ gerekirse anahtarı hub'da ayrıca iptal edin.
## /model seçici ("From gateway")

Claude Code 2.1.129+, `GET /v1/models?limit=1000` aracılığıyla ağ geçidi
modellerini keşfeder ve bunları yerel `/model` seçicisinde "From gateway"
etiketiyle listeler. Seçici yalnızca `claude` veya `anthropic` ile başlayan
modellerini keşfeder ve bunları yerel `/model` seçicisinde listeler. `description`
alanı olmayan bir satır "From gateway" olarak görünür; opencodex her Claude Code CLI
satırı için bir tane gönderir (`Routed by OpenCodex to <provider>/<model>`; yerel satırlarda `Routed by OpenCodex to native <model>`, Fast satırları sona ` · Fast` ekler, 1M satırları temel açıklamayı korur) ve Claude Code
2.1.257+ onun yerine bunu gösterir. Seçici yalnızca `claude` veya `anthropic` ile başlayan
kimlikleri kabul ettiğinden, opencodex yönlendirilen modelleri kararlı, tersine
çevrilebilir takma adlar olarak sunar:

Expand Down
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/zh-cn/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ UI。真实 Anthropic 模型保留其原始 id。合成的 2026 日期是内部
中输入任意路由 id(Claude Code 会原样传递字符串)。

Claude Code 2.1.129+ 通过 `GET /v1/models?limit=1000` 发现网关模型,并在原生 `/model`
选择器中以“From gateway”标签列出。由于选择器只接受以 `claude` 或 `anthropic` 开头的 ID,
选择器中列出。没有 `description` 的行显示为“From gateway”;opencodex 会为 Claude Code CLI 的每一行发送
`description`(`Routed by OpenCodex to <provider>/<model>`;原生行为 `Routed by OpenCodex to native <model>`,Fast 行末尾加 ` · Fast`,1M 行沿用基础描述),Claude Code 2.1.257+ 会改为显示它。
由于选择器只接受以 `claude` 或 `anthropic` 开头的 ID,
opencodex 会将已路由模型公开为稳定且可逆的别名:

| 界面 | 格式 | 示例 |
Expand Down
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/zh-tw/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ apply、輪換/復原或直接 disconnect 均可處理,無須新參數或事
## /model 選擇器(“From gateway”)

Claude Code 2.1.129+ 透過 `GET /v1/models?limit=1000` 發現閘道器模型,並在原生 `/model`
選擇器中以“From gateway”標籤列出。由於選擇器只接受以 `claude` 或 `anthropic` 開頭的 ID,
選擇器中列出。沒有 `description` 的列會顯示為“From gateway”;opencodex 會為 Claude Code CLI 的每一列送出
`description`(`Routed by OpenCodex to <provider>/<model>`;原生列為 `Routed by OpenCodex to native <model>`,Fast 列末尾加上 ` · Fast`,1M 列沿用基礎描述),Claude Code 2.1.257+ 會改為顯示它。
由於選擇器只接受以 `claude` 或 `anthropic` 開頭的 ID,
opencodex 會將已路由模型公開為穩定且可逆的別名:

| 介面 | 格式 | 示例 |
Expand Down
11 changes: 9 additions & 2 deletions src/claude/gateway-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
* subscription-preserving launch deliberately sets no token, so the CLI can never
* refresh its picker list itself — it reads whatever cache exists. We therefore
* pre-write the cache in the exact on-disk schema the CLI uses:
* { baseUrl, fetchedAt, models: [{ id, display_name? }] } (mode 0600)
* { baseUrl, fetchedAt, models: [{ id, display_name?, description? }] } (mode 0600)
* mirroring its `/^(claude|anthropic)/i` usable-id filter. The picker validates
* only `baseUrl === ANTHROPIC_BASE_URL`, so a foreign base URL is simply ignored.
* `description` replaces the picker's generic "From gateway" line (Claude Code >= 2.1.257).
*/
import { mkdirSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
Expand All @@ -19,6 +20,7 @@ import type { OcxConfig } from "../types";
export interface GatewayModelRow {
id: string;
display_name?: string;
description?: string;
}

export interface GatewayModelCacheRefreshOptions {
Expand Down Expand Up @@ -56,7 +58,11 @@ export function writeGatewayModelCache(baseUrl: string, models: readonly Gateway
const payload = {
baseUrl,
fetchedAt: Date.now(),
models: usable.map(m => (m.display_name === undefined ? { id: m.id } : { id: m.id, display_name: m.display_name })),
models: usable.map(m => ({
id: m.id,
...(m.display_name === undefined ? {} : { display_name: m.display_name }),
...(m.description === undefined ? {} : { description: m.description }),
})),
};
writeFileSync(path, JSON.stringify(payload), { encoding: "utf8", mode: 0o600 });
return path;
Expand Down Expand Up @@ -110,6 +116,7 @@ export async function refreshGatewayModelCacheFromProxy(
.map(m => ({
id: m.id as string,
display_name: typeof m.display_name === "string" ? m.display_name : undefined,
description: typeof m.description === "string" ? m.description : undefined,
}));
return writeGatewayModelCache(baseUrl, models, options.configDir);
} catch {
Expand Down
22 changes: 18 additions & 4 deletions src/claude/model-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,14 @@ export interface AnthropicModelInfo {
capabilities: ReturnType<typeof modelCapabilities>;
max_input_tokens: number | null;
max_tokens: null;
/**
* Claude Code (>= 2.1.257) shows this under the picker row instead of the generic
* "From gateway". Readable (CLI) rows only; Desktop 3P rows keep the ModelInfo shape.
*/
description?: string;
}

function modelInfo(id: string, displayName: string, ladder: readonly string[], imageInput: boolean, contextWindow?: number): AnthropicModelInfo {
function modelInfo(id: string, displayName: string, ladder: readonly string[], imageInput: boolean, contextWindow?: number, description?: string): AnthropicModelInfo {
return {
id,
display_name: displayName,
Expand All @@ -91,6 +96,7 @@ function modelInfo(id: string, displayName: string, ladder: readonly string[], i
capabilities: modelCapabilities(ladder, imageInput),
max_input_tokens: typeof contextWindow === "number" && contextWindow > 0 ? contextWindow : null,
max_tokens: null,
...(description === undefined ? {} : { description }),
};
}

Expand Down Expand Up @@ -182,7 +188,13 @@ export function buildAnthropicModelInfos(
// A real model always wins its own id, whatever the iteration order.
if (realDiscoveryIds.has(id) || seen.has(id)) return;
seen.add(id);
out.push({ ...base, id, display_name: `${base.display_name} · Fast` });
out.push({
...base,
id,
display_name: `${base.display_name} · Fast`,
// Fast picks a different tier/variant, so the picker line says so like the name does.
...(base.description === undefined ? {} : { description: `${base.description} · Fast` }),
});
};
for (const slug of nativeSlugs) {
const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
Expand All @@ -192,7 +204,8 @@ export function buildAnthropicModelInfos(
const nativeMaxInput = nativeOpenAiMaxInputTokens(slug, nativeContextCap);
// max_input_tokens is an INPUT limit, so it follows the measured input ceiling rather
// than the total window whenever the model publishes one.
const info = modelInfo(id, `${slug} (native)`, nativeEffectiveLadder(slug), true, nativeMaxInput ?? nativeWindow);
const description = idStyle === "readable" ? `Routed by OpenCodex to native ${slug}` : undefined;
const info = modelInfo(id, `${slug} (native)`, nativeEffectiveLadder(slug), true, nativeMaxInput ?? nativeWindow, description);
out.push(info);
push1mVariant(info, nativeWindow, nativeMaxInput);
// Natives too, not only routed rows: gpt-5.6-sol is the flagship Fast model, and
Expand Down Expand Up @@ -225,7 +238,8 @@ export function buildAnthropicModelInfos(
? Math.min(m.maxInputTokens, m.contextWindow)
: m.maxInputTokens)
: undefined;
const info = modelInfo(id, `${listedModelId} (${m.provider})`, ladder, imageInput, routedMaxInput ?? m.contextWindow);
const description = idStyle === "readable" ? `Routed by OpenCodex to ${m.provider}/${listedModelId}` : undefined;
const info = modelInfo(id, `${listedModelId} (${m.provider})`, ladder, imageInput, routedMaxInput ?? m.contextWindow, description);
out.push(info);
// Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude
// routes — only a genuine >=1M window earns the variant row there.
Expand Down
27 changes: 27 additions & 0 deletions tests/claude-integration/claude-gateway-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,30 @@ describe("Claude Code gateway-model cache pre-write (devlog 260712 030)", () =>
}
});
});

describe("gateway-model cache carries the picker description", () => {
test("writer keeps description so the picker stops reading \"From gateway\"", () => {
const dir = tempDir();
const path = writeGatewayModelCache("http://127.0.0.1:10100", [
{ id: "claude-ocx-xai--grok-4.7", display_name: "grok-4.7 (xai)", description: "Routed by OpenCodex to xai/grok-4.7" },
{ id: "claude-ocx-native--gpt-5.5", display_name: "gpt-5.5 (native)" },
], dir);
expect(JSON.parse(readFileSync(path!, "utf8")).models).toEqual([
{ id: "claude-ocx-xai--grok-4.7", display_name: "grok-4.7 (xai)", description: "Routed by OpenCodex to xai/grok-4.7" },
{ id: "claude-ocx-native--gpt-5.5", display_name: "gpt-5.5 (native)" },
]);
});

test("proxy refresh copies description from /v1/models and ignores non-strings", async () => {
const dir = tempDir();
const fetchImpl = (async () => new Response(JSON.stringify({ data: [
{ id: "claude-ocx-xai--grok-4.7", display_name: "grok-4.7 (xai)", description: "Routed by OpenCodex to xai/grok-4.7" },
{ id: "claude-ocx-p--odd", display_name: "odd (p)", description: 42 },
] }), { headers: { "content-type": "application/json" } })) as unknown as typeof fetch;
const path = await refreshGatewayModelCacheFromProxy(10100, { timeoutMs: 1000, configDir: dir, env: {}, fetchImpl });
expect(JSON.parse(readFileSync(path!, "utf8")).models).toEqual([
{ id: "claude-ocx-xai--grok-4.7", display_name: "grok-4.7 (xai)", description: "Routed by OpenCodex to xai/grok-4.7" },
{ id: "claude-ocx-p--odd", display_name: "odd (p)" },
]);
});
});
30 changes: 30 additions & 0 deletions tests/claude-integration/claude-model-info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,33 @@ describe("saved picker order changes groups after identity selection", () => {
expect(result.map(row => [row.id, row.display_name])).toEqual([["collision", "a (p)"]]);
});
});

describe("Claude Code picker description (replaces the generic \"From gateway\" line)", () => {
test("readable rows describe the route OpenCodex serves them through", () => {
const infos = buildAnthropicModelInfos(["gpt-5.5"], [
{ provider: "xai", id: "grok-4.7", contextWindow: 500_000 },
], undefined, "readable");
const native = infos.find(i => i.display_name === "gpt-5.5 (native)");
const routed = infos.find(i => i.display_name === "grok-4.7 (xai)");
expect(native?.description).toBe("Routed by OpenCodex to native gpt-5.5");
expect(routed?.description).toBe("Routed by OpenCodex to xai/grok-4.7");
});

// A 1M row is the same route with a larger window; a Fast row selects a different tier or
// variant, so its description says so the way its display name does.
test("1M siblings keep the base description and Fast siblings name the Fast tier", () => {
const infos = buildAnthropicModelInfos([], [
{ provider: "p", id: "big", contextWindow: 1_000_000 },
], undefined, "readable", undefined, undefined, false, () => true);
expect(infos.map(i => [i.display_name, i.description])).toEqual([
["big (p)", "Routed by OpenCodex to p/big"],
["big (p) · 1M", "Routed by OpenCodex to p/big"],
["big (p) · Fast", "Routed by OpenCodex to p/big · Fast"],
]);
});

test("Desktop 3P rows stay unchanged (no description field)", () => {
const infos = buildAnthropicModelInfos(["gpt-5.5"], [{ provider: "xai", id: "grok-4.7" }], undefined, "desktop3p");
for (const info of infos) expect("description" in info).toBe(false);
});
});
Loading