1use std::collections::HashMap;
22use std::time::Duration;
23
24#[derive(Debug, Clone)]
26pub struct CompletionRequest {
27 pub model: String,
29 pub messages: Vec<ChatMessage>,
31 pub max_tokens: Option<u32>,
33 pub temperature: Option<f32>,
35 pub system: Option<String>,
37 pub tools: Vec<ToolDefinition>,
39 pub stop: Vec<String>,
41 pub metadata: HashMap<String, String>,
43}
44
45impl Default for CompletionRequest {
46 fn default() -> Self {
47 Self {
48 model: "gpt-4".to_string(),
49 messages: Vec::new(),
50 max_tokens: Some(1024),
51 temperature: Some(0.7),
52 system: None,
53 tools: Vec::new(),
54 stop: Vec::new(),
55 metadata: HashMap::new(),
56 }
57 }
58}
59
60impl CompletionRequest {
61 pub fn new(model: impl Into<String>, user_message: impl Into<String>) -> Self {
63 Self {
64 model: model.into(),
65 messages: vec![ChatMessage::user(user_message)],
66 ..Default::default()
67 }
68 }
69
70 pub fn with_system(mut self, system: impl Into<String>) -> Self {
72 self.system = Some(system.into());
73 self
74 }
75
76 pub fn with_max_tokens(mut self, max: u32) -> Self {
78 self.max_tokens = Some(max);
79 self
80 }
81
82 pub fn with_temperature(mut self, temp: f32) -> Self {
84 self.temperature = Some(temp);
85 self
86 }
87
88 pub fn with_tool(mut self, tool: ToolDefinition) -> Self {
90 self.tools.push(tool);
91 self
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct ChatMessage {
98 pub role: String,
100 pub content: String,
102 pub tool_call_id: Option<String>,
104 pub tool_calls: Vec<ToolCall>,
106}
107
108impl ChatMessage {
109 pub fn user(content: impl Into<String>) -> Self {
111 Self {
112 role: "user".to_string(),
113 content: content.into(),
114 tool_call_id: None,
115 tool_calls: Vec::new(),
116 }
117 }
118
119 pub fn assistant(content: impl Into<String>) -> Self {
121 Self {
122 role: "assistant".to_string(),
123 content: content.into(),
124 tool_call_id: None,
125 tool_calls: Vec::new(),
126 }
127 }
128
129 pub fn tool_response(call_id: impl Into<String>, content: impl Into<String>) -> Self {
131 Self {
132 role: "tool".to_string(),
133 content: content.into(),
134 tool_call_id: Some(call_id.into()),
135 tool_calls: Vec::new(),
136 }
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct ToolDefinition {
143 pub name: String,
145 pub description: String,
147 pub parameters_schema: String,
149}
150
151#[derive(Debug, Clone)]
153pub struct ToolCall {
154 pub id: String,
156 pub name: String,
158 pub arguments: String,
160}
161
162#[derive(Debug, Clone)]
164pub struct CompletionResponse {
165 pub content: String,
167 pub model: String,
169 pub tool_calls: Vec<ToolCall>,
171 pub finish_reason: FinishReason,
173 pub usage: TokenUsage,
175 pub latency: Duration,
177 pub response_id: Option<String>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum FinishReason {
184 Stop,
186 MaxTokens,
188 ToolUse,
190 ContentFilter,
192 Unknown,
194}
195
196#[derive(Debug, Clone, Copy, Default)]
198pub struct TokenUsage {
199 pub prompt_tokens: u32,
201 pub completion_tokens: u32,
203 pub total_tokens: u32,
205}
206
207impl TokenUsage {
208 pub fn estimated_cost(&self, prompt_cost_per_1k: f64, completion_cost_per_1k: f64) -> f64 {
210 (self.prompt_tokens as f64 / 1000.0 * prompt_cost_per_1k)
211 + (self.completion_tokens as f64 / 1000.0 * completion_cost_per_1k)
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct EmbeddingRequest {
218 pub model: String,
220 pub inputs: Vec<String>,
222 pub dimensions: Option<u32>,
224}
225
226#[derive(Debug, Clone)]
228pub struct EmbeddingResponse {
229 pub embeddings: Vec<Vec<f32>>,
231 pub usage: TokenUsage,
233 pub latency: Duration,
235}
236
237pub trait LlmProvider: Send + Sync {
239 fn name(&self) -> &str;
241
242 fn complete(&self, request: &CompletionRequest) -> Result<CompletionResponse, LlmError>;
244
245 fn embed(&self, request: &EmbeddingRequest) -> Result<EmbeddingResponse, LlmError>;
247
248 fn is_available(&self) -> bool;
250
251 fn metrics(&self) -> LlmProviderMetrics;
253}
254
255#[derive(Debug, Clone, Default)]
257pub struct LlmProviderMetrics {
258 pub total_requests: u64,
260 pub total_errors: u64,
262 pub total_tokens: u64,
264 pub total_cost_usd: f64,
266 pub avg_latency: Duration,
268 pub rate_limit_remaining: Option<u32>,
270}
271
272#[derive(Debug, Clone)]
274pub enum LlmError {
275 Unavailable(String),
277 RateLimited {
279 retry_after: Duration,
281 },
282 AuthError(String),
284 InvalidRequest(String),
286 ModelNotFound(String),
288 ContentFiltered,
290 Timeout(Duration),
292 ProviderError(String),
294}
295
296impl std::fmt::Display for LlmError {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 match self {
299 Self::Unavailable(msg) => write!(f, "Provider unavailable: {}", msg),
300 Self::RateLimited { retry_after } => {
301 write!(f, "Rate limited, retry after {:?}", retry_after)
302 }
303 Self::AuthError(msg) => write!(f, "Auth error: {}", msg),
304 Self::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
305 Self::ModelNotFound(model) => write!(f, "Model not found: {}", model),
306 Self::ContentFiltered => write!(f, "Content filtered"),
307 Self::Timeout(d) => write!(f, "Timeout after {:?}", d),
308 Self::ProviderError(msg) => write!(f, "Provider error: {}", msg),
309 }
310 }
311}
312
313impl std::error::Error for LlmError {}
314
315pub struct EchoProvider {
321 total_requests: std::sync::atomic::AtomicU64,
322}
323
324impl EchoProvider {
325 pub fn new() -> Self {
327 Self {
328 total_requests: std::sync::atomic::AtomicU64::new(0),
329 }
330 }
331}
332
333impl Default for EchoProvider {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339impl LlmProvider for EchoProvider {
340 fn name(&self) -> &str {
341 "echo"
342 }
343
344 fn complete(&self, request: &CompletionRequest) -> Result<CompletionResponse, LlmError> {
345 self.total_requests
346 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
347
348 let content = request
349 .messages
350 .last()
351 .map(|m| format!("Echo: {}", m.content))
352 .unwrap_or_else(|| "Echo: (empty)".to_string());
353
354 Ok(CompletionResponse {
355 content,
356 model: request.model.clone(),
357 tool_calls: Vec::new(),
358 finish_reason: FinishReason::Stop,
359 usage: TokenUsage {
360 prompt_tokens: 10,
361 completion_tokens: 10,
362 total_tokens: 20,
363 },
364 latency: Duration::from_micros(100),
365 response_id: Some("echo-001".to_string()),
366 })
367 }
368
369 fn embed(&self, request: &EmbeddingRequest) -> Result<EmbeddingResponse, LlmError> {
370 let dim = request.dimensions.unwrap_or(384) as usize;
371 let embeddings: Vec<Vec<f32>> = request
372 .inputs
373 .iter()
374 .map(|input| {
375 let hash = input
377 .bytes()
378 .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64));
379 (0..dim)
380 .map(|i| ((hash.wrapping_add(i as u64) % 1000) as f32 / 1000.0) - 0.5)
381 .collect()
382 })
383 .collect();
384
385 Ok(EmbeddingResponse {
386 embeddings,
387 usage: TokenUsage {
388 prompt_tokens: request.inputs.len() as u32 * 5,
389 completion_tokens: 0,
390 total_tokens: request.inputs.len() as u32 * 5,
391 },
392 latency: Duration::from_micros(50),
393 })
394 }
395
396 fn is_available(&self) -> bool {
397 true
398 }
399
400 fn metrics(&self) -> LlmProviderMetrics {
401 LlmProviderMetrics {
402 total_requests: self
403 .total_requests
404 .load(std::sync::atomic::Ordering::Relaxed),
405 ..Default::default()
406 }
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn test_echo_provider_complete() {
416 let provider = EchoProvider::new();
417 let request = CompletionRequest::new("test-model", "Hello, world!");
418 let response = provider.complete(&request).unwrap();
419
420 assert!(response.content.contains("Hello, world!"));
421 assert_eq!(response.finish_reason, FinishReason::Stop);
422 assert_eq!(response.usage.total_tokens, 20);
423 }
424
425 #[test]
426 fn test_echo_provider_embed() {
427 let provider = EchoProvider::new();
428 let request = EmbeddingRequest {
429 model: "test-embed".to_string(),
430 inputs: vec!["hello".to_string(), "world".to_string()],
431 dimensions: Some(128),
432 };
433 let response = provider.embed(&request).unwrap();
434
435 assert_eq!(response.embeddings.len(), 2);
436 assert_eq!(response.embeddings[0].len(), 128);
437 }
438
439 #[test]
440 fn test_completion_request_builder() {
441 let req = CompletionRequest::new("gpt-4", "Analyze this audit finding")
442 .with_system("You are an ISA compliance expert")
443 .with_max_tokens(2048)
444 .with_temperature(0.0);
445
446 assert_eq!(req.model, "gpt-4");
447 assert_eq!(req.system.unwrap(), "You are an ISA compliance expert");
448 assert_eq!(req.max_tokens, Some(2048));
449 assert_eq!(req.temperature, Some(0.0));
450 }
451
452 #[test]
453 fn test_chat_message_types() {
454 let user = ChatMessage::user("question");
455 assert_eq!(user.role, "user");
456
457 let assistant = ChatMessage::assistant("answer");
458 assert_eq!(assistant.role, "assistant");
459
460 let tool = ChatMessage::tool_response("call-123", "result");
461 assert_eq!(tool.role, "tool");
462 assert_eq!(tool.tool_call_id.unwrap(), "call-123");
463 }
464
465 #[test]
466 fn test_token_cost_estimation() {
467 let usage = TokenUsage {
468 prompt_tokens: 1000,
469 completion_tokens: 500,
470 total_tokens: 1500,
471 };
472 let cost = usage.estimated_cost(0.03, 0.06);
474 assert!((cost - 0.06).abs() < 0.001); }
476
477 #[test]
478 fn test_provider_metrics() {
479 let provider = EchoProvider::new();
480
481 provider
482 .complete(&CompletionRequest::new("m", "a"))
483 .unwrap();
484 provider
485 .complete(&CompletionRequest::new("m", "b"))
486 .unwrap();
487
488 let m = provider.metrics();
489 assert_eq!(m.total_requests, 2);
490 }
491
492 #[test]
493 fn test_tool_definition() {
494 let tool = ToolDefinition {
495 name: "graph_query".to_string(),
496 description: "Query the knowledge graph".to_string(),
497 parameters_schema: r#"{"type":"object","properties":{"query":{"type":"string"}}}"#
498 .to_string(),
499 };
500 assert_eq!(tool.name, "graph_query");
501 }
502}