#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import http from "node:http"; import crypto from "node:crypto"; import { URL } from "node:url"; import httpProxy from "http-proxy"; const PUBLIC_PORT = Number(process.env.PORT || 7860); const INTERNAL_PORT = Number(process.env.OPENCLAW_INTERNAL_PORT || 18789); const INTERNAL_BASE = `http://127.0.0.1:${INTERNAL_PORT}`; const STATE_DIR = process.env.OPENCLAW_STATE_DIR || "/app/.openclaw"; const RUNTIME_CONFIG_PATH = path.join(STATE_DIR, "worker-runtime.json"); const USAGE_LOG_PATH = path.join(STATE_DIR, "worker-usage.jsonl"); const GATEWAY_TOKEN = String( process.env.OPENCLAW_GATEWAY_TOKEN || process.env.OPENCLAW_TOKEN || "" ).trim(); const WORKER_TOKEN = String( process.env.OPENCLAW_WORKER_TOKEN || "" ).trim(); const ADMIN_TOKEN = String( process.env.OPENCLAW_ADMIN_TOKEN || WORKER_TOKEN ).trim(); const GROQ_API_KEY = String( process.env.GROQ_API_KEY || "" ).trim(); const HF_PROVIDER_API_KEY = String( process.env.OPENCLAW_HUGGINGFACE_API_KEY || process.env.HF_TOKEN || "" ).trim(); if (!GATEWAY_TOKEN) { console.error("[hf-worker-adapter] Missing OPENCLAW_GATEWAY_TOKEN / OPENCLAW_TOKEN"); process.exit(1); } if (!WORKER_TOKEN) { console.error("[hf-worker-adapter] Missing OPENCLAW_WORKER_TOKEN"); process.exit(1); } function nowIso() { return new Date().toISOString(); } function ensureState() { fs.mkdirSync(STATE_DIR, { recursive: true }); if (!fs.existsSync(RUNTIME_CONFIG_PATH)) { fs.writeFileSync( RUNTIME_CONFIG_PATH, JSON.stringify( { version: 1, updatedAt: nowIso(), defaults: { provider: "openclaw", agentId: "main", model: "", sessionKey: "webchat:languageapp", timeoutMs: 120000, temperature: 0.2, maxTokens: null, }, providers: { openclaw: { enabled: true, label: "OpenClaw (internal)", models: [{ id: "openclaw:main", provider: "openclaw", label: "OpenClaw Agent: main", agentId: "main" }], }, groq: { enabled: false, label: "Groq", baseUrl: "https://api.groq.com/openai/v1/chat/completions", models: [], }, huggingface: { enabled: false, label: "Hugging Face", baseUrl: "https://router.huggingface.co/v1/chat/completions", models: [], }, }, }, null, 2 ), "utf8" ); } if (!fs.existsSync(USAGE_LOG_PATH)) { fs.writeFileSync(USAGE_LOG_PATH, "", "utf8"); } } function readRuntimeConfig() { ensureState(); try { return JSON.parse(fs.readFileSync(RUNTIME_CONFIG_PATH, "utf8")); } catch { return { version: 1, updatedAt: nowIso(), defaults: { provider: "openclaw", agentId: "main", model: "", sessionKey: "webchat:languageapp", timeoutMs: 120000, temperature: 0.2, maxTokens: null, }, providers: {}, }; } } function writeRuntimeConfig(cfg) { cfg.updatedAt = nowIso(); fs.writeFileSync(RUNTIME_CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf8"); } function appendUsage(entry) { ensureState(); fs.appendFileSync(USAGE_LOG_PATH, `${JSON.stringify(entry)}\n`, "utf8"); } function readUsage(limit = 100) { ensureState(); const raw = fs.readFileSync(USAGE_LOG_PATH, "utf8"); const lines = raw .split("\n") .map((line) => line.trim()) .filter(Boolean); const rows = lines .slice(-Math.max(1, limit)) .map((line) => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean); return rows.reverse(); } function summarizeUsage(entries) { const byProvider = {}; let successCount = 0; let errorCount = 0; for (const row of entries) { const provider = row.provider || "unknown"; byProvider[provider] ??= { requests: 0, success: 0, errors: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, }; byProvider[provider].requests += 1; byProvider[provider].inputTokens += Number(row.usage?.prompt_tokens || 0); byProvider[provider].outputTokens += Number(row.usage?.completion_tokens || 0); byProvider[provider].totalTokens += Number(row.usage?.total_tokens || 0); if (row.success) { successCount += 1; byProvider[provider].success += 1; } else { errorCount += 1; byProvider[provider].errors += 1; } } return { total: entries.length, successCount, errorCount, byProvider, }; } function json(res, code, payload) { const body = JSON.stringify(payload); res.writeHead(code, { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(body), "cache-control": "no-store", }); res.end(body); } function safeEqual(a, b) { const ba = Buffer.from(String(a || "")); const bb = Buffer.from(String(b || "")); return ba.length === bb.length && crypto.timingSafeEqual(ba, bb); } function parseBearer(req) { const auth = String(req.headers.authorization || ""); const match = auth.match(/^Bearer\s+(.+)$/i); return match ? match[1] : ""; } function requireWorkerBearer(req) { const token = parseBearer(req); return token && safeEqual(token, WORKER_TOKEN); } function requireAdminBearer(req) { const token = parseBearer(req); return token && safeEqual(token, ADMIN_TOKEN); } function readBody(req) { return new Promise((resolve, reject) => { let data = ""; req.setEncoding("utf8"); req.on("data", (chunk) => { data += chunk; }); req.on("end", () => resolve(data)); req.on("error", reject); }); } function normalizeMessages(body, fallbackMessage = "") { if (Array.isArray(body.messages) && body.messages.length > 0) return body.messages; if (fallbackMessage) return [{ role: "user", content: fallbackMessage }]; return []; } function extractTextFromOpenAIResponse(data) { const content = data?.choices?.[0]?.message?.content; if (typeof content === "string") return content.trim(); if (Array.isArray(content)) { return content .map((part) => { if (typeof part === "string") return part; if (part?.type === "text") return String(part?.text || ""); return String(part?.text || part?.content || ""); }) .join("") .trim(); } if (typeof data?.output_text === "string") return data.output_text.trim(); if (typeof data?.text === "string") return data.text.trim(); if (typeof data?.message === "string") return data.message.trim(); return ""; } function extractUsage(data) { return { prompt_tokens: Number(data?.usage?.prompt_tokens || 0), completion_tokens: Number(data?.usage?.completion_tokens || 0), total_tokens: Number(data?.usage?.total_tokens || 0), }; } function classifyError({ statusCode, message }) { const msg = String(message || "").toLowerCase(); if (statusCode === 401 || statusCode === 403) return "auth_error"; if (statusCode === 402) return "billing_error"; if (statusCode === 408) return "timeout"; if (statusCode === 429) return "rate_limit"; if (msg.includes("depleted your monthly included credits")) return "billing_error"; if (msg.includes("purchase pre-paid credits")) return "billing_error"; if (msg.includes("insufficient credits")) return "billing_error"; if (msg.includes("payment required")) return "billing_error"; if (msg.includes("aborted")) return "timeout"; if (msg.includes("timed out")) return "timeout"; if (msg.includes("network")) return "network_error"; if (msg.includes("fetch failed")) return "network_error"; if (msg.includes("econnrefused")) return "network_error"; if (msg.includes("unexpected schema")) return "unexpected_schema"; if (msg.includes("invalid_json")) return "bad_response"; return "failed"; } function detectProviderFailureInText(text) { const t = String(text || "").trim(); const lower = t.toLowerCase(); if (!t) return null; if ( /^402\b/.test(t) || lower.includes("depleted your monthly included credits") || lower.includes("purchase pre-paid credits") || lower.includes("insufficient credits") || lower.includes("payment required") ) { return { kind: "billing_error", statusCode: 402, message: t, }; } return null; } function buildPreview(text, maxLen = 240) { const value = String(text || "").replace(/\s+/g, " ").trim(); return value.length > maxLen ? `${value.slice(0, maxLen)}…` : value; } async function probeInternalGateway() { try { const res = await fetch(`${INTERNAL_BASE}/`, { method: "GET", redirect: "manual", signal: AbortSignal.timeout(5000), }); return { reachable: true, statusCode: res.status, }; } catch (err) { return { reachable: false, error: err?.message || String(err), }; } } async function callOpenClawInternal({ agentId, model, sessionKey, timeoutMs, temperature, maxTokens, messages, }) { const effectiveModel = typeof model === "string" && model.trim().startsWith("openclaw:") ? model.trim() : `openclaw:${agentId}`; const payload = { model: effectiveModel, stream: false, messages, user: sessionKey, temperature, }; if (Number.isFinite(maxTokens) && maxTokens > 0) { payload.max_tokens = maxTokens; } const res = await fetch(`${INTERNAL_BASE}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${GATEWAY_TOKEN}`, "content-type": "application/json", "x-openclaw-agent-id": agentId, "x-openclaw-session-key": sessionKey, }, body: JSON.stringify(payload), signal: AbortSignal.timeout(timeoutMs), }); const rawText = await res.text(); let data = null; try { data = rawText ? JSON.parse(rawText) : null; } catch { data = null; } const responseText = extractTextFromOpenAIResponse(data) || rawText || ""; const embeddedFailure = detectProviderFailureInText(responseText); if (!res.ok) { const err = new Error( data?.error?.message || rawText || `openclaw_http_${res.status}` ); err.statusCode = res.status; err.responseText = responseText; throw err; } if (embeddedFailure) { const err = new Error(embeddedFailure.message); err.statusCode = embeddedFailure.statusCode; err.responseText = responseText; err.errorKind = embeddedFailure.kind; throw err; } return { provider: "openclaw", model: effectiveModel, upstreamStatus: res.status, text: responseText, usage: extractUsage(data), raw: data, }; } async function callOpenAICompatibleProvider({ provider, baseUrl, apiKey, model, sessionKey, timeoutMs, temperature, maxTokens, messages, }) { const payload = { model, stream: false, messages, temperature, user: sessionKey, }; if (Number.isFinite(maxTokens) && maxTokens > 0) { payload.max_tokens = maxTokens; } const res = await fetch(baseUrl, { method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json", }, body: JSON.stringify(payload), signal: AbortSignal.timeout(timeoutMs), }); const rawText = await res.text(); let data = null; try { data = rawText ? JSON.parse(rawText) : null; } catch { data = null; } const responseText = extractTextFromOpenAIResponse(data) || rawText || ""; const embeddedFailure = detectProviderFailureInText(responseText); if (!res.ok) { const err = new Error( data?.error?.message || responseText || rawText || `${provider}_http_${res.status}` ); err.statusCode = res.status; err.responseText = responseText || rawText; throw err; } if (embeddedFailure) { const err = new Error(embeddedFailure.message); err.statusCode = embeddedFailure.statusCode; err.responseText = responseText; err.errorKind = embeddedFailure.kind; throw err; } return { provider, model, upstreamStatus: res.status, text: responseText, usage: extractUsage(data), raw: data, }; } function selectModelForProvider(runtimeCfg, provider, requestedModel) { const byProvider = runtimeCfg.providers?.[provider]; if (requestedModel && String(requestedModel).trim()) return String(requestedModel).trim(); if (runtimeCfg.defaults?.model && runtimeCfg.defaults.provider === provider) { return String(runtimeCfg.defaults.model).trim(); } const firstModel = byProvider?.models?.[0]?.id; return firstModel || ""; } async function executeRequest(body) { const cfg = readRuntimeConfig(); const provider = String( body.provider || cfg.defaults?.provider || "openclaw" ).trim(); const sessionKey = String( body.sessionKey || body.user || cfg.defaults?.sessionKey || "webchat:languageapp" ).trim(); const agentId = String( body.agentId || body.agent || cfg.defaults?.agentId || "main" ).trim(); const timeoutMs = Number( body.timeoutMs || cfg.defaults?.timeoutMs || 120000 ); const temperature = Number.isFinite(Number(body.temperature)) ? Number(body.temperature) : Number(cfg.defaults?.temperature ?? 0.2); const maxTokens = body.maxTokens === null ? null : body.maxTokens !== undefined ? Number(body.maxTokens) : cfg.defaults?.maxTokens; const message = String( body.message || body.prompt || body.task || body.input?.message || "" ).trim(); const messages = normalizeMessages(body, message); if (!messages.length) { const err = new Error("missing_message"); err.statusCode = 400; err.errorKind = "bad_request"; throw err; } const requestedModel = String(body.model || "").trim(); const effectiveModel = selectModelForProvider(cfg, provider, requestedModel); if (provider === "openclaw") { return callOpenClawInternal({ agentId, model: effectiveModel, sessionKey, timeoutMs, temperature, maxTokens, messages, }); } if (provider === "groq") { if (!GROQ_API_KEY) { const err = new Error("groq_api_key_missing"); err.statusCode = 500; err.errorKind = "misconfiguration"; throw err; } return callOpenAICompatibleProvider({ provider: "groq", baseUrl: cfg.providers?.groq?.baseUrl || "https://api.groq.com/openai/v1/chat/completions", apiKey: GROQ_API_KEY, model: effectiveModel, sessionKey, timeoutMs, temperature, maxTokens, messages, }); } if (provider === "huggingface") { if (!HF_PROVIDER_API_KEY) { const err = new Error("huggingface_api_key_missing"); err.statusCode = 500; err.errorKind = "misconfiguration"; throw err; } return callOpenAICompatibleProvider({ provider: "huggingface", baseUrl: cfg.providers?.huggingface?.baseUrl || "https://router.huggingface.co/v1/chat/completions", apiKey: HF_PROVIDER_API_KEY, model: effectiveModel, sessionKey, timeoutMs, temperature, maxTokens, messages, }); } const err = new Error(`unsupported_provider:${provider}`); err.statusCode = 400; err.errorKind = "bad_request"; throw err; } function usageEntryBase({ requestId, provider, model, agentId, sessionKey, source, operation, startedAt, }) { return { requestId, timestamp: startedAt, provider, model, agentId, sessionKey, source, operation, }; } // Proxy für OpenClaw UI / WS const proxy = httpProxy.createProxyServer({ target: INTERNAL_BASE, ws: true, xfwd: true, changeOrigin: false, }); proxy.on("error", (err, req, res) => { try { if (res && !res.headersSent) { res.writeHead(502, { "content-type": "text/plain; charset=utf-8" }); } res?.end(`Bad gateway: ${err?.message || String(err)}`); } catch {} }); const server = http.createServer(async (req, res) => { ensureState(); const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); // --- Worker health -------------------------------------------------------- if (req.method === "GET" && url.pathname === "/api/openclaw/health") { if (!requireWorkerBearer(req)) { return json(res, 401, { ok: false, error: "unauthorized" }); } const probe = await probeInternalGateway(); const runtimeCfg = readRuntimeConfig(); return json(res, probe.reachable ? 200 : 502, { ok: probe.reachable, configured: true, reachable: probe.reachable, mode: "hf-openclaw-adapter", detail: probe.reachable ? `internal gateway reachable (HTTP ${probe.statusCode})` : probe.error || "internal gateway unreachable", defaults: runtimeCfg.defaults, providers: Object.fromEntries( Object.entries(runtimeCfg.providers || {}).map(([key, value]) => [ key, { enabled: Boolean(value?.enabled), modelCount: Array.isArray(value?.models) ? value.models.length : 0, }, ]) ), }); } // --- Worker execute ------------------------------------------------------- if (req.method === "POST" && url.pathname === "/api/openclaw/tasks/execute") { if (!requireWorkerBearer(req)) { return json(res, 401, { ok: false, error: "unauthorized", errorKind: "auth_error" }); } let body = {}; try { const raw = await readBody(req); body = raw ? JSON.parse(raw) : {}; } catch { return json(res, 400, { ok: false, error: "invalid_json", errorKind: "bad_request" }); } const runtimeCfg = readRuntimeConfig(); const provider = String(body.provider || runtimeCfg.defaults?.provider || "openclaw").trim(); const model = String(body.model || selectModelForProvider(runtimeCfg, provider, "") || "").trim(); const agentId = String(body.agentId || body.agent || runtimeCfg.defaults?.agentId || "main").trim(); const sessionKey = String(body.sessionKey || body.user || runtimeCfg.defaults?.sessionKey || "webchat:languageapp").trim(); const requestId = crypto.randomUUID(); const startedAt = nowIso(); const startedPerf = Date.now(); try { const result = await executeRequest(body); const latencyMs = Date.now() - startedPerf; const preview = buildPreview(result.text); const entry = { ...usageEntryBase({ requestId, provider: result.provider, model: result.model, agentId, sessionKey, source: "external-worker", operation: "execute", startedAt, }), finishedAt: nowIso(), success: true, statusCode: result.upstreamStatus, latencyMs, errorKind: null, errorMessage: null, preview, usage: result.usage, }; appendUsage(entry); return json(res, 200, { ok: true, mode: "hf-openclaw-adapter", provider: result.provider, model: result.model, agentId, sessionKey, output: result.text, text: result.text, message: result.text, usage: result.usage, requestId, latencyMs, raw: result.raw, }); } catch (err) { const latencyMs = Date.now() - startedPerf; const statusCode = Number(err?.statusCode || 502); const errorMessage = String(err?.message || err || "unknown_error"); const errorKind = String(err?.errorKind || classifyError({ statusCode, message: errorMessage, })); const entry = { ...usageEntryBase({ requestId, provider, model, agentId, sessionKey, source: "external-worker", operation: "execute", startedAt, }), finishedAt: nowIso(), success: false, statusCode, latencyMs, errorKind, errorMessage, preview: buildPreview(err?.responseText || errorMessage), usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, }, }; appendUsage(entry); return json(res, statusCode >= 400 && statusCode <= 599 ? statusCode : 502, { ok: false, mode: "hf-openclaw-adapter", provider, model, agentId, sessionKey, requestId, latencyMs, error: errorMessage, errorKind, preview: buildPreview(err?.responseText || errorMessage), }); } } // --- Admin: config -------------------------------------------------------- if (req.method === "GET" && url.pathname === "/api/openclaw/admin/config") { if (!requireAdminBearer(req)) { return json(res, 401, { ok: false, error: "unauthorized" }); } return json(res, 200, { ok: true, config: readRuntimeConfig(), }); } if (req.method === "PUT" && url.pathname === "/api/openclaw/admin/config") { if (!requireAdminBearer(req)) { return json(res, 401, { ok: false, error: "unauthorized" }); } let body = {}; try { const raw = await readBody(req); body = raw ? JSON.parse(raw) : {}; } catch { return json(res, 400, { ok: false, error: "invalid_json" }); } const cfg = readRuntimeConfig(); const nextDefaults = body.defaults || {}; const allowedProviders = ["openclaw", "groq", "huggingface"]; const nextProvider = String(nextDefaults.provider || cfg.defaults.provider || "openclaw").trim(); if (!allowedProviders.includes(nextProvider)) { return json(res, 400, { ok: false, error: "invalid_provider" }); } cfg.defaults.provider = nextProvider; cfg.defaults.agentId = String(nextDefaults.agentId || cfg.defaults.agentId || "main").trim(); cfg.defaults.model = String(nextDefaults.model || cfg.defaults.model || "").trim(); cfg.defaults.sessionKey = String(nextDefaults.sessionKey || cfg.defaults.sessionKey || "webchat:languageapp").trim(); if (nextDefaults.timeoutMs !== undefined) { const timeoutMs = Number(nextDefaults.timeoutMs); if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { return json(res, 400, { ok: false, error: "invalid_timeoutMs" }); } cfg.defaults.timeoutMs = timeoutMs; } if (nextDefaults.temperature !== undefined) { const temperature = Number(nextDefaults.temperature); if (!Number.isFinite(temperature)) { return json(res, 400, { ok: false, error: "invalid_temperature" }); } cfg.defaults.temperature = temperature; } if (nextDefaults.maxTokens !== undefined) { if (nextDefaults.maxTokens === null || nextDefaults.maxTokens === "") { cfg.defaults.maxTokens = null; } else { const maxTokens = Number(nextDefaults.maxTokens); if (!Number.isFinite(maxTokens) || maxTokens <= 0) { return json(res, 400, { ok: false, error: "invalid_maxTokens" }); } cfg.defaults.maxTokens = maxTokens; } } if (body.providers && typeof body.providers === "object") { for (const [provider, patch] of Object.entries(body.providers)) { if (!cfg.providers?.[provider]) continue; if (patch?.enabled !== undefined) { cfg.providers[provider].enabled = Boolean(patch.enabled); } if (typeof patch?.baseUrl === "string" && patch.baseUrl.trim()) { cfg.providers[provider].baseUrl = patch.baseUrl.trim(); } } } writeRuntimeConfig(cfg); return json(res, 200, { ok: true, config: cfg, }); } // --- Admin: models -------------------------------------------------------- if (req.method === "GET" && url.pathname === "/api/openclaw/admin/models") { if (!requireAdminBearer(req)) { return json(res, 401, { ok: false, error: "unauthorized" }); } const cfg = readRuntimeConfig(); const models = Object.entries(cfg.providers || {}) .flatMap(([provider, value]) => Array.isArray(value?.models) ? value.models.map((model) => ({ provider, enabled: Boolean(value?.enabled), ...model, })) : [] ); return json(res, 200, { ok: true, models, }); } // --- Admin: providers ----------------------------------------------------- if (req.method === "GET" && url.pathname === "/api/openclaw/admin/providers") { if (!requireAdminBearer(req)) { return json(res, 401, { ok: false, error: "unauthorized" }); } const cfg = readRuntimeConfig(); return json(res, 200, { ok: true, providers: { openclaw: { enabled: Boolean(cfg.providers?.openclaw?.enabled), label: cfg.providers?.openclaw?.label || "OpenClaw (internal)", authConfigured: Boolean(GATEWAY_TOKEN), baseUrl: INTERNAL_BASE, }, groq: { enabled: Boolean(cfg.providers?.groq?.enabled), label: cfg.providers?.groq?.label || "Groq", authConfigured: Boolean(GROQ_API_KEY), baseUrl: cfg.providers?.groq?.baseUrl || "", }, huggingface: { enabled: Boolean(cfg.providers?.huggingface?.enabled), label: cfg.providers?.huggingface?.label || "Hugging Face", authConfigured: Boolean(HF_PROVIDER_API_KEY), baseUrl: cfg.providers?.huggingface?.baseUrl || "", }, }, }); } // --- Admin: usage --------------------------------------------------------- if (req.method === "GET" && url.pathname === "/api/openclaw/admin/usage") { if (!requireAdminBearer(req)) { return json(res, 401, { ok: false, error: "unauthorized" }); } const limit = Math.min(500, Math.max(1, Number(url.searchParams.get("limit") || 100))); const rows = readUsage(limit); return json(res, 200, { ok: true, rows, summary: summarizeUsage(rows), }); } // --- Admin: probe --------------------------------------------------------- if (req.method === "POST" && url.pathname === "/api/openclaw/admin/probe") { if (!requireAdminBearer(req)) { return json(res, 401, { ok: false, error: "unauthorized" }); } let body = {}; try { const raw = await readBody(req); body = raw ? JSON.parse(raw) : {}; } catch { body = {}; } try { const result = await executeRequest({ provider: body.provider, model: body.model, agentId: body.agentId || "main", sessionKey: body.sessionKey || "webchat:languageapp", timeoutMs: body.timeoutMs || 120000, message: 'Antworte exakt mit OPENCLAW_RUNTIME_PROBE_OK und sonst nichts.', }); const ok = String(result.text || "").trim() === "OPENCLAW_RUNTIME_PROBE_OK"; return json(res, ok ? 200 : 502, { ok, provider: result.provider, model: result.model, text: result.text, usage: result.usage, }); } catch (err) { return json(res, Number(err?.statusCode || 502), { ok: false, error: String(err?.message || err), errorKind: String(err?.errorKind || classifyError({ statusCode: Number(err?.statusCode || 502), message: String(err?.message || err), })), }); } } // --- Alles andere -> OpenClaw UI / WS ------------------------------------ proxy.web(req, res); }); server.on("upgrade", (req, socket, head) => { proxy.ws(req, socket, head); }); server.listen(PUBLIC_PORT, "0.0.0.0", () => { ensureState(); console.log( `[hf-worker-adapter] listening on :${PUBLIC_PORT}, proxying UI/WS to ${INTERNAL_BASE}` ); });