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 #[cfg_attr(feature = "serde", serde(default))]
1363 pub default_reasoning_effort: Option<ReasoningEffort>,
1364}
1365
1366#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
1385#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1386#[non_exhaustive]
1387pub struct Pricing {
1388 pub prompt: f64,
1390 pub completion: f64,
1392 pub request: f64,
1394 pub image: f64,
1396 pub web_search: f64,
1398 pub internal_reasoning: f64,
1400 pub input_cache_read: f64,
1402 pub input_cache_write: f64,
1404}
1405
1406#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
1426#[allow(clippy::struct_excessive_bools)]
1427#[non_exhaustive]
1428pub struct SupportedParameters {
1429 pub max_tokens: bool,
1431 pub temperature: bool,
1433 pub top_p: bool,
1435 pub reasoning: bool,
1437 pub include_reasoning: bool,
1439 pub structured_outputs: bool,
1441 pub response_format: bool,
1443 pub stop: bool,
1445 pub frequency_penalty: bool,
1447 pub presence_penalty: bool,
1449 pub seed: bool,
1451}
1452
1453impl Profile {
1454 pub fn new(
1470 name: impl Into<String>,
1471 author: impl Into<String>,
1472 slug: impl Into<String>,
1473 description: impl Into<String>,
1474 context_length: u32,
1475 ) -> Self {
1476 Self {
1477 name: name.into(),
1478 author: author.into(),
1479 slug: slug.into(),
1480 description: description.into(),
1481 abilities: Vec::new(),
1482 context_length,
1483 pricing: None,
1484 default_reasoning_effort: None,
1485 }
1486 }
1487
1488 #[must_use]
1491 pub const fn with_default_reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
1492 self.default_reasoning_effort = Some(effort);
1493 self
1494 }
1495
1496 #[must_use]
1511 pub fn with_ability(self, ability: Ability) -> Self {
1512 self.with_abilities([ability])
1513 }
1514
1515 #[must_use]
1531 pub fn with_abilities(mut self, abilities: impl IntoIterator<Item = Ability>) -> Self {
1532 self.abilities.extend(abilities);
1533 self
1534 }
1535
1536 #[must_use]
1556 pub const fn with_pricing(mut self, pricing: Pricing) -> Self {
1557 self.pricing = Some(pricing);
1558 self
1559 }
1560}
1561
1562#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1578#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1579#[non_exhaustive]
1580pub enum Ability {
1581 ToolUse,
1583 Vision,
1585 Audio,
1587 AudioOutput,
1589 Video,
1591 WebSearch,
1593 Pdf,
1595 CodeExecution,
1597 Reasoning,
1599 ImageGeneration,
1601 ComputerUse,
1603 PromptCaching,
1605 AssistantPrefill,
1607 PerMessageEffort,
1611 MidConversationSystem,
1616}
1617
1618#[cfg(test)]
1619mod tests {
1620 use super::*;
1621
1622 #[test]
1623 fn profile_creation() {
1624 let profile = Profile::new("Test model", "test", "test-model", "A test model", 4096);
1625
1626 assert_eq!(profile.name, "Test model");
1627 assert_eq!(profile.slug, "test-model");
1628 assert_eq!(profile.description, "A test model");
1629 assert_eq!(profile.context_length, 4096);
1630 assert!(
1631 profile.abilities.is_empty(),
1632 "expected no abilities, got {:?}",
1633 profile.abilities
1634 );
1635 assert!(profile.pricing.is_none());
1636 }
1637
1638 #[test]
1639 fn profile_with_single_ability() {
1640 let profile = Profile::new(
1641 "Test vision model",
1642 "test",
1643 "vision-model",
1644 "A vision model",
1645 8192,
1646 )
1647 .with_ability(Ability::Vision);
1648
1649 assert_eq!(profile.abilities.len(), 1);
1650 assert_eq!(profile.abilities[0], Ability::Vision);
1651 }
1652
1653 #[test]
1654 fn profile_with_multiple_abilities() {
1655 let abilities = [Ability::ToolUse, Ability::Vision, Ability::Audio];
1656 let profile = Profile::new(
1657 "Test",
1658 "test",
1659 "multimodal-model",
1660 "A multimodal model",
1661 16384,
1662 )
1663 .with_abilities(abilities);
1664
1665 assert_eq!(profile.abilities.len(), 3);
1666 assert_eq!(profile.abilities, abilities);
1667 }
1668
1669 #[test]
1670 #[allow(clippy::float_cmp)]
1671 fn profile_with_pricing() {
1672 let pricing = Pricing {
1673 prompt: 0.0001,
1674 completion: 0.0002,
1675 request: 0.001,
1676 image: 0.01,
1677 web_search: 0.005,
1678 internal_reasoning: 0.0003,
1679 input_cache_read: 0.00005,
1680 input_cache_write: 0.0001,
1681 };
1682
1683 let profile = Profile::new(
1684 "Test paid model",
1685 "test",
1686 "paid-model",
1687 "A paid model",
1688 2048,
1689 )
1690 .with_pricing(pricing);
1691
1692 assert!(profile.pricing.is_some());
1693 let profile_pricing = profile.pricing.unwrap();
1694 assert_eq!(profile_pricing.prompt, 0.0001);
1695 assert_eq!(profile_pricing.completion, 0.0002);
1696 assert_eq!(profile_pricing.request, 0.001);
1697 assert_eq!(profile_pricing.image, 0.01);
1698 assert_eq!(profile_pricing.web_search, 0.005);
1699 assert_eq!(profile_pricing.internal_reasoning, 0.0003);
1700 assert_eq!(profile_pricing.input_cache_read, 0.00005);
1701 assert_eq!(profile_pricing.input_cache_write, 0.0001);
1702 }
1703
1704 #[test]
1705 fn profile_builder_pattern() {
1706 let pricing = Pricing {
1707 prompt: 0.001,
1708 completion: 0.002,
1709 request: 0.01,
1710 image: 0.1,
1711 web_search: 0.05,
1712 internal_reasoning: 0.003,
1713 input_cache_read: 0.0005,
1714 input_cache_write: 0.001,
1715 };
1716
1717 let profile = Profile::new("Test", "test", "full-model", "A full-featured model", 32768)
1718 .with_ability(Ability::ToolUse)
1719 .with_ability(Ability::Vision)
1720 .with_abilities([Ability::Audio, Ability::WebSearch])
1721 .with_pricing(pricing);
1722
1723 assert_eq!(profile.name, "Test");
1724 assert_eq!(profile.slug, "full-model");
1725 assert_eq!(profile.description, "A full-featured model");
1726 assert_eq!(profile.context_length, 32768);
1727 assert_eq!(profile.abilities.len(), 4);
1728 assert!(profile.abilities.contains(&Ability::ToolUse));
1729 assert!(profile.abilities.contains(&Ability::Vision));
1730 assert!(profile.abilities.contains(&Ability::Audio));
1731 assert!(profile.abilities.contains(&Ability::WebSearch));
1732 assert!(profile.pricing.is_some());
1733 }
1734
1735 #[test]
1736 fn ability_equality() {
1737 assert_eq!(Ability::ToolUse, Ability::ToolUse);
1738 assert_eq!(Ability::Vision, Ability::Vision);
1739 assert_eq!(Ability::Audio, Ability::Audio);
1740 assert_eq!(Ability::WebSearch, Ability::WebSearch);
1741
1742 assert_ne!(Ability::ToolUse, Ability::Vision);
1743 assert_ne!(Ability::Audio, Ability::WebSearch);
1744 }
1745
1746 #[test]
1747 fn ability_debug() {
1748 let ability = Ability::ToolUse;
1749 let debug_str = alloc::format!("{ability:?}");
1750 assert!(debug_str.contains("ToolUse"));
1751 }
1752
1753 #[test]
1754 fn profile_debug() {
1755 let profile = Profile::new("Test model", "test", "debug-model", "A debug model", 1024);
1756 let debug_str = alloc::format!("{profile:?}");
1757 assert!(debug_str.contains("debug-model"));
1758 assert!(debug_str.contains("A debug model"));
1759 assert!(debug_str.contains("1024"));
1760 }
1761
1762 #[test]
1763 fn profile_clone() {
1764 let original = Profile::new("Test model", "test", "original", "Original model", 2048)
1765 .with_ability(Ability::Vision);
1766 let cloned = original.clone();
1767
1768 assert_eq!(original.name, cloned.name);
1769 assert_eq!(original.description, cloned.description);
1770 assert_eq!(original.context_length, cloned.context_length);
1771 assert_eq!(original.abilities, cloned.abilities);
1772 }
1773
1774 #[test]
1775 fn pricing_debug() {
1776 let pricing = Pricing {
1777 prompt: 0.001,
1778 completion: 0.002,
1779 request: 0.01,
1780 image: 0.1,
1781 web_search: 0.05,
1782 internal_reasoning: 0.003,
1783 input_cache_read: 0.0005,
1784 input_cache_write: 0.001,
1785 };
1786
1787 let debug_str = alloc::format!("{pricing:?}");
1788 assert!(debug_str.contains("0.001"));
1789 assert!(debug_str.contains("0.002"));
1790 }
1791
1792 #[test]
1793 #[allow(clippy::float_cmp)]
1794 fn pricing_clone() {
1795 let original = Pricing {
1796 prompt: 0.001,
1797 completion: 0.002,
1798 request: 0.01,
1799 image: 0.1,
1800 web_search: 0.05,
1801 internal_reasoning: 0.003,
1802 input_cache_read: 0.0005,
1803 input_cache_write: 0.001,
1804 };
1805 let cloned = original.clone();
1806
1807 assert_eq!(original.prompt, cloned.prompt);
1808 assert_eq!(original.completion, cloned.completion);
1809 assert_eq!(original.request, cloned.request);
1810 assert_eq!(original.image, cloned.image);
1811 assert_eq!(original.web_search, cloned.web_search);
1812 assert_eq!(original.internal_reasoning, cloned.internal_reasoning);
1813 assert_eq!(original.input_cache_read, cloned.input_cache_read);
1814 assert_eq!(original.input_cache_write, cloned.input_cache_write);
1815 }
1816
1817 #[test]
1818 fn pricing_equality() {
1819 let pricing1 = Pricing {
1820 prompt: 0.001,
1821 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 let pricing2 = Pricing {
1831 prompt: 0.001,
1832 completion: 0.002,
1833 request: 0.01,
1834 image: 0.1,
1835 web_search: 0.05,
1836 internal_reasoning: 0.003,
1837 input_cache_read: 0.0005,
1838 input_cache_write: 0.001,
1839 };
1840
1841 let pricing3 = Pricing {
1842 prompt: 0.002, completion: 0.002,
1844 request: 0.01,
1845 image: 0.1,
1846 web_search: 0.05,
1847 internal_reasoning: 0.003,
1848 input_cache_read: 0.0005,
1849 input_cache_write: 0.001,
1850 };
1851
1852 assert_eq!(pricing1, pricing2);
1853 assert_ne!(pricing1, pricing3);
1854 }
1855
1856 #[test]
1857 fn supported_parameters() {
1858 let params = SupportedParameters {
1859 max_tokens: true,
1860 temperature: true,
1861 top_p: false,
1862 structured_outputs: true,
1863 stop: true,
1864 presence_penalty: true,
1865 ..Default::default()
1866 };
1867
1868 assert!(params.max_tokens);
1869 assert!(params.temperature);
1870 assert!(!params.top_p);
1871 }
1872
1873 #[test]
1874 fn parameters_debug() {
1875 let params = Parameters::default()
1876 .temperature(0.7)
1877 .top_p(0.9)
1878 .top_k(40)
1879 .seed(42)
1880 .max_tokens(1000);
1881
1882 let debug_str = alloc::format!("{params:?}");
1883 assert!(debug_str.contains("0.7"));
1884 assert!(debug_str.contains("42"));
1885 assert!(debug_str.contains("1000"));
1886 }
1887
1888 #[test]
1889 fn parameters_cache_builder_sets_expected_fields() {
1890 let params = Parameters::default()
1891 .prompt_cache_key("project:chat:42")
1892 .prompt_cache_retention(OpenAIPromptCacheRetention::Hours24)
1893 .claude_prompt_cache(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1894 .gemini_cached_content("cachedContents/session-42");
1895
1896 let openai_cache = params
1897 .cache
1898 .openai
1899 .as_ref()
1900 .expect("openai cache should be set");
1901 assert_eq!(openai_cache.key.as_deref(), Some("project:chat:42"));
1902 assert_eq!(
1903 openai_cache.retention,
1904 Some(OpenAIPromptCacheRetention::Hours24)
1905 );
1906 assert_eq!(
1907 params.cache.claude,
1908 Some(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1909 );
1910 assert_eq!(
1911 params
1912 .cache
1913 .gemini
1914 .as_ref()
1915 .map(|cache| cache.cached_content.as_str()),
1916 Some("cachedContents/session-42")
1917 );
1918 }
1919
1920 #[test]
1921 fn cache_options_empty_state_changes_with_provider_values() {
1922 let mut cache = CacheOptions::default();
1923 assert!(cache.is_empty());
1924
1925 cache.openai = Some(OpenAIPromptCache::default());
1926 assert_ne!(cache, CacheOptions::default());
1927 }
1928
1929 #[test]
1930 fn prompt_cache_retention_string_values_match_api() {
1931 assert_eq!(OpenAIPromptCacheRetention::InMemory.as_str(), "in-memory");
1932 assert_eq!(OpenAIPromptCacheRetention::Hours24.as_str(), "24h");
1933 }
1934
1935 #[test]
1936 fn claude_prompt_cache_ttl_string_values_match_api() {
1937 assert_eq!(ClaudePromptCacheTtl::FiveMinutes.as_str(), "5m");
1938 assert_eq!(ClaudePromptCacheTtl::OneHour.as_str(), "1h");
1939 }
1940
1941 #[test]
1942 fn claude_cache_default_strategy_is_automatic() {
1943 let cache = ClaudePromptCache::new(ClaudePromptCacheTtl::FiveMinutes);
1944 assert_eq!(cache.strategy, ClaudePromptCacheStrategy::Automatic);
1945 }
1946
1947 #[test]
1948 fn claude_explicit_breakpoints_default_to_messages_only() {
1949 let breakpoints = ClaudeExplicitCacheBreakpoints::default();
1950 assert_eq!(breakpoints.count(), 1);
1951 assert_eq!(
1952 breakpoints.first.target,
1953 ClaudeCacheBreakpointTarget::LastMessage
1954 );
1955 assert!(breakpoints.second.is_none());
1956 }
1957
1958 #[test]
1959 fn claude_prompt_cache_explicit_builder_preserves_breakpoints() {
1960 let breakpoints = ClaudeExplicitCacheBreakpoints::all();
1961 let params = Parameters::default()
1962 .claude_prompt_cache_explicit(ClaudePromptCacheTtl::OneHour, breakpoints);
1963 let cache = params
1964 .cache
1965 .claude
1966 .expect("claude cache should be set by explicit builder");
1967 assert_eq!(cache.ttl, ClaudePromptCacheTtl::OneHour);
1968 assert_eq!(
1969 cache.strategy,
1970 ClaudePromptCacheStrategy::Explicit(breakpoints)
1971 );
1972 }
1973
1974 #[test]
1975 fn claude_explicit_breakpoint_supports_per_block_ttl_override() {
1976 let breakpoint = ClaudeExplicitCacheBreakpoint::new(ClaudeCacheBreakpointTarget::Tool(0))
1977 .with_ttl(ClaudePromptCacheTtl::OneHour);
1978 assert_eq!(breakpoint.ttl, Some(ClaudePromptCacheTtl::OneHour));
1979 assert_eq!(
1980 breakpoint.effective_ttl(ClaudePromptCacheTtl::FiveMinutes),
1981 ClaudePromptCacheTtl::OneHour
1982 );
1983 }
1984
1985 #[test]
1986 fn claude_prompt_cache_automatic_with_explicit_builder_preserves_breakpoints() {
1987 let breakpoints = ClaudeExplicitCacheBreakpoints::messages_only();
1988 let params = Parameters::default().claude_prompt_cache_automatic_with_explicit(
1989 ClaudePromptCacheTtl::FiveMinutes,
1990 breakpoints,
1991 );
1992 let cache = params
1993 .cache
1994 .claude
1995 .expect("claude cache should be set by combined builder");
1996 assert_eq!(
1997 cache.strategy,
1998 ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints)
1999 );
2000 }
2001}