diff --git a/openhands-cli/README.md b/openhands-cli/README.md
index 740a50b99c..d267a6d4ed 100644
--- a/openhands-cli/README.md
+++ b/openhands-cli/README.md
@@ -33,4 +33,29 @@ uv run openhands
# The binary will be in dist/
./dist/openhands # macOS/Linux
# dist/openhands.exe # Windows
-```
\ No newline at end of file
+```
+
+## Enterprise Gateway Support
+
+For enterprise users with custom LLM gateways, you can provide a gateway configuration file to handle authentication and custom headers/parameters.
+
+### Using Gateway Configuration
+
+```bash
+# Using command line flag
+uv run openhands --gateway-config ~/mycompany-gateway.toml
+
+# Or using environment variable
+export OPENHANDS_GATEWAY_CONFIG=~/mycompany-gateway.toml
+uv run openhands
+```
+
+See `examples/gateway-config-example.toml` for a complete configuration example with comments.
+
+### Key Features
+
+- **OAuth2/Token Exchange**: Automatically handles token acquisition and refresh
+- **Custom Headers**: Add headers required by your gateway
+- **Environment Variables**: Use `${ENV:VAR_NAME}` syntax for sensitive values
+- **Extra Body Parameters**: Include additional fields in LLM request bodies
+- **TOML Format**: Clean, readable configuration with comments
\ No newline at end of file
diff --git a/openhands-cli/examples/gateway-config-example.toml b/openhands-cli/examples/gateway-config-example.toml
new file mode 100644
index 0000000000..4433411865
--- /dev/null
+++ b/openhands-cli/examples/gateway-config-example.toml
@@ -0,0 +1,53 @@
+# Enterprise Gateway Configuration Example for OpenHands CLI
+# Configure OpenHands to work with enterprise LLM gateways
+#
+# Usage:
+# 1. Copy this file and customize it.
+# 2. Reference secrets with ${ENV:VAR_NAME} syntax.
+# 3. Run: openhands --gateway-config /path/to/your-config.toml
+# or export OPENHANDS_GATEWAY_CONFIG=/path/to/your-config.toml and run openhands
+
+# Optional provider name for logging/debugging.
+gateway_provider = "custom"
+
+# === Identity Provider Configuration ===
+# Remove this section entirely if the gateway does not require token exchange.
+gateway_auth_url = "https://identity.example.com/oauth2/token"
+gateway_auth_method = "POST"
+gateway_auth_token_path = "access_token" # Required if using identity provider (dot notation supported)
+gateway_auth_expires_in_path = "expires_in" # Optional; remove if response lacks expiry
+gateway_auth_token_ttl = 3600 # Optional fallback (seconds). Remove for default (300s).
+gateway_auth_verify_ssl = true # Set to false only for local/self-signed testing
+
+[gateway_auth_headers]
+# Headers sent to the identity provider. Remove if not needed.
+Content-Type = "application/json"
+X-Client-Id = "${ENV:GATEWAY_CLIENT_ID}"
+X-Client-Secret = "${ENV:GATEWAY_CLIENT_SECRET}"
+
+[gateway_auth_body]
+# Request body for the identity provider. Remove if not needed.
+grant_type = "client_credentials"
+audience = "llm-gateway"
+scope = "llm:access"
+
+# === Gateway Request Configuration ===
+gateway_token_header = "Authorization" # Optional. Remove to use default ("Authorization")
+gateway_token_prefix = "Bearer " # Optional. Remove for no prefix
+
+[custom_headers]
+# Headers added to every LLM request. Remove entries you do not need.
+X-Gateway-Context = "openhands-cli"
+X-Request-Priority = "normal"
+X-Tenant-Id = "${ENV:TENANT_ID}"
+
+[extra_body_params] # Optional JSON body additions merged into the request payload.
+
+ [extra_body_params.metadata]
+ source = "cli"
+ version = "1.0"
+ user = "${ENV:USER}"
+
+ [extra_body_params.gateway_options]
+ retry_on_failure = true
+ timeout_seconds = 30
diff --git a/openhands-cli/openhands_cli/agent_chat.py b/openhands-cli/openhands_cli/agent_chat.py
index b7fcaaf359..595f372d58 100644
--- a/openhands-cli/openhands_cli/agent_chat.py
+++ b/openhands-cli/openhands_cli/agent_chat.py
@@ -55,9 +55,15 @@ def _print_exit_hint(conversation_id: str) -> None:
-def run_cli_entry(resume_conversation_id: str | None = None) -> None:
+def run_cli_entry(
+ resume_conversation_id: str | None = None,
+ gateway_config_path: str | None = None
+) -> None:
"""Run the agent chat session using the agent SDK.
+ Args:
+ resume_conversation_id: Optional conversation ID to resume
+ gateway_config_path: Optional path to gateway configuration file
Raises:
AgentSetupError: If agent setup fails
@@ -66,7 +72,10 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
"""
try:
- conversation = start_fresh_conversation(resume_conversation_id)
+ conversation = start_fresh_conversation(
+ resume_conversation_id,
+ gateway_config_path=gateway_config_path
+ )
except MissingAgentSpec:
print_formatted_text(HTML('\nSetup is required to use OpenHands CLI.'))
print_formatted_text(HTML('\nGoodbye! 👋'))
diff --git a/openhands-cli/openhands_cli/argparsers/main_parser.py b/openhands-cli/openhands_cli/argparsers/main_parser.py
index 6f28d1e637..718eec6e66 100644
--- a/openhands-cli/openhands_cli/argparsers/main_parser.py
+++ b/openhands-cli/openhands_cli/argparsers/main_parser.py
@@ -1,6 +1,7 @@
"""Main argument parser for OpenHands CLI."""
import argparse
+import os
def create_main_parser() -> argparse.ArgumentParser:
@@ -30,6 +31,14 @@ Examples:
type=str,
help='Conversation ID to resume'
)
+
+ parser.add_argument(
+ '--gateway-config',
+ type=str,
+ default=os.environ.get('OPENHANDS_GATEWAY_CONFIG'),
+ help='Path to enterprise gateway configuration (JSON file). Can also be set via OPENHANDS_GATEWAY_CONFIG environment variable.',
+ metavar='PATH'
+ )
# Only serve as subcommand
subparsers = parser.add_subparsers(
diff --git a/openhands-cli/openhands_cli/gateway_config.py b/openhands-cli/openhands_cli/gateway_config.py
new file mode 100644
index 0000000000..d612f7a74c
--- /dev/null
+++ b/openhands-cli/openhands_cli/gateway_config.py
@@ -0,0 +1,106 @@
+"""
+Gateway configuration loader for OpenHands CLI.
+Handles loading and parsing of enterprise gateway configuration files.
+"""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+from typing import Any
+
+try: # Python 3.11+
+ import tomllib # type: ignore[attr-defined]
+except ModuleNotFoundError: # pragma: no cover - fallback for older interpreters
+ import tomli as tomllib # type: ignore[no-redef]
+
+_VALID_FIELDS: set[str] = {
+ 'gateway_provider',
+ 'gateway_auth_url',
+ 'gateway_auth_method',
+ 'gateway_auth_headers',
+ 'gateway_auth_body',
+ 'gateway_auth_token_path',
+ 'gateway_auth_expires_in_path',
+ 'gateway_auth_token_ttl',
+ 'gateway_token_header',
+ 'gateway_token_prefix',
+ 'gateway_auth_verify_ssl',
+ 'custom_headers',
+ 'extra_body_params',
+}
+
+
+def load_gateway_config(config_path: Path | str) -> dict[str, Any]:
+ """Load gateway configuration from a TOML file.
+
+ Args:
+ config_path: Path to the gateway configuration file
+
+ Returns:
+ Dictionary containing gateway configuration
+
+ Raises:
+ FileNotFoundError: If the config file doesn't exist
+ ValueError: If the file cannot be parsed or is not a mapping
+ """
+ if isinstance(config_path, str):
+ config_path = Path(config_path)
+
+ config_path = config_path.expanduser()
+
+ if not config_path.exists():
+ raise FileNotFoundError(f"Gateway config file not found: {config_path}")
+
+ if config_path.suffix.lower() not in {'.toml', '.tml'}:
+ raise ValueError(f'Gateway configuration must be TOML: {config_path}')
+
+ try:
+ with open(config_path, 'rb') as f:
+ config = tomllib.load(f)
+ except tomllib.TOMLDecodeError as exc:
+ raise ValueError(f'Invalid TOML in gateway config file: {exc}') from exc
+
+ if not isinstance(config, dict):
+ raise ValueError('Gateway config must be a mapping/object')
+
+ unknown_fields = set(config.keys()) - _VALID_FIELDS
+ if unknown_fields:
+ print(f'Warning: Unknown gateway config fields will be ignored: {unknown_fields}')
+
+ return config
+
+
+def expand_env_vars(config: dict[str, Any]) -> dict[str, Any]:
+ """Expand environment variables in config values.
+
+ Supports ${ENV:VAR_NAME} syntax in string values.
+
+ Args:
+ config: Gateway configuration dictionary
+
+ Returns:
+ Configuration with environment variables expanded
+ """
+ import os
+
+ def expand_value(value: Any) -> Any:
+ if isinstance(value, str):
+ # Look for ${ENV:VAR_NAME} pattern
+ pattern = r'\$\{ENV:([^}]+)\}'
+
+ def replacer(match):
+ var_name = match.group(1)
+ env_value = os.environ.get(var_name)
+ if env_value is None:
+ raise ValueError(f"Environment variable {var_name} is not set")
+ return env_value
+
+ return re.sub(pattern, replacer, value)
+ elif isinstance(value, dict):
+ return {k: expand_value(v) for k, v in value.items()}
+ elif isinstance(value, list):
+ return [expand_value(v) for v in value]
+ return value
+
+ return expand_value(config)
diff --git a/openhands-cli/openhands_cli/setup.py b/openhands-cli/openhands_cli/setup.py
index 9e74fa99be..14e82147db 100644
--- a/openhands-cli/openhands_cli/setup.py
+++ b/openhands-cli/openhands_cli/setup.py
@@ -28,13 +28,16 @@ class MissingAgentSpec(Exception):
def setup_conversation(
conversation_id: str | None = None,
- include_security_analyzer: bool = True
+ include_security_analyzer: bool = True,
+ gateway_config_path: str | None = None
) -> BaseConversation:
"""
Setup the conversation with agent.
Args:
conversation_id: conversation ID to use. If not provided, a random UUID will be generated.
+ include_security_analyzer: Whether to include the security analyzer
+ gateway_config_path: Optional path to gateway configuration file
Raises:
MissingAgentSpec: If agent specification is not found or invalid.
@@ -62,6 +65,54 @@ def setup_conversation(
'Agent specification not found. Please configure your agent settings.'
)
+ # Apply gateway configuration if provided
+ if gateway_config_path:
+ from openhands_cli.gateway_config import load_gateway_config, expand_env_vars
+ try:
+ gateway_config = load_gateway_config(gateway_config_path)
+ except FileNotFoundError as e:
+ raise ValueError(
+ f"Gateway configuration file not found: {gateway_config_path}\n"
+ f"Please check the file path or set OPENHANDS_GATEWAY_CONFIG correctly."
+ ) from e
+ except ValueError as e:
+ raise ValueError(
+ f"Invalid gateway configuration file: {e}\n"
+ f"Please ensure {gateway_config_path} is a valid TOML file."
+ ) from e
+
+ try:
+ gateway_config = expand_env_vars(gateway_config)
+ except ValueError as e:
+ raise ValueError(
+ f"Failed to expand environment variables in gateway config: {e}\n"
+ f"Make sure all referenced environment variables are set."
+ ) from e
+
+ # Update the agent's LLM with gateway configuration
+ llm = agent.llm
+ # Create a new LLM instance with the gateway config
+ from openhands.sdk import LLM
+ from openhands_cli.llm_utils import get_llm_metadata
+
+ llm_kwargs = {
+ 'model': llm.model,
+ 'api_key': llm.api_key,
+ 'base_url': llm.base_url,
+ 'service_id': llm.service_id,
+ 'metadata': get_llm_metadata(model_name=llm.model, llm_type='agent'),
+ **gateway_config # Add all gateway config fields
+ }
+
+ try:
+ new_llm = LLM(**llm_kwargs)
+ agent = agent.model_copy(update={'llm': new_llm})
+ print(f"✓ Gateway configuration loaded from: {gateway_config_path}")
+ except Exception as e:
+ raise ValueError(
+ f"Failed to apply gateway configuration: {e}\n"
+ f"Please check that all gateway settings in {gateway_config_path} are valid."
+ ) from e
if not include_security_analyzer:
# Remove security analyzer from agent spec
@@ -89,7 +140,8 @@ def setup_conversation(
def start_fresh_conversation(
- resume_conversation_id: str | None = None
+ resume_conversation_id: str | None = None,
+ gateway_config_path: str | None = None
) -> BaseConversation:
"""Start a fresh conversation by creating a new conversation instance.
@@ -98,6 +150,7 @@ def start_fresh_conversation(
Args:
resume_conversation_id: Optional conversation ID to resume
+ gateway_config_path: Optional path to gateway configuration file
Returns:
BaseConversation: A new conversation instance
@@ -105,7 +158,10 @@ def start_fresh_conversation(
conversation = None
settings_screen = SettingsScreen()
try:
- conversation = setup_conversation(resume_conversation_id)
+ conversation = setup_conversation(
+ resume_conversation_id,
+ gateway_config_path=gateway_config_path
+ )
return conversation
except MissingAgentSpec:
# For first-time users, show the full settings flow with choice between basic/advanced
@@ -113,4 +169,7 @@ def start_fresh_conversation(
# Try once again after settings setup attempt
- return setup_conversation(resume_conversation_id)
+ return setup_conversation(
+ resume_conversation_id,
+ gateway_config_path=gateway_config_path
+ )
diff --git a/openhands-cli/openhands_cli/simple_main.py b/openhands-cli/openhands_cli/simple_main.py
index 343d37a4d3..3d445bc58b 100644
--- a/openhands-cli/openhands_cli/simple_main.py
+++ b/openhands-cli/openhands_cli/simple_main.py
@@ -42,7 +42,10 @@ def main() -> None:
from openhands_cli.agent_chat import run_cli_entry
# Start agent chat
- run_cli_entry(resume_conversation_id=args.resume)
+ run_cli_entry(
+ resume_conversation_id=args.resume,
+ gateway_config_path=args.gateway_config
+ )
except KeyboardInterrupt:
print_formatted_text(HTML('\nGoodbye! 👋'))
except EOFError:
diff --git a/openhands-cli/openhands_cli/tui/settings/settings_screen.py b/openhands-cli/openhands_cli/tui/settings/settings_screen.py
index 7119f275f0..95fc7bbdae 100644
--- a/openhands-cli/openhands_cli/tui/settings/settings_screen.py
+++ b/openhands-cli/openhands_cli/tui/settings/settings_screen.py
@@ -1,6 +1,4 @@
-import json
import os
-from typing import Any
from openhands.sdk import LLM, BaseConversation, LocalFileStore
from openhands.sdk.security.confirmation_policy import NeverConfirm
@@ -25,7 +23,6 @@ from openhands_cli.user_actions.settings_action import (
save_settings_confirmation,
settings_type_confirmation,
)
-from openhands_cli.user_actions.utils import cli_confirm, cli_text_input
class SettingsScreen:
@@ -151,18 +148,17 @@ class SettingsScreen:
def handle_advanced_settings(self, escapable=True):
"""Handle advanced settings configuration with clean step-by-step flow."""
- step_counter = StepCounter(18)
+ step_counter = StepCounter(4)
try:
custom_model = prompt_custom_model(step_counter)
base_url = prompt_base_url(step_counter)
api_key = prompt_api_key(
step_counter,
custom_model.split('/')[0] if len(custom_model.split('/')) > 1 else '',
- self.conversation.agent.llm.api_key if self.conversation else None,
+ self.conversation.state.agent.llm.api_key if self.conversation else None,
escapable=escapable,
)
memory_condensation = choose_memory_condensation(step_counter)
- gateway_settings = self._prompt_gateway_settings(step_counter)
# Confirm save
save_settings_confirmation()
@@ -172,32 +168,16 @@ class SettingsScreen:
# Store the collected settings for persistence
self._save_advanced_settings(
- custom_model,
- base_url,
- api_key,
- memory_condensation,
- gateway_settings,
+ custom_model, base_url, api_key, memory_condensation
)
- def _save_llm_settings(
- self,
- model,
- api_key,
- base_url: str | None = None,
- gateway_options: dict[str, Any] | None = None,
- ) -> None:
- gateway_kwargs = {
- key: value
- for key, value in (gateway_options or {}).items()
- if value is not None and value != {}
- }
+ def _save_llm_settings(self, model, api_key, base_url: str | None = None) -> None:
llm = LLM(
model=model,
api_key=api_key,
base_url=base_url,
service_id='agent',
metadata=get_llm_metadata(model_name=model, llm_type='agent'),
- **gateway_kwargs,
)
agent = self.agent_store.load()
@@ -208,19 +188,9 @@ class SettingsScreen:
self.agent_store.save(agent)
def _save_advanced_settings(
- self,
- custom_model: str,
- base_url: str,
- api_key: str,
- memory_condensation: bool,
- gateway_settings: dict[str, Any] | None,
+ self, custom_model: str, base_url: str, api_key: str, memory_condensation: bool
):
- self._save_llm_settings(
- custom_model,
- api_key,
- base_url=base_url,
- gateway_options=gateway_settings,
- )
+ self._save_llm_settings(custom_model, api_key, base_url=base_url)
agent_spec = self.agent_store.load()
if not agent_spec:
@@ -230,181 +200,3 @@ class SettingsScreen:
agent_spec.model_copy(update={'condenser': None})
self.agent_store.save(agent_spec)
-
- def _prompt_gateway_settings(self, step_counter: StepCounter) -> dict[str, Any] | None:
- """Collect enterprise gateway configuration from the user."""
-
- options = ['Yes, configure gateway settings', 'No, skip']
- try:
- choice = cli_confirm(
- step_counter.next_step(
- 'Configure enterprise gateway settings (e.g., Tachyon)? '
- ),
- options,
- escapable=True,
- )
- except KeyboardInterrupt:
- raise
-
- if choice == 1:
- return {}
-
- gateway_provider = cli_text_input(
- step_counter.next_step(
- 'Gateway provider name (ENTER for "tachyon"): '
- ),
- escapable=True,
- )
- gateway_provider = gateway_provider or 'tachyon'
-
- gateway_auth_url = cli_text_input(
- step_counter.next_step(
- 'Identity provider URL (ENTER to skip if not required): '
- ),
- escapable=True,
- )
-
- method_input = cli_text_input(
- step_counter.next_step(
- 'Identity provider HTTP method (ENTER for POST): '
- ),
- escapable=True,
- )
- gateway_auth_method = method_input.upper() if method_input else 'POST'
-
- gateway_auth_headers = self._prompt_json_mapping(
- step_counter.next_step(
- 'Identity provider headers as JSON (ENTER to skip): '
- )
- )
-
- gateway_auth_body = self._prompt_json_mapping(
- step_counter.next_step(
- 'Identity provider JSON body (ENTER to skip): '
- )
- )
-
- token_path = cli_text_input(
- step_counter.next_step(
- 'Token path in identity provider response (ENTER for access_token): '
- ),
- escapable=True,
- )
- gateway_auth_token_path = token_path or 'access_token'
-
- expires_in_path = cli_text_input(
- step_counter.next_step(
- 'expires_in path in response (ENTER to skip): '
- ),
- escapable=True,
- )
- gateway_auth_expires_in_path = expires_in_path or None
-
- ttl_seconds = self._prompt_optional_int(
- step_counter.next_step(
- 'Token TTL fallback in seconds (ENTER to skip): '
- )
- )
-
- token_header_input = cli_text_input(
- step_counter.next_step(
- 'Header name for gateway token (ENTER for Authorization): '
- ),
- escapable=True,
- )
- gateway_token_header = token_header_input or 'Authorization'
-
- token_prefix_input = cli_text_input(
- step_counter.next_step(
- 'Token prefix (ENTER for "Bearer "): '
- ),
- escapable=True,
- )
- gateway_token_prefix = token_prefix_input if token_prefix_input != '' else 'Bearer '
-
- verify_choice = cli_confirm(
- step_counter.next_step(
- 'Verify TLS certificates for identity provider? '
- '(recommended): '
- ),
- ['Yes', 'No'],
- escapable=True,
- )
- gateway_auth_verify_ssl = verify_choice == 0
-
- custom_headers = self._prompt_json_mapping(
- step_counter.next_step(
- 'Additional headers for gateway requests (ENTER to skip): '
- )
- )
-
- extra_body = self._prompt_json_mapping(
- step_counter.next_step(
- 'Additional JSON body params for gateway requests (ENTER to skip): '
- )
- )
-
- settings: dict[str, Any] = {
- 'gateway_provider': gateway_provider,
- 'gateway_auth_url': gateway_auth_url or None,
- 'gateway_auth_method': gateway_auth_method,
- 'gateway_auth_headers': gateway_auth_headers,
- 'gateway_auth_body': gateway_auth_body,
- 'gateway_auth_token_path': gateway_auth_token_path,
- 'gateway_auth_expires_in_path': gateway_auth_expires_in_path,
- 'gateway_auth_token_ttl': ttl_seconds,
- 'gateway_token_header': gateway_token_header,
- 'gateway_token_prefix': gateway_token_prefix,
- 'gateway_auth_verify_ssl': gateway_auth_verify_ssl,
- 'custom_headers': custom_headers,
- 'extra_body_params': extra_body,
- }
-
- return settings
-
- def _prompt_json_mapping(self, question: str) -> dict[str, Any] | None:
- while True:
- try:
- raw_value = cli_text_input(question, escapable=True)
- except KeyboardInterrupt:
- raise
-
- if not raw_value:
- return None
-
- try:
- parsed = json.loads(raw_value)
- except json.JSONDecodeError as err:
- print_formatted_text(
- HTML(
- f"\nInvalid JSON: {err}. Please enter a JSON object or press ENTER to skip."
- )
- )
- continue
-
- if not isinstance(parsed, dict):
- print_formatted_text(
- HTML(
- '\nPlease enter a JSON object (e.g., {"X-Header": "value"}).'
- )
- )
- continue
-
- return parsed
-
- def _prompt_optional_int(self, question: str) -> int | None:
- while True:
- try:
- raw_value = cli_text_input(question, escapable=True)
- except KeyboardInterrupt:
- raise
-
- if not raw_value:
- return None
-
- try:
- return int(raw_value)
- except ValueError:
- print_formatted_text(
- HTML('\nPlease enter a valid integer value.')
- )
diff --git a/openhands-cli/pyproject.toml b/openhands-cli/pyproject.toml
index 17df247fd3..0c87377c6d 100644
--- a/openhands-cli/pyproject.toml
+++ b/openhands-cli/pyproject.toml
@@ -18,10 +18,12 @@ classifiers = [
# Using Git URLs for dependencies so installs from PyPI pull from GitHub
# TODO: pin package versions once agent-sdk has published PyPI packages
dependencies = [
- "openhands-sdk==1.0.0a3",
- "openhands-tools==1.0.0a3",
+ "fastapi>=0.120.2",
+ "openhands-sdk>=1.0.0a4",
+ "openhands-tools>=1.0.0a3",
"prompt-toolkit>=3",
"typer>=0.17.4",
+ "uvicorn>=0.38.0",
]
scripts = { openhands = "openhands_cli.simple_main:main" }
diff --git a/openhands-cli/tests/commands/test_new_command.py b/openhands-cli/tests/commands/test_new_command.py
index 8b6d10249e..144be95087 100644
--- a/openhands-cli/tests/commands/test_new_command.py
+++ b/openhands-cli/tests/commands/test_new_command.py
@@ -20,7 +20,7 @@ def test_start_fresh_conversation_success(mock_setup_conversation):
# Verify the result
assert result == mock_conversation
- mock_setup_conversation.assert_called_once_with(None)
+ mock_setup_conversation.assert_called_once_with(None, gateway_config_path=None)
@patch('openhands_cli.setup.SettingsScreen')
@@ -49,6 +49,9 @@ def test_start_fresh_conversation_missing_agent_spec(
assert result == mock_conversation
# Should be called twice: first fails, second succeeds
assert mock_setup_conversation.call_count == 2
+ for call in mock_setup_conversation.call_args_list:
+ assert call.args[0] is None
+ assert call.kwargs.get('gateway_config_path') is None
# Settings screen should be called once with first_time=True (new behavior)
mock_settings_screen.configure_settings.assert_called_once_with(first_time=True)
diff --git a/openhands-cli/tests/test_conversation_runner.py b/openhands-cli/tests/test_conversation_runner.py
index 447c0edd17..0461b31a42 100644
--- a/openhands-cli/tests/test_conversation_runner.py
+++ b/openhands-cli/tests/test_conversation_runner.py
@@ -68,6 +68,7 @@ class TestConversationRunner:
convo = Conversation(agent)
convo.max_iteration_per_run = 1
+ convo.max_iteration_per_run = 1
convo.state.agent_status = agent_status
cr = ConversationRunner(convo)
cr.set_confirmation_policy(NeverConfirm())
@@ -108,10 +109,16 @@ class TestConversationRunner:
agent.security_analyzer = MagicMock()
convo = Conversation(agent)
+ convo.max_iteration_per_run = 1
convo.state.agent_status = AgentExecutionStatus.WAITING_FOR_CONFIRMATION
+ def fake_run() -> None:
+ agent.step(convo.state, lambda *_args, **_kwargs: None)
+
cr = ConversationRunner(convo)
cr.set_confirmation_policy(AlwaysConfirm())
with patch.object(
+ convo, 'run', side_effect=fake_run
+ ), patch.object(
cr, '_handle_confirmation_request', return_value=confirmation
) as mock_confirmation_request:
cr.process_message(message=None)
@@ -131,10 +138,15 @@ class TestConversationRunner:
convo = Conversation(agent)
convo.state.agent_status = AgentExecutionStatus.PAUSED
+ def fake_run() -> None:
+ agent.step(convo.state, lambda *_args, **_kwargs: None)
+
cr = ConversationRunner(convo)
cr.set_confirmation_policy(AlwaysConfirm())
- with patch.object(cr, '_handle_confirmation_request') as _mock_h:
+ with patch.object(convo, 'run', side_effect=fake_run), patch.object(
+ cr, '_handle_confirmation_request'
+ ) as _mock_h:
cr.process_message(message=None)
# No confirmation was needed up front; we still expect exactly one run.
diff --git a/openhands-cli/tests/test_main.py b/openhands-cli/tests/test_main.py
index 2e2a4a47ca..99d3d9f96f 100644
--- a/openhands-cli/tests/test_main.py
+++ b/openhands-cli/tests/test_main.py
@@ -26,7 +26,9 @@ class TestMainEntryPoint:
simple_main.main()
# Should call run_cli_entry with no resume conversation ID
- mock_run_agent_chat.assert_called_once_with(resume_conversation_id=None)
+ mock_run_agent_chat.assert_called_once_with(
+ resume_conversation_id=None, gateway_config_path=None
+ )
@patch('openhands_cli.agent_chat.run_cli_entry')
@patch('sys.argv', ['openhands'])
@@ -88,7 +90,8 @@ class TestMainEntryPoint:
# Should call run_cli_entry with the provided resume conversation ID
mock_run_agent_chat.assert_called_once_with(
- resume_conversation_id='test-conversation-id'
+ resume_conversation_id='test-conversation-id',
+ gateway_config_path=None,
)
@@ -97,8 +100,14 @@ class TestMainEntryPoint:
@pytest.mark.parametrize(
"argv,expected_kwargs",
[
- (['openhands'], {"resume_conversation_id": None}),
- (['openhands', '--resume', 'test-id'], {"resume_conversation_id": 'test-id'}),
+ (
+ ['openhands'],
+ {"resume_conversation_id": None, "gateway_config_path": None},
+ ),
+ (
+ ['openhands', '--resume', 'test-id'],
+ {"resume_conversation_id": 'test-id', "gateway_config_path": None},
+ ),
],
)
def test_main_cli_calls_run_cli_entry(monkeypatch, argv, expected_kwargs):