Skip to content

Add LiteRT-LM as a local runtime (add-litert-lm-runtime) - #50

Merged
Ooooze merged 17 commits into
mainfrom
change/add-litert-lm-runtime
Oct 22, 2026
Merged

Ooooze merged 17 commits into
mainfrom
change/add-litert-lm-runtime

Conversation

@Ooooze

@Ooooze Ooooze commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Why

OpenSpec change add-litert-lm-runtime, core part: run Google's LiteRT-LM (.litertlm files such as Gemma 4 E2B/E4B) as a local engine next to llama.cpp, on CPU, GPU and, on Windows, the Intel NPU through OpenVINO.

What changes

  • Engine builds. New kind litert-lm: Google's PyPI wheels, listed with sha256 in conf's manifest (ATOMIC_LITERT_LM_MANIFEST_URL). Each release carries an abi number; core installs only the ABIs it binds (SUPPORTED_LITERT_LM_ABI = [1]). A .whl unpacks as a zip, and only the native part of litert_lm/ is kept under <build>/lib/. The host probes a build before it is switched in. ADR 2026-10-21-litert-lm-builds-are-pypi-wheels-with-an-abi-number.

  • Host mode litert-host probe|inspect|serve of the core binary, on bun:ffi. ffi-bun.ts is the second file allowed to import bun:*. Chunks are copied on the library's thread by a small C shim, compiled with Bun's TinyCC, into a mutex-guarded FIFO that JS drains every 2 ms. A threadsafe JSCallback read freed memory and crashed in about 1 run in 5. The host runs its own OpenAI server:

    • streaming, tool calls, JSON schema (LL_GUIDANCE), thinking, stop;
    • generations run one at a time (FIFO);
    • a pool of four warm conversations;
    • a client that hangs up cancels its generation.

    ADR 2026-10-21-litert-lm-runs-in-a-host-mode-with-its-own-openai-server.

  • Runtime. LiteRtLmRuntime plus the following:

    • the litert_lm block of model.yml and the litert-lm settings schema;
    • four new error codes;
    • GET /runtimes/litert-lm/accelerators and POST …/prepare, which download OpenVINO for a supported Intel NPU;
    • POST /models/litert-lm/check, the verdict for a variant or a file;
    • client methods, routing and listing, GPU residency.
  • Fixes from running it on real hardware:

    • cancel on disconnect under Bun: node:http gives no close once the body is read, so the host now uses core's requestAborted;
    • relative --lib on Windows: SetDllDirectoryW drops the working folder from the DLL search;
    • temperature below 0.01 is sent as greedy: at 0 the GPU sampler answered gibberish;
    • EcoQoS on Windows: the host opts out, since Windows kept the windowless process on efficiency cores. CPU decode went from 6 to 28 tok/s, the RTX 4070 from 68 to 100;
    • JSON schema with speculative decoding (core's default): a constrained conversation now runs without speculative decoding. Its drafts skipped the grammar.

Checked

  • npm run verify steps after merging main:

    • lint, typecheck, format, build, build:bin, test:contract:bun — pass;
    • test:e2e — 217 passed, 10 skipped;
    • unit and contract — 9623 passed, 1 failed: src/diffusion/server-process.test.ts › kills a server that does not finish loading in time. It failed in both full runs after the merge and in one before it, and passes on its own (3/3 each time). The fake engine has 1.5 s to record its pid, which a loaded machine misses. This PR does not touch diffusion;
    • because of that failure, the per-file coverage floors were not evaluated after the merge. Before it they failed for 8 files that fail the same way on main, none of them a LiteRT-LM file.
  • Integration on Google's real library (npm run test:integration), with the wheel installed by core from PyPI and its sha256 checked:

    Platform Result
    macOS arm64 5/5
    Linux x64 (Ubuntu) 5/5
    Windows x64 6/6 (includes the new JSON-schema-with-speculative test)
  • Hardware runs. Checked on each: correctness against CPU, streaming, tool call, JSON schema, conversation isolation, and cancel on disconnect.

    Device Path Gemma 4 E4B decode
    macOS Metal — —
    Linux NVIDIA OpenCL —
    Windows RTX 4070 D3D12 50 tok/s
    Windows Intel iGPU D3D12 11 tok/s
    Windows CPU — 14 tok/s

    Same machine, llama.cpp b11463 on E4B: CUDA 65, Vulkan on the iGPU 7.5, CPU 16. Prefill on CUDA is 7× faster than LiteRT-LM.

  • Network. No sockets other than loopback, and no DNS lookups, from probe, inspect or serve on any OS.

Not checked or not done:

  • Intel NPU: there is no Core Ultra 200V or series 3 machine; this moves to acceptance.
  • GPU on Linux without NVIDIA: the engine fails with VK_ERROR_DEVICE_LOST on the Intel iGPU, so conf keeps GPU closed for linux-x64.
  • usage.completion_tokens: it counts stream chunks, so it is under-counted with speculative decoding. Left as is by decision.
  • Release 6.9: not part of this PR.

🤖 Generated with Claude Code

Ooooze and others added 17 commits October 21, 2026 22:50
Manifest parsing (strict per field, lenient per record, ABI and min core
version gates, PyPI-only URLs), .whl extraction keeping only the native part
of litert_lm/ under lib/, host platform rule (macOS 12+, glibc 2.27+, Windows
x64), install through litert-host probe, active build by published_at among
supported ABIs. Engine-builds host platform and probe become per-engine
tables. Also the provider id, its data layout and settings schema, which the
engine row needs to typecheck (part of 6.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hidden litert-host subcommand (probe, inspect, serve) in both binaries and the
./litert-host package export. bun:ffi bindings stream through a TinyCC shim
that copies chunks on the library thread into a mutex-guarded FIFO drained
from JS (spike S2), behind a LiteRtLibrary interface. The host serves
/v1/models and /v1/chat/completions (stream and non-stream, tools, JSON
schema, thinking as reasoning_content, images, stop, usage, 400s), one
generation at a time with up to four warm conversations. Integration tests
against the real library in a new test:integration project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
litert-lm joins the local provider lists (search order last, owned_by, external
sessions, cloud registry refusal) with LITERT_LM_* error codes and statuses.
model.yml gains a strictly read litert_lm block. LiteRtLmRuntime runs one
litert-host serve per model on the sidecar table behind a keyed gateway:
platform and NPU generation checks, OpenVINO readiness, inspect cached by path
and mtime for the newer-engine check, the file's backends and the context cut,
GPU claim for gpu variants, host exit codes mapped to the provider errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /runtimes/litert-lm/accelerators and POST .../prepare in Foundry Local's
shape (plus proxy): OpenVINO of the active release on an Intel NPU LiteRT-LM
targets (generation by CPU model string), downloaded with sha256 and size,
only openvino/libs kept, litert-lm:accelerators progress, one prepare at a
time, no second download. POST /models/litert-lm/check answers for a variant
without the network, and for a file by path inside the models root or by
model id with its inspect metadata. create.ts registers the runtime where the
platform rule holds, with GPU residency, engine-builds host and the checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An integration test installs the build from conf's release (wheel from PyPI),
loads Gemma 4 E2B on CPU, streams and calls a tool through :1337 and unloads.
docs/contracts.md gains the LiteRT-LM provider row, the ./litert-host export
and the data layout; two ADRs (host mode with its own OpenAI server; PyPI
wheels gated by our ABI number) are indexed; AGENTS.md and the Foundry Local
ADR now name two bun:ffi exceptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e as a helper

The accelerator service, runtime and check are made under the same condition,
so the runtime takes the service's methods directly instead of fallbacks that
never ran; the engine-builds probe of a LiteRT-LM build is liteRtHostProber.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, linked paths

- A conversation gets no sampler unless the request names one, so the file's
  own sampling applies; a request's sampler gets a fresh seed unless it gives
  one (a fixed seed 0 answered every regenerate with the same text).
- The host's server cancels running generations and waits for the library's
  last chunk before it deletes conversations and the engine.
- The check resolves links before judging a path inside the models root.
- compareVersions replaces a second version comparator; the accelerator reads
  the cached manifest before the network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…luded

The runtime loads a model whose file is a link; the check by model_id now does
the same instead of refusing it. Explicit paths still resolve links against
the models root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r Bun

Bun's node:http emits no close on the response or the socket once the
request body is read, so a dropped stream ran to its end and held the
generation lock (10.7 s on the RTX 4070). The host now also asks core's
requestAborted, which reads Bun's native handle flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SetDllDirectoryW drops the working folder from the Windows DLL search,
so a relative --lib failed with "litert-lm.dll does not export ...".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the Windows GPU (RTX 4070) temperature 0 answered gibberish and
0.0001 nothing: the sampler divides logits by it. Below 0.01 the host
now asks for one candidate at temperature 1, the same choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A windowless process counts as background work: Windows ran the host on
efficiency cores at low clocks. On an i7-14700HX CPU decode of Gemma 4
E2B went from 6 to 28 tok/s and the RTX 4070 from 68 to 100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve decoding

Drafted tokens skip the grammar: with speculative decoding on, which is
core's default, a JSON-schema answer came back malformed on GPU and
failed with "compute_mask() called after stop" on CPU (0.18.1, macOS
and Windows). The session config turns it off for such conversations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…runtime

# Conflicts:
#	docs/decisions/INDEX.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ooooze
Ooooze merged commit 7348892 into main Oct 22, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant