@alexeiled/pi-model-router 0.5.0 → 0.5.2

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.
@@ -1,13 +1,12 @@
1
- import type { ThinkingLevel } from '@earendil-works/pi-agent-core';
2
- import type { Context, Message } from '@earendil-works/pi-ai';
3
- import { streamSimple } from '@earendil-works/pi-ai/compat';
4
- import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
5
- import { isRouterTier, parseCanonicalModelRef } from './config';
1
+ import type { Context } from '@earendil-works/pi-ai';
2
+ import { parseCanonicalModelRef } from './config';
6
3
  import {
7
- hasUsableRequestAuth,
8
- type RegistryWithProviderAuth,
9
- resolveDelegatedModel,
10
- } from './constants';
4
+ containsAny,
5
+ countToolResults,
6
+ countWords,
7
+ getLastUserText,
8
+ getRecentConversationText,
9
+ } from './context';
11
10
  import type {
12
11
  RouterPhase,
13
12
  RouterProfile,
@@ -17,67 +16,6 @@ import type {
17
16
  RoutingRule,
18
17
  } from './types';
19
18
 
20
- export const extractTextFromContent = (
21
- content: string | Message['content'],
22
- ): string => {
23
- if (typeof content === 'string') {
24
- return content;
25
- }
26
- return content
27
- .map((part) => {
28
- if (part.type === 'text') return part.text;
29
- if (part.type === 'thinking') return part.thinking;
30
- if (part.type === 'toolCall')
31
- return `${part.name} ${JSON.stringify(part.arguments)}`;
32
- return '';
33
- })
34
- .filter(Boolean)
35
- .join('\n');
36
- };
37
-
38
- export const getLastUserText = (context: Context): string => {
39
- for (let i = context.messages.length - 1; i >= 0; i--) {
40
- const message = context.messages[i];
41
- if (message.role === 'user') {
42
- return extractTextFromContent(message.content).trim();
43
- }
44
- }
45
- return '';
46
- };
47
-
48
- export const getRecentConversationText = (
49
- context: Context,
50
- limit = 6,
51
- ): string => {
52
- return context.messages
53
- .slice(-limit)
54
- .map((message) => extractTextFromContent(message.content).trim())
55
- .filter(Boolean)
56
- .join('\n')
57
- .toLowerCase();
58
- };
59
-
60
- export const countToolResults = (context: Context): number => {
61
- return context.messages.filter((message) => message.role === 'toolResult')
62
- .length;
63
- };
64
-
65
- export const countWords = (text: string): number => {
66
- return text.split(/\s+/).filter(Boolean).length;
67
- };
68
-
69
- export const hasImageAttachment = (context: Context): boolean => {
70
- return context.messages.some(
71
- (message) =>
72
- Array.isArray(message.content) &&
73
- message.content.some((part) => part.type === 'image'),
74
- );
75
- };
76
-
77
- export const containsAny = (text: string, keywords: string[]): boolean => {
78
- return keywords.some((keyword) => text.includes(keyword));
79
- };
80
-
81
19
  export const phaseForTier = (tier: RouterTier): RouterPhase => {
82
20
  if (tier === 'high') return 'planning';
83
21
  if (tier === 'medium') return 'implementation';
@@ -93,11 +31,13 @@ export const resolveAvailableTier = (
93
31
  const order: RouterTier[] = ['low', 'medium', 'high'];
94
32
  const startIdx = order.indexOf(preferred);
95
33
  for (let i = startIdx + 1; i < order.length; i++) {
96
- if (profile[order[i]]) return order[i];
34
+ const tier = order[i];
35
+ if (tier && profile[tier]) return tier;
97
36
  }
98
37
  // Fall "down" as last resort
99
38
  for (let i = startIdx - 1; i >= 0; i--) {
100
- if (profile[order[i]]) return order[i];
39
+ const tier = order[i];
40
+ if (tier && profile[tier]) return tier;
101
41
  }
102
42
  return preferred; // unreachable if profile has ≥1 tier
103
43
  };
@@ -148,6 +88,7 @@ export const decideRouting = (
148
88
  rules?: RoutingRule[],
149
89
  isBudgetExceeded = false,
150
90
  ): RoutingDecision => {
91
+ if (previousDecision?.profile !== profileName) previousDecision = undefined;
151
92
  const prompt = getLastUserText(context).toLowerCase();
152
93
  const recentConversation = getRecentConversationText(context);
153
94
  const toolResultCount = countToolResults(context);
@@ -372,7 +313,10 @@ export const decideRouting = (
372
313
  }
373
314
 
374
315
  // Resolve to nearest available tier if the selected tier is disabled
375
- const resolvedTier = resolveAvailableTier(profile, tier);
316
+ const resolvedTier =
317
+ isBudgetForced && !profile.medium && profile.low
318
+ ? 'low'
319
+ : resolveAvailableTier(profile, tier);
376
320
  if (resolvedTier !== tier) {
377
321
  reasoning = `Resolved from ${tier} to ${resolvedTier} tier (${tier} tier is not configured). Original: ${reasoning}`;
378
322
  phase = phaseForTier(resolvedTier);
@@ -392,92 +336,3 @@ export const decideRouting = (
392
336
  decision.isBudgetForced = isBudgetForced;
393
337
  return decision;
394
338
  };
395
-
396
- export const runClassifier = async (
397
- classifierModelRef: string,
398
- modelRegistry: ExtensionContext['modelRegistry'],
399
- context: Context,
400
- currentPhase?: RouterPhase,
401
- thinking?: ThinkingLevel,
402
- ): Promise<{ tier: RouterTier; reasoning: string } | undefined> => {
403
- try {
404
- const { provider, modelId } = parseCanonicalModelRef(classifierModelRef);
405
- const model = modelRegistry.find(provider, modelId);
406
- if (!model) return undefined;
407
-
408
- const auth = await modelRegistry.getApiKeyAndHeaders(model);
409
- if (!auth.ok || !hasUsableRequestAuth(auth)) return undefined;
410
- const apiKey = auth.apiKey;
411
- const headers = auth.headers;
412
- const requestModel = await resolveDelegatedModel(
413
- modelRegistry as unknown as RegistryWithProviderAuth,
414
- model,
415
- );
416
-
417
- const promptText = getLastUserText(context);
418
- const historyText = getRecentConversationText(context, 4);
419
-
420
- const classifierPrompt = `You are a model router classifier. Your job is to categorize the user's latest request into one of three tiers: "high", "medium", or "low".
421
-
422
- Tiers:
423
- - high: Architecture, design, planning, tradeoff analysis, broad debugging, large refactors, codebase research.
424
- - medium: Implementation of a known plan, multi-file edits, normal coding work, focused debugging, tests/fixes.
425
- - low: Summaries, changelogs, formatting, quick explanations, small bounded transforms, simple read-only lookup.
426
-
427
- ${currentPhase ? `Current conversation phase: ${currentPhase}\n` : ''}
428
- Recent history:
429
- ${historyText}
430
-
431
- Latest user message:
432
- ${promptText}
433
-
434
- Return your decision in exactly two lines:
435
- Tier: [high|medium|low]
436
- Reasoning: [one short sentence]
437
-
438
- ${currentPhase === 'planning' ? 'Consider that the conversation is currently in a planning phase. Bias toward "high" unless the request is clearly a simple implementation or summary.' : ''}
439
- ${currentPhase === 'implementation' ? 'Consider that the conversation is currently in an implementation phase. Bias toward "medium" unless the request is clearly planning or a simple summary.' : ''}`;
440
-
441
- const classifierContext: Context = {
442
- messages: [
443
- { role: 'user', content: classifierPrompt, timestamp: Date.now() },
444
- ],
445
- };
446
-
447
- const reasoningOption =
448
- model.reasoning && thinking && thinking !== 'off' ? thinking : undefined;
449
-
450
- const stream = streamSimple(requestModel, classifierContext, {
451
- apiKey,
452
- headers,
453
- ...(reasoningOption ? { reasoning: reasoningOption } : {}),
454
- });
455
- let fullText = '';
456
- for await (const event of stream) {
457
- if (event.type === 'text_delta' && typeof event.delta === 'string') {
458
- fullText += event.delta;
459
- }
460
- }
461
-
462
- const lines = fullText.trim().split('\n');
463
- const tierLine = lines.find((l) => l.toLowerCase().startsWith('tier:'));
464
- const reasoningLine = lines.find((l) =>
465
- l.toLowerCase().startsWith('reasoning:'),
466
- );
467
-
468
- if (tierLine) {
469
- const tierValue = tierLine.split(':')[1].trim().toLowerCase();
470
- if (isRouterTier(tierValue)) {
471
- return {
472
- tier: tierValue,
473
- reasoning: reasoningLine
474
- ? reasoningLine.split(':')[1].trim()
475
- : 'Classifier decision.',
476
- };
477
- }
478
- }
479
- } catch {
480
- // Ignore classifier errors and fall back to heuristics
481
- }
482
- return undefined;
483
- };
@@ -1,23 +1,61 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { getAgentDir } from '@earendil-works/pi-coding-agent';
4
+ import {
5
+ isObjectRecord,
6
+ isRouterTier,
7
+ isThinkingLevel,
8
+ parseCanonicalModelRef,
9
+ } from './config';
4
10
  import type {
11
+ PersistedStateInput,
5
12
  RouterLastProfileState,
6
13
  RouterPersistedState,
7
14
  RouterPinByProfile,
8
- RouterThinkingByProfile,
9
15
  RoutingDecision,
10
16
  } from './types';
11
17
 
12
18
  const LAST_PROFILE_STATE_FILE = 'model-router-state.json';
13
19
 
14
- const isRecord = (value: unknown): value is Record<string, unknown> =>
15
- typeof value === 'object' && value !== null && !Array.isArray(value);
20
+ const isPhase = (value: unknown) =>
21
+ value === 'planning' || value === 'implementation' || value === 'lightweight';
22
+ const isFiniteNumber = (value: unknown): value is number =>
23
+ typeof value === 'number' && Number.isFinite(value);
24
+ const isModelRef = (value: unknown) => {
25
+ if (typeof value !== 'string') return false;
26
+ try {
27
+ parseCanonicalModelRef(value);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ };
33
+ const isDecision = (value: unknown): value is RoutingDecision =>
34
+ isObjectRecord(value) &&
35
+ isRouterTier(value.tier) &&
36
+ isPhase(value.phase) &&
37
+ isThinkingLevel(value.thinking) &&
38
+ isFiniteNumber(value.timestamp) &&
39
+ [
40
+ 'profile',
41
+ 'targetProvider',
42
+ 'targetModelId',
43
+ 'targetLabel',
44
+ 'reasoning',
45
+ ].every((key) => typeof value[key] === 'string') &&
46
+ ['isClassifier', 'isFallback', 'isBudgetForced', 'isRuleMatched'].every(
47
+ (key) => value[key] === undefined || typeof value[key] === 'boolean',
48
+ );
49
+ const isMap = (value: unknown, validate: (entry: unknown) => boolean) =>
50
+ isObjectRecord(value) &&
51
+ Object.entries(value).every(
52
+ ([key, entry]) => key !== '__proto__' && validate(entry),
53
+ );
16
54
 
17
55
  export const isRouterLastProfileState = (
18
56
  value: unknown,
19
57
  ): value is RouterLastProfileState =>
20
- isRecord(value) &&
58
+ isObjectRecord(value) &&
21
59
  typeof value.selectedProfile === 'string' &&
22
60
  value.selectedProfile.length > 0 &&
23
61
  typeof value.timestamp === 'number';
@@ -58,36 +96,61 @@ export const saveLastRouterProfile = (
58
96
  export const isRouterPersistedState = (
59
97
  value: unknown,
60
98
  ): value is RouterPersistedState => {
61
- if (typeof value !== 'object' || value === null) {
62
- return false;
63
- }
64
- if (!isRecord(value)) {
65
- return false;
66
- }
99
+ if (!isObjectRecord(value)) return false;
67
100
  return (
68
101
  typeof value.enabled === 'boolean' &&
69
102
  typeof value.selectedProfile === 'string' &&
70
- typeof value.timestamp === 'number'
103
+ isFiniteNumber(value.timestamp) &&
104
+ (value.pinTier === undefined || isRouterTier(value.pinTier)) &&
105
+ (value.pinByProfile === undefined ||
106
+ isMap(value.pinByProfile, isRouterTier)) &&
107
+ (value.thinkingByProfile === undefined ||
108
+ isMap(
109
+ value.thinkingByProfile,
110
+ (tiers) =>
111
+ isObjectRecord(tiers) &&
112
+ Object.entries(tiers).every(
113
+ ([tier, level]) => isRouterTier(tier) && isThinkingLevel(level),
114
+ ),
115
+ )) &&
116
+ (value.lastDecision === undefined || isDecision(value.lastDecision)) &&
117
+ (value.debugHistory === undefined ||
118
+ (Array.isArray(value.debugHistory) &&
119
+ value.debugHistory.every(isDecision))) &&
120
+ (value.lastPhase === undefined || isPhase(value.lastPhase)) &&
121
+ (value.lastNonRouterModel === undefined ||
122
+ isModelRef(value.lastNonRouterModel)) &&
123
+ (value.accumulatedCost === undefined ||
124
+ (isFiniteNumber(value.accumulatedCost) && value.accumulatedCost >= 0)) &&
125
+ ['debugEnabled', 'widgetEnabled'].every(
126
+ (key) => value[key] === undefined || typeof value[key] === 'boolean',
127
+ )
71
128
  );
72
129
  };
73
130
 
74
- export const buildPersistedState = (
75
- routerEnabled: boolean,
76
- selectedProfile: string | undefined,
77
- pinnedTierByProfile: RouterPinByProfile,
78
- thinkingByProfile: RouterThinkingByProfile,
79
- debugEnabled: boolean,
80
- widgetEnabled: boolean,
81
- debugHistory: RoutingDecision[],
82
- lastDecision: RoutingDecision | undefined,
83
- lastNonRouterModel: string | undefined,
84
- accumulatedCost: number,
85
- ): RouterPersistedState => {
86
- return {
131
+ export const buildPersistedState = ({
132
+ routerEnabled,
133
+ selectedProfile,
134
+ pinnedTierByProfile,
135
+ thinkingByProfile,
136
+ debugEnabled,
137
+ widgetEnabled,
138
+ debugHistory,
139
+ lastDecision,
140
+ lastNonRouterModel,
141
+ accumulatedCost,
142
+ }: PersistedStateInput): RouterPersistedState => {
143
+ const pinByProfile: RouterPinByProfile = {};
144
+ for (const [profile, tier] of Object.entries(pinnedTierByProfile)) {
145
+ if (tier) pinByProfile[profile] = tier;
146
+ }
147
+ return structuredClone({
87
148
  enabled: routerEnabled,
88
149
  selectedProfile: selectedProfile ?? '',
89
- pinTier: selectedProfile ? pinnedTierByProfile[selectedProfile] : undefined,
90
- pinByProfile: { ...pinnedTierByProfile },
150
+ ...(selectedProfile && pinnedTierByProfile[selectedProfile]
151
+ ? { pinTier: pinnedTierByProfile[selectedProfile] }
152
+ : {}),
153
+ pinByProfile,
91
154
  thinkingByProfile: { ...thinkingByProfile },
92
155
  debugEnabled,
93
156
  widgetEnabled,
@@ -97,5 +160,5 @@ export const buildPersistedState = (
97
160
  lastNonRouterModel,
98
161
  accumulatedCost,
99
162
  timestamp: Date.now(),
100
- };
163
+ });
101
164
  };
@@ -10,49 +10,60 @@ export type RouterThinkingByProfile = Record<string, RouterThinkingByTier>;
10
10
  export interface RoutingRule {
11
11
  matches: string | string[];
12
12
  tier: RouterTier;
13
- reason?: string;
13
+ reason?: string | undefined;
14
14
  }
15
15
 
16
16
  export interface ModelDefinition {
17
17
  model: string;
18
- contextWindow?: number;
19
- maxTokens?: number;
20
- reasoning?: boolean;
21
- thinkingLevels?: ThinkingLevel[];
18
+ contextWindow?: number | undefined;
19
+ maxTokens?: number | undefined;
20
+ reasoning?: boolean | undefined;
21
+ thinkingLevels?: ThinkingLevel[] | undefined;
22
22
  }
23
23
 
24
24
  export interface ClassifierConfig {
25
25
  model: string;
26
- thinking?: ThinkingLevel;
26
+ thinking?: ThinkingLevel | undefined;
27
27
  }
28
28
 
29
29
  export interface RoutedTierConfig {
30
30
  model: string;
31
- thinking?: ThinkingLevel;
32
- fallbacks?: string[];
33
- contextWindow?: number;
34
- maxTokens?: number;
35
- reasoning?: boolean;
36
- thinkingLevels?: ThinkingLevel[];
37
- resolvedContextWindow?: number;
38
- resolvedMaxTokens?: number;
39
- resolvedThinkingLevels?: ThinkingLevel[];
31
+ thinking?: ThinkingLevel | undefined;
32
+ fallbacks?: string[] | undefined;
33
+ contextWindow?: number | undefined;
34
+ maxTokens?: number | undefined;
35
+ reasoning?: boolean | undefined;
36
+ thinkingLevels?: ThinkingLevel[] | undefined;
37
+ resolvedContextWindow?: number | undefined;
38
+ resolvedMaxTokens?: number | undefined;
39
+ resolvedThinkingLevels?: ThinkingLevel[] | undefined;
40
40
  }
41
41
 
42
42
  export interface RouterProfile {
43
- high?: RoutedTierConfig;
44
- medium?: RoutedTierConfig;
45
- low?: RoutedTierConfig;
43
+ high?: RoutedTierConfig | undefined;
44
+ medium?: RoutedTierConfig | undefined;
45
+ low?: RoutedTierConfig | undefined;
46
46
  }
47
47
 
48
48
  export interface RouterConfig {
49
- debug?: boolean;
50
- classifierModel?: ClassifierConfig;
51
- phaseBias?: number;
52
- maxSessionBudget?: number;
53
- rules?: RoutingRule[];
49
+ debug?: boolean | undefined;
50
+ classifierModel?: ClassifierConfig | undefined;
51
+ phaseBias?: number | undefined;
52
+ maxSessionBudget?: number | undefined;
53
+ rules?: RoutingRule[] | undefined;
54
54
  profiles: Record<string, RouterProfile>;
55
- models?: Record<string, ModelDefinition>;
55
+ models?: Record<string, ModelDefinition> | undefined;
56
+ }
57
+
58
+ export interface RouterStatusState {
59
+ routerEnabled: boolean;
60
+ selectedProfile: string | undefined;
61
+ pinnedTierByProfile: RouterPinByProfile;
62
+ lastDecision: RoutingDecision | undefined;
63
+ lastNonRouterModel: string | undefined;
64
+ accumulatedCost: number;
65
+ widgetEnabled: boolean;
66
+ currentConfig: RouterConfig;
56
67
  }
57
68
 
58
69
  export interface RoutingDecision {
@@ -65,10 +76,10 @@ export interface RoutingDecision {
65
76
  reasoning: string;
66
77
  thinking: ThinkingLevel;
67
78
  timestamp: number;
68
- isClassifier?: boolean;
69
- isFallback?: boolean;
70
- isBudgetForced?: boolean;
71
- isRuleMatched?: boolean;
79
+ isClassifier?: boolean | undefined;
80
+ isFallback?: boolean | undefined;
81
+ isBudgetForced?: boolean | undefined;
82
+ isRuleMatched?: boolean | undefined;
72
83
  }
73
84
 
74
85
  export interface RouterLastProfileState {
@@ -76,34 +87,51 @@ export interface RouterLastProfileState {
76
87
  timestamp: number;
77
88
  }
78
89
 
90
+ export interface PersistedStateInput {
91
+ routerEnabled: boolean;
92
+ selectedProfile: string | undefined;
93
+ pinnedTierByProfile: RouterPinByProfile;
94
+ thinkingByProfile: RouterThinkingByProfile;
95
+ debugEnabled: boolean;
96
+ widgetEnabled: boolean;
97
+ debugHistory: RoutingDecision[];
98
+ lastDecision: RoutingDecision | undefined;
99
+ lastNonRouterModel: string | undefined;
100
+ accumulatedCost: number;
101
+ }
102
+
79
103
  export interface RouterPersistedState {
80
104
  enabled: boolean;
81
105
  selectedProfile: string;
82
- pinTier?: RouterTier;
83
- pinByProfile?: RouterPinByProfile;
84
- thinkingByProfile?: RouterThinkingByProfile;
85
- debugEnabled?: boolean;
86
- widgetEnabled?: boolean;
87
- debugHistory?: RoutingDecision[];
88
- lastPhase?: RouterPhase;
89
- lastDecision?: RoutingDecision;
90
- lastNonRouterModel?: string;
91
- accumulatedCost?: number;
106
+ pinTier?: RouterTier | undefined;
107
+ pinByProfile?: RouterPinByProfile | undefined;
108
+ thinkingByProfile?: RouterThinkingByProfile | undefined;
109
+ debugEnabled?: boolean | undefined;
110
+ widgetEnabled?: boolean | undefined;
111
+ debugHistory?: RoutingDecision[] | undefined;
112
+ lastPhase?: RouterPhase | undefined;
113
+ lastDecision?: RoutingDecision | undefined;
114
+ lastNonRouterModel?: string | undefined;
115
+ accumulatedCost?: number | undefined;
92
116
  timestamp: number;
93
117
  }
94
118
 
119
+ export interface RawRouterConfig {
120
+ debug?: unknown;
121
+ classifierModel?: unknown;
122
+ phaseBias?: unknown;
123
+ maxSessionBudget?: unknown;
124
+ rules?: unknown;
125
+ profiles?: unknown;
126
+ models?: unknown;
127
+ }
128
+
95
129
  export interface ConfigLoadResult {
96
130
  config: RouterConfig;
97
131
  warnings: string[];
98
132
  }
99
133
 
100
134
  export interface ParsedConfigFile {
101
- config: Partial<RouterConfig>;
135
+ config: RawRouterConfig;
102
136
  warnings: string[];
103
137
  }
104
-
105
- export interface CustomSessionEntry {
106
- type: string;
107
- customType?: string;
108
- data?: unknown;
109
- }
package/extensions/ui.ts CHANGED
@@ -1,17 +1,11 @@
1
1
  import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
2
  import type {
3
- RouterConfig,
4
3
  RouterPinByProfile,
4
+ RouterStatusState,
5
5
  RouterThinkingByProfile,
6
6
  RoutingDecision,
7
7
  } from './types';
8
8
 
9
- const getEffectiveThinking = (
10
- thinkingByProfile: RouterThinkingByProfile,
11
- profileName: string,
12
- decision: RoutingDecision,
13
- ) => thinkingByProfile[profileName]?.[decision.tier] ?? decision.thinking;
14
-
15
9
  const getDecisionFlags = (decision: RoutingDecision): string[] => {
16
10
  const flags: string[] = [];
17
11
  if (decision.isFallback) flags.push('fallback');
@@ -53,16 +47,18 @@ export const formatModelRef = (ref: string | undefined): string => {
53
47
 
54
48
  export const updateStatus = (
55
49
  ctx: ExtensionContext,
56
- routerEnabled: boolean,
57
- selectedProfile: string | undefined,
58
- pinnedTierByProfile: RouterPinByProfile,
59
- thinkingByProfile: RouterThinkingByProfile,
60
- lastDecision: RoutingDecision | undefined,
61
- lastNonRouterModel: string | undefined,
62
- accumulatedCost: number,
63
- widgetEnabled: boolean,
64
- currentConfig: RouterConfig,
50
+ state: RouterStatusState,
65
51
  ) => {
52
+ const {
53
+ routerEnabled,
54
+ selectedProfile,
55
+ pinnedTierByProfile,
56
+ lastDecision,
57
+ lastNonRouterModel,
58
+ accumulatedCost,
59
+ widgetEnabled,
60
+ currentConfig,
61
+ } = state;
66
62
  const activeRouterProfile = routerEnabled ? selectedProfile : undefined;
67
63
  const statusProfile = selectedProfile ?? 'none';
68
64
  const activePin = selectedProfile
@@ -77,12 +73,7 @@ export const updateStatus = (
77
73
 
78
74
  let statusText: string;
79
75
  if (lastDecision && matchesProfile && matchesPin) {
80
- const effectiveThinking = getEffectiveThinking(
81
- thinkingByProfile,
82
- activeRouterProfile,
83
- lastDecision,
84
- );
85
- statusText = `router:${activeRouterProfile}${pinLabel} -> ${lastDecision.tier} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${effectiveThinking})`;
76
+ statusText = `router:${activeRouterProfile}${pinLabel} -> ${lastDecision.tier} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${lastDecision.thinking})`;
86
77
  } else {
87
78
  statusText = `router:${activeRouterProfile}${pinLabel} -> waiting`;
88
79
  }
@@ -106,16 +97,11 @@ export const updateStatus = (
106
97
  : ''),
107
98
  ];
108
99
  if (lastDecision && lastDecision.profile === statusProfile) {
109
- const effectiveThinking = getEffectiveThinking(
110
- thinkingByProfile,
111
- statusProfile,
112
- lastDecision,
113
- );
114
100
  const flags = getDecisionFlags(lastDecision);
115
101
  const flagsStr = flags.length > 0 ? ` [${flags.join(',')}]` : '';
116
102
 
117
103
  widgetLines.push(
118
- `Route: ${lastDecision.tier}${flagsStr} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${effectiveThinking})`,
104
+ `Route: ${lastDecision.tier}${flagsStr} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${lastDecision.thinking})`,
119
105
  `Phase: ${lastDecision.phase}`,
120
106
  );
121
107
  } else if (!routerEnabled && lastNonRouterModel) {
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@alexeiled/pi-model-router",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "extensions",
7
- "!extensions/*.test.ts",
7
+ "!extensions/**/*.test.ts",
8
+ "!extensions/test",
8
9
  "LICENSE",
9
10
  "model-router.example.json",
10
11
  "README.md",
11
12
  "CHANGELOG.md"
12
13
  ],
13
- "description": "Smart per-turn model router for Pi that optimizes your AI budget and usage limits by dynamically switching LLM tiers based on task complexity, budget, and phase awareness.",
14
+ "description": "Independently maintained Pi model router with tiered routing, soft budget controls, classifier fallback, and custom-provider support.",
14
15
  "keywords": [
15
16
  "pi-package",
16
17
  "pi",
@@ -23,7 +24,7 @@
23
24
  "engines": {
24
25
  "node": ">=22.19.0"
25
26
  },
26
- "packageManager": "npm@11.18.0",
27
+ "packageManager": "npm@12.0.2",
27
28
  "author": "Alexei Ledenev",
28
29
  "contributors": [
29
30
  "Ye Liu"
@@ -51,7 +52,7 @@
51
52
  "tsc": "tsc --noEmit",
52
53
  "build": "tsc",
53
54
  "prepublishOnly": "npm run check && npm test",
54
- "test": "vitest run",
55
+ "test": "vitest run --pool=threads",
55
56
  "check": "biome check --error-on-warnings . && npm run tsc",
56
57
  "lint": "biome lint --error-on-warnings .",
57
58
  "lint:fix": "biome lint --write --error-on-warnings .",