"""Administration von Endpunkten, Keys und Modellen; Modellwahl im Workspace.""" from typing import Annotated from fastapi import APIRouter, Form, Request from fastapi.concurrency import run_in_threadpool from fastapi.responses import HTMLResponse, Response from sqlalchemy import select from app import web from app.ai import service as ai from app.ai.models import MODEL_CLASSES, Model, ProviderKey from app.ai.provider import ProviderError, ResponsesProvider from app.core.errors import Invalid from app.identity.models import Team Text = Annotated[str, Form()] Flag = Annotated[bool, Form()] def router() -> APIRouter: r = APIRouter(prefix="") def page(request: Request, principal, error: str = "", notice: str = "", status: int = 200) -> HTMLResponse: ai.require_platform_admin(principal) with web.db(request).session() as session: context = { "providers": ai.providers(session), "keys": ai.keys(session), "models": ai.models(session), "teams": list(session.scalars(select(Team).order_by(Team.name))), "classes": MODEL_CLASSES, "vault_ready": web.vault(request).ready, "error": error, "notice": notice, } context["provider_names"] = {p.id: p.name for p in context["providers"]} return web.render(request, "admin_models.html", context, status) def act(request: Request, principal, action, notice: str) -> Response: try: with web.db(request).session() as session: action(session) except Invalid as e: return page(request, principal, error=str(e), status=400) return page(request, principal, notice=notice) @r.get("/admin/models") def overview(request: Request, principal: web.User) -> HTMLResponse: return page(request, principal) @r.post("/admin/providers") def save_provider(request: Request, principal: web.Checked, name: Text, base_url: Text, provider_id: Annotated[int | None, Form()] = None, active: Flag = False) -> Response: return act(request, principal, lambda s: ai.save_provider(s, principal, provider_id, name=name, base_url=base_url, active=active), "Endpunkt gespeichert.") @r.post("/admin/providers/{provider_id}/test") async def test_provider(request: Request, principal: web.Checked, provider_id: int) -> HTMLResponse: ai.require_platform_admin(principal) def run() -> tuple[bool, str]: with web.db(request).session() as session: provider = ai.get_provider(session, provider_id) key = session.scalar(select(ProviderKey).where(ProviderKey.provider_id == provider_id) .order_by(ProviderKey.team_id.nulls_first())) if key is None: return False, "Für diesen Endpunkt ist noch kein Key hinterlegt." target = ai.Target(base_url=provider.base_url, api_key=web.vault(request).decrypt(key.secret), model="", model_label="", supports_reasoning=False) known = set(session.scalars(select(Model.name).where(Model.provider_id == provider_id))) try: names = ResponsesProvider(timeout=15).list_models(target) except ProviderError as e: return False, f"{e.user_message} ({e})"[:300] listed = ", ".join(n for n in names if n in known) or "keines der eingetragenen Modelle" return True, f"Verbindung ok, {len(names)} Modelle verfügbar. Eingetragen und verfügbar: {listed}." ok, message = await run_in_threadpool(run) return web.render(request, "_notice.html", {"ok": ok, "message": message}) @r.post("/admin/keys") def set_key(request: Request, principal: web.Checked, provider_id: Annotated[int, Form()], value: Text, team_id: Text = "") -> Response: team = int(team_id) if team_id else None return act(request, principal, lambda s: ai.set_key(s, principal, web.vault(request), provider_id, team, value), "Key gespeichert. Er wird nicht wieder angezeigt.") @r.post("/admin/keys/{key_id}/delete") def delete_key(request: Request, principal: web.Checked, key_id: int) -> Response: return act(request, principal, lambda s: ai.delete_key(s, principal, key_id), "Key gelöscht.") @r.post("/admin/models") def save_model(request: Request, principal: web.Checked, provider_id: Annotated[int, Form()], name: Text, label: Text = "", model_class: Text = "standard", context_window: Annotated[int, Form()] = 128000, supports_tools: Flag = False, supports_reasoning: Flag = False, active: Flag = False, model_id: Annotated[int | None, Form()] = None, input_price: Text = "", cached_price: Text = "", output_price: Text = "") -> Response: return act(request, principal, lambda s: ai.save_model( s, principal, model_id, provider_id=provider_id, name=name, label=label, model_class=model_class, context_window=context_window, supports_tools=supports_tools, supports_reasoning=supports_reasoning, active=active, input_price=input_price, cached_price=cached_price, output_price=output_price), "Modell gespeichert.") @r.post("/admin/models/{model_id}/default") def set_default(request: Request, principal: web.Checked, model_id: int) -> Response: return act(request, principal, lambda s: ai.set_default(s, principal, model_id), "Standardmodell gesetzt.") @r.post("/admin/models/{model_id}/delete") def delete_model(request: Request, principal: web.Checked, model_id: int) -> Response: return act(request, principal, lambda s: ai.delete_model(s, principal, model_id), "Modell gelöscht.") @r.post("/admin/providers/{provider_id}/delete") def delete_provider(request: Request, principal: web.Checked, provider_id: int) -> Response: return act(request, principal, lambda s: ai.delete_provider(s, principal, provider_id), "Endpunkt mit seinen API-Keys gelöscht.") @r.post("/workspaces/{workspace_id}/model") def workspace_model(request: Request, principal: web.Checked, workspace_id: int, model_id: Text = "") -> Response: with web.db(request).session() as session: ai.set_workspace_model(session, principal, workspace_id, int(model_id) if model_id else None) return web.redirect(f"/workspaces/{workspace_id}/settings?tab=general&saved=1") return r