From f891241cbf063a5a53a2039974fca39ad1b27c7b Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Thu, 23 Oct 2025 11:13:07 +0200 Subject: [PATCH 1/6] support `parameters_json_schema` in function declaration Signed-off-by: Jean Mertz --- src/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.rs b/src/types.rs index 0f4e993..0be73a3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -171,6 +171,7 @@ pub struct FunctionDeclaration { pub name: String, pub description: String, pub parameters: Option, + pub parameters_json_schema: Option, pub response: Option, } From 18bb9d94e69c4058fa79200f94ee489a69acf05c Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Thu, 20 Nov 2025 10:34:48 +0100 Subject: [PATCH 2/6] sometimes gemini returns no parts? Signed-off-by: Jean Mertz --- src/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.rs b/src/types.rs index 0be73a3..aedc7d5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -99,6 +99,7 @@ pub enum FunctionCallingMode { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Content { + #[serde(default)] pub parts: Vec, // Optional. The producer of the content. Must be either 'user' or 'model'. // Useful to set for multi-turn conversations, otherwise can be left blank or unset. From 412184789f5ae9bff1822ec6edb06b7b9fde8dfa Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Thu, 20 Nov 2025 10:35:11 +0100 Subject: [PATCH 3/6] improve error debugging capabilities Signed-off-by: Jean Mertz --- src/lib.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 007b29b..6cf8dca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,8 +43,12 @@ pub enum GeminiError { EventSource(#[from] reqwest_eventsource::Error), #[error("API Error: {0}")] Api(Value), - #[error("JSON Error: {0}")] - Json(#[from] serde_json::Error), + #[error("JSON Error: {error} (payload: {data})")] + Json { + data: String, + #[source] + error: serde_json::Error, + }, #[error("Function execution error: {0}")] FunctionExecution(String), } @@ -210,7 +214,10 @@ impl GeminiClient { Event::Open => (), Event::Message(event) => yield serde_json::from_str::(&event.data) - .map_err(Into::into), + .map_err(|error| GeminiError::Json { + data: event.data, + error, + }), }, Err(e) => match e { reqwest_eventsource::Error::StreamEnded => stream.close(), From 554e3f45b922077a27b7cdd141fb9d2a08148472 Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Thu, 20 Nov 2025 16:47:42 +0100 Subject: [PATCH 4/6] add thinking_level support Signed-off-by: Jean Mertz --- src/types.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/types.rs b/src/types.rs index aedc7d5..699e358 100644 --- a/src/types.rs +++ b/src/types.rs @@ -346,6 +346,25 @@ pub struct ThinkingConfig { pub include_thoughts: bool, /// The number of thoughts tokens that the model should generate. pub thinking_budget: Option, + /// Controls the maximum depth of the model's internal reasoning process + /// before it produces a response. If not specified, the default is HIGH. + /// Recommended for Gemini 3 or later models. Use with earlier models + /// results in an error. + pub thinking_level: Option, +} + +/// Allow user to specify how much to think using enum instead of integer +/// budget. +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ThinkingLevel { + /// Unspecified thinking level. + #[default] + ThinkingLevelUnspecified, + /// High thinking level. + High, + /// Low thinking level. + Low, } /// A response candidate generated from the model. From c198dd843c2dccd877ac6839d0c9af3fd948d75b Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Wed, 26 Nov 2025 14:35:41 +0100 Subject: [PATCH 5/6] expand types Signed-off-by: Jean Mertz --- src/lib.rs | 1 + src/types.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6cf8dca..78ded49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -268,6 +268,7 @@ impl GeminiClient { Ok(result) => { request.contents.push(Content { parts: vec![ContentData::FunctionResponse(FunctionResponse { + id: function_call.id.clone(), name: function_call.name.clone(), response: FunctionResponsePayload { content: result }, }) diff --git a/src/types.rs b/src/types.rs index 699e358..9d9882f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -728,6 +728,8 @@ pub struct ContentPart { pub data: ContentData, #[serde(skip_serializing)] pub metadata: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub thought_signature: Option, } impl ContentPart { @@ -736,6 +738,7 @@ impl ContentPart { data: ContentData::Text(text.to_string()), thought, metadata: None, + thought_signature: None, } } @@ -747,6 +750,7 @@ impl ContentPart { }), thought, metadata: None, + thought_signature: None, } } @@ -758,17 +762,25 @@ impl ContentPart { }), thought: false, metadata: None, + thought_signature: None, } } - pub fn new_function_call(name: &str, arguments: Value, thought: bool) -> Self { + pub fn new_function_call( + id: Option<&str>, + name: &str, + arguments: Value, + thought: bool, + ) -> Self { Self { data: ContentData::FunctionCall(FunctionCall { + id: id.map(|s| s.to_string()), name: name.to_string(), arguments, }), thought, metadata: None, + thought_signature: None, } } @@ -779,6 +791,7 @@ impl ContentPart { }), thought: false, metadata: None, + thought_signature: None, } } @@ -787,17 +800,20 @@ impl ContentPart { data: ContentData::CodeExecutionResult(content), thought: false, metadata: None, + thought_signature: None, } } - pub fn new_function_response(name: &str, content: Value) -> Self { + pub fn new_function_response(id: Option<&str>, name: &str, content: Value) -> Self { Self { data: ContentData::FunctionResponse(FunctionResponse { + id: id.map(|s| s.to_string()), name: name.to_string(), response: FunctionResponsePayload { content }, }), thought: false, metadata: None, + thought_signature: None, } } } @@ -812,6 +828,7 @@ impl From for ContentPart { data, thought: false, metadata: None, + thought_signature: None, } } } @@ -831,14 +848,18 @@ pub enum ContentData { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FunctionCall { + #[serde(default)] + pub id: Option, pub name: String, - #[serde(rename = "args")] + #[serde(default, rename = "args")] pub arguments: serde_json::Value, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FunctionResponse { + #[serde(default)] + pub id: Option, pub name: String, pub response: FunctionResponsePayload, } From e745b8824cd3aa2d1914f6beadc577a98c59821d Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Tue, 9 Dec 2025 15:49:17 +0100 Subject: [PATCH 6/6] add missing `UnexpectedToolCall` Signed-off-by: Jean Mertz --- src/lib.rs | 6 ++++-- src/types.rs | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 78ded49..b4f6c90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -255,12 +255,14 @@ impl GeminiClient { return Ok(response); }; - let Some(part) = candidate.content.parts.first() else { + let Some(part) = candidate.content.as_ref().and_then(|c| c.parts.first()) else { return Ok(response); }; if let ContentData::FunctionCall(function_call) = &part.data { - request.contents.push(candidate.content.clone()); + if let Some(content) = candidate.content.clone() { + request.contents.push(content); + } if let Some(handler) = function_handlers.get(&function_call.name) { let mut args = function_call.arguments.clone(); diff --git a/src/types.rs b/src/types.rs index 9d9882f..7b788c3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -245,8 +245,10 @@ pub struct GenerateContentResponse { pub candidates: Vec, pub prompt_feedback: Option, pub usage_metadata: UsageMetadata, - pub model_version: String, - pub response_id: String, + #[serde(default)] + pub model_version: Option, + #[serde(default)] + pub response_id: Option, } /// Specifies the reason why the prompt was blocked. @@ -372,7 +374,14 @@ pub enum ThinkingLevel { #[serde(rename_all = "camelCase")] pub struct Candidate { /// Generated content returned from the model. - pub content: Content, + /// + /// This field is not always populated, e.g.: + /// + /// ```json + /// {"candidates": [{"finishReason": "UNEXPECTED_TOOL_CALL","index": 0}]} + /// ``` + #[serde(default)] + pub content: Option, /// The reason why the model stopped generating tokens. If empty, the model /// has not stopped generating tokens. pub finish_reason: Option, @@ -717,6 +726,8 @@ pub enum FinishReason { /// Token generation stopped because generated images contain safety /// violations. ImageSafety, + /// Model generated a tool call but no tools were enabled in the request. + UnexpectedToolCall, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]