diff --git a/app/RELEASE_NOTES.md b/app/RELEASE_NOTES.md index ab48b2a..aaed924 100644 --- a/app/RELEASE_NOTES.md +++ b/app/RELEASE_NOTES.md @@ -1,6 +1,7 @@ # Release Notes ## Unreleased +- Added an authenticated model discovery page with copyable model IDs and an API example. - Made the usage dashboard the authenticated landing page. - Added an authenticated service-health dashboard with local range controls and admin-configurable retention with immediate purge. - Added lazy, exact hourly request-statistics caching for faster administrator reports, with pricing invalidation and manual UTC-range rebuilds. diff --git a/app/src/app/models/page.tsx b/app/src/app/models/page.tsx new file mode 100644 index 0000000..e33f8dd --- /dev/null +++ b/app/src/app/models/page.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { Check, Copy, Server, Terminal } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { api } from "~/trpc/react"; + +const modelsEndpoint = "https://api.example.com/api/v1/models"; +const curlExample = `curl "${modelsEndpoint}" \\ + -H "Authorization: Bearer "`; + +function modelTypeLabel(modelType: string) { + return modelType === "RERANK" ? "Reranking" : "Chat-Completions"; +} + +export default function ModelsPage() { + const { + data: models = [], + isLoading, + isError, + refetch, + } = api.models.list.useQuery(); + const [copied, setCopied] = useState(null); + + async function copyToClipboard(value: string, key: string, label: string) { + try { + await navigator.clipboard.writeText(value); + setCopied(key); + window.setTimeout(() => setCopied(null), 1600); + toast.success(`${label} kopiert.`); + } catch { + toast.error( + "Kopieren nicht möglich. Bitte die Zwischenablage-Berechtigung prüfen.", + ); + } + } + + return ( +
+
+
+ + API-Katalog +
+

Verfügbare Modelle

+

+ Entdecke die für die API konfigurierten Modelle. IDs lassen sich + direkt kopieren und in API-Anfragen verwenden. +

+
+ +
+ + +
+
+ Modellkatalog + + {isLoading + ? "Konfigurierte Modelle werden geladen…" + : `${models.length} ${models.length === 1 ? "Modell" : "Modelle"} verfügbar`} + +
+ {isError ? ( + + ) : null} +
+
+ + {isLoading ? ( + +
+
+ + ) : isError ? ( +

+ Modelle konnten nicht geladen werden. Bitte versuche es erneut. +

+ ) : models.length === 0 ? ( +

+ Aktuell sind keine Modelle konfiguriert. +

+ ) : ( +
    + {models.map((model) => { + const copyKey = `model:${model.id}`; + const didCopy = copied === copyKey; + return ( +
  • +
    +

    + {model.id} +

    + + {modelTypeLabel(model.modelType)} + +
    + +
  • + ); + })} +
+ )} + + + + + + + + Modelle per API abrufen + + + Beispiel mit API-Key im Bearer-Header. Ersetze den Beispiel-Host + und den Schlüssel durch deine Umgebung. + + + +
+
+								{curlExample}
+							
+
+ +

+ Die Antwort folgt dem OpenAI-Modelllistenformat mit Modell-IDs. + Der hier angezeigte Typ (Chat-Completions oder Reranking) ist + Konfigurationsinformation und kein Feld der API-Antwort. +

+
+
+
+
+ ); +} diff --git a/app/src/components/sidebar-nav.tsx b/app/src/components/sidebar-nav.tsx index be89848..a29c9ef 100644 --- a/app/src/components/sidebar-nav.tsx +++ b/app/src/components/sidebar-nav.tsx @@ -7,6 +7,7 @@ import { FileText, HeartPulse, KeyRound, + Search, Users, } from "lucide-react"; import Link from "next/link"; @@ -37,6 +38,11 @@ const userItems = [ label: "Meine API-Schlüssel", icon: KeyRound, }, + { + href: "/models", + label: "Modelle entdecken", + icon: Search, + }, ]; const adminItems = [ diff --git a/app/src/server/api/root.ts b/app/src/server/api/root.ts index 771600f..e695b2e 100644 --- a/app/src/server/api/root.ts +++ b/app/src/server/api/root.ts @@ -1,5 +1,6 @@ import { adminRouter } from "~/server/api/routers/admin"; import { apiKeyRouter } from "~/server/api/routers/api-key"; +import { modelsRouter } from "~/server/api/routers/models"; import { postRouter } from "~/server/api/routers/post"; import { reportingRouter } from "~/server/api/routers/reporting"; import { serviceHealthRouter } from "~/server/api/routers/service-health"; @@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({ admin: adminRouter, post: postRouter, apiKey: apiKeyRouter, + models: modelsRouter, reporting: reportingRouter, serviceHealth: serviceHealthRouter, }); diff --git a/app/src/server/api/routers/models.ts b/app/src/server/api/routers/models.ts new file mode 100644 index 0000000..32c29b6 --- /dev/null +++ b/app/src/server/api/routers/models.ts @@ -0,0 +1,11 @@ +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; +import { models } from "~/server/db/schema"; + +export const modelsRouter = createTRPCRouter({ + list: protectedProcedure.query(async ({ ctx }) => { + return ctx.db + .select({ id: models.id, modelType: models.modelType }) + .from(models) + .orderBy(models.id); + }), +});