diff --git a/cortex/routers/local_llm.py b/cortex/routers/local_llm.py index be0e62a..8b9e1af 100644 --- a/cortex/routers/local_llm.py +++ b/cortex/routers/local_llm.py @@ -1,13 +1,14 @@ """ -Local LLM settings — per-user host and model configuration. +Model Registry settings — hosts, models, and role assignments. Routes: - GET /settings/local → settings page - POST /settings/local/host → save/create host - POST /settings/local/models/add → add model entry - POST /settings/local/models/{id}/activate → set active model - POST /settings/local/models/{id}/remove → remove model entry - GET /api/local-llm/fetch-models → proxy to host /api/models (JSON) + GET /settings/local → settings page + POST /settings/local/host → save/create a host + POST /settings/local/host/{id}/remove → remove a host (and its models) + POST /settings/local/models/add → add a model entry + POST /settings/local/models/{id}/remove → remove a model + POST /api/models/role → AJAX: set a role assignment + GET /api/local-llm/fetch-models → proxy to host /api/models (JSON) """ import logging from pathlib import Path @@ -19,7 +20,7 @@ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from auth_utils import COOKIE_NAME, decode_token from config import settings as app_settings -import user_settings as us +import model_registry as reg logger = logging.getLogger(__name__) router = APIRouter() @@ -42,81 +43,141 @@ def _get_user(request: Request) -> str | None: # ── Page renderer ───────────────────────────────────────────────────────────── def _render(username: str, success: str = "", error: str = "") -> str: - cfg = us.get_config(username) - hosts = cfg["hosts"] - models = cfg["models"] - active = cfg.get("active_model_id") + registry = reg.get_registry(username) + hosts = registry.get("hosts", []) + models = registry.get("models", []) + roles = registry.get("roles", {}) + builtins = reg._builtins() - # Build a host lookup for model rows host_by_id = {h["id"]: h for h in hosts} - # ── Host section ────────────────────────────────────────────────────────── - if hosts: - h = hosts[0] # one host for now - host_id_val = h["id"] - host_label = h.get("label", "") - host_url = h.get("api_url", "") - host_key_hint = f"…{h['api_key'][-4:]}" if h.get("api_key") else "not set" - else: - host_id_val = "" - host_label = "" - host_url = app_settings.local_api_url - host_key_hint = f"server default (…{app_settings.local_api_key[-4:]})" \ - if app_settings.local_api_key else "not set" + # ── Host rows ───────────────────────────────────────────────────────────── + host_rows = "" + for h in hosts: + key_hint = f"…{h['api_key'][-4:]}" if h.get("api_key") else "not set" + host_rows += f''' +
No hosts configured yet. Add one below.
' + + # ── Host options for add-model form ─────────────────────────────────────── + host_options = "".join( + f'' + for h in hosts + ) + add_model_hidden = "" if hosts else ' style="display:none"' # ── Model rows ──────────────────────────────────────────────────────────── model_rows = "" for m in models: - is_active = m["id"] == active - host = host_by_id.get(m["host_id"], {}) - host_name = host.get("label") or host.get("api_url") or "unknown host" - badge = 'active' if is_active else "" - activate_btn = ( - '✓ Active' - if is_active else - f'''''' + resolved = reg._resolve_model(registry, m["id"]) + if not resolved: + continue + host_name = "" + if m.get("type") == "local_openai" and m.get("host_id"): + h = host_by_id.get(m["host_id"], {}) + host_name = h.get("label") or h.get("api_url", "") + + ctx_badge = f'{m.get("context_k",0)}k ctx' if m.get("context_k") else "" + tags_html = " ".join( + f'{t}' for t in (m.get("tags") or []) ) + host_html = f'{host_name}' if host_name else "" + model_rows += f''' -No models added yet. Use "Add Model" below.
' + model_rows = 'No models added yet.
' - # ── Host select for Add Model ───────────────────────────────────────────── - host_options = "".join( - f'' - for h in hosts - ) - add_section_hidden = "" if hosts else ' style="display:none"' + # ── Role assignment rows ────────────────────────────────────────────────── + # Build option list: (none) + built-ins + user models + model_opts = '\n' + model_opts += '\n' + if models: + model_opts += '\n' + + role_rows = "" + for role in app_settings.get_defined_roles(): + role_cfg = roles.get(role, {}) + role_rows += f'{success}
') if error: @@ -127,7 +188,7 @@ def _render(username: str, success: str = "", error: str = "") -> str: # ── Routes ──────────────────────────────────────────────────────────────────── @router.get("/settings/local", include_in_schema=False) -async def local_llm_page(request: Request): +async def models_page(request: Request): username = _get_user(request) if not username: return RedirectResponse("/login", status_code=302) @@ -145,45 +206,40 @@ async def save_host( username = _get_user(request) if not username: return RedirectResponse("/login", status_code=302) - if not api_url.strip(): return HTMLResponse(_render(username, error="API URL is required.")) - - us.save_host(username, host_id or None, label, api_url, api_key) - logger.info("local LLM host saved: %s", username) + reg.save_host(username, host_id or None, label, api_url, api_key) + logger.info("model registry host saved: %s", username) return HTMLResponse(_render(username, success="Host saved.")) +@router.post("/settings/local/host/{host_id}/remove", include_in_schema=False) +async def remove_host(request: Request, host_id: str): + username = _get_user(request) + if not username: + return RedirectResponse("/login", status_code=302) + reg.remove_host(username, host_id) + return HTMLResponse(_render(username, success="Host removed.")) + + @router.post("/settings/local/models/add", include_in_schema=False) async def add_model( request: Request, host_id: str = Form(...), label: str = Form(""), model_name: str = Form(...), + context_k: int = Form(0), + tags: str = Form(""), ): username = _get_user(request) if not username: return RedirectResponse("/login", status_code=302) - if not model_name.strip(): return HTMLResponse(_render(username, error="Model name is required.")) - - us.add_model(username, host_id, label, model_name) - logger.info("local model added: %s / %s", username, model_name) - return HTMLResponse(_render(username, success=f"Model \"{label or model_name}\" added.")) - - -@router.post("/settings/local/models/{model_id}/activate", include_in_schema=False) -async def activate_model(request: Request, model_id: str): - username = _get_user(request) - if not username: - return RedirectResponse("/login", status_code=302) - - if not us.set_active_model(username, model_id): - return HTMLResponse(_render(username, error="Model not found.")) - - logger.info("active local model set: %s / %s", username, model_id) - return HTMLResponse(_render(username, success="Active model updated.")) + tag_list = [t.strip() for t in tags.split(",") if t.strip()] + reg.save_model(username, None, host_id, label, model_name, context_k, tag_list) + logger.info("model added to registry: %s / %s", username, model_name) + return HTMLResponse(_render(username, success=f'Model "{label or model_name}" added.')) @router.post("/settings/local/models/{model_id}/remove", include_in_schema=False) @@ -191,30 +247,58 @@ async def remove_model(request: Request, model_id: str): username = _get_user(request) if not username: return RedirectResponse("/login", status_code=302) - - us.remove_model(username, model_id) - logger.info("local model removed: %s / %s", username, model_id) + reg.remove_model(username, model_id) return HTMLResponse(_render(username, success="Model removed.")) -@router.get("/api/local-llm/fetch-models") -async def fetch_models(request: Request) -> JSONResponse: - """Proxy to the configured host's /api/models endpoint. +@router.post("/api/models/role") +async def set_role(request: Request) -> JSONResponse: + """AJAX: assign a model to a role priority slot. - Returns [{id, name}] sorted by name, or an error dict. + Body: {"role": "chat", "slot": "primary", "model_id": "abc123" | ""} """ username = _get_user(request) if not username: return JSONResponse({"error": "Not authenticated"}, status_code=401) + try: + body = await request.json() + except Exception: + return JSONResponse({"error": "Invalid JSON"}, status_code=400) - cfg = us.get_config(username) - hosts = cfg.get("hosts", []) + role = body.get("role", "").strip() + slot = body.get("slot", "").strip() + model_id = body.get("model_id", "").strip() or None - # Fall back to .env if no host configured yet - if hosts: - h = hosts[0] - api_url = h.get("api_url", "") - api_key = h.get("api_key", "") + if not role or not slot: + return JSONResponse({"error": "role and slot are required"}, status_code=400) + + ok = reg.set_role(username, role, slot, model_id) + if not ok: + return JSONResponse({"error": f"Invalid slot or model_id not found"}, status_code=400) + + logger.info("role set: %s %s.%s = %s", username, role, slot, model_id) + return JSONResponse({"ok": True}) + + +@router.get("/api/local-llm/fetch-models") +async def fetch_models(request: Request, host_id: str = "") -> JSONResponse: + """Proxy to the host's /api/models endpoint. host_id selects which host.""" + username = _get_user(request) + if not username: + return JSONResponse({"error": "Not authenticated"}, status_code=401) + + registry = reg.get_registry(username) + hosts = registry.get("hosts", []) + + if host_id: + host = next((h for h in hosts if h["id"] == host_id), None) + else: + host = hosts[0] if hosts else None + + # Fall back to .env + if host: + api_url = host.get("api_url", "") + api_key = host.get("api_key", "") else: api_url = app_settings.local_api_url api_key = app_settings.local_api_key diff --git a/cortex/static/local_llm.html b/cortex/static/local_llm.html index b2b4f86..6a0ccab 100644 --- a/cortex/static/local_llm.html +++ b/cortex/static/local_llm.html @@ -3,7 +3,7 @@ -Configure your OpenAI-compatible host and models (Open WebUI, Ollama, LM Studio, etc.)
+Configure hosts, models, and which model handles each task type.
- The API server that hosts your local models. Leave the key blank to keep the existing one. -
-