@phi-code-admin/phi-code 0.75.4 → 0.75.6
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/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +3 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/extensions/phi/README.md +21 -1
- package/extensions/phi/init.ts +388 -376
- package/extensions/phi/models.ts +236 -0
- package/extensions/phi/providers/live-models.ts +493 -0
- package/extensions/phi/providers/opencode-go.ts +15 -10
- package/extensions/phi/setup.ts +102 -40
- package/extensions/phi/smart-router.ts +63 -19
- package/package.json +1 -1
package/extensions/phi/setup.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
pingOpenCodeGo,
|
|
41
41
|
validateOpenCodeGoApiKey,
|
|
42
42
|
} from "./providers/opencode-go.js";
|
|
43
|
+
import { fetchLiveModels, pingProvider, toPersistedModel } from "./providers/live-models.js";
|
|
43
44
|
|
|
44
45
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
45
46
|
|
|
@@ -176,22 +177,9 @@ function getProviderCatalog(): ProviderEntry[] {
|
|
|
176
177
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
177
178
|
|
|
178
179
|
async function probeLocalProvider(provider: ProviderEntry): Promise<string[]> {
|
|
179
|
-
if (!provider.
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const res = await fetch(provider.probeUrl, {
|
|
184
|
-
signal: controller.signal,
|
|
185
|
-
headers: { Authorization: `Bearer ${provider.id === "ollama" ? "ollama" : "lm-studio"}` },
|
|
186
|
-
});
|
|
187
|
-
clearTimeout(timeout);
|
|
188
|
-
if (!res.ok) return [];
|
|
189
|
-
const data = (await res.json()) as { data?: Array<{ id?: string; name?: string }> };
|
|
190
|
-
return (data.data ?? []).map((m) => m.id ?? m.name ?? "").filter(Boolean);
|
|
191
|
-
} catch {
|
|
192
|
-
clearTimeout(timeout);
|
|
193
|
-
return [];
|
|
194
|
-
}
|
|
180
|
+
if (!provider.local) return [];
|
|
181
|
+
const result = await fetchLiveModels(provider.id, { forceRefresh: true, timeoutMs: 2_500 });
|
|
182
|
+
return result.source === "live" ? result.models.map((m) => m.id) : [];
|
|
195
183
|
}
|
|
196
184
|
|
|
197
185
|
function buildStatusWidget(
|
|
@@ -211,8 +199,7 @@ function buildStatusWidget(
|
|
|
211
199
|
lines.push(` ${icon} ${p.displayName}${note}`);
|
|
212
200
|
}
|
|
213
201
|
lines.push("");
|
|
214
|
-
lines.push("
|
|
215
|
-
lines.push(` Default chat : ${assignments.default ?? "(not set)"}`);
|
|
202
|
+
lines.push("Orchestration roles (used by /plan — NOT chat):");
|
|
216
203
|
for (const role of ORCHESTRATION_ROLES) {
|
|
217
204
|
const a = assignments.orchestration[role.key];
|
|
218
205
|
const preferred = a?.preferred ?? "(not set)";
|
|
@@ -220,6 +207,7 @@ function buildStatusWidget(
|
|
|
220
207
|
lines.push(` ${role.label.padEnd(8)} : ${preferred} / ${fallback}`);
|
|
221
208
|
}
|
|
222
209
|
lines.push("");
|
|
210
|
+
lines.push("Chat default model : controlled via `/model` (this wizard never overrides it)");
|
|
223
211
|
lines.push(`Keys file : ${store.configPath} (chmod 0600 on Unix)`);
|
|
224
212
|
return lines;
|
|
225
213
|
}
|
|
@@ -442,21 +430,53 @@ async function configureGenericCloud(
|
|
|
442
430
|
if (!proceed) return undefined;
|
|
443
431
|
}
|
|
444
432
|
|
|
433
|
+
// Persist the key immediately so a downstream fetch failure cannot lose user input.
|
|
445
434
|
store.setKey(provider.id, trimmed, {
|
|
446
435
|
baseUrl: provider.baseUrl,
|
|
447
436
|
api: provider.api,
|
|
448
|
-
models: provider.staticModels.map((id) => ({
|
|
449
|
-
id,
|
|
450
|
-
name: id,
|
|
451
|
-
reasoning: true,
|
|
452
|
-
input: ["text"] as const,
|
|
453
|
-
})),
|
|
454
437
|
});
|
|
438
|
+
|
|
439
|
+
// Optional ping for early auth diagnostics.
|
|
440
|
+
ui.setStatus("setup-ping", `Pinging ${provider.displayName}...`);
|
|
441
|
+
const ping = await pingProvider(provider.id, trimmed, 5_000).catch((err) => ({
|
|
442
|
+
ok: false,
|
|
443
|
+
error: err instanceof Error ? err.message : String(err),
|
|
444
|
+
}));
|
|
445
|
+
ui.setStatus("setup-ping", undefined);
|
|
446
|
+
if (ping.ok) {
|
|
447
|
+
ui.notify(`${provider.displayName} ping OK (200).`, "info");
|
|
448
|
+
} else {
|
|
449
|
+
ui.notify(
|
|
450
|
+
`${provider.displayName} ping failed: ${ping.error ?? "unknown"}. Key saved; you can retry with \`/keys test ${provider.id}\`.`,
|
|
451
|
+
"warning",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Live-fetch the model catalog (falls back to the static list when offline).
|
|
456
|
+
ui.setStatus("setup-fetch", `Fetching ${provider.displayName} model list...`);
|
|
457
|
+
const live = await fetchLiveModels(provider.id, {
|
|
458
|
+
apiKey: trimmed,
|
|
459
|
+
forceRefresh: true,
|
|
460
|
+
timeoutMs: 6_000,
|
|
461
|
+
});
|
|
462
|
+
ui.setStatus("setup-fetch", undefined);
|
|
463
|
+
|
|
464
|
+
const models = (live.models.length > 0
|
|
465
|
+
? live.models
|
|
466
|
+
: provider.staticModels.map((id) => ({ id, name: id, reasoning: true }))
|
|
467
|
+
).map(toPersistedModel);
|
|
468
|
+
|
|
469
|
+
store.setKey(provider.id, trimmed, {
|
|
470
|
+
baseUrl: provider.baseUrl,
|
|
471
|
+
api: provider.api,
|
|
472
|
+
models,
|
|
473
|
+
});
|
|
474
|
+
|
|
455
475
|
ui.notify(
|
|
456
|
-
`${provider.displayName} configured: \`${maskKeyForDisplay(trimmed)}\` (${
|
|
476
|
+
`${provider.displayName} configured: \`${maskKeyForDisplay(trimmed)}\` (${models.length} models, source: ${live.source}${live.error ? `, ${live.error}` : ""})`,
|
|
457
477
|
"info",
|
|
458
478
|
);
|
|
459
|
-
return { providerId: provider.id, modelCount:
|
|
479
|
+
return { providerId: provider.id, modelCount: models.length };
|
|
460
480
|
}
|
|
461
481
|
|
|
462
482
|
async function configureLocal(
|
|
@@ -517,15 +537,22 @@ async function configureAssignments(
|
|
|
517
537
|
return { defaultModel: "default", orchestration: {} };
|
|
518
538
|
}
|
|
519
539
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
540
|
+
ui.notify(
|
|
541
|
+
"Assigning orchestration role models. The chat default is controlled via `/model` — " +
|
|
542
|
+
"this wizard does NOT change it.",
|
|
543
|
+
"info",
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// Sentinel: the orchestrator falls back to the current chat model when a
|
|
547
|
+
// route doesn't pin a specific one. We never ask the user for a "default"
|
|
548
|
+
// chat model here — `/model` owns that.
|
|
549
|
+
const defaultModel = "default";
|
|
523
550
|
|
|
524
551
|
const orchestration: Record<string, RouteAssignment> = {};
|
|
525
552
|
for (const role of ORCHESTRATION_ROLES) {
|
|
526
553
|
const preferred =
|
|
527
554
|
(await pickModelFromCatalog(ui, `${role.label} - preferred model (${role.desc})`, allModelIds)) ??
|
|
528
|
-
|
|
555
|
+
allModelIds[0];
|
|
529
556
|
const fallbackOptions = allModelIds.filter((m) => m !== preferred);
|
|
530
557
|
const fallback = fallbackOptions.length > 0
|
|
531
558
|
? (await pickModelFromCatalog(ui, `${role.label} - fallback model`, fallbackOptions)) ?? preferred
|
|
@@ -539,7 +566,23 @@ async function configureAssignments(
|
|
|
539
566
|
|
|
540
567
|
// ─── Extension ───────────────────────────────────────────────────────────
|
|
541
568
|
|
|
569
|
+
// One-time global guard so a stray async rejection inside the wizard never kills the TUI.
|
|
570
|
+
let setupUnhandledGuard = false;
|
|
571
|
+
function installSetupUnhandledRejectionGuard(): void {
|
|
572
|
+
if (setupUnhandledGuard) return;
|
|
573
|
+
setupUnhandledGuard = true;
|
|
574
|
+
process.on("unhandledRejection", (reason) => {
|
|
575
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
576
|
+
try {
|
|
577
|
+
process.stderr.write(`[phi-setup] swallowed unhandledRejection: ${message}\n`);
|
|
578
|
+
} catch {
|
|
579
|
+
// no-op
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
542
584
|
export default function setupExtension(pi: ExtensionAPI) {
|
|
585
|
+
installSetupUnhandledRejectionGuard();
|
|
543
586
|
pi.registerCommand("setup", {
|
|
544
587
|
description: "Phi Code setup wizard (refonte UX, replaces /phi-init)",
|
|
545
588
|
handler: async (_args, ctx) => {
|
|
@@ -551,10 +594,14 @@ export default function setupExtension(pi: ExtensionAPI) {
|
|
|
551
594
|
// empty file or missing, fine
|
|
552
595
|
}
|
|
553
596
|
|
|
597
|
+
try {
|
|
554
598
|
ui.notify(
|
|
555
599
|
"**φ Phi Code Setup Wizard**\n\n" +
|
|
556
|
-
"This wizard configures providers and assigns models to
|
|
557
|
-
"
|
|
600
|
+
"This wizard configures providers and assigns models to **orchestration roles** " +
|
|
601
|
+
"(used by `/plan`).\n" +
|
|
602
|
+
"The **chat default model is controlled via `/model`** and stays sticky across " +
|
|
603
|
+
"prompts — this wizard will never change it.\n\n" +
|
|
604
|
+
"Keys are stored in `~/.phi/agent/models.json` (chmod 0600 on Unix). " +
|
|
558
605
|
"Edit that file directly later to hot-reload (no restart needed).",
|
|
559
606
|
"info",
|
|
560
607
|
);
|
|
@@ -652,14 +699,21 @@ export default function setupExtension(pi: ExtensionAPI) {
|
|
|
652
699
|
const provider = catalog[providerIndex];
|
|
653
700
|
|
|
654
701
|
let result: { providerId: string; modelCount: number } | undefined;
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
702
|
+
try {
|
|
703
|
+
if (provider.id === "alibaba-codingplan") {
|
|
704
|
+
result = await configureAlibaba(ui, store);
|
|
705
|
+
} else if (provider.id === "opencode-go") {
|
|
706
|
+
result = await configureOpenCodeGo(ui, store);
|
|
707
|
+
} else if (provider.local) {
|
|
708
|
+
result = await configureLocal(ui, store, provider);
|
|
709
|
+
} else {
|
|
710
|
+
result = await configureGenericCloud(ui, store, provider);
|
|
711
|
+
}
|
|
712
|
+
} catch (err) {
|
|
713
|
+
ui.notify(
|
|
714
|
+
`Provider configuration failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
715
|
+
"error",
|
|
716
|
+
);
|
|
663
717
|
}
|
|
664
718
|
|
|
665
719
|
if (result) refreshAvailable();
|
|
@@ -681,12 +735,20 @@ export default function setupExtension(pi: ExtensionAPI) {
|
|
|
681
735
|
"**Setup complete.**\n\n" +
|
|
682
736
|
"Next steps:\n" +
|
|
683
737
|
" - `/keys` to list/manage saved keys\n" +
|
|
738
|
+
" - `/models refresh` to re-fetch the catalog from each provider's API\n" +
|
|
684
739
|
" - `/routing` to inspect routing\n" +
|
|
685
740
|
" - `/agents` to list sub-agents\n" +
|
|
686
741
|
" - `/skills` to list skills\n" +
|
|
687
742
|
" - Edit `~/.phi/agent/models.json` or `routing.json` directly: hot-reload kicks in",
|
|
688
743
|
"info",
|
|
689
744
|
);
|
|
745
|
+
} catch (err) {
|
|
746
|
+
ui.setWidget("setup-status", undefined);
|
|
747
|
+
ui.notify(
|
|
748
|
+
`Setup wizard error: ${err instanceof Error ? err.message : String(err)}`,
|
|
749
|
+
"error",
|
|
750
|
+
);
|
|
751
|
+
}
|
|
690
752
|
},
|
|
691
753
|
});
|
|
692
754
|
}
|
|
@@ -19,6 +19,17 @@ import { homedir } from "node:os";
|
|
|
19
19
|
|
|
20
20
|
interface ExtensionConfig {
|
|
21
21
|
enabled: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* When false (default), the smart router never changes the chat model — it
|
|
24
|
+
* only emits an informational notification per prompt. Use `/model` to
|
|
25
|
+
* choose the chat model; that choice is now the single source of truth
|
|
26
|
+
* and is persisted across sessions via the settings manager.
|
|
27
|
+
*
|
|
28
|
+
* When true (opt-in via `/routing autoswitch on`), the router switches
|
|
29
|
+
* the chat model to the preferred route per prompt, matching the
|
|
30
|
+
* pre-0.75.6 behavior.
|
|
31
|
+
*/
|
|
32
|
+
autoSwitch: boolean;
|
|
22
33
|
notifyOnRecommendation: boolean;
|
|
23
34
|
}
|
|
24
35
|
|
|
@@ -31,7 +42,8 @@ export default function smartRouterExtension(pi: ExtensionAPI) {
|
|
|
31
42
|
let router = new SmartRouter(SmartRouter.defaultConfig());
|
|
32
43
|
let extConfig: ExtensionConfig = {
|
|
33
44
|
enabled: true,
|
|
34
|
-
|
|
45
|
+
autoSwitch: false,
|
|
46
|
+
notifyOnRecommendation: false,
|
|
35
47
|
};
|
|
36
48
|
|
|
37
49
|
/**
|
|
@@ -95,20 +107,30 @@ export default function smartRouterExtension(pi: ExtensionAPI) {
|
|
|
95
107
|
|
|
96
108
|
const modelToUse = targetModel || fallbackModel;
|
|
97
109
|
|
|
98
|
-
if (modelToUse
|
|
99
|
-
|
|
110
|
+
if (!modelToUse) {
|
|
111
|
+
if (extConfig.notifyOnRecommendation) {
|
|
112
|
+
ctx.ui.notify(
|
|
113
|
+
`Routing suggestion: ${recommendation.category} → \`${recommendation.model}\` (not in registry).`,
|
|
114
|
+
"info",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
} else if (modelToUse.id === ctx.model?.id) {
|
|
118
|
+
// Already on the recommended model — nothing to do.
|
|
119
|
+
} else if (extConfig.autoSwitch) {
|
|
120
|
+
// Legacy opt-in behavior: actually swap the chat model.
|
|
100
121
|
const switched = await pi.setModel(modelToUse);
|
|
101
122
|
if (switched && extConfig.notifyOnRecommendation) {
|
|
102
123
|
ctx.ui.notify(
|
|
103
|
-
|
|
104
|
-
"info"
|
|
124
|
+
`Auto-switched (${recommendation.category}) → \`${modelToUse.id}\`${modelToUse.id !== recommendation.model ? " (fallback)" : ""}`,
|
|
125
|
+
"info",
|
|
105
126
|
);
|
|
106
127
|
}
|
|
107
|
-
} else if (extConfig.notifyOnRecommendation
|
|
108
|
-
//
|
|
128
|
+
} else if (extConfig.notifyOnRecommendation) {
|
|
129
|
+
// Default behavior: never override the user's `/model` choice. Just
|
|
130
|
+
// advertise the suggestion — the chat model stays sticky.
|
|
109
131
|
ctx.ui.notify(
|
|
110
|
-
|
|
111
|
-
"info"
|
|
132
|
+
`Routing suggestion: ${recommendation.category} → \`${modelToUse.id}\`. Stay on \`${ctx.model?.id}\` (use \`/routing autoswitch on\` to auto-apply, or \`/model\` to change manually).`,
|
|
133
|
+
"info",
|
|
112
134
|
);
|
|
113
135
|
}
|
|
114
136
|
}
|
|
@@ -125,44 +147,66 @@ export default function smartRouterExtension(pi: ExtensionAPI) {
|
|
|
125
147
|
|
|
126
148
|
if (!arg) {
|
|
127
149
|
const routingConfig = (router as any).config as RoutingConfig;
|
|
128
|
-
let output =
|
|
129
|
-
output += `Status:
|
|
130
|
-
output += `
|
|
150
|
+
let output = `**Smart Router** (powered by sigma-agents)\n\n`;
|
|
151
|
+
output += `Status: ${extConfig.enabled ? "enabled" : "disabled"}\n`;
|
|
152
|
+
output += `Auto-switch: ${extConfig.autoSwitch ? "on (overrides /model per prompt)" : "off (suggestions only — /model is sticky)"}\n`;
|
|
153
|
+
output += `Notifications: ${extConfig.notifyOnRecommendation ? "on" : "off"}\n\n`;
|
|
131
154
|
|
|
132
155
|
output += `**Routes:**\n`;
|
|
133
156
|
for (const [cat, route] of Object.entries(routingConfig.routes)) {
|
|
134
157
|
output += ` **${cat}** → \`${route.preferredModel}\` (fallback: \`${route.fallback}\`) [agent: ${route.agent || "none"}]\n`;
|
|
135
158
|
output += ` Keywords: ${route.keywords.slice(0, 6).join(", ")}${route.keywords.length > 6 ? "..." : ""}\n`;
|
|
136
159
|
}
|
|
137
|
-
output += `\n **default** → \`${routingConfig.default.model}
|
|
160
|
+
output += `\n **default** → \`${routingConfig.default.model}\` (orchestrator fallback only; chat default comes from \`/model\`)\n`;
|
|
138
161
|
|
|
139
162
|
output += `\nConfig: \`${configPath}\``;
|
|
140
|
-
output += `\nCommands: \`/routing enable|disable|notify-on|notify-off|reload|test\``;
|
|
163
|
+
output += `\nCommands: \`/routing [enable|disable|autoswitch on|autoswitch off|notify-on|notify-off|reload|test]\``;
|
|
141
164
|
|
|
142
165
|
ctx.ui.notify(output, "info");
|
|
143
166
|
return;
|
|
144
167
|
}
|
|
145
168
|
|
|
169
|
+
const tokens = arg.split(/\s+/);
|
|
170
|
+
const head = tokens[0];
|
|
171
|
+
|
|
172
|
+
if (head === "autoswitch") {
|
|
173
|
+
const flag = tokens[1];
|
|
174
|
+
if (flag === "on") {
|
|
175
|
+
extConfig.autoSwitch = true;
|
|
176
|
+
ctx.ui.notify(
|
|
177
|
+
"Auto-switch enabled: the smart router will override the chat model per prompt. " +
|
|
178
|
+
"Disable with `/routing autoswitch off` if it gets in your way.",
|
|
179
|
+
"info",
|
|
180
|
+
);
|
|
181
|
+
} else if (flag === "off") {
|
|
182
|
+
extConfig.autoSwitch = false;
|
|
183
|
+
ctx.ui.notify("Auto-switch disabled. The model selected via `/model` is now sticky.", "info");
|
|
184
|
+
} else {
|
|
185
|
+
ctx.ui.notify("Usage: `/routing autoswitch on|off`", "warning");
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
146
190
|
switch (arg) {
|
|
147
191
|
case "enable":
|
|
148
192
|
extConfig.enabled = true;
|
|
149
|
-
ctx.ui.notify("
|
|
193
|
+
ctx.ui.notify("Smart routing enabled.", "info");
|
|
150
194
|
break;
|
|
151
195
|
case "disable":
|
|
152
196
|
extConfig.enabled = false;
|
|
153
|
-
ctx.ui.notify("
|
|
197
|
+
ctx.ui.notify("Smart routing disabled.", "info");
|
|
154
198
|
break;
|
|
155
199
|
case "notify-on":
|
|
156
200
|
extConfig.notifyOnRecommendation = true;
|
|
157
|
-
ctx.ui.notify("
|
|
201
|
+
ctx.ui.notify("Routing notifications enabled.", "info");
|
|
158
202
|
break;
|
|
159
203
|
case "notify-off":
|
|
160
204
|
extConfig.notifyOnRecommendation = false;
|
|
161
|
-
ctx.ui.notify("
|
|
205
|
+
ctx.ui.notify("Routing notifications disabled.", "info");
|
|
162
206
|
break;
|
|
163
207
|
case "reload":
|
|
164
208
|
await loadConfig();
|
|
165
|
-
ctx.ui.notify("
|
|
209
|
+
ctx.ui.notify("Routing config reloaded from disk.", "info");
|
|
166
210
|
break;
|
|
167
211
|
case "test": {
|
|
168
212
|
const tests = [
|