use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use serde_json::{Map, Value};
use crate::attachment::Attachment;
use crate::config::Config;
use crate::cost::{Cost, Tier};
use crate::error::{DEFAULT_FALLBACK_ERRORS, Error, ErrorKind, Result};
use crate::message::{FinishReason, Message, Operation, Role, ToolCall, UsageEntry, UsageStatus};
use crate::model::Model;
use crate::models;
use crate::protocols::{
self, Caching, Request, Schema, StreamAccumulator, StreamState, ToolCalls, ToolChoice,
ToolPrefs,
};
use crate::providers::{ProtocolName, Provider};
use crate::thinking::ThinkingConfig;
use crate::tokens::Tokens;
use crate::tool::{SharedTool, Tool, ToolResult, validate_arguments};
use crate::transport::Connection;
type MessageCallback = Box<dyn FnMut(&Message) + Send + Sync>;
type UnitCallback = Box<dyn FnMut() + Send + Sync>;
type ToolCallCallback = Box<dyn FnMut(&ToolCall) + Send + Sync>;
type ToolResultCallback = Box<dyn FnMut(&ToolResult) + Send + Sync>;
type FallbackCallback = Box<dyn FnMut(&FallbackAttempt) + Send + Sync>;
type RequestCallback = Box<dyn FnMut(&mut Value) + Send + Sync>;
type AttachmentConverter = Box<dyn Fn(&Attachment) -> Result<Option<Attachment>> + Send + Sync>;
pub type UsageRecorder = Box<dyn FnMut(&UsageEntry) + Send + Sync>;
pub type AsyncUsageRecorder = Arc<
dyn Fn(UsageEntry) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>
+ Send
+ Sync,
>;
#[derive(Debug, Clone)]
pub struct Fallback {
pub model: String,
pub provider: Option<String>,
}
impl From<&str> for Fallback {
fn from(model: &str) -> Self {
Fallback {
model: model.into(),
provider: None,
}
}
}
#[derive(Debug, Clone)]
pub struct FallbackAttempt {
pub attempt: usize,
pub error: String,
pub error_kind: ErrorKind,
pub from: String,
pub to: String,
pub to_provider: String,
pub streaming: bool,
pub chunks_yielded: bool,
pub succeeded: Option<bool>,
pub response: Option<Message>,
pub fallback_error: Option<(ErrorKind, String)>,
}
#[derive(Default)]
struct Callbacks {
before_message: Vec<UnitCallback>,
after_message: Vec<MessageCallback>,
before_tool_call: Vec<ToolCallCallback>,
after_tool_result: Vec<ToolResultCallback>,
before_fallback: Vec<FallbackCallback>,
after_fallback: Vec<FallbackCallback>,
before_request: std::sync::Mutex<Vec<RequestCallback>>,
unsupported_attachment: Vec<AttachmentConverter>,
attachment_replacements: std::sync::Mutex<Vec<(Attachment, Attachment)>>,
after_tool_progress: Arc<std::sync::Mutex<Vec<ToolProgressCallback>>>,
}
type ToolProgressCallback = Box<dyn FnMut(&ToolCall, &crate::progress::Progress) + Send + Sync>;
pub type CancellationChecker = Arc<dyn Fn() -> bool + Send + Sync>;
pub struct Chat {
config: Arc<Config>,
model: Model,
provider: Provider,
protocol: Option<ProtocolName>,
connection: Connection,
messages: Vec<Message>,
tools: Vec<SharedTool>,
tool_prefs: ToolPrefs,
temperature: Option<f64>,
max_output_tokens: Option<i64>,
schema: Option<Schema>,
thinking: Option<ThinkingConfig>,
citations: bool,
caching: Option<Caching>,
compaction: Option<Value>,
end_user: Option<String>,
provider_options: Value,
headers: Vec<(String, String)>,
fallbacks: Vec<Fallback>,
fallback_errors: Vec<ErrorKind>,
callbacks: Callbacks,
usage_entries: Vec<UsageEntry>,
usage_recorder: Option<UsageRecorder>,
async_usage_recorder: Option<AsyncUsageRecorder>,
tool_call_decisions: HashMap<String, bool>,
tool_deferrals: HashMap<String, bool>,
cancelled: Arc<AtomicBool>,
cancellation_checker: Option<CancellationChecker>,
mcp: crate::mcp::Collection,
mcp_deferrals: HashMap<String, Option<bool>>,
tool_call_inputs: HashMap<String, Value>,
provider_tools: Vec<crate::provider_tools::ProviderTool>,
concurrency: bool,
}
impl std::fmt::Debug for Chat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("Chat");
f.field("model", &self.model.id)
.field("provider", &self.provider.slug())
.field("messages", &self.messages.len())
.field(
"tools",
&self.tools.iter().map(|t| t.name()).collect::<Vec<_>>(),
);
let mut awaiting: Vec<String> = self
.pending_approvals()
.into_iter()
.map(|c| c.name)
.collect();
awaiting.dedup();
if !awaiting.is_empty() {
f.field("awaiting_approval", &awaiting);
}
f.finish()
}
}
#[derive(Clone)]
pub struct CancelHandle(Arc<AtomicBool>);
impl CancelHandle {
pub fn cancel(&self) {
self.0.store(true, Ordering::SeqCst);
}
}
pub(crate) fn resolve_model(
model_id: &str,
provider: Option<&str>,
assume_model_exists: bool,
) -> Result<(Model, Provider)> {
let provider_kind = provider.map(Provider::resolve_or_err).transpose()?;
let assume = assume_model_exists || provider_kind.is_some_and(|p| p.assume_models_exist());
if assume {
let provider_kind = provider_kind.ok_or_else(|| {
Error::Argument("Provider must be specified if assume_model_exists is true".into())
})?;
let model = models::models()
.find(model_id, Some(provider_kind.slug()))
.unwrap_or_else(|_| Model::default_for(model_id, provider_kind.slug()));
return Ok((model, provider_kind));
}
let model = models::models().find(model_id, provider)?;
let provider = Provider::resolve_or_err(&model.provider)?;
Ok((model, provider))
}
impl Chat {
pub fn new(model: Option<&str>, provider: Option<&str>) -> Result<Chat> {
Chat::with_config(crate::config(), model, provider, false)
}
pub fn with_config(
config: Arc<Config>,
model: Option<&str>,
provider: Option<&str>,
assume_model_exists: bool,
) -> Result<Chat> {
let model_id = model.unwrap_or(&config.default_model).to_string();
let (model, provider) = resolve_model(&model_id, provider, assume_model_exists)?;
provider.ensure_configured(&config)?;
let connection = Connection::new(provider, config.clone())?;
let concurrency = config.tool_concurrency;
Ok(Chat {
config,
model,
provider,
protocol: None,
connection,
messages: Vec::new(),
tools: Vec::new(),
tool_prefs: ToolPrefs::default(),
temperature: None,
max_output_tokens: None,
schema: None,
thinking: None,
citations: false,
caching: None,
compaction: None,
end_user: None,
provider_options: Value::Object(Map::new()),
headers: Vec::new(),
fallbacks: Vec::new(),
fallback_errors: DEFAULT_FALLBACK_ERRORS.to_vec(),
callbacks: Callbacks::default(),
usage_entries: Vec::new(),
usage_recorder: None,
async_usage_recorder: None,
tool_call_decisions: HashMap::new(),
tool_deferrals: HashMap::new(),
cancelled: Arc::new(AtomicBool::new(false)),
cancellation_checker: None,
mcp: crate::mcp::Collection::default(),
mcp_deferrals: HashMap::new(),
tool_call_inputs: HashMap::new(),
provider_tools: Vec::new(),
concurrency,
})
}
pub fn model(&self) -> &Model {
&self.model
}
pub fn provider(&self) -> Provider {
self.provider
}
pub fn messages(&self) -> &[Message] {
&self.messages
}
pub fn tools(&self) -> &[SharedTool] {
&self.tools
}
pub fn temperature(&self) -> Option<f64> {
self.temperature
}
pub fn usage_entries(&self) -> &[UsageEntry] {
&self.usage_entries
}
pub fn config(&self) -> &Arc<Config> {
&self.config
}
pub fn tokens(&self) -> Tokens {
Tokens::aggregate(self.usage_entries.iter().map(|e| &e.tokens))
}
pub fn cost(&self) -> Cost {
let complete = self.usage_entries.iter().all(UsageEntry::cost_available);
let cost = Cost::aggregate(self.usage_entries.iter().map(|e| &e.cost), complete);
if complete {
cost
} else {
cost.mark_incomplete()
}
}
pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
self.set_instructions(Some(instructions.into()), false, false);
self
}
pub fn set_instructions(
&mut self,
instructions: Option<String>,
append: bool,
cache_until_here: bool,
) -> &mut Self {
self.set_instructions_with(instructions, append, &Value::Bool(cache_until_here))
.expect("a Boolean is a valid cache boundary")
}
pub fn set_instructions_with(
&mut self,
instructions: Option<String>,
append: bool,
cache_until_here: &Value,
) -> Result<&mut Self> {
let boundary = Message::cache_boundary_options(cache_until_here)?;
if !append {
self.messages.retain(|m| m.role != Role::System);
}
if let Some(text) = instructions {
let mut m = Message::system(text);
if let Some(ttl) = &boundary {
m = m.with_cache_until_here(ttl.as_deref());
}
self.messages.push(m);
}
Ok(self)
}
pub fn with_tool(mut self, tool: impl Tool + 'static) -> Self {
self.add_tool(Arc::new(tool));
self
}
pub fn with_tools(mut self, tools: impl IntoIterator<Item = SharedTool>) -> Self {
for t in tools {
self.add_tool(t);
}
self
}
pub fn add_tool(&mut self, tool: SharedTool) -> &mut Self {
self.add_tool_deferred(tool, None)
}
pub fn add_tool_deferred(&mut self, tool: SharedTool, defer: Option<bool>) -> &mut Self {
let name = tool.name();
self.tools.retain(|t| t.name() != name);
self.tool_deferrals
.insert(name, defer.unwrap_or_else(|| tool.is_deferred()));
self.tools.push(tool);
self
}
pub fn with_deferred_tools(mut self, tools: impl IntoIterator<Item = SharedTool>) -> Self {
for t in tools {
self.add_tool_deferred(t, Some(true));
}
self
}
pub fn clear_tools(&mut self) -> &mut Self {
self.tools.clear();
self.tool_deferrals.clear();
self
}
pub fn deferred_tools(&self) -> Vec<SharedTool> {
let names = self.deferred_tool_names();
self.combined_tools()
.unwrap_or_else(|_| self.tools.clone())
.into_iter()
.filter(|t| names.contains(&t.name()))
.collect()
}
fn deferred_tool_names(&self) -> Vec<String> {
let mut names: Vec<String> = self
.tool_deferrals
.iter()
.filter(|(_, deferred)| **deferred)
.map(|(name, _)| name.clone())
.collect();
for server in self.mcp.iter() {
let explicit = self.mcp_deferrals.get(&server.name()).copied().flatten();
let Some(Ok(tools)) = server.cached_tools() else {
continue;
};
for tool in tools {
let name = tool.name();
let deferred = explicit.unwrap_or_else(|| {
server.defers(&*tool, server.server_name_of(&name).as_deref())
});
if deferred {
names.push(name);
}
}
}
names
}
fn request_tools(&self) -> Result<Vec<SharedTool>> {
let deferred = self.deferred_tool_names();
Ok(self
.combined_tools()?
.into_iter()
.map(|t| {
if deferred.contains(&t.name()) {
Arc::new(crate::tool::Deferred(t)) as SharedTool
} else {
t
}
})
.collect())
}
pub fn with_mcp(self, server: crate::mcp::Mcp) -> Self {
self.with_mcp_deferred(server, None)
}
pub fn with_mcp_deferred(mut self, server: crate::mcp::Mcp, defer: Option<bool>) -> Self {
self.mcp_deferrals.insert(server.name(), defer);
self.mcp.push(server);
self
}
pub fn clear_mcp(&mut self) -> &mut Self {
self.mcp = crate::mcp::Collection::default();
self.mcp_deferrals.clear();
self
}
pub fn mcp(&self) -> &crate::mcp::Collection {
&self.mcp
}
pub async fn all_tools(&self) -> Result<Vec<SharedTool>> {
self.load_mcp_tools().await?;
self.combined_tools()
}
async fn load_mcp_tools(&self) -> Result<()> {
for server in self.mcp.iter() {
server.tools().await?;
}
Ok(())
}
fn combined_tools(&self) -> Result<Vec<SharedTool>> {
let mut tools: Vec<SharedTool> = self
.tools
.iter()
.filter(|t| t.is_model_visible())
.cloned()
.collect();
for server in self.mcp.iter() {
let Some(server_tools) = server.cached_tools() else {
continue;
};
for tool in server_tools? {
if !tool.is_model_visible() {
continue;
}
let name = tool.name();
if tools.iter().any(|t| t.name() == name) {
return Err(Error::Argument(format!(
"Two tools are named {name}. Rename one with `tool :{name}, as:`"
)));
}
tools.push(tool);
}
}
Ok(tools)
}
pub fn with_provider_tools(
mut self,
tools: impl IntoIterator<Item = crate::provider_tools::ProviderTool>,
) -> Self {
self.provider_tools.extend(tools);
self
}
pub fn clear_provider_tools(&mut self) -> &mut Self {
self.provider_tools.clear();
self
}
pub fn provider_tools(&self) -> &[crate::provider_tools::ProviderTool] {
&self.provider_tools
}
pub fn with_tool_choice(mut self, choice: ToolChoice) -> Result<Self> {
if let ToolChoice::Tool(name) = &choice
&& !self.tools.iter().any(|t| &t.name() == name)
{
let mut valid = vec!["auto".to_string(), "none".into(), "required".into()];
valid.extend(self.tools.iter().map(|t| t.name()));
return Err(Error::InvalidToolChoice(format!(
"Invalid tool choice: {name}. Valid choices are: {}",
valid.join(", ")
)));
}
self.tool_prefs.choice = Some(choice);
Ok(self)
}
pub fn clear_tool_choice(mut self) -> Self {
self.tool_prefs.choice = None;
self
}
pub fn with_tool_calls(mut self, calls: impl Into<Option<ToolCalls>>) -> Self {
self.tool_prefs.calls = calls.into();
self
}
pub fn tool_prefs(&self) -> &protocols::ToolPrefs {
&self.tool_prefs
}
pub fn with_tool_concurrency(mut self, enabled: impl Into<Option<bool>>) -> Self {
self.concurrency = enabled.into().unwrap_or(self.config.tool_concurrency);
self
}
pub fn concurrency(&self) -> bool {
self.concurrency
}
pub fn with_model(mut self, model_id: &str, provider: Option<&str>) -> Result<Self> {
self.switch_model(model_id, provider, false)?;
self.protocol = None;
Ok(self)
}
pub fn with_default_model(self) -> Result<Self> {
let default = self.config.default_model.clone();
self.with_model(&default, None)
}
pub fn with_assumed_model(mut self, model_id: &str, provider: &str) -> Result<Self> {
self.switch_model(model_id, Some(provider), true)?;
Ok(self)
}
fn switch_model(&mut self, model_id: &str, provider: Option<&str>, assume: bool) -> Result<()> {
let (model, provider) = resolve_model(model_id, provider, assume)?;
provider.ensure_configured(&self.config)?;
self.connection = Connection::new(provider, self.config.clone())?;
self.model = model;
self.provider = provider;
Ok(())
}
pub fn with_protocol(mut self, protocol: ProtocolName) -> Self {
self.protocol = Some(protocol);
self
}
pub fn with_temperature(mut self, temperature: impl Into<Option<f64>>) -> Self {
self.temperature = temperature.into();
self
}
pub fn with_max_output_tokens(mut self, max: impl Into<Option<i64>>) -> Self {
self.max_output_tokens = max.into();
self
}
pub fn max_output_tokens(&self) -> Option<i64> {
self.max_output_tokens
}
pub fn protocol(&self) -> Option<ProtocolName> {
self.protocol
}
pub fn thinking(&self) -> Option<&ThinkingConfig> {
self.thinking.as_ref()
}
pub fn with_thinking(mut self, thinking: ThinkingConfig) -> Self {
self.thinking = Some(thinking);
self
}
pub fn with_citations(mut self, enabled: bool) -> Self {
self.citations = enabled;
self
}
pub fn citations(&self) -> bool {
self.citations
}
pub fn with_caching(mut self, options: Value) -> Result<Self> {
self.caching = Some(match options {
Value::Bool(true) => Caching::On(Map::new()),
Value::Bool(false) => Caching::Off,
Value::Object(options) => Caching::On(options),
_ => {
return Err(Error::Argument(
"with_caching accepts true, false, or caching options".into(),
));
}
});
Ok(self)
}
pub fn caching(&self) -> Option<&Caching> {
self.caching.as_ref()
}
pub fn with_compaction(mut self, options: Value) -> Result<Self> {
self.compaction = Some(match options {
Value::Bool(true) => Value::Object(Map::new()),
Value::Bool(false) => Value::Bool(false),
Value::Object(options) => {
let unsupported: Vec<String> = options
.keys()
.filter(|k| !COMPACTION_OPTIONS.contains(&k.as_str()))
.map(|k| format!(":{k}"))
.collect();
if !unsupported.is_empty() {
let accepted: Vec<String> =
COMPACTION_OPTIONS.iter().map(|k| format!(":{k}")).collect();
return Err(Error::Argument(format!(
"with_compaction accepts {}, got {}. Provider-specific settings go through with_provider_options.",
accepted.join(", "),
unsupported.join(", ")
)));
}
Value::Object(options)
}
_ => {
return Err(Error::Argument(
"with_compaction accepts true, false, or compaction options".into(),
));
}
});
Ok(self)
}
pub fn compaction(&self) -> Option<&Value> {
self.compaction.as_ref()
}
pub fn with_end_user(mut self, end_user: Option<&str>) -> Self {
self.end_user = end_user.map(str::to_string);
self
}
pub fn end_user(&self) -> Option<&str> {
self.end_user.as_deref()
}
pub fn with_context(mut self, context: Option<&crate::context::Context>) -> Result<Self> {
let config = context
.map(|c| c.config().clone())
.unwrap_or_else(crate::config);
self.provider.ensure_configured(&config)?;
self.connection = Connection::new(self.provider, config.clone())?;
self.config = config;
Ok(self)
}
pub fn with_schema(mut self, schema: Value) -> Self {
self.schema = normalize_schema(schema);
self
}
pub fn with_schema_for<T: schemars::JsonSchema>(self) -> Self {
let name = T::schema_name().to_string();
let mut schema = crate::tool::schema_for::<T>();
if let Some(obj) = schema.as_object_mut() {
obj.remove("$defs");
}
self.with_schema(serde_json::json!({ "name": name, "schema": schema }))
}
pub fn with_provider_options(mut self, options: Value) -> Self {
self.provider_options = match options {
Value::Null => Value::Object(Map::new()),
options => options,
};
self
}
pub fn provider_options(&self) -> &Value {
&self.provider_options
}
pub fn with_params(self, params: Value) -> Self {
self.with_provider_options(params)
}
pub fn with_headers(mut self, headers: impl IntoIterator<Item = (String, String)>) -> Self {
self.headers = headers.into_iter().collect();
self
}
pub fn headers(&self) -> &[(String, String)] {
&self.headers
}
pub fn with_fallbacks(mut self, models: impl IntoIterator<Item = Fallback>) -> Self {
self.fallbacks = models.into_iter().collect();
self.fallback_errors = DEFAULT_FALLBACK_ERRORS.to_vec();
self
}
pub fn with_fallback_errors(mut self, kinds: Vec<ErrorKind>) -> Self {
self.fallback_errors = kinds;
self
}
pub fn fallbacks(&self) -> &[Fallback] {
&self.fallbacks
}
pub fn fallback_errors(&self) -> &[ErrorKind] {
&self.fallback_errors
}
pub fn before_message(mut self, f: impl FnMut() + Send + Sync + 'static) -> Self {
self.callbacks.before_message.push(Box::new(f));
self
}
pub fn after_message(mut self, f: impl FnMut(&Message) + Send + Sync + 'static) -> Self {
self.callbacks.after_message.push(Box::new(f));
self
}
pub fn before_tool_call(mut self, f: impl FnMut(&ToolCall) + Send + Sync + 'static) -> Self {
self.callbacks.before_tool_call.push(Box::new(f));
self
}
pub fn after_tool_result(mut self, f: impl FnMut(&ToolResult) + Send + Sync + 'static) -> Self {
self.callbacks.after_tool_result.push(Box::new(f));
self
}
pub fn after_tool_progress(
self,
f: impl FnMut(&ToolCall, &crate::progress::Progress) + Send + Sync + 'static,
) -> Self {
if let Ok(mut callbacks) = self.callbacks.after_tool_progress.lock() {
callbacks.push(Box::new(f));
}
self
}
pub fn before_fallback(
mut self,
f: impl FnMut(&FallbackAttempt) + Send + Sync + 'static,
) -> Self {
self.callbacks.before_fallback.push(Box::new(f));
self
}
pub fn after_fallback(
mut self,
f: impl FnMut(&FallbackAttempt) + Send + Sync + 'static,
) -> Self {
self.callbacks.after_fallback.push(Box::new(f));
self
}
pub fn convert_unsupported_attachments(
mut self,
f: impl Fn(&Attachment) -> Result<Option<Attachment>> + Send + Sync + 'static,
) -> Self {
self.callbacks.unsupported_attachment.push(Box::new(f));
self
}
pub fn before_request(self, f: impl FnMut(&mut Value) + Send + Sync + 'static) -> Self {
if let Ok(mut hooks) = self.callbacks.before_request.lock() {
hooks.push(Box::new(f));
}
self
}
pub fn set_usage_recorder(&mut self, recorder: UsageRecorder) {
self.usage_recorder = Some(recorder);
}
pub fn set_async_usage_recorder(&mut self, recorder: AsyncUsageRecorder) {
self.async_usage_recorder = Some(recorder);
}
pub fn clear_async_usage_recorder(&mut self) {
self.async_usage_recorder = None;
}
pub fn add_message(&mut self, message: Message) -> &Message {
self.messages.push(message);
self.messages.last().unwrap() }
pub async fn add_completion(&mut self, mut message: Message, record_usage: bool) -> &Message {
if message.usage_entries.is_empty() {
let entry = UsageEntry {
id: UsageEntry::next_id(),
owner: crate::accounting::usage_owner(),
operation: Operation::Chat,
provider: self.provider.slug().into(),
model: message
.model
.clone()
.unwrap_or_else(|| self.model.id.clone()),
status: UsageStatus::Succeeded,
tokens: message.tokens.clone(),
cost: message.cost(None),
};
message.usage_entries = vec![entry.clone()];
self.record_usage_entry(entry).await;
} else if record_usage {
for entry in message.usage_entries.clone() {
self.record_usage_entry(entry).await;
}
}
for cb in &mut self.callbacks.before_message {
cb();
}
self.messages.push(message);
let message = self.messages.last().unwrap(); for cb in &mut self.callbacks.after_message {
cb(message);
}
message
}
pub fn set_messages(&mut self, messages: Vec<Message>) {
self.messages = messages;
}
pub fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
self.usage_entries = entries;
}
pub fn cache_until_here(&mut self) -> Result<&mut Self> {
self.cache_until_here_with(None)
}
pub fn cache_until_here_with(&mut self, ttl: Option<&str>) -> Result<&mut Self> {
let last = self
.messages
.last_mut()
.ok_or_else(|| Error::Argument("No messages to cache".into()))?;
last.cache_until_here = true;
last.cache_ttl = ttl.map(str::to_string);
Ok(self)
}
pub async fn ask(&mut self, message: impl Into<String>) -> Result<Message> {
self.ask_later(message)?;
self.complete().await
}
pub async fn ask_with(
&mut self,
message: impl Into<String>,
attachments: Vec<Attachment>,
) -> Result<Message> {
self.ask_later_with(message, attachments)?;
self.complete().await
}
pub async fn ask_stream(
&mut self,
message: impl Into<String>,
on_chunk: impl FnMut(&Message) + Send,
) -> Result<Message> {
self.ask_later(message)?;
self.complete_stream(on_chunk).await
}
pub async fn say(&mut self, message: impl Into<String>) -> Result<Message> {
self.ask(message).await
}
pub fn ask_later(&mut self, message: impl Into<String>) -> Result<&mut Self> {
self.ask_later_with(message, Vec::new())
}
pub fn ask_later_with(
&mut self,
message: impl Into<String>,
attachments: Vec<Attachment>,
) -> Result<&mut Self> {
self.raise_if_pending_tool_calls()?;
self.messages
.push(Message::user(message).with_attachments(attachments));
Ok(self)
}
pub async fn ask_prompt(&mut self, prompt: &crate::mcp::Prompt) -> Result<Message> {
self.ask_later_prompt(prompt)?;
self.complete().await
}
pub fn ask_later_prompt(&mut self, prompt: &crate::mcp::Prompt) -> Result<&mut Self> {
self.raise_if_pending_tool_calls()?;
self.messages.extend(prompt.messages.iter().cloned());
Ok(self)
}
pub async fn complete(&mut self) -> Result<Message> {
if self.is_awaiting_tasks() {
self.run_tools().await?;
}
while !self.is_complete() && !self.waiting() {
self.step_inner(None).await?;
}
Ok(self
.last_non_system_message()
.or_else(|| self.messages.last())
.cloned()
.unwrap_or_else(Message::chunk))
}
pub async fn complete_stream(
&mut self,
mut on_chunk: impl FnMut(&Message) + Send,
) -> Result<Message> {
while !self.is_complete() && !self.waiting() {
self.step_inner(Some(&mut on_chunk)).await?;
}
Ok(self
.last_non_system_message()
.or_else(|| self.messages.last())
.cloned()
.unwrap_or_else(Message::chunk))
}
pub async fn step(&mut self) -> Result<Option<Message>> {
self.step_inner(None).await
}
pub async fn step_stream(
&mut self,
mut on_chunk: impl FnMut(&Message) + Send,
) -> Result<Option<Message>> {
self.step_inner(Some(&mut on_chunk)).await
}
pub fn messages_mut(&mut self) -> &mut Vec<Message> {
&mut self.messages
}
async fn step_inner(
&mut self,
on_chunk: Option<&mut (dyn FnMut(&Message) + Send)>,
) -> Result<Option<Message>> {
if self.is_complete() {
return Ok(None);
}
if let Err(e) = self.raise_if_cancelled() {
self.cancel_tasks().await;
return Err(e);
}
if self.pending_tool_response().is_none() {
return self.generate_inner(on_chunk).await.map(Some);
}
let before = self.messages.len();
self.run_tools().await?;
Ok((self.messages.len() > before)
.then(|| self.messages.last().cloned())
.flatten())
}
pub fn is_complete(&self) -> bool {
if self.pending_tool_response().is_some() {
return false;
}
match self.last_non_system_message() {
None => true,
Some(m) => match m.role {
Role::User | Role::Tool => false,
_ => !m.is_tool_call(),
},
}
}
pub async fn generate(&mut self) -> Result<Message> {
self.generate_inner(None).await
}
async fn generate_inner(
&mut self,
mut on_chunk: Option<&mut (dyn FnMut(&Message) + Send)>,
) -> Result<Message> {
self.raise_if_cancelled()?;
if self.fallbacks.is_empty() {
return self.generate_once(on_chunk, &mut false).await;
}
let original = (
self.model.clone(),
self.provider,
self.protocol,
self.connection.clone(),
);
let usage_start = self.usage_entries.len();
let mut queue: std::collections::VecDeque<Fallback> =
self.fallbacks.iter().cloned().collect();
let mut attempt = 0;
let mut active: Option<FallbackAttempt> = None;
let result = loop {
let mut chunks_yielded = false;
let streaming = on_chunk.is_some();
let result = self
.generate_once(
on_chunk
.as_mut()
.map(|f| &mut **f as &mut (dyn FnMut(&Message) + Send)),
&mut chunks_yielded,
)
.await;
if let Some(mut a) = active.take() {
a.succeeded = Some(result.is_ok());
match &result {
Ok(message) => a.response = Some(message.clone()),
Err(e) => a.fallback_error = Some((e.kind(), e.to_string())),
}
for cb in &mut self.callbacks.after_fallback {
cb(&a);
}
}
match result {
Ok(mut message) => {
message.usage_entries = self.usage_entries[usage_start..].to_vec();
if let Some(last) = self.messages.last_mut() {
last.usage_entries = message.usage_entries.clone();
}
break Ok(message);
}
Err(e) if self.fallback_errors.contains(&e.kind()) => {
let Some(next) = queue.pop_front() else {
break Err(e);
};
attempt += 1;
let from = self.model.id.clone();
let from_provider = self.provider;
if let Err(switch_err) =
self.switch_model(&next.model, next.provider.as_deref(), false)
{
break Err(switch_err);
}
if self.provider != from_provider {
self.protocol = None;
}
let a = FallbackAttempt {
attempt,
error: e.to_string(),
error_kind: e.kind(),
from,
to: self.model.id.clone(),
to_provider: self.provider.slug().into(),
streaming,
chunks_yielded,
succeeded: None,
response: None,
fallback_error: None,
};
for cb in &mut self.callbacks.before_fallback {
cb(&a);
}
active = Some(a);
}
Err(e) => break Err(e),
}
};
(self.model, self.provider, self.protocol, self.connection) = original;
result
}
async fn record_usage(&mut self, entry: UsageEntry) {
if let Some(r) = &mut self.usage_recorder {
r(&entry);
}
match self.async_usage_recorder.clone() {
Some(r) => r(entry.clone()).await,
None => crate::accounting::record(&self.config, &entry).await,
}
crate::instrumentation::usage(&self.config, &entry);
self.usage_entries.push(entry);
}
async fn record_usage_entry(&mut self, entry: UsageEntry) {
if let Some(r) = &mut self.usage_recorder {
r(&entry);
} else if let Some(r) = self.async_usage_recorder.clone() {
r(entry.clone()).await;
} else {
crate::accounting::record(&self.config, &entry).await;
}
crate::instrumentation::usage(&self.config, &entry);
self.usage_entries.push(entry);
}
fn preprocessed_messages(&self) -> Result<Vec<Message>> {
let protocol = self
.provider
.resolve_protocol(self.protocol, &self.model, &self.config)
.ok();
self.request_history()
.into_iter()
.map(|m| {
let mut m = if self.foreign_native_content(&m) {
let portable = protocol
.filter(|_| {
m.producing_entry()
.is_some_and(|e| e.provider == self.provider.slug())
})
.and_then(|p| crate::tool_search::portable_raw_content(p, &m));
m.for_request().without_native_content(portable)
} else {
m.for_request()
};
self.replace_unsupported_attachments(&mut m)?;
Ok(m)
})
.collect()
}
fn replace_unsupported_attachments(&self, message: &mut Message) -> Result<()> {
if self.callbacks.unsupported_attachment.is_empty() || message.attachments.is_empty() {
return Ok(());
}
let protocol = self
.provider
.resolve_protocol(self.protocol, &self.model, &self.config)?;
let supported = |a: &Attachment| {
protocols::supported_message_attachment(protocol, self.provider, message.role, a)
};
let mut prepared = Vec::with_capacity(message.attachments.len());
for attachment in &message.attachments {
if supported(attachment) {
prepared.push(attachment.clone());
continue;
}
let Some(replacement) = self.attachment_replacement(attachment)? else {
prepared.push(attachment.clone());
continue;
};
if !supported(&replacement) {
return Err(Error::UnsupportedAttachment(
protocols::anthropic::unsupported(&replacement.mime_type),
));
}
prepared.push(replacement);
}
message.attachments = prepared;
Ok(())
}
fn attachment_replacement(&self, attachment: &Attachment) -> Result<Option<Attachment>> {
let memo = |replacements: &std::sync::Mutex<Vec<(Attachment, Attachment)>>| {
replacements.lock().ok().and_then(|r| {
r.iter()
.find(|(original, _)| original.same_attachment(attachment))
.map(|(_, replacement)| replacement.clone())
})
};
if let Some(replacement) = memo(&self.callbacks.attachment_replacements) {
return Ok(Some(replacement));
}
for convert in &self.callbacks.unsupported_attachment {
if let Some(replacement) = convert(attachment)? {
if let Ok(mut r) = self.callbacks.attachment_replacements.lock() {
r.push((attachment.clone(), replacement.clone()));
}
return Ok(Some(replacement));
}
}
Ok(None)
}
fn foreign_native_content(&self, m: &Message) -> bool {
let carries = m.thinking.is_some()
|| m.raw_reasoning.is_some()
|| m.raw_content.is_some()
|| m.tool_calls
.iter()
.flat_map(|c| c.values())
.any(|c| c.thought_signature.is_some());
if m.role != Role::Assistant || !carries {
return false;
}
let Some(producer) = m.producing_entry() else {
return false;
};
producer.provider != self.provider.slug() || !self.is_current_model(&producer.model)
}
fn is_current_model(&self, model_id: &str) -> bool {
model_id == self.model.id
|| models::models()
.find(model_id, Some(self.provider.slug()))
.is_ok_and(|m| m.id == self.model.id)
}
fn request_history(&self) -> Vec<std::borrow::Cow<'_, Message>> {
use std::borrow::Cow;
let kept: Vec<&Message> = self
.messages
.iter()
.filter(|m| !is_blank_response(m))
.collect();
let answered: Vec<&str> = kept
.iter()
.filter(|m| m.is_tool_result())
.filter_map(|m| m.tool_call_id.as_deref())
.collect();
let current = latest_response(kept.iter().copied());
let mut out = Vec::with_capacity(kept.len());
let mut i = 0;
while i < kept.len() {
let response = kept[i];
out.push(Cow::Borrowed(response));
i += 1;
while i < kept.len() && kept[i].is_tool_result() {
out.push(Cow::Borrowed(kept[i]));
i += 1;
}
if response.is_tool_call() && !current.is_some_and(|c| std::ptr::eq(c, response)) {
for call in response.tool_calls.iter().flat_map(|c| c.values()) {
if !answered.contains(&call.id.as_str()) {
out.push(Cow::Owned(self.unfinished_result(call)));
}
}
}
}
out
}
fn unfinished_result(&self, call: &ToolCall) -> Message {
let responses = self
.provider
.resolve_protocol(self.protocol, &self.model, &self.config)
.is_ok_and(|p| p == ProtocolName::Responses);
if call.remote && responses {
let mut m = Message::tool_result(call.id.clone(), "Denied");
m.raw_content = Some(serde_json::json!([{
"type": "mcp_approval_response", "approval_request_id": call.id, "approve": false
}]));
return m;
}
Message::tool_result(call.id.clone(), UNFINISHED_TOOL_RESULT)
}
pub fn render(&self) -> Result<Value> {
self.render_with(&self.preprocessed_messages()?, false)
.map(|(p, _)| p)
}
fn render_with(&self, messages: &[Message], stream: bool) -> Result<(Value, ProtocolName)> {
let protocol = self
.provider
.resolve_protocol(self.protocol, &self.model, &self.config)?;
let thinking = match &self.thinking {
Some(t) => t.resolve(&self.model)?,
None => None,
};
let tools = self.request_tools()?;
let request = Request {
provider: self.provider,
config: &self.config,
model: &self.model,
messages,
tools: &tools,
tool_prefs: &self.tool_prefs,
temperature: self.temperature,
max_output_tokens: self.max_output_tokens,
schema: self.schema.as_ref(),
thinking: thinking.as_ref(),
citations: self.citations,
caching: self.caching.as_ref(),
stream,
};
let mut payload = protocols::render(protocol, &request)?;
if let Some(end_user) = &self.end_user {
protocols::apply_end_user(protocol, self.provider, &mut payload, end_user);
}
if let Some(Value::Object(compaction)) = &self.compaction {
protocols::apply_compaction(protocol, self.provider, &mut payload, compaction);
}
protocols::deep_merge(&mut payload, &self.provider_options);
if let Some(resolution) =
crate::provider_tools::resolve(protocol, self.provider, &self.provider_tools)?
{
crate::provider_tools::apply(&mut payload, &resolution);
}
crate::tool_search::apply(protocol, &mut payload);
self.apply_before_request_hooks(&mut payload);
protocols::finish_render(protocol, &mut payload)?;
Ok((payload, protocol))
}
fn apply_before_request_hooks(&self, payload: &mut Value) {
if let Ok(mut hooks) = self.callbacks.before_request.lock() {
for hook in hooks.iter_mut() {
hook(payload);
}
}
}
async fn load_attachments(&mut self, messages: &mut [Message]) -> Result<()> {
let client = self.connection.client().clone();
for a in self
.messages
.iter_mut()
.flat_map(|m| m.attachments.iter_mut())
{
a.prepare(&client).await?;
}
for a in messages.iter_mut().flat_map(|m| m.attachments.iter_mut()) {
a.prepare(&client).await?;
}
loop {
let _ = self.render_with(messages, false);
let mut fetched = false;
for a in messages
.iter_mut()
.flat_map(|m| m.attachments.iter_mut())
.filter(|a| a.is_wanted())
{
a.load(&client).await?;
fetched = true;
}
if !fetched {
return Ok(());
}
}
}
async fn generate_once(
&mut self,
on_chunk: Option<&mut (dyn FnMut(&Message) + Send)>,
chunks_yielded: &mut bool,
) -> Result<Message> {
self.raise_if_cancelled()?;
let entries_before = self.usage_entries.len();
let mut event = self.instrumentation_event("chat.rust_llm", on_chunk.is_some());
let result = event
.instrument(self.generate_once_inner(on_chunk, chunks_yielded))
.await;
event.set("response_tokens", || {
crate::instrumentation::tokens_h(&match &result {
Ok(message) => message.tokens(),
Err(_) => Tokens::aggregate(
self.usage_entries[entries_before.min(self.usage_entries.len())..]
.iter()
.map(|e| &e.tokens),
),
})
});
self.finish_completion_event(event, &result);
result
}
fn instrumentation_event(&self, name: &str, streaming: bool) -> crate::instrumentation::Event {
crate::instrumentation::Event::start(&self.config, name, || {
let empty = Tokens::default();
let choice = self.tool_prefs.choice.as_ref().map(|c| match c {
ToolChoice::Auto => "auto".to_string(),
ToolChoice::None => "none".into(),
ToolChoice::Required => "required".into(),
ToolChoice::Tool(name) => name.clone(),
});
let calls = self
.tool_prefs
.calls
.map(|c| if c == ToolCalls::One { "one" } else { "many" });
crate::instrumentation::payload([
("provider", self.provider.slug().into()),
("provider_class", self.provider.display().into()),
("model", self.model.id.clone().into()),
(
"input_messages",
Value::Array(self.messages.iter().map(Message::to_h).collect()),
),
("message_count", self.messages.len().into()),
(
"tools",
Value::Array(self.tools.iter().map(|t| t.name().into()).collect()),
),
("tool_choice", choice.into()),
("tool_call_limit", calls.into()),
("temperature", self.temperature.into()),
("max_output_tokens", self.max_output_tokens.into()),
("provider_options", self.provider_options.clone()),
(
"schema",
self.schema.as_ref().map(|s| s.schema.clone()).into(),
),
("citations", self.citations.into()),
("streaming", streaming.into()),
("tokens", crate::instrumentation::tokens_h(&empty)),
(
"cost",
crate::instrumentation::cost_h(&Cost::new(
&empty,
Some(&self.model),
Tier::Standard,
)),
),
])
})
}
fn finish_completion_event(
&self,
mut event: crate::instrumentation::Event,
result: &Result<Message>,
) {
if let Ok(message) = result {
event.set("response", || message.to_h());
event.set("messages_after", || {
Value::Array(self.messages.iter().map(Message::to_h).collect())
});
event.set("response_role", || message.role.as_str().into());
event.set("tokens", || {
crate::instrumentation::tokens_h(&message.tokens())
});
event.set("cost", || {
crate::instrumentation::cost_h(&message.cost(None))
});
event.set("response_model", || message.model.clone().into());
event.set("tool_call", || message.is_tool_call().into());
event.set("tool_calls", || {
serde_json::to_value(&message.tool_calls).unwrap_or(Value::Null)
});
}
event.finish(result.as_ref().err());
}
async fn generate_once_inner(
&mut self,
mut on_chunk: Option<&mut (dyn FnMut(&Message) + Send)>,
chunks_yielded: &mut bool,
) -> Result<Message> {
let usage_start = self.usage_entries.len();
self.load_mcp_tools().await?;
self.load_attachments(&mut []).await?;
let mut messages = self.request_messages().await?;
let streaming = on_chunk.is_some();
if streaming {
for cb in &mut self.callbacks.before_message {
cb();
}
}
let mut segments = Vec::new();
let mut billed_model = None;
let mut reuploaded = false;
let mut continuation = 0;
while continuation < protocols::anthropic::MAX_PAUSE_TURN_CONTINUATIONS {
let chunk_sink = on_chunk
.as_mut()
.map(|f| &mut **f as &mut (dyn FnMut(&Message) + Send));
let (segment, billed, protocol) = match self
.request_once(&messages, chunk_sink, chunks_yielded)
.await
{
Ok(answer) => answer,
Err(e) => {
if reuploaded || *chunks_yielded || !self.discard_missing_uploads(&e).await {
return Err(e);
}
reuploaded = true;
segments.clear();
messages = self.request_messages().await?;
continuation = 0;
continue;
}
};
continuation += 1;
billed_model = Some(billed);
let paused = protocol == ProtocolName::Anthropic
&& segment.finish_reason == Some(FinishReason::PauseTurn);
if paused {
messages.push(segment.for_request());
}
segments.push(segment);
if !paused {
break;
}
}
let mut message = protocols::anthropic::merge_turn_segments(segments);
message.usage_entries = self.usage_entries[usage_start..].to_vec();
message.model_info = billed_model;
if !streaming {
for cb in &mut self.callbacks.before_message {
cb();
}
}
self.messages.push(message.clone());
for cb in &mut self.callbacks.after_message {
cb(&message);
}
Ok(message)
}
async fn request_messages(&mut self) -> Result<Vec<Message>> {
let mut messages = self.preprocessed_messages()?;
let upload_protocol =
self.provider
.resolve_protocol(self.protocol, &self.model, &self.config)?;
crate::files::preprocess_messages(
&mut messages,
upload_protocol,
self.provider,
&self.config,
&self.connection,
)
.await?;
self.load_attachments(&mut messages).await?;
Ok(messages)
}
async fn discard_missing_uploads(&self, error: &Error) -> bool {
let upload_messages: Vec<Message> = {
let replacements = self
.callbacks
.attachment_replacements
.lock()
.map(|r| r.clone())
.unwrap_or_default();
self.messages
.iter()
.map(|m| {
let mut attachments = m.attachments.clone();
attachments.extend(m.attachments.iter().filter_map(|a| {
replacements
.iter()
.find(|(original, _)| original.same_attachment(a))
.map(|(_, replacement)| replacement.clone())
}));
m.clone().with_attachments(attachments)
})
.collect()
};
!crate::files::discard_missing_uploads(
&upload_messages,
error,
self.provider,
&self.config,
&self.connection,
)
.await
.is_empty()
}
async fn request_once(
&mut self,
messages: &[Message],
on_chunk: Option<&mut (dyn FnMut(&Message) + Send)>,
chunks_yielded: &mut bool,
) -> Result<(Message, Model, ProtocolName)> {
let streaming = on_chunk.is_some();
let (payload, protocol) = self.render_with(messages, streaming)?;
let mut endpoint = protocols::endpoint(protocol, self.provider, &self.model, streaming);
if protocol == ProtocolName::Responses && self.provider == Provider::Perplexity {
endpoint.path = self.provider.agent_url(&self.config)?;
}
if protocol == ProtocolName::RouterChatCompletions {
endpoint.path = self.provider.router_url(&self.config, "chat/completions")?;
}
let mut headers = endpoint.headers;
if let Some(resolution) =
crate::provider_tools::resolve(protocol, self.provider, &self.provider_tools)?
{
headers.extend(
resolution
.headers
.into_iter()
.filter(|(k, _)| !self.headers.iter().any(|(h, _)| h.eq_ignore_ascii_case(k))),
);
}
headers.extend(self.headers.iter().cloned());
if protocol == ProtocolName::Anthropic && matches!(self.compaction, Some(Value::Object(_)))
{
protocols::anthropic::apply_compaction_headers(&mut headers);
}
crate::files::apply_files_beta(protocol, &payload, &mut headers);
let mut attempts = 0usize;
let mut retried: Vec<Tokens> = Vec::new();
let mut on_attempt = |previous: Option<&Error>| {
attempts += 1;
if let Some(e) = previous {
retried.push(failure_tokens(e, None));
}
};
let mut observed = Tokens::default();
let result = if let Some(on_chunk) = on_chunk {
let mut acc = StreamAccumulator::default();
let mut state =
StreamState::for_payload(protocol, self.provider, &self.model, &payload);
let provider = self.provider;
let cancelled = self.cancelled.clone();
let checker = self.cancellation_checker.clone();
let mut on_event = |_event: crate::transport::SseEvent, data: Value| -> Result<()> {
let chunk = protocols::build_chunk(protocol, provider, &mut state, &data)?;
acc.add(&chunk);
*chunks_yielded = true;
on_chunk(&chunk);
let external = checker.as_ref().is_some_and(|c| c());
if cancelled.swap(false, Ordering::SeqCst) || external {
return Err(Error::Cancelled);
}
Ok(())
};
let status = protocols::streaming_error_status(protocol);
let streamed = self
.connection
.stream(
&endpoint.path,
&payload,
&headers,
&mut on_attempt,
&mut on_event,
status,
)
.await;
observed = acc.tokens().clone();
match streamed.and_then(|raw| protocols::finish_stream(protocol, &mut state, acc, raw))
{
Ok((message, Some(last))) => {
on_chunk(&last);
Ok(message)
}
Ok((message, None)) => Ok(message),
Err(e) => Err(e),
}
} else {
match self
.connection
.post(&endpoint.path, &payload, &headers, &mut on_attempt)
.await
{
Ok(raw) => protocols::parse_completion(protocol, self.provider, &self.model, raw),
Err(e) => Err(e),
}
};
let mut call_entries = Vec::new();
for tokens in retried {
let entry = self.entry(UsageStatus::Failed, tokens, None);
self.record_usage(entry.clone()).await;
call_entries.push(entry);
}
let mut message = match result {
Ok(m) => m,
Err(e) => {
let status = if matches!(e, Error::Cancelled) {
UsageStatus::Cancelled
} else {
UsageStatus::Failed
};
if attempts > 0 {
let observed = (!observed.is_empty()).then_some(observed);
self.record_usage(self.entry(status, failure_tokens(&e, observed), None))
.await;
}
return Err(e.claim(protocol, self.provider.slug(), &self.model.id, &payload));
}
};
let billed_model = message
.model
.as_deref()
.and_then(|id| models::models().find(id, Some(self.provider.slug())).ok())
.unwrap_or_else(|| self.model.clone());
let entry = self.entry(
UsageStatus::Succeeded,
message.tokens.clone(),
Some(&billed_model),
);
self.record_usage(entry.clone()).await;
self.raise_if_cancelled()?;
call_entries.push(entry);
message.usage_entries = call_entries.clone();
message.model_info = Some(billed_model.clone());
Ok((message, billed_model, protocol))
}
pub async fn count_tokens(&self, message: Option<&str>) -> Result<i64> {
let protocol = self
.provider
.resolve_protocol(self.protocol, &self.model, &self.config)?;
let path =
crate::tokenization::count_tokens_endpoint(protocol, self.provider, &self.model)?;
self.load_mcp_tools().await?;
let mut messages = self.preprocessed_messages()?;
if let Some(text) = message {
messages.push(Message::user(text));
}
crate::files::preprocess_messages(
&mut messages,
protocol,
self.provider,
&self.config,
&self.connection,
)
.await?;
for m in &mut messages {
for a in &mut m.attachments {
a.load(self.connection.client()).await?;
}
}
let thinking = match &self.thinking {
Some(t) => t.resolve(&self.model)?,
None => None,
};
let tools = self.request_tools()?;
let request = Request {
provider: self.provider,
config: &self.config,
model: &self.model,
messages: &messages,
tools: &tools,
tool_prefs: &self.tool_prefs,
temperature: None,
max_output_tokens: None,
schema: self.schema.as_ref(),
thinking: thinking.as_ref(),
citations: self.citations,
caching: self.caching.as_ref(),
stream: false,
};
let mut rendered = protocols::render(protocol, &request)?;
if protocol == ProtocolName::Anthropic {
crate::tool_search::apply(protocol, &mut rendered);
}
let payload = crate::tokenization::count_tokens_payload(protocol, &self.model, rendered);
let raw = self
.connection
.post(&path, &payload, &[], &mut |_| {})
.await
.map_err(|e| e.claim(protocol, self.provider.slug(), &self.model.id, &payload))?;
crate::tokenization::parse_count_tokens(protocol, &raw.body)
}
pub async fn compact(&mut self) -> Result<Message> {
self.raise_if_cancelled()?;
self.raise_if_pending_tool_calls()?;
let event = self.instrumentation_event("compaction.rust_llm", false);
let result = event.instrument(self.compact_inner()).await;
self.finish_completion_event(event, &result);
result
}
async fn compact_inner(&mut self) -> Result<Message> {
let protocol = self
.provider
.resolve_protocol(self.protocol, &self.model, &self.config)?;
if !(protocol == ProtocolName::Responses
&& matches!(self.provider, Provider::OpenAI | Provider::XAI))
{
return Err(Error::Api(
format!(
"{} doesn't support manual compaction",
self.provider.display()
),
None,
));
}
let usage_start = self.usage_entries.len();
let mut reuploaded = false;
loop {
match self.compact_request(protocol).await {
Err(e) => {
if reuploaded || !self.discard_missing_uploads(&e).await {
return Err(e);
}
reuploaded = true;
continue;
}
Ok(mut message) => {
let entry = self.entry(UsageStatus::Succeeded, message.tokens.clone(), None);
self.record_usage(entry).await;
message.usage_entries = self.usage_entries[usage_start..].to_vec();
self.raise_if_cancelled()?;
for cb in &mut self.callbacks.before_message {
cb();
}
self.messages.push(message.clone());
for cb in &mut self.callbacks.after_message {
cb(&message);
}
return Ok(message);
}
}
}
}
async fn compact_request(&mut self, protocol: ProtocolName) -> Result<Message> {
let mut messages = self.preprocessed_messages()?;
crate::files::preprocess_messages(
&mut messages,
protocol,
self.provider,
&self.config,
&self.connection,
)
.await?;
for m in &mut messages {
for a in &mut m.attachments {
a.load(self.connection.client()).await?;
}
}
let mut payload = protocols::responses::render_compaction_payload(
self.provider,
&self.model.id,
&messages,
)?;
self.apply_before_request_hooks(&mut payload);
let mut attempts = 0usize;
let mut retried: Vec<Tokens> = Vec::new();
let mut on_attempt = |previous: Option<&Error>| {
attempts += 1;
if let Some(e) = previous {
retried.push(failure_tokens(e, None));
}
};
let result = match self
.connection
.post(
"responses/compact",
&payload,
&self.headers,
&mut on_attempt,
)
.await
{
Ok(raw) => {
protocols::responses::parse_compaction_response(self.provider, &self.model.id, raw)
}
Err(e) => Err(e.claim(protocol, self.provider.slug(), &self.model.id, &payload)),
};
for tokens in retried {
self.record_usage(self.entry(UsageStatus::Failed, tokens, None))
.await;
}
match result {
Ok(m) => Ok(m),
Err(e) => {
if attempts > 0 {
self.record_usage(self.entry(
UsageStatus::Failed,
failure_tokens(&e, None),
None,
))
.await;
}
Err(e)
}
}
}
fn entry(&self, status: UsageStatus, tokens: Tokens, model: Option<&Model>) -> UsageEntry {
let model = model.unwrap_or(&self.model);
let cost = Cost::new(&tokens, Some(model), Tier::Standard);
UsageEntry {
id: UsageEntry::next_id(),
owner: crate::accounting::usage_owner(),
operation: Operation::Chat,
provider: self.provider.slug().into(),
model: self.model.id.clone(),
status,
tokens,
cost,
}
}
fn pending_tool_response(&self) -> Option<&Message> {
let response = latest_response(self.messages.iter())?;
(response.is_tool_call() && !self.pending_tool_calls(response).is_empty())
.then_some(response)
}
fn pending_tool_calls(&self, response: &Message) -> Vec<ToolCall> {
let answered: Vec<&str> = self
.messages
.iter()
.filter_map(|m| m.tool_call_id.as_deref())
.collect();
response
.tool_calls
.iter()
.flat_map(|c| c.values())
.filter(|c| !answered.contains(&c.id.as_str()))
.cloned()
.collect()
}
fn find_tool(&self, name: &str) -> Option<SharedTool> {
let tools = self.combined_tools().unwrap_or_else(|_| self.tools.clone());
tools.into_iter().find(|t| t.name() == name)
}
fn approval_pending(&self, call: &ToolCall) -> bool {
if call.remote {
return !self.tool_call_decisions.contains_key(&call.id);
}
let Some(tool) = self.find_tool(&call.name) else {
return false;
};
tool.requires_approval() && self.tool_call_approval(&*tool, call).is_none()
}
fn tool_call_approval(&self, tool: &dyn Tool, call: &ToolCall) -> Option<bool> {
match tool.approval(call) {
Some(decision) => decision,
None => self.tool_call_decisions.get(&call.id).copied(),
}
}
pub async fn run_tools(&mut self) -> Result<&mut Self> {
match self.run_pending_tools().await {
Err(Error::Cancelled) => {
self.cancel_tasks().await;
Err(Error::Cancelled)
}
Err(e) => Err(e),
Ok(()) => Ok(self),
}
}
async fn run_pending_tools(&mut self) -> Result<()> {
self.raise_if_cancelled()?;
let Some(response) = self.pending_tool_response().cloned() else {
return Ok(());
};
self.load_mcp_tools().await?;
let mut executable = Vec::new();
for call in self.pending_tool_calls(&response) {
self.raise_if_cancelled()?;
if self.input_pending(&call) {
continue;
}
if call.remote {
if let Some(&approved) = self.tool_call_decisions.get(&call.id) {
let responses = self
.provider
.resolve_protocol(self.protocol, &self.model, &self.config)
.is_ok_and(|p| p == ProtocolName::Responses);
if !responses {
return Err(Error::Api(
format!(
"{} doesn't support remote tool approvals",
self.provider.display()
),
None,
));
}
let mut m = Message::tool_result(
call.id.clone(),
if approved { "Approved" } else { "Denied" },
);
m.raw_content = Some(serde_json::json!([{
"type": "mcp_approval_response", "approval_request_id": call.id, "approve": approved
}]));
self.push_tool_message(m);
}
continue;
}
let tool = self.find_tool(&call.name);
let decision = match &tool {
Some(t) if t.requires_approval() => self.tool_call_approval(&**t, &call),
_ => Some(self.tool_call_decisions.get(&call.id).copied() != Some(false)),
};
match decision {
None => continue,
Some(false) => {
let denied =
ToolResult::error(format!("The user denied the {} tool call.", call.name));
self.push_tool_result(&call, denied);
}
Some(true) if self.concurrency => executable.push((call, tool)),
Some(true) => {
for cb in &mut self.callbacks.before_tool_call {
cb(&call);
}
let result = match self.tool_invocation(tool, &call).await {
Err(Error::McpInputRequired(paused)) => {
self.tool_call_inputs
.insert(call.id.clone(), paused.input.to_h());
continue;
}
Err(Error::McpTask(task)) => {
self.tool_call_inputs.insert(call.id.clone(), task.to_h());
continue;
}
other => other?,
};
self.tool_call_inputs.remove(&call.id);
self.raise_if_cancelled()?;
for cb in &mut self.callbacks.after_tool_result {
cb(&result);
}
self.push_tool_result(&call, result);
}
}
}
if !executable.is_empty() {
self.run_tools_concurrently(executable).await?;
}
if matches!(
self.tool_prefs.choice,
Some(ToolChoice::Required | ToolChoice::Tool(_))
) {
self.tool_prefs.choice = None;
}
Ok(())
}
async fn run_tools_concurrently(
&mut self,
calls: Vec<(ToolCall, Option<SharedTool>)>,
) -> Result<()> {
use futures::stream::{FuturesUnordered, StreamExt};
let mut running = FuturesUnordered::new();
for (call, tool) in calls {
self.raise_if_cancelled()?;
for cb in &mut self.callbacks.before_tool_call {
cb(&call);
}
let invocation = self.tool_invocation(tool, &call);
running.push(async move { (call, invocation.await) });
}
let mut first_error = None;
while let Some((call, result)) = running.next().await {
let result = match result {
Err(Error::McpInputRequired(paused)) => {
self.tool_call_inputs
.insert(call.id.clone(), paused.input.to_h());
continue;
}
Err(Error::McpTask(task)) => {
self.tool_call_inputs.insert(call.id.clone(), task.to_h());
continue;
}
Err(e) => {
first_error.get_or_insert(e);
continue;
}
Ok(result) => result,
};
self.tool_call_inputs.remove(&call.id);
self.raise_if_cancelled()?;
for cb in &mut self.callbacks.after_tool_result {
cb(&result);
}
self.push_tool_result(&call, result);
}
first_error.map_or(Ok(()), Err)
}
fn tool_invocation(
&self,
tool: Option<SharedTool>,
call: &ToolCall,
) -> impl Future<Output = Result<ToolResult>> + Send + 'static {
let unavailable = tool.is_none().then(|| {
let names: Vec<String> = self
.combined_tools()
.unwrap_or_else(|_| self.tools.clone())
.iter()
.map(|t| t.name())
.collect();
format!(
"Model tried to call unavailable tool `{}`. Available tools: {}.",
call.name,
serde_json::to_string(&names).unwrap_or_default()
)
});
let call = call.clone();
let input = self.tool_call_inputs.get(&call.id).cloned();
let cancelled = self.cancelled.clone();
let listener = self.progress_listener(&call);
let event = tool.as_ref().map(|tool| {
crate::instrumentation::Event::start(&self.config, "tool_call.rust_llm", || {
crate::instrumentation::payload([
("provider", self.provider.slug().into()),
("provider_class", self.provider.display().into()),
("model", self.model.id.clone().into()),
(
"tool_call",
serde_json::to_value(&call).unwrap_or(Value::Null),
),
("tool_name", tool.name().into()),
("tool_arguments", Value::Object(call.arguments())),
("tool_call_id", call.id.clone().into()),
])
})
});
let run = self.run_tool(tool, call, input, cancelled, listener, unavailable);
async move {
let Some(mut event) = event else {
return run.await;
};
let result = event.instrument(run).await;
if let Ok(r) = &result {
event.set("result", || r.content.clone().into());
event.set("result_content", || r.content.clone().into());
event.set("result_class", || "ToolResult".into());
}
let failure = result
.as_ref()
.err()
.filter(|e| !matches!(e, Error::McpInputRequired(_) | Error::McpTask(_)));
event.finish(failure);
result
}
}
#[allow(clippy::manual_async_fn)]
fn run_tool(
&self,
tool: Option<SharedTool>,
call: ToolCall,
input: Option<Value>,
cancelled: Arc<AtomicBool>,
listener: Option<crate::progress::Listener>,
unavailable: Option<String>,
) -> impl Future<Output = Result<ToolResult>> + Send + 'static {
async move {
let Some(tool) = tool else {
return Ok(ToolResult::error(unavailable.unwrap_or_default()));
};
let arguments = call.arguments();
if let Some(problem) = validate_arguments(&*tool, &arguments) {
return Ok(ToolResult::error(format!(
"Invalid tool arguments: {problem}"
)));
}
let run = async {
match &input {
Some(input) => tool.resume(input, arguments, &call).await,
None => tool.execute(arguments, &call).await,
}
};
let result =
crate::progress::watch(cancelled.clone(), crate::progress::listen(listener, run))
.await;
result.map_err(|e| match e.downcast::<Error>() {
Ok(e)
if matches!(
*e,
Error::Cancelled
| Error::Mcp(_)
| Error::McpInputRequired(_)
| Error::McpTask(_)
) =>
{
if matches!(*e, Error::Cancelled) {
cancelled.store(false, Ordering::SeqCst);
}
*e
}
Ok(e) => Error::Tool(e.to_string()),
Err(e) => Error::Tool(e.to_string()),
})
}
}
fn progress_listener(&self, call: &ToolCall) -> Option<crate::progress::Listener> {
let callbacks = self.callbacks.after_tool_progress.clone();
if callbacks.lock().map(|c| c.is_empty()).unwrap_or(true) {
return None;
}
let call = call.clone();
Some(Arc::new(move |progress: &crate::progress::Progress| {
if let Ok(mut callbacks) = callbacks.lock() {
for cb in callbacks.iter_mut() {
cb(&call, progress);
}
}
}))
}
fn push_tool_result(&mut self, call: &ToolCall, result: ToolResult) {
let mut m = Message::tool_result(call.id.clone(), result.content);
m.attachments = result.attachments;
m.mcp_result = result.mcp_result.filter(|r| r.ui_uri.is_some());
self.push_tool_message(m);
}
fn push_tool_message(&mut self, m: Message) {
for cb in &mut self.callbacks.before_message {
cb();
}
self.messages.push(m.clone());
for cb in &mut self.callbacks.after_message {
cb(&m);
}
}
pub fn approve(&mut self, tool_call_id: &str) -> &mut Self {
self.tool_call_decisions.insert(tool_call_id.into(), true);
self
}
pub fn deny(&mut self, tool_call_id: &str) -> &mut Self {
self.tool_call_decisions.insert(tool_call_id.into(), false);
self
}
pub fn set_decisions(&mut self, decisions: impl IntoIterator<Item = (String, bool)>) {
self.tool_call_decisions.extend(decisions);
}
pub fn is_waiting(&self) -> bool {
self.waiting()
}
fn waiting(&self) -> bool {
let Some(response) = self.pending_tool_response() else {
return false;
};
let pending = self.pending_tool_calls(response);
!pending.is_empty()
&& pending
.iter()
.all(|c| self.approval_pending(c) || self.is_paused(c))
}
fn is_paused(&self, call: &ToolCall) -> bool {
self.input_pending(call)
|| self
.tool_call_inputs
.get(&call.id)
.is_some_and(|state| state.get("task").is_some())
}
fn input_pending(&self, call: &ToolCall) -> bool {
self.tool_call_inputs
.get(&call.id)
.and_then(|input| input.get("requests"))
.and_then(Value::as_array)
.is_some_and(|requests| {
requests
.iter()
.any(|r| r.get("response").is_none_or(Value::is_null))
})
}
pub fn is_awaiting_input(&self) -> bool {
self.waiting() && !self.pending_inputs().is_empty()
}
pub fn is_awaiting_tasks(&self) -> bool {
self.waiting() && !self.pending_tasks().is_empty()
}
pub fn pending_tasks(&self) -> Vec<crate::mcp::Task> {
let Some(response) = self.pending_tool_response() else {
return Vec::new();
};
self.pending_tool_calls(response)
.into_iter()
.filter_map(|call| {
let state = self.tool_call_inputs.get(&call.id)?;
(state.get("task").is_some() && !self.input_pending(&call))
.then(|| self.task_for(&call, state))
})
.collect()
}
fn task_for(&self, call: &ToolCall, state: &Value) -> crate::mcp::Task {
self.find_tool(&call.name)
.and_then(|tool| tool.mcp_task(state, call))
.unwrap_or_else(|| crate::mcp::Task::load(None, state, Some(call.clone())))
}
async fn cancel_tasks(&mut self) {
let Some(response) = self.pending_tool_response().cloned() else {
return;
};
for call in self.pending_tool_calls(&response) {
let Some(state) = self.tool_call_inputs.get(&call.id).cloned() else {
continue;
};
if state.get("task").is_none() {
continue;
}
let mut task = self.task_for(&call, &state);
if let Err(e) = task.cancel().await {
tracing::debug!("RustLLM could not cancel task {}: {e}", task.id);
}
self.tool_call_inputs.remove(&call.id);
}
}
pub fn pending_inputs(&self) -> Vec<crate::mcp::InputRequest> {
let Some(response) = self.pending_tool_response() else {
return Vec::new();
};
self.pending_tool_calls(response)
.into_iter()
.flat_map(|call| {
let requests = self
.tool_call_inputs
.get(&call.id)
.and_then(|i| i.get("requests"))
.and_then(Value::as_array)
.cloned();
requests
.unwrap_or_default()
.iter()
.map(|data| crate::mcp::InputRequest::from_h(data, Some(call.clone())))
.filter(|r| !r.is_answered())
.collect::<Vec<_>>()
})
.collect()
}
pub fn answer(
&mut self,
request: &crate::mcp::InputRequest,
values: Map<String, Value>,
) -> Result<&mut Self> {
self.settle_input(request, |pending| pending.answer(values))
}
pub fn decline(&mut self, request: &crate::mcp::InputRequest) -> Result<&mut Self> {
self.settle_input(request, crate::mcp::InputRequest::decline)
}
fn settle_input(
&mut self,
request: &crate::mcp::InputRequest,
settle: impl FnOnce(&mut crate::mcp::InputRequest),
) -> Result<&mut Self> {
let unknown = || Error::Argument("Unknown input request".into());
let call_id = request
.tool_call
.as_ref()
.map(|c| c.id.clone())
.ok_or_else(unknown)?;
let input = self
.tool_call_inputs
.get_mut(&call_id)
.ok_or_else(unknown)?;
let requests = input
.get_mut("requests")
.and_then(Value::as_array_mut)
.ok_or_else(unknown)?;
let data = requests
.iter_mut()
.find(|d| d.get("key").and_then(Value::as_str) == Some(request.key.as_str()))
.ok_or_else(unknown)?;
let mut pending = crate::mcp::InputRequest::from_h(data, None);
settle(&mut pending);
*data = pending.to_h();
Ok(self)
}
pub fn tool_call_inputs(&self) -> &HashMap<String, Value> {
&self.tool_call_inputs
}
pub fn set_tool_call_inputs(&mut self, inputs: impl IntoIterator<Item = (String, Value)>) {
self.tool_call_inputs.extend(inputs);
}
pub fn is_awaiting_approval(&self) -> bool {
self.waiting() && !self.pending_approvals().is_empty()
}
pub fn pending_approvals(&self) -> Vec<ToolCall> {
let Some(response) = self.pending_tool_response() else {
return Vec::new();
};
self.pending_tool_calls(response)
.into_iter()
.filter(|c| self.approval_pending(c))
.collect()
}
fn raise_if_pending_tool_calls(&self) -> Result<()> {
let Some(response) = self.pending_tool_response() else {
return Ok(());
};
let mut names: Vec<String> = self
.pending_tool_calls(response)
.into_iter()
.map(|c| c.name)
.collect();
names.dedup();
Err(Error::PendingToolCalls(format!(
"The last response has unanswered tool calls ({}). Run complete, recording approve or deny \
decisions for calls that require approval, answering pending inputs, and waiting for tasks, before asking again.",
names.join(", ")
)))
}
fn last_non_system_message(&self) -> Option<&Message> {
self.messages.iter().rev().find(|m| m.role != Role::System)
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
pub fn cancel_handle(&self) -> CancelHandle {
CancelHandle(self.cancelled.clone())
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
pub fn set_cancellation_checker(&mut self, checker: Option<CancellationChecker>) {
self.cancellation_checker = checker;
}
fn raise_if_cancelled(&self) -> Result<()> {
let external = self.cancellation_checker.as_ref().is_some_and(|c| c());
if self.cancelled.swap(false, Ordering::SeqCst) || external {
return Err(Error::Cancelled);
}
Ok(())
}
}
pub(crate) fn failure_tokens(error: &Error, observed: Option<Tokens>) -> Tokens {
if let Some(tokens) = observed {
return tokens;
}
let refused = error
.response()
.is_some_and(|r| (400..500).contains(&r.status));
let never_sent = error.kind() == ErrorKind::ConnectionFailed;
if refused || never_sent {
Tokens {
input: Some(0),
output: Some(0),
..Default::default()
}
} else {
Tokens::default()
}
}
pub const COMPACTION_OPTIONS: &[&str] = &["at", "instructions", "pause_after"];
fn normalize_schema(raw: Value) -> Option<Schema> {
if raw.is_null() {
return None;
}
let obj = raw.as_object().cloned().unwrap_or_default();
let mut definition = obj.get("schema").cloned().unwrap_or_else(|| raw.clone());
if let Some(d) = definition.as_object_mut() {
d.remove("$schema");
d.remove("title");
}
let strict = match obj.get("strict").and_then(Value::as_bool) {
Some(s) => Some(s),
None => definition
.as_object_mut()
.and_then(|d| d.remove("strict"))
.and_then(|v| v.as_bool()),
};
let name = obj
.get("name")
.or_else(|| obj.get("title"))
.and_then(Value::as_str)
.unwrap_or("response")
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect::<String>();
Some(Schema {
name: if name.is_empty() {
"response".into()
} else {
name
},
schema: definition,
strict,
description: obj
.get("description")
.and_then(Value::as_str)
.map(str::to_string),
})
}
const UNFINISHED_TOOL_RESULT: &str = r#"{"error":"The tool call did not finish."}"#;
fn latest_response<'a>(
messages: impl DoubleEndedIterator<Item = &'a Message>,
) -> Option<&'a Message> {
messages
.rev()
.find(|m| m.role != Role::System && !m.is_tool_result() && !is_blank_response(m))
}
fn is_blank_response(m: &Message) -> bool {
m.role == Role::Assistant
&& !m.is_tool_call()
&& m.content().trim().is_empty()
&& m.attachments.is_empty()
&& m.thinking.is_none()
&& m.server_tool_calls.is_empty()
&& m.raw_content.is_none()
&& m.raw_reasoning.is_none()
}