import { BadGatewayException, Injectable, ServiceUnavailableException } from "@nestjs/common"; import { ContainerLifecycleService } from "../container-lifecycle/container-lifecycle.service"; import { PrismaService } from "../prisma/prisma.service"; import type { ChatMessage } from "./chat-proxy.dto"; const DEFAULT_OPENCLAW_MODEL = "openclaw:default"; @Injectable() export class ChatProxyService { constructor( private readonly prisma: PrismaService, private readonly containerLifecycle: ContainerLifecycleService ) {} // Get the user's OpenClaw container URL and mark it active. async getContainerUrl(userId: string): Promise { const { url } = await this.containerLifecycle.ensureRunning(userId); await this.containerLifecycle.touch(userId); return url; } // Proxy chat request to OpenClaw. async proxyChat( userId: string, messages: ChatMessage[], signal?: AbortSignal ): Promise { const containerUrl = await this.getContainerUrl(userId); const model = await this.getPreferredModel(userId); const requestInit: RequestInit = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages, model, stream: true, }), }; if (signal) { requestInit.signal = signal; } try { const response = await fetch(`${containerUrl}/v1/chat/completions`, requestInit); if (!response.ok) { const detail = await this.readResponseText(response); const status = `${String(response.status)} ${response.statusText}`.trim(); const message = detail ? `OpenClaw returned ${status}: ${detail}` : `OpenClaw returned ${status}`; throw new BadGatewayException(message); } return response; } catch (error: unknown) { if (error instanceof BadGatewayException) { throw error; } const message = error instanceof Error ? error.message : String(error); throw new ServiceUnavailableException(`Failed to proxy chat to OpenClaw: ${message}`); } } private async getPreferredModel(userId: string): Promise { const config = await this.prisma.userAgentConfig.findUnique({ where: { userId }, select: { primaryModel: true }, }); const primaryModel = config?.primaryModel?.trim(); if (!primaryModel) { return DEFAULT_OPENCLAW_MODEL; } return primaryModel; } private async readResponseText(response: Response): Promise { try { const text = (await response.text()).trim(); return text.length > 0 ? text : null; } catch { return null; } } }