From d2ce88737de60d8d398ef04590064db54218c918 Mon Sep 17 00:00:00 2001 From: adavyas Date: Sat, 14 Mar 2026 16:49:54 -0700 Subject: [PATCH] Preserve Gemini tool context during provider fallback --- src/utils/clients.py | 42 +++++++++++++++--- tests/utils/test_clients.py | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/src/utils/clients.py b/src/utils/clients.py index 31ae3013..0591fc85 100644 --- a/src/utils/clients.py +++ b/src/utils/clients.py @@ -159,12 +159,44 @@ def _build_gemini_contents_from_messages( continue if isinstance(msg.get("content"), list): - text_parts: list[dict[str, str]] = [] + parts: list[dict[str, Any]] = [] for block in cast(list[dict[str, Any]], msg["content"]): - if block.get("type") == "text" and isinstance(block.get("text"), str): - text_parts.append({"text": block["text"]}) - if text_parts: - gemini_contents.append({"role": role, "parts": text_parts}) + block_type = block.get("type") + + if block_type == "text" and isinstance(block.get("text"), str): + parts.append({"text": block["text"]}) + continue + + if block_type == "tool_use" and isinstance(block.get("name"), str): + tool_args = block.get("input") + parts.append( + { + "function_call": { + "name": block["name"], + "args": tool_args if isinstance(tool_args, dict) else {}, + } + } + ) + continue + + if block_type == "tool_result": + tool_result = block.get("content") + if not isinstance(tool_result, str): + tool_result = json.dumps(tool_result) + parts.append( + { + "function_response": { + "name": str(block.get("tool_use_id", "tool_result")), + "response": { + "result": tool_result, + "is_error": bool(block.get("is_error", False)), + }, + } + } + ) + + if parts: + gemini_contents.append({"role": role, "parts": parts}) system_instruction = ( "\n\n".join(system_instruction_parts) if system_instruction_parts else None diff --git a/tests/utils/test_clients.py b/tests/utils/test_clients.py index 1efe5223..b0103f8c 100644 --- a/tests/utils/test_clients.py +++ b/tests/utils/test_clients.py @@ -905,6 +905,94 @@ class TestGoogleClient: "stable instructions\n\nrolling session context" ) + async def test_google_preserves_non_text_content_blocks(self): + """Gemini should preserve tool context when converting content blocks.""" + from google import genai + + mock_client = Mock(spec=genai.Client) + mock_response = Mock() + mock_part = Mock() + mock_part.text = "ok" + mock_part.function_call = None + mock_content = Mock() + mock_content.parts = [mock_part] + mock_finish_reason = Mock() + mock_finish_reason.name = "STOP" + mock_candidate = Mock() + mock_candidate.content = mock_content + mock_candidate.finish_reason = mock_finish_reason + mock_response.candidates = [mock_candidate] + mock_usage_metadata = Mock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + mock_response.usage_metadata = mock_usage_metadata + mock_aio = Mock() + mock_aio.models.generate_content = AsyncMock(return_value=mock_response) + mock_client.aio = mock_aio + + with patch.dict(CLIENTS, {"google": mock_client}): + await honcho_llm_call_inner( + provider="google", + model="gemini-1.5-pro", + prompt="ignored", + max_tokens=100, + messages=[ + { + "role": "assistant", + "content": [ + {"type": "text", "text": "Tool call context"}, + { + "type": "tool_use", + "id": "toolu_1", + "name": "search_memory", + "input": {"query": "tea"}, + }, + ], + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_1", + "content": "Alice likes tea", + } + ], + }, + ], + ) + + call_args = mock_aio.models.generate_content.call_args + assert call_args is not None + assert call_args.kwargs["contents"] == [ + { + "role": "model", + "parts": [ + {"text": "Tool call context"}, + { + "function_call": { + "name": "search_memory", + "args": {"query": "tea"}, + } + }, + ], + }, + { + "role": "user", + "parts": [ + { + "function_response": { + "name": "toolu_1", + "response": { + "result": "Alice likes tea", + "is_error": False, + }, + } + } + ], + }, + ] + async def test_google_streaming(self): """Test Google/Gemini streaming response""" from google import genai