1use crate::error::LlmError;
7use async_trait::async_trait;
8use futures_core::Stream;
9use serde::{Deserialize, Serialize};
10use std::pin::Pin;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Message {
16 pub role: Role,
18 pub content: String,
20 #[serde(default)]
22 pub tool_calls: Vec<ToolCall>,
23 #[serde(default)]
25 pub tool_call_id: Option<String>,
26}
27
28impl Message {
29 pub fn system(content: impl Into<String>) -> Self {
31 Self::text(Role::System, content)
32 }
33
34 pub fn user(content: impl Into<String>) -> Self {
36 Self::text(Role::User, content)
37 }
38
39 pub fn assistant(content: impl Into<String>) -> Self {
41 Self::text(Role::Assistant, content)
42 }
43
44 fn text(role: Role, content: impl Into<String>) -> Self {
45 Self {
46 role,
47 content: content.into(),
48 tool_calls: Vec::new(),
49 tool_call_id: None,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57#[non_exhaustive]
58pub enum Role {
59 System,
61 User,
63 Assistant,
65 Tool,
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77#[non_exhaustive]
78pub struct ToolCall {
79 pub id: String,
81 pub name: String,
83 pub args: serde_json::Value,
85}
86
87impl ToolCall {
88 pub fn new(id: impl Into<String>, name: impl Into<String>, args: serde_json::Value) -> Self {
92 Self {
93 id: id.into(),
94 name: name.into(),
95 args,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102#[non_exhaustive]
103pub struct ToolDef {
104 pub name: String,
106 pub description: String,
108 pub json_schema: serde_json::Value,
110}
111
112impl ToolDef {
113 pub fn new(
119 name: impl Into<String>,
120 description: impl Into<String>,
121 json_schema: serde_json::Value,
122 ) -> Self {
123 Self {
124 name: name.into(),
125 description: description.into(),
126 json_schema,
127 }
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct ChatRequest {
134 pub messages: Vec<Message>,
136 pub tools: Vec<ToolDef>,
138 pub temperature: Option<f32>,
140 pub max_tokens: Option<u32>,
142 pub response_format: ResponseFormat,
144 pub stop: Vec<String>,
146 pub timeout: Option<Duration>,
148}
149
150impl ChatRequest {
151 pub fn new(messages: Vec<Message>) -> Self {
153 Self {
154 messages,
155 tools: Vec::new(),
156 temperature: None,
157 max_tokens: None,
158 response_format: ResponseFormat::Text,
159 stop: Vec::new(),
160 timeout: None,
161 }
162 }
163
164 pub fn builder() -> ChatRequestBuilder {
172 ChatRequestBuilder::default()
173 }
174}
175
176#[derive(Default)]
179pub struct ChatRequestBuilder {
180 messages: Vec<Message>,
181 tools: Vec<ToolDef>,
182 temperature: Option<f32>,
183 max_tokens: Option<u32>,
184 response_format: Option<ResponseFormat>,
185 timeout: Option<Duration>,
186}
187
188impl ChatRequestBuilder {
189 pub fn system(mut self, content: impl Into<String>) -> Self {
191 self.messages.push(Message::system(content));
192 self
193 }
194
195 pub fn user(mut self, content: impl Into<String>) -> Self {
197 self.messages.push(Message::user(content));
198 self
199 }
200
201 pub fn assistant(mut self, content: impl Into<String>) -> Self {
203 self.messages.push(Message::assistant(content));
204 self
205 }
206
207 pub fn message(mut self, message: Message) -> Self {
209 self.messages.push(message);
210 self
211 }
212
213 pub fn tools(mut self, tools: Vec<ToolDef>) -> Self {
215 self.tools = tools;
216 self
217 }
218
219 pub fn temperature(mut self, temperature: f32) -> Self {
221 self.temperature = Some(temperature);
222 self
223 }
224
225 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
227 self.max_tokens = Some(max_tokens);
228 self
229 }
230
231 pub fn response_format(mut self, response_format: ResponseFormat) -> Self {
233 self.response_format = Some(response_format);
234 self
235 }
236
237 pub fn timeout(mut self, timeout: Duration) -> Self {
239 self.timeout = Some(timeout);
240 self
241 }
242
243 pub fn build(self) -> ChatRequest {
246 ChatRequest {
247 messages: self.messages,
248 tools: self.tools,
249 temperature: self.temperature,
250 max_tokens: self.max_tokens,
251 response_format: self.response_format.unwrap_or(ResponseFormat::Text),
252 stop: Vec::new(),
253 timeout: self.timeout,
254 }
255 }
256}
257
258#[derive(Debug, Clone)]
260#[non_exhaustive]
261pub enum ResponseFormat {
262 Text,
264 Json {
266 schema: serde_json::Value,
268 },
269 StructuredOutput {
271 schema: serde_json::Value,
273 },
274}
275
276#[derive(Debug, Clone)]
281#[non_exhaustive]
282pub struct ChatResponse {
283 pub message: Message,
285 pub usage: Usage,
287 pub finish_reason: FinishReason,
289 pub model: String,
294}
295
296impl ChatResponse {
297 pub fn new(
303 message: Message,
304 usage: Usage,
305 finish_reason: FinishReason,
306 model: impl Into<String>,
307 ) -> Self {
308 Self {
309 message,
310 usage,
311 finish_reason,
312 model: model.into(),
313 }
314 }
315}
316
317#[derive(Debug, Clone, Default, Serialize, Deserialize)]
325#[non_exhaustive]
326pub struct Usage {
327 pub prompt_tokens: u32,
331 pub completion_tokens: u32,
334 #[serde(default)]
338 pub cache_read_tokens: u32,
339 #[serde(default)]
342 pub cache_creation_tokens: u32,
343}
344
345impl Usage {
346 pub fn new(prompt_tokens: u32, completion_tokens: u32) -> Self {
351 Self {
352 prompt_tokens,
353 completion_tokens,
354 cache_read_tokens: 0,
355 cache_creation_tokens: 0,
356 }
357 }
358
359 #[must_use]
363 pub fn with_cache_tokens(mut self, cache_read_tokens: u32, cache_creation_tokens: u32) -> Self {
364 self.cache_read_tokens = cache_read_tokens;
365 self.cache_creation_tokens = cache_creation_tokens;
366 self
367 }
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
372#[non_exhaustive]
373pub enum FinishReason {
374 Stop,
376 ToolCalls,
378 Length,
380 ContentFilter,
382 Error,
384}
385
386#[derive(Debug, Clone, Default)]
388pub struct Capabilities {
389 pub tool_calling: bool,
391 pub streaming: bool,
393 pub structured_output: bool,
395 pub embeddings: bool,
397 pub max_context_tokens: u32,
399 pub vision: bool,
401}
402
403impl Capabilities {
404 pub fn builder() -> CapabilitiesBuilder {
419 CapabilitiesBuilder(Capabilities::default())
420 }
421}
422
423#[derive(Debug, Clone, Default)]
425pub struct CapabilitiesBuilder(Capabilities);
426
427impl CapabilitiesBuilder {
428 pub fn tool_calling(mut self, v: bool) -> Self {
430 self.0.tool_calling = v;
431 self
432 }
433 pub fn streaming(mut self, v: bool) -> Self {
435 self.0.streaming = v;
436 self
437 }
438 pub fn structured_output(mut self, v: bool) -> Self {
440 self.0.structured_output = v;
441 self
442 }
443 pub fn embeddings(mut self, v: bool) -> Self {
445 self.0.embeddings = v;
446 self
447 }
448 pub fn max_context_tokens(mut self, v: u32) -> Self {
450 self.0.max_context_tokens = v;
451 self
452 }
453 pub fn vision(mut self, v: bool) -> Self {
455 self.0.vision = v;
456 self
457 }
458 pub fn build(self) -> Capabilities {
460 self.0
461 }
462}
463
464#[derive(Debug, Clone, Default)]
472#[non_exhaustive]
473pub struct ChatChunk {
474 pub delta: String,
476 pub tool_calls: Vec<ToolCall>,
478 pub finish_reason: Option<FinishReason>,
480 pub usage: Option<Usage>,
484}
485
486impl ChatChunk {
487 pub fn new(
493 delta: String,
494 tool_calls: Vec<ToolCall>,
495 finish_reason: Option<FinishReason>,
496 usage: Option<Usage>,
497 ) -> Self {
498 Self {
499 delta,
500 tool_calls,
501 finish_reason,
502 usage,
503 }
504 }
505}
506
507pub type ChunkStream = Pin<Box<dyn Stream<Item = Result<ChatChunk, LlmError>> + Send + 'static>>;
509
510pub type Embedding = Vec<f32>;
512
513#[async_trait]
533pub trait LlmClient: Send + Sync {
534 fn name(&self) -> &str;
536
537 fn capabilities(&self) -> &Capabilities;
539
540 async fn complete(&self, req: ChatRequest) -> Result<ChatResponse, LlmError>;
554
555 async fn stream(&self, req: ChatRequest) -> Result<ChunkStream, LlmError>;
569
570 async fn embed(&self, texts: &[String]) -> Result<Vec<Embedding>, LlmError>;
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587
588 #[allow(dead_code)]
590 fn _assert_dyn_compatible(_: &dyn LlmClient) {}
591
592 #[test]
593 fn chat_request_default_has_no_tools() {
594 let req = ChatRequest::new(vec![]);
595 assert!(req.tools.is_empty());
596 assert!(matches!(req.response_format, ResponseFormat::Text));
597 }
598
599 #[test]
600 fn message_constructors_set_role_and_clear_tool_fields() {
601 let m = Message::system("be terse");
602 assert_eq!(m.role, Role::System);
603 assert_eq!(m.content, "be terse");
604 assert!(m.tool_calls.is_empty());
605 assert!(m.tool_call_id.is_none());
606 let user = Message::user("hi");
607 assert_eq!((user.role, user.content.as_str()), (Role::User, "hi"));
608 let assistant = Message::assistant("ok");
609 assert_eq!(
610 (assistant.role, assistant.content.as_str()),
611 (Role::Assistant, "ok")
612 );
613 }
614
615 #[test]
616 fn chat_request_builder_sets_every_option_and_appends_raw_messages() {
617 let timeout = Duration::from_secs(7);
618 let schema = serde_json::json!({"type": "object"});
619 let req = ChatRequest::builder()
620 .assistant("a")
621 .message(Message::user("raw"))
622 .tools(vec![ToolDef::new("t", "desc", serde_json::json!({}))])
623 .max_tokens(42)
624 .response_format(ResponseFormat::Json {
625 schema: schema.clone(),
626 })
627 .timeout(timeout)
628 .build();
629 let roles: Vec<Role> = req.messages.iter().map(|m| m.role).collect();
630 assert_eq!(roles, vec![Role::Assistant, Role::User]);
631 assert_eq!(req.messages[1].content, "raw");
632 assert_eq!(req.tools.len(), 1);
633 assert_eq!(req.max_tokens, Some(42));
634 assert_eq!(req.timeout, Some(timeout));
635 assert!(matches!(req.response_format, ResponseFormat::Json { .. }));
636 }
637
638 #[test]
639 fn chat_request_builder_orders_messages_and_keeps_defaults() {
640 let req = ChatRequest::builder()
641 .system("sys")
642 .user("u")
643 .temperature(0.5)
644 .build();
645 let roles: Vec<Role> = req.messages.iter().map(|m| m.role).collect();
646 assert_eq!(roles, vec![Role::System, Role::User]);
647 assert_eq!(req.temperature, Some(0.5));
648 assert!(req.tools.is_empty());
649 assert!(matches!(req.response_format, ResponseFormat::Text));
650 }
651
652 #[test]
653 fn capabilities_builder_default_matches_struct_default() {
654 let built = Capabilities::builder().build();
655 let direct = Capabilities::default();
656 assert_eq!(built.tool_calling, direct.tool_calling);
657 assert_eq!(built.streaming, direct.streaming);
658 assert_eq!(built.structured_output, direct.structured_output);
659 assert_eq!(built.embeddings, direct.embeddings);
660 assert_eq!(built.max_context_tokens, direct.max_context_tokens);
661 assert_eq!(built.vision, direct.vision);
662 }
663
664 #[test]
665 fn capabilities_builder_sets_tool_calling() {
666 let c = Capabilities::builder().tool_calling(true).build();
667 assert!(c.tool_calling);
668 }
669
670 #[test]
671 fn capabilities_builder_sets_streaming() {
672 let c = Capabilities::builder().streaming(true).build();
673 assert!(c.streaming);
674 }
675
676 #[test]
677 fn capabilities_builder_sets_structured_output() {
678 let c = Capabilities::builder().structured_output(true).build();
679 assert!(c.structured_output);
680 }
681
682 #[test]
683 fn capabilities_builder_sets_embeddings() {
684 let c = Capabilities::builder().embeddings(true).build();
685 assert!(c.embeddings);
686 }
687
688 #[test]
689 fn capabilities_builder_sets_max_context_tokens() {
690 let c = Capabilities::builder().max_context_tokens(8000).build();
691 assert_eq!(c.max_context_tokens, 8000);
692 }
693
694 #[test]
695 fn capabilities_builder_sets_vision() {
696 let c = Capabilities::builder().vision(true).build();
697 assert!(c.vision);
698 }
699
700 #[test]
701 fn capabilities_builder_chains_all_setters() {
702 let c = Capabilities::builder()
703 .tool_calling(true)
704 .streaming(true)
705 .structured_output(true)
706 .embeddings(true)
707 .max_context_tokens(32_000)
708 .vision(false)
709 .build();
710 assert!(c.tool_calling && c.streaming && c.structured_output && c.embeddings);
711 assert_eq!(c.max_context_tokens, 32_000);
712 assert!(!c.vision);
713 }
714
715 #[test]
716 fn chat_chunk_usage_defaults_to_none_in_struct_literal() {
717 let chunk = ChatChunk {
718 delta: String::new(),
719 tool_calls: vec![],
720 finish_reason: None,
721 usage: None,
722 };
723 assert!(chunk.usage.is_none());
724 }
725
726 #[test]
727 fn chat_chunk_with_usage_round_trips() {
728 let chunk = ChatChunk {
729 delta: "done".into(),
730 tool_calls: vec![],
731 finish_reason: Some(FinishReason::Stop),
732 usage: Some(Usage::new(10, 32)),
733 };
734 let u = chunk.usage.as_ref().expect("usage set");
735 assert_eq!(u.prompt_tokens, 10);
736 assert_eq!(u.completion_tokens, 32);
737 }
738
739 #[test]
740 fn usage_new_sets_token_counts() {
741 let u = Usage::new(12, 34);
742 assert_eq!(u.prompt_tokens, 12);
743 assert_eq!(u.completion_tokens, 34);
744 }
745
746 #[test]
747 fn usage_cache_tokens_default_to_zero() {
748 let u = Usage::new(12, 34);
749 assert_eq!(u.cache_read_tokens, 0);
750 assert_eq!(u.cache_creation_tokens, 0);
751 }
752
753 #[test]
754 fn usage_deserializes_legacy_json_without_cache_fields() {
755 let legacy = r#"{"prompt_tokens":10,"completion_tokens":5}"#;
758 let u: Usage = serde_json::from_str(legacy).unwrap();
759 assert_eq!(u.cache_read_tokens, 0);
760 assert_eq!(u.cache_creation_tokens, 0);
761 }
762
763 #[test]
764 fn usage_with_cache_tokens_sets_cache_fields() {
765 let u = Usage::new(12, 34).with_cache_tokens(100, 8);
769 assert_eq!(u.prompt_tokens, 12);
770 assert_eq!(u.completion_tokens, 34);
771 assert_eq!(u.cache_read_tokens, 100);
772 assert_eq!(u.cache_creation_tokens, 8);
773 }
774
775 #[test]
776 fn tooldef_new_sets_fields() {
777 let d = ToolDef::new("echo", "echoes input", serde_json::json!({"type":"object"}));
778 assert_eq!(d.name, "echo");
779 assert_eq!(d.description, "echoes input");
780 assert_eq!(d.json_schema, serde_json::json!({"type":"object"}));
781 }
782
783 #[test]
784 fn toolcall_new_sets_fields() {
785 let c = ToolCall::new("id-1", "search", serde_json::json!({"q":"x"}));
786 assert_eq!(c.id, "id-1");
787 assert_eq!(c.name, "search");
788 assert_eq!(c.args, serde_json::json!({"q":"x"}));
789 }
790
791 #[test]
792 fn chatresponse_new_carries_model() {
793 let msg = Message {
794 role: Role::Assistant,
795 content: "hi".into(),
796 tool_calls: vec![],
797 tool_call_id: None,
798 };
799 let r = ChatResponse::new(
800 msg,
801 Usage::new(1, 2),
802 FinishReason::Stop,
803 "anthropic:claude-sonnet-4-6",
804 );
805 assert_eq!(r.model, "anthropic:claude-sonnet-4-6");
806 assert_eq!(r.finish_reason, FinishReason::Stop);
807 assert_eq!(r.message.content, "hi");
808 assert_eq!(r.usage.prompt_tokens, 1);
809 }
810}