use std::collections::BTreeMap;
use serde::Serialize;
use validator::Validate;
use super::model_validate::validate_json_schema_value;
use crate::tool::web_search::{ContentSize, SearchEngine, SearchRecencyFilter};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct ThinkingType {
#[serde(rename = "type")]
pub mode: ThinkingMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub clear_thinking: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ThinkingMode {
Enabled,
Disabled,
}
impl ThinkingType {
pub fn enabled() -> Self {
Self {
mode: ThinkingMode::Enabled,
clear_thinking: None,
}
}
pub fn disabled() -> Self {
Self {
mode: ThinkingMode::Disabled,
clear_thinking: None,
}
}
pub fn with_clear_thinking(mut self, clear: bool) -> Self {
self.clear_thinking = Some(clear);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ReasoningEffort {
#[serde(rename = "max")]
Max,
#[serde(rename = "xhigh")]
Xhigh,
#[serde(rename = "high")]
High,
#[serde(rename = "medium")]
Medium,
#[serde(rename = "low")]
Low,
#[serde(rename = "minimal")]
Minimal,
#[serde(rename = "none")]
None,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum Tools {
Function {
function: Function,
},
Retrieval {
retrieval: Retrieval,
},
WebSearch {
web_search: WebSearch,
},
#[serde(rename = "mcp")]
MCP {
mcp: MCP,
},
}
impl Validate for Tools {
fn validate(&self) -> Result<(), validator::ValidationErrors> {
match self {
Self::Function { function } => function.validate(),
Self::Retrieval { retrieval } => retrieval.validate(),
Self::WebSearch { web_search } => web_search.validate(),
Self::MCP { mcp } => mcp.validate(),
}
}
}
impl From<Function> for Tools {
fn from(function: Function) -> Self {
Self::Function { function }
}
}
#[derive(Clone, Serialize, Validate)]
#[validate(schema(function = "validate_function"))]
pub struct Function {
#[validate(length(min = 1, max = 64))]
pub name: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(custom(function = "validate_json_schema_value"))]
pub parameters: Option<serde_json::Value>,
}
impl std::fmt::Debug for Function {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Function")
.field("name", &self.name)
.field("description", &"[REDACTED]")
.field("parameters_configured", &self.parameters.is_some())
.finish()
}
}
fn validate_function(function: &Function) -> Result<(), validator::ValidationError> {
if function.name.trim().is_empty() {
return Err(validator::ValidationError::new("name_must_not_be_blank"));
}
if !function
.name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
{
return Err(validator::ValidationError::new("invalid_function_name"));
}
if function
.parameters
.as_ref()
.is_some_and(|parameters| !parameters.is_object())
{
return Err(validator::ValidationError::new(
"function_parameters_must_be_an_object_schema",
));
}
Ok(())
}
impl Function {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
parameters: serde_json::Value,
) -> Self {
Self {
name: name.into(),
description: description.into(),
parameters: Some(parameters),
}
}
}
#[derive(Clone, Serialize, Validate)]
#[validate(schema(function = "validate_retrieval"))]
pub struct Retrieval {
#[validate(length(min = 1))]
pub knowledge_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1))]
pub prompt_template: Option<String>,
}
impl std::fmt::Debug for Retrieval {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Retrieval")
.field("knowledge_id", &"[REDACTED]")
.field(
"prompt_template_configured",
&self.prompt_template.is_some(),
)
.finish()
}
}
fn validate_retrieval(retrieval: &Retrieval) -> Result<(), validator::ValidationError> {
if retrieval.knowledge_id.trim().is_empty() {
return Err(validator::ValidationError::new(
"knowledge_id_must_not_be_blank",
));
}
if retrieval
.prompt_template
.as_deref()
.is_some_and(|template| template.trim().is_empty())
{
return Err(validator::ValidationError::new(
"prompt_template_must_not_be_blank",
));
}
Ok(())
}
impl Retrieval {
pub fn new(knowledge_id: impl Into<String>) -> Self {
Self {
knowledge_id: knowledge_id.into(),
prompt_template: None,
}
}
pub fn with_prompt_template(mut self, prompt_template: impl Into<String>) -> Self {
self.prompt_template = Some(prompt_template.into());
self
}
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResultSequence {
Before,
After,
}
#[derive(Clone, Serialize, Validate)]
#[validate(schema(function = "validate_web_search"))]
pub struct WebSearch {
pub search_engine: SearchEngine,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_intent: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(range(min = 1, max = 50))]
pub count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_domain_filter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_recency_filter: Option<SearchRecencyFilter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_size: Option<ContentSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result_sequence: Option<ResultSequence>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_result: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_search: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_prompt: Option<String>,
}
impl std::fmt::Debug for WebSearch {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("WebSearch")
.field("search_engine", &self.search_engine)
.field("enable", &self.enable)
.field("search_query_configured", &self.search_query.is_some())
.field("search_intent", &self.search_intent)
.field("count", &self.count)
.field(
"search_domain_filter_configured",
&self.search_domain_filter.is_some(),
)
.field("search_recency_filter", &self.search_recency_filter)
.field("content_size", &self.content_size)
.field("result_sequence", &self.result_sequence)
.field("search_result", &self.search_result)
.field("require_search", &self.require_search)
.field("search_prompt_configured", &self.search_prompt.is_some())
.finish()
}
}
fn validate_web_search(web_search: &WebSearch) -> Result<(), validator::ValidationError> {
if [
web_search.search_query.as_deref(),
web_search.search_domain_filter.as_deref(),
web_search.search_prompt.as_deref(),
]
.into_iter()
.flatten()
.any(|value| value.trim().is_empty())
{
Err(validator::ValidationError::new(
"search_text_must_not_be_blank",
))
} else {
Ok(())
}
}
impl WebSearch {
pub fn new(search_engine: SearchEngine) -> Self {
Self {
search_engine,
enable: None,
search_query: None,
search_intent: None,
count: None,
search_domain_filter: None,
search_recency_filter: None,
content_size: None,
result_sequence: None,
search_result: None,
require_search: None,
search_prompt: None,
}
}
pub fn with_enable(mut self, enable: bool) -> Self {
self.enable = Some(enable);
self
}
pub fn with_search_query(mut self, query: impl Into<String>) -> Self {
self.search_query = Some(query.into());
self
}
pub fn with_search_intent(mut self, search_intent: bool) -> Self {
self.search_intent = Some(search_intent);
self
}
pub fn with_count(mut self, count: u32) -> Self {
self.count = Some(count);
self
}
pub fn with_search_domain_filter(mut self, domain: impl Into<String>) -> Self {
self.search_domain_filter = Some(domain.into());
self
}
pub fn with_search_recency_filter(mut self, filter: SearchRecencyFilter) -> Self {
self.search_recency_filter = Some(filter);
self
}
pub fn with_content_size(mut self, size: ContentSize) -> Self {
self.content_size = Some(size);
self
}
pub fn with_result_sequence(mut self, seq: ResultSequence) -> Self {
self.result_sequence = Some(seq);
self
}
pub fn with_search_result(mut self, enable: bool) -> Self {
self.search_result = Some(enable);
self
}
pub fn with_require_search(mut self, require: bool) -> Self {
self.require_search = Some(require);
self
}
pub fn with_search_prompt(mut self, prompt: impl Into<String>) -> Self {
self.search_prompt = Some(prompt.into());
self
}
}
#[derive(Clone, Serialize, Validate)]
#[validate(schema(function = "validate_mcp"))]
pub struct MCP {
#[validate(length(min = 1))]
pub server_label: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(url)]
pub server_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transport_type: Option<MCPTransportType>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_tools: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<BTreeMap<String, String>>,
}
fn validate_mcp(mcp: &MCP) -> Result<(), validator::ValidationError> {
if mcp.server_label.trim().is_empty() {
return Err(validator::ValidationError::new(
"server_label_must_not_be_blank",
));
}
if let Some(server_url) = mcp.server_url.as_deref() {
let Ok(url) = server_url.parse::<url::Url>() else {
return Err(validator::ValidationError::new("invalid_server_url"));
};
if !matches!(url.scheme(), "http" | "https")
|| !url.username().is_empty()
|| url.password().is_some()
|| url.fragment().is_some()
{
return Err(validator::ValidationError::new("invalid_server_url"));
}
}
if mcp.allowed_tools.iter().any(|tool| tool.trim().is_empty()) {
return Err(validator::ValidationError::new(
"allowed_tool_must_not_be_blank",
));
}
if mcp.headers.as_ref().is_some_and(|headers| {
headers.iter().any(|(name, value)| {
!valid_forwarded_header_name(name) || !valid_forwarded_header_value(value)
})
}) {
return Err(validator::ValidationError::new("invalid_forwarded_header"));
}
Ok(())
}
fn valid_forwarded_header_name(name: &str) -> bool {
!name.is_empty()
&& name.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
})
}
fn valid_forwarded_header_value(value: &str) -> bool {
!value.trim().is_empty()
&& value
.chars()
.all(|character| character == '\t' || !character.is_control())
}
impl std::fmt::Debug for MCP {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("MCP")
.field("server_label", &"[REDACTED]")
.field("server_url_configured", &self.server_url.is_some())
.field("transport_type", &self.transport_type)
.field("allowed_tool_count", &self.allowed_tools.len())
.field("header_count", &self.headers.as_ref().map(BTreeMap::len))
.finish()
}
}
impl MCP {
pub fn new(server_label: impl Into<String>) -> Self {
Self {
server_label: server_label.into(),
server_url: None,
transport_type: Some(MCPTransportType::StreamableHttp),
allowed_tools: Vec::new(),
headers: None,
}
}
pub fn with_server_url(mut self, url: impl Into<String>) -> Self {
self.server_url = Some(url.into());
self
}
pub fn with_transport_type(mut self, transport: MCPTransportType) -> Self {
self.transport_type = Some(transport);
self
}
pub fn with_allowed_tools(mut self, tools: impl Into<Vec<String>>) -> Self {
self.allowed_tools = tools.into();
self
}
pub fn add_allowed_tool(mut self, tool: impl Into<String>) -> Self {
self.allowed_tools.push(tool.into());
self
}
pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
self.headers = Some(headers);
self
}
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers
.get_or_insert_with(BTreeMap::new)
.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum MCPTransportType {
Sse,
StreamableHttp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum ResponseFormat {
Text,
JsonObject,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolChoice {
Auto,
}
impl ToolChoice {
pub const fn auto() -> Self {
Self::Auto
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_thinking_type_enabled_serialization() {
let thinking = ThinkingType::enabled();
let json = serde_json::to_string(&thinking).unwrap();
assert!(json.contains("\"type\":\"enabled\""));
assert!(!json.contains("clear_thinking"));
}
#[test]
fn test_thinking_type_disabled_serialization() {
let thinking = ThinkingType::disabled();
let json = serde_json::to_string(&thinking).unwrap();
assert!(json.contains("\"type\":\"disabled\""));
assert!(!json.contains("clear_thinking"));
}
#[test]
fn test_thinking_type_with_clear_thinking_serialization() {
let thinking = ThinkingType::enabled().with_clear_thinking(false);
let json = serde_json::to_string(&thinking).unwrap();
assert!(json.contains("\"type\":\"enabled\""));
assert!(json.contains("\"clear_thinking\":false"));
}
#[test]
fn test_thinking_type_disabled_with_clear_thinking() {
let thinking = ThinkingType::disabled().with_clear_thinking(true);
let json = serde_json::to_string(&thinking).unwrap();
assert!(json.contains("\"type\":\"disabled\""));
assert!(json.contains("\"clear_thinking\":true"));
}
#[test]
fn test_function_new() {
let params = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let func = Function::new("test_func", "A test function", params);
assert_eq!(func.name, "test_func");
assert_eq!(func.description, "A test function");
assert!(func.parameters.is_some());
}
#[test]
fn test_function_serialization() {
let params = serde_json::json!({
"type": "object",
"properties": {
"value": {"type": "number"}
}
});
let func = Function::new("test_func", "A test function", params);
let json = serde_json::to_string(&func).unwrap();
assert!(json.contains("\"name\":\"test_func\""));
assert!(json.contains("\"description\":\"A test function\""));
assert!(json.contains("\"properties\""));
}
#[test]
fn test_function_validation() {
let params = serde_json::json!({
"type": "object",
"properties": {}
});
let func = Function::new("valid_name", "Description", params.clone());
assert!(func.validate().is_ok());
assert!(
Function::new("valid-name", "Description", params.clone())
.validate()
.is_ok()
);
let invalid_name = Function::new("", "Description", params.clone());
assert!(invalid_name.validate().is_err());
let blank_name = Function::new(" ", "Description", params.clone());
assert!(blank_name.validate().is_err());
let punctuation = Function::new("invalid!", "Description", params.clone());
assert!(punctuation.validate().is_err());
let long_name = Function::new("a".repeat(65), "Description", params);
assert!(long_name.validate().is_err());
}
#[test]
fn test_retrieval_new() {
let retrieval = Retrieval::new("kb_123").with_prompt_template("template");
assert_eq!(retrieval.knowledge_id, "kb_123");
assert_eq!(retrieval.prompt_template, Some("template".to_string()));
}
#[test]
fn test_retrieval_new_without_template() {
let retrieval = Retrieval::new("kb_456");
assert_eq!(retrieval.knowledge_id, "kb_456");
assert!(retrieval.prompt_template.is_none());
}
#[test]
fn test_retrieval_serialization() {
let retrieval = Retrieval::new("kb_789");
let json = serde_json::to_string(&retrieval).unwrap();
assert!(json.contains("\"knowledge_id\":\"kb_789\""));
assert!(!json.contains("prompt_template"));
}
#[test]
fn retrieval_prompt_template_serializes() {
let retrieval = Retrieval::new("kb_builder").with_prompt_template("ctx: {knowledge}");
let json = serde_json::to_value(&retrieval).unwrap();
assert_eq!(json["knowledge_id"], "kb_builder");
assert_eq!(json["prompt_template"], "ctx: {knowledge}");
}
#[test]
fn tool_validation_rejects_blank_optional_values() {
assert!(Retrieval::new(" ").validate().is_err());
assert!(
WebSearch::new(SearchEngine::SearchPro)
.with_search_query(" ")
.validate()
.is_err()
);
assert!(MCP::new(" ").validate().is_err());
assert!(
MCP::new("server")
.with_server_url("ftp://example.com")
.validate()
.is_err()
);
assert!(
MCP::new("server")
.with_header("Authorization\r\nInjected", "secret")
.validate()
.is_err()
);
}
#[test]
fn test_web_search_new() {
let web_search = WebSearch::new(SearchEngine::SearchPro);
assert_eq!(web_search.search_engine, SearchEngine::SearchPro);
assert!(web_search.enable.is_none());
}
#[test]
fn test_web_search_with_enable() {
let web_search = WebSearch::new(SearchEngine::SearchPro).with_enable(true);
assert_eq!(web_search.enable, Some(true));
}
#[test]
fn test_web_search_with_search_query() {
let web_search = WebSearch::new(SearchEngine::SearchPro).with_search_query("test query");
assert_eq!(web_search.search_query, Some("test query".to_string()));
}
#[test]
fn test_web_search_with_search_intent() {
let web_search = WebSearch::new(SearchEngine::SearchPro).with_search_intent(true);
assert_eq!(web_search.search_intent, Some(true));
}
#[test]
fn test_web_search_with_count() {
let web_search = WebSearch::new(SearchEngine::SearchPro).with_count(10);
assert_eq!(web_search.count, Some(10));
}
#[test]
fn test_web_search_with_search_domain_filter() {
let web_search =
WebSearch::new(SearchEngine::SearchPro).with_search_domain_filter("example.com");
assert_eq!(
web_search.search_domain_filter,
Some("example.com".to_string())
);
}
#[test]
fn test_web_search_with_search_recency_filter() {
let filter = SearchRecencyFilter::OneDay;
let web_search = WebSearch::new(SearchEngine::SearchPro).with_search_recency_filter(filter);
assert_eq!(web_search.search_recency_filter, Some(filter));
}
#[test]
fn test_web_search_with_content_size() {
let size = ContentSize::Medium;
let web_search = WebSearch::new(SearchEngine::SearchPro).with_content_size(size);
assert_eq!(web_search.content_size, Some(size));
}
#[test]
fn test_web_search_with_result_sequence() {
let seq = ResultSequence::After;
let web_search = WebSearch::new(SearchEngine::SearchPro).with_result_sequence(seq);
assert_eq!(web_search.result_sequence, Some(seq));
}
#[test]
fn test_web_search_with_search_result() {
let web_search = WebSearch::new(SearchEngine::SearchPro).with_search_result(true);
assert_eq!(web_search.search_result, Some(true));
}
#[test]
fn test_web_search_with_require_search() {
let web_search = WebSearch::new(SearchEngine::SearchPro).with_require_search(true);
assert_eq!(web_search.require_search, Some(true));
}
#[test]
fn test_web_search_with_search_prompt() {
let web_search =
WebSearch::new(SearchEngine::SearchPro).with_search_prompt("custom prompt");
assert_eq!(web_search.search_prompt, Some("custom prompt".to_string()));
}
#[test]
fn test_web_search_serialization() {
let web_search = WebSearch::new(SearchEngine::SearchPro)
.with_enable(true)
.with_count(5);
let json = serde_json::to_string(&web_search).unwrap();
assert!(json.contains("\"search_engine\""));
assert!(json.contains("\"enable\":true"));
assert!(json.contains("\"count\":5"));
}
#[test]
fn test_mcp_new() {
let mcp = MCP::new("server_label");
assert_eq!(mcp.server_label, "server_label");
assert_eq!(mcp.transport_type, Some(MCPTransportType::StreamableHttp));
assert!(mcp.allowed_tools.is_empty());
}
#[test]
fn test_mcp_with_server_url() {
let mcp = MCP::new("server_label").with_server_url("https://example.com");
assert_eq!(mcp.server_url, Some("https://example.com".to_string()));
}
#[test]
fn test_mcp_with_transport_type() {
let mcp = MCP::new("server_label").with_transport_type(MCPTransportType::Sse);
assert_eq!(mcp.transport_type, Some(MCPTransportType::Sse));
}
#[test]
fn test_mcp_with_allowed_tools() {
let mcp = MCP::new("server_label")
.with_allowed_tools(vec!["tool1".to_string(), "tool2".to_string()]);
assert_eq!(mcp.allowed_tools.len(), 2);
assert!(mcp.allowed_tools.contains(&"tool1".to_string()));
}
#[test]
fn test_mcp_add_allowed_tool() {
let mcp = MCP::new("server_label")
.add_allowed_tool("tool1")
.add_allowed_tool("tool2");
assert_eq!(mcp.allowed_tools.len(), 2);
}
#[test]
fn test_mcp_with_headers() {
let mut headers = BTreeMap::new();
headers.insert("Authorization".to_string(), "Bearer token".to_string());
let mcp = MCP::new("server_label").with_headers(headers.clone());
assert_eq!(mcp.headers, Some(headers));
}
#[test]
fn test_mcp_with_header() {
let mcp = MCP::new("server_label").with_header("Authorization", "Bearer token");
let debug = format!("{mcp:?}");
assert!(!debug.contains("Bearer token"));
assert!(!debug.contains("Authorization"));
assert!(debug.contains("header_count"));
let headers = mcp.headers.unwrap();
assert_eq!(
headers.get("Authorization"),
Some(&"Bearer token".to_string())
);
}
#[test]
fn test_mcp_serialization() {
let mcp = MCP::new("server_label")
.with_server_url("https://example.com")
.with_transport_type(MCPTransportType::Sse);
let json = serde_json::to_string(&mcp).unwrap();
assert!(json.contains("\"server_label\":\"server_label\""));
assert!(json.contains("\"server_url\":\"https://example.com\""));
assert!(json.contains("\"transport_type\":\"sse\""));
assert!(!json.contains("allowed_tools"));
}
#[test]
fn mcp_validation_rejects_credentialed_or_fragmented_urls() {
assert!(
MCP::new("server")
.with_server_url("https://user:secret@example.com/mcp")
.validate()
.is_err()
);
assert!(
MCP::new("server")
.with_server_url("https://example.com/mcp#secret")
.validate()
.is_err()
);
}
#[test]
fn tool_debug_output_redacts_caller_content_and_remote_configuration() {
let function = Function::new(
"lookup",
"private-description",
serde_json::json!({"private-schema-key": {"type": "string"}}),
);
let retrieval =
Retrieval::new("private-knowledge").with_prompt_template("private-template");
let search = WebSearch::new(SearchEngine::SearchPro)
.with_search_query("private-query")
.with_search_domain_filter("private.example")
.with_search_prompt("private-search-prompt");
let mcp = MCP::new("private-server")
.with_server_url("https://private.example/mcp?token=private-token")
.add_allowed_tool("private-tool")
.with_header("Authorization", "private-header");
let debug = format!("{function:?} {retrieval:?} {search:?} {mcp:?}");
for secret in [
"private-description",
"private-schema-key",
"private-knowledge",
"private-template",
"private-query",
"private.example",
"private-search-prompt",
"private-server",
"private-token",
"private-tool",
"Authorization",
"private-header",
] {
assert!(!debug.contains(secret), "Debug leaked {secret}");
}
}
#[test]
fn test_mcp_transport_type_sse_serialization() {
let transport = MCPTransportType::Sse;
let json = serde_json::to_string(&transport).unwrap();
assert!(json.contains("\"sse\""));
}
#[test]
fn test_mcp_transport_type_streamable_http_serialization() {
let transport = MCPTransportType::StreamableHttp;
let json = serde_json::to_string(&transport).unwrap();
assert!(json.contains("\"streamable-http\""));
}
#[test]
fn test_response_format_text_serialization() {
let format = ResponseFormat::Text;
let json = serde_json::to_string(&format).unwrap();
assert!(json.contains("\"type\":\"text\""));
}
#[test]
fn test_response_format_json_object_serialization() {
let format = ResponseFormat::JsonObject;
let json = serde_json::to_string(&format).unwrap();
assert!(json.contains("\"type\":\"json_object\""));
}
#[test]
fn test_tool_choice_auto_serializes_as_bare_string() {
let json = serde_json::to_value(ToolChoice::auto()).unwrap();
assert_eq!(json, serde_json::json!("auto"));
}
#[test]
fn test_tools_function_serialization() {
let func = Function::new("test_func", "test", serde_json::json!({}));
let tools = Tools::Function { function: func };
let json = serde_json::to_string(&tools).unwrap();
assert!(json.contains("\"type\":\"function\""));
assert!(json.contains("\"name\":\"test_func\""));
}
#[test]
fn test_tools_retrieval_serialization() {
let retrieval = Retrieval::new("kb_123");
let tools = Tools::Retrieval { retrieval };
let json = serde_json::to_string(&tools).unwrap();
assert!(json.contains("\"type\":\"retrieval\""));
assert!(json.contains("\"knowledge_id\":\"kb_123\""));
}
#[test]
fn test_tools_web_search_serialization() {
let web_search = WebSearch::new(SearchEngine::SearchPro);
let tools = Tools::WebSearch { web_search };
let json = serde_json::to_string(&tools).unwrap();
assert!(json.contains("\"type\":\"web_search\""));
assert!(json.contains("\"search_engine\""));
}
#[test]
fn test_tools_mcp_serialization() {
let mcp = MCP::new("server_label");
let tools = Tools::MCP { mcp };
let json = serde_json::to_string(&tools).unwrap();
assert!(json.contains("\"type\":\"mcp\""));
assert!(json.contains("\"server_label\":\"server_label\""));
}
#[test]
fn test_result_sequence_before_serialization() {
let seq = ResultSequence::Before;
let json = serde_json::to_string(&seq).unwrap();
assert!(json.contains("\"before\""));
}
#[test]
fn test_result_sequence_after_serialization() {
let seq = ResultSequence::After;
let json = serde_json::to_string(&seq).unwrap();
assert!(json.contains("\"after\""));
}
}