@pi-unipi/unipi 2.17.0 → 2.17.1

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/CHANGELOG.md CHANGED
@@ -6,7 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [2.17.0] — 2026-09-15
9
+ ## [2.17.1] — 2026-09-15
10
+
11
+ ### Fixed
12
+
13
+ - `fusion`: **the persisted Fusion pair is restored at startup.** `session_start` now re-applies the saved lead via `pi.setModel()` and restores its thinking level when pi boots on the sidekick model (previously the pair silently dropped to a single-model status); if the lead is unavailable it disables Fusion and warns instead of leaving a half-active pair.
14
+ - `fusion`: **leaving Fusion via `/model` persists.** Selecting a non-lead model now writes the single-model selection to the preset's `active`, so the session stays un-Fused across restarts instead of snapping back to the pair.
15
+ - `fusion`: **manual pricing overrides for un-priced models.** A new `prices` preset key (`{ "provider/id": { "input", "cachedInput", "output" } }`) lets you supply per-million prices for providers that report no pricing; savings estimation and the picker both consume the override. Zero/unavailable pricing now renders no slider marker and shows inline `no pricing data from provider` guidance instead of a misleading $0.
16
+ - `fusion`: **price-panel columns no longer run together.** Column width now reserves space for the longest header/value (e.g. `Sidekick cached input`) instead of a fixed cap, fixing truncated sidekick price headers.
10
17
 
11
18
  ### Added
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.17.0",
3
+ "version": "2.17.1",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -92,7 +92,7 @@
92
92
  "@pi-unipi/core": "2.17.0",
93
93
  "@pi-unipi/footer": "2.17.0",
94
94
  "@pi-unipi/image": "2.17.0",
95
- "@pi-unipi/fusion": "2.17.0",
95
+ "@pi-unipi/fusion": "2.17.1",
96
96
  "@pi-unipi/info-screen": "2.17.0",
97
97
  "@pi-unipi/input-shortcuts": "2.17.0",
98
98
  "@pi-unipi/kanboard": "2.17.0",
@@ -117,11 +117,14 @@ savings above `$0.005`.
117
117
  "default": {"lead": "provider/lead", "sidekick": "provider/sidekick"},
118
118
  "effort": {"provider/sidekick": "high"},
119
119
  "badges": {"provider/sidekick": "new"},
120
+ "prices": {"provider/sidekick": {"input": 0.2, "cachedInput": 0.02, "output": 1.2}},
120
121
  "recent": ["provider/lead"],
121
122
  "active": {"kind": "fusion", "lead": "provider/lead", "sidekick": "provider/sidekick"}
122
123
  }
