import { Router, type IRouter, type Request } from "express"; import { db } from "@workspace/db"; import { aiProvidersTable, type AiProvider } from "@workspace/db"; import { eq } from "drizzle-orm"; import { ListProvidersResponse, CreateProviderBody, UpdateProviderParams, UpdateProviderBody, UpdateProviderResponse, DeleteProviderParams, TestProviderParams, TestProviderResponse, TestProviderConnectionBody, ListProviderModelsBody, ListProviderModelsResponse, } from "@workspace/api-zod"; import { callProvider, listProviderModels } from "../lib/aiAnalysis"; import { t, reqLang } from "../lib/i18n"; const router: IRouter = Router(); function maskToken(token: string | null): string { if (!token) return ""; if (token.length <= 8) return "••••"; return `${token.slice(0, 3)}…${token.slice(-4)}`; } function serializeProvider(p: AiProvider) { return { id: p.id, name: p.name, apiType: p.apiType, baseUrl: p.baseUrl, model: p.model, enabled: p.enabled, hasToken: !!p.apiToken, tokenPreview: maskToken(p.apiToken), createdAt: p.createdAt.toISOString(), }; } router.get("/providers", async (_req, res) => { const rows = await db.select().from(aiProvidersTable).orderBy(aiProvidersTable.id); res.json(ListProvidersResponse.parse(rows.map(serializeProvider))); }); router.post("/providers", async (req, res) => { const parsed = CreateProviderBody.safeParse(req.body); if (!parsed.success) return res .status(400) .json({ message: t("invalidInput", reqLang(req)), details: parsed.error.issues }); const d = parsed.data; const [created] = await db .insert(aiProvidersTable) .values({ name: d.name, apiType: d.apiType, baseUrl: d.baseUrl, model: d.model, apiToken: d.apiToken ?? null, enabled: d.enabled ?? true, }) .returning(); return res .status(201) .json(UpdateProviderResponse.parse(serializeProvider(created))); }); router.patch("/providers/:id", async (req, res) => { const params = UpdateProviderParams.safeParse(req.params); if (!params.success) return res.status(400).json({ message: t("invalidId", reqLang(req)) }); const parsed = UpdateProviderBody.safeParse(req.body); if (!parsed.success) return res .status(400) .json({ message: t("invalidInput", reqLang(req)), details: parsed.error.issues }); const d = parsed.data; const update: Partial = {}; if (d.name !== undefined) update.name = d.name; if (d.apiType !== undefined) update.apiType = d.apiType; if (d.baseUrl !== undefined) update.baseUrl = d.baseUrl; if (d.model !== undefined) update.model = d.model; if (d.enabled !== undefined) update.enabled = d.enabled; if (d.apiToken !== undefined && d.apiToken !== "") update.apiToken = d.apiToken; const [updated] = await db .update(aiProvidersTable) .set(update) .where(eq(aiProvidersTable.id, params.data.id)) .returning(); if (!updated) return res.status(404).json({ message: t("providerNotFound", reqLang(req)) }); return res.json(UpdateProviderResponse.parse(serializeProvider(updated))); }); router.delete("/providers/:id", async (req, res) => { const params = DeleteProviderParams.safeParse(req.params); if (!params.success) return res.status(400).json({ message: t("invalidId", reqLang(req)) }); await db .delete(aiProvidersTable) .where(eq(aiProvidersTable.id, params.data.id)); return res.status(204).send(); }); router.post("/providers/:id/test", async (req, res) => { const params = TestProviderParams.safeParse(req.params); if (!params.success) return res.status(400).json({ message: t("invalidId", reqLang(req)) }); const [provider] = await db .select() .from(aiProvidersTable) .where(eq(aiProvidersTable.id, params.data.id)); if (!provider) return res.status(404).json({ message: t("providerNotFound", reqLang(req)) }); if (!provider.apiToken) { return res.json( TestProviderResponse.parse({ ok: false, message: t("noApiTokenPlain", reqLang(req)), }), ); } try { const reply = await callProvider( provider, "Du bist ein Verbindungstest.", 'Antworte mit dem einzelnen Wort "OK".', ); return res.json( TestProviderResponse.parse({ ok: true, message: t("connSuccessReply", reqLang(req), { reply: reply.trim().slice(0, 80) || t("connReplyEmpty", reqLang(req)), }), }), ); } catch (err) { return res.json( TestProviderResponse.parse({ ok: false, message: err instanceof Error ? err.message : t("connFailed", reqLang(req)), }), ); } }); router.post("/providers/test-connection", async (req, res) => { const parsed = TestProviderConnectionBody.safeParse(req.body); if (!parsed.success) return res .status(400) .json({ message: t("invalidInput", reqLang(req)), details: parsed.error.issues }); const d = parsed.data; let token: string | null = d.apiToken && d.apiToken !== "" ? d.apiToken : null; if (!token && d.providerId !== undefined && d.providerId !== null) { const [existing] = await db .select() .from(aiProvidersTable) .where(eq(aiProvidersTable.id, d.providerId)); if (existing?.apiToken) token = existing.apiToken; } if (!token) { return res.json( TestProviderResponse.parse({ ok: false, message: t("noApiTokenProvided", reqLang(req)), }), ); } const hasModel = typeof d.model === "string" && d.model.trim() !== ""; const provider: AiProvider = { id: d.providerId ?? 0, name: "", apiType: d.apiType, baseUrl: d.baseUrl, model: hasModel ? (d.model as string) : "", apiToken: token, enabled: true, createdAt: new Date(), }; try { if (hasModel) { const reply = await callProvider( provider, "Du bist ein Verbindungstest.", 'Antworte mit dem einzelnen Wort "OK".', ); return res.json( TestProviderResponse.parse({ ok: true, message: t("connSuccessReply", reqLang(req), { reply: reply.trim().slice(0, 80) || t("connReplyEmpty", reqLang(req)), }), }), ); } const models = await listProviderModels(provider); return res.json( TestProviderResponse.parse({ ok: true, message: models.length > 0 ? t("connSuccessModels", reqLang(req), { count: String(models.length) }) : t("connSuccessNoModels", reqLang(req)), }), ); } catch (err) { return res.json( TestProviderResponse.parse({ ok: false, message: err instanceof Error ? err.message : t("connFailed", reqLang(req)), }), ); } }); router.post("/providers/list-models", async (req, res) => { const parsed = ListProviderModelsBody.safeParse(req.body); if (!parsed.success) return res .status(400) .json({ message: t("invalidInput", reqLang(req)), details: parsed.error.issues }); const d = parsed.data; let token: string | null = d.apiToken && d.apiToken !== "" ? d.apiToken : null; if (!token && d.providerId !== undefined && d.providerId !== null) { const [existing] = await db .select() .from(aiProvidersTable) .where(eq(aiProvidersTable.id, d.providerId)); if (existing?.apiToken) token = existing.apiToken; } if (!token) { return res.json( ListProviderModelsResponse.parse({ ok: false, models: [], message: t("noApiTokenProvided", reqLang(req)), }), ); } const provider: AiProvider = { id: d.providerId ?? 0, name: "", apiType: d.apiType, baseUrl: d.baseUrl, model: "", apiToken: token, enabled: true, createdAt: new Date(), }; try { const models = await listProviderModels(provider); return res.json( ListProviderModelsResponse.parse({ ok: true, models }), ); } catch (err) { return res.json( ListProviderModelsResponse.parse({ ok: false, models: [], message: err instanceof Error ? err.message : t("modelsLoadFailed", reqLang(req)), }), ); } }); export default router;