Skip to content
Merged
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
9 changes: 6 additions & 3 deletions docs-site/src/content/docs/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,12 @@ the alias back to the routed model. On older Claude Code versions the picker sta
slots via
`ANTHROPIC_MODEL` or type any routed id with `/model` (Claude Code passes strings through).

**Alias grammar rules:** provider must not contain `/` or `--` or equal `native`; model must not
contain `/`. Routes 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.
**Alias grammar rules:** provider must not contain `/` or `--` or equal `native`.
Model ids may contain `/` — encoded as `~s` in the alias (e.g. `openrouter/anthropic/claude-opus-4-8`
→ `claude-ocx-openrouter--anthropic~sclaude-opus-4-8`). Literal `~` in a model id is encoded as `~t`.
Bare `~` not followed by `s`/`t` is treated as a literal tilde so older persisted aliases keep resolving.
Routes 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
alias decoded → `modelMap` exact match → date-stripped match (`-20250514` removed) → passthrough.
Expand Down
7 changes: 5 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 @@ -81,8 +81,11 @@ Claude Desktop のフッターピッカーで実行中の 3P 会話のモデル
`/model <id>` を使用してください。OpenCodex はピッカーの状態を直接参照できず、各リクエストに
含まれるモデル ID をルーティングします。結果は **Logs → requestedModel** で確認できます。

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

Expand Down
11 changes: 7 additions & 4 deletions docs-site/src/content/docs/ko/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,13 @@ Claude Desktop의 하단 선택기로 이미 실행 중인 3P 대화의 모델
`/model <id>`를 사용하세요. OpenCodex는 선택기 상태를 따로 볼 수 없고 각 요청에 실린 모델 ID를
라우팅해요. 적용 결과는 **Logs → requestedModel**에서 확인할 수 있어요.

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

**모델 해석 순서:** `[1m]` 표식 제거 → 읽기 쉬운 별칭 디코딩 → Desktop 해시 별칭 디코딩 →
`modelMap` 정확히 일치 → 날짜를 제거한 값과 일치(`-20250514` 제거) → 패스스루 순서예요.
Expand Down
10 changes: 7 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 @@ -87,9 +87,13 @@ user-agent `claude-code/*` получает читаемую CLI-форму, а
маршрутизирует id модели из каждого запроса. Результат можно проверить в **Logs → requestedModel**.

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

**Порядок разрешения модели:** удаление маркера `[1m]` → декодирование читаемого алиаса →
декодирование Desktop-хеша → точное совпадение в `modelMap` → совпадение без даты (удаляется
Expand Down
9 changes: 6 additions & 3 deletions docs-site/src/content/docs/zh-cn/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,12 @@ user-agent 会获得易读的 CLI 形式,其他客户端会获得 Desktop 哈
`/model <id>`。OpenCodex 无法读取选择器状态,只会路由每个请求实际携带的模型 ID;可在
**Logs → requestedModel** 中确认结果。

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

**模型解析顺序:**移除 `[1m]` 标记 → 解码易读别名 → 解码 Desktop 哈希别名 →
`modelMap` 精确匹配 → 移除日期后的匹配(移除 `-20250514`)→ 透传。
Expand Down
54 changes: 49 additions & 5 deletions src/claude/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,17 @@
*
* Reversibility rules:
* - providers containing `--` or `/` are not aliased (split boundary safety);
* - model ids containing `/` are not aliased (would be ambiguous on resolve);
* - model ids MAY contain `/` — encoded as `~s` so the alias stays slash-free
* for Claude Code's picker (e.g. openrouter `anthropic/claude-opus-4-8` →
* `claude-ocx-openrouter--anthropic~sclaude-opus-4-8`);
* - model ids MAY contain `~` — encoded as `~t` (so slash encoding cannot
* collide with a literal tilde that older releases already persisted);
* - `~s` / `~t` are reserved escape sequences: decode always treats them as
* `/` and `~` respectively. Model ids that historically contained the literal
* two-char sequences `~s` / `~t` are not preserved (extremely rare; encode would
* write `~ts` / `~tt` for those characters after a literal tilde today);
* - bare `~` not followed by `s`/`t` is left as a literal tilde on decode
* (legacy aliases from before slash encoding);
* - model ids MAY contain `--` (resolve splits on the FIRST `--` only);
* - native OpenAI slugs use the pseudo-provider `native` and resolve back to
* the bare slug; a real provider named "native" is therefore never aliased.
Expand All @@ -18,19 +28,53 @@
import { desktop3pAlias } from "./desktop-3p";

export const CLAUDE_ALIAS_PREFIX = "claude-ocx-";
/** Encoded `/` inside the model portion of a Claude Code alias. */
const CLAUDE_ALIAS_SLASH_ENC = "~s";
/** Encoded literal `~` inside the model portion of a Claude Code alias. */
const CLAUDE_ALIAS_TILDE_ENC = "~t";
const NATIVE_PSEUDO_PROVIDER = "native";

