1use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use thiserror::Error;
7
8use crate::message::ChatMessage;
9use crate::types::{LLMChunk, LLMConfig, LLMFeature, LLMResponse, LLMToolRequest, ToolChoice};
10
11#[async_trait]
17pub trait LLMProvider: Send + Sync {
18 async fn complete(
20 &self,
21 messages: &[ChatMessage],
22 config: Option<&LLMConfig>,
23 ) -> Result<LLMResponse, LLMError>;
24
25 async fn complete_with_tools(
27 &self,
28 _messages: &[ChatMessage],
29 _config: Option<&LLMConfig>,
30 _request: &LLMToolRequest,
31 ) -> Result<LLMResponse, LLMError> {
32 Err(LLMError::Other(format!(
33 "provider '{}' does not support native tool completion",
34 self.provider_name()
35 )))
36 }
37
38 fn configured_tool_choice(&self) -> Option<ToolChoice> {
40 None
41 }
42
43 fn supports_tool_choice(&self, _choice: &ToolChoice) -> bool {
45 false
46 }
47
48 async fn complete_stream(
50 &self,
51 messages: &[ChatMessage],
52 config: Option<&LLMConfig>,
53 ) -> Result<Box<dyn futures::Stream<Item = Result<LLMChunk, LLMError>> + Unpin + Send>, LLMError>;
54
55 fn provider_name(&self) -> &str;
57
58 fn supports(&self, feature: LLMFeature) -> bool;
60
61 fn is_terminal_error(&self, _error: &LLMError) -> bool {
67 false
68 }
69}
70
71#[async_trait]
73pub trait LLMCapability: Send + Sync {
74 async fn select_tool(
75 &self,
76 context: &TaskContext,
77 user_input: &str,
78 ) -> Result<ToolSelection, LLMError>;
79
80 async fn generate_tool_args(
81 &self,
82 tool_id: &str,
83 user_input: &str,
84 schema: &serde_json::Value,
85 ) -> Result<serde_json::Value, LLMError>;
86
87 async fn evaluate_yesno(
88 &self,
89 question: &str,
90 context: &TaskContext,
91 ) -> Result<(bool, String), LLMError>;
92
93 async fn classify(&self, input: &str, categories: &[String])
94 -> Result<(String, f32), LLMError>;
95
96 async fn process_task(
97 &self,
98 context: &TaskContext,
99 system_prompt: &str,
100 ) -> Result<LLMResponse, LLMError>;
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct TaskContext {
106 pub current_state: Option<String>,
107 pub available_tools: Vec<String>,
108 pub memory_slots: HashMap<String, serde_json::Value>,
109 pub recent_messages: Vec<ChatMessage>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ToolSelection {
115 pub tool_id: String,
116 pub confidence: f32,
117 pub reasoning: Option<String>,
118}
119
120#[derive(Debug, Error)]
122pub enum LLMError {
123 #[error("API error: {message}")]
124 API {
125 message: String,
126 status: Option<u16>,
127 },
128
129 #[error("Network error: {0}")]
130 Network(String),
131
132 #[error("Rate limit exceeded: {retry_after:?}")]
133 RateLimit {
134 retry_after: Option<std::time::Duration>,
135 },
136
137 #[error("Configuration error: {0}")]
138 Config(String),
139
140 #[error("Model not found: {0}")]
141 ModelNotFound(String),
142
143 #[error("Content filtered: {0}")]
144 ContentFiltered(String),
145
146 #[error("Serialization error: {0}")]
147 Serialization(String),
148
149 #[error("Other error: {0}")]
150 Other(String),
151}
152
153impl From<serde_json::Error> for LLMError {
154 fn from(err: serde_json::Error) -> Self {
155 LLMError::Serialization(err.to_string())
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::types::FinishReason;
163
164 struct LegacyProvider;
165
166 struct TerminalProvider;
167
168 #[async_trait]
169 impl LLMProvider for LegacyProvider {
170 async fn complete(
171 &self,
172 _messages: &[ChatMessage],
173 _config: Option<&LLMConfig>,
174 ) -> Result<LLMResponse, LLMError> {
175 Ok(LLMResponse::new("legacy", FinishReason::Stop))
176 }
177
178 async fn complete_stream(
179 &self,
180 _messages: &[ChatMessage],
181 _config: Option<&LLMConfig>,
182 ) -> Result<
183 Box<dyn futures::Stream<Item = Result<LLMChunk, LLMError>> + Unpin + Send>,
184 LLMError,
185 > {
186 Ok(Box::new(futures::stream::empty()))
187 }
188
189 fn provider_name(&self) -> &str {
190 "legacy"
191 }
192
193 fn supports(&self, _feature: LLMFeature) -> bool {
194 false
195 }
196 }
197
198 #[async_trait]
199 impl LLMProvider for TerminalProvider {
200 async fn complete(
201 &self,
202 messages: &[ChatMessage],
203 config: Option<&LLMConfig>,
204 ) -> Result<LLMResponse, LLMError> {
205 LegacyProvider.complete(messages, config).await
206 }
207
208 async fn complete_stream(
209 &self,
210 messages: &[ChatMessage],
211 config: Option<&LLMConfig>,
212 ) -> Result<
213 Box<dyn futures::Stream<Item = Result<LLMChunk, LLMError>> + Unpin + Send>,
214 LLMError,
215 > {
216 LegacyProvider.complete_stream(messages, config).await
217 }
218
219 fn provider_name(&self) -> &str {
220 "terminal"
221 }
222
223 fn supports(&self, _feature: LLMFeature) -> bool {
224 false
225 }
226
227 fn is_terminal_error(&self, error: &LLMError) -> bool {
228 matches!(error, LLMError::Config(_) | LLMError::Serialization(_))
229 }
230 }
231
232 #[test]
233 fn additive_tool_methods_preserve_legacy_implementations() {
234 let provider = LegacyProvider;
235 let request = LLMToolRequest {
236 tools: Vec::new(),
237 choice: ToolChoice::Auto,
238 };
239
240 assert!(provider.configured_tool_choice().is_none());
241 assert!(!provider.supports_tool_choice(&ToolChoice::Auto));
242 assert!(!provider.is_terminal_error(&LLMError::Config("legacy".to_string())));
243 let error = futures::executor::block_on(provider.complete_with_tools(&[], None, &request))
244 .unwrap_err();
245 assert!(
246 error
247 .to_string()
248 .contains("does not support native tool completion")
249 );
250 }
251
252 #[test]
253 fn terminal_error_classification_is_additive_and_provider_owned() {
254 let provider = TerminalProvider;
255
256 assert!(provider.is_terminal_error(&LLMError::Config("bad config".to_string())));
257 assert!(provider.is_terminal_error(&LLMError::Serialization("bad history".to_string())));
258 assert!(!provider.is_terminal_error(&LLMError::Network("temporary".to_string())));
259 }
260}