@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.
@@ -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
- * Auto-assign models based on specialization heuristics.
266
- * Works with ANY provider not just Alibaba.
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: match model names against known specialization patterns.
269
- * - "coder" in name code tasks
270
- * - "mini"/"flash"/"fast"/"small" in name explore/test (fast tasks)
271
- * - Largest/best model plan/debug/review (reasoning tasks)
272
- * - If only 1 model available everything uses that model (still works!)
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
- // Categorize models by name heuristics
294
- const lower = availableModels.map(m => ({ id: m, l: m.toLowerCase() }));
295
- const coderModels = lower.filter(m => /coder|code|codestral/.test(m.l)).map(m => m.id);
296
- const fastModels = lower.filter(m => /mini|flash|fast|small|haiku|lite/.test(m.l)).map(m => m.id);
297
- const reasoningModels = lower.filter(m => /max|pro|plus|opus|large|o1|o3/.test(m.l)).map(m => m.id);
298
-
299
- // Pick best for each category, with fallback to first available
300
- const bestCoder = coderModels[0] || reasoningModels[0] || availableModels[0];
301
- const bestReasoning = reasoningModels[0] || availableModels[0];
302
- const bestFast = fastModels[0] || availableModels[availableModels.length - 1] || availableModels[0];
303
- const generalModel = reasoningModels[0] || availableModels[0];
304
- const fallbackModel = availableModels.find(m => m !== generalModel) || generalModel;
305
-
306
- assignments["code"] = { preferred: bestCoder, fallback: generalModel };
307
- assignments["debug"] = { preferred: bestReasoning, fallback: fallbackModel };
308
- assignments["plan"] = { preferred: bestReasoning, fallback: fallbackModel };
309
- assignments["explore"] = { preferred: bestFast, fallback: generalModel };
310
- assignments["test"] = { preferred: bestFast, fallback: generalModel };
311
- assignments["review"] = { preferred: generalModel, fallback: fallbackModel };
312
- assignments["default"] = { preferred: generalModel, fallback: fallbackModel };
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
- availableSkills = [...globalSkills, ...localSkills];
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.57.6",
3
+ "version": "0.57.8",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {