import { BadGatewayException, Injectable, Logger, 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"; interface ContainerConnection { url: string; token: string; } @Injectable() export class ChatProxyService { private readonly logger = new Logger(ChatProxyService.name); 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.getContainerConnection(userId); return url; } // Proxy chat request to OpenClaw. async proxyChat( userId: string, messages: ChatMessage[], signal?: AbortSignal ): Promise { const { url: containerUrl, token: gatewayToken } = await this.getContainerConnection(userId); const model = await this.getPreferredModel(userId); const requestInit: RequestInit = { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${gatewayToken}`, }, 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(); this.logger.warn( detail ? `OpenClaw returned ${status}: ${detail}` : `OpenClaw returned ${status}` ); throw new BadGatewayException(`OpenClaw returned ${status}`); } return response; } catch (error: unknown) { if (error instanceof BadGatewayException) { throw error; } const message = error instanceof Error ? error.message : String(error); this.logger.warn(`Failed to proxy chat request: ${message}`); throw new ServiceUnavailableException("Failed to proxy chat to OpenClaw"); } } private async getContainerConnection(userId: string): Promise { const connection = await this.containerLifecycle.ensureRunning(userId); await this.containerLifecycle.touch(userId); return connection; } 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; } } }