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 stream: Option<bool>,
147
148 pub reasoning_effort: Option<ReasoningEffort>,
150
151 pub include_reasoning: bool,
153
154 pub structured_outputs: bool,
158
159 pub response_format: Option<Schema>,
163 pub websearch: bool,
165 pub code_execution: bool,
167 #[cfg_attr(
169 feature = "serde",
170 serde(default, skip_serializing_if = "NativeTools::is_empty")
171 )]
172 pub native_tools: NativeTools,
173 #[cfg_attr(
175 feature = "serde",
176 serde(default, skip_serializing_if = "CacheOptions::is_empty")
177 )]
178 pub cache: CacheOptions,
179}
180
181macro_rules! impl_with_methods {
182 (
183 impl $ty:ty {
184 $($field:ident : $field_ty:ty),* $(,)?
185 }
186 ) => {
187 impl $ty {
188 $(
189 #[allow(clippy::missing_const_for_fn)]
195 #[must_use] pub fn $field(mut self, value: $field_ty) -> Self {
196 self.$field = Some(value);
197 self
198 }
199 )*
200 }
201 };
202}
203
204impl_with_methods! {
205 impl Parameters {
206 temperature: f32,
207 top_p: f32,
208 top_k: u32,
209 frequency_penalty: f32,
210 presence_penalty: f32,
211 repetition_penalty: f32,
212 min_p: f32,
213 seed: u32,
214 max_tokens: u32,
215 logit_bias: Vec<(String, f32)>,
216 logprobs: bool,
217 top_logprobs: u8,
218 stop: Vec<String>,
219 parallel_tool_calls: bool,
220 stream: bool,
221 }
222}
223
224impl Parameters {
225 #[must_use]
227 pub const fn include_reasoning(mut self, include: bool) -> Self {
228 self.include_reasoning = include;
229 self
230 }
231
232 #[must_use]
234 pub const fn reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
235 self.reasoning_effort = Some(effort);
236 self
237 }
238
239 #[must_use]
241 pub const fn websearch(mut self, enabled: bool) -> Self {
242 self.websearch = enabled;
243 self
244 }
245
246 #[must_use]
248 pub const fn code_execution(mut self, enabled: bool) -> Self {
249 self.code_execution = enabled;
250 self
251 }
252
253 #[must_use]
255 pub fn native_tools(mut self, tools: NativeTools) -> Self {
256 self.native_tools = tools;
257 self
258 }
259
260 #[must_use]
267 pub const fn hosted_web_search(&self) -> bool {
268 self.websearch
269 || self.native_tools.openai.web_search.is_some()
270 || self.native_tools.gemini.google_search
271 || self.native_tools.claude.web_search
272 }
273
274 #[must_use]
279 pub const fn hosted_web_fetch(&self) -> bool {
280 self.native_tools.claude.web_fetch || self.native_tools.gemini.url_context
281 }
282
283 #[must_use]
285 pub fn openai_tools(mut self, tools: OpenAINativeTools) -> Self {
286 self.native_tools.openai = tools;
287 self
288 }
289
290 #[must_use]
292 pub const fn gemini_tools(mut self, tools: GeminiNativeTools) -> Self {
293 self.native_tools.gemini = tools;
294 self
295 }
296
297 #[must_use]
299 pub const fn claude_tools(mut self, tools: ClaudeNativeTools) -> Self {
300 self.native_tools.claude = tools;
301 self
302 }
303
304 #[must_use]
306 pub fn prompt_cache_key(mut self, key: impl Into<String>) -> Self {
307 let cache = self
308 .cache
309 .openai
310 .get_or_insert_with(OpenAIPromptCache::default);
311 cache.key = Some(key.into());
312 self
313 }
314
315 #[must_use]
317 pub fn prompt_cache_retention(mut self, retention: OpenAIPromptCacheRetention) -> Self {
318 let cache = self
319 .cache
320 .openai
321 .get_or_insert_with(OpenAIPromptCache::default);
322 cache.retention = Some(retention);
323 self
324 }
325
326 #[must_use]
328 pub const fn claude_prompt_cache(mut self, cache: ClaudePromptCache) -> Self {
329 self.cache.claude = Some(cache);
330 self
331 }
332
333 #[must_use]
335 pub const fn claude_prompt_cache_automatic(mut self, ttl: ClaudePromptCacheTtl) -> Self {
336 self.cache.claude = Some(ClaudePromptCache::automatic(ttl));
337 self
338 }
339
340 #[must_use]
342 pub const fn claude_prompt_cache_explicit(
343 mut self,
344 ttl: ClaudePromptCacheTtl,
345 breakpoints: ClaudeExplicitCacheBreakpoints,
346 ) -> Self {
347 self.cache.claude = Some(ClaudePromptCache::explicit(ttl, breakpoints));
348 self
349 }
350
351 #[must_use]
353 pub const fn claude_prompt_cache_automatic_with_explicit(
354 mut self,
355 ttl: ClaudePromptCacheTtl,
356 breakpoints: ClaudeExplicitCacheBreakpoints,
357 ) -> Self {
358 self.cache.claude = Some(ClaudePromptCache::automatic_with_explicit(ttl, breakpoints));
359 self
360 }
361
362 #[must_use]
364 pub fn gemini_cached_content(mut self, cached_content: impl Into<String>) -> Self {
365 self.cache.gemini = Some(GeminiPromptCache::new(cached_content));
366 self
367 }
368
369 #[must_use]
371 pub fn without_cache(mut self) -> Self {
372 self.cache = CacheOptions {
373 openai: None,
374 claude: None,
375 gemini: None,
376 };
377 self
378 }
379
380 #[must_use]
382 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
383 self.tool_choice = choice;
384 self
385 }
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Default)]
390#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391pub struct NativeTools {
392 #[cfg_attr(
394 feature = "serde",
395 serde(default, skip_serializing_if = "OpenAINativeTools::is_empty")
396 )]
397 pub openai: OpenAINativeTools,
398 #[cfg_attr(
400 feature = "serde",
401 serde(default, skip_serializing_if = "GeminiNativeTools::is_empty")
402 )]
403 pub gemini: GeminiNativeTools,
404 #[cfg_attr(
406 feature = "serde",
407 serde(default, skip_serializing_if = "ClaudeNativeTools::is_empty")
408 )]
409 pub claude: ClaudeNativeTools,
410}
411
412impl NativeTools {
413 #[must_use]
415 pub const fn is_empty(&self) -> bool {
416 self.openai.is_empty() && self.gemini.is_empty() && self.claude.is_empty()
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Default)]
422#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
423pub struct OpenAINativeTools {
424 #[cfg_attr(
426 feature = "serde",
427 serde(default, skip_serializing_if = "Option::is_none")
428 )]
429 pub web_search: Option<OpenAIWebSearchTool>,
430 #[cfg_attr(
432 feature = "serde",
433 serde(default, skip_serializing_if = "Vec::is_empty")
434 )]
435 pub file_search: Vec<OpenAIFileSearchTool>,
436 #[cfg_attr(
438 feature = "serde",
439 serde(default, skip_serializing_if = "Option::is_none")
440 )]
441 pub code_interpreter: Option<OpenAICodeInterpreterTool>,
442 #[cfg_attr(
444 feature = "serde",
445 serde(default, skip_serializing_if = "Option::is_none")
446 )]
447 pub image_generation: Option<OpenAIImageGenerationTool>,
448 #[cfg_attr(
450 feature = "serde",
451 serde(default, skip_serializing_if = "Vec::is_empty")
452 )]
453 pub mcp: Vec<OpenAIMcpTool>,
454 #[cfg_attr(
456 feature = "serde",
457 serde(default, skip_serializing_if = "Option::is_none")
458 )]
459 pub computer_use: Option<OpenAIComputerUseTool>,
460}
461
462impl OpenAINativeTools {
463 #[must_use]
465 pub const fn is_empty(&self) -> bool {
466 self.web_search.is_none()
467 && self.file_search.is_empty()
468 && self.code_interpreter.is_none()
469 && self.image_generation.is_none()
470 && self.mcp.is_empty()
471 && self.computer_use.is_none()
472 }
473
474 #[must_use]
476 pub fn with_web_search(mut self, tool: OpenAIWebSearchTool) -> Self {
477 self.web_search = Some(tool);
478 self
479 }
480
481 #[must_use]
483 pub fn enable_web_search(mut self) -> Self {
484 self.web_search = Some(OpenAIWebSearchTool::default());
485 self
486 }
487
488 #[must_use]
490 pub fn with_file_search(mut self, tool: OpenAIFileSearchTool) -> Self {
491 self.file_search.push(tool);
492 self
493 }
494
495 #[must_use]
497 pub fn with_code_interpreter(mut self, tool: OpenAICodeInterpreterTool) -> Self {
498 self.code_interpreter = Some(tool);
499 self
500 }
501
502 #[must_use]
504 pub const fn with_image_generation(mut self, tool: OpenAIImageGenerationTool) -> Self {
505 self.image_generation = Some(tool);
506 self
507 }
508
509 #[must_use]
511 pub fn with_mcp(mut self, tool: OpenAIMcpTool) -> Self {
512 self.mcp.push(tool);
513 self
514 }
515
516 #[must_use]
518 pub fn with_computer_use(mut self, tool: OpenAIComputerUseTool) -> Self {
519 self.computer_use = Some(tool);
520 self
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Default)]
526#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
527pub struct OpenAIWebSearchTool {
528 #[cfg_attr(
530 feature = "serde",
531 serde(default, skip_serializing_if = "Option::is_none")
532 )]
533 pub external_web_access: Option<bool>,
534 #[cfg_attr(
536 feature = "serde",
537 serde(default, skip_serializing_if = "Option::is_none")
538 )]
539 pub filters: Option<Value>,
540 #[cfg_attr(
542 feature = "serde",
543 serde(default, skip_serializing_if = "Option::is_none")
544 )]
545 pub user_location: Option<Value>,
546}
547
548impl OpenAIWebSearchTool {
549 #[must_use]
551 pub const fn external_web_access(mut self, allowed: bool) -> Self {
552 self.external_web_access = Some(allowed);
553 self
554 }
555
556 #[must_use]
558 pub fn filters(mut self, filters: Value) -> Self {
559 self.filters = Some(filters);
560 self
561 }
562
563 #[must_use]
565 pub fn user_location(mut self, location: Value) -> Self {
566 self.user_location = Some(location);
567 self
568 }
569}
570
571#[derive(Debug, Clone, PartialEq, Eq)]
573#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
574pub struct OpenAIFileSearchTool {
575 pub vector_store_ids: Vec<String>,
577 #[cfg_attr(
579 feature = "serde",
580 serde(default, skip_serializing_if = "Option::is_none")
581 )]
582 pub max_num_results: Option<u32>,
583 pub include_results: bool,
585 #[cfg_attr(
587 feature = "serde",
588 serde(default, skip_serializing_if = "Option::is_none")
589 )]
590 pub filters: Option<Value>,
591}
592
593impl OpenAIFileSearchTool {
594 #[must_use]
596 pub fn new(vector_store_ids: impl Into<Vec<String>>) -> Self {
597 Self {
598 vector_store_ids: vector_store_ids.into(),
599 max_num_results: None,
600 include_results: false,
601 filters: None,
602 }
603 }
604
605 #[must_use]
607 pub const fn max_num_results(mut self, value: u32) -> Self {
608 self.max_num_results = Some(value);
609 self
610 }
611
612 #[must_use]
614 pub const fn include_results(mut self, include: bool) -> Self {
615 self.include_results = include;
616 self
617 }
618
619 #[must_use]
621 pub fn filters(mut self, filters: Value) -> Self {
622 self.filters = Some(filters);
623 self
624 }
625}
626
627#[derive(Debug, Clone, PartialEq, Eq)]
629#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
630pub struct OpenAICodeInterpreterTool {
631 pub container: OpenAICodeInterpreterContainer,
633}
634
635impl Default for OpenAICodeInterpreterTool {
636 fn default() -> Self {
637 Self {
638 container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::default()),
639 }
640 }
641}
642
643impl OpenAICodeInterpreterTool {
644 #[must_use]
646 pub const fn auto() -> Self {
647 Self {
648 container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::new()),
649 }
650 }
651
652 #[must_use]
654 pub fn existing(container_id: impl Into<String>) -> Self {
655 Self {
656 container: OpenAICodeInterpreterContainer::Existing(container_id.into()),
657 }
658 }
659}
660
661#[derive(Debug, Clone, PartialEq, Eq)]
663#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
664pub enum OpenAICodeInterpreterContainer {
665 Auto(OpenAIAutoContainer),
667 Existing(String),
669}
670
671#[allow(clippy::struct_excessive_bools)]
673#[derive(Debug, Clone, PartialEq, Eq, Default)]
674#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
675pub struct OpenAIAutoContainer {
676 #[cfg_attr(
678 feature = "serde",
679 serde(default, skip_serializing_if = "Option::is_none")
680 )]
681 pub memory_limit: Option<String>,
682 #[cfg_attr(
684 feature = "serde",
685 serde(default, skip_serializing_if = "Vec::is_empty")
686 )]
687 pub file_ids: Vec<String>,
688}
689
690impl OpenAIAutoContainer {
691 #[must_use]
693 pub const fn new() -> Self {
694 Self {
695 memory_limit: None,
696 file_ids: Vec::new(),
697 }
698 }
699
700 #[must_use]
702 pub fn memory_limit(mut self, memory_limit: impl Into<String>) -> Self {
703 self.memory_limit = Some(memory_limit.into());
704 self
705 }
706
707 #[must_use]
709 pub fn file_ids(mut self, file_ids: impl Into<Vec<String>>) -> Self {
710 self.file_ids = file_ids.into();
711 self
712 }
713}
714
715#[derive(Debug, Clone, PartialEq, Eq, Default)]
717#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
718pub struct OpenAIImageGenerationTool {
719 #[cfg_attr(
721 feature = "serde",
722 serde(default, skip_serializing_if = "Option::is_none")
723 )]
724 pub partial_images: Option<u8>,
725}
726
727impl OpenAIImageGenerationTool {
728 #[must_use]
730 pub const fn partial_images(mut self, count: u8) -> Self {
731 self.partial_images = Some(count);
732 self
733 }
734}
735
736#[derive(Debug, Clone, PartialEq, Eq)]
738#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
739pub struct OpenAIMcpTool {
740 pub server_label: String,
742 pub server_url: String,
744 #[cfg_attr(
746 feature = "serde",
747 serde(default, skip_serializing_if = "Option::is_none")
748 )]
749 pub require_approval: Option<String>,
750 #[cfg_attr(
752 feature = "serde",
753 serde(default, skip_serializing_if = "Vec::is_empty")
754 )]
755 pub allowed_tools: Vec<String>,
756}
757
758impl OpenAIMcpTool {
759 #[must_use]
761 pub fn new(server_label: impl Into<String>, server_url: impl Into<String>) -> Self {
762 Self {
763 server_label: server_label.into(),
764 server_url: server_url.into(),
765 require_approval: None,
766 allowed_tools: Vec::new(),
767 }
768 }
769
770 #[must_use]
772 pub fn require_approval(mut self, policy: impl Into<String>) -> Self {
773 self.require_approval = Some(policy.into());
774 self
775 }
776
777 #[must_use]
779 pub fn allowed_tools(mut self, tools: impl Into<Vec<String>>) -> Self {
780 self.allowed_tools = tools.into();
781 self
782 }
783}
784
785#[derive(Debug, Clone, PartialEq, Eq)]
787#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
788pub struct OpenAIComputerUseTool {
789 pub display_width: u32,
791 pub display_height: u32,
793 pub environment: String,
795}
796
797impl OpenAIComputerUseTool {
798 #[must_use]
800 pub fn new(display_width: u32, display_height: u32, environment: impl Into<String>) -> Self {
801 Self {
802 display_width,
803 display_height,
804 environment: environment.into(),
805 }
806 }
807}
808
809#[derive(Debug, Clone, PartialEq, Eq, Default)]
811#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
812pub struct GeminiNativeTools {
813 pub google_search: bool,
815 pub code_execution: bool,
817 pub url_context: bool,
819}
820
821impl GeminiNativeTools {
822 #[must_use]
824 pub const fn is_empty(&self) -> bool {
825 !self.google_search && !self.code_execution && !self.url_context
826 }
827
828 #[must_use]
830 pub const fn google_search(mut self, enabled: bool) -> Self {
831 self.google_search = enabled;
832 self
833 }
834
835 #[must_use]
837 pub const fn code_execution(mut self, enabled: bool) -> Self {
838 self.code_execution = enabled;
839 self
840 }
841
842 #[must_use]
844 pub const fn url_context(mut self, enabled: bool) -> Self {
845 self.url_context = enabled;
846 self
847 }
848}
849
850#[allow(clippy::struct_excessive_bools)]
852#[derive(Debug, Clone, PartialEq, Eq, Default)]
853#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
854pub struct ClaudeNativeTools {
855 pub web_search: bool,
857 pub web_fetch: bool,
859 pub code_execution: bool,
861 pub bash: bool,
863 #[cfg_attr(
865 feature = "serde",
866 serde(default, skip_serializing_if = "Option::is_none")
867 )]
868 pub text_editor: Option<ClaudeTextEditorTool>,
869}
870
871impl ClaudeNativeTools {
872 #[must_use]
874 pub const fn is_empty(&self) -> bool {
875 !self.web_search
876 && !self.web_fetch
877 && !self.code_execution
878 && !self.bash
879 && self.text_editor.is_none()
880 }
881
882 #[must_use]
884 pub const fn web_search(mut self, enabled: bool) -> Self {
885 self.web_search = enabled;
886 self
887 }
888
889 #[must_use]
891 pub const fn web_fetch(mut self, enabled: bool) -> Self {
892 self.web_fetch = enabled;
893 self
894 }
895
896 #[must_use]
898 pub const fn code_execution(mut self, enabled: bool) -> Self {
899 self.code_execution = enabled;
900 self
901 }
902
903 #[must_use]
905 pub const fn bash(mut self, enabled: bool) -> Self {
906 self.bash = enabled;
907 self
908 }
909
910 #[must_use]
912 pub const fn text_editor(mut self, tool: ClaudeTextEditorTool) -> Self {
913 self.text_editor = Some(tool);
914 self
915 }
916}
917
918#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
920#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
921pub struct ClaudeTextEditorTool {
922 pub max_characters: Option<u32>,
924}
925
926impl ClaudeTextEditorTool {
927 #[must_use]
929 pub const fn max_characters(mut self, value: u32) -> Self {
930 self.max_characters = Some(value);
931 self
932 }
933}
934
935#[derive(Debug, Clone, PartialEq, Eq)]
937#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
938#[serde(rename_all = "lowercase")]
939#[derive(Default)]
940pub enum ToolChoice {
941 #[default]
943 Auto,
944 None,
946 Required,
948 Exact(String),
950}
951
952#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
965#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
966#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
967pub enum ReasoningEffort {
968 None,
973 Minimal,
975 Low,
977 Medium,
979 High,
981 XHigh,
983 Max,
985}
986
987#[derive(Debug, Clone, PartialEq, Eq, Default)]
989#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
990pub struct CacheOptions {
991 #[cfg_attr(
993 feature = "serde",
994 serde(default, skip_serializing_if = "Option::is_none")
995 )]
996 pub openai: Option<OpenAIPromptCache>,
997 #[cfg_attr(
999 feature = "serde",
1000 serde(default, skip_serializing_if = "Option::is_none")
1001 )]
1002 pub claude: Option<ClaudePromptCache>,
1003 #[cfg_attr(
1005 feature = "serde",
1006 serde(default, skip_serializing_if = "Option::is_none")
1007 )]
1008 pub gemini: Option<GeminiPromptCache>,
1009}
1010
1011impl CacheOptions {
1012 #[must_use]
1014 pub const fn is_empty(&self) -> bool {
1015 self.openai.is_none() && self.claude.is_none() && self.gemini.is_none()
1016 }
1017}
1018
1019#[derive(Debug, Clone, PartialEq, Eq, Default)]
1021#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1022pub struct OpenAIPromptCache {
1023 #[cfg_attr(
1025 feature = "serde",
1026 serde(default, skip_serializing_if = "Option::is_none")
1027 )]
1028 pub key: Option<String>,
1029 #[cfg_attr(
1031 feature = "serde",
1032 serde(default, skip_serializing_if = "Option::is_none")
1033 )]
1034 pub retention: Option<OpenAIPromptCacheRetention>,
1035}
1036
1037#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1039#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1040#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
1041pub enum OpenAIPromptCacheRetention {
1042 InMemory,
1044 #[cfg_attr(feature = "serde", serde(rename = "24h"))]
1046 Hours24,
1047}
1048
1049impl OpenAIPromptCacheRetention {
1050 #[must_use]
1052 pub const fn as_str(self) -> &'static str {
1053 match self {
1054 Self::InMemory => "in-memory",
1055 Self::Hours24 => "24h",
1056 }
1057 }
1058}
1059
1060#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1062#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1063pub struct ClaudePromptCache {
1064 pub ttl: ClaudePromptCacheTtl,
1066 pub strategy: ClaudePromptCacheStrategy,
1068}
1069
1070impl ClaudePromptCache {
1071 #[must_use]
1073 pub const fn new(ttl: ClaudePromptCacheTtl) -> Self {
1074 Self {
1075 ttl,
1076 strategy: ClaudePromptCacheStrategy::Automatic,
1077 }
1078 }
1079
1080 #[must_use]
1082 pub const fn automatic(ttl: ClaudePromptCacheTtl) -> Self {
1083 Self::new(ttl)
1084 }
1085
1086 #[must_use]
1088 pub const fn explicit(
1089 ttl: ClaudePromptCacheTtl,
1090 breakpoints: ClaudeExplicitCacheBreakpoints,
1091 ) -> Self {
1092 Self {
1093 ttl,
1094 strategy: ClaudePromptCacheStrategy::Explicit(breakpoints),
1095 }
1096 }
1097
1098 #[must_use]
1100 pub const fn automatic_with_explicit(
1101 ttl: ClaudePromptCacheTtl,
1102 breakpoints: ClaudeExplicitCacheBreakpoints,
1103 ) -> Self {
1104 Self {
1105 ttl,
1106 strategy: ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints),
1107 }
1108 }
1109
1110 #[must_use]
1112 pub const fn with_strategy(mut self, strategy: ClaudePromptCacheStrategy) -> Self {
1113 self.strategy = strategy;
1114 self
1115 }
1116}
1117
1118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1121pub enum ClaudePromptCacheTtl {
1122 #[default]
1124 FiveMinutes,
1125 OneHour,
1127}
1128
1129impl ClaudePromptCacheTtl {
1130 #[must_use]
1132 pub const fn as_str(self) -> &'static str {
1133 match self {
1134 Self::FiveMinutes => "5m",
1135 Self::OneHour => "1h",
1136 }
1137 }
1138}
1139
1140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1143pub enum ClaudePromptCacheStrategy {
1144 #[default]
1146 Automatic,
1147 Explicit(ClaudeExplicitCacheBreakpoints),
1149 AutomaticAndExplicit(ClaudeExplicitCacheBreakpoints),
1151}
1152
1153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1156pub struct ClaudeExplicitCacheBreakpoint {
1157 pub target: ClaudeCacheBreakpointTarget,
1159 pub ttl: Option<ClaudePromptCacheTtl>,
1163}
1164
1165impl ClaudeExplicitCacheBreakpoint {
1166 #[must_use]
1168 pub const fn new(target: ClaudeCacheBreakpointTarget) -> Self {
1169 Self { target, ttl: None }
1170 }
1171
1172 #[must_use]
1174 pub const fn with_ttl(mut self, ttl: ClaudePromptCacheTtl) -> Self {
1175 self.ttl = Some(ttl);
1176 self
1177 }
1178
1179 #[must_use]
1181 pub const fn effective_ttl(self, default_ttl: ClaudePromptCacheTtl) -> ClaudePromptCacheTtl {
1182 match self.ttl {
1183 Some(ttl) => ttl,
1184 None => default_ttl,
1185 }
1186 }
1187}
1188
1189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1191#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1192pub enum ClaudeCacheBreakpointTarget {
1193 LastTool,
1195 Tool(usize),
1197 LastSystem,
1199 System(usize),
1201 LastMessage,
1203 Message {
1205 message_index: usize,
1207 block_index: usize,
1209 },
1210}
1211
1212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1215pub struct ClaudeExplicitCacheBreakpoints {
1216 pub first: ClaudeExplicitCacheBreakpoint,
1218 pub second: Option<ClaudeExplicitCacheBreakpoint>,
1220 pub third: Option<ClaudeExplicitCacheBreakpoint>,
1222 pub fourth: Option<ClaudeExplicitCacheBreakpoint>,
1224}
1225
1226impl ClaudeExplicitCacheBreakpoints {
1227 #[must_use]
1229 pub const fn new(first: ClaudeExplicitCacheBreakpoint) -> Self {
1230 Self {
1231 first,
1232 second: None,
1233 third: None,
1234 fourth: None,
1235 }
1236 }
1237
1238 #[must_use]
1240 pub const fn with_second(mut self, second: ClaudeExplicitCacheBreakpoint) -> Self {
1241 self.second = Some(second);
1242 self
1243 }
1244
1245 #[must_use]
1247 pub const fn with_third(mut self, third: ClaudeExplicitCacheBreakpoint) -> Self {
1248 self.third = Some(third);
1249 self
1250 }
1251
1252 #[must_use]
1254 pub const fn with_fourth(mut self, fourth: ClaudeExplicitCacheBreakpoint) -> Self {
1255 self.fourth = Some(fourth);
1256 self
1257 }
1258
1259 pub fn iter(self) -> impl Iterator<Item = ClaudeExplicitCacheBreakpoint> {
1261 [Some(self.first), self.second, self.third, self.fourth]
1262 .into_iter()
1263 .flatten()
1264 }
1265
1266 #[must_use]
1268 pub const fn count(&self) -> usize {
1269 1 + self.second.is_some() as usize
1270 + self.third.is_some() as usize
1271 + self.fourth.is_some() as usize
1272 }
1273
1274 #[must_use]
1276 pub const fn is_full(&self) -> bool {
1277 self.fourth.is_some()
1278 }
1279
1280 #[must_use]
1282 pub const fn messages_only() -> Self {
1283 Self::new(ClaudeExplicitCacheBreakpoint::new(
1284 ClaudeCacheBreakpointTarget::LastMessage,
1285 ))
1286 }
1287
1288 #[must_use]
1290 pub const fn all() -> Self {
1291 Self::new(ClaudeExplicitCacheBreakpoint::new(
1292 ClaudeCacheBreakpointTarget::LastTool,
1293 ))
1294 .with_second(ClaudeExplicitCacheBreakpoint::new(
1295 ClaudeCacheBreakpointTarget::LastSystem,
1296 ))
1297 .with_third(ClaudeExplicitCacheBreakpoint::new(
1298 ClaudeCacheBreakpointTarget::LastMessage,
1299 ))
1300 }
1301}
1302
1303impl Default for ClaudeExplicitCacheBreakpoints {
1304 fn default() -> Self {
1305 Self::messages_only()
1306 }
1307}
1308
1309#[derive(Debug, Clone, PartialEq, Eq)]
1311#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1312pub struct GeminiPromptCache {
1313 pub cached_content: String,
1315}
1316
1317impl GeminiPromptCache {
1318 #[must_use]
1320 pub fn new(cached_content: impl Into<String>) -> Self {
1321 Self {
1322 cached_content: cached_content.into(),
1323 }
1324 }
1325}
1326
1327#[derive(Debug, Clone, PartialEq, PartialOrd)]
1343#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1344#[non_exhaustive]
1345pub struct Profile {
1346 pub name: String,
1348 pub author: String,
1350 pub slug: String,
1352 pub description: String,
1354 pub abilities: Vec<Ability>,
1356 pub context_length: u32,
1358 pub pricing: Option<Pricing>,
1360}
1361
1362#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
1381#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1382#[non_exhaustive]
1383pub struct Pricing {
1384 pub prompt: f64,
1386 pub completion: f64,
1388 pub request: f64,
1390 pub image: f64,
1392 pub web_search: f64,
1394 pub internal_reasoning: f64,
1396 pub input_cache_read: f64,
1398 pub input_cache_write: f64,
1400}
1401
1402#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
1422#[allow(clippy::struct_excessive_bools)]
1423#[non_exhaustive]
1424pub struct SupportedParameters {
1425 pub max_tokens: bool,
1427 pub temperature: bool,
1429 pub top_p: bool,
1431 pub reasoning: bool,
1433 pub include_reasoning: bool,
1435 pub structured_outputs: bool,
1437 pub response_format: bool,
1439 pub stop: bool,
1441 pub frequency_penalty: bool,
1443 pub presence_penalty: bool,
1445 pub seed: bool,
1447}
1448
1449impl Profile {
1450 pub fn new(
1466 name: impl Into<String>,
1467 author: impl Into<String>,
1468 slug: impl Into<String>,
1469 description: impl Into<String>,
1470 context_length: u32,
1471 ) -> Self {
1472 Self {
1473 name: name.into(),
1474 author: author.into(),
1475 slug: slug.into(),
1476 description: description.into(),
1477 abilities: Vec::new(),
1478 context_length,
1479 pricing: None,
1480 }
1481 }
1482
1483 #[must_use]
1498 pub fn with_ability(self, ability: Ability) -> Self {
1499 self.with_abilities([ability])
1500 }
1501
1502 #[must_use]
1518 pub fn with_abilities(mut self, abilities: impl IntoIterator<Item = Ability>) -> Self {
1519 self.abilities.extend(abilities);
1520 self
1521 }
1522
1523 #[must_use]
1543 pub const fn with_pricing(mut self, pricing: Pricing) -> Self {
1544 self.pricing = Some(pricing);
1545 self
1546 }
1547}
1548
1549#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1565#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1566#[non_exhaustive]
1567pub enum Ability {
1568 ToolUse,
1570 Vision,
1572 Audio,
1574 AudioOutput,
1576 Video,
1578 WebSearch,
1580 Pdf,
1582 CodeExecution,
1584 Reasoning,
1586 ImageGeneration,
1588 ComputerUse,
1590 PromptCaching,
1592 AssistantPrefill,
1594}
1595
1596#[cfg(test)]
1597mod tests {
1598 use super::*;
1599
1600 #[test]
1601 fn profile_creation() {
1602 let profile = Profile::new("Test model", "test", "test-model", "A test model", 4096);
1603
1604 assert_eq!(profile.name, "Test model");
1605 assert_eq!(profile.slug, "test-model");
1606 assert_eq!(profile.description, "A test model");
1607 assert_eq!(profile.context_length, 4096);
1608 assert!(
1609 profile.abilities.is_empty(),
1610 "expected no abilities, got {:?}",
1611 profile.abilities
1612 );
1613 assert!(profile.pricing.is_none());
1614 }
1615
1616 #[test]
1617 fn profile_with_single_ability() {
1618 let profile = Profile::new(
1619 "Test vision model",
1620 "test",
1621 "vision-model",
1622 "A vision model",
1623 8192,
1624 )
1625 .with_ability(Ability::Vision);
1626
1627 assert_eq!(profile.abilities.len(), 1);
1628 assert_eq!(profile.abilities[0], Ability::Vision);
1629 }
1630
1631 #[test]
1632 fn profile_with_multiple_abilities() {
1633 let abilities = [Ability::ToolUse, Ability::Vision, Ability::Audio];
1634 let profile = Profile::new(
1635 "Test",
1636 "test",
1637 "multimodal-model",
1638 "A multimodal model",
1639 16384,
1640 )
1641 .with_abilities(abilities);
1642
1643 assert_eq!(profile.abilities.len(), 3);
1644 assert_eq!(profile.abilities, abilities);
1645 }
1646
1647 #[test]
1648 #[allow(clippy::float_cmp)]
1649 fn profile_with_pricing() {
1650 let pricing = Pricing {
1651 prompt: 0.0001,
1652 completion: 0.0002,
1653 request: 0.001,
1654 image: 0.01,
1655 web_search: 0.005,
1656 internal_reasoning: 0.0003,
1657 input_cache_read: 0.00005,
1658 input_cache_write: 0.0001,
1659 };
1660
1661 let profile = Profile::new(
1662 "Test paid model",
1663 "test",
1664 "paid-model",
1665 "A paid model",
1666 2048,
1667 )
1668 .with_pricing(pricing);
1669
1670 assert!(profile.pricing.is_some());
1671 let profile_pricing = profile.pricing.unwrap();
1672 assert_eq!(profile_pricing.prompt, 0.0001);
1673 assert_eq!(profile_pricing.completion, 0.0002);
1674 assert_eq!(profile_pricing.request, 0.001);
1675 assert_eq!(profile_pricing.image, 0.01);
1676 assert_eq!(profile_pricing.web_search, 0.005);
1677 assert_eq!(profile_pricing.internal_reasoning, 0.0003);
1678 assert_eq!(profile_pricing.input_cache_read, 0.00005);
1679 assert_eq!(profile_pricing.input_cache_write, 0.0001);
1680 }
1681
1682 #[test]
1683 fn profile_builder_pattern() {
1684 let pricing = Pricing {
1685 prompt: 0.001,
1686 completion: 0.002,
1687 request: 0.01,
1688 image: 0.1,
1689 web_search: 0.05,
1690 internal_reasoning: 0.003,
1691 input_cache_read: 0.0005,
1692 input_cache_write: 0.001,
1693 };
1694
1695 let profile = Profile::new("Test", "test", "full-model", "A full-featured model", 32768)
1696 .with_ability(Ability::ToolUse)
1697 .with_ability(Ability::Vision)
1698 .with_abilities([Ability::Audio, Ability::WebSearch])
1699 .with_pricing(pricing);
1700
1701 assert_eq!(profile.name, "Test");
1702 assert_eq!(profile.slug, "full-model");
1703 assert_eq!(profile.description, "A full-featured model");
1704 assert_eq!(profile.context_length, 32768);
1705 assert_eq!(profile.abilities.len(), 4);
1706 assert!(profile.abilities.contains(&Ability::ToolUse));
1707 assert!(profile.abilities.contains(&Ability::Vision));
1708 assert!(profile.abilities.contains(&Ability::Audio));
1709 assert!(profile.abilities.contains(&Ability::WebSearch));
1710 assert!(profile.pricing.is_some());
1711 }
1712
1713 #[test]
1714 fn ability_equality() {
1715 assert_eq!(Ability::ToolUse, Ability::ToolUse);
1716 assert_eq!(Ability::Vision, Ability::Vision);
1717 assert_eq!(Ability::Audio, Ability::Audio);
1718 assert_eq!(Ability::WebSearch, Ability::WebSearch);
1719
1720 assert_ne!(Ability::ToolUse, Ability::Vision);
1721 assert_ne!(Ability::Audio, Ability::WebSearch);
1722 }
1723
1724 #[test]
1725 fn ability_debug() {
1726 let ability = Ability::ToolUse;
1727 let debug_str = alloc::format!("{ability:?}");
1728 assert!(debug_str.contains("ToolUse"));
1729 }
1730
1731 #[test]
1732 fn profile_debug() {
1733 let profile = Profile::new("Test model", "test", "debug-model", "A debug model", 1024);
1734 let debug_str = alloc::format!("{profile:?}");
1735 assert!(debug_str.contains("debug-model"));
1736 assert!(debug_str.contains("A debug model"));
1737 assert!(debug_str.contains("1024"));
1738 }
1739
1740 #[test]
1741 fn profile_clone() {
1742 let original = Profile::new("Test model", "test", "original", "Original model", 2048)
1743 .with_ability(Ability::Vision);
1744 let cloned = original.clone();
1745
1746 assert_eq!(original.name, cloned.name);
1747 assert_eq!(original.description, cloned.description);
1748 assert_eq!(original.context_length, cloned.context_length);
1749 assert_eq!(original.abilities, cloned.abilities);
1750 }
1751
1752 #[test]
1753 fn pricing_debug() {
1754 let pricing = Pricing {
1755 prompt: 0.001,
1756 completion: 0.002,
1757 request: 0.01,
1758 image: 0.1,
1759 web_search: 0.05,
1760 internal_reasoning: 0.003,
1761 input_cache_read: 0.0005,
1762 input_cache_write: 0.001,
1763 };
1764
1765 let debug_str = alloc::format!("{pricing:?}");
1766 assert!(debug_str.contains("0.001"));
1767 assert!(debug_str.contains("0.002"));
1768 }
1769
1770 #[test]
1771 #[allow(clippy::float_cmp)]
1772 fn pricing_clone() {
1773 let original = Pricing {
1774 prompt: 0.001,
1775 completion: 0.002,
1776 request: 0.01,
1777 image: 0.1,
1778 web_search: 0.05,
1779 internal_reasoning: 0.003,
1780 input_cache_read: 0.0005,
1781 input_cache_write: 0.001,
1782 };
1783 let cloned = original.clone();
1784
1785 assert_eq!(original.prompt, cloned.prompt);
1786 assert_eq!(original.completion, cloned.completion);
1787 assert_eq!(original.request, cloned.request);
1788 assert_eq!(original.image, cloned.image);
1789 assert_eq!(original.web_search, cloned.web_search);
1790 assert_eq!(original.internal_reasoning, cloned.internal_reasoning);
1791 assert_eq!(original.input_cache_read, cloned.input_cache_read);
1792 assert_eq!(original.input_cache_write, cloned.input_cache_write);
1793 }
1794
1795 #[test]
1796 fn pricing_equality() {
1797 let pricing1 = 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 pricing2 = Pricing {
1809 prompt: 0.001,
1810 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 let pricing3 = Pricing {
1820 prompt: 0.002, completion: 0.002,
1822 request: 0.01,
1823 image: 0.1,
1824 web_search: 0.05,
1825 internal_reasoning: 0.003,
1826 input_cache_read: 0.0005,
1827 input_cache_write: 0.001,
1828 };
1829
1830 assert_eq!(pricing1, pricing2);
1831 assert_ne!(pricing1, pricing3);
1832 }
1833
1834 #[test]
1835 fn supported_parameters() {
1836 let params = SupportedParameters {
1837 max_tokens: true,
1838 temperature: true,
1839 top_p: false,
1840 structured_outputs: true,
1841 stop: true,
1842 presence_penalty: true,
1843 ..Default::default()
1844 };
1845
1846 assert!(params.max_tokens);
1847 assert!(params.temperature);
1848 assert!(!params.top_p);
1849 }
1850
1851 #[test]
1852 fn parameters_debug() {
1853 let params = Parameters::default()
1854 .temperature(0.7)
1855 .top_p(0.9)
1856 .top_k(40)
1857 .seed(42)
1858 .max_tokens(1000);
1859
1860 let debug_str = alloc::format!("{params:?}");
1861 assert!(debug_str.contains("0.7"));
1862 assert!(debug_str.contains("42"));
1863 assert!(debug_str.contains("1000"));
1864 }
1865
1866 #[test]
1867 fn parameters_cache_builder_sets_expected_fields() {
1868 let params = Parameters::default()
1869 .prompt_cache_key("project:chat:42")
1870 .prompt_cache_retention(OpenAIPromptCacheRetention::Hours24)
1871 .claude_prompt_cache(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1872 .gemini_cached_content("cachedContents/session-42");
1873
1874 let openai_cache = params
1875 .cache
1876 .openai
1877 .as_ref()
1878 .expect("openai cache should be set");
1879 assert_eq!(openai_cache.key.as_deref(), Some("project:chat:42"));
1880 assert_eq!(
1881 openai_cache.retention,
1882 Some(OpenAIPromptCacheRetention::Hours24)
1883 );
1884 assert_eq!(
1885 params.cache.claude,
1886 Some(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1887 );
1888 assert_eq!(
1889 params
1890 .cache
1891 .gemini
1892 .as_ref()
1893 .map(|cache| cache.cached_content.as_str()),
1894 Some("cachedContents/session-42")
1895 );
1896 }
1897
1898 #[test]
1899 fn cache_options_empty_state_changes_with_provider_values() {
1900 let mut cache = CacheOptions::default();
1901 assert!(cache.is_empty());
1902
1903 cache.openai = Some(OpenAIPromptCache::default());
1904 assert_ne!(cache, CacheOptions::default());
1905 }
1906
1907 #[test]
1908 fn prompt_cache_retention_string_values_match_api() {
1909 assert_eq!(OpenAIPromptCacheRetention::InMemory.as_str(), "in-memory");
1910 assert_eq!(OpenAIPromptCacheRetention::Hours24.as_str(), "24h");
1911 }
1912
1913 #[test]
1914 fn claude_prompt_cache_ttl_string_values_match_api() {
1915 assert_eq!(ClaudePromptCacheTtl::FiveMinutes.as_str(), "5m");
1916 assert_eq!(ClaudePromptCacheTtl::OneHour.as_str(), "1h");
1917 }
1918
1919 #[test]
1920 fn claude_cache_default_strategy_is_automatic() {
1921 let cache = ClaudePromptCache::new(ClaudePromptCacheTtl::FiveMinutes);
1922 assert_eq!(cache.strategy, ClaudePromptCacheStrategy::Automatic);
1923 }
1924
1925 #[test]
1926 fn claude_explicit_breakpoints_default_to_messages_only() {
1927 let breakpoints = ClaudeExplicitCacheBreakpoints::default();
1928 assert_eq!(breakpoints.count(), 1);
1929 assert_eq!(
1930 breakpoints.first.target,
1931 ClaudeCacheBreakpointTarget::LastMessage
1932 );
1933 assert!(breakpoints.second.is_none());
1934 }
1935
1936 #[test]
1937 fn claude_prompt_cache_explicit_builder_preserves_breakpoints() {
1938 let breakpoints = ClaudeExplicitCacheBreakpoints::all();
1939 let params = Parameters::default()
1940 .claude_prompt_cache_explicit(ClaudePromptCacheTtl::OneHour, breakpoints);
1941 let cache = params
1942 .cache
1943 .claude
1944 .expect("claude cache should be set by explicit builder");
1945 assert_eq!(cache.ttl, ClaudePromptCacheTtl::OneHour);
1946 assert_eq!(
1947 cache.strategy,
1948 ClaudePromptCacheStrategy::Explicit(breakpoints)
1949 );
1950 }
1951
1952 #[test]
1953 fn claude_explicit_breakpoint_supports_per_block_ttl_override() {
1954 let breakpoint = ClaudeExplicitCacheBreakpoint::new(ClaudeCacheBreakpointTarget::Tool(0))
1955 .with_ttl(ClaudePromptCacheTtl::OneHour);
1956 assert_eq!(breakpoint.ttl, Some(ClaudePromptCacheTtl::OneHour));
1957 assert_eq!(
1958 breakpoint.effective_ttl(ClaudePromptCacheTtl::FiveMinutes),
1959 ClaudePromptCacheTtl::OneHour
1960 );
1961 }
1962
1963 #[test]
1964 fn claude_prompt_cache_automatic_with_explicit_builder_preserves_breakpoints() {
1965 let breakpoints = ClaudeExplicitCacheBreakpoints::messages_only();
1966 let params = Parameters::default().claude_prompt_cache_automatic_with_explicit(
1967 ClaudePromptCacheTtl::FiveMinutes,
1968 breakpoints,
1969 );
1970 let cache = params
1971 .cache
1972 .claude
1973 .expect("claude cache should be set by combined builder");
1974 assert_eq!(
1975 cache.strategy,
1976 ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints)
1977 );
1978 }
1979}