@alexeiled/pi-model-router 0.5.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 +16 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/extensions/commands.ts +677 -0
- package/extensions/config.ts +642 -0
- package/extensions/constants.ts +49 -0
- package/extensions/index.ts +573 -0
- package/extensions/provider.ts +650 -0
- package/extensions/routing.ts +483 -0
- package/extensions/state.ts +101 -0
- package/extensions/types.ts +109 -0
- package/extensions/ui.ts +131 -0
- package/model-router.example.json +72 -0
- package/package.json +77 -0
package/extensions/ui.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type {
|
|
3
|
+
RouterConfig,
|
|
4
|
+
RouterPinByProfile,
|
|
5
|
+
RouterThinkingByProfile,
|
|
6
|
+
RoutingDecision,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
const getEffectiveThinking = (
|
|
10
|
+
thinkingByProfile: RouterThinkingByProfile,
|
|
11
|
+
profileName: string,
|
|
12
|
+
decision: RoutingDecision,
|
|
13
|
+
) => thinkingByProfile[profileName]?.[decision.tier] ?? decision.thinking;
|
|
14
|
+
|
|
15
|
+
const getDecisionFlags = (decision: RoutingDecision): string[] => {
|
|
16
|
+
const flags: string[] = [];
|
|
17
|
+
if (decision.isFallback) flags.push('fallback');
|
|
18
|
+
if (decision.isBudgetForced) flags.push('budget-limit');
|
|
19
|
+
if (decision.isRuleMatched) flags.push('rule');
|
|
20
|
+
return flags;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const formatDecision = (decision: RoutingDecision): string => {
|
|
24
|
+
return `${decision.profile}: ${decision.tier} -> ${decision.targetProvider}/${decision.targetModelId} [${decision.thinking}] (${decision.reasoning})`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const formatPinSummary = (
|
|
28
|
+
pinnedTierByProfile: RouterPinByProfile,
|
|
29
|
+
): string => {
|
|
30
|
+
const entries = Object.entries(pinnedTierByProfile)
|
|
31
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
32
|
+
.map(([profile, tier]) => `${profile}:${tier}`);
|
|
33
|
+
return entries.length > 0 ? entries.join(', ') : 'none';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const formatThinkingSummary = (
|
|
37
|
+
thinkingByProfile: RouterThinkingByProfile,
|
|
38
|
+
): string => {
|
|
39
|
+
const entries = Object.entries(thinkingByProfile)
|
|
40
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
41
|
+
.map(([profile, tierMap]) => {
|
|
42
|
+
const tiers = Object.entries(tierMap)
|
|
43
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
44
|
+
.map(([tier, level]) => `${tier}:${level}`);
|
|
45
|
+
return `${profile}(${tiers.join(',')})`;
|
|
46
|
+
});
|
|
47
|
+
return entries.length > 0 ? entries.join(', ') : 'none';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const formatModelRef = (ref: string | undefined): string => {
|
|
51
|
+
return ref ?? 'none';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const updateStatus = (
|
|
55
|
+
ctx: ExtensionContext,
|
|
56
|
+
routerEnabled: boolean,
|
|
57
|
+
selectedProfile: string | undefined,
|
|
58
|
+
pinnedTierByProfile: RouterPinByProfile,
|
|
59
|
+
thinkingByProfile: RouterThinkingByProfile,
|
|
60
|
+
lastDecision: RoutingDecision | undefined,
|
|
61
|
+
lastNonRouterModel: string | undefined,
|
|
62
|
+
accumulatedCost: number,
|
|
63
|
+
widgetEnabled: boolean,
|
|
64
|
+
currentConfig: RouterConfig,
|
|
65
|
+
) => {
|
|
66
|
+
const activeRouterProfile = routerEnabled ? selectedProfile : undefined;
|
|
67
|
+
const statusProfile = selectedProfile ?? 'none';
|
|
68
|
+
const activePin = selectedProfile
|
|
69
|
+
? pinnedTierByProfile[selectedProfile]
|
|
70
|
+
: undefined;
|
|
71
|
+
const pinLabel = activePin ? ` [pin:${activePin}]` : '';
|
|
72
|
+
|
|
73
|
+
if (activeRouterProfile) {
|
|
74
|
+
const matchesProfile =
|
|
75
|
+
lastDecision && lastDecision.profile === activeRouterProfile;
|
|
76
|
+
const matchesPin = activePin ? lastDecision?.tier === activePin : true;
|
|
77
|
+
|
|
78
|
+
let statusText: string;
|
|
79
|
+
if (lastDecision && matchesProfile && matchesPin) {
|
|
80
|
+
const effectiveThinking = getEffectiveThinking(
|
|
81
|
+
thinkingByProfile,
|
|
82
|
+
activeRouterProfile,
|
|
83
|
+
lastDecision,
|
|
84
|
+
);
|
|
85
|
+
statusText = `router:${activeRouterProfile}${pinLabel} -> ${lastDecision.tier} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${effectiveThinking})`;
|
|
86
|
+
} else {
|
|
87
|
+
statusText = `router:${activeRouterProfile}${pinLabel} -> waiting`;
|
|
88
|
+
}
|
|
89
|
+
ctx.ui.setStatus('router', `🚥 ${statusText}`);
|
|
90
|
+
} else {
|
|
91
|
+
ctx.ui.setStatus('router', undefined);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!widgetEnabled) {
|
|
95
|
+
ctx.ui.setWidget('router', undefined);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const widgetLines = [
|
|
100
|
+
`Router: ${routerEnabled ? 'enabled' : 'disabled'}`,
|
|
101
|
+
`Profile: ${statusProfile}${activeRouterProfile ? ' (active)' : ''}`,
|
|
102
|
+
`Pin: ${activePin ?? 'auto'}`,
|
|
103
|
+
`Cost: $${accumulatedCost.toFixed(4)}` +
|
|
104
|
+
(currentConfig.maxSessionBudget
|
|
105
|
+
? ` / $${currentConfig.maxSessionBudget.toFixed(2)}`
|
|
106
|
+
: ''),
|
|
107
|
+
];
|
|
108
|
+
if (lastDecision && lastDecision.profile === statusProfile) {
|
|
109
|
+
const effectiveThinking = getEffectiveThinking(
|
|
110
|
+
thinkingByProfile,
|
|
111
|
+
statusProfile,
|
|
112
|
+
lastDecision,
|
|
113
|
+
);
|
|
114
|
+
const flags = getDecisionFlags(lastDecision);
|
|
115
|
+
const flagsStr = flags.length > 0 ? ` [${flags.join(',')}]` : '';
|
|
116
|
+
|
|
117
|
+
widgetLines.push(
|
|
118
|
+
`Route: ${lastDecision.tier}${flagsStr} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${effectiveThinking})`,
|
|
119
|
+
`Phase: ${lastDecision.phase}`,
|
|
120
|
+
);
|
|
121
|
+
} else if (!routerEnabled && lastNonRouterModel) {
|
|
122
|
+
widgetLines.push(`Fallback: ${lastNonRouterModel}`);
|
|
123
|
+
}
|
|
124
|
+
if (Object.keys(pinnedTierByProfile).length > 1) {
|
|
125
|
+
widgetLines.push(`Pins: ${formatPinSummary(pinnedTierByProfile)}`);
|
|
126
|
+
}
|
|
127
|
+
ctx.ui.setWidget(
|
|
128
|
+
'router',
|
|
129
|
+
widgetLines.map((line) => ctx.ui.theme.fg('dim', line)),
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"debug": false,
|
|
3
|
+
"classifierModel": "flash",
|
|
4
|
+
"phaseBias": 0.5,
|
|
5
|
+
"maxSessionBudget": 1.0,
|
|
6
|
+
"models": {
|
|
7
|
+
"gpt-pro": {
|
|
8
|
+
"model": "openai/gpt-5.4-pro",
|
|
9
|
+
"contextWindow": 256000,
|
|
10
|
+
"maxTokens": 64000,
|
|
11
|
+
"thinkingLevels": ["high", "medium", "low"]
|
|
12
|
+
},
|
|
13
|
+
"flash": {
|
|
14
|
+
"model": "google/gemini-flash-latest",
|
|
15
|
+
"contextWindow": 1048576
|
|
16
|
+
},
|
|
17
|
+
"nano": {
|
|
18
|
+
"model": "openai/gpt-5.4-nano",
|
|
19
|
+
"contextWindow": 128000,
|
|
20
|
+
"maxTokens": 8192,
|
|
21
|
+
"reasoning": false
|
|
22
|
+
},
|
|
23
|
+
"sonnet": {
|
|
24
|
+
"model": "anthropic/claude-3-5-sonnet-20241022",
|
|
25
|
+
"contextWindow": 200000
|
|
26
|
+
}
|
|
27
|
+
},
|
|
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
|
+
"profiles": {
|
|
37
|
+
"auto": {
|
|
38
|
+
"high": {
|
|
39
|
+
"model": "gpt-pro",
|
|
40
|
+
"thinking": "high",
|
|
41
|
+
"fallbacks": ["sonnet"]
|
|
42
|
+
},
|
|
43
|
+
"medium": { "model": "flash", "thinking": "medium" },
|
|
44
|
+
"low": { "model": "nano", "thinking": "low" }
|
|
45
|
+
},
|
|
46
|
+
"cheap": {
|
|
47
|
+
"high": { "model": "flash", "thinking": "low" },
|
|
48
|
+
"medium": { "model": "nano", "thinking": "off" },
|
|
49
|
+
"low": { "model": "google/gemini-flash-lite-latest", "thinking": "off" }
|
|
50
|
+
},
|
|
51
|
+
"deep": {
|
|
52
|
+
"high": {
|
|
53
|
+
"model": "openai/o1-preview",
|
|
54
|
+
"thinking": "xhigh",
|
|
55
|
+
"thinkingLevels": ["xhigh", "high", "medium", "low"]
|
|
56
|
+
},
|
|
57
|
+
"medium": { "model": "gpt-pro", "thinking": "medium" },
|
|
58
|
+
"low": { "model": "flash", "thinking": "low" }
|
|
59
|
+
},
|
|
60
|
+
"anthropic": {
|
|
61
|
+
"high": {
|
|
62
|
+
"model": "sonnet",
|
|
63
|
+
"thinking": "high"
|
|
64
|
+
},
|
|
65
|
+
"medium": {
|
|
66
|
+
"model": "sonnet",
|
|
67
|
+
"thinking": "medium"
|
|
68
|
+
},
|
|
69
|
+
"low": { "model": "anthropic/claude-3-haiku-20240307", "thinking": "low" }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alexeiled/pi-model-router",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"extensions",
|
|
7
|
+
"!extensions/*.test.ts",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"model-router.example.json",
|
|
10
|
+
"README.md",
|
|
11
|
+
"CHANGELOG.md"
|
|
12
|
+
],
|
|
13
|
+
"description": "Smart per-turn model router for Pi that optimizes your AI budget and usage limits by dynamically switching LLM tiers based on task complexity, budget, and phase awareness.",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pi-package",
|
|
16
|
+
"pi",
|
|
17
|
+
"model-router",
|
|
18
|
+
"llm",
|
|
19
|
+
"coding-agent",
|
|
20
|
+
"extension"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.19.0"
|
|
25
|
+
},
|
|
26
|
+
"packageManager": "npm@11.18.0",
|
|
27
|
+
"author": "Alexei Ledenev",
|
|
28
|
+
"contributors": [
|
|
29
|
+
"Ye Liu"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/alexei-led/pi-model-router.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/alexei-led/pi-model-router#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/alexei-led/pi-model-router/issues"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"exports": {
|
|
43
|
+
".": "./extensions/index.ts"
|
|
44
|
+
},
|
|
45
|
+
"pi": {
|
|
46
|
+
"extensions": [
|
|
47
|
+
"./extensions/index.ts"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"tsc": "tsc --noEmit",
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
"prepublishOnly": "npm run check && npm test",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"check": "biome check --error-on-warnings . && npm run tsc",
|
|
56
|
+
"lint": "biome lint --error-on-warnings .",
|
|
57
|
+
"lint:fix": "biome lint --write --error-on-warnings .",
|
|
58
|
+
"format": "biome format --write .",
|
|
59
|
+
"format:check": "biome format .",
|
|
60
|
+
"pack:dry": "npm pack --dry-run"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@earendil-works/pi-agent-core": ">=0.86.0",
|
|
64
|
+
"@earendil-works/pi-ai": ">=0.86.0",
|
|
65
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0",
|
|
66
|
+
"@earendil-works/pi-tui": ">=0.86.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@biomejs/biome": "2.5.14",
|
|
70
|
+
"@earendil-works/pi-agent-core": "0.86.0",
|
|
71
|
+
"@earendil-works/pi-ai": "0.86.0",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "0.86.0",
|
|
73
|
+
"@earendil-works/pi-tui": "0.86.0",
|
|
74
|
+
"typescript": "^7.0.2",
|
|
75
|
+
"vitest": "^5.0.1"
|
|
76
|
+
}
|
|
77
|
+
}
|