Skip to contents

gptr logo

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.55

The 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] 1

System 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.90

gptr_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 FALSE

The 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] 1

The 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.