local-lemonade 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,520 @@
1
+ # local-lemonade
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, four 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. **Harness self-awareness** — every chat, including virgin ones, is told
19
+ where its lemonade machinery lives, so the agent can operate it directly.
20
+
21
+ ---
22
+
23
+ ## Why this exists
24
+
25
+ Lemonade is a *server*, and its contents change constantly: you load models, unload
26
+ them, pull new ones. pi, by contrast, learns about models from a static file. The
27
+ job of this extension is to erase that gap: every time pi starts, it asks lemonade
28
+ what it currently has, and registers exactly that.
29
+
30
+ ## Design philosophy — why this is built pi-native
31
+
32
+ This extension is not just a feature bridge; every structural choice reflects
33
+ how pi itself works:
34
+
35
+ - **Dynamic truth over static files.** pi learns providers from a static file;
36
+ this extension re-asks the server at every startup and re-registers, so
37
+ `/model` always mirrors reality — down to the `loaded` / `on-demand`
38
+ annotations, which come from live health state, never guesses.
39
+ - **Native tools, not protocol shims.** Every capability is a plain
40
+ `pi.registerTool()` function with a `promptSnippet` — pi's own advertisement
41
+ mechanism, flattened by the harness into the provider's dialect. MCP appears
42
+ only where it's genuinely MCP: lemonade's own gateway ships as an optional,
43
+ disabled stdio entry for *other* MCP-consuming setups. Trusted in-process
44
+ code is never wrapped in JSON-RPC — that would add lifecycle overhead and
45
+ couple failure domains for zero benefit.
46
+ - **Harness-level self-awareness.** The model has no inherent knowledge of the
47
+ machinery it runs on — the harness must *tell* it, in the one place it reads
48
+ every turn. The `promptGuidelines` lines inject the machinery's location
49
+ and the configured instance list into every chat's system prompt, so
50
+ "what's loaded on your server?" resolves deterministically in a virgin
51
+ chat instead of by lucky path-hunting.
52
+ - **Advertisement that reflects reality.** Tool snippets use a `[lemonade]`
53
+ prefix (a corpus-bucket convention, so tools group visibly by origin in the
54
+ prompt), and `classify_text`'s description is *generated per session* from
55
+ the live catalog — the advertisement enumerates the classifiers that
56
+ actually exist on the box, and refreshes itself on every startup.
57
+ - **Everything configurable, nothing hardcoded.** Every endpoint is a path key
58
+ in `lemonade.json`. Moving the server, changing ports, or tracking upstream
59
+ API changes is config, not code.
60
+ - **Live state where it matters.** Pulls stream real progress from
61
+ server-owned download jobs with Esc-to-cancel; status shows host resources
62
+ and last-request performance; logs stream over websocket. Destructive
63
+ actions gate behind confirm dialogs and a typed phrase.
64
+ - **Failures speak.** Missing models produce errors naming what to pull — the
65
+ agent relays and recovers. Never a silent failure.
66
+ - **One entry point.** A single command — `/lemonade-setup` — every capability
67
+ navigable from one TUI menu; no command sprawl.
68
+ - **Ground truth snapshotted.** `docs/` holds the official API spec so this
69
+ extension can be audited against it offline, and the behavior reference
70
+ records what was empirically confirmed on which server build.
71
+
72
+ ## Quick start
73
+
74
+ 1. Make sure your lemonade server is running and reachable.
75
+ 2. Create the config: copy the fully-commented `lemonade.example.json`
76
+ (in this folder) to `~/.pi/agent/lemonade.json` and point the first
77
+ `servers` entry's `baseUrl` at your server. This file is **required** —
78
+ the extension fails loudly and registers nothing without it, because
79
+ nothing is hardcoded.
80
+ 3. Start pi. Every chat-capable model on the server appears in `/model` as
81
+ `lemonade-<instance-name>/<model-id>` (e.g. `lemonade-main/Qwen3-8B-GGUF`),
82
+ labeled `loaded` or `on-demand`.
83
+ 4. Pick one and chat. Models marked *on-demand* are auto-loaded by lemonade when
84
+ first requested — if the target slot is occupied by a pinned model, lemonade
85
+ replies with a `slots_pinned_error` you'll see as an error message.
86
+ 5. Say *"transcribe `/path/to/file.mp3`"* in any session — the agent calls
87
+ `transcribe_audio` and Whisper returns the text.
88
+ 6. Type `/lemonade-setup` to manage everything interactively.
89
+
90
+ ## How the pieces work
91
+
92
+ ### Model discovery
93
+
94
+ At every startup (and after `/reload`, and after any change made in the setup
95
+ menu), the extension queries two endpoints:
96
+
97
+ - `GET /v1/models` — the catalog. Each entry carries rich metadata:
98
+ `labels` (chat, reasoning, vision, transcription, image…), `recipe`,
99
+ `max_context_window`, `downloaded`, size.
100
+ - `GET /api/v1/health` — live state: which models are *actually* loaded, per-type
101
+ slot limits, the realtime websocket port.
102
+
103
+ It then registers one pi provider per configured instance, uniformly named
104
+ `lemonade-<instance-name>`, with one pi model per catalog entry, mapped as
105
+ follows:
106
+
107
+ | Lemonade says | pi registers |
108
+ |---|---|
109
+ | `labels` contains `chat` (or `recipe: llamacpp`) | included (by default; see *Chat-only filter*) |
110
+ | `labels` contains `reasoning` | `reasoning: true` |
111
+ | `labels` contains `vision` | image input enabled |
112
+ | `max_context_window` | context window |
113
+ | model appears in `/health` | name suffix `(lemonade-<instance>, loaded)` |
114
+ | not in `/health` | name suffix `(lemonade-<instance>, on-demand)` |
115
+
116
+ If the server is unreachable at startup, the provider registers **nothing** —
117
+ an unreachable model is not listable, so `/model` gets no lemonade entries
118
+ until the box is reachable. (Consequence, accepted: sessions pinned to a
119
+ lemonade-<name> model hard-fail model resolution while offline — "Model not found" —
120
+ and recover on the next startup or `/lemonade-setup` → Refresh once the box
121
+ is back.)
122
+
123
+ **Chat-only filter** — lemonade also hosts transcription (Whisper), image
124
+ generation (Flux), and other non-chat models. pi can only drive chat-completions
125
+ models, so those are filtered out by default. Toggle via *Model management →
126
+ Toggle chat-only filter* if you want them listed anyway.
127
+
128
+ **Compat flags** — the provider is registered with lemonade-appropriate settings
129
+ (`max_tokens` instead of `max_completion_tokens`, `system` instead of `developer`
130
+ role, no `reasoning_effort`), so requests work out of the box.
131
+
132
+ ### Agent tools
133
+
134
+ The extension registers nine tools the LLM can call on your behalf. Two facts
135
+ apply to all of them:
136
+
137
+ **Instance selection.** Every tool takes an optional `server` argument naming
138
+ a configured instance (omit for the default). `"default"` is kept as a
139
+ reserved alias; the default's actual name works too. Unknown names produce an
140
+ error listing available instances. See *Multiple lemonade instances*.
141
+
142
+ **Model selection precedence.** Tools that need a model resolve it in this
143
+ order: (1) an explicit `model` argument in the tool call, (2) the
144
+ extension's configured default (`defaultImageModel`, `defaultUpscaleModel`,
145
+ `defaultTranscriptionModel`, `defaultClassifierModel` in `lemonade.json`),
146
+ (3) auto-pick — the first catalog model carrying the right label (`image`,
147
+ `edit`, `tts`, ...) or recipe (`onnxruntime` for classifiers). Set a default
148
+ in `/lemonade-setup` or config if you want a specific model to win.
149
+
150
+ **How results come back.** Text results (transcripts) are returned inline into
151
+ the conversation. Binary outputs (images, audio, meshes) are *written to
152
+ disk* — `outputDir` (default: the agent's working directory) as
153
+ `lemonade-<kind>-<timestamp>.<ext>` — and the tool returns the saved path.
154
+ The model relays the path to you. To *view* a generated image in the TUI, ask
155
+ the agent to `read` the PNG afterwards: pi renders images inline in supported
156
+ terminals (Kitty, iTerm2, Ghostty, WezTerm, Warp), and reading it also lets
157
+ the model itself see and verify its own output. The TUI cannot render audio
158
+ or `.glb` meshes at all — those live on disk only.
159
+
160
+ Per tool:
161
+
162
+ | Tool | What it does | Model resolution | Output | Caveats |
163
+ |---|---|---|---|---|
164
+ | `transcribe_audio` | Speech → text | `defaultTranscriptionModel` (Whisper-Large-v3) | transcript returned inline (not saved) | whisper.cpp can't demux containers — mp4/m4a/webm/... are converted to 16 kHz mono wav via **ffmpeg** on pi's machine first; raw wav/mp3 go straight through. Unloaded Whisper auto-loads into the separate transcription slot |
165
+ | `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 |
166
+ | `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 |
167
+ | `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 |
168
+ | `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 |
169
+ | `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 |
170
+ | `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 |
171
+ | `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 |
172
+ | `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 |
173
+
174
+ **How `classify_text` handles multiple classifiers and modalities.** Each
175
+ classifier's label universe (phishing/benign, PII/no-PII, 1–5 stars…) is baked
176
+ into the model at training time — the tool is a pipe, not a policy. Three
177
+ layers make multi-classifier use workable: (1) the tool's prompt advertisement
178
+ dynamically enumerates the classifiers actually on the server each session;
179
+ (2) per-call selection via the `model` argument (or `defaultClassifierModel`
180
+ config); (3) runtime self-description — the response's labels reveal the
181
+ chosen model's universe, so a mismatched question ("is this a hot dog?" sent
182
+ to a phishing model) returns an obviously wrong label set and the agent
183
+ retries with the right model. Modality boundaries are architectural:
184
+ `/v1/classify` serves *text encoders only* — image classification routes to
185
+ vision-capable chat models (read the image), and audio classifies only after
186
+ `transcribe_audio`. Sequence-classification models return the flat ranked
187
+ scores; token-classification models (e.g. PII *span* detectors) return
188
+ additional structure, which the tool passes through verbatim.
189
+
190
+ Missing-model behavior is uniform: if no suitable model exists on the server,
191
+ the tool returns a clear error naming what to pull — the agent relays it and
192
+ suggests pulling via `/lemonade-setup`. Never a silent failure.
193
+
194
+ Lemonade also exposes **realtime transcription** over a websocket
195
+ (`ws://<host>:<ws-port>/realtime?model=…`; the port comes from `/health`).
196
+ This extension does not wire that up — it's for live-mic streaming, not file
197
+ transcription.
198
+
199
+ ### Agent self-awareness
200
+
201
+ Every chat's system prompt — including virgin chats with no lemonade context —
202
+ carries three guideline lines injected by this extension (via pi's
203
+ `promptGuidelines` mechanism, attached to `transcribe_audio`):
204
+
205
+ - where the machinery lives: `~/.pi/agent/lemonade.json` (base URL, endpoints,
206
+ defaults) and this README + `docs/` (full reference)
207
+ - what to do with it: query the server's HTTP API directly via `bash` +
208
+ `curl` for state questions (loaded models, health, host resources), and
209
+ point the user at `/lemonade-setup` for interactive management
210
+ - which instances exist: the configured fleet, by name and URL, and that
211
+ tools take an optional `server` argument (this line is generated per session)
212
+
213
+ So asking the agent "what models are loaded on your server?" in a fresh chat
214
+ resolves deterministically: read the config, curl `/v1/health`, summarize —
215
+ no hunting for paths, no lucky guesses.
216
+
217
+ ### `/lemonade-setup` menu
218
+
219
+ A navigable TUI (↑↓ / enter / esc):
220
+
221
+ | Menu | What you can do |
222
+ |---|---|
223
+ | **Server status** | Live health: version, loaded models with type/device/pinned/context, per-type slot limits, websocket port — plus host resources (CPU/RAM/GPU/VRAM) and last-request performance (tok/s, time-to-first-token) |
224
+ | **Server settings** | Discover servers (UDP beacon + HTTP fallback), edit base URL, edit API key, test connection |
225
+ | **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 |
226
+ | **Transcription settings** | Default Whisper model, endpoint path, smoke-test a file |
227
+ | **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 |
228
+
229
+ Notes on the destructive and slow paths:
230
+
231
+ - **Delete** requires two gates: a confirm dialog, then typing the exact phrase
232
+ `i want to delete <model-id>` — three attempts allowed, each prompt shows
233
+ chances remaining, and Esc aborts immediately.
234
+ - **Change context size** prefills the model's *current* context (from live
235
+ health), accepts `32k`, `1m`, or raw token counts, and warns that the model
236
+ is unloaded and reloaded (the new `ctx_size` is saved for future loads).
237
+ - **Pull** uses lemonade's server-owned download jobs: a live progress view
238
+ (percent, bytes, per-file status, updated every second) with **Esc to cancel**
239
+ (via `/v1/downloads/control`). Blocking pull is kept only as a fallback for
240
+ older servers without job support.
241
+ - **Install from Hugging Face** searches the registry through the server
242
+ (`/v1/registry/search`), lists quantization variants with sizes
243
+ (`/v1/pull/variants`), and installs the chosen one as a `user.*` model —
244
+ including vision/mmproj detection — with the same live progress view.
245
+
246
+ Every change is saved to the config file and re-registers the provider
247
+ immediately — no restart or `/reload` needed.
248
+
249
+ **Server discovery** — lemonade broadcasts a JSON beacon
250
+ (`{"service":"lemonade","hostname":…,"url":…}`) roughly once per second on UDP
251
+ port 13305. The setup menu can listen for it, then falls back to probing common
252
+ ports on localhost *and* the currently configured host.
253
+
254
+ > **WSL2 note:** UDP LAN broadcasts do not propagate into WSL2's NAT'd virtual
255
+ > network, so the beacon may never arrive if pi runs inside WSL2. The HTTP
256
+ > fallback still works, and manual URL entry always works.
257
+
258
+ ## Configuration
259
+
260
+ Everything lives in `~/.pi/agent/lemonade.json` — and it is **required**:
261
+ the extension fails loudly at startup (red error in the chat window, nothing
262
+ registered) if the file is missing or unparseable, because no endpoint is
263
+ hardcoded and a config-less run would target a server that isn't yours.
264
+
265
+ The tracked `lemonade.example.json` in this folder is the canonical
266
+ reference: fully commented, documenting every key and its default, and
267
+ doubly useful as a template:
268
+
269
+ ```bash
270
+ cp ~/.pi/agent/extensions/local-lemonade/lemonade.example.json ~/.pi/agent/lemonade.json
271
+ ```
272
+
273
+ Edit the first `servers` entry's `baseUrl`, trim the rest to taste, restart pi.
274
+ `//` comments are allowed in the config file (the loader strips them);
275
+ note that saving via `/lemonade-setup` rewrites the file and removes
276
+ comments, so keep annotations in your copy of the example. Changes made in
277
+ `/lemonade-setup` apply immediately — no restart or `/reload` needed.
278
+
279
+ A minimal working config is one entry:
280
+
281
+ ```json
282
+ { "servers": [{ "name": "main", "baseUrl": "http://your-lemonade-server:13305" }] }
283
+ ```
284
+
285
+ The full shape (values shown are the built-in defaults):
286
+
287
+ ```json
288
+ {
289
+ "servers": [
290
+ { "name": "main", "baseUrl": "http://localhost:13305",
291
+ "description": "Primary lemonade server." }
292
+ ],
293
+ "apiKey": "lemonade",
294
+ "chatPath": "/api/v1",
295
+ "modelsPath": "/v1/models",
296
+ "healthPath": "/api/v1/health",
297
+ "loadPath": "/api/v1/load",
298
+ "unloadPath": "/api/v1/unload",
299
+ "pullPath": "/api/v1/pull",
300
+ "deletePath": "/api/v1/delete",
301
+ "downloadsPath": "/v1/downloads",
302
+ "downloadsControlPath": "/v1/downloads/control",
303
+ "registrySearchPath": "/v1/registry/search",
304
+ "pullVariantsPath": "/v1/pull/variants",
305
+ "transcriptionPath": "/v1/audio/transcriptions",
306
+ "imageGenerationPath": "/v1/images/generations",
307
+ "imageEditPath": "/v1/images/edits",
308
+ "imageVariationPath": "/v1/images/variations",
309
+ "imageUpscalePath": "/v1/images/upscale",
310
+ "speechPath": "/v1/audio/speech",
311
+ "audioGenerationPath": "/v1/audio/generations",
312
+ "mesh3dPath": "/v1/3d/generations",
313
+ "classifyPath": "/v1/classify",
314
+ "beaconPort": 13305,
315
+ "chatOnly": true,
316
+ "defaultTranscriptionModel": "Whisper-Large-v3",
317
+ "defaultImageModel": "",
318
+ "defaultUpscaleModel": "RealESRGAN-x4plus",
319
+ "defaultClassifierModel": "",
320
+ "outputDir": "",
321
+ "servers": [
322
+ { "name": "main", "baseUrl": "http://localhost:13305",
323
+ "description": "Primary lemonade server." }
324
+ ],
325
+ "discoveryTimeoutMs": 5000,
326
+ "beaconTimeoutMs": 3000,
327
+ "loadTimeoutMs": 300000,
328
+ "pullTimeoutMs": 1800000,
329
+ "transcriptionTimeoutMs": 300000,
330
+ "generationTimeoutMs": 600000
331
+ }
332
+ ```
333
+
334
+ | Key | Meaning |
335
+ |---|---|
336
+ | `servers` | **Required, non-empty** — the only instance store: `[{ "name", "baseUrl", "apiKey"?, "description"? }]`. 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. |
337
+ | `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). |
338
+ | `*Path` | API endpoint paths, in case a future lemonade changes them. |
339
+ | `chatOnly` | Filter the provider down to chat-capable models. |
340
+ | `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). |
341
+ | `outputDir` | Where generated files (images, audio, meshes) are saved. Empty = agent's working directory. |
342
+ | `*TimeoutMs` | Per-operation timeouts. `pullTimeoutMs` only caps the *blocking* pull fallback — normal pulls use download jobs with live progress. |
343
+
344
+ ## Offline & unreachable networks
345
+
346
+ When pi starts on a network with no route back to the lemonade LAN (different
347
+ WAN, VPN down, box asleep):
348
+
349
+ - **Startup never blocks longer than `discoveryTimeoutMs`** (~5 s by default —
350
+ lower it in config if you work offline often).
351
+ - **Only the lemonade provider is affected** — every other provider in
352
+ `/model` (built-ins, other extensions) is untouched.
353
+ - **No lemonade listings when unreachable.** If an instance can't be reached
354
+ at startup or refresh, it contributes nothing to `/model` — an unreachable
355
+ model is not listable. This is fully automated in both directions: entries
356
+ appear exactly when the box is reachable and disappear when it isn't
357
+ (until the next startup or Refresh re-checks).
358
+ **The accepted consequence:** sessions or scripts pinned to
359
+ `lemonade-<name>/<model>` hard-fail model resolution while offline
360
+ (`Error: Model "lemonade/<id>" not found`), and resume working once the box
361
+ is back and a Refresh has re-registered it. Non-reachable chat requests are
362
+ the honest outcome; phantom listings are never shown.
363
+ - **Tools fail with actionable text, not cryptic errors**: every tool detects
364
+ the unreachable server and returns the same message — *where* it tried to
365
+ connect and *what to do about it* (reconnect, or point the extension at a
366
+ reachable instance via `/lemonade-setup`). No misleading "pull a model
367
+ first" advice when the real problem is the network, and no bare
368
+ `fetch failed`.
369
+ - **The setup menu diagnoses it**: Server status shows an explicit
370
+ `Unreachable: <health URL>` panel, and Test connection reports the failure.
371
+ - **The agent can self-diagnose**: the self-awareness guidelines point it at
372
+ the config, so asking the agent "are you connected to lemonade?" resolves
373
+ via a direct probe rather than guessing.
374
+
375
+ **Levels of availability checking** (each with one job, each defined once):
376
+
377
+ | Level | When | What it does | Where the logic lives |
378
+ |---|---|---|---|
379
+ | Registration probe | every startup / refresh | decides what goes into the provider: the live catalog, or nothing at all when unreachable — no phantom listings | `registerInstanceProvider` |
380
+ | Tool fetch | every tool call | converts network rejections into the actionable unreachable message; user-cancels pass through | `toolFetch()` — used by all nine tools |
381
+ | Auto-pick | model resolution | distinguishes unreachable (`null`) from no-match (`undefined`) so advice fits the situation | `pickModelByLabels` / `pickModelByRecipe` |
382
+ | Instance resolution | every tool call with a `server` arg | resolves the named box's config view or returns an error listing known instances | `instanceView()` |
383
+
384
+ Tools stay deliberately stateless — reachability is a per-call fact (networks
385
+ change mid-session), so no connection state is cached between calls. The only
386
+ repetition is the three-line binding guard at the top of each tool, which is
387
+ what makes the rest of each tool body instance- and failure-aware for free.
388
+
389
+ ## Multiple lemonade instances
390
+
391
+ The extension supports **simultaneous multi-instance use**. Each configured
392
+ lemonade box becomes its own pi provider, every tool can target any instance,
393
+ and the setup menu operates per-instance.
394
+
395
+ **Configuration** — there is no separate default instance: `servers[]` is
396
+ the *only* instance store, and the first entry IS the default. Every entry
397
+ has a **name** (required) and an optional **description** (free-form context
398
+ for your own reminder's sake). The shared top-level `apiKey` applies to any
399
+ entry that doesn't define its own:
400
+
401
+ ```json
402
+ {
403
+ "servers": [
404
+ { "name": "main", "baseUrl": "http://192.168.1.10:13305",
405
+ "description": "Primary lemonade server." },
406
+ { "name": "second-server", "baseUrl": "http://192.168.1.20:13305",
407
+ "description": "The other box, mostly for experiments." }
408
+ ]
409
+ }
410
+ ```
411
+
412
+ Names identify instances everywhere, without prejudice: provider ids, model
413
+ addresses, and picker brackets are uniformly `lemonade-<name>` for EVERY
414
+ instance — the default reads `[lemonade-main]` exactly like an extra
415
+ reads `[lemonade-second-server]`. Descriptions are free-form context shown in the
416
+ instance lists; edit both via *Manage instances → Edit instance*. Renaming
417
+ any instance changes its provider id (and re-registers it), so pinned
418
+ sessions and `--model lemonade-<old-name>/...` references must follow the
419
+ rename — the cost of a perfectly uniform namespace.
420
+
421
+ **How instances are exposed:**
422
+
423
+ - **Providers:** every instance registers as `lemonade-<name>` — the default
424
+ included, no special case. Picker brackets, model addresses, and provider
425
+ column all read `lemonade-<name>` uniformly. Each is independently annotated `loaded` / `on-demand`
426
+ from that box's own health state. Unreachable boxes register nothing —
427
+ there are no offline placeholders for any instance.
428
+ - **Tools:** every tool takes an optional `server` argument naming an
429
+ instance. Omitted → default. Unknown names return an error listing the
430
+ available instances — never a wrong-box dial. Model auto-pick, status, and
431
+ everything else resolve against the selected instance's catalog.
432
+ - **Self-awareness:** the prompt guideline enumerates the configured
433
+ instances by name and URL every session, so virgin chats know the fleet
434
+ without hunting.
435
+ - **Setup menu:** a *Switch instance* item (appears when extras exist) picks
436
+ which box Status, Model management, and Live logs act on; Server settings →
437
+ *Manage instances* lists (with live reachability), adds, and removes
438
+ instances; discovered servers can be registered as default or as a named
439
+ instance.
440
+
441
+ **The one-place rule:** instance semantics are defined exactly once —
442
+ `instanceView()` returns a *config view* (same settings, swapped
443
+ baseUrl/apiKey) that flows through every existing helper unchanged, so tools,
444
+ discovery, TUI actions, and error handling all inherit instance behavior
445
+ without per-tool logic.
446
+
447
+ ## Reference documentation (API ground truth)
448
+
449
+ This folder's [`docs/`](./docs) directory contains the complete official
450
+ Lemonade endpoint specification, snapshotted from the upstream repo
451
+ (`lemonade-sdk/lemonade`, `docs/api/`) on 2026-09-12 — the same markdown the
452
+ [docs site](https://lemonade-server.ai/docs/api/) renders. It is the source of
453
+ ground truth for every endpoint, parameter, and response shape this extension
454
+ uses:
455
+
456
+ | File | Contents |
457
+ |---|---|
458
+ | [`docs/README.md`](./docs/README.md) | Spec index and design philosophy |
459
+ | [`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 |
460
+ | [`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 |
461
+ | [`docs/mcp.md`](./docs/mcp.md) | The MCP gateway (lemonade as an MCP server) |
462
+ | [`docs/ollama.md`](./docs/ollama.md), [`docs/anthropic.md`](./docs/anthropic.md), [`docs/llamacpp.md`](./docs/llamacpp.md) | Other compatibility surfaces (rerank, slots, etc.) |
463
+
464
+ When lemonade updates, re-sync with:
465
+
466
+ ```bash
467
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/lemonade-sdk/lemonade /tmp/lemonade \
468
+ && cd /tmp/lemonade && git sparse-checkout set docs/api \
469
+ && cp docs/api/*.md ~/.pi/agent/extensions/local-lemonade/docs/
470
+ ```
471
+
472
+ ## Lemonade behavior reference
473
+
474
+ Empirically confirmed against lemonade 11.7.0 (cross-checked with the
475
+ [official endpoint spec](https://lemonade-server.ai/docs/api/)):
476
+
477
+ - **Auto-loading.** A request for an unloaded model triggers an automatic load
478
+ attempt. If the model's slot is free or holds an unpinned model, lemonade
479
+ evicts/loads and serves the request. If the slot is occupied by a *pinned*
480
+ model, you get `slots_pinned_error: "All loaded models of type … are pinned.
481
+ Unload a model first."` — unload or unpin via the setup menu, then retry.
482
+ - **Per-type slots.** The server holds one slot per model type (llm,
483
+ transcription, image, embedding, …), so Whisper can be loaded alongside your
484
+ chat model.
485
+ - **change-ctx.** Implemented as unload → reload with `ctx_size` +
486
+ `save_options: true`, which persists the choice for future loads.
487
+
488
+ ## Troubleshooting
489
+
490
+ | Symptom | Fix |
491
+ |---|---|
492
+ | No `lemonade-<name>/*` models in `/model` | `/lemonade-setup` → Server settings → Test connection; check `baseUrl`; run Refresh |
493
+ | Error: `slots_pinned_error` on first message | The pinned model occupies the slot — unload it (Model management), or select the loaded model |
494
+ | `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 |
495
+ | Transcription of `.mp4` fails with ffmpeg missing | Install ffmpeg on the machine running pi (container formats are converted locally before upload) |
496
+ | Discovery finds nothing | Expected inside WSL2 (UDP broadcasts don't cross the NAT); set the URL manually |
497
+ | Server unreachable at startup | The instance registers nothing — no lemonade models in `/model` until it's reachable; fix the URL and Refresh |
498
+ | Pull seems to hang | It's downloading — the blocking pull path sends no progress. Large models take minutes; the `pullTimeoutMs` cap guarantees control returns |
499
+
500
+ ## MCP Gateway (lemonade as an MCP server)
501
+
502
+ Lemonade exposes itself as a genuine MCP server at `POST /mcp`
503
+ (Streamable HTTP), serving five tools: `lemonade_list_models`, `lemonade_chat`,
504
+ `lemonade_transcribe_audio`, `lemonade_generate_image`, and `lemonade_omni`.
505
+
506
+ pi's MCP manager only accepts *remote* MCP endpoints over HTTPS, so this
507
+ setup ships a tiny local bridge instead:
508
+
509
+ - `~/.pi/agent/bin/lemonade-mcp-proxy.mjs` — stdio→HTTP proxy: reads
510
+ newline-delimited JSON-RPC on stdin, forwards each message to the lemonade
511
+ gateway, writes responses to stdout (SSE frames unwrapped, session id
512
+ passed through).
513
+ - `~/.pi/agent/mcps-local/lemonade/server.json` — MCP manager entry
514
+ (transport: stdio, connection: lazy, **disabled by default**).
515
+
516
+ To use it: open `/mcp` in pi, pick **lemonade**, choose *Test or refresh*,
517
+ inspect the tool manifest, select the tools you want, and enable the server.
518
+ The first approved call starts the connection. Note the overlap: these MCP
519
+ tools duplicate what this extension registers natively — enable them only if
520
+ 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).