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-21 18:11:17 -07:00
committed by GitHub
parent e18fe7a07f
commit 1cd3ad1f49
25 changed files with 1676 additions and 94 deletions

View File

@@ -1,13 +1,55 @@
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<AuthError> 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);
/// 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.
///
/// 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<Request, AuthError> {
let mut request = request;
self.add_auth_headers(&mut request.headers);
Ok(request)
}
}
/// Shared auth handle passed through API clients.

View File

@@ -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;
@@ -55,7 +56,6 @@ impl<T: HttpTransport> EndpointSession<T> {
if let Some(body) = body {
req.body = Some(RequestBody::Json(body.clone()));
}
self.auth.add_auth_headers(&mut req.headers);
req
}
@@ -97,7 +97,14 @@ impl<T: HttpTransport> EndpointSession<T> {
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 +138,14 @@ impl<T: HttpTransport> EndpointSession<T> {
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?;

View File

@@ -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;