Skip to content

Add remote embedding, reranking, and query expansion support - #629

Open
georgelichen wants to merge 5 commits into
tobi:mainfrom
georgelichen:merge-pr-517-remote-llm
Open

Add remote embedding, reranking, and query expansion support#629
georgelichen wants to merge 5 commits into
tobi:mainfrom
georgelichen:merge-pr-517-remote-llm

Conversation

@georgelichen

Copy link
Copy Markdown

This PR ports the remote embedding / reranking work from PR #517 onto the current upstream main, and includes the follow-up fixes needed to make it usable in the current tree.

What is included

  • add OpenAI-compatible remote embedding support
  • add OpenAI-compatible remote reranking support
  • add remote query expansion support via chat completions
  • fix drift against current main (build/model label integration fixes)
  • avoid initializing local node-llama-cpp during qmd embed when remote embedding is configured

Why this PR exists

PR #517 was based on an older branch state. This branch rebases the feature set onto the current upstream main and resolves the integration drift, so the remote LLM path can be reviewed against today's tree.

Verification

  • npx tsc -p tsconfig.build.json
  • npx vitest run --reporter=verbose test/remote-llm.test.ts test/remote-llm-integration.test.ts
  • npx vitest run test/store.test.ts -t "generateEmbeddings" --reporter=verbose
  • npx vitest run test/store.test.ts -t "Token chunking guardrails" --reporter=verbose

Notes

  • Query expansion uses expand_api_* when configured; otherwise normal qmd query "..." still falls back to local expansion.
  • Structured queries (intent:/lex:/vec:/hyde:) skip auto expansion entirely.

Jim Smith and others added 5 commits April 12, 2026 18:26
Support offloading embedding and reranking to remote OpenAI-compatible
servers (vLLM, Ollama, LM Studio, OpenAI) while preserving local query
expansion and tokenization via a hybrid routing layer.

- RemoteLLM: HTTP client with circuit breaker, dimension validation,
  batch splitting, auth headers, configurable timeouts
- HybridLLM: routes embed/rerank → remote, generate/expand → local
- LLM interface: add embedBatch, embedModelName; generalize singleton
  and session management from LlamaCpp to LLM
- Config: QMD_EMBED_API_URL/MODEL env vars or YAML models section
- Skip nomic/Qwen3 text formatting prefixes for remote models
- 36 unit tests + 30 integration tests against live vLLM

