feat(serve): qmd serve shared model server + RemoteQMD client (supersedes #511) - #663
brettdavies wants to merge 10 commits into
Conversation
fbfcdcd to
02f65c7
Compare
ec9811c to
8c55c43
Compare
qmd serve — shared model server, continuing #5114ac1351 to
3a7718d
Compare
|
Thanks for picking this up Brett, and for the care with the credit. The rebase is cleaner than my original #511, and the embed-over-remote work closes the gap I never got around to finishing. On your open questions: Which PR / credit (3): please take this one and close #511. The stacked version is strictly better than what I had, no need to reshape it back onto my branch. One daemon or two (1): I'd keep them separate. They've drifted into genuinely different jobs in practice: the MCP daemon is the agent-facing tool surface, serve is the raw model + index primitive layer that other processes embed against. Folding the primitives into the MCP daemon would mean every Versioning (2): no strong feeling. Happy to match whatever you settle on for #608, including One thing I can offer that isn't on the branch yet: a small guard on For what it's worth, we have been running this in production for a few weeks: serve with the ollama-compat backend in front of an NPU on an ARM board, a few agents in containers sharing one warm instance via |
|
Follow-up on the dim-guard offer: rather than make you wait, I rebased it onto this branch's head and pushed it ready to pull: https://github.com/jaylfc/qmd/tree/dim-guard-on-663 (one commit, 01cb340, applies clean on 3a7718d). |
|
One more ready branch while I was in there: this PR's head carries 6 new |
|
Hi @jaylfc, thanks for both of the commits. Pushing them as ready-to-pick branches instead of just describing them saved real time. 663 PR body updated to reflect their addition. Appreciate you confirming that #511 can be closed in favor of this pr. Great to hear that you've been running it in production for a few weeks. Hard to ask for better signal than that. |
6c4358d to
a49d315
Compare
|
Rebased onto current main (v2.6.3). The #662 lowVram engine commits dropped on their own, since git's patch-id caught them as already-applied. Renamed the bundled client One test fix while rebasing: main's new |
soundadam
left a comment
There was a problem hiding this comment.
I tested this head with Node 22 / pnpm 10: TypeScript passes and the focused suite passes (218 passed, 13 skipped).
Requesting changes for the HTTP security boundary:
serve.tssetsAccess-Control-Allow-Origin: *for every response.- All index and inference routes are unauthenticated, including
/browse,/search,/collections,/vsearch,/embed,/rerank, and/expand. /browseexposes document paths and snippets. With wildcard CORS, a hostile web page can read a QMD instance bound to127.0.0.1; binding to a LAN address additionally exposes it to unauthenticated network clients.
Please make CORS deny-by-default (or explicit trusted origins), require authentication for every non-health route, and refuse non-loopback binding unless authentication is configured or a clearly named unsafe override is supplied. Add browser-origin, missing-token, invalid-token, and non-loopback-negative tests.
The shared-daemon/client direction is useful, but this boundary needs to fail closed before merge.
|
Thanks for the careful pass @soundadam. The security boundary is real: I scoped this pr to the shared model server and the client path, so I'd rather keep the auth/CORS If it's required, the minimal shape I'd propose (no new deps, keeps the loopback dev path zero-config):
That leaves TLS, multi-user, and rate limiting out, and keeps the MCP |
|
@jaylfc thinking about splitting the low-vram engine (#662) back out of #663 so each lands single-concern, the way you |
Adds qmd serve — HTTP server for embedding, reranking, query expansion. Supports local (node-llama-cpp) and rkllama (RK3588 NPU) backends. RemoteLLM client auto-activates via QMD_SERVER env var. Includes: - Batch embedding (single HTTP call for all chunks) - NPU timeout/retry tuning for ARM SBCs - rkllama rerank via logit-based scoring - Index endpoints: /search, /browse, /collections, /status - Security: default bind 127.0.0.1, 50MB body limit, type validation - Updated README and CHANGELOG (cherry picked from commit 108559d) (cherry picked from commit d7b340d)
Embeds the query via the configured backend (rkllama/local), then runs sqlite-vec nearest-neighbour search against stored vectors. Returns ranked results with cosine similarity scores. Enables TinyAgentOS to offer semantic memory search over HTTP. (cherry picked from commit cc225b1) (cherry picked from commit a33cf0f)
…I naming - Backend type: 'rkllama' → 'ollama' (Ollama-compatible, works with rkllama/ollama/etc) - CLI: --backend-url replaces --rkllama-url (old flag kept as deprecated alias) - Class: RKLlamaBackend → OllamaCompatBackend - Default URL: localhost:11434 (standard Ollama port) - All internal comments genericised - --rkllama-url and RKLLAMA_URL env var still work for backwards compat (cherry picked from commit b6c1019) (cherry picked from commit 6c21a7b)
…L reaches RemoteLLM
The serve / RemoteLLM commits earlier in this PR wire `setDefaultLLM(new RemoteLLM(...))` at the CLI entrypoint, but the store layer hardcoded `getDefaultLlamaCpp()` at every embed / expandQuery / rerank call site. The polymorphic accessor was set correctly and then bypassed one layer deeper. `qmd query` continued to load the local `LlamaCpp` for all three pipeline stages, allocating ~5.4 GB of VRAM even when a healthy `qmd serve` daemon was reachable.
On a VRAM-constrained box (e.g. shared with a co-resident Ollama), this silently OOMs the rerank stage with `Failed to create any rerank context`; with more headroom, the bug is invisible but the user is paying for two LLM backends.
Changes:
- `src/store.ts:84` (`getLlm`), `:3562` (embed), `:3737` (expandQuery), `:3783` (rerank) now use `getDefaultLLM()` and accept a widened `LLM` override instead of `LlamaCpp`. `Store.llm` widens to `LLM` to match the documented "Can be LlamaCpp or RemoteLLM" comment.
- `src/cli/qmd.ts:getStore` skips `setDefaultLlamaCpp(new LlamaCpp(...))` when `QMD_REMOTE_URL` is set, so the CLI no longer eagerly allocates VRAM the user delegated to `qmd serve`. The `--remote-url` flag mirrors into `process.env.QMD_REMOTE_URL` so one source of truth gates the guard.
- `src/llm.ts`: `LLM` interface gains optional `embedModelName` / `generateModelName` / `rerankModelName` readonly accessors so existing consumers can keep their `?? DEFAULT_*` fallbacks without casting. Also drops the buggy CommonJS `require("./llm-remote.js")` lazy import in `getDefaultLLM` (broke under vitest ESM resolution); the static import does not introduce a cycle because `llm-remote.ts` only type-imports from `llm.ts`.
- Indexing paths that genuinely need the local engine (`generateEmbeddings`, embedding fingerprint adoption) go through a new `requireLlamaCpp(llm, op)` helper. It accepts a real LlamaCpp unconditionally, refuses with a clear message when the active LLM is a RemoteLLM, and casts through for duck-typed test fixtures so existing mocks keep working. Querying (embed, expand, rerank, vec) works end-to-end against `RemoteLLM`.
Test changes:
- New `test/store-remote-llm.test.ts` (6 tests): covers both `llmOverride` and `getDefaultLLM()` routing for `expandQuery` and `rerank`, plus the `Store` adapter routing through `Store.llm`. All pass under both Node/vitest and Bun.
- `LlamaCpp Integration` describe block in `test/store.test.ts` now opportunistically routes through `qmd serve` (probes `http://127.0.0.1:7832/health` in `beforeAll`, calls `setDefaultLLM(new RemoteLLM(...))` when reachable). This turns the integration suite into an end-to-end regression check for the remote path AND lets the rerank tests pass on VRAM-constrained dev boxes where the local rerank model can't load alongside a co-resident model. Falls back to local LlamaCpp when the daemon isn't running.
- `rerank deduplicates identical chunks across files` spy updated from `getDefaultLlamaCpp` to `getDefaultLLM` to match the new routing.
- Test environment hermetics: `vitest.config.ts` and `src/test-preload.ts` both unset `QMD_REMOTE_URL` so a developer's shell-set env var doesn't accidentally route 200+ unrelated tests through a real daemon (or fail them with `ECONNREFUSED` when it isn't running).
Drop store.ts's requireLlamaCpp gate that refused RemoteLLM for generateEmbeddings and embedding-fingerprint adoption. Both paths use only LLM-interface methods (session.embed, chunkDocumentByTokens), so the LlamaCpp narrowing was overly defensive: it blocked qmd embed when QMD_REMOTE_URL was set without a real technical reason. Widen the supporting machinery to match: LLMSessionManager, LLMSession, withLLMSessionForLlm, and getSessionManager now operate on LLM, not LlamaCpp. embedBatch on the LLM interface accepts EmbedOptions for parity with embed. Add an optional ready() hook so backends with async metadata (RemoteLLM fetching model names from /health) can warm up before callers read embedModelName. chunkDocumentByTokens switches from getDefaultLlamaCpp() to getDefaultLLM(); the detokenize fallback in the pathological-single-line branch now char-truncates when llm.detokenize is absent (RemoteLLM never had it), keeping correctness without requiring a tokens-to-text round trip the dummy remote tokens could not service.
RemoteLLM now fetches /health in the background at construction time so embedModelName, generateModelName, and rerankModelName are populated by the time the embed flow reads them. A ready() method awaits the in-flight warm-up; generateEmbeddings calls it before deriving the active model URI so vectors are tagged with the server's real embedding model, not DEFAULT_EMBED_MODEL. RemoteLLM.embedBatch now forwards options to the server, and the /embed-batch handler on serve threads them to the local LlamaCpp backend (parity with the existing /embed path). ModelBackend.embedBatch widens to accept options across LocalBackend and OllamaCompatBackend. Tests cover both halves: a real RemoteLLM against a tiny inline /health stub drives generateEmbeddings to a clean early return (proving the gate is gone), and a TokenOnlyLLM mock exercises the new char-truncation fallback path in chunkDocumentByTokens for backends without detokenize.
vitest no longer scrubs QMD_REMOTE_URL from the test process: if it's set in the shell, a new setupFile (src/test-setup-remote.ts) registers a RemoteLLM as the default LLM so withLLMSession-driven tests share serve's resident models instead of allocating their own LlamaCpp on the GPU. Unset (CI default) keeps the original hermetic behavior. The LlamaCpp Integration describe block in test/llm.test.ts now also skips when QMD_REMOTE_URL is set, since those tests bind directly to getDefaultLlamaCpp() and would still try to load local models even though serve owns the VRAM. Net effect on this box: the three VRAM-bound failures (mcp hybridQuery reranks, mcp hybridQuery full pipeline, llm session rerank) pass cleanly instead of failing 'context size 2048 too large for the available VRAM'.
When the index was built with a different embedding model than the server is currently using, the query vector's dimension doesn't match the stored vectors and sqlite-vec throws a raw "Dimension mismatch" error that surfaced as an opaque 500. Catch that case and return a 409 with the query model + dimension and a rebuild hint, so the operator immediately knows the index needs rebuilding (or the wrong model is configured) instead of debugging a 500. (cherry picked from commit 01cb340)
…ontracts (cherry picked from commit 81d2e29)
Open PR tobi#705 (OpenAI-compatible remote embedding/reranking) introduces its own RemoteLLM plus HybridLLM for the model-backend tier (qmd talking to a model provider). Our class is a different thing at a different tier: an LLM-shaped client that talks to a remote qmd serve instance over HTTP. Two classes named RemoteLLM would clash on merge and read as the same concept. Rename the class RemoteLLM to RemoteQMD and RemoteLLMConfig to RemoteQMDConfig, and the file src/llm-remote.ts to src/remote-qmd.ts (a near-homophone of tobi#705's src/remote-llm.ts). RemoteQMD also matches the repo's all-caps QMD type convention (QMDStore) and leaves room to grow search/vsearch methods if the client later pulls the corpus, not just models. The getDefaultLLM/setDefaultLLM seam both PRs share is left unchanged. The --remote-url flag and QMD_REMOTE_URL env var are unchanged.
eec6061 to
7fcc630
Compare
|
Split executed, as proposed above. This PR is now serve-only: the #662 engine commits are gone from the branch, along with the The combined shape (engine + serve + the integration surface, i.e. this PR as it stood before the split) lives in #927 as promised. @jaylfc, that branch is the drop-in for production; this one no longer carries the engine. Also rebased onto current
@jaylfc's five commits carry through with authorship intact. |
|
@brettdavies sorry for the slow answer on your July question — it deserved a faster one. Prod shape on my side: serve + ollama-compat end to end. Given that, the split you executed is exactly right: #663 serve-only and #662 engine-only, independently mergeable, with #927 as the tracked combined branch. That matches how I scoped #511 originally, and the serve-only shape is the one I depend on. On your open question 3 (credit): the cherry-picks with Happy to test the serve-only branch against my rkllama setup if that helps it land. |
qmd serve: shared model server, continuing #511Why
Multiple QMD clients each cold-load the embed, generate, and rerank models into their own process. On a 16 GB device running three agents, that is 3× the model memory; on ARM64 / headless servers, each client has to compile
node-llama-cppagainst whatever GPU drivers are installed locally.qmd servecentralises the three models behind an HTTP server. Clients (CLIs in LXC containers, agents on separate hosts) talk to a single warm instance via theRemoteQMDclass introduced in this PR, activated byQMD_REMOTE_URL=http://host:7832. The remote client implements the sameLLMinterface asLlamaCpp, so it slots in transparently anywhere the SDK accepts an LLM.Use cases:
node-llama-cppinside each container.Running this fork on an RTX 3090 Ti alongside an Ollama Gemma 4 26B; the low-vram combination (this PR plus the engine flag in #662, carried together in #927) makes the three-model fit possible.
What this PR adds
qmd servesubcommand: long-running HTTP server with two backends.local(default): loads GGUF models vianode-llama-cpp.ollama: proxies to any Ollama-compatible REST endpoint (Ollama itself, rkllama on RK3588 NPU, etc).RemoteQMD(src/remote-qmd.ts):LLM-interface implementation that POSTs to the server. Auto-activates viaQMD_REMOTE_URL(or--remote-url <url>per-invocation). Fetches the server's actual model names from/healthin the background at construction time so embed call sites tag vectors with the server's model, notDEFAULT_EMBED_MODEL.getDefaultLLMsoQMD_REMOTE_URLactually reachesRemoteQMDat every call site. PreviouslyStore.llmand four call sites insrc/store.tshardcodedgetDefaultLlamaCpp(), bypassing the polymorphic accessor one layer deeper. Invisible on cards with headroom (clients silently paid for two LLM backends); on a 24 GB GPU with ~21 GB held by Ollama, this surfaced asFailed to create any rerank contextduring rerank.qmd embedwhenQMD_REMOTE_URLwas set is gone.LLM.embedBatchaccepts options,LLM.ready?()exposes async warmup soRemoteQMDcan populate model names from/healthbeforegenerateEmbeddingsreads them, and the server's/embed-batchhandler threads options through to the local backend.chunkDocumentByTokenschar-truncates by the actual chars-per-token ratio when the active LLM has nodetokenize(RemoteQMD never had it), eliminating the last LlamaCpp-specific dependency in the embed flow.qmd servewhenQMD_REMOTE_URLis set: a vitest setup file registers aRemoteQMDas the default LLM, so previously-GPU-bound integration tests share the daemon's resident models. The hybridQuery + session-rerank tests that failed under VRAM pressure pass cleanly via the daemon.Endpoints
/embed,/embed-batch/rerank/expand/tokenize/health/status/collections/search?q=…/vsearch/browseDefault bind is
127.0.0.1;--bind 0.0.0.0exposes to network. Body size capped at 50 MB. Strict input validation on every endpoint.Relationship to other open PRs
This PR is independent of the engine
--low-vramPR (#662): both base directly onmainand merge cleanly in either order. #662 addslowVramtoLlamaCppConfig, surfaces it as a global flag / env, and adds the two-pass on-demand reclaim path. The serve-side surface for that flag (help text, startup log tagging models resident vs on-demand, a warning when combined with--backend ollama) lives in the combined PR (#927) together with both stacks; that PR is the production shape for anyone running serve + low-vram on one GPU, and closes as redundant once #662 and this PR both land.This PR also overlaps in problem space with #608 ("Daemon-aware CLI fast-path"). Both keep models warm across requests. The honest comparison:
qmd mcp --http --daemon(+ #608)qmd serve(this PR)query,get,multi_getRemoteQMDqmd clients (multi-host, LXC)0.0.0.0A real question (open below) is whether these should remain two separate daemons or whether the model-primitive endpoints +
RemoteQMDshould land as an extension of the MCP daemon instead. The substance of @jaylfc's work (the primitive endpoints, theRemoteQMDclient, the ollama proxy backend) survives either shape; only the top-levelservesubcommand vsmcp --http --daemonchoice changes.This PR also shares a class name with #705 ("OpenAI-compatible remote embedding, reranking & query expansion"), and the two sit on orthogonal axes. #705 is the model-backend axis (where inference runs): its
RemoteLLM+HybridLLMlet a qmd process offload embedding / reranking / expansion to an OpenAI-compatible provider (vLLM, Ollama, OpenAI) while keeping local generation. This PR is the client axis (where qmd runs):RemoteQMDis a thin client that talks to a remoteqmd serveover HTTP. They compose rather than compete: aRemoteQMDclient points at a remoteqmd serve, and that server chooses its model source: local node-llama-cpp, ollama-compat, or (with #705) an OpenAI-compatible backend viaHybridLLM. Both build on the samegetDefaultLLM/setDefaultLLMseam, which is exactly where #705's hybrid would slot in. To let both land without a class-name collision, this PR renames its clientRemoteLLM→RemoteQMD(src/llm-remote.ts→src/remote-qmd.ts, matching the repo'sQMDStorecasing); #705 keepsRemoteLLMfor the backend tier.RemoteQMDalso leaves room to growsearch/vsearchmethods if the client later pulls the corpus, not just the models.Architecture
This branch bases directly on current
main: no engine commits, no dependency on #662.Commits on this branch:
The earlier commit subjects still say
RemoteLLMbecause they predate the rename;7fcc630flips the class, interface, andsrc/llm-remote.ts→src/remote-qmd.tsin one move (see #705 below).Design notes
Store-layer wiring through
getDefaultLLMThe CLI sets
setDefaultLLM(new RemoteQMD(...))correctly, but the store layer was hardcodinggetDefaultLlamaCpp()at every embed / expandQuery / rerank call site. The polymorphic accessor was being set and then bypassed one layer deeper. This PR:getDefaultLLM()and widens theirllmOverride?: LlamaCppparams toLLM.Store.llmwidens toLLMto match the documented "Can be LlamaCpp or RemoteQMD" comment.cli/qmd.ts:getStoreso it skips the eagersetDefaultLlamaCpp(new LlamaCpp(...))whenQMD_REMOTE_URLis set. The--remote-urlflag mirrors into the env so one source of truth gates the guard.embedModelName/generateModelName/rerankModelNamereadonly accessors to theLLMinterface so existing consumers keep their?? DEFAULT_*fallbacks without casting.require("./remote-qmd.js")ingetDefaultLLM(which fails under vitest ESM resolution) with a static import. No runtime cycle:remote-qmd.tsonly type-imports fromllm.ts.Widened
LLMinterface so embed flows through any backendThe last LlamaCpp-specific dependency in the embed flow was
detokenizeinchunkDocumentByTokens. That lifts in this PR:chunkDocumentByTokensswitches fromgetDefaultLlamaCpp()togetDefaultLLM(). The pathological-single-line fallback that previously neededllm.detokenize(tokens.slice(...))now char-truncates by the actual chars-per-token ratio when the active LLM has nodetokenize(RemoteQMD never had it; dummy remote tokens couldn't have round-tripped anyway).LLMSessionManager,LLMSession,withLLMSessionForLlm, andgetSessionManagerwiden fromLlamaCpptoLLM. Pure type widening (LlamaCppimplementsLLM), so no behavior change for local callers.LLM.embedBatchacceptsEmbedOptionsfor parity withembed.LLM.ready?()hook lets backends with async metadata (RemoteQMDfetching model names from/health) warm up before callers readembedModelName.generateEmbeddingsawaits it so vectors get tagged with the server's actual embedding model, notDEFAULT_EMBED_MODEL.generateEmbeddingsandmaybeAdoptLegacyEmbeddingFingerprintdrop the (previously overly defensive)requireLlamaCppgate. Both already used only LLM-interface methods.Tests routed through serve via vitest setup
Previous behavior:
vitest.config.tsandsrc/test-preload.tsunsetQMD_REMOTE_URLso developer shells could not accidentally route tests through a real daemon. Useful as a default but blocks the case this PR wants: tests that share serve's resident models instead of allocating their own LlamaCpp on the GPU. This PR:src/test-setup-remote.ts: whenQMD_REMOTE_URLis set, register aRemoteQMDas the default LLM viasetDefaultLLM, sowithLLMSession-driven tests route through serve.vitest.config.tsstops scrubbingQMD_REMOTE_URL. Unset (CI default) → hermetic local LlamaCpp; set → route through serve.test/llm.test.tsLlamaCpp Integrationdescribe block now also skips whenQMD_REMOTE_URLis set, since those tests bind directly togetDefaultLlamaCpp()and would still try to load local models even though serve owns the VRAM.Tests
Engine-level concurrency and reclaim tests for the low-vram mode live in #662. Synthetic tests there exercise the two-pass reclaim against fake llama backends so they run without GPU; an opt-in real-GPU integration test gated by
QMD_RECLAIM_INTEGRATION=1exercises the reclaim path end-to-end.On this branch:
test/store-remote-llm.test.ts(8 cases): covers bothllmOverrideandgetDefaultLLM()routing forexpandQueryandrerank, theStoreadapter routing throughStore.llm,generateEmbeddingsaccepting a realRemoteQMD(norequireLlamaCppgate), andchunkDocumentByTokenschar-truncating whendetokenizeis absent. All pass under Node / vitest.QMD_REMOTE_URLunset): 1228 / 1229 pass. The one failure (mcp.test.tscontent-type on the 2025-era no-session initialize path) reproduces identically on unmodifiedmain, so it predates this branch.tsc --noEmitclean.Backwards compatibility
servesubcommand andRemoteQMDclass. Purely additive.QMD_REMOTE_URLand--remote-urlare new (renamed fromQMD_SERVER/--serverin @jaylfc's earlier draft per a naming discussion; see the rename commit for the rationale). Nothing inmainreads them.qmd querypath is unchanged: with noQMD_REMOTE_URLset, behaviour is identical to today.LLMinterface widening is additive:embedBatchgains an optionalEmbedOptionsparameter,ready?()is a new optional method,embedModelName/generateModelName/rerankModelNameare new optional readonly accessors. Existing implementations (LlamaCpp) satisfy the wider interface without changes; existing consumers (no opt-in needed) are unaffected.Things I deliberately did not do
127.0.0.1; if you want network exposure (--bind 0.0.0.0) it is behind a firewall / Tailscale / etc. Adding token auth or mTLS is a separate concern.--backend rkllamawith anything more specific. @jaylfc's rename toollama-compatis the right generic name; specific NPU implementations like rkllama remain compatible via that interface.Open questions for you
qmd mcp --http --daemonbe one process or two? They are both long-running HTTP servers that hold models warm. The differences (MCP protocol vs REST, agent tools vs model primitives, localhost vs network) are real but orthogonal: one daemon could expose both surfaces. If you would prefer a single canonical daemon, the substance of this PR (primitive endpoints +RemoteQMDclient + ollama proxy backend) could land as an extension of the MCP daemon instead of a newservesubcommand. Happy to reshape if that is the call./v1/versioning, like Daemon-aware CLI fast-path: ~4× speedup for qmd query #608 introduced? This PR uses unversioned routes (/embed,/rerank, etc.) matching @jaylfc's original. If you want/v1/embedetc. for consistency with Daemon-aware CLI fast-path: ~4× speedup for qmd query #608's/v1/search, easy rename.(cherry picked from commit …)trailers. If you would rather take feat: remote model server (qmd serve) for shared inference across clients #511 directly instead of this rebased + stacked version, I am fine to close this and re-open a smaller PR against feat: remote model server (qmd serve) for shared inference across clients #511's branch with just my additions.Changelog
Added
qmd servesubcommand: long-running HTTP server exposing embed, rerank, expand, tokenize, vsearch, and read-only index endpoints. Two backends (localfor in-process node-llama-cpp;ollamafor any Ollama-compatible REST endpoint).RemoteQMDclient (src/remote-qmd.ts) implementing theLLMinterface against aqmd serveinstance, auto-activated byQMD_REMOTE_URLor--remote-url <url>.LLM.embedBatchacceptsEmbedOptionsand a new optionalLLM.ready?()hook for backends with async metadata warm-up.LLM.embedModelName/generateModelName/rerankModelNameaccessors so consumers can read the active backend's model identifiers without casting.Changed
src/store.tsembed / expandQuery / rerank paths) route throughgetDefaultLLM()instead of hardcodinggetDefaultLlamaCpp().Store.llmwidens toLLMto match the documented polymorphism.chunkDocumentByTokensno longer requiresdetokenize; it char-truncates by the actual chars-per-token ratio when the active LLM lacks the method. Eliminates the last LlamaCpp-specific dependency in the embed flow.generateEmbeddingsandmaybeAdoptLegacyEmbeddingFingerprintno longer go throughrequireLlamaCpp; they already used only LLM-interface methods.RemoteQMDas the default LLM whenQMD_REMOTE_URLis set, so integration tests share the running daemon's models instead of allocating their own LlamaCpp on the GPU.Type of Change
feat: New feature (non-breaking change which adds functionality)fix: Bug fixBREAKING CHANGE: Breaking API changeRelated Issues/Stories
--low-vrammachinery) since the split; the combined shape lives in feat: low-vram engine + qmd serve combined (production shape of #662 + #663) #927; overlaps in problem space with Daemon-aware CLI fast-path: ~4× speedup for qmd query #608 (compared above).Testing
Files Modified
Modified:
src/cli/qmd.ts:qmd servesubcommand wiring,--remote-urlflag mirroring into env, guarded eagersetDefaultLlamaCppwhenQMD_REMOTE_URLis set.src/llm.ts:LLMinterface widening (embedBatchoptions,ready?(), optional model-name accessors).getDefaultLLMnow uses staticimportforremote-qmd.js.src/store.ts: embed / expandQuery / rerank call sites route throughgetDefaultLLM();Store.llmwidens toLLM.chunkDocumentByTokensswitches togetDefaultLLM()with char-truncation fallback.generateEmbeddingsandmaybeAdoptLegacyEmbeddingFingerprintdrop therequireLlamaCppgate.vitest.config.ts: stops scrubbingQMD_REMOTE_URL.Created:
src/serve.ts: HTTP server entry, request validation, backend dispatch (LocalBackendandOllamaCompatBackendadapter classes live here).src/remote-qmd.ts:RemoteQMDclient.src/test-setup-remote.ts: vitest setup that registersRemoteQMDas the default LLM whenQMD_REMOTE_URLis set.test/store-remote-llm.test.ts: 8 cases covering the store-layer routing and RemoteQMD accept-paths.Renamed:
src/remote-qmd.tsis net-new). Two renames happen inside the branch history (rkllama→ollama-compatin the backend module, and the clientsrc/llm-remote.ts→src/remote-qmd.ts), but neither target exists in base, so both land as created, not renamed.Deleted:
Breaking Changes
Deployment Notes
qmd serveis opt-in; existingqmd queryworkflows are unchanged withQMD_REMOTE_URLunset.