Skip to content

Commit 47a7a91

Browse files
committed
fix(claude): version slash/tilde aliases under claude-ocx2-
1 parent 350a07b commit 47a7a91

7 files changed

Lines changed: 129 additions & 69 deletions

File tree

docs-site/src/content/docs/guides/claude-code.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ with `claude` or `anthropic`, opencodex exposes routed models as stable, reversi
175175

176176
| Surface | Format | Example |
177177
| --- | --- | --- |
178-
| Claude Code CLI | `claude-ocx-<provider>--<model>` | `claude-ocx-native--gpt-5.6-sol` |
178+
| Claude Code CLI | `claude-ocx-<provider>--<model>` (plain) or `claude-ocx2-…` (escaped) | `claude-ocx-native--gpt-5.6-sol` |
179179
| Claude Desktop 3P | `claude-opus-4-8-<code>` (3-char base36 hash) | `claude-opus-4-8-ncb` |
180180

181181
The proxy picks the family per request: `?ids=cli` or `?ids=desktop` wins; otherwise the
@@ -200,11 +200,13 @@ slots via
200200
`ANTHROPIC_MODEL` or type any routed id with `/model` (Claude Code passes strings through).
201201

202202
**Alias grammar rules:** provider must not contain `/` or `--` or equal `native`.
203-
Model ids may contain `/` — encoded as `~s` in the alias (e.g. `openrouter/anthropic/claude-opus-4-8`
204-
`claude-ocx-openrouter--anthropic~sclaude-opus-4-8`). Literal `~` in a model id is encoded as `~t`.
205-
Bare `~` not followed by `s`/`t` is treated as a literal tilde so older persisted aliases keep resolving.
206-
Routes the readable form cannot express fall back to the hashed alias. Model ids MAY contain `--`
207-
(resolution splits on the first `--` only); native slugs containing `--` fall back to the hashed form.
203+
Plain model ids (no `/` or `~`) keep the v1 prefix `claude-ocx-…`. Model ids that contain `/` or
204+
`~` mint the v2 prefix `claude-ocx2-…` with escapes (`/``~s`, `~``~t`), e.g.
205+
`openrouter/anthropic/claude-opus-4-8``claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`.
206+
v1 aliases decode literally (so a historical model id that contained the two-char sequences
207+
`~s` / `~t` is preserved); v2 aliases expand the escapes. Routes the readable form cannot express
208+
fall back to the hashed alias. Model ids MAY contain `--` (resolution splits on the first `--`
209+
only); native slugs containing `--` fall back to the hashed form.
208210

209211
**Model resolution order:** `[1m]` marker stripped → readable alias decoded → Desktop hashed
210212
alias decoded → `modelMap` exact match → date-stripped match (`-20250514` removed) → passthrough.

docs-site/src/content/docs/ja/guides/claude-code.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Claude Desktop のフッターピッカーで実行中の 3P 会話のモデル
8383

8484
**エイリアス構文ルール:** provider には `/``--` を含められず `native` と同じでもいけません。
8585
model ID には `/` を含められ、エイリアス内では `~s` として符号化します(例: `openrouter/anthropic/claude-opus-4-8`
86-
`claude-ocx-openrouter--anthropic~sclaude-opus-4-8`)。model ID のリテラル `~``~t` として符号化します。
86+
`claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`)。model ID のリテラル `~``~t` として符号化します。
8787
`s`/`t` が続かない裸の `~` はリテラルのチルダとして扱い、古い永続化エイリアスも解決し続けます。
8888
読みやすい形式で表現できないルートはハッシュエイリアスに置き換えます。モデル
8989
ID には `--` を含め**られます**(解析時は最初の `--` だけを基準に分割します)。`--` を含む

docs-site/src/content/docs/ko/guides/claude-code.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ Claude Desktop의 하단 선택기로 이미 실행 중인 3P 대화의 모델
118118

