From f039b97fa654e050d9d166145f9a95597b4641f7 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 10 Nov 2026 05:57:04 -0800 Subject: [PATCH] fix(google_genai): list skipped results after every answer to their turn When the caller split the answers to one model turn across two user turns, the skipped result went between them and GPT read it as the second answer. A skipped result now follows the last user turn that answers its model turn --- litellm/google_genai/adapters/transformation.py | 17 ++++++----------- .../google_genai/test_google_genai_adapter.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/litellm/google_genai/adapters/transformation.py b/litellm/google_genai/adapters/transformation.py index dcc5aa1b089..a237be059d0 100644 --- a/litellm/google_genai/adapters/transformation.py +++ b/litellm/google_genai/adapters/transformation.py @@ -423,16 +423,8 @@ def _genai_turn_messages( ) -def _answered_by_next_turn(turns: Sequence[_GenAITurn], index: int) -> bool: - return index + 1 < len(turns) and bool(turns[index + 1].responses) - - -def _skipped_results_after( - turns: Sequence[_GenAITurn], skipped_results: Sequence[tuple[ChatCompletionToolMessage, ...]], index: int -) -> tuple[ChatCompletionToolMessage, ...]: - previous: Final = skipped_results[index - 1] if index > 0 and _answered_by_next_turn(turns, index - 1) else () - own: Final = () if _answered_by_next_turn(turns, index) else skipped_results[index] - return (*previous, *own) +def _last_answering_turn(turns: Sequence[_GenAITurn], index: int) -> int: + return next((later for later in range(index + 1, len(turns)) if not turns[later].responses), len(turns)) - 1 def _genai_contents_messages(contents: Sequence[object]) -> tuple[AllMessageValues, ...]: @@ -458,6 +450,9 @@ def _genai_contents_messages(contents: Sequence[object]) -> tuple[AllMessageValu ) for turn, (start, end) in zip(turns, pairwise(call_starts)) ) + skipped_after: Final = MappingProxyType( + {_last_answering_turn(turns, index): results for index, results in enumerate(skipped_results) if results} + ) return tuple( chain.from_iterable( ( @@ -466,7 +461,7 @@ def _genai_contents_messages(contents: Sequence[object]) -> tuple[AllMessageValu call_ids[call_starts[index] : call_starts[index + 1]], response_ids[response_starts[index] : response_starts[index + 1]], ), - *_skipped_results_after(turns, skipped_results, index), + *skipped_after.get(index, ()), ) for index, turn in enumerate(turns) ) diff --git a/tests/unit/google_genai/test_google_genai_adapter.py b/tests/unit/google_genai/test_google_genai_adapter.py index bf48c9d82df..02eb138c3f1 100644 --- a/tests/unit/google_genai/test_google_genai_adapter.py +++ b/tests/unit/google_genai/test_google_genai_adapter.py @@ -699,6 +699,23 @@ def _ids_answered_right_after(messages: list[dict[str, object]], index: int) -> frozenset({"paris", "rome"}), id="parallel_call_left_out_of_the_answers", ), + pytest.param( + [ + { + "role": "model", + "parts": [ + _tagged_call("get_weather", "paris"), + _tagged_call("get_weather", "rome"), + _tagged_call("get_weather", "oslo"), + ], + }, + {"role": "user", "parts": [_tagged_response("get_weather", "paris")]}, + {"role": "user", "parts": [_tagged_response("get_weather", "rome")]}, + {"role": "model", "parts": [{"text": "Oslo did not answer"}]}, + ], + frozenset({"paris", "rome"}), + id="answers_split_across_user_turns", + ), ], ) def test_a_call_a_later_turn_left_unanswered_gets_one_skipped_result_and_the_others_keep_their_answers(