From 08df5e7bd42def7a42f0b21a02ae05eff05ed2e2 Mon Sep 17 00:00:00 2001 From: Evan Mattiza Date: Tue, 30 Dec 2025 18:21:59 -0600 Subject: [PATCH 1/2] fix(models): Gemini UnboundLocal Exception raised during stream Initialize candidate and event variables to None to prevent UnboundLocalError when stream yields no events. Also conditionally yield metadata only when event exists. --- src/strands/models/gemini.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/strands/models/gemini.py b/src/strands/models/gemini.py index cf7cc604a..45f7f4e18 100644 --- a/src/strands/models/gemini.py +++ b/src/strands/models/gemini.py @@ -426,6 +426,8 @@ async def stream( yield self._format_chunk({"chunk_type": "content_start", "data_type": "text"}) tool_used = False + candidate = None + event = None async for event in response: candidates = event.candidates candidate = candidates[0] if candidates else None @@ -455,7 +457,8 @@ async def stream( "data": "TOOL_USE" if tool_used else (candidate.finish_reason if candidate else "STOP"), } ) - yield self._format_chunk({"chunk_type": "metadata", "data": event.usage_metadata}) + if event: + yield self._format_chunk({"chunk_type": "metadata", "data": event.usage_metadata}) except genai.errors.ClientError as error: if not error.message: From e9ea7c047dd281d9d45c9cefc16b66ffccb1ab2e Mon Sep 17 00:00:00 2001 From: Evan Mattiza Date: Wed, 31 Dec 2025 14:16:32 +0000 Subject: [PATCH 2/2] test(models): add test for empty stream UnboundLocalError Add test case that reproduces UnboundLocalError when Gemini stream yields no events. Without proper initialization of candidate and event variables, an empty stream causes UnboundLocalError. --- tests/strands/models/test_gemini.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/strands/models/test_gemini.py b/tests/strands/models/test_gemini.py index c552a892a..08be9188d 100644 --- a/tests/strands/models/test_gemini.py +++ b/tests/strands/models/test_gemini.py @@ -566,6 +566,25 @@ async def test_stream_response_none_candidates(gemini_client, model, messages, a assert tru_chunks == exp_chunks +@pytest.mark.asyncio +async def test_stream_response_empty_stream(gemini_client, model, messages, agenerator, alist): + """Test that empty stream doesn't raise UnboundLocalError. + + When the stream yields no events, the candidate variable must be initialized + to None to avoid UnboundLocalError when referenced in message_stop chunk. + """ + gemini_client.aio.models.generate_content_stream.return_value = agenerator([]) + + tru_chunks = await alist(model.stream(messages)) + exp_chunks = [ + {"messageStart": {"role": "assistant"}}, + {"contentBlockStart": {"start": {}}}, + {"contentBlockStop": {}}, + {"messageStop": {"stopReason": "end_turn"}}, + ] + assert tru_chunks == exp_chunks + + @pytest.mark.asyncio async def test_stream_response_throttled_exception(gemini_client, model, messages): gemini_client.aio.models.generate_content_stream.side_effect = genai.errors.ClientError(