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.
@@ -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 models) | Wording / drafts — non-secret prompts only |
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 / role | Model | When |
38
- |-------------|-------|------|
39
- | default mid (`--model flash`) | `deepseek-v4-flash` | `--lane mid` cheap execute |
40
- | Grok stand-in (`--model pro`) | `deepseek-v4-pro` | Grok CLI **usage/quota** out (not a missing `grok login`) |
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 stand-in case.
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`). Default model is `opencode/big-pickle` (`CURSOR_ROUTE_OPENCODE_MODEL` / `--model free`). 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).
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 OpenCode Zen Big Pickle", () => {
15
- const prev = process.env.CURSOR_ROUTE_OPENCODE_MODEL;
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/big-pickle");
29
+ expect(openCodeModel()).toBe("opencode/x-preview-f-free");
19
30
  expect(openCodeModel("")).toBe(OPENCODE_DEFAULT_MODEL);
20
- expect(openCodeModel("free")).toBe("opencode/big-pickle");
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({ CURSOR_ROUTE_OPENCODE_BIN: bin }, () => {
105
- const h = opencodeAdapter.health();
106
- expect(h.ok).toBe(true);
107
- expect(h.binary).toBe(bin);
108
- expect(h.detail).toContain("opencode/big-pickle");
109
- });
110
- rmSync(join(tmpdir(), `cr-oc-${process.pid}-ok`), { recursive: true, force: true });
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/big-pickle");
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({ CURSOR_ROUTE_OPENCODE_BIN: bin, CURSOR_ROUTE_ASK: "1" }, () => {
164
- const plan = opencodeAdapter.buildLaunch({
165
- promptFile,
166
- cwd: dir,
167
- alwaysApprove: true,
168
- });
169
- expect(plan.command).not.toContain("--auto");
170
- expect(plan.alwaysApprove).toBe(false);
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/big-pickle");
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
  });
@@ -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 that can use
9
- * OpenCode Zen free models (default `opencode/big-pickle`) to cut Grok /
10
- * DeepSeek usage. Not a lane default: mid stays claude-ds; easy stays
11
- * OpenRouter chat (no tools).
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
- let defaultModel = OPENCODE_DEFAULT_MODEL;
51
- try {
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; default model ${defaultModel}; auth at first start — opencode auth login if jobs fail; mid default remains claude-ds)`,
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 h = openRouterAdapter.health();
28
- expect(h.ok).toBe(true);
29
- expect(h.detail).toContain("openrouter/free");
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 { openRouterModel, openRouterBaseUrl } from "../config.ts";
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 defaults to openrouter/free)",
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: `ok (model ${openRouterModel()} @ ${openRouterBaseUrl()})`,
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 {
@@ -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/openrouter / Anthropic escape hatch). */
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;