1use std::collections::HashMap;
2use std::fmt;
3use std::pin::Pin;
4
5use async_trait::async_trait;
6use futures::stream::Stream;
7#[cfg(not(target_arch = "wasm32"))]
8use futures::stream::StreamExt;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::{ToolCall, error::LLMError};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Usage {
17 #[serde(alias = "input_tokens")]
19 pub prompt_tokens: u32,
20 #[serde(alias = "output_tokens")]
22 pub completion_tokens: u32,
23 pub total_tokens: u32,
25 #[serde(
27 skip_serializing_if = "Option::is_none",
28 alias = "output_tokens_details"
29 )]
30 pub completion_tokens_details: Option<CompletionTokensDetails>,
31 #[serde(
33 skip_serializing_if = "Option::is_none",
34 alias = "input_tokens_details"
35 )]
36 pub prompt_tokens_details: Option<PromptTokensDetails>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct StreamResponse {
42 pub choices: Vec<StreamChoice>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub usage: Option<Usage>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct StreamChoice {
52 pub delta: StreamDelta,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct StreamDelta {
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub content: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub reasoning_content: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub tool_calls: Option<Vec<ToolCall>>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum StreamChunk {
77 Text(String),
79 ReasoningContent(String),
81
82 ToolUseStart {
84 index: usize,
86 id: String,
88 name: String,
90 },
91
92 ToolUseInputDelta {
94 index: usize,
96 partial_json: String,
98 },
99
100 ToolUseComplete {
102 index: usize,
104 tool_call: ToolCall,
106 },
107
108 Done {
110 stop_reason: String,
112 },
113 Usage(Usage),
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct CompletionTokensDetails {
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub reasoning_tokens: Option<u32>,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub audio_tokens: Option<u32>,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct PromptTokensDetails {
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub cached_tokens: Option<u32>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub audio_tokens: Option<u32>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub enum ChatRole {
141 System,
143 User,
145 Assistant,
147 Tool,
149}
150
151impl fmt::Display for ChatRole {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 let value = match self {
154 ChatRole::System => "system",
155 ChatRole::User => "user",
156 ChatRole::Assistant => "assistant",
157 ChatRole::Tool => "tool",
158 };
159 f.write_str(value)
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165#[non_exhaustive]
166pub enum ImageMime {
167 JPEG,
169 PNG,
171 GIF,
173 WEBP,
175}
176
177impl ImageMime {
178 pub fn mime_type(&self) -> &'static str {
179 match self {
180 ImageMime::JPEG => "image/jpeg",
181 ImageMime::PNG => "image/png",
182 ImageMime::GIF => "image/gif",
183 ImageMime::WEBP => "image/webp",
184 }
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
190pub enum MessageType {
191 #[default]
193 Text,
194 Image((ImageMime, Vec<u8>)),
196 Pdf(Vec<u8>),
198 ImageURL(String),
200 ToolUse(Vec<ToolCall>),
202 ToolResult(Vec<ToolCall>),
204}
205
206pub enum ReasoningEffort {
208 Low,
210 Medium,
212 High,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ChatMessage {
219 pub role: ChatRole,
221 pub message_type: MessageType,
223 pub content: String,
225}
226
227#[derive(Debug, Clone, Serialize)]
229pub struct ParameterProperty {
230 #[serde(rename = "type")]
232 pub property_type: String,
233 pub description: String,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub items: Option<Box<ParameterProperty>>,
238 #[serde(skip_serializing_if = "Option::is_none", rename = "enum")]
240 pub enum_list: Option<Vec<String>>,
241}
242
243#[derive(Debug, Clone, Serialize)]
245pub struct ParametersSchema {
246 #[serde(rename = "type")]
248 pub schema_type: String,
249 pub properties: HashMap<String, ParameterProperty>,
251 pub required: Vec<String>,
253}
254
255#[derive(Debug, Clone, Serialize)]
265pub struct FunctionTool {
266 pub name: String,
268 pub description: String,
270 pub parameters: Value,
272}
273
274#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
311
312pub struct StructuredOutputFormat {
313 pub name: String,
315 pub description: Option<String>,
317 pub schema: Option<Value>,
319 pub strict: Option<bool>,
321}
322
323#[derive(Debug, Clone, Serialize)]
325pub struct Tool {
326 #[serde(rename = "type")]
328 pub tool_type: String,
329 pub function: FunctionTool,
331}
332
333#[derive(Debug, Clone, Default)]
336pub enum ToolChoice {
337 Any,
340
341 #[default]
344 Auto,
345
346 Tool(String),
350
351 None,
354}
355
356impl Serialize for ToolChoice {
357 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
358 where
359 S: serde::Serializer,
360 {
361 match self {
362 ToolChoice::Any => serializer.serialize_str("required"),
363 ToolChoice::Auto => serializer.serialize_str("auto"),
364 ToolChoice::None => serializer.serialize_str("none"),
365 ToolChoice::Tool(name) => {
366 use serde::ser::SerializeMap;
367
368 let mut map = serializer.serialize_map(Some(2))?;
370 map.serialize_entry("type", "function")?;
371
372 let mut function_obj = std::collections::HashMap::new();
374 function_obj.insert("name", name.as_str());
375
376 map.serialize_entry("function", &function_obj)?;
377 map.end()
378 }
379 }
380 }
381}
382
383pub trait ChatResponse: std::fmt::Debug + std::fmt::Display + Send + Sync {
384 fn text(&self) -> Option<String>;
385 fn tool_calls(&self) -> Option<Vec<ToolCall>>;
386 fn thinking(&self) -> Option<String> {
387 None
388 }
389 fn usage(&self) -> Option<Usage> {
390 None
391 }
392}
393
394#[derive(Debug, Default, Clone, PartialEq)]
406pub struct SamplingOverrides {
407 pub temperature: Option<f32>,
409 pub top_p: Option<f32>,
411 pub max_tokens: Option<u32>,
413}
414
415impl SamplingOverrides {
416 pub fn empty() -> Self {
419 Self::default()
420 }
421
422 pub fn with_temperature(temperature: f32) -> Self {
424 Self {
425 temperature: Some(temperature),
426 ..Self::default()
427 }
428 }
429
430 pub fn with_top_p(top_p: f32) -> Self {
432 Self {
433 top_p: Some(top_p),
434 ..Self::default()
435 }
436 }
437
438 pub fn with_max_tokens(max_tokens: u32) -> Self {
440 Self {
441 max_tokens: Some(max_tokens),
442 ..Self::default()
443 }
444 }
445}
446
447#[async_trait]
449pub trait ChatProvider: Sync + Send {
450 async fn chat(
461 &self,
462 messages: &[ChatMessage],
463 json_schema: Option<StructuredOutputFormat>,
464 ) -> Result<Box<dyn ChatResponse>, LLMError> {
465 self.chat_with_tools(messages, None, json_schema).await
466 }
467
468 async fn chat_with_tools(
480 &self,
481 messages: &[ChatMessage],
482 tools: Option<&[Tool]>,
483 json_schema: Option<StructuredOutputFormat>,
484 ) -> Result<Box<dyn ChatResponse>, LLMError>;
485
486 async fn chat_and_sampling(
496 &self,
497 messages: &[ChatMessage],
498 json_schema: Option<StructuredOutputFormat>,
499 sampling: Option<&SamplingOverrides>,
500 ) -> Result<Box<dyn ChatResponse>, LLMError> {
501 self.chat_with_tools_and_sampling(messages, None, json_schema, sampling)
502 .await
503 }
504
505 async fn chat_with_tools_and_sampling(
516 &self,
517 messages: &[ChatMessage],
518 tools: Option<&[Tool]>,
519 json_schema: Option<StructuredOutputFormat>,
520 sampling: Option<&SamplingOverrides>,
521 ) -> Result<Box<dyn ChatResponse>, LLMError> {
522 let _ = sampling;
524 self.chat_with_tools(messages, tools, json_schema).await
525 }
526
527 async fn chat_with_web_search(
537 &self,
538 _input: String,
539 ) -> Result<Box<dyn ChatResponse>, LLMError> {
540 Err(LLMError::Generic(
541 "Web search not supported for this provider".to_string(),
542 ))
543 }
544
545 async fn chat_stream(
556 &self,
557 _messages: &[ChatMessage],
558 _json_schema: Option<StructuredOutputFormat>,
559 ) -> Result<std::pin::Pin<Box<dyn Stream<Item = Result<String, LLMError>> + Send>>, LLMError>
560 {
561 Err(LLMError::Generic(
562 "Streaming not supported for this provider".to_string(),
563 ))
564 }
565
566 async fn chat_stream_struct(
584 &self,
585 _messages: &[ChatMessage],
586 _tools: Option<&[Tool]>,
587 _json_schema: Option<StructuredOutputFormat>,
588 ) -> Result<
589 std::pin::Pin<Box<dyn Stream<Item = Result<StreamResponse, LLMError>> + Send>>,
590 LLMError,
591 > {
592 Err(LLMError::Generic(
593 "Structured streaming not supported for this provider".to_string(),
594 ))
595 }
596
597 async fn chat_stream_with_tools(
642 &self,
643 _messages: &[ChatMessage],
644 _tools: Option<&[Tool]>,
645 _json_schema: Option<StructuredOutputFormat>,
646 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, LLMError>> + Send>>, LLMError> {
647 Err(LLMError::Generic(
648 "Streaming with tools not supported for this provider".to_string(),
649 ))
650 }
651
652 async fn chat_stream_and_sampling(
657 &self,
658 messages: &[ChatMessage],
659 json_schema: Option<StructuredOutputFormat>,
660 sampling: Option<&SamplingOverrides>,
661 ) -> Result<std::pin::Pin<Box<dyn Stream<Item = Result<String, LLMError>> + Send>>, LLMError>
662 {
663 let _ = sampling;
664 self.chat_stream(messages, json_schema).await
665 }
666
667 async fn chat_stream_struct_and_sampling(
672 &self,
673 messages: &[ChatMessage],
674 tools: Option<&[Tool]>,
675 json_schema: Option<StructuredOutputFormat>,
676 sampling: Option<&SamplingOverrides>,
677 ) -> Result<
678 std::pin::Pin<Box<dyn Stream<Item = Result<StreamResponse, LLMError>> + Send>>,
679 LLMError,
680 > {
681 let _ = sampling;
682 self.chat_stream_struct(messages, tools, json_schema).await
683 }
684
685 fn model(&self) -> &str {
693 ""
694 }
695}
696
697impl fmt::Display for ReasoningEffort {
698 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699 match self {
700 ReasoningEffort::Low => write!(f, "low"),
701 ReasoningEffort::Medium => write!(f, "medium"),
702 ReasoningEffort::High => write!(f, "high"),
703 }
704 }
705}
706
707impl ChatMessage {
708 pub fn user() -> ChatMessageBuilder {
710 ChatMessageBuilder::new(ChatRole::User)
711 }
712
713 pub fn assistant() -> ChatMessageBuilder {
715 ChatMessageBuilder::new(ChatRole::Assistant)
716 }
717}
718
719#[derive(Debug)]
721pub struct ChatMessageBuilder {
722 role: ChatRole,
723 message_type: MessageType,
724 content: String,
725}
726
727impl ChatMessageBuilder {
728 pub fn new(role: ChatRole) -> Self {
730 Self {
731 role,
732 message_type: MessageType::default(),
733 content: String::default(),
734 }
735 }
736
737 pub fn content<S: Into<String>>(mut self, content: S) -> Self {
739 self.content = content.into();
740 self
741 }
742
743 pub fn image(mut self, image_mime: ImageMime, raw_bytes: Vec<u8>) -> Self {
745 self.message_type = MessageType::Image((image_mime, raw_bytes));
746 self
747 }
748
749 pub fn pdf(mut self, raw_bytes: Vec<u8>) -> Self {
751 self.message_type = MessageType::Pdf(raw_bytes);
752 self
753 }
754
755 pub fn image_url(mut self, url: impl Into<String>) -> Self {
757 self.message_type = MessageType::ImageURL(url.into());
758 self
759 }
760
761 pub fn tool_use(mut self, tools: Vec<ToolCall>) -> Self {
763 self.message_type = MessageType::ToolUse(tools);
764 self
765 }
766
767 pub fn tool_result(mut self, tools: Vec<ToolCall>) -> Self {
769 self.message_type = MessageType::ToolResult(tools);
770 self
771 }
772
773 pub fn build(self) -> ChatMessage {
775 ChatMessage {
776 role: self.role,
777 message_type: self.message_type,
778 content: self.content,
779 }
780 }
781}
782
783#[cfg(not(target_arch = "wasm32"))]
794#[allow(dead_code)]
795pub(crate) fn create_sse_stream<F>(
796 response: reqwest::Response,
797 parser: F,
798) -> std::pin::Pin<Box<dyn Stream<Item = Result<String, LLMError>> + Send>>
799where
800 F: Fn(&str) -> Result<Option<String>, LLMError> + Send + 'static,
801{
802 let stream = response
803 .bytes_stream()
804 .scan(
805 (String::default(), Vec::default()),
806 move |(buffer, utf8_buffer): &mut (String, Vec<u8>),
807 chunk: Result<bytes::Bytes, reqwest::Error>| {
808 let result = match chunk {
809 Ok(bytes) => {
810 utf8_buffer.extend_from_slice(&bytes);
811
812 match String::from_utf8(utf8_buffer.clone()) {
813 Ok(text) => {
814 buffer.push_str(&text);
815 utf8_buffer.clear();
816 }
817 Err(e) => {
818 let valid_up_to = e.utf8_error().valid_up_to();
819 if valid_up_to > 0 {
820 let valid =
823 String::from_utf8_lossy(&utf8_buffer[..valid_up_to]);
824 buffer.push_str(&valid);
825 utf8_buffer.drain(..valid_up_to);
826 }
827 }
828 }
829
830 let mut results = Vec::default();
831
832 while let Some(pos) = buffer.find("\n\n") {
833 let event = buffer[..pos + 2].to_string();
834 buffer.drain(..pos + 2);
835
836 match parser(&event) {
837 Ok(Some(content)) => results.push(Ok(content)),
838 Ok(None) => {}
839 Err(e) => results.push(Err(e)),
840 }
841 }
842
843 Some(results)
844 }
845 Err(e) => Some(vec![Err(LLMError::HttpError(e.to_string()))]),
846 };
847
848 async move { result }
849 },
850 )
851 .flat_map(futures::stream::iter);
852
853 Box::pin(stream)
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859 use bytes::Bytes;
860 use futures::stream::StreamExt;
861
862 #[test]
863 fn test_chat_message_builder_user() {
864 let msg = ChatMessage::user().content("hello").build();
865 assert_eq!(msg.role, ChatRole::User);
866 assert_eq!(msg.content, "hello");
867 assert!(matches!(msg.message_type, MessageType::Text));
868 }
869
870 #[test]
871 fn test_chat_message_builder_assistant() {
872 let msg = ChatMessage::assistant().content("reply").build();
873 assert_eq!(msg.role, ChatRole::Assistant);
874 assert_eq!(msg.content, "reply");
875 }
876
877 #[test]
878 fn test_chat_message_builder_image() {
879 let msg = ChatMessage::user()
880 .content("describe")
881 .image(ImageMime::PNG, vec![1, 2, 3])
882 .build();
883 assert!(matches!(msg.message_type, MessageType::Image(_)));
884 }
885
886 #[test]
887 fn test_chat_message_builder_pdf() {
888 let msg = ChatMessage::user()
889 .content("read")
890 .pdf(vec![4, 5, 6])
891 .build();
892 assert!(matches!(msg.message_type, MessageType::Pdf(_)));
893 }
894
895 #[test]
896 fn test_chat_message_builder_tool_use() {
897 let tc = crate::ToolCall {
898 id: "t1".to_string(),
899 call_type: "function".to_string(),
900 function: crate::FunctionCall {
901 name: "tool".to_string(),
902 arguments: "{}".to_string(),
903 },
904 };
905 let msg = ChatMessage::assistant()
906 .content("calling tool")
907 .tool_use(vec![tc])
908 .build();
909 assert!(matches!(msg.message_type, MessageType::ToolUse(_)));
910 }
911
912 #[test]
913 fn test_chat_message_builder_tool_result() {
914 let tc = crate::ToolCall {
915 id: "t1".to_string(),
916 call_type: "function".to_string(),
917 function: crate::FunctionCall {
918 name: "tool".to_string(),
919 arguments: "result".to_string(),
920 },
921 };
922 let msg = ChatMessageBuilder::new(ChatRole::Tool)
923 .tool_result(vec![tc])
924 .build();
925 assert!(matches!(msg.message_type, MessageType::ToolResult(_)));
926 assert_eq!(msg.role, ChatRole::Tool);
927 }
928
929 #[test]
930 fn test_chat_role_display() {
931 assert_eq!(format!("{}", ChatRole::System), "system");
932 assert_eq!(format!("{}", ChatRole::User), "user");
933 assert_eq!(format!("{}", ChatRole::Assistant), "assistant");
934 assert_eq!(format!("{}", ChatRole::Tool), "tool");
935 }
936
937 #[test]
938 fn test_image_mime_mime_type() {
939 assert_eq!(ImageMime::JPEG.mime_type(), "image/jpeg");
940 assert_eq!(ImageMime::PNG.mime_type(), "image/png");
941 assert_eq!(ImageMime::GIF.mime_type(), "image/gif");
942 assert_eq!(ImageMime::WEBP.mime_type(), "image/webp");
943 }
944
945 #[test]
946 fn test_reasoning_effort_display() {
947 assert_eq!(format!("{}", ReasoningEffort::Low), "low");
948 assert_eq!(format!("{}", ReasoningEffort::Medium), "medium");
949 assert_eq!(format!("{}", ReasoningEffort::High), "high");
950 }
951
952 #[test]
953 fn test_tool_choice_serialization() {
954 let any_json = serde_json::to_value(&ToolChoice::Any).unwrap();
955 assert_eq!(any_json, "required");
956
957 let auto_json = serde_json::to_value(&ToolChoice::Auto).unwrap();
958 assert_eq!(auto_json, "auto");
959
960 let none_json = serde_json::to_value(&ToolChoice::None).unwrap();
961 assert_eq!(none_json, "none");
962
963 let tool_json = serde_json::to_value(ToolChoice::Tool("my_func".to_string())).unwrap();
964 assert_eq!(tool_json["type"], "function");
965 assert_eq!(tool_json["function"]["name"], "my_func");
966 }
967
968 #[test]
969 fn test_structured_output_format_roundtrip() {
970 let format = StructuredOutputFormat {
971 name: "Test".to_string(),
972 description: Some("A test".to_string()),
973 schema: Some(serde_json::json!({"type": "object"})),
974 strict: Some(true),
975 };
976 let json = serde_json::to_string(&format).unwrap();
977 let parsed: StructuredOutputFormat = serde_json::from_str(&json).unwrap();
978 assert_eq!(parsed, format);
979 }
980
981 #[test]
982 fn test_structured_output_format_minimal() {
983 let json_str = r#"{"name":"Minimal"}"#;
984 let parsed: StructuredOutputFormat = serde_json::from_str(json_str).unwrap();
985 assert_eq!(parsed.name, "Minimal");
986 assert_eq!(parsed.description, None);
987 assert_eq!(parsed.schema, None);
988 assert_eq!(parsed.strict, None);
989 }
990
991 #[test]
992 fn test_chat_message_builder_image_url() {
993 let msg = ChatMessage::user()
994 .image_url("https://example.com/img.png")
995 .content("describe this")
996 .build();
997 assert!(matches!(msg.message_type, MessageType::ImageURL(_)));
998 }
999
1000 #[tokio::test]
1001 async fn test_create_sse_stream_handles_split_utf8() {
1002 let test_data = "data: Positive reactions\n\n".as_bytes();
1003
1004 let chunks: Vec<Result<Bytes, reqwest::Error>> = vec![
1005 Ok(Bytes::from(&test_data[..10])),
1006 Ok(Bytes::from(&test_data[10..])),
1007 ];
1008
1009 let mock_response = create_mock_response(chunks);
1010
1011 let parser = |event: &str| -> Result<Option<String>, LLMError> {
1012 if let Some(content) = event.strip_prefix("data: ") {
1013 let content = content.trim();
1014 if content.is_empty() {
1015 return Ok(None);
1016 }
1017 Ok(Some(content.to_string()))
1018 } else {
1019 Ok(None)
1020 }
1021 };
1022
1023 let mut stream = create_sse_stream(mock_response, parser);
1024
1025 let mut results = Vec::new();
1026 while let Some(result) = stream.next().await {
1027 results.push(result);
1028 }
1029
1030 assert_eq!(results.len(), 1);
1031 assert_eq!(results[0].as_ref().unwrap(), "Positive reactions");
1032 }
1033
1034 #[tokio::test]
1035 async fn test_create_sse_stream_handles_split_sse_events() {
1036 let event1 = "data: First event\n\n";
1037 let event2 = "data: Second event\n\n";
1038 let combined = format!("{}{}", event1, event2);
1039 let test_data = combined.as_bytes().to_vec();
1040
1041 let split_point = event1.len() + 5;
1042 let chunks: Vec<Result<Bytes, reqwest::Error>> = vec![
1043 Ok(Bytes::from(test_data[..split_point].to_vec())),
1044 Ok(Bytes::from(test_data[split_point..].to_vec())),
1045 ];
1046
1047 let mock_response = create_mock_response(chunks);
1048
1049 let parser = |event: &str| -> Result<Option<String>, LLMError> {
1050 if let Some(content) = event.strip_prefix("data: ") {
1051 let content = content.trim();
1052 if content.is_empty() {
1053 return Ok(None);
1054 }
1055 Ok(Some(content.to_string()))
1056 } else {
1057 Ok(None)
1058 }
1059 };
1060
1061 let mut stream = create_sse_stream(mock_response, parser);
1062
1063 let mut results = Vec::new();
1064 while let Some(result) = stream.next().await {
1065 results.push(result);
1066 }
1067
1068 assert_eq!(results.len(), 2);
1069 assert_eq!(results[0].as_ref().unwrap(), "First event");
1070 assert_eq!(results[1].as_ref().unwrap(), "Second event");
1071 }
1072
1073 #[tokio::test]
1074 async fn test_create_sse_stream_handles_multibyte_utf8_split() {
1075 let multibyte_char = "✨";
1076 let event = format!("data: Star {}\n\n", multibyte_char);
1077 let test_data = event.as_bytes().to_vec();
1078
1079 let emoji_start = event.find(multibyte_char).unwrap();
1080 let split_in_emoji = emoji_start + 1;
1081
1082 let chunks: Vec<Result<Bytes, reqwest::Error>> = vec![
1083 Ok(Bytes::from(test_data[..split_in_emoji].to_vec())),
1084 Ok(Bytes::from(test_data[split_in_emoji..].to_vec())),
1085 ];
1086
1087 let mock_response = create_mock_response(chunks);
1088
1089 let parser = |event: &str| -> Result<Option<String>, LLMError> {
1090 if let Some(content) = event.strip_prefix("data: ") {
1091 let content = content.trim();
1092 if content.is_empty() {
1093 return Ok(None);
1094 }
1095 Ok(Some(content.to_string()))
1096 } else {
1097 Ok(None)
1098 }
1099 };
1100
1101 let mut stream = create_sse_stream(mock_response, parser);
1102
1103 let mut results = Vec::new();
1104 while let Some(result) = stream.next().await {
1105 results.push(result);
1106 }
1107
1108 assert_eq!(results.len(), 1);
1109 assert_eq!(
1110 results[0].as_ref().unwrap(),
1111 &format!("Star {}", multibyte_char)
1112 );
1113 }
1114
1115 fn create_mock_response(chunks: Vec<Result<Bytes, reqwest::Error>>) -> reqwest::Response {
1116 use http_body_util::StreamBody;
1117 use reqwest::Body;
1118
1119 let frame_stream = futures::stream::iter(
1120 chunks
1121 .into_iter()
1122 .map(|chunk| chunk.map(hyper::body::Frame::data)),
1123 );
1124
1125 let body = StreamBody::new(frame_stream);
1126 let body = Body::wrap(body);
1127
1128 let http_response = http::Response::builder().status(200).body(body).unwrap();
1129
1130 http_response.into()
1131 }
1132}
1133
1134#[cfg(test)]
1136mod model_accessor_tests {
1137 use super::*;
1138
1139 #[test]
1142 fn default_impl_returns_empty_string() {
1143 struct MinimalMock;
1144 #[async_trait]
1145 impl ChatProvider for MinimalMock {
1146 async fn chat_with_tools(
1147 &self,
1148 _messages: &[ChatMessage],
1149 _tools: Option<&[Tool]>,
1150 _json_schema: Option<StructuredOutputFormat>,
1151 ) -> Result<Box<dyn ChatResponse>, crate::error::LLMError> {
1152 unimplemented!()
1153 }
1154 }
1155 let mock = MinimalMock;
1156 assert_eq!(mock.model(), "");
1157 }
1158
1159 #[cfg(all(feature = "ollama", not(target_arch = "wasm32")))]
1161 #[test]
1162 fn ollama_backend_exposes_model_string() {
1163 let ollama = crate::backends::ollama::Ollama::new(
1164 "http://localhost:11434", None, Some("qwen2.5:14b".to_string()), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, );
1184 assert_eq!(ollama.model(), "qwen2.5:14b");
1185 }
1186
1187 #[cfg(all(feature = "anthropic", not(target_arch = "wasm32")))]
1189 #[test]
1190 fn anthropic_backend_exposes_model_string() {
1191 let anthropic = crate::backends::anthropic::Anthropic::new(
1192 "test-key", Some("claude-haiku-4-5-20251001".to_string()), None, None, None, None, None, None, None, None, );
1203 assert_eq!(anthropic.model(), "claude-haiku-4-5-20251001");
1204 }
1205
1206 #[cfg(all(feature = "ollama", not(target_arch = "wasm32")))]
1211 #[test]
1212 fn arc_dyn_chat_provider_dispatches_model_via_deref() {
1213 use std::sync::Arc;
1214 let ollama = crate::backends::ollama::Ollama::new(
1215 "http://localhost:11434",
1216 None,
1217 Some("qwen2.5:14b".to_string()),
1218 None,
1219 None,
1220 None,
1221 None,
1222 None,
1223 None,
1224 None,
1225 None,
1226 None,
1227 None,
1228 None,
1229 None,
1230 None,
1231 None,
1232 None,
1233 None,
1234 );
1235 let arc: Arc<dyn ChatProvider> = Arc::new(ollama);
1236 assert_eq!(arc.model(), "qwen2.5:14b");
1237 }
1238
1239 #[cfg(all(feature = "ollama", not(target_arch = "wasm32")))]
1253 #[tokio::test]
1254 #[ignore]
1255 async fn model_accessor_wires_to_chat_request() {
1256 use httpmock::{Method::POST, MockServer};
1257 use serde_json::json;
1258
1259 let configured_model = "qwen2.5:14b";
1260 let server = MockServer::start();
1261
1262 let provider = crate::backends::ollama::Ollama::new(
1263 server.base_url(),
1264 None,
1265 Some(configured_model.to_string()),
1266 Some(128),
1267 Some(0.0),
1268 None,
1269 None,
1270 None,
1271 None,
1272 None,
1273 None,
1274 None,
1275 None,
1276 None,
1277 None,
1278 None,
1279 None,
1280 None,
1281 None,
1282 );
1283
1284 assert_eq!(provider.model(), configured_model);
1286
1287 let model_in_body = format!("\"model\":\"{configured_model}\"");
1289 let chat_mock = server.mock(|when, then| {
1290 when.method(POST)
1291 .path("/api/chat")
1292 .body_includes(model_in_body.as_str());
1293 then.status(200).json_body(json!({
1294 "message": {
1295 "content": "mock reply",
1296 "tool_calls": null
1297 }
1298 }));
1299 });
1300
1301 let messages = vec![ChatMessage::user().content("ping").build()];
1302 let response = provider
1303 .chat_with_tools(&messages, None, None)
1304 .await
1305 .expect("Mock-backed chat_with_tools must succeed");
1306
1307 assert!(response.text().is_some(), "Response must contain text");
1309
1310 chat_mock.assert();
1312 }
1313}