@alexeiled/pi-model-router 0.6.4 β†’ 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.
@@ -8,6 +8,7 @@ import {
8
8
  parseCanonicalModelRef,
9
9
  } from './config';
10
10
  import type {
11
+ JevContextMetrics,
11
12
  JevDiagnostics,
12
13
  PersistedStateInput,
13
14
  RouterLastProfileState,
@@ -15,7 +16,14 @@ import type {
15
16
  RouterPinByProfile,
16
17
  RoutingDecision,
17
18
  } from './types';
18
- 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';
19
27
 
20
28
  const LAST_PROFILE_STATE_FILE = 'model-router-state.json';
21
29
 
@@ -144,12 +152,35 @@ export const isRouterPersistedState = (
144
152
  );
145
153
  };
146
154
 
155
+ const snapshotContextMetrics = (
156
+ value: unknown,
157
+ ): JevContextMetrics | undefined => {
158
+ if (!isObjectRecord(value)) return undefined;
159
+ const result: JevContextMetrics = {
160
+ currentRequestTokens: 0,
161
+ historyTokens: 0,
162
+ toolTokens: 0,
163
+ historyTurns: 0,
164
+ toolResults: 0,
165
+ truncatedBlocks: 0,
166
+ };
167
+ for (const key of Object.keys(result) as (keyof JevContextMetrics)[]) {
168
+ const number = value[key];
169
+ if (!isFiniteNumber(number) || !Number.isSafeInteger(number) || number < 0)
170
+ return undefined;
171
+ result[key] = number;
172
+ }
173
+ return result;
174
+ };
175
+
147
176
  const snapshotJev = (value: unknown): JevDiagnostics | undefined => {
148
177
  if (!isObjectRecord(value)) return undefined;
149
178
  const outcome = JEV_OUTCOMES.find((entry) => entry === value.outcome);
150
179
  if (!outcome || !isFiniteNumber(value.latencyMs) || value.latencyMs < 0)
151
180
  return undefined;
152
181
  const result: JevDiagnostics = { outcome, latencyMs: value.latencyMs };
182
+ const context = snapshotContextMetrics(value.context);
183
+ if (context) result.context = context;
153
184
  if (
154
185
  typeof value.requestId === 'string' &&
155
186
  /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(value.requestId)
@@ -167,17 +198,35 @@ const snapshotJev = (value: unknown): JevDiagnostics | undefined => {
167
198
  result.resolvedModel = value.resolvedModel;
168
199
  if (isRouterTier(value.choice) || value.choice === 'uncertain')
169
200
  result.choice = value.choice;
170
- 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) {
171
214
  const number = value[key];
172
215
  if (isFiniteNumber(number) && number >= 0 && number <= 1)
173
216
  result[key] = number;
174
217
  }
218
+ const issue = JEV_RESPONSE_ISSUES.find(
219
+ (entry) => entry === value.responseIssue,
220
+ );
221
+ if (issue) result.responseIssue = issue;
175
222
  for (const key of [
176
223
  'startedAt',
177
224
  'timeoutMs',
178
225
  'candidateCount',
179
- 'contextChars',
226
+ 'estimatedInputTokens',
227
+ 'actualInputTokens',
180
228
  'httpStatus',
229
+ 'attempts',
181
230
  ] as const) {
182
231
  const number = value[key];
183
232
  if (isFiniteNumber(number) && Number.isSafeInteger(number) && number >= 0)
@@ -209,6 +258,9 @@ export const snapshotDecision = (
209
258
  ? decision.errorClass
210
259
  : undefined,
211
260
  advisor: isAdvisorOutcome(decision.advisor) ? decision.advisor : undefined,
261
+ bypassReason: isBypassReason(decision.bypassReason)
262
+ ? decision.bypassReason
263
+ : undefined,
212
264
  jev: snapshotJev(decision.jev),
213
265
  reuse:
214
266
  decision.reuse === 'same-turn' ||
@@ -1,4 +1,5 @@
1
1
  import type { ThinkingLevel } from '@earendil-works/pi-agent-core';
2
+ import type { Context } from '@earendil-works/pi-ai';
2
3
 
3
4
  // Descending routing complexity; all tier iteration and ranking derives here.
4
5
  export const ROUTER_TIERS = ['high', 'medium', 'low', 'micro'] as const;
@@ -21,6 +22,8 @@ export interface ModelDefinition {
21
22
  export interface ClassifierConfig {
22
23
  model: string;
23
24
  thinking?: ThinkingLevel | undefined;
25
+ /** Total classifier budget in ms; `DEFAULT_CLASSIFIER_TIMEOUT_MS` when omitted. */
26
+ timeoutMs?: number | undefined;
24
27
  }
25
28
 
26
29
  export interface RoutedTierConfig {
@@ -40,6 +43,36 @@ export interface RoutedTierConfig {
40
43
  resolvedThinkingLevels?: ThinkingLevel[] | undefined;
41
44
  }
42
45
 
46
+ export interface JevContextConfig {
47
+ previousTurns: number;
48
+ maxHistoryTokens: number;
49
+ toolResults: 'none' | 'last' | 'last-error';
50
+ maxToolTokens: number;
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
+ }
58
+ export interface JevTextExcerpt {
59
+ text: string;
60
+ truncated: boolean;
61
+ }
62
+ export interface JevContextState {
63
+ currentRequest: JevTextExcerpt;
64
+ recentDialogue: (JevTextExcerpt & { role: 'user' | 'assistant' })[];
65
+ recentToolEvidence: (JevTextExcerpt & { isError: boolean })[];
66
+ }
67
+ export interface JevContextMetrics {
68
+ currentRequestTokens: number;
69
+ historyTokens: number;
70
+ toolTokens: number;
71
+ historyTurns: number;
72
+ toolResults: number;
73
+ truncatedBlocks: number;
74
+ }
75
+
43
76
  export interface JevConfig {
44
77
  enabled: boolean;
45
78
  apiKey: string;
@@ -47,7 +80,10 @@ export interface JevConfig {
47
80
  model: string;
48
81
  timeoutMs: number;
49
82
  confidenceThreshold: number;
50
- maxStateChars: number;
83
+ probabilityThreshold: number;
84
+ maxStateTokens: number;
85
+ context?: JevContextConfig | undefined;
86
+ retry?: JevRetryConfig | undefined;
51
87
  mode: 'advisory';
52
88
  }
53
89
 
@@ -104,27 +140,46 @@ export interface JevDependencies {
104
140
  }
105
141
 
106
142
  export interface JevRequest {
107
- taskSummary: string;
143
+ context: Context;
108
144
  candidates: readonly JevRouteCandidate[];
109
145
  profile: JevProfileConfig | undefined;
146
+ /** Local fallback tier; abstention mass is assigned to it, never inferred remotely. */
147
+ baselineTier: RouterTier;
110
148
  /** Absolute monotonic deadline supplied by the routing orchestrator. */
111
149
  routingDeadline: number;
112
150
  signal?: AbortSignal | undefined;
113
151
  }
114
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
+
115
169
  export const JEV_OUTCOMES = [
116
170
  'selected',
117
171
  'uncertain',
118
- 'low-confidence',
119
172
  'invalid-response',
120
173
  'http-error',
121
174
  'network-error',
122
175
  'deadline',
123
176
  'cancelled',
124
177
  'unavailable',
178
+ 'input-too-large',
125
179
  ] as const;
126
180
  export type JevOutcome = (typeof JEV_OUTCOMES)[number];
127
181
  export interface JevDiagnostics {
182
+ context?: JevContextMetrics | undefined;
128
183
  /** Locally generated per HTTP request, shared by reusers; never supplied by Jev. */
129
184
  requestId?: string | undefined;
130
185
  outcome: JevOutcome;
@@ -133,13 +188,23 @@ export interface JevDiagnostics {
133
188
  model?: string | undefined;
134
189
  resolvedModel?: string | undefined;
135
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;
136
194
  confidence?: number | undefined;
137
195
  probability?: number | undefined;
196
+ /** Cumulative probability of the selected tier and every lower tier. */
197
+ routeProbability?: number | undefined;
138
198
  threshold?: number | undefined;
199
+ probabilityThreshold?: number | undefined;
139
200
  timeoutMs?: number | undefined;
140
201
  candidateCount?: number | undefined;
141
- contextChars?: number | undefined;
202
+ estimatedInputTokens?: number | undefined;
203
+ actualInputTokens?: number | undefined;
142
204
  httpStatus?: number | undefined;
205
+ /** HTTP attempts made, including the retry of a documented transient status. */
206
+ attempts?: number | undefined;
207
+ responseIssue?: JevResponseIssue | undefined;
143
208
  }
144
209
  export interface JevResult {
145
210
  advice?: JevAdvice | undefined;
@@ -195,6 +260,18 @@ export const ADVISOR_OUTCOMES = [
195
260
  export type AdvisorOutcome = (typeof ADVISOR_OUTCOMES)[number];
196
261
  export const isAdvisorOutcome = (value: unknown): value is AdvisorOutcome =>
197
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);
198
275
 
199
276
  export interface RoutingDecision {
200
277
  profile: string;
@@ -207,6 +284,7 @@ export interface RoutingDecision {
207
284
  routingLatencyMs?: number | undefined;
208
285
  errorClass?: RoutingErrorClass | undefined;
209
286
  advisor?: AdvisorOutcome | undefined;
287
+ bypassReason?: BypassReason | undefined;
210
288
  jev?: JevDiagnostics | undefined;
211
289
  reuse?: 'same-turn' | 'shared' | 'continuation' | undefined;
212
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,14 +112,38 @@ 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)
92
125
  parts.push(`candidates=${metrics.candidateCount}`);
93
- if (metrics.contextChars !== undefined)
94
- parts.push(`context=${metrics.contextChars} chars`);
126
+ if (metrics.context) {
127
+ const context = metrics.context;
128
+ parts.push(
129
+ `stateβ‰ˆ${context.currentRequestTokens + context.historyTokens + context.toolTokens} tokens: ${context.currentRequestTokens} current + ${context.historyTokens} dialogue/${context.historyTurns} turns + ${context.toolTokens} tool/${context.toolResults} results; truncated=${context.truncatedBlocks}`,
130
+ );
131
+ }
132
+ if (metrics.estimatedInputTokens !== undefined)
133
+ parts.push(`requestβ‰ˆ${metrics.estimatedInputTokens} tokens`);
134
+ if (metrics.actualInputTokens !== undefined)
135
+ parts.push(`Jev usage=${metrics.actualInputTokens} input tokens`);
95
136
  if (metrics.httpStatus !== undefined)
96
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.');
97
147
  } else if (decision.errorClass) {
98
148
  parts.push(decision.errorClass);
99
149
  }
@@ -119,10 +169,10 @@ export const formatAdvisorFooter = (
119
169
  let summary: string;
120
170
  switch (metrics.outcome) {
121
171
  case 'selected':
122
- summary = `β†’ ${metrics.choice ?? decision.tier}${confidence}`;
123
- break;
124
- case 'low-confidence':
125
- 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}`;
126
176
  break;
127
177
  case 'uncertain':
128
178
  summary = ': no tier chosen β†’ baseline';
@@ -137,7 +187,7 @@ export const formatAdvisorFooter = (
137
187
  summary = ': network error β†’ baseline';
138
188
  break;
139
189
  case 'invalid-response':
140
- summary = ': invalid response β†’ baseline';
190
+ summary = `: invalid response${metrics.responseIssue ? ` (${metrics.responseIssue})` : ''} β†’ baseline`;
141
191
  break;
142
192
  case 'cancelled':
143
193
  summary = ': cancelled';
@@ -145,6 +195,9 @@ export const formatAdvisorFooter = (
145
195
  case 'unavailable':
146
196
  summary = `: ${metrics.choice ? 'target' : 'advice'} unavailable β†’ baseline`;
147
197
  break;
198
+ case 'input-too-large':
199
+ summary = ': estimated request too large β†’ baseline';
200
+ break;
148
201
  }
149
202
  const latency =
150
203
  metrics.latencyMs >= 1000
@@ -8,7 +8,15 @@
8
8
  "model": "jev-1.13.0",
9
9
  "timeoutMs": 1500,
10
10
  "confidenceThreshold": 0.65,
11
- "maxStateChars": 12000,
11
+ "probabilityThreshold": 0.8,
12
+ "maxStateTokens": 3000,
13
+ "context": {
14
+ "previousTurns": 2,
15
+ "maxHistoryTokens": 500,
16
+ "toolResults": "last-error",
17
+ "maxToolTokens": 250
18
+ },
19
+ "retry": { "maxAttempts": 2, "backoffMs": 400 },
12
20
  "mode": "advisory"
13
21
  },
14
22
  "classifierModel": "flash",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexeiled/pi-model-router",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "extensions",