Codex Handbook
model-provider/src/amazon_bedrock/mod.rs 296 lines
mod auth;mod catalog;mod mantle;use std::path::PathBuf;use std::sync::Arc;use codex_api::Provider;use codex_api::SharedAuthProvider;use codex_login::AuthManager;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::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::Result;use codex_protocol::openai_models::ModelsResponse;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;pub(crate) use catalog::static_model_catalog;use catalog::with_default_only_service_tier;use mantle::runtime_base_url;/// Runtime provider for Amazon Bedrock's OpenAI-compatible Mantle endpoint.#[derive(Clone, Debug)]pub(crate) struct AmazonBedrockModelProvider {    pub(crate) info: ModelProviderInfo,    pub(crate) aws: ModelProviderAwsAuthInfo,    auth_manager: Option<Arc<AuthManager>>,}impl AmazonBedrockModelProvider {    pub(crate) fn new(        provider_info: ModelProviderInfo,        auth_manager: Option<Arc<AuthManager>>,    ) -> Self {        let aws = provider_info            .aws            .clone()            .unwrap_or(ModelProviderAwsAuthInfo {                profile: None,                region: None,            });        Self {            info: provider_info,            aws,            auth_manager,        }    }    fn managed_auth(&self) -> Option<BedrockApiKeyAuth> {        self.auth_manager            .as_ref()            .and_then(|auth_manager| auth_manager.auth_cached())            .and_then(|auth| match auth {                CodexAuth::BedrockApiKey(auth) => Some(auth),                CodexAuth::ApiKey(_)                | CodexAuth::Chatgpt(_)                | CodexAuth::ChatgptAuthTokens(_)                | CodexAuth::AgentIdentity(_)                | CodexAuth::PersonalAccessToken(_) => None,            })    }    async fn auth(&self) -> Option<CodexAuth> {        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.to_api_provider(/*auth_mode*/ None)    }    async fn runtime_base_url(&self) -> Result<Option<String>> {        let managed_auth = self.managed_auth();        Ok(Some(            runtime_base_url(managed_auth.as_ref(), &self.aws).await?,        ))    }    async fn api_auth(&self) -> Result<SharedAuthProvider> {        let managed_auth = self.managed_auth();        resolve_provider_auth(managed_auth.as_ref(), &self.aws).await    }}impl ModelProvider for AmazonBedrockModelProvider {    fn info(&self) -> &ModelProviderInfo {        &self.info    }    fn capabilities(&self) -> ProviderCapabilities {        ProviderCapabilities {            namespace_tools: true,            image_generation: false,            web_search: false,        }    }    fn approval_review_preferred_model(&self) -> &'static str {        AMAZON_BEDROCK_GPT_5_4_MODEL_ID    }    fn memory_extraction_preferred_model(&self) -> &'static str {        AMAZON_BEDROCK_GPT_5_4_MODEL_ID    }    fn memory_consolidation_preferred_model(&self) -> &'static str {        AMAZON_BEDROCK_GPT_5_4_MODEL_ID    }    fn auth_manager(&self) -> Option<Arc<AuthManager>> {        self.managed_auth()            .and_then(|_| self.auth_manager.as_ref().cloned())    }    fn auth(&self) -> ModelProviderFuture<'_, Option<CodexAuth>> {        Box::pin(AmazonBedrockModelProvider::auth(self))    }    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 }),            requires_openai_auth: false,        })    }    fn api_provider(&self) -> ModelProviderFuture<'_, Result<Provider>> {        Box::pin(AmazonBedrockModelProvider::api_provider(self))    }    fn runtime_base_url(&self) -> ModelProviderFuture<'_, Result<Option<String>>> {        Box::pin(AmazonBedrockModelProvider::runtime_base_url(self))    }    fn api_auth(&self) -> ModelProviderFuture<'_, Result<SharedAuthProvider>> {        Box::pin(AmazonBedrockModelProvider::api_auth(self))    }    fn models_manager(        &self,        _codex_home: PathBuf,        config_model_catalog: Option<ModelsResponse>,    ) -> SharedModelsManager {        Arc::new(StaticModelsManager::new(            /*auth_manager*/ None,            config_model_catalog.map_or_else(static_model_catalog, with_default_only_service_tier),        ))    }}#[cfg(test)]mod tests {    use http::HeaderValue;    use pretty_assertions::assert_eq;    use super::*;    #[test]    fn api_provider_for_bedrock_bearer_token_uses_configured_region_endpoint() {        let region = "eu-central-1";        let mut api_provider_info =            ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None);        api_provider_info.base_url = Some(mantle::base_url(region).expect("supported region"));        let api_provider = api_provider_info            .to_api_provider(/*auth_mode*/ None)            .expect("api provider should build");        assert_eq!(            api_provider.base_url,            "https://bedrock-mantle.eu-central-1.api.aws/openai/v1"        );    }    #[tokio::test]    async fn managed_auth_takes_precedence_over_aws_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 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()),            })),            Some(auth_manager.clone()),        );        assert!(Arc::ptr_eq(            &provider                .auth_manager()                .expect("managed Bedrock auth manager should be exposed"),            &auth_manager,        ));        assert_eq!(            provider.auth().await,            Some(CodexAuth::BedrockApiKey(managed_auth))        );        assert_eq!(            provider.account_state(),            Ok(ProviderAccountState {                account: Some(ProviderAccount::AmazonBedrock {                    credential_source: AmazonBedrockCredentialSource::CodexManaged,                }),                requires_openai_auth: false,            })        );        assert_eq!(            provider                .runtime_base_url()                .await                .expect("managed Bedrock region should resolve"),            Some("https://bedrock-mantle.us-east-1.api.aws/openai/v1".to_string())        );        assert_eq!(            provider                .api_auth()                .await                .expect("managed Bedrock auth should resolve")                .to_auth_headers()                .get(http::header::AUTHORIZATION),            Some(&HeaderValue::from_static("Bearer managed-bedrock-api-key"))        );    }    #[tokio::test]    async fn openai_auth_is_not_exposed_to_bedrock() {        let provider = AmazonBedrockModelProvider::new(            ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),            Some(AuthManager::from_auth_for_testing(CodexAuth::from_api_key(                "openai-api-key",            ))),        );        assert!(provider.auth_manager().is_none());        assert_eq!(provider.auth().await, None);        assert_eq!(            provider.account_state(),            Ok(ProviderAccountState {                account: Some(ProviderAccount::AmazonBedrock {                    credential_source: AmazonBedrockCredentialSource::AwsManaged,                }),                requires_openai_auth: false,            })        );    }    #[test]    fn capabilities_disable_unsupported_hosted_tools() {        let provider = AmazonBedrockModelProvider::new(            ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),            /*auth_manager*/ None,        );        assert_eq!(            provider.capabilities(),            ProviderCapabilities {                namespace_tools: true,                image_generation: false,                web_search: false,            }        );    }    #[test]    fn approval_review_preferred_model_uses_bedrock_gpt_5_4() {        let provider = AmazonBedrockModelProvider::new(            ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),            /*auth_manager*/ None,        );        assert_eq!(            provider.approval_review_preferred_model(),            AMAZON_BEDROCK_GPT_5_4_MODEL_ID        );    }}