The Synthesizer

Turns retrieved chunks into query-independent extractive claims — one cached unit that answers many later questions, keeps every number, and captures precise provenance from the model's own citations.

A Synthesizer turns raw chunks into understanding. Where the Retriever fetches the source material, the Synthesizer reads it and writes a structured briefing — by default a query-independent list of atomic, source-grounded claims — using your LLM. It also notes which chunks it actually used; that record is the lineage that drives surgical freshness.

The contract

def synthesize(self, query: str, chunks: list[Chunk]) -> Synthesis:
    ...

Synthesis carries the understanding, the indices of the chunks it used, and an ok flag:

from coalent import Synthesis

Synthesis(
    understanding={"summary": "...", "claims": [...], "facts": {...}},
    used=[0, 2],   # only these chunks become provenance -> precise invalidation
    ok=True,       # False = degrade, don't cache fabricated content
)

The built-in choice for real understanding. It owns the envelope: it presents the candidate sources, requires strict JSON, and reads the model's own used citations to build precise provenance.

from coalent import SemanticCache, LLMSynthesizer, OpenAIProvider

cache = SemanticCache(retriever, LLMSynthesizer(OpenAIProvider(), model="gpt-4o-mini"))

What you get:

  • Extractive understanding New in v0.4 — by default, a query-independent list of source-grounded claims (alongside summary, entities, facts) that keeps every number. See below.
  • Precise provenance — only sources the model cited can invalidate the unit (a change to a retrieved-but-uncited source touches nothing).
  • No garbage — a parse failure degrades (ok=False): the unit keeps its raw evidence and is flagged, never serving fabricated text.

Extractive understanding — one unit, many questions

New in v0.4 By default LLMSynthesizer runs in extractive mode (extract=True). Instead of a question-shaped prose summary, it produces a query-independent list of atomic, source-grounded claims — one claim per fact, every number kept with what it measures, its unit, and any condition attached to it.

Two things follow:

  • One unit answers many questions. Because extraction ignores the seed query, the same source yields the same unit no matter which query first built it — so one cached unit serves diverse later questions instead of only the one it was born for.
  • It keeps every number. In our tests the prose default dropped roughly 40% of the numbers in a source; extraction copies values verbatim. understanding["claims"] is the substance; understanding["summary"] may be terse or empty.

extract=True is the default, so there's nothing to pass:

from coalent import SemanticCache, LLMSynthesizer, OpenAIProvider

cache = SemanticCache(retriever, LLMSynthesizer(OpenAIProvider()))   # extract=True by default

Extractive mode is strongest on structured / reuse-heavy sources — policies, pricing, specs, FAQs — where one document is asked many different questions. It's the foundation the rest of the v0.4 read path stands on: per-claim coverage, cross-unit recall, and atom selection all operate over these claims.

Escape hatch — v0.3 prose

Want the old query-grounded decision summary back? Pass extract=False:

LLMSynthesizer(OpenAIProvider(), extract=False)   # v0.3 prose summary, keyed to the seed query
!

Upgrading from v0.3? Newly built units now cache claims, not prose — existing cached units are untouched until they rebuild. Downstream code that read understanding["summary"] for the substance should read understanding["claims"].

Tune the extraction

The extractive prompt is exported as EXTRACTIVE_INSTRUCTION, so you can start from it and adapt for your domain:

from coalent import LLMSynthesizer, OpenAIProvider, EXTRACTIVE_INSTRUCTION

synth = LLMSynthesizer(
    OpenAIProvider(),
    instruction=EXTRACTIVE_INSTRUCTION + " Prefer SI units and note the effective date of each rate.",
)

Passing your own instruction overrides extract — a custom instruction always wins. Build on EXTRACTIVE_INSTRUCTION when you want to keep the query-independent behavior while adding your own rules.

Your prompt, our envelope

You decide what understanding to produce; Coalent always wraps it with the sources, a strict-JSON contract, and the citation list. Pass your own instruction (a string, or a query -> str function) and the fields you want back:

synth = LLMSynthesizer(
    OpenAIProvider(),
    instruction=(
        "Summarize the incident: likely root cause, blast radius, and the next "
        "action to take. Be specific and cite the runbook steps you used."
    ),
    fields=["summary", "root_cause", "blast_radius", "next_action"],
)

Coalent still injects the candidate sources and requires the model to cite the sources it relied on — so provenance is captured no matter what you ask for. The built-in default now extracts query-independent claims (see above) into summary / claims / entities / facts; a custom instruction like this one overrides the extractor to fit your domain.

Depth — completeness vs cost

New in v0.3 depth (0.0–1.0) dials how much the understanding captures, trading synthesis cost against how often a later query has to escalate to raw:

LLMSynthesizer(OpenAIProvider(), depth=0.8)   # 0.0 terse · 0.5 balanced (default) · 1.0 exhaustive

Watch cache.stats()["escalation_rate"] to tune it — a high rate means the understanding is too thin for your queries, so raise depth.

Providers

A provider is a thin generate(*, model, system, user, max_tokens, temperature) -> str:

ProviderUseNotes
StubProviderdev & testsdeterministic, no network
OpenAIProviderproductioncoalent[openai], reads OPENAI_API_KEY
AnthropicProviderproductioncoalent[anthropic], reads ANTHROPIC_API_KEY
from coalent import LLMSynthesizer, AnthropicProvider

synth = LLMSynthesizer(AnthropicProvider(), model="claude-haiku-4-5")

JSONPassthroughSynthesizer — structured data, no LLM

When your source is already structured — a REST/MCP tool returning JSON — there's nothing to summarize. JSONPassthroughSynthesizer treats the JSON as the understanding: no model call, no latency, no key. Objects become facts, other values become claims, and every chunk is cited so provenance and freshness still work exactly as with an LLM.

from coalent import SemanticCache, JSONPassthroughSynthesizer

cache = SemanticCache(tool_retriever, JSONPassthroughSynthesizer())
# a result like {"employee": "A", "annual_leave": 12} is cached as-is —
# and invalidated like a document when the tool result changes.

Reach for it when the data is the answer; use LLMSynthesizer when raw text needs to be understood. See the MCP & tool results example.

StubSynthesizer (no key)

For dev and tests, StubSynthesizer() returns the same structured shape deterministically — so the whole loop runs without an API key (the detail lives in the retained raw).

Build-side controls (v0.5)

New in v0.5 Two cache-level knobs shape what the Synthesizer sees at build time:

  • split_by_artifact (default off) — when retrieval mixes chunks from several artifacts, build one unit per source instead of one blended unit, so provenance and freshness stay per-source.
  • source_fetcher (callable) — bring your own source-chunk fetcher for widened builds: when widen_chunks=N is set and a miss-triggered build wants more of the dominant source than retrieval returned, Coalent calls it (or a duck-typed retriever.widen(artifact_id, limit=)) to read up to N chunks of that source. Widening never fires at ingest — see The gate ladder.

Custom synthesizers

Any object with synthesize(query, chunks) -> Synthesis works — wrap a local model, or return used=[i for i, _ in enumerate(chunks)] to cite everything. Set ok=False on failure to make Coalent degrade instead of caching bad output.

Next