From f7f459dba2396e175cfcd0d5e9cfd6163d0f4589 Mon Sep 17 00:00:00 2001 From: yashdhumane Date: Sat, 24 Oct 2026 22:08:26 +0530 Subject: [PATCH 1/3] fix: convert iterators to list in t_contents to avoid confusing errors Passing a reversed() iterator or any non-list iterable to generate_content caused confusing errors like 'contents are required' or 'file uri and mime_type are required' instead of a clear message. Fix converts iterables (generators, reversed, map, etc.) to a list before validation, so they work correctly. Fixes #1272 --- google/genai/_transformers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/google/genai/_transformers.py b/google/genai/_transformers.py index 6739647133..1644d8af4d 100644 --- a/google/genai/_transformers.py +++ b/google/genai/_transformers.py @@ -473,6 +473,8 @@ def t_contents( Union[types.ContentListUnion, types.ContentListUnionDict, types.Content] ], ) -> list[types.Content]: + if hasattr(contents, '__iter__') and not isinstance(contents, (list, str, dict, bytes)) and not hasattr(contents, 'role'): + contents = list(contents) if contents is None or (isinstance(contents, list) and not contents): raise ValueError('contents are required.') if not isinstance(contents, list): @@ -1397,3 +1399,4 @@ def t_is_vertex_embed_content_model(model: str) -> bool: def t_json_schema(origin: Any) -> Any: return origin + From 1754f71a41c17b01612207b3258258ac91710d98 Mon Sep 17 00:00:00 2001 From: yashdhumane Date: Wed, 28 Oct 2026 09:59:49 +0530 Subject: [PATCH 2/3] fix: materialize iterators before Pydantic validation in generate_content --- google/genai/_transformers.py | 2 -- google/genai/models.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/google/genai/_transformers.py b/google/genai/_transformers.py index 1644d8af4d..1ab19cd4fa 100644 --- a/google/genai/_transformers.py +++ b/google/genai/_transformers.py @@ -473,8 +473,6 @@ def t_contents( Union[types.ContentListUnion, types.ContentListUnionDict, types.Content] ], ) -> list[types.Content]: - if hasattr(contents, '__iter__') and not isinstance(contents, (list, str, dict, bytes)) and not hasattr(contents, 'role'): - contents = list(contents) if contents is None or (isinstance(contents, list) and not contents): raise ValueError('contents are required.') if not isinstance(contents, list): diff --git a/google/genai/models.py b/google/genai/models.py index c597326eb5..2d5240a2dd 100644 --- a/google/genai/models.py +++ b/google/genai/models.py @@ -6251,6 +6251,9 @@ def generate_content( # The image shows a flat lay arrangement of freshly baked blueberry # scones. """ + import collections.abc + if isinstance(contents, (collections.abc.Iterator, collections.abc.Generator)): + contents = list(contents) incompatible_tools_indexes = ( _extra_utils.find_afc_incompatible_tool_indexes(config) @@ -6431,6 +6434,9 @@ def generate_content_stream( # The image shows a flat lay arrangement of freshly baked blueberry # scones. """ + import collections.abc + if isinstance(contents, (collections.abc.Iterator, collections.abc.Generator)): + contents = list(contents) incompatible_tools_indexes = ( _extra_utils.find_afc_incompatible_tool_indexes(config) @@ -8442,6 +8448,9 @@ async def generate_content( print(response.text) # J'aime les bagels. """ + import collections.abc + if isinstance(contents, (collections.abc.Iterator, collections.abc.Generator)): + contents = list(contents) # Retrieve and cache any MCP sessions if provided. incompatible_tools_indexes = ( _extra_utils.find_afc_incompatible_tool_indexes( @@ -8694,6 +8703,9 @@ async def generate_content_stream( # The image shows a flat lay arrangement of freshly baked blueberry # scones. """ + import collections.abc + if isinstance(contents, (collections.abc.Iterator, collections.abc.Generator)): + contents = list(contents) if not config: parsed_config = None elif isinstance(config, dict): From 34663cd48bd48534972fb014e8e734ede9071d58 Mon Sep 17 00:00:00 2001 From: yashdhumane Date: Thu, 29 Oct 2026 19:35:01 +0530 Subject: [PATCH 3/3] fix: add test for reversed iterator contents and remove trailing blank line --- .../models/test_gc_continuation_token.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/google/genai/tests/models/test_gc_continuation_token.py b/google/genai/tests/models/test_gc_continuation_token.py index 46dcdfcc5c..f2722a0958 100644 --- a/google/genai/tests/models/test_gc_continuation_token.py +++ b/google/genai/tests/models/test_gc_continuation_token.py @@ -4141,3 +4141,25 @@ async def _hop(resp): last = chunks[-1].candidates[0] assert last.finish_reason == types.FinishReason.MAX_TOKENS assert last.continuation_token == b'tok_hop2_max_tokens' + +def test_generate_content_accepts_reversed_iterator(mock_api_client): + """reversed([...]) contents should be materialized before Pydantic validation.""" + models_module = models.Models(mock_api_client) + contents = [ + types.Content(role='user', parts=[types.Part(text='hello')]), + types.Content(role='user', parts=[types.Part(text='world')]), + ] + mock_api_client.request.return_value = { + 'candidates': [{'content': {'role': 'model', 'parts': [{'text': 'ok'}]}, 'finish_reason': 'STOP'}] + } + with mock.patch.object( + models.Models, + '_generate_content_with_continuation', + return_value=types.GenerateContentResponse( + candidates=[types.Candidate(content=types.Content(role='model', parts=[types.Part(text='ok')]))] + ), + ) as mock_gc: + models_module.generate_content(model='gemini-2.5-pro', contents=reversed(contents)) + called_contents = mock_gc.call_args.kwargs['contents'] + assert isinstance(called_contents, list), 'reversed() should be materialized to a list' + assert len(called_contents) == 2