Skip to content

qmd_py.llm.client

Pure HTTP client for the LLM router — embeddings, chat completions (query expansion), and reranking. No local model loading; see Architecture › LLM client.

client

Pure HTTP client for the llama.cpp router (MARQ_LLM_BASE_URL) - no local model loading concept at all, unlike the TS reference's node-llama-cpp based src/llm.ts. Prompt formatting is ported from that file's formatQueryForEmbedding/formatDocForEmbedding/isQwen3EmbeddingModel since those templates must match exactly for embeddings to be comparable to the TS reference's.

LlmClient

LlmClient(
    base_url: str,
    timeout: float = 120.0,
    transport: AsyncBaseTransport | None = None,
)

Thin wrapper over the router's OpenAI-compatible /v1/embeddings, /v1/chat/completions, /rerank, /tokenize, and /detokenize endpoints.

transport is httpx's standard injection seam, used by the tests to serve canned router responses through an httpx.MockTransport without a live router. Production callers leave it None and get httpx's real networking.

Source code in src/qmd_py/llm/client.py
def __init__(
    self,
    base_url: str,
    timeout: float = 120.0,
    transport: httpx.AsyncBaseTransport | None = None,
) -> None:
    """`transport` is httpx's standard injection seam, used by the
    tests to serve canned router responses through an
    `httpx.MockTransport` without a live router. Production callers
    leave it None and get httpx's real networking.
    """
    self._client = httpx.AsyncClient(
        base_url=base_url.rstrip("/"), timeout=timeout, transport=transport
    )

list_models async

list_models() -> list[str]

Model ids the router currently has presets for - backs doctor's check that the configured embed/generate/rerank models actually exist on the router, not just in qmd-py's own settings.

Source code in src/qmd_py/llm/client.py
async def list_models(self) -> list[str]:
    """Model ids the router currently has presets for - backs `doctor`'s
    check that the configured embed/generate/rerank models actually
    exist on the router, not just in qmd-py's own settings."""
    response = await self._client.get("/v1/models")
    response.raise_for_status()
    return [item["id"] for item in response.json()["data"]]

embed async

embed(texts: list[str], model: str) -> list[list[float]]

Embed a batch of texts in one request.

Returns:

  • list[list[float]]

    Vectors positionally matching texts. The router keys its

  • list[list[float]]

    response by index rather than guaranteeing order, so the

  • list[list[float]]

    results are re-sorted here - without that, every chunk in a

  • list[list[float]]

    batch could get another chunk's vector.

Raises:

  • HTTPStatusError

    The router returned an error status.

  • KeyError

    The response had no data array.

Source code in src/qmd_py/llm/client.py
async def embed(self, texts: list[str], model: str) -> list[list[float]]:
    """Embed a batch of texts in one request.

    Returns:
        Vectors positionally matching `texts`. The router keys its
        response by `index` rather than guaranteeing order, so the
        results are re-sorted here - without that, every chunk in a
        batch could get another chunk's vector.

    Raises:
        httpx.HTTPStatusError: The router returned an error status.
        KeyError: The response had no `data` array.
    """
    response = await self._client.post(
        "/v1/embeddings", json={"model": model, "input": texts}
    )
    response.raise_for_status()
    data = response.json()["data"]
    return [item["embedding"] for item in sorted(data, key=lambda d: d["index"])]

chat_json async

chat_json(
    messages: list[dict[str, str]],
    model: str,
    json_schema: dict[str, Any],
    max_tokens: int = 300,
    temperature: float = 0.7,
) -> dict[str, Any]

One chat completion constrained to json_schema via the router's response_format (an OpenAI-compatible, increasingly standard llama.cpp server feature) - returns the parsed JSON object. Used for query expansion (see search/hybrid.py).

Source code in src/qmd_py/llm/client.py
async def chat_json(
    self,
    messages: list[dict[str, str]],
    model: str,
    json_schema: dict[str, Any],
    max_tokens: int = 300,
    temperature: float = 0.7,
) -> dict[str, Any]:
    """One chat completion constrained to `json_schema` via the
    router's `response_format` (an OpenAI-compatible, increasingly
    standard llama.cpp server feature) - returns the parsed JSON
    object. Used for query expansion (see search/hybrid.py)."""
    response = await self._client.post(
        "/v1/chat/completions",
        json={
            "model": model,
            "messages": messages,
            "response_format": {"type": "json_schema", "json_schema": json_schema},
            "max_tokens": max_tokens,
            "temperature": temperature,
        },
    )
    response.raise_for_status()
    content: dict[str, Any] = json.loads(response.json()["choices"][0]["message"]["content"])
    return content

rerank async

rerank(
    query: str, documents: list[str], model: str
) -> list[float]

Relevance scores in the same order as documents - the router's response is keyed by index rather than guaranteed to preserve input order, so results are re-sorted into place here.

