use serde::de::{self, Deserializer};
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
use crate::types::cache::CacheControl;
use crate::types::content::ContentBlockParam;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
impl Tool {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
input_schema: serde_json::Value,
) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
input_schema,
strict: None,
cache_control: None,
}
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = Some(strict);
self
}
pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
self.cache_control = Some(cache_control);
self
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolUnion {
Custom(Tool),
Raw(serde_json::Value),
}
impl From<Tool> for ToolUnion {
fn from(tool: Tool) -> Self {
ToolUnion::Custom(tool)
}
}
impl From<serde_json::Value> for ToolUnion {
fn from(value: serde_json::Value) -> Self {
ToolUnion::Raw(value)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum ToolChoice {
Auto {
disable_parallel_tool_use: Option<bool>,
},
Any {
disable_parallel_tool_use: Option<bool>,
},
None {},
Tool {
name: String,
disable_parallel_tool_use: Option<bool>,
},
Unknown(serde_json::Value),
}
impl Serialize for ToolChoice {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
ToolChoice::Auto {
disable_parallel_tool_use,
}
| ToolChoice::Any {
disable_parallel_tool_use,
} => {
let tag = if matches!(self, ToolChoice::Auto { .. }) {
"auto"
} else {
"any"
};
let len = 1 + usize::from(disable_parallel_tool_use.is_some());
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("type", tag)?;
if let Some(flag) = disable_parallel_tool_use {
map.serialize_entry("disable_parallel_tool_use", flag)?;
}
map.end()
}
ToolChoice::None {} => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("type", "none")?;
map.end()
}
ToolChoice::Tool {
name,
disable_parallel_tool_use,
} => {
let len = 2 + usize::from(disable_parallel_tool_use.is_some());
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("type", "tool")?;
map.serialize_entry("name", name)?;
if let Some(flag) = disable_parallel_tool_use {
map.serialize_entry("disable_parallel_tool_use", flag)?;
}
map.end()
}
ToolChoice::Unknown(value) => value.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for ToolChoice {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let tag = value
.get("type")
.and_then(|t| t.as_str())
.map(str::to_owned);
#[derive(Deserialize)]
struct Parallel {
#[serde(default)]
disable_parallel_tool_use: Option<bool>,
}
#[derive(Deserialize)]
struct ToolFields {
name: String,
#[serde(default)]
disable_parallel_tool_use: Option<bool>,
}
let choice = match tag.as_deref() {
Some("auto") => {
let r: Parallel = serde_json::from_value(value).map_err(de::Error::custom)?;
ToolChoice::Auto {
disable_parallel_tool_use: r.disable_parallel_tool_use,
}
}
Some("any") => {
let r: Parallel = serde_json::from_value(value).map_err(de::Error::custom)?;
ToolChoice::Any {
disable_parallel_tool_use: r.disable_parallel_tool_use,
}
}
Some("none") => ToolChoice::None {},
Some("tool") => {
let r: ToolFields = serde_json::from_value(value).map_err(de::Error::custom)?;
ToolChoice::Tool {
name: r.name,
disable_parallel_tool_use: r.disable_parallel_tool_use,
}
}
_ => ToolChoice::Unknown(value),
};
Ok(choice)
}
}
impl ToolChoice {
pub fn auto() -> Self {
ToolChoice::Auto {
disable_parallel_tool_use: None,
}
}
pub fn any() -> Self {
ToolChoice::Any {
disable_parallel_tool_use: None,
}
}
pub fn none() -> Self {
ToolChoice::None {}
}
pub fn tool(name: impl Into<String>) -> Self {
ToolChoice::Tool {
name: name.into(),
disable_parallel_tool_use: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Text(String),
Blocks(Vec<ContentBlockParam>),
}
impl From<String> for ToolResultContent {
fn from(s: String) -> Self {
ToolResultContent::Text(s)
}
}
impl From<&str> for ToolResultContent {
fn from(s: &str) -> Self {
ToolResultContent::Text(s.to_string())
}
}
impl From<Vec<ContentBlockParam>> for ToolResultContent {
fn from(blocks: Vec<ContentBlockParam>) -> Self {
ToolResultContent::Blocks(blocks)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rt<T>(json: serde_json::Value) -> serde_json::Value
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
let parsed: T = serde_json::from_value(json).expect("deserialize");
let out = serde_json::to_value(&parsed).expect("serialize");
let reparsed: T = serde_json::from_value(out.clone()).expect("re-deserialize");
assert_eq!(parsed, reparsed, "struct round-trip mismatch");
out
}
#[test]
fn custom_tool_definition_round_trips() {
let j = serde_json::json!({
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
},
"strict": true,
"cache_control": {"type": "ephemeral"}
});
assert_eq!(rt::<Tool>(j.clone()), j);
}
#[test]
fn tool_union_custom() {
let j = serde_json::json!({
"name": "noop",
"description": "does nothing",
"input_schema": {"type": "object"}
});
let parsed: ToolUnion = serde_json::from_value(j.clone()).unwrap();
assert!(matches!(parsed, ToolUnion::Custom(_)));
assert_eq!(serde_json::to_value(&parsed).unwrap(), j);
}
#[test]
fn tool_union_server_tool_is_raw() {
let j = serde_json::json!({"type": "web_search_20260209", "name": "web_search"});
let parsed: ToolUnion = serde_json::from_value(j.clone()).unwrap();
assert!(matches!(parsed, ToolUnion::Raw(_)));
assert_eq!(serde_json::to_value(&parsed).unwrap(), j);
}
#[test]
fn tool_choice_variants_round_trip() {
for j in [
serde_json::json!({"type": "auto"}),
serde_json::json!({"type": "any"}),
serde_json::json!({"type": "none"}),
serde_json::json!({"type": "tool", "name": "get_weather"}),
serde_json::json!({"type": "auto", "disable_parallel_tool_use": true}),
] {
assert_eq!(rt::<ToolChoice>(j.clone()), j);
}
}
#[test]
fn unknown_tool_choice_is_preserved() {
let j = serde_json::json!({"type": "future_choice", "name": "x"});
let parsed: ToolChoice = serde_json::from_value(j.clone()).unwrap();
assert!(matches!(parsed, ToolChoice::Unknown(_)));
assert_eq!(serde_json::to_value(&parsed).unwrap(), j);
}
#[test]
fn tool_result_content_string_and_blocks() {
let s = serde_json::json!("done");
assert_eq!(rt::<ToolResultContent>(s.clone()), s);
let blocks = serde_json::json!([{"type": "text", "text": "done"}]);
assert_eq!(rt::<ToolResultContent>(blocks.clone()), blocks);
}
}