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}
1612
1613#[cfg(test)]
1614mod tests {
1615 use super::*;
1616
1617 #[test]
1618 fn profile_creation() {
1619 let profile = Profile::new("Test model", "test", "test-model", "A test model", 4096);
1620
1621 assert_eq!(profile.name, "Test model");
1622 assert_eq!(profile.slug, "test-model");
1623 assert_eq!(profile.description, "A test model");
1624 assert_eq!(profile.context_length, 4096);
1625 assert!(
1626 profile.abilities.is_empty(),
1627 "expected no abilities, got {:?}",
1628 profile.abilities
1629 );
1630 assert!(profile.pricing.is_none());
1631 }
1632
1633 #[test]
1634 fn profile_with_single_ability() {
1635 let profile = Profile::new(
1636 "Test vision model",
1637 "test",
1638 "vision-model",
1639 "A vision model",
1640 8192,
1641 )
1642 .with_ability(Ability::Vision);
1643
1644 assert_eq!(profile.abilities.len(), 1);
1645 assert_eq!(profile.abilities[0], Ability::Vision);
1646 }
1647
1648 #[test]
1649 fn profile_with_multiple_abilities() {
1650 let abilities = [Ability::ToolUse, Ability::Vision, Ability::Audio];
1651 let profile = Profile::new(
1652 "Test",
1653 "test",
1654 "multimodal-model",
1655 "A multimodal model",
1656 16384,
1657 )
1658 .with_abilities(abilities);
1659
1660 assert_eq!(profile.abilities.len(), 3);
1661 assert_eq!(profile.abilities, abilities);
1662 }
1663
1664 #[test]
1665 #[allow(clippy::float_cmp)]
1666 fn profile_with_pricing() {
1667 let pricing = Pricing {
1668 prompt: 0.0001,
1669 completion: 0.0002,
1670 request: 0.001,
1671 image: 0.01,
1672 web_search: 0.005,
1673 internal_reasoning: 0.0003,
1674 input_cache_read: 0.00005,
1675 input_cache_write: 0.0001,
1676 };
1677
1678 let profile = Profile::new(
1679 "Test paid model",
1680 "test",
1681 "paid-model",
1682 "A paid model",
1683 2048,
1684 )
1685 .with_pricing(pricing);
1686
1687 assert!(profile.pricing.is_some());
1688 let profile_pricing = profile.pricing.unwrap();
1689 assert_eq!(profile_pricing.prompt, 0.0001);
1690 assert_eq!(profile_pricing.completion, 0.0002);
1691 assert_eq!(profile_pricing.request, 0.001);
1692 assert_eq!(profile_pricing.image, 0.01);
1693 assert_eq!(profile_pricing.web_search, 0.005);
1694 assert_eq!(profile_pricing.internal_reasoning, 0.0003);
1695 assert_eq!(profile_pricing.input_cache_read, 0.00005);
1696 assert_eq!(profile_pricing.input_cache_write, 0.0001);
1697 }
1698
1699 #[test]
1700 fn profile_builder_pattern() {
1701 let pricing = Pricing {
1702 prompt: 0.001,
1703 completion: 0.002,
1704 request: 0.01,
1705 image: 0.1,
1706 web_search: 0.05,
1707 internal_reasoning: 0.003,
1708 input_cache_read: 0.0005,
1709 input_cache_write: 0.001,
1710 };
1711
1712 let profile = Profile::new("Test", "test", "full-model", "A full-featured model", 32768)
1713 .with_ability(Ability::ToolUse)
1714 .with_ability(Ability::Vision)
1715 .with_abilities([Ability::Audio, Ability::WebSearch])
1716 .with_pricing(pricing);
1717
1718 assert_eq!(profile.name, "Test");
1719 assert_eq!(profile.slug, "full-model");
1720 assert_eq!(profile.description, "A full-featured model");
1721 assert_eq!(profile.context_length, 32768);
1722 assert_eq!(profile.abilities.len(), 4);
1723 assert!(profile.abilities.contains(&Ability::ToolUse));
1724 assert!(profile.abilities.contains(&Ability::Vision));
1725 assert!(profile.abilities.contains(&Ability::Audio));
1726 assert!(profile.abilities.contains(&Ability::WebSearch));
1727 assert!(profile.pricing.is_some());
1728 }
1729
1730 #[test]
1731 fn ability_equality() {
1732 assert_eq!(Ability::ToolUse, Ability::ToolUse);
1733 assert_eq!(Ability::Vision, Ability::Vision);
1734 assert_eq!(Ability::Audio, Ability::Audio);
1735 assert_eq!(Ability::WebSearch, Ability::WebSearch);
1736
1737 assert_ne!(Ability::ToolUse, Ability::Vision);
1738 assert_ne!(Ability::Audio, Ability::WebSearch);
1739 }
1740
1741 #[test]
1742 fn ability_debug() {
1743 let ability = Ability::ToolUse;
1744 let debug_str = alloc::format!("{ability:?}");
1745 assert!(debug_str.contains("ToolUse"));
1746 }
1747
1748 #[test]
1749 fn profile_debug() {
1750 let profile = Profile::new("Test model", "test", "debug-model", "A debug model", 1024);
1751 let debug_str = alloc::format!("{profile:?}");
1752 assert!(debug_str.contains("debug-model"));
1753 assert!(debug_str.contains("A debug model"));
1754 assert!(debug_str.contains("1024"));
1755 }
1756
1757 #[test]
1758 fn profile_clone() {
1759 let original = Profile::new("Test model", "test", "original", "Original model", 2048)
1760 .with_ability(Ability::Vision);
1761 let cloned = original.clone();
1762
1763 assert_eq!(original.name, cloned.name);
1764 assert_eq!(original.description, cloned.description);
1765 assert_eq!(original.context_length, cloned.context_length);
1766 assert_eq!(original.abilities, cloned.abilities);
1767 }
1768
1769 #[test]
1770 fn pricing_debug() {
1771 let pricing = Pricing {
1772 prompt: 0.001,
1773 completion: 0.002,
1774 request: 0.01,
1775 image: 0.1,
1776 web_search: 0.05,
1777 internal_reasoning: 0.003,
1778 input_cache_read: 0.0005,
1779 input_cache_write: 0.001,
1780 };
1781
1782 let debug_str = alloc::format!("{pricing:?}");
1783 assert!(debug_str.contains("0.001"));
1784 assert!(debug_str.contains("0.002"));
1785 }
1786
1787 #[test]
1788 #[allow(clippy::float_cmp)]
1789 fn pricing_clone() {
1790 let original = Pricing {
1791 prompt: 0.001,
1792 completion: 0.002,
1793 request: 0.01,
1794 image: 0.1,
1795 web_search: 0.05,
1796 internal_reasoning: 0.003,
1797 input_cache_read: 0.0005,
1798 input_cache_write: 0.001,
1799 };
1800 let cloned = original.clone();
1801
1802 assert_eq!(original.prompt, cloned.prompt);
1803 assert_eq!(original.completion, cloned.completion);
1804 assert_eq!(original.request, cloned.request);
1805 assert_eq!(original.image, cloned.image);
1806 assert_eq!(original.web_search, cloned.web_search);
1807 assert_eq!(original.internal_reasoning, cloned.internal_reasoning);
1808 assert_eq!(original.input_cache_read, cloned.input_cache_read);
1809 assert_eq!(original.input_cache_write, cloned.input_cache_write);
1810 }
1811
1812 #[test]
1813 fn pricing_equality() {
1814 let pricing1 = Pricing {
1815 prompt: 0.001,
1816 completion: 0.002,
1817 request: 0.01,
1818 image: 0.1,
1819 web_search: 0.05,
1820 internal_reasoning: 0.003,
1821 input_cache_read: 0.0005,
1822 input_cache_write: 0.001,
1823 };
1824
1825 let pricing2 = Pricing {
1826 prompt: 0.001,
1827 completion: 0.002,
1828 request: 0.01,
1829 image: 0.1,
1830 web_search: 0.05,
1831 internal_reasoning: 0.003,
1832 input_cache_read: 0.0005,
1833 input_cache_write: 0.001,
1834 };
1835
1836 let pricing3 = Pricing {
1837 prompt: 0.002, completion: 0.002,
1839 request: 0.01,
1840 image: 0.1,
1841 web_search: 0.05,
1842 internal_reasoning: 0.003,
1843 input_cache_read: 0.0005,
1844 input_cache_write: 0.001,
1845 };
1846
1847 assert_eq!(pricing1, pricing2);
1848 assert_ne!(pricing1, pricing3);
1849 }
1850
1851 #[test]
1852 fn supported_parameters() {
1853 let params = SupportedParameters {
1854 max_tokens: true,
1855 temperature: true,
1856 top_p: false,
1857 structured_outputs: true,
1858 stop: true,
1859 presence_penalty: true,
1860 ..Default::default()
1861 };
1862
1863 assert!(params.max_tokens);
1864 assert!(params.temperature);
1865 assert!(!params.top_p);
1866 }
1867
1868 #[test]
1869 fn parameters_debug() {
1870 let params = Parameters::default()
1871 .temperature(0.7)
1872 .top_p(0.9)
1873 .top_k(40)
1874 .seed(42)
1875 .max_tokens(1000);
1876
1877 let debug_str = alloc::format!("{params:?}");
1878 assert!(debug_str.contains("0.7"));
1879 assert!(debug_str.contains("42"));
1880 assert!(debug_str.contains("1000"));
1881 }
1882
1883 #[test]
1884 fn parameters_cache_builder_sets_expected_fields() {
1885 let params = Parameters::default()
1886 .prompt_cache_key("project:chat:42")
1887 .prompt_cache_retention(OpenAIPromptCacheRetention::Hours24)
1888 .claude_prompt_cache(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1889 .gemini_cached_content("cachedContents/session-42");
1890
1891 let openai_cache = params
1892 .cache
1893 .openai
1894 .as_ref()
1895 .expect("openai cache should be set");
1896 assert_eq!(openai_cache.key.as_deref(), Some("project:chat:42"));
1897 assert_eq!(
1898 openai_cache.retention,
1899 Some(OpenAIPromptCacheRetention::Hours24)
1900 );
1901 assert_eq!(
1902 params.cache.claude,
1903 Some(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1904 );
1905 assert_eq!(
1906 params
1907 .cache
1908 .gemini
1909 .as_ref()
1910 .map(|cache| cache.cached_content.as_str()),
1911 Some("cachedContents/session-42")
1912 );
1913 }
1914
1915 #[test]
1916 fn cache_options_empty_state_changes_with_provider_values() {
1917 let mut cache = CacheOptions::default();
1918 assert!(cache.is_empty());
1919
1920 cache.openai = Some(OpenAIPromptCache::default());
1921 assert_ne!(cache, CacheOptions::default());
1922 }
1923
1924 #[test]
1925 fn prompt_cache_retention_string_values_match_api() {
1926 assert_eq!(OpenAIPromptCacheRetention::InMemory.as_str(), "in-memory");
1927 assert_eq!(OpenAIPromptCacheRetention::Hours24.as_str(), "24h");
1928 }
1929
1930 #[test]
1931 fn claude_prompt_cache_ttl_string_values_match_api() {
1932 assert_eq!(ClaudePromptCacheTtl::FiveMinutes.as_str(), "5m");
1933 assert_eq!(ClaudePromptCacheTtl::OneHour.as_str(), "1h");
1934 }
1935
1936 #[test]
1937 fn claude_cache_default_strategy_is_automatic() {
1938 let cache = ClaudePromptCache::new(ClaudePromptCacheTtl::FiveMinutes);
1939 assert_eq!(cache.strategy, ClaudePromptCacheStrategy::Automatic);
1940 }
1941
1942 #[test]
1943 fn claude_explicit_breakpoints_default_to_messages_only() {
1944 let breakpoints = ClaudeExplicitCacheBreakpoints::default();
1945 assert_eq!(breakpoints.count(), 1);
1946 assert_eq!(
1947 breakpoints.first.target,
1948 ClaudeCacheBreakpointTarget::LastMessage
1949 );
1950 assert!(breakpoints.second.is_none());
1951 }
1952
1953 #[test]
1954 fn claude_prompt_cache_explicit_builder_preserves_breakpoints() {
1955 let breakpoints = ClaudeExplicitCacheBreakpoints::all();
1956 let params = Parameters::default()
1957 .claude_prompt_cache_explicit(ClaudePromptCacheTtl::OneHour, breakpoints);
1958 let cache = params
1959 .cache
1960 .claude
1961 .expect("claude cache should be set by explicit builder");
1962 assert_eq!(cache.ttl, ClaudePromptCacheTtl::OneHour);
1963 assert_eq!(
1964 cache.strategy,
1965 ClaudePromptCacheStrategy::Explicit(breakpoints)
1966 );
1967 }
1968
1969 #[test]
1970 fn claude_explicit_breakpoint_supports_per_block_ttl_override() {
1971 let breakpoint = ClaudeExplicitCacheBreakpoint::new(ClaudeCacheBreakpointTarget::Tool(0))
1972 .with_ttl(ClaudePromptCacheTtl::OneHour);
1973 assert_eq!(breakpoint.ttl, Some(ClaudePromptCacheTtl::OneHour));
1974 assert_eq!(
1975 breakpoint.effective_ttl(ClaudePromptCacheTtl::FiveMinutes),
1976 ClaudePromptCacheTtl::OneHour
1977 );
1978 }
1979
1980 #[test]
1981 fn claude_prompt_cache_automatic_with_explicit_builder_preserves_breakpoints() {
1982 let breakpoints = ClaudeExplicitCacheBreakpoints::messages_only();
1983 let params = Parameters::default().claude_prompt_cache_automatic_with_explicit(
1984 ClaudePromptCacheTtl::FiveMinutes,
1985 breakpoints,
1986 );
1987 let cache = params
1988 .cache
1989 .claude
1990 .expect("claude cache should be set by combined builder");
1991 assert_eq!(
1992 cache.strategy,
1993 ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints)
1994 );
1995 }
1996}