@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 +8 -1
- package/package.json +2 -2
- package/packages/fusion/README.md +3 -0
- package/packages/fusion/package.json +1 -1
- package/packages/fusion/src/index.ts +35 -14
- package/packages/fusion/src/picker.ts +25 -13
- package/packages/fusion/src/preset.ts +18 -0
- package/packages/unipi/bundled.js +70 -25
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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 &&
|
|
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
|
|
184
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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 (
|
|
482
|
-
cols.push(["Input", money(
|
|
483
|
-
cols.push(["Cached input", money(
|
|
484
|
-
cols.push(["Output", money(
|
|
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 (
|
|
490
|
-
cols.push(["Sidekick input", money(
|
|
491
|
-
cols.push(["Sidekick cached input", money(
|
|
492
|
-
cols.push(["Sidekick output", money(
|
|
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
|
|
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
|
|
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
|
|
565
|
-
const
|
|
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 (
|
|
50807
|
-
cols.push(["Input", money(
|
|
50808
|
-
cols.push(["Cached input", money(
|
|
50809
|
-
cols.push(["Output", money(
|
|
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 (
|
|
50815
|
-
cols.push(["Sidekick input", money(
|
|
50816
|
-
cols.push(["Sidekick cached input", money(
|
|
50817
|
-
cols.push(["Sidekick output", money(
|
|
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
|
|
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
|
|
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
|
|
50877
|
-
const
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
});
|