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