1use alloc::{string::String, vec::Vec};
43use schemars::Schema;
44use serde_json::Value;
45
46#[derive(Debug, Default, Clone, PartialEq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[allow(clippy::struct_excessive_bools)]
66pub struct Parameters {
67 pub temperature: Option<f32>,
72 pub top_p: Option<f32>,
77 pub top_k: Option<u32>,
81 pub frequency_penalty: Option<f32>,
85 pub presence_penalty: Option<f32>,
89 pub repetition_penalty: Option<f32>,
96 pub min_p: Option<f32>,
102 pub seed: Option<u32>,
106 pub max_tokens: Option<u32>,
110 pub logit_bias: Option<Vec<(String, f32)>>,
114 pub logprobs: Option<bool>,
118 pub top_logprobs: Option<u8>,
122 pub stop: Option<Vec<String>>,
126 pub tool_choice: ToolChoice,
130
131 pub parallel_tool_calls: Option<bool>,
137
138 pub reasoning_effort: Option<ReasoningEffort>,
140
141 pub include_reasoning: bool,
143
144 pub structured_outputs: bool,
148
149 pub response_format: Option<Schema>,
153 pub websearch: bool,
155 pub code_execution: bool,
157 #[cfg_attr(
159 feature = "serde",
160 serde(default, skip_serializing_if = "NativeTools::is_empty")
161 )]
162 pub native_tools: NativeTools,
163 #[cfg_attr(
165 feature = "serde",
166 serde(default, skip_serializing_if = "CacheOptions::is_empty")
167 )]
168 pub cache: CacheOptions,
169}
170
171macro_rules! impl_with_methods {
172 (
173 impl $ty:ty {
174 $($field:ident : $field_ty:ty),* $(,)?
175 }
176 ) => {
177 impl $ty {
178 $(
179 #[allow(clippy::missing_const_for_fn)]
185 #[must_use] pub fn $field(mut self, value: $field_ty) -> Self {
186 self.$field = Some(value);
187 self
188 }
189 )*
190 }
191 };
192}
193
194impl_with_methods! {
195 impl Parameters {
196 temperature: f32,
197 top_p: f32,
198 top_k: u32,
199 frequency_penalty: f32,
200 presence_penalty: f32,
201 repetition_penalty: f32,
202 min_p: f32,
203 seed: u32,
204 max_tokens: u32,
205 logit_bias: Vec<(String, f32)>,
206 logprobs: bool,
207 top_logprobs: u8,
208 stop: Vec<String>,
209 parallel_tool_calls: bool,
210 }
211}
212
213impl Parameters {
214 #[must_use]
216 pub const fn include_reasoning(mut self, include: bool) -> Self {
217 self.include_reasoning = include;
218 self
219 }
220
221 #[must_use]
223 pub const fn reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
224 self.reasoning_effort = Some(effort);
225 self
226 }
227
228 #[must_use]
230 pub const fn websearch(mut self, enabled: bool) -> Self {
231 self.websearch = enabled;
232 self
233 }
234
235 #[must_use]
237 pub const fn code_execution(mut self, enabled: bool) -> Self {
238 self.code_execution = enabled;
239 self
240 }
241
242 #[must_use]
244 pub fn native_tools(mut self, tools: NativeTools) -> Self {
245 self.native_tools = tools;
246 self
247 }
248
249 #[must_use]
256 pub const fn hosted_web_search(&self) -> bool {
257 self.websearch
258 || self.native_tools.openai.web_search.is_some()
259 || self.native_tools.gemini.google_search
260 || self.native_tools.claude.web_search
261 }
262
263 #[must_use]
268 pub const fn hosted_web_fetch(&self) -> bool {
269 self.native_tools.claude.web_fetch || self.native_tools.gemini.url_context
270 }
271
272 #[must_use]
274 pub fn openai_tools(mut self, tools: OpenAINativeTools) -> Self {
275 self.native_tools.openai = tools;
276 self
277 }
278
279 #[must_use]
281 pub const fn gemini_tools(mut self, tools: GeminiNativeTools) -> Self {
282 self.native_tools.gemini = tools;
283 self
284 }
285
286 #[must_use]
288 pub const fn claude_tools(mut self, tools: ClaudeNativeTools) -> Self {
289 self.native_tools.claude = tools;
290 self
291 }
292
293 #[must_use]
295 pub fn prompt_cache_key(mut self, key: impl Into<String>) -> Self {
296 let cache = self
297 .cache
298 .openai
299 .get_or_insert_with(OpenAIPromptCache::default);
300 cache.key = Some(key.into());
301 self
302 }
303
304 #[must_use]
306 pub fn prompt_cache_retention(mut self, retention: OpenAIPromptCacheRetention) -> Self {
307 let cache = self
308 .cache
309 .openai
310 .get_or_insert_with(OpenAIPromptCache::default);
311 cache.retention = Some(retention);
312 self
313 }
314
315 #[must_use]
317 pub const fn claude_prompt_cache(mut self, cache: ClaudePromptCache) -> Self {
318 self.cache.claude = Some(cache);
319 self
320 }
321
322 #[must_use]
324 pub const fn claude_prompt_cache_automatic(mut self, ttl: ClaudePromptCacheTtl) -> Self {
325 self.cache.claude = Some(ClaudePromptCache::automatic(ttl));
326 self
327 }
328
329 #[must_use]
331 pub const fn claude_prompt_cache_explicit(
332 mut self,
333 ttl: ClaudePromptCacheTtl,
334 breakpoints: ClaudeExplicitCacheBreakpoints,
335 ) -> Self {
336 self.cache.claude = Some(ClaudePromptCache::explicit(ttl, breakpoints));
337 self
338 }
339
340 #[must_use]
342 pub const fn claude_prompt_cache_automatic_with_explicit(
343 mut self,
344 ttl: ClaudePromptCacheTtl,
345 breakpoints: ClaudeExplicitCacheBreakpoints,
346 ) -> Self {
347 self.cache.claude = Some(ClaudePromptCache::automatic_with_explicit(ttl, breakpoints));
348 self
349 }
350
351 #[must_use]
353 pub fn gemini_cached_content(mut self, cached_content: impl Into<String>) -> Self {
354 self.cache.gemini = Some(GeminiPromptCache::new(cached_content));
355 self
356 }
357
358 #[must_use]
360 pub fn without_cache(mut self) -> Self {
361 self.cache = CacheOptions {
362 openai: None,
363 claude: None,
364 gemini: None,
365 };
366 self
367 }
368
369 #[must_use]
371 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
372 self.tool_choice = choice;
373 self
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Default)]
379#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
380pub struct NativeTools {
381 #[cfg_attr(
383 feature = "serde",
384 serde(default, skip_serializing_if = "OpenAINativeTools::is_empty")
385 )]
386 pub openai: OpenAINativeTools,
387 #[cfg_attr(
389 feature = "serde",
390 serde(default, skip_serializing_if = "GeminiNativeTools::is_empty")
391 )]
392 pub gemini: GeminiNativeTools,
393 #[cfg_attr(
395 feature = "serde",
396 serde(default, skip_serializing_if = "ClaudeNativeTools::is_empty")
397 )]
398 pub claude: ClaudeNativeTools,
399}
400
401impl NativeTools {
402 #[must_use]
404 pub const fn is_empty(&self) -> bool {
405 self.openai.is_empty() && self.gemini.is_empty() && self.claude.is_empty()
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Default)]
411#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
412pub struct OpenAINativeTools {
413 #[cfg_attr(
415 feature = "serde",
416 serde(default, skip_serializing_if = "Option::is_none")
417 )]
418 pub web_search: Option<OpenAIWebSearchTool>,
419 #[cfg_attr(
421 feature = "serde",
422 serde(default, skip_serializing_if = "Vec::is_empty")
423 )]
424 pub file_search: Vec<OpenAIFileSearchTool>,
425 #[cfg_attr(
427 feature = "serde",
428 serde(default, skip_serializing_if = "Option::is_none")
429 )]
430 pub code_interpreter: Option<OpenAICodeInterpreterTool>,
431 #[cfg_attr(
433 feature = "serde",
434 serde(default, skip_serializing_if = "Option::is_none")
435 )]
436 pub image_generation: Option<OpenAIImageGenerationTool>,
437 #[cfg_attr(
439 feature = "serde",
440 serde(default, skip_serializing_if = "Vec::is_empty")
441 )]
442 pub mcp: Vec<OpenAIMcpTool>,
443 #[cfg_attr(
445 feature = "serde",
446 serde(default, skip_serializing_if = "Option::is_none")
447 )]
448 pub computer_use: Option<OpenAIComputerUseTool>,
449}
450
451impl OpenAINativeTools {
452 #[must_use]
454 pub const fn is_empty(&self) -> bool {
455 self.web_search.is_none()
456 && self.file_search.is_empty()
457 && self.code_interpreter.is_none()
458 && self.image_generation.is_none()
459 && self.mcp.is_empty()
460 && self.computer_use.is_none()
461 }
462
463 #[must_use]
465 pub fn with_web_search(mut self, tool: OpenAIWebSearchTool) -> Self {
466 self.web_search = Some(tool);
467 self
468 }
469
470 #[must_use]
472 pub fn enable_web_search(mut self) -> Self {
473 self.web_search = Some(OpenAIWebSearchTool::default());
474 self
475 }
476
477 #[must_use]
479 pub fn with_file_search(mut self, tool: OpenAIFileSearchTool) -> Self {
480 self.file_search.push(tool);
481 self
482 }
483
484 #[must_use]
486 pub fn with_code_interpreter(mut self, tool: OpenAICodeInterpreterTool) -> Self {
487 self.code_interpreter = Some(tool);
488 self
489 }
490
491 #[must_use]
493 pub const fn with_image_generation(mut self, tool: OpenAIImageGenerationTool) -> Self {
494 self.image_generation = Some(tool);
495 self
496 }
497
498 #[must_use]
500 pub fn with_mcp(mut self, tool: OpenAIMcpTool) -> Self {
501 self.mcp.push(tool);
502 self
503 }
504
505 #[must_use]
507 pub fn with_computer_use(mut self, tool: OpenAIComputerUseTool) -> Self {
508 self.computer_use = Some(tool);
509 self
510 }
511}
512
513#[derive(Debug, Clone, PartialEq, Eq, Default)]
515#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
516pub struct OpenAIWebSearchTool {
517 #[cfg_attr(
519 feature = "serde",
520 serde(default, skip_serializing_if = "Option::is_none")
521 )]
522 pub external_web_access: Option<bool>,
523 #[cfg_attr(
525 feature = "serde",
526 serde(default, skip_serializing_if = "Option::is_none")
527 )]
528 pub filters: Option<Value>,
529 #[cfg_attr(
531 feature = "serde",
532 serde(default, skip_serializing_if = "Option::is_none")
533 )]
534 pub user_location: Option<Value>,
535}
536
537impl OpenAIWebSearchTool {
538 #[must_use]
540 pub const fn external_web_access(mut self, allowed: bool) -> Self {
541 self.external_web_access = Some(allowed);
542 self
543 }
544
545 #[must_use]
547 pub fn filters(mut self, filters: Value) -> Self {
548 self.filters = Some(filters);
549 self
550 }
551
552 #[must_use]
554 pub fn user_location(mut self, location: Value) -> Self {
555 self.user_location = Some(location);
556 self
557 }
558}
559
560#[derive(Debug, Clone, PartialEq, Eq)]
562#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
563pub struct OpenAIFileSearchTool {
564 pub vector_store_ids: Vec<String>,
566 #[cfg_attr(
568 feature = "serde",
569 serde(default, skip_serializing_if = "Option::is_none")
570 )]
571 pub max_num_results: Option<u32>,
572 pub include_results: bool,
574 #[cfg_attr(
576 feature = "serde",
577 serde(default, skip_serializing_if = "Option::is_none")
578 )]
579 pub filters: Option<Value>,
580}
581
582impl OpenAIFileSearchTool {
583 #[must_use]
585 pub fn new(vector_store_ids: impl Into<Vec<String>>) -> Self {
586 Self {
587 vector_store_ids: vector_store_ids.into(),
588 max_num_results: None,
589 include_results: false,
590 filters: None,
591 }
592 }
593
594 #[must_use]
596 pub const fn max_num_results(mut self, value: u32) -> Self {
597 self.max_num_results = Some(value);
598 self
599 }
600
601 #[must_use]
603 pub const fn include_results(mut self, include: bool) -> Self {
604 self.include_results = include;
605 self
606 }
607
608 #[must_use]
610 pub fn filters(mut self, filters: Value) -> Self {
611 self.filters = Some(filters);
612 self
613 }
614}
615
616#[derive(Debug, Clone, PartialEq, Eq)]
618#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
619pub struct OpenAICodeInterpreterTool {
620 pub container: OpenAICodeInterpreterContainer,
622}
623
624impl Default for OpenAICodeInterpreterTool {
625 fn default() -> Self {
626 Self {
627 container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::default()),
628 }
629 }
630}
631
632impl OpenAICodeInterpreterTool {
633 #[must_use]
635 pub const fn auto() -> Self {
636 Self {
637 container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::new()),
638 }
639 }
640
641 #[must_use]
643 pub fn existing(container_id: impl Into<String>) -> Self {
644 Self {
645 container: OpenAICodeInterpreterContainer::Existing(container_id.into()),
646 }
647 }
648}
649
650#[derive(Debug, Clone, PartialEq, Eq)]
652#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
653pub enum OpenAICodeInterpreterContainer {
654 Auto(OpenAIAutoContainer),
656 Existing(String),
658}
659
660#[allow(clippy::struct_excessive_bools)]
662#[derive(Debug, Clone, PartialEq, Eq, Default)]
663#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
664pub struct OpenAIAutoContainer {
665 #[cfg_attr(
667 feature = "serde",
668 serde(default, skip_serializing_if = "Option::is_none")
669 )]
670 pub memory_limit: Option<String>,
671 #[cfg_attr(
673 feature = "serde",
674 serde(default, skip_serializing_if = "Vec::is_empty")
675 )]
676 pub file_ids: Vec<String>,
677}
678
679impl OpenAIAutoContainer {
680 #[must_use]
682 pub const fn new() -> Self {
683 Self {
684 memory_limit: None,
685 file_ids: Vec::new(),
686 }
687 }
688
689 #[must_use]
691 pub fn memory_limit(mut self, memory_limit: impl Into<String>) -> Self {
692 self.memory_limit = Some(memory_limit.into());
693 self
694 }
695
696 #[must_use]
698 pub fn file_ids(mut self, file_ids: impl Into<Vec<String>>) -> Self {
699 self.file_ids = file_ids.into();
700 self
701 }
702}
703
704#[derive(Debug, Clone, PartialEq, Eq, Default)]
706#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
707pub struct OpenAIImageGenerationTool {
708 #[cfg_attr(
710 feature = "serde",
711 serde(default, skip_serializing_if = "Option::is_none")
712 )]
713 pub partial_images: Option<u8>,
714}
715
716impl OpenAIImageGenerationTool {
717 #[must_use]
719 pub const fn partial_images(mut self, count: u8) -> Self {
720 self.partial_images = Some(count);
721 self
722 }
723}
724
725#[derive(Debug, Clone, PartialEq, Eq)]
727#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
728pub struct OpenAIMcpTool {
729 pub server_label: String,
731 pub server_url: String,
733 #[cfg_attr(
735 feature = "serde",
736 serde(default, skip_serializing_if = "Option::is_none")
737 )]
738 pub require_approval: Option<String>,
739 #[cfg_attr(
741 feature = "serde",
742 serde(default, skip_serializing_if = "Vec::is_empty")
743 )]
744 pub allowed_tools: Vec<String>,
745}
746
747impl OpenAIMcpTool {
748 #[must_use]
750 pub fn new(server_label: impl Into<String>, server_url: impl Into<String>) -> Self {
751 Self {
752 server_label: server_label.into(),
753 server_url: server_url.into(),
754 require_approval: None,
755 allowed_tools: Vec::new(),
756 }
757 }
758
759 #[must_use]
761 pub fn require_approval(mut self, policy: impl Into<String>) -> Self {
762 self.require_approval = Some(policy.into());
763 self
764 }
765
766 #[must_use]
768 pub fn allowed_tools(mut self, tools: impl Into<Vec<String>>) -> Self {
769 self.allowed_tools = tools.into();
770 self
771 }
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
776#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
777pub struct OpenAIComputerUseTool {
778 pub display_width: u32,
780 pub display_height: u32,
782 pub environment: String,
784}
785
786impl OpenAIComputerUseTool {
787 #[must_use]
789 pub fn new(display_width: u32, display_height: u32, environment: impl Into<String>) -> Self {
790 Self {
791 display_width,
792 display_height,
793 environment: environment.into(),
794 }
795 }
796}
797
798#[derive(Debug, Clone, PartialEq, Eq, Default)]
800#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
801pub struct GeminiNativeTools {
802 pub google_search: bool,
804 pub code_execution: bool,
806 pub url_context: bool,
808}
809
810impl GeminiNativeTools {
811 #[must_use]
813 pub const fn is_empty(&self) -> bool {
814 !self.google_search && !self.code_execution && !self.url_context
815 }
816
817 #[must_use]
819 pub const fn google_search(mut self, enabled: bool) -> Self {
820 self.google_search = enabled;
821 self
822 }
823
824 #[must_use]
826 pub const fn code_execution(mut self, enabled: bool) -> Self {
827 self.code_execution = enabled;
828 self
829 }
830
831 #[must_use]
833 pub const fn url_context(mut self, enabled: bool) -> Self {
834 self.url_context = enabled;
835 self
836 }
837}
838
839#[allow(clippy::struct_excessive_bools)]
841#[derive(Debug, Clone, PartialEq, Eq, Default)]
842#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
843pub struct ClaudeNativeTools {
844 pub web_search: bool,
846 pub web_fetch: bool,
848 pub code_execution: bool,
850 pub bash: bool,
852 #[cfg_attr(
854 feature = "serde",
855 serde(default, skip_serializing_if = "Option::is_none")
856 )]
857 pub text_editor: Option<ClaudeTextEditorTool>,
858}
859
860impl ClaudeNativeTools {
861 #[must_use]
863 pub const fn is_empty(&self) -> bool {
864 !self.web_search
865 && !self.web_fetch
866 && !self.code_execution
867 && !self.bash
868 && self.text_editor.is_none()
869 }
870
871 #[must_use]
873 pub const fn web_search(mut self, enabled: bool) -> Self {
874 self.web_search = enabled;
875 self
876 }
877
878 #[must_use]
880 pub const fn web_fetch(mut self, enabled: bool) -> Self {
881 self.web_fetch = enabled;
882 self
883 }
884
885 #[must_use]
887 pub const fn code_execution(mut self, enabled: bool) -> Self {
888 self.code_execution = enabled;
889 self
890 }
891
892 #[must_use]
894 pub const fn bash(mut self, enabled: bool) -> Self {
895 self.bash = enabled;
896 self
897 }
898
899 #[must_use]
901 pub const fn text_editor(mut self, tool: ClaudeTextEditorTool) -> Self {
902 self.text_editor = Some(tool);
903 self
904 }
905}
906
907#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
909#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
910pub struct ClaudeTextEditorTool {
911 pub max_characters: Option<u32>,
913}
914
915impl ClaudeTextEditorTool {
916 #[must_use]
918 pub const fn max_characters(mut self, value: u32) -> Self {
919 self.max_characters = Some(value);
920 self
921 }
922}
923
924#[derive(Debug, Clone, PartialEq, Eq)]
926#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
927#[serde(rename_all = "lowercase")]
928#[derive(Default)]
929pub enum ToolChoice {
930 #[default]
932 Auto,
933 None,
935 Required,
937 Exact(String),
939}
940
941#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
954#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
955#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
956pub enum ReasoningEffort {
957 None,
962 Minimal,
964 Low,
966 Medium,
968 High,
970 XHigh,
972 Max,
974}
975
976#[derive(Debug, Clone, PartialEq, Eq, Default)]
978#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
979pub struct CacheOptions {
980 #[cfg_attr(
982 feature = "serde",
983 serde(default, skip_serializing_if = "Option::is_none")
984 )]
985 pub openai: Option<OpenAIPromptCache>,
986 #[cfg_attr(
988 feature = "serde",
989 serde(default, skip_serializing_if = "Option::is_none")
990 )]
991 pub claude: Option<ClaudePromptCache>,
992 #[cfg_attr(
994 feature = "serde",
995 serde(default, skip_serializing_if = "Option::is_none")
996 )]
997 pub gemini: Option<GeminiPromptCache>,
998}
999
1000impl CacheOptions {
1001 #[must_use]
1003 pub const fn is_empty(&self) -> bool {
1004 self.openai.is_none() && self.claude.is_none() && self.gemini.is_none()
1005 }
1006}
1007
1008#[derive(Debug, Clone, PartialEq, Eq, Default)]
1010#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1011pub struct OpenAIPromptCache {
1012 #[cfg_attr(
1014 feature = "serde",
1015 serde(default, skip_serializing_if = "Option::is_none")
1016 )]
1017 pub key: Option<String>,
1018 #[cfg_attr(
1020 feature = "serde",
1021 serde(default, skip_serializing_if = "Option::is_none")
1022 )]
1023 pub retention: Option<OpenAIPromptCacheRetention>,
1024}
1025
1026#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1028#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1029#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
1030pub enum OpenAIPromptCacheRetention {
1031 InMemory,
1033 #[cfg_attr(feature = "serde", serde(rename = "24h"))]
1035 Hours24,
1036}
1037
1038impl OpenAIPromptCacheRetention {
1039 #[must_use]
1041 pub const fn as_str(self) -> &'static str {
1042 match self {
1043 Self::InMemory => "in-memory",
1044 Self::Hours24 => "24h",
1045 }
1046 }
1047}
1048
1049#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1051#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1052pub struct ClaudePromptCache {
1053 pub ttl: ClaudePromptCacheTtl,
1055 pub strategy: ClaudePromptCacheStrategy,
1057}
1058
1059impl ClaudePromptCache {
1060 #[must_use]
1062 pub const fn new(ttl: ClaudePromptCacheTtl) -> Self {
1063 Self {
1064 ttl,
1065 strategy: ClaudePromptCacheStrategy::Automatic,
1066 }
1067 }
1068
1069 #[must_use]
1071 pub const fn automatic(ttl: ClaudePromptCacheTtl) -> Self {
1072 Self::new(ttl)
1073 }
1074
1075 #[must_use]
1077 pub const fn explicit(
1078 ttl: ClaudePromptCacheTtl,
1079 breakpoints: ClaudeExplicitCacheBreakpoints,
1080 ) -> Self {
1081 Self {
1082 ttl,
1083 strategy: ClaudePromptCacheStrategy::Explicit(breakpoints),
1084 }
1085 }
1086
1087 #[must_use]
1089 pub const fn automatic_with_explicit(
1090 ttl: ClaudePromptCacheTtl,
1091 breakpoints: ClaudeExplicitCacheBreakpoints,
1092 ) -> Self {
1093 Self {
1094 ttl,
1095 strategy: ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints),
1096 }
1097 }
1098
1099 #[must_use]
1101 pub const fn with_strategy(mut self, strategy: ClaudePromptCacheStrategy) -> Self {
1102 self.strategy = strategy;
1103 self
1104 }
1105}
1106
1107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1110pub enum ClaudePromptCacheTtl {
1111 #[default]
1113 FiveMinutes,
1114 OneHour,
1116}
1117
1118impl ClaudePromptCacheTtl {
1119 #[must_use]
1121 pub const fn as_str(self) -> &'static str {
1122 match self {
1123 Self::FiveMinutes => "5m",
1124 Self::OneHour => "1h",
1125 }
1126 }
1127}
1128
1129#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1132pub enum ClaudePromptCacheStrategy {
1133 #[default]
1135 Automatic,
1136 Explicit(ClaudeExplicitCacheBreakpoints),
1138 AutomaticAndExplicit(ClaudeExplicitCacheBreakpoints),
1140}
1141
1142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1144#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1145pub struct ClaudeExplicitCacheBreakpoint {
1146 pub target: ClaudeCacheBreakpointTarget,
1148 pub ttl: Option<ClaudePromptCacheTtl>,
1152}
1153
1154impl ClaudeExplicitCacheBreakpoint {
1155 #[must_use]
1157 pub const fn new(target: ClaudeCacheBreakpointTarget) -> Self {
1158 Self { target, ttl: None }
1159 }
1160
1161 #[must_use]
1163 pub const fn with_ttl(mut self, ttl: ClaudePromptCacheTtl) -> Self {
1164 self.ttl = Some(ttl);
1165 self
1166 }
1167
1168 #[must_use]
1170 pub const fn effective_ttl(self, default_ttl: ClaudePromptCacheTtl) -> ClaudePromptCacheTtl {
1171 match self.ttl {
1172 Some(ttl) => ttl,
1173 None => default_ttl,
1174 }
1175 }
1176}
1177
1178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1181pub enum ClaudeCacheBreakpointTarget {
1182 LastTool,
1184 Tool(usize),
1186 LastSystem,
1188 System(usize),
1190 LastMessage,
1192 Message {
1194 message_index: usize,
1196 block_index: usize,
1198 },
1199}
1200
1201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1203#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1204pub struct ClaudeExplicitCacheBreakpoints {
1205 pub first: ClaudeExplicitCacheBreakpoint,
1207 pub second: Option<ClaudeExplicitCacheBreakpoint>,
1209 pub third: Option<ClaudeExplicitCacheBreakpoint>,
1211 pub fourth: Option<ClaudeExplicitCacheBreakpoint>,
1213}
1214
1215impl ClaudeExplicitCacheBreakpoints {
1216 #[must_use]
1218 pub const fn new(first: ClaudeExplicitCacheBreakpoint) -> Self {
1219 Self {
1220 first,
1221 second: None,
1222 third: None,
1223 fourth: None,
1224 }
1225 }
1226
1227 #[must_use]
1229 pub const fn with_second(mut self, second: ClaudeExplicitCacheBreakpoint) -> Self {
1230 self.second = Some(second);
1231 self
1232 }
1233
1234 #[must_use]
1236 pub const fn with_third(mut self, third: ClaudeExplicitCacheBreakpoint) -> Self {
1237 self.third = Some(third);
1238 self
1239 }
1240
1241 #[must_use]
1243 pub const fn with_fourth(mut self, fourth: ClaudeExplicitCacheBreakpoint) -> Self {
1244 self.fourth = Some(fourth);
1245 self
1246 }
1247
1248 pub fn iter(self) -> impl Iterator<Item = ClaudeExplicitCacheBreakpoint> {
1250 [Some(self.first), self.second, self.third, self.fourth]
1251 .into_iter()
1252 .flatten()
1253 }
1254
1255 #[must_use]
1257 pub const fn count(&self) -> usize {
1258 1 + self.second.is_some() as usize
1259 + self.third.is_some() as usize
1260 + self.fourth.is_some() as usize
1261 }
1262
1263 #[must_use]
1265 pub const fn is_full(&self) -> bool {
1266 self.fourth.is_some()
1267 }
1268
1269 #[must_use]
1271 pub const fn messages_only() -> Self {
1272 Self::new(ClaudeExplicitCacheBreakpoint::new(
1273 ClaudeCacheBreakpointTarget::LastMessage,
1274 ))
1275 }
1276
1277 #[must_use]
1279 pub const fn all() -> Self {
1280 Self::new(ClaudeExplicitCacheBreakpoint::new(
1281 ClaudeCacheBreakpointTarget::LastTool,
1282 ))
1283 .with_second(ClaudeExplicitCacheBreakpoint::new(
1284 ClaudeCacheBreakpointTarget::LastSystem,
1285 ))
1286 .with_third(ClaudeExplicitCacheBreakpoint::new(
1287 ClaudeCacheBreakpointTarget::LastMessage,
1288 ))
1289 }
1290}
1291
1292impl Default for ClaudeExplicitCacheBreakpoints {
1293 fn default() -> Self {
1294 Self::messages_only()
1295 }
1296}
1297
1298#[derive(Debug, Clone, PartialEq, Eq)]
1300#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1301pub struct GeminiPromptCache {
1302 pub cached_content: String,
1304}
1305
1306impl GeminiPromptCache {
1307 #[must_use]
1309 pub fn new(cached_content: impl Into<String>) -> Self {
1310 Self {
1311 cached_content: cached_content.into(),
1312 }
1313 }
1314}
1315
1316#[derive(Debug, Clone, PartialEq, PartialOrd)]
1332#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1333#[non_exhaustive]
1334pub struct Profile {
1335 pub name: String,
1337 pub author: String,
1339 pub slug: String,
1341 pub description: String,
1343 pub abilities: Vec<Ability>,
1345 pub context_length: u32,
1347 pub pricing: Option<Pricing>,
1349}
1350
1351#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
1370#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1371#[non_exhaustive]
1372pub struct Pricing {
1373 pub prompt: f64,
1375 pub completion: f64,
1377 pub request: f64,
1379 pub image: f64,
1381 pub web_search: f64,
1383 pub internal_reasoning: f64,
1385 pub input_cache_read: f64,
1387 pub input_cache_write: f64,
1389}
1390
1391#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
1411#[allow(clippy::struct_excessive_bools)]
1412#[non_exhaustive]
1413pub struct SupportedParameters {
1414 pub max_tokens: bool,
1416 pub temperature: bool,
1418 pub top_p: bool,
1420 pub reasoning: bool,
1422 pub include_reasoning: bool,
1424 pub structured_outputs: bool,
1426 pub response_format: bool,
1428 pub stop: bool,
1430 pub frequency_penalty: bool,
1432 pub presence_penalty: bool,
1434 pub seed: bool,
1436}
1437
1438impl Profile {
1439 pub fn new(
1455 name: impl Into<String>,
1456 author: impl Into<String>,
1457 slug: impl Into<String>,
1458 description: impl Into<String>,
1459 context_length: u32,
1460 ) -> Self {
1461 Self {
1462 name: name.into(),
1463 author: author.into(),
1464 slug: slug.into(),
1465 description: description.into(),
1466 abilities: Vec::new(),
1467 context_length,
1468 pricing: None,
1469 }
1470 }
1471
1472 #[must_use]
1487 pub fn with_ability(self, ability: Ability) -> Self {
1488 self.with_abilities([ability])
1489 }
1490
1491 #[must_use]
1507 pub fn with_abilities(mut self, abilities: impl IntoIterator<Item = Ability>) -> Self {
1508 self.abilities.extend(abilities);
1509 self
1510 }
1511
1512 #[must_use]
1532 pub const fn with_pricing(mut self, pricing: Pricing) -> Self {
1533 self.pricing = Some(pricing);
1534 self
1535 }
1536}
1537
1538#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1554#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1555#[non_exhaustive]
1556pub enum Ability {
1557 ToolUse,
1559 Vision,
1561 Audio,
1563 AudioOutput,
1565 Video,
1567 WebSearch,
1569 Pdf,
1571 CodeExecution,
1573 Reasoning,
1575 ImageGeneration,
1577 ComputerUse,
1579 PromptCaching,
1581 AssistantPrefill,
1583}
1584
1585#[cfg(test)]
1586mod tests {
1587 use super::*;
1588
1589 #[test]
1590 fn profile_creation() {
1591 let profile = Profile::new("Test model", "test", "test-model", "A test model", 4096);
1592
1593 assert_eq!(profile.name, "Test model");
1594 assert_eq!(profile.slug, "test-model");
1595 assert_eq!(profile.description, "A test model");
1596 assert_eq!(profile.context_length, 4096);
1597 assert!(
1598 profile.abilities.is_empty(),
1599 "expected no abilities, got {:?}",
1600 profile.abilities
1601 );
1602 assert!(profile.pricing.is_none());
1603 }
1604
1605 #[test]
1606 fn profile_with_single_ability() {
1607 let profile = Profile::new(
1608 "Test vision model",
1609 "test",
1610 "vision-model",
1611 "A vision model",
1612 8192,
1613 )
1614 .with_ability(Ability::Vision);
1615
1616 assert_eq!(profile.abilities.len(), 1);
1617 assert_eq!(profile.abilities[0], Ability::Vision);
1618 }
1619
1620 #[test]
1621 fn profile_with_multiple_abilities() {
1622 let abilities = [Ability::ToolUse, Ability::Vision, Ability::Audio];
1623 let profile = Profile::new(
1624 "Test",
1625 "test",
1626 "multimodal-model",
1627 "A multimodal model",
1628 16384,
1629 )
1630 .with_abilities(abilities);
1631
1632 assert_eq!(profile.abilities.len(), 3);
1633 assert_eq!(profile.abilities, abilities);
1634 }
1635
1636 #[test]
1637 #[allow(clippy::float_cmp)]
1638 fn profile_with_pricing() {
1639 let pricing = Pricing {
1640 prompt: 0.0001,
1641 completion: 0.0002,
1642 request: 0.001,
1643 image: 0.01,
1644 web_search: 0.005,
1645 internal_reasoning: 0.0003,
1646 input_cache_read: 0.00005,
1647 input_cache_write: 0.0001,
1648 };
1649
1650 let profile = Profile::new(
1651 "Test paid model",
1652 "test",
1653 "paid-model",
1654 "A paid model",
1655 2048,
1656 )
1657 .with_pricing(pricing);
1658
1659 assert!(profile.pricing.is_some());
1660 let profile_pricing = profile.pricing.unwrap();
1661 assert_eq!(profile_pricing.prompt, 0.0001);
1662 assert_eq!(profile_pricing.completion, 0.0002);
1663 assert_eq!(profile_pricing.request, 0.001);
1664 assert_eq!(profile_pricing.image, 0.01);
1665 assert_eq!(profile_pricing.web_search, 0.005);
1666 assert_eq!(profile_pricing.internal_reasoning, 0.0003);
1667 assert_eq!(profile_pricing.input_cache_read, 0.00005);
1668 assert_eq!(profile_pricing.input_cache_write, 0.0001);
1669 }
1670
1671 #[test]
1672 fn profile_builder_pattern() {
1673 let pricing = Pricing {
1674 prompt: 0.001,
1675 completion: 0.002,
1676 request: 0.01,
1677 image: 0.1,
1678 web_search: 0.05,
1679 internal_reasoning: 0.003,
1680 input_cache_read: 0.0005,
1681 input_cache_write: 0.001,
1682 };
1683
1684 let profile = Profile::new("Test", "test", "full-model", "A full-featured model", 32768)
1685 .with_ability(Ability::ToolUse)
1686 .with_ability(Ability::Vision)
1687 .with_abilities([Ability::Audio, Ability::WebSearch])
1688 .with_pricing(pricing);
1689
1690 assert_eq!(profile.name, "Test");
1691 assert_eq!(profile.slug, "full-model");
1692 assert_eq!(profile.description, "A full-featured model");
1693 assert_eq!(profile.context_length, 32768);
1694 assert_eq!(profile.abilities.len(), 4);
1695 assert!(profile.abilities.contains(&Ability::ToolUse));
1696 assert!(profile.abilities.contains(&Ability::Vision));
1697 assert!(profile.abilities.contains(&Ability::Audio));
1698 assert!(profile.abilities.contains(&Ability::WebSearch));
1699 assert!(profile.pricing.is_some());
1700 }
1701
1702 #[test]
1703 fn ability_equality() {
1704 assert_eq!(Ability::ToolUse, Ability::ToolUse);
1705 assert_eq!(Ability::Vision, Ability::Vision);
1706 assert_eq!(Ability::Audio, Ability::Audio);
1707 assert_eq!(Ability::WebSearch, Ability::WebSearch);
1708
1709 assert_ne!(Ability::ToolUse, Ability::Vision);
1710 assert_ne!(Ability::Audio, Ability::WebSearch);
1711 }
1712
1713 #[test]
1714 fn ability_debug() {
1715 let ability = Ability::ToolUse;
1716 let debug_str = alloc::format!("{ability:?}");
1717 assert!(debug_str.contains("ToolUse"));
1718 }
1719
1720 #[test]
1721 fn profile_debug() {
1722 let profile = Profile::new("Test model", "test", "debug-model", "A debug model", 1024);
1723 let debug_str = alloc::format!("{profile:?}");
1724 assert!(debug_str.contains("debug-model"));
1725 assert!(debug_str.contains("A debug model"));
1726 assert!(debug_str.contains("1024"));
1727 }
1728
1729 #[test]
1730 fn profile_clone() {
1731 let original = Profile::new("Test model", "test", "original", "Original model", 2048)
1732 .with_ability(Ability::Vision);
1733 let cloned = original.clone();
1734
1735 assert_eq!(original.name, cloned.name);
1736 assert_eq!(original.description, cloned.description);
1737 assert_eq!(original.context_length, cloned.context_length);
1738 assert_eq!(original.abilities, cloned.abilities);
1739 }
1740
1741 #[test]
1742 fn pricing_debug() {
1743 let pricing = Pricing {
1744 prompt: 0.001,
1745 completion: 0.002,
1746 request: 0.01,
1747 image: 0.1,
1748 web_search: 0.05,
1749 internal_reasoning: 0.003,
1750 input_cache_read: 0.0005,
1751 input_cache_write: 0.001,
1752 };
1753
1754 let debug_str = alloc::format!("{pricing:?}");
1755 assert!(debug_str.contains("0.001"));
1756 assert!(debug_str.contains("0.002"));
1757 }
1758
1759 #[test]
1760 #[allow(clippy::float_cmp)]
1761 fn pricing_clone() {
1762 let original = Pricing {
1763 prompt: 0.001,
1764 completion: 0.002,
1765 request: 0.01,
1766 image: 0.1,
1767 web_search: 0.05,
1768 internal_reasoning: 0.003,
1769 input_cache_read: 0.0005,
1770 input_cache_write: 0.001,
1771 };
1772 let cloned = original.clone();
1773
1774 assert_eq!(original.prompt, cloned.prompt);
1775 assert_eq!(original.completion, cloned.completion);
1776 assert_eq!(original.request, cloned.request);
1777 assert_eq!(original.image, cloned.image);
1778 assert_eq!(original.web_search, cloned.web_search);
1779 assert_eq!(original.internal_reasoning, cloned.internal_reasoning);
1780 assert_eq!(original.input_cache_read, cloned.input_cache_read);
1781 assert_eq!(original.input_cache_write, cloned.input_cache_write);
1782 }
1783
1784 #[test]
1785 fn pricing_equality() {
1786 let pricing1 = Pricing {
1787 prompt: 0.001,
1788 completion: 0.002,
1789 request: 0.01,
1790 image: 0.1,
1791 web_search: 0.05,
1792 internal_reasoning: 0.003,
1793 input_cache_read: 0.0005,
1794 input_cache_write: 0.001,
1795 };
1796
1797 let pricing2 = Pricing {
1798 prompt: 0.001,
1799 completion: 0.002,
1800 request: 0.01,
1801 image: 0.1,
1802 web_search: 0.05,
1803 internal_reasoning: 0.003,
1804 input_cache_read: 0.0005,
1805 input_cache_write: 0.001,
1806 };
1807
1808 let pricing3 = Pricing {
1809 prompt: 0.002, completion: 0.002,
1811 request: 0.01,
1812 image: 0.1,
1813 web_search: 0.05,
1814 internal_reasoning: 0.003,
1815 input_cache_read: 0.0005,
1816 input_cache_write: 0.001,
1817 };
1818
1819 assert_eq!(pricing1, pricing2);
1820 assert_ne!(pricing1, pricing3);
1821 }
1822
1823 #[test]
1824 fn supported_parameters() {
1825 let params = SupportedParameters {
1826 max_tokens: true,
1827 temperature: true,
1828 top_p: false,
1829 structured_outputs: true,
1830 stop: true,
1831 presence_penalty: true,
1832 ..Default::default()
1833 };
1834
1835 assert!(params.max_tokens);
1836 assert!(params.temperature);
1837 assert!(!params.top_p);
1838 }
1839
1840 #[test]
1841 fn parameters_debug() {
1842 let params = Parameters::default()
1843 .temperature(0.7)
1844 .top_p(0.9)
1845 .top_k(40)
1846 .seed(42)
1847 .max_tokens(1000);
1848
1849 let debug_str = alloc::format!("{params:?}");
1850 assert!(debug_str.contains("0.7"));
1851 assert!(debug_str.contains("42"));
1852 assert!(debug_str.contains("1000"));
1853 }
1854
1855 #[test]
1856 fn parameters_cache_builder_sets_expected_fields() {
1857 let params = Parameters::default()
1858 .prompt_cache_key("project:chat:42")
1859 .prompt_cache_retention(OpenAIPromptCacheRetention::Hours24)
1860 .claude_prompt_cache(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1861 .gemini_cached_content("cachedContents/session-42");
1862
1863 let openai_cache = params
1864 .cache
1865 .openai
1866 .as_ref()
1867 .expect("openai cache should be set");
1868 assert_eq!(openai_cache.key.as_deref(), Some("project:chat:42"));
1869 assert_eq!(
1870 openai_cache.retention,
1871 Some(OpenAIPromptCacheRetention::Hours24)
1872 );
1873 assert_eq!(
1874 params.cache.claude,
1875 Some(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1876 );
1877 assert_eq!(
1878 params
1879 .cache
1880 .gemini
1881 .as_ref()
1882 .map(|cache| cache.cached_content.as_str()),
1883 Some("cachedContents/session-42")
1884 );
1885 }
1886
1887 #[test]
1888 fn cache_options_empty_state_changes_with_provider_values() {
1889 let mut cache = CacheOptions::default();
1890 assert!(cache.is_empty());
1891
1892 cache.openai = Some(OpenAIPromptCache::default());
1893 assert!(!cache.is_empty());
1894 }
1895
1896 #[test]
1897 fn prompt_cache_retention_string_values_match_api() {
1898 assert_eq!(OpenAIPromptCacheRetention::InMemory.as_str(), "in-memory");
1899 assert_eq!(OpenAIPromptCacheRetention::Hours24.as_str(), "24h");
1900 }
1901
1902 #[test]
1903 fn claude_prompt_cache_ttl_string_values_match_api() {
1904 assert_eq!(ClaudePromptCacheTtl::FiveMinutes.as_str(), "5m");
1905 assert_eq!(ClaudePromptCacheTtl::OneHour.as_str(), "1h");
1906 }
1907
1908 #[test]
1909 fn claude_cache_default_strategy_is_automatic() {
1910 let cache = ClaudePromptCache::new(ClaudePromptCacheTtl::FiveMinutes);
1911 assert_eq!(cache.strategy, ClaudePromptCacheStrategy::Automatic);
1912 }
1913
1914 #[test]
1915 fn claude_explicit_breakpoints_default_to_messages_only() {
1916 let breakpoints = ClaudeExplicitCacheBreakpoints::default();
1917 assert_eq!(breakpoints.count(), 1);
1918 assert_eq!(
1919 breakpoints.first.target,
1920 ClaudeCacheBreakpointTarget::LastMessage
1921 );
1922 assert!(breakpoints.second.is_none());
1923 }
1924
1925 #[test]
1926 fn claude_prompt_cache_explicit_builder_preserves_breakpoints() {
1927 let breakpoints = ClaudeExplicitCacheBreakpoints::all();
1928 let params = Parameters::default()
1929 .claude_prompt_cache_explicit(ClaudePromptCacheTtl::OneHour, breakpoints);
1930 let cache = params
1931 .cache
1932 .claude
1933 .expect("claude cache should be set by explicit builder");
1934 assert_eq!(cache.ttl, ClaudePromptCacheTtl::OneHour);
1935 assert_eq!(
1936 cache.strategy,
1937 ClaudePromptCacheStrategy::Explicit(breakpoints)
1938 );
1939 }
1940
1941 #[test]
1942 fn claude_explicit_breakpoint_supports_per_block_ttl_override() {
1943 let breakpoint = ClaudeExplicitCacheBreakpoint::new(ClaudeCacheBreakpointTarget::Tool(0))
1944 .with_ttl(ClaudePromptCacheTtl::OneHour);
1945 assert_eq!(breakpoint.ttl, Some(ClaudePromptCacheTtl::OneHour));
1946 assert_eq!(
1947 breakpoint.effective_ttl(ClaudePromptCacheTtl::FiveMinutes),
1948 ClaudePromptCacheTtl::OneHour
1949 );
1950 }
1951
1952 #[test]
1953 fn claude_prompt_cache_automatic_with_explicit_builder_preserves_breakpoints() {
1954 let breakpoints = ClaudeExplicitCacheBreakpoints::messages_only();
1955 let params = Parameters::default().claude_prompt_cache_automatic_with_explicit(
1956 ClaudePromptCacheTtl::FiveMinutes,
1957 breakpoints,
1958 );
1959 let cache = params
1960 .cache
1961 .claude
1962 .expect("claude cache should be set by combined builder");
1963 assert_eq!(
1964 cache.strategy,
1965 ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints)
1966 );
1967 }
1968}