1use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone)]
15pub enum ThinkingMode {
16 Enabled { budget_tokens: u32 },
18 Adaptive,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum Effort {
26 Low,
27 Medium,
28 High,
29 Max,
30}
31
32#[derive(Debug, Clone)]
37pub struct ThinkingConfig {
38 pub mode: ThinkingMode,
40 pub effort: Option<Effort>,
42}
43
44impl ThinkingConfig {
45 pub const DEFAULT_BUDGET_TOKENS: u32 = 10_000;
50
51 pub const MIN_BUDGET_TOKENS: u32 = 1_024;
53
54 #[must_use]
56 pub const fn new(budget_tokens: u32) -> Self {
57 Self {
58 mode: ThinkingMode::Enabled { budget_tokens },
59 effort: None,
60 }
61 }
62
63 #[must_use]
65 pub const fn adaptive() -> Self {
66 Self {
67 mode: ThinkingMode::Adaptive,
68 effort: None,
69 }
70 }
71
72 #[must_use]
74 pub const fn adaptive_with_effort(effort: Effort) -> Self {
75 Self {
76 mode: ThinkingMode::Adaptive,
77 effort: Some(effort),
78 }
79 }
80
81 #[must_use]
83 pub const fn with_effort(mut self, effort: Effort) -> Self {
84 self.effort = Some(effort);
85 self
86 }
87}
88
89impl Default for ThinkingConfig {
90 fn default() -> Self {
91 Self::new(Self::DEFAULT_BUDGET_TOKENS)
92 }
93}
94
95#[derive(Debug, Clone)]
99pub enum ToolChoice {
100 Auto,
102 Tool(String),
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct ResponseFormat {
121 pub name: String,
124 pub schema: serde_json::Value,
130 pub strict: bool,
134}
135
136impl ResponseFormat {
137 #[must_use]
142 pub fn new(name: impl Into<String>, schema: serde_json::Value) -> Self {
143 Self {
144 name: name.into(),
145 schema,
146 strict: true,
147 }
148 }
149
150 #[must_use]
152 pub const fn with_strict(mut self, strict: bool) -> Self {
153 self.strict = strict;
154 self
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum CacheTtl {
165 FiveMinutes,
167 OneHour,
169}
170
171impl CacheTtl {
172 #[must_use]
174 pub const fn as_wire_str(self) -> &'static str {
175 match self {
176 Self::FiveMinutes => "5m",
177 Self::OneHour => "1h",
178 }
179 }
180}
181
182#[derive(Debug, Clone)]
196pub struct CacheConfig {
197 pub enabled: bool,
199 pub ttl: Option<CacheTtl>,
201 pub max_breakpoints: Option<u8>,
203}
204
205impl Default for CacheConfig {
206 fn default() -> Self {
207 Self::enabled()
208 }
209}
210
211impl CacheConfig {
212 #[must_use]
215 pub const fn enabled() -> Self {
216 Self {
217 enabled: true,
218 ttl: None,
219 max_breakpoints: None,
220 }
221 }
222
223 #[must_use]
225 pub const fn disabled() -> Self {
226 Self {
227 enabled: false,
228 ttl: None,
229 max_breakpoints: None,
230 }
231 }
232
233 #[must_use]
235 pub const fn with_ttl(mut self, ttl: CacheTtl) -> Self {
236 self.ttl = Some(ttl);
237 self
238 }
239
240 #[must_use]
242 pub const fn with_max_breakpoints(mut self, max_breakpoints: u8) -> Self {
243 self.max_breakpoints = Some(max_breakpoints);
244 self
245 }
246}
247
248#[derive(Debug, Clone)]
249pub struct ChatRequest {
250 pub system: String,
251 pub messages: Vec<Message>,
252 pub tools: Option<Vec<Tool>>,
253 pub max_tokens: u32,
254 pub max_tokens_explicit: bool,
256 pub session_id: Option<String>,
258 pub cached_content: Option<String>,
262 pub thinking: Option<ThinkingConfig>,
264 pub tool_choice: Option<ToolChoice>,
268 pub response_format: Option<ResponseFormat>,
276 pub cache: Option<CacheConfig>,
282}
283
284impl ChatRequest {
285 pub const DEFAULT_MAX_TOKENS: u32 = 4096;
288
289 #[must_use]
305 pub fn new(system: impl Into<String>, messages: Vec<Message>) -> Self {
306 Self {
307 system: system.into(),
308 messages,
309 tools: None,
310 max_tokens: Self::DEFAULT_MAX_TOKENS,
311 max_tokens_explicit: false,
312 session_id: None,
313 cached_content: None,
314 thinking: None,
315 tool_choice: None,
316 response_format: None,
317 cache: None,
318 }
319 }
320
321 #[must_use]
323 pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
324 self.tools = Some(tools);
325 self
326 }
327
328 #[must_use]
330 pub const fn with_max_tokens(mut self, max_tokens: u32) -> Self {
331 self.max_tokens = max_tokens;
332 self.max_tokens_explicit = true;
333 self
334 }
335
336 #[must_use]
338 pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
339 self.session_id = Some(session_id.into());
340 self
341 }
342
343 #[must_use]
345 pub const fn with_thinking(mut self, thinking: ThinkingConfig) -> Self {
346 self.thinking = Some(thinking);
347 self
348 }
349
350 #[must_use]
352 pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
353 self.tool_choice = Some(tool_choice);
354 self
355 }
356
357 #[must_use]
360 pub fn with_response_format(mut self, response_format: ResponseFormat) -> Self {
361 self.response_format = Some(response_format);
362 self
363 }
364
365 #[must_use]
367 pub const fn with_cache(mut self, cache: CacheConfig) -> Self {
368 self.cache = Some(cache);
369 self
370 }
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct Message {
375 pub role: Role,
376 pub content: Content,
377}
378
379impl Message {
380 #[must_use]
381 pub fn user(text: impl Into<String>) -> Self {
382 Self {
383 role: Role::User,
384 content: Content::Text(text.into()),
385 }
386 }
387
388 #[must_use]
389 pub const fn user_with_content(blocks: Vec<ContentBlock>) -> Self {
390 Self {
391 role: Role::User,
392 content: Content::Blocks(blocks),
393 }
394 }
395
396 #[must_use]
397 pub fn assistant(text: impl Into<String>) -> Self {
398 Self {
399 role: Role::Assistant,
400 content: Content::Text(text.into()),
401 }
402 }
403
404 #[must_use]
405 pub const fn assistant_with_content(blocks: Vec<ContentBlock>) -> Self {
406 Self {
407 role: Role::Assistant,
408 content: Content::Blocks(blocks),
409 }
410 }
411
412 #[must_use]
413 pub fn assistant_with_tool_use(
414 text: Option<String>,
415 id: impl Into<String>,
416 name: impl Into<String>,
417 input: serde_json::Value,
418 ) -> Self {
419 let mut blocks = Vec::new();
420 if let Some(t) = text {
421 blocks.push(ContentBlock::Text { text: t });
422 }
423 blocks.push(ContentBlock::ToolUse {
424 id: id.into(),
425 name: name.into(),
426 input,
427 thought_signature: None,
428 });
429 Self {
430 role: Role::Assistant,
431 content: Content::Blocks(blocks),
432 }
433 }
434
435 #[must_use]
436 pub fn tool_result(
437 tool_use_id: impl Into<String>,
438 content: impl Into<String>,
439 is_error: bool,
440 ) -> Self {
441 Self {
442 role: Role::User,
443 content: Content::Blocks(vec![ContentBlock::ToolResult {
444 tool_use_id: tool_use_id.into(),
445 content: content.into(),
446 is_error: if is_error { Some(true) } else { None },
447 }]),
448 }
449 }
450}
451
452#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
453#[serde(rename_all = "lowercase")]
454pub enum Role {
455 User,
456 Assistant,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
460#[serde(untagged)]
461pub enum Content {
462 Text(String),
463 Blocks(Vec<ContentBlock>),
464}
465
466impl Content {
467 #[must_use]
468 pub fn first_text(&self) -> Option<&str> {
469 match self {
470 Self::Text(s) => Some(s),
471 Self::Blocks(blocks) => blocks.iter().find_map(|b| match b {
472 ContentBlock::Text { text } => Some(text.as_str()),
473 _ => None,
474 }),
475 }
476 }
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct ContentSource {
482 pub media_type: String,
483 pub data: String,
484}
485
486impl ContentSource {
487 #[must_use]
488 pub fn new(media_type: impl Into<String>, data: impl Into<String>) -> Self {
489 Self {
490 media_type: media_type.into(),
491 data: data.into(),
492 }
493 }
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
497#[serde(tag = "type")]
498#[non_exhaustive]
499pub enum ContentBlock {
500 #[serde(rename = "text")]
501 Text { text: String },
502
503 #[serde(rename = "thinking")]
504 Thinking {
505 thinking: String,
506 #[serde(skip_serializing_if = "Option::is_none")]
508 signature: Option<String>,
509 },
510
511 #[serde(rename = "redacted_thinking")]
512 RedactedThinking { data: String },
513
514 #[serde(rename = "tool_use")]
515 ToolUse {
516 id: String,
517 name: String,
518 input: serde_json::Value,
519 #[serde(skip_serializing_if = "Option::is_none")]
522 thought_signature: Option<String>,
523 },
524
525 #[serde(rename = "tool_result")]
526 ToolResult {
527 tool_use_id: String,
528 content: String,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 is_error: Option<bool>,
531 },
532
533 #[serde(rename = "image")]
534 Image { source: ContentSource },
535
536 #[serde(rename = "document")]
537 Document { source: ContentSource },
538}
539
540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
541pub struct Tool {
542 pub name: String,
543 pub description: String,
544 pub input_schema: serde_json::Value,
545 pub display_name: String,
547 pub tier: super::types::ToolTier,
549}
550
551#[derive(Debug, Clone)]
552pub struct ChatResponse {
553 pub id: String,
554 pub content: Vec<ContentBlock>,
555 pub model: String,
556 pub stop_reason: Option<StopReason>,
557 pub usage: Usage,
558}
559
560impl ChatResponse {
561 #[must_use]
562 pub fn first_text(&self) -> Option<&str> {
563 self.content.iter().find_map(|b| match b {
564 ContentBlock::Text { text } => Some(text.as_str()),
565 _ => None,
566 })
567 }
568
569 #[must_use]
570 pub fn first_thinking(&self) -> Option<&str> {
571 self.content.iter().find_map(|b| match b {
572 ContentBlock::Thinking { thinking, .. } => Some(thinking.as_str()),
573 _ => None,
574 })
575 }
576
577 pub fn tool_uses(&self) -> impl Iterator<Item = (&str, &str, &serde_json::Value)> {
578 self.content.iter().filter_map(|b| match b {
579 ContentBlock::ToolUse {
580 id, name, input, ..
581 } => Some((id.as_str(), name.as_str(), input)),
582 _ => None,
583 })
584 }
585
586 #[must_use]
587 pub fn has_tool_use(&self) -> bool {
588 self.content
589 .iter()
590 .any(|b| matches!(b, ContentBlock::ToolUse { .. }))
591 }
592}
593
594#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
595#[serde(rename_all = "snake_case")]
596#[non_exhaustive]
597pub enum StopReason {
598 EndTurn,
599 ToolUse,
600 MaxTokens,
601 StopSequence,
602 Refusal,
603 ModelContextWindowExceeded,
604 #[serde(other)]
613 Unknown,
614}
615
616impl StopReason {
617 #[must_use]
620 pub const fn as_str(&self) -> &'static str {
621 match self {
622 Self::EndTurn => "end_turn",
623 Self::ToolUse => "tool_use",
624 Self::MaxTokens => "max_tokens",
625 Self::StopSequence => "stop_sequence",
626 Self::Refusal => "refusal",
627 Self::ModelContextWindowExceeded => "model_context_window_exceeded",
628 Self::Unknown => "unknown",
629 }
630 }
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct Usage {
635 pub input_tokens: u32,
637 pub output_tokens: u32,
638 #[serde(default)]
640 pub cached_input_tokens: u32,
641 #[serde(default)]
643 pub cache_creation_input_tokens: u32,
644}
645
646#[derive(Debug, Clone)]
647#[non_exhaustive]
648pub enum ChatOutcome {
649 Success(ChatResponse),
650 RateLimited(Option<Duration>),
657 InvalidRequest(String),
658 ServerError(String),
659}
660
661#[must_use]
671pub fn parse_retry_after(value: &str) -> Option<Duration> {
672 let trimmed = value.trim();
673 if trimmed.is_empty() {
674 return None;
675 }
676
677 if let Ok(seconds) = trimmed.parse::<u64>() {
679 return Some(Duration::from_secs(seconds));
680 }
681
682 let target = parse_imf_fixdate(trimmed)?;
684 let now = time::OffsetDateTime::now_utc();
685 if target <= now {
686 return None;
687 }
688 (target - now).try_into().ok()
689}
690
691fn parse_imf_fixdate(value: &str) -> Option<time::OffsetDateTime> {
693 let format = time::format_description::parse_borrowed::<1>(
696 "[weekday repr:short], [day] [month repr:short] [year] \
697 [hour]:[minute]:[second] GMT",
698 )
699 .ok()?;
700 time::PrimitiveDateTime::parse(value, &format)
701 .ok()
702 .map(time::PrimitiveDateTime::assume_utc)
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
710 fn chat_request_new_defaults_then_setters() {
711 let req = ChatRequest::new("sys", vec![Message::user("hi")]);
712 assert_eq!(req.system, "sys");
713 assert_eq!(req.messages.len(), 1);
714 assert_eq!(req.max_tokens, ChatRequest::DEFAULT_MAX_TOKENS);
715 assert!(!req.max_tokens_explicit);
716 assert!(req.tools.is_none());
717 assert!(req.tool_choice.is_none());
718 assert!(req.response_format.is_none());
719
720 let req = req
721 .with_max_tokens(1234)
722 .with_tool_choice(ToolChoice::Auto)
723 .with_response_format(ResponseFormat::new(
724 "r",
725 serde_json::json!({"type": "object"}),
726 ))
727 .with_session_id("s-1");
728 assert_eq!(req.max_tokens, 1234);
729 assert!(req.max_tokens_explicit);
730 assert!(matches!(req.tool_choice, Some(ToolChoice::Auto)));
731 assert!(req.response_format.is_some());
732 assert_eq!(req.session_id.as_deref(), Some("s-1"));
733 }
734
735 #[test]
736 fn stop_reason_known_values_round_trip() -> Result<(), serde_json::Error> {
737 for (json, expected) in [
738 ("\"end_turn\"", StopReason::EndTurn),
739 ("\"tool_use\"", StopReason::ToolUse),
740 ("\"max_tokens\"", StopReason::MaxTokens),
741 ("\"stop_sequence\"", StopReason::StopSequence),
742 ("\"refusal\"", StopReason::Refusal),
743 (
744 "\"model_context_window_exceeded\"",
745 StopReason::ModelContextWindowExceeded,
746 ),
747 ] {
748 let parsed: StopReason = serde_json::from_str(json)?;
749 assert_eq!(parsed, expected);
750 assert_eq!(serde_json::to_string(&parsed)?, json);
751 }
752 Ok(())
753 }
754
755 #[test]
756 fn stop_reason_unknown_value_deserializes_to_unknown() -> Result<(), serde_json::Error> {
757 let parsed: StopReason = serde_json::from_str("\"some_future_reason\"")?;
760 assert_eq!(parsed, StopReason::Unknown);
761 assert_eq!(parsed.as_str(), "unknown");
762 Ok(())
763 }
764
765 #[test]
766 fn stop_reason_unknown_serializes_to_unknown() -> Result<(), serde_json::Error> {
767 assert_eq!(serde_json::to_string(&StopReason::Unknown)?, "\"unknown\"");
768 Ok(())
769 }
770
771 #[test]
779 fn content_block_text_wire_format() -> Result<(), serde_json::Error> {
780 let json = serde_json::to_value(ContentBlock::Text { text: "hi".into() })?;
781 assert_eq!(json, serde_json::json!({"type": "text", "text": "hi"}));
782 Ok(())
783 }
784
785 #[test]
786 fn content_block_thinking_omits_none_signature() -> Result<(), serde_json::Error> {
787 let none = serde_json::to_value(ContentBlock::Thinking {
788 thinking: "t".into(),
789 signature: None,
790 })?;
791 assert_eq!(
792 none,
793 serde_json::json!({"type": "thinking", "thinking": "t"})
794 );
795
796 let some = serde_json::to_value(ContentBlock::Thinking {
797 thinking: "t".into(),
798 signature: Some("sig".into()),
799 })?;
800 assert_eq!(
801 some,
802 serde_json::json!({"type": "thinking", "thinking": "t", "signature": "sig"})
803 );
804 Ok(())
805 }
806
807 #[test]
808 fn content_block_tool_use_omits_none_thought_signature() -> Result<(), serde_json::Error> {
809 let none = serde_json::to_value(ContentBlock::ToolUse {
810 id: "i".into(),
811 name: "n".into(),
812 input: serde_json::json!({"a": 1}),
813 thought_signature: None,
814 })?;
815 assert_eq!(
816 none,
817 serde_json::json!({"type": "tool_use", "id": "i", "name": "n", "input": {"a": 1}})
818 );
819
820 let some = serde_json::to_value(ContentBlock::ToolUse {
821 id: "i".into(),
822 name: "n".into(),
823 input: serde_json::json!({}),
824 thought_signature: Some("ts".into()),
825 })?;
826 assert_eq!(
827 some.get("thought_signature").and_then(|v| v.as_str()),
828 Some("ts")
829 );
830 Ok(())
831 }
832
833 #[test]
834 fn content_block_tool_result_omits_none_is_error() -> Result<(), serde_json::Error> {
835 let none = serde_json::to_value(ContentBlock::ToolResult {
836 tool_use_id: "t".into(),
837 content: "out".into(),
838 is_error: None,
839 })?;
840 assert_eq!(
841 none,
842 serde_json::json!({"type": "tool_result", "tool_use_id": "t", "content": "out"})
843 );
844
845 let some = serde_json::to_value(ContentBlock::ToolResult {
846 tool_use_id: "t".into(),
847 content: "out".into(),
848 is_error: Some(true),
849 })?;
850 assert_eq!(
851 some.get("is_error").and_then(serde_json::Value::as_bool),
852 Some(true)
853 );
854 Ok(())
855 }
856
857 #[test]
858 fn content_block_remaining_variant_tags() -> Result<(), serde_json::Error> {
859 assert_eq!(
860 serde_json::to_value(ContentBlock::RedactedThinking { data: "d".into() })?,
861 serde_json::json!({"type": "redacted_thinking", "data": "d"})
862 );
863 assert_eq!(
864 serde_json::to_value(ContentBlock::Image {
865 source: ContentSource::new("image/png", "b64"),
866 })?,
867 serde_json::json!({"type": "image", "source": {"media_type": "image/png", "data": "b64"}})
868 );
869 assert_eq!(
870 serde_json::to_value(ContentBlock::Document {
871 source: ContentSource::new("application/pdf", "b64"),
872 })?,
873 serde_json::json!({"type": "document", "source": {"media_type": "application/pdf", "data": "b64"}})
874 );
875 Ok(())
876 }
877
878 #[test]
879 fn content_block_every_tag_round_trips() -> Result<(), serde_json::Error> {
880 let blocks = vec![
881 ContentBlock::Text { text: "t".into() },
882 ContentBlock::Thinking {
883 thinking: "th".into(),
884 signature: Some("s".into()),
885 },
886 ContentBlock::RedactedThinking { data: "d".into() },
887 ContentBlock::ToolUse {
888 id: "i".into(),
889 name: "n".into(),
890 input: serde_json::json!({"x": 1}),
891 thought_signature: None,
892 },
893 ContentBlock::ToolResult {
894 tool_use_id: "t".into(),
895 content: "c".into(),
896 is_error: Some(true),
897 },
898 ContentBlock::Image {
899 source: ContentSource::new("image/png", "b"),
900 },
901 ContentBlock::Document {
902 source: ContentSource::new("application/pdf", "b"),
903 },
904 ];
905 for block in blocks {
906 let json = serde_json::to_value(&block)?;
907 let back: ContentBlock = serde_json::from_value(json.clone())?;
908 assert_eq!(serde_json::to_value(&back)?, json);
909 }
910 Ok(())
911 }
912
913 #[test]
916 fn content_text_serializes_as_bare_string() -> Result<(), serde_json::Error> {
917 let json = serde_json::to_value(Content::Text("hello".into()))?;
918 assert_eq!(json, serde_json::json!("hello"));
919 let back: Content = serde_json::from_value(serde_json::json!("hello"))?;
920 assert!(matches!(back, Content::Text(s) if s == "hello"));
921 Ok(())
922 }
923
924 #[test]
925 fn content_blocks_serialize_as_array_including_empty() -> Result<(), serde_json::Error> {
926 let json = serde_json::to_value(Content::Blocks(vec![ContentBlock::Text {
927 text: "x".into(),
928 }]))?;
929 assert_eq!(json, serde_json::json!([{"type": "text", "text": "x"}]));
930
931 let empty = serde_json::to_value(Content::Blocks(vec![]))?;
934 assert_eq!(empty, serde_json::json!([]));
935 let back: Content = serde_json::from_value(empty)?;
936 assert!(matches!(back, Content::Blocks(b) if b.is_empty()));
937 Ok(())
938 }
939
940 #[test]
943 fn message_wire_format_text_and_blocks() -> Result<(), serde_json::Error> {
944 let user = serde_json::to_value(Message::user("hi"))?;
945 assert_eq!(user, serde_json::json!({"role": "user", "content": "hi"}));
946
947 let assistant =
948 serde_json::to_value(Message::assistant_with_content(vec![ContentBlock::Text {
949 text: "yo".into(),
950 }]))?;
951 assert_eq!(
952 assistant,
953 serde_json::json!({"role": "assistant", "content": [{"type": "text", "text": "yo"}]})
954 );
955
956 let back: Message =
957 serde_json::from_value(serde_json::json!({"role": "user", "content": "hi"}))?;
958 assert_eq!(back.role, Role::User);
959 assert!(matches!(back.content, Content::Text(s) if s == "hi"));
960 Ok(())
961 }
962
963 #[test]
966 fn parse_retry_after_delta_seconds() {
967 assert_eq!(parse_retry_after("125"), Some(Duration::from_secs(125)));
968 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
969 assert_eq!(parse_retry_after(" 30 "), Some(Duration::from_secs(30)));
971 }
972
973 #[test]
974 fn parse_retry_after_rejects_garbage_and_empty() {
975 assert_eq!(parse_retry_after(""), None);
976 assert_eq!(parse_retry_after(" "), None);
977 assert_eq!(parse_retry_after("soon"), None);
978 assert_eq!(parse_retry_after("-5"), None);
980 }
981
982 #[test]
983 fn parse_retry_after_past_imf_date_is_none() {
984 assert_eq!(parse_retry_after("Sun, 06 Nov 1994 08:49:37 GMT"), None);
986 }
987
988 #[test]
989 fn parse_retry_after_future_imf_date_is_some() {
990 let parsed = parse_retry_after("Fri, 31 Dec 9999 23:59:59 GMT");
994 assert!(parsed.is_some_and(|d| d > Duration::from_secs(1_000_000)));
995 }
996
997 #[test]
1000 fn cache_ttl_wire_strings() {
1001 assert_eq!(CacheTtl::FiveMinutes.as_wire_str(), "5m");
1002 assert_eq!(CacheTtl::OneHour.as_wire_str(), "1h");
1003 }
1004
1005 #[test]
1006 fn cache_config_builders_and_default_request_cache_is_none() {
1007 let req = ChatRequest::new("sys", vec![Message::user("hi")]);
1008 assert!(
1009 req.cache.is_none(),
1010 "default request must not set a cache config"
1011 );
1012
1013 let enabled = CacheConfig::enabled().with_ttl(CacheTtl::OneHour);
1014 assert!(enabled.enabled);
1015 assert_eq!(enabled.ttl, Some(CacheTtl::OneHour));
1016 assert_eq!(enabled.max_breakpoints, None);
1017
1018 let disabled = CacheConfig::disabled();
1019 assert!(!disabled.enabled);
1020
1021 let capped = CacheConfig::enabled().with_max_breakpoints(2);
1022 assert_eq!(capped.max_breakpoints, Some(2));
1023
1024 let req = ChatRequest::new("s", vec![]).with_cache(CacheConfig::disabled());
1025 assert!(req.cache.is_some_and(|c| !c.enabled));
1026 }
1027}