use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ts_rs::TS;
use crate::error::AiMuxError;
use crate::tool::{Tool, ToolCall};
use crate::types::ProviderMetadata;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct RawToolCall {
pub tool_call_id: String,
pub tool_name: String,
pub input: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_executed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dynamic: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_metadata: Option<ProviderMetadata>,
}
#[derive(Debug, Clone)]
pub struct ToolCallRepairContext {
pub instructions: Option<String>,
pub system: Option<String>,
pub messages: Vec<crate::message::ModelMessage>,
pub tool_call: RawToolCall,
pub tools: Vec<Tool>,
pub error: AiMuxError,
}
impl ToolCallRepairContext {
#[must_use]
pub fn input_schema(&self, tool_name: &str) -> Value {
self.tools
.iter()
.find_map(|tool| match tool {
Tool::Function(tool) if tool.name == tool_name => Some(tool.input_schema.clone()),
_ => None,
})
.unwrap_or_else(|| {
serde_json::json!({
"type": "object",
"properties": {},
"additionalProperties": false
})
})
}
}
type ToolCallRepairFuture =
Pin<Box<dyn Future<Output = Result<Option<RawToolCall>, AiMuxError>> + Send>>;
#[derive(Clone)]
pub struct ToolCallRepair(Arc<dyn Fn(ToolCallRepairContext) -> ToolCallRepairFuture + Send + Sync>);
impl ToolCallRepair {
#[must_use]
pub fn new<F, Fut>(repair: F) -> Self
where
F: Fn(ToolCallRepairContext) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<RawToolCall>, AiMuxError>> + Send + 'static,
{
Self(Arc::new(move |context| Box::pin(repair(context))))
}
async fn repair(
&self,
context: ToolCallRepairContext,
) -> Result<Option<RawToolCall>, AiMuxError> {
(self.0)(context).await
}
}
impl std::fmt::Debug for ToolCallRepair {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("ToolCallRepair(<callback>)")
}
}
pub async fn parse_tool_call(
tool_call: RawToolCall,
tools: Option<&[Tool]>,
repair_tool_call: Option<&ToolCallRepair>,
messages: &[crate::message::ModelMessage],
instructions: Option<&str>,
) -> ToolCall {
let Some(tools) = tools else {
let parsed = if tool_call.provider_executed == Some(true) && tool_call.dynamic == Some(true)
{
parse_json_input(&tool_call)
} else {
Err(AiMuxError::NoSuchTool {
tool_name: tool_call.tool_name.clone(),
available_tools: None,
tool_input: Some(tool_call.input.clone()),
})
};
return match parsed {
Ok(input) => valid_tool_call(tool_call, input, Some(true)),
Err(error) => invalid_tool_call(tool_call, error),
};
};
match parse_and_validate_tool_call(&tool_call, tools) {
Ok((input, dynamic)) => valid_tool_call(tool_call, input, dynamic),
Err(original_error) => {
let Some(repair_tool_call) = repair_tool_call else {
return invalid_tool_call(tool_call, original_error);
};
let context = ToolCallRepairContext {
instructions: instructions.map(str::to_owned),
system: instructions.map(str::to_owned),
messages: messages.to_vec(),
tool_call: tool_call.clone(),
tools: tools.to_vec(),
error: original_error.clone(),
};
let outcome = match repair_tool_call.repair(context).await {
Ok(Some(repaired)) => RepairOutcome::Repaired(repaired),
Ok(None) => RepairOutcome::Unchanged,
Err(repair_error) => RepairOutcome::Failed(repair_error),
};
apply_repair_outcome(tool_call, original_error, tools, outcome)
}
}
}
enum RepairOutcome {
Repaired(RawToolCall),
Unchanged,
Failed(AiMuxError),
}
fn apply_repair_outcome(
tool_call: RawToolCall,
original_error: AiMuxError,
tools: &[Tool],
outcome: RepairOutcome,
) -> ToolCall {
let cause = match outcome {
RepairOutcome::Repaired(repaired) => match parse_and_validate_tool_call(&repaired, tools) {
Ok((input, dynamic)) => return valid_tool_call(repaired, input, dynamic),
Err(repaired_error) => repaired_error,
},
RepairOutcome::Unchanged => return invalid_tool_call(tool_call, original_error),
RepairOutcome::Failed(repair_error) => repair_error,
};
invalid_tool_call(
tool_call,
AiMuxError::ToolCallRepair {
original_error: Box::new(original_error),
cause: Box::new(cause),
},
)
}
fn parse_and_validate_tool_call(
tool_call: &RawToolCall,
tools: &[Tool],
) -> Result<(Value, Option<bool>), AiMuxError> {
let tool = tools.iter().find(|tool| match tool {
Tool::Function(tool) => tool.name == tool_call.tool_name,
Tool::Provider(tool) => tool.name == tool_call.tool_name,
});
let provider_dynamic =
tool_call.provider_executed == Some(true) && tool_call.dynamic == Some(true);
let Some(tool) = tool else {
if provider_dynamic {
return parse_json_input(tool_call).map(|input| (input, Some(true)));
}
return Err(AiMuxError::NoSuchTool {
tool_name: tool_call.tool_name.clone(),
tool_input: Some(tool_call.input.clone()),
available_tools: Some(
tools
.iter()
.map(|tool| match tool {
Tool::Function(tool) => tool.name.clone(),
Tool::Provider(tool) => tool.name.clone(),
})
.collect(),
),
});
};
let input = parse_json_input(tool_call)?;
let Tool::Function(function_tool) = tool else {
return Ok((input, None));
};
let validator = jsonschema::validator_for(&function_tool.input_schema).map_err(|error| {
AiMuxError::InvalidToolInput {
tool_name: tool_call.tool_name.clone(),
tool_input: tool_call.input.clone(),
cause: format!("input schema is invalid: {error}"),
}
})?;
validator
.validate(&input)
.map_err(|error| AiMuxError::InvalidToolInput {
tool_name: tool_call.tool_name.clone(),
tool_input: tool_call.input.clone(),
cause: format!("Type validation failed: Value: {input}.\nError message: {error}"),
})?;
Ok((input, None))
}
fn parse_json_input(tool_call: &RawToolCall) -> Result<Value, AiMuxError> {
if tool_call.input.trim().is_empty() {
return Ok(Value::Object(serde_json::Map::new()));
}
let value: Value =
serde_json::from_str(&tool_call.input).map_err(|error| AiMuxError::InvalidToolInput {
tool_name: tool_call.tool_name.clone(),
tool_input: tool_call.input.clone(),
cause: format!(
"JSON parsing failed: Text: {}.\nError message: {error}",
tool_call.input
),
})?;
if contains_forbidden_prototype(&value) {
return Err(AiMuxError::InvalidToolInput {
tool_name: tool_call.tool_name.clone(),
tool_input: tool_call.input.clone(),
cause: format!(
"JSON parsing failed: Text: {}.\nError message: Object contains forbidden prototype property",
tool_call.input
),
});
}
Ok(value)
}
fn contains_forbidden_prototype(value: &Value) -> bool {
match value {
Value::Object(map) => {
let constructor_prototype = map
.get("constructor")
.and_then(Value::as_object)
.is_some_and(|constructor| constructor.contains_key("prototype"));
if map.contains_key("__proto__") || constructor_prototype {
return true;
}
map.values().any(contains_forbidden_prototype)
}
Value::Array(items) => items.iter().any(contains_forbidden_prototype),
_ => false,
}
}
pub(crate) fn raw_tool_input(input: &Value) -> String {
match input {
Value::String(input) => input.clone(),
input => serde_json::to_string(input).expect("serializing serde_json::Value cannot fail"),
}
}
fn valid_tool_call(tool_call: RawToolCall, input: Value, dynamic: Option<bool>) -> ToolCall {
ToolCall {
tool_call_id: tool_call.tool_call_id,
tool_name: tool_call.tool_name,
input,
provider_executed: tool_call.provider_executed,
dynamic,
thought_signature: tool_call.thought_signature,
provider_metadata: tool_call.provider_metadata,
invalid: None,
error: None,
}
}
fn invalid_tool_call(tool_call: RawToolCall, error: AiMuxError) -> ToolCall {
let input = serde_json::from_str(&tool_call.input)
.unwrap_or_else(|_| Value::String(tool_call.input.clone()));
ToolCall {
tool_call_id: tool_call.tool_call_id,
tool_name: tool_call.tool_name,
input,
provider_executed: tool_call.provider_executed,
dynamic: Some(true),
thought_signature: tool_call.thought_signature,
provider_metadata: tool_call.provider_metadata,
invalid: Some(true),
error: Some(error),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
#[ts(export)]
pub enum ToolCallRepairReply {
Repaired { tool_call: RawToolCall },
Unchanged,
Failed { message: String },
}
impl From<ToolCallRepairReply> for RepairOutcome {
fn from(reply: ToolCallRepairReply) -> Self {
match reply {
ToolCallRepairReply::Repaired { tool_call } => RepairOutcome::Repaired(tool_call),
ToolCallRepairReply::Unchanged => RepairOutcome::Unchanged,
ToolCallRepairReply::Failed { message } => {
RepairOutcome::Failed(AiMuxError::Other(message))
}
}
}
}
pub(crate) fn raw_tool_call_text(error: &AiMuxError) -> Option<String> {
match error {
AiMuxError::InvalidToolInput { tool_input, .. } => Some(tool_input.clone()),
AiMuxError::NoSuchTool { tool_input, .. } => tool_input.clone(),
AiMuxError::ToolCallRepair { original_error, .. } => raw_tool_call_text(original_error),
_ => None,
}
}
fn raw_from_invalid(tool_call: &ToolCall) -> Result<(RawToolCall, AiMuxError), AiMuxError> {
if tool_call.invalid != Some(true) {
return Err(AiMuxError::InvalidArgument(format!(
"tool call '{}' is not invalid; nothing to repair",
tool_call.tool_call_id
)));
}
let error = tool_call.error.clone().ok_or_else(|| {
AiMuxError::InvalidArgument(format!(
"invalid tool call '{}' carries no error",
tool_call.tool_call_id
))
})?;
Ok((
RawToolCall {
tool_call_id: tool_call.tool_call_id.clone(),
tool_name: tool_call.tool_name.clone(),
input: raw_tool_call_text(&error).unwrap_or_else(|| raw_tool_input(&tool_call.input)),
provider_executed: tool_call.provider_executed,
dynamic: tool_call.dynamic,
thought_signature: tool_call.thought_signature.clone(),
provider_metadata: tool_call.provider_metadata.clone(),
},
error,
))
}
fn required_tools(tools: Option<&[Tool]>) -> Result<&[Tool], AiMuxError> {
tools.ok_or_else(|| {
AiMuxError::InvalidArgument(
"tool calls made without a tool set are not repairable".to_string(),
)
})
}
#[must_use]
pub fn tool_call_repair_inputs(
prompt: impl Into<crate::message::ModelPrompt>,
options: &crate::generate::GenerateTextOptions,
) -> (
Vec<crate::message::ModelMessage>,
Option<&str>,
Option<&[Tool]>,
) {
let (messages, instructions) =
crate::generate::split_prompt(prompt.into(), options.instructions.as_deref());
(messages, instructions, options.tools.as_deref())
}
pub fn tool_call_repair_context(
tool_call: &ToolCall,
tools: Option<&[Tool]>,
messages: &[crate::message::ModelMessage],
instructions: Option<&str>,
) -> Result<Option<Value>, AiMuxError> {
let Some(tools) = tools else {
return Ok(None);
};
let (raw, error) = raw_from_invalid(tool_call)?;
let context = ToolCallRepairContext {
instructions: instructions.map(str::to_owned),
system: instructions.map(str::to_owned),
messages: messages.to_vec(),
tool_call: raw,
tools: tools.to_vec(),
error,
};
Ok(Some(serde_json::json!({
"tool_call": context.tool_call,
"error": context.error,
"input_schema": context.input_schema(&context.tool_call.tool_name),
"tools": context.tools,
"messages": context.messages,
"instructions": context.instructions,
})))
}
pub fn apply_tool_call_repair(
tool_call: &ToolCall,
tools: Option<&[Tool]>,
reply: ToolCallRepairReply,
) -> Result<ToolCall, AiMuxError> {
let tools = required_tools(tools)?;
let (raw, error) = raw_from_invalid(tool_call)?;
Ok(apply_repair_outcome(raw, error, tools, reply.into()))
}
pub fn apply_tool_call_repair_to_result(
result: &Value,
tools: Option<&[Tool]>,
tool_call_id: &str,
reply: ToolCallRepairReply,
) -> Result<Value, AiMuxError> {
let mut patched = result.clone();
let target = if patched.get("tool_calls").is_some() {
&mut patched
} else {
patched.get_mut("raw").ok_or_else(|| {
AiMuxError::InvalidArgument("result: neither tool_calls nor raw".to_string())
})?
};
let calls = target
.get_mut("tool_calls")
.and_then(Value::as_array_mut)
.ok_or_else(|| AiMuxError::InvalidArgument("result: no tool_calls array".to_string()))?;
let target_index = calls
.iter()
.position(|call| call.get("tool_call_id").and_then(Value::as_str) == Some(tool_call_id))
.ok_or_else(|| {
AiMuxError::InvalidArgument(format!("result: no tool call '{tool_call_id}'"))
})?;
if calls[target_index + 1..]
.iter()
.any(|call| call.get("tool_call_id").and_then(Value::as_str) == Some(tool_call_id))
{
return Err(AiMuxError::InvalidArgument(format!(
"result: tool call id '{tool_call_id}' is not unique"
)));
}
let original: ToolCall = serde_json::from_value(calls[target_index].clone())
.map_err(|error| AiMuxError::InvalidArgument(format!("result.tool_calls: {error}")))?;
let repaired = apply_tool_call_repair(&original, tools, reply)?;
if repaired.tool_call_id != tool_call_id
&& calls.iter().enumerate().any(|(index, call)| {
index != target_index
&& call.get("tool_call_id").and_then(Value::as_str)
== Some(repaired.tool_call_id.as_str())
})
{
return Err(AiMuxError::InvalidArgument(format!(
"result: repaired tool call id '{}' already exists",
repaired.tool_call_id
)));
}
calls[target_index] = serde_json::to_value(&repaired)
.map_err(|error| AiMuxError::InvalidArgument(format!("tool call: {error}")))?;
let replay_input =
crate::response_messages::response_tool_call_input(&repaired.input, repaired.invalid);
for part in target
.get_mut("response_messages")
.and_then(Value::as_array_mut)
.into_iter()
.flatten()
.filter_map(|message| message.get_mut("content")?.as_array_mut())
.flatten()
{
if part.get("type").and_then(Value::as_str) != Some("tool_call")
|| part.get("tool_call_id").and_then(Value::as_str) != Some(tool_call_id)
{
continue;
}
let Some(part) = part.as_object_mut() else {
continue;
};
part.insert(
"tool_call_id".to_string(),
repaired.tool_call_id.clone().into(),
);
part.insert("tool_name".to_string(), repaired.tool_name.clone().into());
part.insert("input".to_string(), replay_input.clone());
}
Ok(patched)
}