119119
**별칭 문법 규칙:** provider에는 `/``--`를 넣을 수 없고 `native`와 같아도 안 돼요. model ID에
120120
`/`가 있으면 별칭에서 `~s`로 인코딩해요(예: `openrouter/anthropic/claude-opus-4-8`
121-
`claude-ocx-openrouter--anthropic~sclaude-opus-4-8`). model ID의 리터럴 `~``~t`로 인코딩해요.
121+
`claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`). model ID의 리터럴 `~``~t`로 인코딩해요.
122122
`s`/`t`가 따르지 않는 단독 `~`는 예전 설정과의 호환을 위해 리터럴 `~`로 해석해요. 읽기 쉬운
123123
형식으로 표현할 수 없는 라우트는 해시 별칭으로 대체해요. 모델 ID에는 `--`를 넣을 **수 있어요**
124124
(해석할 때 첫 번째 `--`만 기준으로 나눠요). `--`가 포함된 네이티브 슬러그는 해시 형식으로

docs-site/src/content/docs/ru/guides/claude-code.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ user-agent `claude-code/*` получает читаемую CLI-форму, а
8888

8989
**Правила грамматики алиасов:** provider не может содержать `/` или `--` и не может быть равен
9090
`native`. Id моделей могут содержать `/` — в алиасе это кодируется как `~s` (например,
91-
`openrouter/anthropic/claude-opus-4-8``claude-ocx-openrouter--anthropic~sclaude-opus-4-8`).
91+
`openrouter/anthropic/claude-opus-4-8``claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`).
9292
Литеральный `~` в id модели кодируется как `~t`. Голый `~` без следующего `s`/`t`
9393
считается литеральной тильдой, чтобы старые сохранённые алиасы продолжали разрешаться.
9494
Маршруты, которые невозможно выразить читаемой формой,

docs-site/src/content/docs/zh-cn/guides/claude-code.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ user-agent 会获得易读的 CLI 形式,其他客户端会获得 Desktop 哈
9090

9191
**别名语法规则:**provider 不得包含 `/``--`,也不得等于 `native`
9292
model ID 可以包含 `/` — 在别名中编码为 `~s`(例如 `openrouter/anthropic/claude-opus-4-8`
93-
`claude-ocx-openrouter--anthropic~sclaude-opus-4-8`)。model ID 中的字面 `~` 编码为 `~t`
93+
`claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`)。model ID 中的字面 `~` 编码为 `~t`
9494
后面不是 `s`/`t` 的裸 `~` 视为字面波浪号,以便旧版已持久化的别名继续解析。
9595
易读形式无法表达的路由会回退到哈希别名。模型 ID **可以**包含 `--`(解析时只按第一个
9696
`--` 分割);含 `--` 的原生 slug 会回退到哈希形式。

src/claude/alias.ts

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,54 @@
77
* deterministic, reversible, and STABLE across releases (picker selections
88
* persist to Claude Code's settings.json `model` field).
99
*
10+
* Versioned prefixes:
11+
* - `claude-ocx-` (v1) — legacy / plain model ids with no `/` or `~`. Decode
12+
* is literal (no escape expansion), so a persisted model id that literally
13+
* contained the two-char sequences `~s` / `~t` keeps resolving.
14+
* - `claude-ocx2-` (v2) — used whenever the model id needs escape encoding
15+
* (`/` → `~s`, `~` → `~t`). Decode expands those escapes. New slash/tilde
16+
* models always mint v2 so they cannot collide with v1 literals.
17+
*
1018
* Reversibility rules:
1119
* - providers containing `--` or `/` are not aliased (split boundary safety);
12-
* - model ids MAY contain `/` — encoded as `~s` so the alias stays slash-free
13-
* for Claude Code's picker (e.g. openrouter `anthropic/claude-opus-4-8` →
14-
* `claude-ocx-openrouter--anthropic~sclaude-opus-4-8`);
15-
* - model ids MAY contain `~` — encoded as `~t` (so slash encoding cannot
16-
* collide with a literal tilde that older releases already persisted);
17-
* - `~s` / `~t` are reserved escape sequences: decode always treats them as
18-
* `/` and `~` respectively. Model ids that historically contained the literal
19-
* two-char sequences `~s` / `~t` are not preserved (extremely rare; encode would
20-
* write `~ts` / `~tt` for those characters after a literal tilde today);
21-
* - bare `~` not followed by `s`/`t` is left as a literal tilde on decode
22-
* (legacy aliases from before slash encoding);
20+
* - model ids MAY contain `/` or `~` — minted under the v2 prefix with escapes
21+
* (e.g. openrouter `anthropic/claude-opus-4-8` →
22+
* `claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`);
2323
* - model ids MAY contain `--` (resolve splits on the FIRST `--` only);
2424
* - native OpenAI slugs use the pseudo-provider `native` and resolve back to
2525
* the bare slug; a real provider named "native" is therefore never aliased.
2626
*/
2727

