@alexeiled/pi-model-router 0.5.1 → 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.
@@ -8,10 +8,10 @@ import {
8
8
  parseCanonicalModelRef,
9
9
  } from './config';
10
10
  import type {
11
+ PersistedStateInput,
11
12
  RouterLastProfileState,
12
13
  RouterPersistedState,
13
14
  RouterPinByProfile,
14
- RouterThinkingByProfile,
15
15
  RoutingDecision,
16
16
  } from './types';
17
17
 
@@ -128,23 +128,29 @@ export const isRouterPersistedState = (
128
128
  );
129
129
  };
130
130
 
131
- export const buildPersistedState = (
132
- routerEnabled: boolean,
133
- selectedProfile: string | undefined,
134
- pinnedTierByProfile: RouterPinByProfile,
135
- thinkingByProfile: RouterThinkingByProfile,
136
- debugEnabled: boolean,
137
- widgetEnabled: boolean,
138
- debugHistory: RoutingDecision[],
139
- lastDecision: RoutingDecision | undefined,
140
- lastNonRouterModel: string | undefined,
141
- accumulatedCost: number,
142
- ): RouterPersistedState => {
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
+ }
143
147
  return structuredClone({
144
148
  enabled: routerEnabled,
145
149
  selectedProfile: selectedProfile ?? '',
146
- pinTier: selectedProfile ? pinnedTierByProfile[selectedProfile] : undefined,
147
- pinByProfile: { ...pinnedTierByProfile },
150
+ ...(selectedProfile && pinnedTierByProfile[selectedProfile]
151
+ ? { pinTier: pinnedTierByProfile[selectedProfile] }
152
+ : {}),
153
+ pinByProfile,
148
154
  thinkingByProfile: { ...thinkingByProfile },
149
155
  debugEnabled,
150
156
  widgetEnabled,
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexeiled/pi-model-router",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "extensions",
@@ -11,7 +11,7 @@
11
11
  "README.md",
12
12
  "CHANGELOG.md"
13
13
  ],
14
- "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.",
15
15
  "keywords": [
16
16
  "pi-package",
17
17
  "pi",
@@ -24,7 +24,7 @@
24
24
  "engines": {
25
25
  "node": ">=22.19.0"
26
26
  },
27
- "packageManager": "npm@11.18.0",
27
+ "packageManager": "npm@12.0.2",
28
28
  "author": "Alexei Ledenev",
29
29
  "contributors": [
30
30
  "Ye Liu"
@@ -52,7 +52,7 @@
52
52
  "tsc": "tsc --noEmit",
53
53
  "build": "tsc",
54
54
  "prepublishOnly": "npm run check && npm test",
55
- "test": "vitest run",
55
+ "test": "vitest run --pool=threads",
56
56
  "check": "biome check --error-on-warnings . && npm run tsc",
57
57
  "lint": "biome lint --error-on-warnings .",
58
58
  "lint:fix": "biome lint --write --error-on-warnings .",