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 artifact_id: String,
46 media_type: Option<String>,
48 },
49 ProviderFile {
51 provider: String,
53 file_id: String,
55 },
56}
57
58impl MediaSource {
59 pub fn provider_file(provider: impl Into<String>, file_id: impl Into<String>) -> Self {
61 Self::ProviderFile {
62 provider: provider.into(),
63 file_id: file_id.into(),
64 }
65 }
66}
67
68#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
70pub struct ProviderData {
71 pub provider: String,
73 pub kind: String,
75 pub value: Value,
77}
78
79#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
81pub struct Citation {
82 pub uri: Option<String>,
84 pub title: Option<String>,
86 pub start: Option<u64>,
88 pub end: Option<u64>,
90}
91
92#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
94pub struct ReasoningPart {
95 pub text: Option<String>,
97 pub signature: Option<String>,
99 pub redacted: bool,
101 pub provider_data: Vec<ProviderData>,
103}
104
105#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
107pub struct ToolCall {
108 pub id: String,
110 pub name: String,
112 pub arguments: Value,
114 pub raw_arguments: Option<String>,
116 pub metadata: ExtensionMap,
118}
119
120#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
122pub struct ToolResult {
123 pub call_id: String,
125 #[serde(default)]
127 pub name: Option<String>,
128 pub content: Vec<ContentPart>,
130 pub is_error: bool,
132 pub metadata: ExtensionMap,
134}
135
136#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
138#[serde(tag = "type", rename_all = "snake_case")]
139#[non_exhaustive]
140pub enum ContentPart {
141 Text {
143 text: String,
145 },
146 Image {
148 source: MediaSource,
150 },
151 Audio {
153 source: MediaSource,
155 },
156 Document {
158 source: MediaSource,
160 name: Option<String>,
162 },
163 ToolCall(ToolCall),
165 ToolResult(ToolResult),
167 Reasoning(ReasoningPart),
169 Refusal {
171 text: String,
173 },
174 Citation(Citation),
176 ProviderOpaque(ProviderData),
178}
179
180impl ContentPart {
181 pub fn text(value: impl Into<String>) -> Self {
183 Self::Text { text: value.into() }
184 }
185}
186
187#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
189pub struct Message {
190 pub role: Role,
192 pub content: Vec<ContentPart>,
194 pub metadata: BTreeMap<String, Value>,
196}
197
198impl Message {
199 pub fn new(role: Role, content: Vec<ContentPart>) -> Result<Self, ModelError> {
205 if content.is_empty() {
206 return Err(ModelError::local(
207 ModelErrorKind::InvalidRequest,
208 "a message must contain at least one content part",
209 ));
210 }
211 Ok(Self {
212 role,
213 content,
214 metadata: BTreeMap::new(),
215 })
216 }
217
218 pub fn user(text: impl Into<String>) -> Self {
220 Self {
221 role: Role::User,
222 content: vec![ContentPart::text(text)],
223 metadata: BTreeMap::new(),
224 }
225 }
226
227 pub fn system(text: impl Into<String>) -> Self {
229 Self {
230 role: Role::System,
231 content: vec![ContentPart::text(text)],
232 metadata: BTreeMap::new(),
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::{ContentPart, Message, Role, ToolResult};
240 use crate::ModelErrorKind;
241
242 #[test]
243 fn empty_messages_are_rejected() {
244 let error = Message::new(Role::User, Vec::new()).unwrap_err();
245 assert_eq!(error.kind, ModelErrorKind::InvalidRequest);
246 }
247
248 #[test]
249 fn content_round_trips_without_erasing_opaque_data() {
250 let message = Message::new(
251 Role::Assistant,
252 vec![
253 ContentPart::text("answer"),
254 ContentPart::ProviderOpaque(super::ProviderData {
255 provider: "example".into(),
256 kind: "future_block".into(),
257 value: serde_json::json!({"x": 1}),
258 }),
259 ],
260 )
261 .unwrap();
262
263 let encoded = serde_json::to_value(&message).unwrap();
264 let decoded: Message = serde_json::from_value(encoded).unwrap();
265
266 assert_eq!(decoded, message);
267 }
268
269 #[test]
270 fn legacy_tool_results_without_a_name_still_deserialize() {
271 let result: ToolResult = serde_json::from_value(serde_json::json!({
272 "call_id":"call_1",
273 "content":[{"type":"text","text":"ok"}],
274 "is_error":false,
275 "metadata":{}
276 }))
277 .unwrap();
278
279 assert_eq!(result.name, None);
280 }
281}