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