1
2use std::mem;
3
4use anyhow::{Result, anyhow, bail};
5use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
6use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8pub use settings::ModelMode as GoogleModelMode;
9
10pub const API_URL: &str = "https://generativelanguage.googleapis.com";
11
12pub async fn stream_generate_content(
13 client: &dyn HttpClient,
14 api_url: &str,
15 api_key: &str,
16 mut request: GenerateContentRequest,
17) -> Result<BoxStream<'static, Result<GenerateContentResponse>>> {
18 let api_key = api_key.trim();
19 validate_generate_content_request(&request)?;
20
21 // The `model` field is emptied as it is provided as a path parameter.
22 let model_id = mem::take(&mut request.model.model_id);
23
24 let uri =
25 format!("{api_url}/v1beta/models/{model_id}:streamGenerateContent?alt=sse&key={api_key}",);
26
27 let request_builder = HttpRequest::builder()
28 .method(Method::POST)
29 .uri(uri)
30 .header("Content-Type", "application/json");
31
32 let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
33 let mut response = client.send(request).await?;
34 if response.status().is_success() {
35 let reader = BufReader::new(response.into_body());
36 Ok(reader
37 .lines()
38 .filter_map(|line| async move {
39 match line {
40 Ok(line) => {
41 if let Some(line) = line.strip_prefix("data: ") {
42 match serde_json::from_str(line) {
43 Ok(response) => Some(Ok(response)),
44 Err(error) => Some(Err(anyhow!(format!(
45 "Error parsing JSON: {error:?}\n{line:?}"
46 )))),
47 }
48 } else {
49 None
50 }
51 }
52 Err(error) => Some(Err(anyhow!(error))),
53 }
54 })
55 .boxed())
56 } else {
57 let mut text = String::new();
58 response.body_mut().read_to_string(&mut text).await?;
59 Err(anyhow!(
60 "error during streamGenerateContent, status code: {:?}, body: {}",
61 response.status(),
62 text
63 ))
64 }
65}
66
67pub async fn count_tokens(
68 client: &dyn HttpClient,
69 api_url: &str,
70 api_key: &str,
71 request: CountTokensRequest,
72) -> Result<CountTokensResponse> {
73 validate_generate_content_request(&request.generate_content_request)?;
74
75 let uri = format!(
76 "{api_url}/v1beta/models/{model_id}:countTokens?key={api_key}",
77 model_id = &request.generate_content_request.model.model_id,
78 );
79
80 let request = serde_json::to_string(&request)?;
81 let request_builder = HttpRequest::builder()
82 .method(Method::POST)
83 .uri(&uri)
84 .header("Content-Type", "application/json");
85 let http_request = request_builder.body(AsyncBody::from(request))?;
86
87 let mut response = client.send(http_request).await?;
88 let mut text = String::new();
89 response.body_mut().read_to_string(&mut text).await?;
90 anyhow::ensure!(
91 response.status().is_success(),
92 "error during countTokens, status code: {:?}, body: {}",
93 response.status(),
94 text
95 );
96 Ok(serde_json::from_str::<CountTokensResponse>(&text)?)
97}
98
99pub fn validate_generate_content_request(request: &GenerateContentRequest) -> Result<()> {
100 if request.model.is_empty() {
101 bail!("Model must be specified");
102 }
103
104 if request.contents.is_empty() {
105 bail!("Request must contain at least one content item");
106 }
107
108 if let Some(user_content) = request
109 .contents
110 .iter()
111 .find(|content| content.role == Role::User)
112 && user_content.parts.is_empty()
113 {
114 bail!("User content must contain at least one part");
115 }
116
117 Ok(())
118}
119
120#[derive(Debug, Serialize, Deserialize)]
121pub enum Task {
122 #[serde(rename = "generateContent")]
123 GenerateContent,
124 #[serde(rename = "streamGenerateContent")]
125 StreamGenerateContent,
126 #[serde(rename = "countTokens")]
127 CountTokens,
128 #[serde(rename = "embedContent")]
129 EmbedContent,
130 #[serde(rename = "batchEmbedContents")]
131 BatchEmbedContents,
132}
133
134#[derive(Debug, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct GenerateContentRequest {
137 #[serde(default, skip_serializing_if = "ModelName::is_empty")]
138 pub model: ModelName,
139 pub contents: Vec<Content>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub system_instruction: Option<SystemInstruction>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub generation_config: Option<GenerationConfig>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub safety_settings: Option<Vec<SafetySetting>>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub tools: Option<Vec<Tool>>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub tool_config: Option<ToolConfig>,
150}
151
152#[derive(Debug, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct GenerateContentResponse {
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub candidates: Option<Vec<GenerateContentCandidate>>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub prompt_feedback: Option<PromptFeedback>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub usage_metadata: Option<UsageMetadata>,
161}
162
163#[derive(Debug, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct GenerateContentCandidate {
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub index: Option<usize>,
168 pub content: Content,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub finish_reason: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub finish_message: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub safety_ratings: Option<Vec<SafetyRating>>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub citation_metadata: Option<CitationMetadata>,
177}
178
179#[derive(Debug, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct Content {
182 #[serde(default)]
183 pub parts: Vec<Part>,
184 pub role: Role,
185}
186
187#[derive(Debug, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct SystemInstruction {
190 pub parts: Vec<Part>,
191}
192
193#[derive(Debug, PartialEq, Deserialize, Serialize)]
194#[serde(rename_all = "camelCase")]
195pub enum Role {
196 User,
197 Model,
198}
199
200#[derive(Debug, Serialize, Deserialize)]
201#[serde(untagged)]
202pub enum Part {
203 TextPart(TextPart),
204 InlineDataPart(InlineDataPart),
205 FunctionCallPart(FunctionCallPart),
206 FunctionResponsePart(FunctionResponsePart),
207 ThoughtPart(ThoughtPart),
208}
209
210#[derive(Debug, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct TextPart {
213 pub text: String,
214}
215
216#[derive(Debug, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct InlineDataPart {
219 pub inline_data: GenerativeContentBlob,
220}
221
222#[derive(Debug, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct GenerativeContentBlob {
225 pub mime_type: String,
226 pub data: String,
227}
228
229#[derive(Debug, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct FunctionCallPart {
232 pub function_call: FunctionCall,
233 /// Thought signature returned by the model for function calls.
234 /// Only present on the first function call in parallel call scenarios.
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub thought_signature: Option<String>,
237}
238
239#[derive(Debug, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct FunctionResponsePart {
242 pub function_response: FunctionResponse,
243}
244
245#[derive(Debug, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct ThoughtPart {
248 pub thought: bool,
249 pub thought_signature: String,
250}
251
252#[derive(Debug, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct CitationSource {
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub start_index: Option<usize>,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub end_index: Option<usize>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub uri: Option<String>,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub license: Option<String>,
263}
264
265#[derive(Debug, Serialize, Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct CitationMetadata {
268 pub citation_sources: Vec<CitationSource>,
269}
270
271#[derive(Debug, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct PromptFeedback {
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub block_reason: Option<String>,
276 pub safety_ratings: Option<Vec<SafetyRating>>,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub block_reason_message: Option<String>,
279}
280
281#[derive(Debug, Serialize, Deserialize, Default)]
282#[serde(rename_all = "camelCase")]
283pub struct UsageMetadata {
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub prompt_token_count: Option<u64>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub cached_content_token_count: Option<u64>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub candidates_token_count: Option<u64>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub tool_use_prompt_token_count: Option<u64>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub thoughts_token_count: Option<u64>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub total_token_count: Option<u64>,
296}
297
298#[derive(Debug, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct ThinkingConfig {
301 pub thinking_budget: u32,
302}
303
304#[derive(Debug, Deserialize, Serialize)]
305#[serde(rename_all = "camelCase")]
306pub struct GenerationConfig {
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub candidate_count: Option<usize>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub stop_sequences: Option<Vec<String>>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub max_output_tokens: Option<usize>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub temperature: Option<f64>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub top_p: Option<f64>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub top_k: Option<usize>,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub thinking_config: Option<ThinkingConfig>,
321}
322
323#[derive(Debug, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct SafetySetting {
326 pub category: HarmCategory,
327 pub threshold: HarmBlockThreshold,
328}
329
330#[derive(Debug, Serialize, Deserialize)]
331pub enum HarmCategory {
332 #[serde(rename = "HARM_CATEGORY_UNSPECIFIED")]
333 Unspecified,
334 #[serde(rename = "HARM_CATEGORY_DEROGATORY")]
335 Derogatory,
336 #[serde(rename = "HARM_CATEGORY_TOXICITY")]
337 Toxicity,
338 #[serde(rename = "HARM_CATEGORY_VIOLENCE")]
339 Violence,
340 #[serde(rename = "HARM_CATEGORY_SEXUAL")]
341 Sexual,
342 #[serde(rename = "HARM_CATEGORY_MEDICAL")]
343 Medical,
344 #[serde(rename = "HARM_CATEGORY_DANGEROUS")]
345 Dangerous,
346 #[serde(rename = "HARM_CATEGORY_HARASSMENT")]
347 Harassment,
348 #[serde(rename = "HARM_CATEGORY_HATE_SPEECH")]
349 HateSpeech,
350 #[serde(rename = "HARM_CATEGORY_SEXUALLY_EXPLICIT")]
351 SexuallyExplicit,
352 #[serde(rename = "HARM_CATEGORY_DANGEROUS_CONTENT")]
353 DangerousContent,
354}
355
356#[derive(Debug, Serialize, Deserialize)]
357#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
358pub enum HarmBlockThreshold {
359 #[serde(rename = "HARM_BLOCK_THRESHOLD_UNSPECIFIED")]
360 Unspecified,
361 BlockLowAndAbove,
362 BlockMediumAndAbove,
363 BlockOnlyHigh,
364 BlockNone,
365}
366
367#[derive(Debug, Serialize, Deserialize)]
368#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
369pub enum HarmProbability {
370 #[serde(rename = "HARM_PROBABILITY_UNSPECIFIED")]
371 Unspecified,
372 Negligible,
373 Low,
374 Medium,
375 High,
376}
377
378#[derive(Debug, Serialize, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct SafetyRating {
381 pub category: HarmCategory,
382 pub probability: HarmProbability,
383}
384
385#[derive(Debug, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct CountTokensRequest {
388 pub generate_content_request: GenerateContentRequest,
389}
390
391#[derive(Debug, Serialize, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct CountTokensResponse {
394 pub total_tokens: u64,
395}
396
397#[derive(Debug, Serialize, Deserialize)]
398pub struct FunctionCall {
399 pub name: String,
400 pub args: serde_json::Value,
401}
402
403#[derive(Debug, Serialize, Deserialize)]
404pub struct FunctionResponse {
405 pub name: String,
406 pub response: serde_json::Value,
407}
408
409#[derive(Debug, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct Tool {
412 pub function_declarations: Vec<FunctionDeclaration>,
413}
414
415#[derive(Debug, Serialize, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub struct ToolConfig {
418 pub function_calling_config: FunctionCallingConfig,
419}
420
421#[derive(Debug, Serialize, Deserialize)]
422#[serde(rename_all = "camelCase")]
423pub struct FunctionCallingConfig {
424 pub mode: FunctionCallingMode,
425 #[serde(skip_serializing_if = "Option::is_none")]
426 pub allowed_function_names: Option<Vec<String>>,
427}
428
429#[derive(Debug, Serialize, Deserialize)]
430#[serde(rename_all = "lowercase")]
431pub enum FunctionCallingMode {
432 Auto,
433 Any,
434 None,
435}
436
437#[derive(Debug, Serialize, Deserialize)]
438pub struct FunctionDeclaration {
439 pub name: String,
440 pub description: String,
441 pub parameters: serde_json::Value,
442}
443
444#[derive(Debug, Default)]
445pub struct ModelName {
446 pub model_id: String,
447}
448
449impl ModelName {
450 pub fn is_empty(&self) -> bool {
451 self.model_id.is_empty()
452 }
453}
454
455const MODEL_NAME_PREFIX: &str = "models/";
456
457impl Serialize for ModelName {
458 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
459 where
460 S: Serializer,
461 {
462 serializer.serialize_str(&format!("{MODEL_NAME_PREFIX}{}", &self.model_id))
463 }
464}
465
466impl<'de> Deserialize<'de> for ModelName {
467 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
468 where
469 D: Deserializer<'de>,
470 {
471 let string = String::deserialize(deserializer)?;
472 if let Some(id) = string.strip_prefix(MODEL_NAME_PREFIX) {
473 Ok(Self {
474 model_id: id.to_string(),
475 })
476 } else {
477 Err(serde::de::Error::custom(format!(
478 "Expected model name to begin with {}, got: {}",
479 MODEL_NAME_PREFIX, string
480 )))
481 }
482 }
483}
484
485#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
486#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, strum::EnumIter)]
487pub enum Model {
488 #[serde(
489 rename = "gemini-2.5-flash-lite",
490 alias = "gemini-2.5-flash-lite-preview-06-17",
491 alias = "gemini-2.0-flash-lite-preview"
492 )]
493 Gemini25FlashLite,
494 #[serde(
495 rename = "gemini-2.5-flash",
496 alias = "gemini-2.0-flash-thinking-exp",
497 alias = "gemini-2.5-flash-preview-04-17",
498 alias = "gemini-2.5-flash-preview-05-20",
499 alias = "gemini-2.5-flash-preview-latest",
500 alias = "gemini-2.0-flash"
501 )]
502 #[default]
503 Gemini25Flash,
504 #[serde(
505 rename = "gemini-2.5-pro",
506 alias = "gemini-2.0-pro-exp",
507 alias = "gemini-2.5-pro-preview-latest",
508 alias = "gemini-2.5-pro-exp-03-25",
509 alias = "gemini-2.5-pro-preview-03-25",
510 alias = "gemini-2.5-pro-preview-05-06",
511 alias = "gemini-2.5-pro-preview-06-05"
512 )]
513 Gemini25Pro,
514 #[serde(rename = "gemini-3-pro-preview")]
515 Gemini3Pro,
516 #[serde(rename = "custom")]
517 Custom {
518 name: String,
519 /// The name displayed in the UI, such as in the assistant panel model dropdown menu.
520 display_name: Option<String>,
521 max_tokens: u64,
522 #[serde(default)]
523 mode: GoogleModelMode,
524 },
525}
526
527impl Model {
528 pub fn default_fast() -> Self {
529 Self::Gemini25FlashLite
530 }
531
532 pub fn id(&self) -> &str {
533 match self {
534 Self::Gemini25FlashLite => "gemini-2.5-flash-lite",
535 Self::Gemini25Flash => "gemini-2.5-flash",
536 Self::Gemini25Pro => "gemini-2.5-pro",
537 Self::Gemini3Pro => "gemini-3-pro-preview",
538 Self::Custom { name, .. } => name,
539 }
540 }
541 pub fn request_id(&self) -> &str {
542 match self {
543 Self::Gemini25FlashLite => "gemini-2.5-flash-lite",
544 Self::Gemini25Flash => "gemini-2.5-flash",
545 Self::Gemini25Pro => "gemini-2.5-pro",
546 Self::Gemini3Pro => "gemini-3-pro-preview",
547 Self::Custom { name, .. } => name,
548 }
549 }
550
551 pub fn display_name(&self) -> &str {
552 match self {
553 Self::Gemini25FlashLite => "Gemini 2.5 Flash-Lite",
554 Self::Gemini25Flash => "Gemini 2.5 Flash",
555 Self::Gemini25Pro => "Gemini 2.5 Pro",
556 Self::Gemini3Pro => "Gemini 3 Pro",
557 Self::Custom {
558 name, display_name, ..
559 } => display_name.as_ref().unwrap_or(name),
560 }
561 }
562
563 pub fn max_token_count(&self) -> u64 {
564 match self {
565 Self::Gemini25FlashLite => 1_048_576,
566 Self::Gemini25Flash => 1_048_576,
567 Self::Gemini25Pro => 1_048_576,
568 Self::Gemini3Pro => 1_048_576,
569 Self::Custom { max_tokens, .. } => *max_tokens,
570 }
571 }
572
573 pub fn max_output_tokens(&self) -> Option<u64> {
574 match self {
575 Model::Gemini25FlashLite => Some(65_536),
576 Model::Gemini25Flash => Some(65_536),
577 Model::Gemini25Pro => Some(65_536),
578 Model::Gemini3Pro => Some(65_536),
579 Model::Custom { .. } => None,
580 }
581 }
582
583 pub fn supports_tools(&self) -> bool {
584 true
585 }
586
587 pub fn supports_images(&self) -> bool {
588 true
589 }
590
591 pub fn mode(&self) -> GoogleModelMode {
592 match self {
593 Self::Gemini25FlashLite
594 | Self::Gemini25Flash
595 | Self::Gemini25Pro
596 | Self::Gemini3Pro => {
597 GoogleModelMode::Thinking {
598 // By default these models are set to "auto", so we preserve that behavior
599 // but indicate they are capable of thinking mode
600 budget_tokens: None,
601 }
602 }
603 Self::Custom { mode, .. } => *mode,
604 }
605 }
606}
607
608impl std::fmt::Display for Model {
609 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
610 write!(f, "{}", self.id())
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use serde_json::json;
618
619 #[test]
620 fn test_function_call_part_with_signature_serializes_correctly() {
621 let part = FunctionCallPart {
622 function_call: FunctionCall {
623 name: "test_function".to_string(),
624 args: json!({"arg": "value"}),
625 },
626 thought_signature: Some("test_signature".to_string()),
627 };
628
629 let serialized = serde_json::to_value(&part).unwrap();
630
631 assert_eq!(serialized["functionCall"]["name"], "test_function");
632 assert_eq!(serialized["functionCall"]["args"]["arg"], "value");
633 assert_eq!(serialized["thoughtSignature"], "test_signature");
634 }
635
636 #[test]
637 fn test_function_call_part_without_signature_omits_field() {
638 let part = FunctionCallPart {
639 function_call: FunctionCall {
640 name: "test_function".to_string(),
641 args: json!({"arg": "value"}),
642 },
643 thought_signature: None,
644 };
645
646 let serialized = serde_json::to_value(&part).unwrap();
647
648 assert_eq!(serialized["functionCall"]["name"], "test_function");
649 assert_eq!(serialized["functionCall"]["args"]["arg"], "value");
650 // thoughtSignature field should not be present when None
651 assert!(serialized.get("thoughtSignature").is_none());
652 }
653
654 #[test]
655 fn test_function_call_part_deserializes_with_signature() {
656 let json = json!({
657 "functionCall": {
658 "name": "test_function",
659 "args": {"arg": "value"}
660 },
661 "thoughtSignature": "test_signature"
662 });
663
664 let part: FunctionCallPart = serde_json::from_value(json).unwrap();
665
666 assert_eq!(part.function_call.name, "test_function");
667 assert_eq!(part.thought_signature, Some("test_signature".to_string()));
668 }
669
670 #[test]
671 fn test_function_call_part_deserializes_without_signature() {
672 let json = json!({
673 "functionCall": {
674 "name": "test_function",
675 "args": {"arg": "value"}
676 }
677 });
678
679 let part: FunctionCallPart = serde_json::from_value(json).unwrap();
680
681 assert_eq!(part.function_call.name, "test_function");
682 assert_eq!(part.thought_signature, None);
683 }
684
685 #[test]
686 fn test_function_call_part_round_trip() {
687 let original = FunctionCallPart {
688 function_call: FunctionCall {
689 name: "test_function".to_string(),
690 args: json!({"arg": "value", "nested": {"key": "val"}}),
691 },
692 thought_signature: Some("round_trip_signature".to_string()),
693 };
694
695 let serialized = serde_json::to_value(&original).unwrap();
696 let deserialized: FunctionCallPart = serde_json::from_value(serialized).unwrap();
697
698 assert_eq!(deserialized.function_call.name, original.function_call.name);
699 assert_eq!(deserialized.function_call.args, original.function_call.args);
700 assert_eq!(deserialized.thought_signature, original.thought_signature);
701 }
702
703 #[test]
704 fn test_function_call_part_with_empty_signature_serializes() {
705 let part = FunctionCallPart {
706 function_call: FunctionCall {
707 name: "test_function".to_string(),
708 args: json!({"arg": "value"}),
709 },
710 thought_signature: Some("".to_string()),
711 };
712
713 let serialized = serde_json::to_value(&part).unwrap();
714
715 // Empty string should still be serialized (normalization happens at a higher level)
716 assert_eq!(serialized["thoughtSignature"], "");
717 }
718}