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