import { Body, Controller, HttpException, Logger, Post, Req, Res, UnauthorizedException, UseGuards, } from "@nestjs/common"; import type { Response } from "express"; import { AuthGuard } from "../auth/guards/auth.guard"; import type { MaybeAuthenticatedRequest } from "../auth/types/better-auth-request.interface"; import { ChatStreamDto } from "./chat-proxy.dto"; import { ChatProxyService } from "./chat-proxy.service"; @Controller("chat") @UseGuards(AuthGuard) export class ChatProxyController { private readonly logger = new Logger(ChatProxyController.name); constructor(private readonly chatProxyService: ChatProxyService) {} // POST /api/chat/stream // Request: { messages: Array<{role, content}> } // Response: SSE stream of chat completion events @Post("stream") async streamChat( @Body() body: ChatStreamDto, @Req() req: MaybeAuthenticatedRequest, @Res() res: Response ): Promise { const userId = req.user?.id; if (!userId) { throw new UnauthorizedException("No authenticated user found on request"); } const abortController = new AbortController(); req.once("close", () => { abortController.abort(); }); res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); try { const upstreamResponse = await this.chatProxyService.proxyChat( userId, body.messages, abortController.signal ); const upstreamContentType = upstreamResponse.headers.get("content-type"); if (upstreamContentType) { res.setHeader("Content-Type", upstreamContentType); } if (!upstreamResponse.body) { throw new Error("OpenClaw response did not include a stream body"); } for await (const chunk of upstreamResponse.body as unknown as AsyncIterable) { if (res.writableEnded || res.destroyed) { break; } res.write(Buffer.from(chunk)); } } catch (error: unknown) { this.logStreamError(error); if (!res.writableEnded && !res.destroyed) { res.write("event: error\n"); res.write(`data: ${JSON.stringify({ error: this.toSafeClientMessage(error) })}\n\n`); } } finally { if (!res.writableEnded && !res.destroyed) { res.end(); } } } private toSafeClientMessage(error: unknown): string { if (error instanceof HttpException && error.getStatus() < 500) { return "Chat request was rejected"; } return "Chat stream failed"; } private logStreamError(error: unknown): void { if (error instanceof Error) { this.logger.warn(`Chat stream failed: ${error.message}`); return; } this.logger.warn(`Chat stream failed: ${String(error)}`); } }