Skip to content
Draft
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 41 additions & 5 deletions docs-site/src/content/docs/guides/routing-profile-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,34 @@ The **Models → Routing** tab in the OpenCodex dashboard can manage `config.rou

Profile ids are immutable after creation. To use a different id, create a new profile and remove the old one after updating callers.

## Route every prompt automatically

Enable **Automatic prompt routing** when one picker entry should choose a different candidate for
each request. Assign every candidate one or more task tiers:

- **Fast** for greetings, short rewrites, translations, and other lightweight requests.
- **Balanced** for ordinary questions and general work.
- **Powerful** for implementation, debugging, architecture, security, multi-step work, and other
complex requests.

All three tiers must be covered before the profile can be saved. A candidate may serve more than one
tier, so the same model can be used for both balanced and powerful requests.

After saving, select the profile's alias (for example `ocx/auto`) in Codex once. OpenCodex
reclassifies the latest user prompt on **every request**, so a simple follow-up can use the fast
candidate and the next complex coding request can use the powerful candidate without changing the
picker again.

Classification is local and deterministic: it does not make a separate model request and does not
persist the raw prompt as routing metadata. Capability requirements, health, quota, cost limits, and
the profile's normal scoring rules still apply after the task-tier filter. The selected provider,
model, tier reason, and exclusions are available in the route-decision trace and
`ocx logs explain`; OpenCodex does not modify the assistant response with a routing footer.

Prompt routing is provider-agnostic. Any model already configured in OpenCodex can be assigned to a
tier, including DeepSeek, Qwen, Kimi, GLM, and other OpenAI-compatible or native providers. The
provider and model ids in the example below are illustrative.

## Validation and persistence

The dashboard sends the same profile object used by `config.routingProfiles` to the management API. The server validates the complete candidate before writing it:
Expand Down Expand Up @@ -55,15 +83,23 @@ Example save payload:

```json
{
"id": "fast",
"id": "smart",
"mode": "create",
"profile": {
"alias": "ocx/fast",
"alias": "ocx/auto",
"promptRouting": { "enabled": true },
"candidates": [
{ "provider": "anthropic", "model": "claude-sonnet-5" },
{ "provider": "openai", "model": "gpt-5.6" }
{
"provider": "deepseek",
"model": "deepseek-chat",
"taskTiers": ["fast", "balanced"]
},
{
"provider": "openai",
"model": "gpt-5.6",
"taskTiers": ["powerful"]
}
],
"require": { "tools": true, "minContextWindow": 128000 },
"optimize": { "latency": 0.55, "health": 0.25, "cost": 0.1, "quota": 0.1 },
"limits": { "maxEstimatedCostUsd": 0.5, "onUnknownCost": "allow" },
"unknownEvidence": {
Expand Down
43 changes: 36 additions & 7 deletions docs-site/src/content/docs/reference/configuration/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ namespace, or reserved bare native families (`gpt-*`, `o1-*`, `o3-*`, `o4-*`, `c

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `candidates` | `{ provider: string; model: string }[]` | required | Explicit allowlist of `provider/model` refs. No implicit expansion. |
| `candidates` | `{ provider: string; model: string; taskTiers?: ("fast" \| "balanced" \| "powerful")[] }[]` | required | Explicit allowlist of `provider/model` refs. `taskTiers` declares which prompt-complexity tiers a candidate may serve when automatic prompt routing is enabled. No implicit expansion. |
| `alias?` | `string` | — | Optional public model id in place of `policy/<id>`. |
| `promptRouting?` | `{ enabled?: boolean }` | disabled | Locally classify the latest user prompt on every request and restrict eligibility to candidates assigned to the resulting task tier. |
| `require?` | object | `{}` | Hard capability requirements evaluated before scoring (see below). |
| `optimize?` | object | latency 0.55, health 0.25, cost 0.10, quota 0.10 | Scoring weights, normalized deterministically. `health`, `quota`, and `cost` have score dimensions; the configured-priority share is `1 - health - quota - cost` (default 0.55), and `latency` folds into that priority share rather than scoring independently. |
| `limits?` | object | — | Hard limits. `maxEstimatedCostUsd` excludes a candidate when its estimated cost is known and above the cap. When that cap is set, `onUnknownCost` (`"allow"` default, or `"exclude"`) controls unknown estimates: allow prevents a cap-specific exclusion and records `cost.capOutcome: "unknown-allowed"`; exclude emits `cost-limit-unknown` and `capOutcome: "unknown-excluded"`. `onUnknownCost` alone (no cap) is inert. Separate from `unknownEvidence.cost`, which can still exclude or penalize unknown prices via `unknown-price` / scoring. |
Expand All @@ -134,16 +135,44 @@ evidence surface for context-sensitive profiles.
The CLI dry-run accepts request-evidence flags but cannot supply candidate capability evidence yet;
candidate evidence is provided through the API (`POST /api/routing-profiles/dry-run`).

### Automatic prompt routing

With `promptRouting.enabled: true`, OpenCodex classifies the latest user prompt as `fast`,
`balanced`, or `powerful` for every request. The classifier is deterministic and local: it makes no
extra model call and adds only the resulting tier to routing evidence, not the raw prompt. Requests
without usable text, such as image-only input, default to `balanced`. The requested reasoning effort
can also raise or lower the tier.

Every candidate must declare a non-empty `taskTiers` array, and the profile must cover all three
tiers. Tier matching is an eligibility filter; the existing capability, health, quota, cost-limit,
unknown-evidence, and scoring rules still decide among candidates eligible for that tier.

Select the profile alias once in Codex. Each later request through that alias is classified again,
so consecutive turns may use different providers or models. The selected trace reason is
`prompt-tier-fast`, `prompt-tier-balanced`, or `prompt-tier-powerful`, visible through the
route-decision API and `ocx logs explain`.

The tier filter is provider-agnostic: any configured provider/model can participate. DeepSeek, Qwen,
Kimi, GLM, and the ids in the example are illustrative rather than a hardcoded model catalog.

```json
{
"routingProfiles": {
"fast": {
"alias": "ocx/fast",
"smart": {
"alias": "ocx/auto",
"promptRouting": { "enabled": true },
"candidates": [
{ "provider": "anthropic", "model": "claude-sonnet-5" },
{ "provider": "openai", "model": "gpt-5.6-sol" }
{
"provider": "deepseek",
"model": "deepseek-chat",
"taskTiers": ["fast", "balanced"]
},
{
"provider": "openai",
"model": "gpt-5.6-sol",
"taskTiers": ["powerful"]
}
],
"require": { "tools": true, "minContextWindow": 128000 },
"optimize": { "latency": 0.55, "health": 0.25, "cost": 0.10, "quota": 0.10 },
"limits": { "maxEstimatedCostUsd": 0.50, "onUnknownCost": "allow" },
"unknownEvidence": {
Expand All @@ -158,7 +187,7 @@ candidate evidence is provided through the API (`POST /api/routing-profiles/dry-
```

CLI: `ocx route policy list [--json]`, `ocx route policy show <id> [--json]`, and
`ocx route policy dry-run <id> [--model-context <tokens>] [--tools] [--image] [--structured-output] [--json]`.
`ocx route policy dry-run <id> [--task-tier <fast|balanced|powerful>] [--model-context <tokens>] [--tools] [--image] [--structured-output] [--json]`.
Dry-run evaluates candidates without sending any upstream request.

Quota evidence (`optimize.quota`, `require.minQuotaHeadroom`, `unknownEvidence.quota`) comes from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,13 @@ Codex Auth 页面将此 picker 行为作为选择加入项。关闭它会隐藏

## 路由策略配置文件(`config.routingProfiles`)

显式请求的 `policy/<id>`(或配置的别名)会在固定的候选白名单中,根据硬性能力要求与确定性、可解释的评分进行选择。现有模型 ID 永远不会隐式经过配置文件。支持 `candidates`(显式白名单)、可选 `alias`、`require`(`minContextWindow`、`minQuotaHeadroom`、`tools`、`imageInput`、`structuredOutput`、`localOnly`、`remoteAllowed`、`encryptedCodexTasks`、`reasoningEffort`、`serviceTier`)、`optimize`(latency/health/cost/quota 权重)、`limits.maxEstimatedCostUsd`、`unknownEvidence`(allow/penalize/exclude)。未知不会被当作零或免费。
显式请求的 `policy/<id>`(或配置的别名)会在固定的候选白名单中,根据硬性能力要求与确定性、可解释的评分进行选择。现有模型 ID 永远不会隐式经过配置文件。支持 `candidates`(显式白名单)、可选 `alias`、`promptRouting`、`require`(`minContextWindow`、`minQuotaHeadroom`、`tools`、`imageInput`、`structuredOutput`、`localOnly`、`remoteAllowed`、`encryptedCodexTasks`、`reasoningEffort`、`serviceTier`)、`optimize`(latency/health/cost/quota 权重)、`limits.maxEstimatedCostUsd`、`unknownEvidence`(allow/penalize/exclude)。未知不会被当作零或免费。

CLI:`ocx route policy list`、`ocx route policy show <id>`、`ocx route policy dry-run <id> --model-context <tokens> --tools`、`ocx route policy evaluate <id>`。
设置 `"promptRouting": { "enabled": true }` 后,只需在 Codex 中选择一次该配置文件的别名(例如 `ocx/auto`)。OpenCodex 会在每次请求时重新分析最新一条用户提示词,把任务确定性地分为 `fast`、`balanced` 或 `powerful`,再从声明了对应 `taskTiers` 的候选中选择模型。所有三个等级都必须有候选覆盖。该分类在本地完成,不会额外调用模型,也不会把原始提示词写入路由元数据;能力、健康度、额度、成本限制和正常评分仍会继续生效。最终模型与 `prompt-tier-*` 原因可通过路由决策记录和 `ocx logs explain` 查看。

任务等级筛选与提供商无关:任何已经在 OpenCodex 中配置好的模型都能参与,包括 DeepSeek、通义千问、Kimi、智谱 GLM 等国内模型;它们不是写死在路由器里的模型清单。

CLI:`ocx route policy list`、`ocx route policy show <id>`、`ocx route policy dry-run <id> --task-tier powerful --model-context <tokens> --tools`、`ocx route policy evaluate <id>`。

组合是显式的有序/加权目标路由与故障转移;策略配置文件是基于证据在候选之间进行选择。

Expand Down
8 changes: 8 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export const de: Record<TKey, string> = {
"routing.unknownEvidence.exclude": "ausschließen",
"routing.removeCandidate": "Kandidat {provider}/{model} entfernen",
"routing.candidates": "Kandidaten",
"routing.promptRouting": "Automatisches Prompt-Routing",
"routing.promptRoutingHelp": "Klassifiziert den neuesten Benutzer-Prompt lokal und wählt für jede Anfrage eine geeignete Aufgabenstufe.",
"routing.taskTiers": "Aufgabenstufen",
"routing.taskTier.fast": "Schnell",
"routing.taskTier.balanced": "Ausgewogen",
"routing.taskTier.powerful": "Leistungsstark",
"routing.require": "Harte Anforderungen",
"routing.optimize": "Optimierungsgewichte",
"routing.limits": "Grenzen",
Expand All @@ -39,6 +45,7 @@ export const de: Record<TKey, string> = {
"routing.unavailable": "–",
"routing.dryRun": "Trockenlauf-Bewertung",
"routing.dryRunContext": "Kontextfenster der Anfrage (Tokens)",
"routing.dryRunTaskTier": "Aufgabenstufe der Anfrage",
"routing.dryRunTools": "Anfrage benötigt Tools",
"routing.dryRunImage": "Anfrage benötigt Bild-Eingabe",
"routing.dryRunStructured": "Anfrage benötigt strukturierte Ausgabe",
Expand All @@ -52,6 +59,7 @@ export const de: Record<TKey, string> = {
"routing.capOutcome.unknown-allowed": "unbekannt (erlaubt)",
"routing.capOutcome.unknown-excluded": "unbekannt (ausgeschlossen)",
"routing.exclusion.capability-unsatisfied": "Anforderung nicht erfüllt",
"routing.exclusion.task-tier-mismatch": "Nicht dieser Aufgabenstufe zugewiesen",
"routing.exclusion.unknown-capability": "unbekannte Fähigkeit",
"routing.exclusion.cost-limit": "über Kostenobergrenze",
"routing.exclusion.cost-limit-unknown": "unbekannte Kosten unter Obergrenze",
Expand Down
8 changes: 8 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export const en = {
"routing.unknownEvidence.exclude": "exclude",
"routing.removeCandidate": "Remove candidate {provider}/{model}",
"routing.candidates": "Candidates",
"routing.promptRouting": "Automatic prompt routing",
"routing.promptRoutingHelp": "Classify each latest user prompt locally and select an eligible task tier for every request.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope the prompt-routing help text to enabled profiles.

Both locale strings imply that classification runs for every request. The runtime only generates task-tier evidence when prompt routing is enabled for the selected profile.

  • gui/src/i18n/en.ts#L78-L78: change the wording to state that classification applies to requests using this profile.
  • gui/src/i18n/ja.ts#L35-L35: add the equivalent Japanese condition.
📍 Affects 2 files
  • gui/src/i18n/en.ts#L78-L78 (this comment)
  • gui/src/i18n/ja.ts#L35-L35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/en.ts` at line 78, Update the routing.promptRoutingHelp locale
strings to clarify that prompt classification applies only to requests using the
selected profile with prompt routing enabled. Change the English entry in
gui/src/i18n/en.ts at lines 78-78 and add the equivalent Japanese condition in
gui/src/i18n/ja.ts at lines 35-35.

"routing.taskTiers": "Task tiers",
"routing.taskTier.fast": "Fast",
"routing.taskTier.balanced": "Balanced",
"routing.taskTier.powerful": "Powerful",
"routing.require": "Hard requirements",
"routing.optimize": "Optimization weights",
"routing.limits": "Limits",
Expand All @@ -82,6 +88,7 @@ export const en = {
"routing.unavailable": "–",
"routing.dryRun": "Dry-run evaluation",
"routing.dryRunContext": "Request context window (tokens)",
"routing.dryRunTaskTier": "Request task tier",
"routing.dryRunTools": "Request requires tools",
"routing.dryRunImage": "Request requires image input",
"routing.dryRunStructured": "Request requires structured output",
Expand All @@ -95,6 +102,7 @@ export const en = {
"routing.capOutcome.unknown-allowed": "unknown (allowed)",
"routing.capOutcome.unknown-excluded": "unknown (excluded)",
"routing.exclusion.capability-unsatisfied": "capability not met",
"routing.exclusion.task-tier-mismatch": "not assigned to this task tier",
"routing.exclusion.unknown-capability": "unknown capability",
"routing.exclusion.cost-limit": "over cost cap",
"routing.exclusion.cost-limit-unknown": "unknown cost under cap",
Expand Down
8 changes: 8 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export const ja: Record<TKey, string> = {
"routing.unknownEvidence.exclude": "除外",
"routing.removeCandidate": "候補 {provider}/{model} を削除",
"routing.candidates": "候補",
"routing.promptRouting": "自動プロンプトルーティング",
"routing.promptRoutingHelp": "最新のユーザープロンプトをローカルで分類し、リクエストごとに適切なタスク階層を選択します。",
"routing.taskTiers": "タスク階層",
"routing.taskTier.fast": "高速",
"routing.taskTier.balanced": "バランス",
"routing.taskTier.powerful": "高性能",
"routing.require": "必須要件",
"routing.optimize": "最適化ウェイト",
"routing.limits": "制限",
Expand All @@ -39,6 +45,7 @@ export const ja: Record<TKey, string> = {
"routing.unavailable": "–",
"routing.dryRun": "ドライラン評価",
"routing.dryRunContext": "リクエストのコンテキストウィンドウ(トークン)",
"routing.dryRunTaskTier": "リクエストのタスク階層",
"routing.dryRunTools": "リクエストにツールが必要",
"routing.dryRunImage": "リクエストに画像入力が必要",
"routing.dryRunStructured": "リクエストに構造化出力が必要",
Expand All @@ -52,6 +59,7 @@ export const ja: Record<TKey, string> = {
"routing.capOutcome.unknown-allowed": "不明(許可)",
"routing.capOutcome.unknown-excluded": "不明(除外)",
"routing.exclusion.capability-unsatisfied": "能力要件未達",
"routing.exclusion.task-tier-mismatch": "このタスク階層に未割り当て",
"routing.exclusion.unknown-capability": "能力不明",
"routing.exclusion.cost-limit": "コスト上限超過",
"routing.exclusion.cost-limit-unknown": "上限下でコスト不明",
Expand Down
8 changes: 8 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export const ko: Record<TKey, string> = {
"routing.unknownEvidence.exclude": "제외",
"routing.removeCandidate": "후보 {provider}/{model} 제거",
"routing.candidates": "후보",
"routing.promptRouting": "자동 프롬프트 라우팅",
"routing.promptRoutingHelp": "최신 사용자 프롬프트를 로컬에서 분류하고 요청마다 적합한 작업 등급을 선택합니다.",
"routing.taskTiers": "작업 등급",
"routing.taskTier.fast": "빠름",
"routing.taskTier.balanced": "균형",
"routing.taskTier.powerful": "강력",
"routing.require": "필수 요구사항",
"routing.optimize": "최적화 가중치",
"routing.limits": "제한",
Expand All @@ -39,6 +45,7 @@ export const ko: Record<TKey, string> = {
"routing.unavailable": "–",
"routing.dryRun": "드라이런 평가",
"routing.dryRunContext": "요청 컨텍스트 창(토큰)",
"routing.dryRunTaskTier": "요청 작업 등급",
"routing.dryRunTools": "요청에 도구 필요",
"routing.dryRunImage": "요청에 이미지 입력 필요",
"routing.dryRunStructured": "요청에 구조화된 출력 필요",
Expand All @@ -52,6 +59,7 @@ export const ko: Record<TKey, string> = {
"routing.capOutcome.unknown-allowed": "알 수 없음(허용)",
"routing.capOutcome.unknown-excluded": "알 수 없음(제외)",
"routing.exclusion.capability-unsatisfied": "기능 미충족",
"routing.exclusion.task-tier-mismatch": "이 작업 등급에 할당되지 않음",
"routing.exclusion.unknown-capability": "알 수 없는 기능",
"routing.exclusion.cost-limit": "비용 상한 초과",
"routing.exclusion.cost-limit-unknown": "상한 이하 비용 불명",
Expand Down
8 changes: 8 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export const ru: Record<TKey, string> = {
"routing.unknownEvidence.exclude": "исключить",
"routing.removeCandidate": "Удалить кандидата {provider}/{model}",
"routing.candidates": "Кандидаты",
"routing.promptRouting": "Автоматическая маршрутизация промптов",
"routing.promptRoutingHelp": "Локально классифицирует последний промпт пользователя и выбирает подходящий уровень задачи для каждого запроса.",
"routing.taskTiers": "Уровни задач",
"routing.taskTier.fast": "Быстрый",
"routing.taskTier.balanced": "Сбалансированный",
"routing.taskTier.powerful": "Мощный",
"routing.require": "Жёсткие требования",
"routing.optimize": "Веса оптимизации",
"routing.limits": "Лимиты",
Expand All @@ -39,6 +45,7 @@ export const ru: Record<TKey, string> = {
"routing.unavailable": "–",
"routing.dryRun": "Пробная оценка",
"routing.dryRunContext": "Контекстное окно запроса (токены)",
"routing.dryRunTaskTier": "Уровень задачи запроса",
"routing.dryRunTools": "Запрос требует инструменты",
"routing.dryRunImage": "Запрос требует изображения",
"routing.dryRunStructured": "Запрос требует структурированный вывод",
Expand All @@ -52,6 +59,7 @@ export const ru: Record<TKey, string> = {
"routing.capOutcome.unknown-allowed": "неизвестно (разрешено)",
"routing.capOutcome.unknown-excluded": "неизвестно (исключено)",
"routing.exclusion.capability-unsatisfied": "требование не выполнено",
"routing.exclusion.task-tier-mismatch": "Не назначено этому уровню задачи",
"routing.exclusion.unknown-capability": "неизвестная возможность",
"routing.exclusion.cost-limit": "сверх лимита стоимости",
"routing.exclusion.cost-limit-unknown": "неизвестная стоимость при лимите",
Expand Down
Loading
Loading