Skip to main content

starweaver_model/message/
request_parts.rs

1//! Canonical model request parts.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Map;
5
6use super::{Metadata, ToolReturnPart};
7
8/// Cache-point lifetime for providers with per-breakpoint TTL controls.
9#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
10pub enum CachePointTtl {
11    /// Keep the cache entry for at least five minutes.
12    #[serde(rename = "5m")]
13    FiveMinutes,
14    /// Keep the cache entry for at least one hour.
15    #[serde(rename = "1h")]
16    OneHour,
17}
18
19impl CachePointTtl {
20    /// Return the provider wire value.
21    #[must_use]
22    pub const fn as_str(self) -> &'static str {
23        match self {
24            Self::FiveMinutes => "5m",
25            Self::OneHour => "1h",
26        }
27    }
28}
29
30/// Request part sent to a model.
31#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(tag = "kind", rename_all = "snake_case")]
33pub enum ModelRequestPart {
34    /// System or developer instruction.
35    SystemPrompt {
36        /// Prompt text.
37        text: String,
38        /// Application metadata.
39        #[serde(default, skip_serializing_if = "Map::is_empty")]
40        metadata: Metadata,
41    },
42    /// User prompt.
43    UserPrompt {
44        /// User content parts.
45        content: Vec<ContentPart>,
46        /// Optional speaker name.
47        #[serde(default, skip_serializing_if = "Option::is_none")]
48        name: Option<String>,
49        /// Application metadata.
50        #[serde(default, skip_serializing_if = "Map::is_empty")]
51        metadata: Metadata,
52    },
53    /// Tool return sent back to the model.
54    ToolReturn(ToolReturnPart),
55    /// Retry or validation feedback.
56    RetryPrompt {
57        /// Feedback text.
58        text: String,
59        /// Related tool call identifier.
60        #[serde(default, skip_serializing_if = "Option::is_none")]
61        tool_call_id: Option<String>,
62        /// Application metadata.
63        #[serde(default, skip_serializing_if = "Map::is_empty")]
64        metadata: Metadata,
65    },
66    /// Structured instruction fragment.
67    Instruction {
68        /// Instruction text.
69        text: String,
70        /// Application metadata.
71        #[serde(default, skip_serializing_if = "Map::is_empty")]
72        metadata: Metadata,
73    },
74}
75
76/// Multimodal content in user requests.
77#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
78#[serde(tag = "kind", rename_all = "snake_case")]
79pub enum ContentPart {
80    /// Prompt-cache boundary after the preceding content block.
81    ///
82    /// The marker is not model-visible content. A TTL is optional because
83    /// providers such as Anthropic use per-point `5m`/`1h` TTLs, while `OpenAI`
84    /// GPT-5.6 uses a request-wide `30m` policy.
85    CachePoint {
86        /// Optional provider-compatible per-point TTL.
87        #[serde(default, skip_serializing_if = "Option::is_none")]
88        ttl: Option<CachePointTtl>,
89    },
90    /// Plain text.
91    Text {
92        /// Text content.
93        text: String,
94    },
95    /// Image by URL.
96    ImageUrl {
97        /// Image URL.
98        url: String,
99    },
100    /// Generic file by URL and media type.
101    FileUrl {
102        /// File URL.
103        url: String,
104        /// File media type.
105        media_type: String,
106    },
107    /// Inline binary media bytes.
108    Binary {
109        /// Binary payload.
110        data: Vec<u8>,
111        /// Declared or corrected media type.
112        media_type: String,
113    },
114    /// Resource-backed media reference.
115    ResourceRef {
116        /// Resource URI.
117        uri: String,
118        /// Resource media type.
119        media_type: String,
120        /// Resource type, such as `image`, `video`, or `document`.
121        resource_type: String,
122        /// Resource metadata.
123        #[serde(default, skip_serializing_if = "Map::is_empty")]
124        metadata: Metadata,
125    },
126    /// Inline data URL for adapters that accept data URLs.
127    DataUrl {
128        /// Data URL payload.
129        data_url: String,
130        /// Media type carried by the data URL.
131        media_type: String,
132    },
133}
134
135impl ContentPart {
136    /// Build a provider-neutral prompt-cache boundary.
137    #[must_use]
138    pub const fn cache_point() -> Self {
139        Self::CachePoint { ttl: None }
140    }
141
142    /// Build a prompt-cache boundary with a per-point TTL.
143    #[must_use]
144    pub const fn cache_point_with_ttl(ttl: CachePointTtl) -> Self {
145        Self::CachePoint { ttl: Some(ttl) }
146    }
147
148    /// Build a plain text content part.
149    #[must_use]
150    pub fn text(text: impl Into<String>) -> Self {
151        Self::Text { text: text.into() }
152    }
153
154    /// Build an image URL content part.
155    #[must_use]
156    pub fn image_url(url: impl Into<String>) -> Self {
157        Self::ImageUrl { url: url.into() }
158    }
159
160    /// Build a generic file URL content part with an explicit media type.
161    #[must_use]
162    pub fn file_url(url: impl Into<String>, media_type: impl Into<String>) -> Self {
163        Self::FileUrl {
164            url: url.into(),
165            media_type: media_type.into(),
166        }
167    }
168
169    /// Build an inline binary content part with an explicit media type.
170    #[must_use]
171    pub fn binary(data: impl Into<Vec<u8>>, media_type: impl Into<String>) -> Self {
172        Self::Binary {
173            data: data.into(),
174            media_type: media_type.into(),
175        }
176    }
177
178    /// Build an inline image content part.
179    #[must_use]
180    pub fn image_bytes(data: impl Into<Vec<u8>>, media_type: impl Into<String>) -> Self {
181        Self::binary(data, media_type)
182    }
183
184    /// Build an inline audio content part.
185    #[must_use]
186    pub fn audio_bytes(data: impl Into<Vec<u8>>, media_type: impl Into<String>) -> Self {
187        Self::binary(data, media_type)
188    }
189
190    /// Build an inline video content part.
191    #[must_use]
192    pub fn video_bytes(data: impl Into<Vec<u8>>, media_type: impl Into<String>) -> Self {
193        Self::binary(data, media_type)
194    }
195
196    /// Build an inline data URL content part with an explicit media type.
197    #[must_use]
198    pub fn data_url(data_url: impl Into<String>, media_type: impl Into<String>) -> Self {
199        Self::DataUrl {
200            data_url: data_url.into(),
201            media_type: media_type.into(),
202        }
203    }
204
205    /// Build a resource-backed media reference.
206    #[must_use]
207    pub fn resource_ref(
208        uri: impl Into<String>,
209        media_type: impl Into<String>,
210        resource_type: impl Into<String>,
211    ) -> Self {
212        Self::ResourceRef {
213            uri: uri.into(),
214            media_type: media_type.into(),
215            resource_type: resource_type.into(),
216            metadata: Metadata::new(),
217        }
218    }
219
220    /// Attach resource metadata to a resource-backed content part.
221    #[must_use]
222    pub fn with_resource_metadata(mut self, metadata: Metadata) -> Self {
223        if let Self::ResourceRef {
224            metadata: existing, ..
225        } = &mut self
226        {
227            *existing = metadata;
228        }
229        self
230    }
231}