feat: model registry Phase 2 — cloud provider UI (Anthropic + Google)

Adds cloud provider management to /settings/models:
- Google Accounts section: add/remove Gemini API keys with labels
- Add Model form: provider tabs (Local / Google / Anthropic) with
  catalog dropdowns that auto-fill label and context_k
- Provider badges on model rows (Anthropic / Google / Local)
- /settings/local now redirects to /settings/models (canonical URL)
- save_cloud_model() in model_registry for Anthropic/Google entries
- Distill role migration restored in _migrate_from_local_llm
- Test fixes: version assertions updated to V2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-27 20:41:06 -04:00
parent 45c95d20ba
commit f08b033d6c
8 changed files with 585 additions and 297 deletions

View File

@@ -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)