126 lines
6.6 KiB
Python
126 lines
6.6 KiB
Python
"""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
|