cursor-route 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  _(none)_
6
6
 
7
+ ## 0.1.15 — 2026-09-11
8
+
9
+ - Mid default is `deepseek-flash` (DeepSeek V4.1 Flash, native vision). `--model pro` and `--model vision` are legacy aliases to the same id. Pro is off rotation.
10
+ - `--model flash|pro|vision` and legacy full ids (`deepseek-flash`, `deepseek-v4-flash`, `deepseek-v4.1-flash`, `deepseek-v4-pro`, `deepseek-v4-pro[1m]`, `deepseek-v4-flash-vision`, `deepseek-v4-flash-vision-exp`) all launch `deepseek-flash`. `job.model` still records the alias the operator passed.
11
+ - Mid lane still `claude-ds`; no worker swap. Hero demo fixture regenerated to **0.1.15**.
12
+
13
+ ## 0.1.14 — 2026-09-02
14
+
15
+ - Easy-lane OpenRouter live pick now **tier-ranks** free models like the toolkit: Qwen 100, GLM/Kimi 95, DeepSeek 90, coding/chat families 70, **Nemotron 15**, default 40. Score adds a context bonus capped at 131072 (`tier + min(ctx, 131072)/131072 × 5`) so a huge Nemotron 550B free model can no longer outrank Qwen/GLM/Kimi on context alone.
16
+ - Tests: all seven tier buckets; equal-score tier/id tiebreakers; same-tier 131072-vs-262144 cap; ranked-pick cache round-trip.
17
+ - Mid lane still `claude-ds`; no worker swap. Hero demo fixture regenerated to **0.1.14**.
18
+
7
19
  ## 0.1.13 — 2026-08-29
8
20
 
9
21
  - OpenRouter live-pick: do **not** cache the fetch-fail fallback (`openrouter/free`) — health no longer presents a stale fallback as `now openrouter/free`. Health says `cached <id>` only for a ranked catalog hit; otherwise `no catalog cache, fetch-fail fallback openrouter/free`.
package/README.md CHANGED
@@ -116,17 +116,17 @@ docs/fixtures/generate-hero-demo.sh # regenerate docs/fixtures/hero-demo.lo
116
116
 
117
117
  ```text
118
118
  $ cursor-route --version
119
- 0.1.13
119
+ 0.1.15
120
120
 
121
121
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
122
- cursor-route v0.1.13
122
+ cursor-route v0.1.15
123
123
  health: OK
124
124
 
125
125
  $ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
126
126
  dry-run job a1b2c3d4
127
127
  worker: claude-ds
128
128
  model: flash
129
- command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/a1b2c3d4.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions
129
+ command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/a1b2c3d4.prompt' -Model 'deepseek-flash' --dangerously-skip-permissions
130
130
 
131
131
  $ cursor-route jobs --json
132
132
  []
@@ -145,26 +145,27 @@ A real hero GIF is still pending — recording steps live in [docs/DEMO_GIF.md](
145
145
 
146
146
  Always-approve is **on** by default for **coding worktrees only**. It does not authorize LIVE Discord, trading, or irreversible SaaS. Opt out: `--ask` or `CURSOR_ROUTE_ASK=1`.
147
147
 
148
- ### Mid models (Flash vs Pro)
148
+ ### Mid models (Flash)
149
149
 
150
150
  | Flag | Model id | When |
151
151
  |------|----------|------|
152
- | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
153
- | `--model vision` | `deepseek-v4-flash-vision-exp` | Screenshots / ui mocks / image prompts (or auto-pick) |
154
- | `--model pro` | `deepseek-v4-pro` | Harder mid / **hard backup** only not the default Grok-out stand-in |
155
- | `--model deepseek-v4-pro[1m]` | `deepseek-v4-pro[1m]` | Large-context Pro (SKU preserved) |
152
+ | `--model flash` (default) | `deepseek-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
153
+ | `--model vision` | `deepseek-flash` | Screenshots / ui mocks / image prompts (or auto-pick). V4.1 Flash has native vision |
154
+ | `--model pro` | `deepseek-flash` | Legacy aliasPro is off rotation |
155
+
156
+ This map matches DeepSeek's 2026-09-10 API id `deepseek-flash`. `--model pro` and `--model vision` are legacy aliases to the same id (including `deepseek-v4-pro[1m]`).
156
157
 
157
158
  ```bash
158
159
  cursor-route start --lane mid "…" # Flash
159
- cursor-route start --lane mid --model vision "…" # Vision Flash
160
- cursor-route start --lane mid --model pro "…" # Pro (harder mid / hard backup)
160
+ cursor-route start --lane mid --model vision "…" # legacy alias → deepseek-flash
161
+ cursor-route start --lane mid --model pro "…" # legacy alias deepseek-flash
161
162
  # Or set default without a flag:
162
163
  export CURSOR_ROUTE_DS_MODEL=flash # also honors ANTHROPIC_MODEL; --model overrides
163
164
  ```
164
165
 
165
- If `--model` and `CURSOR_ROUTE_DS_MODEL` / `ANTHROPIC_MODEL` are unset, a prompt that looks like a screenshot/image/png/jpg/jpeg/webp/ui mock/multimodal/vision auto-picks vision Flash. Explicit `--model flash|pro|vision` always wins.
166
+ If `--model` and `CURSOR_ROUTE_DS_MODEL` / `ANTHROPIC_MODEL` are unset, a prompt that looks like a screenshot/image/png/jpg/jpeg/webp/ui mock/multimodal/vision auto-picks the vision alias (`job.model` is `vision`; launch id is still `deepseek-flash`). Explicit `--model flash|pro|vision` always wins.
166
167
 
167
- **Grok auth ≠ usage-out:** if `cursor-route health` shows `worker:grok` ✗, run `grok login` (or set `XAI_API_KEY`). That is auth, not the Pro case. When Grok **usage** is exhausted, stay on `--lane mid --model flash` (cheap default). `--model pro` is harder mid / hard backup only.
168
+ **Grok auth ≠ usage-out:** if `cursor-route health` shows `worker:grok` ✗, run `grok login` (or set `XAI_API_KEY`). That is auth, not a model switch. When Grok **usage** is exhausted, stay on `--lane mid --model flash` (cheap default). `--model pro` is a legacy alias to `deepseek-flash`.
168
169
 
169
170
  `CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is an expensive escape hatch: it does **not** pass DeepSeek `--model` ids (Anthropic would reject them).
170
171
 
@@ -180,18 +181,18 @@ npm i -g @anthropic-ai/claude-code
180
181
  export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
181
182
  export ANTHROPIC_AUTH_TOKEN=YOUR_DEEPSEEK_API_KEY # from platform.deepseek.com
182
183
  # Optional shell defaults (CLI --model overrides ANTHROPIC_MODEL for the job):
183
- export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash
184
- export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash
184
+ export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-flash
185
+ export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-flash
185
186
 
186
187
  cursor-route health # worker:claude-ds should be ✓; `lane:mid` is ✓ only when DeepSeek is proven
187
188
  cursor-route start --lane mid "…" # Flash (default; also when Grok usage is out)
188
- cursor-route start --lane mid --model vision "…" # screenshots / ui mocks
189
- cursor-route start --lane mid --model pro "…" # harder mid / hard backup only
189
+ cursor-route start --lane mid --model vision "…" # screenshots / ui mocks (same launch id)
190
+ cursor-route start --lane mid --model pro "…" # legacy alias deepseek-flash
190
191
  ```
191
192
 
192
193
  Persist the same vars under `~/.claude/settings.json` → `"env": { … }` if you want them every shell.
193
194
 
194
- **Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims). The adapter passes `-Model deepseek-v4-flash|deepseek-v4-pro`.
195
+ **Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims). The adapter passes `-Model deepseek-flash`.
195
196
 
196
197
  **Experimental:** `--worker deepseek` runs the official DeepSeek Harness (`dsh`, npm `@deepseek-ai/dsh`) as an opt-in worker — **mid stays on `claude-ds`**; don't default lanes here.
197
198
 
@@ -201,10 +202,10 @@ export DEEPSEEK_API_KEY=YOUR_DEEPSEEK_API_KEY # from platform.deepseek.com
201
202
 
202
203
  cursor-route health # worker:deepseek should be ✓
203
204
  cursor-route start --worker deepseek "…" # Flash (default)
