1use crate::base64_serde;
2use crate::enums::{FunctionResponseScheduling, Language, Outcome, PartMediaResolutionLevel};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[cfg(feature = "mcp")]
7use rmcp::model::CallToolResult;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Content {
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub role: Option<Role>,
16 #[serde(default)]
18 pub parts: Vec<Part>,
19}
20
21impl Content {
22 pub fn user(text: impl Into<String>) -> Self {
24 Self::from_text(text, Role::User)
25 }
26
27 pub fn model(text: impl Into<String>) -> Self {
29 Self::from_text(text, Role::Model)
30 }
31
32 pub fn text(text: impl Into<String>) -> Self {
34 Self::from_text(text, Role::User)
35 }
36
37 #[must_use]
39 pub const fn from_parts(parts: Vec<Part>, role: Role) -> Self {
40 Self {
41 role: Some(role),
42 parts,
43 }
44 }
45
46 #[must_use]
48 pub fn first_text(&self) -> Option<&str> {
49 self.parts.iter().find_map(|part| part.text_value())
50 }
51
52 fn from_text(text: impl Into<String>, role: Role) -> Self {
53 Self {
54 role: Some(role),
55 parts: vec![Part::text(text)],
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "lowercase")]
63pub enum Role {
64 User,
65 Model,
66 Function,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct Part {
73 #[serde(flatten)]
75 pub kind: PartKind,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub thought: Option<bool>,
79 #[serde(
81 default,
82 skip_serializing_if = "Option::is_none",
83 with = "base64_serde::option"
84 )]
85 pub thought_signature: Option<Vec<u8>>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub media_resolution: Option<PartMediaResolution>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub video_metadata: Option<VideoMetadata>,
92}
93
94impl Part {
95 pub fn text(text: impl Into<String>) -> Self {
97 Self {
98 kind: PartKind::Text { text: text.into() },
99 thought: None,
100 thought_signature: None,
101 media_resolution: None,
102 video_metadata: None,
103 }
104 }
105
106 pub fn inline_data(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
108 Self {
109 kind: PartKind::InlineData {
110 inline_data: Blob {
111 mime_type: mime_type.into(),
112 data,
113 display_name: None,
114 },
115 },
116 thought: None,
117 thought_signature: None,
118 media_resolution: None,
119 video_metadata: None,
120 }
121 }
122
123 pub fn file_data(file_uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
125 Self {
126 kind: PartKind::FileData {
127 file_data: FileData {
128 file_uri: file_uri.into(),
129 mime_type: mime_type.into(),
130 display_name: None,
131 },
132 },
133 thought: None,
134 thought_signature: None,
135 media_resolution: None,
136 video_metadata: None,
137 }
138 }
139
140 #[must_use]
142 pub const fn function_call(function_call: FunctionCall) -> Self {
143 Self {
144 kind: PartKind::FunctionCall { function_call },
145 thought: None,
146 thought_signature: None,
147 media_resolution: None,
148 video_metadata: None,
149 }
150 }
151
152 #[must_use]
154 pub const fn function_response(function_response: FunctionResponse) -> Self {
155 Self {
156 kind: PartKind::FunctionResponse { function_response },
157 thought: None,
158 thought_signature: None,
159 media_resolution: None,
160 video_metadata: None,
161 }
162 }
163
164 pub fn executable_code(code: impl Into<String>, language: Language) -> Self {
166 Self {
167 kind: PartKind::ExecutableCode {
168 executable_code: ExecutableCode {
169 code: code.into(),
170 language,
171 },
172 },
173 thought: None,
174 thought_signature: None,
175 media_resolution: None,
176 video_metadata: None,
177 }
178 }
179
180 pub fn code_execution_result(outcome: Outcome, output: impl Into<String>) -> Self {
182 Self {
183 kind: PartKind::CodeExecutionResult {
184 code_execution_result: CodeExecutionResult {
185 outcome,
186 output: Some(output.into()),
187 },
188 },
189 thought: None,
190 thought_signature: None,
191 media_resolution: None,
192 video_metadata: None,
193 }
194 }
195
196 #[must_use]
198 pub const fn with_thought(mut self, thought: bool) -> Self {
199 self.thought = Some(thought);
200 self
201 }
202
203 #[must_use]
205 pub fn with_thought_signature(mut self, signature: Vec<u8>) -> Self {
206 self.thought_signature = Some(signature);
207 self
208 }
209
210 #[must_use]
212 pub const fn with_media_resolution(mut self, resolution: PartMediaResolution) -> Self {
213 self.media_resolution = Some(resolution);
214 self
215 }
216
217 #[must_use]
219 pub fn with_video_metadata(mut self, metadata: VideoMetadata) -> Self {
220 self.video_metadata = Some(metadata);
221 self
222 }
223
224 #[must_use]
226 pub const fn text_value(&self) -> Option<&str> {
227 match &self.kind {
228 PartKind::Text { text } => Some(text.as_str()),
229 _ => None,
230 }
231 }
232
233 #[must_use]
235 pub const fn function_call_ref(&self) -> Option<&FunctionCall> {
236 match &self.kind {
237 PartKind::FunctionCall { function_call } => Some(function_call),
238 _ => None,
239 }
240 }
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase", untagged)]
246pub enum PartKind {
247 Text {
248 text: String,
249 },
250 InlineData {
251 #[serde(rename = "inlineData")]
252 inline_data: Blob,
253 },
254 FileData {
255 #[serde(rename = "fileData")]
256 file_data: FileData,
257 },
258 FunctionCall {
259 #[serde(rename = "functionCall")]
260 function_call: FunctionCall,
261 },
262 FunctionResponse {
263 #[serde(rename = "functionResponse")]
264 function_response: FunctionResponse,
265 },
266 ExecutableCode {
267 #[serde(rename = "executableCode")]
268 executable_code: ExecutableCode,
269 },
270 CodeExecutionResult {
271 #[serde(rename = "codeExecutionResult")]
272 code_execution_result: CodeExecutionResult,
273 },
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct PartMediaResolution {
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub level: Option<PartMediaResolutionLevel>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub num_tokens: Option<i32>,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct Blob {
290 pub mime_type: String,
291 #[serde(with = "base64_serde")]
292 pub data: Vec<u8>,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub display_name: Option<String>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct FileData {
301 pub file_uri: String,
302 pub mime_type: String,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub display_name: Option<String>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct PartialArg {
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub null_value: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub number_value: Option<f64>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub string_value: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub bool_value: Option<bool>,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub json_path: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub will_continue: Option<bool>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct FunctionCall {
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub id: Option<String>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub name: Option<String>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub args: Option<Value>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub partial_args: Option<Vec<PartialArg>>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub will_continue: Option<bool>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct FunctionResponseBlob {
345 pub mime_type: String,
346 #[serde(with = "base64_serde")]
347 pub data: Vec<u8>,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub display_name: Option<String>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub struct FunctionResponseFileData {
356 pub file_uri: String,
357 pub mime_type: String,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub display_name: Option<String>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct FunctionResponsePart {
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub inline_data: Option<FunctionResponseBlob>,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub file_data: Option<FunctionResponseFileData>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct FunctionResponse {
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub will_continue: Option<bool>,
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub scheduling: Option<FunctionResponseScheduling>,
380 #[serde(skip_serializing_if = "Option::is_none")]
381 pub parts: Option<Vec<FunctionResponsePart>>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub id: Option<String>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub name: Option<String>,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub response: Option<Value>,
388}
389
390impl FunctionResponse {
391 #[cfg(feature = "mcp")]
396 pub fn from_mcp_response(
397 name: impl Into<String>,
398 response: &CallToolResult,
399 ) -> Result<Self, serde_json::Error> {
400 let value = serde_json::to_value(response)?;
401 let is_error = response.is_error.unwrap_or(false);
402 let response_value = if is_error {
403 let mut wrapper = serde_json::Map::new();
404 wrapper.insert("error".to_string(), value);
405 Value::Object(wrapper)
406 } else {
407 value
408 };
409 Ok(Self {
410 will_continue: None,
411 scheduling: None,
412 parts: None,
413 id: None,
414 name: Some(name.into()),
415 response: Some(response_value),
416 })
417 }
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
422#[serde(rename_all = "camelCase")]
423pub struct ExecutableCode {
424 pub code: String,
425 pub language: Language,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
430#[serde(rename_all = "camelCase")]
431pub struct CodeExecutionResult {
432 pub outcome: Outcome,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 pub output: Option<String>,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct VideoMetadata {
441 #[serde(skip_serializing_if = "Option::is_none")]
442 pub start_offset: Option<String>,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub end_offset: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub fps: Option<f32>,
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use serde_json::json;
453
454 #[test]
455 fn content_first_text_skips_non_text() {
456 let content = Content::from_parts(
457 vec![
458 Part::inline_data(vec![1, 2, 3], "image/png"),
459 Part::text("first"),
460 Part::text("second"),
461 ],
462 Role::User,
463 );
464 assert_eq!(content.first_text(), Some("first"));
465 }
466
467 #[test]
468 fn part_builders_and_accessors() {
469 let call = FunctionCall {
470 id: Some("call-1".into()),
471 name: Some("lookup".into()),
472 args: Some(json!({"q": "rust"})),
473 partial_args: None,
474 will_continue: None,
475 };
476 let response = FunctionResponse {
477 will_continue: None,
478 scheduling: None,
479 parts: None,
480 id: Some("resp-1".into()),
481 name: Some("lookup".into()),
482 response: Some(json!({"ok": true})),
483 };
484 let metadata = VideoMetadata {
485 start_offset: Some("0s".into()),
486 end_offset: Some("1s".into()),
487 fps: Some(30.0),
488 };
489
490 let part = Part::text("hello")
491 .with_thought(true)
492 .with_thought_signature(vec![1, 2, 3])
493 .with_media_resolution(PartMediaResolution {
494 level: Some(PartMediaResolutionLevel::MediaResolutionLow),
495 num_tokens: None,
496 })
497 .with_video_metadata(metadata);
498 assert_eq!(part.text_value(), Some("hello"));
499
500 let call_part = Part::function_call(call);
501 assert_eq!(
502 call_part.function_call_ref().unwrap().name.as_deref(),
503 Some("lookup")
504 );
505
506 let response_part = Part::function_response(response);
507 let json = serde_json::to_value(&response_part).unwrap();
508 assert!(json.get("functionResponse").is_some());
509
510 let exec_part = Part::executable_code("print('hi')", Language::Python);
511 let exec_json = serde_json::to_value(&exec_part).unwrap();
512 assert_eq!(exec_json["executableCode"]["language"], "PYTHON");
513
514 let result_part = Part::code_execution_result(Outcome::OutcomeOk, "ok");
515 let result_json = serde_json::to_value(&result_part).unwrap();
516 assert_eq!(result_json["codeExecutionResult"]["outcome"], "OUTCOME_OK");
517
518 let file_part = Part::file_data("files/abc", "application/pdf");
519 let file_json = serde_json::to_value(&file_part).unwrap();
520 assert_eq!(file_json["fileData"]["mimeType"], "application/pdf");
521 }
522
523 #[test]
524 fn content_roundtrip() {
525 let content = Content::user("hello");
526 let json = serde_json::to_string(&content).unwrap();
527 let decoded: Content = serde_json::from_str(&json).unwrap();
528 assert_eq!(decoded.parts.len(), 1);
529 }
530
531 #[test]
532 fn blob_base64_serialization() {
533 let blob = Blob {
534 mime_type: "image/png".into(),
535 data: vec![1, 2, 3],
536 display_name: None,
537 };
538 let value = serde_json::to_value(&blob).unwrap();
539 assert!(value["data"].is_string());
540 }
541
542 #[test]
543 fn function_response_media_roundtrip() {
544 let response = FunctionResponse {
545 will_continue: None,
546 scheduling: None,
547 parts: Some(vec![FunctionResponsePart {
548 inline_data: Some(FunctionResponseBlob {
549 mime_type: "image/png".into(),
550 data: vec![1, 2, 3],
551 display_name: None,
552 }),
553 file_data: None,
554 }]),
555 id: Some("fn-1".into()),
556 name: Some("render_chart".into()),
557 response: None,
558 };
559
560 let part = Part::function_response(response);
561 let json = serde_json::to_string(&part).unwrap();
562 assert!(json.contains("inlineData"));
563 }
564
565 #[test]
566 fn function_call_part_deserializes_from_camel_case() {
567 let value = json!({
568 "functionCall": {
569 "name": "add_numbers",
570 "args": { "a": 2.5, "b": 3.1 }
571 },
572 "thoughtSignature": "AQID"
573 });
574 let part: Part = serde_json::from_value(value).unwrap();
575 let call = part.function_call_ref().expect("missing function call");
576 assert_eq!(call.name.as_deref(), Some("add_numbers"));
577 }
578}