2828
import { desktop3pAlias } from "./desktop-3p";
2929

30-
export const CLAUDE_ALIAS_PREFIX = "claude-ocx-";
31-
/** Encoded `/` inside the model portion of a Claude Code alias. */
30+
/** Legacy / plain readable prefix (literal model portion on decode). */
31+
export const CLAUDE_ALIAS_PREFIX_V1 = "claude-ocx-";
32+
/** Escape-encoded readable prefix (`~s`/`~t` expanded on decode). */
33+
export const CLAUDE_ALIAS_PREFIX_V2 = "claude-ocx2-";
34+
/**
35+
* Current write prefix for plain (unescaped) model ids.
36+
* Escape-needing models mint {@link CLAUDE_ALIAS_PREFIX_V2} instead.
37+
*/
38+
export const CLAUDE_ALIAS_PREFIX = CLAUDE_ALIAS_PREFIX_V1;
39+
40+
/** Encoded `/` inside the model portion of a v2 Claude Code alias. */
3241
const CLAUDE_ALIAS_SLASH_ENC = "~s";
33-
/** Encoded literal `~` inside the model portion of a Claude Code alias. */
42+
/** Encoded literal `~` inside the model portion of a v2 Claude Code alias. */
3443
const CLAUDE_ALIAS_TILDE_ENC = "~t";
3544
const NATIVE_PSEUDO_PROVIDER = "native";
3645

46+
function modelNeedsEscapeEncoding(modelId: string): boolean {
47+
return modelId.includes("/") || modelId.includes("~");
48+
}
49+
3750
function encodeModelId(modelId: string): string {
3851
// Escape literal tildes first so slash encoding cannot create ambiguity.
3952
return modelId
4053
.replaceAll("~", CLAUDE_ALIAS_TILDE_ENC)
4154
.replaceAll("/", CLAUDE_ALIAS_SLASH_ENC);
4255
}
4356

