Skip to content

Commit 2a3daa5

Browse files
tbitcsoz-agent
andcommitted
fix: resolve CI lint failures from merge — remove duplicate ollama section, fix ruff errors
- Delete duplicate old 'ollama' CLI block (from auto-merge of main+develop) * Old block used dict-based MODEL_CATALOG/detect_gpu/get_installed_ids that no longer exist in the new dataclass-based ollama_cmds.py * New block (keep) uses CATALOG dataclass, get_vram_gb, get_installed_models - Fix all 26 ruff errors: * E501: break long console.print strings across lines * F401: remove unused 'signal' and 'CATALOG' imports * F541: remove spurious f-prefix on plain string * I001: fix import sort order in ollama_cmds.py and agent/providers/ollama.py * UP035: Iterator from collections.abc instead of typing * B904: raise SystemExit handled separately (not in except clause in new code) Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent b50e908 commit 2a3daa5

3 files changed

Lines changed: 47 additions & 252 deletions

File tree

src/specsmith/agent/providers/ollama.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@
1010
from __future__ import annotations
1111

1212
import json
13+
import os
1314
import urllib.request
1415
from collections.abc import Iterator
1516
from typing import Any
1617

17-
import os
18-
1918
from specsmith.agent.core import (
2019
CompletionResponse,
2120
Message,

src/specsmith/cli.py

Lines changed: 45 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,242 +2592,6 @@ def auth_check(project_dir: str) -> None:
25922592
main.add_command(auth)
25932593

25942594

2595-
# ---------------------------------------------------------------------------
2596-
# Ollama — local LLM model management
2597-
# ---------------------------------------------------------------------------
2598-
2599-
2600-
@main.group(name="ollama")
2601-
def ollama_group() -> None:
2602-
"""Manage local Ollama models (list, download, GPU detection)."""
2603-
2604-
2605-
@ollama_group.command(name="list")
2606-
def ollama_list() -> None:
2607-
"""List locally installed Ollama models."""
2608-
from specsmith.ollama_cmds import get_installed_models, is_running
2609-
2610-
if not is_running():
2611-
console.print("[red]✗[/red] Ollama is not running. Start it with: [bold]ollama serve[/bold]")
2612-
raise SystemExit(1)
2613-
2614-
models = get_installed_models()
2615-
if not models:
2616-
console.print("[yellow]No models installed.[/yellow] Run: [bold]specsmith ollama available[/bold]")
2617-
return
2618-
2619-
console.print(f"[bold]Installed Ollama Models[/bold] ({len(models)})\n")
2620-
for m in models:
2621-
size_gb = m.get("size", 0) / (1024**3)
2622-
modified = m.get("modified_at", "")[:10]
2623-
console.print(f" [green]✓[/green] {m['name']:<35s} {size_gb:.1f}GB [{modified}]")
2624-
2625-
2626-
@ollama_group.command(name="available")
2627-
def ollama_available() -> None:
2628-
"""Show recommended models vs installed. GPU-aware."""
2629-
from specsmith.ollama_cmds import (
2630-
MODEL_CATALOG,
2631-
detect_gpu,
2632-
get_installed_ids,
2633-
gpu_tier,
2634-
is_running,
2635-
)
2636-
2637-
gpus = detect_gpu()
2638-
vram = max((g["vram_gb"] for g in gpus), default=0)
2639-
tier = gpu_tier(vram)
2640-
budget = vram * 0.90 if vram else 999
2641-
2642-
if gpus:
2643-
console.print(f"[bold]GPU:[/bold] {gpus[0]['name']}{vram:.1f}GB VRAM ({tier})\n")
2644-
else:
2645-
console.print("[yellow]No GPU detected — CPU mode (small models only)[/yellow]\n")
2646-
2647-
running = is_running()
2648-
installed_ids: list[str] = []
2649-
if running:
2650-
installed_ids = get_installed_ids()
2651-
else:
2652-
console.print("[dim]Ollama not running — showing catalog only[/dim]\n")
2653-
2654-
console.print(
2655-
f"{'Model':<35s} {'VRAM':<8s} {'Size':<8s} {'Fits?':<7s} {'Status':<12s} Best for"
2656-
)
2657-
console.print("-" * 100)
2658-
for m in MODEL_CATALOG:
2659-
fits = m["vram_gb"] <= budget or not gpus
2660-
is_installed = any(m["id"] in iid or iid in m["id"] for iid in installed_ids)
2661-
fits_str = "[green]✓[/green]" if fits else "[dim]✓ CPU?[/dim]" if not gpus else "[red]✗ VRAM[/red]"
2662-
status = "[green]✓ Installed[/green]" if is_installed else "[dim]⬇ Available[/dim]"
2663-
best = ", ".join(m["best_for"][:2])
2664-
console.print(
2665-
f" {m['name']:<33s} {m['vram_gb']:<8.1f} {m['size_gb']:<8.1f} "
2666-
f"{fits_str:<12s} {status:<20s} {best}"
2667-
)
2668-
2669-
console.print("\n Run [bold]specsmith ollama pull <model-id>[/bold] to download")
2670-
console.print(" e.g. [bold]specsmith ollama pull qwen2.5:14b[/bold]")
2671-
2672-
2673-
@ollama_group.command(name="gpu")
2674-
def ollama_gpu() -> None:
2675-
"""Show GPU information and model tier recommendations."""
2676-
from specsmith.ollama_cmds import detect_gpu, gpu_tier, recommend_models
2677-
2678-
gpus = detect_gpu()
2679-
if not gpus:
2680-
console.print("[yellow]No dedicated GPU detected.[/yellow]")
2681-
console.print(" Ollama can run on CPU but will be slow.")
2682-
console.print(" Recommended: llama3.2:latest (2GB RAM only)")
2683-
return
2684-
2685-
for gpu in gpus:
2686-
vram = gpu["vram_gb"]
2687-
tier = gpu_tier(vram)
2688-
console.print(f"[bold]GPU:[/bold] {gpu['name']}")
2689-
console.print(f" VRAM: {vram:.1f} GB")
2690-
console.print(f" Tier: {tier}")
2691-
console.print()
2692-
2693-
recs = recommend_models(vram)
2694-
console.print(f" [bold]Recommended models for {vram:.1f}GB VRAM:[/bold]")
2695-
for m in recs:
2696-
console.print(
2697-
f" [green]✓[/green] {m['name']:<35s} "
2698-
f"{m['vram_gb']:.1f}GB — {', '.join(m['best_for'][:2])}"
2699-
)
2700-
console.print()
2701-
console.print(" Run [bold]specsmith ollama pull <model-id>[/bold] to download.")
2702-
2703-
2704-
@ollama_group.command(name="pull")
2705-
@click.argument("model")
2706-
def ollama_pull(model: str) -> None:
2707-
"""Download a model from Ollama library.
2708-
2709-
MODEL: model id, e.g. qwen2.5:14b, phi4:latest
2710-
2711-
Press Ctrl-C to cancel the download.
2712-
"""
2713-
import signal
2714-
import sys
2715-
import urllib.error
2716-
2717-
from specsmith.ollama_cmds import MODEL_CATALOG, is_running
2718-
2719-
if not is_running():
2720-
console.print("[red]✗[/red] Ollama is not running. Start it first: [bold]ollama serve[/bold]")
2721-
raise SystemExit(1)
2722-
2723-
# Show model info if in catalog
2724-
info = next((m for m in MODEL_CATALOG if m["id"] == model), None)
2725-
if info:
2726-
console.print(
2727-
f"[bold]Pulling[/bold] {info['name']} ({info['size_gb']:.1f} GB)\n"
2728-
f" Best for: {', '.join(info['best_for'])}\n"
2729-
f" Notes: {info['notes']}\n"
2730-
)
2731-
else:
2732-
console.print(f"[bold]Pulling[/bold] {model} from Ollama library\n")
2733-
2734-
console.print("[dim]Press Ctrl-C to cancel[/dim]\n")
2735-
2736-
# Stream pull progress from Ollama
2737-
import json
2738-
import urllib.request
2739-
from specsmith.ollama_cmds import OLLAMA_API
2740-
2741-
payload = json.dumps({"name": model}).encode()
2742-
req = urllib.request.Request( # noqa: S310
2743-
f"{OLLAMA_API}/api/pull",
2744-
data=payload,
2745-
headers={"Content-Type": "application/json"},
2746-
)
2747-
2748-
last_status = ""
2749-
try:
2750-
with urllib.request.urlopen(req, timeout=600) as resp: # noqa: S310
2751-
for line in resp:
2752-
line = line.strip()
2753-
if not line:
2754-
continue
2755-
try:
2756-
chunk = json.loads(line)
2757-
status = chunk.get("status", "")
2758-
completed = chunk.get("completed", 0)
2759-
total = chunk.get("total", 0)
2760-
if status == "success":
2761-
console.print(f"\n[bold green]✓ {model} downloaded successfully.[/bold green]")
2762-
return
2763-
if total and status == "pulling " + (chunk.get("digest", "")[:12] if chunk.get("digest") else ""):
2764-
pct = int(completed / total * 100)
2765-
bar = "█" * (pct // 5) + "░" * (20 - pct // 5)
2766-
sys.stdout.write(f"\r [{bar}] {pct:3d}% {completed/(1024**3):.2f}/{total/(1024**3):.2f} GB")
2767-
sys.stdout.flush()
2768-
elif status != last_status:
2769-
console.print(f" {status}")
2770-
last_status = status
2771-
except json.JSONDecodeError:
2772-
pass
2773-
except KeyboardInterrupt:
2774-
console.print("\n[yellow]Download cancelled.[/yellow]")
2775-
raise SystemExit(0)
2776-
except urllib.error.URLError as e:
2777-
console.print(f"\n[red]Error: {e}[/red]")
2778-
raise SystemExit(1)
2779-
2780-
2781-
@ollama_group.command(name="suggest")
2782-
@click.argument("task", required=False)
2783-
def ollama_suggest(task: str | None) -> None:
2784-
"""Suggest models for a given task type.
2785-
2786-
TASK: coding | requirements | architecture | chat | analysis | reasoning
2787-
2788-
Example: specsmith ollama suggest coding
2789-
"""
2790-
from specsmith.ollama_cmds import (
2791-
TASK_TAGS,
2792-
detect_gpu,
2793-
get_installed_ids,
2794-
is_running,
2795-
suggest_for_task,
2796-
)
2797-
2798-
if not task:
2799-
console.print("[bold]Available task types:[/bold]")
2800-
for key in TASK_TAGS:
2801-
console.print(f" {key}")
2802-
console.print("\nUsage: [bold]specsmith ollama suggest <task>[/bold]")
2803-
return
2804-
2805-
gpus = detect_gpu()
2806-
vram = max((g["vram_gb"] for g in gpus), default=999)
2807-
suggestions = suggest_for_task(task, vram)
2808-
2809-
if not suggestions:
2810-
console.print(f"[yellow]No models found for task '{task}'.[/yellow]")
2811-
return
2812-
2813-
installed_ids: list[str] = []
2814-
if is_running():
2815-
installed_ids = get_installed_ids()
2816-
2817-
console.print(f"[bold]Model suggestions for task: {task}[/bold]\n")
2818-
for m in suggestions:
2819-
is_installed = any(m["id"] in iid or iid in m["id"] for iid in installed_ids)
2820-
status = "[green]✓ installed[/green]" if is_installed else f"[dim]⬇ {m['size_gb']:.1f}GB[/dim]"
2821-
console.print(
2822-
f" {status} [bold]{m['name']:<35s}[/bold] {m['notes']}"
2823-
)
2824-
console.print(f" [dim]ID: {m['id']} VRAM: {m['vram_gb']:.1f}GB ctx: {m['ctx_k']}K[/dim]")
2825-
console.print("\n Download: [bold]specsmith ollama pull <id>[/bold]")
2826-
2827-
2828-
main.add_command(ollama_group)
2829-
2830-
28312595
# ---------------------------------------------------------------------------
28322596
# Workspace — multi-project management (#17)
28332597
# ---------------------------------------------------------------------------
@@ -3121,12 +2885,18 @@ def ollama_list_cmd() -> None:
31212885
from specsmith.ollama_cmds import get_installed_models, is_running
31222886

31232887
if not is_running():
3124-
console.print("[red]\u2717[/red] Ollama is not running. Start it with: [bold]ollama serve[/bold]")
2888+
console.print(
2889+
"[red]\u2717[/red] Ollama is not running. "
2890+
"Start it with: [bold]ollama serve[/bold]"
2891+
)
31252892
raise SystemExit(1)
31262893

31272894
models = get_installed_models()
31282895
if not models:
3129-
console.print("[yellow]No models installed.[/yellow] Pull one with: specsmith ollama pull <model>")
2896+
console.print(
2897+
"[yellow]No models installed.[/yellow] "
2898+
"Pull one with: specsmith ollama pull <model>"
2899+
)
31302900
return
31312901

31322902
console.print(f"[bold]Installed Ollama Models[/bold] ({len(models)})\n")
@@ -3135,10 +2905,14 @@ def ollama_list_cmd() -> None:
31352905

31362906

31372907
@ollama_group.command(name="available")
3138-
@click.option("--task", default="", help="Filter by task type (code, requirements, architecture, chat, analysis, reasoning).")
2908+
@click.option(
2909+
"--task",
2910+
default="",
2911+
help="Filter by task type: code, requirements, architecture, chat, analysis, reasoning.",
2912+
)
31392913
def ollama_available_cmd(task: str) -> None:
31402914
"""Show models available to download from the curated catalog."""
3141-
from specsmith.ollama_cmds import CATALOG, get_installed_models, get_vram_gb, recommend_models
2915+
from specsmith.ollama_cmds import get_installed_models, get_vram_gb, recommend_models
31422916

31432917
vram = get_vram_gb()
31442918
installed = set(get_installed_models())
@@ -3156,18 +2930,23 @@ def ollama_available_cmd(task: str) -> None:
31562930
# Show catalog entries that fit VRAM budget
31572931
for e in recs:
31582932
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]"
2933+
if is_inst:
2934+
status = "[green]installed[/green]"
2935+
else:
2936+
status = f"[dim]{e.size_gb}GB \u2014 pull to install[/dim]"
31602937
console.print(
31612938
f" {('[bold]' + e.tier + '[/bold]'):30s} {e.name:28s} {status}"
31622939
)
3163-
console.print(f" [dim]{'':<30s} {', '.join(e.best_for[:2]):<28s} {e.notes}[/dim]")
2940+
console.print(
2941+
f" [dim]{'':<30s} {', '.join(e.best_for[:2]):<28s} {e.notes}[/dim]"
2942+
)
31642943
console.print()
31652944

31662945
if not recs:
31672946
console.print("[yellow]No models fit within the detected VRAM budget.[/yellow]")
31682947
console.print("Use a smaller model or run on CPU (all models listed without GPU).")
31692948

3170-
console.print(f"[dim]Pull a model: specsmith ollama pull <model-id>[/dim]")
2949+
console.print("[dim]Pull a model: specsmith ollama pull <model-id>[/dim]")
31712950

31722951

31732952
@ollama_group.command(name="gpu")
@@ -3177,7 +2956,10 @@ def ollama_gpu_cmd() -> None:
31772956

31782957
vram = get_vram_gb()
31792958
if vram > 0:
3180-
console.print(f"[green]\u2713[/green] GPU detected — [bold]{vram:.1f} GB[/bold] VRAM available")
2959+
console.print(
2960+
f"[green]\u2713[/green] GPU detected \u2014 "
2961+
f"[bold]{vram:.1f} GB[/bold] VRAM available"
2962+
)
31812963
# Tier suggestions
31822964
if vram >= 20:
31832965
console.print(" Tier: [bold]Powerful[/bold] — all models supported (Qwen 2.5 32B+)")
@@ -3238,7 +3020,12 @@ def ollama_pull_cmd(model_id: str) -> None:
32383020

32393021

32403022
@ollama_group.command(name="suggest")
3241-
@click.argument("task", type=click.Choice(["code", "requirements", "architecture", "chat", "analysis", "reasoning"]))
3023+
@click.argument(
3024+
"task",
3025+
type=click.Choice(
3026+
["code", "requirements", "architecture", "chat", "analysis", "reasoning"]
3027+
),
3028+
)
32423029
def ollama_suggest_cmd(task: str) -> None:
32433030
"""Suggest the best installed Ollama models for a task.
32443031
@@ -3250,8 +3037,11 @@ def ollama_suggest_cmd(task: str) -> None:
32503037
installed = set(get_installed_models())
32513038
recs = recommend_models(vram_gb=vram, task=task)
32523039

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]
3040+
inst_recs = [
3041+
e for e in recs
3042+
if any(m.startswith(e.id.split(":")[0]) or m == e.id for m in installed)
3043+
]
3044+
not_inst = [e for e in recs if e not in inst_recs]
32553045

32563046
console.print(f"[bold]Model Suggestions[/bold] for task: [bold]{task}[/bold]\n")
32573047

@@ -3264,11 +3054,17 @@ def ollama_suggest_cmd(task: str) -> None:
32643054
if not_inst:
32653055
console.print("[dim]Available to download:[/dim]")
32663056
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}")
3057+
console.print(
3058+
f" [dim]\u21d3[/dim] {e.name:28s} "
3059+
f"{e.size_gb}GB \u2014 specsmith ollama pull {e.id}"
3060+
)
32683061

32693062
if not inst_recs and not not_inst:
32703063
console.print("[yellow]No matching models found.[/yellow]")
3271-
console.print(f" Run [bold]specsmith ollama available --task {task}[/bold] to see options.")
3064+
console.print(
3065+
f" Run [bold]specsmith ollama available --task {task}[/bold] "
3066+
"to see options."
3067+
)
32723068

32733069

32743070
main.add_command(ollama_group)

src/specsmith/ollama_cmds.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
import sys
1616
import urllib.error
1717
import urllib.request
18+
from collections.abc import Iterator
1819
from dataclasses import dataclass, field
19-
from typing import Iterator
2020

2121
OLLAMA_API = "http://localhost:11434"
2222

0 commit comments

Comments
 (0)