function encodeModelId(modelId: string): string {
// Escape literal tildes first so slash encoding cannot create ambiguity.
return modelId
.replaceAll("~", CLAUDE_ALIAS_TILDE_ENC)
.replaceAll("/", CLAUDE_ALIAS_SLASH_ENC);
}

function decodeModelId(encoded: string): string {
let out = "";
for (let i = 0; i < encoded.length; i++) {
if (encoded[i] === "~" && i + 1 < encoded.length) {
const next = encoded[i + 1];
if (next === "s") {
out += "/";
i += 1;
continue;
}
if (next === "t") {
out += "~";
i += 1;
continue;
}
}
// Bare `~` (legacy pre-slash-encoding aliases) stays a literal tilde.
out += encoded[i];
}
return out;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/** Alias for a routed "<provider>/<model>" pair; null when not representable. */
export function aliasForRoute(provider: string, modelId: string): string | null {
if (!provider || provider.includes("--") || provider.includes("/") || provider === NATIVE_PSEUDO_PROVIDER) return null;
if (!modelId || modelId.includes("/")) return null;
return `${CLAUDE_ALIAS_PREFIX}${provider}--${modelId}`;
if (!modelId) return null;
return `${CLAUDE_ALIAS_PREFIX}${provider}--${encodeModelId(modelId)}`;
}

/** Alias for a native OpenAI slug (bare model id, no provider namespace). */
export function aliasForNative(slug: string): string | null {
// Reject "/" — native ids are bare slugs. Literal `~` is fine via ~t encoding.
if (!slug || slug.includes("/") || slug.includes("--")) return null;
return `${CLAUDE_ALIAS_PREFIX}${NATIVE_PSEUDO_PROVIDER}--${slug}`;
return `${CLAUDE_ALIAS_PREFIX}${NATIVE_PSEUDO_PROVIDER}--${encodeModelId(slug)}`;
}

/**
Expand All @@ -43,7 +87,7 @@ export function resolveAlias(id: string): string | null {
const sep = rest.indexOf("--");
if (sep <= 0) return null;
const provider = rest.slice(0, sep);
const model = rest.slice(sep + 2);
const model = decodeModelId(rest.slice(sep + 2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve persisted aliases containing tildes

Existing releases allowed routed model IDs containing ~, so an alias such as claude-ocx-demo--old~model may already be persisted in Claude Code settings and previously resolved to demo/old~model. Unconditionally decoding every tilde now changes that saved selection to demo/old/model, potentially routing requests to a different or nonexistent model. Use a versioned or unambiguous encoding while retaining legacy tilde decoding semantics.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

if (!model) return null;
return provider === NATIVE_PSEUDO_PROVIDER ? model : `${provider}/${model}`;
}
Expand Down
24 changes: 24 additions & 0 deletions src/cli/catalog-prewarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { OcxConfig } from "../types";

type GatherRoutedModels = (config: OcxConfig) => Promise<unknown>;

export type CatalogPrewarmDeps = {
loadConfig?: () => OcxConfig;
importCatalog?: () => Promise<{ gatherRoutedModels: GatherRoutedModels }>;
};

/**
* After the listen port is bound, kick off live provider discovery so the first
* GUI /v1/models and syncModelsToCodex share one gather flight instead of racing
* duplicate upstream /models fetches.
*/
export function scheduleCatalogPrewarm(deps: CatalogPrewarmDeps = {}): void {
void Promise.resolve()
.then(async () => {
const load = deps.loadConfig ?? (await import("../config")).loadConfig;
const { gatherRoutedModels } = await (deps.importCatalog?.() ?? import("../codex/catalog"));
return gatherRoutedModels(load());
})
.catch(() => {});
}

5 changes: 5 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { startTokenGuardian } from "../oauth/token-guardian";
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
import { maybeShowStarPrompt } from "./star-prompt";
import { scheduleCatalogPrewarm } from "./catalog-prewarm";
import { maybeShowUpdatePrompt } from "../update/notify";
import { syncModelsToCodex } from "../codex/sync";
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
Expand Down Expand Up @@ -190,6 +191,10 @@ async function handleStart(options: { block?: boolean } = {}) {
for (let attempt = 0; ; attempt++) {
try {
server = startServer(port);
// Prewarm the live provider model cache as soon as the port is bound so the
// first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
// instead of racing duplicate upstream /models fetches.
scheduleCatalogPrewarm();
break;
} catch (err) {
if (!isAddrInUse(err) || attempt >= 2) throw err;
Expand Down
2 changes: 1 addition & 1 deletion src/codex/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs } from "./catalog/metadata";
export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch";
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch";
export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmissions, resetOpenAiApiCatalogWarningStateForTests, uniqueCatalogModelsForPublicList, uniqueCatalogModelsForRawPublicList, buildComboCatalogOmission, comboCatalogOmissionReason, summarizeComboCatalogOmissions } from "./catalog/aggregation";
export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, effectiveSubagentRoster, buildCatalogEntries, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache } from "./catalog/sync";
Expand Down
Loading
Loading