@phi-code-admin/phi-code 0.57.6 → 0.57.8
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/init.ts +170 -33
- package/extensions/phi/skill-loader.ts +17 -4
- package/package.json +1 -1
package/extensions/phi/init.ts
CHANGED
|
@@ -261,21 +261,126 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
261
261
|
|
|
262
262
|
// ─── MODE: Auto ──────────────────────────────────────────────────
|
|
263
263
|
|
|
264
|
+
// ─── Model Intelligence Database ─────────────────────────────────
|
|
265
|
+
|
|
266
|
+
interface ModelProfile {
|
|
267
|
+
id: string;
|
|
268
|
+
capabilities: {
|
|
269
|
+
coding: number; // 0-100 score for code generation
|
|
270
|
+
reasoning: number; // 0-100 score for debugging/planning
|
|
271
|
+
speed: number; // 0-100 score for fast tasks
|
|
272
|
+
general: number; // 0-100 overall score
|
|
273
|
+
};
|
|
274
|
+
hasReasoning: boolean;
|
|
275
|
+
}
|
|
276
|
+
|
|
264
277
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
278
|
+
* Fetch model profiles from OpenRouter's free API.
|
|
279
|
+
* Classifies each model based on its description, name, and supported parameters.
|
|
280
|
+
* Falls back to name-based heuristics if OpenRouter is unreachable.
|
|
281
|
+
*/
|
|
282
|
+
async function fetchModelProfiles(modelIds: string[]): Promise<Map<string, ModelProfile>> {
|
|
283
|
+
const profiles = new Map<string, ModelProfile>();
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
288
|
+
const res = await fetch("https://openrouter.ai/api/v1/models", {
|
|
289
|
+
signal: controller.signal,
|
|
290
|
+
});
|
|
291
|
+
clearTimeout(timeout);
|
|
292
|
+
|
|
293
|
+
if (res.ok) {
|
|
294
|
+
const data = await res.json() as any;
|
|
295
|
+
const orModels: any[] = data.data || [];
|
|
296
|
+
|
|
297
|
+
for (const modelId of modelIds) {
|
|
298
|
+
// Try exact match first, then fuzzy match by base name
|
|
299
|
+
const baseName = modelId.replace(/:.+$/, "").split("/").pop()?.toLowerCase() || modelId.toLowerCase();
|
|
300
|
+
const match = orModels.find((m: any) => {
|
|
301
|
+
const mId = m.id?.toLowerCase() || "";
|
|
302
|
+
const mName = m.name?.toLowerCase() || "";
|
|
303
|
+
return mId.includes(baseName) || mName.includes(baseName);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (match) {
|
|
307
|
+
const desc = (match.description || "").toLowerCase();
|
|
308
|
+
const name = (match.name || "").toLowerCase();
|
|
309
|
+
const hasReasoning = (match.supported_parameters || []).includes("reasoning")
|
|
310
|
+
|| (match.supported_parameters || []).includes("include_reasoning");
|
|
311
|
+
|
|
312
|
+
// Score based on description keywords and model characteristics
|
|
313
|
+
let coding = 50, reasoning = 50, speed = 50, general = 60;
|
|
314
|
+
|
|
315
|
+
// Coding signals
|
|
316
|
+
if (/cod(e|ing|ex)|program|implement|refactor|software engineer/.test(desc) || /coder|codex|codestral/.test(name)) {
|
|
317
|
+
coding = 85;
|
|
318
|
+
}
|
|
319
|
+
// Reasoning signals
|
|
320
|
+
if (hasReasoning || /reason|think|logic|step.by.step|complex/.test(desc) || /o1|o3|pro|opus/.test(name)) {
|
|
321
|
+
reasoning = 85;
|
|
322
|
+
}
|
|
323
|
+
// Speed signals (smaller/cheaper models)
|
|
324
|
+
const pricing = match.pricing || {};
|
|
325
|
+
const promptCost = parseFloat(pricing.prompt || "0.01");
|
|
326
|
+
if (promptCost < 0.001 || /fast|flash|mini|small|haiku|lite|instant/.test(name)) {
|
|
327
|
+
speed = 85;
|
|
328
|
+
}
|
|
329
|
+
// General quality (larger context = usually better)
|
|
330
|
+
const ctx = match.context_length || 0;
|
|
331
|
+
if (ctx >= 200000) general = 80;
|
|
332
|
+
if (ctx >= 1000000) general = 90;
|
|
333
|
+
if (/frontier|flagship|most.advanced|best|state.of.the.art/.test(desc)) general = 90;
|
|
334
|
+
|
|
335
|
+
profiles.set(modelId, { id: modelId, capabilities: { coding, reasoning, speed, general }, hasReasoning });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// OpenRouter unreachable — will fall back to heuristics
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Fill in any models not found in OpenRouter with name-based heuristics
|
|
344
|
+
for (const modelId of modelIds) {
|
|
345
|
+
if (!profiles.has(modelId)) {
|
|
346
|
+
profiles.set(modelId, classifyByName(modelId));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return profiles;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Fallback: classify model by name patterns when OpenRouter data is unavailable.
|
|
355
|
+
*/
|
|
356
|
+
function classifyByName(modelId: string): ModelProfile {
|
|
357
|
+
const l = modelId.toLowerCase();
|
|
358
|
+
let coding = 50, reasoning = 50, speed = 50, general = 55;
|
|
359
|
+
let hasReasoning = false;
|
|
360
|
+
|
|
361
|
+
if (/coder|code|codestral/.test(l)) coding = 80;
|
|
362
|
+
if (/max|pro|plus|opus|large|o1|o3/.test(l)) { reasoning = 80; general = 75; }
|
|
363
|
+
if (/mini|flash|fast|small|haiku|lite/.test(l)) { speed = 80; }
|
|
364
|
+
if (/o1|o3|deepseek-r1|qwq/.test(l)) { hasReasoning = true; reasoning = 85; }
|
|
365
|
+
|
|
366
|
+
return { id: modelId, capabilities: { coding, reasoning, speed, general }, hasReasoning };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Auto-assign models using OpenRouter rankings + models.dev data.
|
|
371
|
+
* Works with ANY provider — cloud, local, or mixed.
|
|
267
372
|
*
|
|
268
|
-
* Strategy:
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
373
|
+
* Strategy:
|
|
374
|
+
* 1. Fetch model profiles from OpenRouter (free, no API key needed)
|
|
375
|
+
* 2. Score each model for coding, reasoning, speed, and general tasks
|
|
376
|
+
* 3. Assign best model per role based on scores
|
|
377
|
+
* 4. Fall back to name-based heuristics if OpenRouter is unreachable
|
|
378
|
+
* 5. Single model? → everything uses that model (still works!)
|
|
273
379
|
*/
|
|
274
|
-
function autoMode(availableModels: string[]): Record<string, { preferred: string; fallback: string }
|
|
380
|
+
async function autoMode(availableModels: string[], ctx?: any): Promise<Record<string, { preferred: string; fallback: string }>> {
|
|
275
381
|
const assignments: Record<string, { preferred: string; fallback: string }> = {};
|
|
276
382
|
|
|
277
383
|
if (availableModels.length === 0) {
|
|
278
|
-
// Shouldn't happen, but safety fallback
|
|
279
384
|
const fb = { preferred: "qwen3.5-plus", fallback: "qwen3.5-plus" };
|
|
280
385
|
for (const role of TASK_ROLES) assignments[role.key] = fb;
|
|
281
386
|
assignments["default"] = fb;
|
|
@@ -283,33 +388,66 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
283
388
|
}
|
|
284
389
|
|
|
285
390
|
if (availableModels.length === 1) {
|
|
286
|
-
// Single model: everything uses it
|
|
287
391
|
const single = { preferred: availableModels[0], fallback: availableModels[0] };
|
|
288
392
|
for (const role of TASK_ROLES) assignments[role.key] = single;
|
|
289
393
|
assignments["default"] = single;
|
|
290
394
|
return assignments;
|
|
291
395
|
}
|
|
292
396
|
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
397
|
+
// Fetch intelligence from OpenRouter
|
|
398
|
+
if (ctx) ctx.ui.notify("📊 Fetching model rankings from OpenRouter...", "info");
|
|
399
|
+
const profiles = await fetchModelProfiles(availableModels);
|
|
400
|
+
|
|
401
|
+
// Find best model for each capability
|
|
402
|
+
function bestFor(capability: keyof ModelProfile["capabilities"]): string {
|
|
403
|
+
let best = availableModels[0], bestScore = 0;
|
|
404
|
+
for (const id of availableModels) {
|
|
405
|
+
const p = profiles.get(id);
|
|
406
|
+
if (p && p.capabilities[capability] > bestScore) {
|
|
407
|
+
bestScore = p.capabilities[capability];
|
|
408
|
+
best = id;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return best;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function secondBestFor(capability: keyof ModelProfile["capabilities"], excludeId: string): string {
|
|
415
|
+
let best = availableModels.find(m => m !== excludeId) || excludeId;
|
|
416
|
+
let bestScore = 0;
|
|
417
|
+
for (const id of availableModels) {
|
|
418
|
+
if (id === excludeId) continue;
|
|
419
|
+
const p = profiles.get(id);
|
|
420
|
+
if (p && p.capabilities[capability] > bestScore) {
|
|
421
|
+
bestScore = p.capabilities[capability];
|
|
422
|
+
best = id;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return best;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const bestCoder = bestFor("coding");
|
|
429
|
+
const bestReasoner = bestFor("reasoning");
|
|
430
|
+
const bestFast = bestFor("speed");
|
|
431
|
+
const bestGeneral = bestFor("general");
|
|
432
|
+
|
|
433
|
+
assignments["code"] = { preferred: bestCoder, fallback: secondBestFor("coding", bestCoder) };
|
|
434
|
+
assignments["debug"] = { preferred: bestReasoner, fallback: secondBestFor("reasoning", bestReasoner) };
|
|
435
|
+
assignments["plan"] = { preferred: bestReasoner, fallback: secondBestFor("reasoning", bestReasoner) };
|
|
436
|
+
assignments["explore"] = { preferred: bestFast, fallback: secondBestFor("speed", bestFast) };
|
|
437
|
+
assignments["test"] = { preferred: bestFast, fallback: secondBestFor("speed", bestFast) };
|
|
438
|
+
assignments["review"] = { preferred: bestGeneral, fallback: secondBestFor("general", bestGeneral) };
|
|
439
|
+
assignments["default"] = { preferred: bestGeneral, fallback: secondBestFor("general", bestGeneral) };
|
|
440
|
+
|
|
441
|
+
// Show what was assigned and why
|
|
442
|
+
if (ctx) {
|
|
443
|
+
ctx.ui.notify("📊 Model rankings applied:", "info");
|
|
444
|
+
for (const role of TASK_ROLES) {
|
|
445
|
+
const a = assignments[role.key];
|
|
446
|
+
const p = profiles.get(a.preferred);
|
|
447
|
+
const scores = p ? `(coding:${p.capabilities.coding} reasoning:${p.capabilities.reasoning} speed:${p.capabilities.speed})` : "";
|
|
448
|
+
ctx.ui.notify(` ${role.label}: ${a.preferred} ${scores}`, "info");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
313
451
|
|
|
314
452
|
return assignments;
|
|
315
453
|
}
|
|
@@ -347,7 +485,7 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
347
485
|
ctx.ui.notify("Run: `/benchmark all` then `/phi-init` again with mode=benchmark to use results.\n", "info");
|
|
348
486
|
|
|
349
487
|
// Fall back to auto for now
|
|
350
|
-
return autoMode(availableModels);
|
|
488
|
+
return autoMode(availableModels, ctx);
|
|
351
489
|
}
|
|
352
490
|
|
|
353
491
|
function assignFromBenchmark(results: any[], availableModels: string[]): Record<string, { preferred: string; fallback: string }> {
|
|
@@ -517,8 +655,7 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
517
655
|
let assignments: Record<string, { preferred: string; fallback: string }>;
|
|
518
656
|
|
|
519
657
|
if (mode === "auto") {
|
|
520
|
-
assignments = autoMode(allModels);
|
|
521
|
-
ctx.ui.notify("⚡ Auto-assigned models based on public rankings and model specializations.", "info");
|
|
658
|
+
assignments = await autoMode(allModels, ctx);
|
|
522
659
|
} else if (mode === "benchmark") {
|
|
523
660
|
assignments = await benchmarkMode(allModels, ctx);
|
|
524
661
|
} else {
|
|
@@ -29,13 +29,15 @@ interface Skill {
|
|
|
29
29
|
content: string;
|
|
30
30
|
keywords: string[];
|
|
31
31
|
description: string;
|
|
32
|
-
source: "global" | "local";
|
|
32
|
+
source: "global" | "local" | "bundled";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export default function skillLoaderExtension(pi: ExtensionAPI) {
|
|
36
36
|
let availableSkills: Skill[] = [];
|
|
37
37
|
const globalSkillsDir = join(homedir(), ".phi", "agent", "skills");
|
|
38
38
|
const localSkillsDir = join(process.cwd(), ".phi", "skills");
|
|
39
|
+
// Bundled skills shipped with the package (packages/coding-agent/skills/)
|
|
40
|
+
const bundledSkillsDir = join(__dirname, "..", "..", "..", "skills");
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Extract keywords and description from SKILL.md content
|
|
@@ -129,7 +131,7 @@ export default function skillLoaderExtension(pi: ExtensionAPI) {
|
|
|
129
131
|
/**
|
|
130
132
|
* Load skills from a directory
|
|
131
133
|
*/
|
|
132
|
-
async function loadSkillsFromDirectory(directory: string, source: "global" | "local"): Promise<Skill[]> {
|
|
134
|
+
async function loadSkillsFromDirectory(directory: string, source: "global" | "local" | "bundled"): Promise<Skill[]> {
|
|
133
135
|
const skills: Skill[] = [];
|
|
134
136
|
|
|
135
137
|
try {
|
|
@@ -177,12 +179,23 @@ export default function skillLoaderExtension(pi: ExtensionAPI) {
|
|
|
177
179
|
* Load all available skills
|
|
178
180
|
*/
|
|
179
181
|
async function loadAllSkills(): Promise<void> {
|
|
182
|
+
const bundledSkills = await loadSkillsFromDirectory(bundledSkillsDir, "bundled");
|
|
180
183
|
const globalSkills = await loadSkillsFromDirectory(globalSkillsDir, "global");
|
|
181
184
|
const localSkills = await loadSkillsFromDirectory(localSkillsDir, "local");
|
|
182
185
|
|
|
183
|
-
|
|
186
|
+
// Merge: local > global > bundled (local overrides global overrides bundled)
|
|
187
|
+
const seen = new Set<string>();
|
|
188
|
+
availableSkills = [];
|
|
189
|
+
for (const skills of [localSkills, globalSkills, bundledSkills]) {
|
|
190
|
+
for (const skill of skills) {
|
|
191
|
+
if (!seen.has(skill.name)) {
|
|
192
|
+
seen.add(skill.name);
|
|
193
|
+
availableSkills.push(skill);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
184
197
|
|
|
185
|
-
console.log(`Loaded ${availableSkills.length} skills (${globalSkills.length} global, ${localSkills.length} local)`);
|
|
198
|
+
console.log(`Loaded ${availableSkills.length} skills (${bundledSkills.length} bundled, ${globalSkills.length} global, ${localSkills.length} local)`);
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
/**
|