This commit is contained in:
Kentaro Takiguchi 2026-11-06 15:51:01 -05:00 • committed by GitHub
commit 020b8d244b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 80 additions and 3 deletions

View file

@ -255,6 +255,7 @@ def _genai_content_messages(content: object) -> tuple[AllMessageValues, ...]:
return ()
parts: Final = _genai_content_parts(fields.get("parts", ()))
converted: Final = _genai_message_parts(parts)
responses: Final = tuple(response for part in parts if (response := _genai_tool_response(part)) is not None)
role: Final = fields.get("role", "user")
if role == "user":
user_message: Final = (
@ -269,18 +270,20 @@ def _genai_content_messages(content: object) -> tuple[AllMessageValues, ...]:
if converted
else ()
)
responses: Final = tuple(response for part in parts if (response := _genai_tool_response(part)) is not None)
return (*user_message, *responses)
if role != "model":
return ()
text: Final = "".join(part["text"] for part in converted if part["type"] == "text")
calls: Final = [call for part in parts if (call := _genai_tool_call(part)) is not None]
assistant_content: Final = list(converted) if any(part["type"] != "text" for part in converted) else text or None
return (
if responses and not calls and assistant_content is None:
return responses
assistant_message: Final = (
ChatCompletionAssistantMessage(role="assistant", content=assistant_content, tool_calls=calls)
if calls
else ChatCompletionAssistantMessage(role="assistant", content=assistant_content),
else ChatCompletionAssistantMessage(role="assistant", content=assistant_content)
)
return (assistant_message, *responses)
class GoogleGenAIStreamWrapper(AdapterCompletionStreamWrapper):

View file

@ -194,6 +194,80 @@ def test_native_function_call_preserves_arguments_and_matching_response(args: ob
]
@pytest.mark.parametrize("response_key", ["functionResponse", "function_response"])
def test_model_turn_function_response_answers_the_preceding_call(response_key: str) -> None:
result: Final = {"output": "return 1"}
messages: Final = GoogleGenAIAdapter()._transform_contents_to_messages(
[
{"role": "user", "parts": [{"text": "Read /tmp/a.py"}]},
{"role": "model", "parts": [{"functionCall": {"name": "view_file", "args": {"path": "/tmp/a.py"}}}]},
{"role": "model", "parts": [{response_key: {"name": "view_file", "response": result}}]},
]
)
assert [message["role"] for message in messages] == ["user", "assistant", "tool"]
assert messages[2]["tool_call_id"] == messages[1]["tool_calls"][0]["id"]
assert json.loads(messages[2]["content"]) == result
def test_model_turn_with_call_and_response_puts_the_result_after_the_call() -> None:
text: Final = "Checking the weather"
result: Final = {"temp": "4C"}
messages: Final = GoogleGenAIAdapter()._transform_contents_to_messages(
[
{
"role": "model",
"parts": [
{"text": text},
{"functionCall": {"name": "get_weather", "args": {"city": "Oslo"}}},
{"functionResponse": {"name": "get_weather", "response": result}},
],
}
]
)
assert [message["role"] for message in messages] == ["assistant", "tool"]
assert messages[0]["content"] == text
assert messages[1]["tool_call_id"] == messages[0]["tool_calls"][0]["id"]
assert json.loads(messages[1]["content"]) == result
def test_model_turn_text_next_to_a_function_response_is_not_dropped() -> None:
text: Final = "Here is what the file contains"
result: Final = {"output": "return 1"}
messages: Final = GoogleGenAIAdapter()._transform_contents_to_messages(
[{"role": "model", "parts": [{"text": text}, {"functionResponse": {"name": "view_file", "response": result}}]}]
)
assert [message["role"] for message in messages] == ["assistant", "tool"]
assert messages[0]["content"] == text
assert json.loads(messages[1]["content"]) == result
def test_model_turn_function_response_keeps_its_media() -> None:
result: Final = {"ok": True}
mime: Final = "image/png"
data: Final = "bWVkaWE="
messages: Final = GoogleGenAIAdapter()._transform_contents_to_messages(
[
{"role": "model", "parts": [{"functionCall": {"name": "screenshot", "args": {}}}]},
{
"role": "model",
"parts": [
{
"functionResponse": {
"name": "screenshot",
"response": result,
"parts": [{"inlineData": {"mimeType": mime, "data": data}}],
}
}
],
},
]
)
assert [message["role"] for message in messages] == ["assistant", "tool"]
text_part, media_part = messages[1]["content"]
assert json.loads(text_part["text"]) == result
assert media_part["image_url"]["url"] == f"data:{mime};base64,{data}"
def test_adapter_import():
"""Test that the adapter can be imported successfully"""
from litellm.google_genai.adapters.handler import GenerateContentToCompletionHandler