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
6 changes: 3 additions & 3 deletions docs-site/src/content/docs/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ slots via
Plain model ids (no `/` or `~`) keep the v1 prefix `claude-ocx-…`. Model ids that contain `/` or
`~` mint the v2 prefix `claude-ocx2-…` with escapes (`/` → `~s`, `~` → `~t`), e.g.
`openrouter/anthropic/claude-opus-4-8` → `claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`.
v1 aliases decode literally (so a historical model id that contained the two-char sequences
`~s` / `~t` is preserved); v2 aliases expand the escapes. Routes that the readable form cannot
express fall back to the hashed alias. Model ids MAY contain `--` (resolution splits on the first
Both v1 and v2 aliases expand these escapes: v1 decoding remains supported for selections
persisted by releases that minted escaped aliases before the v2 prefix was introduced. Routes that
the readable form cannot express fall back to the hashed alias. Model ids MAY contain `--` (resolution splits on the first
`--` only); native slugs containing `--` fall back to the hashed form.

**Model resolution order:** `[1m]` marker stripped → readable alias decoded → Desktop hashed
Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/content/docs/ja/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ Claude Desktop のフッターピッカーで実行中の 3P 会話のモデル
`/` も `~` も含まない plain な model ID は v1 接頭辞 `claude-ocx-…` のままです。`/` または `~` を含む
model ID は v2 接頭辞 `claude-ocx2-…` で発行し、エスケープします(`/` → `~s`、`~` → `~t`)。例:
`openrouter/anthropic/claude-opus-4-8` → `claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`。
v1 エイリアスはリテラルにデコードします(歴史的に model ID に含まれていた 2 文字列 `~s` / `~t` も保持)
v2 エイリアスはエスケープを展開します。読みやすい形式で表現できないルートはハッシュエイリアスに
v1 と v2 の両方でこれらのエスケープを展開します。v2 接頭辞の導入前に発行され、設定に保存された v1 エイリアスとの互換性も維持します
読みやすい形式で表現できないルートはハッシュエイリアスに
置き換えます。モデル ID には `--` を含め**られます**(解析時は最初の `--` だけを基準に分割します)。
`--` を含むネイティブスラッグはハッシュ形式に置き換えます。

Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/content/docs/ko/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ Claude Desktop의 하단 선택기로 이미 실행 중인 3P 대화의 모델
없는 plain model ID는 v1 접두사 `claude-ocx-…`를 유지해요. `/` 또는 `~`가 있는 model ID는 v2
접두사 `claude-ocx2-…`로 만들고 이스케이프해요(`/` → `~s`, `~` → `~t`). 예:
`openrouter/anthropic/claude-opus-4-8` → `claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`.
v1 별칭은 리터럴로 디코딩해요(예전 model ID에 들어 있던 두 글자 시퀀스 `~s` / `~t`도 그대로 보존).
v2 별칭은 이스케이프를 펼쳐요. 읽기 쉬운 형식으로 표현할 수 없는 라우트는 해시 별칭으로 대체해요.
v1과 v2 별칭 모두 이 이스케이프를 확장해요. v2 접두사 도입 전에 발급되어 설정에 저장된 v1 별칭도 계속 호환돼요.
읽기 쉬운 형식으로 표현할 수 없는 라우트는 해시 별칭으로 대체해요.
모델 ID에는 `--`를 넣을 **수 있어요**(해석할 때 첫 번째 `--`만 기준으로 나눠요). `--`가 포함된
네이티브 슬러그는 해시 형식으로 대체해요.

Expand Down
5 changes: 2 additions & 3 deletions docs-site/src/content/docs/ru/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@ user-agent `claude-code/*` получает читаемую CLI-форму, а
`native`. Обычные id моделей (без `/` и `~`) остаются с префиксом v1 `claude-ocx-…`. Id с `/`
или `~` выпускаются с префиксом v2 `claude-ocx2-…` и экранированием (`/` → `~s`, `~` → `~t`),
например `openrouter/anthropic/claude-opus-4-8` →
`claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`. Алиасы v1 декодируются литерально (исторические
двухсимвольные последовательности `~s` / `~t` в id модели сохраняются); алиасы v2 раскрывают
экранирование. Маршруты, которые невозможно выразить читаемой формой, откатываются на
`claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`. Алиасы v1 и v2 раскрывают это экранирование; поддержка
декодирования v1 сохраняет совместимость с настройками, записанными версиями до появления префикса v2. Маршруты, которые невозможно выразить читаемой формой, откатываются на
хешированный алиас. Id моделей МОГУТ содержать `--` (при разрешении разбиение выполняется только
по первому `--`); нативные слаги с `--` откатываются на хешированную форму.

Expand Down
2 changes: 1 addition & 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 @@ -92,7 +92,7 @@ user-agent 会获得易读的 CLI 形式,其他客户端会获得 Desktop 哈
不含 `/` 或 `~` 的普通 model ID 继续使用 v1 前缀 `claude-ocx-…`。包含 `/` 或 `~` 的 model ID
会使用 v2 前缀 `claude-ocx2-…` 并转义(`/` → `~s`,`~` → `~t`),例如
`openrouter/anthropic/claude-opus-4-8` → `claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`。
v1 别名按字面解码(历史上 model ID 中包含的两字符序列 `~s` / `~t` 会被保留);v2 别名会展开转义
v1 和 v2 别名都会展开这些转义;保留 v1 解码是为了兼容在引入 v2 前已写入设置的别名
易读形式无法表达的路由会回退到哈希别名。模型 ID **可以**包含 `--`(解析时只按第一个 `--` 分割);
含 `--` 的原生 slug 会回退到哈希形式。

Expand Down
14 changes: 8 additions & 6 deletions src/claude/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
* persist to Claude Code's settings.json `model` field).
*
* Versioned prefixes:
* - `claude-ocx-` (v1) — legacy / plain model ids with no `/` or `~`. Decode
* is literal (no escape expansion), so a persisted model id that literally
* contained the two-char sequences `~s` / `~t` keeps resolving.
* - `claude-ocx-` (v1) — legacy / plain model ids. Decode still expands the
* historical `~s` / `~t` escapes so aliases minted by older releases remain
* stable.
* - `claude-ocx2-` (v2) — used whenever the model id needs escape encoding
* (`/` → `~s`, `~` → `~t`). Decode expands those escapes. New slash/tilde
* models always mint v2 so they cannot collide with v1 literals.
Expand Down Expand Up @@ -121,9 +121,11 @@ export function resolveAlias(id: string): string | null {
if (id.startsWith(CLAUDE_ALIAS_PREFIX_V1)) {
const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V1);
if (!parts) return null;
// Literal decode — preserves pre-escape aliases whose model id contained
// the two-char sequences ~s / ~t.
return parts.provider === NATIVE_PSEUDO_PROVIDER ? parts.model : `${parts.provider}/${parts.model}`;
// Older releases minted escaped model ids under v1. Keep expanding those
// escapes so persisted picker selections continue to resolve identically.
const model = decodeEscapedModelId(parts.model);
Comment thread
luvs01 marked this conversation as resolved.
if (!model) return null;
return parts.provider === NATIVE_PSEUDO_PROVIDER ? model : `${parts.provider}/${model}`;
}
return null;
}
Expand Down
18 changes: 10 additions & 8 deletions tests/claude-alias.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,29 @@ describe("claude discovery aliases", () => {
);
});

