This guide covers every breaking change from 0.30 through the unreleased changes
after 0.41. Releases 0.36, 0.37, 0.40 and 0.41 were the disruptive ones; 0.40
alone carried 31 breaking changes, and 0.37 renamed rig-core's library
target.
Sections run newest-first. Find the version you are on and read every section above it, in order. Each one is self-contained.
| You are on | Start at |
|---|---|
| 0.41 | 0.41 → next |
| 0.40 | 0.40 → 0.41 |
| 0.39 | 0.39 → 0.40 |
| 0.38 | 0.38 → 0.39 |
| 0.37 | 0.37 → 0.38 |
| 0.36 | 0.36 → 0.37 |
| 0.35 | 0.35 → 0.36 |
| 0.34 | 0.34 → 0.35 |
| 0.33 | 0.33 → 0.34 |
| 0.32 | 0.32 → 0.33 |
| 0.31 | 0.31 → 0.32 |
| 0.30 | 0.30 → 0.31 |
Everyone should read Silent behavior changes first. Those are the changes that leave your code compiling and make it do something different — the ones a compiler upgrade will not point at.
Nothing in this section produces a compile error. Grouped by the release that introduced it, oldest-first — read from the version you are on upwards, and check each entry against your code before upgrading.
rig-core's default feature switched from reqwest-tls (native TLS) to
reqwest-rustls, alongside the upgrade to reqwest 0.13. Unless you opt back in
with reqwest-native-tls, HTTPS now goes through rustls.
rustls does not read the platform trust store the way native TLS does. Private CAs installed in the macOS Keychain or the Windows certificate store, corporate TLS-inspecting proxies, and servers still on legacy cipher suites can start failing handshakes on a build that changed nothing but the Rig version.
Reasoning changed from { id, reasoning: [String], signature: Option<String> }
to { id, content: [ReasoningContent] }, where each block is typed (Text with
an optional signature, Summary, Encrypted, Redacted). The struct change
is a compile error, but the serde shape change is not: Message derives
Serialize/Deserialize, and there are no field aliases for the old names.
If you store conversation history as JSON, records written by 0.30 or earlier that contain reasoning blocks fail to deserialize on 0.31. Migrate the stored rows, or drop reasoning content from history you replay.
The constant kept its name and changed its value from "embedding-001" to
"gemini-embedding-001". Embeddings written before and after the upgrade come
from different models and are not comparable — re-embed the corpus, or pin the
literal "embedding-001" if the old model is what you want.
See also gemini embedding dimensions in 0.32 → 0.33, which moves the reported dimension count for this model.
reqwest's system-proxy feature was enabled (#1442). HTTP_PROXY,
HTTPS_PROXY, and NO_PROXY in the environment now route provider traffic;
previously they were ignored. On machines that set those variables for unrelated
reasons, provider calls start going through the proxy.
CompletionRequestBuilder::build no longer populates CompletionRequest.preamble.
It inserts the preamble as a leading Message::System in chat_history and
leaves preamble as None. The field survives only as a legacy carrier for
callers that construct CompletionRequest by hand.
If you implement CompletionModel yourself and read request.preamble, the
system prompt silently vanishes. Read the leading Message::System from
chat_history instead. Bundled providers were all updated.
max_tokens was dropped when building Chat Completions requests (#1495) and is
now sent. A limit your code has been setting with no effect starts applying, so
responses that ran to their natural stop can begin truncating.
A tool whose Output serializes to a JSON string used to be handed to the model
as serde_json::to_string(&output) — wrapped in quotes, with newlines escaped
as \n. It is now passed through verbatim (#1608). Non-string outputs are
unchanged.
This is the intended behavior, but a tool returning String now presents to the
model as raw text rather than a quoted JSON literal. Prompts and few-shot
examples tuned against the quoted form are worth re-checking.
0.31 switched rig-core's own HTTP client to rustls; 0.36 finished the job
(#1682). websocket is now an alias for websocket-rustls,
reqwest-middleware for reqwest-middleware-rustls, and the rustls /
native-tls features on the workspace fan out to the companion crates.
If you were relying on those paths still using native TLS, the trust-store
caveat from 0.31 now applies to
them as well. Opt back in with websocket-native-tls /
reqwest-middleware-native-tls.
Tool calls are validated against the registered tools and against tool_choice
before dispatch (#1823). A call naming a tool that does not exist — or any tool
call at all under ToolChoice::None — returns PromptError::UnknownToolCall
instead of being attempted.
Runs that previously limped along on a provider's occasional invented tool name
now stop. 0.38 also adds the recovery path: implement
PromptHook::on_invalid_tool_call and return
InvalidToolCallHookAction::{Retry, Repair, Skip, Fail}, and bound the retries
with .max_invalid_tool_call_retries(n).
Parameter schemas are generated by schemars instead of the previous hand-rolled type mapping (#1576). The macro's surface is unchanged, but what reaches the model is not:
- Integer parameters advertise
"type": "integer"; they were"number". - Structs, enums, and other non-primitive parameters get real schemas with
$defs, instead of a bare{"type": "object"}. - Doc comments on the function and on individual parameters become the tool and
parameter descriptions, replacing the
Function to <name>/Parameter <name>defaults. Option<T>parameters get#[serde(default)], so a model that omits them deserializes toNonerather than failing.
Better schemas usually mean better tool calls, but they are different input to the model. There is also a compile-time consequence — see section 1 of 0.37 → 0.38.
The highest-impact change in this release. max_turns and
default_max_turns now bound the exact total number of model calls,
including the initial call, tool continuations, and retries.
| Budget | Before | After |
|---|---|---|
0 |
initial call + 2 | no model call at all |
1 |
initial call + 2 | only the initial call |
n |
effectively n + 2 |
exactly n |
An unconfigured tool-then-answer flow now needs an explicit budget of 2. To
preserve the maximum allowance of an old explicit budget n, account for the
old effective n + 2; otherwise set the literal total you actually intend.
If you set max_turns at all, re-derive the number. A budget that used to
permit a tool round-trip may now stop after the first model call.
Responses with empty assistant content and no tool calls surface the shared path's "empty response" error on hyperbolic, perplexity, and huggingface. Previously each returned an empty text completion. Code that treated an empty string as a normal outcome now takes an error branch.
The shared conversion drops assistant messages carrying neither text nor tool
calls. Reasoning attached to a text or tool-call turn still round-trips via
reasoning_content; reasoning-only turns no longer survive in history.
Consequences of the GenericCompletionModel consolidation. None of these
change your source, all of them change what goes over the wire:
max_tokensis now forwarded by deepseek, together, hyperbolic, and azure. It was silently dropped before — these providers will now respect a limit your code has been setting all along with no effect.- together's streaming uses standard
stream/stream_optionsrather thanstream_tokens, and a rig-levelToolChoice::Requiredserializes asrequiredinstead of erroring. - perplexity's non-streaming endpoint drops a stray
/v1prefix. - mira sends its preamble as a
systemmessage instead ofuser. response_formatderived fromoutput_schemais deferred while tools are pending a result. groq, mistral, and azure previously applied it unconditionally.- groq's streaming usage no longer falls back to the legacy
x_groq.usageenvelope.
On the shared conversion, Video user content serializes a video_url content
part (an OpenRouter/gateway extension) rather than returning a client-side
conversion error. Providers without video support now reject it server-side
— the failure moved from your process to theirs, and moved later.
- Migrated providers' streaming spans are named
chatwithgen_ai.operation.name = "chat", previouslychat_streaming. Dashboards and alerts keying onchat_streaminggo blank. gen_ai.input.messages/gen_ai.output.messagesare intentionally left empty rather than recording serialized messages.gen_ai.request.modelreports the per-request model override when one applies.- minimax, zai, and xiaomimimo spans stop reporting as
"openai"and report their own provider name.
Ollama's native /api/chat has no top-level max_tokens field, so the value
was serialized into a field the server does not define and silently discarded.
It is now sent as the num_predict model parameter inside options, where
Ollama actually reads it.
Nothing to change — but if you set max_tokens on an Ollama agent at any point
and moved on when it appeared to do nothing, it starts applying now.
Responses that had been running to their natural stop will truncate at the
budget you configured, possibly long ago. Check the value is one you still want.
temperature is unaffected: it was already being sent inside options and only
a redundant top-level copy was removed.
Tool results carrying several ToolResultContent blocks were flattened before
being sent to the Responses and Chat Completions APIs. Individual blocks are now
preserved — retained as multipart when array-form tool results are enabled, and
flattened only where string-form content is required.
Tools that return mixed text/JSON/rich output now present to the model as distinct blocks rather than one merged blob. That is the intended behavior, but it does change what the model sees, so prompts tuned against the flattened shape are worth re-checking.
These macros are #[macro_export]ed, and a cfg inside a macro expansion is
evaluated in the calling crate. The old expansion therefore tested whether
your crate had a feature named wasm, not whether rig-core did. Any caller
without such a feature took the if_not_wasm! branch on every target, browser
wasm included.
They now key on all(target_arch = "wasm32", target_os = "unknown"). If you
defined a wasm feature and expected it to drive these macros, you now get the
target's answer instead. Gate on the target directly if you need the old
association.
CompletionResponse<T> is now CompletionResponse. The provider-native
raw_response field is gone; the normalized response carries the metadata that
callers actually reached into raw_response for:
pub struct CompletionResponse {
pub choice: OneOrMany<AssistantContent>,
pub usage: Usage,
pub message_id: Option<String>,
pub response_id: Option<String>,
pub finish_reason: Option<FinishReason>,
pub provider: String,
pub model: Option<String>,
}| Before | After |
|---|---|
response.raw_response.model |
response.model |
provider stop/finish reason off raw_response |
response.finish_reason |
provider/message identity off raw_response |
response.provider, response.message_id |
response-scoped ID (chatcmpl-*, responseId, …) off raw_response |
response.response_id |
| a genuinely provider-specific field | model.raw_completion(request).await? |
usage is unchanged, including the rule that all-zero values mean the provider
supplied no metrics. model is the identifier the wire response reported, not
the one you requested — it is None when the provider omits it. provider is
always populated, including on a response derived from a stream that ended
before its terminal record.
message_id and response_id are distinct on purpose. message_id holds only
identifiers the provider would recognize on a replayed assistant message (an
OpenAI Responses output-message msg_* ID, an Anthropic msg_* ID); it is what
agent history promotes into Message::Assistant's id. response_id holds
identifiers that name the response as a whole (an OpenAI chat chatcmpl-* ID, a
Gemini responseId, a Cohere generation ID) — useful for logging and support,
never echoed back to a provider. Code that previously read a chat provider's
message_id should read response_id instead; for those providers
message_id is now None.
CompletionResponse is #[non_exhaustive]; build it with
CompletionResponse::new(choice, usage, provider) plus the with_* helpers.
Use with_finish_reason / with_optional_finish_reason rather than assigning
the field: the setters apply FinishReason::reconcile_with_output, which
upgrades a reported Stop to ToolCalls when the turn actually carried tool
calls. Several OpenAI-compatible gateways report stop on a tool-calling turn,
so code branching on ToolCalls would otherwise miss the call.
Tests (or other code) holding a provider's raw response can re-derive the
normalized fields via the additive NormalizeCompletionResponse::normalize
bridge — the same conversion the provider's normalized path uses.
Every built-in provider model exposes both:
let native = model.raw_completion(request).await?; // the provider's own type
let native_stream = model.raw_stream(request).await?; // RawStreamingResult<TheirTerminal>These share one request builder, transport call, parser, telemetry path, and error-preservation path with the normalized methods — the normalized method calls the raw one and maps the result, so there is still exactly one network request.
The trade: raw access now requires the concrete provider model rather than any
CompletionResponse. Code that was generic over CompletionModel could never
touch raw_response without a bound anyway, so in practice this affects code
that had already committed to a provider.
pub enum FinishReason { Stop, Length, ToolCalls, ContentFilter, Other(String) }Unrecognized provider values are preserved verbatim in Other — in the
provider's own spelling, so Gemini's RECITATION stays RECITATION. A provider
adding a new terminal reason surfaces it rather than reading as a natural stop.
None means the provider genuinely reported no reason.
StreamingCompletionResponse<R>, StreamingResult<R>,
StreamedAssistantContent<R>, and the downstream agent streaming types are
concrete. Their terminal record is StreamFinal, which carries normalized
usage, finish reason, provider, provider-reported model, message ID, and
response ID.
A full Reasoning stream event supersedes prior ReasoningDelta events with
the same reasoning id — UIs that render deltas incrementally should replace
the accumulated text when the full block arrives, mirroring what the
aggregated choice already does.
GetTokenUsage is deleted — read StreamFinal::usage (or
StreamingCompletionResponse::usage()) directly. A stream that ends without a
terminal record still reports Usage::new(), the documented zero sentinel.
With GetTokenUsage gone, the telemetry helper
SpanCombinator::record_token_usage takes &Usage instead of a
GetTokenUsage-bounded generic.
A terminal record is now emitted only when the provider signaled genuine completion (its own end-of-response event). Previously, several provider streams synthesized a default-usage terminal record when the connection ended — including streams cut off mid-response. A stream that ends without a terminal record was truncated; treat the missing record as an incomplete turn, not a zero-usage success.
The agent surface enforces this: agent.stream_prompt(...) now yields
Err("provider stream ended without a terminal record; treating the turn as truncated") for a stream the provider never confirmed complete, where it
previously finished "successfully" with zero usage. If you see this error
behind a flaky provider or proxy, the connection was cut mid-response — retry
the turn rather than trusting the partial content.
StreamingCompletionResponse::stream takes the provider descriptor name first:
StreamingCompletionResponse::stream(PROVIDER_NAME, normalized_stream)Provider implementations keep their native terminal type behind
RawStreamingResult<Native> and map it once:
let raw = self.raw_stream(request).await?;
let normalized = rig_core::streaming::normalize_stream(raw, |native| {
Ok(StreamFinal::new(PROVIDER_NAME, native.usage)
.with_optional_finish_reason(map_finish_reason(native.finish_reason)))
});
Ok(StreamingCompletionResponse::stream(PROVIDER_NAME, normalized))normalize_stream applies the same Stop → ToolCalls reconciliation as the
unary path, using the tool calls it actually saw on the stream.
StreamingPrompt<M, R> and StreamingChat<M, R> lost their R parameter:
StreamingPrompt<M>, StreamingChat<M>.
Remove Response, StreamingResponse, Client, and make from custom
implementations. A custom model implements only the normalized operations, and
optionally capabilities:
impl CompletionModel for MyModel {
async fn completion(
&self,
request: CompletionRequest,
) -> Result<CompletionResponse, CompletionError> { /* ... */ }
async fn stream(
&self,
request: CompletionRequest,
) -> Result<StreamingCompletionResponse, CompletionError> { /* ... */ }
}Construction is a separate, optional opt-in. CompletionClient::completion_model
is now required and calls your model's own constructor:
impl CompletionClient for MyClient {
type CompletionModel = MyModel;
fn completion_model(&self, model: impl Into<String>) -> MyModel {
MyModel::new(self.clone(), model.into())
}
}client.completion_model(model) and client.agent(model) are unchanged at call
sites. A model type with no client at all is now expressible: implementing
CompletionModel no longer drags in a client associated type.
A provider extension built on the generic rig::client::Client<Ext, H> cannot
implement CompletionClient for that foreign type itself (orphan rule).
Instead, implement the public ConstructCompletionModel<Client<Ext, H>> hook
on your model type; the blanket CompletionClient implementation over
Client<Ext, H> then supplies completion_model for you.
CompletionModel also no longer requires Clone — the trait demands only
async service behavior, in the spirit of tower::Service; cloning or sharing
a model is the caller's concern — and wrapping in an Arc genuinely works:
CompletionModel is implemented for Arc<M> by forwarding, so Arc<M>
passes through every generic API (CompletionRequestBuilder, agent
construction), and completion_request on an Arc clones the Arc, never
the model. Implementors can drop Clone derives they only carried for the
bound (keeping them is harmless). Generic code that cloned a model through
the trait must now bound M: CompletionModel + Clone explicitly or take the
model by value. The completion_request convenience gates on Self: Clone
individually; every built-in provider model, Arc<M>, and ModelHandle
satisfy it, so call sites on concrete types compile unchanged.
CompletionResponse::finish_reason is now a private field with a
finish_reason() getter: every write flows through with_finish_reason /
with_optional_finish_reason, so the Stop → ToolCalls reconciliation can
no longer be bypassed by direct assignment. Replace field reads with the
getter call.
The identifier and model setters on both CompletionResponse and
StreamFinal now treat an empty string as absent: gateways that echo ""
produce None, matching the streaming paths, and the rule lives in the
setters rather than at provider call sites.
Both invariants also hold through Deserialize: the two types deserialize
via a wire-shape mirror that funnels through new(...) and the setters, so
a persisted "finish_reason": "stop" alongside a tool-call choice comes
back as ToolCalls and a persisted "" identifier comes back as None.
The serialized wire format is unchanged.
Corrupt stream frames (payloads that are not valid JSON) are now surfaced as
Err items on the stream instead of being logged and silently skipped; the
stream keeps consuming, and a later genuine terminal still completes it.
Valid-JSON events whose shape this client doesn't recognize are still skipped
(with a warning) for forward compatibility with new provider event types.
Consumers that drained to None see the same content as before plus any
error items; consumers that stopped at the first Err should drain to
None — see the emission-contract table on StreamFinal.
CompletionModel::composes_native_output_with_tools() is replaced by
CompletionModel::capabilities():
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities::default().with_native_output_tool_composition(true)
}ProviderCapabilities is public and #[non_exhaustive]; start from Default
or ProviderCapabilities::new() and enable what you support. Capabilities are
plain data, so a runtime can snapshot them instead of holding a callback into
the concrete model.
Agent<M>, AgentBuilder<M>, AgentRunner<M>, the prompt/stream request
types, and Extractor<M, T> lost their model parameter: AgentBuilder::new
takes any CompletionModel + 'static and erases it once into a concrete
ModelHandle (which itself implements CompletionModel). Update type
annotations by deleting the parameter — Agent<openai::CompletionModel>
becomes Agent; Extractor<M, T> becomes Extractor<T>. Construction call
sites are unchanged.
Because the stored model is a handle, it can now change at runtime:
Agent::set_model, per-run runner(...).using_model(...), or an
AgentHook::on_model_select hook receiving ModelSelection (which sees the
merged RequestPatch and the previous model, and may pick a different handle
per model call). CompletionModel::capabilities() is captured by value when
the handle is created.
The monolithic core is now a portable contracts crate (rig-core) plus the
classic agent runtime (rig-agent), presented behind the rig facade.
If you depend on the rig facade — almost nothing changes. These keep
working unchanged:
use rig::prelude::*;
use rig::tool::{Tool, ToolContext}; // still the classic contextual trait
use rig::completion::Prompt;If you depend on rig-core directly, it is now portable-only. Agent
construction — AgentBuilder, ExtractorBuilder, contextual tools, hooks, the
run loop — moved to rig-agent. Depend on rig-agent or the facade.
If you depend on rig-agent directly, its root no longer re-exports all of
rig-core. The previous pub use rig_core::*; made it an implicit second
facade. Reach portable items through the explicit namespace:
use rig_agent::core::OneOrMany; // was: rig_agent::OneOrManyThe impl From<rmcp::model::Tool> for ToolDefinition and its &-borrow variant
were removed — with ToolDefinition in rig-core and rmcp::model::Tool
foreign, the orphan rule forbids the impl in rig-agent. Normal MCP use is
unchanged; if you relied on the direct conversion, build the ToolDefinition
from the tool's name, description, and schema_as_json_value().
Provider clients no longer carry inherent .agent() / .extractor() methods.
There is one canonical CompletionClient (in rig-core, providing
completion_model); the classic constructors live on a new AgentClientExt.
use rig::prelude::*; // brings both into scope
let agent = client.agent(model).build(); // AgentClientExt
let extractor = client.extractor::<T>(model).build(); // AgentClientExt
let m = client.completion_model(model); // CompletionClientOr import explicitly: use rig::client::{CompletionClient, AgentClientExt};
The context-free tool contract is now PortableTool (with
PortableToolEmbedding, PortableDynamicTool, portable_tool_definition).
The rig_core::tool::Tool alias is removed.
On the facade, rig::tool::Tool remains the classic contextual trait, so
existing facade code is unchanged. Portable contracts are available as
rig::tool::PortableTool, and in full under rig::tool::portable. A
PortableTool still registers with the classic runtime — it blanket-implements
the contextual Tool.
The largest change in this release. Typed tools now implement only:
Tool::call(&mut ToolContext, Args) -> Result<Output, Error>Author-facing errors stay typed until private runtime erasure normalizes them
into ToolExecutionError.
Tool implementations. Keep one typed type Error for ordinary ?
propagation. Remove classify_error, call_with_extensions, and
call_structured. The optional map_error classifies domain failures at the
erased boundary; its default preserves the source as Other. Return refusals
through map_error with ToolExecutionError::refused. Attach host-only result
metadata with ToolContext::insert_result.
Context. ToolCallExtensions and ToolResultExtensions are replaced by
ToolContext; .tool_extensions(...) becomes .tool_context(...).
Dynamic tools. ToolDyn is removed from the public API — use
DynamicTool. Rig's erased dispatch trait is now private. Typed tools use
Tool::NAME as their sole identity; runtime-named agents convert explicitly
with Agent::into_tool().
Registration:
| Before | After |
|---|---|
AgentBuilder::tools(Vec<Box<dyn ToolDyn>>) |
repeated .tool(...), or dynamic_tools(Vec<DynamicTool>) |
dynamic_tools(sample, index, toolset) |
retrieved_tools |
ToolSetBuilder::dynamic_tool(ToolEmbedding) |
retrieved_tool |
ToolSetBuilder::dynamic_tool(...) (callbacks) |
dynamic_tool(DynamicTool) |
Dispatch:
| Before | After |
|---|---|
ToolSet::{call, call_with_extensions, call_structured} |
ToolSet::execute |
ToolServerHandle::{call_tool, call_tool_with_extensions, call_tool_structured} |
ToolServerHandle::execute |
Errors. Replace ToolError, ToolFailure, ToolFailureKind, ToolReturn,
ToolReturnOutcome, ToolExecutionResult, and ToolOutcome with
ToolExecutionError, ToolErrorKind, and the read-only ToolResult that hooks
observe. ToolSetError is removed.
Explicit ToolExecutionError constructors keep actionable diagnostics
model-visible. The generic from_error path preserves operator diagnostics and
the concrete source but defaults to safe kind-level model feedback — use
with_model_feedback for deliberate replacement text, or with_model_output
for JSON/multimodal feedback.
Model presentation. Serializable outputs convert once into canonical
ToolOutput content blocks. Strings stay literal text, explicit
serde_json::Value stays JSON, and multimodal tools use ToolOutput::content /
ToolOutput::one or return typed ToolResultContent. Rig never reparses
strings to infer rich content. Inspect with as_text / as_json, and decode
explicitly with deserialize_json.
Registry. ToolSet is the single ordered registry and records whether each
tool is always advertised or retrieval-only. ToolSet::{get_tool_definitions, documents} are now synchronous and infallible, and ToolServerHandle
registration/removal no longer returns an artificial Result.
AgentHook::on_event, StepEvent, and Flow are replaced by event-specific
AgentHook methods with their own action types: CompletionCallAction,
ToolCallAction, ToolResultAction, InvalidToolCallAction,
ObservationAction. This makes invalid event/action combinations
unrepresentable.
AgentHook, HookStack, and the internal erased-hook interface no longer carry
a completion-model type parameter. CompletionResponseEvent and
StreamResponseFinish expose canonical Rig content, usage, prompt, and message
ID fields instead of typed provider responses. Direct CompletionModel
completion and streaming APIs still return typed raw provider responses.
Invalid-tool hooks return None to defer; every explicit action, including
Fail, is terminal for that hook stack. The atomically surfaced post-batch
streaming event is named ToolExecutionCommitted — for live host lifecycle
events, observe on_tool_call / on_tool_result.
The raw Completion and StreamingCompletion traits and their Agent
implementations are removed; agent execution state is private.
// before
agent.completion(prompt, history).await?.send().await?;
agent.stream_completion(prompt, history).await?.stream().await?;
// after — pick a turn budget large enough for tool follow-ups
agent.runner(prompt).history(history).max_turns(3).run().await?;
agent.runner(prompt).history(history).max_turns(3).stream().await;The runner consumes tool calls rather than returning the first raw model
response. For intentionally hook-free transport, start from
model.completion_request(prompt).messages(history) then .send() or
.stream().
AgentRun::new(prompt).with_history(history) remains a sans-I/O state machine
for custom drivers. It holds no configured model, tools, memory, or hooks and is
not an alternate execution path for configured agents.
An Agent's default model is set at construction. Per-run overrides now go
through runner(...).using_model(...), Agent::set_model, or a
ModelSelection hook (see the "runtime model swapping" section for the
current release).
Extractor now routes through the full hook lifecycle.
AgentBuilder::dynamic_context and ExtractorBuilder::dynamic_context were
removed in #2174 and restored in #2219. If you are tracking main, you may
have seen the gap; if you are upgrading from a release, the call still exists
and your .dynamic_context(samples, index) calls need no change.
What changed underneath: the separate retrieval pipeline in agent request
construction is gone for good, and the internal DynamicContextStore with it.
The helper is now a thin wrapper over a private AgentHook on the ordinary
completion-call lifecycle. Behavior that is deliberately preserved: retrieval on
every model call, current-prompt query selection with latest-textual-history
fallback, sample-count forwarding, pretty-JSON document formatting,
static-context-before-retrieved-context ordering, failure raised before provider
I/O, and support across blocking, streaming, and extractor execution.
Two consequences of it being an ordinary hook are worth checking:
- Registration order matters. Retrieval and the injected documents now
follow hook registration order relative to your own hooks. Register a stop
policy before
dynamic_contextif it should be able to suppress retrieval — previously the side pipeline ran regardless. - Multiple registrations run sequentially. Several
dynamic_contextcalls now execute in order throughHookStackrather than concurrently through the former side pipeline. If you registered several against independent indexes and depended on the concurrency, expect added latency.
If you want control beyond that — filtering, reranking, caching, per-turn policy
— write your own AgentHook; that is the only passive-RAG execution path now,
and dynamic_context is simply a prepackaged one.
Required-ness now has one source of truth, and the advertised schema always
agrees with the deserializer. Previously a parameter left out of an explicit
required(...) was advertised optional while the generated deserializer still
demanded it — failing at runtime whenever the model legitimately omitted it.
- No
required(...): non-Optionparameters are required;Option<T>is optional and deserializes toNonewhen absent. PreviouslyOptionparameters were advertised as required. If a provider needs everything marked required, list them explicitly. - Explicit
required(...): listed parameters are required. Omitted ones get#[serde(default)], so their types must beOption<T>or implementDefault— a type that is neither is now a compile error instead of a runtime deserialization failure. - Listing an
Option<T>inrequired(...)is a compile error: schemars excludesOptionfields fromrequiredand serde deserializes a missingOptiontoNone, so the directive would be silently ignored on both sides. - Names in
params(...)andrequired(...)must match actual parameters. Malformed or duplicate entries are compile errors rather than silently ignored. - A wildcard context binding (
#[rig(context)] _: &mut ToolContext) is rejected — name it_context.
Two dependency improvements need no action but may let you tidy up: crates using
#[rig_tool] / #[derive(Embed)] no longer need direct serde or serde_json
dependencies, the Embed trait no longer needs importing where it is derived,
and fully qualified &mut rig::tool::ToolContext parameters are recognized
without #[rig(context)] even under renamed dependencies.
PromptError, StructuredOutputError, and VectorStoreError are now
#[non_exhaustive]. Downstream match expressions need a wildcard arm.
Conversation memory load failures surface as the typed
PromptError::MemoryError instead of CompletionError::RequestError.
rig-core's wasm, rig-agent's wasm, and the facade's wasm are all
removed. Building for wasm32-unknown-unknown is the entire opt-in — there
are no wasm feature flags anywhere in the workspace.
Drop features = ["wasm"] from any dependency line. Nothing replaces it; Cargo
rejects the unknown feature at resolution, so this fails loudly.
Relaxing the bounds cannot break implementors — the relaxed markers are
blanket-implemented (impl<T> WasmCompatSend for T {}), so every type that
satisfied the strict form satisfies the relaxed one. The one exception is a
generic consumer on browser wasm that wrote T: WasmCompatSend and then
relied on T: Send internally, and only if it was previously building with the
feature off.
rmcp is native-only. It never compiled for wasm — rmcp's ClientHandler
requires Send + Sync unconditionally, which rig's wasm tool registry cannot
satisfy — but it used to fail with a wall of dyn ErasedTool trait errors. It
now fails with a single explanatory compile_error!.
WASI (wasm32-wasip1 / wasip2) is not supported; its dependency graph has
never built. See crates/rig-agent/README.md for the full target matrix.
1. max_turns — see Silent behavior changes
The single most likely change to alter your program's behavior without a compile error.
Tool authors implement description() and parameters() directly.
Tool::definition(prompt) and ToolDyn::definition(prompt) are removed.
ToolDefinition remains a provider/request artifact generated from registered
tools. Tool::NAME / Tool::name() / ToolDyn::name() are the single source of
truth for advertised and dispatched tool names.
Two large additions that also broke existing surfaces (#2015, #2012). Hooks became composable middleware; tool execution gained structured results. If you implemented hooks or tools against 0.39, expect to rewrite against the new shapes — and note that both were reworked again in 0.41 (section 4 and section 5 above). If you are jumping 0.39 → 0.41, migrate straight to the newer shape and skip this intermediate form.
Unified in #2056. FinalResponse and its accessors (content,
assistant_content, completion_calls) no longer exist as a separate type.
groq, deepseek, mistral, together, moonshot (OpenAI side), perplexity,
hyperbolic, mira, azure, huggingface, and llamafile all lose their hand-rolled
CompletionModel structs, request types, and TryFrom<message::Message>
conversions. CompletionModel in each module is now a type alias for the
generic model, and provider-specific StreamingCompletionResponse types are
replaced by the shared OpenAI one.
A new OpenAICompatibleProvider trait (mirroring AnthropicCompatibleProvider)
is now required by GenericCompletionModel's Ext parameter. It carries the
telemetry provider name and an EMITS_COMPLETE_SINGLE_CHUNK_TOOL_CALLS flag for
llama.cpp-style streaming tool calls, plus SUPPORTS_RESPONSE_FORMAT,
STREAM_INCLUDE_USAGE, and SUPPORTS_TOOLS consts. Provider wire dialects live
in its completion_path, prepare_request, and finalize_request_body hooks.
Also in this area:
openai::ToolChoicegains aFunction { name }variant and is now#[non_exhaustive].openai::CompletionRequestfields are now public;OpenAIRequestParamsgainssupports_response_format.GenericCompletionModel'sstrict_tools/tool_result_array_contentfields are private — use thewith_*builder methods. The redundantwith_modelconstructor is removed; usenew.StreamingCompletionResponseis generic over the provider's streaming usage payload (StreamingCompletionResponse<U = Usage>).- perplexity, hyperbolic, and mira set
SUPPORTS_TOOLS = false;tools/tool_choiceare dropped with a warning during request conversion.
openrouter::{Message, UserContent, ImageUrl} are re-exports of the shared
OpenAI types. The fork's FileContent / VideoUrlContent are replaced by shared
FileData / VideoUrl. ReasoningDetails / ResponseImage move into the
openai module (re-exported from openrouter).
The shared OpenAI types gained OpenRouter's extensions to support this:
UserContent::Video, ImageUrl.detail becomes Option<ImageDetail>, and
Message::Assistant gains reasoning_details, an inbound-only images field,
and a deserialize-only role: "model" alias.
Message conversion goes through openrouter::messages_from_rig_message.
OpenRouter's UserContent builder helpers (image_url, file_base64,
video_url, …) are removed — construct the shared openai content variants
directly. ToolChoice::Specific with multiple function names now errors
client-side.
Not recorded in the CHANGELOG. Found by diffing the public API.
Every provider HTTP-error path is routed through a shared
from_http_response / from_provider_body funnel, so provider_response_*
helpers recover the raw status and body instead of flattening into
ProviderError(String).
Practical impact: RerankError becomes #[non_exhaustive] and gains a
ProviderResponse variant, and public from_http_response /
from_provider_body constructors are added on every capability error. An
exhaustive match on RerankError will no longer compile — add a wildcard arm.
Only its sibling PR (#1951) is in the CHANGELOG.
In the OpenAI Responses API, Output::Unknown was a fieldless variant, so every
unrecognized output item decoded to a unit and its payload was discarded.
Provider-native hosted tools (web_search_call, file_search_call,
computer_use_call, code_interpreter_call) arrive as exactly these items, so
their data was destroyed at the typed-decode boundary.
Output::Unknown is now Output::Unknown(serde_json::Value). Any match arm
binding that variant needs updating — and the data you were previously losing is
now available.
| Removed | Replacement |
|---|---|
providers::galadriel (whole integration) |
none |
evals module + experimental feature |
none |
experimental pipeline module |
none |
rig_derive::ProviderClient derive |
none |
Extractor::{get_inner, into_inner} |
none |
TryFrom<String> for Nothing (always failed) |
none |
streaming::stream_completion_to_stdout |
agent::stream_to_stdout |
AudioGeneration<M> / ImageGeneration<M> / Transcription<M> wrapper traits |
the corresponding *Model APIs and request builders |
providers::anthropic::decoders |
shared SSE machinery |
SpanCombinator::record_model_output |
none |
together::ToolChoice, together::ToolChoiceFunctionKind, moonshot::ToolChoice |
shared openai::ToolChoice |
groq::send_compatible_streaming_request, deepseek::send_compatible_streaming_request |
openai::send_compatible_streaming_request |
raw response types of perplexity / hyperbolic / huggingface (Message, Choice, Usage, Delta, Role) |
each module keeps a CompletionResponse alias to the shared OpenAI payload |
Only two breaking changes.
Both agent loops became thin drivers over a sans-I/O AgentRun state machine.
If you drove the agent loop yourself rather than calling prompt / chat, you
are affected. Note that the surrounding execution API changed again in 0.41 —
see AgentRunner is the only execution
path.
Tool registration became order-deterministic and duplicate-safe, with ToolSet
backed by an IndexMap. Code relying on the previous registration order or on
duplicate-name behavior may see different tools advertised.
The macro now derives schemars::JsonSchema on the generated parameters struct
(#1576), so every parameter type must implement JsonSchema. Primitives,
String, Vec<T>, and Option<T> are covered; your own types need
#[derive(schemars::JsonSchema)]. Previously an unknown type was quietly
advertised as {"type": "object"} with no fields, so this converts a class of
runtime tool-call failures into a compile error.
You do not need a direct schemars dependency — the macro refers to
rig_core::schemars.
For what the model now sees, see
#[rig_tool] advertises a different schema.
Tool calls are checked against the registered tools and the request's
tool_choice before dispatch (#1823). The new PromptError::UnknownToolCall
variant carries the offending tool_name, the available_tools, the
allowed_tools, and the chat_history at the point of failure. PromptError
is not #[non_exhaustive], so an exhaustive match over it needs the new arm.
Recovery is a hook (#1840). PromptHook gains on_invalid_tool_call, taking an
InvalidToolCallContext and returning InvalidToolCallHookAction:
| Action | Effect |
|---|---|
Retry(feedback) |
re-prompt the model with your feedback text |
Repair(tool_name) |
rewrite the call to name a real tool |
Skip |
drop the call and continue |
Fail |
surface PromptError::UnknownToolCall |
Bound the loop with .max_invalid_tool_call_retries(n) on either prompt builder.
MultiTurnStreamItem::final_response and final_response_with_history take
OneOrMany<AssistantContent> where they took &str:
// before
MultiTurnStreamItem::final_response("some text", usage)
// after
MultiTurnStreamItem::final_response(OneOrMany::one(AssistantContent::text("some text")), usage)FinalResponse keeps response() for the concatenated text and adds
content() / assistant_content() for the structured form. MultiTurnStreamItem
is #[non_exhaustive], so its new CompletionCall variant does not break
existing matches.
PromptResponse and TypedPromptResponse gain
completion_calls: Vec<CompletionCall> (#1787), one entry per model call in the
turn, each with a call_index and optional Usage. Streaming emits the same
data as MultiTurnStreamItem::CompletionCall. Aggregate usage is unchanged.
This is additive unless you construct those responses yourself — use
.with_completion_calls(...) if you do.
None of these are #[non_exhaustive], so exhaustive matches and struct literals
break:
| Type | Change |
|---|---|
RawStreamingChoice |
new TextStart and TextAdditionalParams variants |
message::Text |
new additional_params field — use Text::new(text) |
completion::Usage |
new tool_use_prompt_tokens field |
anthropic::Content |
new ServerToolUse and WebSearchToolResult variants |
anthropic::ContentDelta |
new CitationsDelta and Unknown variants |
gemini::FinishReason |
new MalformedResponse, MissingThoughtSignature, TooManyToolCalls, UnexpectedToolCall variants |
openai::responses_api's ArgsTextChunk.content_index and
DeltaTextChunkWithItemId.content_index became Option<u64>, and
DeltaTextChunkWithItemId lost its item_id field (#1828).
EmbeddingModel gains embed_text_with_usage / embed_texts_with_usage
returning EmbeddingResponse { embeddings, usage }, plus
EmbeddingsBuilder::build_with_usage (#1791). Both trait methods have default
implementations that delegate to the existing ones and report zero usage, so
custom embedding models keep compiling.
.hook(...) returned AgentBuilder<M, P2, NoToolConfig>, discarding the
typestate that records how tools were registered. It now returns
AgentBuilder<M, P2, ToolState>. Builders that called .hook(...) after
.tool(...) and then hit a type error can drop the workaround.
Every crate in the workspace moved to the workspace version (#1853). Companion
crates that were on their own numbering — rig-mongodb was at 0.4.7, for
example — jump to 0.38.1. Nothing about their APIs changed; the version line
in your Cargo.toml does.
From 0.38.1 onward, a companion crate's version tracks the rig-core release it
was built against.
rig-core built a library target named rig, so the idiomatic code was
cargo add rig-core followed by use rig::.... In 0.37 the library target is
named rig_core, and the name rig belongs to a new facade crate (#1699).
This breaks every use rig::... in a crate that depends on rig-core. Two
ways out:
# preferred: depend on the facade, keep `use rig::...` unchanged
rig = "0.37"# or: stay on rig-core and rewrite the imports to `use rig_core::...`
rig-core = "0.37"The facade re-exports rig-core and puts every companion crate behind one
feature each (mongodb, qdrant, lancedb, memory, …), so a workspace that
was juggling rig-core plus several rig-* dependencies can collapse to one.
Related: #[rig_tool] and #[derive(Embed)] used to emit hardcoded rig::
paths, which is why depending on rig-core under any other name failed. They
now resolve rig-core or rig through proc-macro-crate, so both layouts work
and renamed dependencies do too.
// before — history passed by value, response only
fn chat<I, T>(&self, prompt: impl Into<Message>, chat_history: I) -> Result<String, PromptError>
where I: IntoIterator<Item = T>, T: Into<Message>;
// after — history borrowed and updated in place
fn chat(&self, prompt: impl Into<Message>, chat_history: &mut Vec<Message>)
-> Result<String, PromptError>;The prompt and every assistant and tool message produced during the turn are appended to the vector you pass (#1733). Do not push the user prompt yourself before calling — you will send it twice. Callers that were manually reconstructing history after each turn should delete that code.
A new rig::memory module (#1702) with the ConversationMemory trait,
MemoryError, a MessageFilter trait, and an InMemoryConversationMemory
backend. AgentBuilder gains .memory(...) and .conversation_id(...); both
prompt builders gain .conversation(id) and .without_memory().
MemoryError is #[non_exhaustive] from the start, and load failures are
fatal while append failures are logged and swallowed. Later releases added
DemotionHook (#1737) and Compactor (#1748) for eviction and rolling
summaries, with the named policies living in the rig-memory companion crate.
Entirely additive — agents without .memory(...) behave as before.
ClientBuilder<Ext, ApiKey = Missing, H = Missing> — the H parameter defaulted
to reqwest::Client and now defaults to Missing, with every provider's
ClientBuilder alias following. Building without supplying a client still
produces a reqwest-backed one, so ordinary Client::builder().api_key(k).build()
chains are unaffected. Code that named H explicitly needs updating.
MockStreamingClient, MockResponse, and friends moved to
rig_core::test_utils behind the new test-utils feature (#1745). Add
rig-core = { version = "0.37", features = ["test-utils"] } to your
[dev-dependencies] if you used them.
rig-core's default feature also picked up derive, so #[rig_tool] and
#[derive(Embed)] are available without opting in. The all feature was
removed; name derive, pdf, and rayon individually.
DocumentSourceKind::FileIdandanthropic::DocumentSource::Filesupport provider-side file IDs (#1740).DocumentSourceKindis#[non_exhaustive], so matches are safe.completion::Usagegainsreasoning_tokens, and gemini'sUsageMetadatagains per-modality token detail — breaking for struct literals.- Ollama's
thinkadditional-parameter accepts"low"/"medium"/"high"as well as a bool (#1747). Bools keep working.
The largest release in this range. Sections 1 and 3 touch every provider integration.
Deprecated since 0.25, removed in #1633. All of this no longer exists:
| Removed | Replacement |
|---|---|
DynClientBuilder, AnyClient, ProviderFactory, DefaultProviders |
construct the provider client directly |
CompletionClientDyn, EmbeddingsClientDyn, TranscriptionClientDyn, ImageGenerationClientDyn, AudioGenerationClientDyn, VerifyClientDyn |
the non-Dyn client traits |
CompletionModelDyn, EmbeddingModelDyn, TranscriptionModelDyn, ImageGenerationModelDyn, AudioGenerationModelDyn |
the corresponding *Model traits |
CompletionModelHandle, TranscriptionModelHandle, ImageGenerationModelHandle, AudioGenerationModelHandle |
the concrete model types |
If you were selecting a provider at runtime through DynClientBuilder, you now
own that dispatch — a match over your own provider enum returning a boxed
Agent, or an enum of concrete clients.
Builders that used to accept a field and fail at build() now encode
required-ness in the type (#1611), using the Missing / Provided<T> markers:
| Builder | Required field(s) |
|---|---|
VectorSearchRequestBuilder |
query, samples |
TranscriptionRequestBuilder |
data |
ImageGenerationRequestBuilder |
prompt |
AudioGenerationRequestBuilder |
text, voice |
ChatBotBuilder |
the chatbot impl |
ClientBuilder |
api_key (NeedsApiKey is now Missing) |
Two consequences. VectorSearchRequestBuilder::build() returns
VectorSearchRequest<F> instead of Result<_, VectorStoreError> — drop the ?.
And TranscriptionRequestBuilder::load_file returns
io::Result<TranscriptionRequestBuilder<M, Provided<Vec<u8>>>>, so it needs a
? where it previously did not.
Naming these builder types explicitly requires the marker parameters; chained builder expressions need no change.
// before
fn from_env() -> Self; // panicked on a missing/invalid variable
fn from_val(input: Self::Input) -> Self;
// after
type Error;
fn from_env() -> Result<Self, Self::Error>;
fn from_val(input: Self::Input) -> Result<Self, Self::Error>;Bundled providers use ProviderClientError (EnvironmentVariable, Http,
InvalidConfiguration) via the ProviderClientResult<T> alias, and
required_env_var / optional_env_var are available for your own
implementations. llamafile::from_url became fallible for the same reason.
Add ? at every Client::from_env() call site — this is the most common
mechanical edit in this release.
openai::CompletionModel, openai::ResponsesCompletionModel,
openai::EmbeddingModel, and anthropic::CompletionModel became type aliases
over generic structs parameterized by a provider extension. The aliases keep
their old parameter lists and their fields stay public, so ordinary use is
unaffected. This is the groundwork that 0.40 extended to eleven more providers.
DynamicContextStoredropped itsRwLock— it is nowArc<Vec<...>>(#1641). Immutable after construction, so tool lookups no longer contend.cohere::CompletionResponse::messagereturnsResult<AssistantMessageParts, CompletionError>instead of a three-element tuple.- Ollama's client builder takes an
OllamaApiKeyinstead ofNothing, so a base URL and key can be set programmatically (#1511). openai::responses_api::Outputgained anUnknowncatch-all (#1552) — exhaustive matches need an arm. (0.40 later gave it a payload.)deepseek::DEEPSEEK_CHATandDEEPSEEK_REASONERare#[deprecated]in favor of thedeepseek-v4-flashnames (#1664).json_utils::empty_or_nonewas removed.#[rig_tool]acceptsname = "...", validated against the 1–64 character, ASCII-alphanumeric-plus_/-rule providers enforce (#1619).
CLAUDE_3_5_HAIKU, CLAUDE_3_5_SONNET, CLAUDE_3_7_SONNET, CLAUDE_4_OPUS,
and CLAUDE_4_SONNET are gone (#1616), replaced by CLAUDE_OPUS_4_6,
CLAUDE_SONNET_4_6, and CLAUDE_HAIKU_4_5. Model ids are plain strings — pass
the literal ("claude-3-5-sonnet-latest") if you need a retired model.
ToolServerRequest, ToolServerResponse, ToolServerRequestMessageKind,
ToolServer::run, ToolServer::handle_message, and
ToolServerHandle::get_tool_definitions left the public API, along with the
ToolServerError::{Canceled, InvalidMessage, SendError} variants (#1607). The
lock-free rework behind them removed contention during tool lookup.
ToolServerHandle remains the supported interface. If you were driving the
message enum directly, move to the handle.
Roughly 60% of rig-core's examples moved under tests/, organized by provider
(#1603), so cargo run --example <name> stopped resolving for those. They run
as ignored tests instead:
cargo test -p rig-core --test <provider> -- --ignored --test-threads=1
--test-threads=1 matters: the provider suites share rate limits.
The rmcp integration was upgraded from 0.x to 1.3 (#1596). If you pass
rmcp::model::Tool or rmcp::service::ServerSink values into
AgentBuilder::rmcp_tool(s), your own rmcp dependency has to move in lockstep.
When a request carries tools that have not produced a result yet, the
response_format derived from output_schema is withheld until the tool round
trip completes (#1622). Structured output still applies to the final answer;
providers stop rejecting the intermediate tool turns.
with_history no longer borrows a &'a mut Vec<Message> (#1563):
// before
agent.prompt("hi").with_history(&mut history).await?;
// after — anything that iterates into messages
agent.prompt("hi").with_history(history.clone()).await?;PromptRequest and TypedPromptRequest lost their 'a lifetime parameter as a
result — PromptRequest<'a, S, M, P> is now PromptRequest<S, M, P>. The
streaming builder's with_history changed the same way, and
CompletionRequestBuilder::{documents, messages} now take
impl IntoIterator<...>.
Because the history is no longer borrowed mutably, the updated conversation
comes back on the response rather than being written into your vector. Read
PromptResponse::messages (see 0.31 → 0.32).
| Before | After |
|---|---|
reqwest-rustls |
reqwest + rustls |
reqwest-native-tls |
reqwest + native-tls |
| — | websocket, reqwest-middleware-native-tls |
default is ["reqwest", "rustls"]. The old composite names are gone, so a
dependency line naming them fails to resolve — which is the loud failure you
want here.
CompletionModel::with_automatic_caching() and with_automatic_caching_1h()
add a top-level cache_control to the request and let the API place the
breakpoint (#1572). The existing with_prompt_caching() — explicit breakpoints
on the system prompt and messages — is unchanged. CacheTtl::{FiveMinutes, OneHour}
and Usage::cache_creation_input_tokens came along with it.
A header set through http_headers() is no longer overwritten by the provider's
generated auth header (#1553). This is what lets OpenAI-compatible endpoints
that want a non-Bearer scheme work. If you were setting an Authorization
header expecting the provider's key to take precedence anyway, it no longer
does.
Message gained a System variant (#1527) — it is not #[non_exhaustive], so
exhaustive matches need the arm. See
Preamble is now a system message
for the behavioral half of this change.
Separately, ProviderToolDefinition and
CompletionRequestBuilder::{provider_tool, provider_tools} expose hosted tools
that run on the provider's side — OpenAI's web_search, file_search,
computer_use, and code_interpreter (#1430).
tool::McpTool, tool::McpToolError, and McpTool::from_mcp_server were
removed in favor of McpClientHandler and the rmcp_tool(s) builder methods
(#1525).
Provider embedding response types — cohere, gemini, mistral, openai, openrouter,
together — changed their vector fields from Vec<f64> to
Vec<serde_json::Number> so they deserialize under
serde_json/arbitrary_precision (#1518, #1526). The embeddings::Embedding
type you actually consume still holds Vec<f64>; only the raw provider structs
changed. Call .as_f64() if you were reading them directly.
gemini::EmbeddingModel::{new, with_model} take ndims: usize instead of
Option<usize> (#1513). ndims() used to return a hardcoded 768 for every
model while output_dimensionality went to the API as null; it now reports
the value you passed, or the model's documented default — 3072 for
gemini-embedding-001, 768 for text-embedding-004.
If you sized a vector-store column from ndims(), the number changes. For
gemini-embedding-001 the vectors were already 3072-wide; the reported count
was simply wrong.
GenerateContentRequest.toolsisOption<Vec<Value>>, andThinkingConfig.thinking_budgetisOption<u32>alongside the newthinking_level: Option<ThinkingLevel>for Gemini 3 (#1520).gemini::Client::{generate_content_api, interactions_api}select between the two Gemini surfaces (#1230).openai::Client::responses_websocketopens a stateful Responses session behind thewebsocketfeature (#1500).- New
llamafileprovider (#1519).
Renamed in #1453, and the response gained messages: Option<Vec<Message>> —
the full conversation for the turn, populated with .extended_details()
(#1450). PromptResponse::new(output, usage) keeps its shape.
TypedPromptRequest<'a, T, M, P> became TypedPromptRequest<'a, T, S, M, P>,
where S: PromptType records whether .extended_details() was called (#1446).
TypedPromptResponse<T> is the extended form, with output, usage, and later
completion_calls. Chained builder expressions are unaffected; explicit type
annotations need the extra parameter.
// before
trait ProviderBuilder: Sized {
type Output: Provider;
// Provider::build did the work
}
// after
trait ProviderBuilder: Sized + Default + Clone {
type Extension<H>;
type ApiKey;
fn build(self, ...) -> ...;
}Provider::build was removed, Client::builder() returns
ClientBuilder<Ext::Builder, NeedsApiKey, reqwest::Client>, and Client::new
takes <Ext::Builder as ProviderBuilder>::ApiKey instead of a bare key type
(#1436). Only affects crates implementing their own provider.
GenericEventSource gained a retry-policy parameter
(GenericEventSource<HttpClient, RequestBody, Retry = ExponentialBackoff>) and
a with_retry_policy constructor (#1428). ReadyState and ready_state() were
removed, and last_event_id() returns Option<&str> rather than &str.
AudioMediaType gained M4A, PCM16, and PCM24; VideoMediaType gained
MOV and WEBM; openrouter's UserContent gained InputAudio and VideoUrl
(#1413). None are #[non_exhaustive] — exhaustive matches need the new arms.
Both moved to archived/ and out of the workspace (#1472). They are no longer
published or built. rig-eternalai had already stopped publishing in an earlier
release; 0.32 is where the source left the tree.
Extractor::dynamic_context(sample, index) mirrors the agent builder, and
extract_with_usage / extract_with_chat_history_with_usage return
ExtractionResponse<T> { data, usage } (#1447). Both additive.
AgentBuilder<M> became AgentBuilder<M, P = (), ToolState = NoToolConfig>,
and the separate AgentBuilderSimple that .tool() used to return no longer
exists. The ToolState parameter is NoToolConfig, WithBuilderTools, or
WithToolServerHandle.
The practical effect is that mixing builder-registered tools with a
ToolServerHandle is now a compile error. In 0.30, .tool() converted the
builder into an AgentBuilderSimple that had no field for the handle — so
.tool_server_handle(h).tool(t) dropped h on the floor, along with any
dynamic_context and dynamic_tools configured up to that point. Pick one
registration style; the compiler now enforces it.
Chained expressions otherwise need no change; explicit AgentBuilder<M>
annotations do.
Agent<M> became Agent<M, P = ()>, which still resolves for existing code.
StreamingPromptHook was removed and its methods folded into PromptHook
(#1352): on_text_delta, on_tool_call_delta, and
on_stream_completion_response_finish joined on_completion_call,
on_completion_response, on_tool_call, and on_tool_result. Every method has
a default, so a hook that only cares about one event stays small.
The types also moved from agent::prompt_request to
agent::prompt_request::hooks; rig::agent::{PromptHook, HookAction, ToolCallHookAction} re-exports them either way.
Hooks can now be attached to the agent instead of the call (#1356):
AgentBuilder::hook(h) sets a default that every prompt and stream_prompt
picks up, and StreamingPrompt/StreamingChat gained a type Hook: PromptHook<M>
associated type — use type Hook = (); if you implement them and want none.
PromptRequest::new was replaced by PromptRequest::from_agent(&agent, prompt).
Reasoning { reasoning: Vec<String>, signature: Option<String> } became
Reasoning { content: Vec<ReasoningContent> }, with variants Text { text, signature }, Summary(String), Encrypted(String), and Redacted { data }
(#1395, #1396). This is what makes reasoning traces survive a round trip across
providers.
Constructors and accessors cover the common cases: Reasoning::new,
new_with_signature, summaries, encrypted, redacted, and
display_text / first_text / first_signature / encrypted_content.
Stored histories need attention — see
Persisted Reasoning JSON no longer round-trips.
CompletionRequest gained output_schema: Option<schemars::Schema> and
model: Option<String> (#1382, #1374), with
AgentBuilder::{output_schema, output_schema_raw},
CompletionRequestBuilder::{output_schema, output_schema_opt, model, model_opt},
the TypedPrompt trait, TypedPromptRequest, and StructuredOutputError.
Additive unless you build CompletionRequest with a struct literal, which the
new fields break — use the builder.
A new ModelLister / ModelListingClient pair with model::listing::{Model, ModelList, ModelListingError} (#1243). Providers that support it can enumerate
models at runtime.
Breaking for custom providers: client::Capabilities gained a
type ModelListing: Capability; associated type. Set it to the unsupported
marker if your provider cannot list models.
The HTTP stack moved to reqwest 0.13 (#1218). Feature names moved with it:
| Before | After |
|---|---|
reqwest-tls |
reqwest-native-tls |
reqwest-rustls (opt-in) |
reqwest-rustls (default) |
reqwest/macos-system-configuration is no longer pulled in. If you pass a
reqwest::Client into a Rig client, it has to be a 0.13 one. For the runtime
consequences, see
The default TLS backend is now rustls.
ImageSource and DocumentSource changed from structs to enums, and
ImageSourceData was removed:
// before — media_type and type carried even for URLs, where they mean nothing
ImageSource { data: ImageSourceData::Url(url), media_type, r#type: SourceType::URL }
// after
ImageSource::Url { url }
ImageSource::Base64 { data, media_type }DocumentSource gained Base64 and Text variants (Url followed in 0.32),
PlainTextMediaType was added, and Content gained RedactedThinking — the
enums model what Anthropic actually accepts for URL-backed images and plain-text
documents (#1403, #1377).
RawStreamingChoice gained a MessageId variant and
StreamingCompletionResponse a message_id: Option<String> field.
RawStreamingChoice is not #[non_exhaustive], so an exhaustive match over it
needs the new arm.
Renamed or relocated items, for searching.
| Old | New | Version |
|---|---|---|
rig_core::tool::Tool (portable) |
rig_core::tool::PortableTool |
0.41 |
rig_agent::<item> (portable re-export) |
rig_agent::core::<item> |
0.41 |
client.agent(...) inherent method |
AgentClientExt::agent (via rig::prelude::*) |
0.41 |
ToolCallExtensions / ToolResultExtensions |
ToolContext |
0.41 |
.tool_extensions(...) |
.tool_context(...) |
0.41 |
ToolDyn (public) |
DynamicTool |
0.41 |
ToolSet::{call, call_with_extensions, call_structured} |
ToolSet::execute |
0.41 |
ToolServerHandle::call_tool* |
ToolServerHandle::execute |
0.41 |
ToolError / ToolFailure / ToolReturn / ToolOutcome / ToolExecutionResult |
ToolExecutionError / ToolErrorKind / ToolResult |
0.41 |
AgentHook::on_event + StepEvent + Flow |
event-specific AgentHook methods + action types |
0.41 |
agent.completion(...) / agent.stream_completion(...) |
agent.runner(...).run() / .stream() |
0.41 |
AgentBuilder::dynamic_context |
unchanged call, now hook-backed (removed in #2174, restored in #2219) | 0.41 |
DynamicContextStore |
none — the side retrieval pipeline is gone for good | 0.41 |
dynamic_tools(sample, index, toolset) |
retrieved_tools |
0.41 |
ToolSetBuilder::dynamic_tool(ToolEmbedding) |
retrieved_tool |
0.41 |
features = ["wasm"] |
nothing — target is the opt-in | 0.41 |
Tool::definition(prompt) |
description() + parameters() |
0.40 |
FinalResponse |
PromptResponse |
0.40 |
streaming::stream_completion_to_stdout |
agent::stream_to_stdout |
0.40 |
groq/deepseek::send_compatible_streaming_request |
openai::send_compatible_streaming_request |
0.40 |
Output::Unknown |
Output::Unknown(Value) |
0.40 |
provider-specific StreamingCompletionResponse |
shared openai::StreamingCompletionResponse |
0.40 |
GenericCompletionModel::with_model |
GenericCompletionModel::new |
0.40 |
MultiTurnStreamItem::final_response(&str, ..) |
final_response(OneOrMany<AssistantContent>, ..) |
0.38 |
DeltaTextChunkWithItemId.item_id |
none | 0.38 |
library target rig in rig-core |
rig_core, or the new rig facade crate |
0.37 |
Chat::chat(prompt, impl IntoIterator) |
Chat::chat(prompt, &mut Vec<Message>) |
0.37 |
rig_core::http_client::MockStreamingClient, streaming::MockResponse |
rig_core::test_utils::* (feature test-utils) |
0.37 |
all feature |
derive + pdf + rayon |
0.37 |
DynClientBuilder / AnyClient / ProviderFactory |
none — dispatch yourself | 0.36 |
CompletionModelDyn / EmbeddingModelDyn / TranscriptionModelDyn / ImageGenerationModelDyn / AudioGenerationModelDyn |
the corresponding *Model traits |
0.36 |
CompletionClientDyn / EmbeddingsClientDyn / TranscriptionClientDyn / ImageGenerationClientDyn / AudioGenerationClientDyn / VerifyClientDyn |
the non-Dyn client traits |
0.36 |
client::NeedsApiKey |
markers::Missing |
0.36 |
ProviderClient::from_env() -> Self |
-> Result<Self, Self::Error> |
0.36 |
VectorSearchRequestBuilder::build() -> Result<_, _> |
infallible build() |
0.36 |
json_utils::empty_or_none |
none | 0.36 |
anthropic::{CLAUDE_3_5_HAIKU, CLAUDE_3_5_SONNET, CLAUDE_3_7_SONNET, CLAUDE_4_OPUS, CLAUDE_4_SONNET} |
CLAUDE_OPUS_4_6 / CLAUDE_SONNET_4_6 / CLAUDE_HAIKU_4_5 |
0.35 |
ToolServerRequest / ToolServerResponse / ToolServerRequestMessageKind |
ToolServerHandle |
0.35 |
reqwest-rustls / reqwest-native-tls features |
reqwest + rustls / native-tls |
0.34 |
PromptRequest<'a, S, M, P> |
PromptRequest<S, M, P> |
0.34 |
tool::McpTool / McpToolError / McpTool::from_mcp_server |
McpClientHandler + rmcp_tool(s) |
0.33 |
CompletionRequest.preamble |
leading Message::System in chat_history |
0.33 |
gemini::EmbeddingModel::new(.., Option<usize>) |
new(.., usize) |
0.33 |
PromptResponse.total_usage |
PromptResponse.usage |
0.32 |
Provider::build |
ProviderBuilder::build |
0.32 |
ProviderBuilder::Output |
ProviderBuilder::Extension<H> |
0.32 |
http_client::sse::ReadyState / ready_state() |
none | 0.32 |
rig-eternalai, rig-wasm |
none — archived | 0.32 |
AgentBuilderSimple |
AgentBuilder<M, P, ToolState> |
0.31 |
StreamingPromptHook |
PromptHook |
0.31 |
PromptRequest::new |
PromptRequest::from_agent |
0.31 |
Reasoning.reasoning / Reasoning.signature |
Reasoning.content: Vec<ReasoningContent> |
0.31 |
anthropic::ImageSourceData |
anthropic::ImageSource enum variants |
0.31 |
reqwest-tls feature |
reqwest-native-tls |
0.31 |