omp-llm-role 1.1.0 → 1.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-llm-role",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "omp plugin: daily model-role updater driven by llm-stats/OpenRouter rankings",
6
6
  "license": "MIT",
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "features.providerPinning": {
92
92
  "type": "boolean",
93
- "description": "Price a model on its own lab's route where one exists (per-model policy; never drops a model)",
93
+ "description": "Bind each request to the model's best provider route (per-model policy; never drops a model) and prefer routes at or above 50 tok/s",
94
94
  "default": false
95
95
  },
96
96
  "features.costCap": {
@@ -147,7 +147,7 @@
147
147
  },
148
148
  "roles.default.features.providerPinning": {
149
149
  "type": "boolean",
150
- "description": "Enable the Prefer the model's own lab endpoint capability for the default role",
150
+ "description": "Enable the Best-route provider pinning capability for the default role",
151
151
  "default": false
152
152
  },
153
153
  "roles.default.features.costCap": {
@@ -157,7 +157,7 @@
157
157
  },
158
158
  "roles.default.preferOwnProvider": {
159
159
  "type": "boolean",
160
- "description": "Price default models on their own lab's route where one exists (soft preference; never drops a model)",
160
+ "description": "Bind default requests to each model's best provider route (soft preference; never drops a model)",
161
161
  "default": false
162
162
  },
163
163
  "roles.default.required": {
@@ -193,6 +193,12 @@
193
193
  "default": 0,
194
194
  "min": 0
195
195
  },
196
+ "roles.default.filters.preferredMinThroughput": {
197
+ "type": "number",
198
+ "description": "Soft throughput floor for default routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
199
+ "default": 0,
200
+ "min": 0
201
+ },
196
202
  "roles.default.lambda": {
197
203
  "type": "number",
198
204
  "description": "Explicit λ ($ per quality point) override for default",
@@ -295,7 +301,7 @@
295
301
  },
296
302
  "roles.smol.features.providerPinning": {
297
303
  "type": "boolean",
298
- "description": "Enable the Prefer the model's own lab endpoint capability for the smol role",
304
+ "description": "Enable the Best-route provider pinning capability for the smol role",
299
305
  "default": false
300
306
  },
301
307
  "roles.smol.features.costCap": {
@@ -305,7 +311,7 @@
305
311
  },
306
312
  "roles.smol.preferOwnProvider": {
307
313
  "type": "boolean",
308
- "description": "Price smol models on their own lab's route where one exists (soft preference; never drops a model)",
314
+ "description": "Bind smol requests to each model's best provider route (soft preference; never drops a model)",
309
315
  "default": false
310
316
  },
311
317
  "roles.smol.required": {
@@ -341,6 +347,12 @@
341
347
  "default": 0,
342
348
  "min": 0
343
349
  },
350
+ "roles.smol.filters.preferredMinThroughput": {
351
+ "type": "number",
352
+ "description": "Soft throughput floor for smol routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
353
+ "default": 0,
354
+ "min": 0
355
+ },
344
356
  "roles.smol.lambda": {
345
357
  "type": "number",
346
358
  "description": "Explicit λ ($ per quality point) override for smol",
@@ -435,7 +447,7 @@
435
447
  },
436
448
  "roles.slow.features.providerPinning": {
437
449
  "type": "boolean",
438
- "description": "Enable the Prefer the model's own lab endpoint capability for the slow role",
450
+ "description": "Enable the Best-route provider pinning capability for the slow role",
439
451
  "default": false
440
452
  },
441
453
  "roles.slow.features.costCap": {
@@ -445,7 +457,7 @@
445
457
  },
446
458
  "roles.slow.preferOwnProvider": {
447
459
  "type": "boolean",
448
- "description": "Price slow models on their own lab's route where one exists (soft preference; never drops a model)",
460
+ "description": "Bind slow requests to each model's best provider route (soft preference; never drops a model)",
449
461
  "default": false
450
462
  },
451
463
  "roles.slow.required": {
@@ -481,6 +493,12 @@
481
493
  "default": 0,
482
494
  "min": 0
483
495
  },
496
+ "roles.slow.filters.preferredMinThroughput": {
497
+ "type": "number",
498
+ "description": "Soft throughput floor for slow routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
499
+ "default": 0,
500
+ "min": 0
501
+ },
484
502
  "roles.slow.lambda": {
485
503
  "type": "number",
486
504
  "description": "Explicit λ ($ per quality point) override for slow",
@@ -575,7 +593,7 @@
575
593
  },
