Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,33 @@ async fn read_amazon_bedrock_provider_capabilities() -> Result<()> {
assert_eq!(received, expected);
Ok(())
}

#[tokio::test]
async fn read_amazon_bedrock_runtime_provider_capabilities() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"model_provider = "amazon-bedrock-runtime"
"#,
)?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
.await?;

let request_id = mcp
.send_model_provider_capabilities_read_request(ModelProviderCapabilitiesReadParams {})
.await?;
let received: ModelProviderCapabilitiesReadResponse =
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;

assert_eq!(
received,
ModelProviderCapabilitiesReadResponse {
namespace_tools: true,
image_generation: false,
web_search: false,
}
);
Ok(())
}
31 changes: 31 additions & 0 deletions codex-rs/app-server/tests/suite/v2/thread_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,37 @@ async fn thread_start_provider_model_fallback_uses_bedrock_static_catalog() -> R
Ok(())
}

#[tokio::test]
async fn thread_start_bedrock_runtime_prefers_global_cross_region_models() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"model_provider = "amazon-bedrock-runtime"
"#,
)?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;

for model in ["global.openai.gpt-5.6-sol", "us.openai.gpt-5.6-sol"] {
let response =
start_thread_with_model(&mut mcp, model, /*allow_provider_model_fallback*/ true)
.await?;
assert_eq!(response.model, model);
}

let response = start_thread_with_model(
&mut mcp,
"openai.gpt-5.6-sol",
/*allow_provider_model_fallback*/ true,
)
.await?;
assert_eq!(response.model, "global.openai.gpt-5.6-sol");

Ok(())
}

#[tokio::test]
async fn thread_start_provider_model_fallback_ignores_dynamic_catalog() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
Expand Down
46 changes: 46 additions & 0 deletions codex-rs/config/src/bedrock_runtime_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use codex_model_provider_info::AMAZON_BEDROCK_RUNTIME_PROVIDER_ID;
use codex_model_provider_info::ModelProviderAwsAuthInfo;
use pretty_assertions::assert_eq;

use super::ConfigToml;

#[test]
fn runtime_provider_accepts_aws_profile_and_region_overrides() {
let config = toml::from_str::<ConfigToml>(
r#"
[model_providers.amazon-bedrock-runtime.aws]
profile = "runtime-profile"
region = "us-west-2"
"#,
)
.expect("Bedrock Runtime AWS overrides should deserialize");

assert_eq!(
config
.model_providers
.get(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID)
.and_then(|provider| provider.aws.clone()),
Some(ModelProviderAwsAuthInfo {
profile: Some("runtime-profile".to_string()),
region: Some("us-west-2".to_string()),
})
);
}

#[test]
fn custom_provider_still_rejects_aws_auth() {
let error = toml::from_str::<ConfigToml>(
r#"
[model_providers.custom]
name = "Custom"

[model_providers.custom.aws]
region = "us-west-2"
"#,
)
.expect_err("custom providers must not accept AWS auth");

assert!(error.to_string().contains(
"provider aws is only supported for `amazon-bedrock` or `amazon-bedrock-runtime`"
));
}
22 changes: 17 additions & 5 deletions codex-rs/config/src/config_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::types::UriBasedFileOpener;
use crate::types::WindowsToml;
use codex_features::FeaturesToml;
use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
use codex_model_provider_info::AMAZON_BEDROCK_RUNTIME_PROVIDER_ID;
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
use codex_model_provider_info::ModelProviderInfo;
Expand Down Expand Up @@ -59,8 +60,9 @@ use serde::Serialize;
use serde::de::Error as SerdeError;
use serde_json::Value as JsonValue;

