Skip to content

Commit c5a9cbc

Browse files
authored
fix: gemini parsing with thoughts (#2215)
<!-- ELLIPSIS_HIDDEN --> > [!IMPORTANT] > Fixes Google response parsing to handle non-thought parts correctly and adds a new `Gemini25ProThinking` client with tests. > > - **Behavior**: > - Fixes `text_content_part()` in `response_handler.rs` to concatenate non-thought parts instead of returning the first one. > - Adds `Gemini25ProThinking` client in `clients.baml` with `thinkingConfig` options. > - **Tests**: > - Adds `test_parse_google_response_with_thinking()` and `test_parse_google_response_with_multiple_non_thought_parts()` in `response_handler.rs`. > - Adds `TestGeminiThinking` function and test in `gemini.baml`. > - **Misc**: > - Updates various client and function definitions across multiple files to support the new `Gemini25ProThinking` client. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=BoundaryML%2Fbaml&utm_source=github&utm_medium=referral)<sup> for eb63880. You can [customize](https://app.ellipsis.dev/BoundaryML/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN -->
1 parent 96e9d64 commit c5a9cbc

40 files changed

Lines changed: 1378 additions & 22 deletions

‎engine/baml-runtime/src/internal/llm_client/primitive/google/response_handler.rs‎

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,17 @@ pub fn parse_google_response<C: WithClient + RequestBuilder>(
102102
}
103103

104104
fn text_content_part(parts: &[Part]) -> Option<String> {
105-
parts
105+
let non_thought_parts: Vec<&str> = parts
106106
.iter()
107-
.position(|part| !part.text.is_empty() && !part.thought.unwrap_or(false))
108-
.map(|index| parts[index].text.clone())
107+
.filter(|part| !part.thought.unwrap_or(false))
108+
.map(|part| part.text.as_str())
109+
.collect();
110+
111+
if non_thought_parts.is_empty() {
112+
None
113+
} else {
114+
Some(non_thought_parts.join(""))
115+
}
109116
}
110117

111118
pub fn scan_google_response_stream(
@@ -291,4 +298,133 @@ mod tests {
291298
panic!("Expected LLMResponse::Success, got {result:?}");
292299
}
293300
}
301+
302+
#[test]
303+
fn test_parse_google_response_with_thinking() {
304+
// Test case that reproduces the bug with thinking responses
305+
let response_json = r#"
306+
{
307+
"candidates": [
308+
{
309+
"content": {
310+
"parts": [
311+
{
312+
"text": "Analyzing the image to determine what type of product this is. It appears to be some kind of car accessory. Looking at the shape and structure...",
313+
"thought": true
314+
},
315+
{
316+
"text": "Based on the mesh pattern and the mounting points, this looks like it's designed to fit in a vehicle's cargo area.",
317+
"thought": true
318+
},
319+
{
320+
"text": "This product is a \"dog cargo guard\" or a barrier designed to separate the trunk/boot from the passenger area of a car. Its main purpose is to safely contain a pet or cargo."
321+
}
322+
],
323+
"role": "model"
324+
},
325+
"finishReason": "STOP"
326+
}
327+
],
328+
"usageMetadata": {
329+
"promptTokenCount": 100,
330+
"candidatesTokenCount": 50,
331+
"totalTokenCount": 150
332+
}
333+
}
334+
"#;
335+
336+
let client = MockClient::new();
337+
let prompt = vec![];
338+
let response_body: serde_json::Value = serde_json::from_str(response_json.trim()).unwrap();
339+
let system_now = web_time::SystemTime::now();
340+
let instant_now = web_time::Instant::now();
341+
let model_name = Some("gemini-2.5-pro".to_string());
342+
343+
let result = parse_google_response(
344+
&client,
345+
either::Right(prompt.as_slice()),
346+
response_body,
347+
system_now,
348+
instant_now,
349+
model_name,
350+
);
351+
352+
if let LLMResponse::Success(actual_result) = result {
353+
// Should contain only the non-thought content
354+
assert_eq!(
355+
actual_result.content,
356+
"This product is a \"dog cargo guard\" or a barrier designed to separate the trunk/boot from the passenger area of a car. Its main purpose is to safely contain a pet or cargo."
357+
);
358+
} else {
359+
panic!("Expected LLMResponse::Success, got {result:?}");
360+
}
361+
}
362+
363+
#[test]
364+
fn test_parse_google_response_with_multiple_non_thought_parts() {
365+
// Test case showing the bug: when there are multiple non-thought parts
366+
let response_json = r#"
367+
{
368+
"candidates": [
369+
{
370+
"content": {
371+
"parts": [
372+
{
373+
"text": "Let me analyze this image...",
374+
"thought": true
375+
},
376+
{
377+
"text": "I can see it's related to vehicles.",
378+
"thought": true
379+
},
380+
{
381+
"text": "This product is a "
382+
},
383+
{
384+
"text": "\"dog cargo guard\""
385+
},
386+
{
387+
"text": " or a barrier designed to separate the trunk/boot from the passenger area of a car."
388+
}
389+
],
390+
"role": "model"
391+
},
392+
"finishReason": "STOP"
393+
}
394+
],
395+
"usageMetadata": {
396+
"promptTokenCount": 100,
397+
"candidatesTokenCount": 50,
398+
"totalTokenCount": 150
399+
}
400+
}
401+
"#;
402+
403+
let client = MockClient::new();
404+
let prompt = vec![];
405+
let response_body: serde_json::Value = serde_json::from_str(response_json.trim()).unwrap();
406+
let system_now = web_time::SystemTime::now();
407+
let instant_now = web_time::Instant::now();
408+
let model_name = Some("gemini-2.5-pro".to_string());
409+
410+
let result = parse_google_response(
411+
&client,
412+
either::Right(prompt.as_slice()),
413+
response_body,
414+
system_now,
415+
instant_now,
416+
model_name,
417+
);
418+
419+
if let LLMResponse::Success(actual_result) = result {
420+
// Currently this will only return "This product is a " (the first non-thought part)
421+
// But it should concatenate all non-thought parts
422+
assert_eq!(
423+
actual_result.content,
424+
"This product is a \"dog cargo guard\" or a barrier designed to separate the trunk/boot from the passenger area of a car."
425+
);
426+
} else {
427+
panic!("Expected LLMResponse::Success, got {result:?}");
428+
}
429+
}
294430
}

