@alexeiled/pi-model-router 0.5.2 → 0.6.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.
@@ -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,57 @@ 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
+
69
135
  export interface RoutingDecision {
70
136
  profile: string;
71
137
  tier: RouterTier;
@@ -73,13 +139,14 @@ export interface RoutingDecision {
73
139
  targetProvider: string;
74
140
  targetModelId: string;
75
141
  targetLabel: string;
76
- reasoning: string;
142
+ reasonCode: RoutingReasonCode;
143
+ routingLatencyMs?: number | undefined;
144
+ errorClass?: RoutingErrorClass | undefined;
77
145
  thinking: ThinkingLevel;
78
146
  timestamp: number;
79
147
  isClassifier?: boolean | undefined;
80
148
  isFallback?: boolean | undefined;
81
149
  isBudgetForced?: boolean | undefined;
82
- isRuleMatched?: boolean | undefined;
83
150
  }
84
151
 
85
152
  export interface RouterLastProfileState {
@@ -117,6 +184,7 @@ export interface RouterPersistedState {
117
184
  }
118
185
 
119
186
  export interface RawRouterConfig {
187
+ jev?: unknown;
120
188
  debug?: unknown;
121
189
  classifierModel?: unknown;
122
190
  phaseBias?: unknown;
package/extensions/ui.ts CHANGED
@@ -5,17 +5,23 @@ import type {
5
5
  RouterThinkingByProfile,
6
6
  RoutingDecision,
7
7
  } from './types';
8
+ import { 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
+
17
22
  export const formatDecision = (decision: RoutingDecision): string => {
18
- return `${decision.profile}: ${decision.tier} -> ${decision.targetProvider}/${decision.targetModelId} [${decision.thinking}] (${decision.reasoning})`;
23
+ const source = formatDecisionSource(decision);
24
+ return `${decision.profile}: ${decision.tier} -> ${decision.targetProvider}/${decision.targetModelId} [${decision.thinking}]${source ? ` (${source})` : ''}`;
19
25
  };
20
26
 
21
27
  export const formatPinSummary = (
@@ -57,7 +63,7 @@ export const updateStatus = (
57
63
  lastNonRouterModel,
58
64
  accumulatedCost,
59
65
  widgetEnabled,
60
- currentConfig,
66
+ maxSessionBudget,
61
67
  } = state;
62
68
  const activeRouterProfile = routerEnabled ? selectedProfile : undefined;
63
69
  const statusProfile = selectedProfile ?? 'none';
@@ -69,7 +75,7 @@ export const updateStatus = (
69
75
  if (activeRouterProfile) {
70
76
  const matchesProfile =
71
77
  lastDecision && lastDecision.profile === activeRouterProfile;
72
- const matchesPin = activePin ? lastDecision?.tier === activePin : true;
78
+ const matchesPin = !activePin || lastDecision?.tier === activePin;
73
79
 
74
80
  let statusText: string;
75
81
  if (lastDecision && matchesProfile && matchesPin) {
@@ -92,9 +98,7 @@ export const updateStatus = (
92
98
  `Profile: ${statusProfile}${activeRouterProfile ? ' (active)' : ''}`,
93
99
  `Pin: ${activePin ?? 'auto'}`,
94
100
  `Cost: $${accumulatedCost.toFixed(4)}` +
95
- (currentConfig.maxSessionBudget
96
- ? ` / $${currentConfig.maxSessionBudget.toFixed(2)}`
97
- : ''),
101
+ (maxSessionBudget ? ` / $${maxSessionBudget.toFixed(2)}` : ''),
98
102
  ];
99
103
  if (lastDecision && lastDecision.profile === statusProfile) {
100
104
  const flags = getDecisionFlags(lastDecision);
@@ -103,6 +107,14 @@ export const updateStatus = (
103
107
  widgetLines.push(
104
108
  `Route: ${lastDecision.tier}${flagsStr} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${lastDecision.thinking})`,
105
109
  `Phase: ${lastDecision.phase}`,
110
+ `Source: ${formatDecisionSource(lastDecision) || 'unknown'}`,
111
+ ...(Number.isFinite(lastDecision.routingLatencyMs)
112
+ ? [`Routing: ${Math.round(lastDecision.routingLatencyMs ?? 0)}ms`]
113
+ : []),
114
+ ...(lastDecision.errorClass === 'deadline' ||
115
+ lastDecision.errorClass === 'advisor-unavailable'
116
+ ? [`Routing error: ${lastDecision.errorClass}`]
117
+ : []),
106
118
  );
107
119
  } else if (!routerEnabled && lastNonRouterModel) {
108
120
  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.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "extensions",