1use serde::{Deserialize, Serialize};
47
48use crate::messages::cache::CacheControl;
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56#[serde(untagged)]
57pub enum Tool {
58 Custom(CustomTool),
60 Builtin(BuiltinTool),
64}
65
66impl Tool {
67 pub fn custom(name: impl Into<String>, input_schema: serde_json::Value) -> Self {
69 Self::Custom(CustomTool {
70 name: name.into(),
71 description: None,
72 input_schema,
73 cache_control: None,
74 })
75 }
76
77 pub fn builtin(value: serde_json::Value) -> Self {
92 Self::Builtin(BuiltinTool::Other(value))
93 }
94
95 #[must_use]
97 pub fn web_search() -> Self {
98 Self::Builtin(BuiltinTool::Known(KnownBuiltinTool::WebSearch20250305 {
99 name: "web_search".into(),
100 max_uses: None,
101 allowed_domains: None,
102 blocked_domains: None,
103 user_location: None,
104 cache_control: None,
105 }))
106 }
107
108 #[must_use]
111 pub fn computer(display_width_px: u32, display_height_px: u32) -> Self {
112 Self::Builtin(BuiltinTool::Known(KnownBuiltinTool::Computer20250124 {
113 name: "computer".into(),
114 display_width_px,
115 display_height_px,
116 display_number: None,
117 cache_control: None,
118 }))
119 }
120
121 #[must_use]
123 pub fn bash() -> Self {
124 Self::Builtin(BuiltinTool::Known(KnownBuiltinTool::Bash20250124 {
125 name: "bash".into(),
126 cache_control: None,
127 }))
128 }
129
130 #[must_use]
132 pub fn text_editor() -> Self {
133 Self::Builtin(BuiltinTool::Known(KnownBuiltinTool::TextEditor20250124 {
134 name: "str_replace_editor".into(),
135 cache_control: None,
136 }))
137 }
138
139 #[must_use]
141 pub fn code_execution() -> Self {
142 Self::Builtin(BuiltinTool::Known(
143 KnownBuiltinTool::CodeExecution20250825 {
144 name: "code_execution".into(),
145 cache_control: None,
146 },
147 ))
148 }
149
150 #[cfg(feature = "schemars-tools")]
158 #[cfg_attr(docsrs, doc(cfg(feature = "schemars-tools")))]
159 pub fn from_schemars<T: schemars::JsonSchema>(name: impl Into<String>) -> Self {
160 let schema = schemars::r#gen::SchemaGenerator::default().into_root_schema_for::<T>();
161 let schema_value =
162 serde_json::to_value(schema).expect("RootSchema is always JSON-serializable");
163 Self::Custom(CustomTool {
164 name: name.into(),
165 description: None,
166 input_schema: schema_value,
167 cache_control: None,
168 })
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174#[non_exhaustive]
175pub struct CustomTool {
176 pub name: String,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub description: Option<String>,
181 pub input_schema: serde_json::Value,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub cache_control: Option<CacheControl>,
186}
187
188impl CustomTool {
189 pub fn new(name: impl Into<String>, input_schema: serde_json::Value) -> Self {
191 Self {
192 name: name.into(),
193 description: None,
194 input_schema,
195 cache_control: None,
196 }
197 }
198
199 #[must_use]
201 pub fn description(mut self, description: impl Into<String>) -> Self {
202 self.description = Some(description.into());
203 self
204 }
205
206 #[must_use]
208 pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
209 self.cache_control = Some(cache_control);
210 self
211 }
212
213 #[must_use]
216 pub fn with_ephemeral_cache(self) -> Self {
217 self.cache_control(CacheControl::ephemeral())
218 }
219}
220
221#[derive(Debug, Clone, PartialEq)]
229pub enum BuiltinTool {
230 Known(KnownBuiltinTool),
232 Other(serde_json::Value),
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241#[serde(tag = "type")]
242#[non_exhaustive]
243pub enum KnownBuiltinTool {
244 #[serde(rename = "web_search_20250305")]
249 WebSearch20250305 {
250 name: String,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 max_uses: Option<u32>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
257 allowed_domains: Option<Vec<String>>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
260 blocked_domains: Option<Vec<String>>,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
263 user_location: Option<UserLocation>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
266 cache_control: Option<CacheControl>,
267 },
268 #[serde(rename = "computer_20250124")]
271 Computer20250124 {
272 name: String,
274 display_width_px: u32,
276 display_height_px: u32,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
280 display_number: Option<u32>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
283 cache_control: Option<CacheControl>,
284 },
285 #[serde(rename = "bash_20250124")]
287 Bash20250124 {
288 name: String,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
292 cache_control: Option<CacheControl>,
293 },
294 #[serde(rename = "text_editor_20250124")]
296 TextEditor20250124 {
297 name: String,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
301 cache_control: Option<CacheControl>,
302 },
303 #[serde(rename = "code_execution_20250825")]
306 CodeExecution20250825 {
307 name: String,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 cache_control: Option<CacheControl>,
312 },
313}
314
315const KNOWN_BUILTIN_TAGS: &[&str] = &[
316 "web_search_20250305",
317 "computer_20250124",
318 "bash_20250124",
319 "text_editor_20250124",
320 "code_execution_20250825",
321];
322
323impl serde::Serialize for BuiltinTool {
324 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
325 match self {
326 BuiltinTool::Known(k) => k.serialize(s),
327 BuiltinTool::Other(v) => v.serialize(s),
328 }
329 }
330}
331
332impl<'de> serde::Deserialize<'de> for BuiltinTool {
333 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
334 let raw = serde_json::Value::deserialize(d)?;
335 crate::forward_compat::dispatch_known_or_other(
336 raw,
337 KNOWN_BUILTIN_TAGS,
338 BuiltinTool::Known,
339 BuiltinTool::Other,
340 )
341 .map_err(serde::de::Error::custom)
342 }
343}
344
345impl From<KnownBuiltinTool> for BuiltinTool {
346 fn from(k: KnownBuiltinTool) -> Self {
347 BuiltinTool::Known(k)
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353#[non_exhaustive]
354pub struct UserLocation {
355 #[serde(rename = "type", default = "default_user_location_kind")]
357 pub kind: String,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub city: Option<String>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub region: Option<String>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub country: Option<String>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub timezone: Option<String>,
370}
371
372fn default_user_location_kind() -> String {
373 "approximate".to_owned()
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
378#[serde(tag = "type", rename_all = "snake_case")]
379#[non_exhaustive]
380pub enum ToolChoice {
381 Auto {
383 #[serde(default, skip_serializing_if = "Option::is_none")]
385 disable_parallel_tool_use: Option<bool>,
386 },
387 Any {
389 #[serde(default, skip_serializing_if = "Option::is_none")]
391 disable_parallel_tool_use: Option<bool>,
392 },
393 Tool {
395 name: String,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
399 disable_parallel_tool_use: Option<bool>,
400 },
401 None,
403}
404
405impl ToolChoice {
406 #[must_use]
408 pub fn auto() -> Self {
409 Self::Auto {
410 disable_parallel_tool_use: None,
411 }
412 }
413
414 #[must_use]
416 pub fn any() -> Self {
417 Self::Any {
418 disable_parallel_tool_use: None,
419 }
420 }
421
422 #[must_use]
424 pub fn tool(name: impl Into<String>) -> Self {
425 Self::Tool {
426 name: name.into(),
427 disable_parallel_tool_use: None,
428 }
429 }
430
431 #[must_use]
433 pub fn none() -> Self {
434 Self::None
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use pretty_assertions::assert_eq;
442 use serde_json::json;
443
444 #[test]
445 fn custom_tool_round_trips() {
446 let t = Tool::Custom(
447 CustomTool::new(
448 "get_weather",
449 json!({"type": "object", "properties": {"city": {"type": "string"}}}),
450 )
451 .description("Look up the weather"),
452 );
453 let v = serde_json::to_value(&t).unwrap();
454 assert_eq!(
455 v,
456 json!({
457 "name": "get_weather",
458 "description": "Look up the weather",
459 "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}}
460 })
461 );
462 let parsed: Tool = serde_json::from_value(v).unwrap();
463 assert_eq!(parsed, t);
464 }
465
466 #[test]
467 fn custom_tool_with_cache_control_round_trips() {
468 let t = Tool::Custom(
469 CustomTool::new("noop", json!({"type": "object"}))
470 .cache_control(CacheControl::ephemeral()),
471 );
472 let v = serde_json::to_value(&t).unwrap();
473 assert_eq!(
474 v,
475 json!({
476 "name": "noop",
477 "input_schema": {"type": "object"},
478 "cache_control": {"type": "ephemeral"}
479 })
480 );
481 let parsed: Tool = serde_json::from_value(v).unwrap();
482 assert_eq!(parsed, t);
483 }
484
485 #[test]
486 fn unknown_builtin_round_trips_through_other() {
487 let raw = json!({"type": "future_builtin_2099", "name": "future_tool"});
489 let t = Tool::builtin(raw.clone());
490 let serialized = serde_json::to_value(&t).unwrap();
491 assert_eq!(serialized, raw, "Other must serialize transparently");
492 let parsed: Tool = serde_json::from_value(serialized).unwrap();
493 assert_eq!(parsed, t);
494 }
495
496 #[test]
497 fn known_builtin_parses_into_typed_variant() {
498 let raw = json!({
499 "type": "web_search_20250305",
500 "name": "web_search",
501 "max_uses": 5
502 });
503 let parsed: Tool = serde_json::from_value(raw).unwrap();
504 match parsed {
505 Tool::Builtin(BuiltinTool::Known(KnownBuiltinTool::WebSearch20250305 {
506 name,
507 max_uses,
508 ..
509 })) => {
510 assert_eq!(name, "web_search");
511 assert_eq!(max_uses, Some(5));
512 }
513 other => panic!("expected typed WebSearch20250305, got {other:?}"),
514 }
515 }
516
517 #[test]
518 fn web_search_default_serializes_to_minimal_wire_form() {
519 let t = Tool::web_search();
520 let v = serde_json::to_value(&t).unwrap();
521 assert_eq!(
522 v,
523 json!({"type": "web_search_20250305", "name": "web_search"})
524 );
525 }
526
527 #[test]
528 fn web_search_with_options_round_trips() {
529 let t = Tool::Builtin(BuiltinTool::Known(KnownBuiltinTool::WebSearch20250305 {
530 name: "web_search".into(),
531 max_uses: Some(3),
532 allowed_domains: Some(vec!["wikipedia.org".into()]),
533 blocked_domains: None,
534 user_location: Some(UserLocation {
535 kind: "approximate".into(),
536 city: Some("Paris".into()),
537 region: None,
538 country: Some("FR".into()),
539 timezone: Some("Europe/Paris".into()),
540 }),
541 cache_control: Some(CacheControl::ephemeral()),
542 }));
543 let v = serde_json::to_value(&t).unwrap();
544 assert_eq!(
545 v,
546 json!({
547 "type": "web_search_20250305",
548 "name": "web_search",
549 "max_uses": 3,
550 "allowed_domains": ["wikipedia.org"],
551 "user_location": {
552 "type": "approximate",
553 "city": "Paris",
554 "country": "FR",
555 "timezone": "Europe/Paris"
556 },
557 "cache_control": {"type": "ephemeral"}
558 })
559 );
560 let parsed: Tool = serde_json::from_value(v).unwrap();
561 assert_eq!(parsed, t);
562 }
563
564 #[test]
565 fn computer_default_serializes_with_required_dims() {
566 let t = Tool::computer(1920, 1080);
567 let v = serde_json::to_value(&t).unwrap();
568 assert_eq!(
569 v,
570 json!({
571 "type": "computer_20250124",
572 "name": "computer",
573 "display_width_px": 1920,
574 "display_height_px": 1080
575 })
576 );
577 }
578
579 #[test]
580 fn bash_text_editor_code_execution_defaults_serialize() {
581 assert_eq!(
582 serde_json::to_value(Tool::bash()).unwrap(),
583 json!({"type": "bash_20250124", "name": "bash"})
584 );
585 assert_eq!(
586 serde_json::to_value(Tool::text_editor()).unwrap(),
587 json!({"type": "text_editor_20250124", "name": "str_replace_editor"})
588 );
589 assert_eq!(
590 serde_json::to_value(Tool::code_execution()).unwrap(),
591 json!({"type": "code_execution_20250825", "name": "code_execution"})
592 );
593 }
594
595 #[test]
596 fn malformed_known_builtin_errors_not_silent_fallthrough() {
597 let raw = json!({
599 "type": "computer_20250124",
600 "name": "computer",
601 "display_width_px": "wide",
602 "display_height_px": 1080
603 });
604 let result: Result<Tool, _> = serde_json::from_value(raw);
605 assert!(
606 result.is_err(),
607 "malformed known builtin must error, not fall through to Other"
608 );
609 }
610
611 #[test]
612 fn untagged_enum_disambiguates_custom_from_builtin() {
613 let custom: Tool = serde_json::from_value(json!({
615 "name": "x",
616 "input_schema": {"type": "object"}
617 }))
618 .unwrap();
619 assert!(matches!(custom, Tool::Custom(_)));
620
621 let builtin: Tool = serde_json::from_value(json!({
623 "type": "web_search_20250305",
624 "name": "web_search"
625 }))
626 .unwrap();
627 assert!(matches!(builtin, Tool::Builtin(_)));
628 }
629
630 #[test]
631 fn tool_choice_auto_round_trips() {
632 let c = ToolChoice::auto();
633 let v = serde_json::to_value(&c).unwrap();
634 assert_eq!(v, json!({"type": "auto"}));
635 let parsed: ToolChoice = serde_json::from_value(v).unwrap();
636 assert_eq!(parsed, c);
637 }
638
639 #[test]
640 fn tool_choice_any_with_no_parallel_round_trips() {
641 let c = ToolChoice::Any {
642 disable_parallel_tool_use: Some(true),
643 };
644 let v = serde_json::to_value(&c).unwrap();
645 assert_eq!(v, json!({"type": "any", "disable_parallel_tool_use": true}));
646 let parsed: ToolChoice = serde_json::from_value(v).unwrap();
647 assert_eq!(parsed, c);
648 }
649
650 #[test]
651 fn tool_choice_specific_tool_round_trips() {
652 let c = ToolChoice::tool("get_weather");
653 let v = serde_json::to_value(&c).unwrap();
654 assert_eq!(v, json!({"type": "tool", "name": "get_weather"}));
655 let parsed: ToolChoice = serde_json::from_value(v).unwrap();
656 assert_eq!(parsed, c);
657 }
658
659 #[test]
660 fn tool_choice_none_round_trips() {
661 let c = ToolChoice::none();
662 let v = serde_json::to_value(&c).unwrap();
663 assert_eq!(v, json!({"type": "none"}));
664 let parsed: ToolChoice = serde_json::from_value(v).unwrap();
665 assert_eq!(parsed, c);
666 }
667
668 #[cfg(feature = "schemars-tools")]
669 #[test]
670 fn from_schemars_builds_custom_tool() {
671 #[derive(schemars::JsonSchema, serde::Deserialize)]
672 #[allow(dead_code)]
673 struct Args {
674 city: String,
675 units: Option<String>,
676 }
677
678 let t = Tool::from_schemars::<Args>("get_weather");
679 match t {
680 Tool::Custom(c) => {
681 assert_eq!(c.name, "get_weather");
682 assert!(c.input_schema.is_object());
684 }
685 Tool::Builtin(_) => panic!("expected Custom"),
686 }
687 }
688}