204
- cursor-route start --worker deepseek --model pro "…" # harder mid / hard backup only
205
+ cursor-route start --worker deepseek --model pro "…" # legacy alias deepseek-flash
205
206
  ```
206
207
 
207
- The adapter launches `dsh --profile headless` with a **per-job Cordis patch** (`jobs/<id>.dsh-patch.yml`, mode 0600) that pins `--model flash|pro|vision` (`deepseek-v4-pro[1m]` preserved) — it never rewrites `~/.dsh/settings.yaml`, so parallel jobs don't race. Always-approve maps to `DSH_PERMISSION_MODE=danger-full-access`; `--ask` drops to `workspace-write`. Your `DEEPSEEK_API_KEY` travels via env only — never in the launch command or patch. Override the binary with `CURSOR_ROUTE_DSH_BIN`.
208
+ The adapter launches `dsh --profile headless` with a **per-job Cordis patch** (`jobs/<id>.dsh-patch.yml`, mode 0600) that pins `--model flash|pro|vision` to `deepseek-flash` — it never rewrites `~/.dsh/settings.yaml`, so parallel jobs don't race. Always-approve maps to `DSH_PERMISSION_MODE=danger-full-access`; `--ask` drops to `workspace-write`. Your `DEEPSEEK_API_KEY` travels via env only — never in the launch command or patch. Override the binary with `CURSOR_ROUTE_DSH_BIN`.
208
209
 
209
210
  **Not the default:** bare `claude` still talking to Anthropic. Health refuses that so a misconfigured install cannot silently burn frontier $ rates. Escape hatch only: `CURSOR_ROUTE_ALLOW_ANTHROPIC=1`.
210
211
 
@@ -214,7 +215,9 @@ No DeepSeek yet? Use `--lane hard` / `--worker grok` (X Premium).
214
215
 
215
216
  `--lane easy` / `--worker openrouter` sends wording/draft prompts to OpenRouter
216
217
  and **live-picks the best free text model** at request time (`GET /models`, rank
217
- `:free` or $0 text models). Do not hardcode a specific model id as the default.
218
+ `:free` or $0 text models). The live rank prefers Qwen/GLM/Kimi coding free
219
+ models over huge general free models (e.g. Nemotron 550B). Do not hardcode a
220
+ specific model id as the default.
218
221
  Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Empty /
219
222
  `free` = live pick. If the catalog fetch fails, the fallback is OpenRouter's
220
223
  **router** id `openrouter/free` (a live router, not a locked model). Get a key
@@ -128,8 +128,16 @@ export function midDeepSeekProofDetail() {
128
128
  }
129
129
  function pickModel(requested, modelId) {
130
130
  if (modelId) {
131
- const alias = requested ?? resolveDsModel(modelId).alias;
132
- return { alias, id: modelId };
131
+ try {
132
+ const resolved = resolveDsModel(modelId);
133
+ return { alias: requested ?? resolved.alias, id: resolved.id };
134
+ }
135
+ catch (e) {
136
+ // Unknown id with an explicit alias: keep the raw id so dsh YAML checks can still reject it.
137
+ if (requested)
138
+ return { alias: requested, id: modelId };
139
+ throw e;
140
+ }
133
141
  }
134
142
  if (requested) {
135
143
  return { alias: requested, id: DS_MODEL_IDS[requested] };
@@ -32,8 +32,16 @@ function findDsh() {
32
32
  /** Same resolution as claude-ds: passed model/modelId wins, else env, else Flash. */
33
33
  function pickModel(requested, modelId) {
34
34
  if (modelId) {
35
- const alias = requested ?? resolveDsModel(modelId).alias;
36
- return { alias, id: modelId };
35
+ try {
36
+ const resolved = resolveDsModel(modelId);
37
+ return { alias: requested ?? resolved.alias, id: resolved.id };
38
+ }
39
+ catch (e) {
40
+ // Unknown id with an explicit alias: keep the raw id so YAML checks can still reject it.
41
+ if (requested)
42
+ return { alias: requested, id: modelId };
43
+ throw e;
44
+ }
37
45
  }
38
46
  if (requested) {
39
47
  return { alias: requested, id: DS_MODEL_IDS[requested] };
package/dist/cli.js CHANGED
@@ -31,7 +31,7 @@ Usage:
31
31
  Start options:
32
32
  --worker <grok|claude-ds|openrouter|deepseek|opencode> Worker adapter (default: grok; deepseek/opencode = opt-in)
33
33
  --lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
34
- --model <flash|pro|vision|free|provider/model> claude-ds/deepseek: flash|pro|vision. opencode/openrouter: free (live catalog pick) or provider/model
34
+ --model <flash|pro|vision|free|provider/model> claude-ds/deepseek: flash|pro|vision (aliases map to deepseek-flash). opencode/openrouter: free (live catalog pick) or provider/model
35
35
  --dir <path> Working directory (default: cwd)
36
36
  --ask Disable always-approve for this job
37
37
  --dry-run Print launch command; do not start
@@ -44,7 +44,7 @@ Env:
44
44
  CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
45
45
  CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
46
46
  CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default; --model ignored)
47
- CURSOR_ROUTE_DS_MODEL Default mid model flash|pro|vision (or full ids / deepseek-v4-pro[1m]); overridden by --model
47
+ CURSOR_ROUTE_DS_MODEL Default mid model flash|pro|vision (aliases map to deepseek-flash); overridden by --model
48
48
  CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
49
49
  CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
50
50
  CURSOR_ROUTE_DSH_BIN Override the dsh binary path (tests / power users)
package/dist/config.js CHANGED
@@ -4,10 +4,12 @@ import { defaultJobsDir } from "./runtime.js";
4
4
  export const WORKERS = ["grok", "claude-ds", "openrouter", "deepseek", "opencode"];
5
5
  export const LANES = ["easy", "mid", "hard"];
6
6
  export const DS_MODELS = ["flash", "pro", "vision"];
7
+ /** DeepSeek V4.1 Flash (native vision). Matches the 2026-09-10 API id. */
8
+ export const DS_FLASH_ID = "deepseek-flash";
7
9
  export const DS_MODEL_IDS = {
8
- flash: "deepseek-v4-flash",
9
- pro: "deepseek-v4-pro",
10
- vision: "deepseek-v4-flash-vision-exp",
10
+ flash: DS_FLASH_ID,
11
+ pro: DS_FLASH_ID,
12
+ vision: DS_FLASH_ID,
11
13
  };
12
14
  /** Prompt looks like a screenshot / image / ui mock — vision auto-pick. */
13
15
  const VISION_PROMPT_RE = /screenshot|\bimage\b|\.png|\.jpe?g|\.webp|ui mock|multimodal|\bvision\b/i;
@@ -15,26 +17,31 @@ export function promptLooksLikeVision(prompt) {
15
17
  return VISION_PROMPT_RE.test(prompt);
16
18
  }
17
19
  /**
18
- * Resolve --model / env to alias + concrete model id.
19
- * Preserves `deepseek-v4-pro[1m]` (does not silently strip the SKU).
20
+ * Resolve --model / env to alias + launch id.
21
+ * V4.1 Flash is native vision; Pro is a legacy alias, not a rotation step.
22
+ * All known aliases launch `deepseek-flash`.
20
23
  * Empty → Flash.
21
24
  */
22
25
  export function resolveDsModel(raw) {
23
26
  if (!raw || !raw.trim()) {
24
- return { alias: "flash", id: DS_MODEL_IDS.flash };
27
+ return { alias: "flash", id: DS_FLASH_ID };
25
28
  }
26
- const v = raw.trim().toLowerCase();
27
- if (v === "flash" || v === "deepseek-v4-flash") {
28
- return { alias: "flash", id: DS_MODEL_IDS.flash };
29
+ // Trim, lower-case, strip a trailing `[…]` SKU (`deepseek-v4-pro[1m]` → `deepseek-v4-pro`).
30
+ const v = raw.trim().toLowerCase().replace(/\[[^\]]*\]\s*$/, "");
31
+ if (v === "flash" ||
32
+ v === "deepseek-flash" ||
33
+ v === "deepseek-v4-flash" ||
34
+ v === "deepseek-v4.1-flash") {
35
+ return { alias: "flash", id: DS_FLASH_ID };
29
36
  }
30
37
  if (v === "pro" || v === "deepseek-v4-pro") {
31
- return { alias: "pro", id: DS_MODEL_IDS.pro };
38
+ return { alias: "pro", id: DS_FLASH_ID };
32
39
  }
33
- if (v === "vision" || v === "deepseek-v4-flash-vision-exp") {
34
- return { alias: "vision", id: DS_MODEL_IDS.vision };
35
- }
36
- if (v === "deepseek-v4-pro[1m]") {
37
- return { alias: "pro", id: "deepseek-v4-pro[1m]" };
40
+ if (v === "vision" ||
41
+ v === "flash-vision" ||
42
+ v === "deepseek-v4-flash-vision" ||
43
+ v === "deepseek-v4-flash-vision-exp") {
44
+ return { alias: "vision", id: DS_FLASH_ID };
38
45
  }
39
46
  throw new Error(`Invalid DeepSeek model ${raw}; expected flash|pro|vision`);
40
47
  }
@@ -72,7 +79,7 @@ function maxConcurrentJobsFromEnv() {
72
79
  */
73
80
  export const config = {
74
81
  product: "cursor-route",
75
- version: "0.1.13",
82
+ version: "0.1.15",
76
83
  get jobsDir() {
77
84
  return defaultJobsDir();
78
85
  },
package/dist/or-free.js CHANGED
@@ -12,7 +12,6 @@ export const OPENROUTER_MODELS_URL_DEFAULT = "https://openrouter.ai/api/v1/model
12
12
  /** Fetch-fail / empty-rank fallback — OpenRouter live router, not a locked model. */
13
13
  export const OPENROUTER_FALLBACK_MODEL = "openrouter/free";
14
14
  const EXCLUDE_RE = /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
15
- const BOOST_RE = /coder|instruct|chat|nemotron|qwen|llama|gemma|gpt-oss|kimi|glm|deepseek/i;
16
15
  function orModelsUrl() {
17
16
  const base = (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1")
18
17
  .trim()
@@ -59,9 +58,28 @@ export function isOrFreeModel(m) {
59
58
  const tagged = id.endsWith(":free");
60
59
  return tagged || pricedFree;
61
60
  }
62
- /** Higher boost wins. Coding/chat families beat generic free. No hardcoded id. */
61
+ /**
62
+ * Toolkit-parity tier for a live OpenRouter free model id (first match wins).
63
+ * Huge general free models (Nemotron) rank well below coding/chat families so
64
+ * the easy-lane live pick does not land on a slow 550B generalist. Order
65
+ * matters: `qwen` is checked before `nemotron`, and the generic coding bucket
66
+ * (70) sits before `nemotron` exactly like the toolkit, so
67
+ * `nvidia/nemotron-3-550b:free` scores 15 — never 70. No hardcoded model id.
68
+ */
63
69
  export function orFreeBoost(id) {
64
- return BOOST_RE.test(id) ? 20 : 10;
70
+ if (/qwen/i.test(id))
71
+ return 100;
72
+ if (/(^|\/)z-ai\/glm|glm/i.test(id))
73
+ return 95;
74
+ if (/kimi|moonshot/i.test(id))
75
+ return 95;
76
+ if (/deepseek|hy-/i.test(id))
77
+ return 90;
78
+ if (/coder|instruct|chat|llama|gemma|gpt-oss|minimax/i.test(id))
79
+ return 70;
80
+ if (/nemotron/i.test(id))
81
+ return 15;
82
+ return 40;
65
83
  }
66
84
  export function rankOrFreeModels(models) {
67
85
  const out = [];
@@ -77,18 +95,24 @@ export function rankOrFreeModels(models) {
77
95
  continue;
78
96
  }
79
97
  const ctx = Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : 0;
98
+ const tier = orFreeBoost(id);
99
+ // Cap the context bonus at 131072 so a huge Nemotron ctx cannot outrank a
100
+ // higher tier on context alone: score = tier + min(ctx, 131072)/131072 * 5.
101
+ const score = tier + (Math.min(ctx, 131_072) / 131_072) * 5;
80
102
  out.push({
81
103
  id,
82
104
  name: (m.name || raw).trim(),
83
105
  context_length: ctx,
84
- boost: orFreeBoost(id),
106
+ boost: score,
85
107
  });
86
108
  }
109
+ // Sort like the toolkit: score desc, then tier desc, then id asc.
87
110
  out.sort((a, b) => {
88
111
  if (b.boost !== a.boost)
89
112
  return b.boost - a.boost;
90
- if (b.context_length !== a.context_length)
91
- return b.context_length - a.context_length;
113
+ const tierDiff = orFreeBoost(b.id) - orFreeBoost(a.id);
114
+ if (tierDiff !== 0)
115
+ return tierDiff;
92
116
  return a.id.localeCompare(b.id);
93
117
  });
94
118
  return out;
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  title: cursor-route workspace — working brief (edit in place)
3
3
  repo: ~/Projects/cursor-route
4
- npm: cursor-route@0.1.13 (publishing this slice)
4
+ npm: cursor-route@0.1.15
5
5
  created: 2026-08-12
6
- updated: 2026-08-29
6
+ updated: 2026-09-11
7
7
  ---
8
8
 
9
9
  # cursor-route — living brief
@@ -19,13 +19,13 @@ Cursor Agent plans. Workers run in tmux via `cursor-route`:
19
19
  | Lane | Worker | Intent |
20
20
  |------|--------|--------|
21
21
  | `easy` | OpenRouter free (live pick) | Wording / drafts — non-secret prompts only |
22
- | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model vision` for screenshots; `--model pro` harder mid / hard backup only) |
22
+ | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash** `deepseek-flash`; `--model vision` / `--model pro` are legacy aliases to the same id) |
23
23
  | `hard` | Grok CLI | Hard implement |
