Implement Amazon Bedrock setup in the app server (#40007)

## What changed

- Implement `account/bedrock/discover` to report AWS profiles and available environment credentials.
- Implement `account/bedrock/setup` for validated AWS profiles and environment credentials, persisting the selected region and profile in the Bedrock provider configuration.
- Resolve Bedrock authentication according to the selected credential source, and clear stale profile configuration when using a Codex-managed Bedrock API key.

## Testing

- Cover profile and environment discovery, setup validation and persistence, conflicting credentials, and authentication-source precedence.

GitOrigin-RevId: 16d700cb0843df768c2fdb6d0c8d26c3402e93df
This commit is contained in:
alexsong-oai
2026-08-21 20:57:17 +00:00
committed by copyberry
parent 51d8d12236
commit e77c2a90af
17 changed files with 951 additions and 90 deletions
+1
View File
@@ -1995,6 +1995,7 @@ dependencies = [
"codex-app-server-protocol",
"codex-app-server-transport",
"codex-arg0",
"codex-aws-auth",
"codex-backend-client",
"codex-chatgpt",
"codex-cloud-config",
@@ -53,10 +53,7 @@ pub enum BedrockSetupParams {
Profile { profile: String, region: String },
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Environment {
credential_type: AwsCredentialType,
region: String,
},
Environment { region: String },
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
AccessKeys {
+1
View File
@@ -36,6 +36,7 @@ axum = { workspace = true, default-features = false, features = [
codex-analytics = { workspace = true }
codex-agent-extension = { workspace = true }
codex-arg0 = { workspace = true }
codex-aws-auth = { workspace = true }
codex-cloud-config = { workspace = true }
codex-code-mode = { workspace = true }
codex-config = { workspace = true }
+6 -3
View File
@@ -1541,9 +1541,12 @@ impl MessageProcessor {
.login_account(request_id.clone(), params)
.await
}
ClientRequest::BedrockDiscover { .. } | ClientRequest::BedrockSetup { .. } => Err(
crate::error_code::method_not_found("Amazon Bedrock setup is not implemented"),
),
ClientRequest::BedrockDiscover { params, .. } => {
self.account_processor.bedrock_discover(params).await
}
ClientRequest::BedrockSetup { params, .. } => {
self.account_processor.bedrock_setup(params).await
}
ClientRequest::LogoutAccount { .. } => {
self.account_processor
.logout_account(request_id.clone())
@@ -1,5 +1,5 @@
use super::bedrock_auth::clear_user_model_provider_if_bedrock;
use super::bedrock_auth::set_user_model_provider_to_bedrock;
use super::bedrock_auth::configure_bedrock_for_managed_auth;
use super::*;
use crate::auth_mode::auth_mode_to_api;
use crate::external_auth::ExternalAuthBridge;
@@ -8,6 +8,7 @@ use codex_app_server_protocol::DesktopOnboardingEntrypoint;
use codex_login::LoginOnboardingEntrypoint;
use codex_model_provider::is_supported_amazon_bedrock_region;
mod bedrock_setup;
mod rate_limit_resets;
// Duration before a browser ChatGPT login attempt is abandoned.
@@ -344,6 +345,24 @@ impl AccountRequestProcessor {
)
}
fn ensure_bedrock_login_allowed(&self) -> Result<(), JSONRPCErrorError> {
if self.auth_manager.is_workload_identity_selected() {
return Err(self.configured_auth_owned_by_host_error());
}
if self.auth_manager.is_external_chatgpt_auth_active() {
return Err(self.external_auth_active_error());
}
if !self
.auth_manager
.is_login_method_allowed(ForcedLoginMethod::Api)
{
return Err(invalid_request(
"Amazon Bedrock login is disabled. Use ChatGPT login instead.",
));
}
Ok(())
}
async fn login_api_key_common(
&self,
params: &LoginApiKeyParams,
@@ -405,17 +424,7 @@ impl AccountRequestProcessor {
region: String,
) {
let result = async {
if self.auth_manager.is_external_chatgpt_auth_active() {
return Err(self.external_auth_active_error());
}
if !self
.auth_manager
.is_login_method_allowed(ForcedLoginMethod::Api)
{
return Err(invalid_request(
"Amazon Bedrock login is disabled. Use ChatGPT login instead.",
));
}
self.ensure_bedrock_login_allowed()?;
let api_key = api_key.trim();
if api_key.is_empty() {
@@ -435,7 +444,7 @@ impl AccountRequestProcessor {
}
}
set_user_model_provider_to_bedrock(&self.config_manager).await?;
configure_bedrock_for_managed_auth(&self.config_manager).await?;
login_with_bedrock_api_key(
&self.config.codex_home,
api_key,
@@ -0,0 +1,174 @@
use super::super::bedrock_auth::ensure_user_model_provider_can_be_bedrock;
use super::super::config_processor::map_error as map_config_error;
use super::AccountRequestProcessor;
use crate::error_code::internal_error;
use crate::error_code::invalid_request;
use codex_app_server_protocol::AwsCredentialType;
use codex_app_server_protocol::BedrockAwsProfile;
use codex_app_server_protocol::BedrockDiscoverParams;
use codex_app_server_protocol::BedrockDiscoverResponse;
use codex_app_server_protocol::BedrockEnvironmentCredential;
use codex_app_server_protocol::BedrockSetupParams;
use codex_app_server_protocol::BedrockSetupResponse;
use codex_app_server_protocol::ClientResponsePayload;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigEdit;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::MergeStrategy;
use codex_login::CodexAuth;
use codex_model_provider::AMAZON_BEDROCK_PROVIDER_ID;
use codex_model_provider::is_supported_amazon_bedrock_region;
const AWS_ACCESS_KEY_ID: &str = "AWS_ACCESS_KEY_ID";
const AWS_SECRET_ACCESS_KEY: &str = "AWS_SECRET_ACCESS_KEY";
const AWS_BEARER_TOKEN_BEDROCK: &str = "AWS_BEARER_TOKEN_BEDROCK";
impl AccountRequestProcessor {
pub(crate) async fn bedrock_discover(
&self,
_params: BedrockDiscoverParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.ensure_bedrock_login_allowed()?;
let profiles = codex_aws_auth::discover_aws_profiles()
.await
.map_err(|err| internal_error(format!("failed to discover AWS profiles: {err}")))?
.into_iter()
.map(|profile| BedrockAwsProfile {
name: profile.name,
region: profile.region,
})
.collect();
let region = non_empty_env_var("AWS_REGION");
let environment_credentials = [
(
AwsCredentialType::AccessKeys,
non_empty_env_var(AWS_ACCESS_KEY_ID).is_some()
&& non_empty_env_var(AWS_SECRET_ACCESS_KEY).is_some(),
),
(
AwsCredentialType::BedrockApiKey,
non_empty_env_var(AWS_BEARER_TOKEN_BEDROCK).is_some(),
),
]
.into_iter()
.filter(|(_, available)| *available)
.map(|(credential_type, _)| BedrockEnvironmentCredential {
credential_type,
region: region.clone(),
})
.collect();
Ok(Some(
BedrockDiscoverResponse {
profiles,
environment_credentials,
}
.into(),
))
}
pub(crate) async fn bedrock_setup(
&self,
params: BedrockSetupParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.ensure_bedrock_login_allowed()?;
ensure_user_model_provider_can_be_bedrock(&self.config_manager).await?;
if matches!(&params, BedrockSetupParams::Environment { .. })
&& matches!(
self.auth_manager.auth_cached(),
Some(CodexAuth::BedrockApiKey(_))
)
{
return Err(invalid_request(
"A Bedrock API key is already configured and takes priority over AWS credentials. Run `codex logout` and try again.",
));
}
let region = match &params {
BedrockSetupParams::Profile { region, .. }
| BedrockSetupParams::Environment { region, .. }
| BedrockSetupParams::AccessKeys { region, .. } => region.trim(),
};
if !is_supported_amazon_bedrock_region(region) {
return Err(invalid_request(format!(
"Amazon Bedrock Mantle does not support region `{region}`"
)));
}
let profile = match &params {
BedrockSetupParams::Profile { profile, .. } => {
let profile = profile.trim();
if profile.is_empty() {
return Err(invalid_request("AWS profile name must not be empty."));
}
codex_aws_auth::validate_aws_profile(profile, region)
.await
.map_err(|err| {
invalid_request(format!(
"failed to load credentials for AWS profile `{profile}`: {err}"
))
})?;
Some(profile.to_string())
}
BedrockSetupParams::Environment { .. } => {
let has_bedrock_api_key = non_empty_env_var(AWS_BEARER_TOKEN_BEDROCK).is_some();
let has_access_keys = non_empty_env_var(AWS_ACCESS_KEY_ID).is_some()
&& non_empty_env_var(AWS_SECRET_ACCESS_KEY).is_some();
if !has_bedrock_api_key && !has_access_keys {
return Err(invalid_request(
"No AWS credentials found. Please Configure AWS credentials or complete AWS sign-in, then try again.",
));
}
None
}
BedrockSetupParams::AccessKeys { .. } => {
// TODO: Support direct AWS access key setup with Codex-managed credential storage.
return Err(invalid_request(
"Direct AWS access key setup is not supported yet.",
));
}
};
// TODO: Validate the selected credentials against Bedrock ListModels once supported.
self.cancel_active_login().await;
self.config_manager
.batch_write(ConfigBatchWriteParams {
edits: [
(
"model_provider",
serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID),
),
(
"model_providers.amazon-bedrock.aws.region",
serde_json::json!(region),
),
(
"model_providers.amazon-bedrock.aws.profile",
serde_json::json!(profile),
),
]
.into_iter()
.map(|(key_path, value)| ConfigEdit {
key_path: key_path.to_string(),
value,
merge_strategy: MergeStrategy::Replace,
})
.collect(),
file_path: None,
expected_version: None,
reload_user_config: false,
})
.await
.map_err(map_config_error)?;
Ok(Some(BedrockSetupResponse {}.into()))
}
}
fn non_empty_env_var(name: &str) -> Option<String> {
std::env::var(name)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
@@ -2,7 +2,8 @@ use super::config_processor::map_error as map_config_error;
use crate::config_manager::ConfigManager;
use crate::error_code::internal_error;
use crate::error_code::invalid_request;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigEdit;
use codex_app_server_protocol::ConfigWriteErrorCode;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::MergeStrategy;
@@ -11,7 +12,40 @@ use codex_config::ConfigLayerSource;
use codex_config::format_config_layer_source;
use codex_model_provider::AMAZON_BEDROCK_PROVIDER_ID;
pub(super) async fn set_user_model_provider_to_bedrock(
pub(super) async fn configure_bedrock_for_managed_auth(
config_manager: &ConfigManager,
) -> Result<(), JSONRPCErrorError> {
ensure_user_model_provider_can_be_bedrock(config_manager).await?;
config_manager
.batch_write(ConfigBatchWriteParams {
edits: [
(
"model_provider",
serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID),
),
(
"model_providers.amazon-bedrock.aws.profile",
serde_json::Value::Null,
),
]
.into_iter()
.map(|(key_path, value)| ConfigEdit {
key_path: key_path.to_string(),
value,
merge_strategy: MergeStrategy::Replace,
})
.collect(),
file_path: None,
expected_version: None,
reload_user_config: false,
})
.await
.map(|_| ())
.map_err(map_config_error)
}
pub(super) async fn ensure_user_model_provider_can_be_bedrock(
config_manager: &ConfigManager,
) -> Result<(), JSONRPCErrorError> {
let layers = config_manager
@@ -45,17 +79,7 @@ pub(super) async fn set_user_model_provider_to_bedrock(
)));
}
config_manager
.write_value(ConfigValueWriteParams {
key_path: "model_provider".to_string(),
value: serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID),
merge_strategy: MergeStrategy::Replace,
file_path: None,
expected_version: None,
})
.await
.map(|_| ())
.map_err(map_config_error)
Ok(())
}
pub(super) async fn clear_user_model_provider_if_bedrock(
@@ -1099,6 +1099,18 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
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())?;
let config_path = codex_home.path().join("config.toml");
let original_config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
format!(
"{original_config}\n[model_providers.amazon-bedrock]\n\
http_headers = {{ X-Existing = \"preserved\" }}\n\
[model_providers.amazon-bedrock.aws]\n\
profile = \"stale-profile\"\n\
region = \"us-east-1\"\n"
),
)?;
login_with_api_key(
codex_home.path(),
"sk-test-key",
@@ -1119,6 +1131,10 @@ async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider() -> R
"model_provider".to_string(),
toml::Value::String("amazon-bedrock".to_string()),
);
expected_config["model_providers"]["amazon-bedrock"]["aws"]
.as_table_mut()
.expect("AWS configuration should be a table")
.remove("profile");
let request_id = mcp
.send_login_account_amazon_bedrock_request(" managed-bedrock-api-key ", " us-west-2 ")
.await?;
@@ -1167,6 +1183,15 @@ async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider() -> R
}
);
assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?;
assert_eq!(
read_account(&mut mcp).await?,
GetAccountResponse {
account: Some(Account::AmazonBedrock {
uses_codex_managed_credentials: true,
}),
requires_openai_auth: false,
}
);
Ok(())
}
@@ -0,0 +1,332 @@
use anyhow::Result;
use app_test_support::TestAppServer;
use codex_app_server_protocol::BedrockDiscoverParams;
use codex_app_server_protocol::BedrockDiscoverResponse;
use codex_app_server_protocol::BedrockSetupParams;
use codex_app_server_protocol::BedrockSetupResponse;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::RequestId;
use codex_config::types::AuthCredentialsStoreMode;
use codex_login::AuthKeyringBackendKind;
use codex_login::login_with_bedrock_api_key;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;
const READ_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 60);
async fn bedrock_app_server(
codex_home: &Path,
environment: &[(&str, Option<&str>)],
) -> Result<TestAppServer> {
let aws_config = codex_home.join("aws-config");
let aws_credentials = codex_home.join("aws-credentials");
std::fs::write(&aws_config, "[profile engineering]\nregion = us-west-2\n")?;
std::fs::write(
&aws_credentials,
"[engineering]\naws_access_key_id = engineering-id\naws_secret_access_key = engineering-secret\n\
[finance]\naws_access_key_id = finance-id\naws_secret_access_key = finance-secret\n",
)?;
let aws_config_path = aws_config.to_string_lossy();
let aws_credentials_path = aws_credentials.to_string_lossy();
TestAppServer::builder()
.with_codex_home(codex_home)
.with_env_overrides(&[
("AWS_CONFIG_FILE", Some(aws_config_path.as_ref())),
(
"AWS_SHARED_CREDENTIALS_FILE",
Some(aws_credentials_path.as_ref()),
),
("AWS_PROFILE", None),
("AWS_ACCESS_KEY_ID", None),
("AWS_SECRET_ACCESS_KEY", None),
("AWS_SESSION_TOKEN", None),
("AWS_BEARER_TOKEN_BEDROCK", None),
("AWS_REGION", None),
("AWS_DEFAULT_REGION", None),
("AWS_EC2_METADATA_DISABLED", Some("true")),
])
.with_env_overrides(environment)
.build_initialized_with_timeout(READ_TIMEOUT)
.await
}
#[tokio::test]
async fn discover_bedrock_profiles_and_environment_credentials() -> Result<()> {
let codex_home = TempDir::new()?;
let mut app_server = bedrock_app_server(
codex_home.path(),
&[
("AWS_PROFILE", Some("engineering")),
("AWS_ACCESS_KEY_ID", Some("environment-id")),
("AWS_SECRET_ACCESS_KEY", Some("environment-secret")),
("AWS_SESSION_TOKEN", Some("environment-token")),
("AWS_BEARER_TOKEN_BEDROCK", Some("environment-bedrock-key")),
("AWS_DEFAULT_REGION", Some("us-west-2")),
],
)
.await?;
let response: BedrockDiscoverResponse = app_server
.request(|request_id| ClientRequest::BedrockDiscover {
request_id,
params: BedrockDiscoverParams {},
})
.await?;
assert_eq!(
serde_json::to_value(response)?,
json!({
"profiles": [
{"name": "engineering", "region": "us-west-2"},
{"name": "finance", "region": null}
],
"environmentCredentials": [
{"type": "accessKeys", "region": null},
{"type": "bedrockApiKey", "region": null}
]
})
);
let response: BedrockSetupResponse = app_server
.request(|request_id| ClientRequest::BedrockSetup {
request_id,
params: BedrockSetupParams::Environment {
region: " us-east-2 ".to_string(),
},
})
.await?;
assert_eq!(response, BedrockSetupResponse {});
let config: toml::Value = toml::from_str(&std::fs::read_to_string(
codex_home.path().join("config.toml"),
)?)?;
assert_eq!(
config,
toml::toml! {
model_provider = "amazon-bedrock"
[model_providers.amazon-bedrock.aws]
region = "us-east-2"
}
.into()
);
assert!(!codex_home.path().join(".env").exists());
Ok(())
}
#[tokio::test]
async fn setup_bedrock_profile_and_environment() -> Result<()> {
let codex_home = TempDir::new()?;
let config_path = codex_home.path().join("config.toml");
let dotenv_path = codex_home.path().join(".env");
std::fs::write(
&config_path,
"[model_providers.amazon-bedrock]\nhttp_headers = { X-Existing = \"preserved\" }\n\
[model_providers.amazon-bedrock.aws]\nprofile = \"old\"\nregion = \"us-east-1\"\n",
)?;
let existing_dotenv = "# existing configuration\nUNRELATED=value\nAWS_ACCESS_KEY_ID=old-id\n\
export AWS_SECRET_ACCESS_KEY=old-secret\nAWS_SESSION_TOKEN=stale-token\n";
std::fs::write(&dotenv_path, existing_dotenv)?;
let mut app_server = bedrock_app_server(
codex_home.path(),
&[
("AWS_ACCESS_KEY_ID", Some("environment-id")),
("AWS_SECRET_ACCESS_KEY", Some("environment-secret")),
("AWS_DEFAULT_REGION", Some("us-east-1")),
],
)
.await?;
let response: BedrockSetupResponse = app_server
.request(|request_id| ClientRequest::BedrockSetup {
request_id,
params: BedrockSetupParams::Profile {
profile: " engineering ".to_string(),
region: " us-west-2 ".to_string(),
},
})
.await?;
assert_eq!(response, BedrockSetupResponse {});
let config: toml::Value = toml::from_str(&std::fs::read_to_string(&config_path)?)?;
assert_eq!(
serde_json::to_value(&config["model_providers"]["amazon-bedrock"]["aws"])?,
json!({"profile": "engineering", "region": "us-west-2"})
);
assert_eq!(std::fs::read_to_string(&dotenv_path)?, existing_dotenv);
let response: BedrockSetupResponse = app_server
.request(|request_id| ClientRequest::BedrockSetup {
request_id,
params: BedrockSetupParams::Environment {
region: "us-east-1".to_string(),
},
})
.await?;
assert_eq!(response, BedrockSetupResponse {});
assert_eq!(std::fs::read_to_string(&dotenv_path)?, existing_dotenv);
let config: toml::Value = toml::from_str(&std::fs::read_to_string(&config_path)?)?;
assert_eq!(
serde_json::to_value(&config["model_providers"]["amazon-bedrock"]["aws"])?,
json!({"region": "us-east-1"})
);
let config: toml::Value = toml::from_str(&std::fs::read_to_string(config_path)?)?;
assert_eq!(
config,
toml::toml! {
model_provider = "amazon-bedrock"
[model_providers.amazon-bedrock]
http_headers = { X-Existing = "preserved" }
[model_providers.amazon-bedrock.aws]
region = "us-east-1"
}
.into()
);
let managed_home = TempDir::new()?;
let managed_config_path = managed_home.path().join("config.toml");
std::fs::write(&managed_config_path, "")?;
login_with_bedrock_api_key(
managed_home.path(),
"managed-bedrock-api-key",
"us-east-1",
AuthCredentialsStoreMode::File,
AuthKeyringBackendKind::default(),
)?;
let auth_path = managed_home.path().join("auth.json");
let expected_auth = std::fs::read_to_string(&auth_path)?;
let mut app_server = bedrock_app_server(
managed_home.path(),
&[(
"AWS_BEARER_TOKEN_BEDROCK",
Some("environment-bedrock-api-key"),
)],
)
.await?;
let mut expected_config: toml::Value =
toml::from_str(&std::fs::read_to_string(&managed_config_path)?)?;
expected_config
.as_table_mut()
.expect("config should be a table")
.extend(toml::toml! {
model_provider = "amazon-bedrock"
[model_providers.amazon-bedrock.aws]
profile = "engineering"
region = "us-west-2"
});
let response: BedrockSetupResponse = app_server
.request(|request_id| ClientRequest::BedrockSetup {
request_id,
params: BedrockSetupParams::Profile {
profile: "engineering".to_string(),
region: "us-west-2".to_string(),
},
})
.await?;
assert_eq!(response, BedrockSetupResponse {});
assert_eq!(
toml::from_str::<toml::Value>(&std::fs::read_to_string(&managed_config_path)?)?,
expected_config
);
let request_id = app_server
.send_raw_request(
"account/bedrock/setup",
Some(json!({"type": "environment", "region": "us-east-1"})),
)
.await?;
let error = timeout(
READ_TIMEOUT,
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(
error.error.message,
"A Bedrock API key is already configured and takes priority over AWS credentials. Run `codex logout` and try again."
);
assert_eq!(std::fs::read_to_string(&auth_path)?, expected_auth);
assert_eq!(
toml::from_str::<toml::Value>(&std::fs::read_to_string(&managed_config_path)?)?,
expected_config
);
assert!(!managed_home.path().join(".env").exists());
Ok(())
}
#[tokio::test]
async fn setup_bedrock_rejects_invalid_or_conflicting_credentials() -> Result<()> {
let codex_home = TempDir::new()?;
let mut app_server = bedrock_app_server(codex_home.path(), &[]).await?;
for (params, expected_error) in [
(
json!({"type": "profile", "profile": "engineering", "region": "us-west-1"}),
"Amazon Bedrock Mantle does not support region `us-west-1`",
),
(
json!({"type": "profile", "profile": " ", "region": "us-west-2"}),
"AWS profile name must not be empty.",
),
(
json!({"type": "profile", "profile": "missing", "region": "us-west-2"}),
"failed to load credentials for AWS profile `missing`:",
),
(
json!({"type": "environment", "region": "us-west-2"}),
"No AWS credentials found. Please Configure AWS credentials or complete AWS sign-in, then try again.",
),
(
json!({
"type": "accessKeys",
"accessKeyId": "test-id",
"secretAccessKey": "test-secret",
"region": "us-west-2",
}),
"Direct AWS access key setup is not supported yet.",
),
] {
let request_id = app_server
.send_raw_request("account/bedrock/setup", Some(params))
.await?;
let error = timeout(
READ_TIMEOUT,
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert!(
error.error.message.contains(expected_error),
"expected {expected_error:?}, got {:?}",
error.error.message
);
}
assert!(!codex_home.path().join(".env").exists());
assert!(!codex_home.path().join("config.toml").exists());
let home = TempDir::new()?;
std::fs::write(
home.path().join("config.toml"),
"forced_login_method = \"chatgpt\"\n",
)?;
let mut app_server = bedrock_app_server(home.path(), &[]).await?;
let request_id = app_server
.send_raw_request("account/bedrock/discover", Some(json!({})))
.await?;
let error = timeout(
READ_TIMEOUT,
app_server.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!(!home.path().join(".env").exists());
Ok(())
}
@@ -6,6 +6,7 @@ mod app_list;
mod app_read;
mod attestation;
mod auto_env;
mod bedrock_setup;
mod client_metadata;
mod code_mode_host;
mod collaboration_mode_list;
+75
View File
@@ -0,0 +1,75 @@
use aws_config::default_provider::region::DefaultRegionChain;
use aws_config::profile::ProfileFileCredentialsProvider;
use aws_config::provider_config::ProviderConfig;
use aws_credential_types::provider::ProvideCredentials;
use aws_types::os_shim_internal::Env;
use aws_types::os_shim_internal::Fs;
use aws_types::region::Region;
use crate::AwsAuthError;
/// A named AWS profile and the region configured directly on that profile.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AwsProfile {
pub name: String,
pub region: Option<String>,
}
/// Discovers AWS profiles using the SDK's shared config and credentials files.
pub async fn discover_aws_profiles() -> Result<Vec<AwsProfile>, AwsAuthError> {
let profiles = aws_config::profile::load(
&Fs::real(),
&Env::real(),
&Default::default(),
/*selected_profile_override*/ None,
)
.await?;
let selected_profile = profiles.selected_profile();
let mut discovered = profiles
.profiles()
.filter_map(|name| {
profiles.get_profile(name).map(|profile| AwsProfile {
name: name.to_string(),
region: profile.get("region").map(str::to_string),
})
})
.collect::<Vec<_>>();
discovered.sort_by(|left, right| {
(left.name != selected_profile, &left.name)
.cmp(&(right.name != selected_profile, &right.name))
});
Ok(discovered)
}
/// Resolves credentials only from the selected AWS profile.
pub async fn validate_aws_profile(profile: &str, region: &str) -> Result<(), AwsAuthError> {
profile_credentials_provider(profile, Some(region))
.await
.provide_credentials()
.await?;
Ok(())
}
pub(crate) async fn profile_credentials_provider(
profile: &str,
region: Option<&str>,
) -> ProfileFileCredentialsProvider {
let region = match region {
Some(region) => Some(Region::new(region.to_string())),
None => {
DefaultRegionChain::builder()
.profile_name(profile)
.build()
.region()
.await
}
};
let provider_config = ProviderConfig::without_region().with_region(region);
ProfileFileCredentialsProvider::builder()
.configure(&provider_config)
.profile_name(profile)
.build()
}
+36 -1
View File
@@ -1,4 +1,5 @@
mod config;
mod discovery;
mod signing;
use std::time::SystemTime;
@@ -10,6 +11,10 @@ use http::HeaderMap;
use http::Method;
use thiserror::Error;
pub use discovery::AwsProfile;
pub use discovery::discover_aws_profiles;
pub use discovery::validate_aws_profile;
/// AWS auth configuration used to resolve credentials and sign requests.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AwsAuthConfig {
@@ -39,10 +44,14 @@ pub struct AwsSignedRequest {
pub enum AwsAuthError {
#[error("AWS service name must not be empty")]
EmptyService,
#[error("AWS profile must be configured")]
MissingProfile,
#[error("AWS SDK config did not resolve a credentials provider")]
MissingCredentialsProvider,
#[error("AWS SDK config did not resolve a region")]
MissingRegion,
#[error("failed to load AWS profiles: {0}")]
ProfileLoad(#[from] aws_config::profile::ProfileFileLoadError),
#[error("failed to load AWS credentials: {0}")]
Credentials(#[from] aws_credential_types::provider::error::CredentialsError),
#[error("request URL is not a valid URI: {0}")]
@@ -89,6 +98,19 @@ impl AwsAuthContext {
})
}
pub async fn load_profile(config: AwsAuthConfig) -> Result<Self, AwsAuthError> {
let profile = config
.profile
.as_deref()
.ok_or(AwsAuthError::MissingProfile)?;
let credentials_provider = SharedCredentialsProvider::new(
discovery::profile_credentials_provider(profile, config.region.as_deref()).await,
);
let mut context = Self::load(config).await?;
context.credentials_provider = credentials_provider;
Ok(context)
}
pub fn region(&self) -> &str {
&self.region
}
@@ -121,8 +143,10 @@ impl AwsAuthError {
| aws_credential_types::provider::error::CredentialsError::ProviderError(_)
),
AwsAuthError::EmptyService
| AwsAuthError::MissingProfile
| AwsAuthError::MissingCredentialsProvider
| AwsAuthError::MissingRegion
| AwsAuthError::ProfileLoad(_)
| AwsAuthError::InvalidUri(_)
| AwsAuthError::BuildHttpRequest(_)
| AwsAuthError::InvalidHeaderValue(_)
@@ -217,6 +241,7 @@ mod tests {
#[test]
fn deterministic_aws_auth_errors_are_not_retryable() {
assert!(!AwsAuthError::EmptyService.is_retryable());
assert!(!AwsAuthError::MissingProfile.is_retryable());
assert!(
!AwsAuthError::Credentials(CredentialsError::not_loaded_no_source()).is_retryable()
);
@@ -247,7 +272,7 @@ mod tests {
}
#[tokio::test]
async fn load_rejects_empty_service_name() {
async fn load_rejects_invalid_configuration() {
let err = AwsAuthContext::load(AwsAuthConfig {
profile: None,
region: None,
@@ -257,5 +282,15 @@ mod tests {
.expect_err("empty service should be rejected");
assert_eq!(err.to_string(), "AWS service name must not be empty");
let err = AwsAuthContext::load_profile(AwsAuthConfig {
profile: None,
region: Some("us-east-1".to_string()),
service: "bedrock".to_string(),
})
.await
.expect_err("profile auth should require a configured profile");
assert_eq!(err.to_string(), "AWS profile must be configured");
}
}
@@ -34,6 +34,7 @@ const AWS_DEFAULT_REGION_ENV_VAR: &str = "AWS_DEFAULT_REGION";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum BedrockAuthSource {
CommandBearerToken,
ConfiguredAwsProfile,
ManagedBearerToken,
EnvBearerToken,
EnvAwsCredentials,
@@ -53,6 +54,12 @@ pub(super) fn auth_source(
) -> BedrockAuthSource {
if provider_info.has_command_auth() {
BedrockAuthSource::CommandBearerToken
} else if provider_info
.aws
.as_ref()
.is_some_and(|aws| aws.profile.is_some())
{
BedrockAuthSource::ConfiguredAwsProfile
} else if matches!(
auth_manager.and_then(AuthManager::auth_cached),
Some(CodexAuth::BedrockApiKey(_))
@@ -70,38 +77,69 @@ pub(super) fn auth_source(
}
pub(super) async fn resolve_auth_method(
source: BedrockAuthSource,
managed_auth: Option<&BedrockApiKeyAuth>,
aws: &ModelProviderAwsAuthInfo,
endpoint: BedrockEndpoint,
) -> Result<BedrockAuthMethod> {
if let Some(managed_auth) = managed_auth {
return Ok(BedrockAuthMethod::ManagedBearerToken {
token: managed_auth.api_key.clone(),
region: managed_auth.region.clone(),
});
match source {
BedrockAuthSource::CommandBearerToken => Err(CodexErr::Fatal(
"Amazon Bedrock command authentication must be resolved by the model provider"
.to_string(),
)),
BedrockAuthSource::ManagedBearerToken => {
let managed_auth = managed_auth.ok_or_else(|| {
CodexErr::Fatal(
"selected Codex-managed Amazon Bedrock API key is no longer available"
.to_string(),
)
})?;
Ok(BedrockAuthMethod::ManagedBearerToken {
token: managed_auth.api_key.clone(),
region: managed_auth.region.clone(),
})
}
BedrockAuthSource::EnvBearerToken => {
let token = non_empty_env_var_from(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR, std::env::var)
.ok_or_else(|| {
CodexErr::Fatal(
"selected `AWS_BEARER_TOKEN_BEDROCK` credential is no longer available"
.to_string(),
)
})?;
let region = bearer_token_region(aws, std::env::var)?;
Ok(BedrockAuthMethod::EnvBearerToken { token, region })
}
BedrockAuthSource::ConfiguredAwsProfile => {
let config = match endpoint {
BedrockEndpoint::Mantle => aws_auth_config(aws),
BedrockEndpoint::Runtime => runtime::aws_auth_config(aws),
};
let context = AwsAuthContext::load_profile(config)
.await
.map_err(aws_auth_error_to_codex_error)?;
Ok(BedrockAuthMethod::AwsSdkAuth { context })
}
BedrockAuthSource::EnvAwsCredentials | BedrockAuthSource::AwsSdk => {
let config = match endpoint {
BedrockEndpoint::Mantle => aws_auth_config(aws),
BedrockEndpoint::Runtime => runtime::aws_auth_config(aws),
};
let context = AwsAuthContext::load(config)
.await
.map_err(aws_auth_error_to_codex_error)?;
Ok(BedrockAuthMethod::AwsSdkAuth { context })
}
}
if let Some(token) = non_empty_env_var_from(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR, std::env::var) {
let region = bearer_token_region(aws, std::env::var)?;
return Ok(BedrockAuthMethod::EnvBearerToken { token, region });
}
let config = match endpoint {
BedrockEndpoint::Mantle => aws_auth_config(aws),
BedrockEndpoint::Runtime => runtime::aws_auth_config(aws),
};
let context = AwsAuthContext::load(config)
.await
.map_err(aws_auth_error_to_codex_error)?;
Ok(BedrockAuthMethod::AwsSdkAuth { context })
}
pub(super) async fn resolve_provider_auth(
source: BedrockAuthSource,
managed_auth: Option<&BedrockApiKeyAuth>,
aws: &ModelProviderAwsAuthInfo,
endpoint: BedrockEndpoint,
) -> Result<SharedAuthProvider> {
match resolve_auth_method(managed_auth, aws, endpoint).await? {
match resolve_auth_method(source, managed_auth, aws, endpoint).await? {
BedrockAuthMethod::ManagedBearerToken { token, .. }
| BedrockAuthMethod::EnvBearerToken { token, .. } => Ok(Arc::new(BearerAuthProvider {
token: Some(token),
@@ -115,11 +153,23 @@ pub(super) async fn resolve_provider_auth(
}
pub(super) async fn resolve_region(
source: BedrockAuthSource,
managed_auth: Option<&BedrockApiKeyAuth>,
aws: &ModelProviderAwsAuthInfo,
endpoint: BedrockEndpoint,
) -> Result<String> {
match resolve_auth_method(managed_auth, aws, endpoint).await? {
if source == BedrockAuthSource::CommandBearerToken {
let config = match endpoint {
BedrockEndpoint::Mantle => aws_auth_config(aws),
BedrockEndpoint::Runtime => runtime::aws_auth_config(aws),
};
let context = AwsAuthContext::load(config)
.await
.map_err(aws_auth_error_to_codex_error)?;
return Ok(context.region().to_string());
}
match resolve_auth_method(source, managed_auth, aws, endpoint).await? {
BedrockAuthMethod::ManagedBearerToken { region, .. }
| BedrockAuthMethod::EnvBearerToken { region, .. } => Ok(region),
BedrockAuthMethod::AwsSdkAuth { context } => Ok(context.region().to_string()),
@@ -239,20 +289,71 @@ mod tests {
#[test]
fn bedrock_auth_source_distinguishes_static_environment_credentials() {
let provider = ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None);
let cases: &[(&[&str], BedrockAuthSource)] = &[
let configured_profile_provider =
ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo {
profile: Some("configured-profile".to_string()),
region: Some("us-west-2".to_string()),
auth_refresh: None,
}));
let managed_auth =
AuthManager::from_auth_for_testing(CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
api_key: "managed-bedrock-api-key".to_string(),
region: "us-east-1".to_string(),
}));
let cases: &[(
&ModelProviderInfo,
Option<&AuthManager>,
&[&str],
BedrockAuthSource,
)] = &[
(
&configured_profile_provider,
Some(managed_auth.as_ref()),
&[
AWS_BEARER_TOKEN_BEDROCK_ENV_VAR,
AWS_ACCESS_KEY_ID_ENV_VAR,
AWS_SECRET_ACCESS_KEY_ENV_VAR,
],
BedrockAuthSource::ConfiguredAwsProfile,
),
(
&provider,
Some(managed_auth.as_ref()),
&[AWS_BEARER_TOKEN_BEDROCK_ENV_VAR],
BedrockAuthSource::ManagedBearerToken,
),
(
&provider,
None,
&[
AWS_BEARER_TOKEN_BEDROCK_ENV_VAR,
AWS_ACCESS_KEY_ID_ENV_VAR,
AWS_SECRET_ACCESS_KEY_ENV_VAR,
],
BedrockAuthSource::EnvBearerToken,
),
(
&provider,
None,
&[AWS_ACCESS_KEY_ID_ENV_VAR, AWS_SECRET_ACCESS_KEY_ENV_VAR],
BedrockAuthSource::EnvAwsCredentials,
),
(&[AWS_ACCESS_KEY_ID_ENV_VAR], BedrockAuthSource::AwsSdk),
(
&provider,
None,
&[AWS_ACCESS_KEY_ID_ENV_VAR],
BedrockAuthSource::AwsSdk,
),
(
&provider,
None,
&["AWS_PROFILE", AWS_REGION_ENV_VAR],
BedrockAuthSource::AwsSdk,
),
];
for (variables, expected) in cases {
let actual = auth_source(&provider, /*auth_manager*/ None, |name| {
for (provider, auth_manager, variables, expected) in cases {
let actual = auth_source(provider, *auth_manager, |name| {
variables
.contains(&name)
.then(|| "configured".to_string())
@@ -5,6 +5,7 @@ use codex_protocol::error::CodexErr;
use codex_protocol::error::Result;
use super::BedrockEndpoint;
use super::auth::BedrockAuthSource;
use super::auth::resolve_region;
const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle";
@@ -55,10 +56,11 @@ pub(super) fn base_url(region: &str) -> Result<String> {
}
pub(super) async fn bedrock_mantle_runtime_base_url(
source: BedrockAuthSource,
managed_auth: Option<&BedrockApiKeyAuth>,
aws: &ModelProviderAwsAuthInfo,
) -> Result<String> {
let region = resolve_region(managed_auth, aws, BedrockEndpoint::Mantle).await?;
let region = resolve_region(source, managed_auth, aws, BedrockEndpoint::Mantle).await?;
base_url(&region)
}
+108 -29
View File
@@ -82,9 +82,10 @@ impl AmazonBedrockModelProvider {
region: None,
auth_refresh: None,
});
let uses_aws_sdk_auth =
auth::auth_source(&provider_info, auth_manager.as_deref(), std::env::var)
== auth::BedrockAuthSource::AwsSdk;
let uses_aws_sdk_auth = matches!(
auth::auth_source(&provider_info, auth_manager.as_deref(), std::env::var),
auth::BedrockAuthSource::ConfiguredAwsProfile | auth::BedrockAuthSource::AwsSdk
);
let auth_recovery = if uses_aws_sdk_auth && aws.auth_refresh.is_some() {
process_shared_state().aws_auth_recovery(&aws)
} else {
@@ -100,7 +101,14 @@ impl AmazonBedrockModelProvider {
}
}
fn auth_source(&self) -> auth::BedrockAuthSource {
auth::auth_source(&self.info, self.auth_manager.as_deref(), std::env::var)
}
fn managed_auth(&self) -> Option<BedrockApiKeyAuth> {
if self.auth_source() != auth::BedrockAuthSource::ManagedBearerToken {
return None;
}
self.auth_manager
.as_ref()
.and_then(|auth_manager| auth_manager.auth_cached())
@@ -117,18 +125,25 @@ impl AmazonBedrockModelProvider {
fn uses_aws_auth_recovery(&self) -> bool {
self.auth_recovery.is_some()
&& auth::auth_source(&self.info, self.auth_manager.as_deref(), std::env::var)
== auth::BedrockAuthSource::AwsSdk
&& matches!(
self.auth_source(),
auth::BedrockAuthSource::ConfiguredAwsProfile | auth::BedrockAuthSource::AwsSdk
)
}
async fn auth(&self) -> Option<CodexAuth> {
if self.info.has_command_auth() {
match self.auth_manager.as_ref() {
match self.auth_source() {
auth::BedrockAuthSource::CommandBearerToken => match self.auth_manager.as_ref() {
Some(auth_manager) => auth_manager.auth().await,
None => None,
},
auth::BedrockAuthSource::ManagedBearerToken => {
self.managed_auth().map(CodexAuth::BedrockApiKey)
}
} else {
self.managed_auth().map(CodexAuth::BedrockApiKey)
auth::BedrockAuthSource::ConfiguredAwsProfile
| auth::BedrockAuthSource::EnvBearerToken
| auth::BedrockAuthSource::EnvAwsCredentials
| auth::BedrockAuthSource::AwsSdk => None,
}
}
@@ -142,25 +157,29 @@ impl AmazonBedrockModelProvider {
if let Some(base_url) = self.info.base_url.clone() {
return Ok(Some(base_url));
}
let auth_source = self.auth_source();
let managed_auth = self.managed_auth();
let base_url = match self.endpoint {
BedrockEndpoint::Mantle => {
bedrock_mantle_runtime_base_url(managed_auth.as_ref(), &self.aws).await?
bedrock_mantle_runtime_base_url(auth_source, managed_auth.as_ref(), &self.aws)
.await?
}
BedrockEndpoint::Runtime => {
bedrock_runtime_base_url(managed_auth.as_ref(), &self.aws).await?
bedrock_runtime_base_url(auth_source, managed_auth.as_ref(), &self.aws).await?
}
};
Ok(Some(base_url))
}
async fn api_auth(&self) -> Result<SharedAuthProvider> {
if self.info.has_command_auth() {
let source = self.auth_source();
if source == auth::BedrockAuthSource::CommandBearerToken {
let auth = self.auth().await;
return resolve_configured_provider_auth(auth.as_ref(), &self.info);
}
let managed_auth = self.managed_auth();
resolve_bedrock_provider_auth(managed_auth.as_ref(), &self.aws, self.endpoint).await
resolve_bedrock_provider_auth(source, managed_auth.as_ref(), &self.aws, self.endpoint).await
}
fn default_model_catalog(&self) -> ModelsResponse {
@@ -208,10 +227,13 @@ impl ModelProvider for AmazonBedrockModelProvider {
}
fn auth_manager(&self) -> Option<Arc<AuthManager>> {
if self.info.has_command_auth() || self.managed_auth().is_some() {
self.auth_manager.clone()
} else {
None
match self.auth_source() {
auth::BedrockAuthSource::CommandBearerToken
| auth::BedrockAuthSource::ManagedBearerToken => self.auth_manager.clone(),
auth::BedrockAuthSource::ConfiguredAwsProfile
| auth::BedrockAuthSource::EnvBearerToken
| auth::BedrockAuthSource::EnvAwsCredentials
| auth::BedrockAuthSource::AwsSdk => None,
}
}
@@ -252,7 +274,8 @@ impl ModelProvider for AmazonBedrockModelProvider {
fn account_state(&self) -> ProviderAccountResult {
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock {
uses_codex_managed_credentials: self.managed_auth().is_some(),
uses_codex_managed_credentials: self.auth_source()
== auth::BedrockAuthSource::ManagedBearerToken,
}),
requires_openai_auth: false,
})
@@ -346,7 +369,7 @@ mod tests {
}
#[tokio::test]
async fn command_auth_uses_configured_base_url_without_resolving_aws() {
async fn command_auth_resolves_configured_and_regional_base_urls() {
let mut provider_info = command_auth_provider(Some("https://proxy.example.com/v1"));
provider_info.aws = Some(ModelProviderAwsAuthInfo {
profile: Some("aws-profile-that-should-not-be-loaded".to_string()),
@@ -381,26 +404,44 @@ mod tests {
requires_openai_auth: false,
})
);
let mut regional_provider_info = command_auth_provider(/*base_url*/ None);
regional_provider_info.aws = Some(ModelProviderAwsAuthInfo {
profile: None,
region: Some("us-west-2".to_string()),
auth_refresh: None,
});
let regional_provider =
AmazonBedrockModelProvider::new(regional_provider_info, /*auth_manager*/ None);
assert_eq!(
regional_provider
.runtime_base_url()
.await
.expect("configured AWS region should resolve for command auth"),
Some("https://bedrock-mantle.us-west-2.api.aws/openai/v1".to_string())
);
}
#[tokio::test]
async fn managed_auth_takes_precedence_over_aws_auth() {
async fn configured_profile_takes_precedence_over_managed_auth() {
let managed_auth = BedrockApiKeyAuth {
api_key: "managed-bedrock-api-key".to_string(),
region: "us-east-1".to_string(),
};
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::BedrockApiKey(managed_auth.clone()));
let aws = ModelProviderAwsAuthInfo {
profile: None,
region: Some("us-west-2".to_string()),
auth_refresh: Some(AwsAuthRefreshConfig {
command: "aws".to_string(),
args: vec!["login".into()],
timeout_ms: NonZeroU64::new(1_000).expect("timeout should be non-zero"),
}),
};
let provider = AmazonBedrockModelProvider::new(
ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo {
profile: Some("aws-profile-that-should-not-be-loaded".to_string()),
region: Some("us-west-2".to_string()),
auth_refresh: Some(AwsAuthRefreshConfig {
command: "aws".to_string(),
args: vec!["login".into()],
timeout_ms: NonZeroU64::new(1_000).expect("timeout should be non-zero"),
}),
})),
ModelProviderInfo::create_amazon_bedrock_provider(Some(aws.clone())),
Some(auth_manager.clone()),
);
@@ -439,6 +480,44 @@ mod tests {
.get(http::header::AUTHORIZATION),
Some(&HeaderValue::from_static("Bearer managed-bedrock-api-key"))
);
assert!(!provider.uses_aws_auth_recovery());
let configured_profile_provider = AmazonBedrockModelProvider::new(
ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo {
profile: Some("configured-aws-profile".to_string()),
..aws
})),
Some(auth_manager),
);
assert!(configured_profile_provider.auth_manager().is_none());
assert_eq!(configured_profile_provider.auth().await, None);
assert_eq!(
configured_profile_provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock {
uses_codex_managed_credentials: false,
}),
requires_openai_auth: false,
})
);
assert_eq!(
configured_profile_provider
.runtime_base_url()
.await
.expect("configured AWS profile region should resolve"),
Some("https://bedrock-mantle.us-west-2.api.aws/openai/v1".to_string())
);
assert!(
configured_profile_provider
.api_auth()
.await
.expect("configured AWS profile auth should resolve")
.to_auth_headers()
.get(http::header::AUTHORIZATION)
.is_none()
);
assert!(configured_profile_provider.uses_aws_auth_recovery());
}
#[tokio::test]
@@ -4,6 +4,7 @@ use codex_model_provider_info::ModelProviderAwsAuthInfo;
use codex_protocol::error::Result;
use super::BedrockEndpoint;
use super::auth::BedrockAuthSource;
use super::auth::resolve_region;
use super::mantle::region_from_config;
@@ -22,10 +23,11 @@ pub(super) fn base_url(region: &str) -> String {
}
pub(super) async fn bedrock_runtime_base_url(
source: BedrockAuthSource,
managed_auth: Option<&BedrockApiKeyAuth>,
aws: &ModelProviderAwsAuthInfo,
) -> Result<String> {
let region = resolve_region(managed_auth, aws, BedrockEndpoint::Runtime).await?;
let region = resolve_region(source, managed_auth, aws, BedrockEndpoint::Runtime).await?;
Ok(base_url(&region))
}