Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/google/adk/models/lite_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,20 @@ def _extract_reasoning_value(message: Message | Delta | None) -> Any:
return message.get("reasoning")


_GEMMA4_MODEL_PATTERN = re.compile(r"gemma-?4")


def _is_gemma4_model(model: str) -> bool:
"""Detects Gemma 4 models across naming conventions.

Ollama uses "gemma4" (e.g. "ollama/gemma4:e2b"), while Hugging Face,
vLLM, and llama.cpp use the hyphenated "gemma-4" (e.g.
"google/gemma-4-26B-A4B"). Both need role='tool_responses' for tool
results.
"""
return bool(_GEMMA4_MODEL_PATTERN.search(model.lower()))


class ChatCompletionFileUrlObject(TypedDict, total=False):
file_data: str
file_id: str
Expand Down Expand Up @@ -997,7 +1011,8 @@ async def _content_to_message_param(
# from the tool call, instead of OpenAI-compatible 'tool' role used by other models.
# Earlier Gemma versions before version 4 do not support tool use,
# so this check is intentionally scoped to only look for "gemma4" in the model name.
tool_role = "tool_responses" if "gemma4" in model.lower() else "tool"
tool_role = "tool_responses" if _is_gemma4_model(model) else "tool"

tool_messages.append(
ChatCompletionToolMessage(
role=tool_role,
Expand Down Expand Up @@ -1165,7 +1180,7 @@ def _ensure_tool_results(messages: List[Message], model: str) -> List[Message]:

healed_messages: List[Message] = []
pending_tool_call_ids: List[str] = []
expected_tool_role = "tool_responses" if "gemma4" in model.lower() else "tool"
expected_tool_role = "tool_responses" if _is_gemma4_model(model) else "tool"

for message in messages:
role = message.get("role")
Expand Down
13 changes: 13 additions & 0 deletions tests/unittests/models/test_lite_llm_gemma_tool_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ async def test_gemma4_model_uses_tool_responses_role(self):
"template; role='tool' causes infinite tool-calling loops."
)

@pytest.mark.asyncio
async def test_gemma4_hf_style_naming_uses_tool_responses_role(self):
"""Hyphenated 'gemma-4' naming should also get role='tool_responses'."""
content = _make_function_response_content()

result = await _content_to_message_param(content, model="google/gemma-4-26B-A4B")

assert _extract_role(result) == "tool_responses", (
"Gemma models require role='tool_responses' to match their chat "
"template; role='tool' causes infinite tool-calling loops."
)

@pytest.mark.asyncio
async def test_gemma4_uppercase_model_name(self):
"""Model name matching should be case-insensitive."""
Expand All @@ -100,6 +112,7 @@ async def test_gemma4_uppercase_model_name(self):

assert _extract_role(result) == "tool_responses"


@pytest.mark.asyncio
async def test_tool_call_id_and_content_preserved(self):
"""Fix must not alter tool_call_id or content — only role changes."""
Expand Down