const RESERVED_MODEL_PROVIDER_IDS: [&str; 4] = [
const RESERVED_MODEL_PROVIDER_IDS: [&str; 5] = [
AMAZON_BEDROCK_PROVIDER_ID,
AMAZON_BEDROCK_RUNTIME_PROVIDER_ID,
OPENAI_PROVIDER_ID,
OLLAMA_OSS_PROVIDER_ID,
LMSTUDIO_OSS_PROVIDER_ID,
Expand Down Expand Up @@ -883,8 +885,10 @@ pub fn validate_reserved_model_provider_ids(
let mut conflicts = model_providers
.keys()
.filter(|key| {
key.as_str() != AMAZON_BEDROCK_PROVIDER_ID
&& RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str())
!matches!(
key.as_str(),
AMAZON_BEDROCK_PROVIDER_ID | AMAZON_BEDROCK_RUNTIME_PROVIDER_ID
) && RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str())
})
.map(|key| format!("`{key}`"))
.collect::<Vec<_>>();
Expand All @@ -905,10 +909,14 @@ pub fn validate_model_providers(
) -> Result<(), String> {
validate_reserved_model_provider_ids(model_providers)?;
for (key, provider) in model_providers {
if key != AMAZON_BEDROCK_PROVIDER_ID {
if !matches!(
key.as_str(),
AMAZON_BEDROCK_PROVIDER_ID | AMAZON_BEDROCK_RUNTIME_PROVIDER_ID
) {
if provider.aws.is_some() {
return Err(format!(
"model_providers.{key}: provider aws is only supported for `{AMAZON_BEDROCK_PROVIDER_ID}`"
"model_providers.{key}: provider aws is only supported for \
`{AMAZON_BEDROCK_PROVIDER_ID}` or `{AMAZON_BEDROCK_RUNTIME_PROVIDER_ID}`"
));
}
if provider.name.trim().is_empty() {
Expand All @@ -935,6 +943,10 @@ where
Ok(model_providers)
}

#[cfg(test)]
#[path = "bedrock_runtime_tests.rs"]
mod bedrock_runtime_tests;

pub fn validate_oss_provider(provider: &str) -> std::io::Result<()> {
match provider {
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => Ok(()),
Expand Down
61 changes: 61 additions & 0 deletions codex-rs/core/tests/suite/web_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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::AMAZON_BEDROCK_RUNTIME_PROVIDER_ID;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::PermissionProfile;
Expand Down Expand Up @@ -147,6 +148,66 @@ async fn amazon_bedrock_web_search_uses_text_only_hosted_tools() {
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn amazon_bedrock_runtime_preserves_cross_region_models_without_web_search() {
skip_if_no_network!();

for model in ["us.openai.gpt-5.6-sol", "global.openai.gpt-5.6-sol"] {
let server = start_mock_server().await;
let response = 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(model)
.with_config(|config| {
let base_url = config.model_provider.base_url.clone();
config.model_provider_id = AMAZON_BEDROCK_RUNTIME_PROVIDER_ID.to_string();
config.model_provider =
ModelProviderInfo::create_amazon_bedrock_runtime_provider(/*aws*/ None);
config.model_provider.base_url = base_url;
config
.web_search_mode
.set(WebSearchMode::Cached)
.expect("test web search mode should satisfy constraints");
});
let test = builder
.build_with_auto_env(&server)
.await
.expect("create test Bedrock Runtime conversation");

test.submit_turn_with_permission_profile(
"hello Bedrock Runtime",
PermissionProfile::Disabled,
)
.await
.expect("submit Bedrock Runtime turn");

let body = response.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!(
(body["model"].as_str(), web_search_tool),
(Some(model), None)
);
}
}

#[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!();
Expand Down
38 changes: 33 additions & 5 deletions codex-rs/model-provider-info/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ pub const OPENAI_PROVIDER_ID: &str = "openai";
pub const CHATGPT_CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api/codex";
const AMAZON_BEDROCK_PROVIDER_NAME: &str = "Amazon Bedrock";
pub const AMAZON_BEDROCK_PROVIDER_ID: &str = "amazon-bedrock";
const AMAZON_BEDROCK_RUNTIME_PROVIDER_NAME: &str = "Amazon Bedrock Runtime";
pub const AMAZON_BEDROCK_RUNTIME_PROVIDER_ID: &str = "amazon-bedrock-runtime";
pub const AMAZON_BEDROCK_GPT_5_5_MODEL_ID: &str = "openai.gpt-5.5";
pub const AMAZON_BEDROCK_GPT_5_4_MODEL_ID: &str = "openai.gpt-5.4";
pub const AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID: &str = "openai.gpt-5.6-sol";
pub const AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID: &str = "openai.gpt-5.6-terra";
pub const AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID: &str = "openai.gpt-5.6-luna";
pub const AMAZON_BEDROCK_RUNTIME_GLOBAL_GPT_5_6_TERRA_MODEL_ID: &str =
"global.openai.gpt-5.6-terra";
pub const AMAZON_BEDROCK_RUNTIME_GLOBAL_GPT_5_6_LUNA_MODEL_ID: &str = "global.openai.gpt-5.6-luna";
pub const AMAZON_BEDROCK_DEFAULT_BASE_URL: &str =
"https://bedrock-mantle.us-east-1.api.aws/openai/v1";
const AMAZON_BEDROCK_MANTLE_CLIENT_AGENT_HEADER: &str = "x-amzn-mantle-client-agent";
Expand Down Expand Up @@ -400,6 +405,15 @@ impl ModelProviderInfo {
}
}

pub fn create_amazon_bedrock_runtime_provider(
aws: Option<ModelProviderAwsAuthInfo>,
) -> ModelProviderInfo {
let mut provider = Self::create_amazon_bedrock_provider(aws);
provider.name = AMAZON_BEDROCK_RUNTIME_PROVIDER_NAME.into();
provider.http_headers = None;
provider
}

pub fn is_openai(&self) -> bool {
self.name == OPENAI_PROVIDER_NAME
}
Expand All @@ -416,6 +430,11 @@ impl ModelProviderInfo {

pub fn is_amazon_bedrock(&self) -> bool {
self.name == AMAZON_BEDROCK_PROVIDER_NAME
|| self.name == AMAZON_BEDROCK_RUNTIME_PROVIDER_NAME
}

pub fn is_amazon_bedrock_runtime(&self) -> bool {
self.name == AMAZON_BEDROCK_RUNTIME_PROVIDER_NAME
}

pub fn has_command_auth(&self) -> bool {
Expand All @@ -436,6 +455,8 @@ pub fn built_in_model_providers(
use ModelProviderInfo as P;
let openai_provider = P::create_openai_provider(openai_base_url);
let amazon_bedrock_provider = P::create_amazon_bedrock_provider(/*aws*/ None);
let amazon_bedrock_runtime_provider =
P::create_amazon_bedrock_runtime_provider(/*aws*/ None);

// We do not want to be in the business of adjucating which third-party
// providers are bundled with Codex CLI, so we only include the OpenAI and
Expand All @@ -444,6 +465,10 @@ pub fn built_in_model_providers(
[
(OPENAI_PROVIDER_ID, openai_provider),
(AMAZON_BEDROCK_PROVIDER_ID, amazon_bedrock_provider),
(
AMAZON_BEDROCK_RUNTIME_PROVIDER_ID,
amazon_bedrock_runtime_provider,
),
(
OLLAMA_OSS_PROVIDER_ID,
create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses),
Expand All @@ -461,27 +486,30 @@ pub fn built_in_model_providers(
/// Merge configured providers into the built-in provider catalog.
///
/// Configured providers extend the built-in set. Built-in providers are not
/// generally overridable, but the built-in Amazon Bedrock provider allows the
/// user to customize its endpoint, authentication, headers, and AWS settings.
/// generally overridable, but built-in Amazon Bedrock providers allow the user
/// to customize their endpoint, authentication, headers, and AWS settings.
pub fn merge_configured_model_providers(
mut model_providers: HashMap<String, ModelProviderInfo>,
configured_model_providers: HashMap<String, ModelProviderInfo>,
) -> Result<HashMap<String, ModelProviderInfo>, String> {
for (key, mut provider) in configured_model_providers {
if key == AMAZON_BEDROCK_PROVIDER_ID {
if matches!(
key.as_str(),
AMAZON_BEDROCK_PROVIDER_ID | AMAZON_BEDROCK_RUNTIME_PROVIDER_ID
) {
let base_url_override = provider.base_url.take();
let auth_override = provider.auth.take();
let aws_override = provider.aws.take();
let http_headers_override = provider.http_headers.take();
if provider != ModelProviderInfo::default() {
return Err(format!(
"model_providers.{AMAZON_BEDROCK_PROVIDER_ID} only supports changing \
"model_providers.{key} only supports changing \
`base_url`, `auth`, `http_headers`, `aws.profile`, and `aws.region`; other non-default \
provider fields are not supported"
));
}

if let Some(built_in_provider) = model_providers.get_mut(AMAZON_BEDROCK_PROVIDER_ID) {
if let Some(built_in_provider) = model_providers.get_mut(&key) {
built_in_provider.base_url = base_url_override;
built_in_provider.auth = auth_override;
if let Some(aws_override) = aws_override {
Expand Down
Loading
Loading