1use schemars::JsonSchema;
2use serde::Deserialize;
3use serde::Deserializer;
4use serde::Serialize;
5use serde::de::Error as _;
6use serde_json::Value as JsonValue;
7use std::collections::HashMap;
8use ts_rs::TS;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
11#[serde(tag = "type", rename_all = "camelCase")]
12#[ts(tag = "type", export_to = "v2/")]
13pub enum DynamicToolSpec {
14 Function(DynamicToolFunctionSpec),
15 Namespace(DynamicToolNamespaceSpec),
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
19#[serde(rename_all = "camelCase")]
20#[ts(export_to = "v2/")]
21pub struct DynamicToolFunctionSpec {
22 pub name: String,
23 pub description: String,
24 pub input_schema: JsonValue,
25 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
26 pub defer_loading: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
30#[serde(rename_all = "camelCase")]
31#[ts(export_to = "v2/")]
32pub struct DynamicToolNamespaceSpec {
33 pub name: String,
34 pub description: String,
35 pub tools: Vec<DynamicToolNamespaceTool>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
39#[serde(tag = "type", rename_all = "camelCase")]
40#[ts(tag = "type", export_to = "v2/")]
41pub enum DynamicToolNamespaceTool {
42 Function(DynamicToolFunctionSpec),
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
46#[serde(rename_all = "camelCase")]
47pub struct DynamicToolCallRequest {
48 pub call_id: String,
49 pub turn_id: String,
50 #[serde(default)]
51 pub started_at_ms: i64,
52 #[serde(default)]
53 pub namespace: Option<String>,
54 pub tool: String,
55 pub arguments: JsonValue,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
59#[serde(rename_all = "camelCase")]
60pub struct DynamicToolResponse {
61 pub content_items: Vec<DynamicToolCallOutputContentItem>,
62 pub success: bool,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
66#[serde(tag = "type", rename_all = "camelCase")]
67#[ts(tag = "type")]
68pub enum DynamicToolCallOutputContentItem {
69 #[serde(rename_all = "camelCase")]
70 InputText { text: String },
71 #[serde(rename_all = "camelCase")]
72 InputImage { image_url: String },
73 #[serde(rename_all = "camelCase")]
74 InputAudio { audio_url: String },
75}
76
77#[derive(Deserialize)]
80#[serde(rename_all = "camelCase")]
81struct LegacyDynamicToolSpec {
82 namespace: Option<String>,
83 name: String,
84 description: String,
85 input_schema: JsonValue,
86 defer_loading: Option<bool>,
87 expose_to_context: Option<bool>,
88}
89
90pub fn normalize_dynamic_tool_specs(
91 values: Vec<JsonValue>,
92) -> Result<Vec<DynamicToolSpec>, serde_json::Error> {
93 let has_legacy_fields = |value: &JsonValue| {
94 value.get("namespace").is_some()
95 || value.get("exposeToContext").is_some()
96 || value.get("type").is_none()
97 };
98 let has_legacy_format = values.iter().any(|value| {
99 has_legacy_fields(value)
100 || value
101 .get("tools")
102 .and_then(JsonValue::as_array)
103 .is_some_and(|tools| tools.iter().any(&has_legacy_fields))
104 });
105 let has_canonical_format = values.iter().any(|value| value.get("type").is_some());
106 if has_legacy_format && has_canonical_format {
107 return Err(serde_json::Error::custom(
108 "dynamic tools must use either canonical or legacy format consistently",
109 ));
110 }
111 if !has_legacy_format {
112 return values.into_iter().map(serde_json::from_value).collect();
113 }
114
115 let tools = values
116 .into_iter()
117 .map(|value| {
118 let tool: LegacyDynamicToolSpec = serde_json::from_value(value)?;
119 let function = DynamicToolFunctionSpec {
120 name: tool.name,
121 description: tool.description,
122 input_schema: tool.input_schema,
123 defer_loading: tool.defer_loading.unwrap_or_else(|| {
124 tool.expose_to_context
125 .map(|visible| !visible)
126 .unwrap_or(false)
127 }),
128 };
129 Ok((tool.namespace, function))
130 })
131 .collect::<Result<Vec<_>, serde_json::Error>>()?;
132 Ok(group_dynamic_tools_by_namespace(tools))
133}
134
135pub fn group_dynamic_tools_by_namespace(
136 tools: Vec<(Option<String>, DynamicToolFunctionSpec)>,
137) -> Vec<DynamicToolSpec> {
138 let mut grouped_tools = Vec::with_capacity(tools.len());
139 let mut namespace_indices = HashMap::<String, usize>::new();
140 for (namespace, function) in tools {
141 let Some(namespace) = namespace else {
142 grouped_tools.push(DynamicToolSpec::Function(function));
143 continue;
144 };
145 let function = DynamicToolNamespaceTool::Function(function);
146 if let Some(index) = namespace_indices.get(&namespace).copied() {
147 let DynamicToolSpec::Namespace(namespace) = &mut grouped_tools[index] else {
148 unreachable!("namespace index must point to a namespace");
149 };
150 namespace.tools.push(function);
151 continue;
152 }
153 namespace_indices.insert(namespace.clone(), grouped_tools.len());
154 grouped_tools.push(DynamicToolSpec::Namespace(DynamicToolNamespaceSpec {
155 name: namespace,
156 description: String::new(),
157 tools: vec![function],
158 }));
159 }
160 grouped_tools
161}
162
163pub fn deserialize_dynamic_tool_specs<'de, D>(
164 deserializer: D,
165) -> Result<Option<Vec<DynamicToolSpec>>, D::Error>
166where
167 D: Deserializer<'de>,
168{
169 let Some(values) = Option::<Vec<JsonValue>>::deserialize(deserializer)? else {
170 return Ok(None);
171 };
172 normalize_dynamic_tool_specs(values)
173 .map(Some)
174 .map_err(D::Error::custom)
175}