1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{ExtensionMap, ModelError, ModelErrorKind};
7
8#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[serde(rename_all = "snake_case")]
11#[non_exhaustive]
12pub enum Role {
13 System,
15 User,
17 Assistant,
19 Tool,
21}
22
23#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[serde(tag = "type", rename_all = "snake_case")]
26#[non_exhaustive]
27pub enum MediaSource {
28 Url {
30 url: String,
32 media_type: Option<String>,
34 },
35 Base64 {
37 media_type: String,
39 data: String,
41 },
42 Artifact {
44 reference: crate::ArtifactRef,
46 },
47 ProviderFile {
49 provider: String,
51 file_id: String,
53 },
54}
55
56impl MediaSource {
57 pub fn provider_file(provider: impl Into<String>, file_id: impl Into<String>) -> Self {
59 Self::ProviderFile {
60 provider: provider.into(),
61 file_id: file_id.into(),
62 }
63 }
64}
65
66#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
68pub struct ProviderData {
69 pub provider: String,
71 pub kind: String,
73 pub value: Value,
75}
76
77#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
79pub struct Citation {
80 pub uri: Option<String>,
82 pub title: Option<String>,
84 pub start: Option<u64>,
86 pub end: Option<u64>,
88}
89
90#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
92pub struct ReasoningPart {
93 pub text: Option<String>,
95 pub signature: Option<String>,
97 pub redacted: bool,
99 pub provider_data: Vec<ProviderData>,
101}
102
103#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
105pub struct ToolCall {
106 pub id: String,
108 pub name: String,
110 pub arguments: Value,
112 pub raw_arguments: Option<String>,
114 pub metadata: ExtensionMap,
116}
117
118#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
120pub struct ToolResult {
121 pub call_id: String,
123 #[serde(default)]
125 pub name: Option<String>,
126 pub content: Vec<ContentPart>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub structured_content: Option<Value>,
132 pub is_error: bool,
134 pub metadata: ExtensionMap,
136}
137
138#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
140#[serde(tag = "type", rename_all = "snake_case")]
141#[non_exhaustive]
142pub enum ContentPart {
143 Text {
145 text: String,
147 },
148 Image {
150 source: MediaSource,
152 },
153 Audio {
155 source: MediaSource,
157 },
158 Document {
160 source: MediaSource,
162 name: Option<String>,
164 },
165 ResourceLink {
167 uri: String,
169 name: String,
171 title: Option<String>,
173 description: Option<String>,
175 media_type: Option<String>,
177 size: Option<u64>,
179 },
180 ToolCall(ToolCall),
182 ToolResult(ToolResult),
184 Reasoning(ReasoningPart),
186 Refusal {
188 text: String,
190 },
191 Citation(Citation),
193 ProviderOpaque(ProviderData),
195}
196
197impl ContentPart {
198 pub fn text(value: impl Into<String>) -> Self {
200 Self::Text { text: value.into() }
201 }
202}
203
204#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
206pub struct Message {
207 pub role: Role,
209 pub content: Vec<ContentPart>,
211 pub metadata: BTreeMap<String, Value>,
213}
214
215impl Message {
216 pub fn new(role: Role, content: Vec<ContentPart>) -> Result<Self, ModelError> {
222 if content.is_empty() {
223 return Err(ModelError::local(
224 ModelErrorKind::InvalidRequest,
225 "a message must contain at least one content part",
226 ));
227 }
228 Ok(Self {
229 role,
230 content,
231 metadata: BTreeMap::new(),
232 })
233 }
234
235 pub fn user(text: impl Into<String>) -> Self {
237 Self {
238 role: Role::User,
239 content: vec![ContentPart::text(text)],
240 metadata: BTreeMap::new(),
241 }
242 }
243
244 pub fn system(text: impl Into<String>) -> Self {
246 Self {
247 role: Role::System,
248 content: vec![ContentPart::text(text)],
249 metadata: BTreeMap::new(),
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::{ContentPart, Message, Role, ToolResult};
257 use crate::ModelErrorKind;
258
259 #[test]
260 fn empty_messages_are_rejected() {
261 let error = Message::new(Role::User, Vec::new()).unwrap_err();
262 assert_eq!(error.kind, ModelErrorKind::InvalidRequest);
263 }
264
265 #[test]
266 fn content_round_trips_without_erasing_opaque_data() {
267 let message = Message::new(
268 Role::Assistant,
269 vec![
270 ContentPart::text("answer"),
271 ContentPart::ProviderOpaque(super::ProviderData {
272 provider: "example".into(),
273 kind: "future_block".into(),
274 value: serde_json::json!({"x": 1}),
275 }),
276 ],
277 )
278 .unwrap();
279
280 let encoded = serde_json::to_value(&message).unwrap();
281 let decoded: Message = serde_json::from_value(encoded).unwrap();
282
283 assert_eq!(decoded, message);
284 }
285
286 #[test]
287 fn legacy_tool_results_without_a_name_still_deserialize() {
288 let result: ToolResult = serde_json::from_value(serde_json::json!({
289 "call_id":"call_1",
290 "content":[{"type":"text","text":"ok"}],
291 "is_error":false,
292 "metadata":{}
293 }))
294 .unwrap();
295
296 assert_eq!(result.name, None);
297 }
298}