From dc75dc1ca2a00c084576ea302da9bcc068e6a732 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 20 Apr 2026 14:59:46 -0700 Subject: [PATCH 01/11] changes --- codex-rs/config/src/config_toml.rs | 73 +++++++++++++++++++ codex-rs/core/config.schema.json | 10 ++- codex-rs/core/src/config/config_tests.rs | 17 +---- codex-rs/model-provider-info/src/lib.rs | 37 +++++++++- .../src/model_provider_info_tests.rs | 52 ++++++++++++- 5 files changed, 168 insertions(+), 21 deletions(-) diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 0c8e14c16d78..5986078bc5d7 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -806,6 +806,8 @@ pub fn validate_model_providers( validate_reserved_model_provider_ids(model_providers)?; for (key, provider) in model_providers { if key == AMAZON_BEDROCK_PROVIDER_ID { + validate_amazon_bedrock_provider_override(provider) + .map_err(|message| format!("model_providers.{key}: {message}"))?; continue; } if provider.aws.is_some() { @@ -825,6 +827,77 @@ pub fn validate_model_providers( Ok(()) } +fn validate_amazon_bedrock_provider_override(provider: &ModelProviderInfo) -> Result<(), String> { + let mut conflicts = Vec::new(); + if !provider.name.trim().is_empty() { + conflicts.push("name"); + } + if provider.base_url.is_some() { + conflicts.push("base_url"); + } + if provider.env_key.is_some() { + conflicts.push("env_key"); + } + if provider.env_key_instructions.is_some() { + conflicts.push("env_key_instructions"); + } + if provider.experimental_bearer_token.is_some() { + conflicts.push("experimental_bearer_token"); + } + if provider.auth.is_some() { + conflicts.push("auth"); + } + if provider.query_params.is_some() { + conflicts.push("query_params"); + } + if provider.http_headers.is_some() { + conflicts.push("http_headers"); + } + if provider.env_http_headers.is_some() { + conflicts.push("env_http_headers"); + } + if provider.request_max_retries.is_some() { + conflicts.push("request_max_retries"); + } + if provider.stream_max_retries.is_some() { + conflicts.push("stream_max_retries"); + } + if provider.stream_idle_timeout_ms.is_some() { + conflicts.push("stream_idle_timeout_ms"); + } + if provider.websocket_connect_timeout_ms.is_some() { + conflicts.push("websocket_connect_timeout_ms"); + } + if provider.requires_openai_auth { + conflicts.push("requires_openai_auth"); + } + if provider.supports_websockets { + conflicts.push("supports_websockets"); + } + if provider.wire_api != Default::default() { + conflicts.push("wire_api"); + } + + let Some(aws) = provider.aws.as_ref() else { + return Ok(()); + }; + if aws.region.is_some() { + conflicts.push("aws.region"); + } + if aws.service.is_some() { + conflicts.push("aws.service"); + } + + if conflicts.is_empty() { + Ok(()) + } else { + Err(format!( + "`{AMAZON_BEDROCK_PROVIDER_ID}` is a built-in provider; only `aws.profile` can be configured, but found {}", + conflicts.join(", ") + )) + } +} + fn deserialize_model_providers<'de, D>( deserializer: D, ) -> Result, D::Error> diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index b0faea2b56d5..fad413680567 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1034,6 +1034,14 @@ "profile": { "description": "AWS profile name to use. When unset, the AWS SDK default chain decides.", "type": "string" + }, + "region": { + "description": "AWS region to use. When unset, the AWS SDK default chain decides.", + "type": "string" + }, + "service": { + "description": "AWS SigV4 service name. Defaults to `bedrock` when unset.", + "type": "string" } }, "type": "object" @@ -2978,4 +2986,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index e7c7aa31dd7d..03c99941aa0e 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -439,9 +439,9 @@ profile = "codex-bedrock" ); } -#[tokio::test] -async fn load_config_rejects_unsupported_amazon_bedrock_overrides() { - let cfg = toml::from_str::( +#[test] +fn rejects_unsupported_amazon_bedrock_overrides() { + let err = toml::from_str::( r#" model_provider = "amazon-bedrock" @@ -455,19 +455,10 @@ supports_websockets = true profile = "codex-bedrock" "#, ) - .expect("Amazon Bedrock unsupported overrides should deserialize"); - - let err = Config::load_from_base_config_with_overrides( - cfg, - ConfigOverrides::default(), - tempdir().expect("tempdir").abs(), - ) - .await .unwrap_err(); - assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); assert!(err.to_string().contains( - "model_providers.amazon-bedrock only supports changing `aws.profile`; other non-default provider fields are not supported" + "model_providers.amazon-bedrock: `amazon-bedrock` is a built-in provider; only `aws.profile` can be configured" )); } diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs index 97b1e166d6be..c13816af01a8 100644 --- a/codex-rs/model-provider-info/src/lib.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -136,11 +136,28 @@ pub struct ModelProviderInfo { pub struct ModelProviderAwsAuthInfo { /// AWS profile name to use. When unset, the AWS SDK default chain decides. pub profile: Option, + /// AWS region to use. When unset, the AWS SDK default chain decides. + pub region: Option, + /// AWS SigV4 service name. Defaults to `bedrock` when unset. + pub service: Option, +} + +impl ModelProviderAwsAuthInfo { + pub fn service_name(&self) -> &str { + self.service.as_deref().unwrap_or("bedrock") + } } impl ModelProviderInfo { pub fn validate(&self) -> std::result::Result<(), String> { - if self.aws.is_some() { + if let Some(aws) = self.aws.as_ref() { + if aws + .service + .as_ref() + .is_some_and(|service| service.trim().is_empty()) + { + return Err("provider aws.service must not be empty".to_string()); + } if self.supports_websockets { // TODO(celia-oai): Support AWS SigV4 signing for WebSocket // upgrade requests before allowing AWS-authenticated providers @@ -352,7 +369,11 @@ impl ModelProviderInfo { env_key_instructions: None, experimental_bearer_token: None, auth: None, - aws: Some(aws.unwrap_or(ModelProviderAwsAuthInfo { profile: None })), + aws: Some(aws.unwrap_or(ModelProviderAwsAuthInfo { + profile: None, + region: None, + service: None, + })), wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -437,7 +458,17 @@ pub fn merge_configured_model_providers( )); } - if let Some(profile) = aws_override.and_then(|aws| aws.profile) + let Some(aws_override) = aws_override else { + continue; + }; + if aws_override.region.is_some() || aws_override.service.is_some() { + return Err(format!( + "model_providers.{AMAZON_BEDROCK_PROVIDER_ID} only supports changing \ +`aws.profile`; other non-default provider fields are not supported" + )); + } + + if let Some(profile) = aws_override.profile && let Some(built_in) = model_providers.get_mut(AMAZON_BEDROCK_PROVIDER_ID) && let Some(aws) = built_in.aws.as_mut() { diff --git a/codex-rs/model-provider-info/src/model_provider_info_tests.rs b/codex-rs/model-provider-info/src/model_provider_info_tests.rs index 20440de32cea..117330a2c4b6 100644 --- a/codex-rs/model-provider-info/src/model_provider_info_tests.rs +++ b/codex-rs/model-provider-info/src/model_provider_info_tests.rs @@ -225,6 +225,8 @@ base_url = "https://bedrock.example.com/v1" [aws] profile = "codex-bedrock" +region = "us-east-1" +service = "bedrock" "#; let provider: ModelProviderInfo = toml::from_str(provider_toml).unwrap(); @@ -233,6 +235,8 @@ profile = "codex-bedrock" provider.aws, Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: Some("us-east-1".to_string()), + service: Some("bedrock".to_string()), }) ); } @@ -248,7 +252,11 @@ fn test_create_amazon_bedrock_provider() { env_key_instructions: None, experimental_bearer_token: None, auth: None, - aws: Some(ModelProviderAwsAuthInfo { profile: None }), + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + service: None, + }), wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -304,6 +312,8 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_profile_override ModelProviderInfo { aws: Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: None, + service: None, }), ..ModelProviderInfo::default() }, @@ -315,6 +325,8 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_profile_override .expect("Amazon Bedrock provider should be built in") .aws = Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: None, + service: None, }); assert_eq!( @@ -334,6 +346,8 @@ fn test_merge_configured_model_providers_rejects_amazon_bedrock_non_default_fiel name: "Custom Bedrock".to_string(), aws: Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: None, + service: None, }), ..ModelProviderInfo::default() }, @@ -356,7 +370,11 @@ fn test_merge_configured_model_providers_allows_amazon_bedrock_default_fields() let configured_model_providers = std::collections::HashMap::from([( AMAZON_BEDROCK_PROVIDER_ID.to_string(), ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { profile: None }), + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + service: None, + }), wire_api: WireApi::Responses, ..ModelProviderInfo::default() }, @@ -374,7 +392,11 @@ fn test_merge_configured_model_providers_allows_amazon_bedrock_default_fields() #[test] fn test_validate_provider_aws_rejects_conflicting_auth() { let provider = ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { profile: None }), + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + service: None, + }), env_key: Some("AWS_BEARER_TOKEN_BEDROCK".to_string()), supports_websockets: false, ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) @@ -389,7 +411,11 @@ fn test_validate_provider_aws_rejects_conflicting_auth() { #[test] fn test_validate_provider_aws_rejects_websockets() { let provider = ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { profile: None }), + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + service: None, + }), requires_openai_auth: false, supports_websockets: true, ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) @@ -401,6 +427,24 @@ fn test_validate_provider_aws_rejects_websockets() { ); } +#[test] +fn test_validate_provider_aws_rejects_empty_service() { + let provider = ModelProviderInfo { + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + service: Some(" ".to_string()), + }), + requires_openai_auth: false, + ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) + }; + + assert_eq!( + provider.validate(), + Err("provider aws.service must not be empty".to_string()) + ); +} + #[test] fn test_deserialize_provider_auth_config_allows_zero_refresh_interval() { let base_dir = tempdir().unwrap(); From a37ddcbfa1afde353e771819e3ef2fca97e586b9 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 20 Apr 2026 15:27:55 -0700 Subject: [PATCH 02/11] changes --- codex-rs/config/src/config_toml.rs | 73 ------------------- codex-rs/core/config.schema.json | 8 -- codex-rs/core/src/config/config_tests.rs | 36 +++++++-- codex-rs/model-provider-info/src/lib.rs | 45 +----------- .../src/model_provider_info_tests.rs | 67 ++++------------- 5 files changed, 46 insertions(+), 183 deletions(-) diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 5986078bc5d7..0c8e14c16d78 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -806,8 +806,6 @@ pub fn validate_model_providers( validate_reserved_model_provider_ids(model_providers)?; for (key, provider) in model_providers { if key == AMAZON_BEDROCK_PROVIDER_ID { - validate_amazon_bedrock_provider_override(provider) - .map_err(|message| format!("model_providers.{key}: {message}"))?; continue; } if provider.aws.is_some() { @@ -827,77 +825,6 @@ pub fn validate_model_providers( Ok(()) } -fn validate_amazon_bedrock_provider_override(provider: &ModelProviderInfo) -> Result<(), String> { - let mut conflicts = Vec::new(); - if !provider.name.trim().is_empty() { - conflicts.push("name"); - } - if provider.base_url.is_some() { - conflicts.push("base_url"); - } - if provider.env_key.is_some() { - conflicts.push("env_key"); - } - if provider.env_key_instructions.is_some() { - conflicts.push("env_key_instructions"); - } - if provider.experimental_bearer_token.is_some() { - conflicts.push("experimental_bearer_token"); - } - if provider.auth.is_some() { - conflicts.push("auth"); - } - if provider.query_params.is_some() { - conflicts.push("query_params"); - } - if provider.http_headers.is_some() { - conflicts.push("http_headers"); - } - if provider.env_http_headers.is_some() { - conflicts.push("env_http_headers"); - } - if provider.request_max_retries.is_some() { - conflicts.push("request_max_retries"); - } - if provider.stream_max_retries.is_some() { - conflicts.push("stream_max_retries"); - } - if provider.stream_idle_timeout_ms.is_some() { - conflicts.push("stream_idle_timeout_ms"); - } - if provider.websocket_connect_timeout_ms.is_some() { - conflicts.push("websocket_connect_timeout_ms"); - } - if provider.requires_openai_auth { - conflicts.push("requires_openai_auth"); - } - if provider.supports_websockets { - conflicts.push("supports_websockets"); - } - if provider.wire_api != Default::default() { - conflicts.push("wire_api"); - } - - let Some(aws) = provider.aws.as_ref() else { - return Ok(()); - }; - if aws.region.is_some() { - conflicts.push("aws.region"); - } - if aws.service.is_some() { - conflicts.push("aws.service"); - } - - if conflicts.is_empty() { - Ok(()) - } else { - Err(format!( - "`{AMAZON_BEDROCK_PROVIDER_ID}` is a built-in provider; only `aws.profile` can be configured, but found {}", - conflicts.join(", ") - )) - } -} - fn deserialize_model_providers<'de, D>( deserializer: D, ) -> Result, D::Error> diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index fad413680567..7c841ea99bae 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1034,14 +1034,6 @@ "profile": { "description": "AWS profile name to use. When unset, the AWS SDK default chain decides.", "type": "string" - }, - "region": { - "description": "AWS region to use. When unset, the AWS SDK default chain decides.", - "type": "string" - }, - "service": { - "description": "AWS SigV4 service name. Defaults to `bedrock` when unset.", - "type": "string" } }, "type": "object" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 03c99941aa0e..6f4f31c6051b 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -49,6 +49,7 @@ use codex_config::types::TuiNotificationSettings; use codex_exec_server::LOCAL_FS; use codex_features::Feature; use codex_features::FeaturesToml; +use codex_model_provider_info::AMAZON_BEDROCK_DEFAULT_BASE_URL; use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; @@ -439,9 +440,9 @@ profile = "codex-bedrock" ); } -#[test] -fn rejects_unsupported_amazon_bedrock_overrides() { - let err = toml::from_str::( +#[tokio::test] +async fn load_config_ignores_unsupported_amazon_bedrock_overrides() { + let cfg = toml::from_str::( r#" model_provider = "amazon-bedrock" @@ -455,11 +456,32 @@ supports_websockets = true profile = "codex-bedrock" "#, ) - .unwrap_err(); + .expect("Amazon Bedrock unsupported overrides should deserialize"); - assert!(err.to_string().contains( - "model_providers.amazon-bedrock: `amazon-bedrock` is a built-in provider; only `aws.profile` can be configured" - )); + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides::default(), + tempdir().expect("tempdir").abs(), + ) + .await + .expect("load config"); + + assert_eq!(config.model_provider.name, "Amazon Bedrock"); + assert_eq!( + config.model_provider.base_url.as_deref(), + Some(AMAZON_BEDROCK_DEFAULT_BASE_URL) + ); + assert_eq!(config.model_provider.wire_api, WireApi::Responses); + assert!(!config.model_provider.requires_openai_auth); + assert!(!config.model_provider.supports_websockets); + assert_eq!( + config + .model_provider + .aws + .as_ref() + .and_then(|aws| aws.profile.as_deref()), + Some("codex-bedrock") + ); } #[test] diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs index c13816af01a8..dcf5a0a339b8 100644 --- a/codex-rs/model-provider-info/src/lib.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -136,28 +136,11 @@ pub struct ModelProviderInfo { pub struct ModelProviderAwsAuthInfo { /// AWS profile name to use. When unset, the AWS SDK default chain decides. pub profile: Option, - /// AWS region to use. When unset, the AWS SDK default chain decides. - pub region: Option, - /// AWS SigV4 service name. Defaults to `bedrock` when unset. - pub service: Option, -} - -impl ModelProviderAwsAuthInfo { - pub fn service_name(&self) -> &str { - self.service.as_deref().unwrap_or("bedrock") - } } impl ModelProviderInfo { pub fn validate(&self) -> std::result::Result<(), String> { - if let Some(aws) = self.aws.as_ref() { - if aws - .service - .as_ref() - .is_some_and(|service| service.trim().is_empty()) - { - return Err("provider aws.service must not be empty".to_string()); - } + if self.aws.is_some() { if self.supports_websockets { // TODO(celia-oai): Support AWS SigV4 signing for WebSocket // upgrade requests before allowing AWS-authenticated providers @@ -369,11 +352,7 @@ impl ModelProviderInfo { env_key_instructions: None, experimental_bearer_token: None, auth: None, - aws: Some(aws.unwrap_or(ModelProviderAwsAuthInfo { - profile: None, - region: None, - service: None, - })), + aws: Some(aws.unwrap_or(ModelProviderAwsAuthInfo { profile: None })), wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -450,25 +429,7 @@ pub fn merge_configured_model_providers( ) -> Result, String> { for (key, mut provider) in configured_model_providers { if key == AMAZON_BEDROCK_PROVIDER_ID { - let aws_override = provider.aws.take(); - if provider != ModelProviderInfo::default() { - return Err(format!( - "model_providers.{AMAZON_BEDROCK_PROVIDER_ID} only supports changing \ -`aws.profile`; other non-default provider fields are not supported" - )); - } - - let Some(aws_override) = aws_override else { - continue; - }; - if aws_override.region.is_some() || aws_override.service.is_some() { - return Err(format!( - "model_providers.{AMAZON_BEDROCK_PROVIDER_ID} only supports changing \ -`aws.profile`; other non-default provider fields are not supported" - )); - } - - if let Some(profile) = aws_override.profile + if let Some(profile) = provider.aws.and_then(|aws| aws.profile) && let Some(built_in) = model_providers.get_mut(AMAZON_BEDROCK_PROVIDER_ID) && let Some(aws) = built_in.aws.as_mut() { diff --git a/codex-rs/model-provider-info/src/model_provider_info_tests.rs b/codex-rs/model-provider-info/src/model_provider_info_tests.rs index 117330a2c4b6..0be24e15a8bd 100644 --- a/codex-rs/model-provider-info/src/model_provider_info_tests.rs +++ b/codex-rs/model-provider-info/src/model_provider_info_tests.rs @@ -225,8 +225,6 @@ base_url = "https://bedrock.example.com/v1" [aws] profile = "codex-bedrock" -region = "us-east-1" -service = "bedrock" "#; let provider: ModelProviderInfo = toml::from_str(provider_toml).unwrap(); @@ -235,8 +233,6 @@ service = "bedrock" provider.aws, Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), - region: Some("us-east-1".to_string()), - service: Some("bedrock".to_string()), }) ); } @@ -252,11 +248,7 @@ fn test_create_amazon_bedrock_provider() { env_key_instructions: None, experimental_bearer_token: None, auth: None, - aws: Some(ModelProviderAwsAuthInfo { - profile: None, - region: None, - service: None, - }), + aws: Some(ModelProviderAwsAuthInfo { profile: None }), wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -312,8 +304,6 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_profile_override ModelProviderInfo { aws: Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), - region: None, - service: None, }), ..ModelProviderInfo::default() }, @@ -325,8 +315,6 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_profile_override .expect("Amazon Bedrock provider should be built in") .aws = Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), - region: None, - service: None, }); assert_eq!( @@ -339,29 +327,32 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_profile_override } #[test] -fn test_merge_configured_model_providers_rejects_amazon_bedrock_non_default_fields() { +fn test_merge_configured_model_providers_ignores_amazon_bedrock_non_default_fields() { let configured_model_providers = std::collections::HashMap::from([( AMAZON_BEDROCK_PROVIDER_ID.to_string(), ModelProviderInfo { name: "Custom Bedrock".to_string(), aws: Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), - region: None, - service: None, }), ..ModelProviderInfo::default() }, )]); + let mut expected = built_in_model_providers(/*openai_base_url*/ None); + expected + .get_mut(AMAZON_BEDROCK_PROVIDER_ID) + .expect("Amazon Bedrock provider should be built in") + .aws = Some(ModelProviderAwsAuthInfo { + profile: Some("codex-bedrock".to_string()), + }); + assert_eq!( merge_configured_model_providers( built_in_model_providers(/*openai_base_url*/ None), configured_model_providers, ), - Err( - "model_providers.amazon-bedrock only supports changing `aws.profile`; other non-default provider fields are not supported" - .to_string() - ) + Ok(expected) ); } @@ -370,11 +361,7 @@ fn test_merge_configured_model_providers_allows_amazon_bedrock_default_fields() let configured_model_providers = std::collections::HashMap::from([( AMAZON_BEDROCK_PROVIDER_ID.to_string(), ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { - profile: None, - region: None, - service: None, - }), + aws: Some(ModelProviderAwsAuthInfo { profile: None }), wire_api: WireApi::Responses, ..ModelProviderInfo::default() }, @@ -392,11 +379,7 @@ fn test_merge_configured_model_providers_allows_amazon_bedrock_default_fields() #[test] fn test_validate_provider_aws_rejects_conflicting_auth() { let provider = ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { - profile: None, - region: None, - service: None, - }), + aws: Some(ModelProviderAwsAuthInfo { profile: None }), env_key: Some("AWS_BEARER_TOKEN_BEDROCK".to_string()), supports_websockets: false, ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) @@ -411,11 +394,7 @@ fn test_validate_provider_aws_rejects_conflicting_auth() { #[test] fn test_validate_provider_aws_rejects_websockets() { let provider = ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { - profile: None, - region: None, - service: None, - }), + aws: Some(ModelProviderAwsAuthInfo { profile: None }), requires_openai_auth: false, supports_websockets: true, ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) @@ -427,24 +406,6 @@ fn test_validate_provider_aws_rejects_websockets() { ); } -#[test] -fn test_validate_provider_aws_rejects_empty_service() { - let provider = ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { - profile: None, - region: None, - service: Some(" ".to_string()), - }), - requires_openai_auth: false, - ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) - }; - - assert_eq!( - provider.validate(), - Err("provider aws.service must not be empty".to_string()) - ); -} - #[test] fn test_deserialize_provider_auth_config_allows_zero_refresh_interval() { let base_dir = tempdir().unwrap(); From 7e1074a559a67b7fd39b00c1c514c8e9d1e05cf4 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 20 Apr 2026 15:49:03 -0700 Subject: [PATCH 03/11] changes --- MODULE.bazel.lock | 25 +- codex-rs/Cargo.lock | 415 +++++++++++++++++- codex-rs/Cargo.toml | 6 + codex-rs/aws-auth/BUILD.bazel | 6 + codex-rs/aws-auth/Cargo.toml | 26 ++ codex-rs/aws-auth/src/config.rs | 38 ++ codex-rs/aws-auth/src/lib.rs | 259 +++++++++++ codex-rs/aws-auth/src/signing.rs | 76 ++++ codex-rs/codex-api/src/auth.rs | 48 +- codex-rs/codex-api/src/endpoint/responses.rs | 4 +- codex-rs/codex-api/src/endpoint/session.rs | 24 +- codex-rs/codex-api/src/lib.rs | 1 + codex-rs/codex-api/tests/clients.rs | 180 ++++++++ codex-rs/codex-client/src/request.rs | 126 ++++++ codex-rs/codex-client/src/transport.rs | 58 +-- codex-rs/model-provider/Cargo.toml | 3 + codex-rs/model-provider/src/auth.rs | 14 + .../model-provider/src/aws_auth_provider.rs | 88 ++++ codex-rs/model-provider/src/lib.rs | 1 + codex-rs/model-provider/src/provider.rs | 21 + 20 files changed, 1347 insertions(+), 72 deletions(-) create mode 100644 codex-rs/aws-auth/BUILD.bazel create mode 100644 codex-rs/aws-auth/Cargo.toml create mode 100644 codex-rs/aws-auth/src/config.rs create mode 100644 codex-rs/aws-auth/src/lib.rs create mode 100644 codex-rs/aws-auth/src/signing.rs create mode 100644 codex-rs/model-provider/src/aws_auth_provider.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index c107884e1dfa..667c77893b04 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -660,14 +660,32 @@ "atoi_2.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.14\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"num-traits/std\"]}}", "atomic-waker_1.1.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7.0\"}],\"features\":{}}", "autocfg_1.5.0": "{\"dependencies\":[],\"features\":{}}", + "aws-config_1.8.12": "{\"dependencies\":[{\"features\":[\"test-util\"],\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-runtime\",\"req\":\"^1.5.17\"},{\"default_features\":false,\"name\":\"aws-sdk-signin\",\"optional\":true,\"req\":\"^1.2.0\"},{\"default_features\":false,\"name\":\"aws-sdk-sso\",\"optional\":true,\"req\":\"^1.91.0\"},{\"default_features\":false,\"name\":\"aws-sdk-ssooidc\",\"optional\":true,\"req\":\"^1.93.0\"},{\"default_features\":false,\"name\":\"aws-sdk-sts\",\"req\":\"^1.95.0\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"rt-tokio\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"features\":[\"default-client\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-http-client\",\"req\":\"^1.1.5\"},{\"name\":\"aws-smithy-json\",\"req\":\"^0.61.8\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"base64-simd\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bytes\",\"req\":\"^1.1.0\"},{\"name\":\"fastrand\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.29\"},{\"name\":\"hex\",\"optional\":true,\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"p256\",\"optional\":true,\"req\":\"^0.13.2\"},{\"default_features\":false,\"features\":[\"std\",\"std_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8.5\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17.5\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.9\"},{\"features\":[\"parsing\"],\"name\":\"time\",\"req\":\"^0.3.4\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.13.1\"},{\"features\":[\"full\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"fmt\",\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2.4\"},{\"name\":\"url\",\"req\":\"^2.5.4\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.18.1\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"allow-compilation\":[],\"behavior-version-latest\":[],\"client-hyper\":[\"aws-smithy-runtime/default-https-client\"],\"credentials-login\":[\"dep:aws-sdk-signin\",\"dep:sha2\",\"dep:zeroize\",\"dep:hex\",\"dep:base64-simd\",\"dep:uuid\",\"uuid?/v4\",\"dep:p256\",\"p256?/arithmetic\",\"p256?/pem\",\"dep:rand\"],\"credentials-process\":[\"tokio/process\"],\"default\":[\"default-https-client\",\"rt-tokio\",\"credentials-process\",\"sso\"],\"default-https-client\":[\"aws-smithy-runtime/default-https-client\"],\"rt-tokio\":[\"aws-smithy-async/rt-tokio\",\"aws-smithy-runtime/rt-tokio\",\"tokio/rt\"],\"rustls\":[\"client-hyper\"],\"sso\":[\"dep:aws-sdk-sso\",\"dep:aws-sdk-ssooidc\",\"dep:ring\",\"dep:hex\",\"dep:zeroize\",\"aws-smithy-runtime-api/http-auth\"],\"test-util\":[\"aws-runtime/test-util\"]}}", + "aws-credential-types_1.2.11": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1.74\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"client\",\"http-auth\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"features\":[\"full\",\"test-util\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"zeroize\",\"req\":\"^1.7.0\"}],\"features\":{\"hardcoded-credentials\":[],\"test-util\":[\"aws-smithy-runtime-api/test-util\"]}}", "aws-lc-rs_1.16.2": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.39.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"dev-tests-only\":[],\"fips\":[\"dep:aws-lc-fips-sys\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}", "aws-lc-sys_0.39.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.26\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"dunce\",\"req\":\"^1.0.5\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"}],\"features\":{\"all-bindings\":[],\"asan\":[],\"bindgen\":[\"dep:bindgen\"],\"default\":[\"all-bindings\"],\"disable-prebuilt-nasm\":[],\"fips\":[\"dep:bindgen\"],\"prebuilt-nasm\":[],\"ssl\":[\"bindgen\",\"all-bindings\"]}}", + "aws-runtime_1.5.17": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"arbitrary\",\"req\":\"^1.3\"},{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"http0-compat\"],\"name\":\"aws-sigv4\",\"req\":\"^1.3.7\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-eventstream\",\"optional\":true,\"req\":\"^0.60.14\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"kind\":\"dev\",\"name\":\"aws-smithy-protocol-test\",\"req\":\"^0.63.7\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"bytes\",\"req\":\"^1.10.0\"},{\"kind\":\"dev\",\"name\":\"bytes-utils\",\"req\":\"^0.1.2\"},{\"kind\":\"dev\",\"name\":\"convert_case\",\"req\":\"^0.6.0\"},{\"name\":\"fastrand\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.29\"},{\"name\":\"http-02x\",\"package\":\"http\",\"req\":\"^0.2.9\"},{\"name\":\"http-1x\",\"optional\":true,\"package\":\"http\",\"req\":\"^1.1.0\"},{\"name\":\"http-body-04x\",\"package\":\"http-body\",\"req\":\"^0.4.5\"},{\"name\":\"http-body-1x\",\"optional\":true,\"package\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.2\"},{\"name\":\"regex-lite\",\"optional\":true,\"req\":\"^0.1.5\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.17\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2.4\"},{\"name\":\"uuid\",\"req\":\"^1\"}],\"features\":{\"event-stream\":[\"dep:aws-smithy-eventstream\",\"aws-sigv4/sign-eventstream\"],\"http-02x\":[],\"http-1x\":[\"dep:http-1x\",\"dep:http-body-1x\"],\"sigv4a\":[\"aws-sigv4/sigv4a\"],\"test-util\":[\"dep:regex-lite\"]}}", + "aws-sdk-sso_1.91.0": "{\"dependencies\":[{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-runtime\",\"req\":\"^1.5.17\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"name\":\"aws-smithy-json\",\"req\":\"^0.61.8\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\",\"http-02x\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"bytes\",\"req\":\"^1.4.0\"},{\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"http\",\"req\":\"^0.2.9\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"regex-lite\",\"req\":\"^0.1.5\"},{\"features\":[\"macros\",\"test-util\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"behavior-version-latest\":[],\"default\":[\"rustls\",\"default-https-client\",\"rt-tokio\"],\"default-https-client\":[\"aws-smithy-runtime/default-https-client\"],\"gated-tests\":[],\"rt-tokio\":[\"aws-smithy-async/rt-tokio\",\"aws-smithy-types/rt-tokio\"],\"rustls\":[\"aws-smithy-runtime/tls-rustls\"],\"test-util\":[\"aws-credential-types/test-util\",\"aws-smithy-runtime/test-util\"]}}", + "aws-sdk-ssooidc_1.93.0": "{\"dependencies\":[{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-runtime\",\"req\":\"^1.5.17\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"name\":\"aws-smithy-json\",\"req\":\"^0.61.8\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\",\"http-02x\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"bytes\",\"req\":\"^1.4.0\"},{\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"http\",\"req\":\"^0.2.9\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"regex-lite\",\"req\":\"^0.1.5\"},{\"features\":[\"macros\",\"test-util\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"behavior-version-latest\":[],\"default\":[\"rustls\",\"default-https-client\",\"rt-tokio\"],\"default-https-client\":[\"aws-smithy-runtime/default-https-client\"],\"gated-tests\":[],\"rt-tokio\":[\"aws-smithy-async/rt-tokio\",\"aws-smithy-types/rt-tokio\"],\"rustls\":[\"aws-smithy-runtime/tls-rustls\"],\"test-util\":[\"aws-credential-types/test-util\",\"aws-smithy-runtime/test-util\"]}}", + "aws-sdk-sts_1.95.0": "{\"dependencies\":[{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-runtime\",\"req\":\"^1.5.17\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-runtime\",\"req\":\"^1.5.17\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"features\":[\"test-util\",\"wire-mock\"],\"kind\":\"dev\",\"name\":\"aws-smithy-http-client\",\"req\":\"^1.1.5\"},{\"name\":\"aws-smithy-json\",\"req\":\"^0.61.8\"},{\"kind\":\"dev\",\"name\":\"aws-smithy-protocol-test\",\"req\":\"^0.63.7\"},{\"name\":\"aws-smithy-query\",\"req\":\"^0.60.9\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\",\"http-02x\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-smithy-xml\",\"req\":\"^0.60.13\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.25\"},{\"name\":\"http\",\"req\":\"^0.2.9\"},{\"kind\":\"dev\",\"name\":\"http-1x\",\"package\":\"http\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"regex-lite\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"features\":[\"macros\",\"test-util\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.16\"}],\"features\":{\"behavior-version-latest\":[],\"default\":[\"rustls\",\"default-https-client\",\"rt-tokio\"],\"default-https-client\":[\"aws-smithy-runtime/default-https-client\"],\"gated-tests\":[],\"rt-tokio\":[\"aws-smithy-async/rt-tokio\",\"aws-smithy-types/rt-tokio\"],\"rustls\":[\"aws-smithy-runtime/tls-rustls\"],\"test-util\":[\"aws-credential-types/test-util\",\"aws-smithy-runtime/test-util\"]}}", + "aws-sigv4_1.3.7": "{\"dependencies\":[{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"test-util\",\"hardcoded-credentials\"],\"kind\":\"dev\",\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-smithy-eventstream\",\"optional\":true,\"req\":\"^0.60.14\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"client\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"bytes\",\"req\":\"^1.10.0\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"crypto-bigint\",\"optional\":true,\"req\":\"^0.5.4\"},{\"name\":\"form_urlencoded\",\"optional\":true,\"req\":\"^1.2.1\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.1\"},{\"name\":\"hmac\",\"req\":\"^0.12\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1.1.0\"},{\"name\":\"http0\",\"optional\":true,\"package\":\"http\",\"req\":\"^0.2.9\"},{\"kind\":\"dev\",\"name\":\"httparse\",\"req\":\"^1.10.1\"},{\"features\":[\"ecdsa\"],\"name\":\"p256\",\"optional\":true,\"req\":\"^0.11\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.3.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.2\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17.5\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.5\",\"target\":\"cfg(not(any(target_arch = \\\"powerpc\\\", target_arch = \\\"powerpc64\\\")))\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.180\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.180\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.104\"},{\"name\":\"sha2\",\"req\":\"^0.10\"},{\"name\":\"subtle\",\"optional\":true,\"req\":\"^2.5.0\"},{\"name\":\"time\",\"req\":\"^0.3.5\"},{\"features\":[\"parsing\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.5\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.7.0\"}],\"features\":{\"default\":[\"sign-http\",\"http1\"],\"http0-compat\":[\"dep:http0\"],\"http1\":[\"dep:http\"],\"sign-eventstream\":[\"dep:aws-smithy-eventstream\"],\"sign-http\":[\"dep:http0\",\"dep:percent-encoding\",\"dep:form_urlencoded\"],\"sigv4a\":[\"dep:p256\",\"dep:crypto-bigint\",\"dep:subtle\",\"dep:zeroize\",\"dep:ring\"]}}", + "aws-smithy-async_1.2.7": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.29\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"kind\":\"dev\",\"name\":\"pin-utils\",\"req\":\"^0.1\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.40.0\"},{\"features\":[\"rt\",\"macros\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.2\"}],\"features\":{\"rt-tokio\":[\"tokio/time\"],\"test-util\":[\"rt-tokio\",\"tokio/rt\"]}}", + "aws-smithy-http-client_1.1.5": "{\"dependencies\":[{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"rt-tokio\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-protocol-test\",\"optional\":true,\"req\":\"^0.63.7\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"features\":[\"http-body-0-4-x\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1.10.0\"},{\"default_features\":false,\"name\":\"h2\",\"req\":\"^0.4.11\"},{\"name\":\"h2-0-3\",\"optional\":true,\"package\":\"h2\",\"req\":\"^0.3.24\"},{\"name\":\"http-02x\",\"optional\":true,\"package\":\"http\",\"req\":\"^0.2.9\"},{\"name\":\"http-1x\",\"optional\":true,\"package\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body-04x\",\"optional\":true,\"package\":\"http-body\",\"req\":\"^0.4.5\"},{\"name\":\"http-body-1x\",\"optional\":true,\"package\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1.2\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1.2\"},{\"features\":[\"client\",\"http1\",\"http2\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.6.0\"},{\"default_features\":false,\"features\":[\"client\",\"http1\",\"http2\",\"tcp\",\"stream\"],\"name\":\"hyper-0-14\",\"optional\":true,\"package\":\"hyper\",\"req\":\"^0.14.26\"},{\"default_features\":false,\"features\":[\"http2\",\"http1\",\"native-tokio\",\"tls12\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.27\"},{\"features\":[\"http1\",\"http2\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.16\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.16\"},{\"features\":[\"serde\"],\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.10.0\"},{\"default_features\":false,\"features\":[\"http1\",\"tls12\",\"logging\",\"acceptor\",\"tokio-runtime\",\"http2\"],\"name\":\"legacy-hyper-rustls\",\"optional\":true,\"package\":\"hyper-rustls\",\"req\":\"^0.24.2\"},{\"name\":\"legacy-rustls\",\"optional\":true,\"package\":\"rustls\",\"req\":\"^0.21.8\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"default_features\":false,\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.31\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.1\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2.2.0\"},{\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.12.0\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rustls-pki-types\",\"req\":\"^1.12.0\"},{\"name\":\"s2n-tls\",\"optional\":true,\"req\":\"^0.3.24\"},{\"name\":\"s2n-tls-hyper\",\"optional\":true,\"req\":\"^0.0.16\"},{\"name\":\"s2n-tls-tokio\",\"optional\":true,\"req\":\"^0.3.24\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.210\"},{\"features\":[\"preserve_order\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.128\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3.2\"},{\"name\":\"tokio\",\"req\":\"^1.40\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"test-util\",\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26.2\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26.2\"},{\"name\":\"tower\",\"optional\":true,\"req\":\"^0.5.2\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"}],\"features\":{\"default-client\":[\"aws-smithy-runtime-api/http-1x\",\"aws-smithy-types/http-body-1-x\",\"dep:hyper\",\"dep:hyper-util\",\"hyper-util?/client-legacy\",\"hyper-util?/client-proxy\",\"dep:http-1x\",\"dep:tower\",\"dep:rustls-pki-types\",\"dep:rustls-native-certs\"],\"hyper-014\":[\"aws-smithy-runtime-api/http-02x\",\"aws-smithy-types/http-body-0-4-x\",\"dep:http-02x\",\"dep:http-body-04x\",\"dep:hyper-0-14\",\"dep:h2-0-3\"],\"legacy-rustls-ring\":[\"dep:legacy-hyper-rustls\",\"dep:legacy-rustls\",\"dep:rustls-native-certs\",\"hyper-014\"],\"legacy-test-util\":[\"test-util\",\"dep:http-02x\",\"aws-smithy-runtime-api/http-02x\",\"aws-smithy-types/http-body-0-4-x\"],\"rustls-aws-lc\":[\"dep:rustls\",\"rustls?/aws_lc_rs\",\"rustls?/prefer-post-quantum\",\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"default-client\"],\"rustls-aws-lc-fips\":[\"dep:rustls\",\"rustls?/fips\",\"rustls?/prefer-post-quantum\",\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"default-client\"],\"rustls-ring\":[\"dep:rustls\",\"rustls?/ring\",\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"default-client\"],\"s2n-tls\":[\"dep:s2n-tls\",\"dep:s2n-tls-hyper\",\"dep:s2n-tls-tokio\",\"default-client\"],\"test-util\":[\"dep:aws-smithy-protocol-test\",\"dep:serde\",\"dep:serde_json\",\"dep:indexmap\",\"dep:bytes\",\"dep:http-1x\",\"aws-smithy-runtime-api/http-1x\",\"dep:http-body-1x\",\"aws-smithy-types/http-body-1-x\",\"tokio/rt\"],\"wire-mock\":[\"test-util\",\"default-client\",\"hyper-util?/server\",\"hyper-util?/server-auto\",\"hyper-util?/service\",\"hyper-util?/server-graceful\",\"tokio/macros\",\"dep:http-body-util\"]}}", + "aws-smithy-http_0.62.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"name\":\"aws-smithy-eventstream\",\"optional\":true,\"req\":\"^0.60.14\"},{\"features\":[\"client\",\"http-02x\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"byte-stream-poll-next\",\"http-body-0-4-x\"],\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"bytes\",\"req\":\"^1.10.0\"},{\"name\":\"bytes-utils\",\"req\":\"^0.1\"},{\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.29\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.29\"},{\"name\":\"http-02x\",\"package\":\"http\",\"req\":\"^0.2.9\"},{\"name\":\"http-1x\",\"package\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body-04x\",\"package\":\"http-body\",\"req\":\"^0.4.5\"},{\"features\":[\"stream\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14.26\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"}],\"features\":{\"event-stream\":[\"aws-smithy-eventstream\"],\"rt-tokio\":[\"aws-smithy-types/rt-tokio\"]}}", + "aws-smithy-json_0.61.9": "{\"dependencies\":[{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}", + "aws-smithy-observability_0.1.5": "{\"dependencies\":[{\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3.1.1\"}],\"features\":{}}", + "aws-smithy-query_0.60.9": "{\"dependencies\":[{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"urlencoding\",\"req\":\"^2.1\"}],\"features\":{}}", + "aws-smithy-runtime-api_1.9.3": "{\"dependencies\":[{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"bytes\",\"req\":\"^1.10.0\"},{\"name\":\"http-02x\",\"package\":\"http\",\"req\":\"^0.2.9\"},{\"name\":\"http-1x\",\"package\":\"http\",\"req\":\"^1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.40.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.7.0\"}],\"features\":{\"client\":[],\"default\":[],\"http-02x\":[],\"http-1x\":[],\"http-auth\":[\"dep:zeroize\"],\"test-util\":[\"aws-smithy-types/test-util\",\"http-1x\"]}}", + "aws-smithy-runtime_1.9.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"rt-tokio\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"name\":\"aws-smithy-http-client\",\"optional\":true,\"req\":\"^1.1.5\"},{\"name\":\"aws-smithy-observability\",\"req\":\"^0.1.5\"},{\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"http-body-0-4-x\"],\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"bytes\",\"req\":\"^1.10.0\"},{\"name\":\"fastrand\",\"req\":\"^2.3.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.29\"},{\"name\":\"http-02x\",\"package\":\"http\",\"req\":\"^0.2.9\"},{\"name\":\"http-1x\",\"package\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body-04x\",\"package\":\"http-body\",\"req\":\"^0.4.5\"},{\"name\":\"http-body-1x\",\"package\":\"http-body\",\"req\":\"^1\"},{\"features\":[\"client\",\"server\",\"tcp\",\"http1\",\"http2\"],\"kind\":\"dev\",\"name\":\"hyper_0_14\",\"package\":\"hyper\",\"req\":\"^0.14.27\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4.0\"},{\"name\":\"tokio\",\"req\":\"^1.40.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"test-util\",\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"},{\"features\":[\"env-filter\",\"fmt\",\"json\"],\"name\":\"tracing-subscriber\",\"optional\":true,\"req\":\"^0.3.16\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2.1\"}],\"features\":{\"client\":[\"aws-smithy-runtime-api/client\",\"aws-smithy-types/http-body-1-x\"],\"connector-hyper-0-14-x\":[\"dep:aws-smithy-http-client\",\"aws-smithy-http-client?/hyper-014\"],\"default-https-client\":[\"dep:aws-smithy-http-client\",\"aws-smithy-http-client?/rustls-aws-lc\"],\"http-auth\":[\"aws-smithy-runtime-api/http-auth\"],\"legacy-test-util\":[\"aws-smithy-runtime-api/test-util\",\"dep:tracing-subscriber\",\"aws-smithy-http-client/test-util\",\"connector-hyper-0-14-x\",\"aws-smithy-http-client/legacy-test-util\"],\"rt-tokio\":[\"tokio/rt\"],\"test-util\":[\"aws-smithy-runtime-api/test-util\",\"dep:tracing-subscriber\",\"aws-smithy-http-client/test-util\",\"legacy-test-util\"],\"tls-rustls\":[\"dep:aws-smithy-http-client\",\"aws-smithy-http-client?/legacy-rustls-ring\",\"connector-hyper-0-14-x\"],\"wire-mock\":[\"legacy-test-util\",\"aws-smithy-http-client/wire-mock\"]}}", + "aws-smithy-types_1.3.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.13.0\"},{\"name\":\"base64-simd\",\"req\":\"^0.8\"},{\"name\":\"bytes\",\"req\":\"^1.10.0\"},{\"name\":\"bytes-utils\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"ciborium\",\"req\":\"^0.2.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3.31\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^0.2.9\"},{\"name\":\"http-1x\",\"optional\":true,\"package\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body-0-4\",\"optional\":true,\"package\":\"http-body\",\"req\":\"^0.4.5\"},{\"name\":\"http-body-1-0\",\"optional\":true,\"package\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"hyper-0-14\",\"optional\":true,\"package\":\"hyper\",\"req\":\"^0.14.26\"},{\"name\":\"itoa\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"num-integer\",\"req\":\"^0.1.44\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"name\":\"ryu\",\"req\":\"^1.0.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.210\",\"target\":\"cfg(aws_sdk_unstable)\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.16.0\"},{\"features\":[\"parsing\"],\"name\":\"time\",\"req\":\"^0.3.4\"},{\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.40.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"fs\",\"io-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1.5\"},{\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.1\"}],\"features\":{\"byte-stream-poll-next\":[],\"http-body-0-4-x\":[\"dep:http-body-0-4\",\"dep:http\"],\"http-body-1-x\":[\"dep:http-body-1-0\",\"dep:http-body-util\",\"dep:http-body-0-4\",\"dep:http-1x\",\"dep:http\"],\"hyper-0-14-x\":[\"dep:hyper-0-14\"],\"rt-tokio\":[\"dep:http-body-0-4\",\"dep:tokio-util\",\"dep:tokio\",\"tokio?/rt\",\"tokio?/fs\",\"tokio?/io-util\",\"tokio-util?/io\",\"dep:futures-core\",\"dep:http\"],\"serde-deserialize\":[],\"serde-serialize\":[],\"test-util\":[]}}", + "aws-smithy-xml_0.60.13": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"aws-smithy-protocol-test\",\"req\":\"^0.63.7\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"xmlparser\",\"req\":\"^0.13.5\"}],\"features\":{}}", + "aws-types_1.3.11": "{\"dependencies\":[{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-runtime\",\"optional\":true,\"req\":\"^1.9.5\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"http-02x\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2.4\"},{\"default_features\":false,\"features\":[\"http2\",\"webpki-roots\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.24.2\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.16.0\"},{\"features\":[\"rt\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2.5\"}],\"features\":{\"examples\":[\"dep:hyper-rustls\",\"aws-smithy-runtime/client\",\"aws-smithy-runtime/connector-hyper-0-14-x\",\"aws-smithy-runtime/tls-rustls\"]}}", "axum-core_0.4.5": "{\"dependencies\":[{\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"kind\":\"dev\",\"name\":\"axum\",\"req\":\"^0.7.2\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.0\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"limit\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{\"__private_docs\":[\"dep:tower-http\"],\"tracing\":[\"dep:tracing\"]}}", "axum-core_0.5.6": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.0\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"limit\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{\"__private_docs\":[\"dep:tower-http\"],\"tracing\":[\"dep:tracing\"]}}", "axum_0.7.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"name\":\"axum-core\",\"req\":\"^0.4.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.4.2\"},{\"features\":[\"__private\"],\"kind\":\"dev\",\"name\":\"axum-macros\",\"req\":\"^0.4.1\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"^0.7\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.24.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.24.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:serde_urlencoded\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:serde_urlencoded\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "axum_0.8.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"axum-core\",\"req\":\"^0.5.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"form_urlencoded\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"client\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"=0.8.4\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.211\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"name\":\"serde_core\",\"req\":\"^1.0.221\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.44\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.44.2\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.28.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.28.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private\":[\"tokio\",\"http1\",\"dep:reqwest\"],\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:serde\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "backtrace_0.3.76": "{\"dependencies\":[{\"default_features\":false,\"name\":\"addr2line\",\"req\":\"^0.25.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.156\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libloading\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"miniz_oxide\",\"req\":\"^0.8\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"default_features\":false,\"features\":[\"read_core\",\"elf\",\"macho\",\"pe\",\"xcoff\",\"unaligned\",\"archive\"],\"name\":\"object\",\"req\":\"^0.37.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"rustc-demangle\",\"req\":\"^0.1.24\"},{\"default_features\":false,\"name\":\"ruzstd\",\"optional\":true,\"req\":\"^0.8.1\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(any(windows, target_os = \\\"cygwin\\\"))\"}],\"features\":{\"coresymbolication\":[],\"dbghelp\":[],\"default\":[\"std\"],\"dl_iterate_phdr\":[],\"dladdr\":[],\"kernel32\":[],\"libunwind\":[],\"ruzstd\":[\"dep:ruzstd\"],\"serialize-serde\":[\"serde\"],\"std\":[],\"unix-backtrace\":[]}}", - "base16ct_0.2.0": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", + "base64-simd_0.8.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.20.0\"},{\"kind\":\"dev\",\"name\":\"const-str\",\"req\":\"^0.5.3\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.8\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"outref\",\"req\":\"^0.5.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"vsimd\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.33\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"}],\"features\":{\"alloc\":[\"vsimd/alloc\"],\"default\":[\"std\",\"detect\"],\"detect\":[\"vsimd/detect\"],\"std\":[\"alloc\",\"vsimd/std\"],\"unstable\":[\"vsimd/unstable\"]}}", "base64_0.21.7": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "base64_0.22.1": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "base64ct_1.8.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.6\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", @@ -693,6 +711,7 @@ "bytemuck_1.25.0": "{\"dependencies\":[{\"name\":\"bytemuck_derive\",\"optional\":true,\"req\":\"^1.10.2\"},{\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.22\"}],\"features\":{\"aarch64_simd\":[],\"align_offset\":[],\"alloc_uninit\":[],\"avx512_simd\":[],\"const_zeroed\":[],\"derive\":[\"bytemuck_derive\"],\"extern_crate_alloc\":[],\"extern_crate_std\":[\"extern_crate_alloc\"],\"impl_core_error\":[],\"latest_stable_rust\":[\"aarch64_simd\",\"avx512_simd\",\"align_offset\",\"alloc_uninit\",\"const_zeroed\",\"derive\",\"impl_core_error\",\"min_const_generics\",\"must_cast\",\"must_cast_extra\",\"pod_saturating\",\"track_caller\",\"transparentwrapper_extra\",\"wasm_simd\",\"zeroable_atomics\",\"zeroable_maybe_uninit\",\"zeroable_unwind_fn\"],\"min_const_generics\":[],\"must_cast\":[],\"must_cast_extra\":[\"must_cast\"],\"nightly_docs\":[],\"nightly_float\":[],\"nightly_portable_simd\":[\"rustversion\"],\"nightly_stdsimd\":[],\"pod_saturating\":[],\"track_caller\":[],\"transparentwrapper_extra\":[],\"unsound_ptr_pod_impl\":[],\"wasm_simd\":[],\"zeroable_atomics\":[],\"zeroable_maybe_uninit\":[],\"zeroable_unwind_fn\":[]}}", "byteorder-lite_0.1.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "byteorder_1.5.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"std\"],\"i128\":[],\"std\":[]}}", + "bytes-utils_0.1.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.144\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde\",\"bytes/serde\"],\"std\":[\"bytes/default\"]}}", "bytes_1.11.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"extra-platforms\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.60\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "bytestring_1.5.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{\"serde\":[\"dep:serde_core\"]}}", "bzip2-sys_0.1.13+1.0.8": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"}],\"features\":{\"__disabled\":[],\"static\":[]}}", @@ -1037,6 +1056,7 @@ "home_0.5.9": "{\"dependencies\":[{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "hostname_0.4.2": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(unix, target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"similar-asserts\",\"req\":\"^1.6.1\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"windows-bindgen\",\"req\":\"^0.65\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"default\":[],\"set\":[]}}", "http-body-util_0.1.3": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\",\"sync\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"}],\"features\":{\"channel\":[\"dep:tokio\"],\"default\":[],\"full\":[\"channel\"]}}", + "http-body_0.4.6": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^0.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"}],\"features\":{}}", "http-body_1.0.1": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^1\"}],\"features\":{}}", "http-range-header_0.4.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.8.3\"}],\"features\":{}}", "http_0.2.12": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"indexmap\",\"req\":\"<=1.8\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"seahash\",\"req\":\"^3.0.5\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}", @@ -1254,6 +1274,7 @@ "ordered-stream_0.2.0": "{\"dependencies\":[{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"futures-executor\",\"req\":\"^0.3.25\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.25\"}],\"features\":{}}", "os_info_3.14.0": "{\"dependencies\":[{\"name\":\"android_system_properties\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"feature\"],\"name\":\"nix\",\"req\":\"^0.30\",\"target\":\"cfg(any(target_os = \\\"aix\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"illumos\\\", target_os = \\\"linux\\\", target_os = \\\"macos\\\", target_os = \\\"netbsd\\\", target_os = \\\"openbsd\\\", target_os = \\\"cygwin\\\"))\"},{\"name\":\"objc2\",\"req\":\"^0.6\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"features\":[\"NSString\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"features\":[\"NSData\",\"NSError\",\"NSEnumerator\",\"NSString\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"objc2-ui-kit\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1\"},{\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0.3\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_LibraryLoader\",\"Win32_System_Registry\",\"Win32_System_SystemInformation\",\"Win32_System_SystemServices\",\"Win32_System_Threading\",\"Win32_UI_WindowsAndMessaging\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"serde\"]}}", "os_pipe_1.2.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.62\",\"target\":\"cfg(not(windows))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Pipes\",\"Win32_Security\"],\"name\":\"windows-sys\",\"req\":\">=0.28, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"io_safety\":[]}}", + "outref_0.5.2": "{\"dependencies\":[],\"features\":{}}", "owo-colors_4.3.0": "{\"dependencies\":[{\"name\":\"supports-color\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"supports-color-2\",\"optional\":true,\"package\":\"supports-color\",\"req\":\"^2.0\"}],\"features\":{\"alloc\":[],\"supports-colors\":[\"dep:supports-color-2\",\"supports-color\"]}}", "p256_0.13.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"blobby\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"der\"],\"name\":\"ecdsa-core\",\"optional\":true,\"package\":\"ecdsa\",\"req\":\"^0.16\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"ecdsa-core\",\"package\":\"ecdsa\",\"req\":\"^0.16\"},{\"default_features\":false,\"features\":[\"hazmat\",\"sec1\"],\"name\":\"elliptic-curve\",\"req\":\"^0.13.1\"},{\"name\":\"hex-literal\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"primeorder\",\"optional\":true,\"req\":\"^0.13\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"primeorder\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"}],\"features\":{\"alloc\":[\"ecdsa-core?/alloc\",\"elliptic-curve/alloc\"],\"arithmetic\":[\"dep:primeorder\",\"elliptic-curve/arithmetic\"],\"bits\":[\"arithmetic\",\"elliptic-curve/bits\"],\"default\":[\"arithmetic\",\"ecdsa\",\"pem\",\"std\"],\"digest\":[\"ecdsa-core/digest\",\"ecdsa-core/hazmat\"],\"ecdh\":[\"arithmetic\",\"elliptic-curve/ecdh\"],\"ecdsa\":[\"arithmetic\",\"ecdsa-core/signing\",\"ecdsa-core/verifying\",\"sha256\"],\"expose-field\":[\"arithmetic\"],\"hash2curve\":[\"arithmetic\",\"elliptic-curve/hash2curve\"],\"jwk\":[\"elliptic-curve/jwk\"],\"pem\":[\"elliptic-curve/pem\",\"ecdsa-core/pem\",\"pkcs8\"],\"pkcs8\":[\"ecdsa-core?/pkcs8\",\"elliptic-curve/pkcs8\"],\"serde\":[\"ecdsa-core?/serde\",\"elliptic-curve/serde\",\"primeorder?/serde\",\"serdect\"],\"sha256\":[\"digest\",\"sha2\"],\"std\":[\"alloc\",\"ecdsa-core?/std\",\"elliptic-curve/std\"],\"test-vectors\":[\"dep:hex-literal\"],\"voprf\":[\"elliptic-curve/voprf\",\"sha2\"]}}", "parking_2.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{}}", @@ -1652,6 +1673,7 @@ "vcpkg_0.2.15": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3.7\"}],\"features\":{}}", "version-compare_0.2.1": "{\"dependencies\":[],\"features\":{}}", "version_check_0.9.5": "{\"dependencies\":[],\"features\":{}}", + "vsimd_0.8.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"const-str\",\"req\":\"^0.5.3\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.8\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.33\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"}],\"features\":{\"alloc\":[],\"detect\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", "vt100_0.16.2": "{\"dependencies\":[{\"name\":\"itoa\",\"req\":\"^1.0.15\"},{\"features\":[\"term\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.30.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.219\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.140\"},{\"kind\":\"dev\",\"name\":\"terminal_size\",\"req\":\"^0.4.2\"},{\"name\":\"unicode-width\",\"req\":\"^0.2.1\"},{\"name\":\"vte\",\"req\":\"^0.15.0\"}],\"features\":{}}", "vte_0.15.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arrayvec\",\"req\":\"^0.7.2\"},{\"default_features\":false,\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2.3.3\"},{\"default_features\":false,\"name\":\"cursor-icon\",\"optional\":true,\"req\":\"^1.0.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.7.4\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.160\"}],\"features\":{\"ansi\":[\"log\",\"cursor-icon\",\"bitflags\"],\"default\":[\"std\"],\"serde\":[\"dep:serde\"],\"std\":[\"memchr/std\"]}}", "wait-timeout_0.2.1": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.56\",\"target\":\"cfg(unix)\"}],\"features\":{}}", @@ -1776,6 +1798,7 @@ "x509-parser_0.18.1": "{\"dependencies\":[{\"features\":[\"datetime\"],\"name\":\"asn1-rs\",\"req\":\"^0.7.0\"},{\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"data-encoding\",\"req\":\"^2.2.1\"},{\"features\":[\"bigint\"],\"name\":\"der-parser\",\"req\":\"^10.0\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"nom\",\"req\":\"^7.0\"},{\"features\":[\"crypto\",\"x509\",\"x962\"],\"name\":\"oid-registry\",\"req\":\"^0.8.1\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17.12\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.35\"}],\"features\":{\"default\":[],\"validate\":[],\"verify\":[\"ring\"],\"verify-aws\":[\"aws-lc-rs\"]}}", "xattr_1.6.1": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.150\",\"target\":\"cfg(any(target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\", target_os = \\\"macos\\\", target_os = \\\"hurd\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"default\":[\"unsupported\"],\"unsupported\":[]}}", "xdg-home_1.3.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{}}", + "xmlparser_0.13.6": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "xz2_0.1.7": "{\"dependencies\":[{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.1.26\"},{\"name\":\"lzma-sys\",\"req\":\"^0.1.18\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"tokio-core\",\"req\":\"^0.1.17\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.12\"}],\"features\":{\"static\":[\"lzma-sys/static\"],\"tokio\":[\"tokio-io\",\"futures\"]}}", "yaml-rust_0.4.5": "{\"dependencies\":[{\"name\":\"linked-hash-map\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"}],\"features\":{}}", "yansi_1.0.1": "{\"dependencies\":[{\"name\":\"is-terminal\",\"optional\":true,\"req\":\"^0.4.11\"}],\"features\":{\"_nightly\":[],\"alloc\":[],\"default\":[\"std\"],\"detect-env\":[\"std\"],\"detect-tty\":[\"is-terminal\",\"std\"],\"hyperlink\":[\"std\"],\"std\":[\"alloc\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4b003212b6a6..d78f66d55965 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -749,6 +749,48 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.0", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + [[package]] name = "aws-lc-rs" version = "1.16.2" @@ -772,6 +814,290 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "aws-runtime" +version = "1.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81b5b2898f6798ad58f484856768bca817e3cd9de0974c24ae0f1113fe88f1b" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.91.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee6402a36f27b52fe67661c6732d684b2635152b676aa2babbfb5204f99115d" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.93.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45a7f750bbd170ee3677671ad782d90b894548f4e4ae168302c57ec9de5cb3e" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55542378e419558e6b1f398ca70adb0b2088077e79ad9f14eb09441f2f7b2164" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "percent-encoding", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e62db736db19c488966c8d787f52e6270be565727236fd5579eaa301e7bc4a" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2", + "http 1.4.0", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f616c3f2260612fe44cede278bafa18e73e6479c4e393e2c4518cf2a9a228a" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fda37911905ea4d3141a01364bc5509a0f32ae3f3b22d6e330c0abfb62d247" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0d43d899f9e508300e587bf582ba54c27a452dd0a9ea294690669138ae14a2" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905cb13a9895626d49cf2ced759b062d913834c7482c38e49557eac4e6193f01" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.8.8" @@ -784,7 +1110,7 @@ dependencies = [ "form_urlencoded", "futures-util", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", "hyper", "hyper-util", @@ -817,7 +1143,7 @@ dependencies = [ "bytes", "futures-core", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -860,6 +1186,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -1059,6 +1395,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "bytestring" version = "1.5.0" @@ -1648,6 +1994,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "codex-aws-auth" +version = "0.0.0" +dependencies = [ + "aws-config", + "aws-credential-types", + "aws-sigv4", + "aws-types", + "bytes", + "http 1.4.0", + "pretty_assertions", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "codex-backend-client" version = "0.0.0" @@ -2511,11 +2872,14 @@ version = "0.0.0" dependencies = [ "async-trait", "codex-api", + "codex-aws-auth", + "codex-client", "codex-login", "codex-model-provider-info", "codex-protocol", "http 1.4.0", "pretty_assertions", + "tokio", ] [[package]] @@ -6443,6 +6807,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -6462,7 +6837,7 @@ dependencies = [ "bytes", "futures-core", "http 1.4.0", - "http-body", + "http-body 1.0.1", "pin-project-lite", ] @@ -6496,7 +6871,7 @@ dependencies = [ "futures-core", "h2", "http 1.4.0", - "http-body", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -6565,7 +6940,7 @@ dependencies = [ "futures-channel", "futures-util", "http 1.4.0", - "http-body", + "http-body 1.0.1", "hyper", "ipnet", "libc", @@ -8651,6 +9026,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "owo-colors" version = "4.3.0" @@ -9599,7 +9980,7 @@ dependencies = [ "const_format", "fnv", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", "itoa", "memchr", @@ -9998,7 +10379,7 @@ dependencies = [ "futures-util", "h2", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", "hyper", "hyper-rustls", @@ -10086,7 +10467,7 @@ dependencies = [ "chrono", "futures", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", "oauth2", "pastey", @@ -11365,7 +11746,7 @@ checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" dependencies = [ "bytes", "futures-util", - "http-body", + "http-body 1.0.1", "http-body-util", "pin-project-lite", ] @@ -12241,7 +12622,7 @@ dependencies = [ "bytes", "h2", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", "hyper", "hyper-timeout", @@ -12328,7 +12709,7 @@ dependencies = [ "bytes", "futures-util", "http 1.4.0", - "http-body", + "http-body 1.0.1", "iri-string", "pin-project-lite", "tower", @@ -12872,6 +13253,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "vt100" version = "0.16.2" @@ -13938,6 +14325,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xz2" version = "0.1.7" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 2c98852aa017..a11cb2e042cb 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "aws-auth", "analytics", "backend-client", "ansi-escape", @@ -112,6 +113,7 @@ app_test_support = { path = "app-server/tests/common" } codex-analytics = { path = "analytics" } codex-ansi-escape = { path = "ansi-escape" } codex-api = { path = "codex-api" } +codex-aws-auth = { path = "aws-auth" } codex-app-server = { path = "app-server" } codex-app-server-client = { path = "app-server-client" } codex-app-server-protocol = { path = "app-server-protocol" } @@ -217,6 +219,10 @@ async-channel = "2.3.1" async-io = "2.6.0" async-stream = "0.3.6" async-trait = "0.1.89" +aws-config = "1" +aws-credential-types = "1" +aws-sigv4 = "1" +aws-types = "1" axum = { version = "0.8", default-features = false } base64 = "0.22.1" bm25 = "2.3.2" diff --git a/codex-rs/aws-auth/BUILD.bazel b/codex-rs/aws-auth/BUILD.bazel new file mode 100644 index 000000000000..d278d5599c73 --- /dev/null +++ b/codex-rs/aws-auth/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "aws-auth", + crate_name = "codex_aws_auth", +) diff --git a/codex-rs/aws-auth/Cargo.toml b/codex-rs/aws-auth/Cargo.toml new file mode 100644 index 000000000000..9e49f7bbe50d --- /dev/null +++ b/codex-rs/aws-auth/Cargo.toml @@ -0,0 +1,26 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-aws-auth" +version.workspace = true + +[lib] +doctest = false +name = "codex_aws_auth" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +aws-config = { workspace = true } +aws-credential-types = { workspace = true } +aws-sigv4 = { workspace = true } +aws-types = { workspace = true } +bytes = { workspace = true } +http = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/codex-rs/aws-auth/src/config.rs b/codex-rs/aws-auth/src/config.rs new file mode 100644 index 000000000000..fa21d7bd3e59 --- /dev/null +++ b/codex-rs/aws-auth/src/config.rs @@ -0,0 +1,38 @@ +use aws_config::BehaviorVersion; +use aws_config::SdkConfig; +use aws_credential_types::provider::SharedCredentialsProvider; +use aws_types::region::Region; + +use crate::AwsAuthConfig; +use crate::AwsAuthError; + +pub(crate) async fn load_sdk_config(config: &AwsAuthConfig) -> Result { + if config.service.trim().is_empty() { + return Err(AwsAuthError::EmptyService); + } + + let mut loader = aws_config::defaults(BehaviorVersion::latest()); + if let Some(region) = config.region.as_ref() { + loader = loader.region(Region::new(region.clone())); + } + if let Some(profile) = config.profile.as_ref() { + loader = loader.profile_name(profile); + } + + Ok(loader.load().await) +} + +pub(crate) fn credentials_provider( + sdk_config: &SdkConfig, +) -> Result { + sdk_config + .credentials_provider() + .ok_or(AwsAuthError::MissingCredentialsProvider) +} + +pub(crate) fn resolved_region(sdk_config: &SdkConfig) -> Result { + sdk_config + .region() + .map(ToString::to_string) + .ok_or(AwsAuthError::MissingRegion) +} diff --git a/codex-rs/aws-auth/src/lib.rs b/codex-rs/aws-auth/src/lib.rs new file mode 100644 index 000000000000..c1245d201287 --- /dev/null +++ b/codex-rs/aws-auth/src/lib.rs @@ -0,0 +1,259 @@ +mod config; +mod signing; + +use std::time::SystemTime; + +use aws_credential_types::provider::ProvideCredentials; +use aws_credential_types::provider::SharedCredentialsProvider; +use bytes::Bytes; +use http::HeaderMap; +use http::Method; +use thiserror::Error; + +/// AWS auth configuration used to resolve credentials and sign requests. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AwsAuthConfig { + pub region: Option, + pub profile: Option, + pub service: String, +} + +/// Generic HTTP request shape consumed by SigV4 signing. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AwsRequestToSign { + pub method: Method, + pub url: String, + pub headers: HeaderMap, + pub body: Bytes, +} + +/// Signed request parts returned to the caller. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AwsSignedRequest { + pub url: String, + pub headers: HeaderMap, +} + +/// Errors returned by credential loading or SigV4 signing. +#[derive(Debug, Error)] +pub enum AwsAuthError { + #[error("AWS service name must not be empty")] + EmptyService, + #[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 credentials: {0}")] + Credentials(#[from] aws_credential_types::provider::error::CredentialsError), + #[error("request URL is not a valid URI: {0}")] + InvalidUri(#[source] http::uri::InvalidUri), + #[error("failed to construct HTTP request for signing: {0}")] + BuildHttpRequest(#[source] http::Error), + #[error("request contains a non-UTF8 header value: {0}")] + InvalidHeaderValue(#[source] http::header::ToStrError), + #[error("failed to build signable request: {0}")] + SigningRequest(#[source] aws_sigv4::http_request::SigningError), + #[error("failed to build SigV4 signing params: {0}")] + SigningParams(String), + #[error("SigV4 signing failed: {0}")] + SigningFailure(#[source] aws_sigv4::http_request::SigningError), +} + +/// Loaded AWS auth context that can sign outbound HTTP requests. +#[derive(Clone)] +pub struct AwsAuthContext { + credentials_provider: SharedCredentialsProvider, + region: String, + service: String, +} + +impl std::fmt::Debug for AwsAuthContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AwsAuthContext") + .field("region", &self.region) + .field("service", &self.service) + .finish_non_exhaustive() + } +} + +impl AwsAuthContext { + pub async fn load(config: AwsAuthConfig) -> Result { + let sdk_config = config::load_sdk_config(&config).await?; + let credentials_provider = config::credentials_provider(&sdk_config)?; + let region = config + .region + .clone() + .unwrap_or(config::resolved_region(&sdk_config)?); + + Ok(Self { + credentials_provider, + region, + service: config.service.trim().to_string(), + }) + } + + pub fn region(&self) -> &str { + &self.region + } + + pub fn service(&self) -> &str { + &self.service + } + + pub async fn sign(&self, request: AwsRequestToSign) -> Result { + self.sign_at(request, SystemTime::now()).await + } + + async fn sign_at( + &self, + request: AwsRequestToSign, + time: SystemTime, + ) -> Result { + let credentials = self.credentials_provider.provide_credentials().await?; + signing::sign_request(&credentials, &self.region, &self.service, request, time) + } +} + +impl AwsAuthError { + /// Returns whether retrying the outbound request can reasonably recover from this auth error. + pub fn is_retryable(&self) -> bool { + match self { + AwsAuthError::Credentials(error) => matches!( + error, + aws_credential_types::provider::error::CredentialsError::CredentialsNotLoaded(_) + | aws_credential_types::provider::error::CredentialsError::ProviderTimedOut(_) + | aws_credential_types::provider::error::CredentialsError::ProviderError(_) + | aws_credential_types::provider::error::CredentialsError::Unhandled(_) + ), + AwsAuthError::EmptyService + | AwsAuthError::MissingCredentialsProvider + | AwsAuthError::MissingRegion + | AwsAuthError::InvalidUri(_) + | AwsAuthError::BuildHttpRequest(_) + | AwsAuthError::InvalidHeaderValue(_) + | AwsAuthError::SigningRequest(_) + | AwsAuthError::SigningParams(_) + | AwsAuthError::SigningFailure(_) => false, + } + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + use std::time::UNIX_EPOCH; + + use aws_credential_types::Credentials; + use aws_credential_types::provider::error::CredentialsError; + use pretty_assertions::assert_eq; + + use super::*; + + fn test_context(session_token: Option<&str>) -> AwsAuthContext { + AwsAuthContext { + credentials_provider: SharedCredentialsProvider::new(Credentials::new( + "AKIDEXAMPLE", + "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + session_token.map(str::to_string), + /*expires_after*/ None, + "unit-test", + )), + region: "us-east-1".to_string(), + service: "bedrock".to_string(), + } + } + + fn test_request() -> AwsRequestToSign { + let mut headers = HeaderMap::new(); + headers.insert( + http::header::CONTENT_TYPE, + http::HeaderValue::from_static("application/json"), + ); + headers.insert("x-test-header", http::HeaderValue::from_static("present")); + AwsRequestToSign { + method: Method::POST, + url: "https://bedrock-runtime.us-east-1.amazonaws.com/v1/responses".to_string(), + headers, + body: Bytes::from_static(br#"{"model":"openai.gpt-oss-120b-1:0"}"#), + } + } + + #[tokio::test] + async fn sign_adds_sigv4_headers_and_preserves_existing_headers() { + let signed = test_context(/*session_token*/ None) + .sign_at( + test_request(), + UNIX_EPOCH + Duration::from_secs(1_700_000_000), + ) + .await + .expect("request should sign"); + + assert_eq!( + signing::header_value(&signed.headers, http::header::CONTENT_TYPE.as_str()), + Some("application/json".to_string()) + ); + assert_eq!( + signing::header_value(&signed.headers, "x-test-header"), + Some("present".to_string()) + ); + assert_eq!( + signed.url, + "https://bedrock-runtime.us-east-1.amazonaws.com/v1/responses" + ); + assert!( + signing::header_value(&signed.headers, http::header::AUTHORIZATION.as_str()) + .is_some_and(|value| value.starts_with("AWS4-HMAC-SHA256 ")) + ); + assert!(signing::header_value(&signed.headers, "x-amz-date").is_some()); + } + + #[test] + fn credentials_provider_failures_are_retryable() { + assert!( + AwsAuthError::Credentials(CredentialsError::provider_error("temporarily unavailable")) + .is_retryable() + ); + assert!( + AwsAuthError::Credentials(CredentialsError::provider_timed_out(Duration::from_secs(1))) + .is_retryable() + ); + } + + #[test] + fn deterministic_aws_auth_errors_are_not_retryable() { + assert!(!AwsAuthError::EmptyService.is_retryable()); + assert!( + !AwsAuthError::Credentials(CredentialsError::invalid_configuration("bad profile")) + .is_retryable() + ); + } + + #[tokio::test] + async fn sign_includes_session_token_when_credentials_have_one() { + let signed = test_context(Some("session-token")) + .sign_at( + test_request(), + UNIX_EPOCH + Duration::from_secs(1_700_000_000), + ) + .await + .expect("request should sign"); + + assert_eq!( + signing::header_value(&signed.headers, "x-amz-security-token"), + Some("session-token".to_string()) + ); + } + + #[tokio::test] + async fn load_rejects_empty_service_name() { + let err = AwsAuthContext::load(AwsAuthConfig { + region: Some("us-east-1".to_string()), + profile: None, + service: " ".to_string(), + }) + .await + .expect_err("empty service should be rejected"); + + assert_eq!(err.to_string(), "AWS service name must not be empty"); + } +} diff --git a/codex-rs/aws-auth/src/signing.rs b/codex-rs/aws-auth/src/signing.rs new file mode 100644 index 000000000000..ac3d3fd30771 --- /dev/null +++ b/codex-rs/aws-auth/src/signing.rs @@ -0,0 +1,76 @@ +use std::str::FromStr; +use std::time::SystemTime; + +use aws_credential_types::Credentials; +use aws_sigv4::http_request::SignableBody; +use aws_sigv4::http_request::SignableRequest; +use aws_sigv4::http_request::SigningSettings; +use aws_sigv4::http_request::sign; +use aws_sigv4::sign::v4; +use http::Request; +use http::Uri; + +use crate::AwsAuthError; +use crate::AwsRequestToSign; +use crate::AwsSignedRequest; + +pub(crate) fn sign_request( + credentials: &Credentials, + region: &str, + service: &str, + request: AwsRequestToSign, + time: SystemTime, +) -> Result { + let signable_headers = request + .headers + .iter() + .map(|(name, value)| { + Ok::<_, AwsAuthError>(( + name.as_str(), + value.to_str().map_err(AwsAuthError::InvalidHeaderValue)?, + )) + }) + .collect::, _>>()?; + let signable_request = SignableRequest::new( + request.method.as_str(), + request.url.as_str(), + signable_headers.into_iter(), + SignableBody::Bytes(request.body.as_ref()), + ) + .map_err(AwsAuthError::SigningRequest)?; + let identity = credentials.clone().into(); + + let signing_params = v4::SigningParams::builder() + .identity(&identity) + .region(region) + .name(service) + .time(time) + .settings(SigningSettings::default()) + .build() + .map_err(|err| AwsAuthError::SigningParams(err.to_string()))?; + let (instructions, _signature) = sign(signable_request, &signing_params.into()) + .map_err(AwsAuthError::SigningFailure)? + .into_parts(); + + let uri = Uri::from_str(&request.url).map_err(AwsAuthError::InvalidUri)?; + let mut http_request = Request::builder() + .method(request.method) + .uri(uri) + .body(()) + .map_err(AwsAuthError::BuildHttpRequest)?; + *http_request.headers_mut() = request.headers; + instructions.apply_to_request_http1x(&mut http_request); + + Ok(AwsSignedRequest { + url: http_request.uri().to_string(), + headers: http_request.headers().clone(), + }) +} + +#[cfg(test)] +pub(crate) fn header_value(headers: &http::HeaderMap, name: &str) -> Option { + headers + .get(name) + .and_then(|value| value.to_str().ok()) + .map(str::to_string) +} diff --git a/codex-rs/codex-api/src/auth.rs b/codex-rs/codex-api/src/auth.rs index d84028a2cec0..5d5ececf51e7 100644 --- a/codex-rs/codex-api/src/auth.rs +++ b/codex-rs/codex-api/src/auth.rs @@ -1,13 +1,53 @@ +use async_trait::async_trait; +use codex_client::Request; +use codex_client::TransportError; use http::HeaderMap; use std::sync::Arc; -/// Adds authentication headers to API requests. +/// Error returned while applying authentication to an outbound request. +#[derive(Debug, thiserror::Error)] +pub enum AuthError { + #[error("request auth build error: {0}")] + Build(String), + #[error("transient auth error: {0}")] + Transient(String), +} + +impl From for TransportError { + fn from(error: AuthError) -> Self { + match error { + AuthError::Build(message) => TransportError::Build(message), + AuthError::Transient(message) => TransportError::Network(message), + } + } +} + +/// Applies authentication to API requests. /// -/// Implementations should be cheap and non-blocking; any asynchronous -/// refresh or I/O should be handled by higher layers before requests -/// reach this interface. +/// Header-only providers can implement `add_auth_headers`; providers that sign +/// complete requests can override `apply_auth`. +#[async_trait] pub trait AuthProvider: Send + Sync { + /// Adds any auth headers that are available without request body access. + /// + /// Implementations should be cheap and non-blocking. This method is also + /// used by telemetry and non-HTTP request paths. fn add_auth_headers(&self, headers: &mut HeaderMap); + + /// Whether Responses requests should include Codex's legacy `session_id` header. + fn should_send_legacy_conversation_header(&self) -> bool { + true + } + + /// Applies auth to a complete outbound request. + /// + /// Header-only auth providers can rely on the default implementation. + /// Request-signing providers can override this to inspect the final URL, + /// headers, and body bytes before the transport sends the request. + async fn apply_auth(&self, mut request: Request) -> Result { + self.add_auth_headers(&mut request.headers); + Ok(request) + } } /// Shared auth handle passed through API clients. diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs index 17b478d1fd77..a8742c09793f 100644 --- a/codex-rs/codex-api/src/endpoint/responses.rs +++ b/codex-rs/codex-api/src/endpoint/responses.rs @@ -89,7 +89,9 @@ impl ResponsesClient { if let Some(ref conv_id) = conversation_id { insert_header(&mut headers, "x-client-request-id", conv_id); } - headers.extend(build_conversation_headers(conversation_id)); + if self.session.should_send_legacy_conversation_header() { + headers.extend(build_conversation_headers(conversation_id)); + } if let Some(subagent) = subagent_header(&session_source) { insert_header(&mut headers, "x-openai-subagent", &subagent); } diff --git a/codex-rs/codex-api/src/endpoint/session.rs b/codex-rs/codex-api/src/endpoint/session.rs index 5c9ec315935d..851e877af89b 100644 --- a/codex-rs/codex-api/src/endpoint/session.rs +++ b/codex-rs/codex-api/src/endpoint/session.rs @@ -8,6 +8,7 @@ use codex_client::RequestBody; use codex_client::RequestTelemetry; use codex_client::Response; use codex_client::StreamResponse; +use codex_client::TransportError; use http::HeaderMap; use http::Method; use serde_json::Value; @@ -43,6 +44,10 @@ impl EndpointSession { &self.provider } + pub(crate) fn should_send_legacy_conversation_header(&self) -> bool { + self.auth.should_send_legacy_conversation_header() + } + fn make_request( &self, method: &Method, @@ -55,7 +60,6 @@ impl EndpointSession { if let Some(body) = body { req.body = Some(RequestBody::Json(body.clone())); } - self.auth.add_auth_headers(&mut req.headers); req } @@ -97,7 +101,14 @@ impl EndpointSession { self.provider.retry.to_policy(), self.request_telemetry.clone(), make_request, - |req| self.transport.execute(req), + |req| { + let auth = self.auth.clone(); + let transport = &self.transport; + async move { + let req = auth.apply_auth(req).await.map_err(TransportError::from)?; + transport.execute(req).await + } + }, ) .await?; @@ -131,7 +142,14 @@ impl EndpointSession { self.provider.retry.to_policy(), self.request_telemetry.clone(), make_request, - |req| self.transport.stream(req), + |req| { + let auth = self.auth.clone(); + let transport = &self.transport; + async move { + let req = auth.apply_auth(req).await.map_err(TransportError::from)?; + transport.stream(req).await + } + }, ) .await?; diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs index d92bb5b35510..0b8aee266b0b 100644 --- a/codex-rs/codex-api/src/lib.rs +++ b/codex-rs/codex-api/src/lib.rs @@ -16,6 +16,7 @@ pub use codex_client::ReqwestTransport; pub use codex_client::TransportError; pub use crate::api_bridge::map_api_error; +pub use crate::auth::AuthError; pub use crate::auth::AuthHeaderTelemetry; pub use crate::auth::AuthProvider; pub use crate::auth::SharedAuthProvider; diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 3184544aa7b2..b7c29069d619 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -5,6 +5,8 @@ use std::time::Duration; use anyhow::Result; use async_trait::async_trait; use bytes::Bytes; +use codex_api::ApiError; +use codex_api::AuthError; use codex_api::AuthProvider; use codex_api::Compression; use codex_api::Provider; @@ -94,6 +96,17 @@ impl AuthProvider for NoAuth { fn add_auth_headers(&self, _headers: &mut HeaderMap) {} } +#[derive(Clone, Default)] +struct NoLegacyConversationHeaderAuth; + +impl AuthProvider for NoLegacyConversationHeaderAuth { + fn add_auth_headers(&self, _headers: &mut HeaderMap) {} + + fn should_send_legacy_conversation_header(&self) -> bool { + false + } +} + #[derive(Clone)] struct StaticAuth { token: String, @@ -164,6 +177,59 @@ impl FlakyTransport { } } +#[derive(Clone)] +struct FailsOnceAuth { + attempts: Arc>, + error: Arc, +} + +impl FailsOnceAuth { + fn transient() -> Self { + Self { + attempts: Arc::new(Mutex::new(0)), + error: Arc::new(AuthError::Transient( + "sts temporarily unavailable".to_string(), + )), + } + } + + fn build() -> Self { + Self { + attempts: Arc::new(Mutex::new(0)), + error: Arc::new(AuthError::Build("invalid auth configuration".to_string())), + } + } + + fn attempts(&self) -> i64 { + *self + .attempts + .lock() + .unwrap_or_else(|err| panic!("mutex poisoned: {err}")) + } +} + +#[async_trait] +impl AuthProvider for FailsOnceAuth { + fn add_auth_headers(&self, _headers: &mut HeaderMap) {} + + async fn apply_auth(&self, request: Request) -> Result { + let mut attempts = self + .attempts + .lock() + .unwrap_or_else(|err| panic!("mutex poisoned: {err}")); + *attempts += 1; + + if *attempts == 1 { + return match self.error.as_ref() { + AuthError::Build(message) => Err(AuthError::Build(message.clone())), + AuthError::Transient(message) => Err(AuthError::Transient(message.clone())), + }; + } + + Ok(request) + } +} + #[async_trait] impl HttpTransport for FlakyTransport { async fn execute(&self, _req: Request) -> Result { @@ -296,6 +362,65 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { Ok(()) } +#[tokio::test] +async fn streaming_client_retries_on_transient_auth_error() -> Result<()> { + let state = RecordingState::default(); + let transport = RecordingTransport::new(state.clone()); + let auth = FailsOnceAuth::transient(); + + let mut provider = provider("openai"); + provider.retry.max_attempts = 2; + + let client = ResponsesClient::new(transport, provider, Arc::new(auth.clone())); + let body = serde_json::json!({ "model": "gpt-test" }); + let _stream = client + .stream( + body, + HeaderMap::new(), + Compression::None, + /*turn_state*/ None, + ) + .await?; + + assert_eq!(auth.attempts(), 2); + assert_eq!(state.take_stream_requests().len(), 1); + Ok(()) +} + +#[tokio::test] +async fn streaming_client_does_not_retry_auth_build_error() -> Result<()> { + let state = RecordingState::default(); + let transport = RecordingTransport::new(state.clone()); + let auth = FailsOnceAuth::build(); + + let mut provider = provider("openai"); + provider.retry.max_attempts = 2; + + let client = ResponsesClient::new(transport, provider, Arc::new(auth.clone())); + let body = serde_json::json!({ "model": "gpt-test" }); + let result = client + .stream( + body, + HeaderMap::new(), + Compression::None, + /*turn_state*/ None, + ) + .await; + let err = match result { + Ok(_) => panic!("auth build errors should fail without retry"), + Err(err) => err, + }; + + assert!(matches!( + err, + ApiError::Transport(TransportError::Build(message)) + if message == "invalid auth configuration" + )); + assert_eq!(auth.attempts(), 1); + assert_eq!(state.take_stream_requests().len(), 0); + Ok(()) +} + #[tokio::test] async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { let state = RecordingState::default(); @@ -373,3 +498,58 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn responses_client_can_omit_legacy_conversation_header() -> Result<()> { + let state = RecordingState::default(); + let transport = RecordingTransport::new(state.clone()); + let client = ResponsesClient::new( + transport, + provider("external"), + Arc::new(NoLegacyConversationHeaderAuth), + ); + + let request = ResponsesApiRequest { + model: "gpt-test".into(), + instructions: "Say hi".into(), + input: Vec::new(), + tools: Vec::new(), + tool_choice: "auto".into(), + parallel_tool_calls: false, + reasoning: None, + store: false, + stream: true, + include: Vec::new(), + service_tier: None, + prompt_cache_key: None, + text: None, + client_metadata: None, + }; + + let _stream = client + .stream_request( + request, + ResponsesOptions { + conversation_id: Some("sess_123".into()), + compression: Compression::None, + ..Default::default() + }, + ) + .await?; + + let requests = state.take_stream_requests(); + assert_eq!(requests.len(), 1); + let req = &requests[0]; + + assert_eq!( + req.headers + .get("x-client-request-id") + .and_then(|v| v.to_str().ok()), + Some("sess_123") + ); + assert_eq!( + req.headers.get("session_id").and_then(|v| v.to_str().ok()), + None + ); + Ok(()) +} diff --git a/codex-rs/codex-client/src/request.rs b/codex-rs/codex-client/src/request.rs index 261bb1cf5023..977eb4beb53c 100644 --- a/codex-rs/codex-client/src/request.rs +++ b/codex-rs/codex-client/src/request.rs @@ -1,6 +1,7 @@ use bytes::Bytes; use http::Method; use reqwest::header::HeaderMap; +use reqwest::header::HeaderValue; use serde::Serialize; use serde_json::Value; use std::time::Duration; @@ -63,6 +64,131 @@ impl Request { self.compression = compression; self } + + /// Convert the request body into the exact bytes that will be sent. + /// + /// Auth schemes such as AWS SigV4 need to sign the final body bytes, including + /// compression and content headers. Calling this method is idempotent for + /// already-finalized raw request bodies. + pub fn prepare_body_for_send(&mut self) -> Result { + match self.body.take() { + Some(RequestBody::Raw(raw_body)) => { + if self.compression != RequestCompression::None { + self.body = Some(RequestBody::Raw(raw_body)); + return Err("request compression cannot be used with raw bodies".to_string()); + } + let body = raw_body.clone(); + self.body = Some(RequestBody::Raw(raw_body)); + Ok(body) + } + Some(RequestBody::Json(body)) => { + let json = serde_json::to_vec(&body).map_err(|err| err.to_string())?; + let bytes = if self.compression != RequestCompression::None { + if self.headers.contains_key(http::header::CONTENT_ENCODING) { + self.body = Some(RequestBody::Json(body)); + return Err( + "request compression was requested but content-encoding is already set" + .to_string(), + ); + } + + let pre_compression_bytes = json.len(); + let compression_start = std::time::Instant::now(); + let (compressed, content_encoding) = match self.compression { + RequestCompression::None => unreachable!("guarded by compression != None"), + RequestCompression::Zstd => ( + zstd::stream::encode_all(std::io::Cursor::new(json), 3) + .map_err(|err| err.to_string())?, + HeaderValue::from_static("zstd"), + ), + }; + let post_compression_bytes = compressed.len(); + let compression_duration = compression_start.elapsed(); + + self.headers + .insert(http::header::CONTENT_ENCODING, content_encoding); + + tracing::debug!( + pre_compression_bytes, + post_compression_bytes, + compression_duration_ms = compression_duration.as_millis(), + "Compressed request body with zstd" + ); + + compressed + } else { + json + }; + + if !self.headers.contains_key(http::header::CONTENT_TYPE) { + self.headers.insert( + http::header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ); + } + + self.compression = RequestCompression::None; + let bytes = Bytes::from(bytes); + self.body = Some(RequestBody::Raw(bytes.clone())); + Ok(bytes) + } + None => { + self.compression = RequestCompression::None; + Ok(Bytes::new()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use http::HeaderValue; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[test] + fn prepare_body_for_send_serializes_json_and_sets_content_type() { + let mut request = + Request::new(Method::POST, "https://example.com/v1/responses".to_string()) + .with_json(&json!({"model": "test-model"})); + + let body = request + .prepare_body_for_send() + .expect("body should prepare"); + + assert_eq!(body, Bytes::from_static(br#"{"model":"test-model"}"#)); + assert_eq!( + request + .headers + .get(http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()), + Some("application/json") + ); + assert_eq!(request.compression, RequestCompression::None); + assert_eq!(request.body, Some(RequestBody::Raw(body))); + } + + #[test] + fn prepare_body_for_send_rejects_existing_content_encoding_when_compressing() { + let mut request = + Request::new(Method::POST, "https://example.com/v1/responses".to_string()) + .with_json(&json!({"model": "test-model"})) + .with_compression(RequestCompression::Zstd); + request.headers.insert( + http::header::CONTENT_ENCODING, + HeaderValue::from_static("gzip"), + ); + + let err = request + .prepare_body_for_send() + .expect_err("conflicting content-encoding should fail"); + + assert_eq!( + err, + "request compression was requested but content-encoding is already set" + ); + } } #[derive(Debug, Clone)] diff --git a/codex-rs/codex-client/src/transport.rs b/codex-rs/codex-client/src/transport.rs index 590379003cb0..323a77b844c8 100644 --- a/codex-rs/codex-client/src/transport.rs +++ b/codex-rs/codex-client/src/transport.rs @@ -3,7 +3,6 @@ use crate::default_client::CodexRequestBuilder; use crate::error::TransportError; use crate::request::Request; use crate::request::RequestBody; -use crate::request::RequestCompression; use crate::request::Response; use async_trait::async_trait; use bytes::Bytes; @@ -42,13 +41,15 @@ impl ReqwestTransport { } } - fn build(&self, req: Request) -> Result { + fn build(&self, mut req: Request) -> Result { + req.prepare_body_for_send().map_err(TransportError::Build)?; + let Request { method, url, - mut headers, + headers, body, - compression, + compression: _, timeout, } = req; @@ -63,57 +64,10 @@ impl ReqwestTransport { match body { Some(RequestBody::Raw(raw_body)) => { - if compression != RequestCompression::None { - return Err(TransportError::Build( - "request compression cannot be used with raw bodies".to_string(), - )); - } builder = builder.headers(headers).body(raw_body); } Some(RequestBody::Json(body)) => { - if compression != RequestCompression::None { - if headers.contains_key(http::header::CONTENT_ENCODING) { - return Err(TransportError::Build( - "request compression was requested but content-encoding is already set" - .to_string(), - )); - } - - let json = serde_json::to_vec(&body) - .map_err(|err| TransportError::Build(err.to_string()))?; - let pre_compression_bytes = json.len(); - let compression_start = std::time::Instant::now(); - let (compressed, content_encoding) = match compression { - RequestCompression::None => unreachable!("guarded by compression != None"), - RequestCompression::Zstd => ( - zstd::stream::encode_all(std::io::Cursor::new(json), 3) - .map_err(|err| TransportError::Build(err.to_string()))?, - http::HeaderValue::from_static("zstd"), - ), - }; - let post_compression_bytes = compressed.len(); - let compression_duration = compression_start.elapsed(); - - // Ensure the server knows to unpack the request body. - headers.insert(http::header::CONTENT_ENCODING, content_encoding); - if !headers.contains_key(http::header::CONTENT_TYPE) { - headers.insert( - http::header::CONTENT_TYPE, - http::HeaderValue::from_static("application/json"), - ); - } - - tracing::info!( - pre_compression_bytes, - post_compression_bytes, - compression_duration_ms = compression_duration.as_millis(), - "Compressed request body with zstd" - ); - - builder = builder.headers(headers).body(compressed); - } else { - builder = builder.headers(headers).json(&body); - } + builder = builder.headers(headers).json(&body); } None => { builder = builder.headers(headers); diff --git a/codex-rs/model-provider/Cargo.toml b/codex-rs/model-provider/Cargo.toml index c904d1bbfa51..ad8cad4e81f5 100644 --- a/codex-rs/model-provider/Cargo.toml +++ b/codex-rs/model-provider/Cargo.toml @@ -15,10 +15,13 @@ workspace = true [dependencies] async-trait = { workspace = true } codex-api = { workspace = true } +codex-aws-auth = { workspace = true } +codex-client = { workspace = true } codex-login = { workspace = true } codex-model-provider-info = { workspace = true } codex-protocol = { workspace = true } http = { workspace = true } +tokio = { workspace = true, features = ["sync"] } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/model-provider/src/auth.rs b/codex-rs/model-provider/src/auth.rs index 64640dcc960e..957677654cac 100644 --- a/codex-rs/model-provider/src/auth.rs +++ b/codex-rs/model-provider/src/auth.rs @@ -1,10 +1,12 @@ use std::sync::Arc; use codex_api::SharedAuthProvider; +use codex_aws_auth::AwsAuthConfig; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_model_provider_info::ModelProviderInfo; +use crate::aws_auth_provider::AwsSigV4AuthProvider; use crate::bearer_auth_provider::BearerAuthProvider; /// Returns the provider-scoped auth manager when this provider uses command-backed auth. @@ -14,6 +16,10 @@ pub(crate) fn auth_manager_for_provider( auth_manager: Option>, provider: &ModelProviderInfo, ) -> Option> { + if provider.aws.is_some() { + return None; + } + match provider.auth.clone() { Some(config) => Some(AuthManager::external_bearer_only(config)), None => auth_manager, @@ -60,5 +66,13 @@ pub(crate) fn resolve_provider_auth( auth: Option<&CodexAuth>, provider: &ModelProviderInfo, ) -> codex_protocol::error::Result { + if let Some(aws) = provider.aws.as_ref() { + return Ok(Arc::new(AwsSigV4AuthProvider::new(AwsAuthConfig { + region: aws.region.clone(), + profile: aws.profile.clone(), + service: aws.service_name().to_string(), + }))); + } + Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?)) } diff --git a/codex-rs/model-provider/src/aws_auth_provider.rs b/codex-rs/model-provider/src/aws_auth_provider.rs new file mode 100644 index 000000000000..b2bea3d0dfd1 --- /dev/null +++ b/codex-rs/model-provider/src/aws_auth_provider.rs @@ -0,0 +1,88 @@ +use codex_api::AuthError; +use codex_api::AuthProvider; +use codex_aws_auth::AwsAuthConfig; +use codex_aws_auth::AwsAuthContext; +use codex_aws_auth::AwsAuthError; +use codex_aws_auth::AwsRequestToSign; +use codex_client::Request; +use http::HeaderMap; +use tokio::sync::OnceCell; + +/// AWS SigV4 auth provider for OpenAI-compatible model-provider requests. +#[derive(Debug)] +pub(crate) struct AwsSigV4AuthProvider { + config: AwsAuthConfig, + context: OnceCell, +} + +impl AwsSigV4AuthProvider { + pub(crate) fn new(config: AwsAuthConfig) -> Self { + Self { + config, + context: OnceCell::new(), + } + } + + async fn context(&self) -> Result<&AwsAuthContext, AuthError> { + self.context + .get_or_try_init(|| AwsAuthContext::load(self.config.clone())) + .await + .map_err(aws_auth_error_to_auth_error) + } +} + +fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError { + if error.is_retryable() { + AuthError::Transient(error.to_string()) + } else { + AuthError::Build(error.to_string()) + } +} + +#[async_trait::async_trait] +impl AuthProvider for AwsSigV4AuthProvider { + fn add_auth_headers(&self, _headers: &mut HeaderMap) {} + + /// AWS SigV4 requests are sent to OpenAI-compatible provider endpoints, not + /// Codex's legacy Responses backend, so avoid signing and forwarding the + /// Codex-private `session_id` compatibility header. + fn should_send_legacy_conversation_header(&self) -> bool { + false + } + + async fn apply_auth(&self, mut request: Request) -> Result { + let body = request.prepare_body_for_send().map_err(AuthError::Build)?; + let context = self.context().await?; + let signed = context + .sign(AwsRequestToSign { + method: request.method.clone(), + url: request.url.clone(), + headers: request.headers.clone(), + body, + }) + .await + .map_err(aws_auth_error_to_auth_error)?; + + request.url = signed.url; + request.headers = signed.headers; + Ok(request) + } +} + +#[cfg(test)] +mod tests { + use codex_api::AuthProvider; + + use super::*; + + #[test] + fn aws_sigv4_auth_disables_legacy_conversation_header() { + let provider = AwsSigV4AuthProvider::new(AwsAuthConfig { + region: Some("us-east-1".to_string()), + profile: Some("codex-bedrock".to_string()), + service: "bedrock-mantle".to_string(), + }); + + assert!(!provider.should_send_legacy_conversation_header()); + } +} diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs index 1874f37e31a2..f69b89a3816e 100644 --- a/codex-rs/model-provider/src/lib.rs +++ b/codex-rs/model-provider/src/lib.rs @@ -1,4 +1,5 @@ mod auth; +mod aws_auth_provider; mod bearer_auth_provider; mod provider; diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index b0a5a30f4e01..63d1bcff3d98 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -89,6 +89,7 @@ impl ModelProvider for ConfiguredModelProvider { mod tests { use std::num::NonZeroU64; + use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_protocol::config_types::ModelProviderAuthInfo; use super::*; @@ -123,4 +124,24 @@ mod tests { assert!(auth_manager.has_external_auth()); } + + #[test] + fn create_model_provider_does_not_use_openai_auth_manager_for_aws_provider() { + let provider = create_model_provider( + ModelProviderInfo { + aws: Some(ModelProviderAwsAuthInfo { + profile: Some("codex-bedrock".to_string()), + region: Some("us-east-1".to_string()), + service: None, + }), + requires_openai_auth: false, + ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) + }, + Some(AuthManager::from_auth_for_testing(CodexAuth::from_api_key( + "openai-api-key", + ))), + ); + + assert!(provider.auth_manager().is_none()); + } } From 05673f5e289f12a19a658ad0e9975a823c6afd78 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 20 Apr 2026 16:01:39 -0700 Subject: [PATCH 04/11] changes --- codex-rs/Cargo.lock | 2 + codex-rs/aws-auth/Cargo.toml | 2 + codex-rs/aws-auth/src/config.rs | 4 - codex-rs/aws-auth/src/lib.rs | 94 +++++++- codex-rs/core/config.schema.json | 2 +- codex-rs/model-provider-info/src/lib.rs | 2 +- codex-rs/model-provider/src/auth.rs | 209 +++++++++++++++++- .../model-provider/src/aws_auth_provider.rs | 57 ++++- codex-rs/model-provider/src/provider.rs | 8 +- 9 files changed, 356 insertions(+), 24 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d78f66d55965..3ea1518a359f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2002,11 +2002,13 @@ dependencies = [ "aws-credential-types", "aws-sigv4", "aws-types", + "base64 0.22.1", "bytes", "http 1.4.0", "pretty_assertions", "thiserror 2.0.18", "tokio", + "url", ] [[package]] diff --git a/codex-rs/aws-auth/Cargo.toml b/codex-rs/aws-auth/Cargo.toml index 9e49f7bbe50d..12bcdd2708ca 100644 --- a/codex-rs/aws-auth/Cargo.toml +++ b/codex-rs/aws-auth/Cargo.toml @@ -17,9 +17,11 @@ aws-config = { workspace = true } aws-credential-types = { workspace = true } aws-sigv4 = { workspace = true } aws-types = { workspace = true } +base64 = { workspace = true } bytes = { workspace = true } http = { workspace = true } thiserror = { workspace = true } +url = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/aws-auth/src/config.rs b/codex-rs/aws-auth/src/config.rs index fa21d7bd3e59..61f77c36aa4a 100644 --- a/codex-rs/aws-auth/src/config.rs +++ b/codex-rs/aws-auth/src/config.rs @@ -1,7 +1,6 @@ use aws_config::BehaviorVersion; use aws_config::SdkConfig; use aws_credential_types::provider::SharedCredentialsProvider; -use aws_types::region::Region; use crate::AwsAuthConfig; use crate::AwsAuthError; @@ -12,9 +11,6 @@ pub(crate) async fn load_sdk_config(config: &AwsAuthConfig) -> Result, pub profile: Option, pub service: String, } @@ -43,6 +45,8 @@ pub enum AwsAuthError { MissingCredentialsProvider, #[error("AWS SDK config did not resolve a region")] MissingRegion, + #[error("Amazon Bedrock bearer token is invalid: {0}")] + InvalidBedrockBearerToken(String), #[error("failed to load AWS credentials: {0}")] Credentials(#[from] aws_credential_types::provider::error::CredentialsError), #[error("request URL is not a valid URI: {0}")] @@ -80,10 +84,7 @@ impl AwsAuthContext { pub async fn load(config: AwsAuthConfig) -> Result { let sdk_config = config::load_sdk_config(&config).await?; let credentials_provider = config::credentials_provider(&sdk_config)?; - let region = config - .region - .clone() - .unwrap_or(config::resolved_region(&sdk_config)?); + let region = config::resolved_region(&sdk_config)?; Ok(Self { credentials_provider, @@ -114,6 +115,48 @@ impl AwsAuthContext { } } +/// Extracts the AWS region embedded in an Amazon Bedrock short-term bearer token. +pub fn region_from_bedrock_bearer_token(token: &str) -> Result { + const PREFIX: &str = "bedrock-api-key-"; + + let token_body = token + .trim() + .strip_prefix(PREFIX) + .ok_or_else(|| invalid_bedrock_bearer_token("missing bedrock-api-key prefix"))?; + let encoded_token = token_body + .split_once("&Version=") + .map_or(token_body, |(encoded, _)| encoded); + let decoded = general_purpose::STANDARD + .decode(encoded_token) + .map_err(|_| invalid_bedrock_bearer_token("base64 payload could not be decoded"))?; + let decoded = String::from_utf8(decoded) + .map_err(|_| invalid_bedrock_bearer_token("decoded payload is not UTF-8"))?; + let decoded_url = if decoded.starts_with("http://") || decoded.starts_with("https://") { + decoded + } else { + format!("https://{decoded}") + }; + let url = Url::parse(&decoded_url) + .map_err(|_| invalid_bedrock_bearer_token("decoded payload is not a URL"))?; + let credential = url + .query_pairs() + .find_map(|(key, value)| (key == "X-Amz-Credential").then_some(value.into_owned())) + .ok_or_else(|| invalid_bedrock_bearer_token("missing X-Amz-Credential"))?; + let mut parts = credential.split('/'); + let _access_key = parts.next(); + let _date = parts.next(); + let region = parts + .next() + .filter(|region| !region.trim().is_empty()) + .ok_or_else(|| invalid_bedrock_bearer_token("credential scope is missing region"))?; + + Ok(region.to_string()) +} + +fn invalid_bedrock_bearer_token(message: &'static str) -> AwsAuthError { + AwsAuthError::InvalidBedrockBearerToken(message.to_string()) +} + impl AwsAuthError { /// Returns whether retrying the outbound request can reasonably recover from this auth error. pub fn is_retryable(&self) -> bool { @@ -128,6 +171,7 @@ impl AwsAuthError { AwsAuthError::EmptyService | AwsAuthError::MissingCredentialsProvider | AwsAuthError::MissingRegion + | AwsAuthError::InvalidBedrockBearerToken(_) | AwsAuthError::InvalidUri(_) | AwsAuthError::BuildHttpRequest(_) | AwsAuthError::InvalidHeaderValue(_) @@ -247,7 +291,6 @@ mod tests { #[tokio::test] async fn load_rejects_empty_service_name() { let err = AwsAuthContext::load(AwsAuthConfig { - region: Some("us-east-1".to_string()), profile: None, service: " ".to_string(), }) @@ -256,4 +299,43 @@ mod tests { assert_eq!(err.to_string(), "AWS service name must not be empty"); } + + #[test] + fn region_from_bedrock_bearer_token_reads_sigv4_credential_scope() { + let decoded = "bedrock.amazonaws.com/?Action=CallWithBearerToken&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIDEXAMPLE%2F20260420%2Fus-west-2%2Fbedrock%2Faws4_request&Version=1"; + let encoded = general_purpose::STANDARD.encode(decoded); + let token = format!("bedrock-api-key-{encoded}"); + + assert_eq!( + region_from_bedrock_bearer_token(&token).expect("token region should parse"), + "us-west-2" + ); + } + + #[test] + fn region_from_bedrock_bearer_token_accepts_unencoded_version_suffix() { + let decoded = "bedrock.amazonaws.com/?Action=CallWithBearerToken&X-Amz-Credential=AKIDEXAMPLE%2F20260420%2Feu-west-1%2Fbedrock%2Faws4_request"; + let encoded = general_purpose::STANDARD.encode(decoded); + let token = format!("bedrock-api-key-{encoded}&Version=1"); + + assert_eq!( + region_from_bedrock_bearer_token(&token).expect("token region should parse"), + "eu-west-1" + ); + } + + #[test] + fn region_from_bedrock_bearer_token_rejects_missing_credential_scope() { + let decoded = "bedrock.amazonaws.com/?Action=CallWithBearerToken"; + let encoded = general_purpose::STANDARD.encode(decoded); + let token = format!("bedrock-api-key-{encoded}"); + + let err = region_from_bedrock_bearer_token(&token) + .expect_err("missing credential scope should fail"); + + assert_eq!( + err.to_string(), + "Amazon Bedrock bearer token is invalid: missing X-Amz-Credential" + ); + } } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 7c841ea99bae..b0faea2b56d5 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2978,4 +2978,4 @@ }, "title": "ConfigToml", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs index dcf5a0a339b8..ba5ac1a863f6 100644 --- a/codex-rs/model-provider-info/src/lib.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -427,7 +427,7 @@ pub fn merge_configured_model_providers( mut model_providers: HashMap, configured_model_providers: HashMap, ) -> Result, String> { - for (key, mut provider) in configured_model_providers { + for (key, provider) in configured_model_providers { if key == AMAZON_BEDROCK_PROVIDER_ID { if let Some(profile) = provider.aws.and_then(|aws| aws.profile) && let Some(built_in) = model_providers.get_mut(AMAZON_BEDROCK_PROVIDER_ID) diff --git a/codex-rs/model-provider/src/auth.rs b/codex-rs/model-provider/src/auth.rs index 957677654cac..6e898761e1f1 100644 --- a/codex-rs/model-provider/src/auth.rs +++ b/codex-rs/model-provider/src/auth.rs @@ -2,13 +2,36 @@ use std::sync::Arc; use codex_api::SharedAuthProvider; use codex_aws_auth::AwsAuthConfig; +use codex_aws_auth::AwsAuthContext; +use codex_aws_auth::AwsAuthError; +use codex_aws_auth::region_from_bedrock_bearer_token; use codex_login::AuthManager; use codex_login::CodexAuth; +use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_model_provider_info::ModelProviderInfo; +use codex_protocol::error::CodexErr; +use crate::aws_auth_provider::AwsBedrockBearerAuthProvider; use crate::aws_auth_provider::AwsSigV4AuthProvider; use crate::bearer_auth_provider::BearerAuthProvider; +const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK"; +const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle"; +const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 12] = [ + "us-east-2", + "us-east-1", + "us-west-2", + "ap-southeast-3", + "ap-south-1", + "ap-northeast-1", + "eu-central-1", + "eu-west-1", + "eu-west-2", + "eu-south-1", + "eu-north-1", + "sa-east-1", +]; + /// Returns the provider-scoped auth manager when this provider uses command-backed auth. /// /// Providers without custom auth continue using the caller-supplied base manager, when present. @@ -62,17 +85,191 @@ fn bearer_auth_provider_from_auth( } } -pub(crate) fn resolve_provider_auth( +pub(crate) async fn resolve_provider_auth( auth: Option<&CodexAuth>, provider: &ModelProviderInfo, ) -> codex_protocol::error::Result { if let Some(aws) = provider.aws.as_ref() { - return Ok(Arc::new(AwsSigV4AuthProvider::new(AwsAuthConfig { - region: aws.region.clone(), - profile: aws.profile.clone(), - service: aws.service_name().to_string(), - }))); + return Ok(resolve_bedrock_auth(aws).await?.auth); } Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?)) } + +pub(crate) async fn resolve_api_provider( + auth: Option<&CodexAuth>, + provider: &ModelProviderInfo, +) -> codex_protocol::error::Result { + if let Some(aws) = provider.aws.as_ref() { + let region = resolve_bedrock_region(aws).await?; + let mut api_provider_info = provider.clone(); + api_provider_info.base_url = Some(bedrock_mantle_base_url(®ion)?); + return api_provider_info.to_api_provider(/*auth_mode*/ None); + } + + provider.to_api_provider(auth.map(CodexAuth::auth_mode)) +} + +struct ResolvedBedrockAuth { + auth: SharedAuthProvider, +} + +async fn resolve_bedrock_auth( + aws: &ModelProviderAwsAuthInfo, +) -> codex_protocol::error::Result { + if let Some(token) = bedrock_bearer_token_from_env() { + return resolve_bedrock_bearer_auth(token); + } + + let config = bedrock_aws_auth_config(aws); + let context = AwsAuthContext::load(config.clone()) + .await + .map_err(aws_auth_error_to_codex_error)?; + Ok(ResolvedBedrockAuth { + auth: Arc::new(AwsSigV4AuthProvider::with_context(config, context)), + }) +} + +async fn resolve_bedrock_region( + aws: &ModelProviderAwsAuthInfo, +) -> codex_protocol::error::Result { + if let Some(token) = bedrock_bearer_token_from_env() { + return region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error); + } + + let context = AwsAuthContext::load(bedrock_aws_auth_config(aws)) + .await + .map_err(aws_auth_error_to_codex_error)?; + Ok(context.region().to_string()) +} + +fn resolve_bedrock_bearer_auth( + token: String, +) -> codex_protocol::error::Result { + let _region = + region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error)?; + Ok(ResolvedBedrockAuth { + auth: Arc::new(AwsBedrockBearerAuthProvider::new(token)), + }) +} + +fn bedrock_bearer_token_from_env() -> Option { + std::env::var(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR) + .ok() + .map(|token| token.trim().to_string()) + .filter(|token| !token.is_empty()) +} + +fn bedrock_aws_auth_config(aws: &ModelProviderAwsAuthInfo) -> AwsAuthConfig { + AwsAuthConfig { + profile: aws.profile.clone(), + service: BEDROCK_MANTLE_SERVICE_NAME.to_string(), + } +} + +fn bedrock_mantle_base_url(region: &str) -> codex_protocol::error::Result { + if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) { + Ok(format!("https://bedrock-mantle.{region}.api.aws/v1")) + } else { + Err(CodexErr::Fatal(format!( + "Amazon Bedrock Mantle does not support region `{region}`" + ))) + } +} + +fn aws_auth_error_to_codex_error(error: AwsAuthError) -> CodexErr { + CodexErr::Fatal(format!("failed to resolve Amazon Bedrock auth: {error}")) +} + +#[cfg(test)] +mod tests { + use codex_model_provider_info::ModelProviderAwsAuthInfo; + use pretty_assertions::assert_eq; + + use super::*; + + fn bedrock_token_for_region(region: &str) -> String { + let encoded = match region { + "us-west-2" => { + "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZ1cy13ZXN0LTIlMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" + } + "eu-central-1" => { + "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZldS1jZW50cmFsLTElMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" + } + _ => panic!("test token fixture missing for {region}"), + }; + format!("bedrock-api-key-{encoded}") + } + + #[test] + fn bedrock_mantle_base_url_uses_region_endpoint() { + assert_eq!( + bedrock_mantle_base_url("ap-northeast-1").expect("supported region"), + "https://bedrock-mantle.ap-northeast-1.api.aws/v1" + ); + } + + #[test] + fn bedrock_mantle_base_url_rejects_unsupported_region() { + let err = bedrock_mantle_base_url("us-west-1").expect_err("unsupported region"); + + assert_eq!( + err.to_string(), + "Fatal error: Amazon Bedrock Mantle does not support region `us-west-1`" + ); + } + + #[test] + fn resolve_bedrock_bearer_auth_uses_token_region_and_header() { + let token = bedrock_token_for_region("us-west-2"); + let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); + let resolved = resolve_bedrock_bearer_auth(token).expect("bearer auth should resolve"); + let mut headers = http::HeaderMap::new(); + + resolved.auth.add_auth_headers(&mut headers); + + assert_eq!(region, "us-west-2"); + assert!( + headers + .get(http::header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.starts_with("Bearer bedrock-api-key-")) + ); + assert!(!resolved.auth.should_send_legacy_conversation_header()); + } + + #[test] + fn resolve_api_provider_for_bedrock_bearer_token_uses_token_region_endpoint() { + let token = bedrock_token_for_region("eu-central-1"); + let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); + let provider = ModelProviderInfo { + aws: Some(ModelProviderAwsAuthInfo { profile: None }), + requires_openai_auth: false, + ..ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None) + }; + let mut api_provider_info = provider; + api_provider_info.base_url = + Some(bedrock_mantle_base_url(®ion).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/v1" + ); + } + + #[test] + fn bedrock_aws_auth_config_uses_profile_and_mantle_service() { + assert_eq!( + bedrock_aws_auth_config(&ModelProviderAwsAuthInfo { + profile: Some("codex-bedrock".to_string()), + }), + AwsAuthConfig { + profile: Some("codex-bedrock".to_string()), + service: "bedrock-mantle".to_string(), + } + ); + } +} diff --git a/codex-rs/model-provider/src/aws_auth_provider.rs b/codex-rs/model-provider/src/aws_auth_provider.rs index b2bea3d0dfd1..a38b546c9785 100644 --- a/codex-rs/model-provider/src/aws_auth_provider.rs +++ b/codex-rs/model-provider/src/aws_auth_provider.rs @@ -6,6 +6,7 @@ use codex_aws_auth::AwsAuthError; use codex_aws_auth::AwsRequestToSign; use codex_client::Request; use http::HeaderMap; +use http::HeaderValue; use tokio::sync::OnceCell; /// AWS SigV4 auth provider for OpenAI-compatible model-provider requests. @@ -16,6 +17,7 @@ pub(crate) struct AwsSigV4AuthProvider { } impl AwsSigV4AuthProvider { + #[cfg(test)] pub(crate) fn new(config: AwsAuthConfig) -> Self { Self { config, @@ -23,6 +25,15 @@ impl AwsSigV4AuthProvider { } } + pub(crate) fn with_context(config: AwsAuthConfig, context: AwsAuthContext) -> Self { + let cell = OnceCell::new(); + let _ = cell.set(context); + Self { + config, + context: cell, + } + } + async fn context(&self) -> Result<&AwsAuthContext, AuthError> { self.context .get_or_try_init(|| AwsAuthContext::load(self.config.clone())) @@ -31,6 +42,35 @@ impl AwsSigV4AuthProvider { } } +/// Amazon Bedrock bearer-token auth provider for OpenAI-compatible requests. +#[derive(Debug)] +pub(crate) struct AwsBedrockBearerAuthProvider { + token: String, +} + +impl AwsBedrockBearerAuthProvider { + pub(crate) fn new(token: String) -> Self { + Self { token } + } +} + +#[async_trait::async_trait] +impl AuthProvider for AwsBedrockBearerAuthProvider { + fn add_auth_headers(&self, headers: &mut HeaderMap) { + let token = &self.token; + if let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}")) { + let _ = headers.insert(http::header::AUTHORIZATION, header); + } + } + + /// Bedrock requests are sent to OpenAI-compatible provider endpoints, not + /// Codex's legacy Responses backend, so avoid forwarding the Codex-private + /// `session_id` compatibility header. + fn should_send_legacy_conversation_header(&self) -> bool { + false + } +} + fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError { if error.is_retryable() { AuthError::Transient(error.to_string()) @@ -78,11 +118,26 @@ mod tests { #[test] fn aws_sigv4_auth_disables_legacy_conversation_header() { let provider = AwsSigV4AuthProvider::new(AwsAuthConfig { - region: Some("us-east-1".to_string()), profile: Some("codex-bedrock".to_string()), service: "bedrock-mantle".to_string(), }); assert!(!provider.should_send_legacy_conversation_header()); } + + #[test] + fn aws_bedrock_bearer_auth_adds_header_and_disables_legacy_conversation_header() { + let provider = AwsBedrockBearerAuthProvider::new("bedrock-token".to_string()); + let mut headers = HeaderMap::new(); + + provider.add_auth_headers(&mut headers); + + assert_eq!( + headers + .get(http::header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()), + Some("Bearer bedrock-token") + ); + assert!(!provider.should_send_legacy_conversation_header()); + } } diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 63d1bcff3d98..4570a357bcc7 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -8,6 +8,7 @@ use codex_login::CodexAuth; use codex_model_provider_info::ModelProviderInfo; use crate::auth::auth_manager_for_provider; +use crate::auth::resolve_api_provider; use crate::auth::resolve_provider_auth; /// Runtime provider abstraction used by model execution. @@ -34,14 +35,13 @@ pub trait ModelProvider: fmt::Debug + Send + Sync { /// Returns provider configuration adapted for the API client. async fn api_provider(&self) -> codex_protocol::error::Result { let auth = self.auth().await; - self.info() - .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode)) + resolve_api_provider(auth.as_ref(), self.info()).await } /// Returns the auth provider used to attach request credentials. async fn api_auth(&self) -> codex_protocol::error::Result { let auth = self.auth().await; - resolve_provider_auth(auth.as_ref(), self.info()) + resolve_provider_auth(auth.as_ref(), self.info()).await } } @@ -131,8 +131,6 @@ mod tests { ModelProviderInfo { aws: Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), - region: Some("us-east-1".to_string()), - service: None, }), requires_openai_auth: false, ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) From 36d8e60d136200d2550cf6902b518b8d9927603b Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 20 Apr 2026 22:03:12 -0700 Subject: [PATCH 05/11] changes --- codex-rs/codex-api/src/auth.rs | 5 -- codex-rs/codex-api/src/endpoint/responses.rs | 4 +- codex-rs/codex-api/src/endpoint/session.rs | 4 -- codex-rs/codex-api/tests/clients.rs | 66 ------------------- codex-rs/model-provider/src/auth.rs | 1 - .../model-provider/src/aws_auth_provider.rs | 35 +--------- 6 files changed, 2 insertions(+), 113 deletions(-) diff --git a/codex-rs/codex-api/src/auth.rs b/codex-rs/codex-api/src/auth.rs index 5d5ececf51e7..b7fc24a99008 100644 --- a/codex-rs/codex-api/src/auth.rs +++ b/codex-rs/codex-api/src/auth.rs @@ -34,11 +34,6 @@ pub trait AuthProvider: Send + Sync { /// used by telemetry and non-HTTP request paths. fn add_auth_headers(&self, headers: &mut HeaderMap); - /// Whether Responses requests should include Codex's legacy `session_id` header. - fn should_send_legacy_conversation_header(&self) -> bool { - true - } - /// Applies auth to a complete outbound request. /// /// Header-only auth providers can rely on the default implementation. diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs index a8742c09793f..17b478d1fd77 100644 --- a/codex-rs/codex-api/src/endpoint/responses.rs +++ b/codex-rs/codex-api/src/endpoint/responses.rs @@ -89,9 +89,7 @@ impl ResponsesClient { if let Some(ref conv_id) = conversation_id { insert_header(&mut headers, "x-client-request-id", conv_id); } - if self.session.should_send_legacy_conversation_header() { - headers.extend(build_conversation_headers(conversation_id)); - } + headers.extend(build_conversation_headers(conversation_id)); if let Some(subagent) = subagent_header(&session_source) { insert_header(&mut headers, "x-openai-subagent", &subagent); } diff --git a/codex-rs/codex-api/src/endpoint/session.rs b/codex-rs/codex-api/src/endpoint/session.rs index 851e877af89b..132c3abd90a8 100644 --- a/codex-rs/codex-api/src/endpoint/session.rs +++ b/codex-rs/codex-api/src/endpoint/session.rs @@ -44,10 +44,6 @@ impl EndpointSession { &self.provider } - pub(crate) fn should_send_legacy_conversation_header(&self) -> bool { - self.auth.should_send_legacy_conversation_header() - } - fn make_request( &self, method: &Method, diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index b7c29069d619..46f5627592b2 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -96,17 +96,6 @@ impl AuthProvider for NoAuth { fn add_auth_headers(&self, _headers: &mut HeaderMap) {} } -#[derive(Clone, Default)] -struct NoLegacyConversationHeaderAuth; - -impl AuthProvider for NoLegacyConversationHeaderAuth { - fn add_auth_headers(&self, _headers: &mut HeaderMap) {} - - fn should_send_legacy_conversation_header(&self) -> bool { - false - } -} - #[derive(Clone)] struct StaticAuth { token: String, @@ -498,58 +487,3 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { Ok(()) } - -#[tokio::test] -async fn responses_client_can_omit_legacy_conversation_header() -> Result<()> { - let state = RecordingState::default(); - let transport = RecordingTransport::new(state.clone()); - let client = ResponsesClient::new( - transport, - provider("external"), - Arc::new(NoLegacyConversationHeaderAuth), - ); - - let request = ResponsesApiRequest { - model: "gpt-test".into(), - instructions: "Say hi".into(), - input: Vec::new(), - tools: Vec::new(), - tool_choice: "auto".into(), - parallel_tool_calls: false, - reasoning: None, - store: false, - stream: true, - include: Vec::new(), - service_tier: None, - prompt_cache_key: None, - text: None, - client_metadata: None, - }; - - let _stream = client - .stream_request( - request, - ResponsesOptions { - conversation_id: Some("sess_123".into()), - compression: Compression::None, - ..Default::default() - }, - ) - .await?; - - let requests = state.take_stream_requests(); - assert_eq!(requests.len(), 1); - let req = &requests[0]; - - assert_eq!( - req.headers - .get("x-client-request-id") - .and_then(|v| v.to_str().ok()), - Some("sess_123") - ); - assert_eq!( - req.headers.get("session_id").and_then(|v| v.to_str().ok()), - None - ); - Ok(()) -} diff --git a/codex-rs/model-provider/src/auth.rs b/codex-rs/model-provider/src/auth.rs index 6e898761e1f1..d30b268c781b 100644 --- a/codex-rs/model-provider/src/auth.rs +++ b/codex-rs/model-provider/src/auth.rs @@ -235,7 +235,6 @@ mod tests { .and_then(|value| value.to_str().ok()) .is_some_and(|value| value.starts_with("Bearer bedrock-api-key-")) ); - assert!(!resolved.auth.should_send_legacy_conversation_header()); } #[test] diff --git a/codex-rs/model-provider/src/aws_auth_provider.rs b/codex-rs/model-provider/src/aws_auth_provider.rs index a38b546c9785..a9a75e537214 100644 --- a/codex-rs/model-provider/src/aws_auth_provider.rs +++ b/codex-rs/model-provider/src/aws_auth_provider.rs @@ -17,14 +17,6 @@ pub(crate) struct AwsSigV4AuthProvider { } impl AwsSigV4AuthProvider { - #[cfg(test)] - pub(crate) fn new(config: AwsAuthConfig) -> Self { - Self { - config, - context: OnceCell::new(), - } - } - pub(crate) fn with_context(config: AwsAuthConfig, context: AwsAuthContext) -> Self { let cell = OnceCell::new(); let _ = cell.set(context); @@ -62,13 +54,6 @@ impl AuthProvider for AwsBedrockBearerAuthProvider { let _ = headers.insert(http::header::AUTHORIZATION, header); } } - - /// Bedrock requests are sent to OpenAI-compatible provider endpoints, not - /// Codex's legacy Responses backend, so avoid forwarding the Codex-private - /// `session_id` compatibility header. - fn should_send_legacy_conversation_header(&self) -> bool { - false - } } fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError { @@ -83,13 +68,6 @@ fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError { impl AuthProvider for AwsSigV4AuthProvider { fn add_auth_headers(&self, _headers: &mut HeaderMap) {} - /// AWS SigV4 requests are sent to OpenAI-compatible provider endpoints, not - /// Codex's legacy Responses backend, so avoid signing and forwarding the - /// Codex-private `session_id` compatibility header. - fn should_send_legacy_conversation_header(&self) -> bool { - false - } - async fn apply_auth(&self, mut request: Request) -> Result { let body = request.prepare_body_for_send().map_err(AuthError::Build)?; let context = self.context().await?; @@ -116,17 +94,7 @@ mod tests { use super::*; #[test] - fn aws_sigv4_auth_disables_legacy_conversation_header() { - let provider = AwsSigV4AuthProvider::new(AwsAuthConfig { - profile: Some("codex-bedrock".to_string()), - service: "bedrock-mantle".to_string(), - }); - - assert!(!provider.should_send_legacy_conversation_header()); - } - - #[test] - fn aws_bedrock_bearer_auth_adds_header_and_disables_legacy_conversation_header() { + fn aws_bedrock_bearer_auth_adds_header() { let provider = AwsBedrockBearerAuthProvider::new("bedrock-token".to_string()); let mut headers = HeaderMap::new(); @@ -138,6 +106,5 @@ mod tests { .and_then(|value| value.to_str().ok()), Some("Bearer bedrock-token") ); - assert!(!provider.should_send_legacy_conversation_header()); } } From 2714225efd150f76f8458e4258818f7c78c87070 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 20 Apr 2026 22:29:19 -0700 Subject: [PATCH 06/11] changes --- .../src/amazon_bedrock_provider.rs | 217 ++++++++++++++++++ codex-rs/model-provider/src/auth.rs | 203 ---------------- .../model-provider/src/aws_auth_provider.rs | 33 +++ codex-rs/model-provider/src/lib.rs | 1 + codex-rs/model-provider/src/provider.rs | 34 ++- 5 files changed, 282 insertions(+), 206 deletions(-) create mode 100644 codex-rs/model-provider/src/amazon_bedrock_provider.rs diff --git a/codex-rs/model-provider/src/amazon_bedrock_provider.rs b/codex-rs/model-provider/src/amazon_bedrock_provider.rs new file mode 100644 index 000000000000..6d5c829cc438 --- /dev/null +++ b/codex-rs/model-provider/src/amazon_bedrock_provider.rs @@ -0,0 +1,217 @@ +use std::sync::Arc; + +use codex_api::Provider; +use codex_api::SharedAuthProvider; +use codex_aws_auth::AwsAuthConfig; +use codex_aws_auth::AwsAuthContext; +use codex_aws_auth::AwsAuthError; +use codex_aws_auth::region_from_bedrock_bearer_token; +use codex_login::AuthManager; +use codex_login::CodexAuth; +use codex_model_provider_info::ModelProviderAwsAuthInfo; +use codex_model_provider_info::ModelProviderInfo; +use codex_protocol::error::CodexErr; + +use crate::aws_auth_provider::AwsBedrockBearerAuthProvider; +use crate::aws_auth_provider::AwsSigV4AuthProvider; +use crate::provider::ModelProvider; + +const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK"; +const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle"; +const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 12] = [ + "us-east-2", + "us-east-1", + "us-west-2", + "ap-southeast-3", + "ap-south-1", + "ap-northeast-1", + "eu-central-1", + "eu-west-1", + "eu-west-2", + "eu-south-1", + "eu-north-1", + "sa-east-1", +]; + +/// 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, +} + +#[async_trait::async_trait] +impl ModelProvider for AmazonBedrockModelProvider { + fn info(&self) -> &ModelProviderInfo { + &self.info + } + + fn auth_manager(&self) -> Option> { + None + } + + async fn auth(&self) -> Option { + None + } + + async fn api_provider(&self) -> codex_protocol::error::Result { + let region = resolve_bedrock_region(&self.aws).await?; + let mut api_provider_info = self.info.clone(); + api_provider_info.base_url = Some(bedrock_mantle_base_url(®ion)?); + api_provider_info.to_api_provider(/*auth_mode*/ None) + } + + async fn api_auth(&self) -> codex_protocol::error::Result { + resolve_bedrock_auth(&self.aws).await + } +} + +async fn resolve_bedrock_auth( + aws: &ModelProviderAwsAuthInfo, +) -> codex_protocol::error::Result { + if let Some(token) = bedrock_bearer_token_from_env() { + return resolve_bedrock_bearer_auth(token); + } + + let config = bedrock_aws_auth_config(aws); + let context = AwsAuthContext::load(config.clone()) + .await + .map_err(aws_auth_error_to_codex_error)?; + Ok(Arc::new(AwsSigV4AuthProvider::with_context( + config, context, + ))) +} + +async fn resolve_bedrock_region( + aws: &ModelProviderAwsAuthInfo, +) -> codex_protocol::error::Result { + if let Some(token) = bedrock_bearer_token_from_env() { + return region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error); + } + + let context = AwsAuthContext::load(bedrock_aws_auth_config(aws)) + .await + .map_err(aws_auth_error_to_codex_error)?; + Ok(context.region().to_string()) +} + +fn resolve_bedrock_bearer_auth(token: String) -> codex_protocol::error::Result { + let _region = + region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error)?; + Ok(Arc::new(AwsBedrockBearerAuthProvider::new(token))) +} + +fn bedrock_bearer_token_from_env() -> Option { + std::env::var(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR) + .ok() + .map(|token| token.trim().to_string()) + .filter(|token| !token.is_empty()) +} + +fn bedrock_aws_auth_config(aws: &ModelProviderAwsAuthInfo) -> AwsAuthConfig { + AwsAuthConfig { + profile: aws.profile.clone(), + service: BEDROCK_MANTLE_SERVICE_NAME.to_string(), + } +} + +fn bedrock_mantle_base_url(region: &str) -> codex_protocol::error::Result { + if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) { + Ok(format!("https://bedrock-mantle.{region}.api.aws/v1")) + } else { + Err(CodexErr::Fatal(format!( + "Amazon Bedrock Mantle does not support region `{region}`" + ))) + } +} + +fn aws_auth_error_to_codex_error(error: AwsAuthError) -> CodexErr { + CodexErr::Fatal(format!("failed to resolve Amazon Bedrock auth: {error}")) +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + fn bedrock_token_for_region(region: &str) -> String { + let encoded = match region { + "us-west-2" => { + "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZ1cy13ZXN0LTIlMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" + } + "eu-central-1" => { + "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZldS1jZW50cmFsLTElMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" + } + _ => panic!("test token fixture missing for {region}"), + }; + format!("bedrock-api-key-{encoded}") + } + + #[test] + fn bedrock_mantle_base_url_uses_region_endpoint() { + assert_eq!( + bedrock_mantle_base_url("ap-northeast-1").expect("supported region"), + "https://bedrock-mantle.ap-northeast-1.api.aws/v1" + ); + } + + #[test] + fn bedrock_mantle_base_url_rejects_unsupported_region() { + let err = bedrock_mantle_base_url("us-west-1").expect_err("unsupported region"); + + assert_eq!( + err.to_string(), + "Fatal error: Amazon Bedrock Mantle does not support region `us-west-1`" + ); + } + + #[test] + fn resolve_bedrock_bearer_auth_uses_token_region_and_header() { + let token = bedrock_token_for_region("us-west-2"); + let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); + let resolved = resolve_bedrock_bearer_auth(token).expect("bearer auth should resolve"); + let mut headers = http::HeaderMap::new(); + + resolved.add_auth_headers(&mut headers); + + assert_eq!(region, "us-west-2"); + assert!( + headers + .get(http::header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.starts_with("Bearer bedrock-api-key-")) + ); + } + + #[test] + fn api_provider_for_bedrock_bearer_token_uses_token_region_endpoint() { + let token = bedrock_token_for_region("eu-central-1"); + let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); + let mut api_provider_info = + ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None); + api_provider_info.base_url = + Some(bedrock_mantle_base_url(®ion).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/v1" + ); + } + + #[test] + fn bedrock_aws_auth_config_uses_profile_and_mantle_service() { + assert_eq!( + bedrock_aws_auth_config(&ModelProviderAwsAuthInfo { + profile: Some("codex-bedrock".to_string()), + }), + AwsAuthConfig { + profile: Some("codex-bedrock".to_string()), + service: "bedrock-mantle".to_string(), + } + ); + } +} diff --git a/codex-rs/model-provider/src/auth.rs b/codex-rs/model-provider/src/auth.rs index d30b268c781b..06af52592a25 100644 --- a/codex-rs/model-provider/src/auth.rs +++ b/codex-rs/model-provider/src/auth.rs @@ -1,37 +1,12 @@ use std::sync::Arc; use codex_api::SharedAuthProvider; -use codex_aws_auth::AwsAuthConfig; -use codex_aws_auth::AwsAuthContext; -use codex_aws_auth::AwsAuthError; -use codex_aws_auth::region_from_bedrock_bearer_token; use codex_login::AuthManager; use codex_login::CodexAuth; -use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_model_provider_info::ModelProviderInfo; -use codex_protocol::error::CodexErr; -use crate::aws_auth_provider::AwsBedrockBearerAuthProvider; -use crate::aws_auth_provider::AwsSigV4AuthProvider; use crate::bearer_auth_provider::BearerAuthProvider; -const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK"; -const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle"; -const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 12] = [ - "us-east-2", - "us-east-1", - "us-west-2", - "ap-southeast-3", - "ap-south-1", - "ap-northeast-1", - "eu-central-1", - "eu-west-1", - "eu-west-2", - "eu-south-1", - "eu-north-1", - "sa-east-1", -]; - /// Returns the provider-scoped auth manager when this provider uses command-backed auth. /// /// Providers without custom auth continue using the caller-supplied base manager, when present. @@ -39,10 +14,6 @@ pub(crate) fn auth_manager_for_provider( auth_manager: Option>, provider: &ModelProviderInfo, ) -> Option> { - if provider.aws.is_some() { - return None; - } - match provider.auth.clone() { Some(config) => Some(AuthManager::external_bearer_only(config)), None => auth_manager, @@ -89,10 +60,6 @@ pub(crate) async fn resolve_provider_auth( auth: Option<&CodexAuth>, provider: &ModelProviderInfo, ) -> codex_protocol::error::Result { - if let Some(aws) = provider.aws.as_ref() { - return Ok(resolve_bedrock_auth(aws).await?.auth); - } - Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?)) } @@ -100,175 +67,5 @@ pub(crate) async fn resolve_api_provider( auth: Option<&CodexAuth>, provider: &ModelProviderInfo, ) -> codex_protocol::error::Result { - if let Some(aws) = provider.aws.as_ref() { - let region = resolve_bedrock_region(aws).await?; - let mut api_provider_info = provider.clone(); - api_provider_info.base_url = Some(bedrock_mantle_base_url(®ion)?); - return api_provider_info.to_api_provider(/*auth_mode*/ None); - } - provider.to_api_provider(auth.map(CodexAuth::auth_mode)) } - -struct ResolvedBedrockAuth { - auth: SharedAuthProvider, -} - -async fn resolve_bedrock_auth( - aws: &ModelProviderAwsAuthInfo, -) -> codex_protocol::error::Result { - if let Some(token) = bedrock_bearer_token_from_env() { - return resolve_bedrock_bearer_auth(token); - } - - let config = bedrock_aws_auth_config(aws); - let context = AwsAuthContext::load(config.clone()) - .await - .map_err(aws_auth_error_to_codex_error)?; - Ok(ResolvedBedrockAuth { - auth: Arc::new(AwsSigV4AuthProvider::with_context(config, context)), - }) -} - -async fn resolve_bedrock_region( - aws: &ModelProviderAwsAuthInfo, -) -> codex_protocol::error::Result { - if let Some(token) = bedrock_bearer_token_from_env() { - return region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error); - } - - let context = AwsAuthContext::load(bedrock_aws_auth_config(aws)) - .await - .map_err(aws_auth_error_to_codex_error)?; - Ok(context.region().to_string()) -} - -fn resolve_bedrock_bearer_auth( - token: String, -) -> codex_protocol::error::Result { - let _region = - region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error)?; - Ok(ResolvedBedrockAuth { - auth: Arc::new(AwsBedrockBearerAuthProvider::new(token)), - }) -} - -fn bedrock_bearer_token_from_env() -> Option { - std::env::var(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR) - .ok() - .map(|token| token.trim().to_string()) - .filter(|token| !token.is_empty()) -} - -fn bedrock_aws_auth_config(aws: &ModelProviderAwsAuthInfo) -> AwsAuthConfig { - AwsAuthConfig { - profile: aws.profile.clone(), - service: BEDROCK_MANTLE_SERVICE_NAME.to_string(), - } -} - -fn bedrock_mantle_base_url(region: &str) -> codex_protocol::error::Result { - if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) { - Ok(format!("https://bedrock-mantle.{region}.api.aws/v1")) - } else { - Err(CodexErr::Fatal(format!( - "Amazon Bedrock Mantle does not support region `{region}`" - ))) - } -} - -fn aws_auth_error_to_codex_error(error: AwsAuthError) -> CodexErr { - CodexErr::Fatal(format!("failed to resolve Amazon Bedrock auth: {error}")) -} - -#[cfg(test)] -mod tests { - use codex_model_provider_info::ModelProviderAwsAuthInfo; - use pretty_assertions::assert_eq; - - use super::*; - - fn bedrock_token_for_region(region: &str) -> String { - let encoded = match region { - "us-west-2" => { - "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZ1cy13ZXN0LTIlMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" - } - "eu-central-1" => { - "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZldS1jZW50cmFsLTElMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" - } - _ => panic!("test token fixture missing for {region}"), - }; - format!("bedrock-api-key-{encoded}") - } - - #[test] - fn bedrock_mantle_base_url_uses_region_endpoint() { - assert_eq!( - bedrock_mantle_base_url("ap-northeast-1").expect("supported region"), - "https://bedrock-mantle.ap-northeast-1.api.aws/v1" - ); - } - - #[test] - fn bedrock_mantle_base_url_rejects_unsupported_region() { - let err = bedrock_mantle_base_url("us-west-1").expect_err("unsupported region"); - - assert_eq!( - err.to_string(), - "Fatal error: Amazon Bedrock Mantle does not support region `us-west-1`" - ); - } - - #[test] - fn resolve_bedrock_bearer_auth_uses_token_region_and_header() { - let token = bedrock_token_for_region("us-west-2"); - let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); - let resolved = resolve_bedrock_bearer_auth(token).expect("bearer auth should resolve"); - let mut headers = http::HeaderMap::new(); - - resolved.auth.add_auth_headers(&mut headers); - - assert_eq!(region, "us-west-2"); - assert!( - headers - .get(http::header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .is_some_and(|value| value.starts_with("Bearer bedrock-api-key-")) - ); - } - - #[test] - fn resolve_api_provider_for_bedrock_bearer_token_uses_token_region_endpoint() { - let token = bedrock_token_for_region("eu-central-1"); - let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); - let provider = ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { profile: None }), - requires_openai_auth: false, - ..ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None) - }; - let mut api_provider_info = provider; - api_provider_info.base_url = - Some(bedrock_mantle_base_url(®ion).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/v1" - ); - } - - #[test] - fn bedrock_aws_auth_config_uses_profile_and_mantle_service() { - assert_eq!( - bedrock_aws_auth_config(&ModelProviderAwsAuthInfo { - profile: Some("codex-bedrock".to_string()), - }), - AwsAuthConfig { - profile: Some("codex-bedrock".to_string()), - service: "bedrock-mantle".to_string(), - } - ); - } -} diff --git a/codex-rs/model-provider/src/aws_auth_provider.rs b/codex-rs/model-provider/src/aws_auth_provider.rs index a9a75e537214..20efad8bcbf6 100644 --- a/codex-rs/model-provider/src/aws_auth_provider.rs +++ b/codex-rs/model-provider/src/aws_auth_provider.rs @@ -9,6 +9,8 @@ use http::HeaderMap; use http::HeaderValue; use tokio::sync::OnceCell; +const LEGACY_SESSION_ID_HEADER: &str = "session_id"; + /// AWS SigV4 auth provider for OpenAI-compatible model-provider requests. #[derive(Debug)] pub(crate) struct AwsSigV4AuthProvider { @@ -69,6 +71,7 @@ impl AuthProvider for AwsSigV4AuthProvider { fn add_auth_headers(&self, _headers: &mut HeaderMap) {} async fn apply_auth(&self, mut request: Request) -> Result { + remove_headers_not_preserved_by_bedrock_mantle(&mut request.headers); let body = request.prepare_body_for_send().map_err(AuthError::Build)?; let context = self.context().await?; let signed = context @@ -87,6 +90,13 @@ impl AuthProvider for AwsSigV4AuthProvider { } } +fn remove_headers_not_preserved_by_bedrock_mantle(headers: &mut HeaderMap) { + // The Bedrock Mantle front door does not preserve this legacy OpenAI header + // for SigV4 verification. Signing it makes the richer Codex agent request + // fail even though raw Responses requests work. + headers.remove(LEGACY_SESSION_ID_HEADER); +} + #[cfg(test)] mod tests { use codex_api::AuthProvider; @@ -107,4 +117,27 @@ mod tests { Some("Bearer bedrock-token") ); } + + #[test] + fn bedrock_mantle_sigv4_strips_legacy_session_id_header() { + let mut headers = HeaderMap::new(); + headers.insert( + LEGACY_SESSION_ID_HEADER, + HeaderValue::from_static("019dae79-15c3-70c3-8736-3219b8602b37"), + ); + headers.insert( + "x-client-request-id", + HeaderValue::from_static("request-id"), + ); + + remove_headers_not_preserved_by_bedrock_mantle(&mut headers); + + assert!(!headers.contains_key(LEGACY_SESSION_ID_HEADER)); + assert_eq!( + headers + .get("x-client-request-id") + .and_then(|value| value.to_str().ok()), + Some("request-id") + ); + } } diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs index f69b89a3816e..7d899f1d4041 100644 --- a/codex-rs/model-provider/src/lib.rs +++ b/codex-rs/model-provider/src/lib.rs @@ -1,3 +1,4 @@ +mod amazon_bedrock_provider; mod auth; mod aws_auth_provider; mod bearer_auth_provider; diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 4570a357bcc7..7dccf6a0bfca 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -5,8 +5,10 @@ use codex_api::Provider; use codex_api::SharedAuthProvider; use codex_login::AuthManager; use codex_login::CodexAuth; +use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_model_provider_info::ModelProviderInfo; +use crate::amazon_bedrock_provider::AmazonBedrockModelProvider; use crate::auth::auth_manager_for_provider; use crate::auth::resolve_api_provider; use crate::auth::resolve_provider_auth; @@ -53,6 +55,17 @@ pub fn create_model_provider( provider_info: ModelProviderInfo, auth_manager: Option>, ) -> SharedModelProvider { + if provider_info.is_amazon_bedrock() { + let aws = provider_info + .aws + .clone() + .unwrap_or(ModelProviderAwsAuthInfo { profile: None }); + return Arc::new(AmazonBedrockModelProvider { + info: provider_info, + aws, + }); + } + let auth_manager = auth_manager_for_provider(auth_manager, &provider_info); Arc::new(ConfiguredModelProvider { info: provider_info, @@ -126,9 +139,24 @@ mod tests { } #[test] - fn create_model_provider_does_not_use_openai_auth_manager_for_aws_provider() { + fn create_model_provider_does_not_use_openai_auth_manager_for_amazon_bedrock_provider() { + let provider = create_model_provider( + ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo { + profile: Some("codex-bedrock".to_string()), + })), + Some(AuthManager::from_auth_for_testing(CodexAuth::from_api_key( + "openai-api-key", + ))), + ); + + assert!(provider.auth_manager().is_none()); + } + + #[test] + fn create_model_provider_does_not_select_bedrock_provider_for_custom_aws_provider() { let provider = create_model_provider( ModelProviderInfo { + name: "Custom".to_string(), aws: Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), }), @@ -136,10 +164,10 @@ mod tests { ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) }, Some(AuthManager::from_auth_for_testing(CodexAuth::from_api_key( - "openai-api-key", + "custom-api-key", ))), ); - assert!(provider.auth_manager().is_none()); + assert!(provider.auth_manager().is_some()); } } From 245c651ce459cd40038fe82e13404c0b4de7bf3e Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 20 Apr 2026 23:26:51 -0700 Subject: [PATCH 07/11] changes --- codex-rs/aws-auth/src/lib.rs | 11 ++- codex-rs/codex-client/src/lib.rs | 1 + codex-rs/codex-client/src/request.rs | 74 +++++++++++-------- codex-rs/codex-client/src/transport.rs | 21 ++---- .../model-provider/src/aws_auth_provider.rs | 10 ++- 5 files changed, 68 insertions(+), 49 deletions(-) diff --git a/codex-rs/aws-auth/src/lib.rs b/codex-rs/aws-auth/src/lib.rs index 86da12bcd06b..193ffb9cc511 100644 --- a/codex-rs/aws-auth/src/lib.rs +++ b/codex-rs/aws-auth/src/lib.rs @@ -163,10 +163,8 @@ impl AwsAuthError { match self { AwsAuthError::Credentials(error) => matches!( error, - aws_credential_types::provider::error::CredentialsError::CredentialsNotLoaded(_) - | aws_credential_types::provider::error::CredentialsError::ProviderTimedOut(_) + aws_credential_types::provider::error::CredentialsError::ProviderTimedOut(_) | aws_credential_types::provider::error::CredentialsError::ProviderError(_) - | aws_credential_types::provider::error::CredentialsError::Unhandled(_) ), AwsAuthError::EmptyService | AwsAuthError::MissingCredentialsProvider @@ -266,10 +264,17 @@ mod tests { #[test] fn deterministic_aws_auth_errors_are_not_retryable() { assert!(!AwsAuthError::EmptyService.is_retryable()); + assert!( + !AwsAuthError::Credentials(CredentialsError::not_loaded_no_source()).is_retryable() + ); assert!( !AwsAuthError::Credentials(CredentialsError::invalid_configuration("bad profile")) .is_retryable() ); + assert!( + !AwsAuthError::Credentials(CredentialsError::unhandled("unexpected response")) + .is_retryable() + ); } #[tokio::test] diff --git a/codex-rs/codex-client/src/lib.rs b/codex-rs/codex-client/src/lib.rs index 7b1bf84753e2..5e2a74c723db 100644 --- a/codex-rs/codex-client/src/lib.rs +++ b/codex-rs/codex-client/src/lib.rs @@ -21,6 +21,7 @@ pub use crate::default_client::CodexHttpClient; pub use crate::default_client::CodexRequestBuilder; pub use crate::error::StreamError; pub use crate::error::TransportError; +pub use crate::request::PreparedRequestBody; pub use crate::request::Request; pub use crate::request::RequestBody; pub use crate::request::RequestCompression; diff --git a/codex-rs/codex-client/src/request.rs b/codex-rs/codex-client/src/request.rs index 977eb4beb53c..5fc076627f3f 100644 --- a/codex-rs/codex-client/src/request.rs +++ b/codex-rs/codex-client/src/request.rs @@ -28,6 +28,18 @@ impl RequestBody { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreparedRequestBody { + pub headers: HeaderMap, + pub body: Option, +} + +impl PreparedRequestBody { + pub fn body_bytes(&self) -> Bytes { + self.body.clone().unwrap_or_default() + } +} + #[derive(Debug, Clone)] pub struct Request { pub method: Method, @@ -68,24 +80,24 @@ impl Request { /// Convert the request body into the exact bytes that will be sent. /// /// Auth schemes such as AWS SigV4 need to sign the final body bytes, including - /// compression and content headers. Calling this method is idempotent for - /// already-finalized raw request bodies. - pub fn prepare_body_for_send(&mut self) -> Result { - match self.body.take() { + /// compression and content headers. Calling this method does not mutate the + /// request. + pub fn prepare_body_for_send(&self) -> Result { + let mut headers = self.headers.clone(); + match self.body.as_ref() { Some(RequestBody::Raw(raw_body)) => { if self.compression != RequestCompression::None { - self.body = Some(RequestBody::Raw(raw_body)); return Err("request compression cannot be used with raw bodies".to_string()); } - let body = raw_body.clone(); - self.body = Some(RequestBody::Raw(raw_body)); - Ok(body) + Ok(PreparedRequestBody { + headers, + body: Some(raw_body.clone()), + }) } Some(RequestBody::Json(body)) => { let json = serde_json::to_vec(&body).map_err(|err| err.to_string())?; let bytes = if self.compression != RequestCompression::None { - if self.headers.contains_key(http::header::CONTENT_ENCODING) { - self.body = Some(RequestBody::Json(body)); + if headers.contains_key(http::header::CONTENT_ENCODING) { return Err( "request compression was requested but content-encoding is already set" .to_string(), @@ -105,8 +117,7 @@ impl Request { let post_compression_bytes = compressed.len(); let compression_duration = compression_start.elapsed(); - self.headers - .insert(http::header::CONTENT_ENCODING, content_encoding); + headers.insert(http::header::CONTENT_ENCODING, content_encoding); tracing::debug!( pre_compression_bytes, @@ -120,22 +131,22 @@ impl Request { json }; - if !self.headers.contains_key(http::header::CONTENT_TYPE) { - self.headers.insert( + if !headers.contains_key(http::header::CONTENT_TYPE) { + headers.insert( http::header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); } - self.compression = RequestCompression::None; - let bytes = Bytes::from(bytes); - self.body = Some(RequestBody::Raw(bytes.clone())); - Ok(bytes) - } - None => { - self.compression = RequestCompression::None; - Ok(Bytes::new()) + Ok(PreparedRequestBody { + headers, + body: Some(Bytes::from(bytes)), + }) } + None => Ok(PreparedRequestBody { + headers, + body: None, + }), } } } @@ -149,24 +160,29 @@ mod tests { #[test] fn prepare_body_for_send_serializes_json_and_sets_content_type() { - let mut request = - Request::new(Method::POST, "https://example.com/v1/responses".to_string()) - .with_json(&json!({"model": "test-model"})); + let request = Request::new(Method::POST, "https://example.com/v1/responses".to_string()) + .with_json(&json!({"model": "test-model"})); - let body = request + let prepared = request .prepare_body_for_send() .expect("body should prepare"); - assert_eq!(body, Bytes::from_static(br#"{"model":"test-model"}"#)); assert_eq!( - request + prepared.body, + Some(Bytes::from_static(br#"{"model":"test-model"}"#)) + ); + assert_eq!( + prepared .headers .get(http::header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()), Some("application/json") ); + assert_eq!( + request.body, + Some(RequestBody::Json(json!({"model": "test-model"}))) + ); assert_eq!(request.compression, RequestCompression::None); - assert_eq!(request.body, Some(RequestBody::Raw(body))); } #[test] diff --git a/codex-rs/codex-client/src/transport.rs b/codex-rs/codex-client/src/transport.rs index 323a77b844c8..4ed062c483bb 100644 --- a/codex-rs/codex-client/src/transport.rs +++ b/codex-rs/codex-client/src/transport.rs @@ -41,14 +41,14 @@ impl ReqwestTransport { } } - fn build(&self, mut req: Request) -> Result { - req.prepare_body_for_send().map_err(TransportError::Build)?; + fn build(&self, req: Request) -> Result { + let prepared = req.prepare_body_for_send().map_err(TransportError::Build)?; let Request { method, url, - headers, - body, + headers: _, + body: _, compression: _, timeout, } = req; @@ -62,16 +62,9 @@ impl ReqwestTransport { builder = builder.timeout(timeout); } - match body { - Some(RequestBody::Raw(raw_body)) => { - builder = builder.headers(headers).body(raw_body); - } - Some(RequestBody::Json(body)) => { - builder = builder.headers(headers).json(&body); - } - None => { - builder = builder.headers(headers); - } + builder = builder.headers(prepared.headers); + if let Some(body) = prepared.body { + builder = builder.body(body); } Ok(builder) } diff --git a/codex-rs/model-provider/src/aws_auth_provider.rs b/codex-rs/model-provider/src/aws_auth_provider.rs index 20efad8bcbf6..ae4c979618dc 100644 --- a/codex-rs/model-provider/src/aws_auth_provider.rs +++ b/codex-rs/model-provider/src/aws_auth_provider.rs @@ -5,6 +5,8 @@ use codex_aws_auth::AwsAuthContext; use codex_aws_auth::AwsAuthError; use codex_aws_auth::AwsRequestToSign; use codex_client::Request; +use codex_client::RequestBody; +use codex_client::RequestCompression; use http::HeaderMap; use http::HeaderValue; use tokio::sync::OnceCell; @@ -72,20 +74,22 @@ impl AuthProvider for AwsSigV4AuthProvider { async fn apply_auth(&self, mut request: Request) -> Result { remove_headers_not_preserved_by_bedrock_mantle(&mut request.headers); - let body = request.prepare_body_for_send().map_err(AuthError::Build)?; + let prepared = request.prepare_body_for_send().map_err(AuthError::Build)?; let context = self.context().await?; let signed = context .sign(AwsRequestToSign { method: request.method.clone(), url: request.url.clone(), - headers: request.headers.clone(), - body, + headers: prepared.headers.clone(), + body: prepared.body_bytes(), }) .await .map_err(aws_auth_error_to_auth_error)?; request.url = signed.url; request.headers = signed.headers; + request.body = prepared.body.map(RequestBody::Raw); + request.compression = RequestCompression::None; Ok(request) } } From 26fa21eda5c6e53399e3594baa11533b70a3da7d Mon Sep 17 00:00:00 2001 From: celia-oai Date: Tue, 21 Apr 2026 10:57:13 -0700 Subject: [PATCH 08/11] changes --- .../auth.rs} | 106 ++++++++- .../src/amazon_bedrock/mantle.rs | 74 ++++++ .../model-provider/src/amazon_bedrock/mod.rs | 81 +++++++ .../src/amazon_bedrock_provider.rs | 217 ------------------ codex-rs/model-provider/src/lib.rs | 3 +- codex-rs/model-provider/src/provider.rs | 2 +- 6 files changed, 252 insertions(+), 231 deletions(-) rename codex-rs/model-provider/src/{aws_auth_provider.rs => amazon_bedrock/auth.rs} (52%) create mode 100644 codex-rs/model-provider/src/amazon_bedrock/mantle.rs create mode 100644 codex-rs/model-provider/src/amazon_bedrock/mod.rs delete mode 100644 codex-rs/model-provider/src/amazon_bedrock_provider.rs diff --git a/codex-rs/model-provider/src/aws_auth_provider.rs b/codex-rs/model-provider/src/amazon_bedrock/auth.rs similarity index 52% rename from codex-rs/model-provider/src/aws_auth_provider.rs rename to codex-rs/model-provider/src/amazon_bedrock/auth.rs index ae4c979618dc..e22122f41617 100644 --- a/codex-rs/model-provider/src/aws_auth_provider.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/auth.rs @@ -1,27 +1,82 @@ +use std::sync::Arc; + use codex_api::AuthError; use codex_api::AuthProvider; +use codex_api::SharedAuthProvider; use codex_aws_auth::AwsAuthConfig; use codex_aws_auth::AwsAuthContext; use codex_aws_auth::AwsAuthError; use codex_aws_auth::AwsRequestToSign; +use codex_aws_auth::region_from_bedrock_bearer_token; use codex_client::Request; use codex_client::RequestBody; use codex_client::RequestCompression; +use codex_model_provider_info::ModelProviderAwsAuthInfo; +use codex_protocol::error::CodexErr; use http::HeaderMap; use http::HeaderValue; use tokio::sync::OnceCell; +use super::mantle; + +const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK"; const LEGACY_SESSION_ID_HEADER: &str = "session_id"; -/// AWS SigV4 auth provider for OpenAI-compatible model-provider requests. +pub(super) async fn resolve_provider_auth( + aws: &ModelProviderAwsAuthInfo, +) -> codex_protocol::error::Result { + if let Some(token) = bearer_token_from_env() { + return resolve_bearer_auth(token); + } + + let config = mantle::aws_auth_config(aws); + let context = AwsAuthContext::load(config.clone()) + .await + .map_err(aws_auth_error_to_codex_error)?; + Ok(Arc::new(BedrockMantleSigV4AuthProvider::with_context( + config, context, + ))) +} + +pub(super) async fn resolve_region( + aws: &ModelProviderAwsAuthInfo, +) -> codex_protocol::error::Result { + if let Some(token) = bearer_token_from_env() { + return region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error); + } + + let context = AwsAuthContext::load(mantle::aws_auth_config(aws)) + .await + .map_err(aws_auth_error_to_codex_error)?; + Ok(context.region().to_string()) +} + +fn resolve_bearer_auth(token: String) -> codex_protocol::error::Result { + let _region = + region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error)?; + Ok(Arc::new(BedrockBearerAuthProvider::new(token))) +} + +fn bearer_token_from_env() -> Option { + std::env::var(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR) + .ok() + .map(|token| token.trim().to_string()) + .filter(|token| !token.is_empty()) +} + +fn aws_auth_error_to_codex_error(error: AwsAuthError) -> CodexErr { + CodexErr::Fatal(format!("failed to resolve Amazon Bedrock auth: {error}")) +} + +/// AWS SigV4 auth provider for Bedrock Mantle OpenAI-compatible requests. #[derive(Debug)] -pub(crate) struct AwsSigV4AuthProvider { +struct BedrockMantleSigV4AuthProvider { config: AwsAuthConfig, context: OnceCell, } -impl AwsSigV4AuthProvider { - pub(crate) fn with_context(config: AwsAuthConfig, context: AwsAuthContext) -> Self { +impl BedrockMantleSigV4AuthProvider { + fn with_context(config: AwsAuthConfig, context: AwsAuthContext) -> Self { let cell = OnceCell::new(); let _ = cell.set(context); Self { @@ -40,18 +95,18 @@ impl AwsSigV4AuthProvider { /// Amazon Bedrock bearer-token auth provider for OpenAI-compatible requests. #[derive(Debug)] -pub(crate) struct AwsBedrockBearerAuthProvider { +struct BedrockBearerAuthProvider { token: String, } -impl AwsBedrockBearerAuthProvider { - pub(crate) fn new(token: String) -> Self { +impl BedrockBearerAuthProvider { + fn new(token: String) -> Self { Self { token } } } #[async_trait::async_trait] -impl AuthProvider for AwsBedrockBearerAuthProvider { +impl AuthProvider for BedrockBearerAuthProvider { fn add_auth_headers(&self, headers: &mut HeaderMap) { let token = &self.token; if let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}")) { @@ -69,7 +124,7 @@ fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError { } #[async_trait::async_trait] -impl AuthProvider for AwsSigV4AuthProvider { +impl AuthProvider for BedrockMantleSigV4AuthProvider { fn add_auth_headers(&self, _headers: &mut HeaderMap) {} async fn apply_auth(&self, mut request: Request) -> Result { @@ -104,12 +159,23 @@ fn remove_headers_not_preserved_by_bedrock_mantle(headers: &mut HeaderMap) { #[cfg(test)] mod tests { use codex_api::AuthProvider; + use pretty_assertions::assert_eq; use super::*; + fn bedrock_token_for_region(region: &str) -> String { + let encoded = match region { + "us-west-2" => { + "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZ1cy13ZXN0LTIlMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" + } + _ => panic!("test token fixture missing for {region}"), + }; + format!("bedrock-api-key-{encoded}") + } + #[test] - fn aws_bedrock_bearer_auth_adds_header() { - let provider = AwsBedrockBearerAuthProvider::new("bedrock-token".to_string()); + fn bedrock_bearer_auth_adds_header() { + let provider = BedrockBearerAuthProvider::new("bedrock-token".to_string()); let mut headers = HeaderMap::new(); provider.add_auth_headers(&mut headers); @@ -122,6 +188,24 @@ mod tests { ); } + #[test] + fn resolve_bedrock_bearer_auth_uses_token_region_and_header() { + let token = bedrock_token_for_region("us-west-2"); + let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); + let resolved = resolve_bearer_auth(token).expect("bearer auth should resolve"); + let mut headers = http::HeaderMap::new(); + + resolved.add_auth_headers(&mut headers); + + assert_eq!(region, "us-west-2"); + assert!( + headers + .get(http::header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.starts_with("Bearer bedrock-api-key-")) + ); + } + #[test] fn bedrock_mantle_sigv4_strips_legacy_session_id_header() { let mut headers = HeaderMap::new(); diff --git a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs new file mode 100644 index 000000000000..be39318a514d --- /dev/null +++ b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs @@ -0,0 +1,74 @@ +use codex_aws_auth::AwsAuthConfig; +use codex_model_provider_info::ModelProviderAwsAuthInfo; +use codex_protocol::error::CodexErr; + +const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle"; +const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 12] = [ + "us-east-2", + "us-east-1", + "us-west-2", + "ap-southeast-3", + "ap-south-1", + "ap-northeast-1", + "eu-central-1", + "eu-west-1", + "eu-west-2", + "eu-south-1", + "eu-north-1", + "sa-east-1", +]; + +pub(super) fn aws_auth_config(aws: &ModelProviderAwsAuthInfo) -> AwsAuthConfig { + AwsAuthConfig { + profile: aws.profile.clone(), + service: BEDROCK_MANTLE_SERVICE_NAME.to_string(), + } +} + +pub(super) fn base_url(region: &str) -> codex_protocol::error::Result { + if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) { + Ok(format!("https://bedrock-mantle.{region}.api.aws/v1")) + } else { + Err(CodexErr::Fatal(format!( + "Amazon Bedrock Mantle does not support region `{region}`" + ))) + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn base_url_uses_region_endpoint() { + assert_eq!( + base_url("ap-northeast-1").expect("supported region"), + "https://bedrock-mantle.ap-northeast-1.api.aws/v1" + ); + } + + #[test] + fn base_url_rejects_unsupported_region() { + let err = base_url("us-west-1").expect_err("unsupported region"); + + assert_eq!( + err.to_string(), + "Fatal error: Amazon Bedrock Mantle does not support region `us-west-1`" + ); + } + + #[test] + fn aws_auth_config_uses_profile_and_mantle_service() { + assert_eq!( + aws_auth_config(&ModelProviderAwsAuthInfo { + profile: Some("codex-bedrock".to_string()), + }), + AwsAuthConfig { + profile: Some("codex-bedrock".to_string()), + service: "bedrock-mantle".to_string(), + } + ); + } +} diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs new file mode 100644 index 000000000000..9564d0d847a8 --- /dev/null +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -0,0 +1,81 @@ +mod auth; +mod mantle; + +use std::sync::Arc; + +use codex_api::Provider; +use codex_api::SharedAuthProvider; +use codex_login::AuthManager; +use codex_login::CodexAuth; +use codex_model_provider_info::ModelProviderAwsAuthInfo; +use codex_model_provider_info::ModelProviderInfo; + +use crate::provider::ModelProvider; + +/// 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, +} + +#[async_trait::async_trait] +impl ModelProvider for AmazonBedrockModelProvider { + fn info(&self) -> &ModelProviderInfo { + &self.info + } + + fn auth_manager(&self) -> Option> { + None + } + + async fn auth(&self) -> Option { + None + } + + async fn api_provider(&self) -> codex_protocol::error::Result { + let region = auth::resolve_region(&self.aws).await?; + let mut api_provider_info = self.info.clone(); + api_provider_info.base_url = Some(mantle::base_url(®ion)?); + api_provider_info.to_api_provider(/*auth_mode*/ None) + } + + async fn api_auth(&self) -> codex_protocol::error::Result { + auth::resolve_provider_auth(&self.aws).await + } +} + +#[cfg(test)] +mod tests { + use codex_aws_auth::region_from_bedrock_bearer_token; + use pretty_assertions::assert_eq; + + use super::*; + + fn bedrock_token_for_region(region: &str) -> String { + let encoded = match region { + "eu-central-1" => { + "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZldS1jZW50cmFsLTElMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" + } + _ => panic!("test token fixture missing for {region}"), + }; + format!("bedrock-api-key-{encoded}") + } + + #[test] + fn api_provider_for_bedrock_bearer_token_uses_token_region_endpoint() { + let token = bedrock_token_for_region("eu-central-1"); + let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); + let mut api_provider_info = + ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None); + api_provider_info.base_url = Some(mantle::base_url(®ion).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/v1" + ); + } +} diff --git a/codex-rs/model-provider/src/amazon_bedrock_provider.rs b/codex-rs/model-provider/src/amazon_bedrock_provider.rs deleted file mode 100644 index 6d5c829cc438..000000000000 --- a/codex-rs/model-provider/src/amazon_bedrock_provider.rs +++ /dev/null @@ -1,217 +0,0 @@ -use std::sync::Arc; - -use codex_api::Provider; -use codex_api::SharedAuthProvider; -use codex_aws_auth::AwsAuthConfig; -use codex_aws_auth::AwsAuthContext; -use codex_aws_auth::AwsAuthError; -use codex_aws_auth::region_from_bedrock_bearer_token; -use codex_login::AuthManager; -use codex_login::CodexAuth; -use codex_model_provider_info::ModelProviderAwsAuthInfo; -use codex_model_provider_info::ModelProviderInfo; -use codex_protocol::error::CodexErr; - -use crate::aws_auth_provider::AwsBedrockBearerAuthProvider; -use crate::aws_auth_provider::AwsSigV4AuthProvider; -use crate::provider::ModelProvider; - -const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK"; -const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle"; -const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 12] = [ - "us-east-2", - "us-east-1", - "us-west-2", - "ap-southeast-3", - "ap-south-1", - "ap-northeast-1", - "eu-central-1", - "eu-west-1", - "eu-west-2", - "eu-south-1", - "eu-north-1", - "sa-east-1", -]; - -/// 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, -} - -#[async_trait::async_trait] -impl ModelProvider for AmazonBedrockModelProvider { - fn info(&self) -> &ModelProviderInfo { - &self.info - } - - fn auth_manager(&self) -> Option> { - None - } - - async fn auth(&self) -> Option { - None - } - - async fn api_provider(&self) -> codex_protocol::error::Result { - let region = resolve_bedrock_region(&self.aws).await?; - let mut api_provider_info = self.info.clone(); - api_provider_info.base_url = Some(bedrock_mantle_base_url(®ion)?); - api_provider_info.to_api_provider(/*auth_mode*/ None) - } - - async fn api_auth(&self) -> codex_protocol::error::Result { - resolve_bedrock_auth(&self.aws).await - } -} - -async fn resolve_bedrock_auth( - aws: &ModelProviderAwsAuthInfo, -) -> codex_protocol::error::Result { - if let Some(token) = bedrock_bearer_token_from_env() { - return resolve_bedrock_bearer_auth(token); - } - - let config = bedrock_aws_auth_config(aws); - let context = AwsAuthContext::load(config.clone()) - .await - .map_err(aws_auth_error_to_codex_error)?; - Ok(Arc::new(AwsSigV4AuthProvider::with_context( - config, context, - ))) -} - -async fn resolve_bedrock_region( - aws: &ModelProviderAwsAuthInfo, -) -> codex_protocol::error::Result { - if let Some(token) = bedrock_bearer_token_from_env() { - return region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error); - } - - let context = AwsAuthContext::load(bedrock_aws_auth_config(aws)) - .await - .map_err(aws_auth_error_to_codex_error)?; - Ok(context.region().to_string()) -} - -fn resolve_bedrock_bearer_auth(token: String) -> codex_protocol::error::Result { - let _region = - region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error)?; - Ok(Arc::new(AwsBedrockBearerAuthProvider::new(token))) -} - -fn bedrock_bearer_token_from_env() -> Option { - std::env::var(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR) - .ok() - .map(|token| token.trim().to_string()) - .filter(|token| !token.is_empty()) -} - -fn bedrock_aws_auth_config(aws: &ModelProviderAwsAuthInfo) -> AwsAuthConfig { - AwsAuthConfig { - profile: aws.profile.clone(), - service: BEDROCK_MANTLE_SERVICE_NAME.to_string(), - } -} - -fn bedrock_mantle_base_url(region: &str) -> codex_protocol::error::Result { - if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) { - Ok(format!("https://bedrock-mantle.{region}.api.aws/v1")) - } else { - Err(CodexErr::Fatal(format!( - "Amazon Bedrock Mantle does not support region `{region}`" - ))) - } -} - -fn aws_auth_error_to_codex_error(error: AwsAuthError) -> CodexErr { - CodexErr::Fatal(format!("failed to resolve Amazon Bedrock auth: {error}")) -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - fn bedrock_token_for_region(region: &str) -> String { - let encoded = match region { - "us-west-2" => { - "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZ1cy13ZXN0LTIlMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" - } - "eu-central-1" => { - "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZldS1jZW50cmFsLTElMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" - } - _ => panic!("test token fixture missing for {region}"), - }; - format!("bedrock-api-key-{encoded}") - } - - #[test] - fn bedrock_mantle_base_url_uses_region_endpoint() { - assert_eq!( - bedrock_mantle_base_url("ap-northeast-1").expect("supported region"), - "https://bedrock-mantle.ap-northeast-1.api.aws/v1" - ); - } - - #[test] - fn bedrock_mantle_base_url_rejects_unsupported_region() { - let err = bedrock_mantle_base_url("us-west-1").expect_err("unsupported region"); - - assert_eq!( - err.to_string(), - "Fatal error: Amazon Bedrock Mantle does not support region `us-west-1`" - ); - } - - #[test] - fn resolve_bedrock_bearer_auth_uses_token_region_and_header() { - let token = bedrock_token_for_region("us-west-2"); - let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); - let resolved = resolve_bedrock_bearer_auth(token).expect("bearer auth should resolve"); - let mut headers = http::HeaderMap::new(); - - resolved.add_auth_headers(&mut headers); - - assert_eq!(region, "us-west-2"); - assert!( - headers - .get(http::header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .is_some_and(|value| value.starts_with("Bearer bedrock-api-key-")) - ); - } - - #[test] - fn api_provider_for_bedrock_bearer_token_uses_token_region_endpoint() { - let token = bedrock_token_for_region("eu-central-1"); - let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); - let mut api_provider_info = - ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None); - api_provider_info.base_url = - Some(bedrock_mantle_base_url(®ion).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/v1" - ); - } - - #[test] - fn bedrock_aws_auth_config_uses_profile_and_mantle_service() { - assert_eq!( - bedrock_aws_auth_config(&ModelProviderAwsAuthInfo { - profile: Some("codex-bedrock".to_string()), - }), - AwsAuthConfig { - profile: Some("codex-bedrock".to_string()), - service: "bedrock-mantle".to_string(), - } - ); - } -} diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs index 7d899f1d4041..54c0dfb0e545 100644 --- a/codex-rs/model-provider/src/lib.rs +++ b/codex-rs/model-provider/src/lib.rs @@ -1,6 +1,5 @@ -mod amazon_bedrock_provider; +mod amazon_bedrock; mod auth; -mod aws_auth_provider; mod bearer_auth_provider; mod provider; diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 7dccf6a0bfca..51dd62077b7c 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -8,7 +8,7 @@ use codex_login::CodexAuth; use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_model_provider_info::ModelProviderInfo; -use crate::amazon_bedrock_provider::AmazonBedrockModelProvider; +use crate::amazon_bedrock::AmazonBedrockModelProvider; use crate::auth::auth_manager_for_provider; use crate::auth::resolve_api_provider; use crate::auth::resolve_provider_auth; From 2d220abf1183b837994c8f1b8444d90dfdf5cfb5 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Tue, 21 Apr 2026 11:07:53 -0700 Subject: [PATCH 09/11] changes --- codex-rs/core/src/config/config_tests.rs | 27 +--- codex-rs/model-provider-info/src/lib.rs | 12 +- .../src/model_provider_info_tests.rs | 15 +- .../model-provider/src/amazon_bedrock/auth.rs | 142 ++++++++---------- .../src/amazon_bedrock/mantle.rs | 3 +- .../model-provider/src/amazon_bedrock/mod.rs | 16 +- codex-rs/model-provider/src/auth.rs | 9 +- codex-rs/model-provider/src/provider.rs | 6 +- 8 files changed, 100 insertions(+), 130 deletions(-) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 6f4f31c6051b..e7c7aa31dd7d 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -49,7 +49,6 @@ use codex_config::types::TuiNotificationSettings; use codex_exec_server::LOCAL_FS; use codex_features::Feature; use codex_features::FeaturesToml; -use codex_model_provider_info::AMAZON_BEDROCK_DEFAULT_BASE_URL; use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; @@ -441,7 +440,7 @@ profile = "codex-bedrock" } #[tokio::test] -async fn load_config_ignores_unsupported_amazon_bedrock_overrides() { +async fn load_config_rejects_unsupported_amazon_bedrock_overrides() { let cfg = toml::from_str::( r#" model_provider = "amazon-bedrock" @@ -458,30 +457,18 @@ profile = "codex-bedrock" ) .expect("Amazon Bedrock unsupported overrides should deserialize"); - let config = Config::load_from_base_config_with_overrides( + let err = Config::load_from_base_config_with_overrides( cfg, ConfigOverrides::default(), tempdir().expect("tempdir").abs(), ) .await - .expect("load config"); + .unwrap_err(); - assert_eq!(config.model_provider.name, "Amazon Bedrock"); - assert_eq!( - config.model_provider.base_url.as_deref(), - Some(AMAZON_BEDROCK_DEFAULT_BASE_URL) - ); - assert_eq!(config.model_provider.wire_api, WireApi::Responses); - assert!(!config.model_provider.requires_openai_auth); - assert!(!config.model_provider.supports_websockets); - assert_eq!( - config - .model_provider - .aws - .as_ref() - .and_then(|aws| aws.profile.as_deref()), - Some("codex-bedrock") - ); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); + assert!(err.to_string().contains( + "model_providers.amazon-bedrock only supports changing `aws.profile`; other non-default provider fields are not supported" + )); } #[test] diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs index ba5ac1a863f6..97b1e166d6be 100644 --- a/codex-rs/model-provider-info/src/lib.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -427,9 +427,17 @@ pub fn merge_configured_model_providers( mut model_providers: HashMap, configured_model_providers: HashMap, ) -> Result, String> { - for (key, provider) in configured_model_providers { + for (key, mut provider) in configured_model_providers { if key == AMAZON_BEDROCK_PROVIDER_ID { - if let Some(profile) = provider.aws.and_then(|aws| aws.profile) + let aws_override = provider.aws.take(); + if provider != ModelProviderInfo::default() { + return Err(format!( + "model_providers.{AMAZON_BEDROCK_PROVIDER_ID} only supports changing \ +`aws.profile`; other non-default provider fields are not supported" + )); + } + + if let Some(profile) = aws_override.and_then(|aws| aws.profile) && let Some(built_in) = model_providers.get_mut(AMAZON_BEDROCK_PROVIDER_ID) && let Some(aws) = built_in.aws.as_mut() { diff --git a/codex-rs/model-provider-info/src/model_provider_info_tests.rs b/codex-rs/model-provider-info/src/model_provider_info_tests.rs index 0be24e15a8bd..20440de32cea 100644 --- a/codex-rs/model-provider-info/src/model_provider_info_tests.rs +++ b/codex-rs/model-provider-info/src/model_provider_info_tests.rs @@ -327,7 +327,7 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_profile_override } #[test] -fn test_merge_configured_model_providers_ignores_amazon_bedrock_non_default_fields() { +fn test_merge_configured_model_providers_rejects_amazon_bedrock_non_default_fields() { let configured_model_providers = std::collections::HashMap::from([( AMAZON_BEDROCK_PROVIDER_ID.to_string(), ModelProviderInfo { @@ -339,20 +339,15 @@ fn test_merge_configured_model_providers_ignores_amazon_bedrock_non_default_fiel }, )]); - let mut expected = built_in_model_providers(/*openai_base_url*/ None); - expected - .get_mut(AMAZON_BEDROCK_PROVIDER_ID) - .expect("Amazon Bedrock provider should be built in") - .aws = Some(ModelProviderAwsAuthInfo { - profile: Some("codex-bedrock".to_string()), - }); - assert_eq!( merge_configured_model_providers( built_in_model_providers(/*openai_base_url*/ None), configured_model_providers, ), - Ok(expected) + Err( + "model_providers.amazon-bedrock only supports changing `aws.profile`; other non-default provider fields are not supported" + .to_string() + ) ); } diff --git a/codex-rs/model-provider/src/amazon_bedrock/auth.rs b/codex-rs/model-provider/src/amazon_bedrock/auth.rs index e22122f41617..3b3ebaed31db 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/auth.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/auth.rs @@ -13,48 +13,62 @@ use codex_client::RequestBody; use codex_client::RequestCompression; use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_protocol::error::CodexErr; +use codex_protocol::error::Result; use http::HeaderMap; -use http::HeaderValue; use tokio::sync::OnceCell; -use super::mantle; +use crate::BearerAuthProvider; + +use super::mantle::aws_auth_config; const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK"; const LEGACY_SESSION_ID_HEADER: &str = "session_id"; -pub(super) async fn resolve_provider_auth( - aws: &ModelProviderAwsAuthInfo, -) -> codex_protocol::error::Result { +enum BedrockAuthMethod { + EnvBearerToken { + token: String, + region: String, + }, + AwsSdkAuth { + config: AwsAuthConfig, + context: AwsAuthContext, + }, +} + +async fn resolve_auth_method(aws: &ModelProviderAwsAuthInfo) -> Result { if let Some(token) = bearer_token_from_env() { - return resolve_bearer_auth(token); + let region = + region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error)?; + return Ok(BedrockAuthMethod::EnvBearerToken { token, region }); } - let config = mantle::aws_auth_config(aws); + let config = aws_auth_config(aws); let context = AwsAuthContext::load(config.clone()) .await .map_err(aws_auth_error_to_codex_error)?; - Ok(Arc::new(BedrockMantleSigV4AuthProvider::with_context( - config, context, - ))) + Ok(BedrockAuthMethod::AwsSdkAuth { config, context }) } -pub(super) async fn resolve_region( +pub(super) async fn resolve_provider_auth( aws: &ModelProviderAwsAuthInfo, -) -> codex_protocol::error::Result { - if let Some(token) = bearer_token_from_env() { - return region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error); +) -> Result { + match resolve_auth_method(aws).await? { + BedrockAuthMethod::EnvBearerToken { token, .. } => Ok(Arc::new(BearerAuthProvider { + token: Some(token), + account_id: None, + is_fedramp_account: false, + })), + BedrockAuthMethod::AwsSdkAuth { config, context } => Ok(Arc::new( + BedrockMantleSigV4AuthProvider::with_context(config, context), + )), } - - let context = AwsAuthContext::load(mantle::aws_auth_config(aws)) - .await - .map_err(aws_auth_error_to_codex_error)?; - Ok(context.region().to_string()) } -fn resolve_bearer_auth(token: String) -> codex_protocol::error::Result { - let _region = - region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error)?; - Ok(Arc::new(BedrockBearerAuthProvider::new(token))) +pub(super) async fn resolve_region(aws: &ModelProviderAwsAuthInfo) -> Result { + match resolve_auth_method(aws).await? { + BedrockAuthMethod::EnvBearerToken { region, .. } => Ok(region), + BedrockAuthMethod::AwsSdkAuth { context, .. } => Ok(context.region().to_string()), + } } fn bearer_token_from_env() -> Option { @@ -68,6 +82,21 @@ fn aws_auth_error_to_codex_error(error: AwsAuthError) -> CodexErr { CodexErr::Fatal(format!("failed to resolve Amazon Bedrock auth: {error}")) } +fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError { + if error.is_retryable() { + AuthError::Transient(error.to_string()) + } else { + AuthError::Build(error.to_string()) + } +} + +fn remove_headers_not_preserved_by_bedrock_mantle(headers: &mut HeaderMap) { + // The Bedrock Mantle front door does not preserve this legacy OpenAI header + // for SigV4 verification. Signing it makes the richer Codex agent request + // fail even though raw Responses requests work. + headers.remove(LEGACY_SESSION_ID_HEADER); +} + /// AWS SigV4 auth provider for Bedrock Mantle OpenAI-compatible requests. #[derive(Debug)] struct BedrockMantleSigV4AuthProvider { @@ -85,7 +114,7 @@ impl BedrockMantleSigV4AuthProvider { } } - async fn context(&self) -> Result<&AwsAuthContext, AuthError> { + async fn context(&self) -> std::result::Result<&AwsAuthContext, AuthError> { self.context .get_or_try_init(|| AwsAuthContext::load(self.config.clone())) .await @@ -93,41 +122,11 @@ impl BedrockMantleSigV4AuthProvider { } } -/// Amazon Bedrock bearer-token auth provider for OpenAI-compatible requests. -#[derive(Debug)] -struct BedrockBearerAuthProvider { - token: String, -} - -impl BedrockBearerAuthProvider { - fn new(token: String) -> Self { - Self { token } - } -} - -#[async_trait::async_trait] -impl AuthProvider for BedrockBearerAuthProvider { - fn add_auth_headers(&self, headers: &mut HeaderMap) { - let token = &self.token; - if let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}")) { - let _ = headers.insert(http::header::AUTHORIZATION, header); - } - } -} - -fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError { - if error.is_retryable() { - AuthError::Transient(error.to_string()) - } else { - AuthError::Build(error.to_string()) - } -} - #[async_trait::async_trait] impl AuthProvider for BedrockMantleSigV4AuthProvider { fn add_auth_headers(&self, _headers: &mut HeaderMap) {} - async fn apply_auth(&self, mut request: Request) -> Result { + async fn apply_auth(&self, mut request: Request) -> std::result::Result { remove_headers_not_preserved_by_bedrock_mantle(&mut request.headers); let prepared = request.prepare_body_for_send().map_err(AuthError::Build)?; let context = self.context().await?; @@ -149,16 +148,10 @@ impl AuthProvider for BedrockMantleSigV4AuthProvider { } } -fn remove_headers_not_preserved_by_bedrock_mantle(headers: &mut HeaderMap) { - // The Bedrock Mantle front door does not preserve this legacy OpenAI header - // for SigV4 verification. Signing it makes the richer Codex agent request - // fail even though raw Responses requests work. - headers.remove(LEGACY_SESSION_ID_HEADER); -} - #[cfg(test)] mod tests { use codex_api::AuthProvider; + use http::HeaderValue; use pretty_assertions::assert_eq; use super::*; @@ -174,28 +167,17 @@ mod tests { } #[test] - fn bedrock_bearer_auth_adds_header() { - let provider = BedrockBearerAuthProvider::new("bedrock-token".to_string()); - let mut headers = HeaderMap::new(); - - provider.add_auth_headers(&mut headers); - - assert_eq!( - headers - .get(http::header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()), - Some("Bearer bedrock-token") - ); - } - - #[test] - fn resolve_bedrock_bearer_auth_uses_token_region_and_header() { + fn bedrock_bearer_auth_uses_token_region_and_header() { let token = bedrock_token_for_region("us-west-2"); let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); - let resolved = resolve_bearer_auth(token).expect("bearer auth should resolve"); + let provider = BearerAuthProvider { + token: Some(token), + account_id: None, + is_fedramp_account: false, + }; let mut headers = http::HeaderMap::new(); - resolved.add_auth_headers(&mut headers); + provider.add_auth_headers(&mut headers); assert_eq!(region, "us-west-2"); assert!( diff --git a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs index be39318a514d..a04aadf5dd85 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs @@ -1,6 +1,7 @@ use codex_aws_auth::AwsAuthConfig; use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_protocol::error::CodexErr; +use codex_protocol::error::Result; const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle"; const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 12] = [ @@ -25,7 +26,7 @@ pub(super) fn aws_auth_config(aws: &ModelProviderAwsAuthInfo) -> AwsAuthConfig { } } -pub(super) fn base_url(region: &str) -> codex_protocol::error::Result { +pub(super) fn base_url(region: &str) -> Result { if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) { Ok(format!("https://bedrock-mantle.{region}.api.aws/v1")) } else { diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs index 9564d0d847a8..021afb83e508 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mod.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -9,8 +9,12 @@ use codex_login::AuthManager; use codex_login::CodexAuth; use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_model_provider_info::ModelProviderInfo; +use codex_protocol::error::Result; use crate::provider::ModelProvider; +use auth::resolve_provider_auth; +use auth::resolve_region; +use mantle::base_url; /// Runtime provider for Amazon Bedrock's OpenAI-compatible Mantle endpoint. #[derive(Clone, Debug)] @@ -33,15 +37,15 @@ impl ModelProvider for AmazonBedrockModelProvider { None } - async fn api_provider(&self) -> codex_protocol::error::Result { - let region = auth::resolve_region(&self.aws).await?; + async fn api_provider(&self) -> Result { + let region = resolve_region(&self.aws).await?; let mut api_provider_info = self.info.clone(); - api_provider_info.base_url = Some(mantle::base_url(®ion)?); + api_provider_info.base_url = Some(base_url(®ion)?); api_provider_info.to_api_provider(/*auth_mode*/ None) } - async fn api_auth(&self) -> codex_protocol::error::Result { - auth::resolve_provider_auth(&self.aws).await + async fn api_auth(&self) -> Result { + resolve_provider_auth(&self.aws).await } } @@ -68,7 +72,7 @@ mod tests { let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); let mut api_provider_info = ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None); - api_provider_info.base_url = Some(mantle::base_url(®ion).expect("supported region")); + api_provider_info.base_url = Some(base_url(®ion).expect("supported region")); let api_provider = api_provider_info .to_api_provider(/*auth_mode*/ None) .expect("api provider should build"); diff --git a/codex-rs/model-provider/src/auth.rs b/codex-rs/model-provider/src/auth.rs index 06af52592a25..64640dcc960e 100644 --- a/codex-rs/model-provider/src/auth.rs +++ b/codex-rs/model-provider/src/auth.rs @@ -56,16 +56,9 @@ fn bearer_auth_provider_from_auth( } } -pub(crate) async fn resolve_provider_auth( +pub(crate) fn resolve_provider_auth( auth: Option<&CodexAuth>, provider: &ModelProviderInfo, ) -> codex_protocol::error::Result { Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?)) } - -pub(crate) async fn resolve_api_provider( - auth: Option<&CodexAuth>, - provider: &ModelProviderInfo, -) -> codex_protocol::error::Result { - provider.to_api_provider(auth.map(CodexAuth::auth_mode)) -} diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 51dd62077b7c..462a27ef7256 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -10,7 +10,6 @@ use codex_model_provider_info::ModelProviderInfo; use crate::amazon_bedrock::AmazonBedrockModelProvider; use crate::auth::auth_manager_for_provider; -use crate::auth::resolve_api_provider; use crate::auth::resolve_provider_auth; /// Runtime provider abstraction used by model execution. @@ -37,13 +36,14 @@ pub trait ModelProvider: fmt::Debug + Send + Sync { /// Returns provider configuration adapted for the API client. async fn api_provider(&self) -> codex_protocol::error::Result { let auth = self.auth().await; - resolve_api_provider(auth.as_ref(), self.info()).await + self.info() + .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode)) } /// Returns the auth provider used to attach request credentials. async fn api_auth(&self) -> codex_protocol::error::Result { let auth = self.auth().await; - resolve_provider_auth(auth.as_ref(), self.info()).await + resolve_provider_auth(auth.as_ref(), self.info()) } } From c11484f00fbed4cbc1c9c8d06b9a231cc2ab7657 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Tue, 21 Apr 2026 14:03:02 -0700 Subject: [PATCH 10/11] changes --- MODULE.bazel.lock | 1 + codex-rs/Cargo.lock | 2 - codex-rs/aws-auth/Cargo.toml | 2 - codex-rs/aws-auth/src/config.rs | 4 + codex-rs/aws-auth/src/lib.rs | 89 +------------------ codex-rs/core/config.schema.json | 4 + codex-rs/core/src/config/config_tests.rs | 24 ++++- codex-rs/model-provider-info/src/lib.rs | 24 +++-- .../src/model_provider_info_tests.rs | 27 ++++-- .../model-provider/src/amazon_bedrock/auth.rs | 50 +++++++---- .../src/amazon_bedrock/mantle.rs | 26 ++++++ .../model-provider/src/amazon_bedrock/mod.rs | 18 +--- codex-rs/model-provider/src/provider.rs | 25 ++---- 13 files changed, 139 insertions(+), 157 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 667c77893b04..aeaeb25d60a4 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -685,6 +685,7 @@ "axum_0.7.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"name\":\"axum-core\",\"req\":\"^0.4.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.4.2\"},{\"features\":[\"__private\"],\"kind\":\"dev\",\"name\":\"axum-macros\",\"req\":\"^0.4.1\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"^0.7\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.24.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.24.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:serde_urlencoded\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:serde_urlencoded\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "axum_0.8.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"axum-core\",\"req\":\"^0.5.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"form_urlencoded\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"client\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"=0.8.4\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.211\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"name\":\"serde_core\",\"req\":\"^1.0.221\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.44\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.44.2\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.28.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.28.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private\":[\"tokio\",\"http1\",\"dep:reqwest\"],\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:serde\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "backtrace_0.3.76": "{\"dependencies\":[{\"default_features\":false,\"name\":\"addr2line\",\"req\":\"^0.25.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.156\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libloading\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"miniz_oxide\",\"req\":\"^0.8\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"default_features\":false,\"features\":[\"read_core\",\"elf\",\"macho\",\"pe\",\"xcoff\",\"unaligned\",\"archive\"],\"name\":\"object\",\"req\":\"^0.37.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"rustc-demangle\",\"req\":\"^0.1.24\"},{\"default_features\":false,\"name\":\"ruzstd\",\"optional\":true,\"req\":\"^0.8.1\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(any(windows, target_os = \\\"cygwin\\\"))\"}],\"features\":{\"coresymbolication\":[],\"dbghelp\":[],\"default\":[\"std\"],\"dl_iterate_phdr\":[],\"dladdr\":[],\"kernel32\":[],\"libunwind\":[],\"ruzstd\":[\"dep:ruzstd\"],\"serialize-serde\":[\"serde\"],\"std\":[],\"unix-backtrace\":[]}}", + "base16ct_0.2.0": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "base64-simd_0.8.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.20.0\"},{\"kind\":\"dev\",\"name\":\"const-str\",\"req\":\"^0.5.3\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.8\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"outref\",\"req\":\"^0.5.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"vsimd\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.33\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"}],\"features\":{\"alloc\":[\"vsimd/alloc\"],\"default\":[\"std\",\"detect\"],\"detect\":[\"vsimd/detect\"],\"std\":[\"alloc\",\"vsimd/std\"],\"unstable\":[\"vsimd/unstable\"]}}", "base64_0.21.7": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "base64_0.22.1": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 3ea1518a359f..d78f66d55965 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2002,13 +2002,11 @@ dependencies = [ "aws-credential-types", "aws-sigv4", "aws-types", - "base64 0.22.1", "bytes", "http 1.4.0", "pretty_assertions", "thiserror 2.0.18", "tokio", - "url", ] [[package]] diff --git a/codex-rs/aws-auth/Cargo.toml b/codex-rs/aws-auth/Cargo.toml index 12bcdd2708ca..9e49f7bbe50d 100644 --- a/codex-rs/aws-auth/Cargo.toml +++ b/codex-rs/aws-auth/Cargo.toml @@ -17,11 +17,9 @@ aws-config = { workspace = true } aws-credential-types = { workspace = true } aws-sigv4 = { workspace = true } aws-types = { workspace = true } -base64 = { workspace = true } bytes = { workspace = true } http = { workspace = true } thiserror = { workspace = true } -url = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/aws-auth/src/config.rs b/codex-rs/aws-auth/src/config.rs index 61f77c36aa4a..3d62832e0ebb 100644 --- a/codex-rs/aws-auth/src/config.rs +++ b/codex-rs/aws-auth/src/config.rs @@ -1,6 +1,7 @@ use aws_config::BehaviorVersion; use aws_config::SdkConfig; use aws_credential_types::provider::SharedCredentialsProvider; +use aws_types::region::Region; use crate::AwsAuthConfig; use crate::AwsAuthError; @@ -14,6 +15,9 @@ pub(crate) async fn load_sdk_config(config: &AwsAuthConfig) -> Result, + pub region: Option, pub service: String, } @@ -45,8 +43,6 @@ pub enum AwsAuthError { MissingCredentialsProvider, #[error("AWS SDK config did not resolve a region")] MissingRegion, - #[error("Amazon Bedrock bearer token is invalid: {0}")] - InvalidBedrockBearerToken(String), #[error("failed to load AWS credentials: {0}")] Credentials(#[from] aws_credential_types::provider::error::CredentialsError), #[error("request URL is not a valid URI: {0}")] @@ -115,48 +111,6 @@ impl AwsAuthContext { } } -/// Extracts the AWS region embedded in an Amazon Bedrock short-term bearer token. -pub fn region_from_bedrock_bearer_token(token: &str) -> Result { - const PREFIX: &str = "bedrock-api-key-"; - - let token_body = token - .trim() - .strip_prefix(PREFIX) - .ok_or_else(|| invalid_bedrock_bearer_token("missing bedrock-api-key prefix"))?; - let encoded_token = token_body - .split_once("&Version=") - .map_or(token_body, |(encoded, _)| encoded); - let decoded = general_purpose::STANDARD - .decode(encoded_token) - .map_err(|_| invalid_bedrock_bearer_token("base64 payload could not be decoded"))?; - let decoded = String::from_utf8(decoded) - .map_err(|_| invalid_bedrock_bearer_token("decoded payload is not UTF-8"))?; - let decoded_url = if decoded.starts_with("http://") || decoded.starts_with("https://") { - decoded - } else { - format!("https://{decoded}") - }; - let url = Url::parse(&decoded_url) - .map_err(|_| invalid_bedrock_bearer_token("decoded payload is not a URL"))?; - let credential = url - .query_pairs() - .find_map(|(key, value)| (key == "X-Amz-Credential").then_some(value.into_owned())) - .ok_or_else(|| invalid_bedrock_bearer_token("missing X-Amz-Credential"))?; - let mut parts = credential.split('/'); - let _access_key = parts.next(); - let _date = parts.next(); - let region = parts - .next() - .filter(|region| !region.trim().is_empty()) - .ok_or_else(|| invalid_bedrock_bearer_token("credential scope is missing region"))?; - - Ok(region.to_string()) -} - -fn invalid_bedrock_bearer_token(message: &'static str) -> AwsAuthError { - AwsAuthError::InvalidBedrockBearerToken(message.to_string()) -} - impl AwsAuthError { /// Returns whether retrying the outbound request can reasonably recover from this auth error. pub fn is_retryable(&self) -> bool { @@ -169,7 +123,6 @@ impl AwsAuthError { AwsAuthError::EmptyService | AwsAuthError::MissingCredentialsProvider | AwsAuthError::MissingRegion - | AwsAuthError::InvalidBedrockBearerToken(_) | AwsAuthError::InvalidUri(_) | AwsAuthError::BuildHttpRequest(_) | AwsAuthError::InvalidHeaderValue(_) @@ -297,6 +250,7 @@ mod tests { async fn load_rejects_empty_service_name() { let err = AwsAuthContext::load(AwsAuthConfig { profile: None, + region: None, service: " ".to_string(), }) .await @@ -304,43 +258,4 @@ mod tests { assert_eq!(err.to_string(), "AWS service name must not be empty"); } - - #[test] - fn region_from_bedrock_bearer_token_reads_sigv4_credential_scope() { - let decoded = "bedrock.amazonaws.com/?Action=CallWithBearerToken&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIDEXAMPLE%2F20260420%2Fus-west-2%2Fbedrock%2Faws4_request&Version=1"; - let encoded = general_purpose::STANDARD.encode(decoded); - let token = format!("bedrock-api-key-{encoded}"); - - assert_eq!( - region_from_bedrock_bearer_token(&token).expect("token region should parse"), - "us-west-2" - ); - } - - #[test] - fn region_from_bedrock_bearer_token_accepts_unencoded_version_suffix() { - let decoded = "bedrock.amazonaws.com/?Action=CallWithBearerToken&X-Amz-Credential=AKIDEXAMPLE%2F20260420%2Feu-west-1%2Fbedrock%2Faws4_request"; - let encoded = general_purpose::STANDARD.encode(decoded); - let token = format!("bedrock-api-key-{encoded}&Version=1"); - - assert_eq!( - region_from_bedrock_bearer_token(&token).expect("token region should parse"), - "eu-west-1" - ); - } - - #[test] - fn region_from_bedrock_bearer_token_rejects_missing_credential_scope() { - let decoded = "bedrock.amazonaws.com/?Action=CallWithBearerToken"; - let encoded = general_purpose::STANDARD.encode(decoded); - let token = format!("bedrock-api-key-{encoded}"); - - let err = region_from_bedrock_bearer_token(&token) - .expect_err("missing credential scope should fail"); - - assert_eq!( - err.to_string(), - "Amazon Bedrock bearer token is invalid: missing X-Amz-Credential" - ); - } } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index b0faea2b56d5..e85207974c5d 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1034,6 +1034,10 @@ "profile": { "description": "AWS profile name to use. When unset, the AWS SDK default chain decides.", "type": "string" + }, + "region": { + "description": "AWS region to use for provider-specific endpoints.", + "type": "string" } }, "type": "object" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index e7c7aa31dd7d..966691f246df 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -395,9 +395,10 @@ fn accepts_amazon_bedrock_aws_profile_override() { r#" [model_providers.amazon-bedrock.aws] profile = "codex-bedrock" +region = "us-west-2" "#, ) - .expect("Amazon Bedrock AWS profile override should deserialize"); + .expect("Amazon Bedrock AWS overrides should deserialize"); assert_eq!( cfg.model_providers @@ -406,6 +407,13 @@ profile = "codex-bedrock" .and_then(|aws| aws.profile.as_deref()), Some("codex-bedrock") ); + assert_eq!( + cfg.model_providers + .get("amazon-bedrock") + .and_then(|provider| provider.aws.as_ref()) + .and_then(|aws| aws.region.as_deref()), + Some("us-west-2") + ); } #[tokio::test] @@ -416,9 +424,10 @@ model_provider = "amazon-bedrock" [model_providers.amazon-bedrock.aws] profile = "codex-bedrock" +region = "us-west-2" "#, ) - .expect("Amazon Bedrock AWS profile override should deserialize"); + .expect("Amazon Bedrock AWS overrides should deserialize"); let config = Config::load_from_base_config_with_overrides( cfg, @@ -437,6 +446,14 @@ profile = "codex-bedrock" .and_then(|aws| aws.profile.as_deref()), Some("codex-bedrock") ); + assert_eq!( + config + .model_provider + .aws + .as_ref() + .and_then(|aws| aws.region.as_deref()), + Some("us-west-2") + ); } #[tokio::test] @@ -453,6 +470,7 @@ supports_websockets = true [model_providers.amazon-bedrock.aws] profile = "codex-bedrock" +region = "us-west-2" "#, ) .expect("Amazon Bedrock unsupported overrides should deserialize"); @@ -467,7 +485,7 @@ profile = "codex-bedrock" assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); assert!(err.to_string().contains( - "model_providers.amazon-bedrock only supports changing `aws.profile`; other non-default provider fields are not supported" + "model_providers.amazon-bedrock only supports changing `aws.profile` and `aws.region`; other non-default provider fields are not supported" )); } diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs index 97b1e166d6be..233897e0e5c2 100644 --- a/codex-rs/model-provider-info/src/lib.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -136,6 +136,8 @@ pub struct ModelProviderInfo { pub struct ModelProviderAwsAuthInfo { /// AWS profile name to use. When unset, the AWS SDK default chain decides. pub profile: Option, + /// AWS region to use for provider-specific endpoints. + pub region: Option, } impl ModelProviderInfo { @@ -352,7 +354,10 @@ impl ModelProviderInfo { env_key_instructions: None, experimental_bearer_token: None, auth: None, - aws: Some(aws.unwrap_or(ModelProviderAwsAuthInfo { profile: None })), + aws: Some(aws.unwrap_or(ModelProviderAwsAuthInfo { + profile: None, + region: None, + })), wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -422,7 +427,7 @@ pub fn built_in_model_providers( /// /// 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 set `aws.profile`. +/// user to set `aws.profile` and `aws.region`. pub fn merge_configured_model_providers( mut model_providers: HashMap, configured_model_providers: HashMap, @@ -433,15 +438,20 @@ pub fn merge_configured_model_providers( if provider != ModelProviderInfo::default() { return Err(format!( "model_providers.{AMAZON_BEDROCK_PROVIDER_ID} only supports changing \ -`aws.profile`; other non-default provider fields are not supported" +`aws.profile` and `aws.region`; other non-default provider fields are not supported" )); } - if let Some(profile) = aws_override.and_then(|aws| aws.profile) - && let Some(built_in) = model_providers.get_mut(AMAZON_BEDROCK_PROVIDER_ID) - && let Some(aws) = built_in.aws.as_mut() + if let Some(aws_override) = aws_override + && let Some(built_in_provider) = model_providers.get_mut(AMAZON_BEDROCK_PROVIDER_ID) + && let Some(built_in_aws) = built_in_provider.aws.as_mut() { - aws.profile = Some(profile); + if let Some(profile) = aws_override.profile { + built_in_aws.profile = Some(profile); + } + if let Some(region) = aws_override.region { + built_in_aws.region = Some(region); + } } } else { model_providers.entry(key).or_insert(provider); diff --git a/codex-rs/model-provider-info/src/model_provider_info_tests.rs b/codex-rs/model-provider-info/src/model_provider_info_tests.rs index 20440de32cea..34e981f4efd1 100644 --- a/codex-rs/model-provider-info/src/model_provider_info_tests.rs +++ b/codex-rs/model-provider-info/src/model_provider_info_tests.rs @@ -225,6 +225,7 @@ base_url = "https://bedrock.example.com/v1" [aws] profile = "codex-bedrock" +region = "us-west-2" "#; let provider: ModelProviderInfo = toml::from_str(provider_toml).unwrap(); @@ -233,6 +234,7 @@ profile = "codex-bedrock" provider.aws, Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: Some("us-west-2".to_string()), }) ); } @@ -248,7 +250,10 @@ fn test_create_amazon_bedrock_provider() { env_key_instructions: None, experimental_bearer_token: None, auth: None, - aws: Some(ModelProviderAwsAuthInfo { profile: None }), + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + }), wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -304,6 +309,7 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_profile_override ModelProviderInfo { aws: Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: Some("us-west-2".to_string()), }), ..ModelProviderInfo::default() }, @@ -315,6 +321,7 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_profile_override .expect("Amazon Bedrock provider should be built in") .aws = Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: Some("us-west-2".to_string()), }); assert_eq!( @@ -334,6 +341,7 @@ fn test_merge_configured_model_providers_rejects_amazon_bedrock_non_default_fiel name: "Custom Bedrock".to_string(), aws: Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: None, }), ..ModelProviderInfo::default() }, @@ -345,7 +353,7 @@ fn test_merge_configured_model_providers_rejects_amazon_bedrock_non_default_fiel configured_model_providers, ), Err( - "model_providers.amazon-bedrock only supports changing `aws.profile`; other non-default provider fields are not supported" + "model_providers.amazon-bedrock only supports changing `aws.profile` and `aws.region`; other non-default provider fields are not supported" .to_string() ) ); @@ -356,7 +364,10 @@ fn test_merge_configured_model_providers_allows_amazon_bedrock_default_fields() let configured_model_providers = std::collections::HashMap::from([( AMAZON_BEDROCK_PROVIDER_ID.to_string(), ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { profile: None }), + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + }), wire_api: WireApi::Responses, ..ModelProviderInfo::default() }, @@ -374,7 +385,10 @@ fn test_merge_configured_model_providers_allows_amazon_bedrock_default_fields() #[test] fn test_validate_provider_aws_rejects_conflicting_auth() { let provider = ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { profile: None }), + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + }), env_key: Some("AWS_BEARER_TOKEN_BEDROCK".to_string()), supports_websockets: false, ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) @@ -389,7 +403,10 @@ fn test_validate_provider_aws_rejects_conflicting_auth() { #[test] fn test_validate_provider_aws_rejects_websockets() { let provider = ModelProviderInfo { - aws: Some(ModelProviderAwsAuthInfo { profile: None }), + aws: Some(ModelProviderAwsAuthInfo { + profile: None, + region: None, + }), requires_openai_auth: false, supports_websockets: true, ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) diff --git a/codex-rs/model-provider/src/amazon_bedrock/auth.rs b/codex-rs/model-provider/src/amazon_bedrock/auth.rs index 3b3ebaed31db..7a5f08446301 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/auth.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/auth.rs @@ -7,7 +7,6 @@ use codex_aws_auth::AwsAuthConfig; use codex_aws_auth::AwsAuthContext; use codex_aws_auth::AwsAuthError; use codex_aws_auth::AwsRequestToSign; -use codex_aws_auth::region_from_bedrock_bearer_token; use codex_client::Request; use codex_client::RequestBody; use codex_client::RequestCompression; @@ -20,6 +19,7 @@ use tokio::sync::OnceCell; use crate::BearerAuthProvider; use super::mantle::aws_auth_config; +use super::mantle::region_from_config; const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK"; const LEGACY_SESSION_ID_HEADER: &str = "session_id"; @@ -37,8 +37,7 @@ enum BedrockAuthMethod { async fn resolve_auth_method(aws: &ModelProviderAwsAuthInfo) -> Result { if let Some(token) = bearer_token_from_env() { - let region = - region_from_bedrock_bearer_token(&token).map_err(aws_auth_error_to_codex_error)?; + let region = bearer_token_region_from_config(aws)?; return Ok(BedrockAuthMethod::EnvBearerToken { token, region }); } @@ -78,6 +77,16 @@ fn bearer_token_from_env() -> Option { .filter(|token| !token.is_empty()) } +fn bearer_token_region_from_config(aws: &ModelProviderAwsAuthInfo) -> Result { + region_from_config(aws).ok_or_else(|| { + CodexErr::Fatal( + "Amazon Bedrock bearer token auth requires \ +`model_providers.amazon-bedrock.aws.region`" + .to_string(), + ) + }) +} + fn aws_auth_error_to_codex_error(error: AwsAuthError) -> CodexErr { CodexErr::Fatal(format!("failed to resolve Amazon Bedrock auth: {error}")) } @@ -156,20 +165,14 @@ mod tests { use super::*; - fn bedrock_token_for_region(region: &str) -> String { - let encoded = match region { - "us-west-2" => { - "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZ1cy13ZXN0LTIlMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" - } - _ => panic!("test token fixture missing for {region}"), - }; - format!("bedrock-api-key-{encoded}") - } - #[test] - fn bedrock_bearer_auth_uses_token_region_and_header() { - let token = bedrock_token_for_region("us-west-2"); - let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); + fn bedrock_bearer_auth_uses_configured_region_and_header() { + let token = "bedrock-api-key-test".to_string(); + let region = bearer_token_region_from_config(&ModelProviderAwsAuthInfo { + profile: None, + region: Some(" us-west-2 ".to_string()), + }) + .expect("configured region should resolve"); let provider = BearerAuthProvider { token: Some(token), account_id: None, @@ -188,6 +191,21 @@ mod tests { ); } + #[test] + fn bedrock_bearer_auth_rejects_missing_configured_region() { + let err = bearer_token_region_from_config(&ModelProviderAwsAuthInfo { + profile: None, + region: None, + }) + .expect_err("missing region should fail"); + + assert_eq!( + err.to_string(), + "Fatal error: Amazon Bedrock bearer token auth requires \ +`model_providers.amazon-bedrock.aws.region`" + ); + } + #[test] fn bedrock_mantle_sigv4_strips_legacy_session_id_header() { let mut headers = HeaderMap::new(); diff --git a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs index a04aadf5dd85..bd53075955c9 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs @@ -22,10 +22,19 @@ const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 12] = [ pub(super) fn aws_auth_config(aws: &ModelProviderAwsAuthInfo) -> AwsAuthConfig { AwsAuthConfig { profile: aws.profile.clone(), + region: region_from_config(aws), service: BEDROCK_MANTLE_SERVICE_NAME.to_string(), } } +pub(super) fn region_from_config(aws: &ModelProviderAwsAuthInfo) -> Option { + aws.region + .as_deref() + .map(str::trim) + .filter(|region| !region.is_empty()) + .map(str::to_string) +} + pub(super) fn base_url(region: &str) -> Result { if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) { Ok(format!("https://bedrock-mantle.{region}.api.aws/v1")) @@ -65,9 +74,26 @@ mod tests { assert_eq!( aws_auth_config(&ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: None, }), AwsAuthConfig { profile: Some("codex-bedrock".to_string()), + region: None, + service: "bedrock-mantle".to_string(), + } + ); + } + + #[test] + fn aws_auth_config_uses_configured_region() { + assert_eq!( + aws_auth_config(&ModelProviderAwsAuthInfo { + profile: None, + region: Some(" us-west-2 ".to_string()), + }), + AwsAuthConfig { + profile: None, + region: Some("us-west-2".to_string()), service: "bedrock-mantle".to_string(), } ); diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs index 021afb83e508..a28262fb7d17 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mod.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -51,28 +51,16 @@ impl ModelProvider for AmazonBedrockModelProvider { #[cfg(test)] mod tests { - use codex_aws_auth::region_from_bedrock_bearer_token; use pretty_assertions::assert_eq; use super::*; - fn bedrock_token_for_region(region: &str) -> String { - let encoded = match region { - "eu-central-1" => { - "YmVkcm9jay5hbWF6b25hd3MuY29tLz9BY3Rpb249Q2FsbFdpdGhCZWFyZXJUb2tlbiZYLUFtei1DcmVkZW50aWFsPUFLSURFWEFNUExFJTJGMjAyNjA0MjAlMkZldS1jZW50cmFsLTElMkZiZWRyb2NrJTJGYXdzNF9yZXF1ZXN0JlZlcnNpb249MQ==" - } - _ => panic!("test token fixture missing for {region}"), - }; - format!("bedrock-api-key-{encoded}") - } - #[test] - fn api_provider_for_bedrock_bearer_token_uses_token_region_endpoint() { - let token = bedrock_token_for_region("eu-central-1"); - let region = region_from_bedrock_bearer_token(&token).expect("bearer token should resolve"); + 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(base_url(®ion).expect("supported region")); + api_provider_info.base_url = Some(base_url(region).expect("supported region")); let api_provider = api_provider_info .to_api_provider(/*auth_mode*/ None) .expect("api provider should build"); diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 462a27ef7256..3075c2a318a8 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -59,7 +59,10 @@ pub fn create_model_provider( let aws = provider_info .aws .clone() - .unwrap_or(ModelProviderAwsAuthInfo { profile: None }); + .unwrap_or(ModelProviderAwsAuthInfo { + profile: None, + region: None, + }); return Arc::new(AmazonBedrockModelProvider { info: provider_info, aws, @@ -143,6 +146,7 @@ mod tests { let provider = create_model_provider( ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo { profile: Some("codex-bedrock".to_string()), + region: None, })), Some(AuthManager::from_auth_for_testing(CodexAuth::from_api_key( "openai-api-key", @@ -151,23 +155,4 @@ mod tests { assert!(provider.auth_manager().is_none()); } - - #[test] - fn create_model_provider_does_not_select_bedrock_provider_for_custom_aws_provider() { - let provider = create_model_provider( - ModelProviderInfo { - name: "Custom".to_string(), - aws: Some(ModelProviderAwsAuthInfo { - profile: Some("codex-bedrock".to_string()), - }), - requires_openai_auth: false, - ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) - }, - Some(AuthManager::from_auth_for_testing(CodexAuth::from_api_key( - "custom-api-key", - ))), - ); - - assert!(provider.auth_manager().is_some()); - } } From 98df4aab9d2f2abf81790af734ef62c2fe121657 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Tue, 21 Apr 2026 17:23:56 -0700 Subject: [PATCH 11/11] changes --- codex-rs/codex-api/src/auth.rs | 11 +++++++++-- codex-rs/model-provider/src/amazon_bedrock/auth.rs | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/codex-rs/codex-api/src/auth.rs b/codex-rs/codex-api/src/auth.rs index b7fc24a99008..e1130c770740 100644 --- a/codex-rs/codex-api/src/auth.rs +++ b/codex-rs/codex-api/src/auth.rs @@ -34,12 +34,19 @@ pub trait AuthProvider: Send + Sync { /// used by telemetry and non-HTTP request paths. fn add_auth_headers(&self, headers: &mut HeaderMap); - /// Applies auth to a complete outbound request. + /// Applies auth to a complete outbound request and returns the request to send. + /// + /// The input `request` is moved into this method. Implementations may mutate + /// the owned request, or replace it entirely, before returning. /// /// Header-only auth providers can rely on the default implementation. /// Request-signing providers can override this to inspect the final URL, /// headers, and body bytes before the transport sends the request. - async fn apply_auth(&self, mut request: Request) -> Result { + /// + /// Callers must always use the returned request as authoritative. + /// If this returns [`AuthError`], the request should not be sent. + async fn apply_auth(&self, request: Request) -> Result { + let mut request = request; self.add_auth_headers(&mut request.headers); Ok(request) } diff --git a/codex-rs/model-provider/src/amazon_bedrock/auth.rs b/codex-rs/model-provider/src/amazon_bedrock/auth.rs index 7a5f08446301..7b6eb36b2e90 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/auth.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/auth.rs @@ -135,7 +135,8 @@ impl BedrockMantleSigV4AuthProvider { impl AuthProvider for BedrockMantleSigV4AuthProvider { fn add_auth_headers(&self, _headers: &mut HeaderMap) {} - async fn apply_auth(&self, mut request: Request) -> std::result::Result { + async fn apply_auth(&self, request: Request) -> std::result::Result { + let mut request = request; remove_headers_not_preserved_by_bedrock_mantle(&mut request.headers); let prepared = request.prepare_body_for_send().map_err(AuthError::Build)?; let context = self.context().await?;