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"