576
594
  "roles.vision.features.providerPinning": {
577
595
  "type": "boolean",
578
- "description": "Enable the Prefer the model's own lab endpoint capability for the vision role",
596
+ "description": "Enable the Best-route provider pinning capability for the vision role",
579
597
  "default": false
580
598
  },
581
599
  "roles.vision.features.costCap": {
@@ -585,7 +603,7 @@
585
603
  },
586
604
  "roles.vision.preferOwnProvider": {
587
605
  "type": "boolean",
588
- "description": "Price vision models on their own lab's route where one exists (soft preference; never drops a model)",
606
+ "description": "Bind vision requests to each model's best provider route (soft preference; never drops a model)",
589
607
  "default": false
590
608
  },
591
609
  "roles.vision.required": {
@@ -621,6 +639,12 @@
621
639
  "default": 0,
622
640
  "min": 0
623
641
  },
642
+ "roles.vision.filters.preferredMinThroughput": {
643
+ "type": "number",
644
+ "description": "Soft throughput floor for vision routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
645
+ "default": 0,
646
+ "min": 0
647
+ },
624
648
  "roles.vision.lambda": {
625
649
  "type": "number",
626
650
  "description": "Explicit λ ($ per quality point) override for vision",
@@ -723,7 +747,7 @@
723
747
  },
724
748
  "roles.plan.features.providerPinning": {
725
749
  "type": "boolean",
726
- "description": "Enable the Prefer the model's own lab endpoint capability for the plan role",
750
+ "description": "Enable the Best-route provider pinning capability for the plan role",
727
751
  "default": false
728
752
  },
729
753
  "roles.plan.features.costCap": {
@@ -733,7 +757,7 @@
733
757
  },
734
758
  "roles.plan.preferOwnProvider": {
735
759
  "type": "boolean",
736
- "description": "Price plan models on their own lab's route where one exists (soft preference; never drops a model)",
760
+ "description": "Bind plan requests to each model's best provider route (soft preference; never drops a model)",
737
761
  "default": false
738
762
  },
739
763
  "roles.plan.required": {
@@ -769,6 +793,12 @@
769
793
  "default": 0,
770
794
  "min": 0
771
795
  },
796
+ "roles.plan.filters.preferredMinThroughput": {
797
+ "type": "number",
798
+ "description": "Soft throughput floor for plan routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
799
+ "default": 0,
800
+ "min": 0
801
+ },
772
802
  "roles.plan.lambda": {
773
803
  "type": "number",
774
804
  "description": "Explicit λ ($ per quality point) override for plan",
@@ -855,7 +885,7 @@
855
885
  },
856
886
  "roles.commit.features.providerPinning": {
857
887
  "type": "boolean",
858
- "description": "Enable the Prefer the model's own lab endpoint capability for the commit role",
888
+ "description": "Enable the Best-route provider pinning capability for the commit role",
859
889
  "default": false
860
890
  },
861
891
  "roles.commit.features.costCap": {
@@ -865,7 +895,7 @@
865
895
  },
866
896
  "roles.commit.preferOwnProvider": {
867
897
  "type": "boolean",
868
- "description": "Price commit models on their own lab's route where one exists (soft preference; never drops a model)",
898
+ "description": "Bind commit requests to each model's best provider route (soft preference; never drops a model)",
869
899
  "default": false
870
900
  },
871
901
  "roles.commit.required": {
@@ -901,6 +931,12 @@
901
931
  "default": 0,
902
932
  "min": 0
903
933
  },
934
+ "roles.commit.filters.preferredMinThroughput": {
935
+ "type": "number",
936
+ "description": "Soft throughput floor for commit routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
937
+ "default": 0,
938
+ "min": 0
939
+ },
904
940
  "roles.commit.lambda": {
905
941
  "type": "number",
906
942
  "description": "Explicit λ ($ per quality point) override for commit",
@@ -987,7 +1023,7 @@
987
1023
  },
988
1024
  "roles.tiny.features.providerPinning": {
989
1025
  "type": "boolean",
990
- "description": "Enable the Prefer the model's own lab endpoint capability for the tiny role",
1026
+ "description": "Enable the Best-route provider pinning capability for the tiny role",
991
1027
  "default": false
992
1028
  },
