Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions src/server/desktop_gateway_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,13 +326,34 @@ async def respond_approval(self, choice: str) -> dict[str, Any]:
return {"resolved": True}


def configured_providers_only(providers: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Keep only providers the user has actually configured.

The registry knows ~30 providers, but the desktop picker should show just
the ones the user set up: a key-taking provider with credentials present
(an ``api_key`` in config, or a subscription/OAuth — the catalog folds both
into ``authenticated``), plus whichever the live session is running on.
Everything else is disabled by not appearing.

Local, no-key providers (Ollama / vLLM / SGLang, ``auth_type == "none"``)
report as ``authenticated`` because they need no key — but the user didn't
*configure* them, so they're excluded unless they're the active provider.
"""
return [
p for p in providers
if p.get("is_current")
or (p.get("authenticated") and p.get("auth_type") == "api_key")
]


def _catalog_from_config() -> dict[str, Any]:
"""Full model catalog from config alone — no live session required.
"""Configured model catalog from config alone — no live session required.

Mirrors the agent-server's ``list_model_providers`` control, but reads the
default provider + its configured model list straight from config so the
desktop model picker populates on the welcome screen (before any session
exists). Sync (config + registry access); call via ``to_thread``.
exists), and filters to only configured providers. Sync (config + registry
access); call via ``to_thread``.
"""
from src.providers.catalog import provider_catalog

Expand All @@ -350,9 +371,12 @@ def _catalog_from_config() -> dict[str, Any]:
provider = None

try:
# current_models is left unset so the active provider shows its FULL
# registry model list (e.g. every deepseek model), not just the one
# `default_model` from config — the picker is where you switch models.
providers = provider_catalog(
current=provider,
current_models=models or None,
current_models=None,
current_ready=bool(provider),
)
except Exception: # noqa: BLE001
Expand All @@ -361,7 +385,7 @@ def _catalog_from_config() -> dict[str, Any]:
return {
"model": models[0] if models else None,
"provider": provider,
"providers": providers,
"providers": configured_providers_only(providers),
}


Expand Down Expand Up @@ -578,10 +602,13 @@ async def model_options(self, params: dict[str, Any]) -> dict[str, Any]:
if session is not None:
result = await session.control_query("list_model_providers", {})
if isinstance(result, dict) and result.get("providers"):
# Only configured providers (+ the running one) — same rule as
# the config-only path, so the picker is consistent whether or
# not a session is live.
return {
"model": result.get("fusion") or result.get("model"),
"provider": result.get("provider"),
"providers": result.get("providers"),
"providers": configured_providers_only(result["providers"]),
}
# No live session yet (the welcome screen opens the picker before the
# first prompt), or the session couldn't answer — enumerate the whole
Expand Down
9 changes: 8 additions & 1 deletion tests/server/test_desktop_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,15 @@ def test_settings_panel_rest_routes(
monkeypatch.setattr("src.config.get_provider_config",
lambda n: {"default_model": "claude-sonnet-4-6"})

# The catalog is filtered to configured providers only; the panel needs a
# valid, non-empty list (the current provider is always present), not a
# specific count.
options = rest.get("/api/model/options", headers=AUTH).json()
assert "providers" in options and len(options["providers"]) > 5
assert "providers" in options and isinstance(options["providers"], list)
assert all(
p.get("is_current") or (p.get("authenticated") and p.get("auth_type") == "api_key")
for p in options["providers"]
)

aux = rest.get("/api/model/auxiliary", headers=AUTH).json()
assert aux["main"] == {"model": "claude-sonnet-4-6", "provider": "anthropic"}
Expand Down
36 changes: 29 additions & 7 deletions tests/server/test_desktop_slash.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,38 @@ async def boom(subtype: str, params: dict) -> object:
assert "backend exploded" in res["output"]


# ─── model catalog from config (no live session) ─────────────────────────────
# ─── model catalog: configured providers only ────────────────────────────────


def test_catalog_from_config_populates_without_session() -> None:
def test_configured_providers_only_filters_the_registry() -> None:
from src.server.desktop_gateway_methods import configured_providers_only

catalog = [
{"slug": "deepseek", "authenticated": True, "auth_type": "api_key"},
{"slug": "openai", "authenticated": True, "auth_type": "api_key"},
# Configured key-provider that the user hasn't set up → dropped.
{"slug": "groq", "authenticated": False, "auth_type": "api_key"},
# Local no-key server → dropped (not user-configured) unless active.
{"slug": "ollama", "authenticated": True, "auth_type": "none"},
# The running provider is always kept, even if the probe missed its key.
{"slug": "vllm", "authenticated": False, "auth_type": "none", "is_current": True},
]
kept = {p["slug"] for p in configured_providers_only(catalog)}
assert kept == {"deepseek", "openai", "vllm"}


def test_catalog_from_config_shows_only_configured() -> None:
from src.server.desktop_gateway_methods import _catalog_from_config

result = _catalog_from_config()
# The registry always knows anthropic + deepseek + more, regardless of
# which providers are configured — the picker needs the full list.
slugs = {p["slug"] for p in result["providers"]}
assert "anthropic" in slugs
assert len(result["providers"]) > 5
# Every returned provider is either configured (key + authenticated) or the
# current one — never the unconfigured long tail.
for p in result["providers"]:
assert p.get("is_current") or (
p.get("authenticated") and p.get("auth_type") == "api_key"
), p["slug"]
# The active provider shows its FULL model list, not just default_model.
current = next((p for p in result["providers"] if p.get("is_current")), None)
if current is not None:
assert len(current["models"]) >= 1

Loading