diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index e2d179883..8087008e0 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -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 diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md index 546204648..cf9416035 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -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 には `--` を含め**られます**(解析時は最初の `--` だけを基準に分割します)。 `--` を含むネイティブスラッグはハッシュ形式に置き換えます。 diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md index 665c350b0..9a7bdcc5b 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -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에는 `--`를 넣을 **수 있어요**(해석할 때 첫 번째 `--`만 기준으로 나눠요). `--`가 포함된 네이티브 슬러그는 해시 형식으로 대체해요. diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index 7d3a65ce9..0e3b1c8ab 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -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 моделей МОГУТ содержать `--` (при разрешении разбиение выполняется только по первому `--`); нативные слаги с `--` откатываются на хешированную форму. diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index 31410c921..7205c95ff 100644 --- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md @@ -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 会回退到哈希形式。 diff --git a/src/claude/alias.ts b/src/claude/alias.ts index db7d74e07..a58d122c6 100644 --- a/src/claude/alias.ts +++ b/src/claude/alias.ts @@ -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. @@ -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); + if (!model) return null; + return parts.provider === NATIVE_PSEUDO_PROVIDER ? model : `${parts.provider}/${model}`; } return null; } diff --git a/tests/claude-alias.test.ts b/tests/claude-alias.test.ts index 213c0af28..a2eeb55de 100644 --- a/tests/claude-alias.test.ts +++ b/tests/claude-alias.test.ts @@ -68,17 +68,18 @@ 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`); @@ -86,9 +87,10 @@ describe("claude discovery aliases", () => { 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", () => {