Codex Handbook
tools/src/tool_spec.rs 133 lines
use crate::FreeformTool;use crate::JsonSchema;use crate::LoadableToolSpec;use crate::ResponsesApiNamespace;use crate::ResponsesApiTool;use codex_protocol::config_types::WebSearchContextSize;use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;use codex_protocol::config_types::WebSearchUserLocationType;use serde::Serialize;use serde_json::Value;/// When serialized as JSON, this produces a valid "Tool" in the OpenAI/// Responses API.#[derive(Debug, Clone, Serialize, PartialEq)]#[serde(tag = "type")]pub enum ToolSpec {    #[serde(rename = "function")]    Function(ResponsesApiTool),    #[serde(rename = "namespace")]    Namespace(ResponsesApiNamespace),    #[serde(rename = "tool_search")]    ToolSearch {        execution: String,        description: String,        parameters: JsonSchema,    },    #[serde(rename = "image_generation")]    ImageGeneration { output_format: String },    // TODO: Understand why we get an error on web_search although the API docs    // say it's supported.    // https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#:~:text=%7B%20type%3A%20%22web_search%22%20%7D%2C    // The `external_web_access` field determines whether the web search is over    // cached or live content.    // https://platform.openai.com/docs/guides/tools-web-search#live-internet-access    #[serde(rename = "web_search")]    WebSearch {        #[serde(skip_serializing_if = "Option::is_none")]        external_web_access: Option<bool>,        #[serde(skip_serializing_if = "Option::is_none")]        filters: Option<ResponsesApiWebSearchFilters>,        #[serde(skip_serializing_if = "Option::is_none")]        user_location: Option<ResponsesApiWebSearchUserLocation>,        #[serde(skip_serializing_if = "Option::is_none")]        search_context_size: Option<WebSearchContextSize>,        #[serde(skip_serializing_if = "Option::is_none")]        search_content_types: Option<Vec<String>>,    },    #[serde(rename = "custom")]    Freeform(FreeformTool),}impl ToolSpec {    pub fn name(&self) -> &str {        match self {            ToolSpec::Function(tool) => tool.name.as_str(),            ToolSpec::Namespace(namespace) => namespace.name.as_str(),            ToolSpec::ToolSearch { .. } => "tool_search",            ToolSpec::ImageGeneration { .. } => "image_generation",            ToolSpec::WebSearch { .. } => "web_search",            ToolSpec::Freeform(tool) => tool.name.as_str(),        }    }}impl From<LoadableToolSpec> for ToolSpec {    fn from(value: LoadableToolSpec) -> Self {        match value {            LoadableToolSpec::Function(tool) => ToolSpec::Function(tool),            LoadableToolSpec::Namespace(namespace) => ToolSpec::Namespace(namespace),        }    }}/// Returns JSON values that are compatible with Function Calling in the/// Responses API:/// https://platform.openai.com/docs/guides/function-calling?api-mode=responsespub fn create_tools_json_for_responses_api(    tools: &[ToolSpec],) -> Result<Vec<Value>, serde_json::Error> {    let mut tools_json = Vec::new();    for tool in tools {        let json = serde_json::to_value(tool)?;        tools_json.push(json);    }    Ok(tools_json)}#[derive(Debug, Clone, Serialize, PartialEq)]pub struct ResponsesApiWebSearchFilters {    #[serde(skip_serializing_if = "Option::is_none")]    pub allowed_domains: Option<Vec<String>>,}impl From<ConfigWebSearchFilters> for ResponsesApiWebSearchFilters {    fn from(filters: ConfigWebSearchFilters) -> Self {        Self {            allowed_domains: filters.allowed_domains,        }    }}#[derive(Debug, Clone, Serialize, PartialEq)]pub struct ResponsesApiWebSearchUserLocation {    #[serde(rename = "type")]    pub r#type: WebSearchUserLocationType,    #[serde(skip_serializing_if = "Option::is_none")]    pub country: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    pub region: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    pub city: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    pub timezone: Option<String>,}impl From<ConfigWebSearchUserLocation> for ResponsesApiWebSearchUserLocation {    fn from(user_location: ConfigWebSearchUserLocation) -> Self {        Self {            r#type: user_location.r#type,            country: user_location.country,            region: user_location.region,            city: user_location.city,            timezone: user_location.timezone,        }    }}#[cfg(test)]#[path = "tool_spec_tests.rs"]mod tests;