send_prompt() accepts an ellmer Chat directly or an
llm_provider_ellmer(). Pin the model when creating the chat
if reproducibility matters; provider defaults can change. The adapter
preserves native turns, content, usage, cost and citations in the full
result.
Callbacks and chat ownership
Each send_prompt() evaluation deep-clones its native
chat. Callback registries, tools and conversation state on that working
chat are independent of the original. A callback’s R closure still
refers to the variables it originally captured. Register callbacks that
modify history on the working chat, inside a prompt wrap’s
parameter_fn, which runs after cloning:
library(tidyprompt)
chat <- ellmer::chat_openai(model = "gpt-4.1-mini")
prompt <- prompt_wrap("Summarize the discussion", parameter_fn = function(llm_provider) {
working <- llm_provider$get_chat()
working$on_request_start(function(turns) {
# Inspect `turns` and, if needed, call working$set_turns(...).
message("Starting a model request")
})
list()
})
result <- send_prompt(prompt, chat, return_mode = "full")
result$ellmer_chat$get_turns()Callbacks require the corresponding methods in your ellmer version. The adapter honors history replacement performed by request callbacks, including compaction, when preparing later feedback turns.
Schemas and tool results
Closed objects use ellmer’s native types. Open objects and
constraints such as numeric bounds or schema composition use
ellmer::type_from_schema() to preserve the schema.
Provider/model support for these schemas still varies. Raw schemas
return JSON-shaped R values rather than ellmer’s typed factors and data
frames. Install jsonvalidate to validate constraints
locally; tidyprompt requires it when a schema has constraints beyond the
basic native types.
Tidyprompt-created tools serialize lists as JSON objects/arrays and
data frames as arrays of row objects. Return an explicit JSON string to
control serialization. Native ellmer tools retain their own result
contract. Rich content results and tools using
ellmer::tool_context() need native ellmer execution.
Cross-provider tool conversion preserves nested argument schemas and the
native convert policy.
Documents and uploaded files
Use add_content() for any native ellmer Content object,
including documents, PDFs and uploaded-file references. Ellmer remains
responsible for file uploads, expiry and provider restrictions.
Attachments work with structured output and streaming, and stay in
native history during feedback without being reattached.
document <- ellmer::content_document_file("report.txt")
result <- add_content("Summarize this document", document) |>
send_prompt(chat, return_mode = "full")Structured streams
With ellmer 0.5.0, answer_as_json() and
stream = TRUE stream raw JSON chunks through the usual
stream_callback(chunk, meta). Final extraction keeps
ellmer’s R coercion, including data frames and factors. Providers/models
that use tool-based structured output, older ellmer versions, and custom
adapters without the required capabilities use blocking
chat_structured() instead.
Stream events and recovery
Set provider parameter stream_content = TRUE to receive
native events in meta$content. The first callback argument
remains a string: non-text events such as citations, thinking and tool
results receive "" and do not enter the accumulated answer.
Inspect meta$content_type to render these separately.
controller <- ellmer::stream_controller()
provider <- llm_provider_ellmer(chat, parameters = list(
stream = TRUE, stream_content = TRUE, stream_controller = controller
))
provider$stream_callback <- function(chunk, meta) {
cat(chunk)
# Call controller$cancel() to stop after the next chunk boundary.
}
result <- tryCatch(
send_prompt("Explain the report", provider),
tidyprompt_stream_error = function(e) {
# Also catches tidyprompt_stream_cancelled, a subclass.
list(partial = e$partial_response, chat = e$ellmer_chat, turn = e$partial_turn)
}
)Cancellation and iteration failures stop validation and feedback requests. The condition exposes the working chat and ellmer’s partial turn; partial text is never treated as a successful answer. Iteration failures do not trigger a second model request. Controllers require a streaming-capable path; the blocking structured fallback cannot be cancelled with a stream controller.
Request limits and observability
max_interactions limits tidyprompt’s outer evaluation
loop. Use send_prompt(prompt, chat, max_requests = 5) or
limit_requests(prompt, max_requests = 5) to bound
individual model requests across tool calls and feedback for regular
tidyprompt providers and ‘ellmer’. If both limits are supplied, the
smaller applies. The max_requests argument defaults to
NULL, which leaves any limit attached to the prompt in
effect. With an ‘ellmer’ provider, this requires ‘ellmer’ 0.5.0 and
counts the blocking structured path explicitly, because that method
bypasses its request hooks. The limit stops before the next model
request and raises tidyprompt_request_limit with the
working provider (and native chat when applicable) attached. It does not
prevent execution of tools requested by an already completed model
request, or bound transport-level retries made within one model
request.
prompt <- limit_requests("Research this question", 5)
prompt <- prompt_wrap(prompt, parameter_fn = function(llm_provider) {
working <- llm_provider$get_chat()
working$conversation_id <- "research-session-123"
working$on_request_end(function(turn) {
print(turn@tokens)
print(turn@cost)
})
list()
})
result <- send_prompt(prompt, chat, return_mode = "full")
result$ellmer_chat$get_tokens()Use native turn metadata and ellmer tracing for usage and latency;
the adapter’s HTTP fields do not represent native requests.
on_request_end itself does not fire on the blocking
structured path in ellmer 0.5.0; read the returned native turns there.
Token estimates and recorded cost cannot guarantee a final spending cap,
because a request may exceed the remaining estimate before it
completes.
Tools followed by structured extraction
Ellmer suppresses tools during native structured extraction, including structured streaming. Tidyprompt diagnoses this combination for both prompt-level tools and tools registered directly on the Chat. Use two evaluations when a task needs both: first gather information using tools, then extract a schema-constrained answer from that conversation.
research <- answer_using_tools("Find the relevant facts", tools = my_tools,
type = "ellmer") |>
send_prompt(chat, return_mode = "full")
# Tool results stay in the history; no new tools are needed for extraction.
extraction_chat <- research$ellmer_chat$clone(deep = TRUE)
extraction_chat$set_tools(list())
answer <- add_msg_to_chat_history(research$chat_history, "Extract the final result") |>
answer_as_json(schema = ellmer::type_object(summary = ellmer::type_string()),
type = "ellmer") |>
send_prompt(extraction_chat)Async, parallel and batch boundaries
send_prompt() is synchronous, including its extraction,
validation and feedback loop. Calling it from an ellmer async or batch
helper does not make that loop asynchronous. For Shiny, the
separate-process approach in
vignette("streaming_shiny_ipc") remains available.
Construct each chat in its worker instead of sharing a mutable Chat
across concurrent evaluations.
Use ellmer’s native async/parallel/batch APIs directly for independent native requests that do not need tidyprompt’s evaluation loop. A future integrated path must define asynchronous validation and tools, cancellation, and scheduling of data-dependent retries; those capabilities are not implied by this adapter.