soup/soup_cli/cli.py

262 lines
7.1 KiB
Python

"""Main CLI entry point — all commands registered here."""
import sys
# UTF-8 stdio bootstrap (v0.40.1 Part A) — must run before any Rich console
# is constructed. On Windows, reconfigures sys.stdout/stderr to UTF-8 so β /
# ✓ / box-drawing chars don't crash with UnicodeEncodeError on cp1251/cp1252.
# POSIX: no-op.
from soup_cli.utils.encoding import force_utf8_stdio
force_utf8_stdio()
_utf8_bootstrap_done = True
import typer # noqa: E402
from rich.console import Console # noqa: E402
from soup_cli import __version__ # noqa: E402
from soup_cli.commands import ( # noqa: E402
adapters,
autopilot,
bench,
can,
chat,
cost,
data,
deploy,
diff,
eval,
export,
generate,
history,
infer,
init,
merge,
migrate,
profile,
push,
recipes,
registry,
runs,
serve,
sweep,
train,
ui,
)
from soup_cli.commands import doctor as doctor_cmd # noqa: E402
from soup_cli.commands import quickstart as quickstart_cmd # noqa: E402
from soup_cli.commands import ( # noqa: E402
tui as tui_cmd,
)
from soup_cli.commands import ( # noqa: E402
why as why_cmd,
)
from soup_cli.utils.constants import GITHUB_URL # noqa: E402
console = Console()
# Global verbose flag — set via callback, read by error handler
_verbose = False
# Global log level (resolved string), set by main() callback
_log_level = "normal"
app = typer.Typer(
name="soup",
help=(
"Fine-tune LLMs in one command. No SSH, no config hell.\n\n"
f"[dim]GitHub: {GITHUB_URL}[/]"
),
no_args_is_help=True,
rich_markup_mode="rich",
)
# Register sub-commands
app.command()(init.init)
app.command()(train.train)
app.command()(chat.chat)
app.command()(cost.cost)
app.command()(push.push)
app.command(name="export")(export.export)
app.command()(merge.merge)
app.add_typer(
data.app, name="data",
help="Dataset tools: inspect, convert, merge, dedup, validate, stats.",
)
app.add_typer(
deploy.app, name="deploy",
help="Deploy models: Ollama integration (deploy, list, remove).",
)
app.add_typer(runs.app, name="runs", help="Experiment tracking: list, show, compare runs.")
app.add_typer(
eval.app, name="eval",
help="Evaluate models: benchmarks, custom evals, LLM judge, leaderboard.",
)
app.command()(migrate.migrate)
app.add_typer(
adapters.app, name="adapters",
help="Adapter management: list, info, compare LoRA adapters.",
)
app.add_typer(
recipes.app, name="recipes",
help="Ready-made configs: list, show, use, search recipes for popular models.",
)
app.command()(serve.serve)
app.command()(sweep.sweep)
app.command(name="diff")(diff.diff)
app.command()(infer.infer)
app.command()(profile.profile)
app.command()(bench.bench)
app.command()(doctor_cmd.doctor)
app.command()(quickstart_cmd.quickstart)
app.command()(ui.ui)
app.command(name="autopilot")(autopilot.autopilot_cmd)
app.add_typer(
registry.app, name="registry",
help="Model Registry: push, list, show, diff, search, promote, delete.",
)
app.command(name="history")(history.history)
app.command(name="why")(why_cmd.why)
app.command(name="tui")(tui_cmd.tui)
app.add_typer(
can.app, name="can",
help="Soup Cans: pack/inspect/verify/fork shareable .can artifacts.",
)
# Register data generate as a subcommand of data
data.app.command(name="generate")(generate.generate)
@app.command()
def version(
full: bool = typer.Option(False, "--full", "-f", help="Show system info and extras"),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Show Soup CLI version."""
import json
import platform
if json_output:
info = {
"version": __version__,
"python": platform.python_version(),
"platform": platform.system().lower(),
}
if full:
for lib in ["torch", "transformers", "peft", "trl", "datasets", "accelerate"]:
try:
mod = __import__(lib)
if hasattr(mod, "__version__"):
info[lib] = mod.__version__
except ImportError:
pass
for name in ["fastapi", "vllm", "datasketch", "lm_eval", "deepspeed", "wandb"]:
try:
mod = __import__(name)
if hasattr(mod, "__version__"):
info[name] = mod.__version__
elif hasattr(mod, "version"):
info[name] = mod.version
else:
info[name] = "installed"
except ImportError:
pass
console.print(json.dumps(info), highlight=False)
return
if not full:
console.print(f"[bold green]soup[/] v{__version__}")
console.print(f"[dim]{GITHUB_URL}[/]")
return
parts = [f"[bold green]soup[/] v{__version__}"]
parts.append(f"Python {platform.python_version()}")
# GPU info
try:
import torch
if torch.cuda.is_available():
parts.append(f"CUDA {torch.version.cuda}")
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
parts.append("MPS")
else:
parts.append("CPU only")
except ImportError:
parts.append("no torch")
# Installed extras
extras = []
for name, label in [
("fastapi", "serve"),
("vllm", "serve-fast"),
("datasketch", "data"),
("lm_eval", "eval"),
("deepspeed", "deepspeed"),
("wandb", "wandb"),
]:
try:
__import__(name)
extras.append(label)
except ImportError:
pass
if extras:
parts.append(f"extras: {', '.join(extras)}")
console.print(" | ".join(parts))
console.print(f"[dim]GitHub: [link={GITHUB_URL}]{GITHUB_URL}[/link][/]")
@app.callback(invoke_without_command=True)
def main(
ctx: typer.Context,
verbose: bool = typer.Option(
False,
"--verbose",
"-V",
help="Show full traceback on errors",
),
log_level: str = typer.Option(
"normal",
"--log-level",
help="Logging tier: quiet | normal | verbose | debug",
),
):
"""Soup — fine-tune LLMs in one command."""
global _verbose, _log_level
_verbose = verbose
from soup_cli.utils.log_level import parse_log_level, setup_logging
try:
tier = parse_log_level(log_level)
except ValueError as exc:
console.print(f"[red]error:[/] {exc}")
raise typer.Exit(code=2) from exc
_log_level = tier.value
setup_logging(tier)
def run():
"""Entry point with friendly error handling."""
try:
app()
except SystemExit:
raise
except typer.Exit:
raise
except KeyboardInterrupt:
console.print("\n[yellow]Interrupted.[/]")
sys.exit(130)
except Exception as exc:
from soup_cli.utils.errors import format_friendly_error
format_friendly_error(exc, verbose=_verbose)
sys.exit(1)
# When invoked via `soup` entry point, use run() for error handling.
# When invoked via `python -m soup_cli`, __main__.py calls run() directly.
if __name__ == "__main__":
run()