Related: tobi#489, tobi#427, tobi#446, tobi#511

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add intent? to LLM interface and ILLMSession expandQuery signature
  (store.ts passes { intent } but interface didn't declare it — tsc error)
- Derive embed model label from getDefaultLLM().embedModelName after
  getStore() so content_vectors.model reflects the actual LLM in use
  (previously always stored DEFAULT_EMBED_MODEL_URI even with remote)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RemoteLLM.expandQuery() calls /chat/completions when expandApiModel is
  configured; throws "expandApiModel not configured" otherwise
- Independent circuit breaker for the expand endpoint
- parseExpandResponse() parses lex/vec/hyde lines, filters terms that
  don't share a word with the original query, falls back gracefully on
  bad model output
- RemoteLLM.supportsExpand getter for routing decisions
- HybridLLM routes expandQuery to remote when remote.supportsExpand,
  otherwise falls back to local LlamaCpp (no interface changes)
- remoteConfigFromEnv() handles QMD_EXPAND_API_URL / QMD_EXPAND_API_MODEL /
  QMD_EXPAND_API_KEY and YAML expand_api_* fields
- Unit tests (mock HTTP server, VCR-style): payload shape, auth header
  fallback, lex/vec/hyde parsing, includeLexical=false filtering,
  fallback on bad output, query-term filtering, circuit breaker,
  HybridLLM routing (remote vs local), config env vars
- Integration tests: live server connectivity, all three types returned,
  includeLexical=false, intent incorporation, HybridLLM routing verified
  via LOCAL_SENTINEL sentinel (new VLLM_EXPAND_URL / VLLM_EXPAND_MODEL
  env vars, skipped when absent)
Merge PR tobi#517 and keep it compatible with the current main branch.

Constraint: Upstream main diverged after PR tobi#517, so a fast-forward merge was not possible
Rejected: Cherry-pick the PR commits directly | would still require the same compatibility fixes and lose merge context
Confidence: medium
Scope-risk: moderate
Directive: Keep RemoteLLM and HybridLLM aligned with the LLM tokenize/detokenize interface and verify Windows CLI wrappers separately from Unix shell scripts
Tested: npx tsc -p tsconfig.build.json; npx vitest run --reporter=verbose test/remote-llm.test.ts test/remote-llm-integration.test.ts
Not-tested: full vitest suite; npm run build wrapper script on Windows; live GitHub Actions
When the active embedding backend is remote, generateEmbeddings now uses
character-space chunking instead of token-based preprocessing. This keeps
qmd embed from initializing node-llama-cpp solely to tokenize input before
calling a remote embedding API.

The change is scoped to indexing. Query-time expansion and reranking keep
their existing routing rules, and a regression test now fails if remote
embedding falls back to local tokenization during indexing.

Constraint: Remote embedding backends do not expose a tokenizer interface in QMD today
Rejected: Change HybridLLM tokenize() globally | would alter query-time behavior and broaden risk unnecessarily
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If remote token-aware chunking is added later, keep qmd embed free of mandatory local llama initialization
Tested: npx tsc -p tsconfig.build.json
Tested: npx vitest run test/store.test.ts -t "generateEmbeddings" --reporter=verbose
Tested: npx vitest run test/store.test.ts -t "Token chunking guardrails" --reporter=verbose
Not-tested: Full end-to-end qmd embed against a live remote embedding service after this code change
@Kaspre

Kaspre commented May 23, 2026

Copy link
Copy Markdown

Running this PR (cherry-picked onto v2.5.1) in production. Endorsing.

Two minor rebase notes for anyone else picking this up against current main:

  1. vectorIndex() needs model = getDefaultLLM().embedModelName set explicitly — that line was dropped during conflict resolution on our end.
  2. One missed call-site rename: getDefaultLlamaCpp at cli/qmd.ts:3716.

Three small follow-ups we ended up needing in our local fork that you may want to fold in:

A) RemoteLLM.rerank sigmoid normalization (~3 lines). For rerankers exposed via llama.cpp's /v1/rerank (notably bge-reranker-v2-m3) and several other cross-encoder backends, relevance_score is log-odds (range roughly -10..+10), not 0..1. The qmd consumer (store.ts blend formula + --min-score 0.3 default) assumes 0..1, so without normalization every blended score ends up negative and "No results found" fires on every query. Applying σ(x) = 1/(1+e^-x) in RemoteLLM.rerank before returning preserves ordering and fixes the range. Safe no-op for rerankers that already emit 0..1.

B) RemoteLLM.expandQuery implementation (~100 lines). The expand_api_* config slots are present but expandQuery() throws, forcing all expansion through HybridLLM's local fallback. We implemented it as a POST to <expand_api_url>/chat/completions with a system prompt that gets a chat model (we run gemma3:4b on ollama cloud) to emit the same lex:/vec:/hyde: line-prefixed format that LocalLLM.expandQuery produces via grammar-constrained sampling. Parsing + fallback shape mirror LocalLLM exactly. End-to-end: 9 expansions in 1.3s against ollama-cloud, vs. ~minutes on a CPU-only host running the 1.7B local model. Happy to file as a follow-up PR if useful.

C) Pre-flight probe at vectorIndex() startup (~30 lines). When usesRemoteEmbedding === true, configuration mistakes currently surface only mid-batch. A 1-token probe at startup catches them before any real work. We hit this the hard way — silent fallback embedded for weeks on the wrong model. Optional, but cheap.

Operational signal: warm-query throughput averages 719ms across diverse queries (range 540–1012ms). No regressions in downstream consumers.

@Kaspre

Kaspre commented Jun 2, 2026

Copy link
Copy Markdown

Hi @georgelichen — I've opened #705, a consolidated PR that builds directly on your RemoteLLM/HybridLLM work here, rebased onto current main. It layers on a few production-correctness refinements: sigmoid normalization for log-odds rerankers, a RemoteLLM.expandQuery implementation, a startup pre-flight embed probe, a HybridLLM.rerank local fallback (symmetric with your expandQuery fallback), and oversized-rerank split/truncate recovery. The core architecture is credited to you in the PR. Would really value your review.

@georgelichen

Copy link
Copy Markdown
Author

Thanks again, @Kaspre — and thanks for building on this work so thoughtfully, and for the attribution in #705.

I've now gone through #705 and left a review there. I really appreciate the consolidation and the production-hardening work on top of the original RemoteLLM/HybridLLM path.

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.

2 participants