1use crate::error::{ApiErrorResponse, Error, Result};
4use crate::streaming::StreamEvent;
5use crate::types::{MessagesRequest, MessagesResponse, RateLimitInfo};
6use eventsource_stream::Eventsource;
7use futures::{Stream, StreamExt, TryStreamExt};
8use reqwest::{Client, StatusCode};
9use std::pin::Pin;
10use tracing::{debug, instrument};
11
12#[cfg(feature = "bedrock")]
13use aws_sdk_bedrockruntime::Client as BedrockClient;
14
15const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
17
18const TOKEN_COUNT_URL: &str = "https://api.anthropic.com/v1/messages/count_tokens";
20
21const API_VERSION: &str = "2023-06-01";
23
24pub enum ClaudeBackend {
26 Anthropic { api_key: String },
28
29 #[cfg(feature = "bedrock")]
31 Bedrock {
32 region: String,
33 bedrock_client: BedrockClient,
34 },
35}
36
37pub struct ClaudeClient {
68 http: Client,
69 backend: ClaudeBackend,
70 api_version: String,
71}
72
73fn parse_rate_limit_headers(headers: &reqwest::header::HeaderMap) -> RateLimitInfo {
74 RateLimitInfo {
75 requests_remaining: headers
76 .get("anthropic-ratelimit-requests-remaining")
77 .and_then(|v| v.to_str().ok())
78 .and_then(|s| s.parse().ok()),
79 tokens_remaining: headers
80 .get("anthropic-ratelimit-tokens-remaining")
81 .and_then(|v| v.to_str().ok())
82 .and_then(|s| s.parse().ok()),
83 requests_reset: headers
84 .get("anthropic-ratelimit-requests-reset")
85 .and_then(|v| v.to_str().ok())
86 .map(String::from),
87 tokens_reset: headers
88 .get("anthropic-ratelimit-tokens-reset")
89 .and_then(|v| v.to_str().ok())
90 .map(String::from),
91 }
92}
93
94impl ClaudeClient {
95 pub fn anthropic(api_key: impl Into<String>) -> Self {
105 Self {
106 http: Client::new(),
107 backend: ClaudeBackend::Anthropic {
108 api_key: api_key.into(),
109 },
110 api_version: API_VERSION.to_string(),
111 }
112 }
113
114 #[cfg(feature = "bedrock")]
134 pub async fn bedrock(region: impl Into<String>) -> Result<Self> {
135 let region = region.into();
136
137 let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
139 let bedrock_config = aws_sdk_bedrockruntime::config::Builder::from(&config)
140 .region(aws_sdk_bedrockruntime::config::Region::new(region.clone()))
141 .build();
142
143 let bedrock_client = BedrockClient::from_conf(bedrock_config);
145
146 Ok(Self {
147 http: Client::new(),
148 backend: ClaudeBackend::Bedrock {
149 region,
150 bedrock_client,
151 },
152 api_version: API_VERSION.to_string(),
153 })
154 }
155
156 #[instrument(skip(self, request), fields(model = %request.model))]
180 pub async fn send_message(&self, request: MessagesRequest) -> Result<MessagesResponse> {
181 match &self.backend {
182 ClaudeBackend::Anthropic { .. } => self.send_anthropic(request).await,
183 #[cfg(feature = "bedrock")]
184 ClaudeBackend::Bedrock { .. } => self.send_bedrock(request).await,
185 }
186 }
187
188 async fn send_anthropic(&self, request: MessagesRequest) -> Result<MessagesResponse> {
190 let api_key = match &self.backend {
191 ClaudeBackend::Anthropic { api_key } => api_key,
192 #[allow(unreachable_patterns)]
193 _ => unreachable!("send_anthropic called with non-Anthropic backend"),
194 };
195
196 debug!("Sending message to Anthropic API");
197
198 let mut request = request;
200 request.stream = Some(false);
201
202 let response = self
203 .http
204 .post(ANTHROPIC_API_URL)
205 .header("x-api-key", api_key)
206 .header("anthropic-version", &self.api_version)
207 .header("content-type", "application/json")
208 .json(&request)
209 .send()
210 .await?;
211
212 let status = response.status();
213 debug!("Received response with status: {}", status);
214
215 match status {
217 StatusCode::OK => {
218 let headers = response.headers().clone();
219 let mut messages_response: MessagesResponse = response.json().await?;
220 messages_response.rate_limit_info = Some(parse_rate_limit_headers(&headers));
221 Ok(messages_response)
222 }
223 StatusCode::TOO_MANY_REQUESTS => {
224 let retry_after = response
226 .headers()
227 .get("retry-after")
228 .and_then(|h| h.to_str().ok())
229 .and_then(|s| s.parse().ok());
230
231 let error_body = response.text().await.unwrap_or_default();
232 Err(Error::RateLimit {
233 retry_after,
234 message: error_body,
235 })
236 }
237 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
238 let error_body = response.text().await.unwrap_or_default();
239 Err(Error::Authentication(error_body))
240 }
241 StatusCode::BAD_REQUEST => {
242 let error_text = response.text().await.unwrap_or_default();
244 if let Ok(api_error) = serde_json::from_str::<ApiErrorResponse>(&error_text) {
245 Err(Error::Api {
246 status: status.as_u16(),
247 message: api_error.error.message,
248 error_type: Some(api_error.error.error_type),
249 })
250 } else {
251 Err(Error::InvalidRequest(error_text))
252 }
253 }
254 _ if status.is_server_error() => {
255 let error_body = response.text().await.unwrap_or_default();
256 Err(Error::Server {
257 status: status.as_u16(),
258 message: error_body,
259 })
260 }
261 _ => {
262 let error_body = response.text().await.unwrap_or_default();
263 Err(Error::Api {
264 status: status.as_u16(),
265 message: error_body,
266 error_type: None,
267 })
268 }
269 }
270 }
271
272 #[cfg(feature = "bedrock")]
274 async fn send_bedrock(&self, request: MessagesRequest) -> Result<MessagesResponse> {
275 let (bedrock_client, model_id) = match &self.backend {
276 ClaudeBackend::Bedrock { bedrock_client, .. } => {
277 let model_id = self.get_bedrock_model_id(&request.model)?;
278 (bedrock_client, model_id)
279 }
280 _ => unreachable!("send_bedrock called with non-Bedrock backend"),
281 };
282
283 debug!("Sending message to AWS Bedrock");
284
285 let body = serde_json::to_string(&request)?;
287
288 let response = bedrock_client
290 .invoke_model()
291 .model_id(&model_id)
292 .content_type("application/json")
293 .body(aws_sdk_bedrockruntime::primitives::Blob::new(
294 body.as_bytes(),
295 ))
296 .send()
297 .await
298 .map_err(|e| Error::Network(format!("Bedrock API call failed: {}", e)))?;
299
300 let response_bytes = response.body().as_ref();
302 let messages_response: MessagesResponse = serde_json::from_slice(response_bytes)?;
303
304 Ok(messages_response)
305 }
306
307 #[cfg(feature = "bedrock")]
309 fn get_bedrock_model_id(&self, model: &str) -> Result<String> {
310 if model.starts_with("anthropic.")
312 || model.starts_with("global.")
313 || model.starts_with("us.")
314 || model.starts_with("eu.")
315 || model.starts_with("ap.")
316 {
317 return Ok(model.to_string());
318 }
319
320 if let Some(model_info) = crate::models::get_model_by_anthropic_id(model) {
322 if let Some(bedrock_id) = model_info.bedrock_id {
323 return Ok(bedrock_id.to_string());
324 }
325 }
326
327 Ok(model.to_string())
329 }
330
331 #[instrument(skip(self, request), fields(model = %request.model))]
367 pub async fn send_streaming(
368 &self,
369 request: MessagesRequest,
370 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send>>> {
371 match &self.backend {
372 ClaudeBackend::Anthropic { .. } => self.send_streaming_anthropic(request).await,
373 #[cfg(feature = "bedrock")]
374 ClaudeBackend::Bedrock { .. } => self.send_streaming_bedrock(request).await,
375 }
376 }
377
378 async fn send_streaming_anthropic(
380 &self,
381 request: MessagesRequest,
382 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send>>> {
383 let api_key = match &self.backend {
384 ClaudeBackend::Anthropic { api_key } => api_key,
385 #[allow(unreachable_patterns)]
386 _ => unreachable!("send_streaming_anthropic called with non-Anthropic backend"),
387 };
388
389 debug!("Sending streaming message to Anthropic API");
390
391 let mut request = request;
393 request.stream = Some(true);
394
395 let response = self
396 .http
397 .post(ANTHROPIC_API_URL)
398 .header("x-api-key", api_key)
399 .header("anthropic-version", &self.api_version)
400 .header("content-type", "application/json")
401 .json(&request)
402 .send()
403 .await?;
404
405 let status = response.status();
406 debug!("Received streaming response with status: {}", status);
407
408 if !status.is_success() {
410 return Err(self.handle_error_response(status, response).await);
411 }
412
413 let byte_stream = response.bytes_stream();
415 let event_stream = byte_stream.eventsource();
416
417 let stream = event_stream.map(|result| {
419 let event = result.map_err(|e| Error::StreamParse(e.to_string()))?;
420
421 if event.data.is_empty() {
423 return Ok(None);
424 }
425
426 let stream_event = match event.event.as_str() {
428 "ping" => Some(StreamEvent::Ping),
429 "error" => {
430 let error: crate::streaming::StreamError = serde_json::from_str(&event.data)
431 .map_err(|e| Error::StreamParse(e.to_string()))?;
432 Some(StreamEvent::Error { error })
433 }
434 _ => {
435 Some(
438 serde_json::from_str::<StreamEvent>(&event.data).map_err(|e| {
439 Error::StreamParse(format!(
440 "Failed to parse event '{}': {}",
441 event.event, e
442 ))
443 })?,
444 )
445 }
446 };
447
448 Ok(stream_event)
449 });
450
451 let filtered_stream = stream.try_filter_map(|opt| async move { Ok(opt) });
453
454 Ok(Box::pin(filtered_stream))
455 }
456
457 #[cfg(feature = "bedrock")]
459 async fn send_streaming_bedrock(
460 &self,
461 request: MessagesRequest,
462 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send>>> {
463 let (bedrock_client, model_id) = match &self.backend {
464 ClaudeBackend::Bedrock { bedrock_client, .. } => {
465 let model_id = self.get_bedrock_model_id(&request.model)?;
466 (bedrock_client, model_id)
467 }
468 _ => unreachable!("send_streaming_bedrock called with non-Bedrock backend"),
469 };
470
471 debug!("Sending streaming message to AWS Bedrock");
472
473 let mut request = request;
475 request.stream = Some(true);
476
477 let body = serde_json::to_string(&request)?;
479
480 let response = bedrock_client
482 .invoke_model_with_response_stream()
483 .model_id(&model_id)
484 .content_type("application/json")
485 .body(aws_sdk_bedrockruntime::primitives::Blob::new(
486 body.as_bytes(),
487 ))
488 .send()
489 .await
490 .map_err(|e| Error::Network(format!("Bedrock streaming API call failed: {}", e)))?;
491
492 let mut event_stream = response.body;
494
495 let stream = async_stream::stream! {
497 loop {
498 match event_stream.recv().await {
499 Ok(Some(event)) => {
500 if let aws_sdk_bedrockruntime::types::ResponseStream::Chunk(payload) = event {
502 let bytes = payload.bytes().ok_or_else(|| {
503 Error::StreamParse("Bedrock chunk missing bytes".into())
504 })?;
505
506 let json_str = std::str::from_utf8(bytes.as_ref())
507 .map_err(|e| Error::StreamParse(format!("Invalid UTF-8: {}", e)))?;
508
509 let stream_event: StreamEvent = serde_json::from_str(json_str)
511 .map_err(|e| Error::StreamParse(format!("Failed to parse Bedrock event: {}", e)))?;
512
513 yield Ok(stream_event);
514 }
515 }
517 Ok(None) => break, Err(e) => {
519 yield Err(Error::StreamParse(format!("Bedrock stream error: {}", e)));
520 break;
521 }
522 }
523 }
524 };
525
526 Ok(Box::pin(stream))
527 }
528
529 async fn handle_error_response(
531 &self,
532 status: StatusCode,
533 response: reqwest::Response,
534 ) -> Error {
535 match status {
536 StatusCode::TOO_MANY_REQUESTS => {
537 let retry_after = response
538 .headers()
539 .get("retry-after")
540 .and_then(|h| h.to_str().ok())
541 .and_then(|s| s.parse().ok());
542
543 let error_body = response.text().await.unwrap_or_default();
544 Error::RateLimit {
545 retry_after,
546 message: error_body,
547 }
548 }
549 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
550 let error_body = response.text().await.unwrap_or_default();
551 Error::Authentication(error_body)
552 }
553 StatusCode::BAD_REQUEST => {
554 let error_text = response.text().await.unwrap_or_default();
555 if let Ok(api_error) = serde_json::from_str::<ApiErrorResponse>(&error_text) {
556 Error::Api {
557 status: status.as_u16(),
558 message: api_error.error.message,
559 error_type: Some(api_error.error.error_type),
560 }
561 } else {
562 Error::InvalidRequest(error_text)
563 }
564 }
565 _ if status.is_server_error() => {
566 let error_body = response.text().await.unwrap_or_default();
567 Error::Server {
568 status: status.as_u16(),
569 message: error_body,
570 }
571 }
572 _ => {
573 let error_body = response.text().await.unwrap_or_default();
574 Error::Api {
575 status: status.as_u16(),
576 message: error_body,
577 error_type: None,
578 }
579 }
580 }
581 }
582
583 pub async fn count_tokens(&self, request: MessagesRequest) -> Result<crate::types::TokenCount> {
608 match &self.backend {
609 ClaudeBackend::Anthropic { api_key } => {
610 let response = self
611 .http
612 .post(TOKEN_COUNT_URL)
613 .header("x-api-key", api_key)
614 .header("anthropic-version", &self.api_version)
615 .header("content-type", "application/json")
616 .json(&request)
617 .send()
618 .await?;
619
620 let status = response.status();
621 if !status.is_success() {
622 return Err(self.handle_error_response(status, response).await);
623 }
624
625 let token_count: crate::types::TokenCount = response.json().await?;
626 Ok(token_count)
627 }
628 #[cfg(feature = "bedrock")]
629 ClaudeBackend::Bedrock { .. } => Err(crate::error::Error::InvalidRequest(
630 "Token counting endpoint is not available for Bedrock".into(),
631 )),
632 }
633 }
634
635 pub async fn send_message_with_retry(
661 &self,
662 request: MessagesRequest,
663 config: crate::retry::RetryConfig,
664 ) -> Result<MessagesResponse> {
665 crate::retry::retry_with_backoff(config, || async {
666 self.send_message(request.clone()).await
667 })
668 .await
669 }
670
671 pub async fn send_streaming_with_retry(
675 &self,
676 request: MessagesRequest,
677 config: crate::retry::RetryConfig,
678 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send>>> {
679 crate::retry::retry_with_backoff(config, || async {
680 self.send_streaming(request.clone()).await
681 })
682 .await
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689
690 #[test]
691 fn test_parse_rate_limit_headers() {
692 use reqwest::header::{HeaderMap, HeaderValue};
693
694 let mut headers = HeaderMap::new();
695 headers.insert(
696 "anthropic-ratelimit-requests-remaining",
697 HeaderValue::from_static("95"),
698 );
699 headers.insert(
700 "anthropic-ratelimit-tokens-remaining",
701 HeaderValue::from_static("49000"),
702 );
703 headers.insert(
704 "anthropic-ratelimit-requests-reset",
705 HeaderValue::from_static("2025-01-01T00:01:00Z"),
706 );
707 headers.insert(
708 "anthropic-ratelimit-tokens-reset",
709 HeaderValue::from_static("2025-01-01T00:01:00Z"),
710 );
711
712 let info = parse_rate_limit_headers(&headers);
713 assert_eq!(info.requests_remaining, Some(95));
714 assert_eq!(info.tokens_remaining, Some(49000));
715 assert_eq!(info.requests_reset.as_deref(), Some("2025-01-01T00:01:00Z"));
716 assert_eq!(info.tokens_reset.as_deref(), Some("2025-01-01T00:01:00Z"));
717 }
718
719 #[test]
720 fn test_parse_rate_limit_headers_empty() {
721 let headers = reqwest::header::HeaderMap::new();
722 let info = parse_rate_limit_headers(&headers);
723 assert!(info.requests_remaining.is_none());
724 assert!(info.tokens_remaining.is_none());
725 }
726
727 #[test]
728 fn test_token_count_url() {
729 assert_eq!(
730 TOKEN_COUNT_URL,
731 "https://api.anthropic.com/v1/messages/count_tokens"
732 );
733 }
734
735 #[test]
736 fn test_client_creation_anthropic() {
737 let client = ClaudeClient::anthropic("test-key");
738
739 match &client.backend {
740 ClaudeBackend::Anthropic { api_key } => {
741 assert_eq!(api_key, "test-key");
742 }
743 #[allow(unreachable_patterns)]
744 _ => panic!("Expected Anthropic backend"),
745 }
746
747 assert_eq!(client.api_version, API_VERSION);
748 }
749
750 #[tokio::test]
751 #[cfg(feature = "bedrock")]
752 #[ignore] async fn test_client_creation_bedrock() {
754 let result = ClaudeClient::bedrock("us-east-1").await;
757
758 if let Ok(client) = result {
759 match &client.backend {
760 ClaudeBackend::Bedrock { region, .. } => {
761 assert_eq!(region, "us-east-1");
762 }
763 _ => panic!("Expected Bedrock backend"),
764 }
765 }
766 }
768}