@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.
@@ -25,7 +25,12 @@ import {
25
25
  resolveContextWindow,
26
26
  resolveMaxTokens,
27
27
  } from './config';
28
- import { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS } from './constants';
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 > 16) {
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 > 16) {
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 + (useJev && jev ? jev.timeoutMs : 10_000);
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
  );
@@ -16,7 +16,14 @@ import type {
16
16
  RouterPinByProfile,
17
17
  RoutingDecision,
18
18
  } from './types';
19
- import { isAdvisorOutcome, isRoutingReasonCode, JEV_OUTCOMES } from './types';
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
- for (const key of ['confidence', 'probability', 'threshold'] as const) {
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' ||
@@ -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
- return 'advice bypassed';
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 = `→ ${metrics.choice ?? decision.tier}${confidence}`;
131
- break;
132
- case 'low-confidence':
133
- summary = `${metrics.choice ?? 'choice'}${confidence}${metrics.threshold !== undefined ? ` <${Math.round(metrics.threshold * 100)}%` : ''} → baseline`;
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 = ': invalid response → baseline';
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexeiled/pi-model-router",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "extensions",