993
1029
  "roles.tiny.features.costCap": {
@@ -997,7 +1033,7 @@
997
1033
  },
998
1034
  "roles.tiny.preferOwnProvider": {
999
1035
  "type": "boolean",
1000
- "description": "Price tiny models on their own lab's route where one exists (soft preference; never drops a model)",
1036
+ "description": "Bind tiny requests to each model's best provider route (soft preference; never drops a model)",
1001
1037
  "default": false
1002
1038
  },
1003
1039
  "roles.tiny.required": {
@@ -1033,6 +1069,12 @@
1033
1069
  "default": 0,
1034
1070
  "min": 0
1035
1071
  },
1072
+ "roles.tiny.filters.preferredMinThroughput": {
1073
+ "type": "number",
1074
+ "description": "Soft throughput floor for tiny routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
1075
+ "default": 0,
1076
+ "min": 0
1077
+ },
1036
1078
  "roles.tiny.lambda": {
1037
1079
  "type": "number",
1038
1080
  "description": "Explicit λ ($ per quality point) override for tiny",
@@ -1111,7 +1153,7 @@
1111
1153
  },
1112
1154
  "roles.task.features.providerPinning": {
1113
1155
  "type": "boolean",
1114
- "description": "Enable the Prefer the model's own lab endpoint capability for the task role",
1156
+ "description": "Enable the Best-route provider pinning capability for the task role",
1115
1157
  "default": false
1116
1158
  },
1117
1159
  "roles.task.features.costCap": {
@@ -1121,7 +1163,7 @@
1121
1163
  },
1122
1164
  "roles.task.preferOwnProvider": {
1123
1165
  "type": "boolean",
1124
- "description": "Price task models on their own lab's route where one exists (soft preference; never drops a model)",
1166
+ "description": "Bind task requests to each model's best provider route (soft preference; never drops a model)",
1125
1167
  "default": false
1126
1168
  },
1127
1169
  "roles.task.required": {
@@ -1157,6 +1199,12 @@
1157
1199
  "default": 0,
1158
1200
  "min": 0
1159
1201
  },
1202
+ "roles.task.filters.preferredMinThroughput": {
1203
+ "type": "number",
1204
+ "description": "Soft throughput floor for task routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
1205
+ "default": 0,
1206
+ "min": 0
1207
+ },
1160
1208
  "roles.task.lambda": {
1161
1209
  "type": "number",
1162
1210
  "description": "Explicit λ ($ per quality point) override for task",
@@ -1259,7 +1307,7 @@
1259
1307
  },
1260
1308
  "roles.advisor.features.providerPinning": {
1261
1309
  "type": "boolean",
1262
- "description": "Enable the Prefer the model's own lab endpoint capability for the advisor role",
1310
+ "description": "Enable the Best-route provider pinning capability for the advisor role",
1263
1311
  "default": false
1264
1312
  },
1265
1313
  "roles.advisor.features.costCap": {
@@ -1269,7 +1317,7 @@
1269
1317
  },
1270
1318
  "roles.advisor.preferOwnProvider": {
1271
1319
  "type": "boolean",
1272
- "description": "Price advisor models on their own lab's route where one exists (soft preference; never drops a model)",
1320
+ "description": "Bind advisor requests to each model's best provider route (soft preference; never drops a model)",
1273
1321
  "default": false
1274
1322
  },
1275
1323
  "roles.advisor.required": {
@@ -1305,6 +1353,12 @@
1305
1353
  "default": 0,
1306
1354
  "min": 0
1307
1355
  },
1356
+ "roles.advisor.filters.preferredMinThroughput": {
1357
+ "type": "number",
1358
+ "description": "Soft throughput floor for advisor routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
1359
+ "default": 0,
1360
+ "min": 0
1361
+ },
1308
1362
  "roles.advisor.lambda": {
1309
1363
  "type": "number",
1310
1364
  "description": "Explicit λ ($ per quality point) override for advisor",
@@ -1391,7 +1445,7 @@
1391
1445
  },
1392
1446
  "roles.designer.features.providerPinning": {
1393
1447
  "type": "boolean",
1394
- "description": "Enable the Prefer the model's own lab endpoint capability for the designer role",
1448
+ "description": "Enable the Best-route provider pinning capability for the designer role",
1395
1449
  "default": false
1396
1450
  },
1397
1451
  "roles.designer.features.costCap": {
@@ -1401,7 +1455,7 @@
1401
1455
  },
