Skip to content

feat: Add interactive commands to chat CLI #21091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

reidliu41
Copy link
Contributor

@reidliu41 reidliu41 commented Jul 17, 2025

Essential Elements of an Effective PR Description Checklist

  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

Purpose

Currently, the chat CLI is very basic. Common actions like changing the model, adjusting
the system prompt, or even clearing the screen require restarting the entire
application. This can be inefficient for anyone using the CLI to test models and prompts
interactively.

This PR introduces an interactive command system, accessible via /, that allows for
real-time interaction and control over the chat session, transforming it into a more
efficient tool.

  • Session Management:

    • /help: Lists all available commands.
    • /settings: Shows the current model and session settings.
    • /history [full]: Displays the conversation history.
    • /clear: Clears the current conversation.
    • /quit, /exit: Exits the chat session.
  • Model & Prompt Control:

    • /model [name]: Switches the model on-the-fly or lists available models.
    • /system [prompt]: Sets or views the system prompt dynamically.
  • Conversation Workflow:

    • /retry: Regenerates the last response from the assistant.
    • /undo: Removes the last user/assistant turn from the conversation.
  • Persistence:

    • /save [filename]: Saves the current conversation to a JSON file.
    • /load : Loads a conversation from a JSON file.
  • Readline Enhancements: Integrates the readline module for persistent command history
    and basic tab-completion for commands.

  • Fully Optional: The entire feature can be disabled at startup with the --disable-commands flag

Test Plan

Test Result

(Optional) Documentation Update

@reidliu41 reidliu41 requested a review from aarnphm as a code owner July 17, 2025 03:10
@mergify mergify bot added the frontend label Jul 17, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The code changes introduce an interactive command system for the chat CLI, enhancing usability. I've identified potential path traversal vulnerabilities and suggested improvements to exception handling and code maintainability to ensure the new functionality is robust and secure.

Comment on lines +152 to +153
filename = args[
0] if args else f"chat_{datetime.now():%Y%m%d_%H%M%S}.json"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The filename is taken directly from user input and used in a file path operation. This can lead to a path traversal vulnerability, potentially allowing a user to write files to unintended locations. I suggest validating that the resolved path is within the current working directory.

Suggested change
filename = args[
0] if args else f"chat_{datetime.now():%Y%m%d_%H%M%S}.json"
filename = args[0] if args else f"chat_{datetime.now():%Y%m%d_%H%M%S}.json"
# Prevent path traversal by ensuring the resolved path is within the current directory.
save_path = os.path.abspath(filename)
if not save_path.startswith(os.path.join(os.getcwd(), '')):
return "Error: Saving files outside the current directory is not allowed."

if not args:
return "Usage: /load <filename>"

filename = args[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The filename from user input is not sanitized, creating a path traversal vulnerability. A user could potentially load a sensitive file from an arbitrary location if they have read permissions. Restrict file loading to the current directory or a designated safe directory to mitigate this risk.

        filename = args[0]

        # Prevent path traversal by ensuring the resolved path is within the current directory.
        load_path = os.path.abspath(filename)
        if not load_path.startswith(os.path.join(os.getcwd(), '')):
            return "Error: Loading files from outside the current directory is not allowed."

Comment on lines +296 to +297
except Exception as e:
return f"Error listing models: {e}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Catching a broad Exception can mask underlying issues, such as network errors or authentication failures. Catch specific exceptions (e.g., openai.APIError) to provide more precise error feedback and avoid hiding bugs.

            except openai.APIError as e:
                return f"Error listing models: {e}"

Comment on lines +138 to +139
except Exception:
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Silently passing on all exceptions with except Exception: pass can hide critical errors. At a minimum, the exception should be logged so that the user is aware that fetching the model list failed and why.

Suggested change
except Exception:
pass
except Exception as e:
print(f"Warning: Could not fetch available models for completion: {e}")

Comment on lines 170 to 183
if result == "__EXIT__":
break

# Check for the special retry signal
if result.startswith("__RETRY__"):
# The command has already modified
# the conversation history.
# We just need to use the content
# provided in the signal
input_message = result[len("__RETRY__"):]
# Treat this as a normal chat message now
is_command_run = False
else:
print(result)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using magic strings like __EXIT__ and __RETRY__ for control flow is brittle and makes the code harder to maintain. Define these as constants in chat_commands.py and import them here.

Suggested change
if result == "__EXIT__":
break
# Check for the special retry signal
if result.startswith("__RETRY__"):
# The command has already modified
# the conversation history.
# We just need to use the content
# provided in the signal
input_message = result[len("__RETRY__"):]
# Treat this as a normal chat message now
is_command_run = False
else:
print(result)
if result == EXIT_COMMAND_SIGNAL:
break
# Check for the special retry signal
if result.startswith(RETRY_COMMAND_SIGNAL_PREFIX):
# The command has already modified
# the conversation history.
# We just need to use the content
# provided in the signal
input_message = result[len(RETRY_COMMAND_SIGNAL_PREFIX):]
# Treat this as a normal chat message now
is_command_run = False
else:
print(result)

@reidliu41
Copy link
Contributor Author

cc @DarkLight1337

Copy link
Contributor

@chaunceyjiang chaunceyjiang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question: why do we need this feature? I think this should be something that third-party tools can support — for example, some AI CLI tools.

Copy link

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

Just a reminder: PRs would not trigger full CI run by default. Instead, it would only run fastcheck CI which starts running only a small and essential subset of CI tests to quickly catch errors. You can run other CI tests on top of those by going to your fastcheck build on Buildkite UI (linked in the PR checks section) and unblock them. If you do not have permission to unblock, ping simon-mo or khluu to add you in our Buildkite org.

Once the PR is approved and ready to go, your PR reviewer(s) can run CI to test the changes comprehensively before merging.

To run CI, PR reviewers can either: Add ready label to the PR or enable auto-merge.

🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants