From 30604c40fc6e9ac914089376f41e118582954f22 Mon Sep 17 00:00:00 2001 From: Kaushik Ashodiya Date: Fri, 12 Sep 2025 11:23:01 -0700 Subject: [PATCH] fix: improve CLI help and version command performance (#10908) Co-authored-by: openhands --- openhands/cli/entry.py | 34 ++++---- openhands/cli/fast_help.py | 172 +++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 openhands/cli/fast_help.py diff --git a/openhands/cli/entry.py b/openhands/cli/entry.py index 1fcb6d4b63..cd753a25a5 100644 --- a/openhands/cli/entry.py +++ b/openhands/cli/entry.py @@ -2,32 +2,24 @@ import sys +# Import only essential modules for CLI help +# Other imports are deferred until they're actually needed import openhands import openhands.cli.suppress_warnings # noqa: F401 -from openhands.cli.gui_launcher import launch_gui_server -from openhands.cli.main import run_cli_command -from openhands.core.config import get_cli_parser -from openhands.core.config.arg_utils import get_subparser +from openhands.cli.fast_help import handle_fast_commands def main(): """Main entry point with subcommand support and backward compatibility.""" - parser = get_cli_parser() - - # If user only asks for --help or -h without a subcommand - if len(sys.argv) == 2 and sys.argv[1] in ('--help', '-h'): - # Print top-level help - print(parser.format_help()) - - # Also print help for `cli` subcommand - print('\n' + '=' * 80) - print('CLI command help:\n') - - cli_parser = get_subparser(parser, 'cli') - print(cli_parser.format_help()) - + # Fast path for help and version commands + if handle_fast_commands(): sys.exit(0) + # Import parser only when needed - only if we're not just showing help + from openhands.core.config import get_cli_parser + + parser = get_cli_parser() + # Special case: no subcommand provided, simulate "openhands cli" if len(sys.argv) == 1 or ( len(sys.argv) > 1 and sys.argv[1] not in ['cli', 'serve'] @@ -42,8 +34,14 @@ def main(): sys.exit(0) if args.command == 'serve': + # Import gui_launcher only when needed + from openhands.cli.gui_launcher import launch_gui_server + launch_gui_server(mount_cwd=args.mount_cwd, gpu=args.gpu) elif args.command == 'cli' or args.command is None: + # Import main only when needed + from openhands.cli.main import run_cli_command + run_cli_command(args) else: parser.print_help() diff --git a/openhands/cli/fast_help.py b/openhands/cli/fast_help.py new file mode 100644 index 0000000000..a4a15eaa89 --- /dev/null +++ b/openhands/cli/fast_help.py @@ -0,0 +1,172 @@ +"""Fast help module for OpenHands CLI. + +This module provides a lightweight implementation of the CLI help and version commands +without loading all the dependencies, which significantly improves the +performance of `openhands --help` and `openhands --version`. + +The approach is to create a simplified version of the CLI parser that only includes +the necessary options for displaying help and version information. This avoids loading +the full OpenHands codebase, which can take several seconds. + +This implementation addresses GitHub issue #10698, which reported that +`openhands --help` was taking around 20 seconds to run. +""" + +import argparse +import sys + + +def get_fast_cli_parser() -> argparse.ArgumentParser: + """Create a lightweight argument parser for CLI help command.""" + # Create a description with welcome message explaining available commands + description = ( + 'Welcome to OpenHands: Code Less, Make More\n\n' + 'OpenHands supports two main commands:\n' + ' serve - Launch the OpenHands GUI server (web interface)\n' + ' cli - Run OpenHands in CLI mode (terminal interface)\n\n' + 'Running "openhands" without a command is the same as "openhands cli"' + ) + + parser = argparse.ArgumentParser( + description=description, + prog='openhands', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='For more information about a command, run: openhands COMMAND --help', + ) + + # Create subparsers + subparsers = parser.add_subparsers( + dest='command', + title='commands', + description='OpenHands supports two main commands:', + metavar='COMMAND', + ) + + # Add 'serve' subcommand + serve_parser = subparsers.add_parser( + 'serve', help='Launch the OpenHands GUI server using Docker (web interface)' + ) + serve_parser.add_argument( + '--mount-cwd', + help='Mount the current working directory into the GUI server container', + action='store_true', + default=False, + ) + serve_parser.add_argument( + '--gpu', + help='Enable GPU support by mounting all GPUs into the Docker container via nvidia-docker', + action='store_true', + default=False, + ) + + # Add 'cli' subcommand with common arguments + cli_parser = subparsers.add_parser( + 'cli', help='Run OpenHands in CLI mode (terminal interface)' + ) + + # Add common arguments + cli_parser.add_argument( + '--config-file', + type=str, + default='config.toml', + help='Path to the config file (default: config.toml in the current directory)', + ) + cli_parser.add_argument( + '-t', + '--task', + type=str, + default='', + help='The task for the agent to perform', + ) + cli_parser.add_argument( + '-f', + '--file', + type=str, + help='Path to a file containing the task. Overrides -t if both are provided.', + ) + cli_parser.add_argument( + '-n', + '--name', + help='Session name', + type=str, + default='', + ) + cli_parser.add_argument( + '--log-level', + help='Set the log level', + type=str, + default=None, + ) + cli_parser.add_argument( + '-l', + '--llm-config', + default=None, + type=str, + help='Replace default LLM ([llm] section in config.toml) config with the specified LLM config, e.g. "llama3" for [llm.llama3] section in config.toml', + ) + cli_parser.add_argument( + '--agent-config', + default=None, + type=str, + help='Replace default Agent ([agent] section in config.toml) config with the specified Agent config, e.g. "CodeAct" for [agent.CodeAct] section in config.toml', + ) + cli_parser.add_argument( + '-v', '--version', action='store_true', help='Show version information' + ) + cli_parser.add_argument( + '--override-cli-mode', + help='Override the default settings for CLI mode', + type=bool, + default=False, + ) + parser.add_argument( + '--conversation', + help='The conversation id to continue', + type=str, + default=None, + ) + + return parser + + +def get_fast_subparser( + parser: argparse.ArgumentParser, name: str +) -> argparse.ArgumentParser: + """Get a subparser by name.""" + for action in parser._actions: + if isinstance(action, argparse._SubParsersAction): + if name in action.choices: + return action.choices[name] + raise ValueError(f"Subparser '{name}' not found") + + +def handle_fast_commands() -> bool: + """Handle fast path commands like help and version. + + Returns: + bool: True if a command was handled, False otherwise. + """ + # Handle --help or -h + if len(sys.argv) == 2 and sys.argv[1] in ('--help', '-h'): + parser = get_fast_cli_parser() + + # Print top-level help + print(parser.format_help()) + + # Also print help for `cli` subcommand + print('\n' + '=' * 80) + print('CLI command help:\n') + + cli_parser = get_fast_subparser(parser, 'cli') + print(cli_parser.format_help()) + + return True + + # Handle --version or -v + if len(sys.argv) == 2 and sys.argv[1] in ('--version', '-v'): + import openhands + + print(f'OpenHands CLI version: {openhands.get_version()}') + return True + + return False