@phi-code-admin/phi-code 0.57.2 → 0.57.3
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/extensions/phi/smart-router.ts +172 -190
- package/package.json +1 -1
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Smart Router Extension - Intelligent model routing for different task types
|
|
3
3
|
*
|
|
4
|
-
* Analyzes user input
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
4
|
+
* Analyzes user input keywords and suggests the optimal model:
|
|
5
|
+
* - code: implement, create, build, refactor → qwen3-coder-plus
|
|
6
|
+
* - debug: fix, bug, error, crash → qwen3-max-2026-01-23
|
|
7
|
+
* - explore: read, analyze, explain, understand → kimi-k2.5
|
|
8
|
+
* - plan: plan, design, architect, spec → qwen3-max-2026-01-23
|
|
9
|
+
* - test: test, verify, validate, check → kimi-k2.5
|
|
10
|
+
* - review: review, audit, quality, security → qwen3.5-plus
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Features:
|
|
14
|
-
* - Input analysis and model recommendations
|
|
15
|
-
* - Configurable routing rules
|
|
16
|
-
* - User notifications via ctx.ui.notify
|
|
17
|
-
*
|
|
18
|
-
* Usage:
|
|
19
|
-
* 1. Copy to packages/coding-agent/extensions/phi/smart-router.ts
|
|
20
|
-
* 2. Optionally configure via ~/.phi/agent/routing.json
|
|
12
|
+
* Configuration: ~/.phi/agent/routing.json (same format as config/routing.json)
|
|
13
|
+
* Command: /routing — show config, enable/disable, test, reload
|
|
21
14
|
*/
|
|
22
15
|
|
|
23
16
|
import type { ExtensionAPI } from "phi-code";
|
|
@@ -25,266 +18,255 @@ import { readFile, mkdir, writeFile, access } from "node:fs/promises";
|
|
|
25
18
|
import { join } from "node:path";
|
|
26
19
|
import { homedir } from "node:os";
|
|
27
20
|
|
|
21
|
+
// ─── Types ───────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
interface RouteEntry {
|
|
24
|
+
description: string;
|
|
25
|
+
keywords: string[];
|
|
26
|
+
preferredModel: string;
|
|
27
|
+
fallback: string;
|
|
28
|
+
agent: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
28
31
|
interface RoutingConfig {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
models: {
|
|
36
|
-
coder: string;
|
|
37
|
-
reasoning: string;
|
|
38
|
-
fast: string;
|
|
39
|
-
};
|
|
32
|
+
routes: Record<string, RouteEntry>;
|
|
33
|
+
default: { model: string; agent: string | null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface FullConfig {
|
|
37
|
+
routing: RoutingConfig;
|
|
40
38
|
enabled: boolean;
|
|
41
39
|
notifyOnRecommendation: boolean;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
// ─── Defaults (aligned with config/routing.json and default-models.json) ──
|
|
43
|
+
|
|
44
|
+
const DEFAULT_ROUTING: RoutingConfig = {
|
|
45
|
+
routes: {
|
|
46
|
+
code: {
|
|
47
|
+
description: "Code generation, implementation, refactoring",
|
|
48
|
+
keywords: ["implement", "create", "build", "refactor", "write", "add", "modify", "update", "generate", "code", "develop", "function", "class"],
|
|
49
|
+
preferredModel: "qwen3-coder-plus",
|
|
50
|
+
fallback: "qwen3.5-plus",
|
|
51
|
+
agent: "code",
|
|
52
|
+
},
|
|
53
|
+
debug: {
|
|
54
|
+
description: "Debugging, fixing, error resolution",
|
|
55
|
+
keywords: ["fix", "bug", "error", "debug", "crash", "broken", "failing", "issue", "troubleshoot", "repair", "solve"],
|
|
56
|
+
preferredModel: "qwen3-max-2026-01-23",
|
|
57
|
+
fallback: "qwen3.5-plus",
|
|
58
|
+
agent: "code",
|
|
59
|
+
},
|
|
60
|
+
explore: {
|
|
61
|
+
description: "Code reading, analysis, understanding",
|
|
62
|
+
keywords: ["read", "analyze", "explain", "understand", "find", "search", "look", "show", "what", "how", "explore", "examine"],
|
|
63
|
+
preferredModel: "kimi-k2.5",
|
|
64
|
+
fallback: "glm-4.7",
|
|
65
|
+
agent: "explore",
|
|
66
|
+
},
|
|
67
|
+
plan: {
|
|
68
|
+
description: "Architecture, design, planning",
|
|
69
|
+
keywords: ["plan", "design", "architect", "spec", "structure", "organize", "strategy", "approach", "roadmap"],
|
|
70
|
+
preferredModel: "qwen3-max-2026-01-23",
|
|
71
|
+
fallback: "qwen3.5-plus",
|
|
72
|
+
agent: "plan",
|
|
73
|
+
},
|
|
74
|
+
test: {
|
|
75
|
+
description: "Testing, validation, verification",
|
|
76
|
+
keywords: ["test", "verify", "validate", "check", "assert", "coverage", "unit", "integration", "e2e"],
|
|
77
|
+
preferredModel: "kimi-k2.5",
|
|
78
|
+
fallback: "glm-4.7",
|
|
79
|
+
agent: "test",
|
|
80
|
+
},
|
|
81
|
+
review: {
|
|
82
|
+
description: "Code review, quality assessment",
|
|
83
|
+
keywords: ["review", "audit", "quality", "security", "improve", "optimize", "refine", "critique"],
|
|
84
|
+
preferredModel: "qwen3.5-plus",
|
|
85
|
+
fallback: "qwen3-max-2026-01-23",
|
|
86
|
+
agent: "review",
|
|
87
|
+
},
|
|
50
88
|
},
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
fast: "anthropic/claude-haiku"
|
|
89
|
+
default: {
|
|
90
|
+
model: "qwen3.5-plus",
|
|
91
|
+
agent: null,
|
|
55
92
|
},
|
|
56
|
-
enabled: true,
|
|
57
|
-
notifyOnRecommendation: true
|
|
58
93
|
};
|
|
59
94
|
|
|
95
|
+
// ─── Extension ───────────────────────────────────────────────────────────
|
|
96
|
+
|
|
60
97
|
export default function smartRouterExtension(pi: ExtensionAPI) {
|
|
61
|
-
let config: RoutingConfig = DEFAULT_CONFIG;
|
|
62
98
|
const configDir = join(homedir(), ".phi", "agent");
|
|
63
99
|
const configPath = join(configDir, "routing.json");
|
|
64
100
|
|
|
101
|
+
let config: FullConfig = {
|
|
102
|
+
routing: DEFAULT_ROUTING,
|
|
103
|
+
enabled: true,
|
|
104
|
+
notifyOnRecommendation: true,
|
|
105
|
+
};
|
|
106
|
+
|
|
65
107
|
/**
|
|
66
|
-
* Load routing
|
|
108
|
+
* Load routing config from ~/.phi/agent/routing.json
|
|
67
109
|
*/
|
|
68
110
|
async function loadConfig() {
|
|
69
111
|
try {
|
|
70
112
|
await access(configPath);
|
|
71
|
-
const
|
|
72
|
-
const userConfig = JSON.parse(
|
|
73
|
-
|
|
74
|
-
// Merge with defaults
|
|
75
|
-
config = {
|
|
76
|
-
...DEFAULT_CONFIG,
|
|
77
|
-
...userConfig,
|
|
78
|
-
patterns: { ...DEFAULT_CONFIG.patterns, ...userConfig.patterns },
|
|
79
|
-
models: { ...DEFAULT_CONFIG.models, ...userConfig.models }
|
|
80
|
-
};
|
|
81
|
-
} catch (error) {
|
|
82
|
-
// Config doesn't exist or is invalid, use defaults
|
|
83
|
-
console.log("Using default routing configuration");
|
|
84
|
-
await saveDefaultConfig();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
113
|
+
const text = await readFile(configPath, "utf-8");
|
|
114
|
+
const userConfig = JSON.parse(text);
|
|
87
115
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
// Support both flat format (routes at top level) and wrapped format
|
|
117
|
+
if (userConfig.routes) {
|
|
118
|
+
config.routing = {
|
|
119
|
+
routes: { ...DEFAULT_ROUTING.routes, ...userConfig.routes },
|
|
120
|
+
default: userConfig.default || DEFAULT_ROUTING.default,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof userConfig.enabled === "boolean") config.enabled = userConfig.enabled;
|
|
125
|
+
if (typeof userConfig.notifyOnRecommendation === "boolean") config.notifyOnRecommendation = userConfig.notifyOnRecommendation;
|
|
126
|
+
} catch {
|
|
127
|
+
// No config file — use defaults, and save them for reference
|
|
128
|
+
try {
|
|
129
|
+
await mkdir(configDir, { recursive: true });
|
|
130
|
+
await writeFile(configPath, JSON.stringify(DEFAULT_ROUTING, null, 2), "utf-8");
|
|
131
|
+
} catch {
|
|
132
|
+
// Can't write, that's fine
|
|
133
|
+
}
|
|
98
134
|
}
|
|
99
135
|
}
|
|
100
136
|
|
|
101
137
|
/**
|
|
102
|
-
* Analyze input text to
|
|
138
|
+
* Analyze input text to classify task type
|
|
103
139
|
*/
|
|
104
|
-
function
|
|
105
|
-
const
|
|
106
|
-
const results: Array<{
|
|
140
|
+
function classifyTask(text: string): { category: string | null; confidence: number; matches: string[]; route: RouteEntry | null } {
|
|
141
|
+
const lower = text.toLowerCase();
|
|
142
|
+
const results: Array<{ category: string; confidence: number; matches: string[]; route: RouteEntry }> = [];
|
|
107
143
|
|
|
108
|
-
|
|
109
|
-
for (const [category, patterns] of Object.entries(config.patterns)) {
|
|
144
|
+
for (const [category, route] of Object.entries(config.routing.routes)) {
|
|
110
145
|
const matches: string[] = [];
|
|
111
|
-
let matchCount = 0;
|
|
112
146
|
|
|
113
|
-
for (const
|
|
114
|
-
if (
|
|
115
|
-
matches.push(
|
|
116
|
-
matchCount++;
|
|
147
|
+
for (const keyword of route.keywords) {
|
|
148
|
+
if (lower.includes(keyword.toLowerCase())) {
|
|
149
|
+
matches.push(keyword);
|
|
117
150
|
}
|
|
118
151
|
}
|
|
119
152
|
|
|
120
|
-
if (
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
matches
|
|
127
|
-
});
|
|
153
|
+
if (matches.length > 0) {
|
|
154
|
+
// Confidence = weighted match ratio
|
|
155
|
+
// More matches = higher confidence, but cap at 95%
|
|
156
|
+
const ratio = matches.length / route.keywords.length;
|
|
157
|
+
const confidence = Math.min(95, Math.round(ratio * 100 + matches.length * 5));
|
|
158
|
+
results.push({ category, confidence, matches, route });
|
|
128
159
|
}
|
|
129
160
|
}
|
|
130
161
|
|
|
131
|
-
// Return the highest confidence match
|
|
132
162
|
if (results.length === 0) {
|
|
133
|
-
return {
|
|
163
|
+
return { category: null, confidence: 0, matches: [], route: null };
|
|
134
164
|
}
|
|
135
165
|
|
|
166
|
+
// Highest confidence wins
|
|
136
167
|
results.sort((a, b) => b.confidence - a.confidence);
|
|
137
168
|
return results[0];
|
|
138
169
|
}
|
|
139
170
|
|
|
140
|
-
|
|
141
|
-
* Get recommended model for task type
|
|
142
|
-
*/
|
|
143
|
-
function getRecommendedModel(taskType: keyof RoutingConfig['patterns'] | null): string | null {
|
|
144
|
-
if (!taskType) return null;
|
|
145
|
-
|
|
146
|
-
switch (taskType) {
|
|
147
|
-
case 'code':
|
|
148
|
-
return config.models.coder;
|
|
149
|
-
case 'debug':
|
|
150
|
-
case 'planning':
|
|
151
|
-
return config.models.reasoning;
|
|
152
|
-
case 'exploration':
|
|
153
|
-
return config.models.fast;
|
|
154
|
-
default:
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
171
|
+
// ─── Input Event ─────────────────────────────────────────────────
|
|
158
172
|
|
|
159
|
-
/**
|
|
160
|
-
* Get task type description
|
|
161
|
-
*/
|
|
162
|
-
function getTaskDescription(taskType: keyof RoutingConfig['patterns'] | null): string {
|
|
163
|
-
switch (taskType) {
|
|
164
|
-
case 'code':
|
|
165
|
-
return 'Code Implementation';
|
|
166
|
-
case 'debug':
|
|
167
|
-
return 'Debugging & Problem Solving';
|
|
168
|
-
case 'exploration':
|
|
169
|
-
return 'Analysis & Understanding';
|
|
170
|
-
case 'planning':
|
|
171
|
-
return 'Planning & Design';
|
|
172
|
-
default:
|
|
173
|
-
return 'General Task';
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Input interceptor for smart routing
|
|
179
|
-
*/
|
|
180
173
|
pi.on("input", async (event, ctx) => {
|
|
181
|
-
// Skip if routing is disabled or this is an extension-generated message
|
|
182
174
|
if (!config.enabled || event.source === "extension") {
|
|
183
175
|
return { action: "continue" };
|
|
184
176
|
}
|
|
185
177
|
|
|
186
|
-
|
|
187
|
-
const analysis = analyzeTaskType(event.text);
|
|
188
|
-
|
|
189
|
-
// Only recommend if we have good confidence (>= 30%)
|
|
190
|
-
if (analysis.type && analysis.confidence >= 30) {
|
|
191
|
-
const recommendedModel = getRecommendedModel(analysis.type);
|
|
192
|
-
const taskDescription = getTaskDescription(analysis.type);
|
|
193
|
-
|
|
194
|
-
if (recommendedModel && config.notifyOnRecommendation) {
|
|
195
|
-
const message = `💡 Detected: ${taskDescription} (${analysis.confidence.toFixed(0)}% confidence)
|
|
196
|
-
Recommended model: ${recommendedModel}
|
|
197
|
-
Matched patterns: ${analysis.matches.join(", ")}`;
|
|
178
|
+
const result = classifyTask(event.text);
|
|
198
179
|
|
|
199
|
-
|
|
180
|
+
if (result.category && result.confidence >= 25 && result.route) {
|
|
181
|
+
if (config.notifyOnRecommendation) {
|
|
182
|
+
ctx.ui.notify(
|
|
183
|
+
`🔀 ${result.route.description} → \`${result.route.preferredModel}\` (${result.confidence}% | ${result.matches.join(", ")})`,
|
|
184
|
+
"info"
|
|
185
|
+
);
|
|
200
186
|
}
|
|
201
187
|
}
|
|
202
188
|
|
|
203
189
|
return { action: "continue" };
|
|
204
190
|
});
|
|
205
191
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
*/
|
|
192
|
+
// ─── /routing Command ────────────────────────────────────────────
|
|
193
|
+
|
|
209
194
|
pi.registerCommand("routing", {
|
|
210
|
-
description: "Show or configure smart routing
|
|
195
|
+
description: "Show or configure smart routing (enable/disable/test/reload)",
|
|
211
196
|
handler: async (args, ctx) => {
|
|
212
|
-
|
|
213
|
-
// Show current configuration
|
|
214
|
-
const statusMessage = `Smart Router Configuration:
|
|
215
|
-
|
|
216
|
-
**Status:** ${config.enabled ? "Enabled" : "Disabled"}
|
|
217
|
-
**Notifications:** ${config.notifyOnRecommendation ? "Enabled" : "Disabled"}
|
|
197
|
+
const arg = args.trim().toLowerCase();
|
|
218
198
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
199
|
+
if (!arg) {
|
|
200
|
+
// Show current config
|
|
201
|
+
let output = `**🔀 Smart Router**\n\n`;
|
|
202
|
+
output += `Status: ${config.enabled ? "✅ Enabled" : "❌ Disabled"}\n`;
|
|
203
|
+
output += `Notifications: ${config.notifyOnRecommendation ? "On" : "Off"}\n\n`;
|
|
224
204
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
205
|
+
output += `**Routes:**\n`;
|
|
206
|
+
for (const [cat, route] of Object.entries(config.routing.routes)) {
|
|
207
|
+
output += ` **${cat}** → \`${route.preferredModel}\` (fallback: \`${route.fallback}\`) [agent: ${route.agent}]\n`;
|
|
208
|
+
output += ` Keywords: ${route.keywords.slice(0, 6).join(", ")}${route.keywords.length > 6 ? "..." : ""}\n`;
|
|
209
|
+
}
|
|
210
|
+
output += `\n **default** → \`${config.routing.default.model}\`\n`;
|
|
230
211
|
|
|
231
|
-
|
|
212
|
+
output += `\nConfig: \`${configPath}\``;
|
|
213
|
+
output += `\nCommands: \`/routing enable|disable|notify-on|notify-off|reload|test\``;
|
|
232
214
|
|
|
233
|
-
ctx.ui.notify(
|
|
215
|
+
ctx.ui.notify(output, "info");
|
|
234
216
|
return;
|
|
235
217
|
}
|
|
236
218
|
|
|
237
|
-
const arg = args.trim().toLowerCase();
|
|
238
|
-
|
|
239
219
|
switch (arg) {
|
|
240
220
|
case "enable":
|
|
241
221
|
config.enabled = true;
|
|
242
|
-
ctx.ui.notify("Smart routing enabled", "info");
|
|
222
|
+
ctx.ui.notify("✅ Smart routing enabled.", "info");
|
|
243
223
|
break;
|
|
244
224
|
case "disable":
|
|
245
225
|
config.enabled = false;
|
|
246
|
-
ctx.ui.notify("Smart routing disabled", "info");
|
|
226
|
+
ctx.ui.notify("❌ Smart routing disabled.", "info");
|
|
247
227
|
break;
|
|
248
228
|
case "notify-on":
|
|
249
229
|
config.notifyOnRecommendation = true;
|
|
250
|
-
ctx.ui.notify("Routing notifications enabled", "info");
|
|
230
|
+
ctx.ui.notify("🔔 Routing notifications enabled.", "info");
|
|
251
231
|
break;
|
|
252
232
|
case "notify-off":
|
|
253
233
|
config.notifyOnRecommendation = false;
|
|
254
|
-
ctx.ui.notify("Routing notifications disabled", "info");
|
|
234
|
+
ctx.ui.notify("🔕 Routing notifications disabled.", "info");
|
|
255
235
|
break;
|
|
256
236
|
case "reload":
|
|
257
237
|
await loadConfig();
|
|
258
|
-
ctx.ui.notify("Routing
|
|
238
|
+
ctx.ui.notify("🔄 Routing config reloaded from disk.", "info");
|
|
259
239
|
break;
|
|
260
|
-
case "test":
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
"
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"
|
|
240
|
+
case "test": {
|
|
241
|
+
const tests = [
|
|
242
|
+
"implement a new user authentication system",
|
|
243
|
+
"fix the crash when uploading files larger than 10MB",
|
|
244
|
+
"explain how the middleware chain works",
|
|
245
|
+
"plan the migration from REST to GraphQL",
|
|
246
|
+
"run all unit tests and check coverage",
|
|
247
|
+
"review the PR for security vulnerabilities",
|
|
248
|
+
"what time is it",
|
|
267
249
|
];
|
|
268
|
-
|
|
269
|
-
let
|
|
270
|
-
for (const input of
|
|
271
|
-
const
|
|
272
|
-
const model =
|
|
273
|
-
|
|
250
|
+
|
|
251
|
+
let output = "**🧪 Routing Test:**\n\n";
|
|
252
|
+
for (const input of tests) {
|
|
253
|
+
const result = classifyTask(input);
|
|
254
|
+
const model = result.route?.preferredModel || config.routing.default.model;
|
|
255
|
+
const tag = result.category || "default";
|
|
256
|
+
output += `"${input}"\n → **${tag}** (${result.confidence}%) → \`${model}\`\n\n`;
|
|
274
257
|
}
|
|
275
|
-
|
|
276
|
-
ctx.ui.notify(testResults, "info");
|
|
258
|
+
ctx.ui.notify(output, "info");
|
|
277
259
|
break;
|
|
260
|
+
}
|
|
278
261
|
default:
|
|
279
|
-
ctx.ui.notify("Usage:
|
|
262
|
+
ctx.ui.notify("Usage: `/routing [enable|disable|notify-on|notify-off|reload|test]`", "warning");
|
|
280
263
|
}
|
|
281
264
|
},
|
|
282
265
|
});
|
|
283
266
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
*/
|
|
267
|
+
// ─── Session Start ───────────────────────────────────────────────
|
|
268
|
+
|
|
287
269
|
pi.on("session_start", async (_event, _ctx) => {
|
|
288
270
|
await loadConfig();
|
|
289
271
|
});
|
|
290
|
-
}
|
|
272
|
+
}
|