diff --git a/cortex/llm_client.py b/cortex/llm_client.py index b810912..6b140e6 100644 --- a/cortex/llm_client.py +++ b/cortex/llm_client.py @@ -177,9 +177,9 @@ async def _local(system_prompt: str, messages: list[dict], model_cfg: dict | Non model = cfg["model_name"] if not api_url: - raise RuntimeError("local_api_url not configured — set LOCAL_API_URL in .env or add a host at /settings/local") + raise RuntimeError("local_api_url not configured — set LOCAL_API_URL in .env or add a host at /settings/models") if not model: - raise RuntimeError("local_model not configured — add a model at /settings/local") + raise RuntimeError("local_model not configured — add a model at /settings/models") host_type = cfg.get("host_type", "openwebui") # "openwebui" uses Open WebUI/Ollama path layout; "openai" uses standard OpenAI layout diff --git a/cortex/model_registry.py b/cortex/model_registry.py index 5d38267..2620949 100644 --- a/cortex/model_registry.py +++ b/cortex/model_registry.py @@ -296,6 +296,8 @@ def _migrate_from_local_llm(username: str, path: Path) -> dict: active_id = old.get("active_model_id") if active_id and any(m["id"] == active_id for m in data["models"]): data["roles"]["chat"] = {"primary": active_id} + if settings.distill_backend_mid == "local": + data["roles"]["distill"] = {"primary": active_id} # Migrate Gemini key from auth.json data = _migrate_v1_to_v2(username, {"version": 1, **data}) @@ -613,6 +615,52 @@ def save_model(username: str, model_id: str | None, host_id: str, return model_id +def save_cloud_model(username: str, model_id: str | None, + provider: str, model_name: str, label: str, + account_id: str | None = None, + credential_id: str | None = None, + context_k: int = 0, + tags: list[str] | None = None) -> str: + """ + Create or update an Anthropic or Google model entry. Returns the model ID. + + provider: "anthropic" | "google" + account_id: Google only — references providers.google.accounts[].id + credential_id: Anthropic only — e.g. "cli" + """ + _TYPE = {"google": "gemini_api", "anthropic": "claude_cli"} + entry_type = _TYPE.get(provider, "gemini_api") + data = _load(username) + tags = tags or [] + + entry: dict = { + "type": entry_type, + "label": label.strip() or model_name.strip(), + "model_name": model_name.strip(), + "provider": provider, + "context_k": context_k, + "tags": tags, + } + if account_id: + entry["account_id"] = account_id + if credential_id: + entry["credential_id"] = credential_id + + if model_id: + for m in data["models"]: + if m["id"] == model_id: + m.update(entry) + _save(username, data) + return model_id + model_id = None + + model_id = secrets.token_hex(4) + entry["id"] = model_id + data["models"].append(entry) + _save(username, data) + return model_id + + def remove_model(username: str, model_id: str) -> bool: """Remove a model and clear any role assignments pointing to it.""" data = _load(username) diff --git a/cortex/routers/local_llm.py b/cortex/routers/local_llm.py index 8d04aab..acb19d1 100644 --- a/cortex/routers/local_llm.py +++ b/cortex/routers/local_llm.py @@ -1,15 +1,19 @@ """ -Model Registry settings — hosts, models, and role assignments. +Model Registry settings — providers, hosts, models, and role assignments. Routes: - 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) + GET /settings/models → settings page (canonical) + GET /settings/local → redirect to /settings/models + POST /settings/local/host → save/create a local host + POST /settings/local/host/{id}/remove → remove a host (and its models) + POST /settings/local/google-account → save/create a Google account + POST /settings/local/google-account/{id}/remove → remove a Google account + POST /settings/local/models/add → add a model (any provider) + 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 json as _json import logging from pathlib import Path @@ -43,21 +47,39 @@ def _get_user(request: Request) -> str | None: # ── Page renderer ───────────────────────────────────────────────────────────── def _render(username: str, success: str = "", error: str = "") -> str: - registry = reg.get_registry(username) - hosts = registry.get("hosts", []) - models = registry.get("models", []) - roles = registry.get("roles", {}) - builtins = reg._builtins() + registry = reg.get_registry(username) + hosts = registry.get("hosts", []) + models = registry.get("models", []) + roles = registry.get("roles", {}) + builtins = reg._builtins() + host_by_id = {h["id"]: h for h in hosts} + goog_accts = registry.get("providers", {}).get("google", {}).get("accounts", []) - host_by_id = {h["id"]: h for h in hosts} + # ── Google account rows ─────────────────────────────────────────────────── + google_account_rows = "" + for a in goog_accts: + hint = (a.get("api_key") or "")[:10] + "…" if a.get("api_key") else "no key" + google_account_rows += f''' +
No accounts configured yet.
' - # ── Host rows ───────────────────────────────────────────────────────────── + # ── Local host rows ─────────────────────────────────────────────────────── host_rows = "" for h in hosts: - key_hint = f"…{h['api_key'][-4:]}" if h.get("api_key") else "not set" - ht = h.get("host_type", "openwebui") - ow_sel = ' selected' if ht == "openwebui" else '' - ai_sel = ' selected' if ht == "openai" else '' + key_hint = f"…{h['api_key'][-4:]}" if h.get("api_key") else "not set" + ht = h.get("host_type", "openwebui") + ow = ' selected' if ht == "openwebui" else '' + ai = ' selected' if ht == "openai" else '' 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 (all providers) ──────────────────────────────────────────── + _PROVIDER_BADGE = { + "claude_cli": ('Anthropic', "Claude CLI"), + "gemini_api": ('Google', ""), + "local_openai": ('Local', ""), + } model_rows = "" for m in models: 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", "") + mtype = m.get("type", "local_openai") + badge, default_secondary = _PROVIDER_BADGE.get(mtype, ("", "")) - 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 "" + if mtype == "local_openai": + h = host_by_id.get(m.get("host_id", ""), {}) + secondary = h.get("label") or h.get("api_url", "") + elif mtype == "gemini_api": + acct = next((a for a in goog_accts if a["id"] == m.get("account_id")), None) + secondary = acct["label"] if acct else "" + else: + secondary = default_secondary + + ctx = f'{m.get("context_k",0)}k' if m.get("context_k") else "" + tags = " ".join(f'{t}' for t in (m.get("tags") or [])) + sec = f'{secondary}' if secondary else "" model_rows += f''' -No models added yet.
' # ── Role assignment rows ────────────────────────────────────────────────── - # Build option list: (none) + built-ins + user models model_opts = '\n' model_opts += '\n' if models: - model_opts += '