feat: add AWS SigV4 auth for OpenAI-compatible model providers (#17820)

## Summary

Add first-class Amazon Bedrock Mantle provider support so Codex can keep
using its existing Responses API transport with OpenAI-compatible
AWS-hosted endpoints such as AOA/Mantle.

This is needed for the AWS launch path, where provider traffic should
authenticate with AWS credentials instead of OpenAI bearer credentials.
Requests are authenticated immediately before transport send, so SigV4
signs the final method, URL, headers, and body bytes that `reqwest` will
send.

## What Changed

- Added a new `codex-aws-auth` crate for loading AWS SDK config,
resolving credentials, and signing finalized HTTP requests with AWS
SigV4.
- Added a built-in `amazon-bedrock` provider that targets Bedrock Mantle
Responses endpoints, defaults to `us-east-1`, supports region/profile
overrides, disables WebSockets, and does not require OpenAI auth.
- Added Amazon Bedrock auth resolution in `codex-model-provider`: prefer
`AWS_BEARER_TOKEN_BEDROCK` when set, otherwise use AWS SDK credentials
and SigV4 signing.
- Added `AuthProvider::apply_auth` and `Request::prepare_body_for_send`
so request-signing providers can sign the exact outbound request after
JSON serialization/compression.
- Determine the region by taking the `aws.region` config first (required
for bearer token codepath), and fallback to SDK default region.

## Testing
Amazon Bedrock Mantle Responses paths:

- Built the local Codex binary with `cargo build`.
- Verified the custom proxy-backed `aws` provider using `env_key =
"AWS_BEARER_TOKEN_BEDROCK"` streamed raw `responses` output with
`response.output_text.delta`, `response.completed`, and `mantle-env-ok`.
- Verified a full `codex exec --profile aws` turn returned
`mantle-env-ok`.
- Confirmed the custom provider used the bearer env var, not AWS profile
auth: bogus `AWS_PROFILE` still passed, empty env var failed locally,
and malformed env var reached Mantle and failed with `401
invalid_api_key`.
- Verified built-in `amazon-bedrock` with `AWS_BEARER_TOKEN_BEDROCK` set
passed despite bogus AWS profiles, returning `amazon-bedrock-env-ok`.
- Verified built-in `amazon-bedrock` SDK/SigV4 auth passed with
`AWS_BEARER_TOKEN_BEDROCK` unset and temporary AWS session env
credentials, returning `amazon-bedrock-sdk-env-ok`.
This commit is contained in:
Celia Chen
2026-04-22 01:11:17 +00:00
committed by GitHub
parent e18fe7a07f
commit 1cd3ad1f49
25 changed files with 1676 additions and 94 deletions
+17 -7
View File
@@ -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<String>,
/// AWS region to use for provider-specific endpoints.
pub region: Option<String>,
}
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<String, ModelProviderInfo>,
configured_model_providers: HashMap<String, ModelProviderInfo>,
@@ -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);
@@ -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)