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
+23
View File
@@ -94,6 +94,7 @@ pub struct ToolsConfig {
pub web_search_tool_type: WebSearchToolType,
pub image_gen_tool: bool,
pub search_tool: bool,
pub namespace_tools: bool,
pub tool_suggest: bool,
pub exec_permission_approvals_enabled: bool,
pub request_permissions_tool_enabled: bool,
@@ -214,6 +215,7 @@ impl ToolsConfig {
web_search_tool_type: model_info.web_search_tool_type,
image_gen_tool: include_image_gen_tool,
search_tool: include_search_tool,
namespace_tools: true,
tool_suggest: include_tool_suggest,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
@@ -241,6 +243,27 @@ impl ToolsConfig {
self
}
pub fn with_namespace_tools_capability(mut self, namespace_tools: bool) -> Self {
if !namespace_tools {
self.namespace_tools = false;
}
self
}
pub fn with_image_generation_capability(mut self, image_generation: bool) -> Self {
if !image_generation {
self.image_gen_tool = false;
}
self
}
pub fn with_web_search_capability(mut self, web_search: bool) -> Self {
if !web_search {
self.web_search_mode = None;
}
self
}
pub fn with_spawn_agent_usage_hint(mut self, spawn_agent_usage_hint: bool) -> Self {
self.spawn_agent_usage_hint = spawn_agent_usage_hint;
self
+32
View File
@@ -231,3 +231,35 @@ fn image_generation_requires_feature_and_supported_model() {
assert!(!auth_disallowed_tools_config.image_gen_tool);
assert!(!unsupported_tools_config.image_gen_tool);
}
#[test]
fn provider_capability_methods_disable_provider_bound_tool_surfaces() {
let model_info = model_info();
let features = Features::with_defaults();
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.search_tool = true;
tools_config.tool_suggest = true;
tools_config.image_gen_tool = true;
tools_config.namespace_tools = true;
let tools_config = tools_config
.with_namespace_tools_capability(/*namespace_tools*/ false)
.with_image_generation_capability(/*image_generation*/ false)
.with_web_search_capability(/*web_search*/ false);
assert!(tools_config.search_tool);
assert!(tools_config.tool_suggest);
assert!(!tools_config.image_gen_tool);
assert!(!tools_config.namespace_tools);
assert_eq!(tools_config.web_search_mode, None);
}
+14 -5
View File
@@ -263,14 +263,18 @@ pub fn build_tool_registry_plan(
let deferred_dynamic_tools = params
.dynamic_tools
.iter()
.filter(|tool| tool.defer_loading)
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
.collect::<Vec<_>>();
let deferred_mcp_tools_for_search = if config.namespace_tools {
params.deferred_mcp_tools
} else {
None
};
if config.search_tool
&& (params.deferred_mcp_tools.is_some() || !deferred_dynamic_tools.is_empty())
&& (deferred_mcp_tools_for_search.is_some() || !deferred_dynamic_tools.is_empty())
{
let mut search_source_infos = params
.deferred_mcp_tools
let mut search_source_infos = deferred_mcp_tools_for_search
.map(|deferred_mcp_tools| {
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| {
ToolSearchSource {
@@ -296,7 +300,7 @@ pub fn build_tool_registry_plan(
);
plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch);
if let Some(deferred_mcp_tools) = params.deferred_mcp_tools {
if let Some(deferred_mcp_tools) = deferred_mcp_tools_for_search {
for tool in deferred_mcp_tools {
plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp);
}
@@ -589,6 +593,11 @@ pub fn build_tool_registry_plan(
);
}
if !config.namespace_tools {
plan.specs
.retain(|configured_tool| !matches!(&configured_tool.spec, ToolSpec::Namespace(_)));
}
plan
}
@@ -1143,6 +1143,84 @@ fn test_build_specs_mcp_tools_converted() {
);
}
#[test]
fn namespace_specs_are_hidden_when_namespace_tools_are_disabled() {
let model_info = model_info();
let features = Features::with_defaults();
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 (tools, handlers) = build_specs(
&tools_config,
Some(HashMap::from([(
ToolName::namespaced("mcp__sample__", "echo"),
mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
)])),
/*deferred_mcp_tools*/ None,
&[],
);
assert_lacks_tool_name(&tools, "mcp__sample__");
assert!(handlers.contains(&ToolHandlerSpec {
name: ToolName::namespaced("mcp__sample__", "echo"),
kind: ToolHandlerKind::Mcp,
}));
}
#[test]
fn namespaced_dynamic_specs_are_hidden_when_namespace_tools_are_disabled() {
let model_info = model_info();
let features = Features::with_defaults();
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 dynamic_tools = vec![
DynamicToolSpec {
namespace: Some("codex_app".to_string()),
name: "automation_update".to_string(),
description: "Create or update automations.".to_string(),
input_schema: json!({"type": "object", "properties": {}}),
defer_loading: false,
},
DynamicToolSpec {
namespace: None,
name: "plain_dynamic".to_string(),
description: "Plain dynamic tool.".to_string(),
input_schema: json!({"type": "object", "properties": {}}),
defer_loading: false,
},
];
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
&dynamic_tools,
);
assert_lacks_tool_name(&tools, "codex_app");
assert_contains_tool_names(&tools, &["plain_dynamic"]);
}
#[test]
fn test_build_specs_mcp_namespace_description_falls_back_when_missing() {
let model_info = model_info();
@@ -1398,6 +1476,44 @@ fn search_tool_requires_model_capability_and_enabled_feature() {
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]);
}
#[test]
fn search_tool_is_hidden_when_only_deferred_namespace_tools_are_available() {
let model_info = search_capable_model_info();
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 (tools, handlers) = build_specs(
&tools_config,
/*mcp_tools*/ None,
Some(vec![deferred_mcp_tool(
"_create_event",
"mcp__codex_apps__calendar",
CODEX_APPS_MCP_SERVER_NAME,
Some("Calendar"),
Some("Plan events and manage your calendar."),
)]),
&[],
);
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
assert!(!handlers.contains(&ToolHandlerSpec {
name: ToolName::plain(TOOL_SEARCH_TOOL_NAME),
kind: ToolHandlerKind::ToolSearch,
}));
}
#[test]
fn search_tool_registers_for_deferred_dynamic_tools() {
let model_info = search_capable_model_info();
@@ -1484,6 +1600,55 @@ fn search_tool_registers_for_deferred_dynamic_tools() {
}));
}
#[test]
fn search_tool_keeps_plain_deferred_dynamic_tools_when_namespace_tools_are_disabled() {
let model_info = search_capable_model_info();
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 dynamic_tools = vec![
DynamicToolSpec {
namespace: Some("codex_app".to_string()),
name: "automation_update".to_string(),
description: "Create or update automations.".to_string(),
input_schema: json!({"type": "object", "properties": {}}),
defer_loading: true,
},
DynamicToolSpec {
namespace: None,
name: "plain_dynamic".to_string(),
description: "Plain dynamic tool.".to_string(),
input_schema: json!({"type": "object", "properties": {}}),
defer_loading: true,
},
];
let (tools, handlers) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
&dynamic_tools,
);
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME, "plain_dynamic"]);
assert_lacks_tool_name(&tools, "codex_app");
assert!(handlers.contains(&ToolHandlerSpec {
name: ToolName::plain(TOOL_SEARCH_TOOL_NAME),
kind: ToolHandlerKind::ToolSearch,
}));
}
#[test]
fn tool_suggest_is_not_registered_without_feature_flag() {
let model_info = search_capable_model_info();