"""Server-side model grant resolution and redacted model views. Grants carry LLM assignments only (grant v2): embedding and search always resolve from the deployment's active profiles, so per-user grants for them were never enforced and are not stored. """ from __future__ import annotations from typing import Any from deeptutor.services.config.model_catalog import ModelCatalogService from deeptutor.services.model_selection import list_llm_options from .context import get_current_user from .grants import load_grant from .paths import get_admin_path_service def admin_catalog_service() -> ModelCatalogService: return ModelCatalogService(path=get_admin_path_service().get_settings_file("model_catalog")) def admin_catalog() -> dict[str, Any]: return admin_catalog_service().load() def _profile_by_id(catalog: dict[str, Any], service: str, profile_id: str) -> dict[str, Any] | None: for profile in catalog.get("services", {}).get(service, {}).get("profiles", []) or []: if str(profile.get("id") or "") == profile_id: return profile return None def _model_by_id(profile: dict[str, Any], model_id: str) -> dict[str, Any] | None: for model in profile.get("models", []) or []: if str(model.get("id") or "") == model_id: return model return None def redacted_model_access(user_id: str | None = None) -> dict[str, list[dict[str, Any]]]: user = get_current_user() if user_id is None: user_id = user.id grant = load_grant(user_id) catalog = admin_catalog() result: dict[str, list[dict[str, Any]]] = {"llm": []} for item in grant.get("models", {}).get("llm", []) or []: profile_id = str(item.get("profile_id") or item.get("id") or "") profile = _profile_by_id(catalog, "llm", profile_id) if not profile: result["llm"].append( { "profile_id": profile_id, "name": item.get("name") or profile_id or "Unavailable profile", "source": "admin", "available": False, } ) continue for model_id in item.get("model_ids") or []: model = _model_by_id(profile, str(model_id)) result["llm"].append( { "profile_id": profile_id, "model_id": str(model_id), "name": (model or {}).get("name") or str(model_id), "model": (model or {}).get("model") or "", "source": "admin", "available": model is not None, } ) return result def allowed_llm_options() -> dict[str, Any]: user = get_current_user() if user.is_admin: return list_llm_options(admin_catalog()) options = [ { "profile_id": item.get("profile_id"), "model_id": item.get("model_id"), "profile_name": item.get("name") or item.get("profile_id") or "LLM", "model_name": item.get("name") or item.get("model") or item.get("model_id"), "label": item.get("name") or item.get("model") or item.get("model_id"), "model": item.get("model") or "", "provider": "", "source": "admin", "is_active_default": False, } for item in redacted_model_access(user.id).get("llm", []) if item.get("available") ] return {"active": None, "options": options} def has_capability_access(capability: str, user_id: str | None = None) -> bool: """Whether the user has at least one usable model for ``capability``. Admins are never gated — they manage the catalog directly. For ordinary users this mirrors exactly what ``redacted_model_access`` exposes to the frontend, so the server-side gate and the UI lock always agree. """ user = get_current_user() if user.is_admin: return True if user_id is None: user_id = user.id items = redacted_model_access(user_id).get(capability, []) or [] return any(item.get("available") for item in items) def apply_allowed_llm_selection(selection: dict[str, Any] | None) -> dict[str, Any] | None: """Allow only admin-granted LLM profile/model selections for ordinary users.""" user = get_current_user() if user.is_admin or not selection: return selection profile_id = str(selection.get("profile_id") or "") model_id = str(selection.get("model_id") or "") for item in redacted_model_access(user.id).get("llm", []): if item.get("profile_id") == profile_id and item.get("model_id") == model_id: return selection raise PermissionError("This model is not assigned to your account.")