Skip to content

Commit 91e22b8

Browse files
authored
Merge pull request #16 from JeanMertz/main
support `parameters_json_schema` in function declaration
2 parents ba3964b + 3e5bcf5 commit 91e22b8

2 files changed

Lines changed: 83 additions & 11 deletions

File tree

src/lib.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ pub enum GeminiError {
4343
EventSource(#[from] reqwest_eventsource::Error),
4444
#[error("API Error: {0}")]
4545
Api(Value),
46-
#[error("JSON Error: {0}")]
47-
Json(#[from] serde_json::Error),
46+
#[error("JSON Error: {error} (payload: {data})")]
47+
Json {
48+
data: String,
49+
#[source]
50+
error: serde_json::Error,
51+
},
4852
#[error("Function execution error: {0}")]
4953
FunctionExecution(String),
5054
}
@@ -210,7 +214,10 @@ impl GeminiClient {
210214
Event::Open => (),
211215
Event::Message(event) => yield
212216
serde_json::from_str::<types::GenerateContentResponse>(&event.data)
213-
.map_err(Into::into),
217+
.map_err(|error| GeminiError::Json {
218+
data: event.data,
219+
error,
220+
}),
214221
},
215222
Err(e) => match e {
216223
reqwest_eventsource::Error::StreamEnded => stream.close(),
@@ -248,19 +255,22 @@ impl GeminiClient {
248255
return Ok(response);
249256
};
250257

251-
let Some(part) = candidate.content.parts.first() else {
258+
let Some(part) = candidate.content.as_ref().and_then(|c| c.parts.first()) else {
252259
return Ok(response);
253260
};
254261

255262
if let ContentData::FunctionCall(function_call) = &part.data {
256-
request.contents.push(candidate.content.clone());
263+
if let Some(content) = candidate.content.clone() {
264+
request.contents.push(content);
265+
}
257266

258267
if let Some(handler) = function_handlers.get(&function_call.name) {
259268
let mut args = function_call.arguments.clone();
260269
match handler.execute(&mut args).await {
261270
Ok(result) => {
262271
request.contents.push(Content {
263272
parts: vec![ContentData::FunctionResponse(FunctionResponse {
273+
id: function_call.id.clone(),
264274
name: function_call.name.clone(),
265275
response: FunctionResponsePayload { content: result },
266276
})

src/types.rs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ pub enum FunctionCallingMode {
9999
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
100100
#[serde(rename_all = "camelCase")]
101101
pub struct Content {
102+
#[serde(default)]
102103
pub parts: Vec<ContentPart>,
103104
// Optional. The producer of the content. Must be either 'user' or 'model'.
104105
// Useful to set for multi-turn conversations, otherwise can be left blank or unset.
@@ -114,6 +115,11 @@ pub struct GenerationConfig {
114115
pub response_mime_type: Option<String>,
115116
#[serde(skip_serializing_if = "Option::is_none")]
116117
pub response_schema: Option<serde_json::Value>,
118+
#[serde(
119+
skip_serializing_if = "Option::is_none",
120+
rename = "_responseJsonSchema"
121+
)]
122+
pub response_json_schema: Option<serde_json::Value>,
117123
#[serde(default, skip_serializing_if = "Vec::is_empty")]
118124
pub response_modalities: Vec<String>,
119125
#[serde(skip_serializing_if = "Option::is_none")]
@@ -171,6 +177,7 @@ pub struct FunctionDeclaration {
171177
pub name: String,
172178
pub description: String,
173179
pub parameters: Option<FunctionParameters>,
180+
pub parameters_json_schema: Option<serde_json::Value>,
174181
pub response: Option<FunctionParameters>,
175182
}
176183

@@ -243,8 +250,10 @@ pub struct GenerateContentResponse {
243250
pub candidates: Vec<Candidate>,
244251
pub prompt_feedback: Option<PromptFeedback>,
245252
pub usage_metadata: UsageMetadata,
246-
pub model_version: String,
247-
pub response_id: String,
253+
#[serde(default)]
254+
pub model_version: Option<String>,
255+
#[serde(default)]
256+
pub response_id: Option<String>,
248257
}
249258

250259
/// Specifies the reason why the prompt was blocked.
@@ -344,14 +353,44 @@ pub struct ThinkingConfig {
344353
pub include_thoughts: bool,
345354
/// The number of thoughts tokens that the model should generate.
346355
pub thinking_budget: Option<u32>,
356+
/// Controls the maximum depth of the model's internal reasoning process
357+
/// before it produces a response. If not specified, the default is HIGH.
358+
/// Recommended for Gemini 3 or later models. Use with earlier models
359+
/// results in an error.
360+
pub thinking_level: Option<ThinkingLevel>,
361+
}
362+
363+
/// Allow user to specify how much to think using enum instead of integer
364+
/// budget.
365+
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq)]
366+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
367+
pub enum ThinkingLevel {
368+
/// Unspecified thinking level.
369+
#[default]
370+
ThinkingLevelUnspecified,
371+
/// Minimal thinking level.
372+
Minimal,
373+
/// Low thinking level.
374+
Low,
375+
/// Medium thinking level.
376+
Medium,
377+
/// High thinking level.
378+
High,
347379
}
348380

349381
/// A response candidate generated from the model.
350382
#[derive(Debug, Serialize, Deserialize, Clone)]
351383
#[serde(rename_all = "camelCase")]
352384
pub struct Candidate {
353385
/// Generated content returned from the model.
354-
pub content: Content,
386+
///
387+
/// This field is not always populated, e.g.:
388+
///
389+
/// ```json
390+
/// {"candidates": [{"finishReason": "UNEXPECTED_TOOL_CALL","index": 0}]}
391+
/// ```
392+
#[serde(default)]
393+
pub content: Option<Content>,
355394
/// The reason why the model stopped generating tokens. If empty, the model
356395
/// has not stopped generating tokens.
357396
pub finish_reason: Option<FinishReason>,
@@ -696,6 +735,8 @@ pub enum FinishReason {
696735
/// Token generation stopped because generated images contain safety
697736
/// violations.
698737
ImageSafety,
738+
/// Model generated a tool call but no tools were enabled in the request.
739+
UnexpectedToolCall,
699740
}
700741

701742
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@@ -707,6 +748,8 @@ pub struct ContentPart {
707748
pub data: ContentData,
708749
#[serde(skip_serializing)]
709750
pub metadata: Option<serde_json::Value>,
751+
#[serde(default, skip_serializing_if = "Option::is_none")]
752+
pub thought_signature: Option<String>,
710753
}
711754

712755
impl ContentPart {
@@ -715,6 +758,7 @@ impl ContentPart {
715758
data: ContentData::Text(text.to_string()),
716759
thought,
717760
metadata: None,
761+
thought_signature: None,
718762
}
719763
}
720764

@@ -726,6 +770,7 @@ impl ContentPart {
726770
}),
727771
thought,
728772
metadata: None,
773+
thought_signature: None,
729774
}
730775
}
731776

@@ -737,17 +782,25 @@ impl ContentPart {
737782
}),
738783
thought: false,
739784
metadata: None,
785+
thought_signature: None,
740786
}
741787
}
742788

743-
pub fn new_function_call(name: &str, arguments: Value, thought: bool) -> Self {
789+
pub fn new_function_call(
790+
id: Option<&str>,
791+
name: &str,
792+
arguments: Value,
793+
thought: bool,
794+
) -> Self {
744795
Self {
745796
data: ContentData::FunctionCall(FunctionCall {
797+
id: id.map(|s| s.to_string()),
746798
name: name.to_string(),
747799
arguments,
748800
}),
749801
thought,
750802
metadata: None,
803+
thought_signature: None,
751804
}
752805
}
753806

@@ -758,6 +811,7 @@ impl ContentPart {
758811
}),
759812
thought: false,
760813
metadata: None,
814+
thought_signature: None,
761815
}
762816
}
763817

@@ -766,17 +820,20 @@ impl ContentPart {
766820
data: ContentData::CodeExecutionResult(content),
767821
thought: false,
768822
metadata: None,
823+
thought_signature: None,
769824
}
770825
}
771826

772-
pub fn new_function_response(name: &str, content: Value) -> Self {
827+
pub fn new_function_response(id: Option<&str>, name: &str, content: Value) -> Self {
773828
Self {
774829
data: ContentData::FunctionResponse(FunctionResponse {
830+
id: id.map(|s| s.to_string()),
775831
name: name.to_string(),
776832
response: FunctionResponsePayload { content },
777833
}),
778834
thought: false,
779835
metadata: None,
836+
thought_signature: None,
780837
}
781838
}
782839
}
@@ -791,6 +848,7 @@ impl From<ContentData> for ContentPart {
791848
data,
792849
thought: false,
793850
metadata: None,
851+
thought_signature: None,
794852
}
795853
}
796854
}
@@ -810,14 +868,18 @@ pub enum ContentData {
810868
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
811869
#[serde(rename_all = "camelCase")]
812870
pub struct FunctionCall {
871+
#[serde(default)]
872+
pub id: Option<String>,
813873
pub name: String,
814-
#[serde(rename = "args")]
874+
#[serde(default, rename = "args")]
815875
pub arguments: serde_json::Value,
816876
}
817877

818878
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
819879
#[serde(rename_all = "camelCase")]
820880
pub struct FunctionResponse {
881+
#[serde(default)]
882+
pub id: Option<String>,
821883
pub name: String,
822884
pub response: FunctionResponsePayload,
823885
}

0 commit comments

Comments
 (0)