@phi-code-admin/phi-code 0.76.11 → 0.76.12

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.
@@ -196,13 +196,22 @@ async function detectLocalProviders(providers: DetectedProvider[]): Promise<void
196
196
  );
197
197
  }
198
198
 
199
- function getAllAvailableModels(providers: DetectedProvider[]): string[] {
200
- const ids = new Set<string>();
199
+ function getAllAvailableModels(providers: DetectedProvider[]): Array<{ ref: string; display: string }> {
200
+ const out: Array<{ ref: string; display: string }> = [];
201
+ const seen = new Set<string>();
201
202
  for (const p of providers) {
202
203
  if (!p.available) continue;
203
- for (const id of p.models) ids.add(id);
204
+ for (const id of p.models) {
205
+ // Provider-qualified reference ("provider/id") so the same model id
206
+ // offered by several providers stays distinct, and the provider is
207
+ // visible at selection. No cross-provider dedup (only exact dupes).
208
+ const ref = `${p.id}/${id}`;
209
+ if (seen.has(ref)) continue;
210
+ seen.add(ref);
211
+ out.push({ ref, display: `${id} [${p.id}]` });
212
+ }
204
213
  }
205
- return [...ids];
214
+ return out;
206
215
  }
207
216
 
208
217
  // ─── Routing Presets ─────────────────────────────────────────────────────
@@ -387,25 +396,32 @@ _Edit this file to customize Phi Code's behavior for your project._
387
396
  // the user's `/model` choice on every routing decision.
388
397
 
389
398
  async function manualMode(
390
- availableModels: string[],
399
+ availableModels: Array<{ ref: string; display: string }>,
391
400
  ctx: any,
392
401
  ): Promise<Record<string, { preferred: string; fallback: string }>> {
393
402
  ctx.ui.notify(
394
403
  "Assign a model to each orchestration role.\n" +
395
404
  "These models are used by `/plan` and the orchestrator — NOT by normal chat.\n" +
396
- "The chat default model is controlled via `/model` (and stays sticky across prompts).\n",
405
+ "The chat default model is controlled via `/model` (and stays sticky across prompts).\n" +
406
+ "Each option shows its provider as `model-id [provider]`, so the same model from\n" +
407
+ "different providers stays distinct.\n",
397
408
  "info",
398
409
  );
399
- const modelOptions = ["default (use current chat model)", ...availableModels];
410
+ // Display strings carry the provider badge; map them back to the canonical
411
+ // "provider/id" reference that gets persisted into routing.json.
412
+ const DEFAULT_OPTION = "default (use current chat model)";
413
+ const refByDisplay = new Map<string, string>([[DEFAULT_OPTION, "default"]]);
414
+ for (const m of availableModels) refByDisplay.set(m.display, m.ref);
415
+ const modelOptions = [DEFAULT_OPTION, ...availableModels.map((m) => m.display)];
400
416
  const assignments: Record<string, { preferred: string; fallback: string }> = {};
401
417
 
402
418
  for (const role of TASK_ROLES) {
403
419
  const chosen = await ctx.ui.select(`${role.label} — ${role.desc}`, modelOptions);
404
- const preferredModel = chosen && chosen !== modelOptions[0] ? chosen : "default";
420
+ const preferredModel = refByDisplay.get(chosen) ?? "default";
405
421
 
406
422
  const fallbackOptions = modelOptions.filter((m) => m !== chosen);
407
423
  const fallbackChoice = await ctx.ui.select(`Fallback for ${role.label}`, fallbackOptions);
408
- const fallback = fallbackChoice && fallbackChoice !== modelOptions[0] ? fallbackChoice : "default";
424
+ const fallback = refByDisplay.get(fallbackChoice) ?? "default";
409
425
 
410
426
  assignments[role.key] = { preferred: preferredModel, fallback };
411
427
  ctx.ui.notify(` ${role.label}: ${preferredModel} (fallback: ${fallback})`, "info");
@@ -415,7 +431,7 @@ _Edit this file to customize Phi Code's behavior for your project._
415
431
  // This is NOT the chat default — `/model` controls that.
416
432
  assignments["default"] = {
417
433
  preferred: "default",
418
- fallback: availableModels[0] || "default",
434
+ fallback: availableModels[0]?.ref || "default",
419
435
  };
420
436
  return assignments;
421
437
  }
@@ -561,16 +561,33 @@ Tag the note with relevant keywords for vector search.
561
561
  ];
562
562
  }
563
563
 
564
+ /**
565
+ * Resolve a routing.json model reference to an available model.
566
+ * Accepts a provider-qualified "provider/id" reference (so the same model id
567
+ * offered by several providers can be disambiguated) and falls back to a bare
568
+ * "id" for legacy configs. Splits on the FIRST slash only, since some model
569
+ * ids themselves contain slashes (e.g. OpenRouter "anthropic/claude-...").
570
+ */
571
+ function resolveModelRef(available: any[], ref: string): any | undefined {
572
+ if (!ref) return undefined;
573
+ const slash = ref.indexOf("/");
574
+ if (slash > 0) {
575
+ const provider = ref.slice(0, slash);
576
+ const id = ref.slice(slash + 1);
577
+ const qualified = available.find((m: any) => m.provider === provider && m.id === id);
578
+ if (qualified) return qualified;
579
+ }
580
+ return available.find((m: any) => m.id === ref);
581
+ }
582
+
564
583
  /**
565
584
  * Switch model for the current phase.
566
585
  */
