Files
mlx-knife/mlx_knife/cli.py
T
The BROKE Team 7d0d6be66d Release MLX Knife 1.1.0-beta3 - Critical Cache Management Fixes
Three major bug fixes for production readiness:

- Issue #21: Fix crash on fresh installations (empty cache directory)
- Issue #22: Suppress urllib3 LibreSSL warnings on macOS Python 3.9
- Issue #23: Fix double rm execution bug - models now deleted in single command

Test improvements:
- 140/140 tests passing (up from 137)
- Added real integration tests for lock cleanup
- Fixed unit tests broken by path corrections

All known cache management issues resolved for stable release.
2025-08-26 01:46:40 +02:00

134 lines
6.5 KiB
Python

# mlx_knife/cli.py
import argparse
import sys
from . import __version__
from .cache_utils import (
check_all_models_health,
check_model_health,
list_models,
rm_model,
run_model,
show_model,
)
from .hf_download import pull_model
from .server import run_server
def main():
parser = argparse.ArgumentParser(
description="MLX Knife CLI (HuggingFace-style cache management for MLX models)"
)
parser.add_argument('--version', action='version', version=f'MLX Knife {__version__}')
subparsers = parser.add_subparsers(dest="cmd")
# list
list_p = subparsers.add_parser("list", help="List available models in cache")
list_p.add_argument("model", nargs="?", help="Specific model to list (optional)")
list_p.add_argument("--all", action="store_true", help="Show all models (not just MLX)")
list_p.add_argument("--framework", choices=["mlx", "pytorch", "tokenizer"], help="Filter by framework")
list_p.add_argument("--health", action="store_true", help="Show health status")
list_p.add_argument("--verbose", action="store_true", help="Show detailed information (requires model argument)")
# pull
pull_p = subparsers.add_parser("pull", help="Download a model from HuggingFace")
pull_p.add_argument("model_spec", help="Model[@hash] (e.g. mlx-community/Qwen2.5-0.5B-Instruct-4bit@a5339a41)")
# run
run_p = subparsers.add_parser("run", help="Run a model with prompt")
run_p.add_argument("model_spec", help="Model[@hash] (e.g. mlx-community/Qwen2.5-0.5B-Instruct-4bit@a5339a41)")
run_p.add_argument("prompt", nargs="?", default=None, help="Prompt text (if not provided, enters interactive mode)")
run_p.add_argument("--interactive", "-i", action="store_true", help="Force interactive dialog mode")
run_p.add_argument("--temperature", type=float, default=0.7, help="Sampling temperature (default: 0.7)")
run_p.add_argument("--max-tokens", type=int, default=None, help="Maximum tokens to generate (default: model context length)")
run_p.add_argument("--top-p", type=float, default=0.9, help="Top-p sampling parameter (default: 0.9)")
run_p.add_argument("--repetition-penalty", type=float, default=1.1, help="Penalty for repeated tokens (default: 1.1)")
run_p.add_argument("--no-stream", action="store_true", help="Disable streaming output")
run_p.add_argument("--no-chat-template", action="store_true", help="Disable chat template formatting (use raw prompt)")
run_p.add_argument("--verbose", "-v", action="store_true", help="Show detailed output (model loading, memory usage, token stats)")
# rm
rm_p = subparsers.add_parser("rm", help="Delete a model from cache")
rm_p.add_argument("model_spec", help="Model[@hash] (e.g. mlx-community/Qwen2.5-0.5B-Instruct-4bit@a5339a41)")
rm_p.add_argument("--force", action="store_true", help="Skip confirmation and clean up cache files automatically")
# health
health_p = subparsers.add_parser("health", help="Check model integrity")
health_p.add_argument("model_spec", nargs="?", help="Model[@hash] (optional)")
health_p.add_argument("--all", action="store_true", help="Check all models in cache")
# show
show_p = subparsers.add_parser("show", help="Show detailed information about a specific model")
show_p.add_argument("model_spec", help="Model[@hash] (e.g. mlx-community/Qwen2.5-0.5B-Instruct-4bit@a5339a41)")
show_p.add_argument("--files", action="store_true", help="List all files and sizes under the model path")
show_p.add_argument("--config", action="store_true", help="Print pretty-formatted config.json")
# server
server_p = subparsers.add_parser("server", help="Start OpenAI-compatible API server")
server_p.add_argument("--host", default="127.0.0.1", help="Server host (default: 127.0.0.1)")
server_p.add_argument("--port", type=int, default=8000, help="Server port (default: 8000)")
server_p.add_argument("--max-tokens", type=int, default=None, help="Default max tokens for completions (default: model-aware dynamic limits)")
server_p.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
server_p.add_argument("--log-level", default="info", choices=["debug", "info", "warning", "error"], help="Log level (default: info)")
args = parser.parse_args()
if args.cmd == "list":
if args.model:
if args.verbose and not args.all and not args.framework and not args.health:
# Show detailed info for a specific model (same as show command)
show_model(args.model)
else:
# Show just the single model row
list_models(show_all=args.all, framework_filter=args.framework, show_health=args.health, single_model=args.model, verbose=args.verbose)
else:
# Normal list behavior - verbose works with MLX models too
list_models(show_all=args.all, framework_filter=args.framework, show_health=args.health, verbose=args.verbose)
elif args.cmd == "pull":
pull_model(args.model_spec)
elif args.cmd == "run":
run_model(
args.model_spec,
prompt=args.prompt,
interactive=args.interactive,
temperature=args.temperature,
max_tokens=args.max_tokens,
top_p=args.top_p,
repetition_penalty=args.repetition_penalty,
stream=not args.no_stream,
use_chat_template=not args.no_chat_template,
verbose=args.verbose
)
elif args.cmd == "rm":
rm_model(args.model_spec, force=args.force)
elif args.cmd == "health":
if args.model_spec:
check_model_health(args.model_spec)
else:
# Default to checking all models if no specific model is provided
check_all_models_health()
elif args.cmd == "show":
show_model(args.model_spec, show_files=args.files, show_config=args.config)
elif args.cmd == "server":
# Validate server arguments
if args.max_tokens is not None and args.max_tokens <= 0:
print(f"Error: --max-tokens must be positive, got: {args.max_tokens}")
sys.exit(1)
if args.port <= 0 or args.port > 65535:
print(f"Error: --port must be between 1-65535, got: {args.port}")
sys.exit(1)
run_server(
host=args.host,
port=args.port,
max_tokens=args.max_tokens,
reload=args.reload,
log_level=args.log_level
)
else:
parser.print_help()
if __name__ == "__main__":
main()