1pub mod bedrock;
50pub mod convert;
51pub mod foundry;
52pub mod vertex;
53
54pub use bedrock::AnthropicBedrockClient;
55pub use foundry::AnthropicFoundryClient;
56pub use vertex::{AnthropicVertexClient, StaticVertexToken, VertexTokenProvider};
57
58use std::collections::{HashMap, VecDeque};
59use std::sync::Arc;
60
61use agent_framework_core::client::{ChatClient, ChatStream};
62use agent_framework_core::error::{Error, Result};
63use agent_framework_core::streaming::Utf8StreamDecoder;
64use agent_framework_core::types::{
65 ChatOptions, ChatResponse, ChatResponseUpdate, Content, FunctionArguments, FunctionCallContent,
66 Message, Role, TextContent, TextReasoningContent, UsageContent,
67};
68use futures::StreamExt;
69use serde_json::Value;
70
71const DEFAULT_BASE_URL: &str = "https://api.anthropic.com";
72const ANTHROPIC_VERSION: &str = "2023-06-01";
73
74fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<f64> {
81 headers
82 .get(reqwest::header::RETRY_AFTER)
83 .and_then(|v| v.to_str().ok())
84 .and_then(|s| s.trim().parse::<f64>().ok())
85 .filter(|s| s.is_finite() && *s >= 0.0)
86}
87
88fn classify_anthropic_error(
118 status: u16,
119 body: &str,
120 message: impl Into<String>,
121 retry_after: Option<f64>,
122) -> Error {
123 let message = message.into();
124 match status {
125 401 | 403 => Error::service_invalid_auth(message),
126 400 if anthropic_error_type(body).as_deref() == Some("invalid_request_error") => {
127 Error::service_invalid_request(message)
128 }
129 _ => Error::service_status(status, message, retry_after),
130 }
131}
132
133fn anthropic_error_type(body: &str) -> Option<String> {
136 let value: Value = serde_json::from_str(body).ok()?;
137 value
138 .get("error")?
139 .get("type")?
140 .as_str()
141 .map(str::to_string)
142}
143
144fn new_message_request(
155 http: &reqwest::Client,
156 url: &str,
157 api_key: &str,
158 betas: &[String],
159) -> reqwest::RequestBuilder {
160 let mut request = http
161 .post(url)
162 .header("x-api-key", api_key)
163 .header("anthropic-version", ANTHROPIC_VERSION)
164 .header("content-type", "application/json");
165 if !betas.is_empty() {
166 request = request.header("anthropic-beta", betas.join(","));
167 }
168 request
169}
170const DEFAULT_MAX_TOKENS: u32 = 1024;
175
176#[derive(Clone)]
178pub struct AnthropicClient {
179 inner: Arc<Inner>,
180}
181
182#[derive(Clone)]
183struct Inner {
184 http: reqwest::Client,
185 api_key: String,
186 base_url: String,
187 model: String,
188 max_tokens: u32,
189 default_options: ChatOptions,
190 additional_beta_flags: Vec<String>,
196}
197
198impl std::fmt::Debug for AnthropicClient {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 f.debug_struct("AnthropicClient")
201 .field("base_url", &self.inner.base_url)
202 .field("model", &self.inner.model)
203 .field("max_tokens", &self.inner.max_tokens)
204 .finish_non_exhaustive()
205 }
206}
207
208impl AnthropicClient {
209 pub fn new(api_key: impl Into<String>, model: impl Into<String>) -> Self {
211 Self {
212 inner: Arc::new(Inner {
213 http: reqwest::Client::new(),
214 api_key: api_key.into(),
215 base_url: DEFAULT_BASE_URL.to_string(),
216 model: model.into(),
217 max_tokens: DEFAULT_MAX_TOKENS,
218 default_options: ChatOptions::default(),
219 additional_beta_flags: Vec::new(),
220 }),
221 }
222 }
223
224 pub fn from_env(model: impl Into<String>) -> Result<Self> {
227 let key = std::env::var("ANTHROPIC_API_KEY")
228 .map_err(|_| Error::Configuration("ANTHROPIC_API_KEY is not set".into()))?;
229 let mut client = Self::new(key, model);
230 if let Ok(base) = std::env::var("ANTHROPIC_BASE_URL") {
231 client = client.with_base_url(base);
232 }
233 Ok(client)
234 }
235
236 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
238 Arc::make_mut(&mut self.inner).base_url = base_url.into();
239 self
240 }
241
242 pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
245 Arc::make_mut(&mut self.inner).max_tokens = max_tokens;
246 self
247 }
248
249 pub fn with_default_options(mut self, options: ChatOptions) -> Self {
253 Arc::make_mut(&mut self.inner).default_options = options;
254 self
255 }
256
257 pub fn with_additional_beta_flags(
266 mut self,
267 flags: impl IntoIterator<Item = impl Into<String>>,
268 ) -> Self {
269 Arc::make_mut(&mut self.inner).additional_beta_flags =
270 flags.into_iter().map(Into::into).collect();
271 self
272 }
273
274 pub fn model(&self) -> &str {
276 &self.inner.model
277 }
278
279 fn build_body(
289 &self,
290 messages: &[Message],
291 options: &ChatOptions,
292 stream: bool,
293 ) -> (Value, Vec<String>) {
294 let mut effective = self.inner.default_options.clone().merge(options.clone());
295 let betas = convert::compute_beta_flags(&mut effective, &self.inner.additional_beta_flags);
296 let model = effective
297 .model
298 .clone()
299 .unwrap_or_else(|| self.inner.model.clone());
300 let max_tokens = effective.max_tokens.unwrap_or(self.inner.max_tokens);
301 let body = convert::build_request(messages, &effective, &model, max_tokens, stream);
302 (body, betas)
303 }
304
305 async fn post(&self, body: &Value, betas: &[String]) -> Result<reqwest::Response> {
306 let url = format!("{}/v1/messages", self.inner.base_url.trim_end_matches('/'));
307 let request = new_message_request(&self.inner.http, &url, &self.inner.api_key, betas);
308 let resp = request
309 .json(body)
310 .send()
311 .await
312 .map_err(|e| Error::service(format!("request failed: {e}")))?;
313 if !resp.status().is_success() {
314 let status = resp.status();
315 let retry_after = parse_retry_after(resp.headers());
316 let text = resp.text().await.unwrap_or_default();
317 return Err(classify_anthropic_error(
318 status.as_u16(),
319 &text,
320 format!("Anthropic API error {status}: {text}"),
321 retry_after,
322 ));
323 }
324 Ok(resp)
325 }
326}
327
328#[async_trait::async_trait]
329impl ChatClient for AnthropicClient {
330 async fn get_response(
331 &self,
332 messages: Vec<Message>,
333 options: ChatOptions,
334 ) -> Result<ChatResponse> {
335 let (body, betas) = self.build_body(&messages, &options, false);
336 let resp = self.post(&body, &betas).await?;
337 let value: Value = resp
338 .json()
339 .await
340 .map_err(|e| Error::service(format!("invalid response json: {e}")))?;
341 if let Some(err) = value.get("error") {
342 let msg = err
343 .get("message")
344 .and_then(Value::as_str)
345 .unwrap_or("unknown Anthropic error")
346 .to_string();
347 return Err(Error::service(msg));
348 }
349 Ok(convert::parse_response(&value))
350 }
351
352 async fn get_streaming_response(
353 &self,
354 messages: Vec<Message>,
355 options: ChatOptions,
356 ) -> Result<ChatStream> {
357 let (body, betas) = self.build_body(&messages, &options, true);
358 let resp = self.post(&body, &betas).await?;
359 Ok(parse_sse_stream(resp).boxed())
360 }
361
362 fn model(&self) -> Option<&str> {
363 Some(&self.inner.model)
364 }
365}
366
367type ByteStream =
368 std::pin::Pin<Box<dyn futures::Stream<Item = reqwest::Result<bytes::Bytes>> + Send>>;
369
370fn parse_sse_stream(
373 resp: reqwest::Response,
374) -> impl futures::Stream<Item = Result<ChatResponseUpdate>> + Send {
375 let byte_stream: ByteStream = Box::pin(resp.bytes_stream());
376 futures::stream::unfold(
377 SseState {
378 byte_stream,
379 buffer: String::new(),
380 utf8: Utf8StreamDecoder::new(),
381 queued: VecDeque::new(),
382 tool_use_ids: HashMap::new(),
383 usage: convert::StreamUsageAccumulator::default(),
384 done: false,
385 },
386 |mut state| async move {
387 loop {
388 if let Some(update) = state.queued.pop_front() {
389 return Some((Ok(update), state));
390 }
391 if state.done {
392 return None;
393 }
394 match state.byte_stream.next().await {
395 Some(Ok(bytes)) => {
396 let decoded = state.utf8.push(&bytes);
397 state.buffer.push_str(&decoded);
398 while let Some(pos) = state.buffer.find('\n') {
399 let line = state.buffer[..pos].trim().to_string();
400 state.buffer.drain(..=pos);
401 let Some(data) = line.strip_prefix("data:") else {
407 continue;
408 };
409 let data = data.trim();
410 if data.is_empty() {
411 continue;
412 }
413 let Ok(value) = serde_json::from_str::<Value>(data) else {
414 continue;
415 };
416 if value.get("type").and_then(Value::as_str) == Some("error") {
417 let msg = value
418 .get("error")
419 .and_then(|e| e.get("message"))
420 .and_then(Value::as_str)
421 .unwrap_or("unknown Anthropic stream error")
422 .to_string();
423 state.done = true;
424 return Some((Err(Error::service(msg)), state));
425 }
426 if let Some(update) = parse_stream_event(
427 &value,
428 &mut state.tool_use_ids,
429 &mut state.usage,
430 ) {
431 state.queued.push_back(update);
432 }
433 }
434 }
435 Some(Err(e)) => {
436 state.done = true;
437 return Some((Err(Error::service(format!("stream error: {e}"))), state));
438 }
439 None => return None,
440 }
441 }
442 },
443 )
444}
445
446struct SseState {
448 byte_stream: ByteStream,
449 buffer: String,
450 utf8: Utf8StreamDecoder,
451 queued: VecDeque<ChatResponseUpdate>,
452 tool_use_ids: HashMap<i64, String>,
455 usage: convert::StreamUsageAccumulator,
457 done: bool,
458}
459
460fn parse_stream_event(
464 value: &Value,
465 tool_use_ids: &mut HashMap<i64, String>,
466 usage_acc: &mut convert::StreamUsageAccumulator,
467) -> Option<ChatResponseUpdate> {
468 match value.get("type").and_then(Value::as_str)? {
469 "message_start" => {
470 let message = value.get("message")?;
471 let response_id = message.get("id").and_then(Value::as_str).map(String::from);
472 let model = message
473 .get("model")
474 .and_then(Value::as_str)
475 .map(String::from);
476 let mut contents = Vec::new();
477 if let Some(usage) = message.get("usage") {
478 if let Some(usage_content) = convert::parse_message_start_usage(usage) {
479 contents.push(Content::Usage(UsageContent {
482 details: usage_acc.increment(&usage_content.details),
483 }));
484 }
485 }
486 Some(ChatResponseUpdate {
487 contents,
488 role: Some(Role::assistant()),
489 response_id,
490 model,
491 ..Default::default()
492 })
493 }
494 "content_block_start" => {
495 let index = value.get("index").and_then(Value::as_i64).unwrap_or(0);
496 let block = value.get("content_block")?;
497 match block.get("type").and_then(Value::as_str)? {
498 "tool_use" | "mcp_tool_use" | "server_tool_use" => {
499 let id = block
500 .get("id")
501 .and_then(Value::as_str)
502 .unwrap_or_default()
503 .to_string();
504 let name = block
505 .get("name")
506 .and_then(Value::as_str)
507 .unwrap_or_default()
508 .to_string();
509 tool_use_ids.insert(index, id.clone());
510 Some(ChatResponseUpdate {
511 contents: vec![Content::FunctionCall(FunctionCallContent::new(
512 id, name, None,
513 ))],
514 role: Some(Role::assistant()),
515 ..Default::default()
516 })
517 }
518 "text" | "thinking" => {
519 None
522 }
523 _ => {
524 let contents = convert::parse_content_blocks(std::slice::from_ref(block));
535 if contents.is_empty() {
536 None
537 } else {
538 Some(ChatResponseUpdate {
539 contents,
540 role: Some(Role::assistant()),
541 ..Default::default()
542 })
543 }
544 }
545 }
546 }
547 "content_block_delta" => {
548 let index = value.get("index").and_then(Value::as_i64).unwrap_or(0);
549 let delta = value.get("delta")?;
550 let content = match delta.get("type").and_then(Value::as_str)? {
551 "text_delta" => Content::Text(TextContent::new(
552 delta
553 .get("text")
554 .and_then(Value::as_str)
555 .unwrap_or_default(),
556 )),
557 "thinking_delta" => Content::TextReasoning(TextReasoningContent {
558 text: delta
559 .get("thinking")
560 .and_then(Value::as_str)
561 .unwrap_or_default()
562 .to_string(),
563 annotations: None,
564 ..Default::default()
565 }),
566 "input_json_delta" => {
567 let call_id = tool_use_ids.get(&index).cloned().unwrap_or_default();
568 let partial = delta
569 .get("partial_json")
570 .and_then(Value::as_str)
571 .unwrap_or_default();
572 Content::FunctionCall(FunctionCallContent::new(
573 call_id,
574 "",
575 Some(FunctionArguments::Raw(partial.to_string())),
576 ))
577 }
578 _ => return None,
579 };
580 Some(ChatResponseUpdate {
581 contents: vec![content],
582 role: Some(Role::assistant()),
583 ..Default::default()
584 })
585 }
586 "message_delta" => {
587 let mut contents = Vec::new();
588 if let Some(usage) = value.get("usage") {
589 contents.push(Content::Usage(UsageContent {
592 details: usage_acc.increment(&convert::parse_usage(usage)),
593 }));
594 }
595 let finish_reason = value
596 .get("delta")
597 .and_then(|d| d.get("stop_reason"))
598 .and_then(Value::as_str)
599 .map(convert::map_stop_reason);
600 Some(ChatResponseUpdate {
601 contents,
602 finish_reason,
603 ..Default::default()
604 })
605 }
606 _ => None,
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615
616 fn sse_frame(event: &str, data: &Value) -> String {
617 format!("event: {event}\ndata: {data}\n\n")
618 }
619
620 async fn collect_updates(text: String) -> Vec<ChatResponseUpdate> {
621 let stream =
622 futures::stream::once(async move { Ok::<_, reqwest::Error>(bytes::Bytes::from(text)) });
623 let byte_stream: ByteStream = Box::pin(stream);
624 let mut state = SseState {
625 byte_stream,
626 buffer: String::new(),
627 utf8: Utf8StreamDecoder::new(),
628 queued: VecDeque::new(),
629 tool_use_ids: HashMap::new(),
630 usage: convert::StreamUsageAccumulator::default(),
631 done: false,
632 };
633 let mut updates = Vec::new();
634 if let Some(Ok(bytes)) = state.byte_stream.next().await {
635 let decoded = state.utf8.push(&bytes);
636 state.buffer.push_str(&decoded);
637 while let Some(pos) = state.buffer.find('\n') {
638 let line = state.buffer[..pos].trim().to_string();
639 state.buffer.drain(..=pos);
640 let Some(data) = line.strip_prefix("data:") else {
641 continue;
642 };
643 let data = data.trim();
644 if data.is_empty() {
645 continue;
646 }
647 let value: Value = serde_json::from_str(data).unwrap();
648 if let Some(update) =
649 parse_stream_event(&value, &mut state.tool_use_ids, &mut state.usage)
650 {
651 updates.push(update);
652 }
653 }
654 }
655 updates
656 }
657
658 #[tokio::test]
659 async fn stream_text_only_accumulates() {
660 let mut text = String::new();
661 text.push_str(&sse_frame(
662 "message_start",
663 &serde_json::json!({
664 "type": "message_start",
665 "message": { "id": "msg_1", "model": "claude-x", "usage": { "input_tokens": 25, "output_tokens": 1 } }
666 }),
667 ));
668 text.push_str(&sse_frame(
669 "content_block_start",
670 &serde_json::json!({ "type": "content_block_start", "index": 0, "content_block": { "type": "text", "text": "" } }),
671 ));
672 text.push_str(&sse_frame(
673 "content_block_delta",
674 &serde_json::json!({ "type": "content_block_delta", "index": 0, "delta": { "type": "text_delta", "text": "Hel" } }),
675 ));
676 text.push_str(&sse_frame(
677 "content_block_delta",
678 &serde_json::json!({ "type": "content_block_delta", "index": 0, "delta": { "type": "text_delta", "text": "lo!" } }),
679 ));
680 text.push_str(&sse_frame(
681 "content_block_stop",
682 &serde_json::json!({ "type": "content_block_stop", "index": 0 }),
683 ));
684 text.push_str(&sse_frame(
685 "message_delta",
686 &serde_json::json!({ "type": "message_delta", "delta": { "stop_reason": "end_turn" }, "usage": { "output_tokens": 15 } }),
687 ));
688 text.push_str(&sse_frame(
689 "message_stop",
690 &serde_json::json!({ "type": "message_stop" }),
691 ));
692
693 let updates = collect_updates(text).await;
694 let resp = ChatResponse::from_updates(updates);
695 assert_eq!(resp.text(), "Hello!");
696 assert_eq!(resp.response_id.as_deref(), Some("msg_1"));
697 assert_eq!(
698 resp.finish_reason,
699 Some(agent_framework_core::types::FinishReason::stop())
700 );
701 let usage = resp.usage_details.unwrap();
702 assert_eq!(usage.input_token_count, Some(25));
705 assert_eq!(usage.output_token_count, Some(15));
706 }
707
708 #[tokio::test]
709 async fn stream_usage_is_not_double_counted_when_deltas_repeat_input_tokens() {
710 let mut text = String::new();
714 text.push_str(&sse_frame(
715 "message_start",
716 &serde_json::json!({
717 "type": "message_start",
718 "message": {
719 "id": "msg_1",
720 "model": "claude-sonnet-4",
721 "usage": {
722 "input_tokens": 25,
723 "cache_read_input_tokens": 8,
724 "output_tokens": 1
725 }
726 }
727 }),
728 ));
729 text.push_str(&sse_frame(
730 "message_delta",
731 &serde_json::json!({
732 "type": "message_delta",
733 "delta": { "stop_reason": "end_turn" },
734 "usage": {
735 "input_tokens": 25,
736 "cache_read_input_tokens": 8,
737 "output_tokens": 15
738 }
739 }),
740 ));
741
742 let updates = collect_updates(text).await;
743 let resp = ChatResponse::from_updates(updates);
744 let usage = resp.usage_details.unwrap();
745 assert_eq!(usage.input_token_count, Some(25));
746 assert_eq!(usage.output_token_count, Some(15));
747 assert_eq!(usage.cache_read_input_token_count, Some(8));
748 }
749
750 #[tokio::test]
751 async fn stream_tool_call_accumulates_arguments() {
752 let mut text = String::new();
753 text.push_str(&sse_frame(
754 "content_block_start",
755 &serde_json::json!({ "type": "content_block_start", "index": 0, "content_block": { "type": "tool_use", "id": "toolu_1", "name": "get_weather", "input": {} } }),
756 ));
757 text.push_str(&sse_frame(
758 "content_block_delta",
759 &serde_json::json!({ "type": "content_block_delta", "index": 0, "delta": { "type": "input_json_delta", "partial_json": "{\"city\": \"San" } }),
760 ));
761 text.push_str(&sse_frame(
762 "content_block_delta",
763 &serde_json::json!({ "type": "content_block_delta", "index": 0, "delta": { "type": "input_json_delta", "partial_json": " Francisco\"}" } }),
764 ));
765 text.push_str(&sse_frame(
766 "content_block_stop",
767 &serde_json::json!({ "type": "content_block_stop", "index": 0 }),
768 ));
769 text.push_str(&sse_frame(
770 "message_delta",
771 &serde_json::json!({ "type": "message_delta", "delta": { "stop_reason": "tool_use" }, "usage": { "output_tokens": 20 } }),
772 ));
773
774 let updates = collect_updates(text).await;
775 let resp = ChatResponse::from_updates(updates);
776 let calls = resp.function_calls();
777 assert_eq!(calls.len(), 1);
778 assert_eq!(calls[0].call_id, "toolu_1");
779 assert_eq!(calls[0].name, "get_weather");
780 assert_eq!(
781 calls[0].parse_arguments().unwrap().get("city").unwrap(),
782 &serde_json::json!("San Francisco")
783 );
784 assert_eq!(
785 resp.finish_reason,
786 Some(agent_framework_core::types::FinishReason::tool_calls())
787 );
788 }
789
790 #[tokio::test]
791 async fn stream_hosted_tool_use_and_result_via_content_block_start() {
792 let mut text = String::new();
798 text.push_str(&sse_frame(
799 "content_block_start",
800 &serde_json::json!({ "type": "content_block_start", "index": 0, "content_block": { "type": "server_tool_use", "id": "srvtoolu_1", "name": "web_search", "input": { "query": "rust" } } }),
801 ));
802 text.push_str(&sse_frame(
803 "content_block_stop",
804 &serde_json::json!({ "type": "content_block_stop", "index": 0 }),
805 ));
806 text.push_str(&sse_frame(
807 "content_block_start",
808 &serde_json::json!({ "type": "content_block_start", "index": 1, "content_block": { "type": "web_search_tool_result", "tool_use_id": "srvtoolu_1", "content": [{ "type": "web_search_result", "url": "https://example.com", "title": "Example" }] } }),
809 ));
810 text.push_str(&sse_frame(
811 "content_block_stop",
812 &serde_json::json!({ "type": "content_block_stop", "index": 1 }),
813 ));
814
815 let updates = collect_updates(text).await;
816 let resp = ChatResponse::from_updates(updates);
817 let calls = resp.function_calls();
818 assert_eq!(calls.len(), 1);
819 assert_eq!(calls[0].call_id, "srvtoolu_1");
820 assert_eq!(calls[0].name, "web_search");
821 let has_function_result = resp
822 .messages
823 .iter()
824 .flat_map(|m| &m.contents)
825 .any(|c| matches!(c, Content::FunctionResult(_)));
826 assert!(
827 has_function_result,
828 "expected a FunctionResult content from the web_search_tool_result block"
829 );
830 }
831
832 #[tokio::test]
833 async fn stream_mcp_tool_use_via_content_block_start() {
834 let text = sse_frame(
835 "content_block_start",
836 &serde_json::json!({ "type": "content_block_start", "index": 0, "content_block": { "type": "mcp_tool_use", "id": "mcptoolu_1", "name": "search_docs", "server_name": "docs", "input": {} } }),
837 );
838 let updates = collect_updates(text).await;
839 let resp = ChatResponse::from_updates(updates);
840 let calls = resp.function_calls();
841 assert_eq!(calls.len(), 1);
842 assert_eq!(calls[0].call_id, "mcptoolu_1");
843 assert_eq!(calls[0].name, "search_docs");
844 }
845
846 #[tokio::test]
847 async fn stream_citations_delta_is_ignored_like_upstream() {
848 let text = sse_frame(
858 "content_block_delta",
859 &serde_json::json!({
860 "type": "content_block_delta",
861 "index": 0,
862 "delta": {
863 "type": "citations_delta",
864 "citation": {
865 "type": "char_location",
866 "cited_text": "example",
867 "document_index": 0,
868 "document_title": "Doc",
869 "start_char_index": 0,
870 "end_char_index": 7
871 }
872 }
873 }),
874 );
875 let updates = collect_updates(text).await;
876 assert!(
877 updates.is_empty(),
878 "citations_delta should not produce an update, matching upstream"
879 );
880 }
881
882 #[tokio::test]
883 async fn stream_error_event_is_surfaced() {
884 let text = sse_frame(
885 "error",
886 &serde_json::json!({ "type": "error", "error": { "type": "overloaded_error", "message": "Overloaded" } }),
887 );
888 let stream =
889 futures::stream::once(async move { Ok::<_, reqwest::Error>(bytes::Bytes::from(text)) });
890 let byte_stream: ByteStream = Box::pin(stream);
891 let mut state = SseState {
892 byte_stream,
893 buffer: String::new(),
894 utf8: Utf8StreamDecoder::new(),
895 queued: VecDeque::new(),
896 tool_use_ids: HashMap::new(),
897 usage: convert::StreamUsageAccumulator::default(),
898 done: false,
899 };
900 let bytes = state.byte_stream.next().await.unwrap().unwrap();
901 let decoded = state.utf8.push(&bytes);
902 state.buffer.push_str(&decoded);
903 let mut saw_error = false;
904 while let Some(pos) = state.buffer.find('\n') {
905 let line = state.buffer[..pos].trim().to_string();
906 state.buffer.drain(..=pos);
907 let Some(data) = line.strip_prefix("data:") else {
908 continue;
909 };
910 let data = data.trim();
911 if data.is_empty() {
912 continue;
913 }
914 let value: Value = serde_json::from_str(data).unwrap();
915 if value.get("type").and_then(Value::as_str) == Some("error") {
916 let msg = value
917 .get("error")
918 .and_then(|e| e.get("message"))
919 .and_then(Value::as_str)
920 .unwrap_or_default();
921 assert_eq!(msg, "Overloaded");
922 saw_error = true;
923 }
924 }
925 assert!(saw_error, "expected the error event to be recognized");
926 }
927
928 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
934
935 #[test]
936 fn from_env_reads_api_key_and_base_url() {
937 let _guard = ENV_MUTEX.lock().unwrap();
938 unsafe {
941 std::env::set_var("ANTHROPIC_API_KEY", "sk-ant-test-123");
942 std::env::set_var("ANTHROPIC_BASE_URL", "https://example.test");
943 }
944 let client = AnthropicClient::from_env("claude-x").unwrap();
945 assert_eq!(client.inner.api_key, "sk-ant-test-123");
946 assert_eq!(client.inner.base_url, "https://example.test");
947 unsafe {
948 std::env::remove_var("ANTHROPIC_API_KEY");
949 std::env::remove_var("ANTHROPIC_BASE_URL");
950 }
951 }
952
953 #[test]
954 fn from_env_errors_when_api_key_missing() {
955 let _guard = ENV_MUTEX.lock().unwrap();
956 unsafe {
958 std::env::remove_var("ANTHROPIC_API_KEY");
959 std::env::remove_var("ANTHROPIC_BASE_URL");
960 }
961 let result = AnthropicClient::from_env("claude-x");
962 assert!(result.is_err());
963 }
964
965 #[test]
968 fn default_max_tokens_is_1024() {
969 let client = AnthropicClient::new("key", "claude-x");
972 let (body, _betas) = client.build_body(&[Message::user("hi")], &ChatOptions::new(), false);
973 assert_eq!(body["max_tokens"], serde_json::json!(1024));
974 }
975
976 #[test]
977 fn with_max_tokens_overrides_default() {
978 let client = AnthropicClient::new("key", "claude-x").with_max_tokens(8192);
979 let (body, _betas) = client.build_body(&[Message::user("hi")], &ChatOptions::new(), false);
980 assert_eq!(body["max_tokens"], serde_json::json!(8192));
981 }
982
983 #[test]
984 fn per_request_max_tokens_overrides_client_default() {
985 let client = AnthropicClient::new("key", "claude-x").with_max_tokens(8192);
986 let options = ChatOptions::new().with_max_tokens(256);
987 let (body, _betas) = client.build_body(&[Message::user("hi")], &options, false);
988 assert_eq!(body["max_tokens"], serde_json::json!(256));
989 }
990
991 #[test]
992 fn with_default_options_merged_under_per_request_options() {
993 let client = AnthropicClient::new("key", "claude-x")
994 .with_default_options(ChatOptions::new().with_temperature(0.2));
995 let (body, _betas) = client.build_body(&[Message::user("hi")], &ChatOptions::new(), false);
996 assert_eq!(body["temperature"], serde_json::json!(0.2_f32));
999
1000 let (body2, _betas2) = client.build_body(
1002 &[Message::user("hi")],
1003 &ChatOptions::new().with_temperature(0.9),
1004 false,
1005 );
1006 assert_eq!(body2["temperature"], serde_json::json!(0.9_f32));
1007 }
1008
1009 #[test]
1012 fn build_body_always_includes_default_beta_flags() {
1013 let client = AnthropicClient::new("key", "claude-x");
1017 let (_body, betas) = client.build_body(&[Message::user("hi")], &ChatOptions::new(), false);
1018 assert!(betas.contains(&"mcp-client-2025-04-04".to_string()));
1019 assert!(betas.contains(&"code-execution-2025-08-25".to_string()));
1020 assert_eq!(betas.len(), 2);
1021 }
1022
1023 #[test]
1024 fn build_body_merges_client_level_additional_beta_flags() {
1025 let client =
1026 AnthropicClient::new("key", "claude-x").with_additional_beta_flags(["my-custom-beta"]);
1027 let (_body, betas) = client.build_body(&[Message::user("hi")], &ChatOptions::new(), false);
1028 assert!(betas.contains(&"my-custom-beta".to_string()));
1029 assert!(betas.contains(&"mcp-client-2025-04-04".to_string()));
1030 assert_eq!(betas.len(), 3);
1031 }
1032
1033 #[test]
1034 fn build_body_merges_per_request_additional_beta_flags_and_strips_them_from_body() {
1035 let client = AnthropicClient::new("key", "claude-x");
1036 let mut options = ChatOptions::new();
1037 options.additional_properties.insert(
1038 "additional_beta_flags".into(),
1039 serde_json::json!(["request-only-beta"]),
1040 );
1041 let (body, betas) = client.build_body(&[Message::user("hi")], &options, false);
1042 assert!(betas.contains(&"request-only-beta".to_string()));
1043 assert!(body.get("additional_beta_flags").is_none());
1046 }
1047
1048 #[test]
1049 fn new_message_request_sets_anthropic_beta_header_when_betas_present() {
1050 let http = reqwest::Client::new();
1053 let betas = vec!["a".to_string(), "b".to_string()];
1054 let request = new_message_request(
1055 &http,
1056 "https://api.anthropic.com/v1/messages",
1057 "test-key",
1058 &betas,
1059 )
1060 .build()
1061 .unwrap();
1062 assert_eq!(request.headers().get("anthropic-beta").unwrap(), "a,b");
1063 }
1064
1065 #[test]
1066 fn new_message_request_omits_anthropic_beta_header_when_betas_empty() {
1067 let http = reqwest::Client::new();
1068 let request = new_message_request(
1069 &http,
1070 "https://api.anthropic.com/v1/messages",
1071 "test-key",
1072 &[],
1073 )
1074 .build()
1075 .unwrap();
1076 assert!(request.headers().get("anthropic-beta").is_none());
1077 }
1078
1079 #[test]
1084 fn classifies_401_and_403_as_invalid_auth() {
1085 for status in [401, 403] {
1086 let body = format!(
1087 r#"{{"type":"error","error":{{"type":"authentication_error","message":"nope {status}"}}}}"#
1088 );
1089 let err = classify_anthropic_error(status, &body, format!("err {status}"), None);
1090 assert!(
1091 matches!(err, Error::ServiceInvalidAuth { .. }),
1092 "status {status}: {err:?}"
1093 );
1094 }
1095 }
1096
1097 #[test]
1098 fn classifies_400_invalid_request_error_as_invalid_request() {
1099 let body = r#"{"type":"error","error":{"type":"invalid_request_error","message":"messages: at least one message is required"}}"#;
1100 let err = classify_anthropic_error(400, body, "err", None);
1101 assert!(
1102 matches!(err, Error::ServiceInvalidRequest { .. }),
1103 "{err:?}"
1104 );
1105 }
1106
1107 #[test]
1108 fn a_400_without_confirming_body_stays_service_status() {
1109 let err = classify_anthropic_error(400, "not json", "err", None);
1114 assert_eq!(err.status(), Some(400), "{err:?}");
1115
1116 let err = classify_anthropic_error(
1117 400,
1118 r#"{"type":"error","error":{"type":"something_else"}}"#,
1119 "err",
1120 None,
1121 );
1122 assert_eq!(err.status(), Some(400), "{err:?}");
1123 }
1124
1125 #[test]
1126 fn leaves_retryable_statuses_as_service_status() {
1127 for status in [408, 429, 500, 529] {
1130 let err = classify_anthropic_error(status, "", format!("err {status}"), Some(1.5));
1131 assert_eq!(err.status(), Some(status), "{err:?}");
1132 assert_eq!(err.retry_after(), Some(1.5), "{err:?}");
1133 }
1134 }
1135
1136 #[test]
1137 fn never_produces_content_filter() {
1138 let bodies = [
1143 "",
1144 "not json",
1145 r#"{"type":"error","error":{"type":"invalid_request_error"}}"#,
1146 r#"{"type":"error","error":{"type":"authentication_error"}}"#,
1147 ];
1148 for status in [400, 401, 403, 404, 422, 429, 500] {
1149 for body in bodies {
1150 let err = classify_anthropic_error(status, body, "err", None);
1151 assert!(
1152 !matches!(err, Error::ServiceContentFilter { .. }),
1153 "status {status}, body {body:?}: {err:?}"
1154 );
1155 }
1156 }
1157 }
1158
1159 }