mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Enable cached web search for Amazon Bedrock (#36938)
## Why Amazon Bedrock supports hosted text web search, but it rejects the `search_content_types` field used for multimodal search and does not support external live or indexed web access. ## What changed - Advertise hosted web search for Amazon Bedrock while marking external web access as unsupported. - Resolve unsupported live and indexed modes to cached search, or disable the tool when cached search is prohibited by managed requirements. - Normalize built-in and configured Bedrock model catalogs to text-only web search, and retain the runtime provider in session configuration so turn setup can apply its capabilities. ## Testing - Cover cached fallback, managed-mode restrictions, text-only tool payloads, provider capabilities, and catalog normalization. GitOrigin-RevId: 310473849257401654388a4ebb42920e03aa3228
This commit is contained in:
@@ -57,7 +57,7 @@ async fn read_amazon_bedrock_provider_capabilities() -> Result<()> {
|
||||
let expected = ModelProviderCapabilitiesReadResponse {
|
||||
namespace_tools: true,
|
||||
image_generation: false,
|
||||
web_search: false,
|
||||
web_search: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
Ok(())
|
||||
|
||||
@@ -71,6 +71,7 @@ use codex_core_plugins::PluginsManager;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::Feature;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_model_provider::ProviderCapabilities;
|
||||
use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::WireApi;
|
||||
@@ -5797,7 +5798,11 @@ fn web_search_mode_disabled_overrides_legacy_request() {
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_preserves_indexed_for_disabled_permissions() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Indexed);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled);
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
&web_search_mode,
|
||||
&PermissionProfile::Disabled,
|
||||
ProviderCapabilities::default(),
|
||||
);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Indexed);
|
||||
}
|
||||
@@ -5806,7 +5811,11 @@ fn web_search_mode_for_turn_preserves_indexed_for_disabled_permissions() {
|
||||
fn web_search_mode_for_turn_uses_preference_for_read_only() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Cached);
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &permission_profile);
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
&web_search_mode,
|
||||
&permission_profile,
|
||||
ProviderCapabilities::default(),
|
||||
);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Cached);
|
||||
}
|
||||
@@ -5814,15 +5823,75 @@ fn web_search_mode_for_turn_uses_preference_for_read_only() {
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_prefers_live_for_disabled_permissions() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Cached);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled);
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
&web_search_mode,
|
||||
&PermissionProfile::Disabled,
|
||||
ProviderCapabilities::default(),
|
||||
);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Live);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_falls_back_when_provider_disallows_external_web_access() {
|
||||
for preferred in [WebSearchMode::Live, WebSearchMode::Indexed] {
|
||||
let web_search_mode = Constrained::allow_any(preferred);
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
&web_search_mode,
|
||||
&PermissionProfile::Disabled,
|
||||
ProviderCapabilities {
|
||||
external_web_access: false,
|
||||
..ProviderCapabilities::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Cached);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_disables_when_external_access_and_cached_are_disallowed()
|
||||
-> anyhow::Result<()> {
|
||||
let allowed = [
|
||||
WebSearchMode::Disabled,
|
||||
WebSearchMode::Live,
|
||||
WebSearchMode::Indexed,
|
||||
];
|
||||
for preferred in [WebSearchMode::Live, WebSearchMode::Indexed] {
|
||||
let web_search_mode = Constrained::new(preferred, move |candidate| {
|
||||
if allowed.contains(candidate) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "web_search_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: format!("{allowed:?}"),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
&web_search_mode,
|
||||
&PermissionProfile::Disabled,
|
||||
ProviderCapabilities {
|
||||
external_web_access: false,
|
||||
..ProviderCapabilities::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Disabled);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_respects_disabled_for_disabled_permissions() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Disabled);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled);
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
&web_search_mode,
|
||||
&PermissionProfile::Disabled,
|
||||
ProviderCapabilities::default(),
|
||||
);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Disabled);
|
||||
}
|
||||
@@ -5842,7 +5911,11 @@ fn web_search_mode_for_turn_falls_back_when_live_is_disallowed() -> anyhow::Resu
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled);
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
&web_search_mode,
|
||||
&PermissionProfile::Disabled,
|
||||
ProviderCapabilities::default(),
|
||||
);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Cached);
|
||||
Ok(())
|
||||
@@ -5867,7 +5940,11 @@ fn web_search_mode_for_turn_does_not_implicitly_select_indexed() -> anyhow::Resu
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled);
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
&web_search_mode,
|
||||
&PermissionProfile::Disabled,
|
||||
ProviderCapabilities::default(),
|
||||
);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Cached);
|
||||
Ok(())
|
||||
@@ -10297,6 +10374,7 @@ async fn requirements_web_search_mode_overrides_danger_full_access_default() ->
|
||||
resolve_web_search_mode_for_turn(
|
||||
&config.web_search_mode,
|
||||
&config.permissions.effective_permission_profile(),
|
||||
ProviderCapabilities::default(),
|
||||
),
|
||||
WebSearchMode::Cached,
|
||||
);
|
||||
|
||||
@@ -81,6 +81,7 @@ use codex_mcp::McpProtocolMode;
|
||||
use codex_mcp::McpServerRegistration;
|
||||
use codex_mcp::ResolvedMcpCatalog;
|
||||
use codex_memories_read::memory_root;
|
||||
use codex_model_provider::ProviderCapabilities;
|
||||
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR;
|
||||
@@ -2988,8 +2989,19 @@ pub fn resolve_bootstrap_http_client_factory(
|
||||
pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
web_search_mode: &Constrained<WebSearchMode>,
|
||||
permission_profile: &PermissionProfile,
|
||||
provider_capabilities: ProviderCapabilities,
|
||||
) -> WebSearchMode {
|
||||
let preferred = web_search_mode.value();
|
||||
let is_allowed = |mode: WebSearchMode| {
|
||||
let provider_supports_mode = match mode {
|
||||
WebSearchMode::Live | WebSearchMode::Indexed => {
|
||||
provider_capabilities.external_web_access
|
||||
}
|
||||
WebSearchMode::Cached | WebSearchMode::Disabled => true,
|
||||
};
|
||||
|
||||
provider_supports_mode && web_search_mode.can_set(&mode).is_ok()
|
||||
};
|
||||
|
||||
if matches!(permission_profile, PermissionProfile::Disabled)
|
||||
&& !matches!(preferred, WebSearchMode::Disabled | WebSearchMode::Indexed)
|
||||
@@ -2999,12 +3011,12 @@ pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
WebSearchMode::Cached,
|
||||
WebSearchMode::Disabled,
|
||||
] {
|
||||
if web_search_mode.can_set(&mode).is_ok() {
|
||||
if is_allowed(mode) {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if web_search_mode.can_set(&preferred).is_ok() {
|
||||
if is_allowed(preferred) {
|
||||
return preferred;
|
||||
}
|
||||
for mode in [
|
||||
@@ -3012,7 +3024,7 @@ pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
WebSearchMode::Live,
|
||||
WebSearchMode::Disabled,
|
||||
] {
|
||||
if web_search_mode.can_set(&mode).is_ok() {
|
||||
if is_allowed(mode) {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ use crate::thread_rollout_truncation::initial_history_has_prior_user_turns;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
@@ -676,7 +677,10 @@ impl Session {
|
||||
let service_tier =
|
||||
get_service_tier(config.service_tier.clone(), fast_mode_enabled, &model_info);
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(
|
||||
config.model_provider.clone(),
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
service_tier,
|
||||
@@ -1630,7 +1634,7 @@ impl Session {
|
||||
|
||||
pub(crate) async fn provider(&self) -> ModelProviderInfo {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.provider.clone()
|
||||
state.session_configuration.provider.info().clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_runtime_config(&self, next_config: Config) {
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_extension_api::ExtensionDataInit;
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::RouteAwareClientPool;
|
||||
use codex_login::auth::AgentIdentityAuthPolicy;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
@@ -65,8 +66,8 @@ pub(crate) struct Session {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SessionConfiguration {
|
||||
/// Provider identifier ("openai", "openrouter", ...).
|
||||
pub(super) provider: ModelProviderInfo,
|
||||
/// Runtime provider and its provider-specific execution policy.
|
||||
pub(super) provider: SharedModelProvider,
|
||||
|
||||
pub(super) collaboration_mode: CollaborationMode,
|
||||
pub(super) model_reasoning_summary: Option<ReasoningSummaryConfig>,
|
||||
@@ -840,7 +841,7 @@ impl Session {
|
||||
let terminal_type = user_agent();
|
||||
let session_model = session_configuration.collaboration_mode.model().to_string();
|
||||
let auth_env_telemetry = collect_auth_env_telemetry(
|
||||
&session_configuration.provider,
|
||||
session_configuration.provider.info(),
|
||||
auth_manager.codex_api_key_env_enabled(),
|
||||
);
|
||||
let mut session_telemetry = SessionTelemetry::new(
|
||||
@@ -1147,7 +1148,7 @@ impl Session {
|
||||
AgentIdentityAuthPolicy::JwtOnly
|
||||
},
|
||||
thread_id,
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.provider.info().clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.originator.clone(),
|
||||
config.model_verbosity,
|
||||
|
||||
@@ -39,6 +39,7 @@ use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_http_client::RouteAwareClientPool;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::auth::AgentIdentityAuthPolicy;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_models_manager::model_info;
|
||||
@@ -4092,7 +4093,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
},
|
||||
};
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(config.model_provider.clone(), /*auth_manager*/ None),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -4201,7 +4202,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
},
|
||||
};
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(config.model_provider.clone(), /*auth_manager*/ None),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -4747,7 +4748,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
};
|
||||
|
||||
SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(config.model_provider.clone(), /*auth_manager*/ None),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -5486,7 +5487,10 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
},
|
||||
};
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(
|
||||
config.model_provider.clone(),
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -5623,7 +5627,10 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
};
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(
|
||||
config.model_provider.clone(),
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -5760,7 +5767,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
Some(auth_manager.clone()),
|
||||
AgentIdentityAuthPolicy::JwtOnly,
|
||||
thread_id,
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.provider.info().clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.originator.clone(),
|
||||
config.model_verbosity,
|
||||
@@ -5895,7 +5902,10 @@ async fn make_session_with_config_and_rx(
|
||||
};
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(
|
||||
config.model_provider.clone(),
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -6005,7 +6015,10 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
};
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(
|
||||
config.model_provider.clone(),
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -7818,7 +7831,10 @@ where
|
||||
};
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
provider: create_model_provider(
|
||||
config.model_provider.clone(),
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
),
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -7954,7 +7970,7 @@ where
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
AgentIdentityAuthPolicy::JwtOnly,
|
||||
thread_id,
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.provider.info().clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.originator.clone(),
|
||||
config.model_verbosity,
|
||||
|
||||
@@ -6,7 +6,6 @@ use codex_core_plugins::TrustedPluginRoots;
|
||||
use codex_core_skills::HostSkillsSnapshot;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -454,8 +453,11 @@ impl Session {
|
||||
session_configuration
|
||||
.apply_permission_profile_to_permissions(&mut per_turn_config.permissions);
|
||||
let permission_profile = session_configuration.permission_profile();
|
||||
let resolved_web_search_mode =
|
||||
resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &permission_profile);
|
||||
let resolved_web_search_mode = resolve_web_search_mode_for_turn(
|
||||
&per_turn_config.web_search_mode,
|
||||
&permission_profile,
|
||||
session_configuration.provider.capabilities(),
|
||||
);
|
||||
if let Err(err) = per_turn_config
|
||||
.web_search_mode
|
||||
.set(resolved_web_search_mode)
|
||||
@@ -488,7 +490,7 @@ impl Session {
|
||||
session_id: SessionId,
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
provider: ModelProviderInfo,
|
||||
provider: SharedModelProvider,
|
||||
session_configuration: &SessionConfiguration,
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
user_shell: &shell::Shell,
|
||||
@@ -513,8 +515,6 @@ impl Session {
|
||||
model_info.slug.as_str(),
|
||||
);
|
||||
let session_source = session_configuration.session_source.clone();
|
||||
let auth_manager_for_context = auth_manager.clone();
|
||||
let provider_for_context = create_model_provider(provider, auth_manager);
|
||||
let session_telemetry_for_context = session_telemetry;
|
||||
let available_models = models_manager.try_list_models().unwrap_or_default();
|
||||
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
|
||||
@@ -555,10 +555,10 @@ impl Session {
|
||||
realtime_active: false,
|
||||
code_mode_available: true,
|
||||
config: per_turn_config,
|
||||
auth_manager: auth_manager_for_context,
|
||||
auth_manager,
|
||||
model_info,
|
||||
session_telemetry: session_telemetry_for_context,
|
||||
provider: provider_for_context,
|
||||
provider,
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
session_source,
|
||||
|
||||
@@ -2289,10 +2289,30 @@ async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates(
|
||||
.await;
|
||||
standalone_web_search.assert_visible_lacks(&["web_search"]);
|
||||
|
||||
let unsupported_provider = probe(|turn| {
|
||||
set_web_search_mode(turn, WebSearchMode::Live);
|
||||
let bedrock_cached_web_search = probe(|turn| {
|
||||
use_bedrock_provider(turn);
|
||||
turn.model_info.web_search_tool_type = WebSearchToolType::Text;
|
||||
})
|
||||
.await;
|
||||
unsupported_provider.assert_visible_lacks(&["web_search"]);
|
||||
assert_eq!(
|
||||
bedrock_cached_web_search.visible_spec("web_search"),
|
||||
&ToolSpec::WebSearch {
|
||||
external_web_access: Some(false),
|
||||
indexed_web_access: None,
|
||||
filters: None,
|
||||
user_location: None,
|
||||
search_context_size: None,
|
||||
search_content_types: None,
|
||||
}
|
||||
);
|
||||
|
||||
let bedrock_with_standalone_web_search = probe(|turn| {
|
||||
set_feature(turn, Feature::StandaloneWebSearch, /*enabled*/ true);
|
||||
set_web_search_mode(turn, WebSearchMode::Cached);
|
||||
use_bedrock_provider(turn);
|
||||
turn.model_info.web_search_tool_type = WebSearchToolType::Text;
|
||||
})
|
||||
.await;
|
||||
bedrock_with_standalone_web_search.assert_visible_contains(&["web_search"]);
|
||||
bedrock_with_standalone_web_search.assert_visible_lacks(&["web"]);
|
||||
}
|
||||
|
||||
@@ -343,8 +343,7 @@ async fn responses_lite_does_not_expose_disabled_standalone_web_search_for_opted
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_lite_does_not_expose_standalone_web_search_for_opted_in_bedrock_provider()
|
||||
-> Result<()> {
|
||||
async fn responses_lite_does_not_expose_standalone_web_search_for_bedrock_provider() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -373,7 +372,7 @@ async fn responses_lite_does_not_expose_standalone_web_search_for_opted_in_bedro
|
||||
config.model_provider.name = "Amazon Bedrock".to_string();
|
||||
config.model_provider.requires_openai_auth = false;
|
||||
config.model_provider.http_headers = None;
|
||||
config.model_provider.supports_standalone_web_search = true;
|
||||
config.model_provider.supports_standalone_web_search = false;
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::auth::BedrockApiKeyAuth;
|
||||
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_4_MODEL_ID;
|
||||
use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::WebSearchToolType;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
@@ -59,6 +66,153 @@ async fn web_search_mode_cached_sets_external_web_access_false() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn amazon_bedrock_web_search_uses_text_only_hosted_tools() {
|
||||
skip_if_no_network!();
|
||||
|
||||
enum ModelCatalog {
|
||||
BuiltIn,
|
||||
Configured,
|
||||
}
|
||||
|
||||
for (case, configured_web_search_mode, model_catalog) in [
|
||||
("cached by default", None, ModelCatalog::BuiltIn),
|
||||
(
|
||||
"unsupported explicit live search falls back to cached",
|
||||
Some(WebSearchMode::Live),
|
||||
ModelCatalog::BuiltIn,
|
||||
),
|
||||
(
|
||||
"unsupported explicit indexed search falls back to cached",
|
||||
Some(WebSearchMode::Indexed),
|
||||
ModelCatalog::BuiltIn,
|
||||
),
|
||||
("configured model catalog", None, ModelCatalog::Configured),
|
||||
] {
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let auth = CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
|
||||
api_key: "dummy".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
});
|
||||
let mut builder = test_codex().with_auth(auth);
|
||||
builder = match model_catalog {
|
||||
ModelCatalog::BuiltIn => builder.with_model(AMAZON_BEDROCK_GPT_5_4_MODEL_ID),
|
||||
ModelCatalog::Configured => builder.with_model_info_override("gpt-5.4", |model_info| {
|
||||
model_info.web_search_tool_type = WebSearchToolType::TextAndImage;
|
||||
}),
|
||||
};
|
||||
builder = builder.with_config(move |config| {
|
||||
let base_url = config.model_provider.base_url.clone();
|
||||
config.model_provider_id = AMAZON_BEDROCK_PROVIDER_ID.to_string();
|
||||
config.model_provider =
|
||||
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None);
|
||||
config.model_provider.base_url = base_url;
|
||||
if let Some(mode) = configured_web_search_mode {
|
||||
config
|
||||
.web_search_mode
|
||||
.set(mode)
|
||||
.expect("test web search mode should satisfy constraints");
|
||||
}
|
||||
});
|
||||
let test = builder
|
||||
.build_with_auto_env(&server)
|
||||
.await
|
||||
.expect("create test Bedrock conversation");
|
||||
|
||||
test.submit_turn_with_permission_profile(
|
||||
"hello Bedrock web search",
|
||||
PermissionProfile::Disabled,
|
||||
)
|
||||
.await
|
||||
.expect("submit Bedrock turn");
|
||||
|
||||
let body = resp_mock.single_request().body_json();
|
||||
assert_eq!(
|
||||
find_web_search_tool(&body),
|
||||
&json!({
|
||||
"type": "web_search",
|
||||
"external_web_access": false,
|
||||
}),
|
||||
"unexpected Bedrock search tool for {case}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn amazon_bedrock_web_search_is_disabled_when_managed_requirements_prohibit_cached_search() {
|
||||
skip_if_no_network!();
|
||||
|
||||
for (allowed_mode, requested_mode) in [
|
||||
("live", WebSearchMode::Live),
|
||||
("indexed", WebSearchMode::Indexed),
|
||||
] {
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let auth = CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
|
||||
api_key: "dummy".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
});
|
||||
let mut builder = test_codex()
|
||||
.with_auth(auth)
|
||||
.with_model(AMAZON_BEDROCK_GPT_5_4_MODEL_ID)
|
||||
.with_cloud_config_bundle(
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(format!(
|
||||
r#"allowed_web_search_modes = ["{allowed_mode}"]"#
|
||||
)),
|
||||
)
|
||||
.with_config(move |config| {
|
||||
assert_eq!(config.web_search_mode.value(), requested_mode);
|
||||
let base_url = config.model_provider.base_url.clone();
|
||||
config.model_provider_id = AMAZON_BEDROCK_PROVIDER_ID.to_string();
|
||||
config.model_provider =
|
||||
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None);
|
||||
config.model_provider.base_url = base_url;
|
||||
});
|
||||
let test = builder
|
||||
.build_with_auto_env(&server)
|
||||
.await
|
||||
.expect("create managed Bedrock conversation");
|
||||
|
||||
test.submit_turn_with_permission_profile(
|
||||
"hello managed Bedrock web search",
|
||||
PermissionProfile::Disabled,
|
||||
)
|
||||
.await
|
||||
.expect("submit managed Bedrock turn");
|
||||
|
||||
let body = resp_mock.single_request().body_json();
|
||||
let web_search_tool = body
|
||||
.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|tools| {
|
||||
tools
|
||||
.iter()
|
||||
.find(|tool| tool.get("type").and_then(Value::as_str) == Some("web_search"))
|
||||
});
|
||||
assert_eq!(
|
||||
web_search_tool, None,
|
||||
"Bedrock search should be disabled when managed requirements only allow {allowed_mode}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn web_search_mode_takes_precedence_over_legacy_flags() {
|
||||
skip_if_no_network!();
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_protocol::openai_models::ModelVisibility;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::openai_models::WebSearchToolType;
|
||||
|
||||
const GPT_5_BEDROCK_CONTEXT_WINDOW: i64 = 272_000;
|
||||
const GPT_5_6_SOL_OPENAI_MODEL_ID: &str = "gpt-5.6-sol";
|
||||
@@ -18,7 +19,7 @@ const GPT_5_5_OPENAI_MODEL_ID: &str = "gpt-5.5";
|
||||
const GPT_5_4_OPENAI_MODEL_ID: &str = "gpt-5.4";
|
||||
|
||||
pub(crate) fn static_model_catalog() -> ModelsResponse {
|
||||
with_default_only_service_tier(ModelsResponse {
|
||||
normalize_bedrock_catalog(ModelsResponse {
|
||||
models: vec![
|
||||
gpt_5_6_bedrock_model(
|
||||
GPT_5_6_SOL_OPENAI_MODEL_ID,
|
||||
@@ -54,12 +55,14 @@ pub(crate) fn static_model_catalog() -> ModelsResponse {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn with_default_only_service_tier(mut catalog: ModelsResponse) -> ModelsResponse {
|
||||
pub(crate) fn normalize_bedrock_catalog(mut catalog: ModelsResponse) -> ModelsResponse {
|
||||
for model in &mut catalog.models {
|
||||
// Amazon Bedrock currently only supports the implicit "default" tier for GPT models.
|
||||
model.additional_speed_tiers.clear();
|
||||
model.service_tiers.clear();
|
||||
model.default_service_tier = None;
|
||||
// Bedrock rejects the `search_content_types` field used by multimodal search.
|
||||
model.web_search_tool_type = WebSearchToolType::Text;
|
||||
}
|
||||
catalog
|
||||
}
|
||||
@@ -148,15 +151,39 @@ mod tests {
|
||||
|
||||
for model in catalog.models {
|
||||
assert_eq!(
|
||||
(model.context_window, model.max_context_window),
|
||||
(
|
||||
model.context_window,
|
||||
model.max_context_window,
|
||||
model.web_search_tool_type,
|
||||
),
|
||||
(
|
||||
Some(GPT_5_BEDROCK_CONTEXT_WINDOW),
|
||||
Some(GPT_5_BEDROCK_CONTEXT_WINDOW)
|
||||
Some(GPT_5_BEDROCK_CONTEXT_WINDOW),
|
||||
WebSearchToolType::Text,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_bedrock_catalogs_normalize_unsupported_model_capabilities() {
|
||||
let model = bundled_openai_model(GPT_5_4_OPENAI_MODEL_ID);
|
||||
let mut expected = model.clone();
|
||||
expected.additional_speed_tiers.clear();
|
||||
expected.service_tiers.clear();
|
||||
expected.default_service_tier = None;
|
||||
expected.web_search_tool_type = WebSearchToolType::Text;
|
||||
|
||||
assert_eq!(
|
||||
normalize_bedrock_catalog(ModelsResponse {
|
||||
models: vec![model],
|
||||
}),
|
||||
ModelsResponse {
|
||||
models: vec![expected],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpt_5_bedrock_models_do_not_include_availability_nux_or_upgrade() {
|
||||
let catalog = static_model_catalog();
|
||||
|
||||
@@ -30,8 +30,8 @@ use crate::provider::ProviderAccountResult;
|
||||
use crate::provider::ProviderAccountState;
|
||||
use crate::provider::ProviderCapabilities;
|
||||
use auth::resolve_provider_auth as resolve_bedrock_provider_auth;
|
||||
use catalog::normalize_bedrock_catalog;
|
||||
pub(crate) use catalog::static_model_catalog;
|
||||
use catalog::with_default_only_service_tier;
|
||||
use mantle::bedrock_mantle_runtime_base_url;
|
||||
pub use mantle::is_supported_amazon_bedrock_region;
|
||||
|
||||
@@ -124,7 +124,8 @@ impl ModelProvider for AmazonBedrockModelProvider {
|
||||
ProviderCapabilities {
|
||||
namespace_tools: true,
|
||||
image_generation: false,
|
||||
web_search: false,
|
||||
web_search: true,
|
||||
external_web_access: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +185,7 @@ impl ModelProvider for AmazonBedrockModelProvider {
|
||||
) -> SharedModelsManager {
|
||||
Arc::new(StaticModelsManager::new(
|
||||
/*auth_manager*/ None,
|
||||
config_model_catalog.map_or_else(static_model_catalog, with_default_only_service_tier),
|
||||
config_model_catalog.map_or_else(static_model_catalog, normalize_bedrock_catalog),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -194,7 +195,7 @@ impl ModelProvider for AmazonBedrockModelProvider {
|
||||
) -> SharedModelsManager {
|
||||
Arc::new(StaticModelsManager::new(
|
||||
/*auth_manager*/ None,
|
||||
config_model_catalog.map_or_else(static_model_catalog, with_default_only_service_tier),
|
||||
config_model_catalog.map_or_else(static_model_catalog, normalize_bedrock_catalog),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -354,7 +355,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capabilities_disable_unsupported_hosted_tools() {
|
||||
fn capabilities_enable_web_search_but_disable_image_generation() {
|
||||
let provider = AmazonBedrockModelProvider::new(
|
||||
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),
|
||||
/*auth_manager*/ None,
|
||||
@@ -365,7 +366,8 @@ mod tests {
|
||||
ProviderCapabilities {
|
||||
namespace_tools: true,
|
||||
image_generation: false,
|
||||
web_search: false,
|
||||
web_search: true,
|
||||
external_web_access: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ pub struct ProviderCapabilities {
|
||||
pub namespace_tools: bool,
|
||||
pub image_generation: bool,
|
||||
pub web_search: bool,
|
||||
pub external_web_access: bool,
|
||||
}
|
||||
|
||||
impl Default for ProviderCapabilities {
|
||||
@@ -43,6 +44,7 @@ impl Default for ProviderCapabilities {
|
||||
namespace_tools: true,
|
||||
image_generation: true,
|
||||
web_search: true,
|
||||
external_web_access: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user