morris5444 commited on
Commit
2d0a3f9
·
verified ·
1 Parent(s): 7e6cb75

Update hf-worker-adapter.mjs

Browse files
Files changed (1) hide show
  1. hf-worker-adapter.mjs +799 -59
hf-worker-adapter.mjs CHANGED
@@ -1,5 +1,7 @@
1
  #!/usr/bin/env node
2
 
 
 
3
  import http from "node:http";
4
  import crypto from "node:crypto";
5
  import { URL } from "node:url";
@@ -9,6 +11,10 @@ const PUBLIC_PORT = Number(process.env.PORT || 7860);
9
  const INTERNAL_PORT = Number(process.env.OPENCLAW_INTERNAL_PORT || 18789);
10
  const INTERNAL_BASE = `http://127.0.0.1:${INTERNAL_PORT}`;
11
 
 
 
 
 
12
  const GATEWAY_TOKEN = String(
13
  process.env.OPENCLAW_GATEWAY_TOKEN || process.env.OPENCLAW_TOKEN || ""
14
  ).trim();
@@ -17,28 +23,170 @@ const WORKER_TOKEN = String(
17
  process.env.OPENCLAW_WORKER_TOKEN || ""
18
  ).trim();
19
 
20
- const DEFAULT_AGENT = String(
21
- process.env.OPENCLAW_WORKER_DEFAULT_AGENT || "main"
22
  ).trim();
23
 
24
- const DEFAULT_SESSION_KEY = String(
25
- process.env.OPENCLAW_WORKER_DEFAULT_SESSION_KEY || "webchat:languageapp"
26
  ).trim();
27
 
28
- const DEFAULT_TIMEOUT_MS = Number(
29
- process.env.OPENCLAW_WORKER_DEFAULT_TIMEOUT_MS || 45000
30
- );
31
 
32
  if (!GATEWAY_TOKEN) {
33
- console.error("[hf-worker-adapter] Missing OPENCLAW_TOKEN or OPENCLAW_GATEWAY_TOKEN");
34
  process.exit(1);
35
  }
