Extending the CLI#
This guide explains how to add new subcommands to the lmcache CLI.
Architecture Overview#
The CLI uses explicit command registration:
Each command is a class inheriting from
BaseCommandinlmcache/cli/commands/base.py.Commands are instantiated and listed in
ALL_COMMANDSinlmcache/cli/commands/__init__.py.At startup,
main.pyiteratesALL_COMMANDSand callscmd.register(subparsers)to wire up argparse.
BaseCommand is an abstract class with four required methods. Forgetting any
of them raises TypeError at instantiation time.
File Layout#
lmcache/cli/
├── __init__.py
├── main.py # Entry point
├── metrics/ # Metrics system
│ ├── __init__.py # Re-exports
│ ├── metrics.py # Metrics collector
│ ├── section.py # Section data class
│ ├── handler.py # StreamHandler, FileHandler
│ └── formatter.py # TerminalFormatter, JsonFormatter
└── commands/
├── __init__.py # ALL_COMMANDS registry
├── base.py # BaseCommand ABC
└── mock.py # Example command
Step-by-Step: Adding a New Command#
Step 1. Create lmcache/cli/commands/describe.py:
# SPDX-License-Identifier: Apache-2.0
import argparse
from lmcache.cli.commands.base import BaseCommand
class DescribeCommand(BaseCommand):
def name(self) -> str:
return "describe"
def help(self) -> str:
return "Describe a running KV cache server."
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument("--url", required=True,
help="LMCache HTTP server URL (e.g. http://localhost:8000)")
def execute(self, args: argparse.Namespace) -> None:
# Connect to server, gather info...
metrics = self.create_metrics("Describe KV Cache", args)
metrics.add("status", "Status", "OK")
metrics.add("chunks", "Cached chunks", 1024)
metrics.emit()
Step 2. Register it in lmcache/cli/commands/__init__.py:
from lmcache.cli.commands.describe import DescribeCommand
ALL_COMMANDS: list[BaseCommand] = [
MockCommand(),
DescribeCommand(), # add here
]
That’s it — lmcache describe --url http://localhost:8000 is now available.
Using the Metrics System#
The metrics system uses a handler + formatter architecture:
Metrics — the collector. Holds sections and entries.
Handler — the destination (stdout, file, etc.).
Formatter — the rendering (ASCII table, JSON, etc.).
BaseCommand.create_metrics() sets up default handlers automatically, so
command authors just build metrics and call emit():
def execute(self, args: argparse.Namespace) -> None:
# create_metrics() auto-registers:
# - StreamHandler → stdout (formatter chosen by --format, default: terminal)
# - FileHandler → if --output is set (same format as --format)
metrics = self.create_metrics("Bench KV Cache Result", args)
# Create named sections
metrics.add_section("ops", "Operations (ops/s)")
metrics["ops"].add("store", "Store", 41.3)
metrics["ops"].add("retrieve", "Retrieve", 127.3)
# Top-level metrics (no section header)
metrics.add("status", "Status", "OK")
# Trigger all handlers
metrics.emit()
The --format and --output flags are added automatically by
BaseCommand.register() — subcommands do not need to add them manually.