Skip to content

Commit 8800781

Browse files
committed
fix: match HF-style gemma-4 model names for tool_responses role
The gemma4 role fix (#5650) only matched Ollama-style names like gemma4:e2b. HF/vLLM/llama.cpp use gemma-4 with a hyphen, e.g. google/gemma-4-26B-A4B, which the substring check missed, causing the wrong tool role on non-Ollama backends. Fixes #6334
1 parent 49c0a36 commit 8800781

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

src/google/adk/models/lite_llm.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,20 @@ def _extract_reasoning_value(message: Message | Delta | None) -> Any:
587587
return message.get("reasoning")
588588

589589

590+
_GEMMA4_MODEL_PATTERN = re.compile(r"gemma-?4")
591+
592+
593+
def _is_gemma4_model(model: str) -> bool:
594+
"""Detects Gemma 4 models across naming conventions.
595+
596+
Ollama uses "gemma4" (e.g. "ollama/gemma4:e2b"), while Hugging Face,
597+
vLLM, and llama.cpp use the hyphenated "gemma-4" (e.g.
598+
"google/gemma-4-26B-A4B"). Both need role='tool_responses' for tool
599+
results.
600+
"""
601+
return bool(_GEMMA4_MODEL_PATTERN.search(model.lower()))
602+
603+
590604
class ChatCompletionFileUrlObject(TypedDict, total=False):
591605
file_data: str
592606
file_id: str
@@ -936,7 +950,8 @@ async def _content_to_message_param(
936950
# from the tool call, instead of OpenAI-compatible 'tool' role used by other models.
937951
# Earlier Gemma versions before version 4 do not support tool use,
938952
# so this check is intentionally scoped to only look for "gemma4" in the model name.
939-
tool_role = "tool_responses" if "gemma4" in model.lower() else "tool"
953+
tool_role = "tool_responses" if _is_gemma4_model(model) else "tool"
954+
940955
tool_messages.append(
941956
ChatCompletionToolMessage(
942957
role=tool_role,
@@ -1113,7 +1128,7 @@ def _ensure_tool_results(messages: List[Message], model: str) -> List[Message]:
11131128

11141129
healed_messages: List[Message] = []
11151130
pending_tool_call_ids: List[str] = []
1116-
expected_tool_role = "tool_responses" if "gemma4" in model.lower() else "tool"
1131+
expected_tool_role = "tool_responses" if _is_gemma4_model(model) else "tool"
11171132

11181133
for message in messages:
11191134
role = message.get("role")

tests/unittests/models/test_lite_llm_gemma_tool_role.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ async def test_gemma4_model_uses_tool_responses_role(self):
9191
"template; role='tool' causes infinite tool-calling loops."
9292
)
9393

94+
@pytest.mark.asyncio
95+
async def test_gemma4_hf_style_naming_uses_tool_responses_role(self):
96+
"""Hyphenated 'gemma-4' naming should also get role='tool_responses'."""
97+
content = _make_function_response_content()
98+
99+
result = await _content_to_message_param(content, model="google/gemma-4-26B-A4B")
100+
101+
assert _extract_role(result) == "tool_responses", (
102+
"Gemma models require role='tool_responses' to match their chat "
103+
"template; role='tool' causes infinite tool-calling loops."
104+
)
105+
94106
@pytest.mark.asyncio
95107
async def test_gemma4_uppercase_model_name(self):
96108
"""Model name matching should be case-insensitive."""
@@ -100,6 +112,7 @@ async def test_gemma4_uppercase_model_name(self):
100112

101113
assert _extract_role(result) == "tool_responses"
102114

115+
103116
@pytest.mark.asyncio
104117
async def test_tool_call_id_and_content_preserved(self):
105118
"""Fix must not alter tool_call_id or content — only role changes."""

0 commit comments

Comments
 (0)