Skip to content

feat: introduce commands for list, reset, studio #38

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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions solo_server/commands/list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import os
import typer
from rich.console import Console

console = Console()

def list_downloaded_models(download_dir: str = "./downloaded_models") -> None:
"""
Lists the models that have been downloaded by checking the given directory.
"""
console.print("[bold cyan]Downloaded Models:[/bold cyan]")
if not os.path.exists(download_dir):
console.print("No downloaded models directory found.", style="bold red")
return
models = os.listdir(download_dir)
if not models:
console.print("No downloaded models found.", style="yellow")
else:
for model in models:
console.print(f" - {model}")

def list_available_models() -> None:
"""
Lists models available for download.
"""
# This is a placeholder list. Replace with an API call or file read if needed.
available_models = [
"model-A",
"model-B",
"model-C",
]
console.print("[bold cyan]Available Models for Download:[/bold cyan]")
for model in available_models:
console.print(f" - {model}")

def solo_list(download_dir: str = "./downloaded_models") -> None:
"""
Lists both downloaded models and available models.
"""
console.print("[bold green]Solo List of Models[/bold green]\n")
list_downloaded_models(download_dir)
console.print("") # Blank line between sections
list_available_models()

app = typer.Typer()

@app.command("list")
def list_models(
download_dir: str = typer.Option("./downloaded_models", help="Directory where models are downloaded.")
):
"""
Command to list downloaded models and available models for download.
"""
solo_list(download_dir)

if __name__ == "__main__":
app()
74 changes: 74 additions & 0 deletions solo_server/commands/reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import typer
import subprocess
from rich.console import Console
import os
import shutil

console = Console()