24
24
  | opt-in | OpenCode | `--worker opencode` coding agent; `--model free` ranks live Zen catalog (Ox Alpha first while listed) |
25
25
 
26
26
  Always-approve on for coding worktrees (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out) — not LIVE Discord/trading. Jobs live in `~/.local/share/cursor-route/jobs`, not in this clone.
27
27
 
28
- Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](../../CHANGELOG.md).
28
+ Install: `npm i -g cursor-route` → **0.1.15** (not LIVE until parent publishes). Release notes: [CHANGELOG.md](../../CHANGELOG.md).
29
29
 
30
30
  ## Open (edit / check off)
31
31
 
@@ -43,6 +43,8 @@ Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](.
43
43
  - [x] **Live Zen free pick** — 0.1.11: `--model free` ranks `GET https://opencode.ai/zen/v1/models` (Ox Alpha first while listed; ~15 min cache; Ox Alpha fallback if fetch fails). Pin with `CURSOR_ROUTE_OPENCODE_MODEL`. Health stays offline (cache/fallback only).
44
44
  - [x] **Flash-first + vision + live OpenRouter pick** — 0.1.12: `--model flash` stays the cheap Grok-out default; `--model vision` + screenshot auto-pick; easy lane live-picks free OpenRouter text models (`openrouter/free` is fetch-fail fallback only). Health never fetches `/models`. `route-orch` ProgRouter + MoRe one-liners. Hero fixture regenerated to 0.1.12. No npm publish in this slice.
45
45
  - [x] **Kimi 0.1.13 follow-up** — do not cache OpenRouter fallback; health labels uncached fallback; no Authorization on GET /models; env-invalid model error names the env vars; `\bimage\b` vision trigger.
46
+ - [x] **Super-audit OR ranker parity (0.1.14)** — tier-rank Qwen/GLM/Kimi above Nemotron 550B in `or-free.ts` (match agent-toolkit `select-openrouter-free-model.ps1`); brief `docs/briefs/2026-09-02_super-audit-or-ranker-parity.md`
47
+ - [x] **V4.1 Flash wire (0.1.15)** — mid default `deepseek-flash` (native vision). `--model pro` / `--model vision` are legacy aliases to the same id. Pro is off rotation. Mid still `claude-ds`. Hero fixture regenerated. npm not LIVE until parent publishes. Brief `docs/briefs/2026-09-11_v41-flash-not-pro.md`
46
48
  - [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
47
49
  - [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
48
50
 
@@ -79,4 +81,7 @@ Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](.
79
81
  | 2026-08-21 | Opt-in `--worker opencode` (Zen free `opencode/big-pickle`, `--auto`, no config rewrite); mid stays claude-ds → 0.1.10 LIVE. |
80
82
  | 2026-08-21 | Live Zen free pick (Ox Alpha first while listed; OpenRouter-style catalog rank) → 0.1.11 LIVE. |
81
83
  | 2026-08-29 | Flash-first docs, `--model vision` + auto-pick, live OpenRouter free pick, route-orch K318/K322, hero fixture 0.1.12. No npm publish. |
82
- | 2026-08-29 | Kimi audit follow-up → 0.1.13: no fallback cache, health labels, unauth GET /models, env error wording. |
84
+ | 2026-08-29 | Kimi audit follow-up → 0.1.13 LIVE on npm: no fallback cache, health labels, unauth GET /models, env error wording. |
85
+ | 2026-09-02 | Tier-rank OpenRouter free picks in `or-free.ts` (Qwen 100 / GLM-Kimi 95 / DeepSeek 90 / coding 70 / Nemotron 15 / default 40; ctx bonus capped at 131072) → 0.1.14. Commit + tag `v0.1.14`. Mid stays claude-ds. |
86
+ | 2026-09-02 | Kimi audit follow-up → ranker tests (seven tiers, score/id ties, 131072 cap, cache round-trip) then **0.1.14 LIVE** on npm. |
87
+ | 2026-09-11 | Map mid models to `deepseek-flash` (V4.1 Flash, native vision). `--model pro` / `--model vision` are legacy aliases. Pro is off rotation. Mid still `claude-ds` → 0.1.15. |
@@ -6,10 +6,10 @@ Simulated capture output for README / tweet assets — replace with a real GIF o
6
6
 
7
7
  ```
8
8
  $ cursor-route --version
9
- 0.1.13
9
+ 0.1.15
10
10
 
11
11
  $ cursor-route health
12
- cursor-route v0.1.13
12
+ cursor-route v0.1.15
13
13
  health: OK
14
14
  ✓ tmux
15
15
  ✓ runtime bun ok
@@ -32,7 +32,7 @@ $ cursor-route jobs --json
32
32
 
33
33
  Verified locally (2026-08-10): headless `claude-ds` smoke returned `CURSOR_ROUTE_SMOKE_OK`.
34
34
  Grok smoke hit 402 (Build usage balance exhausted) — auth/PATH wiring works; top up Grok Build for live demos.
35
- Use `--model flash` when Grok **usage** is out (cheap default). `--model pro` is harder mid / hard backup only — not the default Grok-out stand-in. A missing `grok login` is auth, not the Pro case.
35
+ Use `--model flash` when Grok **usage** is out (cheap default). `--model pro` and `--model vision` are legacy aliases to `deepseek-flash`. A missing `grok login` is auth, not a model switch.
36
36
 
37
37
  Current commands: `health`, `start`, `jobs`, `status`, `capture`, `send`, `attach`, `kill`, `sessions`, `clean`.
38
38
  Headless demos (no tmux) use `--no-tmux` and `capture`/`status` instead of `attach`/`send`.
@@ -1,15 +1,15 @@
1
1
  $ cursor-route --version
2
- 0.1.13
2
+ 0.1.15
3
3
 
4
4
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
5
- cursor-route v0.1.13
5
+ cursor-route v0.1.15
6
6
  health: OK
7
7
 
8
8
  ✓ tmux ok
9
9
  ✓ runtime bun ok
10
10
  ✓ script(1) ok (tty log capture)
11
11
  ✓ worker:grok ok (auth checked at first start — run grok login if jobs fail) @ ~/.grok/bin/grok
12
- ✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-v4-flash) @ ~/.local/bin/claude-ds
12
+ ✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-flash) @ ~/.local/bin/claude-ds
13
13
  ✗ worker:openrouter OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane live-picks a free model at start; pin with CURSOR_ROUTE_OPENROUTER_MODEL) @ node '~/Projects/cursor-route/dist/openrouter-run.js'
14
14
  ✗ worker:deepseek dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.
15
15
  ✓ worker:opencode ok (opencode run; --model free = live Zen pick, now opencode/x-preview-f-free; auth at first start — opencode auth login if jobs fail; mid default remains claude-ds) @ ~/.local/bin/opencode
@@ -22,7 +22,7 @@ $ cursor-route start --lane mid --model flash --dry-run "Add a unit test for she
22
22
  dry-run job a1b2c3d4
23
23
  worker: claude-ds
24
24
  model: flash
25
- command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/a1b2c3d4.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions
25
+ command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/a1b2c3d4.prompt' -Model 'deepseek-flash' --dangerously-skip-permissions
26
26
 
27
27
  $ cursor-route start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
28
28
  dry-run job b2c3d4e5
@@ -48,7 +48,7 @@ $ cursor-route start --lane mid --dry-run --json "Add a failing test then make i
48
48
  "alwaysApprove": true,
49
49
  "tmuxSession": "cursor-route-d4e5f6a7",
50
50
  "createdAt": "2026-08-13T00:00:00.000Z",
51
- "command": "cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/d4e5f6a7.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions"
51
+ "command": "cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/d4e5f6a7.prompt' -Model 'deepseek-flash' --dangerously-skip-permissions"
52
52
  }
53
53
 
54
54
  $ cursor-route jobs --json
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux. `--worker opencode` is an opt-in coding agent on OpenCode Zen free models.
4
4
 
5
- MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.13**)
5
+ MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.15**)
6
6
  GitHub: https://github.com/cemini23/cursor-route
7
7
 
8
8
  ## FAQ
@@ -14,15 +14,16 @@ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding work
14
14
  It uses the familiar strategist and worker-pane shape, but it is not a Codex clone. cursor-route uses Cursor as the planner and DeepSeek plus Grok CLI as workers. Codex is not required.
15
15
 
16
16
  ### Does mid lane use Anthropic Claude?
17
- No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`. Default model is Flash (`--model flash`) — keep Flash when Grok **usage** is out. `--model vision` is for screenshots / ui mocks. `--model pro` is harder mid / **hard backup only**, not the default Grok-out stand-in. A missing `grok login` is auth, not the Pro case.
17
+ No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`. Default model is Flash (`--model flash` → `deepseek-flash`) — keep Flash when Grok **usage** is out. `--model vision` and `--model pro` are legacy aliases to the same id. A missing `grok login` is auth, not a model switch.
18
18
 
19
19
  ### Flash vs Pro?
20
20
  | Flag | Model | When |
21
21
  |------|-------|------|
22
- | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
23
- | `--model vision` | `deepseek-v4-flash-vision-exp` | Screenshots / ui mocks / image prompts (or auto-pick) |
24
- | `--model pro` | `deepseek-v4-pro` | Harder mid / **hard backup** only not the default Grok-out stand-in |
25
- | `--model deepseek-v4-pro[1m]` | preserved SKU | Large-context Pro |
22
+ | `--model flash` (default) | `deepseek-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
23
+ | `--model vision` | `deepseek-flash` | Screenshots / ui mocks / image prompts (or auto-pick). V4.1 Flash has native vision |
24
+ | `--model pro` | `deepseek-flash` | Legacy aliasPro is off rotation |
25
+
26
+ This map matches DeepSeek's 2026-09-10 API id `deepseek-flash`.
26
27
 
27
28
  ### How do I use OpenCode free models?
28
29
  Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. `--model free` ranks live Zen free models (Ox Alpha first while listed). Mid stays `claude-ds`.
@@ -37,6 +38,6 @@ The cursor-route code is open source under MIT. It does not make the worker serv
37
38
 
38
39
  - npm: `npm i -g cursor-route`
39
40
  - Health: `cursor-route health`
40
- - Mid: `cursor-route start --lane mid "…"` (Flash; keep Flash when Grok usage is out) · `--model vision` for screenshots · `--model pro` harder mid / hard backup only
41
+ - Mid: `cursor-route start --lane mid "…"` (Flash `deepseek-flash`; keep Flash when Grok usage is out) · `--model vision` / `--model pro` are legacy aliases to the same id
41
42
  - OpenCode (opt-in): `cursor-route start --worker opencode "…"`
42
43
  - Skill: `/route-orch` (Cursor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-route",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane + OpenCode (opt-in free) are the parallel army \u2014 lane-aware /route orchestration in tmux.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
28
28
  | `mid` | `claude-ds` (DeepSeek via Claude Code harness) | Standard implement / refactor |
29
29
  | `hard` | `grok` | Premium plan in Cursor → Grok implement |
30
30
 
31
- Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies). Easy lane **live-picks** the best free text model at request time (unset / `--model free`). Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Fetch-fail fallback is the OpenRouter router `openrouter/free` only.
31
+ Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies). Easy lane **live-picks** the best free text model at request time (unset / `--model free`), preferring Qwen/GLM/Kimi coding free models over huge general free models (Nemotron). Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Fetch-fail fallback is the OpenRouter router `openrouter/free` only.
32
32
 
33
33
  ## claude-ds models
34
34
 
@@ -36,10 +36,11 @@ One harness. Do not install a second coding loop.
36
36
 
37
37
  | Flag | Model id | When |
38
38
  |------|----------|------|
39
- | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
40
- | `--model vision` | `deepseek-v4-flash-vision-exp` | Screenshots / ui mocks / image prompts (or auto-pick) |
41
- | `--model pro` | `deepseek-v4-pro` | Harder mid / **hard backup** only not the default Grok-out stand-in |
42
- | `--model deepseek-v4-pro[1m]` | preserved SKU | Large-context Pro |
39
+ | `--model flash` (default) | `deepseek-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
40
+ | `--model vision` | `deepseek-flash` | Screenshots / ui mocks / image prompts (or auto-pick) |
41
+ | `--model pro` | `deepseek-flash` | Legacy aliasPro is off rotation |
42
+
43
+ V4.1 Flash has native vision; `--model pro` is a legacy alias (off rotation).
43
44
 
44
45
  ```bash
45
46
  cursor-route start --lane mid --dir "$PWD" "…"
@@ -47,7 +48,7 @@ cursor-route start --lane mid --model vision --dir "$PWD" "…"
47
48
  cursor-route start --lane mid --model pro --dir "$PWD" "…"
48
49
  ```
49
50
 
50
- If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro case. When Grok **usage** is out, stay on Flash.
51
+ If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not a model switch. When Grok **usage** is out, stay on Flash.
51
52
 
52
53
  ## Experimental: --worker deepseek (dsh)
53
54
 
@@ -145,8 +145,14 @@ export function midDeepSeekProofDetail(): string {
145
145
 
146
146
  function pickModel(requested?: DsModelAlias, modelId?: string): { alias: DsModelAlias; id: string } {
147
147
  if (modelId) {
148
- const alias = requested ?? resolveDsModel(modelId).alias;
149
- return { alias, id: modelId };
148
+ try {
149
+ const resolved = resolveDsModel(modelId);
150
+ return { alias: requested ?? resolved.alias, id: resolved.id };
151
+ } catch (e) {
152
+ // Unknown id with an explicit alias: keep the raw id so dsh YAML checks can still reject it.
153
+ if (requested) return { alias: requested, id: modelId };
154
+ throw e;
155
+ }
150
156
  }
151
157
  if (requested) {
152
158
  return { alias: requested, id: DS_MODEL_IDS[requested] };
@@ -43,8 +43,14 @@ function pickModel(
43
43
  modelId?: string,
44
44
  ): { alias: DsModelAlias; id: string } {
45
45
  if (modelId) {
46
- const alias = requested ?? resolveDsModel(modelId).alias;
47
- return { alias, id: modelId };
46
+ try {
47
+ const resolved = resolveDsModel(modelId);
48
+ return { alias: requested ?? resolved.alias, id: resolved.id };
49
+ } catch (e) {
50
+ // Unknown id with an explicit alias: keep the raw id so YAML checks can still reject it.
51
+ if (requested) return { alias: requested, id: modelId };
52
+ throw e;
53
+ }
48
54
  }
49
55
  if (requested) {
50
56
  return { alias: requested, id: DS_MODEL_IDS[requested] };
package/src/cli.test.ts CHANGED
@@ -52,24 +52,32 @@ describe("resolveWorker", () => {
52
52
 
53
53
  describe("resolveDsModel", () => {
54
54
  test("defaults to flash", () => {
55
- expect(resolveDsModel()).toEqual({ alias: "flash", id: "deepseek-v4-flash" });
55
+ expect(resolveDsModel()).toEqual({ alias: "flash", id: "deepseek-flash" });
56
+ expect(resolveDsModel(" ")).toEqual({ alias: "flash", id: "deepseek-flash" });
56
57
  expect(resolveDsModelAlias("")).toBe("flash");
57
58
  });
58
59
  test("accepts aliases and full ids", () => {
59
- expect(resolveDsModel("flash").id).toBe("deepseek-v4-flash");
60
- expect(resolveDsModel("pro").id).toBe("deepseek-v4-pro");
61
- expect(resolveDsModel("vision").id).toBe("deepseek-v4-flash-vision-exp");
62
- expect(resolveDsModel("deepseek-v4-flash-vision-exp")).toEqual({
60
+ expect(resolveDsModel("flash")).toEqual({ alias: "flash", id: "deepseek-flash" });
61
+ expect(resolveDsModel("deepseek-flash")).toEqual({ alias: "flash", id: "deepseek-flash" });
62
+ expect(resolveDsModel("deepseek-v4-flash")).toEqual({ alias: "flash", id: "deepseek-flash" });
63
+ expect(resolveDsModel("deepseek-v4.1-flash")).toEqual({ alias: "flash", id: "deepseek-flash" });
64
+ expect(resolveDsModel("pro")).toEqual({ alias: "pro", id: "deepseek-flash" });
65
+ expect(resolveDsModel("deepseek-v4-pro")).toEqual({ alias: "pro", id: "deepseek-flash" });
66
+ expect(resolveDsModel("deepseek-v4-pro[1m]")).toEqual({ alias: "pro", id: "deepseek-flash" });
67
+ expect(resolveDsModel("vision")).toEqual({ alias: "vision", id: "deepseek-flash" });
68
+ expect(resolveDsModel("flash-vision")).toEqual({ alias: "vision", id: "deepseek-flash" });
69
+ expect(resolveDsModel("deepseek-v4-flash-vision")).toEqual({
63
70
  alias: "vision",
64
- id: "deepseek-v4-flash-vision-exp",
71
+ id: "deepseek-flash",
65
72
  });
66
- expect(resolveDsModel("deepseek-v4-pro[1m]")).toEqual({
67
- alias: "pro",
68
- id: "deepseek-v4-pro[1m]",
73
+ expect(resolveDsModel("deepseek-v4-flash-vision-exp")).toEqual({
74
+ alias: "vision",
75
+ id: "deepseek-flash",
69
76
  });
70
77
  });
71
78
  test("rejects unknown", () => {
72
79
  expect(() => resolveDsModel("opus")).toThrow(/flash\|pro\|vision/);
80
+ expect(() => resolveDsModel("deepseek-chat")).toThrow(/flash\|pro\|vision/);
73
81
  });
74
82
  });
75
83
 
@@ -93,7 +101,7 @@ describe("promptLooksLikeVision", () => {
93
101
  });
94
102
 
95
103
  describe("claude-ds -Model", () => {
96
- test("shim path passes -Model deepseek-v4-flash by default", () => {
104
+ test("shim path passes -Model deepseek-flash by default", () => {
97
105
  const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
98
106
  const prevDs = process.env.CURSOR_ROUTE_DS_MODEL;
99
107
  const prevAm = process.env.ANTHROPIC_MODEL;
@@ -107,7 +115,9 @@ describe("claude-ds -Model", () => {
107
115
  alwaysApprove: true,
108
116
  });
109
117
  expect(plan.command).toContain("-Model");
110
- expect(plan.command).toContain("deepseek-v4-flash");
118
+ expect(plan.command).toContain("deepseek-flash");
119
+ expect(plan.command).not.toContain("deepseek-v4-flash");
120
+ expect(plan.command).not.toContain("deepseek-v4-pro");
111
121
  } finally {
112
122
  if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
113
123
  else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
@@ -117,7 +127,7 @@ describe("claude-ds -Model", () => {
117
127
  else process.env.ANTHROPIC_MODEL = prevAm;
118
128
  }
119
129
  });
120
- test("vision alias maps to deepseek-v4-flash-vision-exp", () => {
130
+ test("vision alias maps to deepseek-flash", () => {
121
131
  const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
122
132
  process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
123
133
  try {
@@ -127,14 +137,14 @@ describe("claude-ds -Model", () => {
127
137
  alwaysApprove: true,
128
138
  model: "vision",
129
139
  });
130
- expect(plan.command).toContain("deepseek-v4-flash-vision-exp");
131
- expect(plan.command).not.toContain("deepseek-v4-flash'");
140
+ expect(plan.command).toContain("deepseek-flash");
141
+ expect(plan.command).not.toContain("deepseek-v4-flash-vision-exp");
132
142
  } finally {
133
143
  if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
134
144
  else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
135
145
  }
136
146
  });
137
- test("pro alias maps to deepseek-v4-pro", () => {
147
+ test("pro alias maps to deepseek-flash", () => {
138
148
  const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
139
149
  process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
140
150
  try {
@@ -144,14 +154,14 @@ describe("claude-ds -Model", () => {
144
154
  alwaysApprove: true,
145
155
  model: "pro",
146
156
  });
147
- expect(plan.command).toContain("deepseek-v4-pro");
148
- expect(plan.command).not.toContain("deepseek-v4-flash");
157
+ expect(plan.command).toContain("deepseek-flash");
158
+ expect(plan.command).not.toContain("deepseek-v4-pro");
149
159
  } finally {
150
160
  if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
151
161
  else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
152
162
  }
153
163
  });
154
- test("preserves deepseek-v4-pro[1m] via modelId", () => {
164
+ test("legacy deepseek-v4-pro[1m] modelId maps to deepseek-flash", () => {
155
165
  const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
156
166
  process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
157
167
  try {
@@ -162,7 +172,8 @@ describe("claude-ds -Model", () => {
162
172
  model: "pro",
163
173
  modelId: "deepseek-v4-pro[1m]",
164
174
  });
165
- expect(plan.command).toContain("deepseek-v4-pro[1m]");
175
+ expect(plan.command).toContain("deepseek-flash");
176
+ expect(plan.command).not.toContain("deepseek-v4-pro");
166
177
  } finally {
167
178
  if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
168
179
  else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
@@ -232,8 +243,8 @@ describe("startJob product path", () => {
232
243
  expect(result.ok).toBe(true);
233
244
  if (!result.ok) return;
234
245
  expect(result.job.model).toBe("pro");
235
- expect(result.command).toContain("deepseek-v4-pro");
236
- expect(result.command).not.toContain("deepseek-v4-flash");
246
+ expect(result.command).toContain("deepseek-flash");
247
+ expect(result.command).not.toContain("deepseek-v4-pro");
237
248
  } finally {
238
249
  if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
239
250
  else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
@@ -306,7 +317,8 @@ describe("startJob product path", () => {
306
317
  expect(result.ok).toBe(true);
307
318
  if (!result.ok) return;
308
319
  expect(result.job.model).toBe("vision");
309
- expect(result.command).toContain("deepseek-v4-flash-vision-exp");
320
+ expect(result.command).toContain("deepseek-flash");
321
+ expect(result.command).not.toContain("deepseek-v4-flash-vision-exp");
310
322
  } finally {
311
323
  if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
312
324
  else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
@@ -342,7 +354,7 @@ describe("startJob product path", () => {
342
354
  expect(result.ok).toBe(true);
343
355
  if (!result.ok) return;
344
356
  expect(result.job.model).toBe("flash");
345
- expect(result.command).toContain("deepseek-v4-flash");
357
+ expect(result.command).toContain("deepseek-flash");
346
358
  expect(result.command).not.toContain("deepseek-v4-flash-vision-exp");
347
359
  } finally {
348
360
  if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
@@ -380,6 +392,45 @@ describe("startJob product path", () => {
380
392
  expect(result.ok).toBe(true);
381
393
  if (!result.ok) return;
382
394
  expect(result.job.model).toBe("flash");
395
+ expect(result.command).toContain("deepseek-flash");
396
+ expect(result.command).not.toContain("deepseek-v4-flash-vision-exp");
397
+ } finally {
398
+ if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
399
+ else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
400
+ if (prev.ds === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
401
+ else process.env.CURSOR_ROUTE_DS_MODEL = prev.ds;
402
+ if (prev.am === undefined) delete process.env.ANTHROPIC_MODEL;
403
+ else process.env.ANTHROPIC_MODEL = prev.am;
404
+ if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
405
+ else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
406
+ rmSync(jobsDir, { recursive: true, force: true });
407
+ }
408
+ });
409
+
410
+ test("--model vision records alias vision and launches deepseek-flash", () => {
411
+ const prev = {
412
+ bin: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
413
+ ds: process.env.CURSOR_ROUTE_DS_MODEL,
414
+ am: process.env.ANTHROPIC_MODEL,
415
+ jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
416
+ };
417
+ const jobsDir = join(tmpdir(), `cr-jobs-vision-flag-${process.pid}`);
418
+ mkdirSync(jobsDir, { recursive: true });
419
+ process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
420
+ delete process.env.CURSOR_ROUTE_DS_MODEL;
421
+ delete process.env.ANTHROPIC_MODEL;
422
+ process.env.CURSOR_ROUTE_JOBS_DIR = jobsDir;
423
+ try {
424
+ const result = startJob({
425
+ prompt: "ping",
426
+ lane: "mid",
427
+ model: "vision",
428
+ dryRun: true,
429
+ });
430
+ expect(result.ok).toBe(true);
431
+ if (!result.ok) return;
432
+ expect(result.job.model).toBe("vision");
433
+ expect(result.command).toContain("deepseek-flash");
383
434
  expect(result.command).not.toContain("deepseek-v4-flash-vision-exp");
384
435
  } finally {
385
436
  if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
@@ -605,15 +656,16 @@ describe("deepseek adapter (dsh)", () => {
605
656
  const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
606
657
  expect(patch).toContain("agent-default-model");
607
658
  expect(patch).toContain("provider: deepseek-official");
608
- expect(patch).toContain("deepseek-v4-flash");
659
+ expect(patch).toContain("deepseek-flash");
609
660
  expect(patch).not.toContain("deepseek-v4-pro");
661
+ expect(patch).not.toContain("deepseek-v4-flash-vision-exp");
610
662
  expect(patch).not.toContain("test-key");
611
663
  },
612
664
  );
613
665
  rmSync(dir, { recursive: true, force: true });
614
666
  });
615
667
 
616
- test("buildLaunch: vision model → deepseek-v4-flash-vision-exp in patch", () => {
668
+ test("buildLaunch: vision model → deepseek-flash in patch", () => {
617
669
  const { dir, bin } = makeFakeDsh("vision");
618
670
  const promptFile = join(dir, "job.prompt");
619
671
  writeFileSync(promptFile, "ping");
@@ -632,13 +684,15 @@ describe("deepseek adapter (dsh)", () => {
632
684
  model: "vision",
633
685
  });
634
686
  const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
635
- expect(patch).toContain("deepseek-v4-flash-vision-exp");
687
+ expect(patch).toContain("deepseek-flash");
688
+ expect(patch).not.toContain("deepseek-v4-flash-vision-exp");
689
+ expect(patch).not.toContain("deepseek-v4-pro");
636
690
  },
637
691
  );
638
692
  rmSync(dir, { recursive: true, force: true });
639
693
  });
640
694
 
641
- test("buildLaunch: pro model → deepseek-v4-pro in patch", () => {
695
+ test("buildLaunch: pro model → deepseek-flash in patch", () => {
642
696
  const { dir, bin } = makeFakeDsh("pro");
643
697
  const promptFile = join(dir, "job.prompt");
644
698
  writeFileSync(promptFile, "ping");
@@ -657,14 +711,15 @@ describe("deepseek adapter (dsh)", () => {
657
711
  model: "pro",
658
712
  });
659
713
  const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
660
- expect(patch).toContain("deepseek-v4-pro");
661
- expect(patch).not.toContain("deepseek-v4-flash");
714
+ expect(patch).toContain("deepseek-flash");
715
+ expect(patch).not.toContain("deepseek-v4-pro");
716
+ expect(patch).not.toContain("deepseek-v4-flash-vision-exp");
662
717
  },
663
718
  );
664
719
  rmSync(dir, { recursive: true, force: true });
665
720
  });
666
721
 
667
- test("buildLaunch: preserves deepseek-v4-pro[1m] in patch via modelId", () => {
722
+ test("buildLaunch: legacy deepseek-v4-pro[1m] modelId maps to deepseek-flash in patch", () => {
668
723
  const { dir, bin } = makeFakeDsh("pro1m");
669
724
  const promptFile = join(dir, "job.prompt");
670
725
  writeFileSync(promptFile, "ping");
@@ -684,7 +739,8 @@ describe("deepseek adapter (dsh)", () => {
684
739
  modelId: "deepseek-v4-pro[1m]",
685
740
  });
686
741
  const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
687
- expect(patch).toContain("deepseek-v4-pro[1m]");
742
+ expect(patch).toContain("deepseek-flash");
743
+ expect(patch).not.toContain("deepseek-v4-pro");
688
744
  },
689
745
  );
690
746
  rmSync(dir, { recursive: true, force: true });
@@ -879,7 +935,7 @@ describe("health", () => {
879
935
  test("returns structured report", () => {
880
936
  const r = runHealth();
881
937
  expect(r.product).toBe("cursor-route");
882
- expect(r.version).toBe("0.1.13");
938
+ expect(r.version).toBe("0.1.15");
883
939
  expect(r.checks.length).toBeGreaterThan(3);
884
940
  expect(r.checks.some((c) => c.name === "tmux")).toBe(true);
885
941
  expect(r.checks.some((c) => c.name === "cursor_cli")).toBe(true);
@@ -891,8 +947,8 @@ describe("health", () => {
891
947
  expect(r.checks.some((c) => c.name === "worker:deepseek")).toBe(true);
892
948
  });
893
949
 
894
- test("config version is 0.1.13", () => {
895
- expect(config.version).toBe("0.1.13");
950
+ test("config version is 0.1.15", () => {
951
+ expect(config.version).toBe("0.1.15");
896
952
  });
897
953
 
898
954
  test("OR-gate: ok can be true while worker:opencode is false", () => {
package/src/cli.ts CHANGED
@@ -57,7 +57,7 @@ Usage:
57
57
  Start options:
58
58
  --worker <grok|claude-ds|openrouter|deepseek|opencode> Worker adapter (default: grok; deepseek/opencode = opt-in)
59
59
  --lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
60
- --model <flash|pro|vision|free|provider/model> claude-ds/deepseek: flash|pro|vision. opencode/openrouter: free (live catalog pick) or provider/model
60
+ --model <flash|pro|vision|free|provider/model> claude-ds/deepseek: flash|pro|vision (aliases map to deepseek-flash). opencode/openrouter: free (live catalog pick) or provider/model
61
61
  --dir <path> Working directory (default: cwd)
62
62
  --ask Disable always-approve for this job
63
63
  --dry-run Print launch command; do not start
@@ -70,7 +70,7 @@ Env:
70
70
  CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
71
71
  CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
72
72
  CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default; --model ignored)
73
- CURSOR_ROUTE_DS_MODEL Default mid model flash|pro|vision (or full ids / deepseek-v4-pro[1m]); overridden by --model
73
+ CURSOR_ROUTE_DS_MODEL Default mid model flash|pro|vision (aliases map to deepseek-flash); overridden by --model
74
74
  CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
75
75
  CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
76
76
  CURSOR_ROUTE_DSH_BIN Override the dsh binary path (tests / power users)
package/src/config.ts CHANGED
@@ -23,10 +23,13 @@ export const WORKERS: WorkerKind[] = ["grok", "claude-ds", "openrouter", "deepse
23
23
  export const LANES: Lane[] = ["easy", "mid", "hard"];
24
24
  export const DS_MODELS: DsModelAlias[] = ["flash", "pro", "vision"];
25
25
 
26
+ /** DeepSeek V4.1 Flash (native vision). Matches the 2026-09-10 API id. */
27
+ export const DS_FLASH_ID = "deepseek-flash";
28
+
26
29
  export const DS_MODEL_IDS: Record<DsModelAlias, string> = {
27
- flash: "deepseek-v4-flash",
28
- pro: "deepseek-v4-pro",
29
- vision: "deepseek-v4-flash-vision-exp",
30
+ flash: DS_FLASH_ID,
31
+ pro: DS_FLASH_ID,
32
+ vision: DS_FLASH_ID,
30
33
  };
31
34
 
32
35
  /** Prompt looks like a screenshot / image / ui mock — vision auto-pick. */
@@ -38,26 +41,35 @@ export function promptLooksLikeVision(prompt: string): boolean {
38
41
  }
39
42
 
40
43
  /**
41
- * Resolve --model / env to alias + concrete model id.
42
- * Preserves `deepseek-v4-pro[1m]` (does not silently strip the SKU).
44
+ * Resolve --model / env to alias + launch id.
45
+ * V4.1 Flash is native vision; Pro is a legacy alias, not a rotation step.
46
+ * All known aliases launch `deepseek-flash`.
43
47
  * Empty → Flash.
44
48
  */
45
49
  export function resolveDsModel(raw?: string | null): DsModelChoice {
46
50
  if (!raw || !raw.trim()) {
47
- return { alias: "flash", id: DS_MODEL_IDS.flash };
51
+ return { alias: "flash", id: DS_FLASH_ID };
48
52
  }
49
- const v = raw.trim().toLowerCase();
50
- if (v === "flash" || v === "deepseek-v4-flash") {
51
- return { alias: "flash", id: DS_MODEL_IDS.flash };
53
+ // Trim, lower-case, strip a trailing `[…]` SKU (`deepseek-v4-pro[1m]` → `deepseek-v4-pro`).
54
+ const v = raw.trim().toLowerCase().replace(/\[[^\]]*\]\s*$/, "");
55
+ if (
56
+ v === "flash" ||
57
+ v === "deepseek-flash" ||
58
+ v === "deepseek-v4-flash" ||
59
+ v === "deepseek-v4.1-flash"
60
+ ) {
61
+ return { alias: "flash", id: DS_FLASH_ID };
52
62
  }
53
63
  if (v === "pro" || v === "deepseek-v4-pro") {
54
- return { alias: "pro", id: DS_MODEL_IDS.pro };
55
- }
56
- if (v === "vision" || v === "deepseek-v4-flash-vision-exp") {
57
- return { alias: "vision", id: DS_MODEL_IDS.vision };
64
+ return { alias: "pro", id: DS_FLASH_ID };
58
65
  }
59
- if (v === "deepseek-v4-pro[1m]") {
60
- return { alias: "pro", id: "deepseek-v4-pro[1m]" };
66
+ if (
67
+ v === "vision" ||
68
+ v === "flash-vision" ||
69
+ v === "deepseek-v4-flash-vision" ||
70
+ v === "deepseek-v4-flash-vision-exp"
71
+ ) {
72
+ return { alias: "vision", id: DS_FLASH_ID };
61
73
  }
62
74
  throw new Error(`Invalid DeepSeek model ${raw}; expected flash|pro|vision`);
63
75
  }
@@ -111,7 +123,7 @@ function maxConcurrentJobsFromEnv(): number {
111
123
  */
112
124
  export const config = {
113
125
  product: "cursor-route",
114
- version: "0.1.13",
126
+ version: "0.1.15",
115
127
  get jobsDir(): string {
116
128
  return defaultJobsDir();
117
129
  },
@@ -62,10 +62,13 @@ describe("openrouter free catalog", () => {
62
62
  ).toBe(false);
63
63
  });
64
64
 
65
- test("boost: coder/qwen outranks generic free; no hardcoded winner id", () => {
66
- expect(orFreeBoost("qwen/qwen-coder:free")).toBe(20);
67
- expect(orFreeBoost("acme/generic:free")).toBe(10);
68
- expect(orFreeBoost("meta-llama/llama-3.3-70b-instruct:free")).toBe(20);
65
+ test("boost: tier ranks Qwen/GLM/Kimi above Nemotron; generic free is default", () => {
66
+ expect(orFreeBoost("qwen/qwen-coder:free")).toBe(100);
67
+ expect(orFreeBoost("z-ai/glm-5.2:free")).toBe(95);
68
+ expect(orFreeBoost("moonshotai/kimi-k2:free")).toBe(95);
69
+ expect(orFreeBoost("acme/generic:free")).toBe(40);
70
+ expect(orFreeBoost("meta-llama/llama-3.3-70b-instruct:free")).toBe(70);
71
+ expect(orFreeBoost("nvidia/nemotron-3-550b:free")).toBe(15);
69
72
  });
70
73
 
71
74
  test("rank prefers a :free coder with larger context over a generic :free", () => {
@@ -85,10 +88,83 @@ describe("openrouter free catalog", () => {
85
88
  expect(ranked[1]?.id).toBe("qwen/small-coder:free");
86
89
  });
87
90
 
91
+ test("all seven tier buckets", () => {
92
+ expect(orFreeBoost("qwen/qwen3-coder:free")).toBe(100);
93
+ expect(orFreeBoost("z-ai/glm-5.2:free")).toBe(95);
94
+ expect(orFreeBoost("moonshotai/kimi-k2:free")).toBe(95);
95
+ expect(orFreeBoost("deepseek/deepseek-chat:free")).toBe(90);
96
+ expect(orFreeBoost("acme/hy-foo:free")).toBe(90);
97
+ expect(orFreeBoost("meta-llama/llama-3.3-70b-instruct:free")).toBe(70);
98
+ expect(orFreeBoost("google/gemma-3-12b:free")).toBe(70);
99
+ expect(orFreeBoost("openai/gpt-oss-120b:free")).toBe(70);
100
+ expect(orFreeBoost("minimax/minimax-m1:free")).toBe(70);
101
+ expect(orFreeBoost("nvidia/nemotron-3-550b:free")).toBe(15);
102
+ expect(orFreeBoost("acme/generic:free")).toBe(40);
103
+ });
104
+
105
+ test("equal score: higher tier wins (Qwen ctx 0 vs GLM at ctx cap)", () => {
106
+ const ranked = rankOrFreeModels([
107
+ { id: "qwen/qwen3-coder:free", context_length: 0 },
108
+ { id: "z-ai/glm-5.2:free", context_length: 131_072 },
109
+ ]);
110
+ expect(ranked[0]?.boost).toBe(ranked[1]?.boost);
111
+ expect(ranked[0]?.id).toBe("qwen/qwen3-coder:free");
112
+ });
113
+
114
+ test("equal score same tier: id ascending", () => {
115
+ const ranked = rankOrFreeModels([
116
+ { id: "qwen/zzz-coder:free", context_length: 8_000 },
117
+ { id: "qwen/aaa-coder:free", context_length: 8_000 },
118
+ ]);
119
+ expect(ranked[0]?.id).toBe("qwen/aaa-coder:free");
120
+ expect(ranked[1]?.id).toBe("qwen/zzz-coder:free");
121
+ });
122
+
123
+ test("ctx bonus caps at 131072: same-tier 131072 vs 262144 ties then id asc", () => {
124
+ const ranked = rankOrFreeModels([
125
+ { id: "qwen/zzz-coder:free", context_length: 262_144 },
126
+ { id: "qwen/aaa-coder:free", context_length: 131_072 },
127
+ ]);
128
+ expect(ranked[0]?.boost).toBe(ranked[1]?.boost);
129
+ expect(ranked[0]?.id).toBe("qwen/aaa-coder:free");
130
+ });
131
+
132
+ test("huge Nemotron free model loses to GLM/Qwen on tier rank", () => {
133
+ const ranked = rankOrFreeModels([
134
+ { id: "nvidia/nemotron-3-550b:free", name: "Nemotron 3 550B", context_length: 262_144 },
135
+ { id: "z-ai/glm-5.2:free", name: "GLM 5.2", context_length: 32_768 },
136
+ { id: "qwen/qwen3-coder:free", name: "Qwen3 Coder", context_length: 32_768 },
137
+ ]);
138
+ expect(ranked[0]?.id).toBe("qwen/qwen3-coder:free");
139
+ expect(ranked[0]?.id).not.toBe("nvidia/nemotron-3-550b:free");
140
+ expect(ranked[1]?.id).toBe("z-ai/glm-5.2:free");
141
+ expect(ranked[2]?.id).toBe("nvidia/nemotron-3-550b:free");
142
+ expect(ranked[0]!.boost).toBeGreaterThan(ranked[2]!.boost);
143
+ });
144
+
145
+ test("huge ctx alone does not beat a higher tier (ctx bonus caps at 131072)", () => {
146
+ const ranked = rankOrFreeModels([
147
+ { id: "nvidia/nemotron-3-550b:free", context_length: 1_048_576 },
148
+ { id: "qwen/qwen3-coder:free", context_length: 1_000 },
149
+ ]);
150
+ expect(ranked[0]?.id).toBe("qwen/qwen3-coder:free");
151
+ });
152
+
88
153
  test("pickOrFreeModel uses the ranked catalog winner", () => {
89
154
  expect(pickOrFreeModel({ catalog: CATALOG })).toBe("qwen/qwen-coder:free");
90
155
  });
91
156
 
157
+ test("pickOrFreeModel winner is not a huge Nemotron when GLM/Qwen are free", () => {
158
+ const winner = pickOrFreeModel({
159
+ catalog: [
160
+ { id: "nvidia/nemotron-3-550b:free", name: "Nemotron 3 550B", context_length: 262_144 },
161
+ { id: "z-ai/glm-5.2:free", name: "GLM 5.2", context_length: 32_768 },
162
+ ],
163
+ });
164
+ expect(winner).not.toBe("nvidia/nemotron-3-550b:free");
165
+ expect(winner).toBe("z-ai/glm-5.2:free");
166
+ });
167
+
92
168
  test("offline / empty catalog falls back to the OpenRouter router", () => {
93
169
  expect(pickOrFreeModel({ catalog: [] })).toBe(OPENROUTER_FALLBACK_MODEL);
94
170
  expect(OPENROUTER_FALLBACK_MODEL).toBe("openrouter/free");
@@ -175,6 +251,18 @@ describe("openRouterModel live pick", () => {
175
251
  expect(existsSync(cache)).toBe(false);
176
252
  });
177
253
  });
254
+
255
+ test("ranked pick is cached; offline second call returns the cached id", () => {
256
+ isolate(() => {
257
+ process.env.CURSOR_ROUTE_OR_CATALOG_JSON = JSON.stringify({ data: CATALOG });
258
+ expect(openRouterModel("free")).toBe("qwen/qwen-coder:free");
259
+ delete process.env.CURSOR_ROUTE_OR_REFRESH;
260
+ process.env.CURSOR_ROUTE_OR_OFFLINE = "1";
261
+ delete process.env.CURSOR_ROUTE_OR_CATALOG_JSON;
262
+ expect(existsSync(process.env.CURSOR_ROUTE_OR_CACHE_PATH!)).toBe(true);
263
+ expect(openRouterModel("free")).toBe("qwen/qwen-coder:free");
264
+ });
265
+ });
178
266
  });
179
267
 
180
268
  describe("openrouter health stays offline", () => {
package/src/or-free.ts CHANGED
@@ -35,8 +35,6 @@ export interface OrFreePick {
35
35
 
36
36
  const EXCLUDE_RE =
37
37
  /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
38
- const BOOST_RE =
39
- /coder|instruct|chat|nemotron|qwen|llama|gemma|gpt-oss|kimi|glm|deepseek/i;
40
38
 
41
39
  function orModelsUrl(): string {
42
40
  const base = (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1")
@@ -88,9 +86,22 @@ export function isOrFreeModel(m: OrModel): boolean {
88
86
  return tagged || pricedFree;
89
87
  }
90
88
 
91
- /** Higher boost wins. Coding/chat families beat generic free. No hardcoded id. */
89
+ /**
90
+ * Toolkit-parity tier for a live OpenRouter free model id (first match wins).
91
+ * Huge general free models (Nemotron) rank well below coding/chat families so
92
+ * the easy-lane live pick does not land on a slow 550B generalist. Order
93
+ * matters: `qwen` is checked before `nemotron`, and the generic coding bucket
94
+ * (70) sits before `nemotron` exactly like the toolkit, so
95
+ * `nvidia/nemotron-3-550b:free` scores 15 — never 70. No hardcoded model id.
96
+ */
92
97
  export function orFreeBoost(id: string): number {
93
- return BOOST_RE.test(id) ? 20 : 10;
98
+ if (/qwen/i.test(id)) return 100;
99
+ if (/(^|\/)z-ai\/glm|glm/i.test(id)) return 95;
100
+ if (/kimi|moonshot/i.test(id)) return 95;
101
+ if (/deepseek|hy-/i.test(id)) return 90;
102
+ if (/coder|instruct|chat|llama|gemma|gpt-oss|minimax/i.test(id)) return 70;
103
+ if (/nemotron/i.test(id)) return 15;
104
+ return 40;
94
105
  }
95
106
 
96
107
  export function rankOrFreeModels(models: OrModel[]): Array<{
@@ -110,16 +121,22 @@ export function rankOrFreeModels(models: OrModel[]): Array<{
110
121
  continue;
111
122
  }
112
123
  const ctx = Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : 0;
124
+ const tier = orFreeBoost(id);
125
+ // Cap the context bonus at 131072 so a huge Nemotron ctx cannot outrank a
126
+ // higher tier on context alone: score = tier + min(ctx, 131072)/131072 * 5.
127
+ const score = tier + (Math.min(ctx, 131_072) / 131_072) * 5;
113
128
  out.push({
114
129
  id,
115
130
  name: (m.name || raw).trim(),
116
131
  context_length: ctx,
117
- boost: orFreeBoost(id),
132
+ boost: score,
118
133
  });
119
134
  }
135
+ // Sort like the toolkit: score desc, then tier desc, then id asc.
120
136
  out.sort((a, b) => {
121
137
  if (b.boost !== a.boost) return b.boost - a.boost;
122
- if (b.context_length !== a.context_length) return b.context_length - a.context_length;
138
+ const tierDiff = orFreeBoost(b.id) - orFreeBoost(a.id);
139
+ if (tierDiff !== 0) return tierDiff;
123
140
  return a.id.localeCompare(b.id);
124
141
  });
125
142
  return out;