llm
OpenAI-compatible LLM transport: chat completions + embeddings.
All config resolves at construction/call time (never at import): constructor arg > env var > default. No retries here — reliability policy (retry-then-skip, cache fallback) belongs to the callers.
ChatClient
A schema-constrained chat completion, against any OpenAI-compatible server -- see the module docstring: config resolves per call/construction, never at import, and reliability policy (retry, degrade) is the caller's.
chat_json(messages, schema)
One chat call, requesting a JSON object matching schema.
Sends reasoning_effort="none" first (see the comment below: chain-
of-thought buys nothing for a small schema-bound reply and roughly
doubled latency when measured), retrying once without it for a
server that rejects the field with a 400 rather than ignoring it.
EmbeddingClient
One embedding vector per call, against any OpenAI-compatible server --
same construction-time config resolution as ChatClient.
embed(text)
The raw embedding for text, untruncated and unnormalized --
embed.truncate_normalize is the caller's job, not this client's.
base_url()
The configured LLM server root, resolved at call time: env var, else
DEFAULT_BASE_URL -- never cached, so a .env change takes effect
without restarting anything that only imports this module.
chat_model()
The configured chat model name, resolved at call time -- see base_url.
default_chat_fn(messages, schema)
Module-level lazy ChatClient; the default chat_fn for explain/tagging.
embed_model()
The configured embedding model name, resolved at call time -- see base_url.
load_env()
Load .env (repo root first, then cwd-upward search); real env always wins.
Called only from process entry points (cli.main, mcp_server.main) — never from library imports, so tests stay dotenv-free.
Any OTHER entry point must call this itself. A standalone script that imports attestation and skips it gets DEFAULT_CHAT_MODEL rather than the model in .env, silently and with no error — a one-off re-tagging script did exactly that on 2026-08-11 and ran against the wrong model until the banner it printed gave it away.