def reset(
download_dir: str = typer.Option("./downloaded_models", help="Directory where models are downloaded."),
container_filter: str = typer.Option("solo", help="Filter string to match Docker container names (default: 'solo').")
) -> None:
"""
Resets the Solo environment by removing all Docker containers matching the filter
and deleting all downloaded models, resulting in a clean setup.
"""
# Check if Docker is running
try:
subprocess.run(["docker", "info"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError:
typer.echo("\n✅ Docker is not running, skipping container removal.\n")
else:
typer.echo("🛑 Removing all Solo Docker containers...")
try:
# List all containers (running or stopped) that match the container_filter
container_ids = subprocess.run(
["docker", "ps", "-a", "-q", "-f", f"name={container_filter}"],
check=True,
capture_output=True,
text=True
).stdout.strip()

if container_ids:
for container_id in container_ids.splitlines():
# Force remove container
subprocess.run(
["docker", "rm", "-f", container_id],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
typer.echo("✅ All Solo Docker containers removed successfully.")
else:
typer.echo("✅ No Solo Docker containers found.")
except subprocess.CalledProcessError as e:
typer.echo(f"❌ Failed to remove Solo Docker containers: {e.stderr if hasattr(e, 'stderr') else str(e)}", err=True)
except Exception as e:
typer.echo(f"⚠️ Unexpected error while removing Docker containers: {e}", err=True)

# Remove the downloaded models directory
typer.echo("🗑️ Removing downloaded models...")
if os.path.exists(download_dir):
try:
shutil.rmtree(download_dir)
typer.echo(f"✅ Downloaded models removed from {download_dir}")
except Exception as e:
typer.echo(f"❌ Failed to remove downloaded models: {e}", err=True)
else:
typer.echo("✅ No downloaded models directory found.")

typer.echo("🔄 Reset complete. Clean setup is ready.")

app = typer.Typer()

@app.command("reset")
def reset_command(
download_dir: str = typer.Option("./downloaded_models", help="Directory where models are downloaded."),
container_filter: str = typer.Option("solo", help="Filter string to match Docker container names (default: 'solo').")
):
reset(download_dir, container_filter)

if __name__ == "__main__":
app()
129 changes: 129 additions & 0 deletions solo_server/commands/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import typer
import os
import json
import click
import subprocess
import time
from enum import Enum
from pathlib import Path

from solo_server.config import CONFIG_PATH
from solo_server.config.config_loader import get_server_config
from solo_server.utils.hardware import detect_hardware
from solo_server.utils.server_utils import start_vllm_server, setup_ollama_server, setup_llama_cpp_server, is_huggingface_repo, pull_model_from_huggingface
from solo_server.utils.llama_cpp_utils import start_llama_cpp_server

class ServerType(str, Enum):
OLLAMA = "ollama"
VLLM = "vllm"
LLAMACPP = "llama.cpp"

def serve(
server: str = typer.Option("ollama", "--server", "-s", help="Server type (ollama, vllm, llama.cpp)"),
model: str = typer.Option(None, "--model", "-m", help="Model name or path"),
port: int = typer.Option(None, "--port", "-p", help="Port to run the server on")
):
"""Start a model server with the specified model"""

# Get hardware info and GPU configuration
cpu_model, cpu_cores, memory_gb, gpu_vendor, gpu_model, gpu_memory, compute_backend, os_name = detect_hardware()

# Load GPU configuration from config file
use_gpu = False
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'r') as f:
config = json.load(f)
use_gpu = config.get('hardware', {}).get('use_gpu', False)

# Only enable GPU if configured and supported
gpu_enabled = use_gpu and gpu_vendor in ["NVIDIA", "AMD", "Apple Silicon"]

# Normalize server name
server = server.lower()

# Validate server type
if server not in [s.value for s in ServerType]:
typer.echo(f"❌ Invalid server type: {server}. Choose from: {', '.join([s.value for s in ServerType])}", err=True)
raise typer.Exit(code=1)

# Get server configurations from YAML
vllm_config = get_server_config('vllm')
ollama_config = get_server_config('ollama')
llama_cpp_config = get_server_config('llama_cpp')

# Set default models based on server type
if not model:
if server == ServerType.VLLM.value:
model = vllm_config.get('default_model', "meta-llama/Llama-3.2-1B-Instruct")
elif server == ServerType.OLLAMA.value:
model = ollama_config.get('default_model', "llama3.2")
elif server == ServerType.LLAMACPP.value:
model = llama_cpp_config.get('default_model', "bartowski/Llama-3.2-1B-Instruct-GGUF/llama-3.2-1B-Instruct-Q4_K_M.gguf")

if not port:
if server == ServerType.VLLM.value:
port = vllm_config.get('default_port', 8000)
elif server == ServerType.OLLAMA.value:
port = ollama_config.get('default_port', 11434)
elif server == ServerType.LLAMACPP.value:
port = llama_cpp_config.get('default_port', 8080)

# Start the appropriate server
if server == ServerType.VLLM.value:
try:
if start_vllm_server(gpu_enabled, cpu_model, gpu_vendor, os_name, port, model):
typer.secho(
f"✅ vLLM server is running at http://localhost:{port}\n"
f"Use 'docker logs -f {vllm_config.get('container_name', 'solo-vllm')}' to view the logs.",
fg=typer.colors.BRIGHT_GREEN
)
except Exception as e:
typer.echo(f"❌ Failed to start vLLM server: {e}", err=True)
raise typer.Exit(code=1)

elif server == ServerType.OLLAMA.value:
# Start Ollama server
if not setup_ollama_server(gpu_enabled, gpu_vendor, port):
typer.echo("❌ Ollama server is not running!", err=True)
raise typer.Exit(code=1)

# Pull the model if not already available
try:
# Check if model exists
container_name = ollama_config.get('container_name', 'solo-ollama')
model_exists = subprocess.run(
["docker", "exec", container_name, "ollama", "list"],
capture_output=True,
text=True,
check=True
).stdout

# Check if this is a HuggingFace model
if is_huggingface_repo(model):
# Pull from HuggingFace
model = pull_model_from_huggingface(container_name, model)
elif model not in model_exists:
typer.echo(f"📥 Pulling model {model}...")
subprocess.run(
["docker", "exec", container_name, "ollama", "pull", model],
check=True
)
except subprocess.CalledProcessError as e:
typer.echo(f"❌ Failed to pull model: {e}", err=True)
raise typer.Exit(code=1)

typer.secho(
f"✅ Ollama server is running at http://localhost:{port}",
fg=typer.colors.BRIGHT_GREEN
)

elif server == ServerType.LLAMACPP.value:
# Start llama.cpp server with the specified model
if start_llama_cpp_server(os_name, model_path=model, port=port):
typer.secho(
f"✅ llama.cpp server is running at http://localhost:{port}",
fg=typer.colors.BRIGHT_GREEN
)
else:
typer.echo("❌ Failed to start llama.cpp server", err=True)
raise typer.Exit(code=1)
22 changes: 22 additions & 0 deletions solo_server/commands/studio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import typer
from rich.console import Console
import webbrowser

console = Console()

def studio() -> None:
"""
Opens the studio page in a web browser.
"""
url = "https://studio.getsolo.tech"
console.print(f"🚀 Opening Studio: [bold]{url}[/bold]...")
try:
webbrowser.open(url)
console.print("✅ Studio opened successfully.")
except Exception as e:
console.print(f"❌ Failed to open Studio: {e}", style="bold red")
except KeyboardInterrupt:
console.print("❌ Operation cancelled by user.", style="bold red")

if __name__ == "__main__":
studio()