Generative models (System 2) produce text and code. Typed decision models (System 1) answer with a logical value, a choice, or a numeric score, together with their probabilities. gptr returns R vectors that can be used in comparisons, filters and control flow. A batch returns one answer per input.
The executed examples use judge, a fake classifier with
synthetic probabilities. Its setup is hidden to keep the examples short.
The outputs show the interface, not model accuracy or calibration.
Logical decisions and probabilities
abstracts = c(
trial = "We randomized 200 adults to drug A or placebo for 12 weeks.",
cohort = "We followed 5000 nurses for 20 years and recorded incident diabetes.",
pilot = "A pilot study of 30 patients; the allocation method is not reported."
)
is_rct = peter("Is this a randomized trial?", abstracts, model = judge)
is_rct
#> trial cohort pilot
#> TRUE (p=0.94) FALSE (p=0.07) TRUE (p=0.55)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
gptr_prob(is_rct)
#> trial cohort pilot
#> 0.94 0.07 0.55
gptr_prob(is_rct, "probabilities")
#> FALSE TRUE
#> trial 0.06 0.94
#> cohort 0.93 0.07
#> pilot 0.45 0.55The gptr_decision result is logical. Its value is
TRUE when P(yes) is at least threshold (0.5 by
default). gptr_prob(x) returns P(yes), while
gptr_prob(x, "confidence") returns
abs(2 * P(yes) - 1); confidence is different from the
probability of a positive answer.
Structured inputs and batch shape
A data frame supplies one record per row. An atomic vector or an
unnamed list supplies one state per element. A fully named list supplies
one structured state; I(x) also requests one state for the
whole object. Multiple inputs must have compatible batch sizes: a single
state can accompany every item in a batch.
studies = data.frame(
abstract = unname(abstracts),
sample_size = c(200L, 5000L, 30L),
row.names = names(abstracts)
)
peter("Was allocation randomized?", studies, model = judge)
#> trial cohort pilot
#> TRUE (p=0.94) FALSE (p=0.07) TRUE (p=0.55)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
record = list(abstract = abstracts[["trial"]], sample_size = 200L,
allocation = "randomized")
length(peter("Was allocation randomized?", record, model = judge))
#> [1] 1System 1 sends the input states to the selected provider, rather than
only an object name. Small structured values can be serialized in full;
large or unsupported values are described or truncated. For a large
object already loaded in R, derive the small text or structured records
the decision needs locally before calling. Check what information the
question needs before sending sensitive data; see
?gptr_egress, ?gptr_security and Execution model for how inline R
computation differs from these inputs.
Uncertainty in filters, functions and loops
min_confidence turns answers below the requested
confidence into NA by default. Unknown confidence is also
uncertain when the minimum is positive. For yes/no decisions,
min_confidence = 0.8 requires P(yes) at least 0.9 or at
most 0.1.
screened = peter("Is this a randomized trial?", abstracts, model = judge,
min_confidence = 0.8)
screened
#> trial cohort pilot
#> TRUE (p=0.94) FALSE (p=0.07) NA (p=0.55)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
names(abstracts)[!is.na(screened) & screened]
#> [1] "trial"
names(abstracts)[is.na(screened)]
#> [1] "pilot"Use isTRUE() for a scalar decision in if or
while when abstention is possible. Decide separately what
to do with an uncertain result; treating NA as exclusion is
a workflow choice, not a negative model answer.
is_trial = function(text) {
peter("Is this a randomized trial?", text, model = judge, min_confidence = 0.8)
}
included = character()
review = character()
for (id in names(abstracts)) {
decision = is_trial(abstracts[[id]])
if (isTRUE(decision)) included = c(included, id)
if (is.na(decision)) review = c(review, id)
}
list(included = included, review = review)
#> $included
#> [1] "trial"
#>
#> $review
#> [1] "pilot"The same function can live in an ordinary R file. A call inside a function or loop does not own a recorded document block. A matching System 1 cache entry can avoid another request; that is distinct from document replay. See Script as history.
uncertain = "stop" signals
gptr_error_s1_uncertain. TRUE or
FALSE supply a yes/no fallback; a function
function(state, answer) can return one replacement of the
answer’s type. Such a function may make another model request if you
explicitly implement that escalation, with its own egress, permissions
and usage.
Choices and fractional scores
samples = c(s1 = "Biopsy of the right lower lobe",
s2 = "Hepatocytes from a resected liver segment",
s3 = "Cortex from an epilepsy resection")
tissue = peter("Which tissue is this?", samples, model = judge,
choices = c("liver", "lung", "brain", "other"))
tissue
#> s1 s2 s3
#> lung (p=0.91) liver (p=0.88) brain (p=0.80)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
gptr_prob(tissue)
#> s1 s2 s3
#> 0.91 0.88 0.80
gptr_prob(tissue, "probabilities")
#> liver lung brain other
#> s1 0.03 0.91 0.03 0.03
#> s2 0.88 0.04 0.03 0.05
#> s3 0.05 0.05 0.80 0.10
severity = peter("Rate this event's severity.",
c(a = "Mild headache", b = "Anaphylaxis requiring adrenaline"),
model = judge, levels = c("mild", "moderate", "severe"))
severity
#> a b
#> 0.20 (conf 0.85) 1.88 (conf 0.90)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
gptr_prob(severity, "probabilities")
#> mild moderate severe
#> a 0.85 0.10 0.05
#> b 0.02 0.08 0.90gptr_choice is character; gptr_prob(tissue)
gives the probability of the selected label. gptr_score is
numeric, the expected zero-based level: here mild = 0,
moderate = 1, severe = 2. The example scores are 0.20 and 1.88, not
rounded labels. For scores, gptr_prob(x) gives confidence;
use "probabilities" to inspect the distribution over
levels. Choice and score confidence are provider-defined measures of
concentration, not established probabilities that the answer is
correct.
Provenance and caching
meta = attr(is_rct, "meta")
meta[c("model", "engine", "calibrated", "execution", "locality", "cached")]
#> $model
#> [1] "judge-s1-1.0"
#>
#> $engine
#> [1] "fake"
#>
#> $calibrated
#> [1] NA
#>
#> $execution
#> [1] "native"
#>
#> $locality
#> [1] "local"
#>
#> $cached
#> [1] FALSE FALSE FALSEThe metadata also includes usage, request IDs and, when available, a
model digest, server version and calibration provenance.
calibrated = NA means unknown; native decision models do
not become calibrated merely by returning probabilities. Emulated
answers have calibrated = FALSE. Slicing a typed result
keeps its probabilities aligned.
Matching answers are cached per input and question. With a
.gptr/ workspace the System 1 cache is durable; without one
it lives under the session’s temporary workspace. Cache identity also
depends on the model and answer options. Changed model identity, live
mode or a missing entry can require a request. A mutable local model
without verified immutable identity cannot promise durable reuse. Use
gptr_cache() to inspect the cache.
System 1 routes or judges System 2
tasks = c("Rename a column.", "Choose a mixed model for repeated measurements.")
for (task in tasks) {
hard = peter("Does this task need the stronger model?", task, model = judge)
session = peter(task, model = if (isTRUE(hard)) strong else fast)
print(session$text)
}
#> [1] "A quick answer from the fast model."
#> [1] "A careful answer from the strong model."
session = peter("Summarize mtcars in one sentence.", model = summary_model)
session |> peter("Did the analysis succeed?", model = judge)
#> [1] TRUE (p=0.94)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
session$turns
#> [1] 1The piped classifier receives a bounded description of the session’s status, designated value and last answer. It adds no agent turn. Judging the answer text is not verification of the calculations or saved artifacts; validate those in R.
Native Jev, local Ollama and explicit emulation
The following examples can contact real providers and are not
executed here. Jev reads TYPESAFE_API_KEY;
gptr_env() also recognizes jev-key,
JEV_KEY, JEV_API_KEY and
TYPESAFE_KEY aliases in a key file.
gptr_env("~/keys/jev.env")
peter("Is this a randomized trial?", abstracts, model = jev)
# Prepare an installed native decision model on your local Ollama server first.
peter("Is this a randomized trial?", abstracts, model = "ollama/clef-flash")Native Clef and Clef Flash use Ollama’s System One API and require server 0.35.1 or later. Their local-only preflight checks the endpoint and discovered model evidence; an unavailable or unsuitable model fails instead of silently falling back to a cloud model. Ollama chat models use a different adapter and return agent sessions.
Emulation uses structured output from a generative model. It must be
enabled explicitly in settings; writing an emulate: model
name alone does not enable it.
gptr_config(system1 = "emulate:anthropic/claude-haiku-4-5", .scope = "session")
peter("Is this a randomized trial?", abstracts,
model = "emulate:anthropic/claude-haiku-4-5")Emulation is a model request, is marked uncalibrated, and does not create native decision capabilities for a chat model. Select a native model when the workflow requires native probabilities, and assess calibration on the data relevant to that workflow.