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