Skip to content
Draft
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
13 changes: 12 additions & 1 deletion openhands-tools/openhands/tools/terminal/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down
23 changes: 23 additions & 0 deletions tests/tools/terminal/test_terminal_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"