Loading...
Loading...
Integrates local AI capabilities into applications using Embeddable Lemonade. Use when the user wants to add local AI, offline AI, private AI, on-device AI, a local LLM, local chat, embeddings, image generation, speech-to-text, or text-to-speech to an existing app; replace or supplement OpenAI, Anthropic, Ollama, or other cloud AI APIs with a local backend; only use to convert user apps. Do not use when the user just wants the agent itself to generate images, transcribe, or speak locally in the current workspace, even to cut their own API bill.
npx skill4agent add amd/skills local-ai-app-integrationlemondhttp://localhost:PORT/api/v1base_urlapi_keyvendor/lemonade/https://lemonade-server.ai/install_options.htmlhttp://localhost:13305/api/v1[ ] 1. Survey the app's current AI integration
[ ] 2. Pick a model + backend profile
[ ] 3. Place Embeddable Lemonade in the app's tree (full package, not just the binary)
[ ] 4. Add a `lemond` launcher (subprocess + API key + port + per-stage logging)
[ ] 5. Re-point the existing client at lemond (base_url, api_key, 120s timeout — all three required)
[ ] 6. Wait for /api/v1/health, install backend, then PULL the model before first use
[ ] 7. Wire shutdown and error recoveryLog every stage. A local integration has many silent failure points — spawn, health, backend install, model download, first inference. Without a log line at each transition, "nothing happened" is indistinguishable from "broke at stage 3." Emit one clear line per stage as you build (see Step 4); the most common dead-end in this integration — a blank result with no error — is invisible without them.
openaiOpenAI(chat.completionsresponses.createanthropicAnthropic(messages.createapi.openai.comapi.anthropic.comlocalhost:11434OPENAI_API_KEYANTHROPIC_API_KEYopenai-pythonopenai-node@anthropic-ai/sdkgo-openaifetch| App's primary need | Default model | Recipe | Why |
|---|---|---|---|
| General chat / assistant | | | Small, fast, good tool calling, fits 8GB systems |
| Coding assistant | | | Strong code, runs on iGPU |
| Vision / multimodal chat | | | Small multimodal default |
| NPU-first on Ryzen AI | | | XDNA2 NPU on Windows |
| Speech-to-text (Windows) | | | One model; probe picks NPU → iGPU/dGPU → CPU automatically |
| Speech-to-text (Linux NPU) | | | Linux NPU path; falls back to |
| Text-to-speech | | | CPU-only, low latency |
| Image generation | | | Single-step generation |
llamacpplemondrocmvulkancpullamacpp_backendvulkanvulkanNote: having an NPU does not mean every recipe supports NPU. Confirm the recipe/backend pair isorinstalledviainstallablebefore committing to it. See reference.md for per-recipe decision rules.GET /api/v1/system-info
https://github.com/lemonade-sdk/lemonade/releases/latestlemonade-embeddable-{VERSION}-windows-x64.ziplemonade-embeddable-{VERSION}-ubuntu-x64.tar.gzDon't hand-build the download URL from the tag. The git tag carries a leading(e.g.v) but the asset filename strips it (v10.8.0), so using the tag verbatim 404s. Ask the GitHub API for the asset by its stable name pattern and use the URL it returns, as below — this stays correct across version and naming changes.lemonade-embeddable-10.8.0-...
# Windows
New-Item -ItemType Directory -Force vendor\lemonade# Linux
mkdir -p vendor/lemonade$rel = Invoke-RestMethod https://api.github.com/repos/lemonade-sdk/lemonade/releases/latest
$asset = $rel.assets | Where-Object { $_.name -like "lemonade-embeddable-*-windows-x64.zip" } | Select-Object -First 1
Invoke-WebRequest $asset.browser_download_url -OutFile lemond.zip
Expand-Archive lemond.zip -DestinationPath "$env:TEMP\lemond-unpack"
$folder = $asset.name -replace '\.zip$','' # unpacked dir = asset name without .zip
Copy-Item -Recurse "$env:TEMP\lemond-unpack\$folder\*" vendor\lemonade\
# Sanity check: resources/ must be nested under vendor\lemonade\ (not flattened)
if (-not (Test-Path vendor\lemonade\resources\*.json)) { throw "resources/ missing — re-extract and copy again" }URL=$(curl -s https://api.github.com/repos/lemonade-sdk/lemonade/releases/latest \
| grep browser_download_url | grep ubuntu-x64.tar.gz | cut -d'"' -f4)
curl -L "$URL" | tar -xz --strip-components=1 -C vendor/lemonadeCopy the full package, not just the binary. The archive contains,lemond[.exe],lemonade[.exe], andLICENSE. Theresources/directory is required — without it lemond starts and passes the health check but fails on every model and backend request. Copying only the binary produces a server that looks healthy but cannot function.resources/
vslemondCLI:lemonadeis the embedded server binary that ships with the app. ThelemondCLI is a separate packaging tool used only during development/build time to install backends. The same embeddable archive unpacked above already contains a matchinglemonadenext tolemonade[.exe], so its version aligns with the bundledlemond[.exe]. Do notlemondto get it: the PyPI package is a separate, older release line whose ports, model names, and install API do not match thepip install lemonade-sdkbundled here, and mixing the two is a known source of silent version mismatches. Keep thelemondCLI,lemonade, and the backends all from the one release downloaded in this step so their versions stay aligned.lemond
lemond[.exe]lemonade[.exe]LICENSEresources/vendor/lemonade/
lemond[.exe] # the only binary the app ships
LICENSE
config.json # generated on first run; commit a seed copy
resources/
server_models.json # do not edit; use GET /api/v1/models at runtime
backend_versions.json
bin/ # backends bundled at packaging time
llamacpp/vulkan/llama-server[.exe]
models/ # pre-bundled model weights (optional)
models--unsloth--Qwen3-4B-GGUF/: Do not edit or rely on this file. It can be stale. The only authoritative model list isserver_models.jsonon a runningGET /api/v1/modelsinstance with the backend already installed.lemond
llamacpp:vulkanllamacpp:rocmPOST /api/v1/installGET /api/v1/system-infomodels/POST /api/v1/pullmodels_dir./modelsconfig.jsonautoPackaging time (developer machine, before bundling). Use the lemonade CLI that shipped insideso it matches the bundledvendor/lemonade/version (prefix withlemondor the full path):./vendor/lemonade/lemonade backends install llamacpp:vulkan vendor/lemonade/lemonade backends install flm:npu # Windows NPU path onlyThis bakes the backend binaries intobefore the app ships.vendor/lemonade/bin/does not need to be running. Use a modernlemondCLI whose version matches the bundledlemonade(the copy in the archive you unpacked works); do notlemondfor it.pip install lemonade-sdkFirst-run / runtime (user's machine, afteris running):lemondhttpPOST /api/v1/install {"recipe": "llamacpp", "backend": "rocm"}Use this for hardware-specific backends (e.g.) that cannot be bundled universally.llamacpp:rocmmust already be running (Step 4 complete).lemond
lemondlemond_launcher.pylemond_launcher.<ext>key = secrets.token_urlsafe(32)socketsubprocesssubprocess.Popen([LEMOND_BIN, LEMOND_DIR, "--port", str(port)], env={**os.environ, "LEMONADE_API_KEY": key})GET /api/v1/healthAuthorization: Bearer {key}portkeyLog one line per lifecycle stage. Build the logging in from the start — not as an afterthought when something breaks. Each silent transition needs a visible marker so a failure points at the exact stage. Aim for:[lemond] Starting on port <port> [lemond] Healthy on port <port> [lemond] <recipe>:<backend> installed (or: already installed / install failed) [lemond] Pulling model <name>... then: Model <name> ready (or: pull returned <status>) [local] <modality> result: <value> (first inference output — empty string here = unpulled model)Logging the first inference result verbatim is what turns the silent-empty failure (Step 6) from a multi-hour mystery into a one-line diagnosis. Route these through the app's normal logging so they can be quieted for release.
Dev-mode file watchers: If the app runs with a file watcher (Tauri, Electron, Next.js, Vite, etc.) that watches the source tree, ensureis excluded from the watched paths. Lemond writes config and cache files at runtime; a watcher that picks these up will restart the app, kill the lemond subprocess, and spawn a new one on a new port — silently breaking any in-flight transcription. Addvendor/lemonade/(or the equivalent) to the watcher's ignore list before testing.vendor/
LEMOND_DIRsecretssocketsubprocessGET http://127.0.0.1:<port>/api/v1/healthlemond/api/v1/healthlemondbase_urlhttp://127.0.0.1:{port}/api/v1api_keyimport httpx
from openai import OpenAI
proc, key, port = start_lemond()
client = OpenAI(
base_url=f"http://127.0.0.1:{port}/api/v1",
api_key=key,
http_client=httpx.Client(timeout=120), # required: 120s for first-run model load
)| Existing client | New | New auth | Timeout |
|---|---|---|---|
| | | |
| | | |
| | | |
Raw | same | | set per-request |
| Ollama-compatible code | | pass key anyway | 120s |
Qwen3-4B-GGUFlemond"local"GET /api/v1/healthPOST /api/v1/load/loadPOST /api/v1/pull
{"model": "Whisper-Large-v3-Turbo"}GET /api/v1/modelsSilent-empty is almost always an unpulled model. If inference returns an empty string / blank output with no HTTP error, the model was not downloaded. Check your pull step before debugging anything else — this is the failure mode that wastes the most time. Log the pull result and the first inference result (see Step 4) so this is diagnosable from the console, not by guesswork.
server spawn → health 200 → backend install → model download → model load → first result| Symptom | Cause | Recovery |
|---|---|---|
| Inference returns empty / blank with HTTP 200, no error | Model never pulled: backend is installed but weights are absent, so lazy-load has nothing to load | |
| Model not pulled yet (same root cause as the empty-result row above) | |
| Backend not installed for this hardware | |
| Subprocess exits immediately | Port race: another process grabbed the port between | The reference launcher retries with a fresh port automatically (3 attempts) |
| First-run backend extraction is slow on cold disk | Extend timeout to 90s on first launch, 30s after |
| HTTP 401 on every request | Forgot the | Audit the client config because Lemonade rejects unauth'd calls when |
proc.terminate()proc.kill()lemondlemond/api/v1/healthvendor/lemonade/lemond[.exe]lemonade[.exe]LICENSEresources/lemondGET /api/v1/healthGET /api/v1/modelsvendor/lemonade/lemondbin/recipe_options.json