@@ -2592,242 +2592,6 @@ def auth_check(project_dir: str) -> None:
25922592main .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 ("\n Usage: [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+ )
31392913def 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+ )
32423029def 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
32743070main .add_command (ollama_group )
0 commit comments