@alexeiled/pi-model-router 0.5.1 → 0.6.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.
@@ -6,12 +6,16 @@ import type {
6
6
  import type { AutocompleteItem } from '@earendil-works/pi-tui';
7
7
  import {
8
8
  getUnsupportedTiers,
9
+ isRouterPinValue,
10
+ isRouterTier,
11
+ isThinkingLevel,
9
12
  parseCanonicalModelRef,
10
13
  profileNames,
11
14
  ROUTER_PIN_VALUES,
12
15
  ROUTER_TIERS,
13
16
  THINKING_LEVELS,
14
17
  } from './config';
18
+ import { preservesRouteCoverage } from './routing';
15
19
  import type {
16
20
  RouterConfig,
17
21
  RouterPinByProfile,
@@ -21,6 +25,7 @@ import type {
21
25
  } from './types';
22
26
  import {
23
27
  formatDecision,
28
+ formatDecisionSource,
24
29
  formatModelRef,
25
30
  formatPinSummary,
26
31
  formatThinkingSummary,
@@ -110,7 +115,7 @@ export const registerCommands = (
110
115
  args: string[],
111
116
  ): AutocompleteItem[] | null => {
112
117
  // thinking [tier] <level|auto>
113
- const tierValues = [...ROUTER_TIERS];
118
+ const tierValues: RouterTier[] = [...ROUTER_TIERS];
114
119
  const levelValues = ['auto', ...THINKING_LEVELS];
115
120
 
116
121
  if (args.length <= 1) {
@@ -136,8 +141,8 @@ export const registerCommands = (
136
141
  ];
137
142
  }
138
143
 
139
- if ((tierValues as string[]).includes(args[0])) {
140
- const tier = args[0];
144
+ const tier = args[0];
145
+ if (isRouterTier(tier)) {
141
146
  const levelPrefix = args[1] ?? '';
142
147
  return levelValues
143
148
  .filter((v) => v.startsWith(levelPrefix))
@@ -168,7 +173,6 @@ export const registerCommands = (
168
173
  `Pins by profile: ${formatPinSummary(state.pinnedTierByProfile)}`,
169
174
  `Thinking overrides: ${formatThinkingSummary(state.thinkingByProfile)}`,
170
175
  `Widget: ${state.widgetEnabled ? 'on' : 'off'}`,
171
- `Phase bias: ${state.currentConfig.phaseBias}`,
172
176
  `Session cost: $${state.accumulatedCost.toFixed(4)}` +
173
177
  (state.currentConfig.maxSessionBudget
174
178
  ? ` / $${state.currentConfig.maxSessionBudget.toFixed(2)}`
@@ -183,7 +187,9 @@ export const registerCommands = (
183
187
  `Last routed tier: ${state.lastDecision.tier}`,
184
188
  `Last phase: ${state.lastDecision.phase}`,
185
189
  `Last model: ${state.lastDecision.targetProvider}/${state.lastDecision.targetModelId} (${state.lastDecision.thinking})`,
186
- `Reason: ${state.lastDecision.reasoning}`,
190
+ ...(formatDecisionSource(state.lastDecision)
191
+ ? [`Reason: ${formatDecisionSource(state.lastDecision)}`]
192
+ : []),
187
193
  );
188
194
  }
189
195
  if (state.lastConfigWarnings && state.lastConfigWarnings.length > 0) {
@@ -233,7 +239,7 @@ export const registerCommands = (
233
239
  [
234
240
  `Profile: ${currentProfile}`,
235
241
  `Pinned tier: ${state.pinnedTierByProfile[currentProfile] ?? 'auto'}`,
236
- `Usage: /router pin <high|medium|low|auto>`,
242
+ `Usage: /router pin <high|medium|low|micro|auto>`,
237
243
  ].join('\n'),
238
244
  'info',
239
245
  );
@@ -242,13 +248,13 @@ export const registerCommands = (
242
248
  }
243
249
 
244
250
  if (args.length > 1) {
245
- ctx.ui.notify('Usage: /router pin <high|medium|low|auto>', 'error');
251
+ ctx.ui.notify('Usage: /router pin <high|medium|low|micro|auto>', 'error');
246
252
  return;
247
253
  }
248
254
 
249
255
  const pinValue = args[0];
250
256
 
251
- if (!ROUTER_PIN_VALUES.some((value) => value === pinValue)) {
257
+ if (!isRouterPinValue(pinValue)) {
252
258
  ctx.ui.notify(
253
259
  `Invalid router pin: ${pinValue}. Use one of: ${ROUTER_PIN_VALUES.join(', ')}`,
254
260
  'error',
@@ -256,7 +262,8 @@ export const registerCommands = (
256
262
  return;
257
263
  }
258
264
 
259
- const nextTier = pinValue === 'auto' ? undefined : (pinValue as RouterTier);
265
+ const nextTier: RouterTier | undefined =
266
+ pinValue === 'auto' ? undefined : pinValue;
260
267
  if (nextTier) {
261
268
  state.pinnedTierByProfile[currentProfile] = nextTier;
262
269
  } else {
@@ -267,7 +274,7 @@ export const registerCommands = (
267
274
  ctx.ui.notify(
268
275
  nextTier
269
276
  ? `Router pinned to ${nextTier}`
270
- : `Router pin cleared; heuristic routing restored`,
277
+ : `Router pin cleared; baseline routing restored`,
271
278
  'info',
272
279
  );
273
280
  };
@@ -303,28 +310,32 @@ export const registerCommands = (
303
310
  let tier: RouterTier | 'all' | undefined;
304
311
  let levelValue = '';
305
312
 
306
- const tierValues = ['high', 'medium', 'low'];
307
313
  const levelValues = ['auto', ...THINKING_LEVELS];
308
314
 
309
315
  if (args.length === 1) {
310
- levelValue = args[0];
316
+ const level = args[0];
317
+ if (!level) return;
318
+ levelValue = level;
311
319
  tier = 'all';
312
320
  } else if (args.length === 2) {
313
- if (tierValues.includes(args[0]) || args[0] === 'all') {
314
- tier = args[0] as RouterTier | 'all';
315
- levelValue = args[1];
321
+ const requestedTier = args[0];
322
+ const requestedLevel = args[1];
323
+ if (!requestedTier || !requestedLevel) return;
324
+ if (isRouterTier(requestedTier) || requestedTier === 'all') {
325
+ tier = requestedTier === 'all' ? 'all' : requestedTier;
326
+ levelValue = requestedLevel;
316
327
  } else {
317
328
  ctx.ui.notify(
318
- `Invalid tier: ${args[0]}. Use high, medium, or low.`,
329
+ `Invalid tier: ${args[0]}. Use high, medium, low, or micro.`,
319
330
  'error',
320
331
  );
321
332
  return;
322
333
  }
323
334
  }
324
335
 
325
- if (tier !== 'all' && !tierValues.includes(tier as string)) {
336
+ if (tier !== 'all' && !tier) {
326
337
  ctx.ui.notify(
327
- `Invalid tier: ${tier}. Use high, medium, or low.`,
338
+ `Invalid tier: ${tier}. Use high, medium, low, or micro.`,
328
339
  'error',
329
340
  );
330
341
  return;
@@ -338,14 +349,34 @@ export const registerCommands = (
338
349
  }
339
350
 
340
351
  const nextLevel =
341
- levelValue === 'auto' ? undefined : (levelValue as ThinkingLevel);
342
- state.thinkingByProfile[currentProfile] ??= {};
343
- const overrides = state.thinkingByProfile[currentProfile];
344
- const tiers = tier === 'all' ? ROUTER_TIERS : [tier as RouterTier];
352
+ levelValue === 'auto'
353
+ ? undefined
354
+ : isThinkingLevel(levelValue)
355
+ ? levelValue
356
+ : undefined;
357
+ const overrides = { ...state.thinkingByProfile[currentProfile] };
358
+ const tiers = tier === 'all' ? ROUTER_TIERS : [tier];
345
359
  for (const targetTier of tiers) {
346
360
  if (nextLevel) overrides[targetTier] = nextLevel;
347
361
  else delete overrides[targetTier];
348
362
  }
363
+ const activeProfile = state.currentConfig.profiles[currentProfile];
364
+ if (
365
+ nextLevel &&
366
+ activeProfile &&
367
+ preservesRouteCoverage(
368
+ activeProfile,
369
+ (provider, id) => ctx.modelRegistry.find(provider, id),
370
+ overrides,
371
+ ) === false
372
+ ) {
373
+ ctx.ui.notify(
374
+ `Router thinking unchanged: '${nextLevel}' leaves no eligible route.`,
375
+ 'warning',
376
+ );
377
+ return;
378
+ }
379
+ state.thinkingByProfile[currentProfile] = overrides;
349
380
  if (Object.keys(overrides).length === 0) {
350
381
  delete state.thinkingByProfile[currentProfile];
351
382
  }
@@ -359,14 +390,13 @@ export const registerCommands = (
359
390
  }
360
391
  // Only warn when the level isn't supported by some tiers; skip for 'off' and 'auto'
361
392
  if (nextLevel && nextLevel !== 'off') {
362
- const unsupported = getUnsupportedTiers(
363
- state.currentConfig.profiles[currentProfile],
364
- nextLevel,
365
- );
393
+ const activeProfile = state.currentConfig.profiles[currentProfile];
394
+ if (!activeProfile) return;
395
+ const unsupported = getUnsupportedTiers(activeProfile, nextLevel);
366
396
  if (unsupported.length > 0) {
367
397
  ctx.ui.notify(
368
398
  `Router thinking (${tier}) set to ${nextLevel}. ` +
369
- `${unsupported.join(', ')} tier${unsupported.length > 1 ? 's' : ''} may not support '${nextLevel}'.`,
399
+ `${unsupported.join(', ')} tier${unsupported.length > 1 ? 's' : ''} may not support '${nextLevel}' and will be skipped when unsupported.`,
370
400
  'warning',
371
401
  );
372
402
  }
@@ -412,19 +442,19 @@ export const registerCommands = (
412
442
 
413
443
  const handleFix = async (args: string[], ctx: ExtensionContext) => {
414
444
  if (args.length !== 1) {
415
- ctx.ui.notify('Usage: /router fix <high|medium|low>', 'error');
445
+ ctx.ui.notify('Usage: /router fix <high|medium|low|micro>', 'error');
416
446
  return;
417
447
  }
418
448
  const tier = args[0]?.toLowerCase();
419
- if (!ROUTER_TIERS.includes(tier as RouterTier)) {
420
- ctx.ui.notify('Usage: /router fix <high|medium|low>', 'error');
449
+ if (!isRouterTier(tier)) {
450
+ ctx.ui.notify('Usage: /router fix <high|medium|low|micro>', 'error');
421
451
  return;
422
452
  }
423
453
  if (!state.lastDecision) {
424
454
  ctx.ui.notify('No recent routing decision to fix.', 'warning');
425
455
  return;
426
456
  }
427
- state.pinnedTierByProfile[state.lastDecision.profile] = tier as RouterTier;
457
+ state.pinnedTierByProfile[state.lastDecision.profile] = tier;
428
458
  actions.persistState();
429
459
  actions.updateStatus(ctx);
430
460
  ctx.ui.notify(
@@ -515,10 +545,12 @@ export const registerCommands = (
515
545
  }
516
546
 
517
547
  if (parts.length === 1 && !hasTrailingSpace) {
518
- return getSubcommandCompletions(parts[0]);
548
+ const subcommand = parts[0];
549
+ return subcommand ? getSubcommandCompletions(subcommand) : null;
519
550
  }
520
551
 
521
552
  const subcommand = parts[0];
553
+ if (!subcommand) return null;
522
554
  const subArgs = parts.slice(1);
523
555
  if (hasTrailingSpace && parts.length === 1) {
524
556
  subArgs.push('');
@@ -558,13 +590,13 @@ export const registerCommands = (
558
590
  }
559
591
  case 'fix': {
560
592
  const fixPrefix = subArgs[0] ?? '';
561
- const items = ['high', 'medium', 'low']
562
- .filter((t) => t.startsWith(fixPrefix.toLowerCase()))
563
- .map((t) => ({
564
- value: `fix ${t}`,
565
- label: t,
566
- description: `Correct decision and pin to ${t} tier`,
567
- }));
593
+ const items = ROUTER_TIERS.filter((t) =>
594
+ t.startsWith(fixPrefix.toLowerCase()),
595
+ ).map((t) => ({
596
+ value: `fix ${t}`,
597
+ label: t,
598
+ description: `Correct decision and pin to ${t} tier`,
599
+ }));
568
600
  return items.length > 0 ? items : null;
569
601
  }
570
602
  case 'widget': {
@@ -597,6 +629,10 @@ export const registerCommands = (
597
629
  const parts = args?.trim().split(/\s+/) ?? [];
598
630
  const subcommand = parts[0];
599
631
  const subArgs = parts.slice(1);
632
+ if (!subcommand) {
633
+ await handleStatus(subArgs, ctx);
634
+ return;
635
+ }
600
636
 
601
637
  switch (subcommand) {
602
638
  case 'profile':
@@ -637,7 +673,7 @@ export const registerCommands = (
637
673
  'Router Subcommands:',
638
674
  ' status Show current status, profile, pin, cost, and last decision.',
639
675
  ' profile [name] Switch to a profile (enables router if off). Lists available if no name.',
640
- ' pin <tier|auto> Force a tier (high|medium|low) or set to auto.',
676
+ ' pin <tier|auto> Force a tier (high|medium|low|micro) or set to auto.',
641
677
  ' thinking [tier] <level> Override thinking level (off|minimal|...|max|auto). Not all tier models may support every level.',
642
678
  ' disable Disable the router and restore the last used non-router model.',
643
679
  ' fix <tier> Correct the last routing decision and pin that tier for the current profile.',