Skip to main content

opendev_http/adapters/bedrock/
mod.rs

1//! AWS Bedrock provider adapter.
2//!
3//! Transforms OpenAI Chat Completions payloads to Amazon Bedrock's
4//! `InvokeModel` format and converts responses back.
5//!
6//! Bedrock uses SigV4 request signing. Since `aws-sigv4` is not available
7//! as a dependency, the signing logic is stubbed with a TODO. In production,
8//! either add `aws-sigv4`/`aws-credential-types` crates or implement
9//! minimal HMAC-SHA256 signing with the `hmac` and `sha2` crates.
10//!
11//! Environment variables:
12//! - `AWS_ACCESS_KEY_ID` — IAM access key
13//! - `AWS_SECRET_ACCESS_KEY` — IAM secret key
14//! - `AWS_REGION` — AWS region (defaults to `us-east-1`)
15//! - `AWS_SESSION_TOKEN` — optional session token for temporary credentials
16
17mod request;
18mod response;
19
20use serde_json::{Value, json};
21
22/// Default AWS region when `AWS_REGION` is not set.
23const DEFAULT_REGION: &str = "us-east-1";
24
25/// Adapter for Amazon Bedrock's InvokeModel API.
26///
27/// Bedrock wraps foundation models behind a REST API at:
28/// `https://bedrock-runtime.{region}.amazonaws.com/model/{model_id}/invoke`
29///
30/// This adapter handles:
31/// - Converting Chat Completions messages to Bedrock's Anthropic-style format
32/// - Building the correct endpoint URL from region + model
33/// - SigV4 header generation (TODO: requires `hmac`/`sha2` crates)
34#[derive(Debug, Clone)]
35pub struct BedrockAdapter {
36    region: String,
37    model_id: String,
38    api_url: String,
39}
40
41impl BedrockAdapter {
42    /// Create a new Bedrock adapter for the given model.
43    ///
44    /// Reads `AWS_REGION` from the environment (defaults to `us-east-1`).
45    pub fn new(model_id: impl Into<String>) -> Self {
46        let model_id = model_id.into();
47        let region = std::env::var("AWS_REGION").unwrap_or_else(|_| DEFAULT_REGION.to_string());
48        let api_url = Self::build_url(&region, &model_id);
49        Self {
50            region,
51            model_id,
52            api_url,
53        }
54    }
55
56    /// Create a new Bedrock adapter with a custom region.
57    pub fn with_region(model_id: impl Into<String>, region: impl Into<String>) -> Self {
58        let model_id = model_id.into();
59        let region = region.into();
60        let api_url = Self::build_url(&region, &model_id);
61        Self {
62            region,
63            model_id,
64            api_url,
65        }
66    }
67
68    /// Build the Bedrock InvokeModel URL.
69    fn build_url(region: &str, model_id: &str) -> String {
70        format!("https://bedrock-runtime.{region}.amazonaws.com/model/{model_id}/invoke")
71    }
72
73    /// Get the configured AWS region.
74    pub fn region(&self) -> &str {
75        &self.region
76    }
77
78    /// Get the model ID.
79    pub fn model_id(&self) -> &str {
80        &self.model_id
81    }
82
83    /// Generate SigV4 authorization headers for the request.
84    ///
85    /// TODO: Implement SigV4 signing. Requires either:
86    /// - Adding `aws-sigv4` + `aws-credential-types` crates, or
87    /// - Adding `hmac` + `sha2` crates for minimal manual signing.
88    ///
89    /// For now, this returns empty headers. To use Bedrock in production,
90    /// implement the SigV4 signing algorithm:
91    /// 1. Create canonical request (method, URI, query, headers, payload hash)
92    /// 2. Create string-to-sign (algorithm, date, scope, canonical request hash)
93    /// 3. Derive signing key via HMAC-SHA256 chain (date → region → service → signing)
94    /// 4. Calculate signature = HMAC-SHA256(signing_key, string_to_sign)
95    /// 5. Build Authorization header
96    #[allow(dead_code)]
97    fn sigv4_headers(&self, _payload: &[u8]) -> Vec<(String, String)> {
98        let _access_key = std::env::var("AWS_ACCESS_KEY_ID").unwrap_or_default();
99        let _secret_key = std::env::var("AWS_SECRET_ACCESS_KEY").unwrap_or_default();
100        let _session_token = std::env::var("AWS_SESSION_TOKEN").ok();
101
102        // TODO: Implement SigV4 signing when `hmac` and `sha2` crates are available.
103        vec![
104            ("Content-Type".into(), "application/json".into()),
105            ("Accept".into(), "application/json".into()),
106        ]
107    }
108}
109
110#[async_trait::async_trait]
111impl super::base::ProviderAdapter for BedrockAdapter {
112    fn provider_name(&self) -> &str {
113        "bedrock"
114    }
115
116    fn convert_request(&self, mut payload: Value) -> Value {
117        // Strip internal reasoning effort field (Bedrock doesn't support it)
118        payload
119            .as_object_mut()
120            .map(|obj| obj.remove("_reasoning_effort"));
121
122        request::extract_system(&mut payload);
123        request::convert_tools(&mut payload);
124        request::convert_tool_messages(&mut payload);
125        request::ensure_max_tokens(&mut payload);
126
127        // Bedrock wraps the model in the URL, not the payload.
128        // Remove fields Bedrock does not accept.
129        if let Some(obj) = payload.as_object_mut() {
130            obj.remove("model");
131            obj.remove("n");
132            obj.remove("frequency_penalty");
133            obj.remove("presence_penalty");
134            obj.remove("logprobs");
135            obj.remove("stream");
136        }
137
138        // Set anthropic_version required by Bedrock's Anthropic models.
139        payload["anthropic_version"] = json!("bedrock-2023-05-31");
140
141        payload
142    }
143
144    fn convert_response(&self, response: Value) -> Value {
145        response::response_to_chat_completions(response, &self.model_id)
146    }
147
148    fn api_url(&self) -> &str {
149        &self.api_url
150    }
151
152    fn extra_headers(&self) -> Vec<(String, String)> {
153        // TODO: SigV4 headers should be generated per-request with the payload.
154        // The ProviderAdapter trait's `extra_headers()` is called without payload
155        // context, so full SigV4 signing would require a trait extension.
156        // For now, return content-type headers only.
157        vec![
158            ("Content-Type".into(), "application/json".into()),
159            ("Accept".into(), "application/json".into()),
160        ]
161    }
162}
163
164#[cfg(test)]
165mod tests;