Preserve Gemini tool context during provider fallback

This commit is contained in:
adavyas 2026-03-14 16:49:54 -07:00
parent 56924eb956
commit d2ce88737d
2 changed files with 125 additions and 5 deletions

View File

@ -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

View File

@ -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