knitr::opts_chunk$set(
collapse = TRUE, comment = "#>",
eval = identical(tolower(Sys.getenv("LLMR_RUN_VIGNETTES", "false")), "true")
)We will show both unstructured and structured pipelines, using open models: - deepseek-chat (DeepSeek) - llama-3.1-8b-instant (Groq) - openai/gpt-oss-20b (Groq)
You will need environment variables DEEPSEEK_API_KEY and GROQ_API_KEY.
library(LLMR)
library(dplyr)
cfg_ds <- llm_config("deepseek", "deepseek-chat")
cfg_groq1 <- llm_config("groq", "llama-3.1-8b-instant")
cfg_groq <- llm_config("groq", "openai/gpt-oss-20b")schema <- list(
type = "object",
properties = list(
label = list(type = "string", description = "Sentiment label"),
score = list(type = "number", description = "Confidence 0..1")
),
required = list("label", "score"),
additionalProperties = FALSE
)
out_s <- llm_fn_structured(
x = words,
prompt = "Classify '{x}' as Positive, Negative, or Neutral with confidence.",
.config = cfg_ds,
.schema = schema,
.fields = c("label", "score")
)
out_sThe shorthand lets you combine output column and prompt in one argument:
df |>
llm_mutate(
category = "Give a short category for: {text}",
.config = cfg_groq
)
# Equivalent to: llm_mutate(category, prompt = "Give...", .config = cfg_groq)Or with multi-turn messages:
Enable structured output directly in llm_mutate() using
.structured = TRUE:
schema <- list(
type = "object",
properties = list(
category = list(type = "string"),
confidence = list(type = "number")
),
required = list("category", "confidence")
)
# Using .structured = TRUE (equivalent to calling llm_mutate_structured)
df |>
llm_mutate(
structured_result = "{text}",
.config = cfg_ds,
.structured = TRUE,
.schema = schema
)This is equivalent to calling llm_mutate_structured()
and supports all the same shorthand syntax.
By default LLMR sends one request per row. With
.rows_per_prompt > 1, several rows are packed into a
single request: each row’s prompt is wrapped in a numbered tag
(<row_1>...</row_1>,
<row_2>...</row_2>, …), the block is appended
to the message, and the model is asked to answer each item inside a
matching numbered tag. LLMR splits the reply back into the original
rows. .rows_per_prompt = Inf sends the whole frame in one
call.
cities |>
llm_mutate(
geo = "Where is {city}? Give country and continent in their own tags.",
.config = cfg_groq1,
.tags = c("country", "continent"),
.rows_per_prompt = 3
)A few points worth keeping in mind:
get_batched_embeddings(), which
splits many texts across several embedding calls. The
.rows_per_prompt argument applies only to generative
calls.<row_i> block the model emits the requested field
tags, so batched tag output is intentionally nested one level. This is
the opposite of the flat-tag guidance for single-row calls; LLMR adjusts
the instruction automatically..structured = TRUE
together with .rows_per_prompt > 1 asks for a single
JSON object {"results":[{"row":i, ...}]} and maps each
element back by its integer row. It emits a one-time
warning, because it relies on the model following the protocol and
replaces strict provider-side schema validation with local parsing..rowpack_recovery (by default the unresolved rows are
retried at half the batch size, recursively, down to single rows).
Unrecoverable rows are returned as NA with a diagnostic
finish reason.temperature = 0, and modest batch sizes.llm_mutate() adds <col>_batch,
<col>_bn, and <col>_bi columns
identifying the batch, its size, and the row’s position within it. Token
counts and wall-clock duration are attributed once per batch (on its
first resolved row) so that summing those columns is correct. One
caveat: when a batch reply is entirely unusable and its rows succeed
only through recovery calls, the failed call’s spend has no successful
row to land on, so sums can slightly undercount in heavy-recovery
runs.llm_preview() renders exactly what llm_fn()
/ llm_mutate() would send, without any API call and without
reading or encoding files. It flags problems up front: missing files, a
"file" role combined with
.rows_per_prompt > 1, an embedding config with row
batching, and so on. The batch plan columns show how rows would be
grouped into calls.
df <- data.frame(text = c("good", "bad", "fine"), stringsAsFactors = FALSE)
LLMR::llm_preview(df, prompt = "Sentiment of: {text}", .rows_per_prompt = 2)After a run, llm_usage() summarizes outcomes and token
totals, and llm_failures() lists the rows that failed or
were truncated. Both read the diagnostic columns that
llm_mutate() and call_llm_par() already
produce. llm_usage() reports tokens, not dollars: multiply
by your provider’s current per-token prices yourself.
out <- df |>
llm_mutate(sentiment = "One-word sentiment for: {text}", .config = cfg_groq)
llm_usage(out) # counts + sent/received/total/reasoning tokens
llm_failures(out) # which rows failed or were truncated, and whyFor a call_llm_par() result you can re-run only the
failures with llm_par_resume().
schema2 <- list(
type = "object",
properties = list(
category = list(type = "string"),
rationale = list(type = "string")
),
required = list("category", "rationale"),
additionalProperties = FALSE
)
# Traditional call
df_s <- df |>
llm_mutate_structured(
annot,
prompt = "Extract category and a one-sentence rationale for: {text}",
.config = cfg_groq,
.schema = schema2
# Because a schema is present, fields auto-hoist; you can also pass:
# .fields = c("category", "rationale")
)
df_s
# Or use shorthand
df |>
llm_mutate_structured(
annot = "Extract category and rationale for: {text}",
.config = cfg_groq,
.schema = schema2
)