Skip to content

Commit 16b6432

Browse files
authored
fix: Gemini responses lacking content (#1030)
1 parent f60abf9 commit 16b6432

File tree

3 files changed

+75
-18
lines changed

3 files changed

+75
-18
lines changed

rig-core/src/providers/gemini/completion.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,18 @@ where
126126
.await
127127
.map_err(CompletionError::HttpError)?;
128128

129-
let response: GenerateContentResponse = serde_json::from_slice(&response_body)?;
129+
let response_text = String::from_utf8_lossy(&response_body).to_string();
130+
tracing::debug!("Received raw response from Gemini API: {}", response_text);
131+
132+
let response: GenerateContentResponse = serde_json::from_slice(&response_body)
133+
.map_err(|err| {
134+
tracing::error!(
135+
error = %err,
136+
body = %response_text,
137+
"Failed to deserialize Gemini completion response"
138+
);
139+
CompletionError::JsonError(err)
140+
})?;
130141

131142
match response.usage_metadata {
132143
Some(ref usage) => tracing::info!(target: "rig",
@@ -304,6 +315,21 @@ impl TryFrom<GenerateContentResponse> for completion::CompletionResponse<Generat
304315

305316
let content = candidate
306317
.content
318+
.as_ref()
319+
.ok_or_else(|| {
320+
let reason = candidate
321+
.finish_reason
322+
.as_ref()
323+
.map(|r| format!("finish_reason={r:?}"))
324+
.unwrap_or_else(|| "finish_reason=<unknown>".to_string());
325+
let message = candidate
326+
.finish_message
327+
.as_deref()
328+
.unwrap_or("no finish message provided");
329+
CompletionError::ResponseError(format!(
330+
"Gemini candidate missing content ({reason}, finish_message={message})"
331+
))
332+
})?
307333
.parts
308334
.iter()
309335
.map(|Part { thought, part, .. }| {
@@ -439,12 +465,12 @@ pub mod gemini_api_types {
439465
.candidates
440466
.iter()
441467
.filter_map(|x| {
442-
if x.content.role.as_ref().is_none_or(|y| y != &Role::Model) {
468+
let content = x.content.as_ref()?;
469+
if content.role.as_ref().is_none_or(|y| y != &Role::Model) {
443470
return None;
444471
}
445472

446-
let res = x
447-
.content
473+
let res = content
448474
.parts
449475
.iter()
450476
.filter_map(|part| {
@@ -475,7 +501,8 @@ pub mod gemini_api_types {
475501
#[serde(rename_all = "camelCase")]
476502
pub struct ContentCandidate {
477503
/// Output only. Generated content returned from the model.
478-
pub content: Content,
504+
#[serde(skip_serializing_if = "Option::is_none")]
505+
pub content: Option<Content>,
479506
/// Optional. Output only. The reason why the model stopped generating tokens.
480507
/// If empty, the model has not stopped generating tokens.
481508
pub finish_reason: Option<FinishReason>,
@@ -494,6 +521,8 @@ pub mod gemini_api_types {
494521
pub logprobs_result: Option<LogprobsResult>,
495522
/// Output only. Index of the candidate in the list of response candidates.
496523
pub index: Option<i32>,
524+
/// Output only. Additional information about why the model stopped generating tokens.
525+
pub finish_message: Option<String>,
497526
}
498527

499528
#[derive(Clone, Debug, Deserialize, Serialize)]

rig-core/src/providers/gemini/streaming.rs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,12 @@ where
152152
continue;
153153
};
154154

155-
for part in &choice.content.parts {
155+
let Some(content) = choice.content.as_ref() else {
156+
tracing::debug!(finish_reason = ?choice.finish_reason, "Streaming candidate missing content");
157+
continue;
158+
};
159+
160+
for part in &content.parts {
156161
match part {
157162
Part {
158163
part: PartKind::Text(text),
@@ -165,7 +170,7 @@ where
165170
part: PartKind::Text(text),
166171
..
167172
} => {
168-
text_response += text;
173+
text_response.push_str(text);
169174
yield Ok(streaming::RawStreamingChoice::Message(text.clone()));
170175
},
171176
Part {
@@ -186,7 +191,7 @@ where
186191
}
187192
}
188193

189-
if choice.content.parts.is_empty() {
194+
if content.parts.is_empty() {
190195
tracing::trace!(reason = ?choice.finish_reason, "There is no part in the streaming content");
191196
}
192197

@@ -252,12 +257,16 @@ mod tests {
252257

253258
let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
254259
assert_eq!(response.candidates.len(), 1);
255-
assert_eq!(response.candidates[0].content.parts.len(), 1);
260+
let content = response.candidates[0]
261+
.content
262+
.as_ref()
263+
.expect("candidate should contain content");
264+
assert_eq!(content.parts.len(), 1);
256265

257266
if let Part {
258267
part: PartKind::Text(text),
259268
..
260-
} = &response.candidates[0].content.parts[0]
269+
} = &content.parts[0]
261270
{
262271
assert_eq!(text, "Hello, world!");
263272
} else {
@@ -289,14 +298,18 @@ mod tests {
289298

290299
let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
291300
assert_eq!(response.candidates.len(), 1);
292-
assert_eq!(response.candidates[0].content.parts.len(), 3);
301+
let content = response.candidates[0]
302+
.content
303+
.as_ref()
304+
.expect("candidate should contain content");
305+
assert_eq!(content.parts.len(), 3);
293306

294307
// Verify all three text parts are present
295308
for (i, expected_text) in ["Hello, ", "world!", " How are you?"].iter().enumerate() {
296309
if let Part {
297310
part: PartKind::Text(text),
298311
..
299-
} = &response.candidates[0].content.parts[i]
312+
} = &content.parts[i]
300313
{
301314
assert_eq!(text, expected_text);
302315
} else {
@@ -337,13 +350,17 @@ mod tests {
337350
});
338351

339352
let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
340-
assert_eq!(response.candidates[0].content.parts.len(), 2);
353+
let content = response.candidates[0]
354+
.content
355+
.as_ref()
356+
.expect("candidate should contain content");
357+
assert_eq!(content.parts.len(), 2);
341358

342359
// Verify first tool call
343360
if let Part {
344361
part: PartKind::FunctionCall(call),
345362
..
346-
} = &response.candidates[0].content.parts[0]
363+
} = &content.parts[0]
347364
{
348365
assert_eq!(call.name, "get_weather");
349366
} else {
@@ -354,7 +371,7 @@ mod tests {
354371
if let Part {
355372
part: PartKind::FunctionCall(call),
356373
..
357-
} = &response.candidates[0].content.parts[1]
374+
} = &content.parts[1]
358375
{
359376
assert_eq!(call.name, "get_temperature");
360377
} else {
@@ -399,7 +416,11 @@ mod tests {
399416
});
400417

401418
let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
402-
let parts = &response.candidates[0].content.parts;
419+
let content = response.candidates[0]
420+
.content
421+
.as_ref()
422+
.expect("candidate should contain content");
423+
let parts = &content.parts;
403424
assert_eq!(parts.len(), 4);
404425

405426
// Verify reasoning (thought) part
@@ -469,7 +490,11 @@ mod tests {
469490
});
470491

471492
let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
472-
assert_eq!(response.candidates[0].content.parts.len(), 0);
493+
let content = response.candidates[0]
494+
.content
495+
.as_ref()
496+
.expect("candidate should contain content");
497+
assert_eq!(content.parts.len(), 0);
473498
}
474499

475500
#[test]

rig-core/src/providers/gemini/transcription.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ impl TryFrom<GenerateContentResponse>
145145
TranscriptionError::ResponseError("No response candidates in response".into())
146146
})?;
147147

148-
let part = candidate.content.parts.first();
148+
let part = candidate
149+
.content
150+
.as_ref()
151+
.and_then(|content| content.parts.first());
149152

150153
let text = match part {
151154
Some(Part {

0 commit comments

Comments
 (0)