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):