@cyrilmarin/dsh-lemonade
v0.4.0
Published
Lemonade Server (OpenAI-compatible) LLM provider plugin for the DeepSeek Harness
Readme
dsh-lemonade-provider
A dsh plugin that integrates Lemonade Server as a model provider for DeepSeek Harness.
Lemonade exposes an OpenAI-compatible API (Chat Completions). This plugin wires
this API to the Harness's ctx.llm service under the lemonade provider
route:
- Chat Completions streaming (SSE) via
POST {baseURL}/chat/completions - Model discovery live from
GET {baseURL}/models - Optional API key (
LEMONADE_API_KEY) — only needed when the server is configured with authentication - Vision support: Harness image blocks are sent as
image_url(data URL) tovisionmodels - Tool calling in standard OpenAI format
Prerequisites
- A running Lemonade Server (default
http://localhost:13305) - Node.js ≥ 22
- A dsh installation (profile), e.g. the
webprofile
The package must be built beforehand:
pnpm install && pnpm build("Development" section below) — dsh loads code fromlib/.
Installation in a profile
From the profile directory (e.g. ~/.dsh/profiles/web):
pnpm add file:../dsh-lemonade-providerOr from npm (the published package is @cyrilmarin/dsh-lemonade):
pnpm add @cyrilmarin/dsh-lemonade@latestOr, via the dsh command line:
dsh plugin --profile web add file:../dsh-lemonade-providerThen add an entry to the profile's cordis.patch.yml (see
example/cordis.patch.yml):
- id: llm-lemonade
name: 'llm-lemonade'
config:
baseURL: http://localhost:13305
baseURLis the server root (an old/v1suffix from legacy configs is also supported). IfLEMONADE_BASE_URLis set, it is used whenbaseURLis omitted.
Configuration
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| baseURL | string | http://localhost:13305 (or LEMONADE_BASE_URL) | Normalized to scheme://host/api — /api appended if missing, /v1/… appended per endpoint |
| apiKeyEnv | string (credential-ref) | LEMONADE_API_KEY | Regular API key (for /v1/* endpoints) |
| adminApiKeyEnv | string (credential-ref) | LEMONADE_ADMIN_API_KEY | Optional admin API key (for internal /internal/* and /metrics endpoints) |
| requireAuth | boolean | false | Fail when the key is absent (protected remote) |
| models | array | [] | User-pinned advisory model catalog |
| defaultContextWindow | number | 32768 | Context window used when the server doesn't declare one |
| maxTokens | number | 8192 | Default output cap |
| streamIdleTimeoutMs | number | 300000 | SSE stream idle timeout |
| listingTimeoutMs | number | 5000 | Max time one live model-listing query may take (abort if exceeded) |
| retryPolicy | object | default values | Provider retry policy |
Each entry in models: id (required), name, description,
contextWindow, maxTokens, vision (boolean).
Model Discovery
The Harness Models page can query GET {baseURL}/models through the
discovery registered for the llm-lemonade settings namespace. Undownloaded
models and those routed to other endpoints (embeddings, image,
TTS, transcription, …) are excluded from the proposed list; the declared
context window (max_context_window) is reused when present.
UI Configuration (Settings → Models)
The dsh Settings/Models page has no third-party exit gate: its editor only
knows the llm-deepseek and llm-pi-ai namespaces. The plugin therefore wires
its edit card to the pi-ai card on the Models page through a targeted patch
of the bundled dsh-client-ui-settings-models bundle (llm-lemonade route →
pi-ai family). After any npm cache reinstallation (npm exec), rerun:
node scripts/patch-models-ui.mjs
node scripts/patch-models-ui-admin.mjsThe form (Settings → Models -> Lemonade row) allows entering the API key
(optional, stored via the credentials service under LEMONADE_API_KEY),
the optional admin key (LEMONADE_ADMIN_API_KEY — internal endpoints
/internal/* and /metrics, via the patch-models-ui-admin.mjs patch),
the base URL (falling back to "customized"), and selecting models served by
Lemonade via "Fetch available models" (llm.discoverModels).
Lemonade View (Conversation Tab)
A Lemonade tab (next to Chat/Trajectory) exposes the entry points of the
Lemonade-specific API (health/liveness, telemetry, models with
Load/Unload/Delete/Files/Update/Info, LoRA adapter list/load/unload,
controllable downloads, cloud keys, and a live log stream).
The browser calls the dsh server on the same origin (/dsh-lemonade/api/<op>);
the host proxy to Lemonade (src/server-api.ts) resolves baseURL + key
(these never leave the host). Key selection is per endpoint: regular
endpoints (/v1/*, /live) authenticate with LEMONADE_API_KEY, and control
endpoints (/internal/*, /metrics) with LEMONADE_ADMIN_API_KEY (falling
back to the regular key). The route is registered via
ctx.webServer.register({ kind: 'prefix', path: '/dsh-lemonade/api', ... })
when the webServer service is available.
The model table supports batch operations: a select-all checkbox in the
header and one per row let you mark models, then the Load selected /
Unload selected / Delete selected buttons dispatch one call per selected
model through the host proxy and report a done/total progress line plus how
many operations failed. The batch delete prompts in a confirmation modal
(instead of the browser's native confirm()) before it runs. A Logs pane
(Logs button in the tab header) opens a live stream of the Lemonade server's
own logs: the browser holds a plain SSE connection to the host proxy, which opens
a WebSocket client to Lemonade's /logs/stream (the log port is discovered
from GET /v1/health → websocket_port, which shares the Realtime Audio port)
and re-emits every upstream log message as an SSE event.
Client browser bundle
The browser half lives in src/client/index.js and is copied verbatim to
lib/client.js by the build (scripts/copy-client.mjs). The bundle registers
itself with the module loader under the package name — @cyrilmarin/dsh-lemonade
— because the harness keys plugin client modules by package name in its boot
manifest:
window.__ModuleLoader__.load({ id: "@cyrilmarin/dsh-lemonade", factory: (require) => { /* … */ } })The registration id must match the graph row id exactly; a mismatch makes the
harness fail with "loaded without registering <id> via __ModuleLoader__.load".
Because lib/ is gitignored (build output), always rebuild after touching
src/client/index.js and reinstall the package in the profile before reloading
the GUI.
Development
pnpm install
pnpm build # compile TypeScript to lib/
pnpm test # protocol tests (simulated SSE server)Structure
src/index.ts— plugin: config schema,apply, discovery, credentialssrc/adapter.ts—LemonadeAdapter extends LlmAdapter(fetch + SSE)src/serialize.ts— Harness messages → OpenAI wire formatsrc/translate.ts— SSE payloads →StreamChunkchunkssrc/client/index.js— browser half (Lemonade conversation tab), copied tolib/client.jstest/adapter.test.mjs— dependency-free test suite (mock HTTP)
License
MIT
Release
Release automation is a self-contained script with no third-party dependency:
scripts/release.mjs. Available commands:
pnpm release:dry # show the plan (no file writes, no mutating git commands)
pnpm release # auto-detect the bump from the conventional commits
pnpm release:major # force a major bump
pnpm release:minor # force a minor bump
pnpm release:patch # force a patch bumpThe release script:
Determines the increment from the conventional commits between the last tag (or the full history when no tag exists) and HEAD:
| Commit | Bump | | --- | --- | |
type(scope)!: …subject orBREAKING CHANGE:in the body | major | |feat| minor | |fix,perf| patch | | any other commit not yet tagged | patch (fallback) |With no commit to publish, it prints a message and exits 0 without doing anything.
Bumps
versioninpackage.json(a prerelease suffix, if present, is dropped — the release is final).Commits
chore(release): <version>(stagespackage.jsononly).Creates the annotated tag
v<version>(prefix configurable via--tag-prefix).Pushes the current branch and the tag to
origin(omitted with--no-push).
The npm publication itself is not performed by the script: it is triggered by the GitHub Release created on the pushed tag (workflow .github/workflows/publish.yml).
