Skip to main content

autoagents_llm/
lib.rs

1//! AutoAgents LLM is a unified interface for interacting with Large Language Model providers.
2//!
3//! # Overview
4//! This crate provides a consistent API for working with different LLM backends by abstracting away
5//! provider-specific implementation details. It supports:
6//!
7//! - Chat-based interactions
8//! - Text completion
9//! - Embeddings generation
10//! - Multiple providers (OpenAI, Anthropic, etc.)
11//! - Request validation and retry logic
12//!
13//! # Architecture
14//! The crate is organized into modules that handle different aspects of LLM interactions:
15
16use std::fmt::Display;
17
18use serde::{Deserialize, Serialize};
19
20// The OpenAI Responses backend supports a WASI Preview2 (`wasm32-wasip2`)
21// HTTP transport via the `wasi-http` feature using `golem-wasi-http` over the
22// `wasi:http` host interface. The unsupported combinations below produce
23// precise, actionable compile errors.
24
25// 1. Any HTTP provider feature on non-WASI browser wasm (e.g. `wasm32-unknown-unknown`)
26//    is unsupported: there is no socket/HTTP host interface available there.
27#[cfg(all(
28    target_arch = "wasm32",
29    not(target_os = "wasi"),
30    any(
31        feature = "openai",
32        feature = "anthropic",
33        feature = "ollama",
34        feature = "deepseek",
35        feature = "xai",
36        feature = "phind",
37        feature = "google",
38        feature = "groq",
39        feature = "azure_openai",
40        feature = "openrouter",
41        feature = "minimax"
42    )
43))]
44compile_error!(
45    "autoagents-llm HTTP provider backends are not supported on non-WASI wasm32 targets \
46(such as wasm32-unknown-unknown). Use a native target, or target wasm32-wasip2 with the \
47`wasi-http` feature for the OpenAI Responses backend."
48);
49
50// 2. WASI Preview1 (`wasm32-wasip1`) has no Preview2 HTTP host interface; the only
51//    WASI HTTP transport shipped by this crate requires Preview2.
52#[cfg(all(
53    target_arch = "wasm32",
54    target_os = "wasi",
55    target_env = "p1",
56    any(
57        feature = "openai",
58        feature = "anthropic",
59        feature = "ollama",
60        feature = "deepseek",
61        feature = "xai",
62        feature = "phind",
63        feature = "google",
64        feature = "groq",
65        feature = "azure_openai",
66        feature = "openrouter",
67        feature = "minimax"
68    )
69))]
70compile_error!(
71    "autoagents-llm HTTP provider backends are not supported on wasm32-wasip1. \
72WASI HTTP requires the Preview2 target (wasm32-wasip2); rebuild with `--target wasm32-wasip2` \
73and the `wasi-http` feature for the OpenAI Responses backend."
74);
75
76// 3. On WASI Preview2 the OpenAI Responses backend requires the `wasi-http` feature
77//    to pull in the `wasip2` HTTP bindings.
78#[cfg(all(
79    target_arch = "wasm32",
80    target_os = "wasi",
81    target_env = "p2",
82    feature = "openai",
83    not(feature = "wasi-http")
84))]
85compile_error!(
86    "autoagents-llm OpenAI Responses backend on wasm32-wasip2 requires the `wasi-http` feature. \
87Rebuild with `--features openai,wasi-http`."
88);
89
90// 4. Other HTTP providers are not yet wired to the WASI Preview2 transport.
91#[cfg(all(
92    target_arch = "wasm32",
93    target_os = "wasi",
94    target_env = "p2",
95    any(
96        feature = "anthropic",
97        feature = "ollama",
98        feature = "deepseek",
99        feature = "xai",
100        feature = "phind",
101        feature = "google",
102        feature = "groq",
103        feature = "azure_openai",
104        feature = "openrouter",
105        feature = "minimax"
106    )
107))]
108compile_error!(
109    "autoagents-llm only supports the OpenAI Responses backend on wasm32-wasip2 in this release. \
110Remove the non-openai HTTP provider features, or build for a native target."
111);
112
113/// Backend implementations for supported LLM providers like OpenAI, Anthropic, etc.
114pub mod backends;
115
116/// Builder pattern for configuring and instantiating LLM providers
117pub mod builder;
118
119/// Chat-based interactions with language models (e.g. ChatGPT style)
120pub mod chat;
121
122/// Text completion capabilities (e.g. GPT-3 style completion)
123pub mod completion;
124
125/// Vector embeddings generation for text
126pub mod embedding;
127
128/// Error types and handling
129pub mod error;
130
131/// Shared configuration constants.
132pub mod config;
133
134/// Centralized HTTP response handling for provider backends.
135#[cfg(any(
136    not(target_arch = "wasm32"),
137    all(
138        target_arch = "wasm32",
139        target_os = "wasi",
140        target_env = "p2",
141        feature = "wasi-http"
142    )
143))]
144pub mod http;
145
146/// Evaluator for LLM providers
147pub mod evaluator;
148
149/// Secret store for storing API keys and other sensitive information
150#[cfg(not(target_arch = "wasm32"))]
151pub mod secret_store;
152
153/// Listing models support
154pub mod models;
155
156mod protocol;
157pub mod providers;
158mod request_diagnostics;
159
160/// Composable optimization pipeline for LLM providers.
161pub mod pipeline;
162
163/// Built-in optimization passes (cache, etc.). Not available on WASM.
164#[cfg(all(not(target_arch = "wasm32"), feature = "optim"))]
165pub mod optim;
166
167/// Direct WASI Preview2 (`wasm32-wasip2`) HTTP transport used by the OpenAI
168/// Responses backend when the `wasi-http` feature is enabled.
169#[cfg(all(
170    target_arch = "wasm32",
171    target_os = "wasi",
172    target_env = "p2",
173    feature = "wasi-http"
174))]
175mod wasi_http;
176
177//Re-export for convenience
178pub use async_trait::async_trait;
179pub use chat::SamplingOverrides;
180
181/// Unit config for providers with no provider-specific options.
182#[derive(Debug, Default, Clone)]
183pub struct NoConfig;
184
185/// Provides an associated configuration type for LLM provider builders.
186///
187/// Implement this alongside [`LLMProvider`] to expose provider-specific builder
188/// options. Use [`NoConfig`] for providers with no special options.
189pub trait HasConfig {
190    /// Provider-specific configuration type.
191    type Config: Default + Send + Sync + 'static;
192}
193
194/// Core trait that all LLM providers must implement, combining chat, completion
195/// and embedding capabilities into a unified interface
196pub trait LLMProvider:
197    chat::ChatProvider
198    + completion::CompletionProvider
199    + embedding::EmbeddingProvider
200    + models::ModelsProvider
201    + Send
202    + Sync
203    + 'static
204{
205}
206
207/// Tool call represents a function call that an LLM wants to make.
208/// This is a standardized structure used across all providers.
209#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
210pub struct ToolCall {
211    /// The ID of the tool call.
212    pub id: String,
213    /// The type of the tool call (usually "function").
214    #[serde(rename = "type")]
215    pub call_type: String,
216    /// The function to call.
217    pub function: FunctionCall,
218}
219
220impl Display for ToolCall {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        write!(
223            f,
224            "ToolCall {{ id: {}, type: {}, function: {:?} }}",
225            self.id, self.call_type, self.function
226        )
227    }
228}
229
230/// FunctionCall contains details about which function to call and with what arguments.
231#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
232pub struct FunctionCall {
233    /// The name of the function to call.
234    pub name: String,
235    /// The arguments to pass to the function, typically serialized as a JSON string.
236    pub arguments: String,
237}
238
239/// Default value for call_type field in ToolCall
240pub fn default_call_type() -> String {
241    "function".to_string()
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::HasConfig;
248    use crate::chat::{ChatMessage, ChatProvider, ChatResponse, StructuredOutputFormat, Tool};
249    use crate::completion::CompletionProvider;
250    use crate::embedding::EmbeddingProvider;
251    use crate::error::LLMError;
252    use async_trait::async_trait;
253    use serde_json::json;
254
255    #[test]
256    fn test_tool_call_creation() {
257        let tool_call = ToolCall {
258            id: "call_123".to_string(),
259            call_type: "function".to_string(),
260            function: FunctionCall {
261                name: "test_function".to_string(),
262                arguments: "{\"param\": \"value\"}".to_string(),
263            },
264        };
265
266        assert_eq!(tool_call.id, "call_123");
267        assert_eq!(tool_call.call_type, "function");
268        assert_eq!(tool_call.function.name, "test_function");
269        assert_eq!(tool_call.function.arguments, "{\"param\": \"value\"}");
270    }
271
272    #[test]
273    fn test_tool_call_serialization() {
274        let tool_call = ToolCall {
275            id: "call_456".to_string(),
276            call_type: "function".to_string(),
277            function: FunctionCall {
278                name: "serialize_test".to_string(),
279                arguments: "{\"test\": true}".to_string(),
280            },
281        };
282
283        let serialized = serde_json::to_string(&tool_call).unwrap();
284        let deserialized: ToolCall = serde_json::from_str(&serialized).unwrap();
285
286        assert_eq!(deserialized.id, "call_456");
287        assert_eq!(deserialized.call_type, "function");
288        assert_eq!(deserialized.function.name, "serialize_test");
289        assert_eq!(deserialized.function.arguments, "{\"test\": true}");
290    }
291
292    #[test]
293    fn test_tool_call_equality() {
294        let tool_call1 = ToolCall {
295            id: "call_1".to_string(),
296            call_type: "function".to_string(),
297            function: FunctionCall {
298                name: "equal_test".to_string(),
299                arguments: "{}".to_string(),
300            },
301        };
302
303        let tool_call2 = ToolCall {
304            id: "call_1".to_string(),
305            call_type: "function".to_string(),
306            function: FunctionCall {
307                name: "equal_test".to_string(),
308                arguments: "{}".to_string(),
309            },
310        };
311
312        let tool_call3 = ToolCall {
313            id: "call_2".to_string(),
314            call_type: "function".to_string(),
315            function: FunctionCall {
316                name: "equal_test".to_string(),
317                arguments: "{}".to_string(),
318            },
319        };
320
321        assert_eq!(tool_call1, tool_call2);
322        assert_ne!(tool_call1, tool_call3);
323    }
324
325    #[test]
326    fn test_tool_call_clone() {
327        let tool_call = ToolCall {
328            id: "clone_test".to_string(),
329            call_type: "function".to_string(),
330            function: FunctionCall {
331                name: "test_clone".to_string(),
332                arguments: "{\"clone\": true}".to_string(),
333            },
334        };
335
336        let cloned = tool_call.clone();
337        assert_eq!(tool_call, cloned);
338        assert_eq!(tool_call.id, cloned.id);
339        assert_eq!(tool_call.function.name, cloned.function.name);
340    }
341
342    #[test]
343    fn test_tool_call_debug() {
344        let tool_call = ToolCall {
345            id: "debug_test".to_string(),
346            call_type: "function".to_string(),
347            function: FunctionCall {
348                name: "debug_function".to_string(),
349                arguments: "{}".to_string(),
350            },
351        };
352
353        let debug_str = format!("{tool_call:?}");
354        assert!(debug_str.contains("ToolCall"));
355        assert!(debug_str.contains("debug_test"));
356        assert!(debug_str.contains("debug_function"));
357    }
358
359    #[test]
360    fn test_function_call_creation() {
361        let function_call = FunctionCall {
362            name: "test_function".to_string(),
363            arguments: "{\"param1\": \"value1\", \"param2\": 42}".to_string(),
364        };
365
366        assert_eq!(function_call.name, "test_function");
367        assert_eq!(
368            function_call.arguments,
369            "{\"param1\": \"value1\", \"param2\": 42}"
370        );
371    }
372
373    #[test]
374    fn test_function_call_serialization() {
375        let function_call = FunctionCall {
376            name: "serialize_function".to_string(),
377            arguments: "{\"data\": [1, 2, 3]}".to_string(),
378        };
379
380        let serialized = serde_json::to_string(&function_call).unwrap();
381        let deserialized: FunctionCall = serde_json::from_str(&serialized).unwrap();
382
383        assert_eq!(deserialized.name, "serialize_function");
384        assert_eq!(deserialized.arguments, "{\"data\": [1, 2, 3]}");
385    }
386
387    #[test]
388    fn test_function_call_equality() {
389        let func1 = FunctionCall {
390            name: "equal_func".to_string(),
391            arguments: "{}".to_string(),
392        };
393
394        let func2 = FunctionCall {
395            name: "equal_func".to_string(),
396            arguments: "{}".to_string(),
397        };
398
399        let func3 = FunctionCall {
400            name: "different_func".to_string(),
401            arguments: "{}".to_string(),
402        };
403
404        assert_eq!(func1, func2);
405        assert_ne!(func1, func3);
406    }
407
408    #[test]
409    fn test_function_call_clone() {
410        let function_call = FunctionCall {
411            name: "clone_func".to_string(),
412            arguments: "{\"clone\": \"test\"}".to_string(),
413        };
414
415        let cloned = function_call.clone();
416        assert_eq!(function_call, cloned);
417        assert_eq!(function_call.name, cloned.name);
418        assert_eq!(function_call.arguments, cloned.arguments);
419    }
420
421    #[test]
422    fn test_function_call_debug() {
423        let function_call = FunctionCall {
424            name: "debug_func".to_string(),
425            arguments: "{}".to_string(),
426        };
427
428        let debug_str = format!("{function_call:?}");
429        assert!(debug_str.contains("FunctionCall"));
430        assert!(debug_str.contains("debug_func"));
431    }
432
433    #[test]
434    fn test_tool_call_with_empty_values() {
435        let tool_call = ToolCall {
436            id: String::default(),
437            call_type: String::default(),
438            function: FunctionCall {
439                name: String::default(),
440                arguments: String::default(),
441            },
442        };
443
444        assert!(tool_call.id.is_empty());
445        assert!(tool_call.call_type.is_empty());
446        assert!(tool_call.function.name.is_empty());
447        assert!(tool_call.function.arguments.is_empty());
448    }
449
450    #[test]
451    fn test_tool_call_with_complex_arguments() {
452        let complex_args = json!({
453            "nested": {
454                "array": [1, 2, 3],
455                "object": {
456                    "key": "value"
457                }
458            },
459            "simple": "string"
460        });
461
462        let tool_call = ToolCall {
463            id: "complex_call".to_string(),
464            call_type: "function".to_string(),
465            function: FunctionCall {
466                name: "complex_function".to_string(),
467                arguments: complex_args.to_string(),
468            },
469        };
470
471        let serialized = serde_json::to_string(&tool_call).unwrap();
472        let deserialized: ToolCall = serde_json::from_str(&serialized).unwrap();
473
474        assert_eq!(deserialized.id, "complex_call");
475        assert_eq!(deserialized.function.name, "complex_function");
476        // Arguments should be preserved as string
477        assert!(deserialized.function.arguments.contains("nested"));
478        assert!(deserialized.function.arguments.contains("array"));
479    }
480
481    #[test]
482    fn test_tool_call_with_unicode() {
483        let tool_call = ToolCall {
484            id: "unicode_call".to_string(),
485            call_type: "function".to_string(),
486            function: FunctionCall {
487                name: "unicode_function".to_string(),
488                arguments: "{\"message\": \"Hello δΈ–η•Œ! 🌍\"}".to_string(),
489            },
490        };
491
492        let serialized = serde_json::to_string(&tool_call).unwrap();
493        let deserialized: ToolCall = serde_json::from_str(&serialized).unwrap();
494
495        assert_eq!(deserialized.id, "unicode_call");
496        assert_eq!(deserialized.function.name, "unicode_function");
497        assert!(deserialized.function.arguments.contains("Hello δΈ–η•Œ! 🌍"));
498    }
499
500    #[test]
501    fn test_tool_call_large_arguments() {
502        let large_arg = "x".repeat(10000);
503        let tool_call = ToolCall {
504            id: "large_call".to_string(),
505            call_type: "function".to_string(),
506            function: FunctionCall {
507                name: "large_function".to_string(),
508                arguments: format!("{{\"large_param\": \"{large_arg}\"}}"),
509            },
510        };
511
512        let serialized = serde_json::to_string(&tool_call).unwrap();
513        let deserialized: ToolCall = serde_json::from_str(&serialized).unwrap();
514
515        assert_eq!(deserialized.id, "large_call");
516        assert_eq!(deserialized.function.name, "large_function");
517        assert!(deserialized.function.arguments.len() > 10000);
518    }
519
520    // Mock LLM provider for testing
521    struct MockLLMProvider;
522
523    #[async_trait]
524    impl chat::ChatProvider for MockLLMProvider {
525        async fn chat(
526            &self,
527            _messages: &[ChatMessage],
528            _json_schema: Option<StructuredOutputFormat>,
529        ) -> Result<Box<dyn ChatResponse>, LLMError> {
530            Ok(Box::new(MockChatResponse {
531                text: Some("Mock response".into()),
532            }))
533        }
534
535        async fn chat_with_tools(
536            &self,
537            _messages: &[ChatMessage],
538            _tools: Option<&[Tool]>,
539            _json_schema: Option<StructuredOutputFormat>,
540        ) -> Result<Box<dyn ChatResponse>, LLMError> {
541            Ok(Box::new(MockChatResponse {
542                text: Some("Mock response".into()),
543            }))
544        }
545    }
546
547    #[async_trait]
548    impl completion::CompletionProvider for MockLLMProvider {
549        async fn complete(
550            &self,
551            _req: &completion::CompletionRequest,
552            _json_schema: Option<chat::StructuredOutputFormat>,
553        ) -> Result<completion::CompletionResponse, error::LLMError> {
554            Ok(completion::CompletionResponse {
555                text: "Mock completion".to_string(),
556            })
557        }
558    }
559
560    #[async_trait]
561    impl embedding::EmbeddingProvider for MockLLMProvider {
562        async fn embed(&self, input: Vec<String>) -> Result<Vec<Vec<f32>>, error::LLMError> {
563            let mut embeddings = Vec::new();
564            for (i, _) in input.iter().enumerate() {
565                embeddings.push(vec![i as f32, (i + 1) as f32]);
566            }
567            Ok(embeddings)
568        }
569    }
570
571    #[async_trait]
572    impl models::ModelsProvider for MockLLMProvider {}
573
574    impl LLMProvider for MockLLMProvider {}
575
576    impl HasConfig for MockLLMProvider {
577        type Config = NoConfig;
578    }
579
580    struct MockChatResponse {
581        text: Option<String>,
582    }
583
584    impl chat::ChatResponse for MockChatResponse {
585        fn text(&self) -> Option<String> {
586            self.text.clone()
587        }
588
589        fn tool_calls(&self) -> Option<Vec<ToolCall>> {
590            None
591        }
592    }
593
594    impl std::fmt::Debug for MockChatResponse {
595        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
596            write!(f, "MockChatResponse")
597        }
598    }
599
600    impl std::fmt::Display for MockChatResponse {
601        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602            write!(f, "{}", self.text.as_deref().unwrap_or(""))
603        }
604    }
605
606    #[tokio::test]
607    async fn test_llm_provider_trait_chat() {
608        let provider = MockLLMProvider;
609        let messages = vec![chat::ChatMessage::user().content("Test").build()];
610
611        let response = provider.chat(&messages, None).await.unwrap();
612        assert_eq!(response.text(), Some("Mock response".to_string()));
613    }
614
615    #[tokio::test]
616    async fn test_chat_and_sampling_default_impl_ignores_sampling() {
617        // Default trait impl delegates to chat_with_tools, dropping sampling.
618        // Backends without per-call sampling support must not error when
619        // overrides are passed β€” `Some(SamplingOverrides::...)` is silently
620        // ignored. Asserts the contract documented on
621        // `chat_with_tools_and_sampling`.
622        let provider = MockLLMProvider;
623        let messages = vec![chat::ChatMessage::user().content("Test").build()];
624
625        // None override path: identical to chat_with_tools.
626        let baseline = provider.chat(&messages, None).await.unwrap();
627        let none_override = provider
628            .chat_and_sampling(&messages, None, None)
629            .await
630            .unwrap();
631        assert_eq!(baseline.text(), none_override.text());
632
633        // Some override path: also succeeds (mock ignores it).
634        let with_overrides = provider
635            .chat_and_sampling(
636                &messages,
637                None,
638                Some(&chat::SamplingOverrides {
639                    temperature: Some(0.0),
640                    top_p: Some(0.9),
641                    max_tokens: Some(64),
642                }),
643            )
644            .await
645            .unwrap();
646        assert_eq!(with_overrides.text(), Some("Mock response".to_string()));
647    }
648
649    #[tokio::test]
650    async fn test_chat_with_tools_and_sampling_default_impl_ignores_sampling() {
651        let provider = MockLLMProvider;
652        let messages = vec![chat::ChatMessage::user().content("Test").build()];
653
654        let response = provider
655            .chat_with_tools_and_sampling(
656                &messages,
657                None,
658                None,
659                Some(&chat::SamplingOverrides::with_temperature(0.0)),
660            )
661            .await
662            .unwrap();
663        assert_eq!(response.text(), Some("Mock response".to_string()));
664    }
665
666    #[test]
667    fn test_sampling_overrides_helpers() {
668        let empty = chat::SamplingOverrides::empty();
669        assert_eq!(empty, chat::SamplingOverrides::default());
670        assert!(empty.temperature.is_none());
671        assert!(empty.top_p.is_none());
672        assert!(empty.max_tokens.is_none());
673
674        let temp = chat::SamplingOverrides::with_temperature(0.0);
675        assert_eq!(temp.temperature, Some(0.0));
676        assert_eq!(temp.top_p, None);
677
678        let top_p = chat::SamplingOverrides::with_top_p(0.95);
679        assert_eq!(top_p.top_p, Some(0.95));
680        assert_eq!(top_p.temperature, None);
681
682        let max_tok = chat::SamplingOverrides::with_max_tokens(128);
683        assert_eq!(max_tok.max_tokens, Some(128));
684
685        // SamplingOverrides is re-exported at crate root.
686        let from_root = SamplingOverrides::with_temperature(0.5);
687        assert_eq!(from_root.temperature, Some(0.5));
688    }
689
690    #[tokio::test]
691    async fn test_llm_provider_trait_completion() {
692        let provider = MockLLMProvider;
693        let request = completion::CompletionRequest::new("Test prompt");
694
695        let response = provider.complete(&request, None).await.unwrap();
696        assert_eq!(response.text, "Mock completion");
697    }
698
699    #[tokio::test]
700    async fn test_llm_provider_trait_embedding() {
701        let provider = MockLLMProvider;
702        let input = vec!["First".to_string(), "Second".to_string()];
703
704        let embeddings = provider.embed(input).await.unwrap();
705        assert_eq!(embeddings.len(), 2);
706        assert_eq!(embeddings[0], vec![0.0, 1.0]);
707        assert_eq!(embeddings[1], vec![1.0, 2.0]);
708    }
709}