Each recipe is an independent pipeline, so a recipe's answer is identical whether it is run alone or alongside others. Extraction is shared through the ingest cache, and segmentation is shared between recipes whose segment specs are identical, so comparing five readers over one chunking costs one chunking, not five.
Arguments
- source
File path, web address, or raw text; see
gr_ingest().- question
The question.
- recipes
A character vector of recipe names, or a list of
gr_recipes.- client
A
gr_client.- allow_duplicates
Run duplicates anyway (useful at temperature > 0 to measure variance).
- on_error
"continue"keeps going and records the failure;"stop"aborts the whole comparison.- correct
How to tell whether an answer is right, for the
correctandusd_per_correctcolumns.NULL(the default) adds neither. A character vector marks an answer right when its text contains any of the strings, ignoring case and runs of white space; an answer that found nothing, or a recipe that failed, is wrong, and one that found nothing because a limit stopped it isNA: the spending, token or call limit, the model allow-list or the output ceiling. A function is called with each recipe's gr_answer and returnsTRUE,FALSEorNA(not known); when it fails or returns anything else, a warning of classgr_bad_correctnames the recipe and itscorrectisNA. An invalidcorrectargument is an error of the same class.- ...
Overrides applied to every recipe.
Value
A list with answers (named list of gr_answer), summary, trace
(shared across all recipes, so it records every recipe's calls) and
document (source and ingestion stats). summary is a data frame with
one row per recipe and these columns:
recipe,segmenter,chunks,reader,signature,settingsWhich pipeline the row is, and what was changed from the recipe's defaults.
partial,chunks_used,answer_chars,not_found,errorWhat the answer is, and what went wrong, if anything.
calls,tokens_in,tokens_outThe model and embeddings requests the recipe made, its segmentation's included, and the tokens of those that were billed: a request answered from a cache or a recording is counted in
callsand costs nothing.usdWhat those requests cost, at the prices registered with
gr_register_model().NAwhen a model the recipe used has no registered price.usd_estimatedThe share of
usdspent on requests whose token counts were estimated locally because the provider reported none, from 0 to 1.NAwhen no request records how its counts were obtained, or whenusdis 0 or unknown.correct,usd_per_correctOnly with
correct: whether the answer was right, andusddivided by the number of right answers, which isusdfor a right answer,Inffor a wrong one that cost something andNAfor a wrong one that cost nothing or one whose correctness is not known.premiseOnly when a recipe checks the question's premise: what the model said the document states instead, or
NA; seepremiseingr_read_spec().
Details
Recipes that resolve to identical ingestion, identical segmentation and an
identical read spec are collapsed with a warning rather than billed twice. A
shared reader signature alone is not enough: two retrieve recipes with
different top_k share a signature and are genuinely different runs.
Each row of summary says what its recipe cost, priced from the requests
on the trace by the model each one used (see gr_trace_cost()). A recipe is
charged what it would cost run alone: a segmentation it shares with an
earlier recipe is charged to both, as max_calls counts it for both, so
sum(summary$usd) can be more than the comparison spent, which is
sum(gr_trace_cost(trace)$usd). Give correct and each row also says
whether the answer was right and what a right answer cost.
A comparison is one run for the spending and token limits: max_cost_usd
and max_tokens_total in gr_options() are checked against what every
recipe and every segmentation has spent and used so far, so a recipe that
reaches either limit stops partial and the ones after it are refused and
recorded as failed. max_calls is counted for
each recipe on its own, its segmentation included, so a recipe's answer
does not depend on its position in the list. A segmentation that a limit
cut short is not shared with a later recipe.
Examples
cl <- gr_mock_client(function(m, p) "Revenue was 45.2 million dollars.")
# Three pipelines over one document. Extraction is shared, so this costs one
# extraction, not three; only the segmentation and the reader vary.
cmp <- gr_compare(gr_example(), "What was revenue?",
c("fast", "precise", "needle"), client = cl)
#> Using cached ingestion for this document + settings.
#> Segmenting with 'paragraph' (cap 4000 tokens, overlap 0).
#> Reading with 'stuff' (all|1|none) over 1 chunk(s).
#> Segmenting with 'sentence' (cap 600 tokens, overlap 60).
#> Reading with 'skim' (all|N+1|none) over 2 chunk(s).
#> Segmenting with 'semantic' (cap 500 tokens, overlap 50).
#> Reading with 'retrieve' (topk|1|none) over 2 chunk(s).
cmp$summary[, c("recipe", "segmenter", "chunks", "reader", "signature", "chunks_used")]
#> recipe segmenter chunks reader signature chunks_used
#> 1 fast paragraph 1 stuff all|1|none 1
#> 2 precise sentence 2 skim all|N+1|none 2
#> 3 needle semantic 2 retrieve topk|1|none 2
# One trace covers all three, so this is the cost of the whole comparison.
gr_trace_summary(cmp$trace)
#> run_id calls cached steps tokens_in tokens_out errors
#> 1 run_20261106103537.693_67245a 5 0 16 2122 65 0
#> elapsed_s embed_calls embed_tokens tokens_reasoning tokens_cached_in
#> 1 0.11 0 0 0 0
#> usage_estimated
#> 1 1
# What each recipe cost, and what a right answer cost. The mock is free, so
# register a price for it under another name to see the columns filled.
gr_register_model("priced-mock", context_window = 128000, max_output = 4096,
input_usd = 2, output_usd = 8)
cmp <- gr_compare(gr_example(), "What was revenue?", c("fast", "precise"),
client = cl, model = "priced-mock", correct = "45.2 million")
#> Using cached ingestion for this document + settings.
#> Segmenting with 'paragraph' (cap 4000 tokens, overlap 0).
#> Reading with 'stuff' (all|1|none) over 1 chunk(s).
#> Segmenting with 'sentence' (cap 600 tokens, overlap 60).
#> Reading with 'skim' (all|N+1|none) over 2 chunk(s).
cmp$summary[, c("recipe", "calls", "usd", "correct", "usd_per_correct")]
#> recipe calls usd correct usd_per_correct
#> 1 fast 1 0.001318 TRUE 0.001318
#> 2 precise 3 0.001998 TRUE 0.001998