@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.
@@ -1,18 +1,15 @@
1
1
  import type { ThinkingLevel } from '@earendil-works/pi-agent-core';
2
2
 
3
- export type RouterTier = 'high' | 'medium' | 'low';
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
- currentConfig: RouterConfig;
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
- reasoning: string;
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
- return `${decision.profile}: ${decision.tier} -> ${decision.targetProvider}/${decision.targetModelId} [${decision.thinking}] (${decision.reasoning})`;
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
- currentConfig,
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 ? lastDecision?.tier === activePin : true;
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
- (currentConfig.maxSessionBudget
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": "low" }
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexeiled/pi-model-router",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "extensions",