Support custom transports for Amazon Bedrock (#33695)

## What changed

- Allow the built-in `amazon-bedrock` provider to override `base_url`, `auth`,
  and `http_headers` in addition to its AWS profile and region.
- Use command-based bearer authentication and configured endpoints without
  applying AWS request signing, while retaining regional endpoint resolution
  for the default Bedrock configuration.
- Replace the Bedrock account `credentialSource` enum with the
  `usesCodexManagedCredentials` boolean so command-authenticated and other
  externally managed configurations are reported consistently.

## Testing

- Cover configuration merging and validation, command-authenticated proxy
  requests with custom headers, and account reporting for managed and external
  credentials.

GitOrigin-RevId: d1acbe602060470583b5e12f8d304bee5be46f4c
This commit is contained in:
Celia Chen authored and copyberry committed 2026-07-16 21:41:00 +00:00
1 parent 18110b810f
commit 315195492c
20 files changed
+362 -148

No files matched your search

@@ -80,7 +80,7 @@ fn non_empty_env_var_from(
.filter(|value| !value.is_empty())
}
fn bearer_token_region(
pub(super) fn bearer_token_region(
aws: &ModelProviderAwsAuthInfo,
env_var: impl Fn(&'static str) -> std::result::Result<String, std::env::VarError> + Copy,
) -> Result<String> {
@@ -54,7 +54,7 @@ pub(super) fn base_url(region: &str) -> Result<String> {
}
}
pub(super) async fn runtime_base_url(
pub(super) async fn bedrock_mantle_runtime_base_url(
managed_auth: Option<&BedrockApiKeyAuth>,
aws: &ModelProviderAwsAuthInfo,
) -> Result<String> {
@@ -17,22 +17,23 @@ use codex_model_provider_info::ModelProviderAwsAuthInfo;
use codex_model_provider_info::ModelProviderInfo;
use codex_models_manager::manager::SharedModelsManager;
use codex_models_manager::manager::StaticModelsManager;
use codex_protocol::account::AmazonBedrockCredentialSource;
use codex_protocol::account::ProviderAccount;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result;
use codex_protocol::openai_models::ModelsResponse;
use crate::auth::auth_manager_for_provider;
use crate::auth::resolve_provider_auth as resolve_configured_provider_auth;
use crate::provider::ModelProvider;
use crate::provider::ModelProviderFuture;
use crate::provider::ProviderAccountResult;
use crate::provider::ProviderAccountState;
use crate::provider::ProviderCapabilities;
use auth::resolve_provider_auth;
use auth::resolve_provider_auth as resolve_bedrock_provider_auth;
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;
use mantle::runtime_base_url;
/// Runtime provider for Amazon Bedrock's OpenAI-compatible Mantle endpoint.
#[derive(Clone, Debug)]
@@ -47,6 +48,7 @@ impl AmazonBedrockModelProvider {
provider_info: ModelProviderInfo,
auth_manager: Option<Arc<AuthManager>>,
) -> Self {
let auth_manager = auth_manager_for_provider(auth_manager, &provider_info);
let aws = provider_info
.aws
.clone()
@@ -77,27 +79,39 @@ impl AmazonBedrockModelProvider {
}
async fn auth(&self) -> Option<CodexAuth> {
self.managed_auth().map(CodexAuth::BedrockApiKey)
if self.info.has_command_auth() {
match self.auth_manager.as_ref() {
Some(auth_manager) => auth_manager.auth().await,
None => None,
}
} else {
self.managed_auth().map(CodexAuth::BedrockApiKey)
}
}
async fn api_provider(&self) -> Result<Provider> {
let managed_auth = self.managed_auth();
let mut api_provider_info = self.info.clone();
api_provider_info.base_url =
Some(runtime_base_url(managed_auth.as_ref(), &self.aws).await?);
api_provider_info.base_url = self.runtime_base_url().await?;
api_provider_info.to_api_provider(/*auth_mode*/ None)
}
async fn runtime_base_url(&self) -> Result<Option<String>> {
if let Some(base_url) = self.info.base_url.clone() {
return Ok(Some(base_url));
}
let managed_auth = self.managed_auth();
Ok(Some(
runtime_base_url(managed_auth.as_ref(), &self.aws).await?,
bedrock_mantle_runtime_base_url(managed_auth.as_ref(), &self.aws).await?,
))
}
async fn api_auth(&self) -> Result<SharedAuthProvider> {
if self.info.has_command_auth() {
let auth = self.auth().await;
return resolve_configured_provider_auth(auth.as_ref(), &self.info);
}
let managed_auth = self.managed_auth();
resolve_provider_auth(managed_auth.as_ref(), &self.aws).await
resolve_bedrock_provider_auth(managed_auth.as_ref(), &self.aws).await
}
}
@@ -127,8 +141,11 @@ impl ModelProvider for AmazonBedrockModelProvider {
}
fn auth_manager(&self) -> Option<Arc<AuthManager>> {
self.managed_auth()
.and_then(|_| self.auth_manager.as_ref().cloned())
if self.info.has_command_auth() || self.managed_auth().is_some() {
self.auth_manager.clone()
} else {
None
}
}
fn auth(&self) -> ModelProviderFuture<'_, Option<CodexAuth>> {
@@ -136,13 +153,10 @@ impl ModelProvider for AmazonBedrockModelProvider {
}
fn account_state(&self) -> ProviderAccountResult {
let credential_source = if self.managed_auth().is_some() {
AmazonBedrockCredentialSource::CodexManaged
} else {
AmazonBedrockCredentialSource::AwsManaged
};
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock { credential_source }),
account: Some(ProviderAccount::AmazonBedrock {
uses_codex_managed_credentials: self.managed_auth().is_some(),
}),
requires_openai_auth: false,
})
}
@@ -191,11 +205,30 @@ mod error_tests;
#[cfg(test)]
mod tests {
use std::num::NonZeroU64;
use codex_protocol::config_types::ModelProviderAuthInfo;
use http::HeaderValue;
use pretty_assertions::assert_eq;
use super::*;
fn command_auth_provider(base_url: Option<&str>) -> ModelProviderInfo {
let mut provider = ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None);
provider.base_url = base_url.map(str::to_string);
provider.auth = Some(ModelProviderAuthInfo {
command: "token-fetcher".to_string(),
args: vec!["fetch".to_string()],
timeout_ms: NonZeroU64::new(5_000).expect("timeout should be non-zero"),
refresh_interval_ms: 300_000,
cwd: std::env::current_dir()
.expect("current directory should be available")
.try_into()
.expect("current directory should be absolute"),
});
provider
}
#[test]
fn api_provider_for_bedrock_bearer_token_uses_configured_region_endpoint() {
let region = "eu-central-1";
@@ -212,6 +245,39 @@ mod tests {
);
}
#[tokio::test]
async fn command_auth_uses_configured_base_url_without_resolving_aws() {
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()),
region: Some("us-west-2".to_string()),
});
let provider = AmazonBedrockModelProvider::new(provider_info, /*auth_manager*/ None);
assert_eq!(
provider
.runtime_base_url()
.await
.expect("configured base URL should resolve"),
Some("https://proxy.example.com/v1".to_string())
);
assert!(
provider
.auth_manager()
.expect("command auth manager should be exposed")
.has_external_auth()
);
assert_eq!(
provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock {
uses_codex_managed_credentials: false,
}),
requires_openai_auth: false,
})
);
}
#[tokio::test]
async fn managed_auth_takes_precedence_over_aws_auth() {
let managed_auth = BedrockApiKeyAuth {
@@ -242,7 +308,7 @@ mod tests {
provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::CodexManaged,
uses_codex_managed_credentials: true,
}),
requires_openai_auth: false,
})
@@ -280,7 +346,7 @@ mod tests {
provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::AwsManaged,
uses_codex_managed_credentials: false,
}),
requires_openai_auth: false,
})