@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.
- package/CHANGELOG.md +10 -0
- package/README.md +4 -2
- package/extensions/classifier.ts +93 -0
- package/extensions/commands.ts +42 -22
- package/extensions/config.ts +49 -46
- package/extensions/context.ts +58 -0
- package/extensions/index.ts +96 -127
- package/extensions/provider.ts +54 -41
- package/extensions/routing.ts +13 -156
- package/extensions/state.ts +21 -15
- package/extensions/types.ts +73 -45
- package/extensions/ui.ts +14 -28
- package/package.json +4 -4
package/extensions/state.ts
CHANGED
|
@@ -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
|
|
133
|
-
selectedProfile
|
|
134
|
-
pinnedTierByProfile
|
|
135
|
-
thinkingByProfile
|
|
136
|
-
debugEnabled
|
|
137
|
-
widgetEnabled
|
|
138
|
-
debugHistory
|
|
139
|
-
lastDecision
|
|
140
|
-
lastNonRouterModel
|
|
141
|
-
accumulatedCost
|
|
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
|
-
|
|
147
|
-
|
|
150
|
+
...(selectedProfile && pinnedTierByProfile[selectedProfile]
|
|
151
|
+
? { pinTier: pinnedTierByProfile[selectedProfile] }
|
|
152
|
+
: {}),
|
|
153
|
+
pinByProfile,
|
|
148
154
|
thinkingByProfile: { ...thinkingByProfile },
|
|
149
155
|
debugEnabled,
|
|
150
156
|
widgetEnabled,
|
package/extensions/types.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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} (${
|
|
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.
|
|
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": "
|
|
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@
|
|
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 .",
|