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}
50
51#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
53pub struct ProviderData {
54 pub provider: String,
56 pub kind: String,
58 pub value: Value,
60}
61
62#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64pub struct Citation {
65 pub uri: Option<String>,
67 pub title: Option<String>,
69 pub start: Option<u64>,
71 pub end: Option<u64>,
73}
74
75#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
77pub struct ReasoningPart {
78 pub text: Option<String>,
80 pub signature: Option<String>,
82 pub redacted: bool,
84 pub provider_data: Vec<ProviderData>,
86}
87
88#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
90pub struct ToolCall {
91 pub id: String,
93 pub name: String,
95 pub arguments: Value,
97 pub raw_arguments: Option<String>,
99 pub metadata: ExtensionMap,
101}
102
103#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
105pub struct ToolResult {
106 pub call_id: String,
108 #[serde(default)]
110 pub name: Option<String>,
111 pub content: Vec<ContentPart>,
113 pub is_error: bool,
115 pub metadata: ExtensionMap,
117}
118
119#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
121#[serde(tag = "type", rename_all = "snake_case")]
122#[non_exhaustive]
123pub enum ContentPart {
124 Text {
126 text: String,
128 },
129 Image {
131 source: MediaSource,
133 },
134 Audio {
136 source: MediaSource,
138 },
139 Document {
141 source: MediaSource,
143 name: Option<String>,
145 },
146 ToolCall(ToolCall),
148 ToolResult(ToolResult),
150 Reasoning(ReasoningPart),
152 Refusal {
154 text: String,
156 },
157 Citation(Citation),
159 ProviderOpaque(ProviderData),
161}
162
163impl ContentPart {
164 pub fn text(value: impl Into<String>) -> Self {
166 Self::Text { text: value.into() }
167 }
168}
169
170#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
172pub struct Message {
173 pub role: Role,
175 pub content: Vec<ContentPart>,
177 pub metadata: BTreeMap<String, Value>,
179}
180
181impl Message {
182 pub fn new(role: Role, content: Vec<ContentPart>) -> Result<Self, ModelError> {
188 if content.is_empty() {
189 return Err(ModelError::local(
190 ModelErrorKind::InvalidRequest,
191 "a message must contain at least one content part",
192 ));
193 }
194 Ok(Self {
195 role,
196 content,
197 metadata: BTreeMap::new(),
198 })
199 }
200
201 pub fn user(text: impl Into<String>) -> Self {
203 Self {
204 role: Role::User,
205 content: vec![ContentPart::text(text)],
206 metadata: BTreeMap::new(),
207 }
208 }
209
210 pub fn system(text: impl Into<String>) -> Self {
212 Self {
213 role: Role::System,
214 content: vec![ContentPart::text(text)],
215 metadata: BTreeMap::new(),
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::{ContentPart, Message, Role, ToolResult};
223 use crate::ModelErrorKind;
224
225 #[test]
226 fn empty_messages_are_rejected() {
227 let error = Message::new(Role::User, Vec::new()).unwrap_err();
228 assert_eq!(error.kind, ModelErrorKind::InvalidRequest);
229 }
230
231 #[test]
232 fn content_round_trips_without_erasing_opaque_data() {
233 let message = Message::new(
234 Role::Assistant,
235 vec![
236 ContentPart::text("answer"),
237 ContentPart::ProviderOpaque(super::ProviderData {
238 provider: "example".into(),
239 kind: "future_block".into(),
240 value: serde_json::json!({"x": 1}),
241 }),
242 ],
243 )
244 .unwrap();
245
246 let encoded = serde_json::to_value(&message).unwrap();
247 let decoded: Message = serde_json::from_value(encoded).unwrap();
248
249 assert_eq!(decoded, message);
250 }
251
252 #[test]
253 fn legacy_tool_results_without_a_name_still_deserialize() {
254 let result: ToolResult = serde_json::from_value(serde_json::json!({
255 "call_id":"call_1",
256 "content":[{"type":"text","text":"ok"}],
257 "is_error":false,
258 "metadata":{}
259 }))
260 .unwrap();
261
262 assert_eq!(result.name, None);
263 }
264}