The fake-runtime fixture, the scenarios the suite is required to cover, and the acceptance criteria those scenarios establish.
Use unit and integration tests.
Minimum fake runtime test fixture should simulate:
- startup delay;
- health endpoint, including a period of 503
loadingwhile the port is already bound; /v1/models;- a multi-model mode with load/unload endpoints, a residency listing and auto-load on request;
- OpenAI-compatible chat completions;
- streaming, with a configurable chunk count, inter-chunk delay and chunk padding — the padding is what lets a test outrun a client that is not reading, so the gateway's backpressure path is actually entered rather than assumed;
- model mismatch;
- crash;
- stop delay.
Critical integration scenario:
1. Request coding-quality.
2. Gateway starts MTPLX/qwen38.
3. Waits for readiness.
4. Verifies model identity.
5. Proxies request.
6. Start a streaming request.
7. Request coding-fast while stream is active.
8. coding-fast waits.
9. Existing stream completes.
10. qwen38 drains and stops.
11. qwen36 starts.
12. Readiness succeeds.
13. Model identity is verified.
14. Queued request is forwarded.
15. Another coding-fast request does NOT restart qwen36.
Also test:
- two simultaneous requests for same cold model;
- runtime crash;
- wrong model returned;
- startup timeout;
- client disconnect during stream;
- cancellation;
- malformed config;
- missing executable;
- two entries with the same
backend_modelbut differentoptionsproduce different argv and force a restart when switching between them; - a reserved argument in
optionsorextra_argsfails config loading withRUNTIME_OPTION_RESERVED; - invalid adapter options fail with
RUNTIME_OPTIONS_INVALID; - an
unload_modeladapter releases its model without stopping the server; - a
stop_serveradapter releases by stopping the server, including one it attached to rather than spawned; - cross-adapter switching, oMLX → MTPLX → oMLX, asserting exactly one resident model after each transition;
- Ollama launch, tag normalization and explicit load/unload residency;
- a multi-model runtime that auto-loads a second model is brought back to one resident model before
ready; - a pinned model blocking the slot fails with
RUNTIME_MODEL_PINNEDinstead of leaving two models resident; - a
keep_residententry on its ownstop_serverruntime survives a switch: its server is never stopped, and the rotating entries still release each other; - a
keep_residententry sharing anunload_modelserver stays loaded while another model on that same server rotates — the case an adapter's stray-unloading would otherwise undo; - returning to a model already in memory calls neither
acquirenorrelease; keep_residenton astop_serverruntime shared with another entry fails config loading withCONFIG_INVALID, and so does the same clash spelled as two runtimes on one port;- shutdown releases kept entries too;
- an idle window releases the rotating occupant and nothing else: a kept entry survives it and is still released by shutdown (§29);
- an idle window leaves a
stop_serverserver the gateway attached to running, and unloads from anunload_modelserver whoever started it; - the idle timer arms after a request that was cancelled mid-acquire, where no lease was ever released — the case that distinguishes arming on the queue pump from arming on lease release;
- a request arriving while the idle sweep is mid-release queues behind it rather than racing it;
- a failed idle release leaves the gateway running and is retried on the next window;
- a shutdown landing mid-sweep releases each loaded entry once, not twice, and leaves no timer behind;
server.idle_unloadis read in every duration spelling, defaults to an hour, and0disables the sweep entirely;- a
disabledentry is omitted from/v1/modelsand from an agent's model list, and a request naming it is refused withMODEL_NOT_FOUNDbefore the scheduler is reached (§12); - disabling an entry an
agents:role names still loads the configuration, and fails only inapplyanddoctor; probe --saverefreshing a rediscovered entry leaves a hand-writtendisabled: truein place;- a runtime whose every model is disabled is reported as inert rather than dropping out of
doctorsilently; - attach when a healthy server already answers, spawn when nothing does;
- a server-scoped option on a model, or a model-scoped one on a runtime, is rejected and told where it belongs (§12);
- 503
loadingfrom health is treated asloading, not as a failure; probeagainst a fake server returns its models, and against a dead port reports "not running" rather than failing;probe --savereplaces only the probed adapter's entries, leaves others intact and writes a backup;probe --saveon a missing config creates one;- colliding discovered ids fail with
DISCOVERY_NAME_CONFLICTand leave the config byte-identical; - a discovered id starting with
-is skipped and reported, never rewritten; - an adapter reporting both a served list and an installed catalogue builds entries from the catalogue only, never writing one model twice;
- a model the runtime declares unusable is skipped and reported, not written;
- an existing entry that names no adapter is left in place and reported, never silently skipped over;
- an unowned entry whose key discovery produces is rewritten rather than failing as a collision;
POST /switchgoes through the scheduler: it drains, releases and acquires like a request-triggered switch, and a second switch for the same entry joins the first;lrd statusandlrd switchexit non-zero with a readable message when no gateway answers;- an Anthropic request routes, resolves and switches exactly like a chat-completions request, and its stream passes through unaltered;
- a request on a surface the runtime does not serve fails with
UPSTREAM_SURFACE_UNSUPPORTED; applymerges into an existing agent config, preserving unrelated keys, providers and settings, and writes a backup;applyrun twice produces an identical file;apply claudemaps roles fromagents:, and CLI flags override them for one run;applyrefuses a role whose runtime lacks the Anthropic surface, before writing anything;applywrites an env-var reference rather than a secret, and refuses where the format cannot express one;applyon an unparseable agent config reports and leaves the file untouched;apply opencodewith noagents.opencodewrites the provider block and leaves an existing default model selection untouched;apply claude/apply codexwith no entry inagents:asks once per role, from the configured model ids, and writes the answers;- a role left unset at the prompt is not written, and choosing nothing writes no file at all;
applywith no terminal to ask on fails readably instead of blocking on a prompt;- the client's
Authorizationheader reaches the upstream unchanged.
The gateway must satisfy every criterion below. Almost all of them are held by the automated suite described above; the ones marked (manual) have no meaningful automated form and are checked by hand.
- starts on localhost;
- exposes
/health; - exposes
/v1/models; - exposes
/v1/chat/completions; - exposes
/v1/messagesand/v1/messages/count_tokens; - exposes
/statusand/switchon loopback only; - supports streaming;
- returns OpenAI-compatible errors.
npm packproduces a tarball holding onlydist/index.js,bin/lrd.mjs, the manifest,README.mdandLICENSE, and declaring nodependencies(manual);npm i -g <tarball>installs one package with nonode_modulesand putslrdon PATH (manual);- the bundle serves a real request end to end —
lrd serve, then/status,/v1/modelsand one proxied completion (manual). The e2e suite consumes each package'stscoutput, not the bundle, so bundling regressions are invisible to it; lrd servestarts the gateway (manual);lrd statusreports the resident model, and fails readably with no gateway;lrd probereports what each backend serves, without needing a config;lrd probe <adapter> --url=... --savewrites a config, backing up the previous one;lrd doctornames the config file it loaded;lrd switch <model>goes through the scheduler;lrd apply opencode|claude|codexwrites a working configuration, merging and backing up.
- each agent integration is its own package (manual);
agents:roles validate againstmodels:and against runtime surfaces;- an agent absent from
agents:is still applied — without a mapping where its provider block stands alone, by asking where it does not; - OpenCode and Codex reach the gateway over the OpenAI surface;
- Claude Code reaches it over the Anthropic surface, with opus/sonnet/haiku mapped;
- no secret is ever written into an agent's configuration.
models:entries load from YAML;- an entry resolves to adapter + backend model + validated options;
- adapter options and
extra_argsreach the launch argv; - reserved arguments are rejected with
RUNTIME_OPTION_RESERVED; - logical model IDs are exposed through
/v1/models.
- runtime can start through CLI/process command;
- runtime can stop through CLI/process command;
- readiness is verified through HTTP;
- model identity is verified;
- switching is serialized;
- active requests drain before normal switching.
- MTPLX adapter works;
- LM Studio adapter exists and uses supported CLI/process lifecycle, releasing the model without stopping the server;
- oMLX adapter works: attaches or spawns, loads and unloads through documented HTTP, holds one resident model;
- Ollama adapter works: attaches or spawns, loads and unloads through its native API, holds one resident model;
- custom adapter works from YAML;
- core does not depend on concrete adapters (manual).
- at most one model is resident at any time, across all adapters, unless an entry sets
keep_resident; - the occupant is released before the next entry is acquired;
- cross-adapter switching works in both directions;
- a runtime that loaded an extra model on its own is corrected before
ready; - a model that cannot be released fails the switch instead of doubling residency;
- a kept entry is never released by a switch, and reaching it never releases the occupant;
- exactly one entry holds the serving token, whatever else is loaded;
- an adapter enforcing residency is told which models to spare;
- an idle gateway gives the memory back, and says why it did (§29).
- cold start works;
- warm requests do not restart runtime;
- model switching works;
- concurrent same-model requests are handled correctly;
- streaming survives normal gateway operation;
- startup/readiness/model mismatch failures are reported.
- default bind is localhost;
- no HTTP request can directly execute a configured command;
- custom commands are trusted configuration only.