feat: disable capabilities by model provider (#19442)

## Why

Unsupported features must fail closed and Codex must not expose
OpenAI-hosted fallback paths when the active provider cannot support
them. In practice, Bedrock should not surface app connectors, MCP
servers, tool search/suggestions, image generation, web search, or JS
REPL until those paths are explicitly supported for that provider.

This PR moves that decision into provider-owned capability metadata
instead of scattering Bedrock-specific checks across callers.

## What changed

- Adds `ProviderCapabilities` to `codex-model-provider`, with default
support for existing providers and a Bedrock override that disables
unsupported launch surfaces.
- Adds `ToolCapabilityBounds` to `codex-tools` so provider capability
limits can clamp otherwise-enabled tool config.
- Applies capability bounds when building session and review-thread tool
config.
- Routes MCP/app connector configuration through
`McpManager::mcp_config`, which filters configured MCP servers and app
connectors based on the active provider.
- Updates app-server MCP list/read paths to use the filtered MCP config.
- Adds coverage for default provider capabilities, Bedrock disabled
capabilities, and optional tool-surface clamping.

## Testing

built locally and verified that bedrock responses api now return without
errors calling unsupported tools.
This commit is contained in:
Celia Chen
2026-04-28 17:51:30 -07:00
committed by GitHub
Unverified
parent cb8b1bbcd6
commit f8fe96d548
12 changed files with 390 additions and 8 deletions
+4 -3
View File
@@ -107,7 +107,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
use crate::tools::handlers::unavailable_tool_message;
use crate::tools::tool_search_entry::build_tool_search_entries;
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
let mut builder = ToolRegistryBuilder::new();
let mcp_tool_plan_inputs = mcp_tools.as_ref().map(map_mcp_tools_for_plan);
@@ -170,7 +170,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
});
let deferred_dynamic_tools = dynamic_tools
.iter()
.filter(|tool| tool.defer_loading)
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
.cloned()
.collect::<Vec<_>>();
let mut tool_search_handler = None;
@@ -270,7 +270,8 @@ pub(crate) fn build_specs_with_discoverable_tools(
}
ToolHandlerKind::ToolSearch => {
if tool_search_handler.is_none() {
let entries = build_tool_search_entries(
let entries = build_tool_search_entries_for_config(
config,
deferred_mcp_tools.as_ref(),
&deferred_dynamic_tools,
);
+58
View File
@@ -20,6 +20,7 @@ use codex_tools::AdditionalProperties;
use codex_tools::ConfiguredToolSpec;
use codex_tools::DiscoverableTool;
use codex_tools::JsonSchema;
use codex_tools::LoadableToolSpec;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ShellCommandBackendConfig;
@@ -40,6 +41,7 @@ use std::collections::BTreeMap;
use std::path::PathBuf;
use super::*;
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
rmcp::model::Tool {
@@ -1030,6 +1032,62 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
assert!(registry.has_handler(&mcp_alias));
}
#[tokio::test]
async fn tool_search_entries_skip_namespace_outputs_when_namespace_tools_are_disabled() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::ToolSearch);
let available_models = Vec::new();
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
tools_config.namespace_tools = false;
let mcp_tools = HashMap::from([(
"mcp__test_server__echo".to_string(),
mcp_tool_info(mcp_tool(
"echo",
"Echo",
serde_json::json!({"type": "object"}),
)),
)]);
let dynamic_tools = vec![
DynamicToolSpec {
namespace: Some("codex_app".to_string()),
name: "automation_update".to_string(),
description: "Create or update automations.".to_string(),
input_schema: serde_json::json!({"type": "object", "properties": {}}),
defer_loading: true,
},
DynamicToolSpec {
namespace: None,
name: "plain_dynamic".to_string(),
description: "Plain dynamic tool.".to_string(),
input_schema: serde_json::json!({"type": "object", "properties": {}}),
defer_loading: true,
},
];
let entries =
build_tool_search_entries_for_config(&tools_config, Some(&mcp_tools), &dynamic_tools);
let outputs = entries
.into_iter()
.map(|entry| entry.output)
.collect::<Vec<_>>();
assert_eq!(outputs.len(), 1);
match &outputs[0] {
LoadableToolSpec::Function(tool) => assert_eq!(tool.name, "plain_dynamic"),
LoadableToolSpec::Namespace(_) => panic!("namespace tool_search output should be hidden"),
}
}
#[tokio::test]
async fn direct_mcp_tools_register_namespaced_handlers() {
let config = test_config().await;
@@ -2,6 +2,7 @@ use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tools::LoadableToolSpec;
use codex_tools::ToolSearchResultSource;
use codex_tools::ToolsConfig;
use codex_tools::dynamic_tool_to_loadable_tool_spec;
use codex_tools::tool_search_result_source_to_loadable_tool_spec;
use std::collections::HashMap;
@@ -52,6 +53,24 @@ pub(crate) fn build_tool_search_entries(
entries
}
pub(crate) fn build_tool_search_entries_for_config(
config: &ToolsConfig,
mcp_tools: Option<&HashMap<String, ToolInfo>>,
dynamic_tools: &[DynamicToolSpec],
) -> Vec<ToolSearchEntry> {
let mcp_tools = if config.namespace_tools {
mcp_tools
} else {
None
};
let dynamic_tools = dynamic_tools
.iter()
.filter(|tool| config.namespace_tools || tool.namespace.is_none())
.cloned()
.collect::<Vec<_>>();
build_tool_search_entries(mcp_tools, &dynamic_tools)
}
fn mcp_tool_search_entry(info: &ToolInfo) -> Result<ToolSearchEntry, serde_json::Error> {
Ok(ToolSearchEntry {
search_text: build_mcp_search_text(info),