pi-llamacpp-infra 1.3.0 β†’ 1.3.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/README.md CHANGED
@@ -286,11 +286,12 @@ Every assistant message therefore carries a realistic `usage.cost.total`: pi's o
286
286
  ### Setup
287
287
 
288
288
  ```
289
- /llamacpp-infra config β†’ πŸ’° Energy cost
289
+ /llamacpp-infra config β†’ πŸ’° Energy cost (global: currency, master toggle)
290
+ /llamacpp-infra config β†’ 🌐 Servers β†’ machine β†’ πŸ’° Energy cost (per machine)
290
291
  ```
291
292
 
292
- 1. **Currency** β€” USD, EUR, GBP or CNY. It only changes the display unit; tariffs are stored per kWh in that currency.
293
- 2. **Per-server power draw & tariff** β€” the power draw belongs to the *machine*, so every server (machine) gets one profile: `kW` during inference (e.g. `0.15` for 150 W β€” GPU TDP + idle draw is a good approximation) and the electricity price per kWh. All models served by that machine inherit it.
293
+ 1. **Currency** (global) β€” USD, EUR, GBP or CNY. It only changes the display unit; tariffs are stored per kWh in that currency.
294
+ 2. **Per-server power draw & tariff** β€” the power draw belongs to the *machine*, so each server gets its own profile inside its own config menu (`🌐 Servers` β†’ select the machine β†’ `πŸ’° Energy cost`; adding a new server asks for it right away). Set `kW` during inference (e.g. `0.15` for 150 W β€” GPU TDP + idle draw is a good approximation) and the electricity price per kWh. All models served by that machine inherit it.
294
295
 
295
296
  Once set, each provider request is timed (`before_provider_request` β†’ assistant `message_end`, partial/aborted requests included) and charged. The footer shows the session total as `πŸ’°` (e.g. `πŸ’°3.2c` = 3.2 euro-cents; `Β’`/`c`/`p`/`εˆ†` per currency); enable/disable anytime from the same menu.
296
297
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-llamacpp-infra",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Discovery, metrics and control of llama.cpp-family servers for pi (llama.cpp, ZINC, DwarfStar/ds4, lucebox, LM Studio): scan machines (localhost, LAN, Tailscale), register models, live metrics, per-model thinking budgets, vision detection, native config UI.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/core.ts CHANGED
@@ -96,6 +96,16 @@ export function normalizeSettings(raw: unknown): SettingsConfig {
96
96
  return s;
97
97
  }
98
98
 
