@phi-code-admin/phi-code 0.57.5 → 0.57.7
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 +255 -19
- package/package.json +1 -1
package/extensions/phi/init.ts
CHANGED
|
@@ -23,6 +23,7 @@ interface DetectedProvider {
|
|
|
23
23
|
baseUrl: string;
|
|
24
24
|
models: string[];
|
|
25
25
|
available: boolean;
|
|
26
|
+
local?: boolean; // True for Ollama/LM Studio (models discovered at runtime)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
interface RoutingConfig {
|
|
@@ -82,15 +83,65 @@ function detectProviders(): DetectedProvider[] {
|
|
|
82
83
|
models: ["llama-3.3-70b-versatile", "mixtral-8x7b-32768"],
|
|
83
84
|
available: false,
|
|
84
85
|
},
|
|
86
|
+
{
|
|
87
|
+
name: "Ollama",
|
|
88
|
+
envVar: "OLLAMA",
|
|
89
|
+
baseUrl: "http://localhost:11434/v1",
|
|
90
|
+
models: [], // Discovered at runtime
|
|
91
|
+
available: false,
|
|
92
|
+
local: true,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "LM Studio",
|
|
96
|
+
envVar: "LM_STUDIO",
|
|
97
|
+
baseUrl: "http://localhost:1234/v1",
|
|
98
|
+
models: [], // Discovered at runtime
|
|
99
|
+
available: false,
|
|
100
|
+
local: true,
|
|
101
|
+
},
|
|
85
102
|
];
|
|
86
103
|
|
|
87
104
|
for (const p of providers) {
|
|
88
|
-
p.
|
|
105
|
+
if (p.local) {
|
|
106
|
+
// Local providers: check if server is running by probing the URL
|
|
107
|
+
p.available = false; // Will be checked async in detectLocalProviders()
|
|
108
|
+
} else {
|
|
109
|
+
p.available = !!process.env[p.envVar];
|
|
110
|
+
}
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
return providers;
|
|
92
114
|
}
|
|
93
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Detect local providers (Ollama, LM Studio) by probing their endpoints
|
|
118
|
+
* and fetching available models dynamically.
|
|
119
|
+
*/
|
|
120
|
+
async function detectLocalProviders(providers: DetectedProvider[]): Promise<void> {
|
|
121
|
+
for (const p of providers) {
|
|
122
|
+
if (!p.local) continue;
|
|
123
|
+
try {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
126
|
+
const res = await fetch(`${p.baseUrl}/models`, {
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
headers: { Authorization: `Bearer ${p.envVar === "OLLAMA" ? "ollama" : "lm-studio"}` },
|
|
129
|
+
});
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
if (res.ok) {
|
|
132
|
+
const data = await res.json() as any;
|
|
133
|
+
const models = (data.data || []).map((m: any) => m.id).filter(Boolean);
|
|
134
|
+
if (models.length > 0) {
|
|
135
|
+
p.models = models;
|
|
136
|
+
p.available = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Server not running — that's fine
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
94
145
|
function getAllAvailableModels(providers: DetectedProvider[]): string[] {
|
|
95
146
|
return providers.filter(p => p.available).flatMap(p => p.models);
|
|
96
147
|
}
|
|
@@ -210,18 +261,193 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
210
261
|
|
|
211
262
|
// ─── MODE: Auto ──────────────────────────────────────────────────
|
|
212
263
|
|
|
213
|
-
|
|
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
|
+
|
|
277
|
+
/**
|
|
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.
|
|
372
|
+
*
|
|
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!)
|
|
379
|
+
*/
|
|
380
|
+
async function autoMode(availableModels: string[], ctx?: any): Promise<Record<string, { preferred: string; fallback: string }>> {
|
|
214
381
|
const assignments: Record<string, { preferred: string; fallback: string }> = {};
|
|
215
382
|
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
assignments[
|
|
383
|
+
if (availableModels.length === 0) {
|
|
384
|
+
const fb = { preferred: "qwen3.5-plus", fallback: "qwen3.5-plus" };
|
|
385
|
+
for (const role of TASK_ROLES) assignments[role.key] = fb;
|
|
386
|
+
assignments["default"] = fb;
|
|
387
|
+
return assignments;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (availableModels.length === 1) {
|
|
391
|
+
const single = { preferred: availableModels[0], fallback: availableModels[0] };
|
|
392
|
+
for (const role of TASK_ROLES) assignments[role.key] = single;
|
|
393
|
+
assignments["default"] = single;
|
|
394
|
+
return assignments;
|
|
395
|
+
}
|
|
396
|
+
|
|
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
|
+
}
|
|
220
450
|
}
|
|
221
|
-
assignments["default"] = {
|
|
222
|
-
preferred: availableModels.includes("qwen3.5-plus") ? "qwen3.5-plus" : availableModels[0],
|
|
223
|
-
fallback: availableModels[0],
|
|
224
|
-
};
|
|
225
451
|
|
|
226
452
|
return assignments;
|
|
227
453
|
}
|
|
@@ -259,7 +485,7 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
259
485
|
ctx.ui.notify("Run: `/benchmark all` then `/phi-init` again with mode=benchmark to use results.\n", "info");
|
|
260
486
|
|
|
261
487
|
// Fall back to auto for now
|
|
262
|
-
return autoMode(availableModels);
|
|
488
|
+
return autoMode(availableModels, ctx);
|
|
263
489
|
}
|
|
264
490
|
|
|
265
491
|
function assignFromBenchmark(results: any[], availableModels: string[]): Record<string, { preferred: string; fallback: string }> {
|
|
@@ -376,21 +602,32 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
376
602
|
ctx.ui.notify("║ Φ Phi Code Setup Wizard ║", "info");
|
|
377
603
|
ctx.ui.notify("╚══════════════════════════════════════╝\n", "info");
|
|
378
604
|
|
|
379
|
-
// 1. Detect API keys
|
|
380
|
-
ctx.ui.notify("🔍 Detecting
|
|
605
|
+
// 1. Detect API keys and local providers
|
|
606
|
+
ctx.ui.notify("🔍 Detecting providers...", "info");
|
|
381
607
|
const providers = detectProviders();
|
|
608
|
+
|
|
609
|
+
// Probe local providers (Ollama, LM Studio)
|
|
610
|
+
ctx.ui.notify("🔍 Probing local model servers...", "info");
|
|
611
|
+
await detectLocalProviders(providers);
|
|
612
|
+
|
|
382
613
|
const available = providers.filter(p => p.available);
|
|
383
614
|
|
|
384
615
|
if (available.length === 0) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
"
|
|
616
|
+
const cloudProviders = providers.filter(p => !p.local);
|
|
617
|
+
ctx.ui.notify("❌ No providers found. Options:\n\n" +
|
|
618
|
+
"**Cloud providers** (set API key):\n" +
|
|
619
|
+
cloudProviders.map(p => ` export ${p.envVar}="your-key" # ${p.name}`).join("\n") +
|
|
620
|
+
"\n\n**Local providers** (start the server):\n" +
|
|
621
|
+
" • Ollama: `ollama serve` (default port 11434)\n" +
|
|
622
|
+
" • LM Studio: Start server in app (default port 1234)\n" +
|
|
623
|
+
"\n💡 Free options: Alibaba Coding Plan (cloud, $0) or Ollama (local, free)", "error");
|
|
388
624
|
return;
|
|
389
625
|
}
|
|
390
626
|
|
|
391
627
|
ctx.ui.notify(`✅ Found ${available.length} provider(s):`, "info");
|
|
392
628
|
for (const p of available) {
|
|
393
|
-
|
|
629
|
+
const tag = p.local ? " (local)" : "";
|
|
630
|
+
ctx.ui.notify(` • ${p.name}${tag} — ${p.models.length} model(s)${p.local ? ": " + p.models.join(", ") : ""}`, "info");
|
|
394
631
|
}
|
|
395
632
|
|
|
396
633
|
const allModels = getAllAvailableModels(providers);
|
|
@@ -418,8 +655,7 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
418
655
|
let assignments: Record<string, { preferred: string; fallback: string }>;
|
|
419
656
|
|
|
420
657
|
if (mode === "auto") {
|
|
421
|
-
assignments = autoMode(allModels);
|
|
422
|
-
ctx.ui.notify("⚡ Auto-assigned models based on public rankings and model specializations.", "info");
|
|
658
|
+
assignments = await autoMode(allModels, ctx);
|
|
423
659
|
} else if (mode === "benchmark") {
|
|
424
660
|
assignments = await benchmarkMode(allModels, ctx);
|
|
425
661
|
} else {
|