123
124
  ```
124
125
 
126
+ `prices` is an optional manual override for models whose provider reports no pricing.
127
+
125
128
  ## Status
126
129
 
127
130
  - [x] Preset store, project layering, curation UI, autocomplete boost
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/fusion",
3
- "version": "2.17.0",
3
+ "version": "2.17.1",
4
4
  "description": "Devin-style model picker, fusion presets (lead + sidekick), and Local Fusion runtime for UniPi",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -21,6 +21,7 @@ import { homedir } from "node:os";
21
21
  import { join } from "node:path";
22
22
  import {
23
23
  effortLabel,
24
+ globalPresetPath,
24
25
  isEffortLevel,
25
26
  loadPreset,
26
27
  modelKey,
@@ -60,20 +61,18 @@ function findModel(reg: Registry | undefined, key: string): Model<Api> | undefin
60
61
  return modelBykey.get(key);
61
62
  }
62
63
 
63
- function costOf(m: Model<Api> | undefined): PickerModel["cost"] {
64
- const cost = m?.cost;
65
- return cost && typeof cost.input === "number"
66
- ? { input: cost.input, cachedInput: cost.cacheRead ?? 0, output: cost.output }
67
- : undefined;
64
+ function costOf(m: Model<Api> | undefined, override?: PickerModel["cost"]): PickerModel["cost"] {
65
+ const cost = override ?? (m?.cost && typeof m.cost.input === "number" ? { input: m.cost.input, cachedInput: m.cost.cacheRead ?? 0, output: m.cost.output } : undefined);
66
+ return cost && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0) ? cost : undefined;
68
67
  }
69
68
 
70
- function toPickerModel(m: Model<Api>, badge?: FusionPreset["badges"][string]): PickerModel {
69
+ function toPickerModel(m: Model<Api>, badge?: FusionPreset["badges"][string], override?: PickerModel["cost"]): PickerModel {
71
70
  return {
72
71
  key: modelKey(m),
73
72
  name: m.name || m.id,
74
73
  provider: m.provider,
75
74
  badge,
76
- cost: costOf(m),
75
+ cost: costOf(m, override),
77
76
  reasoning: Boolean(m.reasoning),
78
77
  };
79
78
  }
@@ -130,9 +129,10 @@ export default function fusionExtension(pi: ExtensionAPI): void {
130
129
  function statusSavings(ctx: ExtensionContext): number | undefined {
131
130
  if (active?.kind !== "fusion" || runtime === undefined) return undefined;
132
131
  const reg = registryOf(ctx);
132
+ const preset = loadPreset(ctx.cwd ?? process.cwd()).preset;
133
133
  const lead = findModel(reg, active.lead);
134
134
  const side = findModel(reg, active.sidekick);
135
- return estimateSavings(runtime.usage, costOf(lead), costOf(side)).savedUsd;
135
+ return estimateSavings(runtime.usage, costOf(lead, preset.prices[active.lead]), costOf(side, preset.prices[active.sidekick])).savedUsd;
136
136
  }
137
137
 
138
138
  function publishStatus(ctx: ExtensionContext): void {
@@ -180,8 +180,14 @@ export default function fusionExtension(pi: ExtensionAPI): void {
180
180
  function savingsStats(ctx: ExtensionContext): string {
181
181
  if (active?.kind !== "fusion" || runtime === undefined) return "Fusion is not active — pick a Fusion pair with /unipi:model.";
182
182
  const reg = registryOf(ctx);
183
- const savings = estimateSavings(runtime.usage, costOf(findModel(reg, active.lead)), costOf(findModel(reg, active.sidekick)));
184
- return `Sidekick tokens: in ${String(runtime.usage.input)} · out ${String(runtime.usage.output)} · cached ${String(runtime.usage.cacheRead)} · cache write ${String(runtime.usage.cacheWrite)}\nSidekick cost: $${savings.sidekickUsd.toFixed(2)} · at lead prices: $${savings.atLeadUsd.toFixed(2)} · saved: $${savings.savedUsd.toFixed(2)}\nHandoffs: ${String(runtime.reports.size)} · runtime alive: ${String(runtime.isAlive())} · busy: ${String(runtime.isBusy())}`;
183
+ const preset = loadPreset(ctx.cwd ?? process.cwd()).preset;
184
+ const leadCost = costOf(findModel(reg, active.lead), preset.prices[active.lead]);
185
+ const sidekickCost = costOf(findModel(reg, active.sidekick), preset.prices[active.sidekick]);
186
+ const savings = estimateSavings(runtime.usage, leadCost, sidekickCost);
187
+ const pricing = leadCost === undefined && sidekickCost === undefined
188
+ ? '\nPricing unavailable from provider — set "prices" in ~/.unipi/config/fusion/preset.json to estimate savings.'
189
+ : "";
190
+ return `Sidekick tokens: in ${String(runtime.usage.input)} · out ${String(runtime.usage.output)} · cached ${String(runtime.usage.cacheRead)} · cache write ${String(runtime.usage.cacheWrite)}\nSidekick cost: $${savings.sidekickUsd.toFixed(2)} · at lead prices: $${savings.atLeadUsd.toFixed(2)} · saved: $${savings.savedUsd.toFixed(2)}\nHandoffs: ${String(runtime.reports.size)} · runtime alive: ${String(runtime.isAlive())} · busy: ${String(runtime.isBusy())}${pricing}`;
185
191
  }
186
192
 
187
193
  registerFusionTools(pi, {
@@ -267,7 +273,7 @@ export default function fusionExtension(pi: ExtensionAPI): void {
267
273
  const cwd = ctx.cwd ?? process.cwd();
268
274
  const loaded = loadPreset(cwd);
269
275
  const preset = loaded.preset;
270
- const models = reg.getAvailable().map((m) => toPickerModel(m, preset.badges[modelKey(m)]));
276
+ const models = reg.getAvailable().map((m) => toPickerModel(m, preset.badges[modelKey(m)], preset.prices[modelKey(m)]));
271
277
  if (models.length === 0) {
272
278
  ctx.ui.notify("No models available. Use /login to add a provider.", "warning");
273
279
  return;
@@ -347,13 +353,26 @@ export default function fusionExtension(pi: ExtensionAPI): void {
347
353
  },
348
354
  });
349
355
 
350
- pi.on("session_start", (_e, ctx) => {
356
+ pi.on("session_start", async (_e, ctx) => {
351
357
  stopRuntime();
352
358
  nudged = false;
353
359
  modelBykey.clear();
354
360
  active = loadPreset(ctx.cwd ?? process.cwd()).preset.active;
355
- // Only keep a Fusion status if the session actually runs on that lead.
356
- if (active?.kind === "fusion" && ctx.model && modelKey(ctx.model) !== active.lead) active = undefined;
361
+ if (active?.kind === "fusion" && (!ctx.model || modelKey(ctx.model) !== active.lead)) {
362
+ const leadKey = active.lead;
363
+ const lead = findModel(registryOf(ctx), leadKey);
364
+ const restored = lead !== undefined && await pi.setModel(lead);
365
+ if (restored) {
366
+ try {
367
+ pi.setThinkingLevel(active.leadEffort ?? "medium");
368
+ } catch {
369
+ /* provider may not support thinking */
370
+ }
371
+ } else {
372
+ active = undefined;
373
+ if (ctx.hasUI) ctx.ui.notify(`Fusion lead ${leadKey} unavailable — Fusion off`, "warning");
374
+ }
375
+ }
357
376
  publishStatus(ctx);
358
377
  if (ctx.hasUI) ctx.ui.addAutocompleteProvider(createModelBoostProvider);
359
378
  });
@@ -369,6 +388,8 @@ export default function fusionExtension(pi: ExtensionAPI): void {
369
388
  if (active?.kind === "fusion" && modelKey(event.model) !== active.lead) {
370
389
  stopRuntime();
371
390
  active = { kind: "single", model: modelKey(event.model) };
391
+ const loaded = loadPreset(ctx.cwd ?? process.cwd());
392
+ saveRuntimeState(globalPresetPath(), { effort: loaded.preset.effort, recent: loaded.preset.recent, active });
372
393
  publishStatus(ctx);
373
394
  }
374
395
  });
@@ -128,6 +128,10 @@ function money(perMillion: number): string {
128
128
  return `$${rounded.replace(/\.0+$/u, "").replace(/(\.\d)0$/u, "$1")} / 1M`;
129
129
  }
130
130
 
131
+ function hasPricing(cost: PickerModel["cost"]): cost is NonNullable<PickerModel["cost"]> {
132
+ return cost !== undefined && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0);
133
+ }
134
+
131
135
  function pad(text: string, width: number): string {
132
136
  const w = visibleWidth(text);
133
137
  return w >= width ? text : text + " ".repeat(width - w);
@@ -161,7 +165,7 @@ export class ModelPicker {
161
165
  this.onRenderRequest = options.onRenderRequest;
162
166
  this.visibleRows = options.visibleRows ?? DEFAULT_VISIBLE_ROWS;
163
167
  this.modelsByKey = new Map(options.state.models.map((m) => [m.key, m]));
164
- const prices = options.state.models.map((m) => (m.cost ? blendedPrice(m.cost) : undefined)).filter((p): p is number => p !== undefined);
168
+ const prices = options.state.models.map((m) => (hasPricing(m.cost) ? blendedPrice(m.cost) : undefined)).filter((p): p is number => p !== undefined && p > 0);
165
169
  this.priceRange = { min: prices.length > 0 ? Math.min(...prices) : 0, max: prices.length > 0 ? Math.max(...prices) : 0 };
166
170
  this.effort = { ...options.state.effort };
167
171
  const active = options.state.active;
@@ -477,24 +481,27 @@ export class ModelPicker {
477
481
  const primaryKey = row.kind === "fusion" ? this.lead : row.key;
478
482
  const primary = primaryKey === undefined ? undefined : this.modelsByKey.get(primaryKey);
479
483
  const side = row.kind === "fusion" && this.sidekick !== undefined ? this.modelsByKey.get(this.sidekick) : undefined;
484
+ const primaryCost = primary?.cost;
485
+ const sideCost = side?.cost;
480
486
  const cols: Array<[string, string]> = [];
481
- if (primary?.cost) {
482
- cols.push(["Input", money(primary.cost.input)]);
483
- cols.push(["Cached input", money(primary.cost.cachedInput)]);
484
- cols.push(["Output", money(primary.cost.output)]);
487
+ if (hasPricing(primaryCost)) {
488
+ cols.push(["Input", money(primaryCost.input)]);
489
+ cols.push(["Cached input", money(primaryCost.cachedInput)]);
490
+ cols.push(["Output", money(primaryCost.output)]);
485
491
  } else {
486
492
  cols.push(["Input", "—"], ["Cached input", "—"], ["Output", "—"]);
487
493
  }
488
494
  if (row.kind === "fusion") {
489
- if (side?.cost) {
490
- cols.push(["Sidekick input", money(side.cost.input)]);
491
- cols.push(["Sidekick cached input", money(side.cost.cachedInput)]);
492
- cols.push(["Sidekick output", money(side.cost.output)]);
495
+ if (hasPricing(sideCost)) {
496
+ cols.push(["Sidekick input", money(sideCost.input)]);
497
+ cols.push(["Sidekick cached input", money(sideCost.cachedInput)]);
498
+ cols.push(["Sidekick output", money(sideCost.output)]);
493
499
  } else {
494
500
  cols.push(["Sidekick input", "—"], ["Sidekick cached input", "—"], ["Sidekick output", "—"]);
495
501
  }
496
502
  }
497
- const colWidth = Math.max(10, Math.min(18, Math.floor((width - 4) / cols.length)));
503
+ const need = Math.max(...cols.map(([h, v]) => Math.max(visibleWidth(h), visibleWidth(v)))) + 3;
504
+ const colWidth = Math.max(10, Math.min(need, Math.floor((width - 4) / cols.length)));
498
505
  const head = cols.map(([h]) => pad(t.fg("dim", h), colWidth)).join("");
499
506
  const vals = cols.map(([, v]) => pad(t.fg("text", v), colWidth)).join("");
500
507
  const desc =
@@ -506,7 +513,11 @@ export class ModelPicker {
506
513
  const badges = this.state.models.some((m) => m.badge !== undefined)
507
514
  ? `${t.fg("success", "✱")} ${t.fg("dim", "New")} ${t.fg("accent", "✱")} ${t.fg("dim", "Promotion")} ${t.fg("warning", "✱")} ${t.fg("dim", "Beta")} ${t.fg("dim", "·")}`
508
515
  : "";
509
- const description = `${badges}${badges.length > 0 ? " " : ""}${desc}`;
516
+ const noPricing = row.kind === "fusion"
517
+ ? !hasPricing(primaryCost) || !hasPricing(sideCost)
518
+ : !hasPricing(primaryCost);
519
+ const pricing = noPricing ? t.fg("dim", " · no pricing data from provider") : "";
520
+ const description = `${badges}${badges.length > 0 ? " " : ""}${desc}${pricing}`;
510
521
  return [truncateToWidth(` ${head}`, width - 1), truncateToWidth(` ${vals}`, width - 1), truncateToWidth(` ${description}`, width - 1)];
511
522
  }
512
523
 
@@ -561,8 +572,9 @@ export class ModelPicker {
561
572
  const sliderCells = Math.min(48, Math.max(1, width - 6));
562
573
  const sliderKey = row?.kind === "fusion" ? this.lead : row?.key;
563
574
  const sliderModel = sliderKey === undefined ? undefined : this.modelsByKey.get(sliderKey);
564
- const sliderPrice = sliderModel?.cost === undefined ? undefined : blendedPrice(sliderModel.cost);
565
- const marker = sliderPrice === undefined ? undefined : sliderPosition(sliderPrice, this.priceRange.min, this.priceRange.max, sliderCells);
575
+ const sliderCost = sliderModel?.cost;
576
+ const sliderPrice = hasPricing(sliderCost) ? blendedPrice(sliderCost) : undefined;
577
+ const marker = this.priceRange.max <= 0 || sliderPrice === undefined ? undefined : sliderPosition(sliderPrice, this.priceRange.min, this.priceRange.max, sliderCells);
566
578
  lines.push(truncateToWidth(` ${renderSlider(sliderCells, marker)}`, width - 1));
567
579
  lines.push(...this.renderPricePanel(row, width));
568
580
  lines.push("");
@@ -61,6 +61,8 @@ export interface FusionPreset {
61
61
  recent: ModelKey[];
62
62
  /** Optional hand-curated model badge metadata. */
63
63
  badges: Record<ModelKey, FusionBadge>;
64
+ /** Manual pricing overrides for providers that report no pricing. */
65
+ prices: Record<ModelKey, { input: number; cachedInput: number; output: number }>;
64
66
  /** What the user last confirmed in the picker. */
65
67
  active?: ActiveSelection | undefined;
66
68
  }
@@ -74,6 +76,7 @@ export function emptyPreset(): FusionPreset {
74
76
  effort: {},
75
77
  recent: [],
76
78
  badges: {},
79
+ prices: {},
77
80
  };
78
81
  }
79
82
 
@@ -136,6 +139,20 @@ export function parsePreset(raw: unknown): Partial<FusionPreset> {
136
139
  }
137
140
  out.badges = badges;
138
141
  }
142
+ if (typeof r["prices"] === "object" && r["prices"] !== null) {
143
+ const prices: FusionPreset["prices"] = {};
144
+ for (const [k, v] of Object.entries(r["prices"] as Record<string, unknown>)) {
145
+ if (typeof v !== "object" || v === null) continue;
146
+ const price = v as Record<string, unknown>;
147
+ const input = price["input"];
148
+ const cachedInput = price["cachedInput"];
149
+ const output = price["output"];
150
+ if ([input, cachedInput, output].every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0)) {
151
+ prices[k] = { input: input as number, cachedInput: cachedInput as number, output: output as number };
152
+ }
153
+ }
154
+ out.prices = prices;
155
+ }
139
156
  const active = r["active"];
140
157
  if (typeof active === "object" && active !== null) {
141
158
  const a = active as Record<string, unknown>;
@@ -167,6 +184,7 @@ export function mergePresets(base: FusionPreset, over: Partial<FusionPreset>): F
167
184
  effort: { ...base.effort, ...(over.effort ?? {}) },
168
185
  recent: over.recent ?? base.recent,
169
186
  badges: { ...base.badges, ...(over.badges ?? {}) },
187
+ prices: { ...base.prices, ...(over.prices ?? {}) },
170
188
  active: over.active ?? base.active,
171
189
  };
172
190
  }
@@ -50322,7 +50322,8 @@ function emptyPreset() {
50322
50322
  default: {},
50323
50323
  effort: {},
50324
50324
  recent: [],
50325
- badges: {}
50325
+ badges: {},
50326
+ prices: {}
50326
50327
  };
50327
50328
  }
50328
50329
  function globalPresetPath(home = homedir35()) {
@@ -50377,6 +50378,20 @@ function parsePreset2(raw) {
50377
50378
  }
50378
50379
  out.badges = badges;
50379
50380
  }
50381
+ if (typeof r["prices"] === "object" && r["prices"] !== null) {
50382
+ const prices = {};
50383
+ for (const [k, v] of Object.entries(r["prices"])) {
50384
+ if (typeof v !== "object" || v === null) continue;
50385
+ const price = v;
50386
+ const input = price["input"];
50387
+ const cachedInput = price["cachedInput"];
50388
+ const output = price["output"];
50389
+ if ([input, cachedInput, output].every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0)) {
50390
+ prices[k] = { input, cachedInput, output };
50391
+ }
50392
+ }
50393
+ out.prices = prices;
50394
+ }
50380
50395
  const active = r["active"];
50381
50396
  if (typeof active === "object" && active !== null) {
50382
50397
  const a = active;
@@ -50403,6 +50418,7 @@ function mergePresets(base, over) {
50403
50418
  effort: { ...base.effort, ...over.effort ?? {} },
50404
50419
  recent: over.recent ?? base.recent,
50405
50420
  badges: { ...base.badges, ...over.badges ?? {} },
50421
+ prices: { ...base.prices, ...over.prices ?? {} },
50406
50422
  active: over.active ?? base.active
50407
50423
  };
50408
50424
  }
@@ -50522,6 +50538,9 @@ function money(perMillion) {
50522
50538
  const rounded = perMillion >= 10 ? perMillion.toFixed(0) : perMillion >= 1 ? perMillion.toFixed(1) : perMillion.toFixed(2);
50523
50539
  return `$${rounded.replace(/\.0+$/u, "").replace(/(\.\d)0$/u, "$1")} / 1M`;
50524
50540
  }
50541
+ function hasPricing(cost) {
50542
+ return cost !== void 0 && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0);
50543
+ }
50525
50544
  function pad(text, width) {
50526
50545
  const w = visibleWidth23(text);
50527
50546
  return w >= width ? text : text + " ".repeat(width - w);
@@ -50552,7 +50571,7 @@ var ModelPicker = class {
50552
50571
  this.onRenderRequest = options.onRenderRequest;
50553
50572
  this.visibleRows = options.visibleRows ?? DEFAULT_VISIBLE_ROWS;
50554
50573
  this.modelsByKey = new Map(options.state.models.map((m) => [m.key, m]));
50555
- const prices = options.state.models.map((m) => m.cost ? blendedPrice(m.cost) : void 0).filter((p) => p !== void 0);
50574
+ const prices = options.state.models.map((m) => hasPricing(m.cost) ? blendedPrice(m.cost) : void 0).filter((p) => p !== void 0 && p > 0);
50556
50575
  this.priceRange = { min: prices.length > 0 ? Math.min(...prices) : 0, max: prices.length > 0 ? Math.max(...prices) : 0 };
50557
50576
  this.effort = { ...options.state.effort };
50558
50577
  const active = options.state.active;
@@ -50802,29 +50821,34 @@ var ModelPicker = class {
50802
50821
  const primaryKey = row.kind === "fusion" ? this.lead : row.key;
50803
50822
  const primary = primaryKey === void 0 ? void 0 : this.modelsByKey.get(primaryKey);
50804
50823
  const side = row.kind === "fusion" && this.sidekick !== void 0 ? this.modelsByKey.get(this.sidekick) : void 0;
50824
+ const primaryCost = primary?.cost;
50825
+ const sideCost = side?.cost;
50805
50826
  const cols = [];
50806
- if (primary?.cost) {
50807
- cols.push(["Input", money(primary.cost.input)]);
50808
- cols.push(["Cached input", money(primary.cost.cachedInput)]);
50809
- cols.push(["Output", money(primary.cost.output)]);
50827
+ if (hasPricing(primaryCost)) {
50828
+ cols.push(["Input", money(primaryCost.input)]);
50829
+ cols.push(["Cached input", money(primaryCost.cachedInput)]);
50830
+ cols.push(["Output", money(primaryCost.output)]);
50810
50831
  } else {
50811
50832
  cols.push(["Input", "\u2014"], ["Cached input", "\u2014"], ["Output", "\u2014"]);
50812
50833
  }
50813
50834
  if (row.kind === "fusion") {
50814
- if (side?.cost) {
50815
- cols.push(["Sidekick input", money(side.cost.input)]);
50816
- cols.push(["Sidekick cached input", money(side.cost.cachedInput)]);
50817
- cols.push(["Sidekick output", money(side.cost.output)]);
50835
+ if (hasPricing(sideCost)) {
50836
+ cols.push(["Sidekick input", money(sideCost.input)]);
50837
+ cols.push(["Sidekick cached input", money(sideCost.cachedInput)]);
50838
+ cols.push(["Sidekick output", money(sideCost.output)]);
50818
50839
  } else {
50819
50840
  cols.push(["Sidekick input", "\u2014"], ["Sidekick cached input", "\u2014"], ["Sidekick output", "\u2014"]);
50820
50841
  }
50821
50842
  }
50822
- const colWidth = Math.max(10, Math.min(18, Math.floor((width - 4) / cols.length)));
50843
+ const need = Math.max(...cols.map(([h, v]) => Math.max(visibleWidth23(h), visibleWidth23(v)))) + 3;
50844
+ const colWidth = Math.max(10, Math.min(need, Math.floor((width - 4) / cols.length)));
50823
50845
  const head = cols.map(([h]) => pad(t.fg("dim", h), colWidth)).join("");
50824
50846
  const vals = cols.map(([, v]) => pad(t.fg("text", v), colWidth)).join("");
50825
50847
  const desc = row.kind === "fusion" ? t.fg("dim", "Pairs frontier intelligence with cost-efficient execution") : primary?.reasoning ? t.fg("dim", "Reasoning model \xB7 \u2190/\u2192 adjusts thinking effort") : t.fg("dim", "Non-reasoning model \xB7 effort is ignored by the provider");
50826
50848
  const badges = this.state.models.some((m) => m.badge !== void 0) ? `${t.fg("success", "\u2731")} ${t.fg("dim", "New")} ${t.fg("accent", "\u2731")} ${t.fg("dim", "Promotion")} ${t.fg("warning", "\u2731")} ${t.fg("dim", "Beta")} ${t.fg("dim", "\xB7")}` : "";
50827
- const description = `${badges}${badges.length > 0 ? " " : ""}${desc}`;
50849
+ const noPricing = row.kind === "fusion" ? !hasPricing(primaryCost) || !hasPricing(sideCost) : !hasPricing(primaryCost);
50850
+ const pricing = noPricing ? t.fg("dim", " \xB7 no pricing data from provider") : "";
50851
+ const description = `${badges}${badges.length > 0 ? " " : ""}${desc}${pricing}`;
50828
50852
  return [truncateToWidth26(` ${head}`, width - 1), truncateToWidth26(` ${vals}`, width - 1), truncateToWidth26(` ${description}`, width - 1)];
50829
50853
  }
50830
50854
  hintLine(row) {
@@ -50873,8 +50897,9 @@ var ModelPicker = class {
50873
50897
  const sliderCells = Math.min(48, Math.max(1, width - 6));
50874
50898
  const sliderKey = row?.kind === "fusion" ? this.lead : row?.key;
50875
50899
  const sliderModel = sliderKey === void 0 ? void 0 : this.modelsByKey.get(sliderKey);
50876
- const sliderPrice = sliderModel?.cost === void 0 ? void 0 : blendedPrice(sliderModel.cost);
50877
- const marker = sliderPrice === void 0 ? void 0 : sliderPosition(sliderPrice, this.priceRange.min, this.priceRange.max, sliderCells);
50900
+ const sliderCost = sliderModel?.cost;
50901
+ const sliderPrice = hasPricing(sliderCost) ? blendedPrice(sliderCost) : void 0;
50902
+ const marker = this.priceRange.max <= 0 || sliderPrice === void 0 ? void 0 : sliderPosition(sliderPrice, this.priceRange.min, this.priceRange.max, sliderCells);
50878
50903
  lines.push(truncateToWidth26(` ${renderSlider(sliderCells, marker)}`, width - 1));
50879
50904
  lines.push(...this.renderPricePanel(row, width));
50880
50905
  lines.push("");
@@ -51487,17 +51512,17 @@ function findModel(reg, key) {
51487
51512
  if (modelBykey.size === 0 && reg) for (const m of reg.getAvailable()) modelBykey.set(modelKey(m), m);
51488
51513
  return modelBykey.get(key);
51489
51514
  }
51490
- function costOf(m) {
51491
- const cost = m?.cost;
51492
- return cost && typeof cost.input === "number" ? { input: cost.input, cachedInput: cost.cacheRead ?? 0, output: cost.output } : void 0;
51515
+ function costOf(m, override) {
51516
+ const cost = override ?? (m?.cost && typeof m.cost.input === "number" ? { input: m.cost.input, cachedInput: m.cost.cacheRead ?? 0, output: m.cost.output } : void 0);
51517
+ return cost && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0) ? cost : void 0;
51493
51518
  }
51494
- function toPickerModel(m, badge) {
51519
+ function toPickerModel(m, badge, override) {
51495
51520
  return {
51496
51521
  key: modelKey(m),
51497
51522
  name: m.name || m.id,
51498
51523
  provider: m.provider,
51499
51524
  badge,
51500
- cost: costOf(m),
51525
+ cost: costOf(m, override),
51501
51526
  reasoning: Boolean(m.reasoning)
51502
51527
  };
51503
51528
  }
@@ -51546,9 +51571,10 @@ function fusionExtension(pi) {
51546
51571
  function statusSavings(ctx) {
51547
51572
  if (active?.kind !== "fusion" || runtime === void 0) return void 0;
51548
51573
  const reg = registryOf(ctx);
51574
+ const preset2 = loadPreset(ctx.cwd ?? process.cwd()).preset;
51549
51575
  const lead = findModel(reg, active.lead);
51550
51576
  const side = findModel(reg, active.sidekick);
51551
- return estimateSavings(runtime.usage, costOf(lead), costOf(side)).savedUsd;
51577
+ return estimateSavings(runtime.usage, costOf(lead, preset2.prices[active.lead]), costOf(side, preset2.prices[active.sidekick])).savedUsd;
51552
51578
  }
51553
51579
  function publishStatus(ctx) {
51554
51580
  const reg = registryOf(ctx);
@@ -51589,10 +51615,14 @@ function fusionExtension(pi) {
51589
51615
  function savingsStats(ctx) {
51590
51616
  if (active?.kind !== "fusion" || runtime === void 0) return "Fusion is not active \u2014 pick a Fusion pair with /unipi:model.";
51591
51617
  const reg = registryOf(ctx);
51592
- const savings = estimateSavings(runtime.usage, costOf(findModel(reg, active.lead)), costOf(findModel(reg, active.sidekick)));
51618
+ const preset2 = loadPreset(ctx.cwd ?? process.cwd()).preset;
51619
+ const leadCost = costOf(findModel(reg, active.lead), preset2.prices[active.lead]);
51620
+ const sidekickCost = costOf(findModel(reg, active.sidekick), preset2.prices[active.sidekick]);
51621
+ const savings = estimateSavings(runtime.usage, leadCost, sidekickCost);
51622
+ const pricing = leadCost === void 0 && sidekickCost === void 0 ? '\nPricing unavailable from provider \u2014 set "prices" in ~/.unipi/config/fusion/preset.json to estimate savings.' : "";
51593
51623
  return `Sidekick tokens: in ${String(runtime.usage.input)} \xB7 out ${String(runtime.usage.output)} \xB7 cached ${String(runtime.usage.cacheRead)} \xB7 cache write ${String(runtime.usage.cacheWrite)}
