Describe a reading configuration
Usage
gr_read_spec(
reader = "map_reduce",
model = NULL,
temperature = NULL,
max_answer_tokens = 1500L,
max_chunk_tokens = 700L,
max_summary_tokens = 500L,
top_k = 6L,
min_score = -Inf,
mmr = 1,
context_order = c("relevance", "document", "edges"),
rerank_candidates = 20L,
rerank_min_score = 4,
fan_in = 5L,
max_levels = 5L,
max_rounds = 4L,
preview_tokens = 1200L,
restate = c("auto", "always", "never"),
members = NULL,
cite = FALSE,
skim_model = NULL,
summary_model = NULL,
parallel = NULL,
delay_between_calls = 0,
on_overflow = c("warn", "error"),
effort = NULL,
premise = FALSE,
verify = FALSE,
...
)Arguments
- reader
Reader name; see
gr_readers().- model
Chat model id.
NULL(the default) means the model of the client the spec is read with, sogr_client(model = "gpt-4o-mini")is the model that answers, is budgeted for and is billed. A model named here, or in a recipe, is used whatever the client's.- temperature
Sampling temperature, or
NULLto omit the field. You do not need to null it yourself for reasoning models: it is dropped automatically for any model whose registry entry hassupports_temperature = FALSE, which includes the default model. Check withgr_model_info(model)$supports_temperature.- max_answer_tokens
Output cap for final answers and merges.
- max_chunk_tokens
Output cap for per-chunk calls.
- max_summary_tokens
Output cap for summarisation calls.
- top_k
For
retrieve,rerankanditerative: chunks to use.- min_score
For
retrieve: chunks scoring below this are dropped, but if that would leave nothing the single best chunk is used anyway.-Infdisables the filter. Not a cosine similarity when embeddings fall back to lexical vectors: the score is then a blend of cosine and BM25.- mmr
Diversity of selection, for
retrieveanditerative.1(the default) is plain top-k. Below 1, chunks are picked greedily bymmr * relevance - (1 - mmr) * similarity to what is already picked, so three chunks saying the same thing do not all get in and pay for each other. Costs nothing: the vectors are already computed.0.7is a reasonable place to start;0selects for novelty alone and will happily pick irrelevant chunks because they are different.- context_order
Where the selected chunks sit in the prompt.
"relevance"(default) is most relevant first;"document"restores the order they appear in the document, which reads better when chunks are consecutive;"edges"puts the strongest first and second-strongest last, burying the weakest in the middle, because transformers attend measurably better to the beginning and end of a long context than to its middle. Selection is unaffected. This decides only placement, and it applies toretrieveandrerank, the two readers that put several ranked chunks in one prompt.- rerank_candidates, rerank_min_score
For
rerank: how many chunks to score, and the score below which a chunk is discarded. The candidates are the chunks the word-matching prefilter ranks highest. When no chunk shares a word with the question, it cannot rank them, so they are picked by embedding similarity instead, or, when embeddings cannot rank them either, spread evenly over the document and the answer marked partial. Either way the run warns (gr_rerank_prefilter) andnotes$prefiltersays which.- fan_in, max_levels
For
hierarchical: summaries combined per call, and the recursion depth cap.- max_rounds
For
iterative: retrieve-assess cycles.- preview_tokens
For
preview: the cap on the outline the planner sees. The outline is built from section labels, sizes and short excerpts (never the full text), and per-section excerpts shrink until the whole thing fits, so every section stays visible to the planner rather than the outline being truncated and some sections never being offered to it at all. The planner is an LLM call about a long document and so prone to exactly the degradation this package manages; keeping its input small is the mitigation.- restate
Whether to repeat the question before the excerpts as well as after them:
"auto"when the body is long enough to bury the first ask,"always", or"never". A setting rather than a rule, because whether it helps is a question about your corpus and your model. Pointgr_compare()at two recipes differing only in this and find out.- members
For
ensemble: the reader names to combine.- cite
Ask for chunk-level citations (
[chunk 3]) in the answer. Map those ids back to pages viaans$evidence. Forced off forhierarchical, which answers from summaries: summaries carry no[chunk N]ids, so asking for citations there asks the model to invent them. Forskim, when its evidence has to be consolidated to fit and no[chunk N]label survives the consolidation, citations are not asked for either (notes$cite_dropped = TRUE); when some survive, a citation is checked against those.- skim_model, summary_model
Optional cheaper models for the per-chunk stages.
skim_modelis used byskim's extraction andrerank's relevance scoring;summary_modelbyhierarchical's summarisation.- parallel
Run per-chunk calls in parallel. Needs the
futureandfuture.applypackages; without them the run is sequential and says so.The trace is complete either way: workers keep their own and the parent absorbs them, in input order, so a parallel run reports the same calls, tokens and cost as the same run made sequentially. So is what the client keeps: a mock's or backend's
$calls()and$embeds(), and the hit, miss and write counts of an attached cache, include the calls made in workers, added back in input order. A replay client always runs one request at a time, since it hands out its recording in order.Two things do not cross the process boundary. The limits in
gr_options()are checked before a batch is sent and not inside it, since a worker cannot see what the others spend. So the pre-flight check, which runs in the parent, holds a parallel run to its worst case, every reply at its cap and as many merge or summary levels as replies that size need, against bothmax_callsandmax_cost_usd; and each batch goes to the workers only when it fits what the run has left at its own worst case, and otherwise runs one request at a time, each checked. And a handler that records calls in variables of its own (a closure passed togr_backend_client()orgr_mock_client()) records a worker's calls in that worker's copy, which this process never sees; ask the trace, or the client's$calls(), instead.- delay_between_calls
Seconds to sleep between sequential calls, for rate-limit shaping. Honoured by
map_reduce,refineandskim; the other readers do not sleep.- on_overflow
For
stuff:"warn"(truncate and say so) or"error".- effort
How much a reasoning model reasons before it answers, or
NULL(the default) to send nothing, so every request goes out at the provider's default exactly as before. One string, such as"low", applies to every call. A character vector named by stage sets it per stage:c(extract = "low", "synthesise.section" = "high"). A name covers each call whose trace label is that name or starts with it and a dot; the longest name that covers a label wins; an unnamed element, or one named"default", covers the labels no name does, and a label nothing covers is sent no effort. A name must cover a stem some registered reader, segmenter or stage declares (thecallscolumn ofgr_readers()andgr_segmenters()), or a label under one, so a misspelt name is an error of classhr_unknown_effort_keythat lists the stems, not a setting that reaches no call. It reaches the segmenters that call a model too, when the spec is part of a recipe. See the section on reasoning effort.- premise
Check the question's assumptions against the document. A question can take for granted something the document contradicts ("Why did revenue fall?" of a report in which it rose). Without this check a reader answers it on its own terms or reports
NOT_IN_DOCUMENT, and either way what the document does say is lost. Withpremise = TRUEthe prompts that answer the question tell the model that, when the excerpts state the contrary of an assumption in it, it is to say so in one sentence giving what they state instead, rather than answer. That sentence becomes$answerand$premise(see gr_answer), andis_not_found()isFALSEfor it. Excerpts that are merely silent are stillNOT_IN_DOCUMENT. See "Checking the premise" below for how each reader combines what its requests report. The verdict is the model's reading of the excerpts, not a check harnessr makes, so weigh it as you would any answer. Off by default because it changes what is sent: the prompts are longer, so a cache, a recorded replay or agr_read_many()store made without it does not answer a run with it, and the reverse.screenandextract, which answer no question, ignore it.- verify
Check the answer's quotations against the chunks they come from, and let the check decide what is accepted.
FALSE(the default) sends the same requests and accepts answers as before; thequote_*shadow columns are still recorded on checked evidence (see "Verifying quotations"). WithTRUE,gr_read()runsgr_verify_evidence()on every answer, writes its columns onto$evidence, marks the answer partial when a quotation is not found, and records aread.verifystep in the trace. Theiterativereader also asks each step that says it can answer for the passages it rests on, word for word with their chunk numbers, and stops only when every one is found in the chunk it names; a refused step is told which quotations were not found, and a run that ends with none accepted answersNOT_IN_DOCUMENT. Its evidence is then the quotations, not every chunk it was shown. A step that reports the premise false (premise = TRUE) is not asked for quotations and is not refused for lacking them: its verdict stops the loop as it does withoutverify. See "Verifying quotations" below.- ...
Extra fields for custom readers.
Checking the premise
With premise = TRUE, every request that answers the question carries the
rule: the single answering request of stuff, retrieve, rerank,
skim, hierarchical and preview; each chunk's request and the merge of
map_reduce; the draft and every revision of refine; each step and the
final answer of iterative; and the adjudication of ensemble. Requests
that only pick, score, summarise or copy out passages do not.
map_reduce treats a chunk that contradicts the premise differently from
one that only lacks the answer. The second is dropped, as it always was.
The first is passed to the merge as a finding that says the premise is
contradicted and what the excerpt states instead, and the merge, told the
same rule, weighs it against the chunks that did answer and may itself
report the premise false. notes$premise_chunks lists the chunks that
reported it, whatever the merge decided. ensemble does the same with its
members (notes$premise_members). refine carries the verdict in its
draft, so a later excerpt can add to the correction or overturn it.
iterative's step reports it in a false_premise field of its reply,
which is in the schema only when the premise is checked, and stops there.
A merge or adjudication that failed returns the findings joined, which is
no verdict: $premise is NA, and the notes still name the chunks or
members that reported it.
The answer records the check: $premise is NA when nothing contradicted
the question, and the correction when something did. gr_compare() and
gr_read_many() add a premise column to their summaries, print() and
gr_audit_report() say when the model reported the premise false, and the
trace has a read.premise step with the outcome.
Verifying quotations
A reader that puts the chunks it read into $evidence has quoted them by
construction, so for stuff, retrieve, rerank and the default
iterative a passing check says only that the chunks were the ones sent.
The check means something where a model chose what to quote: skim,
extract, screen, and iterative under verify = TRUE.
Two matchers score each quotation. The one that decides verified is the
check described in gr_verify_evidence(), unchanged. Beside it the answer
carries quote_status, quote_sim, quote_coverage, quote_edits and
quote_critical_ok from an edit-distance matcher that tolerates ligatures,
line-break hyphens and OCR letter errors while requiring numbers, units
and negations to be identical. It is recorded only, and decides nothing in
this release.
verify = TRUE changes what iterative sends (its schema and
instructions), so a cache or recording made without it does not answer a
run with it. The other readers send exactly what they sent before.
Out-of-range values
Numeric arguments are clamped into a usable range and the change is warned
about, never applied silently: top_k [1, 1e4], fan_in [2, 32],
max_levels [1, 12], max_rounds [1, 20], rerank_candidates [1, 1e4],
rerank_min_score [0, 10], preview_tokens [100, 1e5],
delay_between_calls [0, 600],
token caps [16, 1e6].
Reasoning effort
The default models reason before they write a reply, and that reasoning is
billed as output and counted against the same output cap as the reply.
Nothing sets how much unless effort does, so a corpus's hundreds of
per-chunk calls reason as long as the one call that drafts its claims.
The effort is sent as reasoning = {"effort": ...} on the Responses API and
as reasoning_effort on Chat Completions, and a gr_mock_client() or
gr_backend_client() handler finds it in params$effort. It is sent only to
a model whose gr_model_info() says reasoning = TRUE, and left out without
a warning for any other, which would refuse the field, as temperature is
left out for a model that refuses that; register a gateway's reasoning model
with gr_register_model() to reach it. For the calls it covers it replaces
an effort the client's extra_body sets. A gr_ellmer_client() cannot take
it per call, says so once (gr_effort_unsupported) and does not record it
as sent. A value is passed
through as given; one that is not "none", "minimal", "low",
"medium", "high" or "xhigh" is warned about (gr_effort_unknown) and
sent anyway, since providers add levels.
Each request's effort is in its trace step (params$effort, and an effort
column of as.data.frame(trace)), and gr_audit_report() shows it beside
what each stage cost. It is part of the gr_cache() key, and, when set, of
a recipe's store key in gr_read_many(), of what gr_compare() counts as
a different recipe, and of the key a gr_replay_client() matches on, so
runs at different efforts never share answers. Unset, none of those keys
change.
The labels are the stage column of as.data.frame(trace):
segment.context, segment.proposition; stuff.answer; map.answer,
reduce; refine.draft, refine.revise; skim.extract,
skim.consolidate, skim.answer; retrieve.answer; rerank.score,
rerank.answer; hier.summarise.L1 (and L2, ...), hier.answer;
iterative.step, iterative.final; ensemble.adjudicate; preview.plan,
preview.skim, preview.answer; screen.decide; extract.chunk,
extract.resolve; and, for gr_claims(), gr_outline() and
gr_synthesise(), claims.draw, claims.reconcile, outline.derive,
synthesise.section, synthesise.batch, synthesise.merge and
synthesise.revise.structure (.cut, .register). A merge that takes
more than one level labels the levels below its final merge .level1,
.level2 and so on, which its own name covers.
A starting point, not a default: low for the many mechanical calls and high for the few that judge.
effort = c("extract.chunk" = "low", "skim.extract" = "low",
"rerank.score" = "low", segment = "low", "map.answer" = "low",
"claims.draw" = "high", "outline.derive" = "high",
"synthesise.section" = "high", "ensemble.adjudicate" = "high")Reasoning shares the reply's cap, so a call at "high" can spend all of it
and come back cut off (finish_reason = "length", warned about where it
matters). Give such stages room: max_answer_tokens here,
max_section_tokens in gr_synthesise(), max_outline_tokens in
gr_outline().
Requests to a reasoning model are sent with an output cap of at least 2048
tokens whatever the effort, and the cost limits are checked at that cap.
See also
gr_readers() for the available readers and their call costs,
gr_read(), gr_recipe()
Other reading functions:
gr_answer,
gr_check_reader(),
gr_read(),
gr_reader_signature(),
gr_readers(),
gr_register_reader(),
is_not_found(),
new_answer()
Examples
gr_read_spec("retrieve", top_k = 8)$top_k
#> [1] 8
# Out-of-range settings are corrected loudly, not quietly.
suppressWarnings(gr_read_spec("hierarchical", fan_in = 999)$fan_in)
#> [1] 32
# Reasoning effort by stage: little for the per-chunk calls, more for the
# merge. The trace says which request carried which.
ch <- gr_segment(gr_example(), list(method = "paragraph", max_tokens = 150))
#> Using cached ingestion for this document + settings.
#> Segmenting with 'paragraph' (cap 150 tokens, overlap 0).
cl <- gr_mock_client(function(m, p) "Revenue was 45.2 million dollars.")
sp <- gr_read_spec("map_reduce", model = "gpt-5.6-luna",
effort = c(map = "low", reduce = "high"))
tr <- gr_trace()
invisible(gr_read(ch, "What was revenue?", cl, sp, trace = tr))
#> Reading with 'map_reduce' (all|N+logN|tree) over 5 chunk(s).
unique(as.data.frame(tr)[, c("stage", "effort")])
#> stage effort
#> 1 map.answer low
#> 6 reduce high
# A loaded question, checked against the document. The mock plays a model
# that finds the excerpts contradict it.
cl <- gr_mock_client(function(m, p)
"FALSE_PREMISE: Revenue rose 12% in 2023, to 45.2 million.")
ch <- gr_segment(gr_example(), list(method = "sentence", max_tokens = 400))
#> Using cached ingestion for this document + settings.
#> Segmenting with 'sentence' (cap 400 tokens, overlap 0).
ans <- gr_read(ch, "Why did revenue fall in 2023?", cl,
gr_read_spec("stuff", premise = TRUE))
#> Reading with 'stuff' (all|1|none) over 2 chunk(s).
ans$premise
#> [1] "Revenue rose 12% in 2023, to 45.2 million."
is_not_found(ans$answer)
#> [1] FALSE