1#![allow(clippy::enum_variant_names)]
28
29use serde::{Deserialize, Serialize, de};
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33#[serde(rename_all = "lowercase")]
34pub enum Role {
35 User,
37 Model,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43#[serde(untagged)]
44pub enum Part {
45 Text {
47 text: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 thought: Option<bool>,
52 #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
55 thought_signature: Option<String>,
56 },
57 InlineData {
58 #[serde(rename = "inlineData")]
60 inline_data: Blob,
61 },
62 FileData {
64 #[serde(rename = "fileData")]
65 file_data: FileDataRef,
66 },
67 FunctionCall {
69 #[serde(rename = "functionCall")]
71 function_call: super::tools::FunctionCall,
72 #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
75 thought_signature: Option<String>,
76 },
77 FunctionResponse {
79 #[serde(rename = "functionResponse")]
81 function_response: super::tools::FunctionResponse,
82 #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
85 thought_signature: Option<String>,
86 },
87 ToolCall {
89 #[serde(rename = "toolCall")]
90 tool_call: serde_json::Value,
91 #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
94 thought_signature: Option<String>,
95 },
96 ToolResponse {
98 #[serde(rename = "toolResponse")]
99 tool_response: serde_json::Value,
100 #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
103 thought_signature: Option<String>,
104 },
105 ExecutableCode {
107 #[serde(rename = "executableCode")]
108 executable_code: serde_json::Value,
109 #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
112 thought_signature: Option<String>,
113 },
114 CodeExecutionResult {
116 #[serde(rename = "codeExecutionResult")]
117 code_execution_result: serde_json::Value,
118 #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
121 thought_signature: Option<String>,
122 },
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
127#[serde(rename_all = "camelCase")]
128pub struct Blob {
129 pub mime_type: String,
131 pub data: String,
133}
134
135impl Blob {
136 pub fn new(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
138 Self { mime_type: mime_type.into(), data: data.into() }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155#[serde(rename_all = "camelCase")]
156pub struct FileDataRef {
157 pub mime_type: String,
158 pub file_uri: String,
159}
160
161#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
163#[serde(rename_all = "camelCase")]
164pub struct Content {
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub parts: Option<Vec<Part>>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub role: Option<Role>,
171}
172
173impl Content {
174 pub fn text(text: impl Into<String>) -> Self {
176 Self {
177 parts: Some(vec![Part::Text {
178 text: text.into(),
179 thought: None,
180 thought_signature: None,
181 }]),
182 role: None,
183 }
184 }
185
186 pub fn function_call(function_call: super::tools::FunctionCall) -> Self {
188 Self {
189 parts: Some(vec![Part::FunctionCall { function_call, thought_signature: None }]),
190 role: None,
191 }
192 }
193
194 pub fn function_call_with_thought(
196 function_call: super::tools::FunctionCall,
197 thought_signature: impl Into<String>,
198 ) -> Self {
199 Self {
200 parts: Some(vec![Part::FunctionCall {
201 function_call,
202 thought_signature: Some(thought_signature.into()),
203 }]),
204 role: None,
205 }
206 }
207
208 pub fn text_with_thought_signature(
210 text: impl Into<String>,
211 thought_signature: impl Into<String>,
212 ) -> Self {
213 Self {
214 parts: Some(vec![Part::Text {
215 text: text.into(),
216 thought: None,
217 thought_signature: Some(thought_signature.into()),
218 }]),
219 role: None,
220 }
221 }
222
223 pub fn thought_with_signature(
225 text: impl Into<String>,
226 thought_signature: impl Into<String>,
227 ) -> Self {
228 Self {
229 parts: Some(vec![Part::Text {
230 text: text.into(),
231 thought: Some(true),
232 thought_signature: Some(thought_signature.into()),
233 }]),
234 role: None,
235 }
236 }
237
238 pub fn function_response(function_response: super::tools::FunctionResponse) -> Self {
240 Self {
241 parts: Some(vec![Part::FunctionResponse {
242 function_response,
243 thought_signature: None,
244 }]),
245 role: None,
246 }
247 }
248
249 pub fn function_response_json(name: impl Into<String>, response: serde_json::Value) -> Self {
251 Self {
252 parts: Some(vec![Part::FunctionResponse {
253 function_response: super::tools::FunctionResponse::new(name, response),
254 thought_signature: None,
255 }]),
256 role: None,
257 }
258 }
259
260 pub fn inline_data(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
262 Self {
263 parts: Some(vec![Part::InlineData { inline_data: Blob::new(mime_type, data) }]),
264 role: None,
265 }
266 }
267
268 pub fn function_response_multimodal(function_response: super::tools::FunctionResponse) -> Self {
274 Self {
275 parts: Some(vec![Part::FunctionResponse {
276 function_response,
277 thought_signature: None,
278 }]),
279 role: None,
280 }
281 }
282
283 pub fn with_role(mut self, role: Role) -> Self {
285 self.role = Some(role);
286 self
287 }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct Message {
293 pub content: Content,
295 pub role: Role,
297}
298
299impl Message {
300 pub fn user(text: impl Into<String>) -> Self {
302 Self { content: Content::text(text).with_role(Role::User), role: Role::User }
303 }
304
305 pub fn model(text: impl Into<String>) -> Self {
307 Self { content: Content::text(text).with_role(Role::Model), role: Role::Model }
308 }
309
310 pub fn embed(text: impl Into<String>) -> Self {
312 Self { content: Content::text(text), role: Role::Model }
313 }
314
315 pub fn function(name: impl Into<String>, response: serde_json::Value) -> Self {
317 Self {
318 content: Content::function_response_json(name, response).with_role(Role::Model),
319 role: Role::Model,
320 }
321 }
322
323 pub fn function_str(
325 name: impl Into<String>,
326 response: impl Into<String>,
327 ) -> Result<Self, serde_json::Error> {
328 let response_str = response.into();
329 let json = serde_json::from_str(&response_str)?;
330 Ok(Self {
331 content: Content::function_response_json(name, json).with_role(Role::Model),
332 role: Role::Model,
333 })
334 }
335}
336
337#[derive(Debug, Clone, Serialize, PartialEq)]
339#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
340pub enum Modality {
341 ModalityUnspecified,
343 Text,
345 Image,
347 Audio,
349 Video,
351 Document,
353 Unknown,
355}
356
357impl Modality {
358 fn from_wire_str(value: &str) -> Self {
359 match value {
360 "MODALITY_UNSPECIFIED" => Self::ModalityUnspecified,
361 "TEXT" => Self::Text,
362 "IMAGE" => Self::Image,
363 "AUDIO" => Self::Audio,
364 "VIDEO" => Self::Video,
365 "DOCUMENT" => Self::Document,
366 _ => Self::Unknown,
367 }
368 }
369
370 fn from_wire_number(value: i64) -> Self {
371 match value {
372 0 => Self::ModalityUnspecified,
373 1 => Self::Text,
374 2 => Self::Image,
375 3 => Self::Video,
376 4 => Self::Audio,
377 5 => Self::Document,
378 _ => Self::Unknown,
379 }
380 }
381}
382
383impl<'de> Deserialize<'de> for Modality {
384 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
385 where
386 D: serde::Deserializer<'de>,
387 {
388 let value = serde_json::Value::deserialize(deserializer)?;
389 match value {
390 serde_json::Value::String(s) => Ok(Self::from_wire_str(&s)),
391 serde_json::Value::Number(n) => n
392 .as_i64()
393 .map(Self::from_wire_number)
394 .ok_or_else(|| de::Error::custom("modality must be an integer-compatible number")),
395 _ => Err(de::Error::custom("modality must be a string or integer")),
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn test_tool_call_deserialize_and_roundtrip() {
406 let json = r#"{"toolCall": {"name": "google_search", "args": {"query": "rust lang"}}}"#;
407 let part: Part = serde_json::from_str(json).expect("should deserialize toolCall");
408 match &part {
409 Part::ToolCall { tool_call, .. } => {
410 assert_eq!(tool_call["name"], "google_search");
411 assert_eq!(tool_call["args"]["query"], "rust lang");
412 }
413 other => panic!("expected Part::ToolCall, got {other:?}"),
414 }
415 let serialized = serde_json::to_string(&part).expect("should serialize");
417 let deserialized: Part =
418 serde_json::from_str(&serialized).expect("should deserialize again");
419 assert_eq!(part, deserialized);
420 }
421
422 #[test]
423 fn test_tool_response_deserialize_and_roundtrip() {
424 let json = r#"{"toolResponse": {"name": "google_search", "output": {"results": []}}, "thoughtSignature": "sig_123"}"#;
425 let part: Part = serde_json::from_str(json).expect("should deserialize toolResponse");
426 match &part {
427 Part::ToolResponse { tool_response, thought_signature } => {
428 assert_eq!(tool_response["name"], "google_search");
429 assert_eq!(tool_response["output"]["results"], serde_json::json!([]));
430 assert_eq!(thought_signature.as_deref(), Some("sig_123"));
431 }
432 other => panic!("expected Part::ToolResponse, got {other:?}"),
433 }
434 let serialized = serde_json::to_string(&part).expect("should serialize");
436 let deserialized: Part =
437 serde_json::from_str(&serialized).expect("should deserialize again");
438 assert_eq!(part, deserialized);
439 }
440
441 #[test]
442 fn test_code_execution_parts_preserve_thought_signature() {
443 let executable = serde_json::json!({
444 "executableCode": { "language": "python", "code": "print(1)" },
445 "thoughtSignature": "sig_exec"
446 });
447 let result = serde_json::json!({
448 "codeExecutionResult": { "outcome": "OUTCOME_OK", "output": "1" },
449 "thoughtSignature": "sig_result"
450 });
451
452 let executable_part: Part =
453 serde_json::from_value(executable).expect("should deserialize executable code");
454 let result_part: Part =
455 serde_json::from_value(result).expect("should deserialize code execution result");
456
457 match executable_part {
458 Part::ExecutableCode { thought_signature, .. } => {
459 assert_eq!(thought_signature.as_deref(), Some("sig_exec"));
460 }
461 other => panic!("expected Part::ExecutableCode, got {other:?}"),
462 }
463
464 match result_part {
465 Part::CodeExecutionResult { thought_signature, .. } => {
466 assert_eq!(thought_signature.as_deref(), Some("sig_result"));
467 }
468 other => panic!("expected Part::CodeExecutionResult, got {other:?}"),
469 }
470 }
471
472 #[test]
475 fn test_file_data_ref_serde_round_trip() {
476 let file_ref = FileDataRef {
477 mime_type: "application/pdf".to_string(),
478 file_uri: "gs://bucket/report.pdf".to_string(),
479 };
480 let json = serde_json::to_string(&file_ref).unwrap();
481 assert!(json.contains("mimeType"));
482 assert!(json.contains("fileUri"));
483 let deserialized: FileDataRef = serde_json::from_str(&json).unwrap();
484 assert_eq!(file_ref, deserialized);
485 }
486
487 #[test]
488 fn test_part_file_data_serde_round_trip() {
489 let part = Part::FileData {
490 file_data: FileDataRef {
491 mime_type: "image/jpeg".to_string(),
492 file_uri: "https://example.com/img.jpg".to_string(),
493 },
494 };
495 let json = serde_json::to_string(&part).unwrap();
496 assert!(json.contains("fileData"));
497 let deserialized: Part = serde_json::from_str(&json).unwrap();
498 assert_eq!(part, deserialized);
499 }
500
501 #[test]
502 fn test_function_response_new_backward_compat() {
503 let fr =
504 super::super::tools::FunctionResponse::new("tool", serde_json::json!({"ok": true}));
505 let json = serde_json::to_string(&fr).unwrap();
506 let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&json).unwrap();
508 assert!(map.contains_key("name"));
509 assert!(map.contains_key("response"));
510 assert!(!map.contains_key("inline_data"));
511 assert!(!map.contains_key("file_data"));
512 }
513
514 #[test]
515 fn test_function_response_with_inline_data_constructor() {
516 let blobs = vec![Blob::new("image/png", "base64data")];
517 let fr = super::super::tools::FunctionResponse::with_inline_data(
518 "chart",
519 serde_json::json!({"status": "ok"}),
520 blobs.clone(),
521 );
522 assert_eq!(fr.name, "chart");
523 assert_eq!(fr.parts.len(), 1);
524 assert!(matches!(
525 &fr.parts[0],
526 super::super::tools::FunctionResponsePart::InlineData { inline_data }
527 if inline_data == &blobs[0]
528 ));
529 }
530
531 #[test]
532 fn test_function_response_with_file_data_constructor() {
533 let files = vec![FileDataRef {
534 mime_type: "application/pdf".to_string(),
535 file_uri: "gs://b/f.pdf".to_string(),
536 }];
537 let fr = super::super::tools::FunctionResponse::with_file_data(
538 "doc",
539 serde_json::json!({"ok": true}),
540 files.clone(),
541 );
542 assert_eq!(fr.name, "doc");
543 assert_eq!(fr.parts.len(), 1);
544 assert!(matches!(
545 &fr.parts[0],
546 super::super::tools::FunctionResponsePart::FileData { file_data }
547 if file_data == &files[0]
548 ));
549 }
550
551 #[test]
552 fn test_function_response_inline_data_only_constructor() {
553 let blobs = vec![Blob::new("audio/wav", "audiodata")];
554 let fr =
555 super::super::tools::FunctionResponse::inline_data_only("audio_tool", blobs.clone());
556 assert_eq!(fr.name, "audio_tool");
557 assert!(fr.response.is_none());
558 assert_eq!(fr.parts.len(), 1);
559 }
560
561 #[test]
562 fn test_content_function_response_multimodal_parts_nested() {
563 use super::super::tools::FunctionResponsePart;
564 let blobs = [Blob::new("image/png", "img1"), Blob::new("image/jpeg", "img2")];
565 let files = [FileDataRef {
566 mime_type: "application/pdf".to_string(),
567 file_uri: "gs://b/f.pdf".to_string(),
568 }];
569 let mut fr_parts: Vec<FunctionResponsePart> = blobs
570 .iter()
571 .map(|b| FunctionResponsePart::InlineData { inline_data: b.clone() })
572 .collect();
573 fr_parts
574 .extend(files.iter().map(|f| FunctionResponsePart::FileData { file_data: f.clone() }));
575 let fr = super::super::tools::FunctionResponse {
576 name: "tool".to_string(),
577 response: Some(serde_json::json!({"ok": true})),
578 parts: fr_parts,
579 };
580 let content = Content::function_response_multimodal(fr);
581 let content_parts = content.parts.unwrap();
582 assert_eq!(content_parts.len(), 1);
584 assert!(matches!(&content_parts[0], Part::FunctionResponse { .. }));
585 if let Part::FunctionResponse { function_response, .. } = &content_parts[0] {
587 assert_eq!(function_response.parts.len(), 3);
589 } else {
590 panic!("expected FunctionResponse part");
591 }
592 }
593
594 #[test]
595 fn test_multimodal_function_response_wire_format() {
596 use super::super::tools::FunctionResponsePart;
599 let fr = super::super::tools::FunctionResponse {
600 name: "get_image".to_string(),
601 response: Some(serde_json::json!({"image_ref": {"$ref": "photo.jpg"}})),
602 parts: vec![FunctionResponsePart::InlineData {
603 inline_data: Blob::new("image/jpeg", "base64encodeddata"),
604 }],
605 };
606
607 let part = Part::FunctionResponse { function_response: fr, thought_signature: None };
608 let json = serde_json::to_value(&part).unwrap();
609
610 let fr_obj = &json["functionResponse"];
612 assert_eq!(fr_obj["name"], "get_image");
613 assert!(fr_obj["response"].is_object());
614 assert!(fr_obj["parts"].is_array());
615 assert_eq!(fr_obj["parts"].as_array().unwrap().len(), 1);
616
617 let inline = &fr_obj["parts"][0]["inlineData"];
619 assert_eq!(inline["mimeType"], "image/jpeg");
620 assert_eq!(inline["data"], "base64encodeddata");
621 }
622
623 #[test]
624 fn test_json_only_function_response_has_no_parts_key() {
625 let fr = super::super::tools::FunctionResponse::new(
627 "simple_tool",
628 serde_json::json!({"result": "ok"}),
629 );
630 let part = Part::FunctionResponse { function_response: fr, thought_signature: None };
631 let json = serde_json::to_string(&part).unwrap();
632 assert!(
634 !json.contains(r#""parts""#),
635 "JSON-only response should not have parts key: {json}"
636 );
637 }
638}