99
+ /** True if two servers would collide on the same (host, port) endpoint. */
100
+ export function serversShareEndpoint(
101
+ a: Pick<import("./types.ts").ServerConfig, "host" | "ports">,
102
+ b: Pick<import("./types.ts").ServerConfig, "host" | "ports">,
103
+ ): boolean {
104
+ if (a.host !== b.host) return false;
105
+ const aPorts = new Set(a.ports);
106
+ return b.ports.some((p) => aPorts.has(p));
107
+ }
108
+
99
109
  function normalizeServer(s: Record<string, unknown>): import("./types.ts").ServerConfig {
100
110
  return {
101
111
  id: String(s.id ?? "local"),
package/src/ui.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  modelOptions,
8
8
  saveConfig,
9
9
  serverLabel,
10
+ serversShareEndpoint,
10
11
  shared,
11
12
  } from "./core.ts";
12
13
  import type {
@@ -225,7 +226,7 @@ export async function showConfigMenu(ctx: ExtensionContext, deps: UiDeps): Promi
225
226
  {
226
227
  value: "cost",
227
228
  label: `πŸ’° Energy cost: ${config.settings.costTracking ? "ON" : "OFF"}`,
228
- description: `currency ${currencySymbol(config.settings.currency)} Β· ${config.servers.filter((s) => s.costProfile && s.costProfile.kW > 0).length} server(s) with kW`,
229
+ description: `currency ${currencySymbol(config.settings.currency)} · kW/tariff per server under 🌐 Servers`,
229
230
  },
230
231
  {
231
232
  value: "metrics",
@@ -333,6 +334,11 @@ async function showServerMenu(ctx: ExtensionContext, srv: ServerConfig, deps: Ui
333
334
  label: srv.apiKey ? "πŸ”‘ API key: set" : "πŸ”‘ API key: none",
334
335
  description: srv.apiKey ? "clear or replace bearer token" : "optional",
335
336
  },
337
+ {
338
+ value: "cost",
339
+ label: srv.costProfile?.kW ? `πŸ’° Energy cost: ${formatProfile(srv.costProfile, config.settings.currency)}` : "πŸ’° Energy cost: not set",
340
+ description: "power draw during inference (kW) + electricity tariff",
341
+ },
336
342
  { value: "test", label: "πŸ§ͺ Test this server", description: `probe ${srv.ports.length} port(s) now` },
337
343
  { value: "delete", label: "πŸ—‘οΈ Delete server", description: "remove from configuration" },
338
344
  { value: "__back", label: "← Back", description: "" },
@@ -348,6 +354,15 @@ async function showServerMenu(ctx: ExtensionContext, srv: ServerConfig, deps: Ui
348
354
  ctx.ui.notify("❌ Host cannot be empty", "error");
349
355
  break;
350
356
  }
357
+ const dup = config.servers.find((s) => s.id !== srv.id && serversShareEndpoint(s, { host: trimmed, ports: srv.ports }));
358
+ if (dup) {
359
+ const sharedPorts = srv.ports.filter((p) => dup.ports.includes(p)).join(", ");
360
+ ctx.ui.notify(
361
+ `⚠️ Server "${serverLabel(dup)}" already probes ${trimmed}:${sharedPorts} β€” pick a different host`,
362
+ "warning",
363
+ );
364
+ break;
365
+ }
351
366
  srv.host = trimmed;
352
367
  saveConfig(config);
353
368
  ctx.ui.notify(`βœ… Host updated: ${srv.host}`, "info");
@@ -409,6 +424,10 @@ async function showServerMenu(ctx: ExtensionContext, srv: ServerConfig, deps: Ui
409
424
  }
410
425
  break;
411
426
  }
427
+ case "cost": {
428
+ await showServerCostMenu(ctx, srv, config);
429
+ break;
430
+ }
412
431
  case "test": {
413
432
  ctx.ui.setStatus("llamacpp-infra", "πŸ§ͺ testing…");
414
433
  const localServers = config.settings.detectVision ? scanLocalServers() : new Map();
@@ -460,10 +479,6 @@ async function addServerFlow(ctx: ExtensionContext, deps: UiDeps): Promise<void>
460
479
  ctx.ui.notify("❌ Host cannot be empty", "error");
461
480
  return;
462
481
  }
463
- if (config.servers.some((s) => s.host === trimmedHost)) {
464
- ctx.ui.notify(`⚠️ A server with host "${trimmedHost}" already exists`, "warning");
465
- return;
466
- }
467
482
  const portsRaw = await ctx.ui.input("βž• Ports to probe", "e.g. 1234, 8000, 8080-8082");
468
483
  if (portsRaw === undefined) return;
469
484
  const ports = parsePorts(portsRaw);
@@ -471,6 +486,15 @@ async function addServerFlow(ctx: ExtensionContext, deps: UiDeps): Promise<void>
471
486
  ctx.ui.notify("❌ No valid ports in input", "error");
472
487
  return;
473
488
  }
489
+ const dup = config.servers.find((s) => serversShareEndpoint(s, { host: trimmedHost, ports }));
490
+ if (dup) {
491
+ const sharedPorts = ports.filter((p) => dup.ports.includes(p)).join(", ");
492
+ ctx.ui.notify(
493
+ `⚠️ Server "${serverLabel(dup)}" already probes ${trimmedHost}:${sharedPorts} β€” use different ports`,
494
+ "warning",
495
+ );
496
+ return;
497
+ }
474
498
  const label = await ctx.ui.input("🏷️ Label (optional)", trimmedHost);
475
499
  if (label === undefined) return;
476
500
  const probeDs4 = await ctx.ui.confirm(
@@ -486,6 +510,12 @@ async function addServerFlow(ctx: ExtensionContext, deps: UiDeps): Promise<void>
486
510
  saveConfig(config);
487
511
  ctx.ui.notify(`βž• Server added: ${label.trim() || trimmedHost} (${trimmedHost}) β€” ports ${ports.join(", ")}`, "info");
488
512
  await deps.rescan(ctx);
513
+
514
+ // Per-machine energy cost: offered right after adding, since it belongs to
515
+ // the machine's own configuration (also editable later under 🌐 Servers).
516
+ const srv = config.servers.find((s) => s.id === id)!;
517
+ const wantCost = await ctx.ui.confirm("πŸ’° Energy cost?", "Set this machine's power draw (kW) and tariff to estimate electricity cost of local inference?");
518
+ if (wantCost) await showServerCostMenu(ctx, srv, config);
489
519
  }
490
520
 
491
521
  // ── Thinking budgets ───────────────────────────────────────────────────────
@@ -646,8 +676,8 @@ async function showCostMenu(ctx: ExtensionContext, deps: UiDeps): Promise<void>
646
676
  },
