mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Support model-specific function description prefixes (#51930)
## What changed Add `functions_namespace_functions_description_prefixes` to model tool messages, keyed by unqualified tool name. Prepend this guidance to descriptions in the `functions` namespace across direct declarations, Code Mode documentation, and tool-search results, including custom tools. Trim prefixes, ignore empty values and unavailable functions, and enforce a combined 256-byte UTF-8 limit including separators for reachable tools. Keep overrides local to each step, preserve search ranking text, and retain runtime caches for unaffected tools. ## Testing Extend coverage for direct and indirect descriptions, clearing prefixes when switching models, namespace scoping for deferred dynamic tools, and tool-search cache reuse and reset. GitOrigin-RevId: f434e37cec010764fe3e09fb12c339e933b3c21c
This commit is contained in:
@@ -74,11 +74,11 @@ impl ToolSearchHandlerCache {
|
||||
.entries()
|
||||
.filter(|tool| tool.exposure.is_deferred())
|
||||
.filter_map(|tool| {
|
||||
if tool.runtime.immutable_spec().is_some() {
|
||||
Some(ToolSearchSource::Immutable(Arc::downgrade(&tool.runtime)))
|
||||
if let Some(runtime) = tool.cached_runtime() {
|
||||
Some(ToolSearchSource::Immutable(Arc::downgrade(runtime)))
|
||||
} else {
|
||||
tool.runtime
|
||||
.search_info()
|
||||
registry
|
||||
.search_info(tool)
|
||||
.map(Box::new)
|
||||
.map(ToolSearchSource::Dynamic)
|
||||
}
|
||||
@@ -313,12 +313,12 @@ impl ToolSearchHandler {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(runtime) = router.tool_runtime(tool_name) else {
|
||||
let Some(tool) = router.registered_tool(tool_name) else {
|
||||
continue;
|
||||
};
|
||||
let definitions = prepare_code_mode_tool_definitions(
|
||||
Some(runtime.as_ref()),
|
||||
|| Cow::Owned(runtime.spec()),
|
||||
tool.cached_runtime().map(Arc::as_ref),
|
||||
|| Cow::Owned(Arc::unwrap_or_clone(tool.spec())),
|
||||
code_mode_input_schema_max_bytes,
|
||||
tool_description_first,
|
||||
);
|
||||
@@ -375,10 +375,10 @@ mod tests {
|
||||
#[test]
|
||||
fn cache_reuses_immutable_handlers_and_rebuilds_for_current_registry_changes() {
|
||||
let cache = ToolSearchHandlerCache::default();
|
||||
let runtime: Arc<dyn CoreToolRuntime> = Arc::new(
|
||||
McpHandler::new(tool_info("calendar", "create_event", "Create events"))
|
||||
.expect("MCP tool should convert"),
|
||||
);
|
||||
let mut info = tool_info("calendar", "create_event", "Create events");
|
||||
info.callable_namespace = "functions".to_string();
|
||||
let runtime: Arc<dyn CoreToolRuntime> =
|
||||
Arc::new(McpHandler::new(info.clone()).expect("MCP tool should convert"));
|
||||
let mut registry = ToolRegistry::default();
|
||||
registry.register_trusted_with_exposure(Arc::clone(&runtime), ToolExposure::Deferred);
|
||||
|
||||
@@ -390,14 +390,46 @@ mod tests {
|
||||
assert!(!Arc::ptr_eq(&first, &without_sources));
|
||||
|
||||
let mut replacement_registry = ToolRegistry::default();
|
||||
let replacement = Arc::new(
|
||||
McpHandler::new(tool_info("calendar", "create_event", "Create events"))
|
||||
.expect("replacement MCP tool should convert"),
|
||||
);
|
||||
let replacement =
|
||||
Arc::new(McpHandler::new(info).expect("replacement MCP tool should convert"));
|
||||
replacement_registry.register_trusted_with_exposure(replacement, ToolExposure::Deferred);
|
||||
let replacement = cache.get_or_build(&replacement_registry, ToolSearchSourceListing::Omit);
|
||||
assert!(!Arc::ptr_eq(&without_sources, &replacement));
|
||||
|
||||
registry
|
||||
.apply_functions_namespace_function_prefixes(
|
||||
Some(&std::collections::BTreeMap::from([(
|
||||
"create_event".to_string(),
|
||||
"Guidance.".to_string(),
|
||||
)])),
|
||||
[ToolName::plain("create_event")],
|
||||
)
|
||||
.expect("valid function prefixes");
|
||||
let prefixed = cache.get_or_build(®istry, ToolSearchSourceListing::Include);
|
||||
let reused = cache.get_or_build(®istry, ToolSearchSourceListing::Include);
|
||||
assert!(Arc::ptr_eq(&prefixed, &reused));
|
||||
assert_eq!(
|
||||
prefixed.search_infos[0].entry.search_text,
|
||||
first.search_infos[0].entry.search_text
|
||||
);
|
||||
let mut expected = first.search("Create events", /*limit*/ 1);
|
||||
let LoadableToolSpec::Namespace(namespace) = &mut expected[0] else {
|
||||
panic!("namespace")
|
||||
};
|
||||
let ResponsesApiNamespaceTool::Function(tool) = &mut namespace.tools[0] else {
|
||||
panic!("function")
|
||||
};
|
||||
tool.description = format!("Guidance.\n\n{}", tool.description);
|
||||
assert_eq!(prefixed.search("Create events", /*limit*/ 1), expected);
|
||||
registry
|
||||
.apply_functions_namespace_function_prefixes(/*prefixes*/ None, [])
|
||||
.expect("clear prefixes");
|
||||
let cleared = cache.get_or_build(®istry, ToolSearchSourceListing::Include);
|
||||
assert_eq!(
|
||||
cleared.search("Create events", /*limit*/ 1),
|
||||
first.search("Create events", /*limit*/ 1)
|
||||
);
|
||||
|
||||
let mut disabled_registry = ToolRegistry::default();
|
||||
disabled_registry.register_trusted_with_exposure(runtime, ToolExposure::Direct);
|
||||
let disabled = cache.get_or_build(&disabled_registry, ToolSearchSourceListing::Omit);
|
||||
|
||||
@@ -139,7 +139,9 @@ impl ToolCallRuntime {
|
||||
.record_tool_call(&call, &source, &step_context);
|
||||
let router = &step_context.tool_router;
|
||||
let supports_parallel = router.tool_supports_parallel(&call);
|
||||
let tool_runtime = router.tool_runtime(&call.tool_name);
|
||||
let tool_runtime = router
|
||||
.registered_tool(&call.tool_name)
|
||||
.map(|tool| Arc::clone(&tool.runtime));
|
||||
let finishes_on_cancellation = tool_runtime
|
||||
.as_ref()
|
||||
.is_some_and(|runtime| runtime.finishes_on_cancellation());
|
||||
|
||||
@@ -38,7 +38,9 @@ use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_rollout::state_db;
|
||||
use codex_shell_command::parse_command::parse_shell_script;
|
||||
use codex_tools::FunctionsNamespaceFunctionPrefixes;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use futures::future::BoxFuture;
|
||||
use indexmap::IndexMap;
|
||||
@@ -303,16 +305,83 @@ pub(crate) struct PostToolUsePayload {
|
||||
pub(crate) struct RegisteredTool {
|
||||
pub(crate) runtime: Arc<dyn CoreToolRuntime>,
|
||||
pub(crate) exposure: ToolExposure,
|
||||
/// Per-step description override; never written back to a shared runtime.
|
||||
pub(crate) model_spec: Option<Arc<ToolSpec>>,
|
||||
}
|
||||
|
||||
impl RegisteredTool {
|
||||
/// Direct declarations and Code Mode read this spec; execution uses the runtime.
|
||||
pub(crate) fn spec(&self) -> Arc<ToolSpec> {
|
||||
self.model_spec
|
||||
.as_ref()
|
||||
.or_else(|| self.runtime.immutable_spec())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Arc::new(self.runtime.spec()))
|
||||
}
|
||||
|
||||
/// Runtime caches describe the original spec, so cannot serve an overridden one.
|
||||
pub(crate) fn cached_runtime(&self) -> Option<&Arc<dyn CoreToolRuntime>> {
|
||||
(self.model_spec.is_none() && self.runtime.immutable_spec().is_some())
|
||||
.then_some(&self.runtime)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ToolRegistry {
|
||||
tools: IndexMap<ToolName, RegisteredTool>,
|
||||
functions_namespace_function_prefixes: FunctionsNamespaceFunctionPrefixes,
|
||||
first_collision: Option<ToolName>,
|
||||
pub(crate) tool_policy: Arc<ToolPolicy>,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
pub(crate) fn search_info(&self, tool: &RegisteredTool) -> Option<ToolSearchInfo> {
|
||||
let info = tool.runtime.search_info()?;
|
||||
// A runtime may advertise a different search schema or description. Preserve it,
|
||||
// along with its ranking text and source, rather than substituting the normal spec.
|
||||
match self
|
||||
.functions_namespace_function_prefixes
|
||||
.prepare(&tool.runtime.tool_name(), || {
|
||||
info.entry.to_loadable_spec().into()
|
||||
}) {
|
||||
Some(spec) => ToolSearchInfo::from_spec(info.entry.search_text, spec, info.source_info),
|
||||
None => Some(info),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply catalog prefixes to this step's specs, budgeting only reachable tools.
|
||||
/// The planner supplies the names; this does not change execution or exposure.
|
||||
pub(crate) fn apply_functions_namespace_function_prefixes(
|
||||
&mut self,
|
||||
prefixes: Option<&BTreeMap<String, String>>,
|
||||
available_tools: impl IntoIterator<Item = ToolName>,
|
||||
) -> Result<(), &'static str> {
|
||||
let prefixes = FunctionsNamespaceFunctionPrefixes::new(prefixes, available_tools)?;
|
||||
for tool in self.tools.values_mut() {
|
||||
tool.model_spec = prefixes
|
||||
.prepare(&tool.runtime.tool_name(), || tool.runtime.spec())
|
||||
.map(Arc::new);
|
||||
}
|
||||
self.functions_namespace_function_prefixes = prefixes;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_tool(
|
||||
&self,
|
||||
runtime: Arc<dyn CoreToolRuntime>,
|
||||
exposure: ToolExposure,
|
||||
) -> RegisteredTool {
|
||||
let model_spec = self
|
||||
.functions_namespace_function_prefixes
|
||||
.prepare(&runtime.tool_name(), || runtime.spec())
|
||||
.map(Arc::new);
|
||||
RegisteredTool {
|
||||
runtime,
|
||||
exposure,
|
||||
model_spec,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_tool_policy(tool_policy: Arc<ToolPolicy>) -> Self {
|
||||
Self {
|
||||
tool_policy,
|
||||
@@ -359,9 +428,10 @@ impl ToolRegistry {
|
||||
if !self.tool_policy.allows(&tool_name) {
|
||||
return;
|
||||
}
|
||||
let tool = self.prepare_tool(runtime, exposure);
|
||||
match self.tools.entry(tool_name) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(RegisteredTool { runtime, exposure });
|
||||
entry.insert(tool);
|
||||
}
|
||||
Entry::Occupied(entry) => {
|
||||
let tool_name = entry.key();
|
||||
@@ -381,8 +451,8 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
let exposure = runtime.exposure();
|
||||
self.tools
|
||||
.shift_insert(0, tool_name, RegisteredTool { runtime, exposure });
|
||||
let tool = self.prepare_tool(runtime, exposure);
|
||||
self.tools.shift_insert(0, tool_name, tool);
|
||||
}
|
||||
|
||||
pub(crate) fn register_external(&mut self, runtime: Arc<dyn CoreToolRuntime>) -> bool {
|
||||
@@ -409,9 +479,10 @@ impl ToolRegistry {
|
||||
return false;
|
||||
}
|
||||
|
||||
let tool = self.prepare_tool(runtime, exposure);
|
||||
match self.tools.entry(tool_name) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(RegisteredTool { runtime, exposure });
|
||||
entry.insert(tool);
|
||||
true
|
||||
}
|
||||
Entry::Occupied(entry) => {
|
||||
@@ -503,11 +574,14 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
pub(crate) fn tool(&self, name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
|
||||
self.tools
|
||||
.get(&name.clone().with_default_namespace())
|
||||
self.registered_tool(name)
|
||||
.map(|tool| Arc::clone(&tool.runtime))
|
||||
}
|
||||
|
||||
pub(crate) fn registered_tool(&self, name: &ToolName) -> Option<&RegisteredTool> {
|
||||
self.tools.get(&name.clone().with_default_namespace())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tool_names_for_test(&self) -> Vec<ToolName> {
|
||||
let mut names = self.tools.keys().cloned().collect::<Vec<_>>();
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::tools::context::ToolPayload;
|
||||
#[cfg(test)]
|
||||
use crate::tools::handlers::ToolSearchHandlerCache;
|
||||
use crate::tools::registry::AnyToolResult;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::RegisteredTool;
|
||||
use crate::tools::registry::ToolArgumentDiffConsumer;
|
||||
use crate::tools::registry::ToolExposure;
|
||||
use crate::tools::registry::ToolRegistry;
|
||||
@@ -249,8 +249,8 @@ impl ToolRouter {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn tool_runtime(&self, tool_name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
|
||||
self.registry.tool(tool_name)
|
||||
pub(crate) fn registered_tool(&self, tool_name: &ToolName) -> Option<&RegisteredTool> {
|
||||
self.registry.registered_tool(tool_name)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all, err)]
|
||||
|
||||
@@ -190,8 +190,8 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
|
||||
|
||||
assert_eq!(
|
||||
router
|
||||
.tool_runtime(&ToolName::plain(parallel_tool_name))
|
||||
.map(|runtime| runtime.tool_name()),
|
||||
.registered_tool(&ToolName::plain(parallel_tool_name))
|
||||
.map(|tool| tool.runtime.tool_name()),
|
||||
Some(ToolName::plain(parallel_tool_name))
|
||||
);
|
||||
|
||||
@@ -362,8 +362,8 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
|
||||
assert!(router.tool_supports_parallel(&call));
|
||||
assert_eq!(
|
||||
router
|
||||
.tool_runtime(&call.tool_name)
|
||||
.map(|runtime| runtime.tool_name()),
|
||||
.registered_tool(&call.tool_name)
|
||||
.map(|tool| tool.runtime.tool_name()),
|
||||
Some(call.tool_name.clone())
|
||||
);
|
||||
|
||||
@@ -378,8 +378,8 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
|
||||
assert!(!router.tool_supports_parallel(&different_server_call));
|
||||
assert_eq!(
|
||||
router
|
||||
.tool_runtime(&different_server_call.tool_name)
|
||||
.map(|runtime| runtime.tool_name()),
|
||||
.registered_tool(&different_server_call.tool_name)
|
||||
.map(|tool| tool.runtime.tool_name()),
|
||||
Some(different_server_call.tool_name.clone())
|
||||
);
|
||||
|
||||
@@ -392,7 +392,7 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
|
||||
encrypted_function_args: None,
|
||||
};
|
||||
assert!(!router.tool_supports_parallel(&hidden_call));
|
||||
assert!(router.tool_runtime(&hidden_call.tool_name).is_some());
|
||||
assert!(router.registered_tool(&hidden_call.tool_name).is_some());
|
||||
|
||||
let nested_only_call = ToolCall {
|
||||
tool_name: ToolName::namespaced("mcp__nested_echo__", "query_with_delay"),
|
||||
@@ -524,6 +524,7 @@ fn mcp_runtime(tool_info: codex_mcp::ToolInfo) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
exposure: runtime.exposure(),
|
||||
runtime,
|
||||
model_spec: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
|
||||
use crate::tools::hosted_spec::WebSearchToolOptions;
|
||||
use crate::tools::hosted_spec::create_web_search_tool;
|
||||
use crate::tools::multi_agent_tool::multi_agent_v2_handler;
|
||||
#[cfg(test)]
|
||||
use crate::tools::registry::RegisteredTool;
|
||||
use crate::tools::registry::ToolExposure;
|
||||
use crate::tools::registry::ToolRegistry;
|
||||
@@ -374,6 +373,7 @@ pub(crate) fn finalize_tool_router(
|
||||
mut hosted_specs: Vec<ToolSpec>,
|
||||
tool_search_handler_cache: &ToolSearchHandlerCache,
|
||||
) -> CodexResult<ToolRouter> {
|
||||
let model_messages = ResolvedModelMessages::from_model(model_info);
|
||||
hosted_specs.retain(|spec| registry.tool_policy.allows(&ToolName::plain(spec.name())));
|
||||
apply_direct_model_only_namespace_overrides(turn_context, &mut registry);
|
||||
enforce_strict_3p_tools(turn_context, model_info, &mut registry);
|
||||
@@ -390,13 +390,32 @@ pub(crate) fn finalize_tool_router(
|
||||
}
|
||||
}
|
||||
let tool_search_name = ToolName::plain(TOOL_SEARCH_TOOL_NAME);
|
||||
if model_info.supports_search_tool
|
||||
let search_exposure = if model_info.supports_search_tool
|
||||
&& registry.tool_policy.allows(&tool_search_name)
|
||||
&& registry.entries().any(|tool| {
|
||||
tool.runtime.tool_name() != tool_search_name
|
||||
&& tool.exposure.is_deferred()
|
||||
&& tool.runtime.search_info().is_some()
|
||||
})
|
||||
{
|
||||
}) {
|
||||
Some(
|
||||
if turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::CodeModeToolSearch)
|
||||
{
|
||||
tool_exposure_with_namespace_override(
|
||||
turn_context,
|
||||
tool_search_name.clone(),
|
||||
ToolExposure::Direct,
|
||||
)
|
||||
} else {
|
||||
ToolExposure::Direct
|
||||
},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if search_exposure.is_some() {
|
||||
if registry.remove(&tool_search_name).is_some() {
|
||||
registry.record_collision(tool_search_name.clone());
|
||||
}
|
||||
@@ -423,10 +442,24 @@ pub(crate) fn finalize_tool_router(
|
||||
registry.record_collision(tool_name);
|
||||
}
|
||||
}
|
||||
append_tool_search_executor(turn_context, &mut registry, tool_search_handler_cache);
|
||||
}
|
||||
|
||||
let model_messages = ResolvedModelMessages::from_model(model_info);
|
||||
prepare_function_description_prefixes(
|
||||
turn_context,
|
||||
model_info,
|
||||
&mut registry,
|
||||
search_exposure,
|
||||
)?;
|
||||
|
||||
if let Some(exposure) = search_exposure {
|
||||
append_tool_search_executor(
|
||||
turn_context,
|
||||
&mut registry,
|
||||
tool_search_handler_cache,
|
||||
exposure,
|
||||
);
|
||||
}
|
||||
|
||||
let indirect_prefixes = IndirectNamespacePrefixes::new(
|
||||
model_messages.indirect_description_prefixes(),
|
||||
registry.mcp_namespaces(),
|
||||
@@ -621,16 +654,12 @@ fn build_model_visible_specs(
|
||||
let mut specs = Vec::new();
|
||||
for tool in registry.entries() {
|
||||
let exposure = tool.exposure;
|
||||
if !exposure.is_direct() {
|
||||
if !is_directly_visible(turn_context, model_info, tool) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tool_name = tool.runtime.tool_name();
|
||||
if is_hidden_by_code_mode_only(turn_context, model_info, &tool_name, exposure) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let spec = tool.runtime.spec();
|
||||
let spec = Arc::unwrap_or_clone(tool.spec());
|
||||
specs.push(spec_for_model_request(
|
||||
turn_context,
|
||||
model_info,
|
||||
@@ -849,6 +878,20 @@ fn is_hidden_by_code_mode_only(
|
||||
))
|
||||
}
|
||||
|
||||
fn is_directly_visible(
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
tool: &RegisteredTool,
|
||||
) -> bool {
|
||||
tool.exposure.is_direct()
|
||||
&& !is_hidden_by_code_mode_only(
|
||||
turn_context,
|
||||
model_info,
|
||||
&tool.runtime.tool_name(),
|
||||
tool.exposure,
|
||||
)
|
||||
}
|
||||
|
||||
fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName) -> bool {
|
||||
let tool_name = tool_name.clone().with_default_namespace();
|
||||
tool_name.namespace.as_ref().is_some_and(|namespace| {
|
||||
@@ -860,6 +903,131 @@ fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_function_description_prefixes(
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
registry: &mut ToolRegistry,
|
||||
search_exposure: Option<ToolExposure>,
|
||||
) -> CodexResult<()> {
|
||||
let model_messages = ResolvedModelMessages::from_model(model_info);
|
||||
let prefixes = model_messages.functions_namespace_functions_description_prefixes();
|
||||
let mut available_tools = Vec::new();
|
||||
if prefixes.is_some_and(|prefixes| !prefixes.is_empty()) {
|
||||
let code_mode_enabled = matches!(
|
||||
effective_tool_mode(turn_context, model_info),
|
||||
ToolMode::CodeMode | ToolMode::CodeModeOnly
|
||||
);
|
||||
let search_name = ToolName::plain(TOOL_SEARCH_TOOL_NAME);
|
||||
let search_visible = search_exposure.is_some_and(|exposure| {
|
||||
!is_hidden_by_code_mode_only(turn_context, model_info, &search_name, exposure)
|
||||
|| (code_mode_enabled
|
||||
&& registry
|
||||
.tool_policy
|
||||
.allows(&ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME))
|
||||
&& turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::CodeModeToolSearch)
|
||||
&& exposure.is_available_in_code_mode()
|
||||
&& !is_excluded_from_code_mode(turn_context, &search_name))
|
||||
});
|
||||
available_tools.extend(
|
||||
registry
|
||||
.entries()
|
||||
.filter(|tool| {
|
||||
is_directly_visible(turn_context, model_info, tool)
|
||||
|| (search_visible
|
||||
&& tool.exposure.is_deferred()
|
||||
&& tool.runtime.search_info().is_some())
|
||||
})
|
||||
.map(|tool| tool.runtime.tool_name()),
|
||||
);
|
||||
available_tools.extend(
|
||||
code_mode_tools(turn_context, model_info, registry)
|
||||
.map(|(_, _, tool)| tool.runtime.tool_name()),
|
||||
);
|
||||
// These wrappers are generated after their inner descriptions are ready.
|
||||
if code_mode_enabled {
|
||||
available_tools.extend(
|
||||
[
|
||||
codex_code_mode::PUBLIC_TOOL_NAME,
|
||||
codex_code_mode::WAIT_TOOL_NAME,
|
||||
]
|
||||
.into_iter()
|
||||
.map(ToolName::plain)
|
||||
.filter(|name| registry.tool_policy.allows(name)),
|
||||
);
|
||||
}
|
||||
}
|
||||
registry
|
||||
.apply_functions_namespace_function_prefixes(prefixes, available_tools)
|
||||
.map_err(|error| CodexErrorDetails::InvalidRequest(error.to_owned()).into())
|
||||
}
|
||||
|
||||
/// Select the same nested tools for prefix budgeting and Code Mode construction.
|
||||
/// A registered tool alone is not reachable when policy disallows the exec wrapper.
|
||||
fn code_mode_tools<'a>(
|
||||
turn_context: &'a TurnContext,
|
||||
model_info: &'a ModelInfo,
|
||||
registry: &'a ToolRegistry,
|
||||
) -> impl Iterator<Item = (String, Arc<ToolSpec>, &'a RegisteredTool)> {
|
||||
let tool_mode = effective_tool_mode(turn_context, model_info);
|
||||
let enabled = matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly)
|
||||
&& registry
|
||||
.tool_policy
|
||||
.allows(&ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME));
|
||||
let mut seen = HashSet::new();
|
||||
registry.entries().filter_map(move |tool| {
|
||||
if !enabled || !tool.exposure.is_available_in_code_mode() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tool_name = tool.runtime.tool_name();
|
||||
// Deferred third-party tools still need an exec route to be discoverable.
|
||||
if is_excluded_from_code_mode(turn_context, &tool_name)
|
||||
&& !(code_mode_only_strict_3p_tools(turn_context, model_info)
|
||||
&& tool.runtime.is_third_party_tool())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let spec = tool.spec();
|
||||
// Derive the name without serializing and augmenting every tool schema.
|
||||
let code_mode_name = match spec.as_ref() {
|
||||
ToolSpec::Function(_) | ToolSpec::Freeform(_) => {
|
||||
codex_tools::code_mode_name_for_tool_name(&tool_name)
|
||||
}
|
||||
ToolSpec::Namespace(namespace) if !namespace.tools.is_empty() => {
|
||||
codex_tools::code_mode_name_for_tool_name(&tool_name)
|
||||
}
|
||||
ToolSpec::ToolSearch { .. }
|
||||
if turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::CodeModeToolSearch) =>
|
||||
{
|
||||
codex_tools::code_mode_name_for_tool_name(&tool_name)
|
||||
}
|
||||
ToolSpec::Namespace(_) | ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !codex_code_mode::is_code_mode_nested_tool(&code_mode_name) {
|
||||
return None;
|
||||
}
|
||||
let code_mode_name = codex_code_mode::normalize_code_mode_identifier(&code_mode_name);
|
||||
if !seen.insert(code_mode_name.clone()) {
|
||||
tracing::warn!(
|
||||
tool_name = %tool_name,
|
||||
code_mode_name = %code_mode_name,
|
||||
"skipping tool with a duplicate normalized code-mode name"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some((code_mode_name, spec, tool))
|
||||
})
|
||||
}
|
||||
|
||||
fn register_code_mode_executors(
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
@@ -877,64 +1045,10 @@ fn register_code_mode_executors(
|
||||
let mut deferred_exec_prompt_tool_specs = Vec::new();
|
||||
let mut included_deferred_mcp_output_schema = false;
|
||||
let deferred_tools_guidance_enabled = model_info.supports_search_tool;
|
||||
for tool in registry.entries() {
|
||||
for (name, spec, tool) in code_mode_tools(turn_context, model_info, registry) {
|
||||
code_mode_tool_names.insert(name, tool.runtime.tool_name());
|
||||
let exposure = tool.exposure;
|
||||
if !exposure.is_available_in_code_mode() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tool_name = tool.runtime.tool_name();
|
||||
// Deferred third-party tools still need an exec route to be discoverable.
|
||||
if is_excluded_from_code_mode(turn_context, &tool_name)
|
||||
&& !(code_mode_only_strict_3p_tools(turn_context, model_info)
|
||||
&& tool.runtime.is_third_party_tool())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let immutable_spec = tool.runtime.immutable_spec();
|
||||
let cached_runtime = immutable_spec.map(|_| Arc::clone(&tool.runtime));
|
||||
let spec = immutable_spec
|
||||
.map(Arc::clone)
|
||||
.unwrap_or_else(|| Arc::new(tool.runtime.spec()));
|
||||
// Derive the name without serializing and augmenting every tool schema.
|
||||
let code_mode_name = match spec.as_ref() {
|
||||
ToolSpec::Function(_) | ToolSpec::Freeform(_) => {
|
||||
codex_tools::code_mode_name_for_tool_name(&tool_name)
|
||||
}
|
||||
ToolSpec::Namespace(namespace) if !namespace.tools.is_empty() => {
|
||||
codex_tools::code_mode_name_for_tool_name(&tool_name)
|
||||
}
|
||||
ToolSpec::ToolSearch { .. }
|
||||
if turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::CodeModeToolSearch) =>
|
||||
{
|
||||
codex_tools::code_mode_name_for_tool_name(&tool_name)
|
||||
}
|
||||
ToolSpec::Namespace(_) | ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if !codex_code_mode::is_code_mode_nested_tool(&code_mode_name) {
|
||||
continue;
|
||||
}
|
||||
match code_mode_tool_names.entry(codex_code_mode::normalize_code_mode_identifier(
|
||||
&code_mode_name,
|
||||
)) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(tool_name);
|
||||
}
|
||||
Entry::Occupied(_) => {
|
||||
tracing::warn!(
|
||||
tool_name = %tool_name,
|
||||
code_mode_name = %code_mode_name,
|
||||
"skipping tool with a duplicate normalized code-mode name"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let cached_runtime = tool.cached_runtime().cloned();
|
||||
|
||||
if exposure == ToolExposure::Deferred {
|
||||
if deferred_tools_guidance_enabled
|
||||
@@ -1542,6 +1656,7 @@ fn append_tool_search_executor(
|
||||
turn_context: &TurnContext,
|
||||
registry: &mut ToolRegistry,
|
||||
tool_search_handler_cache: &ToolSearchHandlerCache,
|
||||
exposure: ToolExposure,
|
||||
) {
|
||||
let source_listing = if turn_context
|
||||
.config
|
||||
@@ -1553,16 +1668,6 @@ fn append_tool_search_executor(
|
||||
ToolSearchSourceListing::Include
|
||||
};
|
||||
let handler = tool_search_handler_cache.get_or_build(registry, source_listing);
|
||||
// Preserve native search's exposure when nested search is disabled.
|
||||
let exposure = if turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::CodeModeToolSearch)
|
||||
{
|
||||
tool_exposure_with_namespace_override(turn_context, handler.tool_name(), handler.exposure())
|
||||
} else {
|
||||
handler.exposure()
|
||||
};
|
||||
registry.register_trusted_with_exposure(handler, exposure);
|
||||
}
|
||||
|
||||
|
||||
@@ -430,6 +430,7 @@ fn mcp_runtime(
|
||||
RegisteredTool {
|
||||
runtime: handler,
|
||||
exposure,
|
||||
model_spec: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1766,6 +1767,7 @@ async fn strict_namespace_ownership_requires_tool_namespace_inventory_opt_in() {
|
||||
RegisteredTool {
|
||||
runtime: Arc::new(McpHandler::new(tool).expect("MCP tool spec should build")),
|
||||
exposure,
|
||||
model_spec: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -2062,6 +2064,7 @@ async fn strict_tool_collisions_allow_multiple_tools_in_one_namespace() {
|
||||
McpHandler::new(undocumented_tool).expect("MCP tool spec should build"),
|
||||
),
|
||||
exposure: ToolExposure::Direct,
|
||||
model_spec: None,
|
||||
},
|
||||
mcp_runtime("shared", "shared", "lookup", ToolExposure::Direct),
|
||||
mcp_runtime("shared", "shared", "list", ToolExposure::Direct),
|
||||
@@ -2101,6 +2104,7 @@ async fn relaxed_tool_collisions_preserve_first_nonempty_namespace_description()
|
||||
RegisteredTool {
|
||||
runtime: Arc::new(McpHandler::new(tool).expect("MCP tool spec should build")),
|
||||
exposure: ToolExposure::Direct,
|
||||
model_spec: None,
|
||||
}
|
||||
};
|
||||
let plan = probe_with(
|
||||
|
||||
Reference in New Issue
Block a user