test("literal '~' mints v2 (~t); v1 bare '~' and literal ~s/~t still resolve", () => {
test("literal '~' mints v2 while v1 keeps decoding historical escapes", () => {
expect(aliasForRoute("demo", "old~model")).toBe(`${CLAUDE_ALIAS_PREFIX_V2}demo--old~tmodel`);
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V2}demo--old~tmodel`)).toBe("demo/old~model");
// Pre-escape v1 aliases kept literal tildes in the model portion.
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--old~model`)).toBe("demo/old~model");
// v1 literal ~s / ~t are preserved (the versioned-prefix compatibility fix).
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--old~smodel`)).toBe("demo/old~smodel");
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--old~tmodel`)).toBe("demo/old~tmodel");
// Older releases minted escaped aliases under v1; persisted selections
// must keep resolving after new aliases move to v2.
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--old~smodel`)).toBe("demo/old/model");
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--old~tmodel`)).toBe("demo/old~model");
});

test("v2 reserved escapes round-trip / and ~ without colliding with v1 literals", () => {
test("v2 reserved escapes round-trip while v1 remains backward-compatible", () => {
expect(aliasForRoute("demo", "a/b")).toBe(`${CLAUDE_ALIAS_PREFIX_V2}demo--a~sb`);
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V2}demo--a~sb`)).toBe("demo/a/b");
expect(aliasForRoute("demo", "a~b")).toBe(`${CLAUDE_ALIAS_PREFIX_V2}demo--a~tb`);
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V2}demo--a~tb`)).toBe("demo/a~b");
expect(aliasForRoute("demo", "a~/b")).toBe(`${CLAUDE_ALIAS_PREFIX_V2}demo--a~t~sb`);
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V2}demo--a~t~sb`)).toBe("demo/a~/b");

// Same wire bytes under v1 stay literal — no silent remap to slash/tilde.
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--a~sb`)).toBe("demo/a~sb");
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--a~tb`)).toBe("demo/a~tb");
// The same wire bytes under v1 remain compatibility inputs for aliases
// minted before the v2 prefix was introduced.
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--a~sb`)).toBe("demo/a/b");
expect(resolveAlias(`${CLAUDE_ALIAS_PREFIX_V1}demo--a~tb`)).toBe("demo/a~b");
});

test("resolveAlias rejects non-aliases and malformed ids", () => {
Expand Down
Loading