@alexeiled/pi-model-router 0.6.5 → 0.7.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 +27 -0
- package/README.md +56 -251
- package/extensions/classifier.ts +4 -5
- package/extensions/commands.ts +278 -576
- package/extensions/config.ts +52 -8
- package/extensions/constants.ts +7 -0
- package/extensions/context.ts +6 -2
- package/extensions/jev.ts +325 -66
- package/extensions/provider.ts +30 -4
- package/extensions/state.ts +29 -2
- package/extensions/types.ts +50 -1
- package/extensions/ui.ts +49 -7
- package/model-router.example.json +2 -0
- package/package.json +1 -1
package/extensions/provider.ts
CHANGED
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
resolveContextWindow,
|
|
26
26
|
resolveMaxTokens,
|
|
27
27
|
} from './config';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_CLASSIFIER_TIMEOUT_MS,
|
|
30
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
31
|
+
DEFAULT_MAX_TOKENS,
|
|
32
|
+
MAX_TURN_CACHE_ENTRIES,
|
|
33
|
+
} from './constants';
|
|
29
34
|
import { extractTextFromContent, hasImageAttachment } from './context';
|
|
30
35
|
import { createJevCandidate, runJevDetailed } from './jev';
|
|
31
36
|
import {
|
|
@@ -69,8 +74,10 @@ const createJevFlightKey = (
|
|
|
69
74
|
model: config.model,
|
|
70
75
|
timeoutMs: config.timeoutMs,
|
|
71
76
|
confidenceThreshold: config.confidenceThreshold,
|
|
77
|
+
probabilityThreshold: config.probabilityThreshold,
|
|
72
78
|
maxStateTokens: config.maxStateTokens,
|
|
73
79
|
context: config.context,
|
|
80
|
+
retry: config.retry,
|
|
74
81
|
});
|
|
75
82
|
|
|
76
83
|
const waitForAbortable = async <T>(
|
|
@@ -369,7 +376,7 @@ export const registerRouterProvider = (
|
|
|
369
376
|
const rememberContinuation = (record: ContinuationRecord) => {
|
|
370
377
|
continuations.delete(record.turn);
|
|
371
378
|
continuations.set(record.turn, record);
|
|
372
|
-
while (continuations.size >
|
|
379
|
+
while (continuations.size > MAX_TURN_CACHE_ENTRIES) {
|
|
373
380
|
const oldest = continuations.keys().next().value;
|
|
374
381
|
if (oldest === undefined) break;
|
|
375
382
|
continuations.delete(oldest);
|
|
@@ -383,7 +390,7 @@ export const registerRouterProvider = (
|
|
|
383
390
|
) => {
|
|
384
391
|
advisedTurns.delete(turn);
|
|
385
392
|
advisedTurns.set(turn, { policy, config, decision });
|
|
386
|
-
while (advisedTurns.size >
|
|
393
|
+
while (advisedTurns.size > MAX_TURN_CACHE_ENTRIES) {
|
|
387
394
|
const oldest = advisedTurns.keys().next().value;
|
|
388
395
|
if (oldest === undefined) break;
|
|
389
396
|
advisedTurns.delete(oldest);
|
|
@@ -591,6 +598,18 @@ export const registerRouterProvider = (
|
|
|
591
598
|
);
|
|
592
599
|
decision.isBudgetForced = baseline.isBudgetForced;
|
|
593
600
|
decision.advisor = advisorConfigured ? 'bypassed' : 'none';
|
|
601
|
+
if (advisorConfigured)
|
|
602
|
+
decision.bypassReason = pinnedTier
|
|
603
|
+
? 'pinned'
|
|
604
|
+
: isBudgetExceeded
|
|
605
|
+
? 'budget'
|
|
606
|
+
: toolContinuation
|
|
607
|
+
? 'tool-continuation'
|
|
608
|
+
: !user || !turn
|
|
609
|
+
? 'no-user-turn'
|
|
610
|
+
: advisedTurns.has(turn)
|
|
611
|
+
? 'turn-advised'
|
|
612
|
+
: undefined;
|
|
594
613
|
}
|
|
595
614
|
|
|
596
615
|
// Tool results never invoke advisors, even when their prior route cannot be reused.
|
|
@@ -606,13 +625,18 @@ export const registerRouterProvider = (
|
|
|
606
625
|
) {
|
|
607
626
|
const started = performance.now();
|
|
608
627
|
const routingDeadline =
|
|
609
|
-
started +
|
|
628
|
+
started +
|
|
629
|
+
(useJev && jev
|
|
630
|
+
? jev.timeoutMs
|
|
631
|
+
: (state.currentConfig.classifierModel?.timeoutMs ??
|
|
632
|
+
DEFAULT_CLASSIFIER_TIMEOUT_MS));
|
|
610
633
|
const candidates = primaryRoutePairs(profile, pairs).map(
|
|
611
634
|
createJevCandidate,
|
|
612
635
|
);
|
|
613
636
|
// A single primary bypasses advice, not a baseline's eligible fallback.
|
|
614
637
|
if (candidates.length <= 1) {
|
|
615
638
|
decision.advisor = 'bypassed';
|
|
639
|
+
decision.bypassReason = 'single-candidate';
|
|
616
640
|
rememberAdvisedDecision(
|
|
617
641
|
turn,
|
|
618
642
|
decision,
|
|
@@ -629,6 +653,8 @@ export const registerRouterProvider = (
|
|
|
629
653
|
context,
|
|
630
654
|
candidates,
|
|
631
655
|
profile: profile.jev,
|
|
656
|
+
baselineTier: selectBaselineRoute(model.id, profile, pairs)
|
|
657
|
+
.pair.tier,
|
|
632
658
|
routingDeadline,
|
|
633
659
|
},
|
|
634
660
|
);
|
package/extensions/state.ts
CHANGED
|
@@ -16,7 +16,14 @@ import type {
|
|
|
16
16
|
RouterPinByProfile,
|
|
17
17
|
RoutingDecision,
|
|
18
18
|
} from './types';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
isAdvisorOutcome,
|
|
21
|
+
isBypassReason,
|
|
22
|
+
isRoutingReasonCode,
|
|
23
|
+
JEV_OUTCOMES,
|
|
24
|
+
JEV_RESPONSE_ISSUES,
|
|
25
|
+
JEV_SELECTION_BASES,
|
|
26
|
+
} from './types';
|
|
20
27
|
|
|
21
28
|
const LAST_PROFILE_STATE_FILE = 'model-router-state.json';
|
|
22
29
|
|
|
@@ -191,11 +198,27 @@ const snapshotJev = (value: unknown): JevDiagnostics | undefined => {
|
|
|
191
198
|
result.resolvedModel = value.resolvedModel;
|
|
192
199
|
if (isRouterTier(value.choice) || value.choice === 'uncertain')
|
|
193
200
|
result.choice = value.choice;
|
|
194
|
-
|
|
201
|
+
if (isRouterTier(value.selectedTier))
|
|
202
|
+
result.selectedTier = value.selectedTier;
|
|
203
|
+
const basis = JEV_SELECTION_BASES.find(
|
|
204
|
+
(entry) => entry === value.selectionBasis,
|
|
205
|
+
);
|
|
206
|
+
if (basis) result.selectionBasis = basis;
|
|
207
|
+
for (const key of [
|
|
208
|
+
'confidence',
|
|
209
|
+
'probability',
|
|
210
|
+
'routeProbability',
|
|
211
|
+
'threshold',
|
|
212
|
+
'probabilityThreshold',
|
|
213
|
+
] as const) {
|
|
195
214
|
const number = value[key];
|
|
196
215
|
if (isFiniteNumber(number) && number >= 0 && number <= 1)
|
|
197
216
|
result[key] = number;
|
|
198
217
|
}
|
|
218
|
+
const issue = JEV_RESPONSE_ISSUES.find(
|
|
219
|
+
(entry) => entry === value.responseIssue,
|
|
220
|
+
);
|
|
221
|
+
if (issue) result.responseIssue = issue;
|
|
199
222
|
for (const key of [
|
|
200
223
|
'startedAt',
|
|
201
224
|
'timeoutMs',
|
|
@@ -203,6 +226,7 @@ const snapshotJev = (value: unknown): JevDiagnostics | undefined => {
|
|
|
203
226
|
'estimatedInputTokens',
|
|
204
227
|
'actualInputTokens',
|
|
205
228
|
'httpStatus',
|
|
229
|
+
'attempts',
|
|
206
230
|
] as const) {
|
|
207
231
|
const number = value[key];
|
|
208
232
|
if (isFiniteNumber(number) && Number.isSafeInteger(number) && number >= 0)
|
|
@@ -234,6 +258,9 @@ export const snapshotDecision = (
|
|
|
234
258
|
? decision.errorClass
|
|
235
259
|
: undefined,
|
|
236
260
|
advisor: isAdvisorOutcome(decision.advisor) ? decision.advisor : undefined,
|
|
261
|
+
bypassReason: isBypassReason(decision.bypassReason)
|
|
262
|
+
? decision.bypassReason
|
|
263
|
+
: undefined,
|
|
237
264
|
jev: snapshotJev(decision.jev),
|
|
238
265
|
reuse:
|
|
239
266
|
decision.reuse === 'same-turn' ||
|
package/extensions/types.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface ModelDefinition {
|
|
|
22
22
|
export interface ClassifierConfig {
|
|
23
23
|
model: string;
|
|
24
24
|
thinking?: ThinkingLevel | undefined;
|
|
25
|
+
/** Total classifier budget in ms; `DEFAULT_CLASSIFIER_TIMEOUT_MS` when omitted. */
|
|
26
|
+
timeoutMs?: number | undefined;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface RoutedTierConfig {
|
|
@@ -47,6 +49,12 @@ export interface JevContextConfig {
|
|
|
47
49
|
toolResults: 'none' | 'last' | 'last-error';
|
|
48
50
|
maxToolTokens: number;
|
|
49
51
|
}
|
|
52
|
+
export interface JevRetryConfig {
|
|
53
|
+
/** HTTP attempts in total; 1 disables retries. */
|
|
54
|
+
maxAttempts: number;
|
|
55
|
+
/** First backoff delay; doubled per attempt, raised by `Retry-After`. */
|
|
56
|
+
backoffMs: number;
|
|
57
|
+
}
|
|
50
58
|
export interface JevTextExcerpt {
|
|
51
59
|
text: string;
|
|
52
60
|
truncated: boolean;
|
|
@@ -72,8 +80,10 @@ export interface JevConfig {
|
|
|
72
80
|
model: string;
|
|
73
81
|
timeoutMs: number;
|
|
74
82
|
confidenceThreshold: number;
|
|
83
|
+
probabilityThreshold: number;
|
|
75
84
|
maxStateTokens: number;
|
|
76
85
|
context?: JevContextConfig | undefined;
|
|
86
|
+
retry?: JevRetryConfig | undefined;
|
|
77
87
|
mode: 'advisory';
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -133,15 +143,32 @@ export interface JevRequest {
|
|
|
133
143
|
context: Context;
|
|
134
144
|
candidates: readonly JevRouteCandidate[];
|
|
135
145
|
profile: JevProfileConfig | undefined;
|
|
146
|
+
/** Local fallback tier; abstention mass is assigned to it, never inferred remotely. */
|
|
147
|
+
baselineTier: RouterTier;
|
|
136
148
|
/** Absolute monotonic deadline supplied by the routing orchestrator. */
|
|
137
149
|
routingDeadline: number;
|
|
138
150
|
signal?: AbortSignal | undefined;
|
|
139
151
|
}
|
|
140
152
|
|
|
153
|
+
export const JEV_SELECTION_BASES = ['choice', 'probability'] as const;
|
|
154
|
+
export type JevSelectionBasis = (typeof JEV_SELECTION_BASES)[number];
|
|
155
|
+
|
|
156
|
+
/** Local validation codes; remote error text is never retained. */
|
|
157
|
+
export const JEV_RESPONSE_ISSUES = [
|
|
158
|
+
'unreadable-body',
|
|
159
|
+
'missing-answer',
|
|
160
|
+
'unexpected-answer-type',
|
|
161
|
+
'unknown-choice',
|
|
162
|
+
'invalid-confidence',
|
|
163
|
+
'distribution-keys',
|
|
164
|
+
'distribution-sum',
|
|
165
|
+
'distribution-argmax',
|
|
166
|
+
] as const;
|
|
167
|
+
export type JevResponseIssue = (typeof JEV_RESPONSE_ISSUES)[number];
|
|
168
|
+
|
|
141
169
|
export const JEV_OUTCOMES = [
|
|
142
170
|
'selected',
|
|
143
171
|
'uncertain',
|
|
144
|
-
'low-confidence',
|
|
145
172
|
'invalid-response',
|
|
146
173
|
'http-error',
|
|
147
174
|
'network-error',
|
|
@@ -161,14 +188,23 @@ export interface JevDiagnostics {
|
|
|
161
188
|
model?: string | undefined;
|
|
162
189
|
resolvedModel?: string | undefined;
|
|
163
190
|
choice?: RouterTier | 'uncertain' | undefined;
|
|
191
|
+
/** Acted-on tier, which a conservative probability selection can raise above `choice`. */
|
|
192
|
+
selectedTier?: RouterTier | undefined;
|
|
193
|
+
selectionBasis?: JevSelectionBasis | undefined;
|
|
164
194
|
confidence?: number | undefined;
|
|
165
195
|
probability?: number | undefined;
|
|
196
|
+
/** Cumulative probability of the selected tier and every lower tier. */
|
|
197
|
+
routeProbability?: number | undefined;
|
|
166
198
|
threshold?: number | undefined;
|
|
199
|
+
probabilityThreshold?: number | undefined;
|
|
167
200
|
timeoutMs?: number | undefined;
|
|
168
201
|
candidateCount?: number | undefined;
|
|
169
202
|
estimatedInputTokens?: number | undefined;
|
|
170
203
|
actualInputTokens?: number | undefined;
|
|
171
204
|
httpStatus?: number | undefined;
|
|
205
|
+
/** HTTP attempts made, including the retry of a documented transient status. */
|
|
206
|
+
attempts?: number | undefined;
|
|
207
|
+
responseIssue?: JevResponseIssue | undefined;
|
|
172
208
|
}
|
|
173
209
|
export interface JevResult {
|
|
174
210
|
advice?: JevAdvice | undefined;
|
|
@@ -224,6 +260,18 @@ export const ADVISOR_OUTCOMES = [
|
|
|
224
260
|
export type AdvisorOutcome = (typeof ADVISOR_OUTCOMES)[number];
|
|
225
261
|
export const isAdvisorOutcome = (value: unknown): value is AdvisorOutcome =>
|
|
226
262
|
ADVISOR_OUTCOMES.some((outcome) => outcome === value);
|
|
263
|
+
/** Why a configured advisor was not asked on this decision. */
|
|
264
|
+
export const BYPASS_REASONS = [
|
|
265
|
+
'pinned',
|
|
266
|
+
'budget',
|
|
267
|
+
'single-candidate',
|
|
268
|
+
'tool-continuation',
|
|
269
|
+
'no-user-turn',
|
|
270
|
+
'turn-advised',
|
|
271
|
+
] as const;
|
|
272
|
+
export type BypassReason = (typeof BYPASS_REASONS)[number];
|
|
273
|
+
export const isBypassReason = (value: unknown): value is BypassReason =>
|
|
274
|
+
BYPASS_REASONS.some((reason) => reason === value);
|
|
227
275
|
|
|
228
276
|
export interface RoutingDecision {
|
|
229
277
|
profile: string;
|
|
@@ -236,6 +284,7 @@ export interface RoutingDecision {
|
|
|
236
284
|
routingLatencyMs?: number | undefined;
|
|
237
285
|
errorClass?: RoutingErrorClass | undefined;
|
|
238
286
|
advisor?: AdvisorOutcome | undefined;
|
|
287
|
+
bypassReason?: BypassReason | undefined;
|
|
239
288
|
jev?: JevDiagnostics | undefined;
|
|
240
289
|
reuse?: 'same-turn' | 'shared' | 'continuation' | undefined;
|
|
241
290
|
thinking: ThinkingLevel;
|
package/extensions/ui.ts
CHANGED
|
@@ -25,6 +25,25 @@ export const formatDecisionSource = (decision: RoutingDecision): string =>
|
|
|
25
25
|
? decision.reasonCode
|
|
26
26
|
: '';
|
|
27
27
|
|
|
28
|
+
const formatBypassReason = (decision: RoutingDecision): string => {
|
|
29
|
+
switch (decision.bypassReason) {
|
|
30
|
+
case 'pinned':
|
|
31
|
+
return `pinned ${decision.tier}`;
|
|
32
|
+
case 'budget':
|
|
33
|
+
return 'over budget';
|
|
34
|
+
case 'single-candidate':
|
|
35
|
+
return `only ${decision.tier} eligible`;
|
|
36
|
+
case 'tool-continuation':
|
|
37
|
+
return 'tool turn';
|
|
38
|
+
case 'no-user-turn':
|
|
39
|
+
return 'no user turn';
|
|
40
|
+
case 'turn-advised':
|
|
41
|
+
return 'turn already advised';
|
|
42
|
+
default:
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
28
47
|
export const formatAdvisorLabel = (
|
|
29
48
|
decision: RoutingDecision,
|
|
30
49
|
): string | undefined => {
|
|
@@ -32,8 +51,10 @@ export const formatAdvisorLabel = (
|
|
|
32
51
|
switch (decision.advisor) {
|
|
33
52
|
case 'none':
|
|
34
53
|
return 'local baseline';
|
|
35
|
-
case 'bypassed':
|
|
36
|
-
|
|
54
|
+
case 'bypassed': {
|
|
55
|
+
const reason = formatBypassReason(decision);
|
|
56
|
+
return reason ? `advice skipped: ${reason}` : 'advice bypassed';
|
|
57
|
+
}
|
|
37
58
|
case 'jev':
|
|
38
59
|
return '🧭 Jev ✓';
|
|
39
60
|
case 'jev-fallback':
|
|
@@ -76,6 +97,11 @@ export const formatAdvisorDetail = (
|
|
|
76
97
|
);
|
|
77
98
|
if (metrics.choice && metrics.choice !== 'uncertain')
|
|
78
99
|
parts.push(`choice=${metrics.choice}`);
|
|
100
|
+
if (metrics.selectedTier && metrics.selectedTier !== metrics.choice)
|
|
101
|
+
parts.push(`selected=${metrics.selectedTier}`);
|
|
102
|
+
if (metrics.selectionBasis) parts.push(`basis=${metrics.selectionBasis}`);
|
|
103
|
+
if (metrics.routeProbability !== undefined)
|
|
104
|
+
parts.push(`route-p=${(metrics.routeProbability * 100).toFixed(1)}%`);
|
|
79
105
|
if (metrics.probability !== undefined)
|
|
80
106
|
parts.push(
|
|
81
107
|
`${metrics.outcome === 'uncertain' ? 'abstention-p' : 'p'}=${(metrics.probability * 100).toFixed(1)}%`,
|
|
@@ -86,6 +112,13 @@ export const formatAdvisorDetail = (
|
|
|
86
112
|
);
|
|
87
113
|
if (metrics.threshold !== undefined && metrics.outcome !== 'uncertain')
|
|
88
114
|
parts.push(`threshold=${(metrics.threshold * 100).toFixed(1)}%`);
|
|
115
|
+
if (
|
|
116
|
+
metrics.probabilityThreshold !== undefined &&
|
|
117
|
+
metrics.selectionBasis === 'probability'
|
|
118
|
+
)
|
|
119
|
+
parts.push(
|
|
120
|
+
`route-threshold=${(metrics.probabilityThreshold * 100).toFixed(1)}%`,
|
|
121
|
+
);
|
|
89
122
|
if (metrics.timeoutMs !== undefined)
|
|
90
123
|
parts.push(`budget=${metrics.timeoutMs}ms`);
|
|
91
124
|
if (metrics.candidateCount !== undefined)
|
|
@@ -102,6 +135,15 @@ export const formatAdvisorDetail = (
|
|
|
102
135
|
parts.push(`Jev usage=${metrics.actualInputTokens} input tokens`);
|
|
103
136
|
if (metrics.httpStatus !== undefined)
|
|
104
137
|
parts.push(`HTTP ${metrics.httpStatus}`);
|
|
138
|
+
if (metrics.attempts !== undefined && metrics.attempts > 1)
|
|
139
|
+
parts.push(`attempts=${metrics.attempts}`);
|
|
140
|
+
if (metrics.responseIssue) parts.push(`response=${metrics.responseIssue}`);
|
|
141
|
+
if (metrics.httpStatus === 401)
|
|
142
|
+
parts.push('Check the user-config Jev API key.');
|
|
143
|
+
if (metrics.httpStatus === 422)
|
|
144
|
+
parts.push('Jev rejected the request shape.');
|
|
145
|
+
if (metrics.httpStatus === 429 || metrics.httpStatus === 529)
|
|
146
|
+
parts.push('Transient Jev limit; the router retried once within budget.');
|
|
105
147
|
} else if (decision.errorClass) {
|
|
106
148
|
parts.push(decision.errorClass);
|
|
107
149
|
}
|
|
@@ -127,10 +169,10 @@ export const formatAdvisorFooter = (
|
|
|
127
169
|
let summary: string;
|
|
128
170
|
switch (metrics.outcome) {
|
|
129
171
|
case 'selected':
|
|
130
|
-
summary =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
172
|
+
summary =
|
|
173
|
+
metrics.selectionBasis === 'probability'
|
|
174
|
+
? `${metrics.choice ?? 'choice'}${confidence}${metrics.threshold !== undefined ? ` <${Math.round(metrics.threshold * 100)}%` : ''} → ${metrics.selectedTier ?? decision.tier}`
|
|
175
|
+
: `→ ${metrics.choice ?? decision.tier}${confidence}`;
|
|
134
176
|
break;
|
|
135
177
|
case 'uncertain':
|
|
136
178
|
summary = ': no tier chosen → baseline';
|
|
@@ -145,7 +187,7 @@ export const formatAdvisorFooter = (
|
|
|
145
187
|
summary = ': network error → baseline';
|
|
146
188
|
break;
|
|
147
189
|
case 'invalid-response':
|
|
148
|
-
summary =
|
|
190
|
+
summary = `: invalid response${metrics.responseIssue ? ` (${metrics.responseIssue})` : ''} → baseline`;
|
|
149
191
|
break;
|
|
150
192
|
case 'cancelled':
|
|
151
193
|
summary = ': cancelled';
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"model": "jev-1.13.0",
|
|
9
9
|
"timeoutMs": 1500,
|
|
10
10
|
"confidenceThreshold": 0.65,
|
|
11
|
+
"probabilityThreshold": 0.8,
|
|
11
12
|
"maxStateTokens": 3000,
|
|
12
13
|
"context": {
|
|
13
14
|
"previousTurns": 2,
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
"toolResults": "last-error",
|
|
16
17
|
"maxToolTokens": 250
|
|
17
18
|
},
|
|
19
|
+
"retry": { "maxAttempts": 2, "backoffMs": 400 },
|
|
18
20
|
"mode": "advisory"
|
|
19
21
|
},
|
|
20
22
|
"classifierModel": "flash",
|