Skip to content

Commit b50e908

Browse files
committed
release: v0.3.2 — ollama local model management
Co-Authored-By: Oz <oz-agent@warp.dev> # Conflicts: # src/specsmith/ollama_cmds.py
2 parents eb196a6 + 83ab921 commit b50e908

3 files changed

Lines changed: 386 additions & 208 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "specsmith"
7-
version = "0.3.1"
7+
version = "0.3.2"
88
description = "Applied Epistemic Engineering toolkit — forge epistemically-governed scaffolds, stress-test belief systems, and run AEE pipelines."
99
readme = "README.md"
1010
license = "MIT"

src/specsmith/cli.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3105,6 +3105,175 @@ def patent_prior_art_cmd(claim: str, max_results: int, project_dir: str, save_re
31053105
main.add_command(patent_group)
31063106

31073107

3108+
# ---------------------------------------------------------------------------
3109+
# Ollama — local model management
3110+
# ---------------------------------------------------------------------------
3111+
3112+
3113+
@main.group(name="ollama")
3114+
def ollama_group() -> None:
3115+
"""Manage Ollama local LLM models."""
3116+
3117+
3118+
@ollama_group.command(name="list")
3119+
def ollama_list_cmd() -> None:
3120+
"""List locally installed Ollama models."""
3121+
from specsmith.ollama_cmds import get_installed_models, is_running
3122+
3123+
if not is_running():
3124+
console.print("[red]\u2717[/red] Ollama is not running. Start it with: [bold]ollama serve[/bold]")
3125+
raise SystemExit(1)
3126+
3127+
models = get_installed_models()
3128+
if not models:
3129+
console.print("[yellow]No models installed.[/yellow] Pull one with: specsmith ollama pull <model>")
3130+
return
3131+
3132+
console.print(f"[bold]Installed Ollama Models[/bold] ({len(models)})\n")
3133+
for m in models:
3134+
console.print(f" [green]\u2713[/green] {m}")
3135+
3136+
3137+
@ollama_group.command(name="available")
3138+
@click.option("--task", default="", help="Filter by task type (code, requirements, architecture, chat, analysis, reasoning).")
3139+
def ollama_available_cmd(task: str) -> None:
3140+
"""Show models available to download from the curated catalog."""
3141+
from specsmith.ollama_cmds import CATALOG, get_installed_models, get_vram_gb, recommend_models
3142+
3143+
vram = get_vram_gb()
3144+
installed = set(get_installed_models())
3145+
recs = recommend_models(vram_gb=vram, task=task)
3146+
3147+
header = "[bold]Available Ollama Models[/bold]"
3148+
if task:
3149+
header += f" (task: {task})"
3150+
if vram > 0:
3151+
header += f" [dim]— GPU VRAM: {vram:.1f} GB[/dim]"
3152+
else:
3153+
header += " [dim]— no GPU detected (CPU mode)[/dim]"
3154+
console.print(header + "\n")
3155+
3156+
# Show catalog entries that fit VRAM budget
3157+
for e in recs:
3158+
is_inst = any(m.startswith(e.id.split(":")[0]) or m == e.id for m in installed)
3159+
status = "[green]installed[/green]" if is_inst else f"[dim]{e.size_gb}GB — pull to install[/dim]"
3160+
console.print(
3161+
f" {('[bold]' + e.tier + '[/bold]'):30s} {e.name:28s} {status}"
3162+
)
3163+
console.print(f" [dim]{'':<30s} {', '.join(e.best_for[:2]):<28s} {e.notes}[/dim]")
3164+
console.print()
3165+
3166+
if not recs:
3167+
console.print("[yellow]No models fit within the detected VRAM budget.[/yellow]")
3168+
console.print("Use a smaller model or run on CPU (all models listed without GPU).")
3169+
3170+
console.print(f"[dim]Pull a model: specsmith ollama pull <model-id>[/dim]")
3171+
3172+
3173+
@ollama_group.command(name="gpu")
3174+
def ollama_gpu_cmd() -> None:
3175+
"""Detect GPU and available VRAM."""
3176+
from specsmith.ollama_cmds import get_vram_gb
3177+
3178+
vram = get_vram_gb()
3179+
if vram > 0:
3180+
console.print(f"[green]\u2713[/green] GPU detected — [bold]{vram:.1f} GB[/bold] VRAM available")
3181+
# Tier suggestions
3182+
if vram >= 20:
3183+
console.print(" Tier: [bold]Powerful[/bold] — all models supported (Qwen 2.5 32B+)")
3184+
elif vram >= 9:
3185+
console.print(" Tier: [bold]Capable[/bold] — 14B models (Phi-4, Qwen 14B, Gemma 12B)")
3186+
elif vram >= 5:
3187+
console.print(" Tier: [bold]Balanced[/bold] — 7B models (Qwen 7B, Mistral, Coder 7B)")
3188+
else:
3189+
console.print(" Tier: [bold]Tiny[/bold] — small models only (Llama 3.2 3B)")
3190+
else:
3191+
console.print("[yellow]\u2014[/yellow] No GPU detected — models will run on CPU (slow)")
3192+
console.print(" Recommend: llama3.2:latest or mistral:latest for CPU use")
3193+
3194+
3195+
@ollama_group.command(name="pull")
3196+
@click.argument("model_id")
3197+
def ollama_pull_cmd(model_id: str) -> None:
3198+
"""Download a model via Ollama (streams progress).
3199+
3200+
MODEL_ID: Ollama model tag, e.g. qwen2.5:14b
3201+
3202+
Examples:\n
3203+
specsmith ollama pull qwen2.5:14b\n
3204+
specsmith ollama pull phi4:latest
3205+
"""
3206+
from specsmith.ollama_cmds import is_running, pull_model
3207+
3208+
if not is_running():
3209+
console.print(
3210+
"[red]\u2717[/red] Ollama is not running.\n"
3211+
" Start it: [bold]ollama serve[/bold]\n"
3212+
" Or open the Ollama desktop app."
3213+
)
3214+
raise SystemExit(1)
3215+
3216+
console.print(f"[bold]Pulling[/bold] {model_id} …")
3217+
last_status = ""
3218+
for chunk in pull_model(model_id):
3219+
status = chunk.get("status", "")
3220+
if chunk.get("status") == "error":
3221+
console.print(f"[red]\u2717 {chunk.get('message', 'unknown error')}[/red]")
3222+
raise SystemExit(1)
3223+
completed = chunk.get("completed", 0)
3224+
total = chunk.get("total", 0)
3225+
if total and completed:
3226+
pct = int(completed / total * 100)
3227+
mb = completed // (1024 * 1024)
3228+
total_mb = total // (1024 * 1024)
3229+
line = f" {status}: {pct}% ({mb}/{total_mb} MB)"
3230+
elif status and status != last_status:
3231+
line = f" {status}"
3232+
else:
3233+
continue
3234+
last_status = status
3235+
console.print(line)
3236+
3237+
console.print(f"[green]\u2713[/green] {model_id} ready.")
3238+
3239+
3240+
@ollama_group.command(name="suggest")
3241+
@click.argument("task", type=click.Choice(["code", "requirements", "architecture", "chat", "analysis", "reasoning"]))
3242+
def ollama_suggest_cmd(task: str) -> None:
3243+
"""Suggest the best installed Ollama models for a task.
3244+
3245+
TASK: code | requirements | architecture | chat | analysis | reasoning
3246+
"""
3247+
from specsmith.ollama_cmds import get_installed_models, get_vram_gb, recommend_models
3248+
3249+
vram = get_vram_gb()
3250+
installed = set(get_installed_models())
3251+
recs = recommend_models(vram_gb=vram, task=task)
3252+
3253+
inst_recs = [e for e in recs if any(m.startswith(e.id.split(":")[0]) or m == e.id for m in installed)]
3254+
not_inst = [e for e in recs if e not in inst_recs]
3255+
3256+
console.print(f"[bold]Model Suggestions[/bold] for task: [bold]{task}[/bold]\n")
3257+
3258+
if inst_recs:
3259+
console.print("[green]Ready to use:[/green]")
3260+
for e in inst_recs[:3]:
3261+
console.print(f" [green]\u2713[/green] {e.name:28s} {e.notes}")
3262+
console.print()
3263+
3264+
if not_inst:
3265+
console.print("[dim]Available to download:[/dim]")
3266+
for e in not_inst[:3]:
3267+
console.print(f" [dim]\u21d3[/dim] {e.name:28s} {e.size_gb}GB — specsmith ollama pull {e.id}")
3268+
3269+
if not inst_recs and not not_inst:
3270+
console.print("[yellow]No matching models found.[/yellow]")
3271+
console.print(f" Run [bold]specsmith ollama available --task {task}[/bold] to see options.")
3272+
3273+
3274+
main.add_command(ollama_group)
3275+
3276+
31083277
# ---------------------------------------------------------------------------
31093278
# Credits check + hard cap (#52)
31103279
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)