51594
51624
  Sidekick cost: $${savings.sidekickUsd.toFixed(2)} \xB7 at lead prices: $${savings.atLeadUsd.toFixed(2)} \xB7 saved: $${savings.savedUsd.toFixed(2)}
51595
- Handoffs: ${String(runtime.reports.size)} \xB7 runtime alive: ${String(runtime.isAlive())} \xB7 busy: ${String(runtime.isBusy())}`;
51625
+ Handoffs: ${String(runtime.reports.size)} \xB7 runtime alive: ${String(runtime.isAlive())} \xB7 busy: ${String(runtime.isBusy())}${pricing}`;
51596
51626
  }
51597
51627
  registerFusionTools(pi, {
51598
51628
  getRuntime,
@@ -51664,7 +51694,7 @@ ${leadPolicy(identity(ctx))}` } : void 0);
51664
51694
  const cwd = ctx.cwd ?? process.cwd();
51665
51695
  const loaded = loadPreset(cwd);
51666
51696
  const preset2 = loaded.preset;
51667
- const models = reg.getAvailable().map((m) => toPickerModel(m, preset2.badges[modelKey(m)]));
51697
+ const models = reg.getAvailable().map((m) => toPickerModel(m, preset2.badges[modelKey(m)], preset2.prices[modelKey(m)]));
51668
51698
  if (models.length === 0) {
51669
51699
  ctx.ui.notify("No models available. Use /login to add a provider.", "warning");
51670
51700
  return;
@@ -51739,12 +51769,25 @@ ${String(result2.curation.lead.length)} lead \xB7 ${String(result2.curation.side
51739
51769
  );
51740
51770
  }
51741
51771
  });
51742
- pi.on("session_start", (_e, ctx) => {
51772
+ pi.on("session_start", async (_e, ctx) => {
51743
51773
  stopRuntime();
51744
51774
  nudged = false;
51745
51775
  modelBykey.clear();
51746
51776
  active = loadPreset(ctx.cwd ?? process.cwd()).preset.active;
51747
- if (active?.kind === "fusion" && ctx.model && modelKey(ctx.model) !== active.lead) active = void 0;
51777
+ if (active?.kind === "fusion" && (!ctx.model || modelKey(ctx.model) !== active.lead)) {
51778
+ const leadKey = active.lead;
51779
+ const lead = findModel(registryOf(ctx), leadKey);
51780
+ const restored = lead !== void 0 && await pi.setModel(lead);
51781
+ if (restored) {
51782
+ try {
51783
+ pi.setThinkingLevel(active.leadEffort ?? "medium");
51784
+ } catch {
51785
+ }
51786
+ } else {
51787
+ active = void 0;
51788
+ if (ctx.hasUI) ctx.ui.notify(`Fusion lead ${leadKey} unavailable \u2014 Fusion off`, "warning");
51789
+ }
51790
+ }
51748
51791
  publishStatus(ctx);
51749
51792
  if (ctx.hasUI) ctx.ui.addAutocompleteProvider(createModelBoostProvider);
51750
51793
  });
@@ -51756,6 +51799,8 @@ ${String(result2.curation.lead.length)} lead \xB7 ${String(result2.curation.side
51756
51799
  if (active?.kind === "fusion" && modelKey(event.model) !== active.lead) {
51757
51800
  stopRuntime();
51758
51801
  active = { kind: "single", model: modelKey(event.model) };
51802
+ const loaded = loadPreset(ctx.cwd ?? process.cwd());
51803
+ saveRuntimeState(globalPresetPath(), { effort: loaded.preset.effort, recent: loaded.preset.recent, active });
51759
51804
  publishStatus(ctx);
51760
51805
  }
51761
51806
  });