@alexeiled/pi-model-router 0.5.2 → 0.6.1
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 +18 -0
- package/README.md +150 -24
- package/extensions/classifier.ts +96 -70
- package/extensions/commands.ts +42 -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 +372 -143
- package/extensions/routing.ts +236 -296
- package/extensions/state.ts +55 -10
- package/extensions/types.ts +92 -12
- package/extensions/ui.ts +59 -8
- 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,68 @@ 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
|
+
export const ADVISOR_OUTCOMES = [
|
|
135
|
+
'none',
|
|
136
|
+
'bypassed',
|
|
137
|
+
'jev',
|
|
138
|
+
'jev-fallback',
|
|
139
|
+
'classifier',
|
|
140
|
+
'classifier-fallback',
|
|
141
|
+
] as const;
|
|
142
|
+
export type AdvisorOutcome = (typeof ADVISOR_OUTCOMES)[number];
|
|
143
|
+
export const isAdvisorOutcome = (value: unknown): value is AdvisorOutcome =>
|
|
144
|
+
ADVISOR_OUTCOMES.some((outcome) => outcome === value);
|
|
145
|
+
|
|
69
146
|
export interface RoutingDecision {
|
|
70
147
|
profile: string;
|
|
71
148
|
tier: RouterTier;
|
|
@@ -73,13 +150,15 @@ export interface RoutingDecision {
|
|
|
73
150
|
targetProvider: string;
|
|
74
151
|
targetModelId: string;
|
|
75
152
|
targetLabel: string;
|
|
76
|
-
|
|
153
|
+
reasonCode: RoutingReasonCode;
|
|
154
|
+
routingLatencyMs?: number | undefined;
|
|
155
|
+
errorClass?: RoutingErrorClass | undefined;
|
|
156
|
+
advisor?: AdvisorOutcome | undefined;
|
|
77
157
|
thinking: ThinkingLevel;
|
|
78
158
|
timestamp: number;
|
|
79
159
|
isClassifier?: boolean | undefined;
|
|
80
160
|
isFallback?: boolean | undefined;
|
|
81
161
|
isBudgetForced?: boolean | undefined;
|
|
82
|
-
isRuleMatched?: boolean | undefined;
|
|
83
162
|
}
|
|
84
163
|
|
|
85
164
|
export interface RouterLastProfileState {
|
|
@@ -117,6 +196,7 @@ export interface RouterPersistedState {
|
|
|
117
196
|
}
|
|
118
197
|
|
|
119
198
|
export interface RawRouterConfig {
|
|
199
|
+
jev?: unknown;
|
|
120
200
|
debug?: unknown;
|
|
121
201
|
classifierModel?: unknown;
|
|
122
202
|
phaseBias?: unknown;
|
package/extensions/ui.ts
CHANGED
|
@@ -5,17 +5,67 @@ import type {
|
|
|
5
5
|
RouterThinkingByProfile,
|
|
6
6
|
RoutingDecision,
|
|
7
7
|
} from './types';
|
|
8
|
+
import { isAdvisorOutcome, 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
|
+
|
|
22
|
+
export const formatAdvisorLabel = (
|
|
23
|
+
decision: RoutingDecision,
|
|
24
|
+
): string | undefined => {
|
|
25
|
+
if (!isAdvisorOutcome(decision.advisor)) return undefined;
|
|
26
|
+
switch (decision.advisor) {
|
|
27
|
+
case 'none':
|
|
28
|
+
case 'bypassed':
|
|
29
|
+
return undefined;
|
|
30
|
+
case 'jev':
|
|
31
|
+
return '🧭 Jev ✓';
|
|
32
|
+
case 'jev-fallback':
|
|
33
|
+
return '🧭 Jev ↪ base';
|
|
34
|
+
case 'classifier':
|
|
35
|
+
return '🧠 Classifier ✓';
|
|
36
|
+
case 'classifier-fallback':
|
|
37
|
+
return '🧠 Classifier ↪ base';
|
|
38
|
+
default:
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const formatAdvisorDetail = (
|
|
44
|
+
decision: RoutingDecision,
|
|
45
|
+
): string | undefined => {
|
|
46
|
+
const label = formatAdvisorLabel(decision);
|
|
47
|
+
if (!label) return undefined;
|
|
48
|
+
const latency = Number.isFinite(decision.routingLatencyMs)
|
|
49
|
+
? ` · ${Math.round(decision.routingLatencyMs ?? 0)}ms`
|
|
50
|
+
: '';
|
|
51
|
+
return `${label}${latency}`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const formatAdvisorFooter = (decision: RoutingDecision): string => {
|
|
55
|
+
if (
|
|
56
|
+
!decision.advisor ||
|
|
57
|
+
decision.advisor === 'none' ||
|
|
58
|
+
decision.advisor === 'bypassed'
|
|
59
|
+
)
|
|
60
|
+
return '';
|
|
61
|
+
const label = formatAdvisorLabel(decision);
|
|
62
|
+
return label ? ` · ${label}` : '';
|
|
63
|
+
};
|
|
64
|
+
|
|
17
65
|
export const formatDecision = (decision: RoutingDecision): string => {
|
|
18
|
-
|
|
66
|
+
const source = formatDecisionSource(decision);
|
|
67
|
+
const advisor = formatAdvisorLabel(decision);
|
|
68
|
+
return `${decision.profile}: ${decision.tier} -> ${decision.targetProvider}/${decision.targetModelId} [${decision.thinking}]${source ? ` (${source})` : ''}${advisor ? ` [${advisor}]` : ''}`;
|
|
19
69
|
};
|
|
20
70
|
|
|
21
71
|
export const formatPinSummary = (
|
|
@@ -57,7 +107,7 @@ export const updateStatus = (
|
|
|
57
107
|
lastNonRouterModel,
|
|
58
108
|
accumulatedCost,
|
|
59
109
|
widgetEnabled,
|
|
60
|
-
|
|
110
|
+
maxSessionBudget,
|
|
61
111
|
} = state;
|
|
62
112
|
const activeRouterProfile = routerEnabled ? selectedProfile : undefined;
|
|
63
113
|
const statusProfile = selectedProfile ?? 'none';
|
|
@@ -69,11 +119,11 @@ export const updateStatus = (
|
|
|
69
119
|
if (activeRouterProfile) {
|
|
70
120
|
const matchesProfile =
|
|
71
121
|
lastDecision && lastDecision.profile === activeRouterProfile;
|
|
72
|
-
const matchesPin = activePin
|
|
122
|
+
const matchesPin = !activePin || lastDecision?.tier === activePin;
|
|
73
123
|
|
|
74
124
|
let statusText: string;
|
|
75
125
|
if (lastDecision && matchesProfile && matchesPin) {
|
|
76
|
-
statusText = `router:${activeRouterProfile}${pinLabel} -> ${lastDecision.tier} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${lastDecision.thinking})`;
|
|
126
|
+
statusText = `router:${activeRouterProfile}${pinLabel} -> ${lastDecision.tier} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${lastDecision.thinking})${formatAdvisorFooter(lastDecision)}`;
|
|
77
127
|
} else {
|
|
78
128
|
statusText = `router:${activeRouterProfile}${pinLabel} -> waiting`;
|
|
79
129
|
}
|
|
@@ -92,17 +142,18 @@ export const updateStatus = (
|
|
|
92
142
|
`Profile: ${statusProfile}${activeRouterProfile ? ' (active)' : ''}`,
|
|
93
143
|
`Pin: ${activePin ?? 'auto'}`,
|
|
94
144
|
`Cost: $${accumulatedCost.toFixed(4)}` +
|
|
95
|
-
(
|
|
96
|
-
? ` / $${currentConfig.maxSessionBudget.toFixed(2)}`
|
|
97
|
-
: ''),
|
|
145
|
+
(maxSessionBudget ? ` / $${maxSessionBudget.toFixed(2)}` : ''),
|
|
98
146
|
];
|
|
99
147
|
if (lastDecision && lastDecision.profile === statusProfile) {
|
|
100
148
|
const flags = getDecisionFlags(lastDecision);
|
|
101
149
|
const flagsStr = flags.length > 0 ? ` [${flags.join(',')}]` : '';
|
|
150
|
+
const advisorDetail = formatAdvisorDetail(lastDecision);
|
|
102
151
|
|
|
103
152
|
widgetLines.push(
|
|
104
153
|
`Route: ${lastDecision.tier}${flagsStr} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${lastDecision.thinking})`,
|
|
105
154
|
`Phase: ${lastDecision.phase}`,
|
|
155
|
+
`Source: ${formatDecisionSource(lastDecision) || 'unknown'}`,
|
|
156
|
+
...(advisorDetail ? [advisorDetail] : []),
|
|
106
157
|
);
|
|
107
158
|
} else if (!routerEnabled && lastNonRouterModel) {
|
|
108
159
|
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"
|