cursor-route 0.1.10 → 0.1.13
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 +23 -0
- package/CONTRIBUTING.md +3 -2
- package/README.md +44 -28
- package/SECURITY.md +1 -1
- package/dist/adapters/claude-ds.js +1 -1
- package/dist/adapters/opencode.js +8 -13
- package/dist/adapters/openrouter.js +13 -7
- package/dist/cli.js +26 -9
- package/dist/config.js +14 -34
- package/dist/jobs.js +50 -14
- package/dist/openrouter-run.js +1 -1
- package/dist/or-free.js +216 -0
- package/dist/zen-free.js +237 -0
- package/docs/briefs/WORKING.md +17 -9
- package/docs/demo-notes.md +3 -3
- package/docs/fixtures/generate-hero-demo.sh +3 -0
- package/docs/fixtures/hero-demo.log +5 -3
- package/llms.txt +10 -8
- package/package.json +1 -1
- package/skills/route-orch/SKILL.md +13 -8
- package/src/adapters/claude-ds.ts +1 -1
- package/src/adapters/opencode.test.ts +84 -25
- package/src/adapters/opencode.ts +8 -12
- package/src/adapters/openrouter.test.ts +32 -3
- package/src/adapters/openrouter.ts +17 -7
- package/src/adapters/types.ts +2 -2
- package/src/cli.test.ts +266 -5
- package/src/cli.ts +24 -8
- package/src/config.ts +28 -38
- package/src/jobs.ts +47 -15
- package/src/openrouter-run.ts +1 -1
- package/src/or-free.test.ts +222 -0
- package/src/or-free.ts +243 -0
- package/src/zen-free.test.ts +137 -0
- package/src/zen-free.ts +258 -0
|
@@ -24,27 +24,30 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
|
|
|
24
24
|
|
|
25
25
|
| Lane | Worker | Use when |
|
|
26
26
|
|------|--------|----------|
|
|
27
|
-
| `easy` | `openrouter` (OpenRouter free
|
|
27
|
+
| `easy` | `openrouter` (live OpenRouter free pick) | Wording / drafts — non-secret prompts only |
|
|
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).
|
|
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.
|
|
32
32
|
|
|
33
33
|
## claude-ds models
|
|
34
34
|
|
|
35
35
|
One harness. Do not install a second coding loop.
|
|
36
36
|
|
|
37
|
-
| Flag
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
|
37
|
+
| Flag | Model id | When |
|
|
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 |
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
45
|
cursor-route start --lane mid --dir "$PWD" "…"
|
|
46
|
+
cursor-route start --lane mid --model vision --dir "$PWD" "…"
|
|
44
47
|
cursor-route start --lane mid --model pro --dir "$PWD" "…"
|
|
45
48
|
```
|
|
46
49
|
|
|
47
|
-
If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro
|
|
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.
|
|
48
51
|
|
|
49
52
|
## Experimental: --worker deepseek (dsh)
|
|
50
53
|
|
|
@@ -71,7 +74,7 @@ cursor-route start --worker opencode --model free --dir "$PWD" "…"
|
|
|
71
74
|
cursor-route start --worker opencode --model opencode/hy3-free --dir "$PWD" "…"
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
Health ✓ needs `opencode` on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`).
|
|
77
|
+
Health ✓ needs `opencode` on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`). `--model free` / unset ranks the live OpenCode Zen catalog (Ox Alpha first while listed; pin with `CURSOR_ROUTE_OPENCODE_MODEL` or `--model provider/model`). 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 free option.
|
|
75
78
|
|
|
76
79
|
## Workflow
|
|
77
80
|
|
|
@@ -116,6 +119,8 @@ Verify criteria are an **external eval contract** (AutoDesign pattern), fixed by
|
|
|
116
119
|
- **External eval contract (AutoDesign):** do not rewrite Verify / Success criteria mid-run to make a failing job look green — capture + exit status are the contract (see Verify / claim closeout).
|
|
117
120
|
- **Skill misevolution:** do not auto-edit `route-orch` or promote skill variants from worker trajectories without operator HITL — write-time approval ≠ safe retrieval later.
|
|
118
121
|
- **On verify fail:** prefer reconsidering the plan/definition (wrong approach) over grinding the same tactic; attribute failure to stage when possible (spawn vs execute vs verify).
|
|
122
|
+
- **Step-wise re-route (ProgRouter):** `--lane` is the first pick. If progress stalls or verify fails, `send` a correction or spawn a follow-up on a different worker/model — do not lock the first worker for the whole job. Parent still closes on capture/exit evidence.
|
|
123
|
+
- **Do not auto-spawn N panes for multi-perspective:** spawn workers for **implementation parallelism** (independent files/jobs). Multi-perspective reasoning stays in the Cursor parent. Do not fan out “one agent per viewpoint.”
|
|
119
124
|
|
|
120
125
|
## Always-approve
|
|
121
126
|
|
|
@@ -212,7 +212,7 @@ export const claudeDsAdapter: Adapter = {
|
|
|
212
212
|
resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude");
|
|
213
213
|
|
|
214
214
|
// Anthropic escape hatch: do not pass DeepSeek model ids (unknown to Anthropic).
|
|
215
|
-
// --model flash|pro is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
|
|
215
|
+
// --model flash|pro|vision is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
|
|
216
216
|
if (isAnthropicEscape) {
|
|
217
217
|
const parts = [
|
|
218
218
|
shellQuote(resolved.binary),
|
|
@@ -11,16 +11,36 @@ import {
|
|
|
11
11
|
} from "../config.ts";
|
|
12
12
|
|
|
13
13
|
describe("openCodeModel", () => {
|
|
14
|
-
test("defaults to
|
|
15
|
-
const prev =
|
|
14
|
+
test("defaults to live Zen pick (Ox Alpha fallback when offline)", () => {
|
|
15
|
+
const prev = {
|
|
16
|
+
model: process.env.CURSOR_ROUTE_OPENCODE_MODEL,
|
|
17
|
+
offline: process.env.CURSOR_ROUTE_ZEN_OFFLINE,
|
|
18
|
+
cache: process.env.CURSOR_ROUTE_ZEN_CACHE_PATH,
|
|
19
|
+
json: process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON,
|
|
20
|
+
refresh: process.env.CURSOR_ROUTE_ZEN_REFRESH,
|
|
21
|
+
};
|
|
22
|
+
const cache = join(tmpdir(), `cr-oc-model-${process.pid}.json`);
|
|
16
23
|
delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
24
|
+
delete process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON;
|
|
25
|
+
process.env.CURSOR_ROUTE_ZEN_OFFLINE = "1";
|
|
26
|
+
process.env.CURSOR_ROUTE_ZEN_CACHE_PATH = cache;
|
|
27
|
+
process.env.CURSOR_ROUTE_ZEN_REFRESH = "1";
|
|
17
28
|
try {
|
|
18
|
-
expect(openCodeModel()).toBe("opencode/
|
|
29
|
+
expect(openCodeModel()).toBe("opencode/x-preview-f-free");
|
|
19
30
|
expect(openCodeModel("")).toBe(OPENCODE_DEFAULT_MODEL);
|
|
20
|
-
expect(openCodeModel("free")).toBe("opencode/
|
|
31
|
+
expect(openCodeModel("free")).toBe("opencode/x-preview-f-free");
|
|
21
32
|
} finally {
|
|
22
|
-
if (prev === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
23
|
-
else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev;
|
|
33
|
+
if (prev.model === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
34
|
+
else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev.model;
|
|
35
|
+
if (prev.offline === undefined) delete process.env.CURSOR_ROUTE_ZEN_OFFLINE;
|
|
36
|
+
else process.env.CURSOR_ROUTE_ZEN_OFFLINE = prev.offline;
|
|
37
|
+
if (prev.cache === undefined) delete process.env.CURSOR_ROUTE_ZEN_CACHE_PATH;
|
|
38
|
+
else process.env.CURSOR_ROUTE_ZEN_CACHE_PATH = prev.cache;
|
|
39
|
+
if (prev.json === undefined) delete process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON;
|
|
40
|
+
else process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON = prev.json;
|
|
41
|
+
if (prev.refresh === undefined) delete process.env.CURSOR_ROUTE_ZEN_REFRESH;
|
|
42
|
+
else process.env.CURSOR_ROUTE_ZEN_REFRESH = prev.refresh;
|
|
43
|
+
rmSync(cache, { force: true });
|
|
24
44
|
}
|
|
25
45
|
});
|
|
26
46
|
|
|
@@ -100,14 +120,21 @@ describe("opencode adapter", () => {
|
|
|
100
120
|
});
|
|
101
121
|
|
|
102
122
|
test("health: binary → ok (auth at first start)", () => {
|
|
103
|
-
const { bin } = makeFake("ok");
|
|
104
|
-
withEnv(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
const { dir, bin } = makeFake("ok");
|
|
124
|
+
withEnv(
|
|
125
|
+
{
|
|
126
|
+
CURSOR_ROUTE_OPENCODE_BIN: bin,
|
|
127
|
+
CURSOR_ROUTE_ZEN_CACHE_PATH: join(dir, "missing-cache.json"),
|
|
128
|
+
},
|
|
129
|
+
() => {
|
|
130
|
+
const h = opencodeAdapter.health();
|
|
131
|
+
expect(h.ok).toBe(true);
|
|
132
|
+
expect(h.binary).toBe(bin);
|
|
133
|
+
expect(h.detail).toContain("live Zen pick");
|
|
134
|
+
expect(h.detail).toContain("opencode/x-preview-f-free");
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
rmSync(dir, { recursive: true, force: true });
|
|
111
138
|
});
|
|
112
139
|
|
|
113
140
|
test("buildLaunch: default free model + --auto; prompt via cat", () => {
|
|
@@ -119,6 +146,10 @@ describe("opencode adapter", () => {
|
|
|
119
146
|
CURSOR_ROUTE_OPENCODE_BIN: bin,
|
|
120
147
|
CURSOR_ROUTE_OPENCODE_MODEL: undefined,
|
|
121
148
|
CURSOR_ROUTE_ASK: undefined,
|
|
149
|
+
CURSOR_ROUTE_ZEN_OFFLINE: "1",
|
|
150
|
+
CURSOR_ROUTE_ZEN_CACHE_PATH: join(dir, "zen-cache.json"),
|
|
151
|
+
CURSOR_ROUTE_ZEN_REFRESH: "1",
|
|
152
|
+
CURSOR_ROUTE_ZEN_CATALOG_JSON: undefined,
|
|
122
153
|
},
|
|
123
154
|
() => {
|
|
124
155
|
const plan = opencodeAdapter.buildLaunch({
|
|
@@ -128,7 +159,7 @@ describe("opencode adapter", () => {
|
|
|
128
159
|
});
|
|
129
160
|
expect(plan.command).toContain("run");
|
|
130
161
|
expect(plan.command).toContain("--auto");
|
|
131
|
-
expect(plan.command).toContain("opencode/
|
|
162
|
+
expect(plan.command).toContain("opencode/x-preview-f-free");
|
|
132
163
|
expect(plan.command).toContain("$(cat");
|
|
133
164
|
expect(plan.command).toContain("--dir");
|
|
134
165
|
expect(plan.alwaysApprove).toBe(true);
|
|
@@ -160,15 +191,24 @@ describe("opencode adapter", () => {
|
|
|
160
191
|
const { dir, bin } = makeFake("ask-env");
|
|
161
192
|
const promptFile = join(dir, "job.prompt");
|
|
162
193
|
writeFileSync(promptFile, "ping");
|
|
163
|
-
withEnv(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
194
|
+
withEnv(
|
|
195
|
+
{
|
|
196
|
+
CURSOR_ROUTE_OPENCODE_BIN: bin,
|
|
197
|
+
CURSOR_ROUTE_ASK: "1",
|
|
198
|
+
CURSOR_ROUTE_ZEN_OFFLINE: "1",
|
|
199
|
+
CURSOR_ROUTE_ZEN_CACHE_PATH: join(dir, "zen-cache.json"),
|
|
200
|
+
CURSOR_ROUTE_ZEN_REFRESH: "1",
|
|
201
|
+
},
|
|
202
|
+
() => {
|
|
203
|
+
const plan = opencodeAdapter.buildLaunch({
|
|
204
|
+
promptFile,
|
|
205
|
+
cwd: dir,
|
|
206
|
+
alwaysApprove: true,
|
|
207
|
+
});
|
|
208
|
+
expect(plan.command).not.toContain("--auto");
|
|
209
|
+
expect(plan.alwaysApprove).toBe(false);
|
|
210
|
+
},
|
|
211
|
+
);
|
|
172
212
|
rmSync(dir, { recursive: true, force: true });
|
|
173
213
|
});
|
|
174
214
|
|
|
@@ -183,6 +223,9 @@ describe("opencode adapter", () => {
|
|
|
183
223
|
CURSOR_ROUTE_OPENCODE_BIN: undefined,
|
|
184
224
|
PATH: empty,
|
|
185
225
|
CURSOR_ROUTE_OPENCODE_MODEL: undefined,
|
|
226
|
+
CURSOR_ROUTE_ZEN_OFFLINE: "1",
|
|
227
|
+
CURSOR_ROUTE_ZEN_CACHE_PATH: join(dir, "zen-cache.json"),
|
|
228
|
+
CURSOR_ROUTE_ZEN_REFRESH: "1",
|
|
186
229
|
},
|
|
187
230
|
() => {
|
|
188
231
|
const plan = opencodeAdapter.buildLaunch({
|
|
@@ -208,10 +251,18 @@ describe("opencode adapter", () => {
|
|
|
208
251
|
bin: process.env.CURSOR_ROUTE_OPENCODE_BIN,
|
|
209
252
|
model: process.env.CURSOR_ROUTE_OPENCODE_MODEL,
|
|
210
253
|
jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
|
|
254
|
+
offline: process.env.CURSOR_ROUTE_ZEN_OFFLINE,
|
|
255
|
+
cache: process.env.CURSOR_ROUTE_ZEN_CACHE_PATH,
|
|
256
|
+
refresh: process.env.CURSOR_ROUTE_ZEN_REFRESH,
|
|
257
|
+
json: process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON,
|
|
211
258
|
};
|
|
212
259
|
process.env.CURSOR_ROUTE_OPENCODE_BIN = bin;
|
|
213
260
|
delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
261
|
+
delete process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON;
|
|
214
262
|
process.env.CURSOR_ROUTE_JOBS_DIR = join(dir, "jobs");
|
|
263
|
+
process.env.CURSOR_ROUTE_ZEN_OFFLINE = "1";
|
|
264
|
+
process.env.CURSOR_ROUTE_ZEN_CACHE_PATH = join(dir, "zen-cache.json");
|
|
265
|
+
process.env.CURSOR_ROUTE_ZEN_REFRESH = "1";
|
|
215
266
|
try {
|
|
216
267
|
const result = startJob({
|
|
217
268
|
prompt: "ping",
|
|
@@ -221,7 +272,7 @@ describe("opencode adapter", () => {
|
|
|
221
272
|
expect(result.ok).toBe(true);
|
|
222
273
|
if (!result.ok) return;
|
|
223
274
|
expect(result.job.worker).toBe("opencode");
|
|
224
|
-
expect(result.job.model).toBe("opencode/
|
|
275
|
+
expect(result.job.model).toBe("opencode/x-preview-f-free");
|
|
225
276
|
expect(result.command).toContain("run");
|
|
226
277
|
expect(result.command).toContain("--auto");
|
|
227
278
|
expect(result.command).toContain("$(cat");
|
|
@@ -232,6 +283,14 @@ describe("opencode adapter", () => {
|
|
|
232
283
|
else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev.model;
|
|
233
284
|
if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
|
|
234
285
|
else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
|
|
286
|
+
if (prev.offline === undefined) delete process.env.CURSOR_ROUTE_ZEN_OFFLINE;
|
|
287
|
+
else process.env.CURSOR_ROUTE_ZEN_OFFLINE = prev.offline;
|
|
288
|
+
if (prev.cache === undefined) delete process.env.CURSOR_ROUTE_ZEN_CACHE_PATH;
|
|
289
|
+
else process.env.CURSOR_ROUTE_ZEN_CACHE_PATH = prev.cache;
|
|
290
|
+
if (prev.refresh === undefined) delete process.env.CURSOR_ROUTE_ZEN_REFRESH;
|
|
291
|
+
else process.env.CURSOR_ROUTE_ZEN_REFRESH = prev.refresh;
|
|
292
|
+
if (prev.json === undefined) delete process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON;
|
|
293
|
+
else process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON = prev.json;
|
|
235
294
|
rmSync(dir, { recursive: true, force: true });
|
|
236
295
|
}
|
|
237
296
|
});
|
package/src/adapters/opencode.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import type { Adapter, WorkerHealth } from "./types.ts";
|
|
4
4
|
import { shellQuote } from "../util.ts";
|
|
5
|
-
import { openCodeModel, OPENCODE_DEFAULT_MODEL } from "../config.ts";
|
|
5
|
+
import { openCodeModel, OPENCODE_DEFAULT_MODEL, cachedZenFreePick } from "../config.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Opt-in OpenCode worker (`opencode run`) — a coding agent
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Opt-in OpenCode worker (`opencode run`) — a coding agent on live OpenCode
|
|
9
|
+
* Zen free models (`--model free` ranks the catalog; Ox Alpha wins while
|
|
10
|
+
* listed). Not a lane default: mid stays claude-ds; easy stays OpenRouter
|
|
11
|
+
* chat (no tools).
|
|
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,17 +47,13 @@ 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
|
-
|
|
51
|
-
|
|
52
|
-
defaultModel = openCodeModel();
|
|
53
|
-
} catch {
|
|
54
|
-
/* invalid env — health still ok; startJob will fail loud */
|
|
55
|
-
}
|
|
50
|
+
// Health stays offline: show a fresh cache hit, else the Ox Alpha fallback.
|
|
51
|
+
const pick = cachedZenFreePick() || OPENCODE_DEFAULT_MODEL;
|
|
56
52
|
return {
|
|
57
53
|
worker: "opencode",
|
|
58
54
|
ok: true,
|
|
59
55
|
binary,
|
|
60
|
-
detail: `ok (opencode run;
|
|
56
|
+
detail: `ok (opencode run; --model free = live Zen pick, now ${pick}; auth at first start — opencode auth login if jobs fail; mid default remains claude-ds)`,
|
|
61
57
|
};
|
|
62
58
|
},
|
|
63
59
|
buildLaunch({ promptFile, cwd, alwaysApprove, modelId }) {
|
|
@@ -11,6 +11,10 @@ afterEach(() => {
|
|
|
11
11
|
delete process.env.OPENROUTER_API_KEY;
|
|
12
12
|
delete process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
|
|
13
13
|
delete process.env.OPENROUTER_BASE_URL;
|
|
14
|
+
delete process.env.CURSOR_ROUTE_OR_OFFLINE;
|
|
15
|
+
delete process.env.CURSOR_ROUTE_OR_CATALOG_JSON;
|
|
16
|
+
delete process.env.CURSOR_ROUTE_OR_CACHE_PATH;
|
|
17
|
+
delete process.env.CURSOR_ROUTE_OR_REFRESH;
|
|
14
18
|
});
|
|
15
19
|
|
|
16
20
|
describe("openrouter adapter", () => {
|
|
@@ -20,13 +24,38 @@ describe("openrouter adapter", () => {
|
|
|
20
24
|
expect(h.worker).toBe("openrouter");
|
|
21
25
|
expect(h.ok).toBe(false);
|
|
22
26
|
expect(h.detail).toContain("OPENROUTER_API_KEY");
|
|
27
|
+
expect(h.detail).not.toMatch(/defaults to openrouter\/free/);
|
|
23
28
|
});
|
|
24
29
|
|
|
25
30
|
test("health passes with a fake key (no network)", () => {
|
|
26
31
|
setKey();
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const prev = {
|
|
33
|
+
model: process.env.CURSOR_ROUTE_OPENROUTER_MODEL,
|
|
34
|
+
offline: process.env.CURSOR_ROUTE_OR_OFFLINE,
|
|
35
|
+
json: process.env.CURSOR_ROUTE_OR_CATALOG_JSON,
|
|
36
|
+
cache: process.env.CURSOR_ROUTE_OR_CACHE_PATH,
|
|
37
|
+
};
|
|
38
|
+
delete process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
|
|
39
|
+
process.env.CURSOR_ROUTE_OR_OFFLINE = "1";
|
|
40
|
+
delete process.env.CURSOR_ROUTE_OR_CATALOG_JSON;
|
|
41
|
+
process.env.CURSOR_ROUTE_OR_CACHE_PATH = "/tmp/cursor-route-or-health-missing.json";
|
|
42
|
+
try {
|
|
43
|
+
const h = openRouterAdapter.health();
|
|
44
|
+
expect(h.ok).toBe(true);
|
|
45
|
+
expect(h.detail.toLowerCase()).toMatch(/live pick/);
|
|
46
|
+
expect(h.detail).toMatch(/fallback/);
|
|
47
|
+
expect(h.detail).not.toMatch(/now openrouter\/free/);
|
|
48
|
+
expect(h.detail).not.toMatch(/defaults to openrouter\/free/);
|
|
49
|
+
} finally {
|
|
50
|
+
if (prev.model === undefined) delete process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
|
|
51
|
+
else process.env.CURSOR_ROUTE_OPENROUTER_MODEL = prev.model;
|
|
52
|
+
if (prev.offline === undefined) delete process.env.CURSOR_ROUTE_OR_OFFLINE;
|
|
53
|
+
else process.env.CURSOR_ROUTE_OR_OFFLINE = prev.offline;
|
|
54
|
+
if (prev.json === undefined) delete process.env.CURSOR_ROUTE_OR_CATALOG_JSON;
|
|
55
|
+
else process.env.CURSOR_ROUTE_OR_CATALOG_JSON = prev.json;
|
|
56
|
+
if (prev.cache === undefined) delete process.env.CURSOR_ROUTE_OR_CACHE_PATH;
|
|
57
|
+
else process.env.CURSOR_ROUTE_OR_CACHE_PATH = prev.cache;
|
|
58
|
+
}
|
|
30
59
|
});
|
|
31
60
|
|
|
32
61
|
test("buildLaunch passes key via env and never echoes it in the command", () => {
|
|
@@ -3,7 +3,11 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import type { Adapter, WorkerHealth } from "./types.ts";
|
|
5
5
|
import { shellQuote } from "../util.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
cachedOrFreePick,
|
|
8
|
+
OPENROUTER_FALLBACK_MODEL,
|
|
9
|
+
openRouterBaseUrl,
|
|
10
|
+
} from "../config.ts";
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Resolve how to invoke the one-shot runner. Prefer the compiled dist via node
|
|
@@ -22,11 +26,11 @@ function resolveRunner(): { command: string } | null {
|
|
|
22
26
|
return null;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
function openRouterEnv(): Record<string, string> | undefined {
|
|
29
|
+
function openRouterEnv(modelId?: string): Record<string, string> | undefined {
|
|
26
30
|
const key = process.env.OPENROUTER_API_KEY;
|
|
27
31
|
if (!key) return undefined;
|
|
28
32
|
const env: Record<string, string> = { OPENROUTER_API_KEY: key };
|
|
29
|
-
const model = process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
|
|
33
|
+
const model = modelId || process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
|
|
30
34
|
if (model) env.CURSOR_ROUTE_OPENROUTER_MODEL = model;
|
|
31
35
|
const base = process.env.OPENROUTER_BASE_URL;
|
|
32
36
|
if (base) env.OPENROUTER_BASE_URL = base;
|
|
@@ -44,7 +48,7 @@ export const openRouterAdapter: Adapter = {
|
|
|
44
48
|
ok: false,
|
|
45
49
|
binary: runner?.command ?? null,
|
|
46
50
|
detail:
|
|
47
|
-
"OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model
|
|
51
|
+
"OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane live-picks a free model at start; pin with CURSOR_ROUTE_OPENROUTER_MODEL)",
|
|
48
52
|
};
|
|
49
53
|
}
|
|
50
54
|
if (!runner) {
|
|
@@ -55,19 +59,25 @@ export const openRouterAdapter: Adapter = {
|
|
|
55
59
|
detail: "openrouter-run not found — run bun run build (or use Bun from a source clone)",
|
|
56
60
|
};
|
|
57
61
|
}
|
|
62
|
+
// Health stays offline: ranked cache hit, else label the router fallback
|
|
63
|
+
// (do not present openrouter/free as a fresh live pick).
|
|
64
|
+
const cached = cachedOrFreePick();
|
|
65
|
+
const detail = cached
|
|
66
|
+
? `ok (live pick at start; cached ${cached} @ ${openRouterBaseUrl()})`
|
|
67
|
+
: `ok (live pick at start; no catalog cache, fetch-fail fallback ${OPENROUTER_FALLBACK_MODEL} @ ${openRouterBaseUrl()})`;
|
|
58
68
|
return {
|
|
59
69
|
worker: "openrouter",
|
|
60
70
|
ok: true,
|
|
61
71
|
binary: runner.command,
|
|
62
|
-
detail
|
|
72
|
+
detail,
|
|
63
73
|
};
|
|
64
74
|
},
|
|
65
|
-
buildLaunch({ promptFile }) {
|
|
75
|
+
buildLaunch({ promptFile, modelId }) {
|
|
66
76
|
const runner = resolveRunner();
|
|
67
77
|
if (!runner) throw new Error("openrouter runner not available — run: bun run build");
|
|
68
78
|
// Missing key is tolerated here so `--dry-run` can still print the command;
|
|
69
79
|
// real starts are gated by the health preflight (which requires the key).
|
|
70
|
-
const env = openRouterEnv();
|
|
80
|
+
const env = openRouterEnv(modelId);
|
|
71
81
|
|
|
72
82
|
// No interactive approval concept for a pure HTTP call — nothing to auto-approve.
|
|
73
83
|
return {
|
package/src/adapters/types.ts
CHANGED
|
@@ -24,9 +24,9 @@ export interface Adapter {
|
|
|
24
24
|
promptFile: string;
|
|
25
25
|
cwd: string;
|
|
26
26
|
alwaysApprove: boolean;
|
|
27
|
-
/** Mid-lane DeepSeek flash|pro (claude-ds + deepseek; ignored by grok
|
|
27
|
+
/** Mid-lane DeepSeek flash|pro|vision (claude-ds + deepseek; ignored by grok / Anthropic escape hatch). */
|
|
28
28
|
model?: DsModelAlias;
|
|
29
|
-
/** Concrete DeepSeek id (preserves pro[1m]) or OpenCode provider/model. */
|
|
29
|
+
/** Concrete DeepSeek id (preserves pro[1m]) or OpenCode/OpenRouter provider/model. */
|
|
30
30
|
modelId?: string;
|
|
31
31
|
/** True on --dry-run: adapters may drop artifacts they just wrote (e.g. dsh patch). */
|
|
32
32
|
dryRun?: boolean;
|