567
586
  async function switchModelForPhase(phase: OrchestratorPhase, ctx: any): Promise<string> {
568
587
  const available = ctx.modelRegistry?.getAvailable?.() || [];
569
- const preferred = available.find((m: any) => m.id === phase.model);
570
- const fallback = available.find((m: any) => m.id === phase.fallback);
571
- const target = preferred || fallback;
588
+ const target = resolveModelRef(available, phase.model) || resolveModelRef(available, phase.fallback);
572
589
 
573
- if (target && target.id !== ctx.model?.id) {
590
+ if (target && (target.id !== ctx.model?.id || target.provider !== ctx.model?.provider)) {
574
591
  const switched = await pi.setModel(target);
575
592
  if (switched) return target.id;
576
593
  }
@@ -516,23 +516,25 @@ async function configureLocal(
516
516
  async function pickModelFromCatalog(
517
517
  ui: ExtensionUIContext,
518
518
  prompt: string,
519
- allModelIds: string[],
519
+ models: Array<{ ref: string; display: string }>,
520
520
  ): Promise<string | undefined> {
521
- if (allModelIds.length === 0) {
521
+ if (models.length === 0) {
522
522
  ui.notify("No models available. Configure a provider first.", "warning");
523
523
  return undefined;
524
524
  }
525
- const options = ["(keep default)", ...allModelIds];
525
+ const KEEP = "(keep default)";
526
+ const options = [KEEP, ...models.map((m) => m.display)];
526
527
  const choice = await ui.select(prompt, options);
527
- if (!choice || choice === options[0]) return undefined;
528
- return choice;
528
+ if (!choice || choice === KEEP) return undefined;
529
+ // Map the displayed "id [provider]" back to the canonical "provider/id" ref.
530
+ return models.find((m) => m.display === choice)?.ref;
529
531
  }
530
532
 
531
533
  async function configureAssignments(
532
534
  ui: ExtensionUIContext,
533
- allModelIds: string[],
535
+ models: Array<{ ref: string; display: string }>,
534
536
  ): Promise<{ defaultModel: string; orchestration: Record<string, RouteAssignment> }> {
535
- if (allModelIds.length === 0) {
537
+ if (models.length === 0) {
536
538
  ui.notify("No models available for assignment. Configure a provider first.", "warning");
537
539
  return { defaultModel: "default", orchestration: {} };
538
540
  }
@@ -551,11 +553,11 @@ async function configureAssignments(
551
553
  const orchestration: Record<string, RouteAssignment> = {};
552
554
  for (const role of ORCHESTRATION_ROLES) {
553
555
  const preferred =
554
- (await pickModelFromCatalog(ui, `${role.label} - preferred model (${role.desc})`, allModelIds)) ??
555
- allModelIds[0];
556
- const fallbackOptions = allModelIds.filter((m) => m !== preferred);
557
- const fallback = fallbackOptions.length > 0
558
- ? (await pickModelFromCatalog(ui, `${role.label} - fallback model`, fallbackOptions)) ?? preferred
556
+ (await pickModelFromCatalog(ui, `${role.label} - preferred model (${role.desc})`, models)) ??
557
+ models[0].ref;
558
+ const fallbackModels = models.filter((m) => m.ref !== preferred);
559
+ const fallback = fallbackModels.length > 0
560
+ ? (await pickModelFromCatalog(ui, `${role.label} - fallback model`, fallbackModels)) ?? preferred
559
561
  : preferred;
560
562
  orchestration[role.key] = { preferred, fallback };
561
563
  ui.notify(` ${role.label}: ${preferred} / ${fallback}`, "info");
@@ -675,17 +677,23 @@ export default function setupExtension(pi: ExtensionAPI) {
675
677
  }
676
678
 
677
679
  if (action === "Assign models to agent roles") {
678
- const allModelIds: string[] = [];
680
+ const allModels: Array<{ ref: string; display: string }> = [];
681
+ const seenRefs = new Set<string>();
679
682
  for (const p of catalog) {
680
683
  const stored = store.getProvider(p.id);
681
684
  if (!stored) continue;
682
685
  const models = Array.isArray(stored.models) ? stored.models : [];
683
686
  for (const m of models) {
684
687
  const id = (m as { id?: string }).id;
685
- if (typeof id === "string" && !allModelIds.includes(id)) allModelIds.push(id);
688
+ if (typeof id !== "string") continue;
689
+ // Provider-qualified ref so the same id from different providers stays distinct.
690
+ const ref = `${p.id}/${id}`;
691
+ if (seenRefs.has(ref)) continue;
692
+ seenRefs.add(ref);
693
+ allModels.push({ ref, display: `${id} [${p.id}]` });
686
694
  }
687
695
  }
688
- const { defaultModel, orchestration } = await configureAssignments(ui, allModelIds);
696
+ const { defaultModel, orchestration } = await configureAssignments(ui, allModels);
689
697
  assignments.default = defaultModel;
690
698
  assignments.orchestration = orchestration;
691
699
  refreshAvailable();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.76.11",
3
+ "version": "0.76.12",
4
4
  "description": "Coding agent CLI with persistent memory, sub-agents, intelligent routing, and orchestration",
5
5
  "type": "module",
6
6
  "piConfig": {