cursor-route 0.1.14 → 0.1.16
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 +12 -0
- package/README.md +26 -24
- package/SECURITY.md +1 -1
- package/dist/adapters/claude-ds.js +10 -2
- package/dist/adapters/deepseek.js +10 -2
- package/dist/adapters/opencode.js +4 -4
- package/dist/cli.js +2 -2
- package/dist/config.js +23 -16
- package/dist/zen-free.js +30 -15
- package/docs/briefs/WORKING.md +12 -6
- package/docs/demo-notes.md +3 -3
- package/docs/fixtures/hero-demo.log +5 -5
- package/llms.txt +10 -9
- package/package.json +1 -1
- package/skills/route-orch/SKILL.md +7 -6
- package/src/adapters/claude-ds.ts +8 -2
- package/src/adapters/deepseek.ts +8 -2
- package/src/adapters/opencode.test.ts +1 -1
- package/src/adapters/opencode.ts +4 -4
- package/src/cli.test.ts +90 -34
- package/src/cli.ts +2 -2
- package/src/config.ts +28 -16
- package/src/zen-free.test.ts +83 -13
- package/src/zen-free.ts +29 -12
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
_(none)_
|
|
6
6
|
|
|
7
|
+
## 0.1.16 — 2026-09-16
|
|
8
|
+
|
|
9
|
+
- `--worker opencode --model free` now ranks the **strongest listed-free coding model** by capability tokens (ultra/pro/max > preview > generic > flash/lite), then coding-family, then `context_length`. No locked live winner id.
|
|
10
|
+
- Ox Alpha (`opencode/x-preview-f-free`) is the offline / fetch-fail / empty-catalog **fallback only**, not a live pin. Pin `CURSOR_ROUTE_OPENCODE_MODEL` to freeze an id.
|
|
11
|
+
- `hy3-free` is may-train (−20), not a coding-family boost. Contributor-free is penalized. Mid lane still `claude-ds`; no worker swap. Hero demo fixture regenerated to **0.1.16**.
|
|
12
|
+
|
|
13
|
+
## 0.1.15 — 2026-09-11
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- `--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.
|
|
17
|
+
- Mid lane still `claude-ds`; no worker swap. Hero demo fixture regenerated to **0.1.15**.
|
|
18
|
+
|
|
7
19
|
## 0.1.14 — 2026-09-02
|
|
8
20
|
|
|
9
21
|
- 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.
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ If you already live in Cursor, X Premium (Grok CLI), and DeepSeek — stop payin
|
|
|
29
29
|
| Grok CLI | X Premium | `--lane hard` implement |
|
|
30
30
|
| DeepSeek via claude-ds | DeepSeek API / plan | `--lane mid` implement (**Flash** default; `--model vision` for screenshots; `--model pro` harder mid / hard backup only) |
|
|
31
31
|
| OpenRouter free models | OpenRouter API (free tier) | `--lane easy` live free pick at start — non-secret prompts only (see Security) |
|
|
32
|
-
| OpenCode (opt-in) | OpenCode Zen free models | `--worker opencode` implement on live Zen free pick (
|
|
32
|
+
| OpenCode (opt-in) | OpenCode Zen free models | `--worker opencode` implement on live Zen free pick (strongest listed-free; no locked id) |
|
|
33
33
|
| Codex / extra Claude | Optional | Not required for v0 |
|
|
34
34
|
|
|
35
35
|
Exact dollars vary — the point is **reuse subscriptions you already have**.
|
|
@@ -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.
|
|
119
|
+
0.1.16
|
|
120
120
|
|
|
121
121
|
$ CURSOR_ROUTE_RELAXED=1 cursor-route health
|
|
122
|
-
cursor-route v0.1.
|
|
122
|
+
cursor-route v0.1.16
|
|
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-
|
|
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
|
|
148
|
+
### Mid models (Flash)
|
|
149
149
|
|
|
150
150
|
| Flag | Model id | When |
|
|
151
151
|
|------|----------|------|
|
|
152
|
-
| `--model flash` (default) | `deepseek-
|
|
153
|
-
| `--model vision` | `deepseek-
|
|
154
|
-
| `--model pro` | `deepseek-
|
|
155
|
-
|
|
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 alias — Pro 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 "…" #
|
|
160
|
-
cursor-route start --lane mid --model pro "…" #
|
|
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
|
|
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
|
|
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-
|
|
184
|
-
export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-
|
|
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 "…" #
|
|
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-
|
|
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 "…" #
|
|
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`
|
|
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
|
|
|
@@ -242,9 +243,10 @@ key-shaped material in `start` / `send`, and the runner re-checks the prompt fil
|
|
|
242
243
|
## OpenCode setup (opt-in free coding worker)
|
|
243
244
|
|
|
244
245
|
`--worker opencode` runs [OpenCode](https://opencode.ai) as a **coding agent** on
|
|
245
|
-
live OpenCode Zen free models (`--model free` fetches the catalog and ranks
|
|
246
|
-
same idea as the OpenRouter free picker).
|
|
247
|
-
|
|
246
|
+
live OpenCode Zen free models (`--model free` fetches the catalog and ranks the
|
|
247
|
+
strongest listed-free coding model, same idea as the OpenRouter free picker).
|
|
248
|
+
There is no locked live id. Offline / empty-catalog fallback is
|
|
249
|
+
`opencode/x-preview-f-free`. This is **not** a lane default — mid stays
|
|
248
250
|
`claude-ds`; easy stays OpenRouter chat (no tools). Use it to burn fewer Grok /
|
|
249
251
|
DeepSeek tokens on implement work.
|
|
250
252
|
|
|
@@ -270,7 +272,7 @@ for ~15 minutes (`CURSOR_ROUTE_ZEN_CACHE_MINUTES`).
|
|
|
270
272
|
**Non-secret prompts:** several Zen free models may log or train on prompts during
|
|
271
273
|
their free period (see [OpenCode Zen](https://opencode.ai/docs/zen/) privacy notes).
|
|
272
274
|
The same refuse gate as every lane still applies. `opencode/x-preview-f-free`
|
|
273
|
-
(Ox Alpha) is the zero-retention
|
|
275
|
+
(Ox Alpha) is the zero-retention **fallback** option if you need it.
|
|
274
276
|
|
|
275
277
|
## Jobs directory
|
|
276
278
|
|
package/SECURITY.md
CHANGED
|
@@ -15,7 +15,7 @@ Do **not** open a public issue for credential leaks or RCE-class bugs.
|
|
|
15
15
|
|
|
16
16
|
1. **No secrets in prompts or job files.** Do not paste API keys, SSH private keys, Discord tokens, or `.env` contents into `cursor-route start` / `send` prompts.
|
|
17
17
|
2. **Free OpenRouter easy-lane models may log prompts.** `--lane easy` / `--worker openrouter` is for **non-secret** wording/drafts only. The refuse gate (`start` / `send`, plus the runner's re-check) applies to every lane — never send key-shaped material to the easy lane.
|
|
18
|
-
3. **OpenCode Zen free models may log or train during their free period.** `--worker opencode --model free`
|
|
18
|
+
3. **OpenCode Zen free models may log or train during their free period.** `--worker opencode --model free` ranks the strongest listed-free coding model. Treat free Zen models like the easy lane for secrets. Pin `CURSOR_ROUTE_OPENCODE_MODEL` for a frozen id. Ox Alpha (`opencode/x-preview-f-free`) remains the zero-retention **fallback** option.
|
|
19
19
|
4. **Always-approve is powerful.** Default headless workers skip interactive permission prompts (`--always-approve` / `--dangerously-skip-permissions` / OpenCode `--auto`). Opt out with `--ask` / `CURSOR_ROUTE_ASK=1` when you need HITL.
|
|
20
20
|
5. **Install from known channels only.** Prefer `npm i -g cursor-route` or a git clone of this repo. Do not pipe unknown `curl | bash` installers as the primary path.
|
|
21
21
|
6. **Workers inherit your auth.** Grok CLI, Claude Code / claude-ds, and OpenCode use *your* local login. Treat worker panes like you would treat your own shell.
|
|
@@ -128,8 +128,16 @@ export function midDeepSeekProofDetail() {
|
|
|
128
128
|
}
|
|
129
129
|
function pickModel(requested, modelId) {
|
|
130
130
|
if (modelId) {
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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] };
|
|
@@ -4,9 +4,9 @@ import { shellQuote } from "../util.js";
|
|
|
4
4
|
import { openCodeModel, OPENCODE_DEFAULT_MODEL, cachedZenFreePick } from "../config.js";
|
|
5
5
|
/**
|
|
6
6
|
* Opt-in OpenCode worker (`opencode run`) — a coding agent on live OpenCode
|
|
7
|
-
* Zen free models (`--model free` ranks the
|
|
8
|
-
*
|
|
9
|
-
* chat (no tools).
|
|
7
|
+
* Zen free models (`--model free` ranks the strongest listed-free coding
|
|
8
|
+
* model). Not a lane default: mid stays claude-ds; easy stays OpenRouter
|
|
9
|
+
* chat (no tools). Pin `CURSOR_ROUTE_OPENCODE_MODEL` to freeze an id.
|
|
10
10
|
*
|
|
11
11
|
* We never rewrite ~/.config/opencode/opencode.json (parallel jobs would
|
|
12
12
|
* race). Always-approve maps to `opencode run --auto` (still honors explicit
|
|
@@ -42,7 +42,7 @@ export const opencodeAdapter = {
|
|
|
42
42
|
detail: "opencode not found — install: npm i -g opencode-ai (or brew install opencode). Then: opencode auth login. Mid default remains claude-ds.",
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
// Health stays offline: show a fresh cache hit, else the
|
|
45
|
+
// Health stays offline: show a fresh cache hit, else the fallback id.
|
|
46
46
|
const pick = cachedZenFreePick() || OPENCODE_DEFAULT_MODEL;
|
|
47
47
|
return {
|
|
48
48
|
worker: "opencode",
|
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 (
|
|
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:
|
|
9
|
-
pro:
|
|
10
|
-
vision:
|
|
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 +
|
|
19
|
-
*
|
|
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:
|
|
27
|
+
return { alias: "flash", id: DS_FLASH_ID };
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
38
|
+
return { alias: "pro", id: DS_FLASH_ID };
|
|
32
39
|
}
|
|
33
|
-
if (v === "vision" ||
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return { alias: "
|
|
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.
|
|
82
|
+
version: "0.1.16",
|
|
76
83
|
get jobsDir() {
|
|
77
84
|
return defaultJobsDir();
|
|
78
85
|
},
|
package/dist/zen-free.js
CHANGED
|
@@ -11,12 +11,15 @@ import { join } from "node:path";
|
|
|
11
11
|
* `big-pickle` / `x-preview-f-free`; the CLI wants `opencode/<id>`.
|
|
12
12
|
*/
|
|
13
13
|
export const ZEN_MODELS_URL_DEFAULT = "https://opencode.ai/zen/v1/models";
|
|
14
|
-
/** Offline / fetch-fail fallback —
|
|
14
|
+
/** Offline / fetch-fail fallback only — not a live pin. */
|
|
15
15
|
export const OPENCODE_DEFAULT_MODEL = "opencode/x-preview-f-free";
|
|
16
16
|
const EXCLUDE_RE = /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
17
|
+
const POWER_ULTRA_RE = /ultra|(^|[-/])opus($|[-/])|(^|[-/_])pro($|[-/])|(^|[-/])max($|[-/])|plus|(^|[-/])large($|[-/])/;
|
|
18
|
+
const POWER_PREVIEW_RE = /x-preview|ox-alpha|oxalpha|(^|[-/])alpha($|[-/])|preview/;
|
|
19
|
+
const POWER_FLASH_RE = /flash|lightning|lite|mini|nano|small|haiku|fin-/;
|
|
20
|
+
const CODING_FAMILY_RE = /coder|code|instruct|laguna|glm|qwen|kimi|deepseek|gpt-oss|north|gemma|nemotron|mimo|muse/;
|
|
21
|
+
const CONTRIBUTOR_RE = /contributor-free/;
|
|
22
|
+
const MAY_TRAIN_RE = /(^|\/)big-pickle$|(^|\/)hy3-free$/;
|
|
20
23
|
function zenModelsUrl() {
|
|
21
24
|
return (process.env.CURSOR_ROUTE_ZEN_MODELS_URL || ZEN_MODELS_URL_DEFAULT).trim();
|
|
22
25
|
}
|
|
@@ -65,20 +68,32 @@ export function isZenFreeModel(m) {
|
|
|
65
68
|
/(^|\/)big-pickle$/i.test(id);
|
|
66
69
|
return tagged || pricedFree;
|
|
67
70
|
}
|
|
68
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Higher boost wins. Generic capability tokens only — never a locked model id.
|
|
73
|
+
* Coding-family matches the model slug so the provider `opencode/` does not
|
|
74
|
+
* trip `code`.
|
|
75
|
+
*/
|
|
69
76
|
export function zenFreeBoost(id) {
|
|
70
77
|
const s = id.toLowerCase();
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
const slug = s.includes("/") ? s.slice(s.lastIndexOf("/") + 1) : s;
|
|
79
|
+
let score = 0;
|
|
80
|
+
if (POWER_ULTRA_RE.test(s))
|
|
81
|
+
score += 50;
|
|
82
|
+
else if (POWER_PREVIEW_RE.test(s))
|
|
83
|
+
score += 42;
|
|
84
|
+
else if (POWER_FLASH_RE.test(s))
|
|
85
|
+
score += 15;
|
|
86
|
+
else
|
|
87
|
+
score += 30;
|
|
88
|
+
if (CODING_FAMILY_RE.test(slug))
|
|
89
|
+
score += 20;
|
|
90
|
+
else if (s.endsWith("-free") || s.endsWith(":free"))
|
|
91
|
+
score += 10;
|
|
73
92
|
if (CONTRIBUTOR_RE.test(s))
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return 10;
|
|
79
|
-
if (/(^|\/)big-pickle$/.test(s))
|
|
80
|
-
return 5;
|
|
81
|
-
return 0;
|
|
93
|
+
score -= 25;
|
|
94
|
+
if (MAY_TRAIN_RE.test(s))
|
|
95
|
+
score -= 20;
|
|
96
|
+
return score;
|
|
82
97
|
}
|
|
83
98
|
export function rankZenFreeModels(models) {
|
|
84
99
|
const out = [];
|
package/docs/briefs/WORKING.md
CHANGED
|
@@ -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.
|
|
4
|
+
npm: cursor-route@0.1.16
|
|
5
5
|
created: 2026-08-12
|
|
6
|
-
updated: 2026-09-
|
|
6
|
+
updated: 2026-09-16
|
|
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
|
|
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
|
-
| opt-in | OpenCode | `--worker opencode` coding agent; `--model free` ranks
|
|
24
|
+
| opt-in | OpenCode | `--worker opencode` coding agent; `--model free` ranks strongest listed-free Zen coding model (no locked id; offline fallback `opencode/x-preview-f-free`) |
|
|
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.
|
|
28
|
+
Install: `npm i -g cursor-route` → **0.1.16** (parent publishes). Release notes: [CHANGELOG.md](../../CHANGELOG.md).
|
|
29
29
|
|
|
30
30
|
## Open (edit / check off)
|
|
31
31
|
|
|
@@ -44,7 +44,9 @@ Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](.
|
|
|
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
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. Brief `docs/briefs/2026-09-11_v41-flash-not-pro.md`
|
|
47
48
|
- [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
|
|
49
|
+
- [x] **Zen strongest-free ranker (0.1.16)** — `zenFreeBoost` ports capability tokens (ultra/pro/max > preview > generic > flash/lite), then coding-family, then `context_length`. No locked live id. Ox Alpha is offline fallback only. SIP: `docs/briefs/2026-09-16_zen-strongest-free.md`. Mid still `claude-ds`.
|
|
48
50
|
- [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
|
|
49
51
|
|
|
50
52
|
## Repo map
|
|
@@ -81,4 +83,8 @@ Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](.
|
|
|
81
83
|
| 2026-08-21 | Live Zen free pick (Ox Alpha first while listed; OpenRouter-style catalog rank) → 0.1.11 LIVE. |
|
|
82
84
|
| 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. |
|
|
83
85
|
| 2026-08-29 | Kimi audit follow-up → 0.1.13 LIVE on npm: no fallback cache, health labels, unauth GET /models, env error wording. |
|
|
84
|
-
| 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`.
|
|
86
|
+
| 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. |
|
|
87
|
+
| 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. |
|
|
88
|
+
| 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 LIVE** on npm. |
|
|
89
|
+
| 2026-09-16 | Pickup brief: `docs/briefs/2026-09-16_zen-strongest-free.md`. Port private `/route` Zen capability-token ranker so `--model free` picks strongest listed-free coding model. Target npm `0.1.16`. Open checkbox added. |
|
|
90
|
+
| 2026-09-16 | Ship Zen strongest-free ranker → **0.1.16** (commit + tag; parent publishes npm). Ox Alpha is offline fallback only, not a live pin. Mid still `claude-ds`. |
|
package/docs/demo-notes.md
CHANGED
|
@@ -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.
|
|
9
|
+
0.1.16
|
|
10
10
|
|
|
11
11
|
$ cursor-route health
|
|
12
|
-
cursor-route v0.1.
|
|
12
|
+
cursor-route v0.1.16
|
|
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`
|
|
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.
|
|
2
|
+
0.1.16
|
|
3
3
|
|
|
4
4
|
$ CURSOR_ROUTE_RELAXED=1 cursor-route health
|
|
5
|
-
cursor-route v0.1.
|
|
5
|
+
cursor-route v0.1.16
|
|
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-
|
|
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-
|
|
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-
|
|
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,30 +2,31 @@
|
|
|
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.
|
|
5
|
+
MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.16**)
|
|
6
6
|
GitHub: https://github.com/cemini23/cursor-route
|
|
7
7
|
|
|
8
8
|
## FAQ
|
|
9
9
|
|
|
10
10
|
### What is cursor-route?
|
|
11
|
-
cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only). `--worker opencode` is an opt-in coding agent (`--model free` ranks the
|
|
11
|
+
cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only). `--worker opencode` is an opt-in coding agent (`--model free` ranks the strongest listed-free Zen coding model; pin to freeze) — not a lane default.
|
|
12
12
|
|
|
13
13
|
### How is this different from Codex orchestrator?
|
|
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`
|
|
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-
|
|
23
|
-
| `--model vision` | `deepseek-
|
|
24
|
-
| `--model pro` | `deepseek-
|
|
25
|
-
|
|
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 alias — Pro 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
|
-
Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. `--model free` ranks
|
|
29
|
+
Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. `--model free` ranks the strongest listed-free Zen coding model (offline fallback `opencode/x-preview-f-free`). Mid stays `claude-ds`.
|
|
29
30
|
|
|
30
31
|
### How do I install?
|
|
31
32
|
Run `npm i -g cursor-route`, install tmux if needed, then run `cursor-route health`. Copy the Cursor skill from `$(npm root -g)/cursor-route/skills/route-orch`. Source: https://github.com/cemini23/cursor-route
|
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "0.1.16",
|
|
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",
|
|
@@ -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-
|
|
40
|
-
| `--model vision` | `deepseek-
|
|
41
|
-
| `--model pro` | `deepseek-
|
|
42
|
-
|
|
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 alias — Pro 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
|
|
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
|
|
|
@@ -74,7 +75,7 @@ cursor-route start --worker opencode --model free --dir "$PWD" "…"
|
|
|
74
75
|
cursor-route start --worker opencode --model opencode/hy3-free --dir "$PWD" "…"
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
Health ✓ needs `opencode` on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`). `--model free` / unset ranks the
|
|
78
|
+
Health ✓ needs `opencode` on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`). `--model free` / unset ranks the strongest listed-free OpenCode Zen coding model (pin with `CURSOR_ROUTE_OPENCODE_MODEL` or `--model provider/model` to freeze). Always-approve → `opencode run --auto`; `--ask` omits `--auto`. Never rewrites `~/.config/opencode/opencode.json`. Free Zen models may log/train — keep secrets off this worker (same refuse gate as easy). Ox Alpha (`opencode/x-preview-f-free`) is the zero-retention fallback option.
|
|
78
79
|
|
|
79
80
|
## Workflow
|
|
80
81
|
|
|
@@ -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
|
-
|
|
149
|
-
|
|
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] };
|
package/src/adapters/deepseek.ts
CHANGED
|
@@ -43,8 +43,14 @@ function pickModel(
|
|
|
43
43
|
modelId?: string,
|
|
44
44
|
): { alias: DsModelAlias; id: string } {
|
|
45
45
|
if (modelId) {
|
|
46
|
-
|
|
47
|
-
|
|
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] };
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "../config.ts";
|
|
12
12
|
|
|
13
13
|
describe("openCodeModel", () => {
|
|
14
|
-
test("defaults to live Zen pick (
|
|
14
|
+
test("defaults to live Zen pick (fallback id when offline)", () => {
|
|
15
15
|
const prev = {
|
|
16
16
|
model: process.env.CURSOR_ROUTE_OPENCODE_MODEL,
|
|
17
17
|
offline: process.env.CURSOR_ROUTE_ZEN_OFFLINE,
|
package/src/adapters/opencode.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { openCodeModel, OPENCODE_DEFAULT_MODEL, cachedZenFreePick } from "../con
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Opt-in OpenCode worker (`opencode run`) — a coding agent on live OpenCode
|
|
9
|
-
* Zen free models (`--model free` ranks the
|
|
10
|
-
*
|
|
11
|
-
* chat (no tools).
|
|
9
|
+
* Zen free models (`--model free` ranks the strongest listed-free coding
|
|
10
|
+
* model). Not a lane default: mid stays claude-ds; easy stays OpenRouter
|
|
11
|
+
* chat (no tools). Pin `CURSOR_ROUTE_OPENCODE_MODEL` to freeze an id.
|
|
12
12
|
*
|
|
13
13
|
* We never rewrite ~/.config/opencode/opencode.json (parallel jobs would
|
|
14
14
|
* race). Always-approve maps to `opencode run --auto` (still honors explicit
|
|
@@ -47,7 +47,7 @@ export const opencodeAdapter: Adapter = {
|
|
|
47
47
|
"opencode not found — install: npm i -g opencode-ai (or brew install opencode). Then: opencode auth login. Mid default remains claude-ds.",
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
// Health stays offline: show a fresh cache hit, else the
|
|
50
|
+
// Health stays offline: show a fresh cache hit, else the fallback id.
|
|
51
51
|
const pick = cachedZenFreePick() || OPENCODE_DEFAULT_MODEL;
|
|
52
52
|
return {
|
|
53
53
|
worker: "opencode",
|
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-
|
|
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")
|
|
60
|
-
expect(resolveDsModel("
|
|
61
|
-
expect(resolveDsModel("
|
|
62
|
-
expect(resolveDsModel("deepseek-v4-flash
|
|
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-
|
|
71
|
+
id: "deepseek-flash",
|
|
65
72
|
});
|
|
66
|
-
expect(resolveDsModel("deepseek-v4-
|
|
67
|
-
alias: "
|
|
68
|
-
id: "deepseek-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
148
|
-
expect(plan.command).not.toContain("deepseek-v4-
|
|
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("
|
|
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-
|
|
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-
|
|
236
|
-
expect(result.command).not.toContain("deepseek-v4-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
661
|
-
expect(patch).not.toContain("deepseek-v4-
|
|
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:
|
|
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-
|
|
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.
|
|
938
|
+
expect(r.version).toBe("0.1.16");
|
|
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.
|
|
895
|
-
expect(config.version).toBe("0.1.
|
|
950
|
+
test("config version is 0.1.16", () => {
|
|
951
|
+
expect(config.version).toBe("0.1.16");
|
|
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 (
|
|
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:
|
|
28
|
-
pro:
|
|
29
|
-
vision:
|
|
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 +
|
|
42
|
-
*
|
|
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:
|
|
51
|
+
return { alias: "flash", id: DS_FLASH_ID };
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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:
|
|
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 (
|
|
60
|
-
|
|
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.
|
|
126
|
+
version: "0.1.16",
|
|
115
127
|
get jobsDir(): string {
|
|
116
128
|
return defaultJobsDir();
|
|
117
129
|
},
|
package/src/zen-free.test.ts
CHANGED
|
@@ -38,30 +38,91 @@ describe("zen free catalog", () => {
|
|
|
38
38
|
).toBe(true);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test("
|
|
42
|
-
expect(zenFreeBoost("opencode/
|
|
43
|
-
expect(zenFreeBoost("opencode/
|
|
41
|
+
test("capability tokens: ultra > preview > generic coding > flash > may-train", () => {
|
|
42
|
+
expect(zenFreeBoost("opencode/nemotron-3-ultra-free")).toBe(70);
|
|
43
|
+
expect(zenFreeBoost("opencode/x-preview-f-free")).toBe(52);
|
|
44
|
+
expect(zenFreeBoost("opencode/laguna-s-2.1-free")).toBe(50);
|
|
45
|
+
expect(zenFreeBoost("opencode/deepseek-v4-flash-free")).toBe(35);
|
|
46
|
+
expect(zenFreeBoost("opencode/muse-spark-1.2-contributor-free")).toBe(25);
|
|
44
47
|
expect(zenFreeBoost("opencode/hy3-free")).toBe(20);
|
|
45
|
-
expect(zenFreeBoost("opencode/big-pickle")).toBe(
|
|
46
|
-
expect(zenFreeBoost("opencode/muse-spark-1.2-contributor-free")).toBe(1);
|
|
48
|
+
expect(zenFreeBoost("opencode/big-pickle")).toBe(10);
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
test("
|
|
51
|
+
test("relative boosts: ultra > preview > flash > big-pickle", () => {
|
|
52
|
+
expect(zenFreeBoost("opencode/nemotron-3-ultra-free")).toBeGreaterThan(
|
|
53
|
+
zenFreeBoost("opencode/x-preview-f-free"),
|
|
54
|
+
);
|
|
55
|
+
expect(zenFreeBoost("opencode/x-preview-f-free")).toBeGreaterThan(
|
|
56
|
+
zenFreeBoost("opencode/deepseek-v4-flash-free"),
|
|
57
|
+
);
|
|
58
|
+
expect(zenFreeBoost("opencode/deepseek-v4-flash-free")).toBeGreaterThan(
|
|
59
|
+
zenFreeBoost("opencode/big-pickle"),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("hy3-free is may-train, not coding-family", () => {
|
|
64
|
+
// else 30 + -free 10 − 20 may-train = 20. Coding-family would be 30.
|
|
65
|
+
expect(zenFreeBoost("opencode/hy3-free")).toBe(20);
|
|
66
|
+
expect(zenFreeBoost("opencode/hy3-free")).toBeLessThan(
|
|
67
|
+
zenFreeBoost("opencode/laguna-s-2.1-free"),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("rank: paid/whisper dropped; preview may beat laguna (52 > 50)", () => {
|
|
50
72
|
const ranked = rankZenFreeModels(CATALOG);
|
|
51
73
|
expect(ranked[0]?.id).toBe("opencode/x-preview-f-free");
|
|
52
74
|
expect(ranked.map((r) => r.id)).not.toContain("opencode/claude-opus-4-6");
|
|
53
75
|
expect(ranked.map((r) => r.id)).not.toContain("opencode/whisper-free");
|
|
54
|
-
expect(ranked.at(-1)?.id).toBe("opencode/
|
|
76
|
+
expect(ranked.at(-1)?.id).toBe("opencode/big-pickle");
|
|
55
77
|
});
|
|
56
78
|
|
|
57
|
-
test("rank without
|
|
79
|
+
test("rank without preview: coding free beats big-pickle", () => {
|
|
58
80
|
const ranked = rankZenFreeModels([
|
|
59
81
|
{ id: "big-pickle" },
|
|
60
82
|
{ id: "laguna-s-2.1-free" },
|
|
61
83
|
{ id: "muse-spark-1.2-contributor-free" },
|
|
62
84
|
]);
|
|
63
85
|
expect(ranked[0]?.id).toBe("opencode/laguna-s-2.1-free");
|
|
64
|
-
expect(ranked[1]?.id).toBe("opencode/
|
|
86
|
+
expect(ranked[1]?.id).toBe("opencode/muse-spark-1.2-contributor-free");
|
|
87
|
+
expect(ranked[2]?.id).toBe("opencode/big-pickle");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("hy3 + preview + big-pickle → preview", () => {
|
|
91
|
+
expect(
|
|
92
|
+
pickZenFreeModel({
|
|
93
|
+
catalog: [{ id: "hy3-free" }, { id: "x-preview-f-free" }, { id: "big-pickle" }],
|
|
94
|
+
}),
|
|
95
|
+
).toBe("opencode/x-preview-f-free");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("hy3 + big-pickle + flash → flash coding", () => {
|
|
99
|
+
expect(
|
|
100
|
+
pickZenFreeModel({
|
|
101
|
+
catalog: [
|
|
102
|
+
{ id: "hy3-free" },
|
|
103
|
+
{ id: "big-pickle" },
|
|
104
|
+
{ id: "deepseek-v4-flash-free" },
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
).toBe("opencode/deepseek-v4-flash-free");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("flash + ultra + preview → ultra", () => {
|
|
111
|
+
expect(
|
|
112
|
+
pickZenFreeModel({
|
|
113
|
+
catalog: [
|
|
114
|
+
{ id: "deepseek-v4-flash-free" },
|
|
115
|
+
{ id: "nemotron-3-ultra-free" },
|
|
116
|
+
{ id: "x-preview-f-free" },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
).toBe("opencode/nemotron-3-ultra-free");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("only big-pickle → big-pickle", () => {
|
|
123
|
+
expect(pickZenFreeModel({ catalog: [{ id: "big-pickle" }] })).toBe(
|
|
124
|
+
"opencode/big-pickle",
|
|
125
|
+
);
|
|
65
126
|
});
|
|
66
127
|
|
|
67
128
|
test("asOpenCodeZenId prefixes bare Zen ids", () => {
|
|
@@ -73,8 +134,11 @@ describe("zen free catalog", () => {
|
|
|
73
134
|
expect(pickZenFreeModel({ catalog: CATALOG })).toBe("opencode/x-preview-f-free");
|
|
74
135
|
});
|
|
75
136
|
|
|
76
|
-
test("offline / empty catalog falls back to Ox Alpha", () => {
|
|
137
|
+
test("offline / empty / paid-only catalog falls back to Ox Alpha", () => {
|
|
77
138
|
expect(pickZenFreeModel({ catalog: [] })).toBe(OPENCODE_DEFAULT_MODEL);
|
|
139
|
+
expect(pickZenFreeModel({ catalog: [{ id: "claude-opus-4-6" }] })).toBe(
|
|
140
|
+
OPENCODE_DEFAULT_MODEL,
|
|
141
|
+
);
|
|
78
142
|
expect(OPENCODE_DEFAULT_MODEL).toBe("opencode/x-preview-f-free");
|
|
79
143
|
});
|
|
80
144
|
});
|
|
@@ -119,10 +183,16 @@ describe("openCodeModel live pick", () => {
|
|
|
119
183
|
});
|
|
120
184
|
});
|
|
121
185
|
|
|
122
|
-
test("free + injected catalog →
|
|
186
|
+
test("free + injected catalog → strongest listed-free (ultra)", () => {
|
|
123
187
|
isolate(() => {
|
|
124
|
-
process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON = JSON.stringify({
|
|
125
|
-
|
|
188
|
+
process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON = JSON.stringify({
|
|
189
|
+
data: [
|
|
190
|
+
{ id: "deepseek-v4-flash-free" },
|
|
191
|
+
{ id: "nemotron-3-ultra-free" },
|
|
192
|
+
{ id: "x-preview-f-free" },
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
expect(openCodeModel("free")).toBe("opencode/nemotron-3-ultra-free");
|
|
126
196
|
});
|
|
127
197
|
});
|
|
128
198
|
|
package/src/zen-free.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
|
|
15
15
|
export const ZEN_MODELS_URL_DEFAULT = "https://opencode.ai/zen/v1/models";
|
|
16
16
|
|
|
17
|
-
/** Offline / fetch-fail fallback —
|
|
17
|
+
/** Offline / fetch-fail fallback only — not a live pin. */
|
|
18
18
|
export const OPENCODE_DEFAULT_MODEL = "opencode/x-preview-f-free";
|
|
19
19
|
|
|
20
20
|
export interface ZenModel {
|
|
@@ -36,10 +36,14 @@ export interface ZenFreePick {
|
|
|
36
36
|
|
|
37
37
|
const EXCLUDE_RE =
|
|
38
38
|
/lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
|
|
39
|
-
const
|
|
40
|
-
/
|
|
41
|
-
const
|
|
42
|
-
const
|
|
39
|
+
const POWER_ULTRA_RE =
|
|
40
|
+
/ultra|(^|[-/])opus($|[-/])|(^|[-/_])pro($|[-/])|(^|[-/])max($|[-/])|plus|(^|[-/])large($|[-/])/;
|
|
41
|
+
const POWER_PREVIEW_RE = /x-preview|ox-alpha|oxalpha|(^|[-/])alpha($|[-/])|preview/;
|
|
42
|
+
const POWER_FLASH_RE = /flash|lightning|lite|mini|nano|small|haiku|fin-/;
|
|
43
|
+
const CODING_FAMILY_RE =
|
|
44
|
+
/coder|code|instruct|laguna|glm|qwen|kimi|deepseek|gpt-oss|north|gemma|nemotron|mimo|muse/;
|
|
45
|
+
const CONTRIBUTOR_RE = /contributor-free/;
|
|
46
|
+
const MAY_TRAIN_RE = /(^|\/)big-pickle$|(^|\/)hy3-free$/;
|
|
43
47
|
|
|
44
48
|
function zenModelsUrl(): string {
|
|
45
49
|
return (process.env.CURSOR_ROUTE_ZEN_MODELS_URL || ZEN_MODELS_URL_DEFAULT).trim();
|
|
@@ -94,15 +98,28 @@ export function isZenFreeModel(m: ZenModel): boolean {
|
|
|
94
98
|
return tagged || pricedFree;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Higher boost wins. Generic capability tokens only — never a locked model id.
|
|
103
|
+
* Coding-family matches the model slug so the provider `opencode/` does not
|
|
104
|
+
* trip `code`.
|
|
105
|
+
*/
|
|
98
106
|
export function zenFreeBoost(id: string): number {
|
|
99
107
|
const s = id.toLowerCase();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
if (
|
|
105
|
-
|
|
108
|
+
const slug = s.includes("/") ? s.slice(s.lastIndexOf("/") + 1) : s;
|
|
109
|
+
let score = 0;
|
|
110
|
+
|
|
111
|
+
if (POWER_ULTRA_RE.test(s)) score += 50;
|
|
112
|
+
else if (POWER_PREVIEW_RE.test(s)) score += 42;
|
|
113
|
+
else if (POWER_FLASH_RE.test(s)) score += 15;
|
|
114
|
+
else score += 30;
|
|
115
|
+
|
|
116
|
+
if (CODING_FAMILY_RE.test(slug)) score += 20;
|
|
117
|
+
else if (s.endsWith("-free") || s.endsWith(":free")) score += 10;
|
|
118
|
+
|
|
119
|
+
if (CONTRIBUTOR_RE.test(s)) score -= 25;
|
|
120
|
+
if (MAY_TRAIN_RE.test(s)) score -= 20;
|
|
121
|
+
|
|
122
|
+
return score;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
export function rankZenFreeModels(models: ZenModel[]): Array<{
|