pi-llamacpp-infra 1.2.0 → 1.2.2

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
@@ -1,5 +1,7 @@
1
1
  # llamacpp-infra — Discovery, Metrics & Control for llama.cpp-family Servers
2
2
 
3
+ ![llamacpp-infra banner](https://raw.githubusercontent.com/noguerol/llamacpp-infra/main/docs/banner.jpeg)
4
+
3
5
  **llamacpp-infra** turns pi into a first-class citizen of local llama.cpp infrastructure. It probes any number of machines — localhost, LAN or Tailscale — discovers every model served by llama.cpp and its variants (including LM Studio), registers them into pi's native `/model` list, and gives you live Prometheus metrics, per-model thinking budgets, vision detection and a full configuration UI — all without leaving the pi prompt.
4
6
 
5
7
  ---
@@ -24,7 +26,7 @@ Anything else (vLLM, Ollama, cloud APIs…) is out of scope — use pi's built-i
24
26
  - **Compact model ids** — models appear as `Name (host:port)` in pi's `/model` picker, like a native provider; the raw GGUF path/alias is sent to the server automatically on every request
25
27
  - **Single-model & router modes** — llama.cpp single-model mode (one GGUF per instance) and router mode (multiple models per server, with per-model status and args)
26
28
  - **Per-model metadata badges** — 👁️ vision (mmproj / modalities), 🚀 drafter (speculative decoding), 🗜️ quant tag from GGUF filename, 🧠 KV cache quantization (from server args or `/proc`)
27
- - **Live Prometheus metrics** — polls `/metrics` (or `/stats`) and renders a compact widget with instantaneous prompt/gen throughput; auto-activates for llamacpp-infra models only
29
+ - **Live speed & metrics** — a constantly updating footer reading of the active model's prefill () and generation (🔥) token speed, measured straight from the stream (per token, ~10 updates/s); when pi is idle it also mirrors other clients the server's `/metrics` endpoint reports. Lives in the footer's status line, so no extra terminal row is taken. Works even without `--metrics`
28
30
  - **Thinking budgets** — llama.cpp accepts `thinking_budget_tokens` per request; configure budgets per thinking level (minimal/low/medium/high/xhigh/max) per model; models with budgets are registered with reasoning enabled
29
31
  - **Header warmup** — pre-caches the system prompt KV on llama.cpp-family servers so the first real request is faster
30
32
  - **LM Studio support** — uses LM Studio's OpenAI-compatible `/v1` API, enriches names/context/quant/vision from `/api/v1/models` (or legacy `/api/v0/models`), and avoids llama.cpp-only request fields
@@ -35,7 +37,7 @@ Anything else (vLLM, Ollama, cloud APIs…) is out of scope — use pi's built-i
35
37
 
36
38
  ## Install
37
39
 
38
- llamacpp-infra is a [pi package](https://pi.dev/packages): one extension (`src/index.ts`) plus an inlined warmup module, declared in `package.json`.
40
+ llamacpp-infra is a [pi package](https://pi.dev/packages): a small entrypoint (`src/index.ts`) backed by several focused modules. Heavier pieces (UI, scan engine, metrics, header warmup) are loaded lazily on first use so the extension stays light at startup. The package is declared in `package.json`.
39
41
 
40
42
  ```bash
41
43
  # From GitHub
@@ -97,7 +99,7 @@ No special payload workaround is required: LM Studio accepts standard OpenAI cha
97
99
  | `/llamacpp-infra scan` | Rescan all servers now |
98
100
  | `/llamacpp-infra status` | Detailed per-endpoint report |
99
101
  | `/llamacpp-infra list` | List discovered models with metadata badges |
100
- | `/llamacpp-infra metrics` | Toggle the live metrics widget |
102
+ | `/llamacpp-infra metrics` | Toggle live speed & metrics in the footer |
101
103
  | `/llamacpp-infra help` | Command help |
102
104
 
103
105
  ### `/llamacpp-infra config`
@@ -109,7 +111,7 @@ The main config menu branches into submenus:
109
111
  - **📋 Models** — per-model options (thinking budgets, replace/remove)
110
112
  - **🧪 Test** — connectivity test of all configured servers
111
113
  - **🧠 Thinking budgets** — configure per-model thinking_budget_tokens per level
112
- - **📈 Metrics** — enable/disable widget, poll interval
114
+ - **📈 Metrics** — enable/disable footer metrics, server poll interval
113
115
  - **⚙️ Settings** — discovery timeout, poll interval/budget, startup grace, fail limit, vision detection, prefix model IDs, name badges, unloaded router models, header warmup
114
116
  - **ℹ️ About** — extension info
115
117
 
@@ -222,7 +224,7 @@ Everything is configurable through the UI, but the persisted file is `~/.pi/agen
222
224
  | `showBadgesInNames` | `true` | Append 👁️🚀💤 badges to model display names |
223
225
  | `includeUnloadedRouterModels` | `false` | Router mode: list models that are not currently loaded |
224
226
  | `warmup` | `true` | Pre-cache system prompt KV on llama.cpp servers |
225
- | `metricsEnabled` | `true` | Auto-show live metrics widget for llamacpp-infra models |
227
+ | `metricsEnabled` | `true` | Show live speed & metrics in the footer for llamacpp-infra models |
226
228
  | `metricsPollMs` | `5000` | How often `/metrics` is fetched |
227
229
 
228
230
  ### Thinking budgets
@@ -243,33 +245,59 @@ With `prefixModelIds: false` the machine tag is omitted (`ModelName`); it is re-
243
245
 
244
246
  llama.cpp-family models are registered as reasoning models, exactly like a native pi provider: the footer shows `ModelName (host:port) • <level>`, the thinking selector offers levels with token estimates, and pi sends the configured `thinking_budget_tokens` budget on each request. Per-model budgets configured in the extension override pi's global per-level budgets.
245
247
 
246
- ## Live Metrics Widget
248
+ ## Live Speed & Metrics (footer)
247
249
 
248
- When enabled, the metrics widget appears automatically when the active model is from llamacpp-infra:
250
+ When enabled, the speed reading appears in the footer's status line (no extra terminal row) whenever the active model is from llamacpp-infra, updating constantly while tokens flow. Both entries are kept ultra-compact so they coexist with other extensions on pi's single status line (which truncates from the end):
249
251
 
250
252
  ```
251
- 📊 local:8080 ⚡ 42.3 t/s prompt · 38.1 t/s gen · 1.2k tokens
253
+ 🦙(12) ⚡… (before the first token)
254
+ 🦙(12) ⚡ 420 t/s 🔥 38.1 t/s (while streaming)
255
+ 🦙(12) ⚡ 420 t/s 🔥 38.1 t/s (just after the answer ends)
256
+ 🦙(12) ⏸ (between turns)
257
+ 🦙(12) ▶2 ⚡ 150 t/s 🔥 18.0 t/s (pi idle, server busy for other clients)
252
258
  ```
253
259
 
254
- It polls the server's Prometheus `/metrics` endpoint (or JSON `/stats`) and shows instantaneous throughput. The poll interval is configurable (default 5s).
260
+ (`🦙(n)` is the extension's model-count status; both live on the same footer line, so no extra row is consumed.)
261
+
262
+ - **Client measurement (always, no `--metrics` needed)** — prefill speed = `prompt tokens ÷ (request → first token)` (pi's `usage.input`, OpenAI-style `prompt_tokens` as fallback); generation speed = a moving 1.5 s window over per-token arrival samples. Updated ~every 100 ms while a stream is live (throttled, and unchanged text is skipped, so the footer never churns).
263
+ - **Server supplement (only when pi is idle)** — the poller fetches the server's Prometheus `/metrics` endpoint (or JSON `/stats`) every `metricsPollMs` (default 5 s). If the server reports other clients processing, their ⚡/🔥 rates are shown (`▶n`); when the server is idle, the plain `⏸` reading returns.
255
264
 
256
265
  ## Architecture
257
266
 
258
267
  ```
259
268
  llamacpp-infra/
260
- ├── package.json # pi package manifest (pi-package)
261
- ├── LICENSE # MIT
269
+ ├── package.json # pi package manifest (pi-package)
270
+ ├── LICENSE # MIT
262
271
  ├── README.md
263
272
  └── src/
264
- ├── index.ts # Extension entry point (~2600 lines)
265
- └── prompt-warmup.ts # Header warmup module (inlined, ~600 lines)
273
+ ├── index.ts # Entrypoint: hooks, command, lifecycle. Statically imports core + types.
274
+ ├── core.ts # Config persistence, shared state, id helpers, compat profile (loaded at startup).
275
+ ├── types.ts # Shared interfaces (type-only; erased at runtime).
276
+ ├── scan.ts # Discovery engine (lazy: HTTP probing, /props, LM Studio catalog, /proc, kind detection).
277
+ ├── registration.ts # Scan → pi-model mapping + provider registration (lazy).
278
+ ├── metrics.ts # Server /metrics poller → ServerMetricsState (lazy; only if `metricsEnabled`).
279
+ ├── speed.ts # Client-side speed tracker + footer status line (lazy; only if `metricsEnabled`).
280
+ ├── ui.ts # /llamacpp-infra subcommands, menus, status, help (lazy).
281
+ └── prompt-warmup.ts # Header warmup: capture + cache system prompt KV (lazy; only if `warmup`).
266
282
  ```
267
283
 
268
- Two-file extension with zero external dependencies (only pi's bundled `@earendil-works/pi-coding-agent` + Node built-ins):
284
+ Module load profile:
285
+
286
+ | Module | Loaded when | Approx. size |
287
+ |---|---|---|
288
+ | `index.ts` + `core.ts` (+ `types.ts`) | Startup (static) | ~25 KB |
289
+ | `scan.ts` + `registration.ts` | First discovery (dynamic) | ~27 KB |
290
+ | `prompt-warmup.ts` | Primed at load if `warmup` enabled; not loaded when disabled | ~15 KB; skipped entirely when `warmup` is OFF |
291
+ | `metrics.ts` + `speed.ts` | Primed at load if `metricsEnabled`; not loaded when disabled | ~18 KB; skipped entirely when `metricsEnabled` is OFF |
292
+ | `ui.ts` | First `/llamacpp-infra …` command (dynamic) | ~32 KB |
293
+
294
+ Zero external npm dependencies (only pi's bundled `@earendil-works/pi-coding-agent` + Node built-ins).
295
+
296
+ Subsystems:
269
297
 
270
298
  - **Discovery engine** — multi-server probing with timeouts, retry budgets, and per-server kind detection (llama.cpp, ZINC, DwarfStar, lucebox, LM Studio)
271
299
  - **Router support** — single-model and multi-model llama.cpp modes with per-model status, args parsing and metadata extraction
272
- - **Metrics subsystem** — Prometheus endpoint discovery, polling, and compact widget rendering
300
+ - **Speed & metrics subsystem** — client-side per-token speed measurement (prefill + moving-window generation), throttled footer status updates, and server `/metrics` polling that supplements the footer while the client is idle
273
301
  - **Thinking budgets** — per-model per-level configuration with automatic `reasoning` registration
274
302
  - **Config persistence** — `~/.pi/agent/llamacpp-infra.json` with one-time migration from `local-models.json`
275
303
  - **/proc scanner** — local llama-server process detection for vision, KV cache quant, and drafter flags
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-llamacpp-infra",
3
- "version": "1.2.0",
4
- "description": "Discovery, metrics and control of llama.cpp-family servers for pi: probes any number of machines (localhost, LAN, Tailscale), registers every model into pi's native /model list, and provides live Prometheus metrics, per-model thinking budgets, vision detection and a native config UI. Supports llama.cpp, ZINC, DwarfStar/ds4, lucebox and LM Studio.",
3
+ "version": "1.2.2",
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",
7
7
  "llamacpp",
@@ -28,7 +28,8 @@
28
28
  "pi": {
29
29
  "extensions": [
30
30
  "./src/index.ts"
31
- ]
31
+ ],
32
+ "image": "https://raw.githubusercontent.com/noguerol/llamacpp-infra/main/docs/preview.jpeg"
32
33
  },
33
34
  "files": [
34
35
  "src"
package/src/core.ts ADDED
@@ -0,0 +1,200 @@
1
+ // Core: config persistence, constants, id helpers, shared mutable state.
2
+ // Statically imported by the entrypoint → must stay small.
3
+
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
7
+ import type {
8
+ CompatProfile,
9
+ InfraConfig,
10
+ ModelOptions,
11
+ ServerConfig,
12
+ ServerKind,
13
+ SettingsConfig,
14
+ } from "./types.ts";
15
+
16
+ // ── Provider identity ───────────────────────────────────────────────────────
17
+ export const PROVIDER_NAME = "llamacpp-infra";
18
+ export const STATUS_KEY = "llamacpp-infra";
19
+ export const CONFIG_FILE = "llamacpp-infra.json";
20
+ export const LEGACY_CONFIG_FILE = "local-models.json";
21
+ export const METRICS_STATUS_KEY = "llamacpp-infra-speed";
22
+ export const DEFAULT_API_KEY = "no-auth";
23
+ export const THINKING_BUDGET_FIELD = "thinking_budget_tokens";
24
+
25
+ // ── Settings defaults ───────────────────────────────────────────────────────
26
+ export const DEFAULT_SETTINGS: SettingsConfig = {
27
+ discoveryTimeoutMs: 2000,
28
+ pollIntervalMs: 4000,
29
+ pollMaxMs: 90_000,
30
+ startupGraceMs: 40_000,
31
+ knownGoodFailLimit: 3,
32
+ detectVision: true,
33
+ prefixModelIds: true,
34
+ warmup: true,
35
+ metricsEnabled: true,
36
+ metricsPollMs: 5000,
37
+ includeUnloadedRouterModels: false,
38
+ showBadgesInNames: true,
39
+ };
40
+
41
+ export const DEFAULT_SERVERS: ServerConfig[] = [
42
+ {
43
+ id: "local",
44
+ host: "127.0.0.1",
45
+ label: "Local",
46
+ ports: [8000, 8001, 8002, 8080, 8081, 8082, 1234],
47
+ enabled: true,
48
+ probeDs4: false,
49
+ },
50
+ ];
51
+
52
+ // ── Debug logging ──────────────────────────────────────────────────────────
53
+ export const DEBUG = process.env.PI_LLAMACPP_INFRA_DEBUG === "1" || process.env.PI_LLAMACPP_INFRA_DEBUG === "true";
54
+ export function debugLog(...args: unknown[]): void {
55
+ if (DEBUG) console.debug(`[llamacpp-infra]`, ...args);
56
+ }
57
+
58
+ // ── Config persistence ─────────────────────────────────────────────────────
59
+ export function getConfigPath(): string {
60
+ return join(getAgentDir(), CONFIG_FILE);
61
+ }
62
+
63
+ export function loadConfig(): InfraConfig {
64
+ const defaults: InfraConfig = {
65
+ servers: DEFAULT_SERVERS,
66
+ settings: { ...DEFAULT_SETTINGS },
67
+ modelOptions: {},
68
+ };
69
+ const path = getConfigPath();
70
+ if (existsSync(path)) {
71
+ try {
72
+ const raw = JSON.parse(readFileSync(path, "utf-8")) as Partial<InfraConfig>;
73
+ return {
74
+ servers: Array.isArray(raw.servers) ? raw.servers : defaults.servers,
75
+ settings: { ...DEFAULT_SETTINGS, ...(raw.settings ?? {}) },
76
+ modelOptions: raw.modelOptions ?? {},
77
+ };
78
+ } catch (err) {
79
+ console.error(`[llamacpp-infra] Config load error: ${err}`);
80
+ }
81
+ return defaults;
82
+ }
83
+ const legacyPath = join(getAgentDir(), LEGACY_CONFIG_FILE);
84
+ if (existsSync(legacyPath)) {
85
+ try {
86
+ const raw = JSON.parse(readFileSync(legacyPath, "utf-8")) as Partial<InfraConfig>;
87
+ const migrated: InfraConfig = {
88
+ servers: Array.isArray(raw.servers) ? raw.servers : defaults.servers,
89
+ settings: { ...DEFAULT_SETTINGS, ...(raw.settings ?? {}) },
90
+ modelOptions: raw.modelOptions ?? {},
91
+ };
92
+ debugLog(`migrated legacy config from ${legacyPath}`);
93
+ return migrated;
94
+ } catch {
95
+ // fall through to defaults
96
+ }
97
+ }
98
+ return defaults;
99
+ }
100
+
101
+ export function saveConfig(config: InfraConfig): void {
102
+ try {
103
+ const dir = getAgentDir();
104
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
105
+ writeFileSync(getConfigPath(), JSON.stringify(config, null, 2), "utf-8");
106
+ debugLog(`config saved to ${getConfigPath()}`);
107
+ } catch (err) {
108
+ console.error(`[llamacpp-infra] Config save error: ${err}`);
109
+ }
110
+ }
111
+
112
+ // ── Compat profile ─────────────────────────────────────────────────────────
113
+ export function supportsThinkingBudget(kind: ServerKind | "unknown" | "auto" | undefined): boolean {
114
+ return kind === "llamacpp" || kind === "lucebox";
115
+ }
116
+
117
+ export function makeCompat(kind: ServerKind | "unknown" | "auto"): CompatProfile {
118
+ const usageInStreaming = kind !== "zinc";
119
+ return {
120
+ supportsDeveloperRole: false,
121
+ supportsReasoningEffort: false,
122
+ maxTokensField: "max_tokens" as const,
123
+ supportsUsageInStreaming: usageInStreaming,
124
+ supportsStrictMode: false,
125
+ ...(supportsThinkingBudget(kind) ? { thinkingTokenBudgetField: THINKING_BUDGET_FIELD } : {}),
126
+ };
127
+ }
128
+
129
+ // ── Shared mutable state ───────────────────────────────────────────────────
130
+ // Maps used by hooks + lazy modules. Kept in one place so lazy imports share
131
+ // them via static import of core.ts.
132
+ export const shared = {
133
+ activeConfig: undefined as InfraConfig | undefined,
134
+ zincModelIds: new Set<string>(),
135
+ modelBaseUrls: new Map<string, string>(),
136
+ endpointKinds: new Map<string, string>(),
137
+ serverModelIds: new Map<string, string>(),
138
+ compactModelIds: new Map<string, string>(),
139
+ lastScan: undefined as import("./types.ts").ScanResult | undefined,
140
+ lastModels: [] as import("./types.ts").PiModel[],
141
+ registeredCount: 0,
142
+ lastError: undefined as string | undefined,
143
+ rIncludeUnloaded: false,
144
+ };
145
+
146
+ export function setActiveConfig(c: InfraConfig | undefined): void {
147
+ shared.activeConfig = c;
148
+ }
149
+
150
+ export function modelOptions(): Record<string, ModelOptions> {
151
+ return shared.activeConfig?.modelOptions ?? {};
152
+ }
153
+
154
+ /** Compact id registered in pi for a raw server model id (or the input). */
155
+ export function compactIdFor(modelId: string | undefined): string | undefined {
156
+ if (!modelId) return undefined;
157
+ return shared.compactModelIds.get(modelId) ?? modelId;
158
+ }
159
+
160
+ /** Raw server-side id to send in requests for a registered model id (or the input). */
161
+ export function rawIdFor(modelId: string | undefined): string | undefined {
162
+ if (!modelId) return undefined;
163
+ return shared.serverModelIds.get(modelId) ?? modelId;
164
+ }
165
+
166
+ // ── Thinking-level normalization (used by the per-model budget hook) ──────
167
+ export function normalizeLevel(level: string | undefined): "minimal" | "low" | "medium" | "high" | undefined {
168
+ switch (level) {
169
+ case "minimal":
170
+ case "low":
171
+ case "medium":
172
+ case "high":
173
+ return level;
174
+ case "xhigh":
175
+ case "max":
176
+ return "high";
177
+ default:
178
+ return undefined;
179
+ }
180
+ }
181
+
182
+ // ── Server-label / id helpers ──────────────────────────────────────────────
183
+ export function serverLabel(srv: ServerConfig): string {
184
+ return srv.label?.trim() || srv.host;
185
+ }
186
+
187
+ export function idSafeHost(host: string): string {
188
+ return host.trim().toLowerCase() || "host";
189
+ }
190
+
191
+ export function isLocalHost(host: string): boolean {
192
+ const h = host.trim().toLowerCase();
193
+ return h === "127.0.0.1" || h === "localhost" || h === "::1";
194
+ }
195
+
196
+ export function baseName(p: string): string {
197
+ const cleaned = p.replace(/\\/g, "/");
198
+ const last = cleaned.split("/").pop() || cleaned;
199
+ return last;
200
+ }