@alexeiled/pi-model-router 0.5.2 → 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.
- package/CHANGELOG.md +11 -0
- package/README.md +141 -22
- package/extensions/classifier.ts +96 -70
- package/extensions/commands.ts +39 -23
- package/extensions/config.ts +193 -102
- package/extensions/context.ts +61 -28
- package/extensions/index.ts +29 -10
- package/extensions/jev.ts +223 -0
- package/extensions/provider.ts +346 -145
- package/extensions/routing.ts +236 -296
- package/extensions/state.ts +53 -10
- package/extensions/types.ts +80 -12
- package/extensions/ui.ts +19 -7
- package/model-router.example.json +17 -10
- package/package.json +1 -1
package/extensions/types.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import type { ThinkingLevel } from '@earendil-works/pi-agent-core';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Descending routing complexity; all tier iteration and ranking derives here.
|
|
4
|
+
export const ROUTER_TIERS = ['high', 'medium', 'low', 'micro'] as const;
|
|
5
|
+
export type RouterTier = (typeof ROUTER_TIERS)[number];
|
|
6
|
+
export type ClassifierTier = RouterTier;
|
|
4
7
|
export type RouterPin = RouterTier | 'auto';
|
|
5
8
|
export type RouterPhase = 'planning' | 'implementation' | 'lightweight';
|
|
6
9
|
export type RouterPinByProfile = Partial<Record<string, RouterTier>>;
|
|
7
10
|
export type RouterThinkingByTier = Partial<Record<RouterTier, ThinkingLevel>>;
|
|
8
11
|
export type RouterThinkingByProfile = Record<string, RouterThinkingByTier>;
|
|
9
12
|
|
|
10
|
-
export interface RoutingRule {
|
|
11
|
-
matches: string | string[];
|
|
12
|
-
tier: RouterTier;
|
|
13
|
-
reason?: string | undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
13
|
export interface ModelDefinition {
|
|
17
14
|
model: string;
|
|
18
15
|
contextWindow?: number | undefined;
|
|
@@ -28,8 +25,12 @@ export interface ClassifierConfig {
|
|
|
28
25
|
|
|
29
26
|
export interface RoutedTierConfig {
|
|
30
27
|
model: string;
|
|
28
|
+
/** False for normalization defaults; omitted on legacy in-memory profiles. */
|
|
29
|
+
thinkingExplicit?: boolean | undefined;
|
|
31
30
|
thinking?: ThinkingLevel | undefined;
|
|
32
31
|
fallbacks?: string[] | undefined;
|
|
32
|
+
/** Canonical targets and exact alias metadata, in the same order as fallbacks. */
|
|
33
|
+
resolvedFallbacks?: ModelDefinition[] | undefined;
|
|
33
34
|
contextWindow?: number | undefined;
|
|
34
35
|
maxTokens?: number | undefined;
|
|
35
36
|
reasoning?: boolean | undefined;
|
|
@@ -39,18 +40,35 @@ export interface RoutedTierConfig {
|
|
|
39
40
|
resolvedThinkingLevels?: ThinkingLevel[] | undefined;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
export interface JevConfig {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
apiKey: string;
|
|
46
|
+
endpoint: string;
|
|
47
|
+
model: string;
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
confidenceThreshold: number;
|
|
50
|
+
maxStateChars: number;
|
|
51
|
+
mode: 'advisory';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface JevProfileConfig {
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
export interface RouterProfile {
|
|
59
|
+
baselineTier?: RouterTier | undefined;
|
|
60
|
+
jev?: JevProfileConfig | undefined;
|
|
43
61
|
high?: RoutedTierConfig | undefined;
|
|
44
62
|
medium?: RoutedTierConfig | undefined;
|
|
45
63
|
low?: RoutedTierConfig | undefined;
|
|
64
|
+
micro?: RoutedTierConfig | undefined;
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
export interface RouterConfig {
|
|
68
|
+
jev?: JevConfig | undefined;
|
|
49
69
|
debug?: boolean | undefined;
|
|
50
70
|
classifierModel?: ClassifierConfig | undefined;
|
|
51
|
-
phaseBias?: number | undefined;
|
|
52
71
|
maxSessionBudget?: number | undefined;
|
|
53
|
-
rules?: RoutingRule[] | undefined;
|
|
54
72
|
profiles: Record<string, RouterProfile>;
|
|
55
73
|
models?: Record<string, ModelDefinition> | undefined;
|
|
56
74
|
}
|
|
@@ -63,9 +81,57 @@ export interface RouterStatusState {
|
|
|
63
81
|
lastNonRouterModel: string | undefined;
|
|
64
82
|
accumulatedCost: number;
|
|
65
83
|
widgetEnabled: boolean;
|
|
66
|
-
|
|
84
|
+
maxSessionBudget: number | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RoutePair {
|
|
88
|
+
tier: RouterTier;
|
|
89
|
+
model: string;
|
|
90
|
+
thinking: ThinkingLevel;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface JevRouteCandidate extends RoutePair {
|
|
94
|
+
id: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface JevDependencies {
|
|
98
|
+
fetch?: typeof fetch;
|
|
99
|
+
now?: () => number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface JevRequest {
|
|
103
|
+
taskSummary: string;
|
|
104
|
+
candidates: readonly JevRouteCandidate[];
|
|
105
|
+
profile: JevProfileConfig | undefined;
|
|
106
|
+
/** Absolute monotonic deadline supplied by the routing orchestrator. */
|
|
107
|
+
routingDeadline: number;
|
|
108
|
+
signal?: AbortSignal | undefined;
|
|
67
109
|
}
|
|
68
110
|
|
|
111
|
+
/** Only allowlisted local identity and numeric diagnostics cross the adapter boundary. */
|
|
112
|
+
export interface JevAdvice {
|
|
113
|
+
candidateId: string;
|
|
114
|
+
confidence: number;
|
|
115
|
+
latencyMs: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const ROUTING_REASON_CODES = [
|
|
119
|
+
'baseline',
|
|
120
|
+
'pinned',
|
|
121
|
+
'continuation',
|
|
122
|
+
'classifier',
|
|
123
|
+
'jev',
|
|
124
|
+
'fallback',
|
|
125
|
+
'budget',
|
|
126
|
+
'legacy',
|
|
127
|
+
] as const;
|
|
128
|
+
export type RoutingReasonCode = (typeof ROUTING_REASON_CODES)[number];
|
|
129
|
+
export const isRoutingReasonCode = (
|
|
130
|
+
value: unknown,
|
|
131
|
+
): value is RoutingReasonCode =>
|
|
132
|
+
ROUTING_REASON_CODES.some((code) => code === value);
|
|
133
|
+
export type RoutingErrorClass = 'advisor-unavailable' | 'deadline';
|
|
134
|
+
|
|
69
135
|
export interface RoutingDecision {
|
|
70
136
|
profile: string;
|
|
71
137
|
tier: RouterTier;
|
|
@@ -73,13 +139,14 @@ export interface RoutingDecision {
|
|
|
73
139
|
targetProvider: string;
|
|
74
140
|
targetModelId: string;
|
|
75
141
|
targetLabel: string;
|
|
76
|
-
|
|
142
|
+
reasonCode: RoutingReasonCode;
|
|
143
|
+
routingLatencyMs?: number | undefined;
|
|
144
|
+
errorClass?: RoutingErrorClass | undefined;
|
|
77
145
|
thinking: ThinkingLevel;
|
|
78
146
|
timestamp: number;
|
|
79
147
|
isClassifier?: boolean | undefined;
|
|
80
148
|
isFallback?: boolean | undefined;
|
|
81
149
|
isBudgetForced?: boolean | undefined;
|
|
82
|
-
isRuleMatched?: boolean | undefined;
|
|
83
150
|
}
|
|
84
151
|
|
|
85
152
|
export interface RouterLastProfileState {
|
|
@@ -117,6 +184,7 @@ export interface RouterPersistedState {
|
|
|
117
184
|
}
|
|
118
185
|
|
|
119
186
|
export interface RawRouterConfig {
|
|
187
|
+
jev?: unknown;
|
|
120
188
|
debug?: unknown;
|
|
121
189
|
classifierModel?: unknown;
|
|
122
190
|
phaseBias?: unknown;
|
package/extensions/ui.ts
CHANGED
|
@@ -5,17 +5,23 @@ import type {
|
|
|
5
5
|
RouterThinkingByProfile,
|
|
6
6
|
RoutingDecision,
|
|
7
7
|
} from './types';
|
|
8
|
+
import { isRoutingReasonCode } from './types';
|
|
8
9
|
|
|
9
10
|
const getDecisionFlags = (decision: RoutingDecision): string[] => {
|
|
10
11
|
const flags: string[] = [];
|
|
11
12
|
if (decision.isFallback) flags.push('fallback');
|
|
12
13
|
if (decision.isBudgetForced) flags.push('budget-limit');
|
|
13
|
-
if (decision.isRuleMatched) flags.push('rule');
|
|
14
14
|
return flags;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
export const formatDecisionSource = (decision: RoutingDecision): string =>
|
|
18
|
+
isRoutingReasonCode(decision.reasonCode) && decision.reasonCode !== 'legacy'
|
|
19
|
+
? decision.reasonCode
|
|
20
|
+
: '';
|
|
21
|
+
|
|
17
22
|
export const formatDecision = (decision: RoutingDecision): string => {
|
|
18
|
-
|
|
23
|
+
const source = formatDecisionSource(decision);
|
|
24
|
+
return `${decision.profile}: ${decision.tier} -> ${decision.targetProvider}/${decision.targetModelId} [${decision.thinking}]${source ? ` (${source})` : ''}`;
|
|
19
25
|
};
|
|
20
26
|
|
|
21
27
|
export const formatPinSummary = (
|
|
@@ -57,7 +63,7 @@ export const updateStatus = (
|
|
|
57
63
|
lastNonRouterModel,
|
|
58
64
|
accumulatedCost,
|
|
59
65
|
widgetEnabled,
|
|
60
|
-
|
|
66
|
+
maxSessionBudget,
|
|
61
67
|
} = state;
|
|
62
68
|
const activeRouterProfile = routerEnabled ? selectedProfile : undefined;
|
|
63
69
|
const statusProfile = selectedProfile ?? 'none';
|
|
@@ -69,7 +75,7 @@ export const updateStatus = (
|
|
|
69
75
|
if (activeRouterProfile) {
|
|
70
76
|
const matchesProfile =
|
|
71
77
|
lastDecision && lastDecision.profile === activeRouterProfile;
|
|
72
|
-
const matchesPin = activePin
|
|
78
|
+
const matchesPin = !activePin || lastDecision?.tier === activePin;
|
|
73
79
|
|
|
74
80
|
let statusText: string;
|
|
75
81
|
if (lastDecision && matchesProfile && matchesPin) {
|
|
@@ -92,9 +98,7 @@ export const updateStatus = (
|
|
|
92
98
|
`Profile: ${statusProfile}${activeRouterProfile ? ' (active)' : ''}`,
|
|
93
99
|
`Pin: ${activePin ?? 'auto'}`,
|
|
94
100
|
`Cost: $${accumulatedCost.toFixed(4)}` +
|
|
95
|
-
(
|
|
96
|
-
? ` / $${currentConfig.maxSessionBudget.toFixed(2)}`
|
|
97
|
-
: ''),
|
|
101
|
+
(maxSessionBudget ? ` / $${maxSessionBudget.toFixed(2)}` : ''),
|
|
98
102
|
];
|
|
99
103
|
if (lastDecision && lastDecision.profile === statusProfile) {
|
|
100
104
|
const flags = getDecisionFlags(lastDecision);
|
|
@@ -103,6 +107,14 @@ export const updateStatus = (
|
|
|
103
107
|
widgetLines.push(
|
|
104
108
|
`Route: ${lastDecision.tier}${flagsStr} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${lastDecision.thinking})`,
|
|
105
109
|
`Phase: ${lastDecision.phase}`,
|
|
110
|
+
`Source: ${formatDecisionSource(lastDecision) || 'unknown'}`,
|
|
111
|
+
...(Number.isFinite(lastDecision.routingLatencyMs)
|
|
112
|
+
? [`Routing: ${Math.round(lastDecision.routingLatencyMs ?? 0)}ms`]
|
|
113
|
+
: []),
|
|
114
|
+
...(lastDecision.errorClass === 'deadline' ||
|
|
115
|
+
lastDecision.errorClass === 'advisor-unavailable'
|
|
116
|
+
? [`Routing error: ${lastDecision.errorClass}`]
|
|
117
|
+
: []),
|
|
106
118
|
);
|
|
107
119
|
} else if (!routerEnabled && lastNonRouterModel) {
|
|
108
120
|
widgetLines.push(`Fallback: ${lastNonRouterModel}`);
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"debug": false,
|
|
3
|
+
"jev": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"apiKey": "<rendered by chezmoi/1Password in user config only>",
|
|
6
|
+
"endpoint": "https://api.typesafe.ai/v1/systemone",
|
|
7
|
+
"model": "jev-1.13.0",
|
|
8
|
+
"timeoutMs": 750,
|
|
9
|
+
"confidenceThreshold": 0.65,
|
|
10
|
+
"maxStateChars": 12000,
|
|
11
|
+
"mode": "advisory"
|
|
12
|
+
},
|
|
3
13
|
"classifierModel": "flash",
|
|
4
|
-
"phaseBias": 0.5,
|
|
5
14
|
"maxSessionBudget": 1.0,
|
|
6
15
|
"models": {
|
|
7
16
|
"gpt-pro": {
|
|
@@ -25,30 +34,27 @@
|
|
|
25
34
|
"contextWindow": 200000
|
|
26
35
|
}
|
|
27
36
|
},
|
|
28
|
-
"rules": [
|
|
29
|
-
{
|
|
30
|
-
"matches": ["deploy", "production", "release"],
|
|
31
|
-
"tier": "high",
|
|
32
|
-
"reason": "Safety check for production tasks"
|
|
33
|
-
},
|
|
34
|
-
{ "matches": "changelog", "tier": "low" }
|
|
35
|
-
],
|
|
36
37
|
"profiles": {
|
|
37
38
|
"auto": {
|
|
39
|
+
"baselineTier": "medium",
|
|
40
|
+
"jev": { "enabled": false },
|
|
38
41
|
"high": {
|
|
39
42
|
"model": "gpt-pro",
|
|
40
43
|
"thinking": "high",
|
|
41
44
|
"fallbacks": ["sonnet"]
|
|
42
45
|
},
|
|
43
46
|
"medium": { "model": "flash", "thinking": "medium" },
|
|
44
|
-
"low": { "model": "nano", "thinking": "
|
|
47
|
+
"low": { "model": "nano", "thinking": "off" },
|
|
48
|
+
"micro": { "model": "nano", "thinking": "off" }
|
|
45
49
|
},
|
|
46
50
|
"cheap": {
|
|
51
|
+
"jev": { "enabled": false },
|
|
47
52
|
"high": { "model": "flash", "thinking": "low" },
|
|
48
53
|
"medium": { "model": "nano", "thinking": "off" },
|
|
49
54
|
"low": { "model": "google/gemini-flash-lite-latest", "thinking": "off" }
|
|
50
55
|
},
|
|
51
56
|
"deep": {
|
|
57
|
+
"jev": { "enabled": false },
|
|
52
58
|
"high": {
|
|
53
59
|
"model": "openai/o1-preview",
|
|
54
60
|
"thinking": "xhigh",
|
|
@@ -58,6 +64,7 @@
|
|
|
58
64
|
"low": { "model": "flash", "thinking": "low" }
|
|
59
65
|
},
|
|
60
66
|
"anthropic": {
|
|
67
|
+
"jev": { "enabled": false },
|
|
61
68
|
"high": {
|
|
62
69
|
"model": "sonnet",
|
|
63
70
|
"thinking": "high"
|