feat(setup): dynamic model discovery for local providers in wizard

Replace the static model text prompt with live API queries:
- _fetch_local_models(): queries /v1/models (LM Studio, llama.cpp) or
  /api/tags (Ollama) and returns a questionary.select list
- _fetch_lmstudio_available_models(): queries LM Studio's beta
  /api/v0/models to list downloaded-but-not-loaded models
- _load_lmstudio_model(): tries /api/v0/models/load to load a model
  in-place; falls back to telling the user to load manually
- Cloud providers keep the existing text-input behaviour

Also replace hardcoded LMSTUDIO_MODEL in integration tests with a
lmstudio_model fixture that queries the API at runtime and uses
whichever model is currently loaded (skips if none).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-19 10:53:15 +02:00
parent 9735a5559e
commit 5eb81404c2
2 changed files with 123 additions and 30 deletions
+88 -4
View File
@@ -153,11 +153,95 @@ def _check_local_server(provider: Provider) -> None:
)
def _fetch_local_models(provider: Provider) -> list[str]:
"""Return currently loaded/available models from a local provider's API."""
if not provider.base_url:
return []
try:
if provider.id == "ollama":
resp = httpx.get(f"{provider.base_url}/api/tags", timeout=3.0)
resp.raise_for_status()
return [m["name"] for m in resp.json().get("models", [])]
else:
resp = httpx.get(f"{provider.base_url}/models", timeout=3.0)
resp.raise_for_status()
return [m["id"] for m in resp.json().get("data", [])]
except Exception:
return []
def _fetch_lmstudio_available_models() -> list[str]:
"""Return all downloaded (not necessarily loaded) models from LM Studio's beta API."""
try:
resp = httpx.get("http://localhost:1234/api/v0/models", timeout=3.0)
resp.raise_for_status()
return [m["id"] for m in resp.json().get("data", [])]
except Exception:
return []
def _load_lmstudio_model(model_id: str) -> bool:
"""Attempt to load a model via LM Studio's beta API. Returns True on success."""
try:
resp = httpx.post(
"http://localhost:1234/api/v0/models/load",
json={"identifier": model_id},
timeout=60.0,
)
return resp.is_success
except Exception:
return False
def _choose_model(provider: Provider) -> str:
model = questionary.text(
"Model name:",
default=provider.default_model,
).ask()
if provider.group != "Local":
model = questionary.text("Model name:", default=provider.default_model).ask()
if model is None:
raise SystemExit(0)
return model.strip()
_MANUAL = "__manual__"
loaded = _fetch_local_models(provider)
if loaded:
choices = loaded + [questionary.Choice("── Enter manually ──", value=_MANUAL)]
selected = questionary.select("Select model:", choices=choices).ask()
if selected is None:
raise SystemExit(0)
if selected != _MANUAL:
return selected
elif provider.id == "lmstudio":
console.print(" [yellow]No model currently loaded in LM Studio.[/yellow]")
available = _fetch_lmstudio_available_models()
if available:
choices = available + [questionary.Choice("── Enter manually ──", value=_MANUAL)]
selected = questionary.select(
"Select a downloaded model to load:", choices=choices
).ask()
if selected is None:
raise SystemExit(0)
if selected != _MANUAL:
console.print(f" Loading [bold]{selected}[/bold]...", end=" ")
if _load_lmstudio_model(selected):
console.print("[green]✓ Loaded[/green]")
else:
console.print(
"[yellow]Could not load via API — "
"please load the model manually in LM Studio.[/yellow]"
)
return selected
else:
console.print(Panel(
"No models are loaded or downloaded in LM Studio.\n"
"Open LM Studio → Local Server tab → load a model, then re-run setup.",
border_style="yellow",
))
else:
console.print(f" [yellow]No models found at {provider.base_url}.[/yellow]")
model = questionary.text("Model name:", default=provider.default_model).ask()
if model is None:
raise SystemExit(0)
return model.strip()