cursor-route 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  _(none)_
6
6
 
7
+ ## 0.1.14 — 2026-09-02
8
+
9
+ - 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.
10
+ - Tests: all seven tier buckets; equal-score tier/id tiebreakers; same-tier 131072-vs-262144 cap; ranked-pick cache round-trip.
11
+ - Mid lane still `claude-ds`; no worker swap. Hero demo fixture regenerated to **0.1.14**.
12
+
7
13
  ## 0.1.13 — 2026-08-29
8
14
 
9
15
  - OpenRouter live-pick: do **not** cache the fetch-fail fallback (`openrouter/free`) — health no longer presents a stale fallback as `now openrouter/free`. Health says `cached <id>` only for a ranked catalog hit; otherwise `no catalog cache, fetch-fail fallback openrouter/free`.
package/README.md CHANGED
@@ -116,10 +116,10 @@ docs/fixtures/generate-hero-demo.sh # regenerate docs/fixtures/hero-demo.lo
116
116
 
117
117
  ```text
118
118
  $ cursor-route --version
119
- 0.1.13
119
+ 0.1.14
120
120
 
121
121
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
122
- cursor-route v0.1.13
122
+ cursor-route v0.1.14
123
123
  health: OK
124
124
 
125
125
  $ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
@@ -214,7 +214,9 @@ No DeepSeek yet? Use `--lane hard` / `--worker grok` (X Premium).
214
214
 
215
215
  `--lane easy` / `--worker openrouter` sends wording/draft prompts to OpenRouter
216
216
  and **live-picks the best free text model** at request time (`GET /models`, rank
217
- `:free` or $0 text models). Do not hardcode a specific model id as the default.
217
+ `:free` or $0 text models). The live rank prefers Qwen/GLM/Kimi coding free
218
+ models over huge general free models (e.g. Nemotron 550B). Do not hardcode a
219
+ specific model id as the default.
218
220
  Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Empty /
219
221
  `free` = live pick. If the catalog fetch fails, the fallback is OpenRouter's
220
222
  **router** id `openrouter/free` (a live router, not a locked model). Get a key
package/dist/config.js CHANGED
@@ -72,7 +72,7 @@ function maxConcurrentJobsFromEnv() {
72
72
  */
73
73
  export const config = {
74
74
  product: "cursor-route",
75
- version: "0.1.13",
75
+ version: "0.1.14",
76
76
  get jobsDir() {
77
77
  return defaultJobsDir();
78
78
  },
package/dist/or-free.js CHANGED
@@ -12,7 +12,6 @@ export const OPENROUTER_MODELS_URL_DEFAULT = "https://openrouter.ai/api/v1/model
12
12
  /** Fetch-fail / empty-rank fallback — OpenRouter live router, not a locked model. */
13
13
  export const OPENROUTER_FALLBACK_MODEL = "openrouter/free";
14
14
  const EXCLUDE_RE = /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
15
- const BOOST_RE = /coder|instruct|chat|nemotron|qwen|llama|gemma|gpt-oss|kimi|glm|deepseek/i;
16
15
  function orModelsUrl() {
17
16
  const base = (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1")
18
17
  .trim()
@@ -59,9 +58,28 @@ export function isOrFreeModel(m) {
59
58
  const tagged = id.endsWith(":free");
60
59
  return tagged || pricedFree;
61
60
  }
62
- /** Higher boost wins. Coding/chat families beat generic free. No hardcoded id. */
61
+ /**
62
+ * Toolkit-parity tier for a live OpenRouter free model id (first match wins).
63
+ * Huge general free models (Nemotron) rank well below coding/chat families so
64
+ * the easy-lane live pick does not land on a slow 550B generalist. Order
65
+ * matters: `qwen` is checked before `nemotron`, and the generic coding bucket
66
+ * (70) sits before `nemotron` exactly like the toolkit, so
67
+ * `nvidia/nemotron-3-550b:free` scores 15 — never 70. No hardcoded model id.
68
+ */
63
69
  export function orFreeBoost(id) {
64
- return BOOST_RE.test(id) ? 20 : 10;
70
+ if (/qwen/i.test(id))
71
+ return 100;
72
+ if (/(^|\/)z-ai\/glm|glm/i.test(id))
73
+ return 95;
74
+ if (/kimi|moonshot/i.test(id))
75
+ return 95;
76
+ if (/deepseek|hy-/i.test(id))
77
+ return 90;
78
+ if (/coder|instruct|chat|llama|gemma|gpt-oss|minimax/i.test(id))
79
+ return 70;
80
+ if (/nemotron/i.test(id))
81
+ return 15;
82
+ return 40;
65
83
  }
66
84
  export function rankOrFreeModels(models) {
67
85
  const out = [];
@@ -77,18 +95,24 @@ export function rankOrFreeModels(models) {
77
95
  continue;
78
96
  }
79
97
  const ctx = Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : 0;
98
+ const tier = orFreeBoost(id);
99
+ // Cap the context bonus at 131072 so a huge Nemotron ctx cannot outrank a
100
+ // higher tier on context alone: score = tier + min(ctx, 131072)/131072 * 5.
101
+ const score = tier + (Math.min(ctx, 131_072) / 131_072) * 5;
80
102
  out.push({
81
103
  id,
82
104
  name: (m.name || raw).trim(),
83
105
  context_length: ctx,
84
- boost: orFreeBoost(id),
106
+ boost: score,
85
107
  });
86
108
  }
109
+ // Sort like the toolkit: score desc, then tier desc, then id asc.
87
110
  out.sort((a, b) => {
88
111
  if (b.boost !== a.boost)
89
112
  return b.boost - a.boost;
90
- if (b.context_length !== a.context_length)
91
- return b.context_length - a.context_length;
113
+ const tierDiff = orFreeBoost(b.id) - orFreeBoost(a.id);
114
+ if (tierDiff !== 0)
115
+ return tierDiff;
92
116
  return a.id.localeCompare(b.id);
93
117
  });
94
118
  return out;
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  title: cursor-route workspace — working brief (edit in place)
3
3
  repo: ~/Projects/cursor-route
4
- npm: cursor-route@0.1.13 (publishing this slice)
4
+ npm: cursor-route@0.1.14 (not yet published — Kimi audit next)
5
5
  created: 2026-08-12
6
- updated: 2026-08-29
6
+ updated: 2026-09-02
7
7
  ---
8
8
 
9
9
  # cursor-route — living brief
@@ -43,6 +43,7 @@ Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](.
43
43
  - [x] **Live Zen free pick** — 0.1.11: `--model free` ranks `GET https://opencode.ai/zen/v1/models` (Ox Alpha first while listed; ~15 min cache; Ox Alpha fallback if fetch fails). Pin with `CURSOR_ROUTE_OPENCODE_MODEL`. Health stays offline (cache/fallback only).
