diff --git a/src/server/desktop_serve.py b/src/server/desktop_serve.py index 3aed5198..c556e842 100644 --- a/src/server/desktop_serve.py +++ b/src/server/desktop_serve.py @@ -166,6 +166,60 @@ async def model_info(request: Request) -> Response: except Exception: # noqa: BLE001 — inspection endpoint, degrade soft return JSONResponse({"provider": None, "model": None}) + async def model_options_rest(request: Request) -> Response: + if not _token_ok(state, _rest_token(request)): + return JSONResponse({"error": "unauthorized"}, status_code=401) + from src.server.desktop_gateway_methods import _catalog_from_config + + import asyncio as _asyncio + return JSONResponse(await _asyncio.to_thread(_catalog_from_config)) + + async def model_auxiliary(request: Request) -> Response: + if not _token_ok(state, _rest_token(request)): + return JSONResponse({"error": "unauthorized"}, status_code=401) + # Auxiliary task-model assignments aren't wired to a control yet; the + # panel reads `main` and an (empty) task list and renders "using the + # main model for everything", which is the true state. + from src.config import get_default_provider, get_provider_config + + try: + provider = get_default_provider() + model = (get_provider_config(provider) or {}).get("default_model") + except Exception: # noqa: BLE001 + provider, model = None, None + return JSONResponse({"main": {"model": model, "provider": provider}, "tasks": []}) + + async def config_schema(request: Request) -> Response: + if not _token_ok(state, _rest_token(request)): + return JSONResponse({"error": "unauthorized"}, status_code=401) + # No dynamic config schema in this backend — the structured settings + # panels render from their own known fields; the schema only drives the + # Advanced tab's generated rows, which degrade to none. + return JSONResponse({"fields": {}, "category_order": []}) + + async def cron_jobs(request: Request) -> Response: + if not _token_ok(state, _rest_token(request)): + return JSONResponse({"error": "unauthorized"}, status_code=401) + return JSONResponse({"jobs": []}) + + async def elevenlabs_voices(request: Request) -> Response: + if not _token_ok(state, _rest_token(request)): + return JSONResponse({"error": "unauthorized"}, status_code=401) + return JSONResponse({"voices": []}) + + async def session_detail(request: Request) -> Response: + if not _token_ok(state, _rest_token(request)): + return JSONResponse({"error": "unauthorized"}, status_code=401) + from src.server.desktop_sessions import load_session_messages + + sid = request.path_params["session_id"] + found = load_session_messages(state.saved_sessions_dir(), sid) + if found is None: + return JSONResponse({"error": "session not found"}, status_code=404) + return JSONResponse({"session_id": found["session_id"], + "messages": found["messages"], + "message_count": found["message_count"]}) + def _sessions_slice(request: Request, *, profile_tag: str = "default") -> dict[str, Any]: """One filtered slice of the saved-session list (shared by the profile-scoped route and the batched sidebar).""" @@ -334,6 +388,12 @@ async def not_found(request: Request) -> Response: Route("/api/profiles/sessions", profile_sessions), Route("/api/profiles/sessions/sidebar", sidebar_sessions), Route("/api/model/info", model_info), + Route("/api/model/options", model_options_rest), + Route("/api/model/auxiliary", model_auxiliary), + Route("/api/config/schema", config_schema), + Route("/api/cron/jobs", cron_jobs), + Route("/api/audio/elevenlabs/voices", elevenlabs_voices), + Route("/api/sessions/{session_id}", session_detail), Route("/api/audio/transcribe", audio_transcribe, methods=["POST"]), Route("/", index), WebSocketRoute("/api/ws", gateway_ws), diff --git a/tests/server/test_desktop_sessions.py b/tests/server/test_desktop_sessions.py index 19c226f6..7e82d8b1 100644 --- a/tests/server/test_desktop_sessions.py +++ b/tests/server/test_desktop_sessions.py @@ -141,6 +141,45 @@ def test_model_info_reports_defaults( assert body == {"provider": "anthropic", "model": "claude-fable-5"} +def test_settings_panel_rest_routes( + rest: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """The Model/Voice/Advanced settings panels each await a REST route; a 404 + on any leaves the panel stuck on skeletons. Assert the shapes they read.""" + monkeypatch.setattr("src.config.get_default_provider", lambda: "anthropic") + monkeypatch.setattr("src.config.get_provider_config", + lambda n: {"default_model": "claude-sonnet-4-6"}) + + options = rest.get("/api/model/options", headers=AUTH).json() + assert "providers" in options and len(options["providers"]) > 5 + + aux = rest.get("/api/model/auxiliary", headers=AUTH).json() + assert aux["main"] == {"model": "claude-sonnet-4-6", "provider": "anthropic"} + assert aux["tasks"] == [] + + schema = rest.get("/api/config/schema", headers=AUTH).json() + assert schema == {"fields": {}, "category_order": []} + + assert rest.get("/api/cron/jobs", headers=AUTH).json() == {"jobs": []} + assert rest.get("/api/audio/elevenlabs/voices", headers=AUTH).json() == {"voices": []} + + # All are token-gated. + for path in ("/api/model/options", "/api/model/auxiliary", "/api/config/schema", + "/api/cron/jobs", "/api/audio/elevenlabs/voices"): + assert rest.get(path).status_code == 401 + + +def test_session_detail_route(rest: TestClient, tmp_path: Path) -> None: + _write_session( + tmp_path / "sessions", "chat-x", preview="hi", count=1, + messages=[{"role": "user", "content": "hi"}], + ) + body = rest.get("/api/sessions/chat-x", headers=AUTH).json() + assert body["session_id"] == "chat-x" + assert body["message_count"] == 1 + assert rest.get("/api/sessions/missing", headers=AUTH).status_code == 404 + + def test_profile_sessions_slice_filters_sources( rest: TestClient, tmp_path: Path ) -> None: