@@ -3105,6 +3105,175 @@ def patent_prior_art_cmd(claim: str, max_results: int, project_dir: str, save_re
31053105main .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