pi-lemonade-link 1.0.0

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 ADDED
@@ -0,0 +1,598 @@
1
+ # pi-lemonade-link
2
+
3
+ A [pi](https://github.com/earendil-works/pi-mono) extension that connects pi to a
4
+ self-hosted [Lemonade](https://github.com/lemonade-sdk/lemonade) server — a local
5
+ LLM/audio/image server with GPU acceleration — and keeps that connection in sync
6
+ with whatever the server is actually doing.
7
+
8
+ No hardcoded endpoints, no manual model lists. One extension, five capabilities:
9
+
10
+ 1. **Live model discovery** — every chat model your lemonade box advertises shows
11
+ up in `/model` automatically, annotated with whether it's loaded right now.
12
+ 2. **Nine agent tools** — transcription, image generation/editing/upscaling,
13
+ text-to-speech, music/SFX, 3D meshes, and text classification, all callable
14
+ by the LLM, all model-aware.
15
+ 3. **A setup menu** — `/lemonade-setup` opens a navigable TUI for status,
16
+ discovery, configuration, model management (live-progress pulls, Hugging
17
+ Face installs), and live log streaming.
18
+ 4. **A lemonade status bar** — while a lemonade model is active, an extra
19
+ below-editor row appears (your footer is never touched): instance name,
20
+ tok/s and prefix-cache hit % derived locally from the session's own
21
+ messages, plus polled busy/queue, CPU/GPU/NPU and VRAM from the active
22
+ instance.
23
+ Switching to a non-lemonade model removes the row.
24
+ 5. **Harness self-awareness** — every chat, including virgin ones, is told
25
+ where its lemonade machinery lives, so the agent can operate it directly.
26
+
27
+ ---
28
+
29
+ ## Why this exists
30
+
31
+ Lemonade is a *server*, and its contents change constantly: you load models, unload
32
+ them, pull new ones. pi, by contrast, learns about models from a static file. The
33
+ job of this extension is to erase that gap: every time pi starts, it asks lemonade
34
+ what it currently has, and registers exactly that.
35
+
36
+ ## Design philosophy — why this is built pi-native
37
+
38
+ This extension is not just a feature bridge; every structural choice reflects
39
+ how pi itself works:
40
+
41
+ - **Dynamic truth over static files.** pi learns providers from a static file;
42
+ this extension re-asks the server at every startup and re-registers, so
43
+ `/model` always mirrors reality — down to the `loaded` / `on-demand`
44
+ annotations, which come from live health state, never guesses.
45
+ - **Native tools, not protocol shims.** Every capability is a plain
46
+ `pi.registerTool()` function with a `promptSnippet` — pi's own advertisement
47
+ mechanism, flattened by the harness into the provider's dialect. MCP appears
48
+ only where it's genuinely MCP: lemonade's own gateway ships as an optional,
49
+ disabled stdio entry for *other* MCP-consuming setups. Trusted in-process
50
+ code is never wrapped in JSON-RPC — that would add lifecycle overhead and
51
+ couple failure domains for zero benefit.
52
+ - **Harness-level self-awareness.** The model has no inherent knowledge of the
53
+ machinery it runs on — the harness must *tell* it, in the one place it reads
54
+ every turn. The `promptGuidelines` lines inject the machinery's location
55
+ and the configured instance list into every chat's system prompt, so
56
+ "what's loaded on your server?" resolves deterministically in a virgin
57
+ chat instead of by lucky path-hunting.
58
+ - **Advertisement that reflects reality.** Tool snippets use a `[lemonade]`
59
+ prefix (a corpus-bucket convention, so tools group visibly by origin in the
60
+ prompt), and `classify_text`'s description is *generated per session* from
61
+ the live catalog — the advertisement enumerates the classifiers that
62
+ actually exist on the box, and refreshes itself on every startup.
63
+ - **Everything configurable, nothing hardcoded.** Every endpoint is a path key
64
+ in `lemonade.json`. Moving the server, changing ports, or tracking upstream
65
+ API changes is config, not code.
66
+ - **Live state where it matters.** Pulls stream real progress from
67
+ server-owned download jobs with Esc-to-cancel; the status panel streams the
68
+ full state of the box — every metric from five endpoints, grouped by source,
69
+ re-fetched live while open, cyclable across instances with `n`/`p`; the
70
+ status bar surfaces per-turn tok/s, cache hits, box busyness and VRAM while
71
+ you chat, appearing only when a lemonade model is active; logs stream over
72
+ websocket. Destructive actions gate behind confirm dialogs and a typed
73
+ phrase.
74
+ - **Failures speak.** Missing models produce errors naming what to pull — the
75
+ agent relays and recovers. Never a silent failure.
76
+ - **One entry point.** A single command — `/lemonade-setup` — every capability
77
+ navigable from one TUI menu; no command sprawl.
78
+ - **Ground truth snapshotted.** `docs/` holds the official API spec so this
79
+ extension can be audited against it offline, and the behavior reference
80
+ records what was empirically confirmed on which server build.
81
+
82
+ ## Quick start
83
+
84
+ 1. Make sure your lemonade server is running and reachable.
85
+ 2. Configure an instance — either way works:
86
+ - **Zero-touch:** just start pi. If `~/.pi/agent/lemonade.json` doesn't
87
+ exist, the extension creates a minimal, commented blank one (zero
88
+ servers) and shows an info notice pointing you at `/lemonade-setup`,
89
+ which opens a **first-run wizard**: *Discover servers* (UDP beacon +
90
+ HTTP fallback) or *Add instance manually*. Once the first instance
91
+ exists, everything registers and the normal menu takes over — no
92
+ restart needed.
93
+ - **By hand:** copy the fully-commented `lemonade.example.json` to
94
+ `~/.pi/agent/lemonade.json` and point the first `servers` entry's
95
+ `baseUrl` at your server. Nothing is hardcoded, so the file is the
96
+ source of truth — a config that exists but is unparseable or invalid
97
+ still fails loudly (red error, nothing registered).
98
+ 3. Start pi. Every chat-capable model on the server appears in `/model` as
99
+ `lemonade-<instance-name>/<model-id>` (e.g. `lemonade-main/Qwen3-8B-GGUF`),
100
+ labeled `loaded` or `on-demand`.
101
+ 4. Pick one and chat. Models marked *on-demand* are auto-loaded by lemonade when
102
+ first requested — if the target slot is occupied by a pinned model, lemonade
103
+ replies with a `slots_pinned_error` you'll see as an error message.
104
+ 5. Say *"transcribe `/path/to/file.mp3`"* in any session — the agent calls
105
+ `transcribe_audio` and Whisper returns the text.
106
+ 6. Type `/lemonade-setup` to manage everything interactively.
107
+
108
+ ## How the pieces work
109
+
110
+ ### Model discovery
111
+
112
+ At every startup (and after `/reload`, and after any change made in the setup
113
+ menu), the extension queries two endpoints:
114
+
115
+ - `GET /v1/models` — the catalog. Each entry carries rich metadata:
116
+ `labels` (chat, reasoning, vision, transcription, image…), `recipe`,
117
+ `max_context_window`, `downloaded`, size.
118
+ - `GET /v1/health` — live state: which models are *actually* loaded, per-type
119
+ slot limits, the realtime websocket port.
120
+
121
+ It then registers one pi provider per configured instance, uniformly named
122
+ `lemonade-<instance-name>`, with one pi model per catalog entry, mapped as
123
+ follows:
124
+
125
+ | Lemonade says | pi registers |
126
+ |---|---|
127
+ | `labels` contains `chat` (or `recipe: llamacpp`) | included (by default; see *Chat-only filter*) |
128
+ | `labels` contains `reasoning` | `reasoning: true` |
129
+ | `labels` contains `vision` | image input enabled |
130
+ | `max_context_window` | context window |
131
+ | model appears in `/health` | name suffix `(lemonade-<instance>, loaded)` |
132
+ | not in `/health` | name suffix `(lemonade-<instance>, on-demand)` |
133
+
134
+ If the server is unreachable at startup, the provider registers **nothing** —
135
+ an unreachable model is not listable, so `/model` gets no lemonade entries
136
+ until the box is reachable. (Consequence, accepted: sessions pinned to a
137
+ lemonade-<name> model hard-fail model resolution while offline — "Model not found" —
138
+ and recover on the next startup or `/lemonade-setup` → Refresh once the box
139
+ is back.)
140
+
141
+ **Chat-only filter** — lemonade also hosts transcription (Whisper), image
142
+ generation (Flux), and other non-chat models. pi can only drive chat-completions
143
+ models, so those are filtered out by default. Toggle via *Model management →
144
+ Toggle chat-only filter* if you want them listed anyway.
145
+
146
+ **Compat flags** — the provider is registered with lemonade-appropriate settings
147
+ (`max_tokens` instead of `max_completion_tokens`, `system` instead of `developer`
148
+ role, no `reasoning_effort`), so requests work out of the box.
149
+
150
+ ### Agent tools
151
+
152
+ The extension registers nine tools the LLM can call on your behalf. Two facts
153
+ apply to all of them:
154
+
155
+ **Instance selection.** Every tool takes an optional `server` argument naming
156
+ a configured instance (omit for the default). `"default"` is kept as a
157
+ reserved alias; the default's actual name works too. Unknown names produce an
158
+ error listing available instances. See *Multiple lemonade instances*.
159
+
160
+ **Model selection precedence.** Tools that need a model resolve it in this
161
+ order: (1) an explicit `model` argument in the tool call, (2) the
162
+ extension's configured default (`defaultImageModel`, `defaultUpscaleModel`,
163
+ `defaultTranscriptionModel`, `defaultClassifierModel` in `lemonade.json`),
164
+ (3) auto-pick — the first catalog model carrying the right label (`image`,
165
+ `edit`, `tts`, ...) or recipe (`onnxruntime` for classifiers). Set a default
166
+ in `/lemonade-setup` or config if you want a specific model to win.
167
+
168
+ **How results come back.** Text results (transcripts) are returned inline into
169
+ the conversation. Binary outputs (images, audio, meshes) are *written to
170
+ disk* — `outputDir` (default: the agent's working directory) as
171
+ `lemonade-<kind>-<timestamp>.<ext>` — and the tool returns the saved path.
172
+ The model relays the path to you. To *view* a generated image in the TUI, ask
173
+ the agent to `read` the PNG afterwards: pi renders images inline in supported
174
+ terminals (Kitty, iTerm2, Ghostty, WezTerm, Warp), and reading it also lets
175
+ the model itself see and verify its own output. The TUI cannot render audio
176
+ or `.glb` meshes at all — those live on disk only.
177
+
178
+ Per tool:
179
+
180
+ | Tool | What it does | Model resolution | Output | Caveats |
181
+ |---|---|---|---|---|
182
+ | `transcribe_audio` | Speech → text | `defaultTranscriptionModel` (Whisper-Large-v3), else first catalog model labeled `transcription`/`whisper` | transcript returned inline (not saved) | the transcription API accepts raw wav only — every other format (mp3, mp4, m4a, webm, flac, ...) is converted to 16 kHz mono wav via **ffmpeg** on pi's machine first. Unloaded Whisper auto-loads into the separate transcription slot |
183
+ | `generate_image` | Text → PNG | arg → `defaultImageModel` → first catalog model labeled `image` | PNG on disk + path | First call may include model load (observed ~12 s). Turbo models want `steps: 4`, `cfg_scale: 1`; the tool schema documents this for the model |
184
+ | `edit_image` | Image + prompt → edited PNG | arg → `defaultImageModel` → first model labeled `edit` (then `image`) | PNG on disk + path | multipart upload of the source PNG; optional `mask_path` (white = edit, black = preserve). Edit-capable models on current build: the Flux family |
185
+ | `vary_image` | Image → variation PNG | arg → `defaultImageModel` → first `edit`/`image` model | PNG on disk + path | no prompt parameter — variation is derived from the input image alone |
186
+ | `upscale_image` | Image → 4x PNG | arg → `defaultUpscaleModel` (RealESRGAN-x4plus) | PNG on disk + path | needs an upscale model pulled on the server; `-anime` variant exists for art. Sends the image as base64 JSON, not multipart |
187
+ | `text_to_speech` | Text → spoken audio | arg → first model labeled `tts` (OpenMOSS-TTS on current builds) | mp3/wav on disk + path | if the TTS model isn't pulled, the request can trigger a slow auto-pull that outlasts the request — pull it via the setup menu first. `voice`/`speed`/`response_format` supported |
188
+ | `generate_audio` | Prompt → music / SFX | arg → first model labeled `music`/`sfx`/`audio` | wav on disk + path | ACE-Step for music (optional `lyrics` with `[verse]`/`[chorus]` tags = sung vocals; omit = instrumental), ThinkSound for SFX. Not downloaded by default — pull first |
189
+ | `classify_text` | Text → ranked label confidences | arg → `defaultClassifierModel` → first catalog model with the `onnxruntime` recipe | inline text readout | encoder classifiers (phishing, PII, prompt-injection...) — millisecond inference in its own classification slot; scores returned highest-first, so the top line is the verdict. Pull classifiers via the setup menu or HF install |
190
+ | `generate_3d_model` | Image → textured `.glb` | arg → first model labeled `3d` (TRELLIS-3D) | .glb on disk + path | minutes per mesh; `resolution` 512/1024/1536; needs TRELLIS pulled. TUI cannot render it — open the file in a 3D viewer |
191
+
192
+ **How `classify_text` handles multiple classifiers and modalities.** Each
193
+ classifier's label universe (phishing/benign, PII/no-PII, 1–5 stars…) is baked
194
+ into the model at training time — the tool is a pipe, not a policy. Three
195
+ layers make multi-classifier use workable: (1) the tool's prompt advertisement
196
+ dynamically enumerates the classifiers actually on the server each session;
197
+ (2) per-call selection via the `model` argument (or `defaultClassifierModel`
198
+ config); (3) runtime self-description — the response's labels reveal the
199
+ chosen model's universe, so a mismatched question ("is this a hot dog?" sent
200
+ to a phishing model) returns an obviously wrong label set and the agent
201
+ retries with the right model. Modality boundaries are architectural:
202
+ `/v1/classify` serves *text encoders only* — image classification routes to
203
+ vision-capable chat models (read the image), and audio classifies only after
204
+ `transcribe_audio`. Sequence-classification models return the flat ranked
205
+ scores; token-classification models (e.g. PII *span* detectors) return
206
+ additional structure, which the tool passes through verbatim.
207
+
208
+ Missing-model behavior is uniform: if no suitable model exists on the server,
209
+ the tool returns a clear error naming what to pull — the agent relays it and
210
+ suggests pulling via `/lemonade-setup`. Never a silent failure.
211
+
212
+ Lemonade also exposes **realtime transcription** over a websocket
213
+ (`ws://<host>:<ws-port>/realtime?model=…`; the port comes from `/health`).
214
+ This extension does not wire that up — it's for live-mic streaming, not file
215
+ transcription.
216
+
217
+ ### Agent self-awareness
218
+
219
+ Every chat's system prompt — including virgin chats with no lemonade context —
220
+ carries three guideline lines injected by this extension (via pi's
221
+ `promptGuidelines` mechanism, attached to `transcribe_audio`):
222
+
223
+ - where the machinery lives: `~/.pi/agent/lemonade.json` (base URL, endpoints,
224
+ defaults) and this README + `docs/` (full reference)
225
+ - what to do with it: query the server's HTTP API directly via `bash` +
226
+ `curl` for state questions (loaded models, health, host resources), and
227
+ point the user at `/lemonade-setup` for interactive management
228
+ - which instances exist: the configured fleet, by name and URL, and that
229
+ tools take an optional `server` argument (this line is generated per session)
230
+
231
+ So asking the agent "what models are loaded on your server?" in a fresh chat
232
+ resolves deterministically: read the config, curl `/v1/health`, summarize —
233
+ no hunting for paths, no lucky guesses.
234
+
235
+ ### Lemonade status bar (below-editor widget)
236
+
237
+ While a **lemonade model is active**, the extension adds one extra row below
238
+ the editor (pi's `setWidget` mechanism, `belowEditor` placement). It is a
239
+ separate slot from the footer: the built-in footer and any other extension's
240
+ custom footer are **never touched**, and the row disappears the moment a
241
+ non-lemonade model is selected. `session_start` and `model_select` events drive
242
+ the lifecycle; the instance name comes from the active model's `provider`
243
+ field (`lemonade-<instance>` — pi keeps `provider` and `id` separate; the
244
+ `lemonade-<instance>/<model>` form is picker-display only), so switching
245
+ between instances swaps the row onto the new box automatically. If the
246
+ model isn't resolvable at session start, the first turn re-checks.
247
+
248
+ ```text
249
+ 🍋 stackshack · 14.9 tok/s · cache-hit 99% · idle · CPU 2% · GPU 2% · NPU 0% · VRAM 38.8G
250
+ ```
251
+
252
+ The layout is **fixed** — every segment is present from the first render with
253
+ `…` placeholders until its value lands, and segments never appear or vanish
254
+ afterwards. Narrow terminals truncate the tail (VRAM first).
255
+
256
+ | Segment | Source | Cost |
257
+ |---|---|---|
258
+ | `🍋 <instance>` | the active model's `provider` field | none |
259
+ | `<n> tok/s` | the session's own `message_start` → `message_end` timing over `usage.output` — always *your* stream, immune to the server's last-client stats | none (event-driven, updates as each message completes) |
260
+ | `cache-hit <pct>%` | the session's own `usage`: pi-ai maps llama.cpp prefix-cache hits onto `usage.cacheRead` (and `usage.input` is the uncached remainder), so the hit rate is `cacheRead / (cacheRead + input)` per request — never another client's numbers | none (per message) |
261
+ | `idle` / `busy` / `busy ·Nq` / `offline` | polled `/v1/health` (`is_busy`/`is_streaming`) + `/metrics` queue depth (`requests_processing` + `requests_deferred`); `offline` in warning color when the box can't be reached | polled every `barPollMs` |
262
+ | `CPU <n>%` · `GPU <n>%` · `NPU <n>%` · `VRAM <n>G` | polled `/v1/system-stats` (`cpu_percent`, `gpu_percent`, `npu_percent`, `vram_gb`). A box whose system-stats reports `npu_percent: null` drops the NPU segment permanently (decided once on first poll — no flicker); an unreachable box keeps the last-known gauge values under the `offline` marker | polled every `barPollMs` |
263
+
264
+ Notes:
265
+
266
+ - **tok/s is the last *completed* message's rate** — it includes TTFT, so it reads slightly conservative vs. llama.cpp's own tps. It does not animate mid-generation (the working spinner covers that).
267
+ - **`barPollMs: 0`** drops the polled segments; the row degrades to instance + tok/s + cache-hit %.
268
+ - **`statusBar: false`** disables the row entirely.
269
+ - A poll in flight when you switch models is discarded — the new instance's row never shows the old box's numbers.
270
+
271
+ ### `/lemonade-setup` menu
272
+
273
+ A navigable TUI (↑↓ / enter / esc). On a fresh install (zero instances
274
+ configured), `/lemonade-setup` instead opens a **first-run wizard** —
275
+ *Discover servers* (UDP beacon + HTTP fallback) or *Add instance manually* —
276
+ and the normal menu takes over once the first instance exists.
277
+
278
+ | Menu | What you can do |
279
+ |---|---|
280
+ | **Server status** | The complete state of the box, grouped by the endpoint each metric came from — every server metric flat on one **live** panel that re-fetches every `statusPollMs` (default 2 s) while open, with each snapshot timestamped in the header; a transient fetch failure keeps the last good snapshot. Scrollable (wrap-aware `↑↓/j/k/pgup/pgdn/home/end` scrolling + mouse wheel, `q`/`esc`/`enter` return; position indicator in the footer). With multiple instances configured, `n`/`p` cycles between boxes without leaving the view — the title names the instance shown, and the menu's active instance follows it on exit. Sections: `/v1/health` (loaded models with busy/streaming/backend-health/pid/slot-pool, per-type slot limits, pinned counts, telemetry state), `/v1/system-stats` (CPU/RAM used-total-%, GPU, VRAM, NPU), `/v1/stats` (last-request tok/s, TTFT, in/out tokens, prefix-cache hit rate, lifetime token/request counters, routing decisions), `/v1/system-info` (CPU cores/threads, OS, GPU/NPU inventory incl. NPU power mode and TOPS, model-storage drive usage), and `/metrics` (llama.cpp backend: request queue depth, busy slots per decode, peak sequence length). Each section degrades to an "(unreachable)" line independently — one dead endpoint never blanks the panel |
281
+ | **Server settings** | Discover servers (UDP beacon + HTTP fallback), edit base URL, edit API key, test connection |
282
+ | **Model management** | List catalog (including not-yet-downloaded registry entries), load / unload, pull (download) with live progress + esc-to-cancel, install from Hugging Face (search → pick variant → install as `user.*`), delete, change context size (unload → reload with `ctx_size`, saved), toggle chat-only filter, refresh provider |
283
+ | **Transcription settings** | Default Whisper model, endpoint path, smoke-test a file |
284
+ | **Live server logs** | WebSocket stream of the server's log: full snapshot backlog (up to 5000 entries) then live entries as they happen; Esc to close |
285
+
286
+ Notes on the destructive and slow paths:
287
+
288
+ - **Delete** requires two gates: a confirm dialog, then typing the exact phrase
289
+ `i want to delete <model-id>` — three attempts allowed, each prompt shows
290
+ chances remaining, and Esc aborts immediately.
291
+ - **Change context size** prefills the model's *current* context (from live
292
+ health), accepts `32k`, `1m`, or raw token counts, and warns that the model
293
+ is unloaded and reloaded (the new `ctx_size` is saved for future loads).
294
+ - **Pull** uses lemonade's server-owned download jobs: a live progress view
295
+ (percent, bytes, per-file status, updated every second) with **Esc to cancel**
296
+ (via `/v1/downloads/control`). Blocking pull is kept only as a fallback for
297
+ older servers without job support.
298
+ - **Install from Hugging Face** searches the registry through the server
299
+ (`/v1/registry/search`), lists quantization variants with sizes
300
+ (`/v1/pull/variants`), and installs the chosen one as a `user.*` model —
301
+ including vision/mmproj detection — with the same live progress view.
302
+
303
+ Every change is saved to the config file and re-registers the provider
304
+ immediately — no restart or `/reload` needed.
305
+
306
+ **Server discovery** — lemonade broadcasts a JSON beacon
307
+ (`{"service":"lemonade","hostname":…,"url":…}`) roughly once per second on UDP
308
+ port 13305. The setup menu can listen for it, then falls back to probing common
309
+ ports on localhost *and* the currently configured host.
310
+
311
+ > **WSL2 note:** UDP LAN broadcasts do not propagate into WSL2's NAT'd virtual
312
+ > network, so the beacon may never arrive if pi runs inside WSL2. The HTTP
313
+ > fallback still works, and manual URL entry always works.
314
+
315
+ ## Configuration
316
+
317
+ Everything lives in `~/.pi/agent/lemonade.json` — and it is **created for
318
+ you on first run**: if the file is missing, the extension writes a minimal,
319
+ commented blank config (zero servers) instead of blocking, shows an info
320
+ notice pointing at `/lemonade-setup`, and opens a first-run wizard there
321
+ (discovery or manual entry). A config that exists but is unparseable or
322
+ invalid still fails loudly at startup (red error in the chat window, nothing
323
+ registered), because no endpoint is hardcoded and a broken file must not
324
+ target a server that isn't yours.
325
+
326
+ The tracked `lemonade.example.json` in this folder is the canonical
327
+ reference: fully commented, documenting every key and its default, and
328
+ doubly useful as a template:
329
+
330
+ ```bash
331
+ cp ~/.pi/agent/extensions/pi-lemonade-link/lemonade.example.json ~/.pi/agent/lemonade.json
332
+ ```
333
+
334
+ Edit the first `servers` entry's `baseUrl`, trim the rest to taste, restart pi.
335
+ `//` comments are allowed in the config file (the loader strips them);
336
+ note that saving via `/lemonade-setup` rewrites the file and removes
337
+ comments, so keep annotations in your copy of the example. Changes made in
338
+ `/lemonade-setup` apply immediately — no restart or `/reload` needed.
339
+
340
+ A minimal working config is one entry:
341
+
342
+ ```json
343
+ { "servers": [{ "name": "main", "baseUrl": "http://your-lemonade-server:13305" }] }
344
+ ```
345
+
346
+ The full shape (values shown are the built-in defaults):
347
+
348
+ ```json
349
+ {
350
+ "servers": [
351
+ { "name": "main", "baseUrl": "http://localhost:13305",
352
+ "description": "Primary lemonade server." }
353
+ ],
354
+ "apiKey": "lemonade",
355
+ "chatPath": "/api/v1",
356
+ "modelsPath": "/v1/models",
357
+ "healthPath": "/v1/health",
358
+ "loadPath": "/v1/load",
359
+ "unloadPath": "/v1/unload",
360
+ "pullPath": "/v1/pull",
361
+ "deletePath": "/v1/delete",
362
+ "downloadsPath": "/v1/downloads",
363
+ "downloadsControlPath": "/v1/downloads/control",
364
+ "registrySearchPath": "/v1/registry/search",
365
+ "pullVariantsPath": "/v1/pull/variants",
366
+ "transcriptionPath": "/v1/audio/transcriptions",
367
+ "imageGenerationPath": "/v1/images/generations",
368
+ "imageEditPath": "/v1/images/edits",
369
+ "imageVariationPath": "/v1/images/variations",
370
+ "imageUpscalePath": "/v1/images/upscale",
371
+ "speechPath": "/v1/audio/speech",
372
+ "audioGenerationPath": "/v1/audio/generations",
373
+ "mesh3dPath": "/v1/3d/generations",
374
+ "classifyPath": "/v1/classify",
375
+ "beaconPort": 13305,
376
+ "chatOnly": true,
377
+ "defaultTranscriptionModel": "Whisper-Large-v3",
378
+ "defaultImageModel": "",
379
+ "defaultUpscaleModel": "RealESRGAN-x4plus",
380
+ "defaultClassifierModel": "",
381
+ "outputDir": "",
382
+ "discoveryTimeoutMs": 5000,
383
+ "beaconTimeoutMs": 3000,
384
+ "loadTimeoutMs": 300000,
385
+ "pullTimeoutMs": 1800000,
386
+ "transcriptionTimeoutMs": 300000,
387
+ "generationTimeoutMs": 600000,
388
+ "statusPollMs": 2000,
389
+ "statusBar": true,
390
+ "barPollMs": 5000
391
+ }
392
+ ```
393
+
394
+ | Key | Meaning |
395
+ |---|---|
396
+ | `servers` | **Required** — the only instance store: `[{ "name", "baseUrl", "apiKey"?, "description"? }]`. May be empty on a fresh install (the first `/lemonade-setup` run offers a wizard); the FIRST entry is the default instance (targeted when tools' `server` argument is omitted); reordering changes the default. Each registers as a `lemonade-<name>` provider. |
397
+ | `apiKey` | Shared fallback `Authorization: Bearer …` for instances without their own. A dummy value works for auth-less servers (pi requires non-empty auth to list models). |
398
+ | `*Path` | API endpoint paths, in case a future lemonade changes them. |
399
+ | `chatOnly` | Filter the provider down to chat-capable models. |
400
+ | `defaultTranscriptionModel` / `defaultImageModel` / `defaultUpscaleModel` / `defaultClassifierModel` | Fixed model ids for the tools. Empty = auto-pick from the live catalog (by label, or by `onnxruntime` recipe for classifiers). |
401
+ | `outputDir` | Where generated files (images, audio, meshes) are saved. Empty = agent's working directory. |
402
+ | `*TimeoutMs` | Per-operation timeouts. `pullTimeoutMs` only caps the *blocking* pull fallback — normal pulls use download jobs with live progress. |
403
+ | `statusPollMs` | How often the setup-menu status panel re-fetches while open (live refresh). `0` = static snapshot. |
404
+ | `statusBar` | Show the below-editor lemonade status bar while a lemonade model is active (see *Lemonade status bar*). |
405
+ | `barPollMs` | Server-poll cadence for the status bar's busy/queue + CPU/GPU/NPU/VRAM segments. `0` = local metrics only (instance, tok/s, cache-hit %). |
406
+
407
+ ## Offline & unreachable networks
408
+
409
+ When pi starts on a network with no route back to the lemonade LAN (different
410
+ WAN, VPN down, box asleep):
411
+
412
+ - **Startup never blocks longer than `discoveryTimeoutMs`** (~5 s by default —
413
+ lower it in config if you work offline often).
414
+ - **Only the lemonade provider is affected** — every other provider in
415
+ `/model` (built-ins, other extensions) is untouched.
416
+ - **No lemonade listings when unreachable.** If an instance can't be reached
417
+ at startup or refresh, it contributes nothing to `/model` — an unreachable
418
+ model is not listable. This is fully automated in both directions: entries
419
+ appear exactly when the box is reachable and disappear when it isn't
420
+ (until the next startup or Refresh re-checks).
421
+ **The accepted consequence:** sessions or scripts pinned to
422
+ `lemonade-<name>/<model>` hard-fail model resolution while offline
423
+ (`Error: Model "lemonade/<id>" not found`), and resume working once the box
424
+ is back and a Refresh has re-registered it. Non-reachable chat requests are
425
+ the honest outcome; phantom listings are never shown.
426
+ - **Tools fail with actionable text, not cryptic errors**: every tool detects
427
+ the unreachable server and returns the same message — *where* it tried to
428
+ connect and *what to do about it* (reconnect, or point the extension at a
429
+ reachable instance via `/lemonade-setup`). No misleading "pull a model
430
+ first" advice when the real problem is the network, and no bare
431
+ `fetch failed`.
432
+ - **The setup menu diagnoses it**: Server status shows an explicit
433
+ `Unreachable: <health URL>` panel, and Test connection reports the failure.
434
+ - **The agent can self-diagnose**: the self-awareness guidelines point it at
435
+ the config, so asking the agent "are you connected to lemonade?" resolves
436
+ via a direct probe rather than guessing.
437
+
438
+ **Levels of availability checking** (each with one job, each defined once):
439
+
440
+ | Level | When | What it does | Where the logic lives |
441
+ |---|---|---|---|
442
+ | Registration probe | every startup / refresh | decides what goes into the provider: the live catalog, or nothing at all when unreachable — no phantom listings | `registerInstanceProvider` |
443
+ | Tool fetch | every tool call | converts network rejections into the actionable unreachable message; user-cancels pass through | `toolFetch()` — used by all nine tools |
444
+ | Auto-pick | model resolution | distinguishes unreachable (`null`) from no-match (`undefined`) so advice fits the situation | `pickModelByLabels` / `pickModelByRecipe` |
445
+ | Instance resolution | every tool call with a `server` arg | resolves the named box's config view or returns an error listing known instances | `instanceView()` |
446
+
447
+ Tools stay deliberately stateless — reachability is a per-call fact (networks
448
+ change mid-session), so no connection state is cached between calls. The only
449
+ repetition is the three-line binding guard at the top of each tool, which is
450
+ what makes the rest of each tool body instance- and failure-aware for free.
451
+
452
+ ## Multiple lemonade instances
453
+
454
+ The extension supports **simultaneous multi-instance use**. Each configured
455
+ lemonade box becomes its own pi provider, every tool can target any instance,
456
+ and the setup menu operates per-instance.
457
+
458
+ **Configuration** — there is no separate default instance: `servers[]` is
459
+ the *only* instance store, and the first entry IS the default. Every entry
460
+ has a **name** (required) and an optional **description** (free-form context
461
+ for your own reminder's sake). The shared top-level `apiKey` applies to any
462
+ entry that doesn't define its own:
463
+
464
+ ```json
465
+ {
466
+ "servers": [
467
+ { "name": "main", "baseUrl": "http://192.168.1.10:13305",
468
+ "description": "Primary lemonade server." },
469
+ { "name": "second-server", "baseUrl": "http://192.168.1.20:13305",
470
+ "description": "The other box, mostly for experiments." }
471
+ ]
472
+ }
473
+ ```
474
+
475
+ Names identify instances everywhere, without prejudice: provider ids, model
476
+ addresses, and picker brackets are uniformly `lemonade-<name>` for EVERY
477
+ instance — the default reads `[lemonade-main]` exactly like an extra
478
+ reads `[lemonade-second-server]`. Descriptions are free-form context shown in the
479
+ instance lists; edit both via *Manage instances → Edit instance*. Renaming
480
+ any instance changes its provider id (and re-registers it), so pinned
481
+ sessions and `--model lemonade-<old-name>/...` references must follow the
482
+ rename — the cost of a perfectly uniform namespace.
483
+
484
+ **How instances are exposed:**
485
+
486
+ - **Providers:** every instance registers as `lemonade-<name>` — the default
487
+ included, no special case. Picker brackets, model addresses, and provider
488
+ column all read `lemonade-<name>` uniformly. Each is independently annotated `loaded` / `on-demand`
489
+ from that box's own health state. Unreachable boxes register nothing —
490
+ there are no offline placeholders for any instance.
491
+ - **Tools:** every tool takes an optional `server` argument naming an
492
+ instance. Omitted → default. Unknown names return an error listing the
493
+ available instances — never a wrong-box dial. Model auto-pick, status, and
494
+ everything else resolve against the selected instance's catalog.
495
+ - **Self-awareness:** the prompt guideline enumerates the configured
496
+ instances by name and URL every session, so virgin chats know the fleet
497
+ without hunting.
498
+ - **Setup menu:** a *Switch instance* item (appears when extras exist) picks
499
+ which box Status, Model management, and Live logs act on — and the status
500
+ panel itself cycles instances in-place with `n`/`p`, syncing the menu's
501
+ active instance on exit; Server settings → *Manage instances* lists (with
502
+ live reachability), adds, and removes instances; discovered servers can be
503
+ registered as default or as a named instance.
504
+
505
+ **The one-place rule:** instance semantics are defined exactly once —
506
+ `instanceView()` returns a *config view* (same settings, swapped
507
+ baseUrl/apiKey) that flows through every existing helper unchanged, so tools,
508
+ discovery, TUI actions, and error handling all inherit instance behavior
509
+ without per-tool logic.
510
+
511
+ ## Reference documentation (API ground truth)
512
+
513
+ This folder's [`docs/`](./docs) directory contains the complete official
514
+ Lemonade endpoint specification, snapshotted from the upstream repo
515
+ (`lemonade-sdk/lemonade`, `docs/api/`) on 2026-09-12 — the same markdown the
516
+ [docs site](https://lemonade-server.ai/docs/api/) renders. It is the source of
517
+ ground truth for every endpoint, parameter, and response shape this extension
518
+ uses:
519
+
520
+ | File | Contents |
521
+ |---|---|
522
+ | [`docs/README.md`](./docs/README.md) | Spec index and design philosophy |
523
+ | [`docs/openai.md`](./docs/openai.md) | OpenAI-compatible surface: chat/completions, embeddings, audio (transcription, speech, generations), images (generation/edit/variation/upscale), realtime, `/v1/models` + labels taxonomy |
524
+ | [`docs/lemonade.md`](./docs/lemonade.md) | Lemonade-specific surface: load/unload/pull/delete, download jobs + control, registry search + pull variants, health/stats/system-stats/system-info, classify, audio/3D generation, log streaming, job engine, install/uninstall |
525
+ | [`docs/mcp.md`](./docs/mcp.md) | The MCP gateway (lemonade as an MCP server) |
526
+ | [`docs/ollama.md`](./docs/ollama.md), [`docs/anthropic.md`](./docs/anthropic.md), [`docs/llamacpp.md`](./docs/llamacpp.md) | Other compatibility surfaces (rerank, slots, etc.) |
527
+
528
+ When lemonade updates, re-sync with:
529
+
530
+ ```bash
531
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/lemonade-sdk/lemonade /tmp/lemonade \
532
+ && cd /tmp/lemonade && git sparse-checkout set docs/api \
533
+ && cp docs/api/*.md ~/.pi/agent/extensions/pi-lemonade-link/docs/
534
+ ```
535
+
536
+ ## Lemonade behavior reference
537
+
538
+ Empirically confirmed against lemonade 11.7.0 (cross-checked with the
539
+ [official endpoint spec](https://lemonade-server.ai/docs/api/)):
540
+
541
+ - **Auto-loading.** A request for an unloaded model triggers an automatic load
542
+ attempt. If the model's slot is free or holds an unpinned model, lemonade
543
+ evicts/loads and serves the request. If the slot is occupied by a *pinned*
544
+ model, you get `slots_pinned_error: "All loaded models of type … are pinned.
545
+ Unload a model first."` — unload or unpin via the setup menu, then retry.
546
+ - **Per-type slots.** The server holds one slot per model type (llm,
547
+ transcription, image, embedding, …), so Whisper can be loaded alongside your
548
+ chat model.
549
+ - **change-ctx.** Implemented as unload → reload with `ctx_size` +
550
+ `save_options: true`, which persists the choice for future loads.
551
+ - **Status panel sources.** All five metric endpoints are live-confirmed:
552
+ `/v1/health` (incl. per-model `is_busy`/`is_streaming`/`backend_health`/
553
+ `slot_pool`/`residency_class`/`last_use`/`pid` and `pinned_models` counters),
554
+ `/v1/system-stats`, `/v1/stats` (cumulative `*_total` counters + per-request
555
+ prefix-cache hits via `cache_tokens`), `/v1/system-info` (hardware inventory
556
+ + `model_storage` drive usage), and root-level `/metrics` (Prometheus text;
557
+ `lemonade_llamacpp_*` backend gauges incl. `requests_processing`/
558
+ `requests_deferred` queue depth and `n_tokens_max` peak sequence length).
559
+ Caveats seen in the field: the Linux `amd_gpu` device `name` can come back as
560
+ a raw device id (the panel falls back to `family`), and `/metrics` requires
561
+ the bearer key when `LEMONADE_API_KEY` is set. Power draw in watts is *not*
562
+ exposed by any lemonade endpoint — utilization only (`*_percent`).
563
+
564
+ ## Troubleshooting
565
+
566
+ | Symptom | Fix |
567
+ |---|---|
568
+ | No `lemonade-<name>/*` models in `/model` | `/lemonade-setup` → Server settings → Test connection; check `baseUrl`; run Refresh |
569
+ | Error: `slots_pinned_error` on first message | The pinned model occupies the slot — unload it (Model management), or select the loaded model |
570
+ | `transcribe_audio` "not available" from the agent | Tools need a `promptSnippet` to appear in the system prompt — that's pi behavior, not a bug here; this extension sets it |
571
+ | Transcription of `.mp4` fails with ffmpeg missing | Install ffmpeg on the machine running pi (container formats are converted locally before upload) |
572
+ | Discovery finds nothing | Expected inside WSL2 (UDP broadcasts don't cross the NAT); set the URL manually |
573
+ | Server unreachable at startup | The instance registers nothing — no lemonade models in `/model` until it's reachable; fix the URL and Refresh |
574
+ | Pull seems to hang | It's downloading — the blocking pull path sends no progress. Large models take minutes; the `pullTimeoutMs` cap guarantees control returns |
575
+ | Status panel shows "(unreachable)" for some sections | That one endpoint failed for this box — `/metrics` in particular requires the API key when the server sets `LEMONADE_API_KEY`. The rest of the panel stays live |
576
+ | Status panel never refreshes | `statusPollMs` is `0` in the config — that disables live refresh. Set it to a poll interval in ms (default 2000) |
577
+
578
+ ## MCP Gateway (lemonade as an MCP server)
579
+
580
+ Lemonade exposes itself as a genuine MCP server at `POST /mcp`
581
+ (Streamable HTTP), serving five tools: `lemonade_list_models`, `lemonade_chat`,
582
+ `lemonade_transcribe_audio`, `lemonade_generate_image`, and `lemonade_omni`.
583
+
584
+ pi's MCP manager only accepts *remote* MCP endpoints over HTTPS, so this
585
+ setup ships a tiny local bridge instead:
586
+
587
+ - `~/.pi/agent/bin/lemonade-mcp-proxy.mjs` — stdio→HTTP proxy: reads
588
+ newline-delimited JSON-RPC on stdin, forwards each message to the lemonade
589
+ gateway, writes responses to stdout (SSE frames unwrapped, session id
590
+ passed through).
591
+ - `~/.pi/agent/mcps-local/lemonade/server.json` — MCP manager entry
592
+ (transport: stdio, connection: lazy, **disabled by default**).
593
+
594
+ To use it: open `/mcp` in pi, pick **lemonade**, choose *Test or refresh*,
595
+ inspect the tool manifest, select the tools you want, and enable the server.
596
+ The first approved call starts the connection. Note the overlap: these MCP
597
+ tools duplicate what this extension registers natively — enable them only if
598
+ you want lemonade tools available in *other* MCP-consuming setups.
package/docs/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # Lemonade Endpoints Spec
2
+
3
+ The Lemonade HTTP service provides a wide array of standards-compliant and custom endpoints.
4
+
5
+ Our design philosophy is:
6
+
7
+ 1. Ensure that Lemonade works out-of-box with all popular local AI apps.
8
+ 2. Prioritize using a pre-existing standard for functionality whenever possible.
9
+ 3. Add sufficient custom functionality to enable developers to build highly polished experiences.
10
+
11
+ This spec details all supported endpoints. It is organized into pages that correspond to which organization (OpenAI, Ollama, Lemonade, etc.) defined the endpoints.
12
+
13
+ | API | Description |
14
+ |-----|-------------|
15
+ | [OpenAI-Compatible API](./openai.md) | Start here for the main API surface used by most SDKs and clients. |
16
+ | [Ollama-Compatible API](./ollama.md) | Use this if your client expects Ollama-style behavior and routes. |
17
+ | [Anthropic-Compatible API](./anthropic.md) | Use this for clients built around Anthropic's message format. |
18
+ | [MCP Gateway](./mcp.md) | Use this to expose Lemonade as a Model Context Protocol server (POST /mcp). |
19
+ | [llama.cpp-Specific API](./llamacpp.md) | Reference for llama.cpp-specific compatibility and conventions. |
20
+ | [Lemonade-Specific API](./lemonade.md) | Local-first API for managing lifecycle, configuration, backends, etc. |
21
+
22
+ A running server also serves these pages itself: [`GET /v1/docs`](./lemonade.md#get-v1docs) lists what it has and [`GET /v1/docs/{page}`](./lemonade.md#get-v1docspage) returns one, so the reference always matches the installed version and stays available offline.
@@ -0,0 +1,11 @@
1
+ # Anthropic-Compatible API
2
+
3
+ Lemonade supports an initial Anthropic Messages compatibility endpoint for applications that call Claude-style APIs.
4
+
5
+ | Endpoint | Status | Notes |
6
+ |----------|--------|-------|
7
+ | `POST /v1/messages` | Supported | Supports both streaming and non-streaming. Query params like `?beta=true` are accepted. |
8
+
9
+ Current scope focuses on message generation parity for common fields (`model`, `messages`, `system`, `max_tokens`, `temperature`, `stream`, and basic `tools`). Unsupported or unimplemented Anthropic-specific fields are ignored and surfaced via warning logs/headers.
10
+
11
+ Those limits apply only where Lemonade converts to and from the OpenAI shape. A cloud provider registered with `--wire-format anthropic` is relayed unconverted, so no field is dropped — see [Cloud Offload](../guide/configuration/cloud.md#providers-that-speak-the-anthropic-messages-format).