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]
251 pub fn openai_tools(mut self, tools: OpenAINativeTools) -> Self {
252 self.native_tools.openai = tools;
253 self
254 }
255
256 #[must_use]
258 pub const fn gemini_tools(mut self, tools: GeminiNativeTools) -> Self {
259 self.native_tools.gemini = tools;
260 self
261 }
262
263 #[must_use]
265 pub const fn claude_tools(mut self, tools: ClaudeNativeTools) -> Self {
266 self.native_tools.claude = tools;
267 self
268 }
269
270 #[must_use]
272 pub fn prompt_cache_key(mut self, key: impl Into<String>) -> Self {
273 let cache = self
274 .cache
275 .openai
276 .get_or_insert_with(OpenAIPromptCache::default);
277 cache.key = Some(key.into());
278 self
279 }
280
281 #[must_use]
283 pub fn prompt_cache_retention(mut self, retention: OpenAIPromptCacheRetention) -> Self {
284 let cache = self
285 .cache
286 .openai
287 .get_or_insert_with(OpenAIPromptCache::default);
288 cache.retention = Some(retention);
289 self
290 }
291
292 #[must_use]
294 pub const fn claude_prompt_cache(mut self, cache: ClaudePromptCache) -> Self {
295 self.cache.claude = Some(cache);
296 self
297 }
298
299 #[must_use]
301 pub const fn claude_prompt_cache_automatic(mut self, ttl: ClaudePromptCacheTtl) -> Self {
302 self.cache.claude = Some(ClaudePromptCache::automatic(ttl));
303 self
304 }
305
306 #[must_use]
308 pub const fn claude_prompt_cache_explicit(
309 mut self,
310 ttl: ClaudePromptCacheTtl,
311 breakpoints: ClaudeExplicitCacheBreakpoints,
312 ) -> Self {
313 self.cache.claude = Some(ClaudePromptCache::explicit(ttl, breakpoints));
314 self
315 }
316
317 #[must_use]
319 pub const fn claude_prompt_cache_automatic_with_explicit(
320 mut self,
321 ttl: ClaudePromptCacheTtl,
322 breakpoints: ClaudeExplicitCacheBreakpoints,
323 ) -> Self {
324 self.cache.claude = Some(ClaudePromptCache::automatic_with_explicit(ttl, breakpoints));
325 self
326 }
327
328 #[must_use]
330 pub fn gemini_cached_content(mut self, cached_content: impl Into<String>) -> Self {
331 self.cache.gemini = Some(GeminiPromptCache::new(cached_content));
332 self
333 }
334
335 #[must_use]
337 pub fn without_cache(mut self) -> Self {
338 self.cache = CacheOptions {
339 openai: None,
340 claude: None,
341 gemini: None,
342 };
343 self
344 }
345
346 #[must_use]
348 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
349 self.tool_choice = choice;
350 self
351 }
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Default)]
356#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
357pub struct NativeTools {
358 #[cfg_attr(
360 feature = "serde",
361 serde(default, skip_serializing_if = "OpenAINativeTools::is_empty")
362 )]
363 pub openai: OpenAINativeTools,
364 #[cfg_attr(
366 feature = "serde",
367 serde(default, skip_serializing_if = "GeminiNativeTools::is_empty")
368 )]
369 pub gemini: GeminiNativeTools,
370 #[cfg_attr(
372 feature = "serde",
373 serde(default, skip_serializing_if = "ClaudeNativeTools::is_empty")
374 )]
375 pub claude: ClaudeNativeTools,
376}
377
378impl NativeTools {
379 #[must_use]
381 pub const fn is_empty(&self) -> bool {
382 self.openai.is_empty() && self.gemini.is_empty() && self.claude.is_empty()
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Default)]
388#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
389pub struct OpenAINativeTools {
390 #[cfg_attr(
392 feature = "serde",
393 serde(default, skip_serializing_if = "Option::is_none")
394 )]
395 pub web_search: Option<OpenAIWebSearchTool>,
396 #[cfg_attr(
398 feature = "serde",
399 serde(default, skip_serializing_if = "Vec::is_empty")
400 )]
401 pub file_search: Vec<OpenAIFileSearchTool>,
402 #[cfg_attr(
404 feature = "serde",
405 serde(default, skip_serializing_if = "Option::is_none")
406 )]
407 pub code_interpreter: Option<OpenAICodeInterpreterTool>,
408 #[cfg_attr(
410 feature = "serde",
411 serde(default, skip_serializing_if = "Option::is_none")
412 )]
413 pub image_generation: Option<OpenAIImageGenerationTool>,
414 #[cfg_attr(
416 feature = "serde",
417 serde(default, skip_serializing_if = "Vec::is_empty")
418 )]
419 pub mcp: Vec<OpenAIMcpTool>,
420 #[cfg_attr(
422 feature = "serde",
423 serde(default, skip_serializing_if = "Option::is_none")
424 )]
425 pub computer_use: Option<OpenAIComputerUseTool>,
426}
427
428impl OpenAINativeTools {
429 #[must_use]
431 pub const fn is_empty(&self) -> bool {
432 self.web_search.is_none()
433 && self.file_search.is_empty()
434 && self.code_interpreter.is_none()
435 && self.image_generation.is_none()
436 && self.mcp.is_empty()
437 && self.computer_use.is_none()
438 }
439
440 #[must_use]
442 pub fn with_web_search(mut self, tool: OpenAIWebSearchTool) -> Self {
443 self.web_search = Some(tool);
444 self
445 }
446
447 #[must_use]
449 pub fn enable_web_search(mut self) -> Self {
450 self.web_search = Some(OpenAIWebSearchTool::default());
451 self
452 }
453
454 #[must_use]
456 pub fn with_file_search(mut self, tool: OpenAIFileSearchTool) -> Self {
457 self.file_search.push(tool);
458 self
459 }
460
461 #[must_use]
463 pub fn with_code_interpreter(mut self, tool: OpenAICodeInterpreterTool) -> Self {
464 self.code_interpreter = Some(tool);
465 self
466 }
467
468 #[must_use]
470 pub const fn with_image_generation(mut self, tool: OpenAIImageGenerationTool) -> Self {
471 self.image_generation = Some(tool);
472 self
473 }
474
475 #[must_use]
477 pub fn with_mcp(mut self, tool: OpenAIMcpTool) -> Self {
478 self.mcp.push(tool);
479 self
480 }
481
482 #[must_use]
484 pub fn with_computer_use(mut self, tool: OpenAIComputerUseTool) -> Self {
485 self.computer_use = Some(tool);
486 self
487 }
488}
489
490#[derive(Debug, Clone, PartialEq, Eq, Default)]
492#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
493pub struct OpenAIWebSearchTool {
494 #[cfg_attr(
496 feature = "serde",
497 serde(default, skip_serializing_if = "Option::is_none")
498 )]
499 pub external_web_access: Option<bool>,
500 #[cfg_attr(
502 feature = "serde",
503 serde(default, skip_serializing_if = "Option::is_none")
504 )]
505 pub filters: Option<Value>,
506 #[cfg_attr(
508 feature = "serde",
509 serde(default, skip_serializing_if = "Option::is_none")
510 )]
511 pub user_location: Option<Value>,
512}
513
514impl OpenAIWebSearchTool {
515 #[must_use]
517 pub const fn external_web_access(mut self, allowed: bool) -> Self {
518 self.external_web_access = Some(allowed);
519 self
520 }
521
522 #[must_use]
524 pub fn filters(mut self, filters: Value) -> Self {
525 self.filters = Some(filters);
526 self
527 }
528
529 #[must_use]
531 pub fn user_location(mut self, location: Value) -> Self {
532 self.user_location = Some(location);
533 self
534 }
535}
536
537#[derive(Debug, Clone, PartialEq, Eq)]
539#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
540pub struct OpenAIFileSearchTool {
541 pub vector_store_ids: Vec<String>,
543 #[cfg_attr(
545 feature = "serde",
546 serde(default, skip_serializing_if = "Option::is_none")
547 )]
548 pub max_num_results: Option<u32>,
549 pub include_results: bool,
551 #[cfg_attr(
553 feature = "serde",
554 serde(default, skip_serializing_if = "Option::is_none")
555 )]
556 pub filters: Option<Value>,
557}
558
559impl OpenAIFileSearchTool {
560 #[must_use]
562 pub fn new(vector_store_ids: impl Into<Vec<String>>) -> Self {
563 Self {
564 vector_store_ids: vector_store_ids.into(),
565 max_num_results: None,
566 include_results: false,
567 filters: None,
568 }
569 }
570
571 #[must_use]
573 pub const fn max_num_results(mut self, value: u32) -> Self {
574 self.max_num_results = Some(value);
575 self
576 }
577
578 #[must_use]
580 pub const fn include_results(mut self, include: bool) -> Self {
581 self.include_results = include;
582 self
583 }
584
585 #[must_use]
587 pub fn filters(mut self, filters: Value) -> Self {
588 self.filters = Some(filters);
589 self
590 }
591}
592
593#[derive(Debug, Clone, PartialEq, Eq)]
595#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
596pub struct OpenAICodeInterpreterTool {
597 pub container: OpenAICodeInterpreterContainer,
599}
600
601impl Default for OpenAICodeInterpreterTool {
602 fn default() -> Self {
603 Self {
604 container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::default()),
605 }
606 }
607}
608
609impl OpenAICodeInterpreterTool {
610 #[must_use]
612 pub const fn auto() -> Self {
613 Self {
614 container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::new()),
615 }
616 }
617
618 #[must_use]
620 pub fn existing(container_id: impl Into<String>) -> Self {
621 Self {
622 container: OpenAICodeInterpreterContainer::Existing(container_id.into()),
623 }
624 }
625}
626
627#[derive(Debug, Clone, PartialEq, Eq)]
629#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
630pub enum OpenAICodeInterpreterContainer {
631 Auto(OpenAIAutoContainer),
633 Existing(String),
635}
636
637#[allow(clippy::struct_excessive_bools)]
639#[derive(Debug, Clone, PartialEq, Eq, Default)]
640#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
641pub struct OpenAIAutoContainer {
642 #[cfg_attr(
644 feature = "serde",
645 serde(default, skip_serializing_if = "Option::is_none")
646 )]
647 pub memory_limit: Option<String>,
648 #[cfg_attr(
650 feature = "serde",
651 serde(default, skip_serializing_if = "Vec::is_empty")
652 )]
653 pub file_ids: Vec<String>,
654}
655
656impl OpenAIAutoContainer {
657 #[must_use]
659 pub const fn new() -> Self {
660 Self {
661 memory_limit: None,
662 file_ids: Vec::new(),
663 }
664 }
665
666 #[must_use]
668 pub fn memory_limit(mut self, memory_limit: impl Into<String>) -> Self {
669 self.memory_limit = Some(memory_limit.into());
670 self
671 }
672
673 #[must_use]
675 pub fn file_ids(mut self, file_ids: impl Into<Vec<String>>) -> Self {
676 self.file_ids = file_ids.into();
677 self
678 }
679}
680
681#[derive(Debug, Clone, PartialEq, Eq, Default)]
683#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
684pub struct OpenAIImageGenerationTool {
685 #[cfg_attr(
687 feature = "serde",
688 serde(default, skip_serializing_if = "Option::is_none")
689 )]
690 pub partial_images: Option<u8>,
691}
692
693impl OpenAIImageGenerationTool {
694 #[must_use]
696 pub const fn partial_images(mut self, count: u8) -> Self {
697 self.partial_images = Some(count);
698 self
699 }
700}
701
702#[derive(Debug, Clone, PartialEq, Eq)]
704#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
705pub struct OpenAIMcpTool {
706 pub server_label: String,
708 pub server_url: String,
710 #[cfg_attr(
712 feature = "serde",
713 serde(default, skip_serializing_if = "Option::is_none")
714 )]
715 pub require_approval: Option<String>,
716 #[cfg_attr(
718 feature = "serde",
719 serde(default, skip_serializing_if = "Vec::is_empty")
720 )]
721 pub allowed_tools: Vec<String>,
722}
723
724impl OpenAIMcpTool {
725 #[must_use]
727 pub fn new(server_label: impl Into<String>, server_url: impl Into<String>) -> Self {
728 Self {
729 server_label: server_label.into(),
730 server_url: server_url.into(),
731 require_approval: None,
732 allowed_tools: Vec::new(),
733 }
734 }
735
736 #[must_use]
738 pub fn require_approval(mut self, policy: impl Into<String>) -> Self {
739 self.require_approval = Some(policy.into());
740 self
741 }
742
743 #[must_use]
745 pub fn allowed_tools(mut self, tools: impl Into<Vec<String>>) -> Self {
746 self.allowed_tools = tools.into();
747 self
748 }
749}
750
751#[derive(Debug, Clone, PartialEq, Eq)]
753#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
754pub struct OpenAIComputerUseTool {
755 pub display_width: u32,
757 pub display_height: u32,
759 pub environment: String,
761}
762
763impl OpenAIComputerUseTool {
764 #[must_use]
766 pub fn new(display_width: u32, display_height: u32, environment: impl Into<String>) -> Self {
767 Self {
768 display_width,
769 display_height,
770 environment: environment.into(),
771 }
772 }
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Default)]
777#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
778pub struct GeminiNativeTools {
779 pub google_search: bool,
781 pub code_execution: bool,
783 pub url_context: bool,
785}
786
787impl GeminiNativeTools {
788 #[must_use]
790 pub const fn is_empty(&self) -> bool {
791 !self.google_search && !self.code_execution && !self.url_context
792 }
793
794 #[must_use]
796 pub const fn google_search(mut self, enabled: bool) -> Self {
797 self.google_search = enabled;
798 self
799 }
800
801 #[must_use]
803 pub const fn code_execution(mut self, enabled: bool) -> Self {
804 self.code_execution = enabled;
805 self
806 }
807
808 #[must_use]
810 pub const fn url_context(mut self, enabled: bool) -> Self {
811 self.url_context = enabled;
812 self
813 }
814}
815
816#[allow(clippy::struct_excessive_bools)]
818#[derive(Debug, Clone, PartialEq, Eq, Default)]
819#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
820pub struct ClaudeNativeTools {
821 pub web_search: bool,
823 pub web_fetch: bool,
825 pub code_execution: bool,
827 pub bash: bool,
829 #[cfg_attr(
831 feature = "serde",
832 serde(default, skip_serializing_if = "Option::is_none")
833 )]
834 pub text_editor: Option<ClaudeTextEditorTool>,
835}
836
837impl ClaudeNativeTools {
838 #[must_use]
840 pub const fn is_empty(&self) -> bool {
841 !self.web_search
842 && !self.web_fetch
843 && !self.code_execution
844 && !self.bash
845 && self.text_editor.is_none()
846 }
847
848 #[must_use]
850 pub const fn web_search(mut self, enabled: bool) -> Self {
851 self.web_search = enabled;
852 self
853 }
854
855 #[must_use]
857 pub const fn web_fetch(mut self, enabled: bool) -> Self {
858 self.web_fetch = enabled;
859 self
860 }
861
862 #[must_use]
864 pub const fn code_execution(mut self, enabled: bool) -> Self {
865 self.code_execution = enabled;
866 self
867 }
868
869 #[must_use]
871 pub const fn bash(mut self, enabled: bool) -> Self {
872 self.bash = enabled;
873 self
874 }
875
876 #[must_use]
878 pub const fn text_editor(mut self, tool: ClaudeTextEditorTool) -> Self {
879 self.text_editor = Some(tool);
880 self
881 }
882}
883
884#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
886#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
887pub struct ClaudeTextEditorTool {
888 pub max_characters: Option<u32>,
890}
891
892impl ClaudeTextEditorTool {
893 #[must_use]
895 pub const fn max_characters(mut self, value: u32) -> Self {
896 self.max_characters = Some(value);
897 self
898 }
899}
900
901#[derive(Debug, Clone, PartialEq, Eq)]
903#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
904#[serde(rename_all = "lowercase")]
905#[derive(Default)]
906pub enum ToolChoice {
907 #[default]
909 Auto,
910 None,
912 Required,
914 Exact(String),
916}
917
918#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
931#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
932#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
933pub enum ReasoningEffort {
934 None,
939 Minimal,
941 Low,
943 Medium,
945 High,
947 XHigh,
949 Max,
951}
952
953#[derive(Debug, Clone, PartialEq, Eq, Default)]
955#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
956pub struct CacheOptions {
957 #[cfg_attr(
959 feature = "serde",
960 serde(default, skip_serializing_if = "Option::is_none")
961 )]
962 pub openai: Option<OpenAIPromptCache>,
963 #[cfg_attr(
965 feature = "serde",
966 serde(default, skip_serializing_if = "Option::is_none")
967 )]
968 pub claude: Option<ClaudePromptCache>,
969 #[cfg_attr(
971 feature = "serde",
972 serde(default, skip_serializing_if = "Option::is_none")
973 )]
974 pub gemini: Option<GeminiPromptCache>,
975}
976
977impl CacheOptions {
978 #[must_use]
980 pub const fn is_empty(&self) -> bool {
981 self.openai.is_none() && self.claude.is_none() && self.gemini.is_none()
982 }
983}
984
985#[derive(Debug, Clone, PartialEq, Eq, Default)]
987#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
988pub struct OpenAIPromptCache {
989 #[cfg_attr(
991 feature = "serde",
992 serde(default, skip_serializing_if = "Option::is_none")
993 )]
994 pub key: Option<String>,
995 #[cfg_attr(
997 feature = "serde",
998 serde(default, skip_serializing_if = "Option::is_none")
999 )]
1000 pub retention: Option<OpenAIPromptCacheRetention>,
1001}
1002
1003#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1005#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1006#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
1007pub enum OpenAIPromptCacheRetention {
1008 InMemory,
1010 #[cfg_attr(feature = "serde", serde(rename = "24h"))]
1012 Hours24,
1013}
1014
1015impl OpenAIPromptCacheRetention {
1016 #[must_use]
1018 pub const fn as_str(self) -> &'static str {
1019 match self {
1020 Self::InMemory => "in-memory",
1021 Self::Hours24 => "24h",
1022 }
1023 }
1024}
1025
1026#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1028#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1029pub struct ClaudePromptCache {
1030 pub ttl: ClaudePromptCacheTtl,
1032 pub strategy: ClaudePromptCacheStrategy,
1034}
1035
1036impl ClaudePromptCache {
1037 #[must_use]
1039 pub const fn new(ttl: ClaudePromptCacheTtl) -> Self {
1040 Self {
1041 ttl,
1042 strategy: ClaudePromptCacheStrategy::Automatic,
1043 }
1044 }
1045
1046 #[must_use]
1048 pub const fn automatic(ttl: ClaudePromptCacheTtl) -> Self {
1049 Self::new(ttl)
1050 }
1051
1052 #[must_use]
1054 pub const fn explicit(
1055 ttl: ClaudePromptCacheTtl,
1056 breakpoints: ClaudeExplicitCacheBreakpoints,
1057 ) -> Self {
1058 Self {
1059 ttl,
1060 strategy: ClaudePromptCacheStrategy::Explicit(breakpoints),
1061 }
1062 }
1063
1064 #[must_use]
1066 pub const fn automatic_with_explicit(
1067 ttl: ClaudePromptCacheTtl,
1068 breakpoints: ClaudeExplicitCacheBreakpoints,
1069 ) -> Self {
1070 Self {
1071 ttl,
1072 strategy: ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints),
1073 }
1074 }
1075
1076 #[must_use]
1078 pub const fn with_strategy(mut self, strategy: ClaudePromptCacheStrategy) -> Self {
1079 self.strategy = strategy;
1080 self
1081 }
1082}
1083
1084#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1086#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1087pub enum ClaudePromptCacheTtl {
1088 #[default]
1090 FiveMinutes,
1091 OneHour,
1093}
1094
1095impl ClaudePromptCacheTtl {
1096 #[must_use]
1098 pub const fn as_str(self) -> &'static str {
1099 match self {
1100 Self::FiveMinutes => "5m",
1101 Self::OneHour => "1h",
1102 }
1103 }
1104}
1105
1106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1109pub enum ClaudePromptCacheStrategy {
1110 #[default]
1112 Automatic,
1113 Explicit(ClaudeExplicitCacheBreakpoints),
1115 AutomaticAndExplicit(ClaudeExplicitCacheBreakpoints),
1117}
1118
1119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1122pub struct ClaudeExplicitCacheBreakpoint {
1123 pub target: ClaudeCacheBreakpointTarget,
1125 pub ttl: Option<ClaudePromptCacheTtl>,
1129}
1130
1131impl ClaudeExplicitCacheBreakpoint {
1132 #[must_use]
1134 pub const fn new(target: ClaudeCacheBreakpointTarget) -> Self {
1135 Self { target, ttl: None }
1136 }
1137
1138 #[must_use]
1140 pub const fn with_ttl(mut self, ttl: ClaudePromptCacheTtl) -> Self {
1141 self.ttl = Some(ttl);
1142 self
1143 }
1144
1145 #[must_use]
1147 pub const fn effective_ttl(self, default_ttl: ClaudePromptCacheTtl) -> ClaudePromptCacheTtl {
1148 match self.ttl {
1149 Some(ttl) => ttl,
1150 None => default_ttl,
1151 }
1152 }
1153}
1154
1155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1158pub enum ClaudeCacheBreakpointTarget {
1159 LastTool,
1161 Tool(usize),
1163 LastSystem,
1165 System(usize),
1167 LastMessage,
1169 Message {
1171 message_index: usize,
1173 block_index: usize,
1175 },
1176}
1177
1178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1181pub struct ClaudeExplicitCacheBreakpoints {
1182 pub first: ClaudeExplicitCacheBreakpoint,
1184 pub second: Option<ClaudeExplicitCacheBreakpoint>,
1186 pub third: Option<ClaudeExplicitCacheBreakpoint>,
1188 pub fourth: Option<ClaudeExplicitCacheBreakpoint>,
1190}
1191
1192impl ClaudeExplicitCacheBreakpoints {
1193 #[must_use]
1195 pub const fn new(first: ClaudeExplicitCacheBreakpoint) -> Self {
1196 Self {
1197 first,
1198 second: None,
1199 third: None,
1200 fourth: None,
1201 }
1202 }
1203
1204 #[must_use]
1206 pub const fn with_second(mut self, second: ClaudeExplicitCacheBreakpoint) -> Self {
1207 self.second = Some(second);
1208 self
1209 }
1210
1211 #[must_use]
1213 pub const fn with_third(mut self, third: ClaudeExplicitCacheBreakpoint) -> Self {
1214 self.third = Some(third);
1215 self
1216 }
1217
1218 #[must_use]
1220 pub const fn with_fourth(mut self, fourth: ClaudeExplicitCacheBreakpoint) -> Self {
1221 self.fourth = Some(fourth);
1222 self
1223 }
1224
1225 pub fn iter(self) -> impl Iterator<Item = ClaudeExplicitCacheBreakpoint> {
1227 [Some(self.first), self.second, self.third, self.fourth]
1228 .into_iter()
1229 .flatten()
1230 }
1231
1232 #[must_use]
1234 pub const fn count(&self) -> usize {
1235 1 + self.second.is_some() as usize
1236 + self.third.is_some() as usize
1237 + self.fourth.is_some() as usize
1238 }
1239
1240 #[must_use]
1242 pub const fn is_full(&self) -> bool {
1243 self.fourth.is_some()
1244 }
1245
1246 #[must_use]
1248 pub const fn messages_only() -> Self {
1249 Self::new(ClaudeExplicitCacheBreakpoint::new(
1250 ClaudeCacheBreakpointTarget::LastMessage,
1251 ))
1252 }
1253
1254 #[must_use]
1256 pub const fn all() -> Self {
1257 Self::new(ClaudeExplicitCacheBreakpoint::new(
1258 ClaudeCacheBreakpointTarget::LastTool,
1259 ))
1260 .with_second(ClaudeExplicitCacheBreakpoint::new(
1261 ClaudeCacheBreakpointTarget::LastSystem,
1262 ))
1263 .with_third(ClaudeExplicitCacheBreakpoint::new(
1264 ClaudeCacheBreakpointTarget::LastMessage,
1265 ))
1266 }
1267}
1268
1269impl Default for ClaudeExplicitCacheBreakpoints {
1270 fn default() -> Self {
1271 Self::messages_only()
1272 }
1273}
1274
1275#[derive(Debug, Clone, PartialEq, Eq)]
1277#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1278pub struct GeminiPromptCache {
1279 pub cached_content: String,
1281}
1282
1283impl GeminiPromptCache {
1284 #[must_use]
1286 pub fn new(cached_content: impl Into<String>) -> Self {
1287 Self {
1288 cached_content: cached_content.into(),
1289 }
1290 }
1291}
1292
1293#[derive(Debug, Clone, PartialEq, PartialOrd)]
1309#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1310#[non_exhaustive]
1311pub struct Profile {
1312 pub name: String,
1314 pub author: String,
1316 pub slug: String,
1318 pub description: String,
1320 pub abilities: Vec<Ability>,
1322 pub context_length: u32,
1324 pub pricing: Option<Pricing>,
1326}
1327
1328#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
1347#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1348#[non_exhaustive]
1349pub struct Pricing {
1350 pub prompt: f64,
1352 pub completion: f64,
1354 pub request: f64,
1356 pub image: f64,
1358 pub web_search: f64,
1360 pub internal_reasoning: f64,
1362 pub input_cache_read: f64,
1364 pub input_cache_write: f64,
1366}
1367
1368#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
1388#[allow(clippy::struct_excessive_bools)]
1389#[non_exhaustive]
1390pub struct SupportedParameters {
1391 pub max_tokens: bool,
1393 pub temperature: bool,
1395 pub top_p: bool,
1397 pub reasoning: bool,
1399 pub include_reasoning: bool,
1401 pub structured_outputs: bool,
1403 pub response_format: bool,
1405 pub stop: bool,
1407 pub frequency_penalty: bool,
1409 pub presence_penalty: bool,
1411 pub seed: bool,
1413}
1414
1415impl Profile {
1416 pub fn new(
1432 name: impl Into<String>,
1433 author: impl Into<String>,
1434 slug: impl Into<String>,
1435 description: impl Into<String>,
1436 context_length: u32,
1437 ) -> Self {
1438 Self {
1439 name: name.into(),
1440 author: author.into(),
1441 slug: slug.into(),
1442 description: description.into(),
1443 abilities: Vec::new(),
1444 context_length,
1445 pricing: None,
1446 }
1447 }
1448
1449 #[must_use]
1464 pub fn with_ability(self, ability: Ability) -> Self {
1465 self.with_abilities([ability])
1466 }
1467
1468 #[must_use]
1484 pub fn with_abilities(mut self, abilities: impl IntoIterator<Item = Ability>) -> Self {
1485 self.abilities.extend(abilities);
1486 self
1487 }
1488
1489 #[must_use]
1509 pub const fn with_pricing(mut self, pricing: Pricing) -> Self {
1510 self.pricing = Some(pricing);
1511 self
1512 }
1513}
1514
1515#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1531#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1532#[non_exhaustive]
1533pub enum Ability {
1534 ToolUse,
1536 Vision,
1538 Audio,
1540 AudioOutput,
1542 Video,
1544 WebSearch,
1546 Pdf,
1548 CodeExecution,
1550 Reasoning,
1552 ImageGeneration,
1554 ComputerUse,
1556 PromptCaching,
1558 AssistantPrefill,
1560}
1561
1562#[cfg(test)]
1563mod tests {
1564 use super::*;
1565
1566 #[test]
1567 fn profile_creation() {
1568 let profile = Profile::new("Test model", "test", "test-model", "A test model", 4096);
1569
1570 assert_eq!(profile.name, "Test model");
1571 assert_eq!(profile.slug, "test-model");
1572 assert_eq!(profile.description, "A test model");
1573 assert_eq!(profile.context_length, 4096);
1574 assert!(
1575 profile.abilities.is_empty(),
1576 "expected no abilities, got {:?}",
1577 profile.abilities
1578 );
1579 assert!(profile.pricing.is_none());
1580 }
1581
1582 #[test]
1583 fn profile_with_single_ability() {
1584 let profile = Profile::new(
1585 "Test vision model",
1586 "test",
1587 "vision-model",
1588 "A vision model",
1589 8192,
1590 )
1591 .with_ability(Ability::Vision);
1592
1593 assert_eq!(profile.abilities.len(), 1);
1594 assert_eq!(profile.abilities[0], Ability::Vision);
1595 }
1596
1597 #[test]
1598 fn profile_with_multiple_abilities() {
1599 let abilities = [Ability::ToolUse, Ability::Vision, Ability::Audio];
1600 let profile = Profile::new(
1601 "Test",
1602 "test",
1603 "multimodal-model",
1604 "A multimodal model",
1605 16384,
1606 )
1607 .with_abilities(abilities);
1608
1609 assert_eq!(profile.abilities.len(), 3);
1610 assert_eq!(profile.abilities, abilities);
1611 }
1612
1613 #[test]
1614 #[allow(clippy::float_cmp)]
1615 fn profile_with_pricing() {
1616 let pricing = Pricing {
1617 prompt: 0.0001,
1618 completion: 0.0002,
1619 request: 0.001,
1620 image: 0.01,
1621 web_search: 0.005,
1622 internal_reasoning: 0.0003,
1623 input_cache_read: 0.00005,
1624 input_cache_write: 0.0001,
1625 };
1626
1627 let profile = Profile::new(
1628 "Test paid model",
1629 "test",
1630 "paid-model",
1631 "A paid model",
1632 2048,
1633 )
1634 .with_pricing(pricing);
1635
1636 assert!(profile.pricing.is_some());
1637 let profile_pricing = profile.pricing.unwrap();
1638 assert_eq!(profile_pricing.prompt, 0.0001);
1639 assert_eq!(profile_pricing.completion, 0.0002);
1640 assert_eq!(profile_pricing.request, 0.001);
1641 assert_eq!(profile_pricing.image, 0.01);
1642 assert_eq!(profile_pricing.web_search, 0.005);
1643 assert_eq!(profile_pricing.internal_reasoning, 0.0003);
1644 assert_eq!(profile_pricing.input_cache_read, 0.00005);
1645 assert_eq!(profile_pricing.input_cache_write, 0.0001);
1646 }
1647
1648 #[test]
1649 fn profile_builder_pattern() {
1650 let pricing = Pricing {
1651 prompt: 0.001,
1652 completion: 0.002,
1653 request: 0.01,
1654 image: 0.1,
1655 web_search: 0.05,
1656 internal_reasoning: 0.003,
1657 input_cache_read: 0.0005,
1658 input_cache_write: 0.001,
1659 };
1660
1661 let profile = Profile::new("Test", "test", "full-model", "A full-featured model", 32768)
1662 .with_ability(Ability::ToolUse)
1663 .with_ability(Ability::Vision)
1664 .with_abilities([Ability::Audio, Ability::WebSearch])
1665 .with_pricing(pricing);
1666
1667 assert_eq!(profile.name, "Test");
1668 assert_eq!(profile.slug, "full-model");
1669 assert_eq!(profile.description, "A full-featured model");
1670 assert_eq!(profile.context_length, 32768);
1671 assert_eq!(profile.abilities.len(), 4);
1672 assert!(profile.abilities.contains(&Ability::ToolUse));
1673 assert!(profile.abilities.contains(&Ability::Vision));
1674 assert!(profile.abilities.contains(&Ability::Audio));
1675 assert!(profile.abilities.contains(&Ability::WebSearch));
1676 assert!(profile.pricing.is_some());
1677 }
1678
1679 #[test]
1680 fn ability_equality() {
1681 assert_eq!(Ability::ToolUse, Ability::ToolUse);
1682 assert_eq!(Ability::Vision, Ability::Vision);
1683 assert_eq!(Ability::Audio, Ability::Audio);
1684 assert_eq!(Ability::WebSearch, Ability::WebSearch);
1685
1686 assert_ne!(Ability::ToolUse, Ability::Vision);
1687 assert_ne!(Ability::Audio, Ability::WebSearch);
1688 }
1689
1690 #[test]
1691 fn ability_debug() {
1692 let ability = Ability::ToolUse;
1693 let debug_str = alloc::format!("{ability:?}");
1694 assert!(debug_str.contains("ToolUse"));
1695 }
1696
1697 #[test]
1698 fn profile_debug() {
1699 let profile = Profile::new("Test model", "test", "debug-model", "A debug model", 1024);
1700 let debug_str = alloc::format!("{profile:?}");
1701 assert!(debug_str.contains("debug-model"));
1702 assert!(debug_str.contains("A debug model"));
1703 assert!(debug_str.contains("1024"));
1704 }
1705
1706 #[test]
1707 fn profile_clone() {
1708 let original = Profile::new("Test model", "test", "original", "Original model", 2048)
1709 .with_ability(Ability::Vision);
1710 let cloned = original.clone();
1711
1712 assert_eq!(original.name, cloned.name);
1713 assert_eq!(original.description, cloned.description);
1714 assert_eq!(original.context_length, cloned.context_length);
1715 assert_eq!(original.abilities, cloned.abilities);
1716 }
1717
1718 #[test]
1719 fn pricing_debug() {
1720 let pricing = Pricing {
1721 prompt: 0.001,
1722 completion: 0.002,
1723 request: 0.01,
1724 image: 0.1,
1725 web_search: 0.05,
1726 internal_reasoning: 0.003,
1727 input_cache_read: 0.0005,
1728 input_cache_write: 0.001,
1729 };
1730
1731 let debug_str = alloc::format!("{pricing:?}");
1732 assert!(debug_str.contains("0.001"));
1733 assert!(debug_str.contains("0.002"));
1734 }
1735
1736 #[test]
1737 #[allow(clippy::float_cmp)]
1738 fn pricing_clone() {
1739 let original = Pricing {
1740 prompt: 0.001,
1741 completion: 0.002,
1742 request: 0.01,
1743 image: 0.1,
1744 web_search: 0.05,
1745 internal_reasoning: 0.003,
1746 input_cache_read: 0.0005,
1747 input_cache_write: 0.001,
1748 };
1749 let cloned = original.clone();
1750
1751 assert_eq!(original.prompt, cloned.prompt);
1752 assert_eq!(original.completion, cloned.completion);
1753 assert_eq!(original.request, cloned.request);
1754 assert_eq!(original.image, cloned.image);
1755 assert_eq!(original.web_search, cloned.web_search);
1756 assert_eq!(original.internal_reasoning, cloned.internal_reasoning);
1757 assert_eq!(original.input_cache_read, cloned.input_cache_read);
1758 assert_eq!(original.input_cache_write, cloned.input_cache_write);
1759 }
1760
1761 #[test]
1762 fn pricing_equality() {
1763 let pricing1 = Pricing {
1764 prompt: 0.001,
1765 completion: 0.002,
1766 request: 0.01,
1767 image: 0.1,
1768 web_search: 0.05,
1769 internal_reasoning: 0.003,
1770 input_cache_read: 0.0005,
1771 input_cache_write: 0.001,
1772 };
1773
1774 let pricing2 = Pricing {
1775 prompt: 0.001,
1776 completion: 0.002,
1777 request: 0.01,
1778 image: 0.1,
1779 web_search: 0.05,
1780 internal_reasoning: 0.003,
1781 input_cache_read: 0.0005,
1782 input_cache_write: 0.001,
1783 };
1784
1785 let pricing3 = Pricing {
1786 prompt: 0.002, completion: 0.002,
1788 request: 0.01,
1789 image: 0.1,
1790 web_search: 0.05,
1791 internal_reasoning: 0.003,
1792 input_cache_read: 0.0005,
1793 input_cache_write: 0.001,
1794 };
1795
1796 assert_eq!(pricing1, pricing2);
1797 assert_ne!(pricing1, pricing3);
1798 }
1799
1800 #[test]
1801 fn supported_parameters() {
1802 let params = SupportedParameters {
1803 max_tokens: true,
1804 temperature: true,
1805 top_p: false,
1806 structured_outputs: true,
1807 stop: true,
1808 presence_penalty: true,
1809 ..Default::default()
1810 };
1811
1812 assert!(params.max_tokens);
1813 assert!(params.temperature);
1814 assert!(!params.top_p);
1815 }
1816
1817 #[test]
1818 fn parameters_debug() {
1819 let params = Parameters::default()
1820 .temperature(0.7)
1821 .top_p(0.9)
1822 .top_k(40)
1823 .seed(42)
1824 .max_tokens(1000);
1825
1826 let debug_str = alloc::format!("{params:?}");
1827 assert!(debug_str.contains("0.7"));
1828 assert!(debug_str.contains("42"));
1829 assert!(debug_str.contains("1000"));
1830 }
1831
1832 #[test]
1833 fn parameters_cache_builder_sets_expected_fields() {
1834 let params = Parameters::default()
1835 .prompt_cache_key("project:chat:42")
1836 .prompt_cache_retention(OpenAIPromptCacheRetention::Hours24)
1837 .claude_prompt_cache(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1838 .gemini_cached_content("cachedContents/session-42");
1839
1840 let openai_cache = params
1841 .cache
1842 .openai
1843 .as_ref()
1844 .expect("openai cache should be set");
1845 assert_eq!(openai_cache.key.as_deref(), Some("project:chat:42"));
1846 assert_eq!(
1847 openai_cache.retention,
1848 Some(OpenAIPromptCacheRetention::Hours24)
1849 );
1850 assert_eq!(
1851 params.cache.claude,
1852 Some(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1853 );
1854 assert_eq!(
1855 params
1856 .cache
1857 .gemini
1858 .as_ref()
1859 .map(|cache| cache.cached_content.as_str()),
1860 Some("cachedContents/session-42")
1861 );
1862 }
1863
1864 #[test]
1865 fn cache_options_empty_state_changes_with_provider_values() {
1866 let mut cache = CacheOptions::default();
1867 assert!(cache.is_empty());
1868
1869 cache.openai = Some(OpenAIPromptCache::default());
1870 assert!(!cache.is_empty());
1871 }
1872
1873 #[test]
1874 fn prompt_cache_retention_string_values_match_api() {
1875 assert_eq!(OpenAIPromptCacheRetention::InMemory.as_str(), "in-memory");
1876 assert_eq!(OpenAIPromptCacheRetention::Hours24.as_str(), "24h");
1877 }
1878
1879 #[test]
1880 fn claude_prompt_cache_ttl_string_values_match_api() {
1881 assert_eq!(ClaudePromptCacheTtl::FiveMinutes.as_str(), "5m");
1882 assert_eq!(ClaudePromptCacheTtl::OneHour.as_str(), "1h");
1883 }
1884
1885 #[test]
1886 fn claude_cache_default_strategy_is_automatic() {
1887 let cache = ClaudePromptCache::new(ClaudePromptCacheTtl::FiveMinutes);
1888 assert_eq!(cache.strategy, ClaudePromptCacheStrategy::Automatic);
1889 }
1890
1891 #[test]
1892 fn claude_explicit_breakpoints_default_to_messages_only() {
1893 let breakpoints = ClaudeExplicitCacheBreakpoints::default();
1894 assert_eq!(breakpoints.count(), 1);
1895 assert_eq!(
1896 breakpoints.first.target,
1897 ClaudeCacheBreakpointTarget::LastMessage
1898 );
1899 assert!(breakpoints.second.is_none());
1900 }
1901
1902 #[test]
1903 fn claude_prompt_cache_explicit_builder_preserves_breakpoints() {
1904 let breakpoints = ClaudeExplicitCacheBreakpoints::all();
1905 let params = Parameters::default()
1906 .claude_prompt_cache_explicit(ClaudePromptCacheTtl::OneHour, breakpoints);
1907 let cache = params
1908 .cache
1909 .claude
1910 .expect("claude cache should be set by explicit builder");
1911 assert_eq!(cache.ttl, ClaudePromptCacheTtl::OneHour);
1912 assert_eq!(
1913 cache.strategy,
1914 ClaudePromptCacheStrategy::Explicit(breakpoints)
1915 );
1916 }
1917
1918 #[test]
1919 fn claude_explicit_breakpoint_supports_per_block_ttl_override() {
1920 let breakpoint = ClaudeExplicitCacheBreakpoint::new(ClaudeCacheBreakpointTarget::Tool(0))
1921 .with_ttl(ClaudePromptCacheTtl::OneHour);
1922 assert_eq!(breakpoint.ttl, Some(ClaudePromptCacheTtl::OneHour));
1923 assert_eq!(
1924 breakpoint.effective_ttl(ClaudePromptCacheTtl::FiveMinutes),
1925 ClaudePromptCacheTtl::OneHour
1926 );
1927 }
1928
1929 #[test]
1930 fn claude_prompt_cache_automatic_with_explicit_builder_preserves_breakpoints() {
1931 let breakpoints = ClaudeExplicitCacheBreakpoints::messages_only();
1932 let params = Parameters::default().claude_prompt_cache_automatic_with_explicit(
1933 ClaudePromptCacheTtl::FiveMinutes,
1934 breakpoints,
1935 );
1936 let cache = params
1937 .cache
1938 .claude
1939 .expect("claude cache should be set by combined builder");
1940 assert_eq!(
1941 cache.strategy,
1942 ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints)
1943 );
1944 }
1945}