1use std::collections::BTreeMap;
17
18use serde::Serialize;
19use validator::Validate;
20
21use super::model_validate::validate_json_schema_value;
22use crate::tool::web_search::{ContentSize, SearchEngine, SearchRecencyFilter};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
50pub struct ThinkingType {
51 #[serde(rename = "type")]
53 pub mode: ThinkingMode,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
61 pub clear_thinking: Option<bool>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
66#[serde(rename_all = "lowercase")]
67pub enum ThinkingMode {
68 Enabled,
71 Disabled,
73}
74
75impl ThinkingType {
76 pub fn enabled() -> Self {
78 Self {
79 mode: ThinkingMode::Enabled,
80 clear_thinking: None,
81 }
82 }
83
84 pub fn disabled() -> Self {
86 Self {
87 mode: ThinkingMode::Disabled,
88 clear_thinking: None,
89 }
90 }
91
92 pub fn with_clear_thinking(mut self, clear: bool) -> Self {
97 self.clear_thinking = Some(clear);
98 self
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
137pub enum ReasoningEffort {
138 #[serde(rename = "max")]
141 Max,
142 #[serde(rename = "xhigh")]
144 Xhigh,
145 #[serde(rename = "high")]
147 High,
148 #[serde(rename = "medium")]
150 Medium,
151 #[serde(rename = "low")]
153 Low,
154 #[serde(rename = "minimal")]
156 Minimal,
157 #[serde(rename = "none")]
159 None,
160}
161
162#[derive(Debug, Clone, Serialize)]
188#[serde(tag = "type")]
189#[serde(rename_all = "snake_case")]
190pub enum Tools {
191 Function {
197 function: Function,
199 },
200
201 Retrieval {
206 retrieval: Retrieval,
208 },
209
210 WebSearch {
215 web_search: WebSearch,
217 },
218
219 #[serde(rename = "mcp")]
224 MCP {
225 mcp: MCP,
227 },
228}
229
230impl Validate for Tools {
231 fn validate(&self) -> Result<(), validator::ValidationErrors> {
232 match self {
233 Self::Function { function } => function.validate(),
234 Self::Retrieval { retrieval } => retrieval.validate(),
235 Self::WebSearch { web_search } => web_search.validate(),
236 Self::MCP { mcp } => mcp.validate(),
237 }
238 }
239}
240
241impl From<Function> for Tools {
242 fn from(function: Function) -> Self {
243 Self::Function { function }
244 }
245}
246
247#[derive(Clone, Serialize, Validate)]
254#[validate(schema(function = "validate_function"))]
255pub struct Function {
256 #[validate(length(min = 1, max = 64))]
258 pub name: String,
259
260 pub description: String,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 #[validate(custom(function = "validate_json_schema_value"))]
266 pub parameters: Option<serde_json::Value>,
267}
268
269impl std::fmt::Debug for Function {
270 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 formatter
272 .debug_struct("Function")
273 .field("name", &self.name)
274 .field("description", &"[REDACTED]")
275 .field("parameters_configured", &self.parameters.is_some())
276 .finish()
277 }
278}
279
280fn validate_function(function: &Function) -> Result<(), validator::ValidationError> {
281 if function.name.trim().is_empty() {
282 return Err(validator::ValidationError::new("name_must_not_be_blank"));
283 }
284 if !function
285 .name
286 .bytes()
287 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
288 {
289 return Err(validator::ValidationError::new("invalid_function_name"));
290 }
291 if function
292 .parameters
293 .as_ref()
294 .is_some_and(|parameters| !parameters.is_object())
295 {
296 return Err(validator::ValidationError::new(
297 "function_parameters_must_be_an_object_schema",
298 ));
299 }
300 Ok(())
301}
302
303impl Function {
304 pub fn new(
321 name: impl Into<String>,
322 description: impl Into<String>,
323 parameters: serde_json::Value,
324 ) -> Self {
325 Self {
326 name: name.into(),
327 description: description.into(),
328 parameters: Some(parameters),
329 }
330 }
331}
332
333#[derive(Clone, Serialize, Validate)]
363#[validate(schema(function = "validate_retrieval"))]
364pub struct Retrieval {
365 #[validate(length(min = 1))]
368 pub knowledge_id: String,
369 #[serde(skip_serializing_if = "Option::is_none")]
372 #[validate(length(min = 1))]
373 pub prompt_template: Option<String>,
374}
375
376impl std::fmt::Debug for Retrieval {
377 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 formatter
379 .debug_struct("Retrieval")
380 .field("knowledge_id", &"[REDACTED]")
381 .field(
382 "prompt_template_configured",
383 &self.prompt_template.is_some(),
384 )
385 .finish()
386 }
387}
388
389fn validate_retrieval(retrieval: &Retrieval) -> Result<(), validator::ValidationError> {
390 if retrieval.knowledge_id.trim().is_empty() {
391 return Err(validator::ValidationError::new(
392 "knowledge_id_must_not_be_blank",
393 ));
394 }
395 if retrieval
396 .prompt_template
397 .as_deref()
398 .is_some_and(|template| template.trim().is_empty())
399 {
400 return Err(validator::ValidationError::new(
401 "prompt_template_must_not_be_blank",
402 ));
403 }
404 Ok(())
405}
406
407impl Retrieval {
408 pub fn new(knowledge_id: impl Into<String>) -> Self {
411 Self {
412 knowledge_id: knowledge_id.into(),
413 prompt_template: None,
414 }
415 }
416
417 pub fn with_prompt_template(mut self, prompt_template: impl Into<String>) -> Self {
419 self.prompt_template = Some(prompt_template.into());
420 self
421 }
422}
423
424#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
426#[serde(rename_all = "snake_case")]
427pub enum ResultSequence {
428 Before,
430 After,
432}
433
434#[derive(Clone, Serialize, Validate)]
436#[validate(schema(function = "validate_web_search"))]
437pub struct WebSearch {
438 pub search_engine: SearchEngine,
441
442 #[serde(skip_serializing_if = "Option::is_none")]
444 pub enable: Option<bool>,
445
446 #[serde(skip_serializing_if = "Option::is_none")]
448 pub search_query: Option<String>,
449
450 #[serde(skip_serializing_if = "Option::is_none")]
453 pub search_intent: Option<bool>,
454
455 #[serde(skip_serializing_if = "Option::is_none")]
457 #[validate(range(min = 1, max = 50))]
458 pub count: Option<u32>,
459
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub search_domain_filter: Option<String>,
463
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub search_recency_filter: Option<SearchRecencyFilter>,
467
468 #[serde(skip_serializing_if = "Option::is_none")]
470 pub content_size: Option<ContentSize>,
471
472 #[serde(skip_serializing_if = "Option::is_none")]
474 pub result_sequence: Option<ResultSequence>,
475
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub search_result: Option<bool>,
479
480 #[serde(skip_serializing_if = "Option::is_none")]
482 pub require_search: Option<bool>,
483
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub search_prompt: Option<String>,
487}
488
489impl std::fmt::Debug for WebSearch {
490 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 formatter
492 .debug_struct("WebSearch")
493 .field("search_engine", &self.search_engine)
494 .field("enable", &self.enable)
495 .field("search_query_configured", &self.search_query.is_some())
496 .field("search_intent", &self.search_intent)
497 .field("count", &self.count)
498 .field(
499 "search_domain_filter_configured",
500 &self.search_domain_filter.is_some(),
501 )
502 .field("search_recency_filter", &self.search_recency_filter)
503 .field("content_size", &self.content_size)
504 .field("result_sequence", &self.result_sequence)
505 .field("search_result", &self.search_result)
506 .field("require_search", &self.require_search)
507 .field("search_prompt_configured", &self.search_prompt.is_some())
508 .finish()
509 }
510}
511
512fn validate_web_search(web_search: &WebSearch) -> Result<(), validator::ValidationError> {
513 if [
514 web_search.search_query.as_deref(),
515 web_search.search_domain_filter.as_deref(),
516 web_search.search_prompt.as_deref(),
517 ]
518 .into_iter()
519 .flatten()
520 .any(|value| value.trim().is_empty())
521 {
522 Err(validator::ValidationError::new(
523 "search_text_must_not_be_blank",
524 ))
525 } else {
526 Ok(())
527 }
528}
529
530impl WebSearch {
531 pub fn new(search_engine: SearchEngine) -> Self {
534 Self {
535 search_engine,
536 enable: None,
537 search_query: None,
538 search_intent: None,
539 count: None,
540 search_domain_filter: None,
541 search_recency_filter: None,
542 content_size: None,
543 result_sequence: None,
544 search_result: None,
545 require_search: None,
546 search_prompt: None,
547 }
548 }
549
550 pub fn with_enable(mut self, enable: bool) -> Self {
552 self.enable = Some(enable);
553 self
554 }
555 pub fn with_search_query(mut self, query: impl Into<String>) -> Self {
557 self.search_query = Some(query.into());
558 self
559 }
560 pub fn with_search_intent(mut self, search_intent: bool) -> Self {
562 self.search_intent = Some(search_intent);
563 self
564 }
565 pub fn with_count(mut self, count: u32) -> Self {
567 self.count = Some(count);
568 self
569 }
570 pub fn with_search_domain_filter(mut self, domain: impl Into<String>) -> Self {
572 self.search_domain_filter = Some(domain.into());
573 self
574 }
575 pub fn with_search_recency_filter(mut self, filter: SearchRecencyFilter) -> Self {
577 self.search_recency_filter = Some(filter);
578 self
579 }
580 pub fn with_content_size(mut self, size: ContentSize) -> Self {
582 self.content_size = Some(size);
583 self
584 }
585 pub fn with_result_sequence(mut self, seq: ResultSequence) -> Self {
587 self.result_sequence = Some(seq);
588 self
589 }
590 pub fn with_search_result(mut self, enable: bool) -> Self {
592 self.search_result = Some(enable);
593 self
594 }
595 pub fn with_require_search(mut self, require: bool) -> Self {
597 self.require_search = Some(require);
598 self
599 }
600 pub fn with_search_prompt(mut self, prompt: impl Into<String>) -> Self {
602 self.search_prompt = Some(prompt.into());
603 self
604 }
605}
606#[derive(Clone, Serialize, Validate)]
612#[validate(schema(function = "validate_mcp"))]
613pub struct MCP {
614 #[validate(length(min = 1))]
617 pub server_label: String,
618
619 #[serde(skip_serializing_if = "Option::is_none")]
621 #[validate(url)]
622 pub server_url: Option<String>,
623
624 #[serde(skip_serializing_if = "Option::is_none")]
626 pub transport_type: Option<MCPTransportType>,
627
628 #[serde(skip_serializing_if = "Vec::is_empty")]
630 pub allowed_tools: Vec<String>,
631
632 #[serde(skip_serializing_if = "Option::is_none")]
634 pub headers: Option<BTreeMap<String, String>>,
635}
636
637fn validate_mcp(mcp: &MCP) -> Result<(), validator::ValidationError> {
638 if mcp.server_label.trim().is_empty() {
639 return Err(validator::ValidationError::new(
640 "server_label_must_not_be_blank",
641 ));
642 }
643 if let Some(server_url) = mcp.server_url.as_deref() {
644 let Ok(url) = server_url.parse::<url::Url>() else {
645 return Err(validator::ValidationError::new("invalid_server_url"));
646 };
647 if !matches!(url.scheme(), "http" | "https")
648 || !url.username().is_empty()
649 || url.password().is_some()
650 || url.fragment().is_some()
651 {
652 return Err(validator::ValidationError::new("invalid_server_url"));
653 }
654 }
655 if mcp.allowed_tools.iter().any(|tool| tool.trim().is_empty()) {
656 return Err(validator::ValidationError::new(
657 "allowed_tool_must_not_be_blank",
658 ));
659 }
660 if mcp.headers.as_ref().is_some_and(|headers| {
661 headers.iter().any(|(name, value)| {
662 !valid_forwarded_header_name(name) || !valid_forwarded_header_value(value)
663 })
664 }) {
665 return Err(validator::ValidationError::new("invalid_forwarded_header"));
666 }
667 Ok(())
668}
669
670fn valid_forwarded_header_name(name: &str) -> bool {
671 !name.is_empty()
672 && name.bytes().all(|byte| {
673 byte.is_ascii_alphanumeric()
674 || matches!(
675 byte,
676 b'!' | b'#'
677 | b'$'
678 | b'%'
679 | b'&'
680 | b'\''
681 | b'*'
682 | b'+'
683 | b'-'
684 | b'.'
685 | b'^'
686 | b'_'
687 | b'`'
688 | b'|'
689 | b'~'
690 )
691 })
692}
693
694fn valid_forwarded_header_value(value: &str) -> bool {
695 !value.trim().is_empty()
696 && value
697 .chars()
698 .all(|character| character == '\t' || !character.is_control())
699}
700
701impl std::fmt::Debug for MCP {
702 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
703 formatter
704 .debug_struct("MCP")
705 .field("server_label", &"[REDACTED]")
706 .field("server_url_configured", &self.server_url.is_some())
707 .field("transport_type", &self.transport_type)
708 .field("allowed_tool_count", &self.allowed_tools.len())
709 .field("header_count", &self.headers.as_ref().map(BTreeMap::len))
710 .finish()
711 }
712}
713
714impl MCP {
715 pub fn new(server_label: impl Into<String>) -> Self {
718 Self {
719 server_label: server_label.into(),
720 server_url: None,
721 transport_type: Some(MCPTransportType::StreamableHttp),
722 allowed_tools: Vec::new(),
723 headers: None,
724 }
725 }
726
727 pub fn with_server_url(mut self, url: impl Into<String>) -> Self {
729 self.server_url = Some(url.into());
730 self
731 }
732 pub fn with_transport_type(mut self, transport: MCPTransportType) -> Self {
734 self.transport_type = Some(transport);
735 self
736 }
737 pub fn with_allowed_tools(mut self, tools: impl Into<Vec<String>>) -> Self {
739 self.allowed_tools = tools.into();
740 self
741 }
742 pub fn add_allowed_tool(mut self, tool: impl Into<String>) -> Self {
744 self.allowed_tools.push(tool.into());
745 self
746 }
747 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
749 self.headers = Some(headers);
750 self
751 }
752 pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
754 self.headers
755 .get_or_insert_with(BTreeMap::new)
756 .insert(key.into(), value.into());
757 self
758 }
759}
760
761#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
763#[serde(rename_all = "kebab-case")]
764pub enum MCPTransportType {
765 Sse,
767 StreamableHttp,
769}
770
771#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
773#[serde(rename_all = "snake_case")]
774#[serde(tag = "type")]
775pub enum ResponseFormat {
776 Text,
778 JsonObject,
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
798#[serde(rename_all = "lowercase")]
799pub enum ToolChoice {
800 Auto,
802}
803
804impl ToolChoice {
805 pub const fn auto() -> Self {
807 Self::Auto
808 }
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814
815 #[test]
817 fn test_thinking_type_enabled_serialization() {
818 let thinking = ThinkingType::enabled();
819 let json = serde_json::to_string(&thinking).unwrap();
820 assert!(json.contains("\"type\":\"enabled\""));
821 assert!(!json.contains("clear_thinking"));
822 }
823
824 #[test]
825 fn test_thinking_type_disabled_serialization() {
826 let thinking = ThinkingType::disabled();
827 let json = serde_json::to_string(&thinking).unwrap();
828 assert!(json.contains("\"type\":\"disabled\""));
829 assert!(!json.contains("clear_thinking"));
830 }
831
832 #[test]
833 fn test_thinking_type_with_clear_thinking_serialization() {
834 let thinking = ThinkingType::enabled().with_clear_thinking(false);
835 let json = serde_json::to_string(&thinking).unwrap();
836 assert!(json.contains("\"type\":\"enabled\""));
837 assert!(json.contains("\"clear_thinking\":false"));
838 }
839
840 #[test]
841 fn test_thinking_type_disabled_with_clear_thinking() {
842 let thinking = ThinkingType::disabled().with_clear_thinking(true);
843 let json = serde_json::to_string(&thinking).unwrap();
844 assert!(json.contains("\"type\":\"disabled\""));
845 assert!(json.contains("\"clear_thinking\":true"));
846 }
847
848 #[test]
850 fn test_function_new() {
851 let params = serde_json::json!({
852 "type": "object",
853 "properties": {
854 "name": {"type": "string"}
855 }
856 });
857 let func = Function::new("test_func", "A test function", params);
858
859 assert_eq!(func.name, "test_func");
860 assert_eq!(func.description, "A test function");
861 assert!(func.parameters.is_some());
862 }
863
864 #[test]
865 fn test_function_serialization() {
866 let params = serde_json::json!({
867 "type": "object",
868 "properties": {
869 "value": {"type": "number"}
870 }
871 });
872 let func = Function::new("test_func", "A test function", params);
873 let json = serde_json::to_string(&func).unwrap();
874
875 assert!(json.contains("\"name\":\"test_func\""));
876 assert!(json.contains("\"description\":\"A test function\""));
877 assert!(json.contains("\"properties\""));
878 }
879
880 #[test]
881 fn test_function_validation() {
882 let params = serde_json::json!({
883 "type": "object",
884 "properties": {}
885 });
886 let func = Function::new("valid_name", "Description", params.clone());
887
888 assert!(func.validate().is_ok());
890 assert!(
891 Function::new("valid-name", "Description", params.clone())
892 .validate()
893 .is_ok()
894 );
895
896 let invalid_name = Function::new("", "Description", params.clone());
897 assert!(invalid_name.validate().is_err());
898 let blank_name = Function::new(" ", "Description", params.clone());
899 assert!(blank_name.validate().is_err());
900 let punctuation = Function::new("invalid!", "Description", params.clone());
901 assert!(punctuation.validate().is_err());
902
903 let long_name = Function::new("a".repeat(65), "Description", params);
904 assert!(long_name.validate().is_err());
905 }
906
907 #[test]
909 fn test_retrieval_new() {
910 let retrieval = Retrieval::new("kb_123").with_prompt_template("template");
911 assert_eq!(retrieval.knowledge_id, "kb_123");
912 assert_eq!(retrieval.prompt_template, Some("template".to_string()));
913 }
914
915 #[test]
916 fn test_retrieval_new_without_template() {
917 let retrieval = Retrieval::new("kb_456");
918 assert_eq!(retrieval.knowledge_id, "kb_456");
919 assert!(retrieval.prompt_template.is_none());
920 }
921
922 #[test]
923 fn test_retrieval_serialization() {
924 let retrieval = Retrieval::new("kb_789");
925 let json = serde_json::to_string(&retrieval).unwrap();
926 assert!(json.contains("\"knowledge_id\":\"kb_789\""));
927 assert!(!json.contains("prompt_template"));
929 }
930
931 #[test]
932 fn retrieval_prompt_template_serializes() {
933 let retrieval = Retrieval::new("kb_builder").with_prompt_template("ctx: {knowledge}");
934 let json = serde_json::to_value(&retrieval).unwrap();
935 assert_eq!(json["knowledge_id"], "kb_builder");
936 assert_eq!(json["prompt_template"], "ctx: {knowledge}");
937 }
938
939 #[test]
940 fn tool_validation_rejects_blank_optional_values() {
941 assert!(Retrieval::new(" ").validate().is_err());
942 assert!(
943 WebSearch::new(SearchEngine::SearchPro)
944 .with_search_query(" ")
945 .validate()
946 .is_err()
947 );
948 assert!(MCP::new(" ").validate().is_err());
949 assert!(
950 MCP::new("server")
951 .with_server_url("ftp://example.com")
952 .validate()
953 .is_err()
954 );
955 assert!(
956 MCP::new("server")
957 .with_header("Authorization\r\nInjected", "secret")
958 .validate()
959 .is_err()
960 );
961 }
962
963 #[test]
965 fn test_web_search_new() {
966 let web_search = WebSearch::new(SearchEngine::SearchPro);
967 assert_eq!(web_search.search_engine, SearchEngine::SearchPro);
968 assert!(web_search.enable.is_none());
969 }
970
971 #[test]
972 fn test_web_search_with_enable() {
973 let web_search = WebSearch::new(SearchEngine::SearchPro).with_enable(true);
974 assert_eq!(web_search.enable, Some(true));
975 }
976
977 #[test]
978 fn test_web_search_with_search_query() {
979 let web_search = WebSearch::new(SearchEngine::SearchPro).with_search_query("test query");
980 assert_eq!(web_search.search_query, Some("test query".to_string()));
981 }
982
983 #[test]
984 fn test_web_search_with_search_intent() {
985 let web_search = WebSearch::new(SearchEngine::SearchPro).with_search_intent(true);
986 assert_eq!(web_search.search_intent, Some(true));
987 }
988
989 #[test]
990 fn test_web_search_with_count() {
991 let web_search = WebSearch::new(SearchEngine::SearchPro).with_count(10);
992 assert_eq!(web_search.count, Some(10));
993 }
994
995 #[test]
996 fn test_web_search_with_search_domain_filter() {
997 let web_search =
998 WebSearch::new(SearchEngine::SearchPro).with_search_domain_filter("example.com");
999 assert_eq!(
1000 web_search.search_domain_filter,
1001 Some("example.com".to_string())
1002 );
1003 }
1004
1005 #[test]
1006 fn test_web_search_with_search_recency_filter() {
1007 let filter = SearchRecencyFilter::OneDay;
1008 let web_search = WebSearch::new(SearchEngine::SearchPro).with_search_recency_filter(filter);
1009 assert_eq!(web_search.search_recency_filter, Some(filter));
1010 }
1011
1012 #[test]
1013 fn test_web_search_with_content_size() {
1014 let size = ContentSize::Medium;
1015 let web_search = WebSearch::new(SearchEngine::SearchPro).with_content_size(size);
1016 assert_eq!(web_search.content_size, Some(size));
1017 }
1018
1019 #[test]
1020 fn test_web_search_with_result_sequence() {
1021 let seq = ResultSequence::After;
1022 let web_search = WebSearch::new(SearchEngine::SearchPro).with_result_sequence(seq);
1023 assert_eq!(web_search.result_sequence, Some(seq));
1024 }
1025
1026 #[test]
1027 fn test_web_search_with_search_result() {
1028 let web_search = WebSearch::new(SearchEngine::SearchPro).with_search_result(true);
1029 assert_eq!(web_search.search_result, Some(true));
1030 }
1031
1032 #[test]
1033 fn test_web_search_with_require_search() {
1034 let web_search = WebSearch::new(SearchEngine::SearchPro).with_require_search(true);
1035 assert_eq!(web_search.require_search, Some(true));
1036 }
1037
1038 #[test]
1039 fn test_web_search_with_search_prompt() {
1040 let web_search =
1041 WebSearch::new(SearchEngine::SearchPro).with_search_prompt("custom prompt");
1042 assert_eq!(web_search.search_prompt, Some("custom prompt".to_string()));
1043 }
1044
1045 #[test]
1046 fn test_web_search_serialization() {
1047 let web_search = WebSearch::new(SearchEngine::SearchPro)
1048 .with_enable(true)
1049 .with_count(5);
1050 let json = serde_json::to_string(&web_search).unwrap();
1051 assert!(json.contains("\"search_engine\""));
1052 assert!(json.contains("\"enable\":true"));
1053 assert!(json.contains("\"count\":5"));
1054 }
1055
1056 #[test]
1058 fn test_mcp_new() {
1059 let mcp = MCP::new("server_label");
1060 assert_eq!(mcp.server_label, "server_label");
1061 assert_eq!(mcp.transport_type, Some(MCPTransportType::StreamableHttp));
1062 assert!(mcp.allowed_tools.is_empty());
1063 }
1064
1065 #[test]
1066 fn test_mcp_with_server_url() {
1067 let mcp = MCP::new("server_label").with_server_url("https://example.com");
1068 assert_eq!(mcp.server_url, Some("https://example.com".to_string()));
1069 }
1070
1071 #[test]
1072 fn test_mcp_with_transport_type() {
1073 let mcp = MCP::new("server_label").with_transport_type(MCPTransportType::Sse);
1074 assert_eq!(mcp.transport_type, Some(MCPTransportType::Sse));
1075 }
1076
1077 #[test]
1078 fn test_mcp_with_allowed_tools() {
1079 let mcp = MCP::new("server_label")
1080 .with_allowed_tools(vec!["tool1".to_string(), "tool2".to_string()]);
1081 assert_eq!(mcp.allowed_tools.len(), 2);
1082 assert!(mcp.allowed_tools.contains(&"tool1".to_string()));
1083 }
1084
1085 #[test]
1086 fn test_mcp_add_allowed_tool() {
1087 let mcp = MCP::new("server_label")
1088 .add_allowed_tool("tool1")
1089 .add_allowed_tool("tool2");
1090 assert_eq!(mcp.allowed_tools.len(), 2);
1091 }
1092
1093 #[test]
1094 fn test_mcp_with_headers() {
1095 let mut headers = BTreeMap::new();
1096 headers.insert("Authorization".to_string(), "Bearer token".to_string());
1097 let mcp = MCP::new("server_label").with_headers(headers.clone());
1098 assert_eq!(mcp.headers, Some(headers));
1099 }
1100
1101 #[test]
1102 fn test_mcp_with_header() {
1103 let mcp = MCP::new("server_label").with_header("Authorization", "Bearer token");
1104 let debug = format!("{mcp:?}");
1105 assert!(!debug.contains("Bearer token"));
1106 assert!(!debug.contains("Authorization"));
1107 assert!(debug.contains("header_count"));
1108 let headers = mcp.headers.unwrap();
1109 assert_eq!(
1110 headers.get("Authorization"),
1111 Some(&"Bearer token".to_string())
1112 );
1113 }
1114
1115 #[test]
1116 fn test_mcp_serialization() {
1117 let mcp = MCP::new("server_label")
1118 .with_server_url("https://example.com")
1119 .with_transport_type(MCPTransportType::Sse);
1120 let json = serde_json::to_string(&mcp).unwrap();
1121 assert!(json.contains("\"server_label\":\"server_label\""));
1122 assert!(json.contains("\"server_url\":\"https://example.com\""));
1123 assert!(json.contains("\"transport_type\":\"sse\""));
1124 assert!(!json.contains("allowed_tools"));
1126 }
1127
1128 #[test]
1129 fn mcp_validation_rejects_credentialed_or_fragmented_urls() {
1130 assert!(
1131 MCP::new("server")
1132 .with_server_url("https://user:secret@example.com/mcp")
1133 .validate()
1134 .is_err()
1135 );
1136 assert!(
1137 MCP::new("server")
1138 .with_server_url("https://example.com/mcp#secret")
1139 .validate()
1140 .is_err()
1141 );
1142 }
1143
1144 #[test]
1145 fn tool_debug_output_redacts_caller_content_and_remote_configuration() {
1146 let function = Function::new(
1147 "lookup",
1148 "private-description",
1149 serde_json::json!({"private-schema-key": {"type": "string"}}),
1150 );
1151 let retrieval =
1152 Retrieval::new("private-knowledge").with_prompt_template("private-template");
1153 let search = WebSearch::new(SearchEngine::SearchPro)
1154 .with_search_query("private-query")
1155 .with_search_domain_filter("private.example")
1156 .with_search_prompt("private-search-prompt");
1157 let mcp = MCP::new("private-server")
1158 .with_server_url("https://private.example/mcp?token=private-token")
1159 .add_allowed_tool("private-tool")
1160 .with_header("Authorization", "private-header");
1161 let debug = format!("{function:?} {retrieval:?} {search:?} {mcp:?}");
1162 for secret in [
1163 "private-description",
1164 "private-schema-key",
1165 "private-knowledge",
1166 "private-template",
1167 "private-query",
1168 "private.example",
1169 "private-search-prompt",
1170 "private-server",
1171 "private-token",
1172 "private-tool",
1173 "Authorization",
1174 "private-header",
1175 ] {
1176 assert!(!debug.contains(secret), "Debug leaked {secret}");
1177 }
1178 }
1179
1180 #[test]
1182 fn test_mcp_transport_type_sse_serialization() {
1183 let transport = MCPTransportType::Sse;
1184 let json = serde_json::to_string(&transport).unwrap();
1185 assert!(json.contains("\"sse\""));
1186 }
1187
1188 #[test]
1189 fn test_mcp_transport_type_streamable_http_serialization() {
1190 let transport = MCPTransportType::StreamableHttp;
1191 let json = serde_json::to_string(&transport).unwrap();
1192 assert!(json.contains("\"streamable-http\""));
1193 }
1194
1195 #[test]
1197 fn test_response_format_text_serialization() {
1198 let format = ResponseFormat::Text;
1199 let json = serde_json::to_string(&format).unwrap();
1200 assert!(json.contains("\"type\":\"text\""));
1201 }
1202
1203 #[test]
1204 fn test_response_format_json_object_serialization() {
1205 let format = ResponseFormat::JsonObject;
1206 let json = serde_json::to_string(&format).unwrap();
1207 assert!(json.contains("\"type\":\"json_object\""));
1208 }
1209
1210 #[test]
1212 fn test_tool_choice_auto_serializes_as_bare_string() {
1213 let json = serde_json::to_value(ToolChoice::auto()).unwrap();
1214 assert_eq!(json, serde_json::json!("auto"));
1215 }
1216
1217 #[test]
1219 fn test_tools_function_serialization() {
1220 let func = Function::new("test_func", "test", serde_json::json!({}));
1221 let tools = Tools::Function { function: func };
1222 let json = serde_json::to_string(&tools).unwrap();
1223 assert!(json.contains("\"type\":\"function\""));
1224 assert!(json.contains("\"name\":\"test_func\""));
1225 }
1226
1227 #[test]
1228 fn test_tools_retrieval_serialization() {
1229 let retrieval = Retrieval::new("kb_123");
1230 let tools = Tools::Retrieval { retrieval };
1231 let json = serde_json::to_string(&tools).unwrap();
1232 assert!(json.contains("\"type\":\"retrieval\""));
1233 assert!(json.contains("\"knowledge_id\":\"kb_123\""));
1234 }
1235
1236 #[test]
1237 fn test_tools_web_search_serialization() {
1238 let web_search = WebSearch::new(SearchEngine::SearchPro);
1239 let tools = Tools::WebSearch { web_search };
1240 let json = serde_json::to_string(&tools).unwrap();
1241 assert!(json.contains("\"type\":\"web_search\""));
1242 assert!(json.contains("\"search_engine\""));
1243 }
1244
1245 #[test]
1246 fn test_tools_mcp_serialization() {
1247 let mcp = MCP::new("server_label");
1248 let tools = Tools::MCP { mcp };
1249 let json = serde_json::to_string(&tools).unwrap();
1250 assert!(json.contains("\"type\":\"mcp\""));
1251 assert!(json.contains("\"server_label\":\"server_label\""));
1252 }
1253
1254 #[test]
1256 fn test_result_sequence_before_serialization() {
1257 let seq = ResultSequence::Before;
1258 let json = serde_json::to_string(&seq).unwrap();
1259 assert!(json.contains("\"before\""));
1260 }
1261
1262 #[test]
1263 fn test_result_sequence_after_serialization() {
1264 let seq = ResultSequence::After;
1265 let json = serde_json::to_string(&seq).unwrap();
1266 assert!(json.contains("\"after\""));
1267 }
1268}