44
44
  - [x] **Flash-first + vision + live OpenRouter pick** — 0.1.12: `--model flash` stays the cheap Grok-out default; `--model vision` + screenshot auto-pick; easy lane live-picks free OpenRouter text models (`openrouter/free` is fetch-fail fallback only). Health never fetches `/models`. `route-orch` ProgRouter + MoRe one-liners. Hero fixture regenerated to 0.1.12. No npm publish in this slice.
45
45
  - [x] **Kimi 0.1.13 follow-up** — do not cache OpenRouter fallback; health labels uncached fallback; no Authorization on GET /models; env-invalid model error names the env vars; `\bimage\b` vision trigger.
46
+ - [x] **Super-audit OR ranker parity (0.1.14)** — tier-rank Qwen/GLM/Kimi above Nemotron 550B in `or-free.ts` (match agent-toolkit `select-openrouter-free-model.ps1`); brief `docs/briefs/2026-09-02_super-audit-or-ranker-parity.md`
46
47
  - [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
47
48
  - [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
48
49
 
@@ -79,4 +80,5 @@ Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](.
79
80
  | 2026-08-21 | Opt-in `--worker opencode` (Zen free `opencode/big-pickle`, `--auto`, no config rewrite); mid stays claude-ds → 0.1.10 LIVE. |
80
81
  | 2026-08-21 | Live Zen free pick (Ox Alpha first while listed; OpenRouter-style catalog rank) → 0.1.11 LIVE. |
81
82
  | 2026-08-29 | Flash-first docs, `--model vision` + auto-pick, live OpenRouter free pick, route-orch K318/K322, hero fixture 0.1.12. No npm publish. |
82
- | 2026-08-29 | Kimi audit follow-up → 0.1.13: no fallback cache, health labels, unauth GET /models, env error wording. |
83
+ | 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`. **No npm publish** — parent Kimi-audits then publishes. Mid stays claude-ds. |
@@ -6,10 +6,10 @@ Simulated capture output for README / tweet assets — replace with a real GIF o
6
6
 
7
7
  ```
8
8
  $ cursor-route --version
9
- 0.1.13
9
+ 0.1.14
10
10
 
11
11
  $ cursor-route health
12
- cursor-route v0.1.13
12
+ cursor-route v0.1.14
13
13
  health: OK
14
14
  ✓ tmux
15
15
  ✓ runtime bun ok
@@ -1,8 +1,8 @@
1
1
  $ cursor-route --version
2
- 0.1.13
2
+ 0.1.14
3
3
 
4
4
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
5
- cursor-route v0.1.13
5
+ cursor-route v0.1.14
6
6
  health: OK
7
7
 
8
8
  ✓ tmux ok
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux. `--worker opencode` is an opt-in coding agent on OpenCode Zen free models.
4
4
 
5
- MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.13**)
5
+ MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.14**)
6
6
  GitHub: https://github.com/cemini23/cursor-route
