import express from "express"; import crypto from "node:crypto"; const app = express(); app.use(express.json({ limit: "10mb" })); const PORT = Number(process.env.PORT || 3080); const OPENCLAW_BASE_URL = process.env.OPENCLAW_BASE_URL || "http://gateway:18789"; const OPENCLAW_BEARER_TOKEN = process.env.OPENCLAW_BEARER_TOKEN || ""; const OPENCLAW_MODELS = (process.env.OPENCLAW_MODELS || "openclaw-main") .split(",") .map((s) => s.trim()) .filter(Boolean); function nowSeconds() { return Math.floor(Date.now() / 1000); } function makeId(prefix) { return `${prefix}-${crypto.randomUUID()}`; } function pseudoModelToAgent(model) { if (!model) return "main"; if (model.startsWith("openclaw-")) { return model.slice("openclaw-".length); } if (model.startsWith("openclaw:")) { return model.slice("openclaw:".length); } if (model.startsWith("agent:")) { return model.slice("agent:".length); } return model; } function agentToOpenClawModel(agent) { return `openclaw:${agent}`; } function flattenContent(content) { if (typeof content === "string") return content; if (Array.isArray(content)) { return content .map((part) => { if (!part) return ""; if (typeof part === "string") return part; if (part?.type === "text") return part.text || ""; if (part?.type === "input_text") return part.text || ""; if (typeof part?.text === "string") return part.text; return ""; }) .join("\n"); } if (content == null) return ""; if (typeof content === "object") { if (typeof content.text === "string") return content.text; return JSON.stringify(content); } return String(content); } function normalizeMessages(messages = []) { if (!Array.isArray(messages)) return []; return messages .filter((m) => m && typeof m === "object") .map((m) => ({ role: typeof m.role === "string" && m.role.length ? m.role : "user", content: flattenContent(m.content), })) .filter((m) => m.content !== undefined); } function extractAssistantTextFromResponsesApi(payload) { if (!payload) return ""; if (typeof payload.output_text === "string" && payload.output_text.length) { return payload.output_text; } const output = Array.isArray(payload.output) ? payload.output : []; const texts = []; for (const item of output) { if (item?.type === "message" && Array.isArray(item.content)) { for (const part of item.content) { if (part?.type === "output_text" && typeof part.text === "string") { texts.push(part.text); } if (part?.type === "text" && typeof part.text === "string") { texts.push(part.text); } } } } return texts.join("\n").trim(); } function buildOpenClawInput(messages = []) { if (!Array.isArray(messages)) return []; return messages .filter((m) => m && typeof m === "object") .map((m) => ({ role: typeof m.role === "string" && m.role.length ? m.role : "user", content: [ { type: "input_text", text: flattenContent(m.content) || "", }, ], })); } function buildChatCompletionResponse({ model, content, finishReason = "stop" }) { return { id: makeId("chatcmpl"), object: "chat.completion", created: nowSeconds(), model, choices: [ { index: 0, message: { role: "assistant", content, }, finish_reason: finishReason, }, ], usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, }, }; } function sseWrite(res, obj) { res.write(`data: ${JSON.stringify(obj)}\n\n`); } app.get("/health", (_req, res) => { res.json({ ok: true }); }); app.get("/v1/models", (_req, res) => { res.json({ object: "list", data: OPENCLAW_MODELS.map((modelId) => ({ id: modelId, object: "model", created: 0, owned_by: "openclaw", })), }); }); app.post("/v1/chat/completions", async (req, res) => { try { const { model, messages = [], stream = false, user, temperature, max_tokens, } = req.body || {}; console.log("Incoming model:", model); console.log("Incoming messages preview:", JSON.stringify(messages?.slice?.(0, 3), null, 2)); const agent = pseudoModelToAgent(model || OPENCLAW_MODELS[0] || "openclaw-hermes"); const openclawModel = agentToOpenClawModel(agent); const normalizedMessages = normalizeMessages(messages); if (!normalizedMessages.length) { return res.status(400).json({ error: { message: "No valid messages were provided", type: "invalid_request_error", }, }); } const derivedUser = user || req.header("x-conversation-id") || req.header("x-session-id") || `librechat-${crypto.randomUUID()}`; const body = { model: openclawModel, input: buildOpenClawInput(normalizedMessages), user: derivedUser, temperature, max_output_tokens: max_tokens, stream: false }; const headers = { "content-type": "application/json", }; if (OPENCLAW_BEARER_TOKEN) { headers.authorization = `Bearer ${OPENCLAW_BEARER_TOKEN}`; } const upstream = await fetch(`${OPENCLAW_BASE_URL}/v1/responses`, { method: "POST", headers, body: JSON.stringify(body), }); const text = await upstream.text(); if (!upstream.ok) { res.status(upstream.status).json({ error: { message: text || "OpenClaw upstream error", type: "upstream_error", }, }); return; } const payload = JSON.parse(text); const assistantText = extractAssistantTextFromResponsesApi(payload); if (!stream) { res.json( buildChatCompletionResponse({ model: model || OPENCLAW_MODELS[0] || "openclaw-main", content: assistantText, }) ); return; } res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache, no-transform"); res.setHeader("Connection", "keep-alive"); sseWrite(res, { id: makeId("chatcmpl"), object: "chat.completion.chunk", created: nowSeconds(), model: model || OPENCLAW_MODELS[0] || "openclaw-main", choices: [ { index: 0, delta: { role: "assistant" }, finish_reason: null, }, ], }); if (assistantText) { sseWrite(res, { id: makeId("chatcmpl"), object: "chat.completion.chunk", created: nowSeconds(), model: model || OPENCLAW_MODELS[0] || "openclaw-main", choices: [ { index: 0, delta: { content: assistantText }, finish_reason: null, }, ], }); } sseWrite(res, { id: makeId("chatcmpl"), object: "chat.completion.chunk", created: nowSeconds(), model: model || OPENCLAW_MODELS[0] || "openclaw-main", choices: [ { index: 0, delta: {}, finish_reason: "stop", }, ], }); res.write("data: [DONE]\n\n"); res.end(); } catch (err) { res.status(500).json({ error: { message: err instanceof Error ? err.message : "Internal server error", type: "server_error", }, }); } }); app.listen(PORT, () => { console.log(`openclaw-librechat-bridge listening on ${PORT}`); });