Spaces:
Running
Running
| 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}` | |
| ); | |
| }); |