From b79e75b4e60d30e64487e95ffb8902bad15c8c1e Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 6 Jul 2026 17:09:34 -0700 Subject: [PATCH] app-server: add managed Bedrock login --- codex-rs/app-server/src/request_processors.rs | 2 + .../request_processors/account_processor.rs | 66 ++- .../src/request_processors/bedrock_auth.rs | 35 ++ .../request_processors/config_processor.rs | 2 +- .../tests/common/test_app_server.rs | 14 + codex-rs/app-server/tests/suite/v2/account.rs | 436 ++++++++++++++++++ .../src/amazon_bedrock/mantle.rs | 7 +- .../model-provider/src/amazon_bedrock/mod.rs | 1 + codex-rs/model-provider/src/lib.rs | 2 + 9 files changed, 561 insertions(+), 4 deletions(-) create mode 100644 codex-rs/app-server/src/request_processors/bedrock_auth.rs diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index da88d0e067..46c4b38676 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -387,6 +387,7 @@ use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::complete_device_code_login; use codex_login::login_with_api_key; +use codex_login::login_with_bedrock_api_key; use codex_login::oauth_client_id; use codex_login::request_device_code; use codex_login::run_login_server; @@ -497,6 +498,7 @@ use codex_app_server_protocol::ServerRequest; mod account_processor; mod apps_processor; +mod bedrock_auth; mod catalog_processor; mod command_exec_processor; mod config_processor; diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index d98977ce58..243e45632a 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -1,7 +1,9 @@ +use super::bedrock_auth::set_user_model_provider_to_bedrock; use super::*; use crate::auth_mode::auth_mode_to_api; use crate::external_auth::ExternalAuthBridge; use chrono::DateTime; +use codex_model_provider::is_supported_amazon_bedrock_region; mod rate_limit_resets; @@ -293,8 +295,9 @@ impl AccountRequestProcessor { ) .await; } - LoginAccountParams::AmazonBedrock { .. } => { - return Err(invalid_request("Amazon Bedrock login is not implemented")); + LoginAccountParams::AmazonBedrock { api_key, region } => { + self.login_amazon_bedrock_v2(request_id, api_key, region) + .await; } } Ok(()) @@ -359,6 +362,65 @@ impl AccountRequestProcessor { } } + async fn login_amazon_bedrock_v2( + &self, + request_id: ConnectionRequestId, + api_key: String, + region: String, + ) { + let result = async { + if self.auth_manager.is_external_chatgpt_auth_active() { + return Err(self.external_auth_active_error()); + } + if matches!( + self.config.forced_login_method, + Some(ForcedLoginMethod::Chatgpt) + ) { + return Err(invalid_request( + "Amazon Bedrock login is disabled. Use ChatGPT login instead.", + )); + } + + let api_key = api_key.trim(); + if api_key.is_empty() { + return Err(invalid_request("Amazon Bedrock API key must not be empty.")); + } + let region = region.trim(); + if !is_supported_amazon_bedrock_region(region) { + return Err(invalid_request(format!( + "Amazon Bedrock Mantle does not support region `{region}`" + ))); + } + + { + let mut guard = self.active_login.lock().await; + if let Some(active) = guard.take() { + drop(active); + } + } + + login_with_bedrock_api_key( + &self.config.codex_home, + api_key, + region, + self.config.cli_auth_credentials_store_mode, + self.config.auth_keyring_backend_kind(), + ) + .map_err(|err| internal_error(format!("failed to save Amazon Bedrock auth: {err}")))?; + set_user_model_provider_to_bedrock(&self.config_manager).await?; + self.auth_manager.reload().await; + Ok(LoginAccountResponse::AmazonBedrock {}) + } + .await; + let logged_in = result.is_ok(); + self.outgoing.send_result(request_id, result).await; + + if logged_in { + self.send_login_success_notifications(/*login_id*/ None) + .await; + } + } + // Build options for a ChatGPT login attempt; performs validation. async fn login_chatgpt_common( &self, diff --git a/codex-rs/app-server/src/request_processors/bedrock_auth.rs b/codex-rs/app-server/src/request_processors/bedrock_auth.rs new file mode 100644 index 0000000000..88576e5ffe --- /dev/null +++ b/codex-rs/app-server/src/request_processors/bedrock_auth.rs @@ -0,0 +1,35 @@ +use super::config_processor::map_error as map_config_error; +use crate::config_manager::ConfigManager; +use codex_app_server_protocol::ConfigValueWriteParams; +use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::MergeStrategy; +use codex_model_provider::AMAZON_BEDROCK_PROVIDER_ID; + +pub(super) async fn set_user_model_provider_to_bedrock( + config_manager: &ConfigManager, +) -> Result<(), JSONRPCErrorError> { + write_user_model_provider( + config_manager, + serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID), + /*expected_version*/ None, + ) + .await +} + +async fn write_user_model_provider( + config_manager: &ConfigManager, + value: serde_json::Value, + expected_version: Option, +) -> Result<(), JSONRPCErrorError> { + config_manager + .write_value(ConfigValueWriteParams { + key_path: "model_provider".to_string(), + value, + merge_strategy: MergeStrategy::Replace, + file_path: None, + expected_version, + }) + .await + .map(|_| ()) + .map_err(map_config_error) +} diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index b6dbb30b08..0eeb1aaca9 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -555,7 +555,7 @@ fn map_network_unix_socket_permission_to_api( } } -fn map_error(err: ConfigManagerError) -> JSONRPCErrorError { +pub(super) fn map_error(err: ConfigManagerError) -> JSONRPCErrorError { if let Some(code) = err.write_error_code() { return config_write_error(code, err.to_string()); } diff --git a/codex-rs/app-server/tests/common/test_app_server.rs b/codex-rs/app-server/tests/common/test_app_server.rs index 825ea46ab8..3e289637d6 100644 --- a/codex-rs/app-server/tests/common/test_app_server.rs +++ b/codex-rs/app-server/tests/common/test_app_server.rs @@ -1334,6 +1334,20 @@ impl TestAppServer { self.send_login_account_request(params).await } + /// Send an `account/login/start` JSON-RPC request for managed Amazon Bedrock login. + pub async fn send_login_account_amazon_bedrock_request( + &mut self, + api_key: &str, + region: &str, + ) -> anyhow::Result { + let params = serde_json::json!({ + "type": "amazonBedrock", + "apiKey": api_key, + "region": region, + }); + self.send_request("account/login/start", Some(params)).await + } + /// Send an `account/login/start` JSON-RPC request for ChatGPT login. pub async fn send_login_account_chatgpt_request(&mut self) -> anyhow::Result { let params = serde_json::json!({ diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index a8e8c0903d..be9d6c5a30 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -5,24 +5,30 @@ use app_test_support::to_response; use app_test_support::ChatGptAuthFixture; use app_test_support::ChatGptIdTokenClaims; +use app_test_support::DEFAULT_CLIENT_NAME; use app_test_support::encode_id_token; use app_test_support::write_chatgpt_auth; use app_test_support::write_models_cache; use chrono::Duration as ChronoDuration; use chrono::Utc; use codex_app_server_protocol::Account; +use codex_app_server_protocol::AccountLoginCompletedNotification; +use codex_app_server_protocol::AccountUpdatedNotification; use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::CancelLoginAccountParams; use codex_app_server_protocol::CancelLoginAccountResponse; use codex_app_server_protocol::CancelLoginAccountStatus; use codex_app_server_protocol::ChatgptAuthTokensRefreshReason; use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse; +use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::GetAccountParams; use codex_app_server_protocol::GetAccountResponse; use codex_app_server_protocol::GetAuthStatusParams; use codex_app_server_protocol::GetAuthStatusResponse; +use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LoginAccountResponse; @@ -33,13 +39,17 @@ use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnStatus; use codex_config::types::AuthCredentialsStoreMode; +use codex_login::AuthDotJson; use codex_login::AuthKeyringBackendKind; use codex_login::CLIENT_ID_OVERRIDE_ENV_VAR; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; +use codex_login::auth::BedrockApiKeyAuth; +use codex_login::load_auth_dot_json; use codex_login::login_with_api_key; use codex_login::login_with_bedrock_api_key; use codex_protocol::account::AmazonBedrockCredentialSource; use codex_protocol::account::PlanType as AccountPlanType; +use codex_protocol::auth::AuthMode as DomainAuthMode; use core_test_support::responses; use pretty_assertions::assert_eq; use serde_json::json; @@ -142,6 +152,70 @@ shell_snapshot = false std::fs::write(config_toml, contents) } +fn read_config_toml(codex_home: &Path) -> Result { + Ok(toml::from_str(&std::fs::read_to_string( + codex_home.join("config.toml"), + )?)?) +} + +fn load_file_auth(codex_home: &Path) -> Result> { + Ok(load_auth_dot_json( + codex_home, + AuthCredentialsStoreMode::File, + AuthKeyringBackendKind::default(), + )?) +} + +fn aws_managed_bedrock_config() -> CreateConfigTomlParams { + CreateConfigTomlParams { + model_provider_id: Some("amazon-bedrock".to_string()), + extra_provider_config: Some( + r#"[model_providers.amazon-bedrock.aws] +profile = "codex-bedrock" +region = "us-west-2" +"# + .to_string(), + ), + ..Default::default() + } +} + +async fn read_account(mcp: &mut TestAppServer) -> Result { + let request_id = mcp + .send_get_account_request(GetAccountParams { + refresh_token: false, + }) + .await?; + let response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + to_response(response) +} + +async fn assert_account_updated( + mcp: &mut TestAppServer, + auth_mode: Option, +) -> Result<()> { + let notification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("account/updated"), + ) + .await??; + let ServerNotification::AccountUpdated(payload) = notification.try_into()? else { + bail!("unexpected notification") + }; + assert_eq!( + payload, + AccountUpdatedNotification { + auth_mode, + plan_type: None, + } + ); + Ok(()) +} + async fn mock_device_code_usercode(server: &MockServer, interval_seconds: u64) { Mock::given(method("POST")) .and(path("/api/accounts/deviceauth/usercode")) @@ -1011,6 +1085,368 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> { Ok(()) } +#[tokio::test] +async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; + login_with_api_key( + codex_home.path(), + "sk-test-key", + AuthCredentialsStoreMode::File, + AuthKeyringBackendKind::default(), + )?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let mut expected_config = read_config_toml(codex_home.path())?; + expected_config + .as_table_mut() + .expect("config should be a table") + .insert( + "model_provider".to_string(), + toml::Value::String("amazon-bedrock".to_string()), + ); + let request_id = mcp + .send_login_account_amazon_bedrock_request(" managed-bedrock-api-key ", " us-west-2 ") + .await?; + let response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + to_response::(response)?, + LoginAccountResponse::AmazonBedrock {} + ); + + assert_eq!( + load_file_auth(codex_home.path())?, + Some(AuthDotJson { + auth_mode: Some(DomainAuthMode::BedrockApiKey), + openai_api_key: None, + tokens: None, + last_refresh: None, + agent_identity: None, + personal_access_token: None, + bedrock_api_key: Some(BedrockApiKeyAuth { + api_key: "managed-bedrock-api-key".to_string(), + region: "us-west-2".to_string(), + }), + }) + ); + assert_eq!(read_config_toml(codex_home.path())?, expected_config); + + let notification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("account/login/completed"), + ) + .await??; + let ServerNotification::AccountLoginCompleted(payload) = notification.try_into()? else { + bail!("unexpected notification") + }; + assert_eq!( + payload, + AccountLoginCompletedNotification { + login_id: None, + success: true, + error: None, + } + ); + assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?; + + Ok(()) +} + +#[tokio::test] +async fn managed_bedrock_login_requires_experimental_api() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; + let initialized = mcp + .initialize_with_capabilities( + ClientInfo { + name: DEFAULT_CLIENT_NAME.to_string(), + title: None, + version: "0.1.0".to_string(), + }, + Some(InitializeCapabilities { + experimental_api: false, + ..Default::default() + }), + ) + .await?; + assert!(matches!(initialized, JSONRPCMessage::Response(_))); + + let request_id = mcp + .send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2") + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + error.error.message, + "account/login/start.amazonBedrock requires experimentalApi capability" + ); + assert_eq!(load_file_auth(codex_home.path())?, None); + Ok(()) +} + +#[tokio::test] +async fn login_managed_bedrock_updates_active_bedrock_account() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), aws_managed_bedrock_config())?; + + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let request_id = mcp + .send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2") + .await?; + let response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + to_response::(response)?, + LoginAccountResponse::AmazonBedrock {} + ); + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("account/login/completed"), + ) + .await??; + assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?; + assert_eq!( + read_account(&mut mcp).await?, + GetAccountResponse { + account: Some(Account::AmazonBedrock { + credential_source: AmazonBedrockCredentialSource::CodexManaged, + }), + requires_openai_auth: false, + } + ); + + assert!(codex_home.path().join("auth.json").exists()); + Ok(()) +} + +#[tokio::test] +async fn login_account_amazon_bedrock_rejects_invalid_credentials_without_changes() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; + + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let expected_config = read_config_toml(codex_home.path())?; + + let request_id = mcp + .send_login_account_amazon_bedrock_request(" ", "us-west-2") + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + error.error.message, + "Amazon Bedrock API key must not be empty." + ); + + let request_id = mcp + .send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-1") + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + error.error.message, + "Amazon Bedrock Mantle does not support region `us-west-1`" + ); + assert_eq!(load_file_auth(codex_home.path())?, None); + assert_eq!(read_config_toml(codex_home.path())?, expected_config); + + Ok(()) +} + +#[tokio::test] +async fn login_account_amazon_bedrock_persists_under_provider_override() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; + + let mut mcp = TestAppServer::new_with_args( + codex_home.path(), + &["-c", "model_provider=\"mock_provider\""], + ) + .await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let mut expected_config = read_config_toml(codex_home.path())?; + expected_config + .as_table_mut() + .expect("config should be a table") + .insert( + "model_provider".to_string(), + toml::Value::String("amazon-bedrock".to_string()), + ); + let request_id = mcp + .send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2") + .await?; + let response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + to_response::(response)?, + LoginAccountResponse::AmazonBedrock {} + ); + assert_eq!( + load_file_auth(codex_home.path())?, + Some(AuthDotJson { + auth_mode: Some(DomainAuthMode::BedrockApiKey), + openai_api_key: None, + tokens: None, + last_refresh: None, + agent_identity: None, + personal_access_token: None, + bedrock_api_key: Some(BedrockApiKeyAuth { + api_key: "managed-bedrock-api-key".to_string(), + region: "us-west-2".to_string(), + }), + }) + ); + assert_eq!(read_config_toml(codex_home.path())?, expected_config); + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("account/login/completed"), + ) + .await??; + assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?; + assert_eq!( + read_account(&mut mcp).await?, + GetAccountResponse { + account: None, + requires_openai_auth: false, + } + ); + Ok(()) +} + +#[tokio::test] +async fn login_account_amazon_bedrock_rejected_when_forced_chatgpt() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + CreateConfigTomlParams { + forced_method: Some("chatgpt".to_string()), + ..Default::default() + }, + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let request_id = mcp + .send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2") + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!( + error.error.message, + "Amazon Bedrock login is disabled. Use ChatGPT login instead." + ); + assert_eq!(load_file_auth(codex_home.path())?, None); + Ok(()) +} + +#[tokio::test] +async fn login_account_amazon_bedrock_allowed_when_forced_api() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + CreateConfigTomlParams { + forced_method: Some("api".to_string()), + ..Default::default() + }, + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let request_id = mcp + .send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2") + .await?; + let response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!( + to_response::(response)?, + LoginAccountResponse::AmazonBedrock {} + ); + Ok(()) +} + +#[tokio::test] +async fn login_account_amazon_bedrock_rejected_with_external_chatgpt_auth() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; + let access_token = encode_id_token( + &ChatGptIdTokenClaims::new() + .email("embedded@example.com") + .plan_type("pro") + .chatgpt_account_id(WORKSPACE_ID_EMBEDDED), + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let set_id = mcp + .send_chatgpt_auth_tokens_login_request( + access_token, + WORKSPACE_ID_EMBEDDED.to_string(), + Some("pro".to_string()), + ) + .await?; + let set_response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(set_id)), + ) + .await??; + assert_eq!( + to_response::(set_response)?, + LoginAccountResponse::ChatgptAuthTokens {} + ); + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("account/updated"), + ) + .await??; + + let request_id = mcp + .send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2") + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + error.error.message, + "External auth is active. Use account/login/start (chatgptAuthTokens) to update it or account/logout to clear it." + ); + assert_eq!(load_file_auth(codex_home.path())?, None); + Ok(()) +} + #[tokio::test] async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs index 9727802d11..e31ce96edc 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs @@ -39,8 +39,13 @@ pub(super) fn region_from_config(aws: &ModelProviderAwsAuthInfo) -> Option bool { + BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) +} + pub(super) fn base_url(region: &str) -> Result { - if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) { + if is_supported_amazon_bedrock_region(region) { Ok(format!("https://bedrock-mantle.{region}.api.aws/openai/v1")) } else { Err(CodexErr::Fatal(format!( diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs index 9fdf937bcb..2a7230e081 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mod.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -31,6 +31,7 @@ use crate::provider::ProviderCapabilities; use auth::resolve_provider_auth; pub(crate) use catalog::static_model_catalog; use catalog::with_default_only_service_tier; +pub use mantle::is_supported_amazon_bedrock_region; use mantle::runtime_base_url; /// Runtime provider for Amazon Bedrock's OpenAI-compatible Mantle endpoint. diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs index 39ff31e6e6..47d477b88b 100644 --- a/codex-rs/model-provider/src/lib.rs +++ b/codex-rs/model-provider/src/lib.rs @@ -4,6 +4,7 @@ mod bearer_auth_provider; mod models_endpoint; mod provider; +pub use amazon_bedrock::is_supported_amazon_bedrock_region; pub use auth::AgentIdentitySessionFallback; pub use auth::ProviderAuthScope; pub use auth::ResolvedProviderAuth; @@ -12,6 +13,7 @@ pub use auth::auth_provider_from_auth_manager; pub use auth::unauthenticated_auth_provider; pub use bearer_auth_provider::BearerAuthProvider; pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider; +pub use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID; pub use codex_model_provider_info::CHATGPT_CODEX_BASE_URL; pub use codex_protocol::account::ProviderAccount; pub use provider::ModelProvider;