‎integ-tests/baml_src/clients.baml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,21 @@ client<llm> Gemini {
152152
}
153153
}
154154

155+
client<llm> Gemini25ProThinking {
156+
provider google-ai
157+
retry_policy Constant
158+
options {
159+
model "gemini-2.5-pro"
160+
api_key env.GOOGLE_API_KEY
161+
generationConfig {
162+
thinkingConfig {
163+
thinkingBudget 1024
164+
includeThoughts true
165+
}
166+
}
167+
}
168+
}
169+
155170
client<llm> GeminiOpenAiGeneric {
156171
provider "openai-generic"
157172
options {

‎integ-tests/baml_src/test-files/providers/gemini.baml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ function TestGeminiOpenAiGeneric() -> string {
3030
"#
3131
}
3232

33+
function TestGeminiThinking(input: string) -> string {
34+
client Gemini25ProThinking
35+
prompt #"
36+
Analyze this product and tell me what it is: {{ input }}
37+
"#
38+
}
39+
40+
test TestGeminiThinking {
41+
functions [TestGeminiThinking]
42+
args {
43+
input "A mesh barrier with mounting points designed for vehicle cargo areas"
44+
}
45+
}
46+
3347
test TestName {
3448
functions [TestGeminiSystem]
3549
args {

‎integ-tests/baml_src/test-files/providers/tests.baml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ test TestAWS {
1616

1717
test TestProvider {
1818
functions [
19-
TestAnthropic, TestVertex, PromptTestOpenAI, TestAzure, TestOllama, TestGemini, TestAws,
19+
TestAnthropic, TestVertex, PromptTestOpenAI, TestAzure, TestOllama, TestGemini, TestGeminiThinking, TestAws,
2020
TestAwsInvalidRegion,
2121
TestOpenAIShorthand,
2222
TestAnthropicShorthand,

‎integ-tests/go/baml_client/baml_source_map.go‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎integ-tests/go/baml_client/functions.go‎

Lines changed: 58 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎integ-tests/go/baml_client/functions_stream.go‎

Lines changed: 78 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎integ-tests/openapi/baml_client/openapi.yaml‎

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)