647
677
  {
648
678
  value: "servers",
649
- label: "πŸ–₯️ Per-server kW / tariff",
650
- description: `${withKw.length} server(s) configured`,
679
+ label: "πŸ–₯️ Per-server kW / tariff…",
680
+ description: `${withKw.length} configured β€” edit under 🌐 Servers β†’ machine`,
651
681
  },
652
682
  { value: "__back", label: "← Back", description: "" },
653
683
  ]);
@@ -668,33 +698,22 @@ async function showCostMenu(ctx: ExtensionContext, deps: UiDeps): Promise<void>
668
698
  }
669
699
  }
670
700
  else if (action === "servers") {
671
- await showServerCostMenu(ctx, config);
701
+ await showServersMenu(ctx, deps);
672
702
  }
673
703
  }
674
704
  }
675
705
 
676
- async function showServerCostMenu(ctx: ExtensionContext, config: NonNullable<typeof shared.activeConfig>): Promise<void> {
706
+ /** Per-machine energy cost (kW + tariff). Lives inside the server's own menu. */
707
+ async function showServerCostMenu(ctx: ExtensionContext, srv: ServerConfig, config: NonNullable<typeof shared.activeConfig>): Promise<void> {
677
708
  for (;;) {
678
- const items = config.servers.map((srv) => ({
679
- value: srv.id,
680
- label: `${srv.costProfile?.kW ? "πŸ’°" : "Β·"} ${serverLabel(srv)}`,
681
- description: srv.costProfile?.kW ? formatProfile(srv.costProfile, config.settings.currency) : `${srv.host} β€” no kW set`,
682
- }));
683
- items.push({ value: "__back", label: "← Back", description: "" });
684
- const picked = await selectFrom(ctx, "πŸ’° Per-server energy cost β€” select a machine", items);
685
- if (!picked || picked === "__back") return;
686
- const srv = config.servers.find((s) => s.id === picked);
687
- if (!srv) return;
688
-
689
709
  const p = srv.costProfile;
690
- const sub = await selectFrom(ctx, `πŸ’° ${serverLabel(srv)} (${srv.host})`, [
691
- { value: "kW", label: p?.kW ? `⚑ Power draw: ${p.kW} kW` : "⚑ Set power draw (kW)", description: "W consumed during inference (e.g. 0.15 for 150 W)" },
710
+ const sub = await selectFrom(ctx, `πŸ’° ${serverLabel(srv)} β€” energy cost`, [
711
+ { value: "kW", label: p?.kW ? `⚑ Power draw: ${p.kW} kW` : "⚑ Set power draw (kW)", description: "watts consumed during inference, e.g. 0.15 for 150 W" },
692
712
  { value: "rate", label: p?.ratePerKwh ? `🧾 Tariff: ${p.ratePerKwh} ${currencySymbol(config.settings.currency)}/kWh` : `🧾 Set tariff (${currencySymbol(config.settings.currency)}/kWh)`, description: "electricity price per kWh" },
693
- { value: "label", label: p?.label ? `🏷️ Label: ${p.label}` : "🏷️ Set label", description: "optional, shown in menus" },
694
- ...(p?.kW ? [{ value: "clear", label: "πŸ—‘οΈ Remove cost profile", description: "stop estimating cost for this machine" }] : []),
713
+ ...(p?.kW || p?.ratePerKwh ? [{ value: "clear", label: "πŸ—‘οΈ Remove cost profile", description: "stop estimating cost for this machine" }] : []),
695
714
  { value: "__back", label: "← Back", description: "" },
696
715
  ]);
697
- if (!sub || sub === "__back") continue;
716
+ if (!sub || sub === "__back") return;
698
717
 
699
718
  if (sub === "kW") {
700
719
  const raw = await ctx.ui.input("⚑ Power draw in kW (e.g. 0.15 = 150 W)", p?.kW ? String(p.kW) : "0.15");
@@ -712,13 +731,6 @@ async function showServerCostMenu(ctx: ExtensionContext, config: NonNullable<typ
712
731
  saveConfig(config);
713
732
  ctx.ui.notify(`🧾 ${serverLabel(srv)}: ${rate} ${currencySymbol(config.settings.currency)}/kWh`, "info");
714
733
  }
715
- else if (sub === "label") {
716
- const raw = await ctx.ui.input("🏷️ Label", p?.label ?? "");
717
- if (raw !== undefined) {
718
- srv.costProfile = { ...(srv.costProfile ?? { kW: 0.15, ratePerKwh: 0.21 }), label: raw.trim() || undefined };
719
- saveConfig(config);
720
- }
721
- }
722
734
  else if (sub === "clear") {
723
735
  delete srv.costProfile;
724
736
  saveConfig(config);