99 *
1010 * Reversibility rules:
1111 * - providers containing `--` or `/` are not aliased (split boundary safety);
12- * - model ids containing `/` are not aliased (would be ambiguous on resolve);
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);
1323 * - model ids MAY contain `--` (resolve splits on the FIRST `--` only);
1424 * - native OpenAI slugs use the pseudo-provider `native` and resolve back to
1525 * the bare slug; a real provider named "native" is therefore never aliased.
1828import { desktop3pAlias } from "./desktop-3p" ;
1929
2030export const CLAUDE_ALIAS_PREFIX = "claude-ocx-" ;
31+ /** Encoded `/` inside the model portion of a Claude Code alias. */
32+ const CLAUDE_ALIAS_SLASH_ENC = "~s" ;
33+ /** Encoded literal `~` inside the model portion of a Claude Code alias. */
34+ const CLAUDE_ALIAS_TILDE_ENC = "~t" ;
2135const NATIVE_PSEUDO_PROVIDER = "native" ;
2236
37+ function encodeModelId ( modelId : string ) : string {
38+ // Escape literal tildes first so slash encoding cannot create ambiguity.
39+ return modelId
40+ . replaceAll ( "~" , CLAUDE_ALIAS_TILDE_ENC )
41+ . replaceAll ( "/" , CLAUDE_ALIAS_SLASH_ENC ) ;
42+ }
43+
44+ function decodeModelId ( encoded : string ) : string {
45+ let out = "" ;
46+ for ( let i = 0 ; i < encoded . length ; i ++ ) {
47+ if ( encoded [ i ] === "~" && i + 1 < encoded . length ) {
48+ const next = encoded [ i + 1 ] ;
49+ if ( next === "s" ) {
50+ out += "/" ;
51+ i += 1 ;
52+ continue ;
53+ }
54+ if ( next === "t" ) {
55+ out += "~" ;
56+ i += 1 ;
57+ continue ;
58+ }
59+ }
60+ // Bare `~` (legacy pre-slash-encoding aliases) stays a literal tilde.
61+ out += encoded [ i ] ;
62+ }
63+ return out ;
64+ }
65+
2366/** Alias for a routed "<provider>/<model>" pair; null when not representable. */
2467export function aliasForRoute ( provider : string , modelId : string ) : string | null {
2568 if ( ! provider || provider . includes ( "--" ) || provider . includes ( "/" ) || provider === NATIVE_PSEUDO_PROVIDER ) return null ;
26- if ( ! modelId || modelId . includes ( "/" ) ) return null ;
27- return `${ CLAUDE_ALIAS_PREFIX } ${ provider } --${ modelId } ` ;
69+ if ( ! modelId ) return null ;
70+ return `${ CLAUDE_ALIAS_PREFIX } ${ provider } --${ encodeModelId ( modelId ) } ` ;
2871}
2972
3073/** Alias for a native OpenAI slug (bare model id, no provider namespace). */
3174export function aliasForNative ( slug : string ) : string | null {
75+ // Reject "/" — native ids are bare slugs. Literal `~` is fine via ~t encoding.
3276 if ( ! slug || slug . includes ( "/" ) || slug . includes ( "--" ) ) return null ;
33- return `${ CLAUDE_ALIAS_PREFIX } ${ NATIVE_PSEUDO_PROVIDER } --${ slug } ` ;
77+ return `${ CLAUDE_ALIAS_PREFIX } ${ NATIVE_PSEUDO_PROVIDER } --${ encodeModelId ( slug ) } ` ;
3478}
3579
3680/**
@@ -43,7 +87,7 @@ export function resolveAlias(id: string): string | null {
4387 const sep = rest . indexOf ( "--" ) ;
4488 if ( sep <= 0 ) return null ;
4589 const provider = rest . slice ( 0 , sep ) ;
46- const model = rest . slice ( sep + 2 ) ;
90+ const model = decodeModelId ( rest . slice ( sep + 2 ) ) ;
4791 if ( ! model ) return null ;
4892 return provider === NATIVE_PSEUDO_PROVIDER ? model : `${ provider } /${ model } ` ;
4993}
0 commit comments