44-
function decodeModelId(encoded: string): string {
57+
function decodeEscapedModelId(encoded: string): string {
4558
let out = "";
4659
for (let i = 0; i < encoded.length; i++) {
4760
if (encoded[i] === "~" && i + 1 < encoded.length) {
@@ -57,45 +70,68 @@ function decodeModelId(encoded: string): string {
5770
continue;
5871
}
5972
}
60-
// Bare `~` (legacy pre-slash-encoding aliases) stays a literal tilde.
6173
out += encoded[i];
6274
}
6375
return out;
6476
}
6577

78+
function splitAlias(id: string, prefix: string): { provider: string; model: string } | null {
79+
const rest = id.slice(prefix.length);
80+
const sep = rest.indexOf("--");
81+
if (sep <= 0) return null;
82+
const provider = rest.slice(0, sep);
83+
const model = rest.slice(sep + 2);
84+
if (!provider || !model) return null;
85+
return { provider, model };
86+
}
87+
6688
/** Alias for a routed "<provider>/<model>" pair; null when not representable. */
6789
export function aliasForRoute(provider: string, modelId: string): string | null {
6890
if (!provider || provider.includes("--") || provider.includes("/") || provider === NATIVE_PSEUDO_PROVIDER) return null;
6991
if (!modelId) return null;
70-
return `${CLAUDE_ALIAS_PREFIX}${provider}--${encodeModelId(modelId)}`;
92+
if (modelNeedsEscapeEncoding(modelId)) {
93+
return `${CLAUDE_ALIAS_PREFIX_V2}${provider}--${encodeModelId(modelId)}`;
94+
}
95+
return `${CLAUDE_ALIAS_PREFIX_V1}${provider}--${modelId}`;
7196
}
7297

7398
/** Alias for a native OpenAI slug (bare model id, no provider namespace). */
7499
export function aliasForNative(slug: string): string | null {
75-
// Reject "/" — native ids are bare slugs. Literal `~` is fine via ~t encoding.
100+
// Reject "/" — native ids are bare slugs. Literal `~` is fine via v2 + ~t.
76101
if (!slug || slug.includes("/") || slug.includes("--")) return null;
77-
return `${CLAUDE_ALIAS_PREFIX}${NATIVE_PSEUDO_PROVIDER}--${encodeModelId(slug)}`;
102+
if (modelNeedsEscapeEncoding(slug)) {
103+
return `${CLAUDE_ALIAS_PREFIX_V2}${NATIVE_PSEUDO_PROVIDER}--${encodeModelId(slug)}`;
104+
}
105+
return `${CLAUDE_ALIAS_PREFIX_V1}${NATIVE_PSEUDO_PROVIDER}--${slug}`;
78106
}
79107

80108
/**
81109
* Reverse an alias to the inbound model string routeModel understands:
82110
* routed -> "<provider>/<model>", native -> bare slug. Null when not an alias.
83111
*/
84112
export function resolveAlias(id: string): string | null {
85-
if (!id.startsWith(CLAUDE_ALIAS_PREFIX)) return null;
86-
const rest = id.slice(CLAUDE_ALIAS_PREFIX.length);
87-
const sep = rest.indexOf("--");
88-
if (sep <= 0) return null;
89-
const provider = rest.slice(0, sep);
90-
const model = decodeModelId(rest.slice(sep + 2));
91-
if (!model) return null;
92-
return provider === NATIVE_PSEUDO_PROVIDER ? model : `${provider}/${model}`;
113+
// Check v2 before v1 for clarity (prefixes are disjoint: ocx2 vs ocx-).
114+
if (id.startsWith(CLAUDE_ALIAS_PREFIX_V2)) {
115+
const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V2);
116+
if (!parts) return null;
117+
const model = decodeEscapedModelId(parts.model);
118+
if (!model) return null;
119+
return parts.provider === NATIVE_PSEUDO_PROVIDER ? model : `${parts.provider}/${model}`;
120+
}
121+
if (id.startsWith(CLAUDE_ALIAS_PREFIX_V1)) {
122+
const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V1);
123+
if (!parts) return null;
124+
// Literal decode — preserves pre-escape aliases whose model id contained
125+
// the two-char sequences ~s / ~t.
126+
return parts.provider === NATIVE_PSEUDO_PROVIDER ? parts.model : `${parts.provider}/${parts.model}`;
127+
}
128+
return null;
93129
}
94130

95131
/**
96132
* Claude Code (CLI) surface alias — devlog 050 + audit 051 #2.
97133
*
98-
* The readable `claude-ocx-*` form when representable; otherwise the desktop-3p
134+
* The readable `claude-ocx*` form when representable; otherwise the desktop-3p
99135
* hash so the model still appears in discovery (collisions follow the same
100136
* first-wins policy as the desktop registry — audit 051 #1). Real Anthropic
101137
* models pass through unchanged (they must keep hitting the sk-ant passthrough).

0 commit comments

Comments
 (0)