1use super::cloud::{AwsBedrockConfig, InvokeRequest};
4use async_trait::async_trait;
5use futures::Stream;
6use serde::Serialize;
7use sha2::{Digest, Sha256};
8use std::{collections::BTreeMap, fmt, pin::Pin};
9
10#[derive(Clone)]
11pub struct RegisteredClient {
12 id: String,
13 secret: String,
14 pub expires_at: u64,
15}
16impl RegisteredClient {
17 pub fn new(id: impl Into<String>, secret: impl Into<String>, expires_at: u64) -> Self {
18 Self {
19 id: id.into(),
20 secret: secret.into(),
21 expires_at,
22 }
23 }
24 pub fn id(&self) -> &str {
25 &self.id
26 }
27 pub fn secret(&self) -> &str {
28 &self.secret
29 }
30}
31impl fmt::Debug for RegisteredClient {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.debug_struct("RegisteredClient")
34 .field("id", &"[REDACTED]")
35 .field("secret", &"[REDACTED]")
36 .field("expires_at", &self.expires_at)
37 .finish()
38 }
39}
40#[derive(Clone)]
41pub struct DeviceAuthorization {
42 device_code: String,
43 pub user_code: String,
44 pub verification_uri: String,
45 pub interval: u64,
46 pub expires_in: u64,
47}
48impl DeviceAuthorization {
49 pub fn new(
50 d: impl Into<String>,
51 u: impl Into<String>,
52 v: impl Into<String>,
53 interval: u64,
54 expires_in: u64,
55 ) -> Self {
56 Self {
57 device_code: d.into(),
58 user_code: u.into(),
59 verification_uri: v.into(),
60 interval,
61 expires_in,
62 }
63 }
64 pub fn device_code(&self) -> &str {
65 &self.device_code
66 }
67}
68impl fmt::Debug for DeviceAuthorization {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.write_str("DeviceAuthorization([REDACTED])")
71 }
72}
73#[derive(Clone)]
74pub struct SsoToken {
75 access: String,
76 refresh: Option<String>,
77 pub expires_in: u64,
78}
79impl SsoToken {
80 pub fn new(a: impl Into<String>, r: Option<&str>, e: u64) -> Self {
81 Self {
82 access: a.into(),
83 refresh: r.map(str::to_owned),
84 expires_in: e,
85 }
86 }
87 pub fn access(&self) -> &str {
88 &self.access
89 }
90 pub fn refresh(&self) -> Option<&str> {
91 self.refresh.as_deref()
92 }
93}
94impl fmt::Debug for SsoToken {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str("SsoToken([REDACTED])")
97 }
98}
99#[derive(Clone)]
100pub struct RoleCredentials {
101 access_key: String,
102 secret_key: String,
103 session_token: String,
104 pub expires_at: u64,
105}
106impl RoleCredentials {
107 pub fn new(a: impl Into<String>, s: impl Into<String>, t: impl Into<String>, e: u64) -> Self {
108 Self {
109 access_key: a.into(),
110 secret_key: s.into(),
111 session_token: t.into(),
112 expires_at: e,
113 }
114 }
115 pub fn access_key(&self) -> &str {
116 &self.access_key
117 }
118 pub fn secret_key(&self) -> &str {
119 &self.secret_key
120 }
121 pub fn session_token(&self) -> &str {
122 &self.session_token
123 }
124}
125impl fmt::Debug for RoleCredentials {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 f.write_str("RoleCredentials([REDACTED])")
128 }
129}
130#[derive(Debug, Clone)]
131pub struct Account {
132 pub id: String,
133 pub name: String,
134}
135#[derive(Debug, Clone)]
136pub struct Role {
137 pub name: String,
138}
139#[derive(Debug, Clone)]
140pub struct FoundationModel {
141 pub id: String,
142 pub name: String,
143 pub supports_text: bool,
144 pub supports_converse: bool,
145}
146impl FoundationModel {
147 pub fn new(i: impl Into<String>, n: impl Into<String>, t: bool, c: bool) -> Self {
148 Self {
149 id: i.into(),
150 name: n.into(),
151 supports_text: t,
152 supports_converse: c,
153 }
154 }
155}
156#[derive(Debug, Clone, Serialize)]
157pub struct CatalogEntry {
158 pub id: String,
159 pub display_name: String,
160 pub source: &'static str,
161 pub region: String,
162}
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
164pub struct Usage {
165 pub input_tokens: u64,
166 pub output_tokens: u64,
167}
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct ToolCall {
170 pub id: String,
171 pub name: String,
172 pub arguments: String,
173}
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct ConverseOutput {
176 pub text: String,
177 pub tool_calls: Vec<ToolCall>,
178 pub usage: Usage,
179}
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum ConverseEvent {
182 TextDelta(String),
183 ToolArguments { id: String, delta: String },
184 Usage(Usage),
185 Done,
186}
187#[derive(Debug, Clone, Copy)]
188pub enum TokenGrant<'a> {
189 DeviceCode(&'a str),
190 RefreshToken(&'a str),
191}
192#[derive(Debug, Clone, Copy)]
193pub enum Selection {
194 Explicit,
195}
196#[derive(Debug, thiserror::Error)]
197pub enum AwsError {
198 #[error("explicit selection required: {0}")]
199 SelectionRequired(&'static str),
200 #[error("authorization pending")]
201 AuthorizationPending,
202 #[error("authorization polling slowed down")]
203 SlowDown,
204 #[error("AWS authorization denied or expired")]
205 AuthorizationFailed,
206 #[error("invalid model id")]
207 InvalidModel,
208 #[error("AWS operation failed (details redacted)")]
209 Upstream,
210}
211
212#[derive(Clone)]
213pub struct SignedRequest {
214 pub method: &'static str,
215 pub host: String,
216 pub path: String,
217 pub body: Vec<u8>,
218 headers: BTreeMap<String, String>,
219}
220impl SignedRequest {
221 pub fn header(&self, n: &str) -> Option<&str> {
222 self.headers.get(n).map(String::as_str)
223 }
224 pub fn has_header(&self, n: &str) -> bool {
225 self.headers.contains_key(n)
226 }
227}
228impl fmt::Debug for SignedRequest {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 f.debug_struct("SignedRequest")
231 .field("method", &self.method)
232 .field("host", &self.host)
233 .field("path", &self.path)
234 .field("headers", &"[REDACTED]")
235 .finish()
236 }
237}
238
239pub type ConverseEventStream =
240 Pin<Box<dyn Stream<Item = Result<ConverseEvent, AwsError>> + Send + 'static>>;
241
242#[async_trait]
243pub trait AwsApi: Send + Sync + Clone + 'static {
244 async fn register_client(&self, region: &str) -> Result<RegisteredClient, AwsError>;
245 async fn start_device_authorization(
246 &self,
247 client: &RegisteredClient,
248 start_url: &str,
249 ) -> Result<DeviceAuthorization, AwsError>;
250 async fn create_token(
251 &self,
252 client: &RegisteredClient,
253 region: &str,
254 grant: TokenGrant<'_>,
255 ) -> Result<SsoToken, AwsError>;
256 async fn list_accounts(
257 &self,
258 region: &str,
259 access_token: &str,
260 ) -> Result<Vec<Account>, AwsError>;
261 async fn list_account_roles(
262 &self,
263 region: &str,
264 access_token: &str,
265 account: &str,
266 ) -> Result<Vec<Role>, AwsError>;
267 async fn get_role_credentials(
268 &self,
269 region: &str,
270 access_token: &str,
271 account: &str,
272 role: &str,
273 ) -> Result<RoleCredentials, AwsError>;
274 async fn list_foundation_models(
275 &self,
276 request: SignedRequest,
277 ) -> Result<Vec<FoundationModel>, AwsError>;
278 async fn converse(&self, request: SignedRequest) -> Result<ConverseOutput, AwsError>;
279 async fn converse_stream(
280 &self,
281 request: SignedRequest,
282 ) -> Result<ConverseEventStream, AwsError>;
283}
284#[derive(Debug, Clone, Serialize)]
285pub struct PublicContext {
286 pub sso_region: String,
287 pub account_id: String,
288 pub role_name: String,
289 pub bedrock_region: String,
290}
291pub struct AwsBedrockBroker<A: AwsApi> {
292 api: A,
293 config: AwsBedrockConfig,
294 credentials: RoleCredentials,
295}
296impl<A: AwsApi> AwsBedrockBroker<A> {
297 pub fn from_credentials(
301 api: A,
302 config: AwsBedrockConfig,
303 credentials: RoleCredentials,
304 ) -> Self {
305 Self {
306 api,
307 config,
308 credentials,
309 }
310 }
311
312 pub async fn login(api: A, config: AwsBedrockConfig, _: Selection) -> Result<Self, AwsError> {
313 let c = api.register_client(&config.sso_region).await?;
314 let d = api
315 .start_device_authorization(&c, &config.sso_start_url)
316 .await?;
317 let token = api
318 .create_token(
319 &c,
320 &config.sso_region,
321 TokenGrant::DeviceCode(&d.device_code),
322 )
323 .await?;
324 let accounts = api.list_accounts(&config.sso_region, &token.access).await?;
325 if accounts.len() != 1 {
326 return Err(AwsError::SelectionRequired("account"));
327 }
328 if accounts[0].id != config.account_id {
329 return Err(AwsError::SelectionRequired("configured account"));
330 }
331 let roles = api
332 .list_account_roles(&config.sso_region, &token.access, &config.account_id)
333 .await?;
334 if roles.len() != 1 || roles[0].name != config.role_name {
335 return Err(AwsError::SelectionRequired("role"));
336 }
337 let credentials = api
338 .get_role_credentials(
339 &config.sso_region,
340 &token.access,
341 &config.account_id,
342 &config.role_name,
343 )
344 .await?;
345 Ok(Self {
346 api,
347 config,
348 credentials,
349 })
350 }
351 pub fn public_context(&self) -> PublicContext {
352 PublicContext {
353 sso_region: self.config.sso_region.clone(),
354 account_id: self.config.account_id.clone(),
355 role_name: self.config.role_name.clone(),
356 bedrock_region: self.config.bedrock_region.clone(),
357 }
358 }
359 pub async fn catalog(&self) -> Result<Vec<CatalogEntry>, AwsError> {
360 let r = sign_bedrock_request(
361 &self.config.bedrock_region,
362 "GET",
363 "/foundation-models",
364 b"",
365 &self.credentials,
366 chrono::Utc::now().timestamp().max(0) as u64,
367 )?;
368 Ok(self
369 .api
370 .list_foundation_models(r)
371 .await?
372 .into_iter()
373 .filter(|m| m.supports_text && m.supports_converse)
374 .map(|m| CatalogEntry {
375 id: format!("aws-bedrock/{}", m.id),
376 display_name: m.name,
377 source: "dynamic",
378 region: self.config.bedrock_region.clone(),
379 })
380 .collect())
381 }
382 pub async fn converse(
383 &self,
384 model: &str,
385 request: InvokeRequest,
386 ) -> Result<ConverseOutput, AwsError> {
387 let id = valid_model(model)?;
388 let body = serde_json::to_vec(&request).map_err(|_| AwsError::Upstream)?;
389 let path = format!("/model/{id}/converse");
390 let r = sign_bedrock_request(
391 &self.config.bedrock_region,
392 "POST",
393 &path,
394 &body,
395 &self.credentials,
396 chrono::Utc::now().timestamp().max(0) as u64,
397 )?;
398 self.api.converse(r).await
399 }
400 pub async fn converse_stream(
401 &self,
402 model: &str,
403 request: InvokeRequest,
404 ) -> Result<ConverseEventStream, AwsError> {
405 let id = valid_model(model)?;
406 let body = serde_json::to_vec(&request).map_err(|_| AwsError::Upstream)?;
407 let path = format!("/model/{id}/converse-stream");
408 let r = sign_bedrock_request(
409 &self.config.bedrock_region,
410 "POST",
411 &path,
412 &body,
413 &self.credentials,
414 chrono::Utc::now().timestamp().max(0) as u64,
415 )?;
416 self.api.converse_stream(r).await
417 }
418}
419
420#[derive(Clone)]
421pub struct AwsHttpApi {
422 http: reqwest::Client,
423 sso_region: String,
424}
425impl AwsHttpApi {
426 pub fn new(http: reqwest::Client, sso_region: impl Into<String>) -> Self {
427 Self {
428 http,
429 sso_region: sso_region.into(),
430 }
431 }
432}
433fn aws_json_error(_status: reqwest::StatusCode) -> AwsError {
434 AwsError::Upstream
435}
436#[async_trait]
437impl AwsApi for AwsHttpApi {
438 async fn register_client(&self, region: &str) -> Result<RegisteredClient, AwsError> {
439 let u = format!("https://oidc.{region}.amazonaws.com/client/register");
440 let v:serde_json::Value=self.http.post(u).json(&serde_json::json!({"clientName":"synaps-cli","clientType":"public","scopes":["sso:account:access"]})).send().await.map_err(|_|AwsError::Upstream)?.error_for_status().map_err(|e|aws_json_error(e.status().unwrap_or(reqwest::StatusCode::BAD_GATEWAY)))?.json().await.map_err(|_|AwsError::Upstream)?;
441 Ok(RegisteredClient::new(
442 v["clientId"].as_str().ok_or(AwsError::Upstream)?,
443 v["clientSecret"].as_str().ok_or(AwsError::Upstream)?,
444 v["clientSecretExpiresAt"]
445 .as_u64()
446 .ok_or(AwsError::Upstream)?,
447 ))
448 }
449 async fn start_device_authorization(
450 &self,
451 c: &RegisteredClient,
452 start: &str,
453 ) -> Result<DeviceAuthorization, AwsError> {
454 let u = format!(
455 "https://oidc.{}.amazonaws.com/device_authorization",
456 self.sso_region
457 );
458 let v: serde_json::Value = self
459 .http
460 .post(u)
461 .json(&serde_json::json!({"clientId":c.id,"clientSecret":c.secret,"startUrl":start}))
462 .send()
463 .await
464 .map_err(|_| AwsError::Upstream)?
465 .error_for_status()
466 .map_err(|_| AwsError::Upstream)?
467 .json()
468 .await
469 .map_err(|_| AwsError::Upstream)?;
470 Ok(DeviceAuthorization::new(
471 v["deviceCode"].as_str().ok_or(AwsError::Upstream)?,
472 v["userCode"].as_str().ok_or(AwsError::Upstream)?,
473 v.get("verificationUriComplete")
474 .or_else(|| v.get("verificationUri"))
475 .and_then(|x| x.as_str())
476 .ok_or(AwsError::Upstream)?,
477 v["interval"].as_u64().unwrap_or(5),
478 v["expiresIn"].as_u64().ok_or(AwsError::Upstream)?,
479 ))
480 }
481 async fn create_token(
482 &self,
483 c: &RegisteredClient,
484 region: &str,
485 g: TokenGrant<'_>,
486 ) -> Result<SsoToken, AwsError> {
487 let (grant, key) = match g {
488 TokenGrant::DeviceCode(x) => (
489 "urn:ietf:params:oauth:grant-type:device_code",
490 ("deviceCode", x),
491 ),
492 TokenGrant::RefreshToken(x) => ("refresh_token", ("refreshToken", x)),
493 };
494 let mut body =
495 serde_json::json!({"clientId":c.id,"clientSecret":c.secret,"grantType":grant});
496 body[key.0] = key.1.into();
497 let r = self
498 .http
499 .post(format!("https://oidc.{region}.amazonaws.com/token"))
500 .json(&body)
501 .send()
502 .await
503 .map_err(|_| AwsError::Upstream)?;
504 if !r.status().is_success() {
505 let t = r.text().await.unwrap_or_default();
506 return Err(if t.contains("AuthorizationPending") {
507 AwsError::AuthorizationPending
508 } else if t.contains("SlowDown") {
509 AwsError::SlowDown
510 } else {
511 AwsError::AuthorizationFailed
512 });
513 }
514 let v: serde_json::Value = r.json().await.map_err(|_| AwsError::Upstream)?;
515 Ok(SsoToken::new(
516 v["accessToken"].as_str().ok_or(AwsError::Upstream)?,
517 v["refreshToken"].as_str(),
518 v["expiresIn"].as_u64().ok_or(AwsError::Upstream)?,
519 ))
520 }
521 async fn list_accounts(&self, r: &str, t: &str) -> Result<Vec<Account>, AwsError> {
522 let v: serde_json::Value = self
523 .http
524 .get(format!(
525 "https://portal.sso.{r}.amazonaws.com/assignment/accounts"
526 ))
527 .header("x-amz-sso_bearer_token", t)
528 .send()
529 .await
530 .map_err(|_| AwsError::Upstream)?
531 .error_for_status()
532 .map_err(|_| AwsError::Upstream)?
533 .json()
534 .await
535 .map_err(|_| AwsError::Upstream)?;
536 Ok(v["accountList"]
537 .as_array()
538 .ok_or(AwsError::Upstream)?
539 .iter()
540 .filter_map(|x| {
541 Some(Account {
542 id: x["accountId"].as_str()?.into(),
543 name: x["accountName"].as_str().unwrap_or("").into(),
544 })
545 })
546 .collect())
547 }
548 async fn list_account_roles(&self, r: &str, t: &str, a: &str) -> Result<Vec<Role>, AwsError> {
549 let v: serde_json::Value = self
550 .http
551 .get(format!(
552 "https://portal.sso.{r}.amazonaws.com/assignment/roles?account_id={a}"
553 ))
554 .header("x-amz-sso_bearer_token", t)
555 .send()
556 .await
557 .map_err(|_| AwsError::Upstream)?
558 .error_for_status()
559 .map_err(|_| AwsError::Upstream)?
560 .json()
561 .await
562 .map_err(|_| AwsError::Upstream)?;
563 Ok(v["roleList"]
564 .as_array()
565 .ok_or(AwsError::Upstream)?
566 .iter()
567 .filter_map(|x| {
568 Some(Role {
569 name: x["roleName"].as_str()?.into(),
570 })
571 })
572 .collect())
573 }
574 async fn get_role_credentials(
575 &self,
576 r: &str,
577 t: &str,
578 a: &str,
579 role: &str,
580 ) -> Result<RoleCredentials, AwsError> {
581 let v:serde_json::Value=self.http.get(format!("https://portal.sso.{r}.amazonaws.com/federation/credentials?account_id={a}&role_name={role}")).header("x-amz-sso_bearer_token",t).send().await.map_err(|_|AwsError::Upstream)?.error_for_status().map_err(|_|AwsError::Upstream)?.json().await.map_err(|_|AwsError::Upstream)?;
582 let c = &v["roleCredentials"];
583 Ok(RoleCredentials::new(
584 c["accessKeyId"].as_str().ok_or(AwsError::Upstream)?,
585 c["secretAccessKey"].as_str().ok_or(AwsError::Upstream)?,
586 c["sessionToken"].as_str().ok_or(AwsError::Upstream)?,
587 c["expiration"].as_u64().ok_or(AwsError::Upstream)?,
588 ))
589 }
590 async fn list_foundation_models(
591 &self,
592 q: SignedRequest,
593 ) -> Result<Vec<FoundationModel>, AwsError> {
594 let v = self.send_signed(q).await?;
595 Ok(v["modelSummaries"]
596 .as_array()
597 .ok_or(AwsError::Upstream)?
598 .iter()
599 .filter_map(|x| {
600 Some(FoundationModel::new(
601 x["modelId"].as_str()?,
602 x["modelName"].as_str().unwrap_or(""),
603 x["inputModalities"].as_array()?.iter().any(|v| v == "TEXT"),
604 x["inferenceTypesSupported"].as_array().is_some(),
605 ))
606 })
607 .collect())
608 }
609 async fn converse(&self, q: SignedRequest) -> Result<ConverseOutput, AwsError> {
610 let v = self.send_signed(q).await?;
611 Ok(ConverseOutput {
612 text: v
613 .pointer("/output/message/content/0/text")
614 .and_then(|x| x.as_str())
615 .unwrap_or("")
616 .into(),
617 tool_calls: vec![],
618 usage: Usage {
619 input_tokens: v["usage"]["inputTokens"].as_u64().unwrap_or(0),
620 output_tokens: v["usage"]["outputTokens"].as_u64().unwrap_or(0),
621 },
622 })
623 }
624 async fn converse_stream(&self, q: SignedRequest) -> Result<ConverseEventStream, AwsError> {
625 use futures::StreamExt;
626 let url = format!("https://{}{}", q.host, q.path);
627 let mut request = self.http.post(url).body(q.body);
628 for (name, value) in q.headers {
629 request = request.header(name, value);
630 }
631 let response = request
632 .send()
633 .await
634 .map_err(|_| AwsError::Upstream)?
635 .error_for_status()
636 .map_err(|_| AwsError::Upstream)?;
637 let chunks = response.bytes_stream();
638 const MAX_FRAME: usize = 1024 * 1024;
642 let stream = futures::stream::unfold(
643 (Box::pin(chunks), Vec::<u8>::new(), false),
644 |(mut chunks, mut buffer, done)| async move {
645 if done {
646 return None;
647 }
648 loop {
649 if buffer.len() >= 4 {
650 let total = u32::from_be_bytes(buffer[..4].try_into().ok()?) as usize;
651 if !(16..=MAX_FRAME).contains(&total) {
652 return Some((Err(AwsError::Upstream), (chunks, buffer, true)));
653 }
654 if buffer.len() >= total {
655 let frame: Vec<u8> = buffer.drain(..total).collect();
656 let event = decode_event_frame(&frame);
657 let terminal = matches!(event, Ok(Some(ConverseEvent::Done)) | Err(_));
658 match event {
659 Ok(Some(event)) => {
660 return Some((Ok(event), (chunks, buffer, terminal)))
661 }
662 Ok(None) => continue,
663 Err(error) => return Some((Err(error), (chunks, buffer, true))),
664 }
665 }
666 }
667 match chunks.next().await {
668 Some(Ok(chunk)) if buffer.len() + chunk.len() <= MAX_FRAME => {
669 buffer.extend_from_slice(&chunk)
670 }
671 Some(_) => return Some((Err(AwsError::Upstream), (chunks, buffer, true))),
672 None => return Some((Err(AwsError::Upstream), (chunks, buffer, true))),
673 }
674 }
675 },
676 );
677 Ok(Box::pin(stream))
678 }
679}
680
681fn decode_event_frame(bytes: &[u8]) -> Result<Option<ConverseEvent>, AwsError> {
682 if bytes.len() < 16 {
683 return Err(AwsError::Upstream);
684 }
685 let total = u32::from_be_bytes(bytes[0..4].try_into().unwrap()) as usize;
686 let headers = u32::from_be_bytes(bytes[4..8].try_into().unwrap()) as usize;
687 if total != bytes.len()
688 || headers > total - 16
689 || crc32(&bytes[..8]) != u32::from_be_bytes(bytes[8..12].try_into().unwrap())
690 || crc32(&bytes[..total - 4]) != u32::from_be_bytes(bytes[total - 4..].try_into().unwrap())
691 {
692 return Err(AwsError::Upstream);
693 }
694 let header_bytes = &bytes[12..12 + headers];
695 let payload = &bytes[12 + headers..total - 4];
696 let value: serde_json::Value =
697 serde_json::from_slice(payload).map_err(|_| AwsError::Upstream)?;
698 validate_event_headers(header_bytes, &value)?;
699 if let Some(delta) = value
700 .pointer("/contentBlockDelta/delta/text")
701 .and_then(|v| v.as_str())
702 {
703 Ok(Some(ConverseEvent::TextDelta(delta.to_owned())))
704 } else if let Some(delta) = value
705 .pointer("/contentBlockDelta/delta/toolUse/input")
706 .and_then(|v| v.as_str())
707 {
708 Ok(Some(ConverseEvent::ToolArguments {
709 id: value
710 .pointer("/contentBlockDelta/delta/toolUse/toolUseId")
711 .and_then(|v| v.as_str())
712 .unwrap_or_default()
713 .to_owned(),
714 delta: delta.to_owned(),
715 }))
716 } else if let Some(usage) = value.pointer("/metadata/usage") {
717 Ok(Some(ConverseEvent::Usage(Usage {
718 input_tokens: usage["inputTokens"].as_u64().unwrap_or(0),
719 output_tokens: usage["outputTokens"].as_u64().unwrap_or(0),
720 })))
721 } else if value.get("messageStop").is_some() {
722 Ok(Some(ConverseEvent::Done))
723 } else if value.get("internalServerException").is_some()
724 || value.get("modelStreamErrorException").is_some()
725 {
726 Err(AwsError::Upstream)
727 } else {
728 Ok(None)
729 }
730}
731
732fn validate_event_headers(bytes: &[u8], value: &serde_json::Value) -> Result<(), AwsError> {
733 let mut cursor = 0;
734 let mut values = std::collections::HashMap::new();
735 while cursor < bytes.len() {
736 let name_len = *bytes.get(cursor).ok_or(AwsError::Upstream)? as usize;
737 cursor += 1;
738 let name = std::str::from_utf8(
739 bytes
740 .get(cursor..cursor + name_len)
741 .ok_or(AwsError::Upstream)?,
742 )
743 .map_err(|_| AwsError::Upstream)?;
744 cursor += name_len;
745 if bytes.get(cursor) != Some(&7) {
746 return Err(AwsError::Upstream);
747 }
748 cursor += 1;
749 let len = u16::from_be_bytes(
750 bytes
751 .get(cursor..cursor + 2)
752 .ok_or(AwsError::Upstream)?
753 .try_into()
754 .unwrap(),
755 ) as usize;
756 cursor += 2;
757 let text = std::str::from_utf8(bytes.get(cursor..cursor + len).ok_or(AwsError::Upstream)?)
758 .map_err(|_| AwsError::Upstream)?;
759 cursor += len;
760 if values.insert(name, text).is_some() {
761 return Err(AwsError::Upstream);
762 }
763 }
764 if values.get(":content-type") != Some(&"application/json") {
765 return Err(AwsError::Upstream);
766 }
767 let message = *values.get(":message-type").ok_or(AwsError::Upstream)?;
768 let kind = match message {
769 "event" => *values.get(":event-type").ok_or(AwsError::Upstream)?,
770 "exception" => *values.get(":exception-type").ok_or(AwsError::Upstream)?,
771 _ => return Err(AwsError::Upstream),
772 };
773 if values.contains_key(if message == "event" {
774 ":exception-type"
775 } else {
776 ":event-type"
777 }) || value.get(kind).is_none()
778 {
779 return Err(AwsError::Upstream);
780 }
781 Ok(())
782}
783
784pub fn decode_converse_stream(mut bytes: &[u8]) -> Result<Vec<ConverseEvent>, AwsError> {
788 let mut out = Vec::new();
789 while !bytes.is_empty() {
790 if bytes.len() < 16 {
791 return Err(AwsError::Upstream);
792 }
793 let total = u32::from_be_bytes(bytes[0..4].try_into().unwrap()) as usize;
794 let headers = u32::from_be_bytes(bytes[4..8].try_into().unwrap()) as usize;
795 if total < 16 || total > bytes.len() || headers > total - 16 {
796 return Err(AwsError::Upstream);
797 }
798 if crc32(&bytes[..8]) != u32::from_be_bytes(bytes[8..12].try_into().unwrap())
799 || crc32(&bytes[..total - 4])
800 != u32::from_be_bytes(bytes[total - 4..total].try_into().unwrap())
801 {
802 return Err(AwsError::Upstream);
803 }
804 match decode_event_frame(&bytes[..total])? {
805 Some(ConverseEvent::Done) if out.iter().any(|e| matches!(e, ConverseEvent::Done)) => {
806 return Err(AwsError::Upstream);
807 }
808 Some(event) => out.push(event),
809 None => {}
810 }
811 bytes = &bytes[total..];
812 }
813 if !matches!(out.last(), Some(ConverseEvent::Done)) {
814 return Err(AwsError::Upstream);
816 }
817 if out
818 .iter()
819 .filter(|e| matches!(e, ConverseEvent::Done))
820 .count()
821 != 1
822 {
823 return Err(AwsError::Upstream);
824 }
825 Ok(out)
826}
827
828fn crc32(bytes: &[u8]) -> u32 {
829 let mut crc = !0u32;
830 for byte in bytes {
831 crc ^= *byte as u32;
832 for _ in 0..8 {
833 crc = if crc & 1 != 0 {
834 (crc >> 1) ^ 0xedb8_8320
835 } else {
836 crc >> 1
837 };
838 }
839 }
840 !crc
841}
842impl AwsHttpApi {
843 async fn send_signed(&self, q: SignedRequest) -> Result<serde_json::Value, AwsError> {
844 let url = format!("https://{}{}", q.host, q.path);
845 let mut b = match q.method {
846 "GET" => self.http.get(url),
847 _ => self.http.post(url).body(q.body),
848 };
849 for (k, v) in q.headers {
850 b = b.header(k, v)
851 }
852 b.send()
853 .await
854 .map_err(|_| AwsError::Upstream)?
855 .error_for_status()
856 .map_err(|_| AwsError::Upstream)?
857 .json()
858 .await
859 .map_err(|_| AwsError::Upstream)
860 }
861}
862
863fn valid_model(v: &str) -> Result<&str, AwsError> {
864 let v = v
865 .strip_prefix("aws-bedrock/")
866 .ok_or(AwsError::InvalidModel)?;
867 if v.is_empty()
868 || v.contains('/')
869 || !v
870 .bytes()
871 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-' | b'_' | b':'))
872 {
873 Err(AwsError::InvalidModel)
874 } else {
875 Ok(v)
876 }
877}
878fn hmac(key: &[u8], data: &[u8]) -> Vec<u8> {
879 let mut k = [0u8; 64];
880 if key.len() > 64 {
881 k[..32].copy_from_slice(&Sha256::digest(key))
882 } else {
883 k[..key.len()].copy_from_slice(key)
884 }
885 let mut i = [0x36; 64];
886 let mut o = [0x5c; 64];
887 for x in 0..64 {
888 i[x] ^= k[x];
889 o[x] ^= k[x]
890 }
891 let inner = Sha256::new().chain_update(i).chain_update(data).finalize();
892 Sha256::new()
893 .chain_update(o)
894 .chain_update(inner)
895 .finalize()
896 .to_vec()
897}
898fn hex(v: impl AsRef<[u8]>) -> String {
899 v.as_ref().iter().map(|b| format!("{b:02x}")).collect()
900}
901pub fn sign_bedrock_request(
902 region: &str,
903 method: &str,
904 path: &str,
905 body: &[u8],
906 c: &RoleCredentials,
907 epoch: u64,
908) -> Result<SignedRequest, AwsError> {
909 if method != "GET" && method != "POST" {
910 return Err(AwsError::Upstream);
911 }
912 let dt = chrono::DateTime::from_timestamp(epoch as i64, 0).ok_or(AwsError::Upstream)?;
913 let date = dt.format("%Y%m%d").to_string();
914 let amz = dt.format("%Y%m%dT%H%M%SZ").to_string();
915 let runtime = path.starts_with("/model/");
916 let host = if runtime {
917 format!("bedrock-runtime.{region}.amazonaws.com")
918 } else {
919 format!("bedrock.{region}.amazonaws.com")
920 };
921 let payload = hex(Sha256::digest(body));
922 let content_type = "application/json";
923 let accepts_eventstream = runtime && path.ends_with("converse-stream");
924 let canonical=format!("{method}\n{path}\n\ncontent-type:{content_type}\nhost:{host}\nx-amz-content-sha256:{payload}\nx-amz-date:{amz}\nx-amz-security-token:{}\n\ncontent-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token\n{payload}",c.session_token);
925 let scope = format!("{date}/{region}/bedrock/aws4_request");
926 let sts = format!(
927 "AWS4-HMAC-SHA256\n{amz}\n{scope}\n{}",
928 hex(Sha256::digest(canonical))
929 );
930 let kd = hmac(format!("AWS4{}", c.secret_key).as_bytes(), date.as_bytes());
931 let kr = hmac(&kd, region.as_bytes());
932 let ks = hmac(&kr, b"bedrock");
933 let key = hmac(&ks, b"aws4_request");
934 let sig = hex(hmac(&key, sts.as_bytes()));
935 let mut headers = BTreeMap::new();
936 headers.insert("content-type".into(), content_type.into());
937 if accepts_eventstream {
938 headers.insert("accept".into(), "application/vnd.amazon.eventstream".into());
939 }
940 headers.insert("host".into(), host.clone());
941 headers.insert("x-amz-content-sha256".into(), payload);
942 headers.insert("x-amz-date".into(), amz);
943 headers.insert("x-amz-security-token".into(), c.session_token.clone());
944 headers.insert("authorization".into(),format!("AWS4-HMAC-SHA256 Credential={}/{scope}, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature={sig}",c.access_key));
945 Ok(SignedRequest {
946 method: if method == "GET" { "GET" } else { "POST" },
947 host,
948 path: path.into(),
949 body: body.into(),
950 headers,
951 })
952}