mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
1879 lines
64 KiB
Rust
1879 lines
64 KiB
Rust
use super::*;
|
|
use crate::AdditionalProperties;
|
|
use crate::ConfiguredToolSpec;
|
|
use crate::DiscoverablePluginInfo;
|
|
use crate::DiscoverableTool;
|
|
use crate::FreeformTool;
|
|
use crate::JsonSchema;
|
|
use crate::ResponsesApiTool;
|
|
use crate::ResponsesApiWebSearchFilters;
|
|
use crate::ResponsesApiWebSearchUserLocation;
|
|
use crate::ToolHandlerSpec;
|
|
use crate::ToolRegistryPlanAppTool;
|
|
use crate::ToolsConfigParams;
|
|
use crate::WaitAgentTimeoutOptions;
|
|
use crate::mcp_call_tool_result_output_schema;
|
|
use codex_app_server_protocol::AppInfo;
|
|
use codex_features::Feature;
|
|
use codex_features::Features;
|
|
use codex_protocol::config_types::WebSearchConfig;
|
|
use codex_protocol::config_types::WebSearchMode;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
|
use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
|
|
use codex_protocol::openai_models::InputModality;
|
|
use codex_protocol::openai_models::ModelInfo;
|
|
use codex_protocol::openai_models::WebSearchToolType;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashMap;
|
|
|
|
const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
|
|
const DEFAULT_AGENT_TYPE_DESCRIPTION: &str = "Test agent type description.";
|
|
const DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000;
|
|
const MIN_WAIT_TIMEOUT_MS: i64 = 10_000;
|
|
const MAX_WAIT_TIMEOUT_MS: i64 = 3_600_000;
|
|
|
|
#[test]
|
|
fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let mut actual = BTreeMap::new();
|
|
let mut duplicate_names = Vec::new();
|
|
for tool in &tools {
|
|
let name = tool.name().to_string();
|
|
if actual.insert(name.clone(), tool.spec.clone()).is_some() {
|
|
duplicate_names.push(name);
|
|
}
|
|
}
|
|
assert!(
|
|
duplicate_names.is_empty(),
|
|
"duplicate tool entries detected: {duplicate_names:?}"
|
|
);
|
|
|
|
let mut expected = BTreeMap::new();
|
|
for spec in [
|
|
create_exec_command_tool(CommandToolOptions {
|
|
allow_login_shell: true,
|
|
exec_permission_approvals_enabled: false,
|
|
}),
|
|
create_write_stdin_tool(),
|
|
create_update_plan_tool(),
|
|
request_user_input_tool_spec(/*default_mode_request_user_input*/ false),
|
|
create_apply_patch_freeform_tool(),
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
},
|
|
create_view_image_tool(ViewImageToolOptions {
|
|
can_request_original_image_detail: config.can_request_original_image_detail,
|
|
}),
|
|
] {
|
|
expected.insert(spec.name().to_string(), spec);
|
|
}
|
|
let collab_specs = if config.multi_agent_v2 {
|
|
vec![
|
|
create_spawn_agent_tool_v2(spawn_agent_tool_options(&config)),
|
|
create_send_message_tool(),
|
|
create_wait_agent_tool_v2(wait_agent_timeout_options()),
|
|
create_close_agent_tool_v2(),
|
|
]
|
|
} else {
|
|
vec![
|
|
create_spawn_agent_tool_v1(spawn_agent_tool_options(&config)),
|
|
create_send_input_tool_v1(),
|
|
create_wait_agent_tool_v1(wait_agent_timeout_options()),
|
|
create_close_agent_tool_v1(),
|
|
]
|
|
};
|
|
for spec in collab_specs {
|
|
expected.insert(spec.name().to_string(), spec);
|
|
}
|
|
if !config.multi_agent_v2 {
|
|
let spec = create_resume_agent_tool();
|
|
expected.insert(spec.name().to_string(), spec);
|
|
}
|
|
|
|
if config.exec_permission_approvals_enabled {
|
|
let spec = create_request_permissions_tool(request_permissions_tool_description());
|
|
expected.insert(spec.name().to_string(), spec);
|
|
}
|
|
|
|
assert_eq!(
|
|
actual.keys().collect::<Vec<_>>(),
|
|
expected.keys().collect::<Vec<_>>(),
|
|
"tool name set mismatch"
|
|
);
|
|
|
|
for name in expected.keys() {
|
|
let mut actual_spec = actual.get(name).expect("present").clone();
|
|
let mut expected_spec = expected.get(name).expect("present").clone();
|
|
strip_descriptions_tool(&mut actual_spec);
|
|
strip_descriptions_tool(&mut expected_spec);
|
|
assert_eq!(actual_spec, expected_spec, "spec mismatch for {name}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_collab_tools_enabled() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Collab);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&["spawn_agent", "send_input", "wait_agent", "close_agent"],
|
|
);
|
|
assert_lacks_tool_name(&tools, "spawn_agents_on_csv");
|
|
assert_lacks_tool_name(&tools, "list_agents");
|
|
|
|
let spawn_agent = find_tool(&tools, "spawn_agent");
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &spawn_agent.spec else {
|
|
panic!("spawn_agent should be a function tool");
|
|
};
|
|
let JsonSchema::Object { properties, .. } = parameters else {
|
|
panic!("spawn_agent should use object params");
|
|
};
|
|
assert!(properties.contains_key("fork_context"));
|
|
assert!(!properties.contains_key("fork_turns"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Collab);
|
|
features.enable(Feature::MultiAgentV2);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"spawn_agent",
|
|
"send_message",
|
|
"followup_task",
|
|
"wait_agent",
|
|
"close_agent",
|
|
"list_agents",
|
|
],
|
|
);
|
|
|
|
let spawn_agent = find_tool(&tools, "spawn_agent");
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
parameters,
|
|
output_schema,
|
|
..
|
|
}) = &spawn_agent.spec
|
|
else {
|
|
panic!("spawn_agent should be a function tool");
|
|
};
|
|
let JsonSchema::Object {
|
|
properties,
|
|
required,
|
|
..
|
|
} = parameters
|
|
else {
|
|
panic!("spawn_agent should use object params");
|
|
};
|
|
assert!(properties.contains_key("task_name"));
|
|
assert!(properties.contains_key("message"));
|
|
assert!(properties.contains_key("fork_turns"));
|
|
assert!(!properties.contains_key("items"));
|
|
assert!(!properties.contains_key("fork_context"));
|
|
assert_eq!(
|
|
required.as_ref(),
|
|
Some(&vec!["task_name".to_string(), "message".to_string()])
|
|
);
|
|
let output_schema = output_schema
|
|
.as_ref()
|
|
.expect("spawn_agent should define output schema");
|
|
assert_eq!(
|
|
output_schema["required"],
|
|
json!(["agent_id", "task_name", "nickname"])
|
|
);
|
|
|
|
let send_message = find_tool(&tools, "send_message");
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &send_message.spec else {
|
|
panic!("send_message should be a function tool");
|
|
};
|
|
let JsonSchema::Object {
|
|
properties,
|
|
required,
|
|
..
|
|
} = parameters
|
|
else {
|
|
panic!("send_message should use object params");
|
|
};
|
|
assert!(properties.contains_key("target"));
|
|
assert!(!properties.contains_key("interrupt"));
|
|
assert!(properties.contains_key("message"));
|
|
assert!(!properties.contains_key("items"));
|
|
assert_eq!(
|
|
required.as_ref(),
|
|
Some(&vec!["target".to_string(), "message".to_string()])
|
|
);
|
|
|
|
let followup_task = find_tool(&tools, "followup_task");
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &followup_task.spec else {
|
|
panic!("followup_task should be a function tool");
|
|
};
|
|
let JsonSchema::Object {
|
|
properties,
|
|
required,
|
|
..
|
|
} = parameters
|
|
else {
|
|
panic!("followup_task should use object params");
|
|
};
|
|
assert!(properties.contains_key("target"));
|
|
assert!(properties.contains_key("message"));
|
|
assert!(!properties.contains_key("items"));
|
|
assert_eq!(
|
|
required.as_ref(),
|
|
Some(&vec!["target".to_string(), "message".to_string()])
|
|
);
|
|
|
|
let wait_agent = find_tool(&tools, "wait_agent");
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
parameters,
|
|
output_schema,
|
|
..
|
|
}) = &wait_agent.spec
|
|
else {
|
|
panic!("wait_agent should be a function tool");
|
|
};
|
|
let JsonSchema::Object {
|
|
properties,
|
|
required,
|
|
..
|
|
} = parameters
|
|
else {
|
|
panic!("wait_agent should use object params");
|
|
};
|
|
assert!(!properties.contains_key("targets"));
|
|
assert!(properties.contains_key("timeout_ms"));
|
|
assert_eq!(required, &None);
|
|
let output_schema = output_schema
|
|
.as_ref()
|
|
.expect("wait_agent should define output schema");
|
|
assert_eq!(
|
|
output_schema["properties"]["message"]["description"],
|
|
json!("Brief wait summary without the agent's final content.")
|
|
);
|
|
|
|
let list_agents = find_tool(&tools, "list_agents");
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
parameters,
|
|
output_schema,
|
|
..
|
|
}) = &list_agents.spec
|
|
else {
|
|
panic!("list_agents should be a function tool");
|
|
};
|
|
let JsonSchema::Object {
|
|
properties,
|
|
required,
|
|
..
|
|
} = parameters
|
|
else {
|
|
panic!("list_agents should use object params");
|
|
};
|
|
assert!(properties.contains_key("path_prefix"));
|
|
assert_eq!(required.as_ref(), None);
|
|
let output_schema = output_schema
|
|
.as_ref()
|
|
.expect("list_agents should define output schema");
|
|
assert_eq!(
|
|
output_schema["properties"]["agents"]["items"]["required"],
|
|
json!(["agent_name", "agent_status", "last_task_message"])
|
|
);
|
|
assert_lacks_tool_name(&tools, "send_input");
|
|
assert_lacks_tool_name(&tools, "resume_agent");
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::SpawnCsv);
|
|
features.normalize_dependencies();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"spawn_agent",
|
|
"send_input",
|
|
"wait_agent",
|
|
"close_agent",
|
|
"spawn_agents_on_csv",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn view_image_tool_omits_detail_without_original_detail_feature() {
|
|
let mut model_info = model_info();
|
|
model_info.supports_image_detail_original = true;
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else {
|
|
panic!("view_image should be a function tool");
|
|
};
|
|
let JsonSchema::Object { properties, .. } = parameters else {
|
|
panic!("view_image should use an object schema");
|
|
};
|
|
assert!(!properties.contains_key("detail"));
|
|
}
|
|
|
|
#[test]
|
|
fn view_image_tool_includes_detail_with_original_detail_feature() {
|
|
let mut model_info = model_info();
|
|
model_info.supports_image_detail_original = true;
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ImageDetailOriginal);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else {
|
|
panic!("view_image should be a function tool");
|
|
};
|
|
let JsonSchema::Object { properties, .. } = parameters else {
|
|
panic!("view_image should use an object schema");
|
|
};
|
|
assert!(properties.contains_key("detail"));
|
|
let Some(JsonSchema::String {
|
|
description: Some(description),
|
|
}) = properties.get("detail")
|
|
else {
|
|
panic!("view_image detail should include a description");
|
|
};
|
|
assert!(description.contains("only supported value is `original`"));
|
|
assert!(description.contains("omit this field for default resized behavior"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_agent_job_worker_tools_enabled() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::SpawnCsv);
|
|
features.normalize_dependencies();
|
|
features.enable(Feature::Sqlite);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::SubAgent(SubAgentSource::Other(
|
|
"agent_job:test".to_string(),
|
|
)),
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
"spawn_agents_on_csv",
|
|
"report_agent_job_result",
|
|
],
|
|
);
|
|
assert_lacks_tool_name(&tools, "request_user_input");
|
|
}
|
|
|
|
#[test]
|
|
fn request_user_input_description_reflects_default_mode_feature_flag() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
|
|
assert_eq!(
|
|
request_user_input_tool.spec,
|
|
request_user_input_tool_spec(/*default_mode_request_user_input*/ false)
|
|
);
|
|
|
|
features.enable(Feature::DefaultModeRequestUserInput);
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
|
|
assert_eq!(
|
|
request_user_input_tool.spec,
|
|
request_user_input_tool_spec(/*default_mode_request_user_input*/ true)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_requires_feature_flag() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
assert_lacks_tool_name(&tools, "request_permissions");
|
|
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::RequestPermissionsTool);
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
let request_permissions_tool = find_tool(&tools, "request_permissions");
|
|
assert_eq!(
|
|
request_permissions_tool.spec,
|
|
create_request_permissions_tool(request_permissions_tool_description())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_tool_is_independent_from_additional_permissions() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ExecPermissionApprovals);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_lacks_tool_name(&tools, "request_permissions");
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_requires_feature_flag() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert!(
|
|
!tools.iter().any(|tool| tool.spec.name() == "js_repl"),
|
|
"js_repl should be disabled when the feature is off"
|
|
);
|
|
assert!(
|
|
!tools.iter().any(|tool| tool.spec.name() == "js_repl_reset"),
|
|
"js_repl_reset should be disabled when the feature is off"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_enabled_adds_tools() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::JsRepl);
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]);
|
|
}
|
|
|
|
#[test]
|
|
fn image_generation_tools_require_feature_and_supported_model() {
|
|
let supported_model_info = model_info();
|
|
let mut unsupported_model_info = supported_model_info.clone();
|
|
unsupported_model_info.input_modalities = vec![InputModality::Text];
|
|
let default_features = Features::with_defaults();
|
|
let mut image_generation_features = default_features.clone();
|
|
image_generation_features.enable(Feature::ImageGeneration);
|
|
|
|
let available_models = Vec::new();
|
|
let default_tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &supported_model_info,
|
|
available_models: &available_models,
|
|
features: &default_features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (default_tools, _) = build_specs(
|
|
&default_tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
assert!(
|
|
!default_tools
|
|
.iter()
|
|
.any(|tool| tool.spec.name() == "image_generation"),
|
|
"image_generation should be disabled by default"
|
|
);
|
|
|
|
let supported_tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &supported_model_info,
|
|
available_models: &available_models,
|
|
features: &image_generation_features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (supported_tools, _) = build_specs(
|
|
&supported_tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
assert_contains_tool_names(&supported_tools, &["image_generation"]);
|
|
let image_generation_tool = find_tool(&supported_tools, "image_generation");
|
|
assert_eq!(
|
|
serde_json::to_value(&image_generation_tool.spec).expect("serialize image tool"),
|
|
serde_json::json!({
|
|
"type": "image_generation",
|
|
"output_format": "png"
|
|
})
|
|
);
|
|
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &unsupported_model_info,
|
|
available_models: &available_models,
|
|
features: &image_generation_features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
assert!(
|
|
!tools
|
|
.iter()
|
|
.any(|tool| tool.spec.name() == "image_generation"),
|
|
"image_generation should be disabled for unsupported models"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_mode_cached_sets_external_web_access_false() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(false),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_mode_live_sets_external_web_access_true() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_config_is_forwarded_to_tool_spec() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
let web_search_config = WebSearchConfig {
|
|
filters: Some(codex_protocol::config_types::WebSearchFilters {
|
|
allowed_domains: Some(vec!["example.com".to_string()]),
|
|
}),
|
|
user_location: Some(codex_protocol::config_types::WebSearchUserLocation {
|
|
r#type: codex_protocol::config_types::WebSearchUserLocationType::Approximate,
|
|
country: Some("US".to_string()),
|
|
region: Some("California".to_string()),
|
|
city: Some("San Francisco".to_string()),
|
|
timezone: Some("America/Los_Angeles".to_string()),
|
|
}),
|
|
search_context_size: Some(codex_protocol::config_types::WebSearchContextSize::High),
|
|
};
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
})
|
|
.with_web_search_config(Some(web_search_config.clone()));
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: web_search_config
|
|
.filters
|
|
.map(ResponsesApiWebSearchFilters::from),
|
|
user_location: web_search_config
|
|
.user_location
|
|
.map(ResponsesApiWebSearchUserLocation::from),
|
|
search_context_size: web_search_config.search_context_size,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_tool_type_text_and_image_sets_search_content_types() {
|
|
let mut model_info = model_info();
|
|
model_info.web_search_tool_type = WebSearchToolType::TextAndImage;
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: Some(vec!["text".to_string(), "image".to_string()]),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_resource_tools_are_hidden_without_mcp_servers() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert!(
|
|
!tools.iter().any(|tool| matches!(
|
|
tool.spec.name(),
|
|
"list_mcp_resources" | "list_mcp_resource_templates" | "read_mcp_resource"
|
|
)),
|
|
"MCP resource tools should be omitted when no MCP servers are configured"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_resource_tools_are_included_when_mcp_servers_are_present() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::new()),
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"list_mcp_resources",
|
|
"list_mcp_resource_templates",
|
|
"read_mcp_resource",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn test_parallel_support_flags() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert!(find_tool(&tools, "exec_command").supports_parallel_tool_calls);
|
|
assert!(!find_tool(&tools, "write_stdin").supports_parallel_tool_calls);
|
|
}
|
|
|
|
#[test]
|
|
fn test_test_model_info_includes_sync_tool() {
|
|
let mut model_info = model_info();
|
|
model_info.experimental_supported_tools = vec!["test_sync_tool".to_string()];
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_mcp_tools_converted() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"test_server/do_something_cool".to_string(),
|
|
mcp_tool(
|
|
"do_something_cool",
|
|
"Do something cool",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"string_argument": { "type": "string" },
|
|
"number_argument": { "type": "number" },
|
|
"object_argument": {
|
|
"type": "object",
|
|
"properties": {
|
|
"string_property": { "type": "string" },
|
|
"number_property": { "type": "number" },
|
|
},
|
|
"required": ["string_property", "number_property"],
|
|
"additionalProperties": false,
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
)])),
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "test_server/do_something_cool");
|
|
assert_eq!(
|
|
&tool.spec,
|
|
&ToolSpec::Function(ResponsesApiTool {
|
|
name: "test_server/do_something_cool".to_string(),
|
|
parameters: JsonSchema::Object {
|
|
properties: BTreeMap::from([
|
|
(
|
|
"string_argument".to_string(),
|
|
JsonSchema::String { description: None }
|
|
),
|
|
(
|
|
"number_argument".to_string(),
|
|
JsonSchema::Number { description: None }
|
|
),
|
|
(
|
|
"object_argument".to_string(),
|
|
JsonSchema::Object {
|
|
properties: BTreeMap::from([
|
|
(
|
|
"string_property".to_string(),
|
|
JsonSchema::String { description: None }
|
|
),
|
|
(
|
|
"number_property".to_string(),
|
|
JsonSchema::Number { description: None }
|
|
),
|
|
]),
|
|
required: Some(vec![
|
|
"string_property".to_string(),
|
|
"number_property".to_string(),
|
|
]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
),
|
|
]),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
description: "Do something cool".to_string(),
|
|
strict: false,
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
|
|
defer_loading: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_mcp_tools_sorted_by_name() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let tools_map = HashMap::from([
|
|
(
|
|
"test_server/do".to_string(),
|
|
mcp_tool("a", "a", serde_json::json!({"type": "object"})),
|
|
),
|
|
(
|
|
"test_server/something".to_string(),
|
|
mcp_tool("b", "b", serde_json::json!({"type": "object"})),
|
|
),
|
|
(
|
|
"test_server/cool".to_string(),
|
|
mcp_tool("c", "c", serde_json::json!({"type": "object"})),
|
|
),
|
|
]);
|
|
|
|
let (tools, _) = build_specs(&tools_config, Some(tools_map), /*app_tools*/ None, &[]);
|
|
|
|
let mcp_names: Vec<_> = tools
|
|
.iter()
|
|
.map(|tool| tool.name().to_string())
|
|
.filter(|name| name.starts_with("test_server/"))
|
|
.collect();
|
|
let expected = vec![
|
|
"test_server/cool".to_string(),
|
|
"test_server/do".to_string(),
|
|
"test_server/something".to_string(),
|
|
];
|
|
assert_eq!(mcp_names, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_description_lists_each_codex_apps_connector_once() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::ToolSearch);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([
|
|
(
|
|
"mcp__codex_apps__calendar_create_event".to_string(),
|
|
mcp_tool(
|
|
"calendar_create_event",
|
|
"Create calendar event",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
),
|
|
(
|
|
"mcp__rmcp__echo".to_string(),
|
|
mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
|
),
|
|
])),
|
|
Some(vec![
|
|
app_tool(
|
|
"_create_event",
|
|
"mcp__codex_apps__calendar",
|
|
CODEX_APPS_MCP_SERVER_NAME,
|
|
Some("Calendar"),
|
|
Some("Plan events and manage your calendar."),
|
|
),
|
|
app_tool(
|
|
"_list_events",
|
|
"mcp__codex_apps__calendar",
|
|
CODEX_APPS_MCP_SERVER_NAME,
|
|
Some("Calendar"),
|
|
Some("Plan events and manage your calendar."),
|
|
),
|
|
app_tool(
|
|
"_search_threads",
|
|
"mcp__codex_apps__gmail",
|
|
CODEX_APPS_MCP_SERVER_NAME,
|
|
Some("Gmail"),
|
|
Some("Find and summarize email threads."),
|
|
),
|
|
app_tool(
|
|
"echo", "rmcp", "rmcp", /*connector_name*/ None,
|
|
/*connector_description*/ None,
|
|
),
|
|
]),
|
|
&[],
|
|
);
|
|
|
|
let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else {
|
|
panic!("expected tool_search tool");
|
|
};
|
|
let description = description.as_str();
|
|
assert!(description.contains("- Calendar: Plan events and manage your calendar."));
|
|
assert!(description.contains("- Gmail: Find and summarize email threads."));
|
|
assert_eq!(
|
|
description
|
|
.matches("- Calendar: Plan events and manage your calendar.")
|
|
.count(),
|
|
1
|
|
);
|
|
assert!(!description.contains("mcp__rmcp__echo"));
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_requires_model_capability_and_feature_flag() {
|
|
let model_info = search_capable_model_info();
|
|
let app_tools = Some(vec![app_tool(
|
|
"calendar_create_event",
|
|
"mcp__codex_apps__calendar",
|
|
CODEX_APPS_MCP_SERVER_NAME,
|
|
Some("Calendar"),
|
|
/*connector_description*/ None,
|
|
)]);
|
|
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &ModelInfo {
|
|
supports_search_tool: false,
|
|
..model_info.clone()
|
|
},
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
app_tools.clone(),
|
|
&[],
|
|
);
|
|
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
app_tools.clone(),
|
|
&[],
|
|
);
|
|
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ToolSearch);
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, app_tools, &[]);
|
|
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_is_not_registered_without_feature_flag() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ToolSearch);
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::Plugins);
|
|
features.disable(Feature::ToolSuggest);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs_with_discoverable_tools(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
Some(vec![discoverable_connector(
|
|
"connector_2128aebfecb84f64a069897515042a44",
|
|
"Google Calendar",
|
|
"Plan events and schedules.",
|
|
)]),
|
|
&[],
|
|
);
|
|
|
|
assert!(
|
|
!tools
|
|
.iter()
|
|
.any(|tool| tool.name() == TOOL_SUGGEST_TOOL_NAME)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_can_be_registered_without_search_tool() {
|
|
let model_info = ModelInfo {
|
|
supports_search_tool: false,
|
|
..search_capable_model_info()
|
|
};
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::Plugins);
|
|
features.enable(Feature::ToolSuggest);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs_with_discoverable_tools(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
Some(vec![discoverable_connector(
|
|
"connector_2128aebfecb84f64a069897515042a44",
|
|
"Google Calendar",
|
|
"Plan events and schedules.",
|
|
)]),
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(&tools, &[TOOL_SUGGEST_TOOL_NAME]);
|
|
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
|
|
let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) = &tool_suggest.spec else {
|
|
panic!("expected function tool");
|
|
};
|
|
assert!(description.contains(
|
|
"Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin"
|
|
));
|
|
assert!(description.contains(
|
|
"You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means."
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_description_lists_discoverable_tools() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::Plugins);
|
|
features.enable(Feature::ToolSearch);
|
|
features.enable(Feature::ToolSuggest);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let discoverable_tools = vec![
|
|
discoverable_connector(
|
|
"connector_2128aebfecb84f64a069897515042a44",
|
|
"Google Calendar",
|
|
"Plan events and schedules.",
|
|
),
|
|
discoverable_connector(
|
|
"connector_68df038e0ba48191908c8434991bbac2",
|
|
"Gmail",
|
|
"Find and summarize email threads.",
|
|
),
|
|
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
|
|
id: "sample@test".to_string(),
|
|
name: "Sample Plugin".to_string(),
|
|
description: None,
|
|
has_skills: true,
|
|
mcp_server_names: vec!["sample-docs".to_string()],
|
|
app_connector_ids: vec!["connector_sample".to_string()],
|
|
})),
|
|
];
|
|
|
|
let (tools, _) = build_specs_with_discoverable_tools(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
Some(discoverable_tools),
|
|
&[],
|
|
);
|
|
|
|
let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
description,
|
|
parameters,
|
|
..
|
|
}) = &tool_suggest.spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
assert!(description.contains(
|
|
"Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin"
|
|
));
|
|
assert!(description.contains("Google Calendar"));
|
|
assert!(description.contains("Gmail"));
|
|
assert!(description.contains("Sample Plugin"));
|
|
assert!(description.contains("Plan events and schedules."));
|
|
assert!(description.contains("Find and summarize email threads."));
|
|
assert!(description.contains("id: `sample@test`, type: plugin, action: install"));
|
|
assert!(description.contains("`action_type`: `install` or `enable`"));
|
|
assert!(
|
|
description.contains("skills; MCP servers: sample-docs; app connectors: connector_sample")
|
|
);
|
|
assert!(
|
|
description.contains(
|
|
"You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means."
|
|
)
|
|
);
|
|
assert!(description.contains(
|
|
"For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely."
|
|
));
|
|
assert!(description.contains(
|
|
"For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself."
|
|
));
|
|
assert!(description.contains(
|
|
"Do not suggest a plugin just because one of its connectors or capabilities seems relevant."
|
|
));
|
|
assert!(description.contains(
|
|
"Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard."
|
|
));
|
|
assert!(description.contains("DO NOT explore or recommend tools that are not on this list."));
|
|
assert!(!description.contains("{{discoverable_tools}}"));
|
|
assert!(!description.contains("tool_search fails to find a good match"));
|
|
let JsonSchema::Object { required, .. } = parameters else {
|
|
panic!("expected object parameters");
|
|
};
|
|
assert_eq!(
|
|
required.as_ref(),
|
|
Some(&vec![
|
|
"tool_type".to_string(),
|
|
"action_type".to_string(),
|
|
"tool_id".to_string(),
|
|
"suggest_reason".to_string(),
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"mcp__sample__echo".to_string(),
|
|
mcp_tool(
|
|
"echo",
|
|
"Echo text",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"message": {"type": "string"}
|
|
},
|
|
"required": ["message"],
|
|
"additionalProperties": false
|
|
}),
|
|
),
|
|
)])),
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
|
|
&find_tool(&tools, "mcp__sample__echo").spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
|
|
assert_eq!(
|
|
description,
|
|
"Echo text\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };\n```"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
|
|
&find_tool(&tools, VIEW_IMAGE_TOOL_NAME).spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
|
|
assert_eq!(
|
|
description,
|
|
"View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags).\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: { path: string; }): Promise<{ detail: string | null; image_url: string; }>; };\n```"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_only_exec_description_includes_full_nested_tool_details() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::CodeModeOnly);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
|
|
else {
|
|
panic!("expected freeform tool");
|
|
};
|
|
|
|
assert!(!description.contains("Enabled nested tools:"));
|
|
assert!(!description.contains("Nested tool reference:"));
|
|
assert!(description.starts_with(
|
|
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"
|
|
));
|
|
assert!(description.contains("### `update_plan` (`update_plan`)"));
|
|
assert!(description.contains("### `view_image` (`view_image`)"));
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*app_tools*/ None,
|
|
&[],
|
|
);
|
|
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
|
|
else {
|
|
panic!("expected freeform tool");
|
|
};
|
|
|
|
assert!(!description.starts_with(
|
|
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"
|
|
));
|
|
assert!(!description.contains("### `update_plan` (`update_plan`)"));
|
|
assert!(!description.contains("### `view_image` (`view_image`)"));
|
|
}
|
|
|
|
fn model_info() -> ModelInfo {
|
|
serde_json::from_value(json!({
|
|
"slug": "gpt-5-codex",
|
|
"display_name": "GPT-5 Codex",
|
|
"description": null,
|
|
"supported_reasoning_levels": [],
|
|
"shell_type": "shell_command",
|
|
"visibility": "list",
|
|
"supported_in_api": true,
|
|
"priority": 1,
|
|
"availability_nux": null,
|
|
"upgrade": null,
|
|
"base_instructions": "base",
|
|
"model_messages": null,
|
|
"supports_reasoning_summaries": false,
|
|
"default_reasoning_summary": "auto",
|
|
"support_verbosity": false,
|
|
"default_verbosity": null,
|
|
"apply_patch_tool_type": "freeform",
|
|
"truncation_policy": {
|
|
"mode": "bytes",
|
|
"limit": 10000
|
|
},
|
|
"supports_parallel_tool_calls": false,
|
|
"supports_image_detail_original": false,
|
|
"context_window": null,
|
|
"auto_compact_token_limit": null,
|
|
"effective_context_window_percent": 95,
|
|
"experimental_supported_tools": [],
|
|
"input_modalities": ["text", "image"],
|
|
"supports_search_tool": false
|
|
}))
|
|
.expect("deserialize test model")
|
|
}
|
|
|
|
fn search_capable_model_info() -> ModelInfo {
|
|
ModelInfo {
|
|
supports_search_tool: true,
|
|
..model_info()
|
|
}
|
|
}
|
|
|
|
fn build_specs<'a>(
|
|
config: &ToolsConfig,
|
|
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
|
app_tools: Option<Vec<ToolRegistryPlanAppTool<'a>>>,
|
|
dynamic_tools: &[DynamicToolSpec],
|
|
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
|
|
build_specs_with_discoverable_tools(
|
|
config,
|
|
mcp_tools,
|
|
app_tools,
|
|
/*discoverable_tools*/ None,
|
|
dynamic_tools,
|
|
)
|
|
}
|
|
|
|
fn build_specs_with_discoverable_tools<'a>(
|
|
config: &ToolsConfig,
|
|
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
|
app_tools: Option<Vec<ToolRegistryPlanAppTool<'a>>>,
|
|
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
|
dynamic_tools: &[DynamicToolSpec],
|
|
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
|
|
let plan = build_tool_registry_plan(
|
|
config,
|
|
ToolRegistryPlanParams {
|
|
mcp_tools: mcp_tools.as_ref(),
|
|
app_tools: app_tools.as_deref(),
|
|
discoverable_tools: discoverable_tools.as_deref(),
|
|
dynamic_tools,
|
|
default_agent_type_description: DEFAULT_AGENT_TYPE_DESCRIPTION,
|
|
wait_agent_timeouts: wait_agent_timeout_options(),
|
|
codex_apps_mcp_server_name: CODEX_APPS_MCP_SERVER_NAME,
|
|
},
|
|
);
|
|
(plan.specs, plan.handlers)
|
|
}
|
|
|
|
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
|
|
rmcp::model::Tool {
|
|
name: name.to_string().into(),
|
|
title: None,
|
|
description: Some(description.to_string().into()),
|
|
input_schema: std::sync::Arc::new(rmcp::model::object(input_schema)),
|
|
output_schema: None,
|
|
annotations: None,
|
|
execution: None,
|
|
icons: None,
|
|
meta: None,
|
|
}
|
|
}
|
|
|
|
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
|
|
let slug = name.replace(' ', "-").to_lowercase();
|
|
DiscoverableTool::Connector(Box::new(AppInfo {
|
|
id: id.to_string(),
|
|
name: name.to_string(),
|
|
description: Some(description.to_string()),
|
|
logo_url: None,
|
|
logo_url_dark: None,
|
|
distribution_channel: None,
|
|
branding: None,
|
|
app_metadata: None,
|
|
labels: None,
|
|
install_url: Some(format!("https://chatgpt.com/apps/{slug}/{id}")),
|
|
is_accessible: false,
|
|
is_enabled: true,
|
|
plugin_display_names: Vec::new(),
|
|
}))
|
|
}
|
|
|
|
fn app_tool<'a>(
|
|
tool_name: &'a str,
|
|
tool_namespace: &'a str,
|
|
server_name: &'a str,
|
|
connector_name: Option<&'a str>,
|
|
connector_description: Option<&'a str>,
|
|
) -> ToolRegistryPlanAppTool<'a> {
|
|
ToolRegistryPlanAppTool {
|
|
tool_name,
|
|
tool_namespace,
|
|
server_name,
|
|
connector_name,
|
|
connector_description,
|
|
}
|
|
}
|
|
|
|
fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&str]) {
|
|
use std::collections::HashSet;
|
|
|
|
let mut names = HashSet::new();
|
|
let mut duplicates = Vec::new();
|
|
for name in tools.iter().map(ConfiguredToolSpec::name) {
|
|
if !names.insert(name) {
|
|
duplicates.push(name);
|
|
}
|
|
}
|
|
assert!(
|
|
duplicates.is_empty(),
|
|
"duplicate tool entries detected: {duplicates:?}"
|
|
);
|
|
for expected in expected_subset {
|
|
assert!(
|
|
names.contains(expected),
|
|
"expected tool {expected} to be present; had: {names:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
|
|
let names = tools
|
|
.iter()
|
|
.map(ConfiguredToolSpec::name)
|
|
.collect::<Vec<_>>();
|
|
assert!(
|
|
!names.contains(&expected_absent),
|
|
"expected tool {expected_absent} to be absent; had: {names:?}"
|
|
);
|
|
}
|
|
|
|
fn request_user_input_tool_spec(default_mode_request_user_input: bool) -> ToolSpec {
|
|
create_request_user_input_tool(request_user_input_tool_description(
|
|
default_mode_request_user_input,
|
|
))
|
|
}
|
|
|
|
fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions<'_> {
|
|
SpawnAgentToolOptions {
|
|
available_models: &config.available_models,
|
|
agent_type_description: agent_type_description(config, DEFAULT_AGENT_TYPE_DESCRIPTION),
|
|
}
|
|
}
|
|
|
|
fn wait_agent_timeout_options() -> WaitAgentTimeoutOptions {
|
|
WaitAgentTimeoutOptions {
|
|
default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS,
|
|
min_timeout_ms: MIN_WAIT_TIMEOUT_MS,
|
|
max_timeout_ms: MAX_WAIT_TIMEOUT_MS,
|
|
}
|
|
}
|
|
|
|
fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a ConfiguredToolSpec {
|
|
tools
|
|
.iter()
|
|
.find(|tool| tool.name() == expected_name)
|
|
.unwrap_or_else(|| panic!("expected tool {expected_name}"))
|
|
}
|
|
|
|
fn strip_descriptions_schema(schema: &mut JsonSchema) {
|
|
match schema {
|
|
JsonSchema::Boolean { description }
|
|
| JsonSchema::String { description }
|
|
| JsonSchema::Number { description } => {
|
|
*description = None;
|
|
}
|
|
JsonSchema::Array { items, description } => {
|
|
strip_descriptions_schema(items);
|
|
*description = None;
|
|
}
|
|
JsonSchema::Object {
|
|
properties,
|
|
required: _,
|
|
additional_properties,
|
|
} => {
|
|
for value in properties.values_mut() {
|
|
strip_descriptions_schema(value);
|
|
}
|
|
if let Some(AdditionalProperties::Schema(schema)) = additional_properties {
|
|
strip_descriptions_schema(schema);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn strip_descriptions_tool(spec: &mut ToolSpec) {
|
|
match spec {
|
|
ToolSpec::ToolSearch { parameters, .. } => strip_descriptions_schema(parameters),
|
|
ToolSpec::Function(ResponsesApiTool { parameters, .. }) => {
|
|
strip_descriptions_schema(parameters);
|
|
}
|
|
ToolSpec::Freeform(FreeformTool { .. })
|
|
| ToolSpec::LocalShell {}
|
|
| ToolSpec::ImageGeneration { .. }
|
|
| ToolSpec::WebSearch { .. } => {}
|
|
}
|
|
}
|