autoagents_llm/
builder.rs

1//! Builder module for configuring and instantiating LLM providers.
2//!
3//! This module provides a flexible builder pattern for creating and configuring
4//! LLM (Large Language Model) provider instances with various settings and options.
5
6use crate::{
7    chat::{
8        FunctionTool, ParameterProperty, ParametersSchema, ReasoningEffort, StructuredOutputFormat,
9        Tool, ToolChoice,
10    },
11    error::LLMError,
12    LLMProvider,
13};
14use std::{collections::HashMap, marker::PhantomData};
15
16/// A function type for validating LLM provider outputs.
17/// Takes a response string and returns Ok(()) if valid, or Err with an error message if invalid.
18pub type ValidatorFn = dyn Fn(&str) -> Result<(), String> + Send + Sync + 'static;
19
20/// Supported LLM backend providers.
21#[derive(Debug, Clone)]
22pub enum LLMBackend {
23    /// OpenAI API provider (GPT-3, GPT-4, etc.)
24    OpenAI,
25    /// Anthropic API provider (Claude models)
26    Anthropic,
27    /// Ollama local LLM provider for self-hosted models
28    Ollama,
29    /// DeepSeek API provider for their LLM models
30    DeepSeek,
31    /// X.AI (formerly Twitter) API provider
32    XAI,
33    /// Phind API provider for code-specialized models
34    Phind,
35    /// Google Gemini API provider
36    Google,
37    /// Groq API provider
38    Groq,
39    /// Azure OpenAI API provider
40    AzureOpenAI,
41}
42
43/// Implements string parsing for LLMBackend enum.
44///
45/// Converts a string representation of a backend provider name into the corresponding
46/// LLMBackend variant. The parsing is case-insensitive.
47///
48/// # Arguments
49///
50/// * `s` - The string to parse
51///
52/// # Returns
53///
54/// * `Ok(LLMBackend)` - The corresponding backend variant if valid
55/// * `Err(LLMError)` - An error if the string doesn't match any known backend
56///
57/// # Examples
58///
59/// ```
60/// use std::str::FromStr;
61/// use autoagents_llm::builder::LLMBackend;
62///
63/// let backend = LLMBackend::from_str("openai").unwrap();
64/// assert!(matches!(backend, LLMBackend::OpenAI));
65///
66/// let err = LLMBackend::from_str("invalid").unwrap_err();
67/// assert!(err.to_string().contains("Unknown LLM backend"));
68/// ```
69impl std::str::FromStr for LLMBackend {
70    type Err = LLMError;
71
72    fn from_str(s: &str) -> Result<Self, Self::Err> {
73        match s.to_lowercase().as_str() {
74            "openai" => Ok(LLMBackend::OpenAI),
75            "anthropic" => Ok(LLMBackend::Anthropic),
76            "ollama" => Ok(LLMBackend::Ollama),
77            "deepseek" => Ok(LLMBackend::DeepSeek),
78            "xai" => Ok(LLMBackend::XAI),
79            "phind" => Ok(LLMBackend::Phind),
80            "google" => Ok(LLMBackend::Google),
81            "groq" => Ok(LLMBackend::Groq),
82            "azure-openai" => Ok(LLMBackend::AzureOpenAI),
83            _ => Err(LLMError::InvalidRequest(format!(
84                "Unknown LLM backend: {s}"
85            ))),
86        }
87    }
88}
89
90/// Builder for configuring and instantiating LLM providers.
91///
92/// Provides a fluent interface for setting various configuration options
93/// like model selection, API keys, generation parameters, etc.
94pub struct LLMBuilder<L: LLMProvider> {
95    /// Selected backend provider
96    pub(crate) backend: PhantomData<L>,
97    /// API key for authentication with the provider
98    pub(crate) api_key: Option<String>,
99    /// Base URL for API requests (primarily for self-hosted instances)
100    pub(crate) base_url: Option<String>,
101    /// Model identifier/name to use
102    pub(crate) model: Option<String>,
103    /// Maximum tokens to generate in responses
104    pub(crate) max_tokens: Option<u32>,
105    /// Temperature parameter for controlling response randomness (0.0-1.0)
106    pub(crate) temperature: Option<f32>,
107    /// System prompt/context to guide model behavior
108    pub(crate) system: Option<String>,
109    /// Request timeout duration in seconds
110    pub(crate) timeout_seconds: Option<u64>,
111    /// Whether to enable streaming responses
112    pub(crate) stream: Option<bool>,
113    /// Top-p (nucleus) sampling parameter
114    pub(crate) top_p: Option<f32>,
115    /// Top-k sampling parameter
116    pub(crate) top_k: Option<u32>,
117    /// Format specification for embedding outputs
118    pub(crate) embedding_encoding_format: Option<String>,
119    /// Vector dimensions for embedding outputs
120    pub(crate) embedding_dimensions: Option<u32>,
121    /// Optional validation function for response content
122    pub(crate) validator: Option<Box<ValidatorFn>>,
123    /// Number of retry attempts when validation fails
124    pub(crate) validator_attempts: usize,
125    /// Function tools
126    pub(crate) tools: Option<Vec<Tool>>,
127    /// Tool choice
128    pub(crate) tool_choice: Option<ToolChoice>,
129    /// Enable parallel tool use
130    pub(crate) enable_parallel_tool_use: Option<bool>,
131    /// Enable reasoning
132    pub(crate) reasoning: Option<bool>,
133    /// Enable reasoning effort
134    pub(crate) reasoning_effort: Option<String>,
135    /// reasoning_budget_tokens
136    pub(crate) reasoning_budget_tokens: Option<u32>,
137    /// JSON schema for structured output
138    pub(crate) json_schema: Option<StructuredOutputFormat>,
139    /// API Version
140    pub(crate) api_version: Option<String>,
141    /// Deployment Id
142    pub(crate) deployment_id: Option<String>,
143    /// Voice
144    #[allow(dead_code)]
145    pub(crate) voice: Option<String>,
146}
147
148impl<L: LLMProvider> Default for LLMBuilder<L> {
149    fn default() -> Self {
150        Self {
151            backend: PhantomData,
152            api_key: None,
153            base_url: None,
154            model: None,
155            max_tokens: None,
156            temperature: None,
157            system: None,
158            timeout_seconds: None,
159            stream: None,
160            top_p: None,
161            top_k: None,
162            embedding_encoding_format: None,
163            embedding_dimensions: None,
164            validator: None,
165            validator_attempts: 0,
166            tools: None,
167            tool_choice: None,
168            enable_parallel_tool_use: None,
169            reasoning: None,
170            reasoning_effort: None,
171            reasoning_budget_tokens: None,
172            json_schema: None,
173            api_version: None,
174            deployment_id: None,
175            voice: None,
176        }
177    }
178}
179
180impl<L: LLMProvider> LLMBuilder<L> {
181    /// Creates a new empty builder instance with default values.
182    pub fn new() -> Self {
183        Self::default()
184    }
185
186    /// Sets the API key for authentication.
187    pub fn api_key(mut self, key: impl Into<String>) -> Self {
188        self.api_key = Some(key.into());
189        self
190    }
191
192    /// Sets the base URL for API requests.
193    pub fn base_url(mut self, url: impl Into<String>) -> Self {
194        self.base_url = Some(url.into());
195        self
196    }
197
198    /// Sets the model identifier to use.
199    pub fn model(mut self, model: impl Into<String>) -> Self {
200        self.model = Some(model.into());
201        self
202    }
203
204    /// Sets the maximum number of tokens to generate.
205    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
206        self.max_tokens = Some(max_tokens);
207        self
208    }
209
210    /// Sets the temperature for controlling response randomness (0.0-1.0).
211    pub fn temperature(mut self, temperature: f32) -> Self {
212        self.temperature = Some(temperature);
213        self
214    }
215
216    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
217        self.tools = Some(tools);
218        self
219    }
220
221    /// Sets the system prompt/context.
222    pub fn system(mut self, system: impl Into<String>) -> Self {
223        self.system = Some(system.into());
224        self
225    }
226
227    /// Sets the reasoning flag.
228    pub fn reasoning_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
229        self.reasoning_effort = Some(reasoning_effort.to_string());
230        self
231    }
232
233    /// Sets the reasoning flag.
234    pub fn reasoning(mut self, reasoning: bool) -> Self {
235        self.reasoning = Some(reasoning);
236        self
237    }
238
239    /// Sets the reasoning budget tokens.
240    pub fn reasoning_budget_tokens(mut self, reasoning_budget_tokens: u32) -> Self {
241        self.reasoning_budget_tokens = Some(reasoning_budget_tokens);
242        self
243    }
244
245    /// Sets the request timeout in seconds.
246    pub fn timeout_seconds(mut self, timeout_seconds: u64) -> Self {
247        self.timeout_seconds = Some(timeout_seconds);
248        self
249    }
250
251    /// Enables or disables streaming responses.
252    pub fn stream(mut self, stream: bool) -> Self {
253        self.stream = Some(stream);
254        self
255    }
256
257    /// Sets the top-p (nucleus) sampling parameter.
258    pub fn top_p(mut self, top_p: f32) -> Self {
259        self.top_p = Some(top_p);
260        self
261    }
262
263    /// Sets the top-k sampling parameter.
264    pub fn top_k(mut self, top_k: u32) -> Self {
265        self.top_k = Some(top_k);
266        self
267    }
268
269    /// Sets the encoding format for embeddings.
270    pub fn embedding_encoding_format(
271        mut self,
272        embedding_encoding_format: impl Into<String>,
273    ) -> Self {
274        self.embedding_encoding_format = Some(embedding_encoding_format.into());
275        self
276    }
277
278    /// Sets the dimensions for embeddings.
279    pub fn embedding_dimensions(mut self, embedding_dimensions: u32) -> Self {
280        self.embedding_dimensions = Some(embedding_dimensions);
281        self
282    }
283
284    /// Sets the JSON schema for structured output.
285    pub fn schema(mut self, schema: impl Into<StructuredOutputFormat>) -> Self {
286        self.json_schema = Some(schema.into());
287        self
288    }
289
290    /// Sets a validation function to verify LLM responses.
291    ///
292    /// # Arguments
293    ///
294    /// * `f` - Function that takes a response string and returns Ok(()) if valid, or Err with error message if invalid
295    pub fn validator<F>(mut self, f: F) -> Self
296    where
297        F: Fn(&str) -> Result<(), String> + Send + Sync + 'static,
298    {
299        self.validator = Some(Box::new(f));
300        self
301    }
302
303    /// Sets the number of retry attempts for validation failures.
304    ///
305    /// # Arguments
306    ///
307    /// * `attempts` - Maximum number of times to retry generating a valid response
308    pub fn validator_attempts(mut self, attempts: usize) -> Self {
309        self.validator_attempts = attempts;
310        self
311    }
312
313    /// Adds a function tool to the builder
314    pub fn function(mut self, function_builder: FunctionBuilder) -> Self {
315        if self.tools.is_none() {
316            self.tools = Some(Vec::new());
317        }
318        if let Some(tools) = &mut self.tools {
319            tools.push(function_builder.build());
320        }
321        self
322    }
323
324    /// Enable parallel tool use
325    pub fn enable_parallel_tool_use(mut self, enable: bool) -> Self {
326        self.enable_parallel_tool_use = Some(enable);
327        self
328    }
329
330    /// Set tool choice.  Note that if the choice is given as Tool(name), and that
331    /// tool isn't available, the builder will fail.
332    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
333        self.tool_choice = Some(choice);
334        self
335    }
336
337    /// Explicitly disable the use of tools, even if they are provided.
338    pub fn disable_tools(mut self) -> Self {
339        self.tool_choice = Some(ToolChoice::None);
340        self
341    }
342
343    /// Set the API version.
344    pub fn api_version(mut self, api_version: impl Into<String>) -> Self {
345        self.api_version = Some(api_version.into());
346        self
347    }
348
349    /// Set the deployment id. Used in Azure OpenAI.
350    pub fn deployment_id(mut self, deployment_id: impl Into<String>) -> Self {
351        self.deployment_id = Some(deployment_id.into());
352        self
353    }
354
355    // Validate that tool configuration is consistent and valid
356    #[allow(dead_code)]
357    pub(crate) fn validate_tool_config(
358        &self,
359    ) -> Result<(Option<Vec<Tool>>, Option<ToolChoice>), LLMError> {
360        match self.tool_choice {
361            Some(ToolChoice::Tool(ref name)) => {
362                match self.tools.clone().map(|tools| tools.iter().any(|tool| tool.function.name == *name)) {
363                        Some(true) => Ok((self.tools.clone(), self.tool_choice.clone())),
364                        _ => Err(LLMError::ToolConfigError(format!("Tool({name}) cannot be tool choice: no tool with name {name} found.  Did you forget to add it with .function?"))),
365                    }
366            }
367            Some(_) if self.tools.is_none() => Err(LLMError::ToolConfigError(
368                "Tool choice cannot be set without tools configured".to_string(),
369            )),
370            _ => Ok((self.tools.clone(), self.tool_choice.clone())),
371        }
372    }
373}
374
375/// Builder for function parameters
376pub struct ParamBuilder {
377    name: String,
378    property_type: String,
379    description: String,
380    items: Option<Box<ParameterProperty>>,
381    enum_list: Option<Vec<String>>,
382}
383
384impl ParamBuilder {
385    /// Creates a new parameter builder
386    pub fn new(name: impl Into<String>) -> Self {
387        Self {
388            name: name.into(),
389            property_type: "string".to_string(),
390            description: String::new(),
391            items: None,
392            enum_list: None,
393        }
394    }
395
396    /// Sets the parameter type
397    pub fn type_of(mut self, type_str: impl Into<String>) -> Self {
398        self.property_type = type_str.into();
399        self
400    }
401
402    /// Sets the parameter description
403    pub fn description(mut self, desc: impl Into<String>) -> Self {
404        self.description = desc.into();
405        self
406    }
407
408    /// Sets the array item type for array parameters
409    pub fn items(mut self, item_property: ParameterProperty) -> Self {
410        self.items = Some(Box::new(item_property));
411        self
412    }
413
414    /// Sets the enum values for enum parameters
415    pub fn enum_values(mut self, values: Vec<String>) -> Self {
416        self.enum_list = Some(values);
417        self
418    }
419
420    /// Builds the parameter property
421    fn build(self) -> (String, ParameterProperty) {
422        (
423            self.name,
424            ParameterProperty {
425                property_type: self.property_type,
426                description: self.description,
427                items: self.items,
428                enum_list: self.enum_list,
429            },
430        )
431    }
432}
433
434/// Builder for function tools
435pub struct FunctionBuilder {
436    name: String,
437    description: String,
438    parameters: Vec<ParamBuilder>,
439    required: Vec<String>,
440    raw_schema: Option<serde_json::Value>,
441}
442
443impl FunctionBuilder {
444    /// Creates a new function builder
445    pub fn new(name: impl Into<String>) -> Self {
446        Self {
447            name: name.into(),
448            description: String::new(),
449            parameters: Vec::new(),
450            required: Vec::new(),
451            raw_schema: None,
452        }
453    }
454
455    /// Sets the function description
456    pub fn description(mut self, desc: impl Into<String>) -> Self {
457        self.description = desc.into();
458        self
459    }
460
461    /// Adds a parameter to the function
462    pub fn param(mut self, param: ParamBuilder) -> Self {
463        self.parameters.push(param);
464        self
465    }
466
467    /// Marks parameters as required
468    pub fn required(mut self, param_names: Vec<String>) -> Self {
469        self.required = param_names;
470        self
471    }
472
473    /// Provides a full JSON Schema for the parameters.  Using this method
474    /// bypasses the DSL and allows arbitrary complex schemas (nested arrays,
475    /// objects, oneOf, etc.).
476    pub fn json_schema(mut self, schema: serde_json::Value) -> Self {
477        self.raw_schema = Some(schema);
478        self
479    }
480
481    /// Builds the function tool
482    fn build(self) -> Tool {
483        let parameters_value = if let Some(schema) = self.raw_schema {
484            schema
485        } else {
486            let mut properties = HashMap::new();
487            for param in self.parameters {
488                let (name, prop) = param.build();
489                properties.insert(name, prop);
490            }
491
492            serde_json::to_value(ParametersSchema {
493                schema_type: "object".to_string(),
494                properties,
495                required: self.required,
496            })
497            .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()))
498        };
499
500        Tool {
501            tool_type: "function".to_string(),
502            function: FunctionTool {
503                name: self.name,
504                description: self.description,
505                parameters: parameters_value,
506            },
507        }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514    use crate::error::LLMError;
515    use serde_json::json;
516    use std::str::FromStr;
517
518    #[test]
519    fn test_llm_backend_from_str() {
520        assert!(matches!(
521            LLMBackend::from_str("openai").unwrap(),
522            LLMBackend::OpenAI
523        ));
524        assert!(matches!(
525            LLMBackend::from_str("OpenAI").unwrap(),
526            LLMBackend::OpenAI
527        ));
528        assert!(matches!(
529            LLMBackend::from_str("OPENAI").unwrap(),
530            LLMBackend::OpenAI
531        ));
532        assert!(matches!(
533            LLMBackend::from_str("anthropic").unwrap(),
534            LLMBackend::Anthropic
535        ));
536        assert!(matches!(
537            LLMBackend::from_str("ollama").unwrap(),
538            LLMBackend::Ollama
539        ));
540        assert!(matches!(
541            LLMBackend::from_str("deepseek").unwrap(),
542            LLMBackend::DeepSeek
543        ));
544        assert!(matches!(
545            LLMBackend::from_str("xai").unwrap(),
546            LLMBackend::XAI
547        ));
548        assert!(matches!(
549            LLMBackend::from_str("phind").unwrap(),
550            LLMBackend::Phind
551        ));
552        assert!(matches!(
553            LLMBackend::from_str("google").unwrap(),
554            LLMBackend::Google
555        ));
556        assert!(matches!(
557            LLMBackend::from_str("groq").unwrap(),
558            LLMBackend::Groq
559        ));
560        assert!(matches!(
561            LLMBackend::from_str("azure-openai").unwrap(),
562            LLMBackend::AzureOpenAI
563        ));
564
565        let result = LLMBackend::from_str("invalid");
566        assert!(result.is_err());
567        assert!(result
568            .unwrap_err()
569            .to_string()
570            .contains("Unknown LLM backend"));
571    }
572
573    #[test]
574    fn test_param_builder_new() {
575        let builder = ParamBuilder::new("test_param");
576        assert_eq!(builder.name, "test_param");
577        assert_eq!(builder.property_type, "string");
578        assert_eq!(builder.description, "");
579        assert!(builder.items.is_none());
580        assert!(builder.enum_list.is_none());
581    }
582
583    #[test]
584    fn test_param_builder_fluent_interface() {
585        let builder = ParamBuilder::new("test_param")
586            .type_of("integer")
587            .description("A test parameter")
588            .enum_values(vec!["option1".to_string(), "option2".to_string()]);
589
590        assert_eq!(builder.name, "test_param");
591        assert_eq!(builder.property_type, "integer");
592        assert_eq!(builder.description, "A test parameter");
593        assert_eq!(
594            builder.enum_list,
595            Some(vec!["option1".to_string(), "option2".to_string()])
596        );
597    }
598
599    #[test]
600    fn test_param_builder_with_items() {
601        let item_property = ParameterProperty {
602            property_type: "string".to_string(),
603            description: "Array item".to_string(),
604            items: None,
605            enum_list: None,
606        };
607
608        let builder = ParamBuilder::new("array_param")
609            .type_of("array")
610            .description("An array parameter")
611            .items(item_property);
612
613        assert_eq!(builder.name, "array_param");
614        assert_eq!(builder.property_type, "array");
615        assert_eq!(builder.description, "An array parameter");
616        assert!(builder.items.is_some());
617    }
618
619    #[test]
620    fn test_param_builder_build() {
621        let builder = ParamBuilder::new("test_param")
622            .type_of("string")
623            .description("A test parameter");
624
625        let (name, property) = builder.build();
626        assert_eq!(name, "test_param");
627        assert_eq!(property.property_type, "string");
628        assert_eq!(property.description, "A test parameter");
629    }
630
631    #[test]
632    fn test_function_builder_new() {
633        let builder = FunctionBuilder::new("test_function");
634        assert_eq!(builder.name, "test_function");
635        assert_eq!(builder.description, "");
636        assert!(builder.parameters.is_empty());
637        assert!(builder.required.is_empty());
638        assert!(builder.raw_schema.is_none());
639    }
640
641    #[test]
642    fn test_function_builder_fluent_interface() {
643        let param = ParamBuilder::new("name")
644            .type_of("string")
645            .description("Name");
646        let builder = FunctionBuilder::new("test_function")
647            .description("A test function")
648            .param(param)
649            .required(vec!["name".to_string()]);
650
651        assert_eq!(builder.name, "test_function");
652        assert_eq!(builder.description, "A test function");
653        assert_eq!(builder.parameters.len(), 1);
654        assert_eq!(builder.required, vec!["name".to_string()]);
655    }
656
657    #[test]
658    fn test_function_builder_with_json_schema() {
659        let schema = json!({
660            "type": "object",
661            "properties": {
662                "name": {"type": "string"},
663                "age": {"type": "integer"}
664            },
665            "required": ["name", "age"]
666        });
667
668        let builder = FunctionBuilder::new("test_function").json_schema(schema.clone());
669        assert_eq!(builder.raw_schema, Some(schema));
670    }
671
672    #[test]
673    fn test_function_builder_build_with_parameters() {
674        let param = ParamBuilder::new("name").type_of("string");
675        let tool = FunctionBuilder::new("test_function")
676            .description("A test function")
677            .param(param)
678            .required(vec!["name".to_string()])
679            .build();
680
681        assert_eq!(tool.tool_type, "function");
682        assert_eq!(tool.function.name, "test_function");
683        assert_eq!(tool.function.description, "A test function");
684        assert!(tool.function.parameters.is_object());
685    }
686
687    #[test]
688    fn test_function_builder_build_with_raw_schema() {
689        let schema = json!({
690            "type": "object",
691            "properties": {
692                "name": {"type": "string"}
693            }
694        });
695
696        let tool = FunctionBuilder::new("test_function")
697            .json_schema(schema.clone())
698            .build();
699
700        assert_eq!(tool.function.parameters, schema);
701    }
702
703    // Mock LLM provider for testing
704    struct MockLLMProvider;
705
706    #[async_trait::async_trait]
707    impl crate::chat::ChatProvider for MockLLMProvider {
708        async fn chat_with_tools(
709            &self,
710            _messages: &[crate::chat::ChatMessage],
711            _tools: Option<&[crate::chat::Tool]>,
712            _json_schema: Option<crate::chat::StructuredOutputFormat>,
713        ) -> Result<Box<dyn crate::chat::ChatResponse>, LLMError> {
714            unimplemented!()
715        }
716    }
717
718    #[async_trait::async_trait]
719    impl crate::completion::CompletionProvider for MockLLMProvider {
720        async fn complete(
721            &self,
722            _req: &crate::completion::CompletionRequest,
723            _json_schema: Option<crate::chat::StructuredOutputFormat>,
724        ) -> Result<crate::completion::CompletionResponse, LLMError> {
725            unimplemented!()
726        }
727    }
728
729    #[async_trait::async_trait]
730    impl crate::embedding::EmbeddingProvider for MockLLMProvider {
731        async fn embed(&self, _text: Vec<String>) -> Result<Vec<Vec<f32>>, LLMError> {
732            unimplemented!()
733        }
734    }
735
736    #[async_trait::async_trait]
737    impl crate::models::ModelsProvider for MockLLMProvider {}
738
739    impl crate::LLMProvider for MockLLMProvider {}
740
741    #[test]
742    fn test_llm_builder_new() {
743        let builder = LLMBuilder::<MockLLMProvider>::new();
744        assert!(builder.api_key.is_none());
745        assert!(builder.base_url.is_none());
746        assert!(builder.model.is_none());
747        assert!(builder.max_tokens.is_none());
748        assert!(builder.temperature.is_none());
749        assert!(builder.system.is_none());
750        assert!(builder.timeout_seconds.is_none());
751        assert!(builder.stream.is_none());
752        assert!(builder.tools.is_none());
753        assert!(builder.tool_choice.is_none());
754    }
755
756    #[test]
757    fn test_llm_builder_default() {
758        let builder = LLMBuilder::<MockLLMProvider>::default();
759        assert!(builder.api_key.is_none());
760        assert!(builder.base_url.is_none());
761        assert!(builder.model.is_none());
762        assert_eq!(builder.validator_attempts, 0);
763    }
764
765    #[test]
766    fn test_llm_builder_api_key() {
767        let builder = LLMBuilder::<MockLLMProvider>::new().api_key("test_key");
768        assert_eq!(builder.api_key, Some("test_key".to_string()));
769    }
770
771    #[test]
772    fn test_llm_builder_base_url() {
773        let builder = LLMBuilder::<MockLLMProvider>::new().base_url("https://api.example.com");
774        assert_eq!(
775            builder.base_url,
776            Some("https://api.example.com".to_string())
777        );
778    }
779
780    #[test]
781    fn test_llm_builder_model() {
782        let builder = LLMBuilder::<MockLLMProvider>::new().model("gpt-4");
783        assert_eq!(builder.model, Some("gpt-4".to_string()));
784    }
785
786    #[test]
787    fn test_llm_builder_max_tokens() {
788        let builder = LLMBuilder::<MockLLMProvider>::new().max_tokens(1000);
789        assert_eq!(builder.max_tokens, Some(1000));
790    }
791
792    #[test]
793    fn test_llm_builder_temperature() {
794        let builder = LLMBuilder::<MockLLMProvider>::new().temperature(0.7);
795        assert_eq!(builder.temperature, Some(0.7));
796    }
797
798    #[test]
799    fn test_llm_builder_system() {
800        let builder = LLMBuilder::<MockLLMProvider>::new().system("You are a helpful assistant");
801        assert_eq!(
802            builder.system,
803            Some("You are a helpful assistant".to_string())
804        );
805    }
806
807    #[test]
808    fn test_llm_builder_reasoning_effort() {
809        let builder = LLMBuilder::<MockLLMProvider>::new()
810            .reasoning_effort(crate::chat::ReasoningEffort::High);
811        assert_eq!(builder.reasoning_effort, Some("high".to_string()));
812    }
813
814    #[test]
815    fn test_llm_builder_reasoning() {
816        let builder = LLMBuilder::<MockLLMProvider>::new().reasoning(true);
817        assert_eq!(builder.reasoning, Some(true));
818    }
819
820    #[test]
821    fn test_llm_builder_reasoning_budget_tokens() {
822        let builder = LLMBuilder::<MockLLMProvider>::new().reasoning_budget_tokens(5000);
823        assert_eq!(builder.reasoning_budget_tokens, Some(5000));
824    }
825
826    #[test]
827    fn test_llm_builder_timeout_seconds() {
828        let builder = LLMBuilder::<MockLLMProvider>::new().timeout_seconds(30);
829        assert_eq!(builder.timeout_seconds, Some(30));
830    }
831
832    #[test]
833    fn test_llm_builder_stream() {
834        let builder = LLMBuilder::<MockLLMProvider>::new().stream(true);
835        assert_eq!(builder.stream, Some(true));
836    }
837
838    #[test]
839    fn test_llm_builder_top_p() {
840        let builder = LLMBuilder::<MockLLMProvider>::new().top_p(0.9);
841        assert_eq!(builder.top_p, Some(0.9));
842    }
843
844    #[test]
845    fn test_llm_builder_top_k() {
846        let builder = LLMBuilder::<MockLLMProvider>::new().top_k(50);
847        assert_eq!(builder.top_k, Some(50));
848    }
849
850    #[test]
851    fn test_llm_builder_embedding_encoding_format() {
852        let builder = LLMBuilder::<MockLLMProvider>::new().embedding_encoding_format("float");
853        assert_eq!(builder.embedding_encoding_format, Some("float".to_string()));
854    }
855
856    #[test]
857    fn test_llm_builder_embedding_dimensions() {
858        let builder = LLMBuilder::<MockLLMProvider>::new().embedding_dimensions(1536);
859        assert_eq!(builder.embedding_dimensions, Some(1536));
860    }
861
862    #[test]
863    fn test_llm_builder_schema() {
864        let schema = crate::chat::StructuredOutputFormat {
865            name: "Test".to_string(),
866            description: None,
867            schema: None,
868            strict: None,
869        };
870        let builder = LLMBuilder::<MockLLMProvider>::new().schema(schema.clone());
871        assert_eq!(builder.json_schema, Some(schema));
872    }
873
874    #[test]
875    fn test_llm_builder_validator() {
876        let builder = LLMBuilder::<MockLLMProvider>::new().validator(|response| {
877            if response.contains("error") {
878                Err("Response contains error".to_string())
879            } else {
880                Ok(())
881            }
882        });
883        assert!(builder.validator.is_some());
884    }
885
886    #[test]
887    fn test_llm_builder_validator_attempts() {
888        let builder = LLMBuilder::<MockLLMProvider>::new().validator_attempts(3);
889        assert_eq!(builder.validator_attempts, 3);
890    }
891
892    #[test]
893    fn test_llm_builder_function() {
894        let function = FunctionBuilder::new("test_function")
895            .description("A test function")
896            .param(ParamBuilder::new("name").type_of("string"));
897
898        let builder = LLMBuilder::<MockLLMProvider>::new().function(function);
899        assert!(builder.tools.is_some());
900        assert_eq!(builder.tools.as_ref().unwrap().len(), 1);
901    }
902
903    #[test]
904    fn test_llm_builder_multiple_functions() {
905        let function1 = FunctionBuilder::new("function1");
906        let function2 = FunctionBuilder::new("function2");
907
908        let builder = LLMBuilder::<MockLLMProvider>::new()
909            .function(function1)
910            .function(function2);
911
912        assert!(builder.tools.is_some());
913        assert_eq!(builder.tools.as_ref().unwrap().len(), 2);
914    }
915
916    #[test]
917    fn test_llm_builder_enable_parallel_tool_use() {
918        let builder = LLMBuilder::<MockLLMProvider>::new().enable_parallel_tool_use(true);
919        assert_eq!(builder.enable_parallel_tool_use, Some(true));
920    }
921
922    #[test]
923    fn test_llm_builder_tool_choice() {
924        let builder = LLMBuilder::<MockLLMProvider>::new().tool_choice(ToolChoice::Auto);
925        assert!(matches!(builder.tool_choice, Some(ToolChoice::Auto)));
926    }
927
928    #[test]
929    fn test_llm_builder_disable_tools() {
930        let builder = LLMBuilder::<MockLLMProvider>::new().disable_tools();
931        assert!(matches!(builder.tool_choice, Some(ToolChoice::None)));
932    }
933
934    #[test]
935    fn test_llm_builder_api_version() {
936        let builder = LLMBuilder::<MockLLMProvider>::new().api_version("2023-05-15");
937        assert_eq!(builder.api_version, Some("2023-05-15".to_string()));
938    }
939
940    #[test]
941    fn test_llm_builder_deployment_id() {
942        let builder = LLMBuilder::<MockLLMProvider>::new().deployment_id("my-deployment");
943        assert_eq!(builder.deployment_id, Some("my-deployment".to_string()));
944    }
945
946    #[test]
947    fn test_llm_builder_validate_tool_config_valid() {
948        let function = FunctionBuilder::new("test_function");
949        let builder = LLMBuilder::<MockLLMProvider>::new()
950            .function(function)
951            .tool_choice(ToolChoice::Tool("test_function".to_string()));
952
953        let result = builder.validate_tool_config();
954        assert!(result.is_ok());
955    }
956
957    #[test]
958    fn test_llm_builder_validate_tool_config_invalid_tool_name() {
959        let function = FunctionBuilder::new("test_function");
960        let builder = LLMBuilder::<MockLLMProvider>::new()
961            .function(function)
962            .tool_choice(ToolChoice::Tool("nonexistent_function".to_string()));
963
964        let result = builder.validate_tool_config();
965        assert!(result.is_err());
966        assert!(result
967            .unwrap_err()
968            .to_string()
969            .contains("no tool with name nonexistent_function found"));
970    }
971
972    #[test]
973    fn test_llm_builder_validate_tool_config_tool_choice_without_tools() {
974        let builder = LLMBuilder::<MockLLMProvider>::new().tool_choice(ToolChoice::Auto);
975
976        let result = builder.validate_tool_config();
977        assert!(result.is_err());
978        assert!(result
979            .unwrap_err()
980            .to_string()
981            .contains("Tool choice cannot be set without tools configured"));
982    }
983
984    #[test]
985    fn test_llm_builder_validate_tool_config_auto_choice() {
986        let function = FunctionBuilder::new("test_function");
987        let builder = LLMBuilder::<MockLLMProvider>::new()
988            .function(function)
989            .tool_choice(ToolChoice::Auto);
990
991        let result = builder.validate_tool_config();
992        assert!(result.is_ok());
993    }
994
995    #[test]
996    fn test_llm_builder_validate_tool_config_no_tool_choice() {
997        let function = FunctionBuilder::new("test_function");
998        let builder = LLMBuilder::<MockLLMProvider>::new().function(function);
999
1000        let result = builder.validate_tool_config();
1001        assert!(result.is_ok());
1002    }
1003
1004    #[test]
1005    fn test_llm_builder_chaining() {
1006        let builder = LLMBuilder::<MockLLMProvider>::new()
1007            .api_key("test_key")
1008            .model("gpt-4")
1009            .max_tokens(2000)
1010            .temperature(0.8)
1011            .system("You are helpful")
1012            .timeout_seconds(60)
1013            .stream(true)
1014            .top_p(0.95)
1015            .top_k(40)
1016            .embedding_encoding_format("float")
1017            .embedding_dimensions(1536)
1018            .validator_attempts(5)
1019            .reasoning(true)
1020            .reasoning_budget_tokens(10000)
1021            .api_version("2023-05-15")
1022            .deployment_id("test-deployment");
1023
1024        assert_eq!(builder.api_key, Some("test_key".to_string()));
1025        assert_eq!(builder.model, Some("gpt-4".to_string()));
1026        assert_eq!(builder.max_tokens, Some(2000));
1027        assert_eq!(builder.temperature, Some(0.8));
1028        assert_eq!(builder.system, Some("You are helpful".to_string()));
1029        assert_eq!(builder.timeout_seconds, Some(60));
1030        assert_eq!(builder.stream, Some(true));
1031        assert_eq!(builder.top_p, Some(0.95));
1032        assert_eq!(builder.top_k, Some(40));
1033        assert_eq!(builder.embedding_encoding_format, Some("float".to_string()));
1034        assert_eq!(builder.embedding_dimensions, Some(1536));
1035        assert_eq!(builder.validator_attempts, 5);
1036        assert_eq!(builder.reasoning, Some(true));
1037        assert_eq!(builder.reasoning_budget_tokens, Some(10000));
1038        assert_eq!(builder.api_version, Some("2023-05-15".to_string()));
1039        assert_eq!(builder.deployment_id, Some("test-deployment".to_string()));
1040    }
1041}