Skip to main content

systemprompt_models/wire/canonical/request/
content.rs

1//! Message roles, content parts and cache breakpoints of a canonical request.
2//!
3//! `cache_control` is Anthropic's prompt-caching breakpoint. It rides on the
4//! canonical model so a rebuilt request (system-prompt override, cross-wire
5//! translation) re-emits it exactly where the client placed it; wires without
6//! prompt caching ignore it.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use serde_json::Value;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Role {
15    System,
16    User,
17    Assistant,
18    Tool,
19}
20
21impl Role {
22    pub const fn as_str(self) -> &'static str {
23        match self {
24            Self::System => "system",
25            Self::User => "user",
26            Self::Assistant => "assistant",
27            Self::Tool => "tool",
28        }
29    }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ImageDetail {
34    Auto,
35    Low,
36    High,
37}
38
39impl ImageDetail {
40    pub const fn as_str(self) -> &'static str {
41        match self {
42            Self::Auto => "auto",
43            Self::Low => "low",
44            Self::High => "high",
45        }
46    }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum CacheTtl {
51    FiveMinutes,
52    OneHour,
53}
54
55impl CacheTtl {
56    pub const fn as_str(self) -> &'static str {
57        match self {
58            Self::FiveMinutes => "5m",
59            Self::OneHour => "1h",
60        }
61    }
62
63    #[must_use]
64    pub fn parse(value: &str) -> Option<Self> {
65        match value {
66            "5m" => Some(Self::FiveMinutes),
67            "1h" => Some(Self::OneHour),
68            _ => None,
69        }
70    }
71}
72
73// Why: Anthropic's only cache type is `ephemeral`; the breakpoint is the
74// block's presence, the TTL is the one optional knob.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76pub struct CacheControl {
77    pub ttl: Option<CacheTtl>,
78}
79
80impl CacheControl {
81    pub const EPHEMERAL: Self = Self { ttl: None };
82
83    #[must_use]
84    pub const fn with_ttl(ttl: CacheTtl) -> Self {
85        Self { ttl: Some(ttl) }
86    }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct SystemBlock {
91    pub text: String,
92    pub cache_control: Option<CacheControl>,
93}
94
95impl SystemBlock {
96    #[must_use]
97    pub fn text(text: impl Into<String>) -> Self {
98        Self {
99            text: text.into(),
100            cache_control: None,
101        }
102    }
103}
104
105#[derive(Debug, Clone)]
106pub enum ImageSource {
107    Base64 {
108        media_type: String,
109        data: String,
110        detail: Option<ImageDetail>,
111    },
112    Url {
113        url: String,
114        detail: Option<ImageDetail>,
115    },
116}
117
118#[derive(Debug, Clone)]
119pub enum CanonicalContent {
120    Text {
121        text: String,
122        cache_control: Option<CacheControl>,
123    },
124    Image {
125        source: ImageSource,
126        cache_control: Option<CacheControl>,
127    },
128    ToolUse {
129        id: String,
130        name: String,
131        // JSON: MCP tool-call arguments are the tool's own JSON object.
132        input: Value,
133        // Why: Gemini requires function-call `thoughtSignature` values replayed verbatim.
134        signature: Option<String>,
135        cache_control: Option<CacheControl>,
136    },
137    ToolResult {
138        tool_use_id: String,
139        content: Vec<Self>,
140        is_error: bool,
141        structured_content: Option<Value>,
142        // JSON: MCP `_meta` is an open map of vendor-prefixed keys.
143        meta: Option<Value>,
144        cache_control: Option<CacheControl>,
145    },
146    Thinking {
147        text: String,
148        signature: Option<String>,
149        // Why: OpenAI Responses requires the reasoning item ID and encrypted content
150        // replayed verbatim for stateless reasoning continuity.
151        id: Option<String>,
152        encrypted_content: Option<String>,
153    },
154}
155
156impl CanonicalContent {
157    #[must_use]
158    pub fn text(text: impl Into<String>) -> Self {
159        Self::Text {
160            text: text.into(),
161            cache_control: None,
162        }
163    }
164
165    #[must_use]
166    pub const fn image(source: ImageSource) -> Self {
167        Self::Image {
168            source,
169            cache_control: None,
170        }
171    }
172
173    // Why: thinking blocks cannot carry a breakpoint on the Messages API.
174    #[must_use]
175    pub const fn cache_control(&self) -> Option<CacheControl> {
176        match self {
177            Self::Text { cache_control, .. }
178            | Self::Image { cache_control, .. }
179            | Self::ToolUse { cache_control, .. }
180            | Self::ToolResult { cache_control, .. } => *cache_control,
181            Self::Thinking { .. } => None,
182        }
183    }
184}
185
186#[derive(Debug, Clone)]
187pub struct CanonicalMessage {
188    pub role: Role,
189    pub content: Vec<CanonicalContent>,
190}