Skip to main content

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