mirror of
https://github.com/openai/codex.git
synced 2026-05-04 21:32:21 +03:00
stacked on #17402. MCP tools returned by `tool_search` (deferred tools) get registered in our `ToolRegistry` with a different format than directly available tools. this leads to two different ways of accessing MCP tools from our tool catalog, only one of which works for each. fix this by registering all MCP tools with the namespace format, since this info is already available. also, direct MCP tools are registered to responsesapi without a namespace, while deferred MCP tools have a namespace. this means we can receive MCP `FunctionCall`s in both formats from namespaces. fix this by always registering MCP tools with namespace, regardless of deferral status. make code mode track `ToolName` provenance of tools so it can map the literal JS function name string to the correct `ToolName` for invocation, rather than supporting both in core. this lets us unify to a single canonical `ToolName` representation for each MCP tool and force everywhere to use that one, without supporting fallbacks.
199 lines
6.6 KiB
Rust
199 lines
6.6 KiB
Rust
use crate::FreeformTool;
|
|
use crate::JsonSchema;
|
|
use crate::ResponsesApiNamespace;
|
|
use crate::ResponsesApiTool;
|
|
use codex_protocol::config_types::WebSearchConfig;
|
|
use codex_protocol::config_types::WebSearchContextSize;
|
|
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
|
use codex_protocol::config_types::WebSearchMode;
|
|
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
|
use codex_protocol::config_types::WebSearchUserLocationType;
|
|
use codex_protocol::openai_models::WebSearchToolType;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
const WEB_SEARCH_TEXT_AND_IMAGE_CONTENT_TYPES: [&str; 2] = ["text", "image"];
|
|
|
|
/// 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 = "local_shell")]
|
|
LocalShell {},
|
|
#[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::LocalShell {} => "local_shell",
|
|
ToolSpec::ImageGeneration { .. } => "image_generation",
|
|
ToolSpec::WebSearch { .. } => "web_search",
|
|
ToolSpec::Freeform(tool) => tool.name.as_str(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn create_local_shell_tool() -> ToolSpec {
|
|
ToolSpec::LocalShell {}
|
|
}
|
|
|
|
pub fn create_image_generation_tool(output_format: &str) -> ToolSpec {
|
|
ToolSpec::ImageGeneration {
|
|
output_format: output_format.to_string(),
|
|
}
|
|
}
|
|
|
|
pub struct WebSearchToolOptions<'a> {
|
|
pub web_search_mode: Option<WebSearchMode>,
|
|
pub web_search_config: Option<&'a WebSearchConfig>,
|
|
pub web_search_tool_type: WebSearchToolType,
|
|
}
|
|
|
|
pub fn create_web_search_tool(options: WebSearchToolOptions<'_>) -> Option<ToolSpec> {
|
|
let external_web_access = match options.web_search_mode {
|
|
Some(WebSearchMode::Cached) => Some(false),
|
|
Some(WebSearchMode::Live) => Some(true),
|
|
Some(WebSearchMode::Disabled) | None => None,
|
|
}?;
|
|
|
|
let search_content_types = match options.web_search_tool_type {
|
|
WebSearchToolType::Text => None,
|
|
WebSearchToolType::TextAndImage => Some(
|
|
WEB_SEARCH_TEXT_AND_IMAGE_CONTENT_TYPES
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect(),
|
|
),
|
|
};
|
|
|
|
Some(ToolSpec::WebSearch {
|
|
external_web_access: Some(external_web_access),
|
|
filters: options
|
|
.web_search_config
|
|
.and_then(|config| config.filters.clone().map(Into::into)),
|
|
user_location: options
|
|
.web_search_config
|
|
.and_then(|config| config.user_location.clone().map(Into::into)),
|
|
search_context_size: options
|
|
.web_search_config
|
|
.and_then(|config| config.search_context_size),
|
|
search_content_types,
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct ConfiguredToolSpec {
|
|
pub spec: ToolSpec,
|
|
pub supports_parallel_tool_calls: bool,
|
|
}
|
|
|
|
impl ConfiguredToolSpec {
|
|
pub fn new(spec: ToolSpec, supports_parallel_tool_calls: bool) -> Self {
|
|
Self {
|
|
spec,
|
|
supports_parallel_tool_calls,
|
|
}
|
|
}
|
|
|
|
pub fn name(&self) -> &str {
|
|
self.spec.name()
|
|
}
|
|
}
|
|
|
|
/// Returns JSON values that are compatible with Function Calling in the
|
|
/// Responses API:
|
|
/// https://platform.openai.com/docs/guides/function-calling?api-mode=responses
|
|
pub 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;
|