From b0f1c974b5d3b92cd9e490c137780232dfa49aa9 Mon Sep 17 00:00:00 2001 From: Jeff May Date: Mon, 15 Jun 2026 12:46:39 -0700 Subject: [PATCH] fix(terminal): remap 'commands' alias to 'command' for google/gemma-4 google/gemma-4 models consistently emit "commands" (plural) as the terminal tool's argument name instead of the correct "command" (singular), causing every shell invocation to fail with a Pydantic validation error. A mode='before' model_validator on TerminalAction silently remaps the wrong key before validation so the error path is never reached. This is intentionally placed on the model rather than in fix_malformed_tool_arguments because the two utilities address different problems: fix_malformed_tool_arguments corrects wrong VALUE types (JSON- encoded strings, chunked lists); a model_validator is the right layer for wrong KEY names, keeping each concern co-located with what it describes. Co-Authored-By: Claude Sonnet 4.6 --- .../openhands/tools/terminal/definition.py | 13 ++++++++++- tests/tools/terminal/test_terminal_tool.py | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/openhands-tools/openhands/tools/terminal/definition.py b/openhands-tools/openhands/tools/terminal/definition.py index 69df4d4e94..dadad3223b 100644 --- a/openhands-tools/openhands/tools/terminal/definition.py +++ b/openhands-tools/openhands/tools/terminal/definition.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Literal -from pydantic import Field +from pydantic import Field, model_validator if TYPE_CHECKING: @@ -113,6 +113,17 @@ class TerminalAction(Action): description="If True, reset the terminal by creating a new session. Use this only when the terminal becomes unresponsive. Note that all previously set environment variables and session state will be lost after reset. Cannot be used with is_input=True.", # noqa ) + @model_validator(mode="before") + @classmethod + def _remap_commands_alias(cls, values: object) -> object: + # Some models (e.g. google/gemma-4) emit "commands" (plural) instead of + # the correct "command" field name. Silently remap before validation so + # the error-handling path is never reached for this known mismatch. + if isinstance(values, dict) and "commands" in values and "command" not in values: + values = dict(values) + values["command"] = values.pop("commands") + return values + @property def visualize(self) -> Text: """Return Rich Text representation with a shell-style prompt.""" diff --git a/tests/tools/terminal/test_terminal_tool.py b/tests/tools/terminal/test_terminal_tool.py index a1a0ec7707..a8c84e12e0 100644 --- a/tests/tools/terminal/test_terminal_tool.py +++ b/tests/tools/terminal/test_terminal_tool.py @@ -105,3 +105,26 @@ def test_bash_tool_to_openai_tool(): assert openai_tool["function"]["name"] == "terminal" assert "description" in openai_tool["function"] assert "parameters" in openai_tool["function"] + + +# --------------------------------------------------------------------------- +# Tests for "commands" (plural) → "command" alias (google/gemma-4 mismatch) +# --------------------------------------------------------------------------- + + +def test_terminal_action_commands_alias_remapped(): + """'commands' (plural) is silently remapped to 'command' (google/gemma-4).""" + action = TerminalAction.model_validate({"commands": "git remote -v"}) + assert action.command == "git remote -v" + + +def test_terminal_action_command_canonical_unchanged(): + """'command' (singular, canonical) continues to work normally.""" + action = TerminalAction.model_validate({"command": "echo hi"}) + assert action.command == "echo hi" + + +def test_terminal_action_command_takes_precedence_over_commands(): + """When both keys are present, 'command' wins and 'commands' is ignored.""" + action = TerminalAction.model_validate({"command": "echo hi", "commands": "echo ignored"}) + assert action.command == "echo hi"