import { z } from "zod"; import type { FastifyInstance } from "fastify"; import { prisma } from "./db.js"; import { requireAdmin } from "./auth.js"; import { runMultiplex } from "./llm/multiplexer.js"; import { runMultiplexStream } from "./llm/streaming.js"; export async function registerRoutes(app: FastifyInstance) { app.get("/health", async () => ({ ok: true })); app.get("/v1/chats", async (req) => { requireAdmin(req); const chats = await prisma.chat.findMany({ orderBy: { updatedAt: "desc" }, take: 100, select: { id: true, title: true, createdAt: true, updatedAt: true }, }); return { chats }; }); app.post("/v1/chats", async (req) => { requireAdmin(req); const Body = z.object({ title: z.string().optional() }); const body = Body.parse(req.body ?? {}); const chat = await prisma.chat.create({ data: { title: body.title } }); return { chat }; }); app.get("/v1/chats/:chatId", async (req) => { requireAdmin(req); const Params = z.object({ chatId: z.string() }); const { chatId } = Params.parse(req.params); const chat = await prisma.chat.findUnique({ where: { id: chatId }, include: { messages: { orderBy: { createdAt: "asc" } }, calls: { orderBy: { createdAt: "desc" } } }, }); if (!chat) return app.httpErrors.notFound("chat not found"); return { chat }; }); app.post("/v1/chats/:chatId/messages", async (req) => { requireAdmin(req); const Params = z.object({ chatId: z.string() }); const Body = z.object({ role: z.enum(["system", "user", "assistant", "tool"]), content: z.string(), name: z.string().optional(), metadata: z.unknown().optional(), }); const { chatId } = Params.parse(req.params); const body = Body.parse(req.body); const msg = await prisma.message.create({ data: { chatId, role: body.role as any, content: body.content, name: body.name, metadata: body.metadata as any, }, }); return { message: msg }; }); // Main: create a completion via provider+model and store everything. app.post("/v1/chat-completions", async (req) => { requireAdmin(req); const Body = z.object({ chatId: z.string().optional(), provider: z.enum(["openai", "anthropic", "xai"]), model: z.string().min(1), messages: z.array( z.object({ role: z.enum(["system", "user", "assistant", "tool"]), content: z.string(), name: z.string().optional(), }) ), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().int().positive().optional(), }); const body = Body.parse(req.body); // ensure chat exists if provided if (body.chatId) { const exists = await prisma.chat.findUnique({ where: { id: body.chatId }, select: { id: true } }); if (!exists) return app.httpErrors.notFound("chat not found"); } // store user messages (anything not assistant) for DB fidelity if (body.chatId) { const toInsert = body.messages.filter((m) => m.role !== "assistant"); if (toInsert.length) { await prisma.message.createMany({ data: toInsert.map((m) => ({ chatId: body.chatId!, role: m.role as any, content: m.content, name: m.name })), }); } } const result = await runMultiplex(body); return { chatId: body.chatId ?? null, ...result, }; }); // Streaming SSE endpoint. app.post("/v1/chat-completions/stream", async (req, reply) => { requireAdmin(req); const Body = z.object({ chatId: z.string().optional(), provider: z.enum(["openai", "anthropic", "xai"]), model: z.string().min(1), messages: z.array( z.object({ role: z.enum(["system", "user", "assistant", "tool"]), content: z.string(), name: z.string().optional(), }) ), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().int().positive().optional(), }); const body = Body.parse(req.body); // ensure chat exists if provided if (body.chatId) { const exists = await prisma.chat.findUnique({ where: { id: body.chatId }, select: { id: true } }); if (!exists) return app.httpErrors.notFound("chat not found"); } // store user messages (anything not assistant) for DB fidelity if (body.chatId) { const toInsert = body.messages.filter((m) => m.role !== "assistant"); if (toInsert.length) { await prisma.message.createMany({ data: toInsert.map((m) => ({ chatId: body.chatId!, role: m.role as any, content: m.content, name: m.name })), }); } } reply.raw.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }); const send = (event: string, data: any) => { reply.raw.write(`event: ${event}\n`); reply.raw.write(`data: ${JSON.stringify(data)}\n\n`); }; for await (const ev of runMultiplexStream(body)) { if (ev.type === "meta") send("meta", ev); else if (ev.type === "delta") send("delta", ev); else if (ev.type === "done") send("done", ev); else if (ev.type === "error") send("error", ev); } reply.raw.end(); return reply; }); }