@@ -63,81 +63,6 @@ public void processRawResponses_withTextChunks_emitsPartialResponses() {
6363 isFunctionCallResponse ());
6464 }
6565
66- // Regression test for b/513501918. gemini-3.1-flash-lite emits an extra trailing chunk after a
67- // function call: `{parts:[{text:""}], finishReason:STOP}`. That chunk must not be propagated as
68- // a non-partial event because BaseLlmFlow#run would treat it as the final response and
69- // terminate the loop before the function response is sent back to the model. The chunk's
70- // metadata (e.g. `finishReason`, `usageMetadata`) is preserved by emitting it on a content-less
71- // partial response instead of dropping the chunk entirely.
72- @ Test
73- public void
74- processRawResponses_functionCallThenEmptyTextWithStop_emitsFunctionCallAndMetadataOnlyPartial () {
75- Flowable <GenerateContentResponse > rawResponses =
76- Flowable .just (
77- toResponse (Part .fromFunctionCall ("test_function" , ImmutableMap .of ())),
78- toResponseWithText ("" , FinishReason .Known .STOP ));
79-
80- Flowable <LlmResponse > llmResponses = Gemini .processRawResponses (rawResponses );
81-
82- assertLlmResponses (
83- llmResponses ,
84- isFunctionCallResponse (),
85- isContentlessPartialWithFinishReason (FinishReason .Known .STOP ));
86- }
87-
88- // Same as above but with `usageMetadata` on the trailing empty chunk: the metadata must survive
89- // on the emitted content-less partial.
90- @ Test
91- public void
92- processRawResponses_functionCallThenEmptyTextWithUsageMetadata_preservesUsageMetadata () {
93- GenerateContentResponseUsageMetadata metadata = createUsageMetadata (5 , 10 , 15 );
94- Flowable <GenerateContentResponse > rawResponses =
95- Flowable .just (
96- toResponse (Part .fromFunctionCall ("test_function" , ImmutableMap .of ())),
97- toResponseWithText ("" , FinishReason .Known .STOP , metadata ));
98-
99- Flowable <LlmResponse > llmResponses = Gemini .processRawResponses (rawResponses );
100-
101- assertLlmResponses (
102- llmResponses , isFunctionCallResponse (), isContentlessPartialWithUsageMetadata (metadata ));
103- }
104-
105- // Same as above but without a finishReason or usageMetadata: the trailing empty chunk carries no
106- // useful payload and must be suppressed entirely.
107- @ Test
108- public void processRawResponses_functionCallThenEmptyText_doesNotEmitExtraEmptyResponse () {
109- Flowable <GenerateContentResponse > rawResponses =
110- Flowable .just (
111- toResponse (Part .fromFunctionCall ("test_function" , ImmutableMap .of ())),
112- toResponseWithText ("" ));
113-
114- Flowable <LlmResponse > llmResponses = Gemini .processRawResponses (rawResponses );
115-
116- assertLlmResponses (llmResponses , isFunctionCallResponse ());
117- }
118-
119- // Combined scenario: leading partial text, then a function call, then the trailing empty-text
120- // chunk with STOP. Accumulated text must still be flushed, the function call must still be
121- // emitted, and the trailing chunk must surface only its metadata on a content-less partial.
122- @ Test
123- public void
124- processRawResponses_textThenFunctionCallThenEmptyTextWithStop_emitsTextFunctionCallAndMetadata () {
125- Flowable <GenerateContentResponse > rawResponses =
126- Flowable .just (
127- toResponseWithText ("Thinking..." ),
128- toResponse (Part .fromFunctionCall ("test_function" , ImmutableMap .of ())),
129- toResponseWithText ("" , FinishReason .Known .STOP ));
130-
131- Flowable <LlmResponse > llmResponses = Gemini .processRawResponses (rawResponses );
132-
133- assertLlmResponses (
134- llmResponses ,
135- isPartialTextResponse ("Thinking..." ),
136- isFinalTextResponse ("Thinking..." ),
137- isFunctionCallResponse (),
138- isContentlessPartialWithFinishReason (FinishReason .Known .STOP ));
139- }
140-
14166 @ Test
14267 public void processRawResponses_textAndStopReason_emitsPartialThenFinalText () {
14368 Flowable <GenerateContentResponse > rawResponses =
@@ -250,93 +175,6 @@ public void processRawResponses_thoughtChunksAndStop_includeUsageMetadata() {
250175 isFinalThoughtResponseWithUsageMetadata ("Thinking deeply" , metadata2 ));
251176 }
252177
253- // Test cases for the shouldEmit filter applied by generateContent after processRawResponses.
254- // shouldEmit drops chunks that are empty-text-only AND carry no useful metadata; everything else
255- // is forwarded. processRawResponses normally already strips empty-text-only chunks, so shouldEmit
256- // is defense-in-depth, but it must still behave correctly when fed any LlmResponse directly.
257-
258- @ Test
259- public void shouldEmit_emptyTextOnlyResponseWithNoMetadata_returnsFalse () {
260- LlmResponse response =
261- LlmResponse .builder ()
262- .content (Content .builder ().role ("model" ).parts (Part .fromText ("" )).build ())
263- .build ();
264-
265- assertThat (Gemini .shouldEmit (response )).isFalse ();
266- }
267-
268- @ Test
269- public void shouldEmit_emptyTextOnlyResponseWithFinishReason_returnsTrue () {
270- LlmResponse response =
271- LlmResponse .builder ()
272- .content (Content .builder ().role ("model" ).parts (Part .fromText ("" )).build ())
273- .finishReason (new FinishReason (FinishReason .Known .STOP ))
274- .build ();
275-
276- assertThat (Gemini .shouldEmit (response )).isTrue ();
277- }
278-
279- @ Test
280- public void shouldEmit_emptyTextOnlyResponseWithUsageMetadata_returnsTrue () {
281- LlmResponse response =
282- LlmResponse .builder ()
283- .content (Content .builder ().role ("model" ).parts (Part .fromText ("" )).build ())
284- .usageMetadata (createUsageMetadata (5 , 10 , 15 ))
285- .build ();
286-
287- assertThat (Gemini .shouldEmit (response )).isTrue ();
288- }
289-
290- @ Test
291- public void shouldEmit_nonEmptyTextResponse_returnsTrue () {
292- LlmResponse response =
293- LlmResponse .builder ()
294- .content (Content .builder ().role ("model" ).parts (Part .fromText ("hello" )).build ())
295- .build ();
296-
297- assertThat (Gemini .shouldEmit (response )).isTrue ();
298- }
299-
300- @ Test
301- public void shouldEmit_functionCallResponse_returnsTrue () {
302- LlmResponse response =
303- LlmResponse .builder ()
304- .content (
305- Content .builder ()
306- .role ("model" )
307- .parts (Part .fromFunctionCall ("test_function" , ImmutableMap .of ()))
308- .build ())
309- .build ();
310-
311- assertThat (Gemini .shouldEmit (response )).isTrue ();
312- }
313-
314- @ Test
315- public void shouldEmit_contentlessResponse_returnsTrue () {
316- // A response with no content at all is not an empty-text-only response, so it should pass
317- // through regardless of metadata. This is the shape emitted by processRawResponses after it
318- // strips empty-text content while preserving metadata.
319- LlmResponse response = LlmResponse .builder ().build ();
320-
321- assertThat (Gemini .shouldEmit (response )).isTrue ();
322- }
323-
324- @ Test
325- public void shouldEmit_multiPartResponseWithEmptyTextPart_returnsTrue () {
326- // Only single-part empty-text responses are considered "empty-text-only". A multi-part response
327- // is treated as carrying semantic content and must always pass through.
328- LlmResponse response =
329- LlmResponse .builder ()
330- .content (
331- Content .builder ()
332- .role ("model" )
333- .parts (Part .fromText ("" ), Part .fromText ("hello" ))
334- .build ())
335- .build ();
336-
337- assertThat (Gemini .shouldEmit (response )).isTrue ();
338- }
339-
340178 @ Test
341179 public void processRawResponses_thoughtAndTextWithStop_onlyFinalTextIncludesUsageMetadata () {
342180 GenerateContentResponseUsageMetadata metadata1 = createUsageMetadata (5 , 5 , 10 );
@@ -394,26 +232,6 @@ private static Predicate<LlmResponse> isFunctionCallResponse() {
394232 };
395233 }
396234
397- private static Predicate <LlmResponse > isContentlessPartialWithFinishReason (
398- FinishReason .Known expectedFinishReason ) {
399- return response -> {
400- assertThat (response .partial ()).hasValue (true );
401- assertThat (response .content ()).isEmpty ();
402- assertThat (response .finishReason ().map (fr -> fr .knownEnum ())).hasValue (expectedFinishReason );
403- return true ;
404- };
405- }
406-
407- private static Predicate <LlmResponse > isContentlessPartialWithUsageMetadata (
408- GenerateContentResponseUsageMetadata expectedMetadata ) {
409- return response -> {
410- assertThat (response .partial ()).hasValue (true );
411- assertThat (response .content ()).isEmpty ();
412- assertThat (response .usageMetadata ()).hasValue (expectedMetadata );
413- return true ;
414- };
415- }
416-
417235 private static Predicate <LlmResponse > isEmptyResponse () {
418236 return response -> {
419237 assertThat (response .partial ()).isEmpty ();
0 commit comments