7
7
 
8
8
  ## FAQ
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-route",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane + OpenCode (opt-in free) are the parallel army \u2014 lane-aware /route orchestration in tmux.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
28
28
  | `mid` | `claude-ds` (DeepSeek via Claude Code harness) | Standard implement / refactor |
29
29
  | `hard` | `grok` | Premium plan in Cursor → Grok implement |
30
30
 
31
- Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies). Easy lane **live-picks** the best free text model at request time (unset / `--model free`). Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Fetch-fail fallback is the OpenRouter router `openrouter/free` only.
31
+ Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies). Easy lane **live-picks** the best free text model at request time (unset / `--model free`), preferring Qwen/GLM/Kimi coding free models over huge general free models (Nemotron). Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Fetch-fail fallback is the OpenRouter router `openrouter/free` only.
32
32
 
33
33
  ## claude-ds models
34
34
 
package/src/cli.test.ts CHANGED
@@ -879,7 +879,7 @@ describe("health", () => {
879
879
  test("returns structured report", () => {
880
880
  const r = runHealth();
881
881
  expect(r.product).toBe("cursor-route");
882
- expect(r.version).toBe("0.1.13");
882
+ expect(r.version).toBe("0.1.14");
883
883
  expect(r.checks.length).toBeGreaterThan(3);
884
884
  expect(r.checks.some((c) => c.name === "tmux")).toBe(true);
885
885
  expect(r.checks.some((c) => c.name === "cursor_cli")).toBe(true);
@@ -891,8 +891,8 @@ describe("health", () => {
891
891
  expect(r.checks.some((c) => c.name === "worker:deepseek")).toBe(true);
892
892
  });
893
893
 
894
- test("config version is 0.1.13", () => {
895
- expect(config.version).toBe("0.1.13");
894
+ test("config version is 0.1.14", () => {
895
+ expect(config.version).toBe("0.1.14");
896
896
  });
897
897
 
898
898
  test("OR-gate: ok can be true while worker:opencode is false", () => {
package/src/config.ts CHANGED
@@ -111,7 +111,7 @@ function maxConcurrentJobsFromEnv(): number {
111
111
  */
112
112
  export const config = {
113
113
  product: "cursor-route",
114
- version: "0.1.13",
114
+ version: "0.1.14",
115
115
  get jobsDir(): string {
116
116
  return defaultJobsDir();
117
117
  },
@@ -62,10 +62,13 @@ describe("openrouter free catalog", () => {
62
62
  ).toBe(false);
63
63
  });
64
64
 
65
- test("boost: coder/qwen outranks generic free; no hardcoded winner id", () => {
66
- expect(orFreeBoost("qwen/qwen-coder:free")).toBe(20);
67
- expect(orFreeBoost("acme/generic:free")).toBe(10);
68
- expect(orFreeBoost("meta-llama/llama-3.3-70b-instruct:free")).toBe(20);
65
+ test("boost: tier ranks Qwen/GLM/Kimi above Nemotron; generic free is default", () => {
66
+ expect(orFreeBoost("qwen/qwen-coder:free")).toBe(100);
67
+ expect(orFreeBoost("z-ai/glm-5.2:free")).toBe(95);
68
+ expect(orFreeBoost("moonshotai/kimi-k2:free")).toBe(95);
69
+ expect(orFreeBoost("acme/generic:free")).toBe(40);
70
+ expect(orFreeBoost("meta-llama/llama-3.3-70b-instruct:free")).toBe(70);
71
+ expect(orFreeBoost("nvidia/nemotron-3-550b:free")).toBe(15);
69
72
  });
70
73
 
71
74
  test("rank prefers a :free coder with larger context over a generic :free", () => {
@@ -85,10 +88,83 @@ describe("openrouter free catalog", () => {
85
88
  expect(ranked[1]?.id).toBe("qwen/small-coder:free");
86
89
  });
87
90
 
91
+ test("all seven tier buckets", () => {
92
+ expect(orFreeBoost("qwen/qwen3-coder:free")).toBe(100);
93
+ expect(orFreeBoost("z-ai/glm-5.2:free")).toBe(95);
94
+ expect(orFreeBoost("moonshotai/kimi-k2:free")).toBe(95);
95
+ expect(orFreeBoost("deepseek/deepseek-chat:free")).toBe(90);
96
+ expect(orFreeBoost("acme/hy-foo:free")).toBe(90);
97
+ expect(orFreeBoost("meta-llama/llama-3.3-70b-instruct:free")).toBe(70);
98
+ expect(orFreeBoost("google/gemma-3-12b:free")).toBe(70);
99
+ expect(orFreeBoost("openai/gpt-oss-120b:free")).toBe(70);
100
+ expect(orFreeBoost("minimax/minimax-m1:free")).toBe(70);
101
+ expect(orFreeBoost("nvidia/nemotron-3-550b:free")).toBe(15);
102
+ expect(orFreeBoost("acme/generic:free")).toBe(40);
103
+ });
104
+
105
+ test("equal score: higher tier wins (Qwen ctx 0 vs GLM at ctx cap)", () => {
106
+ const ranked = rankOrFreeModels([
107
+ { id: "qwen/qwen3-coder:free", context_length: 0 },
108
+ { id: "z-ai/glm-5.2:free", context_length: 131_072 },
109
+ ]);
110
+ expect(ranked[0]?.boost).toBe(ranked[1]?.boost);
111
+ expect(ranked[0]?.id).toBe("qwen/qwen3-coder:free");
112
+ });
113
+
114
+ test("equal score same tier: id ascending", () => {
115
+ const ranked = rankOrFreeModels([
116
+ { id: "qwen/zzz-coder:free", context_length: 8_000 },
117
+ { id: "qwen/aaa-coder:free", context_length: 8_000 },
118
+ ]);
119
+ expect(ranked[0]?.id).toBe("qwen/aaa-coder:free");
120
+ expect(ranked[1]?.id).toBe("qwen/zzz-coder:free");
121
+ });
122
+
123
+ test("ctx bonus caps at 131072: same-tier 131072 vs 262144 ties then id asc", () => {
124
+ const ranked = rankOrFreeModels([
125
+ { id: "qwen/zzz-coder:free", context_length: 262_144 },
126
+ { id: "qwen/aaa-coder:free", context_length: 131_072 },
127
+ ]);
128
+ expect(ranked[0]?.boost).toBe(ranked[1]?.boost);
129
+ expect(ranked[0]?.id).toBe("qwen/aaa-coder:free");
130
+ });
131
+
132
+ test("huge Nemotron free model loses to GLM/Qwen on tier rank", () => {
133
+ const ranked = rankOrFreeModels([
134
+ { id: "nvidia/nemotron-3-550b:free", name: "Nemotron 3 550B", context_length: 262_144 },
135
+ { id: "z-ai/glm-5.2:free", name: "GLM 5.2", context_length: 32_768 },
136
+ { id: "qwen/qwen3-coder:free", name: "Qwen3 Coder", context_length: 32_768 },
137
+ ]);
138
+ expect(ranked[0]?.id).toBe("qwen/qwen3-coder:free");
139
+ expect(ranked[0]?.id).not.toBe("nvidia/nemotron-3-550b:free");
140
+ expect(ranked[1]?.id).toBe("z-ai/glm-5.2:free");
141
+ expect(ranked[2]?.id).toBe("nvidia/nemotron-3-550b:free");
142
+ expect(ranked[0]!.boost).toBeGreaterThan(ranked[2]!.boost);
143
+ });
144
+
145
+ test("huge ctx alone does not beat a higher tier (ctx bonus caps at 131072)", () => {
146
+ const ranked = rankOrFreeModels([
147
+ { id: "nvidia/nemotron-3-550b:free", context_length: 1_048_576 },
148
+ { id: "qwen/qwen3-coder:free", context_length: 1_000 },
149
+ ]);
150
+ expect(ranked[0]?.id).toBe("qwen/qwen3-coder:free");
151
+ });
152
+
88
153
  test("pickOrFreeModel uses the ranked catalog winner", () => {
89
154
  expect(pickOrFreeModel({ catalog: CATALOG })).toBe("qwen/qwen-coder:free");
90
155
  });
91
156
 
157
+ test("pickOrFreeModel winner is not a huge Nemotron when GLM/Qwen are free", () => {
158
+ const winner = pickOrFreeModel({
159
+ catalog: [
160
+ { id: "nvidia/nemotron-3-550b:free", name: "Nemotron 3 550B", context_length: 262_144 },
161
+ { id: "z-ai/glm-5.2:free", name: "GLM 5.2", context_length: 32_768 },
162
+ ],
163
+ });
164
+ expect(winner).not.toBe("nvidia/nemotron-3-550b:free");
165
+ expect(winner).toBe("z-ai/glm-5.2:free");
166
+ });
167
+
92
168
  test("offline / empty catalog falls back to the OpenRouter router", () => {
93
169
  expect(pickOrFreeModel({ catalog: [] })).toBe(OPENROUTER_FALLBACK_MODEL);
94
170
  expect(OPENROUTER_FALLBACK_MODEL).toBe("openrouter/free");
@@ -175,6 +251,18 @@ describe("openRouterModel live pick", () => {
175
251
  expect(existsSync(cache)).toBe(false);
176
252
  });
177
253
  });
254
+
255
+ test("ranked pick is cached; offline second call returns the cached id", () => {
256
+ isolate(() => {
257
+ process.env.CURSOR_ROUTE_OR_CATALOG_JSON = JSON.stringify({ data: CATALOG });
258
+ expect(openRouterModel("free")).toBe("qwen/qwen-coder:free");
259
+ delete process.env.CURSOR_ROUTE_OR_REFRESH;
260
+ process.env.CURSOR_ROUTE_OR_OFFLINE = "1";
261
+ delete process.env.CURSOR_ROUTE_OR_CATALOG_JSON;
262
+ expect(existsSync(process.env.CURSOR_ROUTE_OR_CACHE_PATH!)).toBe(true);
263
+ expect(openRouterModel("free")).toBe("qwen/qwen-coder:free");
264
+ });
265
+ });
178
266
  });
179
267
 
180
268
  describe("openrouter health stays offline", () => {
package/src/or-free.ts CHANGED
@@ -35,8 +35,6 @@ export interface OrFreePick {
35
35
 
36
36
  const EXCLUDE_RE =
37
37
  /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
38
- const BOOST_RE =
39
- /coder|instruct|chat|nemotron|qwen|llama|gemma|gpt-oss|kimi|glm|deepseek/i;
40
38
 
41
39
  function orModelsUrl(): string {
42
40
  const base = (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1")
@@ -88,9 +86,22 @@ export function isOrFreeModel(m: OrModel): boolean {
88
86
  return tagged || pricedFree;
89
87
  }
90
88
 
91
- /** Higher boost wins. Coding/chat families beat generic free. No hardcoded id. */
89
+ /**
90
+ * Toolkit-parity tier for a live OpenRouter free model id (first match wins).
91
+ * Huge general free models (Nemotron) rank well below coding/chat families so
92
+ * the easy-lane live pick does not land on a slow 550B generalist. Order
93
+ * matters: `qwen` is checked before `nemotron`, and the generic coding bucket
94
+ * (70) sits before `nemotron` exactly like the toolkit, so
95
+ * `nvidia/nemotron-3-550b:free` scores 15 — never 70. No hardcoded model id.
96
+ */
92
97
  export function orFreeBoost(id: string): number {
93
- return BOOST_RE.test(id) ? 20 : 10;
98
+ if (/qwen/i.test(id)) return 100;
99
+ if (/(^|\/)z-ai\/glm|glm/i.test(id)) return 95;
100
+ if (/kimi|moonshot/i.test(id)) return 95;
101
+ if (/deepseek|hy-/i.test(id)) return 90;
102
+ if (/coder|instruct|chat|llama|gemma|gpt-oss|minimax/i.test(id)) return 70;
103
+ if (/nemotron/i.test(id)) return 15;
104
+ return 40;
94
105
  }
95
106
 
96
107
  export function rankOrFreeModels(models: OrModel[]): Array<{
@@ -110,16 +121,22 @@ export function rankOrFreeModels(models: OrModel[]): Array<{
110
121
  continue;
111
122
  }
112
123
  const ctx = Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : 0;
124
+ const tier = orFreeBoost(id);
125
+ // Cap the context bonus at 131072 so a huge Nemotron ctx cannot outrank a
126
+ // higher tier on context alone: score = tier + min(ctx, 131072)/131072 * 5.
127
+ const score = tier + (Math.min(ctx, 131_072) / 131_072) * 5;
113
128
  out.push({
114
129
  id,
115
130
  name: (m.name || raw).trim(),
116
131
  context_length: ctx,
117
- boost: orFreeBoost(id),
132
+ boost: score,
118
133
  });
119
134
  }
135
+ // Sort like the toolkit: score desc, then tier desc, then id asc.
120
136
  out.sort((a, b) => {
121
137
  if (b.boost !== a.boost) return b.boost - a.boost;
122
- if (b.context_length !== a.context_length) return b.context_length - a.context_length;
138
+ const tierDiff = orFreeBoost(b.id) - orFreeBoost(a.id);
139
+ if (tierDiff !== 0) return tierDiff;
123
140
  return a.id.localeCompare(b.id);
124
141
  });
125
142
  return out;