36
-
37
  if (!WORKER_TOKEN) {
38
  console.error("[hf-worker-adapter] Missing OPENCLAW_WORKER_TOKEN");
39
  process.exit(1);
40
  }
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  function json(res, code, payload) {
43
  const body = JSON.stringify(payload);
44
  res.writeHead(code, {
@@ -50,16 +198,25 @@ function json(res, code, payload) {
50
  }
51
 
52
  function safeEqual(a, b) {
53
- const ba = Buffer.from(a);
54
- const bb = Buffer.from(b);
55
  return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
56
  }
57
 
58
- function requireBearer(req) {
59
  const auth = String(req.headers.authorization || "");
60
  const match = auth.match(/^Bearer\s+(.+)$/i);
61
- if (!match) return false;
62
- return safeEqual(match[1], WORKER_TOKEN);
 
 
 
 
 
 
 
 
 
63
  }
64
 
65
  function readBody(req) {
@@ -74,26 +231,91 @@ function readBody(req) {
74
  });
75
  }
76
 
 
 
 
 
 
 
77
  function extractTextFromOpenAIResponse(data) {
78
- const choice = data?.choices?.[0];
79
- const content = choice?.message?.content;
80
 
81
- if (typeof content === "string") return content;
82
 
83
  if (Array.isArray(content)) {
84
  return content
85
  .map((part) => {
86
  if (typeof part === "string") return part;
87
- if (part?.type === "text") return part?.text || "";
88
- return part?.text || part?.content || "";
89
  })
90
  .join("")
91
  .trim();
92
  }
93
 
 
 
 
 
94
  return "";
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  async function probeInternalGateway() {
98
  try {
99
  const res = await fetch(`${INTERNAL_BASE}/`, {
@@ -114,26 +336,36 @@ async function probeInternalGateway() {
114
  }
115
  }
116
 
117
- async function executeViaChatCompletions({
118
- message,
119
- messages,
120
  agentId,
 
121
  sessionKey,
122
  timeoutMs,
 
 
 
123
  }) {
 
 
 
 
 
124
  const payload = {
125
- model: `openclaw:${agentId}`,
126
  stream: false,
127
- messages: Array.isArray(messages) && messages.length > 0
128
- ? messages
129
- : [{ role: "user", content: message }],
130
  user: sessionKey,
 
131
  };
132
 
 
 
 
 
133
  const res = await fetch(`${INTERNAL_BASE}/v1/chat/completions`, {
134
  method: "POST",
135
  headers: {
136
- "authorization": `Bearer ${GATEWAY_TOKEN}`,
137
  "content-type": "application/json",
138
  "x-openclaw-agent-id": agentId,
139
  "x-openclaw-session-key": sessionKey,
@@ -143,7 +375,77 @@ async function executeViaChatCompletions({
143
  });
144
 
145
  const rawText = await res.text();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
 
147
  let data = null;
148
  try {
149
  data = rawText ? JSON.parse(rawText) : null;
@@ -151,21 +453,179 @@ async function executeViaChatCompletions({
151
  data = null;
152
  }
153
 
 
 
 
154
  if (!res.ok) {
155
- throw new Error(
156
- `openclaw_http_${res.status}: ${data?.error?.message || rawText || "unknown error"}`
157
  );
 
 
 
158
  }
159
 
160
- const text = extractTextFromOpenAIResponse(data);
 
 
 
 
 
 
161
 
162
  return {
163
- text,
 
 
 
 
164
  raw: data,
165
  };
166
  }
167
 
168
- // Proxy für alles außer /api/openclaw/*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  const proxy = httpProxy.createProxyServer({
170
  target: INTERNAL_BASE,
171
  ws: true,
@@ -183,15 +643,18 @@ proxy.on("error", (err, req, res) => {
183
  });
184
 
185
  const server = http.createServer(async (req, res) => {
 
 
186
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
187
 
188
- // Health
189
  if (req.method === "GET" && url.pathname === "/api/openclaw/health") {
190
- if (!requireBearer(req)) {
191
  return json(res, 401, { ok: false, error: "unauthorized" });
192
  }
193
 
194
  const probe = await probeInternalGateway();
 
195
 
196
  return json(res, probe.reachable ? 200 : 502, {
197
  ok: probe.reachable,
@@ -201,13 +664,23 @@ const server = http.createServer(async (req, res) => {
201
  detail: probe.reachable
202
  ? `internal gateway reachable (HTTP ${probe.statusCode})`
203
  : probe.error || "internal gateway unreachable",
 
 
 
 
 
 
 
 
 
 
204
  });
205
  }
206
 
207
- // Execute
208
  if (req.method === "POST" && url.pathname === "/api/openclaw/tasks/execute") {
209
- if (!requireBearer(req)) {
210
- return json(res, 401, { ok: false, error: "unauthorized" });
211
  }
212
 
213
  let body = {};
@@ -215,53 +688,319 @@ const server = http.createServer(async (req, res) => {
215
  const raw = await readBody(req);
216
  body = raw ? JSON.parse(raw) : {};
217
  } catch {
218
- return json(res, 400, { ok: false, error: "invalid_json" });
219
  }
220
 
221
- const agentId = String(body.agentId || body.agent || DEFAULT_AGENT).trim();
222
- const sessionKey = String(body.sessionKey || body.user || DEFAULT_SESSION_KEY).trim();
223
- const timeoutMs = Number(body.timeoutMs || DEFAULT_TIMEOUT_MS);
224
-
225
- const message =
226
- String(body.message || body.prompt || body.task || body.input?.message || "").trim();
227
 
228
- const messages = Array.isArray(body.messages) ? body.messages : null;
229
-
230
- if (!message && (!messages || messages.length === 0)) {
231
- return json(res, 400, { ok: false, error: "missing_message" });
232
- }
233
 
234
  try {
235
- const result = await executeViaChatCompletions({
236
- message,
237
- messages,
238
- agentId,
239
- sessionKey,
240
- timeoutMs,
241
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  return json(res, 200, {
244
  ok: true,
245
  mode: "hf-openclaw-adapter",
 
 
246
  agentId,
247
  sessionKey,
248
  output: result.text,
249
  text: result.text,
250
  message: result.text,
 
 
 
251
  raw: result.raw,
252
  });
253
  } catch (err) {
254
- return json(res, 502, {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  ok: false,
256
  mode: "hf-openclaw-adapter",
 
 
257
  agentId,
258
  sessionKey,
259
- error: err?.message || String(err),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  });
261
  }
262
  }
263
 
264
- // Alles andere an internes OpenClaw weiterreichen
265
  proxy.web(req, res);
266
  });
267
 
@@ -270,6 +1009,7 @@ server.on("upgrade", (req, socket, head) => {
270
  });
271
 
272
  server.listen(PUBLIC_PORT, "0.0.0.0", () => {
 
273
  console.log(
274
  `[hf-worker-adapter] listening on :${PUBLIC_PORT}, proxying UI/WS to ${INTERNAL_BASE}`
275
  );
 
1
  #!/usr/bin/env node
2
 
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
  import http from "node:http";
6
  import crypto from "node:crypto";
7
  import { URL } from "node:url";
 
11
  const INTERNAL_PORT = Number(process.env.OPENCLAW_INTERNAL_PORT || 18789);
12
  const INTERNAL_BASE = `http://127.0.0.1:${INTERNAL_PORT}`;
13
 
14
+ const STATE_DIR = process.env.OPENCLAW_STATE_DIR || "/app/.openclaw";
15
+ const RUNTIME_CONFIG_PATH = path.join(STATE_DIR, "worker-runtime.json");
16
+ const USAGE_LOG_PATH = path.join(STATE_DIR, "worker-usage.jsonl");
17
+
18
  const GATEWAY_TOKEN = String(
19
  process.env.OPENCLAW_GATEWAY_TOKEN || process.env.OPENCLAW_TOKEN || ""
20
  ).trim();
 
23
  process.env.OPENCLAW_WORKER_TOKEN || ""
24
  ).trim();
25
 
26
+ const ADMIN_TOKEN = String(
27
+ process.env.OPENCLAW_ADMIN_TOKEN || WORKER_TOKEN
28
  ).trim();
29
 
30
+ const GROQ_API_KEY = String(
31
+ process.env.GROQ_API_KEY || ""
32
  ).trim();
33
 
34
+ const HF_PROVIDER_API_KEY = String(
35
+ process.env.OPENCLAW_HUGGINGFACE_API_KEY || process.env.HF_TOKEN || ""
36
+ ).trim();
37
 
38
  if (!GATEWAY_TOKEN) {
39
+ console.error("[hf-worker-adapter] Missing OPENCLAW_GATEWAY_TOKEN / OPENCLAW_TOKEN");
40
  process.exit(1);
41
  }
 
42
  if (!WORKER_TOKEN) {
43
  console.error("[hf-worker-adapter] Missing OPENCLAW_WORKER_TOKEN");
44
  process.exit(1);
45
  }
46
 
47
+ function nowIso() {
48
+ return new Date().toISOString();
49
+ }
50
+
51
+ function ensureState() {
52
+ fs.mkdirSync(STATE_DIR, { recursive: true });
53
+ if (!fs.existsSync(RUNTIME_CONFIG_PATH)) {
54
+ fs.writeFileSync(
55
+ RUNTIME_CONFIG_PATH,
56
+ JSON.stringify(
57
+ {
58
+ version: 1,
59
+ updatedAt: nowIso(),
60
+ defaults: {
61
+ provider: "openclaw",
62
+ agentId: "main",
63
+ model: "",
64
+ sessionKey: "webchat:languageapp",
65
+ timeoutMs: 120000,
66
+ temperature: 0.2,
67
+ maxTokens: null,
68
+ },
69
+ providers: {
70
+ openclaw: {
71
+ enabled: true,
72
+ label: "OpenClaw (internal)",
73
+ models: [{ id: "openclaw:main", provider: "openclaw", label: "OpenClaw Agent: main", agentId: "main" }],
74
+ },
75
+ groq: {
76
+ enabled: false,
77
+ label: "Groq",
78
+ baseUrl: "https://api.groq.com/openai/v1/chat/completions",
79
+ models: [],
80
+ },
81
+ huggingface: {
82
+ enabled: false,
83
+ label: "Hugging Face",
84
+ baseUrl: "https://router.huggingface.co/v1/chat/completions",
85
+ models: [],
86
+ },
87
+ },
88
+ },
89
+ null,
90
+ 2
91
+ ),
92
+ "utf8"
93
+ );
94
+ }
95
+ if (!fs.existsSync(USAGE_LOG_PATH)) {
96
+ fs.writeFileSync(USAGE_LOG_PATH, "", "utf8");
97
+ }
98
+ }
99
+
100
+ function readRuntimeConfig() {
101
+ ensureState();
102
+ try {
103
+ return JSON.parse(fs.readFileSync(RUNTIME_CONFIG_PATH, "utf8"));
104
+ } catch {
105
+ return {
106
+ version: 1,
107
+ updatedAt: nowIso(),
108
+ defaults: {
109
+ provider: "openclaw",
110
+ agentId: "main",
111
+ model: "",
112
+ sessionKey: "webchat:languageapp",
113
+ timeoutMs: 120000,
114
+ temperature: 0.2,
115
+ maxTokens: null,
116
+ },
117
+ providers: {},
118
+ };
119
+ }
120
+ }
121
+
122
+ function writeRuntimeConfig(cfg) {
123
+ cfg.updatedAt = nowIso();
124
+ fs.writeFileSync(RUNTIME_CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf8");
125
+ }
126
+
127
+ function appendUsage(entry) {
128
+ ensureState();
129
+ fs.appendFileSync(USAGE_LOG_PATH, `${JSON.stringify(entry)}\n`, "utf8");
130
+ }
131
+
132
+ function readUsage(limit = 100) {
133
+ ensureState();
134
+ const raw = fs.readFileSync(USAGE_LOG_PATH, "utf8");
135
+ const lines = raw
136
+ .split("\n")
137
+ .map((line) => line.trim())
138
+ .filter(Boolean);
139
+ const rows = lines
140
+ .slice(-Math.max(1, limit))
141
+ .map((line) => {
142
+ try {
143
+ return JSON.parse(line);
144
+ } catch {
145
+ return null;
146
+ }
147
+ })
148
+ .filter(Boolean);
149
+ return rows.reverse();
150
+ }
151
+
152
+ function summarizeUsage(entries) {
153
+ const byProvider = {};
154
+ let successCount = 0;
155
+ let errorCount = 0;
156
+
157
+ for (const row of entries) {
158
+ const provider = row.provider || "unknown";
159
+ byProvider[provider] ??= {
160
+ requests: 0,
161
+ success: 0,
162
+ errors: 0,
163
+ inputTokens: 0,
164
+ outputTokens: 0,
165
+ totalTokens: 0,
166
+ };
167
+
168
+ byProvider[provider].requests += 1;
169
+ byProvider[provider].inputTokens += Number(row.usage?.prompt_tokens || 0);
170
+ byProvider[provider].outputTokens += Number(row.usage?.completion_tokens || 0);
171
+ byProvider[provider].totalTokens += Number(row.usage?.total_tokens || 0);
172
+
173
+ if (row.success) {
174
+ successCount += 1;
175
+ byProvider[provider].success += 1;
176
+ } else {
177
+ errorCount += 1;
178
+ byProvider[provider].errors += 1;
179
+ }
180
+ }
181
+
182
+ return {
183
+ total: entries.length,
184
+ successCount,
185
+ errorCount,
186
+ byProvider,
187
+ };
188
+ }
189
+
190
  function json(res, code, payload) {
191
  const body = JSON.stringify(payload);
192
  res.writeHead(code, {
 
198
  }
199
 
200
  function safeEqual(a, b) {
201
+ const ba = Buffer.from(String(a || ""));
202
+ const bb = Buffer.from(String(b || ""));
203
  return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
204
  }
205
 
206
+ function parseBearer(req) {
207
  const auth = String(req.headers.authorization || "");
208
  const match = auth.match(/^Bearer\s+(.+)$/i);
209
+ return match ? match[1] : "";
210
+ }
211
+
212
+ function requireWorkerBearer(req) {
213
+ const token = parseBearer(req);
214
+ return token && safeEqual(token, WORKER_TOKEN);
215
+ }
216
+
217
+ function requireAdminBearer(req) {
218
+ const token = parseBearer(req);
219
+ return token && safeEqual(token, ADMIN_TOKEN);
220
  }
221
 
222
  function readBody(req) {
 
231
  });
232
  }
233
 
234
+ function normalizeMessages(body, fallbackMessage = "") {
235
+ if (Array.isArray(body.messages) && body.messages.length > 0) return body.messages;
236
+ if (fallbackMessage) return [{ role: "user", content: fallbackMessage }];
237
+ return [];
238
+ }
239
+
240
  function extractTextFromOpenAIResponse(data) {
241
+ const content = data?.choices?.[0]?.message?.content;
 
242
 
243
+ if (typeof content === "string") return content.trim();
244
 
245
  if (Array.isArray(content)) {
246
  return content
247
  .map((part) => {
248
  if (typeof part === "string") return part;
249
+ if (part?.type === "text") return String(part?.text || "");
250
+ return String(part?.text || part?.content || "");
251
  })
252
  .join("")
253
  .trim();
254
  }
255
 
256
+ if (typeof data?.output_text === "string") return data.output_text.trim();
257
+ if (typeof data?.text === "string") return data.text.trim();
258
+ if (typeof data?.message === "string") return data.message.trim();
259
+
260
  return "";
261
  }
262
 
263
+ function extractUsage(data) {
264
+ return {
265
+ prompt_tokens: Number(data?.usage?.prompt_tokens || 0),
266
+ completion_tokens: Number(data?.usage?.completion_tokens || 0),
267
+ total_tokens: Number(data?.usage?.total_tokens || 0),
268
+ };
269
+ }
270
+
271
+ function classifyError({ statusCode, message }) {
272
+ const msg = String(message || "").toLowerCase();
273
+
274
+ if (statusCode === 401 || statusCode === 403) return "auth_error";
275
+ if (statusCode === 402) return "billing_error";
276
+ if (statusCode === 408) return "timeout";
277
+ if (statusCode === 429) return "rate_limit";
278
+ if (msg.includes("depleted your monthly included credits")) return "billing_error";
279
+ if (msg.includes("purchase pre-paid credits")) return "billing_error";
280
+ if (msg.includes("insufficient credits")) return "billing_error";
281
+ if (msg.includes("payment required")) return "billing_error";
282
+ if (msg.includes("aborted")) return "timeout";
283
+ if (msg.includes("timed out")) return "timeout";
284
+ if (msg.includes("network")) return "network_error";
285
+ if (msg.includes("fetch failed")) return "network_error";
286
+ if (msg.includes("econnrefused")) return "network_error";
287
+ if (msg.includes("unexpected schema")) return "unexpected_schema";
288
+ if (msg.includes("invalid_json")) return "bad_response";
289
+ return "failed";
290
+ }
291
+
292
+ function detectProviderFailureInText(text) {
293
+ const t = String(text || "").trim();
294
+ const lower = t.toLowerCase();
295
+ if (!t) return null;
296
+
297
+ if (
298
+ /^402\b/.test(t) ||
299
+ lower.includes("depleted your monthly included credits") ||
300
+ lower.includes("purchase pre-paid credits") ||
301
+ lower.includes("insufficient credits") ||
302
+ lower.includes("payment required")
303
+ ) {
304
+ return {
305
+ kind: "billing_error",
306
+ statusCode: 402,
307
+ message: t,
308
+ };
309
+ }
310
+
311
+ return null;
312
+ }
313
+
314
+ function buildPreview(text, maxLen = 240) {
315
+ const value = String(text || "").replace(/\s+/g, " ").trim();
316
+ return value.length > maxLen ? `${value.slice(0, maxLen)}…` : value;
317
+ }
318
+
319
  async function probeInternalGateway() {
320
  try {
321
  const res = await fetch(`${INTERNAL_BASE}/`, {
 
336
  }
337
  }
338
 
339
+ async function callOpenClawInternal({
 
 
340
  agentId,
341
+ model,
342
  sessionKey,
343
  timeoutMs,
344
+ temperature,
345
+ maxTokens,
346
+ messages,
347
  }) {
348
+ const effectiveModel =
349
+ typeof model === "string" && model.trim().startsWith("openclaw:")
350
+ ? model.trim()
351
+ : `openclaw:${agentId}`;
352
+
353
  const payload = {
354
+ model: effectiveModel,
355
  stream: false,
356
+ messages,
 
 
357
  user: sessionKey,
358
+ temperature,
359
  };
360
 
361
+ if (Number.isFinite(maxTokens) && maxTokens > 0) {
362
+ payload.max_tokens = maxTokens;
363
+ }
364
+
365
  const res = await fetch(`${INTERNAL_BASE}/v1/chat/completions`, {
366
  method: "POST",
367
  headers: {
368
+ authorization: `Bearer ${GATEWAY_TOKEN}`,
369
  "content-type": "application/json",
370
  "x-openclaw-agent-id": agentId,
371
  "x-openclaw-session-key": sessionKey,
 
375
  });
376
 
377
  const rawText = await res.text();
378
+ let data = null;
379
+ try {
380
+ data = rawText ? JSON.parse(rawText) : null;
381
+ } catch {
382
+ data = null;
383
+ }
384
+
385
+ const responseText = extractTextFromOpenAIResponse(data) || rawText || "";
386
+ const embeddedFailure = detectProviderFailureInText(responseText);
387
+
388
+ if (!res.ok) {
389
+ const err = new Error(
390
+ data?.error?.message || rawText || `openclaw_http_${res.status}`
391
+ );
392
+ err.statusCode = res.status;
393
+ err.responseText = responseText;
394
+ throw err;
395
+ }
396
+
397
+ if (embeddedFailure) {
398
+ const err = new Error(embeddedFailure.message);
399
+ err.statusCode = embeddedFailure.statusCode;
400
+ err.responseText = responseText;
401
+ err.errorKind = embeddedFailure.kind;
402
+ throw err;
403
+ }
404
+
405
+ return {
406
+ provider: "openclaw",
407
+ model: effectiveModel,
408
+ upstreamStatus: res.status,
409
+ text: responseText,
410
+ usage: extractUsage(data),
411
+ raw: data,
412
+ };
413
+ }
414
+
415
+ async function callOpenAICompatibleProvider({
416
+ provider,
417
+ baseUrl,
418
+ apiKey,
419
+ model,
420
+ sessionKey,
421
+ timeoutMs,
422
+ temperature,
423
+ maxTokens,
424
+ messages,
425
+ }) {
426
+ const payload = {
427
+ model,
428
+ stream: false,
429
+ messages,
430
+ temperature,
431
+ user: sessionKey,
432
+ };
433
+
434
+ if (Number.isFinite(maxTokens) && maxTokens > 0) {
435
+ payload.max_tokens = maxTokens;
436
+ }
437
+
438
+ const res = await fetch(baseUrl, {
439
+ method: "POST",
440
+ headers: {
441
+ authorization: `Bearer ${apiKey}`,
442
+ "content-type": "application/json",
443
+ },
444
+ body: JSON.stringify(payload),
445
+ signal: AbortSignal.timeout(timeoutMs),
446
+ });
447
 
448
+ const rawText = await res.text();
449
  let data = null;
450
  try {
451
  data = rawText ? JSON.parse(rawText) : null;
 
453
  data = null;
454
  }
455
 
456
+ const responseText = extractTextFromOpenAIResponse(data) || rawText || "";
457
+ const embeddedFailure = detectProviderFailureInText(responseText);
458
+
459
  if (!res.ok) {
460
+ const err = new Error(
461
+ data?.error?.message || responseText || rawText || `${provider}_http_${res.status}`
462
  );
463
+ err.statusCode = res.status;
464
+ err.responseText = responseText || rawText;
465
+ throw err;
466
  }
467
 
468
+ if (embeddedFailure) {
469
+ const err = new Error(embeddedFailure.message);
470
+ err.statusCode = embeddedFailure.statusCode;
471
+ err.responseText = responseText;
472
+ err.errorKind = embeddedFailure.kind;
473
+ throw err;
474
+ }
475
 
476
  return {
477
+ provider,
478
+ model,
479
+ upstreamStatus: res.status,
480
+ text: responseText,
481
+ usage: extractUsage(data),
482
  raw: data,
483
  };
484
  }
485
 
486
+ function selectModelForProvider(runtimeCfg, provider, requestedModel) {
487
+ const byProvider = runtimeCfg.providers?.[provider];
488
+ if (requestedModel && String(requestedModel).trim()) return String(requestedModel).trim();
489
+
490
+ if (runtimeCfg.defaults?.model && runtimeCfg.defaults.provider === provider) {
491
+ return String(runtimeCfg.defaults.model).trim();
492
+ }
493
+
494
+ const firstModel = byProvider?.models?.[0]?.id;
495
+ return firstModel || "";
496
+ }
497
+
498
+ async function executeRequest(body) {
499
+ const cfg = readRuntimeConfig();
500
+
501
+ const provider = String(
502
+ body.provider || cfg.defaults?.provider || "openclaw"
503
+ ).trim();
504
+
505
+ const sessionKey = String(
506
+ body.sessionKey || body.user || cfg.defaults?.sessionKey || "webchat:languageapp"
507
+ ).trim();
508
+
509
+ const agentId = String(
510
+ body.agentId || body.agent || cfg.defaults?.agentId || "main"
511
+ ).trim();
512
+
513
+ const timeoutMs = Number(
514
+ body.timeoutMs || cfg.defaults?.timeoutMs || 120000
515
+ );
516
+
517
+ const temperature = Number.isFinite(Number(body.temperature))
518
+ ? Number(body.temperature)
519
+ : Number(cfg.defaults?.temperature ?? 0.2);
520
+
521
+ const maxTokens =
522
+ body.maxTokens === null
523
+ ? null
524
+ : body.maxTokens !== undefined
525
+ ? Number(body.maxTokens)
526
+ : cfg.defaults?.maxTokens;
527
+
528
+ const message = String(
529
+ body.message || body.prompt || body.task || body.input?.message || ""
530
+ ).trim();
531
+
532
+ const messages = normalizeMessages(body, message);
533
+
534
+ if (!messages.length) {
535
+ const err = new Error("missing_message");
536
+ err.statusCode = 400;
537
+ err.errorKind = "bad_request";
538
+ throw err;
539
+ }
540
+
541
+ const requestedModel = String(body.model || "").trim();
542
+ const effectiveModel = selectModelForProvider(cfg, provider, requestedModel);
543
+
544
+ if (provider === "openclaw") {
545
+ return callOpenClawInternal({
546
+ agentId,
547
+ model: effectiveModel,
548
+ sessionKey,
549
+ timeoutMs,
550
+ temperature,
551
+ maxTokens,
552
+ messages,
553
+ });
554
+ }
555
+
556
+ if (provider === "groq") {
557
+ if (!GROQ_API_KEY) {
558
+ const err = new Error("groq_api_key_missing");
559
+ err.statusCode = 500;
560
+ err.errorKind = "misconfiguration";
561
+ throw err;
562
+ }
563
+ return callOpenAICompatibleProvider({
564
+ provider: "groq",
565
+ baseUrl:
566
+ cfg.providers?.groq?.baseUrl ||
567
+ "https://api.groq.com/openai/v1/chat/completions",
568
+ apiKey: GROQ_API_KEY,
569
+ model: effectiveModel,
570
+ sessionKey,
571
+ timeoutMs,
572
+ temperature,
573
+ maxTokens,
574
+ messages,
575
+ });
576
+ }
577
+
578
+ if (provider === "huggingface") {
579
+ if (!HF_PROVIDER_API_KEY) {
580
+ const err = new Error("huggingface_api_key_missing");
581
+ err.statusCode = 500;
582
+ err.errorKind = "misconfiguration";
583
+ throw err;
584
+ }
585
+ return callOpenAICompatibleProvider({
586
+ provider: "huggingface",
587
+ baseUrl:
588
+ cfg.providers?.huggingface?.baseUrl ||
589
+ "https://router.huggingface.co/v1/chat/completions",
590
+ apiKey: HF_PROVIDER_API_KEY,
591
+ model: effectiveModel,
592
+ sessionKey,
593
+ timeoutMs,
594
+ temperature,
595
+ maxTokens,
596
+ messages,
597
+ });
598
+ }
599
+
600
+ const err = new Error(`unsupported_provider:${provider}`);
601
+ err.statusCode = 400;
602
+ err.errorKind = "bad_request";
603
+ throw err;
604
+ }
605
+
606
+ function usageEntryBase({
607
+ requestId,
608
+ provider,
609
+ model,
610
+ agentId,
611
+ sessionKey,
612
+ source,
613
+ operation,
614
+ startedAt,
615
+ }) {
616
+ return {
617
+ requestId,
618
+ timestamp: startedAt,
619
+ provider,
620
+ model,
621
+ agentId,
622
+ sessionKey,
623
+ source,
624
+ operation,
625
+ };
626
+ }
627
+
628
+ // Proxy für OpenClaw UI / WS
629
  const proxy = httpProxy.createProxyServer({
630
  target: INTERNAL_BASE,
631
  ws: true,
 
643
  });
644
 
645
  const server = http.createServer(async (req, res) => {
646
+ ensureState();
647
+
648
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
649
 
650
+ // --- Worker health --------------------------------------------------------
651
  if (req.method === "GET" && url.pathname === "/api/openclaw/health") {
652
+ if (!requireWorkerBearer(req)) {
653
  return json(res, 401, { ok: false, error: "unauthorized" });
654
  }
655
 
656
  const probe = await probeInternalGateway();
657
+ const runtimeCfg = readRuntimeConfig();
658
 
659
  return json(res, probe.reachable ? 200 : 502, {
660
  ok: probe.reachable,
 
664
  detail: probe.reachable
665
  ? `internal gateway reachable (HTTP ${probe.statusCode})`
666
  : probe.error || "internal gateway unreachable",
667
+ defaults: runtimeCfg.defaults,
668
+ providers: Object.fromEntries(
669
+ Object.entries(runtimeCfg.providers || {}).map(([key, value]) => [
670
+ key,
671
+ {
672
+ enabled: Boolean(value?.enabled),
673
+ modelCount: Array.isArray(value?.models) ? value.models.length : 0,
674
+ },
675
+ ])
676
+ ),
677
  });
678
  }
679
 
680
+ // --- Worker execute -------------------------------------------------------
681
  if (req.method === "POST" && url.pathname === "/api/openclaw/tasks/execute") {
682
+ if (!requireWorkerBearer(req)) {
683
+ return json(res, 401, { ok: false, error: "unauthorized", errorKind: "auth_error" });
684
  }
685
 
686
  let body = {};
 
688
  const raw = await readBody(req);
689
  body = raw ? JSON.parse(raw) : {};
690
  } catch {
691
+ return json(res, 400, { ok: false, error: "invalid_json", errorKind: "bad_request" });
692
  }
693
 
694
+ const runtimeCfg = readRuntimeConfig();
695
+ const provider = String(body.provider || runtimeCfg.defaults?.provider || "openclaw").trim();
696
+ const model = String(body.model || selectModelForProvider(runtimeCfg, provider, "") || "").trim();
697
+ const agentId = String(body.agentId || body.agent || runtimeCfg.defaults?.agentId || "main").trim();
698
+ const sessionKey = String(body.sessionKey || body.user || runtimeCfg.defaults?.sessionKey || "webchat:languageapp").trim();
 
699
 
700
+ const requestId = crypto.randomUUID();
701
+ const startedAt = nowIso();
702
+ const startedPerf = Date.now();
 
 
703
 
704
  try {
705
+ const result = await executeRequest(body);
706
+ const latencyMs = Date.now() - startedPerf;
707
+ const preview = buildPreview(result.text);
708
+
709
+ const entry = {
710
+ ...usageEntryBase({
711
+ requestId,
712
+ provider: result.provider,
713
+ model: result.model,
714
+ agentId,
715
+ sessionKey,
716
+ source: "external-worker",
717
+ operation: "execute",
718
+ startedAt,
719
+ }),
720
+ finishedAt: nowIso(),
721
+ success: true,
722
+ statusCode: result.upstreamStatus,
723
+ latencyMs,
724
+ errorKind: null,
725
+ errorMessage: null,
726
+ preview,
727
+ usage: result.usage,
728
+ };
729
+ appendUsage(entry);
730
 
731
  return json(res, 200, {
732
  ok: true,
733
  mode: "hf-openclaw-adapter",
734
+ provider: result.provider,
735
+ model: result.model,
736
  agentId,
737
  sessionKey,
738
  output: result.text,
739
  text: result.text,
740
  message: result.text,
741
+ usage: result.usage,
742
+ requestId,
743
+ latencyMs,
744
  raw: result.raw,
745
  });
746
  } catch (err) {
747
+ const latencyMs = Date.now() - startedPerf;
748
+ const statusCode = Number(err?.statusCode || 502);
749
+ const errorMessage = String(err?.message || err || "unknown_error");
750
+ const errorKind = String(err?.errorKind || classifyError({
751
+ statusCode,
752
+ message: errorMessage,
753
+ }));
754
+
755
+ const entry = {
756
+ ...usageEntryBase({
757
+ requestId,
758
+ provider,
759
+ model,
760
+ agentId,
761
+ sessionKey,
762
+ source: "external-worker",
763
+ operation: "execute",
764
+ startedAt,
765
+ }),
766
+ finishedAt: nowIso(),
767
+ success: false,
768
+ statusCode,
769
+ latencyMs,
770
+ errorKind,
771
+ errorMessage,
772
+ preview: buildPreview(err?.responseText || errorMessage),
773
+ usage: {
774
+ prompt_tokens: 0,
775
+ completion_tokens: 0,
776
+ total_tokens: 0,
777
+ },
778
+ };
779
+ appendUsage(entry);
780
+
781
+ return json(res, statusCode >= 400 && statusCode <= 599 ? statusCode : 502, {
782
  ok: false,
783
  mode: "hf-openclaw-adapter",
784
+ provider,
785
+ model,
786
  agentId,
787
  sessionKey,
788
+ requestId,
789
+ latencyMs,
790
+ error: errorMessage,
791
+ errorKind,
792
+ preview: buildPreview(err?.responseText || errorMessage),
793
+ });
794
+ }
795
+ }
796
+
797
+ // --- Admin: config --------------------------------------------------------
798
+ if (req.method === "GET" && url.pathname === "/api/openclaw/admin/config") {
799
+ if (!requireAdminBearer(req)) {
800
+ return json(res, 401, { ok: false, error: "unauthorized" });
801
+ }
802
+
803
+ return json(res, 200, {
804
+ ok: true,
805
+ config: readRuntimeConfig(),
806
+ });
807
+ }
808
+
809
+ if (req.method === "PUT" && url.pathname === "/api/openclaw/admin/config") {
810
+ if (!requireAdminBearer(req)) {
811
+ return json(res, 401, { ok: false, error: "unauthorized" });
812
+ }
813
+
814
+ let body = {};
815
+ try {
816
+ const raw = await readBody(req);
817
+ body = raw ? JSON.parse(raw) : {};
818
+ } catch {
819
+ return json(res, 400, { ok: false, error: "invalid_json" });
820
+ }
821
+
822
+ const cfg = readRuntimeConfig();
823
+
824
+ const nextDefaults = body.defaults || {};
825
+ const allowedProviders = ["openclaw", "groq", "huggingface"];
826
+ const nextProvider = String(nextDefaults.provider || cfg.defaults.provider || "openclaw").trim();
827
+
828
+ if (!allowedProviders.includes(nextProvider)) {
829
+ return json(res, 400, { ok: false, error: "invalid_provider" });
830
+ }
831
+
832
+ cfg.defaults.provider = nextProvider;
833
+ cfg.defaults.agentId = String(nextDefaults.agentId || cfg.defaults.agentId || "main").trim();
834
+ cfg.defaults.model = String(nextDefaults.model || cfg.defaults.model || "").trim();
835
+ cfg.defaults.sessionKey = String(nextDefaults.sessionKey || cfg.defaults.sessionKey || "webchat:languageapp").trim();
836
+
837
+ if (nextDefaults.timeoutMs !== undefined) {
838
+ const timeoutMs = Number(nextDefaults.timeoutMs);
839
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
840
+ return json(res, 400, { ok: false, error: "invalid_timeoutMs" });
841
+ }
842
+ cfg.defaults.timeoutMs = timeoutMs;
843
+ }
844
+
845
+ if (nextDefaults.temperature !== undefined) {
846
+ const temperature = Number(nextDefaults.temperature);
847
+ if (!Number.isFinite(temperature)) {
848
+ return json(res, 400, { ok: false, error: "invalid_temperature" });
849
+ }
850
+ cfg.defaults.temperature = temperature;
851
+ }
852
+
853
+ if (nextDefaults.maxTokens !== undefined) {
854
+ if (nextDefaults.maxTokens === null || nextDefaults.maxTokens === "") {
855
+ cfg.defaults.maxTokens = null;
856
+ } else {
857
+ const maxTokens = Number(nextDefaults.maxTokens);
858
+ if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
859
+ return json(res, 400, { ok: false, error: "invalid_maxTokens" });
860
+ }
861
+ cfg.defaults.maxTokens = maxTokens;
862
+ }
863
+ }
864
+
865
+ if (body.providers && typeof body.providers === "object") {
866
+ for (const [provider, patch] of Object.entries(body.providers)) {
867
+ if (!cfg.providers?.[provider]) continue;
868
+ if (patch?.enabled !== undefined) {
869
+ cfg.providers[provider].enabled = Boolean(patch.enabled);
870
+ }
871
+ if (typeof patch?.baseUrl === "string" && patch.baseUrl.trim()) {
872
+ cfg.providers[provider].baseUrl = patch.baseUrl.trim();
873
+ }
874
+ }
875
+ }
876
+
877
+ writeRuntimeConfig(cfg);
878
+
879
+ return json(res, 200, {
880
+ ok: true,
881
+ config: cfg,
882
+ });
883
+ }
884
+
885
+ // --- Admin: models --------------------------------------------------------
886
+ if (req.method === "GET" && url.pathname === "/api/openclaw/admin/models") {
887
+ if (!requireAdminBearer(req)) {
888
+ return json(res, 401, { ok: false, error: "unauthorized" });
889
+ }
890
+
891
+ const cfg = readRuntimeConfig();
892
+ const models = Object.entries(cfg.providers || {})
893
+ .flatMap(([provider, value]) =>
894
+ Array.isArray(value?.models)
895
+ ? value.models.map((model) => ({
896
+ provider,
897
+ enabled: Boolean(value?.enabled),
898
+ ...model,
899
+ }))
900
+ : []
901
+ );
902
+
903
+ return json(res, 200, {
904
+ ok: true,
905
+ models,
906
+ });
907
+ }
908
+
909
+ // --- Admin: providers -----------------------------------------------------
910
+ if (req.method === "GET" && url.pathname === "/api/openclaw/admin/providers") {
911
+ if (!requireAdminBearer(req)) {
912
+ return json(res, 401, { ok: false, error: "unauthorized" });
913
+ }
914
+
915
+ const cfg = readRuntimeConfig();
916
+
917
+ return json(res, 200, {
918
+ ok: true,
919
+ providers: {
920
+ openclaw: {
921
+ enabled: Boolean(cfg.providers?.openclaw?.enabled),
922
+ label: cfg.providers?.openclaw?.label || "OpenClaw (internal)",
923
+ authConfigured: Boolean(GATEWAY_TOKEN),
924
+ baseUrl: INTERNAL_BASE,
925
+ },
926
+ groq: {
927
+ enabled: Boolean(cfg.providers?.groq?.enabled),
928
+ label: cfg.providers?.groq?.label || "Groq",
929
+ authConfigured: Boolean(GROQ_API_KEY),
930
+ baseUrl: cfg.providers?.groq?.baseUrl || "",
931
+ },
932
+ huggingface: {
933
+ enabled: Boolean(cfg.providers?.huggingface?.enabled),
934
+ label: cfg.providers?.huggingface?.label || "Hugging Face",
935
+ authConfigured: Boolean(HF_PROVIDER_API_KEY),
936
+ baseUrl: cfg.providers?.huggingface?.baseUrl || "",
937
+ },
938
+ },
939
+ });
940
+ }
941
+
942
+ // --- Admin: usage ---------------------------------------------------------
943
+ if (req.method === "GET" && url.pathname === "/api/openclaw/admin/usage") {
944
+ if (!requireAdminBearer(req)) {
945
+ return json(res, 401, { ok: false, error: "unauthorized" });
946
+ }
947
+
948
+ const limit = Math.min(500, Math.max(1, Number(url.searchParams.get("limit") || 100)));
949
+ const rows = readUsage(limit);
950
+
951
+ return json(res, 200, {
952
+ ok: true,
953
+ rows,
954
+ summary: summarizeUsage(rows),
955
+ });
956
+ }
957
+
958
+ // --- Admin: probe ---------------------------------------------------------
959
+ if (req.method === "POST" && url.pathname === "/api/openclaw/admin/probe") {
960
+ if (!requireAdminBearer(req)) {
961
+ return json(res, 401, { ok: false, error: "unauthorized" });
962
+ }
963
+
964
+ let body = {};
965
+ try {
966
+ const raw = await readBody(req);
967
+ body = raw ? JSON.parse(raw) : {};
968
+ } catch {
969
+ body = {};
970
+ }
971
+
972
+ try {
973
+ const result = await executeRequest({
974
+ provider: body.provider,
975
+ model: body.model,
976
+ agentId: body.agentId || "main",
977
+ sessionKey: body.sessionKey || "webchat:languageapp",
978
+ timeoutMs: body.timeoutMs || 120000,
979
+ message: 'Antworte exakt mit OPENCLAW_RUNTIME_PROBE_OK und sonst nichts.',
980
+ });
981
+
982
+ const ok = String(result.text || "").trim() === "OPENCLAW_RUNTIME_PROBE_OK";
983
+
984
+ return json(res, ok ? 200 : 502, {
985
+ ok,
986
+ provider: result.provider,
987
+ model: result.model,
988
+ text: result.text,
989
+ usage: result.usage,
990
+ });
991
+ } catch (err) {
992
+ return json(res, Number(err?.statusCode || 502), {
993
+ ok: false,
994
+ error: String(err?.message || err),
995
+ errorKind: String(err?.errorKind || classifyError({
996
+ statusCode: Number(err?.statusCode || 502),
997
+ message: String(err?.message || err),
998
+ })),
999
  });
1000
  }
1001
  }
1002
 
1003
+ // --- Alles andere -> OpenClaw UI / WS ------------------------------------
1004
  proxy.web(req, res);
1005
  });
1006
 
 
1009
  });
1010
 
1011
  server.listen(PUBLIC_PORT, "0.0.0.0", () => {
1012
+ ensureState();
1013
  console.log(
1014
  `[hf-worker-adapter] listening on :${PUBLIC_PORT}, proxying UI/WS to ${INTERNAL_BASE}`
1015
  );