Always returns one score per document (unscored ones stay 0.0); callers rely on that length, hybrid.py zipping it against its candidate chunks with strict=True. Out-of-range indices are dropped rather than assigned: a stray large index would raise IndexError, and - worse, because it fails silently - a negative one would write onto the wrong document from the end.

Source code in src/qmd_py/llm/client.py
async def rerank(self, query: str, documents: list[str], model: str) -> list[float]:
    """Relevance scores in the same order as `documents` - the
    router's response is keyed by `index` rather than guaranteed to
    preserve input order, so results are re-sorted into place here.

    Always returns one score per document (unscored ones stay 0.0);
    callers rely on that length, hybrid.py zipping it against its
    candidate chunks with `strict=True`. Out-of-range indices are
    dropped rather than assigned: a stray large index would raise
    IndexError, and - worse, because it fails silently - a negative
    one would write onto the wrong document from the end.
    """
    response = await self._client.post(
        "/rerank", json={"model": model, "query": query, "documents": documents}
    )
    response.raise_for_status()
    results = response.json()["results"]
    scores = [0.0] * len(documents)
    for r in results:
        index = r["index"]
        if 0 <= index < len(scores):
            scores[index] = r["relevance_score"]
    return scores

tokenize async

tokenize(text: str, model: str) -> list[int]

Count tokens using the model's own tokenizer.

Used to fit text to the reranker's per-pair budget, where a chars-per-token estimate proved unsafe for dense code.

Returns:

  • list[int]

    Token ids; callers generally only need the length.

Raises:

  • HTTPStatusError

    The router returned an error status.

Source code in src/qmd_py/llm/client.py
async def tokenize(self, text: str, model: str) -> list[int]:
    """Count tokens using the model's own tokenizer.

    Used to fit text to the reranker's per-pair budget, where a
    chars-per-token estimate proved unsafe for dense code.

    Returns:
        Token ids; callers generally only need the length.

    Raises:
        httpx.HTTPStatusError: The router returned an error status.
    """
    response = await self._client.post("/tokenize", json={"model": model, "content": text})
    response.raise_for_status()
    tokens: list[int] = response.json()["tokens"]
    return tokens

aclose async

aclose() -> None

Close the underlying HTTP connection pool.

Prefer the async context manager. Once closed the client cannot be reused - further calls raise RuntimeError.

Source code in src/qmd_py/llm/client.py
async def aclose(self) -> None:
    """Close the underlying HTTP connection pool.

    Prefer the async context manager. Once closed the client cannot
    be reused - further calls raise `RuntimeError`.
    """
    await self._client.aclose()

is_qwen3_embedding_model

is_qwen3_embedding_model(model: str) -> bool

Whether a model slug names a Qwen3 embedding model.

Decides which prompt template the embedding helpers apply. Matched on the slug alone, since the router exposes no model metadata - so a model whose name doesn't say "qwen" and "embed" gets the default template even if it is one.

Source code in src/qmd_py/llm/client.py
def is_qwen3_embedding_model(model: str) -> bool:
    """Whether a model slug names a Qwen3 embedding model.

    Decides which prompt template the embedding helpers apply. Matched on
    the slug alone, since the router exposes no model metadata - so a
    model whose name doesn't say "qwen" and "embed" gets the default
    template even if it is one.
    """
    return bool(_QWEN_EMBED_PATTERN.search(model) or _EMBED_QWEN_PATTERN.search(model))

format_query_for_embedding

format_query_for_embedding(query: str, model: str) -> str

nomic-style task-prefix format (default) vs. Qwen3-Embedding's instruct format - the two embedding-prompt styles the TS reference supports.

Source code in src/qmd_py/llm/client.py
def format_query_for_embedding(query: str, model: str) -> str:
    """nomic-style task-prefix format (default) vs. Qwen3-Embedding's
    instruct format - the two embedding-prompt styles the TS reference
    supports."""
    if is_qwen3_embedding_model(model):
        return f"Instruct: Retrieve relevant documents for the given query\nQuery: {query}"
    return f"task: search result | query: {query}"

format_doc_for_embedding

format_doc_for_embedding(
    text: str, title: str | None, model: str
) -> str

Wrap a document chunk in the model's expected document template.

Must match format_query_for_embedding()'s counterpart for the same model: asymmetric embedding models are trained on a specific query and document framing, and mixing templates degrades similarity silently rather than failing.

Source code in src/qmd_py/llm/client.py
def format_doc_for_embedding(text: str, title: str | None, model: str) -> str:
    """Wrap a document chunk in the model's expected document template.

    Must match `format_query_for_embedding()`'s counterpart for the same
    model: asymmetric embedding models are trained on a specific query
    and document framing, and mixing templates degrades similarity
    silently rather than failing.
    """
    if is_qwen3_embedding_model(model):
        return f"{title}\n{text}" if title else text
    return f"title: {title or 'none'} | text: {text}"