1402
1456
  "roles.designer.preferOwnProvider": {
1403
1457
  "type": "boolean",
1404
- "description": "Price designer models on their own lab's route where one exists (soft preference; never drops a model)",
1458
+ "description": "Bind designer requests to each model's best provider route (soft preference; never drops a model)",
1405
1459
  "default": false
1406
1460
  },
1407
1461
  "roles.designer.required": {
@@ -1437,6 +1491,12 @@
1437
1491
  "default": 0,
1438
1492
  "min": 0
1439
1493
  },
1494
+ "roles.designer.filters.preferredMinThroughput": {
1495
+ "type": "number",
1496
+ "description": "Soft throughput floor for designer routes (tok/s; 0 = off) — a below-floor route sorts last but is never dropped",
1497
+ "default": 0,
1498
+ "min": 0
1499
+ },
1440
1500
  "roles.designer.lambda": {
1441
1501
  "type": "number",
1442
1502
  "description": "Explicit λ ($ per quality point) override for designer",
@@ -88,6 +88,24 @@ export type ProbeVerdict = "ok" | "blocked" | "unknown";
88
88
 
89
89
  const OPENROUTER_COMPLETIONS_URL = "https://openrouter.ai/api/v1/chat/completions";
90
90
 
91
+ /** POST a one-token completion to OpenRouter with the shared request skeleton
92
+ * (Bearer auth, JSON body, 20s timeout). Shared by `probeModel` and
93
+ * `fetchAllowedProviders`. */
94
+ function completionRequest(token: string, body: unknown, fetchImpl: typeof fetch): Promise<Response> {
95
+ return fetchImpl(OPENROUTER_COMPLETIONS_URL, {
96
+ method: "POST",
97
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
98
+ body: JSON.stringify(body),
99
+ signal: AbortSignal.timeout(20_000),
100
+ });
101
+ }
102
+
103
+ /** Extract `error.message` from a parsed OpenRouter error body, or "" when the
104
+ * body does not carry one. */
105
+ function errorMessage(body: unknown): string {
106
+ return isRecord(body) && isRecord(body.error) && typeof body.error.message === "string" ? body.error.message : "";
107
+ }
108
+
91
109
  /** 404 body OpenRouter returns when the account's allowed-providers privacy
92
110
  * whitelist (or a data-policy toggle) leaves a model without a runnable endpoint. */
93
111
  const BLOCKED_RE = /no allowed providers/i;
@@ -102,22 +120,71 @@ const BLOCKED_RE = /no allowed providers/i;
102
120
  */
103
121
  export async function probeModel(token: string, catalogId: string, fetchImpl: typeof fetch = fetch): Promise<ProbeVerdict> {
104
122
  try {
105
- const res = await fetchImpl(OPENROUTER_COMPLETIONS_URL, {
106
- method: "POST",
107
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
108
- body: JSON.stringify({ model: catalogId, messages: [{ role: "user", content: "ok" }], max_tokens: 1 }),
109
- signal: AbortSignal.timeout(20_000),
110
- });
123
+ const res = await completionRequest(token, { model: catalogId, messages: [{ role: "user", content: "ok" }], max_tokens: 1 }, fetchImpl);
111
124
  if (res.ok) return "ok";
112
125
  if (res.status !== 404) return "unknown";
113
126
  const body: unknown = await res.json().catch(() => null);
114
- const message = isRecord(body) && isRecord(body.error) && typeof body.error.message === "string" ? body.error.message : "";
127
+ const message = errorMessage(body);
115
128
  return BLOCKED_RE.test(message) ? "blocked" : "unknown";
116
129
  } catch {
117
130
  return "unknown";
118
131
  }
119
132
  }
120
133
 
134
+ /** 404 body OpenRouter returns when the account's allowed-providers privacy
135
+ * whitelist leaves the canary provider without a runnable endpoint; the message
136
+ * names the whitelist after "permits only:". */
137
+ const ALLOWED_PROVIDERS_RE = /permits only:\s*([^.]*)\./i;
138
+
139
+ /**
140
+ * Harvest the account's allowed-providers privacy whitelist. The whitelist is
141
+ * invisible to `/api/v1/key` and the catalog endpoints; the only place it
142
+ * appears is the 404 body of a request that fails because of it. A canary
143
+ * completion on a valid model with a nonexistent provider slug (`provider.only`)
144
+ * 404s and names the whitelist. Returns null when the body does not name a
145
+ * whitelist (no whitelist set), the response is not a 404, or the request
146
+ * throws — never throws.
147
+ */
148
+ export async function fetchAllowedProviders(token: string, modelId: string, fetchImpl: typeof fetch = fetch): Promise<Set<string> | null> {
149
+ try {
150
+ const res = await completionRequest(
151
+ token,
152
+ {
153
+ model: modelId,
154
+ messages: [{ role: "user", content: "ok" }],
155
+ max_tokens: 1,
156
+ provider: { only: ["__omp-llm-role-canary__"] },
157
+ },
158
+ fetchImpl,
159
+ );
160
+ if (res.status !== 404) return null;
161
+ const body: unknown = await res.json().catch(() => null);
162
+ const message = errorMessage(body);
163
+ const match = ALLOWED_PROVIDERS_RE.exec(message);
164
+ if (!match) return null;
165
+ const providers = match[1]
166
+ .split(",")
167
+ .map((s) => s.trim())
168
+ .filter((s) => s.length > 0);
169
+ return providers.length > 0 ? new Set(providers) : null;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Whether a route's `providerSlug` is permitted by the account's
177
+ * allowed-providers whitelist. Whitelist entries are bare lab names
178
+ * (`coreweave`) while route slugs are tiered (`coreweave/fp8`), so a match is
179
+ * the entry itself or an `entry/` prefix.
180
+ */
181
+ export function providerAllowed(allowed: ReadonlySet<string>, slug: string): boolean {
182
+ for (const entry of allowed) {
183
+ if (slug === entry || slug.startsWith(`${entry}/`)) return true;
184
+ }
185
+ return false;
186
+ }
187
+
121
188
  // ---------------------------------------------------------------------------
122
189
  // Keyed-catalog availability
123
190
  // ---------------------------------------------------------------------------
@@ -228,12 +295,23 @@ export function rankingIdOf(catalogId: string): string {
228
295
  return noFree.endsWith("-latest") ? noFree.slice(0, -"-latest".length) : noFree;
229
296
  }
230
297
 
298
+ /** Split a selector's trailing thinking suffix (`provider/model[:level]`):
299
+ * `base` without the suffix, `level` the suffix or null. The single home for
300
+ * the `:level` strip shared by `currentRankingId`, `providerPinOfSelector`, and
301
+ * the extension's selector handling. */
302
+ export function splitThinkingSuffix(selector: string): { base: string; level: string | null } {
303
+ const colon = selector.lastIndexOf(":");
304
+ if (colon !== -1 && selector.slice(colon + 1) in THINKING_LEVELS) {
305
+ return { base: selector.slice(0, colon), level: selector.slice(colon + 1) };
306
+ }
307
+ return { base: selector, level: null };
308
+ }
309
+
231
310
  /** Identity of a configured selector for hysteresis/chain keys: drop `:level`,
232
311
  * `openrouter/`, and a trailing `@<slug>` provider pin, then rankingIdOf. A
233
312
  * tiered pin (`@deepinfra/fp8`) is dropped whole — everything from the last `@`. */
234
313
  export function currentRankingId(selector: string): string {
235
- const colon = selector.lastIndexOf(":");
236
- const base = colon !== -1 && selector.slice(colon + 1) in THINKING_LEVELS ? selector.slice(0, colon) : selector;
314
+ const { base } = splitThinkingSuffix(selector);
237
315
  const unprefixed = base.startsWith("openrouter/") ? base.slice("openrouter/".length) : base;
238
316
  const at = unprefixed.lastIndexOf("@");
239
317
  const unpinned = at !== -1 ? unprefixed.slice(0, at) : unprefixed;
@@ -50,7 +50,7 @@ const USAGE = [
50
50
  " --lambda <n> explicit λ override ($ per quality point)",
51
51
  " --feature <id,...> capability flag(s) to author on the role (see --list-features);",
52
52
  " repeatable, comma-separated, merged",
53
- " --prefer-own-provider price the role on each model's own lab route where one exists",
53
+ " --prefer-own-provider bind the role to each model's best provider route where one exists",
54
54
  " --list-features print the capability flags and the settings each applies, then exit",
55
55
  " --lock <path> settings lock file (default ~/.omp/plugins/omp-plugins.lock.json)",
56
56
  " --dry-run validate and print without writing",