1use std::error::Error;
14use std::fmt;
15use std::pin::Pin;
16
17use async_trait::async_trait;
18use serde::{Deserialize, Serialize};
19use tokio_stream::Stream;
20
21use crate::turn::ConversationTurnId;
22
23#[derive(Debug, Clone)]
29#[must_use]
30pub struct RunnerError {
31 pub kind: ErrorKind,
33 pub message: String,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ErrorKind {
40 Internal,
42 ExternalService,
44 Timeout,
46 BinaryNotFound,
48 AuthFailure,
50 Config,
52 Guardrail,
54 ContextLength,
61 ModelUnavailable,
67}
68
69impl ErrorKind {
70 #[must_use]
76 pub const fn is_transient(self) -> bool {
77 matches!(self, Self::Timeout | Self::ExternalService)
78 }
79}
80
81impl RunnerError {
82 pub fn internal(message: impl Into<String>) -> Self {
84 Self {
85 kind: ErrorKind::Internal,
86 message: message.into(),
87 }
88 }
89
90 pub fn external_service(service: impl Into<String>, message: impl Into<String>) -> Self {
92 Self {
93 kind: ErrorKind::ExternalService,
94 message: format!("{}: {}", service.into(), message.into()),
95 }
96 }
97
98 pub fn binary_not_found(binary: impl Into<String>) -> Self {
100 Self {
101 kind: ErrorKind::BinaryNotFound,
102 message: format!("Binary not found: {}", binary.into()),
103 }
104 }
105
106 pub fn auth_failure(message: impl Into<String>) -> Self {
108 Self {
109 kind: ErrorKind::AuthFailure,
110 message: message.into(),
111 }
112 }
113
114 pub fn config(message: impl Into<String>) -> Self {
116 Self {
117 kind: ErrorKind::Config,
118 message: message.into(),
119 }
120 }
121
122 pub fn timeout(message: impl Into<String>) -> Self {
124 Self {
125 kind: ErrorKind::Timeout,
126 message: message.into(),
127 }
128 }
129
130 pub fn guardrail(message: impl Into<String>) -> Self {
132 Self {
133 kind: ErrorKind::Guardrail,
134 message: message.into(),
135 }
136 }
137
138 pub fn context_length(message: impl Into<String>) -> Self {
140 Self {
141 kind: ErrorKind::ContextLength,
142 message: message.into(),
143 }
144 }
145
146 pub fn model_unavailable(model: impl Into<String>) -> Self {
148 let model = model.into();
149 Self {
150 kind: ErrorKind::ModelUnavailable,
151 message: format!("Model {model:?} is not available"),
152 }
153 }
154}
155
156impl fmt::Display for RunnerError {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 write!(f, "{:?}: {}", self.kind, self.message)
159 }
160}
161
162impl Error for RunnerError {}
163
164bitflags::bitflags! {
169 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
174 pub struct LlmCapabilities: u16 {
175 const STREAMING = 0b0000_0000_0001;
177 const FUNCTION_CALLING = 0b0000_0000_0010;
179 const VISION = 0b0000_0000_0100;
181 const JSON_MODE = 0b0000_0000_1000;
183 const SYSTEM_MESSAGES = 0b0000_0001_0000;
185 const SDK_TOOL_CALLING = 0b0000_0010_0000;
187 const TEMPERATURE = 0b0000_0100_0000;
189 const MAX_TOKENS = 0b0000_1000_0000;
191 const TOP_P = 0b0001_0000_0000;
193 const STOP_SEQUENCES = 0b0010_0000_0000;
195 const RESPONSE_FORMAT = 0b0100_0000_0000;
197 }
198}
199
200impl LlmCapabilities {
201 #[must_use]
203 pub const fn text_only() -> Self {
204 Self::STREAMING.union(Self::SYSTEM_MESSAGES)
205 }
206
207 #[must_use]
209 pub const fn full_featured() -> Self {
210 Self::STREAMING
211 .union(Self::FUNCTION_CALLING)
212 .union(Self::VISION)
213 .union(Self::JSON_MODE)
214 .union(Self::SYSTEM_MESSAGES)
215 }
216
217 #[must_use]
219 pub const fn supports_streaming(&self) -> bool {
220 self.contains(Self::STREAMING)
221 }
222
223 #[must_use]
225 pub const fn supports_function_calling(&self) -> bool {
226 self.contains(Self::FUNCTION_CALLING)
227 }
228
229 #[must_use]
231 pub const fn supports_vision(&self) -> bool {
232 self.contains(Self::VISION)
233 }
234
235 #[must_use]
237 pub const fn supports_json_mode(&self) -> bool {
238 self.contains(Self::JSON_MODE)
239 }
240
241 #[must_use]
243 pub const fn supports_system_messages(&self) -> bool {
244 self.contains(Self::SYSTEM_MESSAGES)
245 }
246
247 #[must_use]
249 pub const fn supports_sdk_tool_calling(&self) -> bool {
250 self.contains(Self::SDK_TOOL_CALLING)
251 }
252
253 #[must_use]
255 pub const fn supports_temperature(&self) -> bool {
256 self.contains(Self::TEMPERATURE)
257 }
258
259 #[must_use]
261 pub const fn supports_max_tokens(&self) -> bool {
262 self.contains(Self::MAX_TOKENS)
263 }
264
265 #[must_use]
267 pub const fn supports_top_p(&self) -> bool {
268 self.contains(Self::TOP_P)
269 }
270
271 #[must_use]
273 pub const fn supports_stop_sequences(&self) -> bool {
274 self.contains(Self::STOP_SEQUENCES)
275 }
276
277 #[must_use]
279 pub const fn supports_response_format(&self) -> bool {
280 self.contains(Self::RESPONSE_FORMAT)
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
290#[serde(rename_all = "lowercase")]
291pub enum MessageRole {
292 System,
294 User,
296 Assistant,
298 Tool,
300}
301
302impl MessageRole {
303 #[must_use]
305 pub const fn as_str(&self) -> &'static str {
306 match self {
307 Self::System => "system",
308 Self::User => "user",
309 Self::Assistant => "assistant",
310 Self::Tool => "tool",
311 }
312 }
313}
314
315const VALID_IMAGE_MIME_TYPES: &[&str] = &["image/png", "image/jpeg", "image/webp", "image/gif"];
317
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub struct ImagePart {
321 pub data: String,
323 pub mime_type: String,
325}
326
327impl ImagePart {
328 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Result<Self, RunnerError> {
336 let mime_type = mime_type.into();
337 if !VALID_IMAGE_MIME_TYPES.contains(&mime_type.as_str()) {
338 return Err(RunnerError::config(format!(
339 "Unsupported image MIME type '{mime_type}'; expected one of: {}",
340 VALID_IMAGE_MIME_TYPES.join(", ")
341 )));
342 }
343 Ok(Self {
344 data: data.into(),
345 mime_type,
346 })
347 }
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct ChatMessage {
353 pub role: MessageRole,
355 pub content: String,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub images: Option<Vec<ImagePart>>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub tool_calls: Option<Vec<ToolCallRequest>>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub tool_call_id: Option<String>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub name: Option<String>,
369}
370
371impl ChatMessage {
372 #[must_use]
374 pub fn new(role: MessageRole, content: impl Into<String>) -> Self {
375 Self {
376 role,
377 content: content.into(),
378 images: None,
379 tool_calls: None,
380 tool_call_id: None,
381 name: None,
382 }
383 }
384
385 #[must_use]
387 pub fn system(content: impl Into<String>) -> Self {
388 Self::new(MessageRole::System, content)
389 }
390
391 #[must_use]
393 pub fn user(content: impl Into<String>) -> Self {
394 Self::new(MessageRole::User, content)
395 }
396
397 #[must_use]
399 pub fn user_with_images(content: impl Into<String>, images: Vec<ImagePart>) -> Self {
400 Self {
401 role: MessageRole::User,
402 content: content.into(),
403 images: Some(images),
404 tool_calls: None,
405 tool_call_id: None,
406 name: None,
407 }
408 }
409
410 #[must_use]
412 pub fn assistant(content: impl Into<String>) -> Self {
413 Self::new(MessageRole::Assistant, content)
414 }
415
416 #[must_use]
418 pub fn tool(
419 name: impl Into<String>,
420 tool_call_id: impl Into<String>,
421 content: impl Into<String>,
422 ) -> Self {
423 Self {
424 role: MessageRole::Tool,
425 content: content.into(),
426 images: None,
427 tool_calls: None,
428 tool_call_id: Some(tool_call_id.into()),
429 name: Some(name.into()),
430 }
431 }
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct ToolCallRequest {
441 pub id: String,
443 pub function_name: String,
445 pub arguments: serde_json::Value,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct ToolDefinition {
452 pub name: String,
454 pub description: String,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub parameters: Option<serde_json::Value>,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
463pub enum ToolChoice {
464 Auto,
466 None,
468 Required,
470 Specific {
472 name: String,
474 },
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
479pub enum ResponseFormat {
480 Text,
482 JsonObject,
484 JsonSchema {
486 name: String,
488 schema: serde_json::Value,
490 },
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
499pub struct McpHeader {
500 pub name: String,
502 pub value: String,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
512pub enum McpTransport {
513 Http {
515 url: String,
517 headers: Vec<McpHeader>,
519 },
520 Sse {
522 url: String,
524 headers: Vec<McpHeader>,
526 },
527 Stdio {
529 command: String,
531 args: Vec<String>,
533 env: Vec<McpHeader>,
535 },
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
544pub struct McpServerConfig {
545 pub name: String,
547 pub transport: McpTransport,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct ChatRequest {
554 pub messages: Vec<ChatMessage>,
556 pub model: Option<String>,
558 pub temperature: Option<f32>,
564 pub max_tokens: Option<u32>,
570 pub stream: bool,
572 #[serde(default, skip_serializing_if = "Option::is_none")]
574 pub tools: Option<Vec<ToolDefinition>>,
575 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub tool_choice: Option<ToolChoice>,
578 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub top_p: Option<f32>,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub stop: Option<Vec<String>>,
584 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub response_format: Option<ResponseFormat>,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
594 pub turn_id: Option<ConversationTurnId>,
595 #[serde(default, skip_serializing_if = "Vec::is_empty")]
601 pub mcp_servers: Vec<McpServerConfig>,
602}
603
604impl ChatRequest {
605 #[must_use]
607 pub const fn new(messages: Vec<ChatMessage>) -> Self {
608 Self {
609 messages,
610 model: None,
611 temperature: None,
612 max_tokens: None,
613 stream: false,
614 tools: None,
615 tool_choice: None,
616 top_p: None,
617 stop: None,
618 response_format: None,
619 turn_id: None,
620 mcp_servers: Vec::new(),
621 }
622 }
623
624 #[must_use]
626 pub fn with_mcp_servers(mut self, mcp_servers: Vec<McpServerConfig>) -> Self {
627 self.mcp_servers = mcp_servers;
628 self
629 }
630
631 #[must_use]
633 pub fn with_model(mut self, model: impl Into<String>) -> Self {
634 self.model = Some(model.into());
635 self
636 }
637
638 #[must_use]
640 pub const fn with_temperature(mut self, temperature: f32) -> Self {
641 self.temperature = Some(temperature);
642 self
643 }
644
645 #[must_use]
647 pub const fn with_max_tokens(mut self, max_tokens: u32) -> Self {
648 self.max_tokens = Some(max_tokens);
649 self
650 }
651
652 #[must_use]
654 pub const fn with_streaming(mut self) -> Self {
655 self.stream = true;
656 self
657 }
658
659 #[must_use]
661 pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
662 self.tools = Some(tools);
663 self
664 }
665
666 #[must_use]
668 pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
669 self.tool_choice = Some(tool_choice);
670 self
671 }
672
673 #[must_use]
675 pub const fn with_top_p(mut self, top_p: f32) -> Self {
676 self.top_p = Some(top_p);
677 self
678 }
679
680 #[must_use]
682 pub fn with_stop(mut self, stop: Vec<String>) -> Self {
683 self.stop = Some(stop);
684 self
685 }
686
687 #[must_use]
689 pub fn with_response_format(mut self, response_format: ResponseFormat) -> Self {
690 self.response_format = Some(response_format);
691 self
692 }
693
694 #[must_use]
700 pub const fn with_turn_id(mut self, turn_id: ConversationTurnId) -> Self {
701 self.turn_id = Some(turn_id);
702 self
703 }
704
705 #[must_use]
707 pub fn has_images(&self) -> bool {
708 self.messages
709 .iter()
710 .any(|m| m.images.as_ref().is_some_and(|imgs| !imgs.is_empty()))
711 }
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
716pub struct ChatResponse {
717 pub content: String,
719 pub model: String,
721 pub usage: Option<TokenUsage>,
723 pub finish_reason: Option<String>,
725 #[serde(skip_serializing_if = "Option::is_none")]
727 pub warnings: Option<Vec<String>>,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub tool_calls: Option<Vec<ToolCallRequest>>,
731}
732
733#[derive(Debug, Clone, Serialize, Deserialize)]
735pub struct TokenUsage {
736 pub prompt_tokens: u32,
738 pub completion_tokens: u32,
740 pub total_tokens: u32,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize)]
746pub struct StreamChunk {
747 pub delta: String,
749 pub is_final: bool,
751 pub finish_reason: Option<String>,
753}
754
755pub type ChatStream = Pin<Box<dyn Stream<Item = Result<StreamChunk, RunnerError>> + Send>>;
757
758#[async_trait]
768pub trait LlmProvider: Send + Sync {
769 fn name(&self) -> &'static str;
771
772 fn display_name(&self) -> &str;
774
775 fn capabilities(&self) -> LlmCapabilities;
777
778 fn default_model(&self) -> &str;
780
781 fn available_models(&self) -> &[String];
783
784 async fn complete(&self, request: &ChatRequest) -> Result<ChatResponse, RunnerError>;
786
787 async fn complete_stream(&self, request: &ChatRequest) -> Result<ChatStream, RunnerError>;
792
793 async fn health_check(&self) -> Result<bool, RunnerError>;
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use serde_json::json;
801
802 #[test]
803 fn is_transient_classification() {
804 assert!(ErrorKind::Timeout.is_transient());
805 assert!(ErrorKind::ExternalService.is_transient());
806 assert!(!ErrorKind::Internal.is_transient());
807 assert!(!ErrorKind::BinaryNotFound.is_transient());
808 assert!(!ErrorKind::AuthFailure.is_transient());
809 assert!(!ErrorKind::Config.is_transient());
810 assert!(!ErrorKind::Guardrail.is_transient());
811 assert!(!ErrorKind::ContextLength.is_transient());
812 assert!(!ErrorKind::ModelUnavailable.is_transient());
813 }
814
815 #[test]
816 fn model_unavailable_constructor() {
817 let err = RunnerError::model_unavailable("claude-opus-4.6-fast");
818 assert_eq!(err.kind, ErrorKind::ModelUnavailable);
819 assert!(err.message.contains("claude-opus-4.6-fast"));
820 }
821
822 #[test]
823 fn tool_call_request_serde_round_trip() {
824 let tc = ToolCallRequest {
825 id: "call_1".to_owned(),
826 function_name: "get_weather".to_owned(),
827 arguments: json!({"city": "Paris"}),
828 };
829 let json = serde_json::to_string(&tc).unwrap(); let deserialized: ToolCallRequest = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.id, "call_1");
832 assert_eq!(deserialized.function_name, "get_weather");
833 assert_eq!(deserialized.arguments["city"], "Paris");
834 }
835
836 #[test]
837 fn tool_definition_serde_round_trip() {
838 let td = ToolDefinition {
839 name: "search".to_owned(),
840 description: "Search the web".to_owned(),
841 parameters: Some(json!({"type": "object", "properties": {"q": {"type": "string"}}})),
842 };
843 let json = serde_json::to_string(&td).unwrap(); let deserialized: ToolDefinition = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.name, "search");
846 assert!(deserialized.parameters.is_some());
847 }
848
849 #[test]
850 fn tool_definition_without_parameters() {
851 let td = ToolDefinition {
852 name: "ping".to_owned(),
853 description: "Check connectivity".to_owned(),
854 parameters: None,
855 };
856 let json = serde_json::to_string(&td).unwrap(); assert!(!json.contains("parameters"));
858 let deserialized: ToolDefinition = serde_json::from_str(&json).unwrap(); assert!(deserialized.parameters.is_none());
860 }
861
862 #[test]
863 fn tool_choice_serde_variants() {
864 let auto = ToolChoice::Auto;
865 let json = serde_json::to_string(&auto).unwrap(); let deserialized: ToolChoice = serde_json::from_str(&json).unwrap(); assert!(matches!(deserialized, ToolChoice::Auto));
868
869 let none = ToolChoice::None;
870 let json = serde_json::to_string(&none).unwrap(); let deserialized: ToolChoice = serde_json::from_str(&json).unwrap(); assert!(matches!(deserialized, ToolChoice::None));
873
874 let required = ToolChoice::Required;
875 let json = serde_json::to_string(&required).unwrap(); let deserialized: ToolChoice = serde_json::from_str(&json).unwrap(); assert!(matches!(deserialized, ToolChoice::Required));
878
879 let specific = ToolChoice::Specific {
880 name: "get_weather".to_owned(),
881 };
882 let json = serde_json::to_string(&specific).unwrap(); let deserialized: ToolChoice = serde_json::from_str(&json).unwrap(); assert!(matches!(deserialized, ToolChoice::Specific { name } if name == "get_weather"));
885 }
886
887 #[test]
888 fn response_format_serde_variants() {
889 let text = ResponseFormat::Text;
890 let json = serde_json::to_string(&text).unwrap(); let deserialized: ResponseFormat = serde_json::from_str(&json).unwrap(); assert!(matches!(deserialized, ResponseFormat::Text));
893
894 let json_obj = ResponseFormat::JsonObject;
895 let json = serde_json::to_string(&json_obj).unwrap(); let deserialized: ResponseFormat = serde_json::from_str(&json).unwrap(); assert!(matches!(deserialized, ResponseFormat::JsonObject));
898
899 let json_schema = ResponseFormat::JsonSchema {
900 name: "person".to_owned(),
901 schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}),
902 };
903 let json = serde_json::to_string(&json_schema).unwrap(); let deserialized: ResponseFormat = serde_json::from_str(&json).unwrap(); assert!(
906 matches!(deserialized, ResponseFormat::JsonSchema { name, .. } if name == "person")
907 );
908 }
909
910 #[test]
911 fn chat_message_tool_constructor() {
912 let msg = ChatMessage::tool("get_weather", "call_1", r#"{"temp": 72}"#);
913 assert_eq!(msg.role, MessageRole::Tool);
914 assert_eq!(msg.content, r#"{"temp": 72}"#);
915 assert_eq!(msg.tool_call_id.as_deref(), Some("call_1"));
916 assert_eq!(msg.name.as_deref(), Some("get_weather"));
917 assert!(msg.tool_calls.is_none());
918 }
919
920 #[test]
921 fn chat_message_regular_constructors_have_none_tool_fields() {
922 let user = ChatMessage::user("hello");
923 assert!(user.tool_calls.is_none());
924 assert!(user.tool_call_id.is_none());
925 assert!(user.name.is_none());
926 assert!(user.images.is_none());
927 }
928
929 #[test]
930 fn image_part_valid_mime_types() {
931 for mime in &["image/png", "image/jpeg", "image/webp", "image/gif"] {
932 let part = ImagePart::new("base64data", *mime);
933 assert!(part.is_ok(), "Expected {mime} to be valid");
934 }
935 }
936
937 #[test]
938 fn image_part_invalid_mime_type() {
939 let err = ImagePart::new("data", "image/bmp").unwrap_err();
940 assert_eq!(err.kind, ErrorKind::Config);
941 assert!(err.message.contains("image/bmp"));
942 }
943
944 #[test]
945 fn user_with_images_constructor() {
946 let img = ImagePart::new("aGVsbG8=", "image/png").unwrap(); let msg = ChatMessage::user_with_images("describe this", vec![img]);
948 assert_eq!(msg.role, MessageRole::User);
949 assert_eq!(msg.content, "describe this");
950 let images = msg.images.as_ref().unwrap(); assert_eq!(images.len(), 1);
952 assert_eq!(images[0].mime_type, "image/png");
953 }
954
955 #[test]
956 fn chat_request_has_images() {
957 let img = ImagePart::new("data", "image/jpeg").unwrap(); let with = ChatRequest::new(vec![ChatMessage::user_with_images("x", vec![img])]);
959 assert!(with.has_images());
960
961 let without = ChatRequest::new(vec![ChatMessage::user("text only")]);
962 assert!(!without.has_images());
963 }
964
965 #[test]
966 fn chat_request_has_images_empty_vec() {
967 let msg = ChatMessage::user_with_images("x", vec![]);
968 let req = ChatRequest::new(vec![msg]);
969 assert!(!req.has_images());
970 }
971
972 #[test]
973 fn image_part_serde_round_trip() {
974 let img = ImagePart::new("aGVsbG8=", "image/png").unwrap(); let json = serde_json::to_string(&img).unwrap(); let deserialized: ImagePart = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized, img);
978 }
979
980 #[test]
981 fn chat_message_with_images_serde_round_trip() {
982 let img = ImagePart::new("data", "image/jpeg").unwrap(); let msg = ChatMessage::user_with_images("describe", vec![img]);
984 let json = serde_json::to_string(&msg).unwrap(); let deserialized: ChatMessage = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.images.as_ref().unwrap().len(), 1); assert_eq!(deserialized.images.unwrap()[0].mime_type, "image/jpeg"); }
989
990 #[test]
991 fn chat_message_without_images_backward_compat() {
992 let json = r#"{"role":"user","content":"hello"}"#;
993 let msg: ChatMessage = serde_json::from_str(json).unwrap(); assert!(msg.images.is_none());
995 assert_eq!(msg.content, "hello");
996 }
997
998 #[test]
999 fn chat_message_images_not_serialized_when_none() {
1000 let msg = ChatMessage::user("hello");
1001 let json = serde_json::to_string(&msg).unwrap(); assert!(!json.contains("images"));
1003 }
1004
1005 #[test]
1006 fn chat_request_builder_methods() {
1007 let req = ChatRequest::new(vec![ChatMessage::user("hi")])
1008 .with_tools(vec![ToolDefinition {
1009 name: "test".to_owned(),
1010 description: "test fn".to_owned(),
1011 parameters: None,
1012 }])
1013 .with_tool_choice(ToolChoice::Required)
1014 .with_top_p(0.9)
1015 .with_stop(vec!["END".to_owned()])
1016 .with_response_format(ResponseFormat::JsonObject);
1017
1018 assert!(req.tools.is_some());
1019 assert!(matches!(req.tool_choice, Some(ToolChoice::Required)));
1020 assert_eq!(req.top_p, Some(0.9));
1021 assert_eq!(req.stop.as_ref().unwrap()[0], "END"); assert!(matches!(
1023 req.response_format,
1024 Some(ResponseFormat::JsonObject)
1025 ));
1026 }
1027
1028 #[test]
1029 fn message_role_tool_as_str() {
1030 assert_eq!(MessageRole::Tool.as_str(), "tool");
1031 }
1032
1033 #[test]
1034 fn capability_flags_new_fields() {
1035 let caps = LlmCapabilities::TOP_P
1036 | LlmCapabilities::STOP_SEQUENCES
1037 | LlmCapabilities::RESPONSE_FORMAT;
1038 assert!(caps.supports_top_p());
1039 assert!(caps.supports_stop_sequences());
1040 assert!(caps.supports_response_format());
1041
1042 let empty = LlmCapabilities::empty();
1043 assert!(!empty.supports_top_p());
1044 assert!(!empty.supports_stop_sequences());
1045 assert!(!empty.supports_response_format());
1046 }
1047}