clawmux 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -13
- package/dist/cli.cjs +3768 -112
- package/dist/index.cjs +235 -34
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -79,8 +79,8 @@ async function writeWebResponse(res, response) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// src/cli.ts
|
|
82
|
-
var
|
|
83
|
-
var
|
|
82
|
+
var import_promises6 = require("node:fs/promises");
|
|
83
|
+
var import_node_path6 = require("node:path");
|
|
84
84
|
var import_node_child_process = require("node:child_process");
|
|
85
85
|
var import_node_os = require("node:os");
|
|
86
86
|
|
|
@@ -159,98 +159,3719 @@ async function parseJsonBody(req) {
|
|
|
159
159
|
return { body: JSON.parse(text), error: null };
|
|
160
160
|
} catch {
|
|
161
161
|
return {
|
|
162
|
-
body: null,
|
|
163
|
-
error: jsonResponse({ error: "invalid JSON body" }, 400)
|
|
162
|
+
body: null,
|
|
163
|
+
error: jsonResponse({ error: "invalid JSON body" }, 400)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function dispatch(req) {
|
|
168
|
+
const url = new URL(req.url);
|
|
169
|
+
const { pathname } = url;
|
|
170
|
+
const method = req.method.toUpperCase();
|
|
171
|
+
for (const route of routes) {
|
|
172
|
+
if (route.method === method && route.match(pathname)) {
|
|
173
|
+
const handler = customHandlers.get(route.key) ?? route.handler;
|
|
174
|
+
if (method === "POST") {
|
|
175
|
+
const { body, error } = await parseJsonBody(req);
|
|
176
|
+
if (error)
|
|
177
|
+
return error;
|
|
178
|
+
return handler(req, body);
|
|
179
|
+
}
|
|
180
|
+
return handler(req, null);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return jsonResponse({ error: "not found" }, 404);
|
|
184
|
+
}
|
|
185
|
+
function setRouteHandler(path, handler) {
|
|
186
|
+
customHandlers.set(path, handler);
|
|
187
|
+
}
|
|
188
|
+
function clearCustomHandlers() {
|
|
189
|
+
customHandlers.clear();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/utils/runtime.ts
|
|
193
|
+
var import_promises = require("node:fs/promises");
|
|
194
|
+
var isBun = typeof globalThis.Bun !== "undefined";
|
|
195
|
+
async function readFileText(path) {
|
|
196
|
+
if (isBun) {
|
|
197
|
+
const bun = globalThis.Bun;
|
|
198
|
+
return bun.file(path).text();
|
|
199
|
+
}
|
|
200
|
+
return import_promises.readFile(path, "utf-8");
|
|
201
|
+
}
|
|
202
|
+
async function fileExists(path) {
|
|
203
|
+
if (isBun) {
|
|
204
|
+
const bun = globalThis.Bun;
|
|
205
|
+
return bun.file(path).exists();
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
await import_promises.access(path);
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/proxy/server.ts
|
|
216
|
+
function createServer(config) {
|
|
217
|
+
if (isBun) {
|
|
218
|
+
return createBunServer(config);
|
|
219
|
+
}
|
|
220
|
+
return createNodeServer(config);
|
|
221
|
+
}
|
|
222
|
+
function createBunServer(config) {
|
|
223
|
+
let server = null;
|
|
224
|
+
return {
|
|
225
|
+
start() {
|
|
226
|
+
if (server)
|
|
227
|
+
return;
|
|
228
|
+
const bun = globalThis.Bun;
|
|
229
|
+
server = bun.serve({
|
|
230
|
+
port: config.port,
|
|
231
|
+
hostname: config.host,
|
|
232
|
+
fetch: dispatch
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
stop() {
|
|
236
|
+
if (!server)
|
|
237
|
+
return;
|
|
238
|
+
server.stop(true);
|
|
239
|
+
server = null;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function createNodeServer(config) {
|
|
244
|
+
let server = null;
|
|
245
|
+
return {
|
|
246
|
+
async start() {
|
|
247
|
+
if (server)
|
|
248
|
+
return;
|
|
249
|
+
const { createServer: createHttpServer } = await import("node:http");
|
|
250
|
+
const { toWebRequest: toWebRequest2, writeWebResponse: writeWebResponse2 } = await Promise.resolve().then(() => exports_node_http_adapter);
|
|
251
|
+
const httpServer = createHttpServer(async (req, res) => {
|
|
252
|
+
try {
|
|
253
|
+
const webReq = toWebRequest2(req);
|
|
254
|
+
const webRes = await dispatch(webReq);
|
|
255
|
+
await writeWebResponse2(res, webRes);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
258
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
259
|
+
res.end(JSON.stringify({ error: message }));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
await new Promise((resolve) => {
|
|
263
|
+
httpServer.listen(config.port, config.host, resolve);
|
|
264
|
+
});
|
|
265
|
+
server = httpServer;
|
|
266
|
+
},
|
|
267
|
+
stop() {
|
|
268
|
+
if (!server)
|
|
269
|
+
return;
|
|
270
|
+
server.close();
|
|
271
|
+
server = null;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/config/loader.ts
|
|
277
|
+
var import_promises2 = require("node:fs/promises");
|
|
278
|
+
var import_node_path = require("node:path");
|
|
279
|
+
|
|
280
|
+
// src/config/defaults.ts
|
|
281
|
+
var DEFAULT_CONFIG = {
|
|
282
|
+
compression: {
|
|
283
|
+
threshold: 0.75,
|
|
284
|
+
model: "",
|
|
285
|
+
targetRatio: 0.6
|
|
286
|
+
},
|
|
287
|
+
routing: {
|
|
288
|
+
models: {
|
|
289
|
+
LIGHT: "",
|
|
290
|
+
MEDIUM: "",
|
|
291
|
+
HEAVY: ""
|
|
292
|
+
},
|
|
293
|
+
contextWindows: {}
|
|
294
|
+
},
|
|
295
|
+
server: {
|
|
296
|
+
port: 3456,
|
|
297
|
+
host: "127.0.0.1"
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
function applyDefaults(partial) {
|
|
301
|
+
const defaults = DEFAULT_CONFIG;
|
|
302
|
+
return {
|
|
303
|
+
compression: {
|
|
304
|
+
threshold: partial.compression.threshold ?? defaults.compression.threshold,
|
|
305
|
+
model: partial.compression.model ?? defaults.compression.model,
|
|
306
|
+
targetRatio: partial.compression.targetRatio ?? defaults.compression.targetRatio
|
|
307
|
+
},
|
|
308
|
+
routing: {
|
|
309
|
+
models: {
|
|
310
|
+
LIGHT: partial.routing.models.LIGHT ?? defaults.routing.models.LIGHT,
|
|
311
|
+
MEDIUM: partial.routing.models.MEDIUM ?? defaults.routing.models.MEDIUM,
|
|
312
|
+
HEAVY: partial.routing.models.HEAVY ?? defaults.routing.models.HEAVY
|
|
313
|
+
},
|
|
314
|
+
contextWindows: { ...defaults.routing.contextWindows, ...partial.routing.contextWindows }
|
|
315
|
+
},
|
|
316
|
+
server: {
|
|
317
|
+
port: partial.server?.port ?? defaults.server.port,
|
|
318
|
+
host: partial.server?.host ?? defaults.server.host
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/config/validator.ts
|
|
324
|
+
function isObject(value) {
|
|
325
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
326
|
+
}
|
|
327
|
+
function requireNumber(errors, path, value) {
|
|
328
|
+
if (typeof value !== "number") {
|
|
329
|
+
errors.push(`${path}: must be a number, got ${typeof value}`);
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
function requireString(errors, path, value) {
|
|
335
|
+
if (typeof value !== "string") {
|
|
336
|
+
errors.push(`${path}: must be a string, got ${typeof value}`);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
function requireInRange(errors, path, value, min, max) {
|
|
342
|
+
if (value < min || value > max) {
|
|
343
|
+
errors.push(`${path}: must be between ${min} and ${max}, got ${value}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function checkRequiredString(errors, errorPath, obj, key) {
|
|
347
|
+
if (!isObject(obj)) {
|
|
348
|
+
errors.push(`${errorPath}: is required`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const value = obj[key];
|
|
352
|
+
if (typeof value !== "string" || value === "") {
|
|
353
|
+
errors.push(`${errorPath}: is required`);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
function checkProviderModelFormat(errors, path, model) {
|
|
359
|
+
if (!model.includes("/")) {
|
|
360
|
+
errors.push(`${path} must be in 'provider/model' format (e.g., 'anthropic/claude-sonnet-4-20250514')`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const providerName = model.split("/", 2)[0];
|
|
364
|
+
if (providerName.toLowerCase().startsWith("clawmux-")) {
|
|
365
|
+
errors.push(`Self-referencing model detected: ${model}. This would cause an infinite routing loop.`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function checkOptionalNumberRange(errors, path, value, min, max) {
|
|
369
|
+
if (requireNumber(errors, path, value)) {
|
|
370
|
+
requireInRange(errors, path, value, min, max);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function validateConfig(raw) {
|
|
374
|
+
const errors = [];
|
|
375
|
+
const obj = isObject(raw) ? raw : {};
|
|
376
|
+
const compression = isObject(obj.compression) ? obj.compression : {};
|
|
377
|
+
const threshold = compression.threshold;
|
|
378
|
+
if (threshold === undefined) {
|
|
379
|
+
errors.push("compression.threshold: is required");
|
|
380
|
+
} else {
|
|
381
|
+
checkOptionalNumberRange(errors, "compression.threshold", threshold, 0.1, 0.95);
|
|
382
|
+
}
|
|
383
|
+
if (compression.model === undefined || compression.model === "") {
|
|
384
|
+
errors.push("compression.model: is required");
|
|
385
|
+
} else if (requireString(errors, "compression.model", compression.model)) {
|
|
386
|
+
checkProviderModelFormat(errors, "compression.model", compression.model);
|
|
387
|
+
}
|
|
388
|
+
if (compression.targetRatio !== undefined) {
|
|
389
|
+
checkOptionalNumberRange(errors, "compression.targetRatio", compression.targetRatio, 0.2, 0.9);
|
|
390
|
+
}
|
|
391
|
+
const routing = isObject(obj.routing) ? obj.routing : {};
|
|
392
|
+
const models = isObject(routing.models) ? routing.models : {};
|
|
393
|
+
const light = checkRequiredString(errors, "routing.models.LIGHT", models, "LIGHT");
|
|
394
|
+
const medium = checkRequiredString(errors, "routing.models.MEDIUM", models, "MEDIUM");
|
|
395
|
+
const heavy = checkRequiredString(errors, "routing.models.HEAVY", models, "HEAVY");
|
|
396
|
+
if (light)
|
|
397
|
+
checkProviderModelFormat(errors, "routing.models.LIGHT", light);
|
|
398
|
+
if (medium)
|
|
399
|
+
checkProviderModelFormat(errors, "routing.models.MEDIUM", medium);
|
|
400
|
+
if (heavy)
|
|
401
|
+
checkProviderModelFormat(errors, "routing.models.HEAVY", heavy);
|
|
402
|
+
if (routing.contextWindows !== undefined) {
|
|
403
|
+
if (!isObject(routing.contextWindows)) {
|
|
404
|
+
errors.push("routing.contextWindows: must be an object");
|
|
405
|
+
} else {
|
|
406
|
+
for (const [key, value] of Object.entries(routing.contextWindows)) {
|
|
407
|
+
if (typeof key !== "string") {
|
|
408
|
+
errors.push(`routing.contextWindows: keys must be strings`);
|
|
409
|
+
}
|
|
410
|
+
if (typeof value !== "number" || value <= 0) {
|
|
411
|
+
errors.push(`routing.contextWindows["${key}"]: must be a positive number, got ${String(value)}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const server = obj.server !== undefined && isObject(obj.server) ? obj.server : null;
|
|
417
|
+
if (server !== null && server.port !== undefined) {
|
|
418
|
+
checkOptionalNumberRange(errors, "server.port", server.port, 1024, 65535);
|
|
419
|
+
}
|
|
420
|
+
if (errors.length > 0) {
|
|
421
|
+
return { valid: false, errors };
|
|
422
|
+
}
|
|
423
|
+
return { valid: true, config: applyDefaults(obj) };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/config/loader.ts
|
|
427
|
+
function findConfigPath() {
|
|
428
|
+
const envPath = process.env.CLAWMUX_CONFIG;
|
|
429
|
+
if (envPath) {
|
|
430
|
+
return import_node_path.resolve(envPath);
|
|
431
|
+
}
|
|
432
|
+
return import_node_path.resolve(process.cwd(), "clawmux.json");
|
|
433
|
+
}
|
|
434
|
+
async function loadConfig(configPath) {
|
|
435
|
+
const filePath = configPath ?? findConfigPath();
|
|
436
|
+
let raw;
|
|
437
|
+
try {
|
|
438
|
+
raw = await import_promises2.readFile(filePath, "utf-8");
|
|
439
|
+
} catch (err) {
|
|
440
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
441
|
+
return {
|
|
442
|
+
valid: false,
|
|
443
|
+
errors: [`Failed to read config file at ${filePath}: ${message}`]
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
let parsed;
|
|
447
|
+
try {
|
|
448
|
+
parsed = JSON.parse(raw);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
451
|
+
return {
|
|
452
|
+
valid: false,
|
|
453
|
+
errors: [`Invalid JSON in config file ${filePath}: ${message}`]
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
return validateConfig(parsed);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/config/watcher.ts
|
|
460
|
+
var import_node_fs = require("node:fs");
|
|
461
|
+
var import_promises3 = require("node:fs/promises");
|
|
462
|
+
function createConfigWatcher(configPath, onReload, options) {
|
|
463
|
+
const debounceMs = options?.debounceMs ?? 2000;
|
|
464
|
+
let watcher = null;
|
|
465
|
+
let debounceTimer = null;
|
|
466
|
+
let reloading = false;
|
|
467
|
+
let pendingReload = false;
|
|
468
|
+
async function reloadConfig() {
|
|
469
|
+
if (reloading) {
|
|
470
|
+
pendingReload = true;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
reloading = true;
|
|
474
|
+
try {
|
|
475
|
+
let raw;
|
|
476
|
+
try {
|
|
477
|
+
raw = await import_promises3.readFile(configPath, "utf-8");
|
|
478
|
+
} catch {
|
|
479
|
+
console.warn(`[config] Config file ${configPath} not found, keeping old config`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
let parsed;
|
|
483
|
+
try {
|
|
484
|
+
parsed = JSON.parse(raw);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
487
|
+
console.warn(`[config] Invalid JSON in config change, ignored: ${message}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const result = validateConfig(parsed);
|
|
491
|
+
if (result.valid) {
|
|
492
|
+
onReload(result.config);
|
|
493
|
+
console.log("[config] Reloaded clawmux.json");
|
|
494
|
+
} else {
|
|
495
|
+
console.warn(`[config] Invalid config change ignored: ${result.errors.join(", ")}`);
|
|
496
|
+
}
|
|
497
|
+
} finally {
|
|
498
|
+
reloading = false;
|
|
499
|
+
if (pendingReload) {
|
|
500
|
+
pendingReload = false;
|
|
501
|
+
scheduleReload();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function scheduleReload() {
|
|
506
|
+
if (debounceTimer !== null) {
|
|
507
|
+
clearTimeout(debounceTimer);
|
|
508
|
+
}
|
|
509
|
+
debounceTimer = setTimeout(() => {
|
|
510
|
+
debounceTimer = null;
|
|
511
|
+
reloadConfig();
|
|
512
|
+
}, debounceMs);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
start() {
|
|
516
|
+
if (watcher !== null)
|
|
517
|
+
return;
|
|
518
|
+
watcher = import_node_fs.watch(configPath, (eventType) => {
|
|
519
|
+
if (eventType === "rename") {
|
|
520
|
+
console.warn(`[config] Config file ${configPath} was deleted, keeping old config`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
scheduleReload();
|
|
524
|
+
});
|
|
525
|
+
watcher.on("error", (err) => {
|
|
526
|
+
console.warn(`[config] Watcher error: ${err instanceof Error ? err.message : String(err)}`);
|
|
527
|
+
});
|
|
528
|
+
},
|
|
529
|
+
stop() {
|
|
530
|
+
if (debounceTimer !== null) {
|
|
531
|
+
clearTimeout(debounceTimer);
|
|
532
|
+
debounceTimer = null;
|
|
533
|
+
}
|
|
534
|
+
if (watcher !== null) {
|
|
535
|
+
watcher.close();
|
|
536
|
+
watcher = null;
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
isWatching() {
|
|
540
|
+
return watcher !== null;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/openclaw/config-reader.ts
|
|
546
|
+
var import_promises4 = require("node:fs/promises");
|
|
547
|
+
var import_node_path2 = require("node:path");
|
|
548
|
+
var ENV_VAR_PATTERN = /^\$\{([^}]+)\}$/;
|
|
549
|
+
function resolveEnvVar(value) {
|
|
550
|
+
const match = value.match(ENV_VAR_PATTERN);
|
|
551
|
+
if (match) {
|
|
552
|
+
return process.env[match[1]] ?? "";
|
|
553
|
+
}
|
|
554
|
+
return value;
|
|
555
|
+
}
|
|
556
|
+
function getHomeDir() {
|
|
557
|
+
return process.env.HOME ?? "/root";
|
|
558
|
+
}
|
|
559
|
+
function getConfigPath(override) {
|
|
560
|
+
if (override)
|
|
561
|
+
return override;
|
|
562
|
+
if (process.env.OPENCLAW_CONFIG_PATH)
|
|
563
|
+
return process.env.OPENCLAW_CONFIG_PATH;
|
|
564
|
+
return import_node_path2.join(getHomeDir(), ".openclaw", "openclaw.json");
|
|
565
|
+
}
|
|
566
|
+
async function readOpenClawConfig(configPath) {
|
|
567
|
+
const path = getConfigPath(configPath);
|
|
568
|
+
let text;
|
|
569
|
+
try {
|
|
570
|
+
text = await import_promises4.readFile(path, "utf-8");
|
|
571
|
+
} catch (err) {
|
|
572
|
+
const code = err.code;
|
|
573
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
574
|
+
throw new Error(`openclaw.json not found at ${path}. Ensure OpenClaw is installed.`);
|
|
575
|
+
}
|
|
576
|
+
throw err;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
return JSON.parse(text);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
582
|
+
throw new Error(`Failed to parse openclaw.json: ${message}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function getAuthProfilesPath(agentId) {
|
|
586
|
+
const id = agentId ?? "main";
|
|
587
|
+
return import_node_path2.join(getHomeDir(), ".openclaw", "agents", id, "agent", "auth-profiles.json");
|
|
588
|
+
}
|
|
589
|
+
function parseAuthProfilesFile(text) {
|
|
590
|
+
try {
|
|
591
|
+
const parsed = JSON.parse(text);
|
|
592
|
+
if (Array.isArray(parsed))
|
|
593
|
+
return parsed;
|
|
594
|
+
if (parsed && typeof parsed === "object" && parsed.profiles) {
|
|
595
|
+
return Object.entries(parsed.profiles).map(([key, profile]) => ({
|
|
596
|
+
provider: profile.provider ?? key.split(":")[0],
|
|
597
|
+
apiKey: profile.access ?? profile.apiKey ?? profile.key,
|
|
598
|
+
token: profile.token
|
|
599
|
+
})).filter((p) => {
|
|
600
|
+
const token = p.apiKey ?? p.token;
|
|
601
|
+
if (!token || !token.includes("."))
|
|
602
|
+
return true;
|
|
603
|
+
try {
|
|
604
|
+
const payload = token.split(".")[1];
|
|
605
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64").toString());
|
|
606
|
+
if (decoded.exp && decoded.exp * 1000 < Date.now())
|
|
607
|
+
return false;
|
|
608
|
+
} catch (_) {
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return [];
|
|
615
|
+
} catch (err) {
|
|
616
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
617
|
+
throw new Error(`Failed to parse auth-profiles.json: ${message}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function readAuthProfiles(_agentId, _profilesPath, agentsDirOverride) {
|
|
621
|
+
const agentsDir = agentsDirOverride ?? import_node_path2.join(getHomeDir(), ".openclaw", "agents");
|
|
622
|
+
let agentDirs;
|
|
623
|
+
try {
|
|
624
|
+
agentDirs = (await import_promises4.readdir(agentsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
625
|
+
} catch {
|
|
626
|
+
const path = getAuthProfilesPath(_agentId);
|
|
627
|
+
try {
|
|
628
|
+
return parseAuthProfilesFile(await import_promises4.readFile(path, "utf-8"));
|
|
629
|
+
} catch {
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const ordered = ["main", ...agentDirs.filter((d) => d !== "main")];
|
|
634
|
+
const merged = new Map;
|
|
635
|
+
for (const agentId of ordered) {
|
|
636
|
+
const profilePath = import_node_path2.join(agentsDir, agentId, "agent", "auth-profiles.json");
|
|
637
|
+
try {
|
|
638
|
+
const text = await import_promises4.readFile(profilePath, "utf-8");
|
|
639
|
+
const profiles = parseAuthProfilesFile(text);
|
|
640
|
+
for (const p of profiles) {
|
|
641
|
+
merged.set(p.provider, p);
|
|
642
|
+
}
|
|
643
|
+
} catch {}
|
|
644
|
+
}
|
|
645
|
+
return Array.from(merged.values());
|
|
646
|
+
}
|
|
647
|
+
function getProviderConfig(provider, config) {
|
|
648
|
+
return config.models?.providers?.[provider];
|
|
649
|
+
}
|
|
650
|
+
function lookupContextWindowFromConfig(modelKey, config) {
|
|
651
|
+
const [provider, ...rest] = modelKey.split("/");
|
|
652
|
+
const modelId = rest.join("/");
|
|
653
|
+
if (!provider || !modelId)
|
|
654
|
+
return;
|
|
655
|
+
const providerConfig = config.models?.providers?.[provider];
|
|
656
|
+
if (!providerConfig?.models)
|
|
657
|
+
return;
|
|
658
|
+
const model = providerConfig.models.find((m) => m.id === modelId);
|
|
659
|
+
return model?.contextWindow;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/openclaw/model-limits.ts
|
|
663
|
+
var import_promises5 = require("node:fs/promises");
|
|
664
|
+
var import_node_path3 = require("node:path");
|
|
665
|
+
var DEFAULT_CONTEXT_TOKENS = 200000;
|
|
666
|
+
var cachedCatalog = null;
|
|
667
|
+
function resolveContextWindow(modelKey, clawmuxContextWindows, openclawConfig, piAiCatalog) {
|
|
668
|
+
const fromClawmux = clawmuxContextWindows[modelKey];
|
|
669
|
+
if (typeof fromClawmux === "number" && fromClawmux > 0) {
|
|
670
|
+
return fromClawmux;
|
|
671
|
+
}
|
|
672
|
+
const fromOpenclaw = lookupContextWindowFromConfig(modelKey, openclawConfig);
|
|
673
|
+
if (typeof fromOpenclaw === "number" && fromOpenclaw > 0) {
|
|
674
|
+
return fromOpenclaw;
|
|
675
|
+
}
|
|
676
|
+
if (piAiCatalog) {
|
|
677
|
+
const [provider, ...rest] = modelKey.split("/");
|
|
678
|
+
const modelId = rest.join("/");
|
|
679
|
+
if (provider && modelId) {
|
|
680
|
+
const providerModels = piAiCatalog[provider];
|
|
681
|
+
if (providerModels) {
|
|
682
|
+
const entry = providerModels[modelId];
|
|
683
|
+
if (entry && typeof entry.contextWindow === "number" && entry.contextWindow > 0) {
|
|
684
|
+
return entry.contextWindow;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return DEFAULT_CONTEXT_TOKENS;
|
|
690
|
+
}
|
|
691
|
+
function resolveCompressionContextWindow(routingModels, clawmuxContextWindows, openclawConfig, piAiCatalog) {
|
|
692
|
+
const modelKeys = [routingModels.LIGHT, routingModels.MEDIUM, routingModels.HEAVY];
|
|
693
|
+
const uniqueKeys = [...new Set(modelKeys.filter((k) => k !== ""))];
|
|
694
|
+
if (uniqueKeys.length === 0) {
|
|
695
|
+
return DEFAULT_CONTEXT_TOKENS;
|
|
696
|
+
}
|
|
697
|
+
let min = Infinity;
|
|
698
|
+
for (const key of uniqueKeys) {
|
|
699
|
+
const window = resolveContextWindow(key, clawmuxContextWindows, openclawConfig, piAiCatalog);
|
|
700
|
+
if (window < min) {
|
|
701
|
+
min = window;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return min === Infinity ? DEFAULT_CONTEXT_TOKENS : min;
|
|
705
|
+
}
|
|
706
|
+
async function findOpenClawNodeModulesPath() {
|
|
707
|
+
try {
|
|
708
|
+
const { execSync } = await import("node:child_process");
|
|
709
|
+
const whichResult = execSync("which openclaw", { encoding: "utf-8" }).trim();
|
|
710
|
+
if (!whichResult)
|
|
711
|
+
return;
|
|
712
|
+
const resolved = await import_promises5.realpath(whichResult);
|
|
713
|
+
let dir = import_node_path3.dirname(resolved);
|
|
714
|
+
for (let i = 0;i < 10; i++) {
|
|
715
|
+
const candidate = import_node_path3.join(dir, "node_modules", "@mariozechner", "pi-ai", "dist", "models.generated.js");
|
|
716
|
+
try {
|
|
717
|
+
if (await fileExists(candidate)) {
|
|
718
|
+
return candidate;
|
|
719
|
+
}
|
|
720
|
+
} catch {}
|
|
721
|
+
const parent = import_node_path3.dirname(dir);
|
|
722
|
+
if (parent === dir)
|
|
723
|
+
break;
|
|
724
|
+
dir = parent;
|
|
725
|
+
}
|
|
726
|
+
} catch {}
|
|
727
|
+
const homeDir = process.env.HOME ?? "/root";
|
|
728
|
+
const fallbackPaths = [
|
|
729
|
+
import_node_path3.join(homeDir, ".npm-global", "lib", "node_modules", "openclaw", "node_modules", "@mariozechner", "pi-ai", "dist", "models.generated.js"),
|
|
730
|
+
import_node_path3.join(homeDir, ".local", "lib", "node_modules", "openclaw", "node_modules", "@mariozechner", "pi-ai", "dist", "models.generated.js")
|
|
731
|
+
];
|
|
732
|
+
for (const path of fallbackPaths) {
|
|
733
|
+
try {
|
|
734
|
+
if (await fileExists(path)) {
|
|
735
|
+
return path;
|
|
736
|
+
}
|
|
737
|
+
} catch {}
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
function parseCatalogFromSource(source) {
|
|
742
|
+
const modelsMatch = source.match(/export\s+const\s+MODELS\s*=\s*(\{[\s\S]*\});?\s*$/m);
|
|
743
|
+
if (!modelsMatch)
|
|
744
|
+
return;
|
|
745
|
+
try {
|
|
746
|
+
const fn = new Function(`return (${modelsMatch[1]});`);
|
|
747
|
+
const result = fn();
|
|
748
|
+
if (typeof result === "object" && result !== null && !Array.isArray(result)) {
|
|
749
|
+
return result;
|
|
750
|
+
}
|
|
751
|
+
} catch (err) {
|
|
752
|
+
console.warn("[clawmux] Failed to parse pi-ai model catalog:", err instanceof Error ? err.message : String(err));
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
async function loadPiAiCatalog() {
|
|
757
|
+
if (cachedCatalog !== null) {
|
|
758
|
+
return cachedCatalog;
|
|
759
|
+
}
|
|
760
|
+
const filePath = await findOpenClawNodeModulesPath();
|
|
761
|
+
if (!filePath) {
|
|
762
|
+
console.warn("[clawmux] pi-ai model catalog not found — using default context windows");
|
|
763
|
+
cachedCatalog = undefined;
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
const source = await readFileText(filePath);
|
|
768
|
+
const catalog = parseCatalogFromSource(source);
|
|
769
|
+
if (catalog) {
|
|
770
|
+
const providerCount = Object.keys(catalog).length;
|
|
771
|
+
const modelCount = Object.values(catalog).reduce((sum, models) => sum + Object.keys(models).length, 0);
|
|
772
|
+
console.log(`[clawmux] Loaded pi-ai model catalog: ${providerCount} providers, ${modelCount} models`);
|
|
773
|
+
cachedCatalog = catalog;
|
|
774
|
+
return catalog;
|
|
775
|
+
}
|
|
776
|
+
console.warn("[clawmux] pi-ai model catalog found but could not be parsed");
|
|
777
|
+
cachedCatalog = undefined;
|
|
778
|
+
return;
|
|
779
|
+
} catch (err) {
|
|
780
|
+
console.warn("[clawmux] Failed to load pi-ai model catalog:", err instanceof Error ? err.message : String(err));
|
|
781
|
+
cachedCatalog = undefined;
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/adapters/registry.ts
|
|
787
|
+
var adapters = new Map;
|
|
788
|
+
function registerAdapter(adapter) {
|
|
789
|
+
adapters.set(adapter.apiType, adapter);
|
|
790
|
+
}
|
|
791
|
+
function getAdapter(apiType) {
|
|
792
|
+
return adapters.get(apiType);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/adapters/tool-converter.ts
|
|
796
|
+
function toOpenAITools(tools) {
|
|
797
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
798
|
+
return;
|
|
799
|
+
return tools.map((tool) => {
|
|
800
|
+
if (tool.type === "function" && tool.function)
|
|
801
|
+
return tool;
|
|
802
|
+
return {
|
|
803
|
+
type: "function",
|
|
804
|
+
function: {
|
|
805
|
+
name: tool.name ?? "",
|
|
806
|
+
description: tool.description ?? "",
|
|
807
|
+
parameters: tool.input_schema ?? tool.parameters ?? { type: "object", properties: {} }
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
function toAnthropicTools(tools) {
|
|
813
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
814
|
+
return;
|
|
815
|
+
return tools.map((tool) => {
|
|
816
|
+
if (tool.input_schema && !tool.type)
|
|
817
|
+
return tool;
|
|
818
|
+
const fn = tool.function ?? tool;
|
|
819
|
+
return {
|
|
820
|
+
name: fn.name ?? "",
|
|
821
|
+
description: fn.description ?? "",
|
|
822
|
+
input_schema: fn.parameters ?? { type: "object", properties: {} }
|
|
823
|
+
};
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/adapters/anthropic.ts
|
|
828
|
+
class AnthropicAdapter {
|
|
829
|
+
apiType = "anthropic-messages";
|
|
830
|
+
parseRequest(body) {
|
|
831
|
+
const raw = body;
|
|
832
|
+
const model = String(raw.model ?? "");
|
|
833
|
+
const messages = raw.messages ?? [];
|
|
834
|
+
const stream = raw.stream !== false;
|
|
835
|
+
const maxTokens = typeof raw.max_tokens === "number" ? raw.max_tokens : undefined;
|
|
836
|
+
const system = raw.system;
|
|
837
|
+
return {
|
|
838
|
+
model,
|
|
839
|
+
messages,
|
|
840
|
+
system,
|
|
841
|
+
stream,
|
|
842
|
+
maxTokens,
|
|
843
|
+
rawBody: raw
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
847
|
+
const url = `${baseUrl}/v1/messages`;
|
|
848
|
+
const headers = {
|
|
849
|
+
"x-api-key": auth.apiKey,
|
|
850
|
+
"anthropic-version": "2023-06-01",
|
|
851
|
+
"content-type": "application/json"
|
|
852
|
+
};
|
|
853
|
+
const isHaiku = targetModel.toLowerCase().includes("haiku");
|
|
854
|
+
const hasThinking = "thinking" in parsed.rawBody;
|
|
855
|
+
if (hasThinking && !isHaiku) {
|
|
856
|
+
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
|
|
857
|
+
}
|
|
858
|
+
const ANTHROPIC_SAMPLING_KEYS = [
|
|
859
|
+
"temperature",
|
|
860
|
+
"top_p",
|
|
861
|
+
"top_k",
|
|
862
|
+
"stop_sequences",
|
|
863
|
+
"metadata",
|
|
864
|
+
"service_tier"
|
|
865
|
+
];
|
|
866
|
+
const samplingParams = {};
|
|
867
|
+
for (const key of ANTHROPIC_SAMPLING_KEYS) {
|
|
868
|
+
if (key in parsed.rawBody) {
|
|
869
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
let bodyObj = {
|
|
873
|
+
model: targetModel,
|
|
874
|
+
messages: parsed.messages,
|
|
875
|
+
stream: parsed.stream,
|
|
876
|
+
...samplingParams
|
|
877
|
+
};
|
|
878
|
+
if (parsed.system !== undefined) {
|
|
879
|
+
bodyObj.system = parsed.system;
|
|
880
|
+
}
|
|
881
|
+
if (parsed.maxTokens !== undefined) {
|
|
882
|
+
bodyObj.max_tokens = parsed.maxTokens;
|
|
883
|
+
}
|
|
884
|
+
if (parsed.rawBody.tools) {
|
|
885
|
+
bodyObj.tools = toAnthropicTools(parsed.rawBody.tools);
|
|
886
|
+
}
|
|
887
|
+
if (!isHaiku && hasThinking) {
|
|
888
|
+
bodyObj.thinking = parsed.rawBody.thinking;
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
url,
|
|
892
|
+
method: "POST",
|
|
893
|
+
headers,
|
|
894
|
+
body: JSON.stringify(bodyObj)
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
modifyMessages(rawBody, compressedMessages) {
|
|
898
|
+
return {
|
|
899
|
+
...rawBody,
|
|
900
|
+
messages: compressedMessages
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
parseResponse(body) {
|
|
904
|
+
const raw = body;
|
|
905
|
+
const id = String(raw.id ?? "");
|
|
906
|
+
const model = String(raw.model ?? "");
|
|
907
|
+
let content = "";
|
|
908
|
+
const contentBlocks = raw.content;
|
|
909
|
+
if (Array.isArray(contentBlocks)) {
|
|
910
|
+
const textParts = [];
|
|
911
|
+
for (const block of contentBlocks) {
|
|
912
|
+
if (typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string") {
|
|
913
|
+
textParts.push(block.text);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
content = textParts.join("");
|
|
917
|
+
}
|
|
918
|
+
const stopReason = typeof raw.stop_reason === "string" ? raw.stop_reason : null;
|
|
919
|
+
let usage;
|
|
920
|
+
const rawUsage = raw.usage;
|
|
921
|
+
if (rawUsage) {
|
|
922
|
+
usage = {
|
|
923
|
+
inputTokens: typeof rawUsage.input_tokens === "number" ? rawUsage.input_tokens : 0,
|
|
924
|
+
outputTokens: typeof rawUsage.output_tokens === "number" ? rawUsage.output_tokens : 0
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return { id, model, content, role: "assistant", stopReason, usage };
|
|
928
|
+
}
|
|
929
|
+
buildResponse(parsed) {
|
|
930
|
+
const result = {
|
|
931
|
+
id: parsed.id,
|
|
932
|
+
type: "message",
|
|
933
|
+
role: "assistant",
|
|
934
|
+
model: parsed.model,
|
|
935
|
+
content: [{ type: "text", text: parsed.content }],
|
|
936
|
+
stop_reason: parsed.stopReason
|
|
937
|
+
};
|
|
938
|
+
if (parsed.usage) {
|
|
939
|
+
result.usage = {
|
|
940
|
+
input_tokens: parsed.usage.inputTokens,
|
|
941
|
+
output_tokens: parsed.usage.outputTokens
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
parseStreamChunk(chunk) {
|
|
947
|
+
const events = [];
|
|
948
|
+
let eventType = "";
|
|
949
|
+
let dataStr = "";
|
|
950
|
+
for (const line of chunk.split(`
|
|
951
|
+
`)) {
|
|
952
|
+
if (line.startsWith("event: ")) {
|
|
953
|
+
eventType = line.slice(7).trim();
|
|
954
|
+
} else if (line.startsWith("data: ")) {
|
|
955
|
+
dataStr = line.slice(6);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (!eventType && !dataStr)
|
|
959
|
+
return events;
|
|
960
|
+
if (eventType === "ping" || eventType === "content_block_start") {
|
|
961
|
+
return events;
|
|
962
|
+
}
|
|
963
|
+
let data = {};
|
|
964
|
+
if (dataStr) {
|
|
965
|
+
try {
|
|
966
|
+
data = JSON.parse(dataStr);
|
|
967
|
+
} catch {
|
|
968
|
+
return events;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
switch (eventType) {
|
|
972
|
+
case "message_start": {
|
|
973
|
+
const message = data.message;
|
|
974
|
+
events.push({
|
|
975
|
+
type: "message_start",
|
|
976
|
+
id: String(message?.id ?? data.id ?? ""),
|
|
977
|
+
model: String(message?.model ?? data.model ?? "")
|
|
978
|
+
});
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
case "content_block_delta": {
|
|
982
|
+
const delta = data.delta;
|
|
983
|
+
if (delta?.type === "text_delta" && typeof delta.text === "string") {
|
|
984
|
+
events.push({
|
|
985
|
+
type: "content_delta",
|
|
986
|
+
text: delta.text,
|
|
987
|
+
index: typeof data.index === "number" ? data.index : 0
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
case "content_block_stop": {
|
|
993
|
+
events.push({
|
|
994
|
+
type: "content_stop",
|
|
995
|
+
index: typeof data.index === "number" ? data.index : 0
|
|
996
|
+
});
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
case "message_delta": {
|
|
1000
|
+
const rawUsage = data.usage;
|
|
1001
|
+
let usage;
|
|
1002
|
+
if (rawUsage) {
|
|
1003
|
+
usage = {
|
|
1004
|
+
inputTokens: typeof rawUsage.input_tokens === "number" ? rawUsage.input_tokens : 0,
|
|
1005
|
+
outputTokens: typeof rawUsage.output_tokens === "number" ? rawUsage.output_tokens : 0
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
events.push({ type: "message_stop", usage });
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
case "message_stop": {
|
|
1012
|
+
events.push({ type: "message_stop" });
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return events;
|
|
1017
|
+
}
|
|
1018
|
+
buildStreamChunk(event) {
|
|
1019
|
+
switch (event.type) {
|
|
1020
|
+
case "message_start":
|
|
1021
|
+
return `event: message_start
|
|
1022
|
+
data: ${JSON.stringify({
|
|
1023
|
+
type: "message_start",
|
|
1024
|
+
message: {
|
|
1025
|
+
id: event.id,
|
|
1026
|
+
type: "message",
|
|
1027
|
+
role: "assistant",
|
|
1028
|
+
model: event.model
|
|
1029
|
+
}
|
|
1030
|
+
})}
|
|
1031
|
+
|
|
1032
|
+
`;
|
|
1033
|
+
case "content_delta":
|
|
1034
|
+
return `event: content_block_delta
|
|
1035
|
+
data: ${JSON.stringify({
|
|
1036
|
+
type: "content_block_delta",
|
|
1037
|
+
index: event.index,
|
|
1038
|
+
delta: { type: "text_delta", text: event.text }
|
|
1039
|
+
})}
|
|
1040
|
+
|
|
1041
|
+
`;
|
|
1042
|
+
case "content_stop":
|
|
1043
|
+
return `event: content_block_stop
|
|
1044
|
+
data: ${JSON.stringify({
|
|
1045
|
+
type: "content_block_stop",
|
|
1046
|
+
index: event.index
|
|
1047
|
+
})}
|
|
1048
|
+
|
|
1049
|
+
`;
|
|
1050
|
+
case "message_stop":
|
|
1051
|
+
if (event.usage) {
|
|
1052
|
+
return `event: message_delta
|
|
1053
|
+
data: ${JSON.stringify({
|
|
1054
|
+
type: "message_delta",
|
|
1055
|
+
usage: {
|
|
1056
|
+
input_tokens: event.usage.inputTokens,
|
|
1057
|
+
output_tokens: event.usage.outputTokens
|
|
1058
|
+
}
|
|
1059
|
+
})}
|
|
1060
|
+
|
|
1061
|
+
` + `event: message_stop
|
|
1062
|
+
data: ${JSON.stringify({
|
|
1063
|
+
type: "message_stop"
|
|
1064
|
+
})}
|
|
1065
|
+
|
|
1066
|
+
`;
|
|
1067
|
+
}
|
|
1068
|
+
return `event: message_stop
|
|
1069
|
+
data: ${JSON.stringify({
|
|
1070
|
+
type: "message_stop"
|
|
1071
|
+
})}
|
|
1072
|
+
|
|
1073
|
+
`;
|
|
1074
|
+
case "error":
|
|
1075
|
+
return `event: error
|
|
1076
|
+
data: ${JSON.stringify({
|
|
1077
|
+
type: "error",
|
|
1078
|
+
error: { message: event.message }
|
|
1079
|
+
})}
|
|
1080
|
+
|
|
1081
|
+
`;
|
|
1082
|
+
default:
|
|
1083
|
+
return "";
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/adapters/openai-shared.ts
|
|
1089
|
+
function isRecord(value) {
|
|
1090
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1091
|
+
}
|
|
1092
|
+
function isMessageArray(value) {
|
|
1093
|
+
if (!Array.isArray(value))
|
|
1094
|
+
return false;
|
|
1095
|
+
return value.every((item) => isRecord(item) && typeof item.role === "string" && ("content" in item));
|
|
1096
|
+
}
|
|
1097
|
+
function extractSystemMessage(messages) {
|
|
1098
|
+
const systemMessages = [];
|
|
1099
|
+
const filtered = [];
|
|
1100
|
+
for (const msg of messages) {
|
|
1101
|
+
if (msg.role === "system" || msg.role === "developer") {
|
|
1102
|
+
if (typeof msg.content === "string") {
|
|
1103
|
+
systemMessages.push(msg.content);
|
|
1104
|
+
} else if (Array.isArray(msg.content)) {
|
|
1105
|
+
for (const part of msg.content) {
|
|
1106
|
+
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
1107
|
+
systemMessages.push(part.text);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
} else {
|
|
1112
|
+
filtered.push(msg);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
system: systemMessages.length > 0 ? systemMessages.join(`
|
|
1117
|
+
`) : undefined,
|
|
1118
|
+
filtered
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function parseOpenAIBody(body) {
|
|
1122
|
+
if (!isRecord(body)) {
|
|
1123
|
+
throw new Error("Request body must be a JSON object");
|
|
1124
|
+
}
|
|
1125
|
+
const model = typeof body.model === "string" ? body.model : "";
|
|
1126
|
+
if (!model) {
|
|
1127
|
+
throw new Error("Missing required field: model");
|
|
1128
|
+
}
|
|
1129
|
+
const rawMessages = body.messages ?? body.input;
|
|
1130
|
+
if (!isMessageArray(rawMessages)) {
|
|
1131
|
+
throw new Error("Request must contain a valid 'messages' or 'input' array");
|
|
1132
|
+
}
|
|
1133
|
+
const { system, filtered } = extractSystemMessage(rawMessages);
|
|
1134
|
+
const stream = body.stream === true;
|
|
1135
|
+
const rawMax = body.max_tokens ?? body.max_output_tokens;
|
|
1136
|
+
const maxTokens = typeof rawMax === "number" ? rawMax : undefined;
|
|
1137
|
+
const rawBody = { ...body };
|
|
1138
|
+
return {
|
|
1139
|
+
model,
|
|
1140
|
+
messages: filtered,
|
|
1141
|
+
system,
|
|
1142
|
+
stream,
|
|
1143
|
+
maxTokens,
|
|
1144
|
+
rawBody
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/adapters/openai-completions.ts
|
|
1149
|
+
class OpenAICompletionsAdapter {
|
|
1150
|
+
apiType = "openai-completions";
|
|
1151
|
+
parseRequest(body) {
|
|
1152
|
+
return parseOpenAIBody(body);
|
|
1153
|
+
}
|
|
1154
|
+
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
1155
|
+
const messages = [];
|
|
1156
|
+
if (parsed.system !== undefined) {
|
|
1157
|
+
messages.push({ role: "system", content: parsed.system });
|
|
1158
|
+
}
|
|
1159
|
+
messages.push(...parsed.messages);
|
|
1160
|
+
const OPENAI_SAMPLING_KEYS = [
|
|
1161
|
+
"temperature",
|
|
1162
|
+
"top_p",
|
|
1163
|
+
"frequency_penalty",
|
|
1164
|
+
"presence_penalty",
|
|
1165
|
+
"logprobs",
|
|
1166
|
+
"top_logprobs",
|
|
1167
|
+
"seed",
|
|
1168
|
+
"stop",
|
|
1169
|
+
"n",
|
|
1170
|
+
"logit_bias",
|
|
1171
|
+
"response_format",
|
|
1172
|
+
"reasoning_effort"
|
|
1173
|
+
];
|
|
1174
|
+
const samplingParams = {};
|
|
1175
|
+
for (const key of OPENAI_SAMPLING_KEYS) {
|
|
1176
|
+
if (key in parsed.rawBody) {
|
|
1177
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const upstreamBody = {
|
|
1181
|
+
model: targetModel,
|
|
1182
|
+
messages,
|
|
1183
|
+
stream: parsed.stream,
|
|
1184
|
+
...samplingParams
|
|
1185
|
+
};
|
|
1186
|
+
if (parsed.maxTokens !== undefined) {
|
|
1187
|
+
upstreamBody.max_tokens = parsed.maxTokens;
|
|
1188
|
+
}
|
|
1189
|
+
if (parsed.rawBody.tools) {
|
|
1190
|
+
upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
url: /\/v\d+\/?$/.test(baseUrl) ? `${baseUrl.replace(/\/$/, "")}/chat/completions` : `${baseUrl}/v1/chat/completions`,
|
|
1194
|
+
method: "POST",
|
|
1195
|
+
headers: {
|
|
1196
|
+
"Content-Type": "application/json",
|
|
1197
|
+
Authorization: `Bearer ${auth.apiKey}`
|
|
1198
|
+
},
|
|
1199
|
+
body: JSON.stringify(upstreamBody)
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
modifyMessages(rawBody, compressedMessages) {
|
|
1203
|
+
return {
|
|
1204
|
+
...rawBody,
|
|
1205
|
+
messages: compressedMessages
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
parseResponse(body) {
|
|
1209
|
+
const raw = body;
|
|
1210
|
+
const id = String(raw.id ?? "");
|
|
1211
|
+
const model = String(raw.model ?? "");
|
|
1212
|
+
let content = "";
|
|
1213
|
+
let stopReason = null;
|
|
1214
|
+
const choices = raw.choices;
|
|
1215
|
+
if (Array.isArray(choices) && choices.length > 0) {
|
|
1216
|
+
const choice = choices[0];
|
|
1217
|
+
const message = choice.message;
|
|
1218
|
+
if (message) {
|
|
1219
|
+
const messageText = message.content ?? message.reasoning_content;
|
|
1220
|
+
if (typeof messageText === "string") {
|
|
1221
|
+
content = messageText;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (typeof choice.finish_reason === "string") {
|
|
1225
|
+
stopReason = choice.finish_reason;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
let usage;
|
|
1229
|
+
const rawUsage = raw.usage;
|
|
1230
|
+
if (rawUsage) {
|
|
1231
|
+
usage = {
|
|
1232
|
+
inputTokens: typeof rawUsage.prompt_tokens === "number" ? rawUsage.prompt_tokens : 0,
|
|
1233
|
+
outputTokens: typeof rawUsage.completion_tokens === "number" ? rawUsage.completion_tokens : 0
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
return { id, model, content, role: "assistant", stopReason, usage };
|
|
1237
|
+
}
|
|
1238
|
+
buildResponse(parsed) {
|
|
1239
|
+
const result = {
|
|
1240
|
+
id: parsed.id,
|
|
1241
|
+
object: "chat.completion",
|
|
1242
|
+
model: parsed.model,
|
|
1243
|
+
choices: [
|
|
1244
|
+
{
|
|
1245
|
+
index: 0,
|
|
1246
|
+
message: { role: "assistant", content: parsed.content },
|
|
1247
|
+
finish_reason: parsed.stopReason
|
|
1248
|
+
}
|
|
1249
|
+
]
|
|
1250
|
+
};
|
|
1251
|
+
if (parsed.usage) {
|
|
1252
|
+
result.usage = {
|
|
1253
|
+
prompt_tokens: parsed.usage.inputTokens,
|
|
1254
|
+
completion_tokens: parsed.usage.outputTokens,
|
|
1255
|
+
total_tokens: parsed.usage.inputTokens + parsed.usage.outputTokens
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
return result;
|
|
1259
|
+
}
|
|
1260
|
+
parseStreamChunk(chunk) {
|
|
1261
|
+
const events = [];
|
|
1262
|
+
for (const line of chunk.split(`
|
|
1263
|
+
`)) {
|
|
1264
|
+
const trimmed = line.trim();
|
|
1265
|
+
if (!trimmed.startsWith("data: "))
|
|
1266
|
+
continue;
|
|
1267
|
+
const payload = trimmed.slice(6);
|
|
1268
|
+
if (payload === "[DONE]") {
|
|
1269
|
+
events.push({ type: "message_stop" });
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
let data;
|
|
1273
|
+
try {
|
|
1274
|
+
data = JSON.parse(payload);
|
|
1275
|
+
} catch {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const choices = data.choices;
|
|
1279
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1280
|
+
if (data.id && data.model) {
|
|
1281
|
+
events.push({
|
|
1282
|
+
type: "message_start",
|
|
1283
|
+
id: String(data.id),
|
|
1284
|
+
model: String(data.model)
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const choice = choices[0];
|
|
1290
|
+
const delta = choice.delta;
|
|
1291
|
+
const finishReason = choice.finish_reason;
|
|
1292
|
+
const textContent = delta?.content ?? delta?.reasoning_content;
|
|
1293
|
+
if (delta?.role === "assistant" && textContent == null) {
|
|
1294
|
+
events.push({
|
|
1295
|
+
type: "message_start",
|
|
1296
|
+
id: String(data.id ?? ""),
|
|
1297
|
+
model: String(data.model ?? "")
|
|
1298
|
+
});
|
|
1299
|
+
} else if (typeof textContent === "string") {
|
|
1300
|
+
events.push({
|
|
1301
|
+
type: "content_delta",
|
|
1302
|
+
text: textContent,
|
|
1303
|
+
index: typeof choice.index === "number" ? choice.index : 0
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
if (typeof finishReason === "string" && finishReason !== "") {
|
|
1307
|
+
events.push({
|
|
1308
|
+
type: "content_stop",
|
|
1309
|
+
index: typeof choice.index === "number" ? choice.index : 0
|
|
1310
|
+
});
|
|
1311
|
+
let usage;
|
|
1312
|
+
const rawUsage = data.usage;
|
|
1313
|
+
if (rawUsage) {
|
|
1314
|
+
usage = {
|
|
1315
|
+
inputTokens: typeof rawUsage.prompt_tokens === "number" ? rawUsage.prompt_tokens : 0,
|
|
1316
|
+
outputTokens: typeof rawUsage.completion_tokens === "number" ? rawUsage.completion_tokens : 0
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
events.push({ type: "message_stop", usage });
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return events;
|
|
1323
|
+
}
|
|
1324
|
+
buildStreamChunk(event) {
|
|
1325
|
+
switch (event.type) {
|
|
1326
|
+
case "message_start":
|
|
1327
|
+
return `data: ${JSON.stringify({
|
|
1328
|
+
id: event.id,
|
|
1329
|
+
object: "chat.completion.chunk",
|
|
1330
|
+
model: event.model,
|
|
1331
|
+
choices: [
|
|
1332
|
+
{
|
|
1333
|
+
index: 0,
|
|
1334
|
+
delta: { role: "assistant" },
|
|
1335
|
+
finish_reason: null
|
|
1336
|
+
}
|
|
1337
|
+
]
|
|
1338
|
+
})}
|
|
1339
|
+
|
|
1340
|
+
`;
|
|
1341
|
+
case "content_delta":
|
|
1342
|
+
return `data: ${JSON.stringify({
|
|
1343
|
+
id: "",
|
|
1344
|
+
object: "chat.completion.chunk",
|
|
1345
|
+
choices: [
|
|
1346
|
+
{
|
|
1347
|
+
index: event.index,
|
|
1348
|
+
delta: { content: event.text },
|
|
1349
|
+
finish_reason: null
|
|
1350
|
+
}
|
|
1351
|
+
]
|
|
1352
|
+
})}
|
|
1353
|
+
|
|
1354
|
+
`;
|
|
1355
|
+
case "content_stop":
|
|
1356
|
+
return `data: ${JSON.stringify({
|
|
1357
|
+
id: "",
|
|
1358
|
+
object: "chat.completion.chunk",
|
|
1359
|
+
choices: [
|
|
1360
|
+
{
|
|
1361
|
+
index: event.index,
|
|
1362
|
+
delta: {},
|
|
1363
|
+
finish_reason: "stop"
|
|
1364
|
+
}
|
|
1365
|
+
]
|
|
1366
|
+
})}
|
|
1367
|
+
|
|
1368
|
+
`;
|
|
1369
|
+
case "message_stop":
|
|
1370
|
+
return `data: [DONE]
|
|
1371
|
+
|
|
1372
|
+
`;
|
|
1373
|
+
case "error":
|
|
1374
|
+
return `data: ${JSON.stringify({
|
|
1375
|
+
error: { message: event.message }
|
|
1376
|
+
})}
|
|
1377
|
+
|
|
1378
|
+
`;
|
|
1379
|
+
default:
|
|
1380
|
+
return "";
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
var openaiCompletionsAdapter = new OpenAICompletionsAdapter;
|
|
1385
|
+
registerAdapter(openaiCompletionsAdapter);
|
|
1386
|
+
|
|
1387
|
+
// src/adapters/openai-responses.ts
|
|
1388
|
+
class OpenAIResponsesAdapter {
|
|
1389
|
+
apiType = "openai-responses";
|
|
1390
|
+
parseRequest(body) {
|
|
1391
|
+
return parseOpenAIBody(body);
|
|
1392
|
+
}
|
|
1393
|
+
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
1394
|
+
const input = [];
|
|
1395
|
+
if (parsed.system !== undefined) {
|
|
1396
|
+
input.push({ role: "system", content: parsed.system });
|
|
1397
|
+
}
|
|
1398
|
+
input.push(...parsed.messages);
|
|
1399
|
+
const OPENAI_RESPONSES_SAMPLING_KEYS = [
|
|
1400
|
+
"temperature",
|
|
1401
|
+
"top_p",
|
|
1402
|
+
"truncation",
|
|
1403
|
+
"reasoning",
|
|
1404
|
+
"reasoning_effort",
|
|
1405
|
+
"text",
|
|
1406
|
+
"metadata",
|
|
1407
|
+
"store",
|
|
1408
|
+
"include"
|
|
1409
|
+
];
|
|
1410
|
+
const samplingParams = {};
|
|
1411
|
+
for (const key of OPENAI_RESPONSES_SAMPLING_KEYS) {
|
|
1412
|
+
if (key in parsed.rawBody) {
|
|
1413
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const upstreamBody = {
|
|
1417
|
+
model: targetModel,
|
|
1418
|
+
input,
|
|
1419
|
+
stream: parsed.stream,
|
|
1420
|
+
...samplingParams
|
|
1421
|
+
};
|
|
1422
|
+
if (parsed.maxTokens !== undefined) {
|
|
1423
|
+
upstreamBody.max_output_tokens = parsed.maxTokens;
|
|
1424
|
+
}
|
|
1425
|
+
if (parsed.rawBody.tools) {
|
|
1426
|
+
upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
|
|
1427
|
+
}
|
|
1428
|
+
return {
|
|
1429
|
+
url: `${baseUrl}/v1/responses`,
|
|
1430
|
+
method: "POST",
|
|
1431
|
+
headers: {
|
|
1432
|
+
"Content-Type": "application/json",
|
|
1433
|
+
Authorization: `Bearer ${auth.apiKey}`
|
|
1434
|
+
},
|
|
1435
|
+
body: JSON.stringify(upstreamBody)
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
modifyMessages(rawBody, compressedMessages) {
|
|
1439
|
+
const hasInput = "input" in rawBody;
|
|
1440
|
+
const fieldName = hasInput ? "input" : "messages";
|
|
1441
|
+
return {
|
|
1442
|
+
...rawBody,
|
|
1443
|
+
[fieldName]: compressedMessages
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
parseResponse(body) {
|
|
1447
|
+
const raw = body;
|
|
1448
|
+
const id = String(raw.id ?? "");
|
|
1449
|
+
const model = String(raw.model ?? "");
|
|
1450
|
+
let content = "";
|
|
1451
|
+
let stopReason = null;
|
|
1452
|
+
const output = raw.output;
|
|
1453
|
+
if (Array.isArray(output)) {
|
|
1454
|
+
const textParts = [];
|
|
1455
|
+
for (const item of output) {
|
|
1456
|
+
if (item.type === "message") {
|
|
1457
|
+
const msgContent = item.content;
|
|
1458
|
+
if (Array.isArray(msgContent)) {
|
|
1459
|
+
for (const part of msgContent) {
|
|
1460
|
+
if (part.type === "output_text" && typeof part.text === "string") {
|
|
1461
|
+
textParts.push(part.text);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
content = textParts.join("");
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof raw.status === "string") {
|
|
1470
|
+
stopReason = raw.status;
|
|
1471
|
+
}
|
|
1472
|
+
let usage;
|
|
1473
|
+
const rawUsage = raw.usage;
|
|
1474
|
+
if (rawUsage) {
|
|
1475
|
+
usage = {
|
|
1476
|
+
inputTokens: typeof rawUsage.input_tokens === "number" ? rawUsage.input_tokens : 0,
|
|
1477
|
+
outputTokens: typeof rawUsage.output_tokens === "number" ? rawUsage.output_tokens : 0
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
return { id, model, content, role: "assistant", stopReason, usage };
|
|
1481
|
+
}
|
|
1482
|
+
buildResponse(parsed) {
|
|
1483
|
+
const result = {
|
|
1484
|
+
id: parsed.id,
|
|
1485
|
+
object: "response",
|
|
1486
|
+
model: parsed.model,
|
|
1487
|
+
status: parsed.stopReason ?? "completed",
|
|
1488
|
+
output: [
|
|
1489
|
+
{
|
|
1490
|
+
type: "message",
|
|
1491
|
+
role: "assistant",
|
|
1492
|
+
content: [
|
|
1493
|
+
{ type: "output_text", text: parsed.content }
|
|
1494
|
+
]
|
|
1495
|
+
}
|
|
1496
|
+
]
|
|
1497
|
+
};
|
|
1498
|
+
if (parsed.usage) {
|
|
1499
|
+
result.usage = {
|
|
1500
|
+
input_tokens: parsed.usage.inputTokens,
|
|
1501
|
+
output_tokens: parsed.usage.outputTokens,
|
|
1502
|
+
total_tokens: parsed.usage.inputTokens + parsed.usage.outputTokens
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
return result;
|
|
1506
|
+
}
|
|
1507
|
+
parseStreamChunk(chunk) {
|
|
1508
|
+
const events = [];
|
|
1509
|
+
for (const line of chunk.split(`
|
|
1510
|
+
`)) {
|
|
1511
|
+
const trimmed = line.trim();
|
|
1512
|
+
if (!trimmed.startsWith("data: "))
|
|
1513
|
+
continue;
|
|
1514
|
+
const payload = trimmed.slice(6);
|
|
1515
|
+
if (payload === "[DONE]") {
|
|
1516
|
+
events.push({ type: "message_stop" });
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
let data;
|
|
1520
|
+
try {
|
|
1521
|
+
data = JSON.parse(payload);
|
|
1522
|
+
} catch {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
const eventType = String(data.type ?? "");
|
|
1526
|
+
switch (eventType) {
|
|
1527
|
+
case "response.created": {
|
|
1528
|
+
const response = data.response;
|
|
1529
|
+
events.push({
|
|
1530
|
+
type: "message_start",
|
|
1531
|
+
id: String(response?.id ?? data.id ?? ""),
|
|
1532
|
+
model: String(response?.model ?? data.model ?? "")
|
|
1533
|
+
});
|
|
1534
|
+
break;
|
|
1535
|
+
}
|
|
1536
|
+
case "response.output_text.delta": {
|
|
1537
|
+
events.push({
|
|
1538
|
+
type: "content_delta",
|
|
1539
|
+
text: typeof data.delta === "string" ? data.delta : "",
|
|
1540
|
+
index: typeof data.output_index === "number" ? data.output_index : 0
|
|
1541
|
+
});
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
case "response.output_text.done": {
|
|
1545
|
+
events.push({
|
|
1546
|
+
type: "content_stop",
|
|
1547
|
+
index: typeof data.output_index === "number" ? data.output_index : 0
|
|
1548
|
+
});
|
|
1549
|
+
break;
|
|
1550
|
+
}
|
|
1551
|
+
case "response.completed": {
|
|
1552
|
+
let usage;
|
|
1553
|
+
const response = data.response;
|
|
1554
|
+
const rawUsage = response?.usage;
|
|
1555
|
+
if (rawUsage) {
|
|
1556
|
+
usage = {
|
|
1557
|
+
inputTokens: typeof rawUsage.input_tokens === "number" ? rawUsage.input_tokens : 0,
|
|
1558
|
+
outputTokens: typeof rawUsage.output_tokens === "number" ? rawUsage.output_tokens : 0
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
events.push({ type: "message_stop", usage });
|
|
1562
|
+
break;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return events;
|
|
1567
|
+
}
|
|
1568
|
+
buildStreamChunk(event) {
|
|
1569
|
+
switch (event.type) {
|
|
1570
|
+
case "message_start":
|
|
1571
|
+
return `data: ${JSON.stringify({
|
|
1572
|
+
type: "response.created",
|
|
1573
|
+
response: {
|
|
1574
|
+
id: event.id,
|
|
1575
|
+
object: "response",
|
|
1576
|
+
model: event.model,
|
|
1577
|
+
status: "in_progress"
|
|
1578
|
+
}
|
|
1579
|
+
})}
|
|
1580
|
+
|
|
1581
|
+
`;
|
|
1582
|
+
case "content_delta":
|
|
1583
|
+
return `data: ${JSON.stringify({
|
|
1584
|
+
type: "response.output_text.delta",
|
|
1585
|
+
output_index: event.index,
|
|
1586
|
+
delta: event.text
|
|
1587
|
+
})}
|
|
1588
|
+
|
|
1589
|
+
`;
|
|
1590
|
+
case "content_stop":
|
|
1591
|
+
return `data: ${JSON.stringify({
|
|
1592
|
+
type: "response.output_text.done",
|
|
1593
|
+
output_index: event.index
|
|
1594
|
+
})}
|
|
1595
|
+
|
|
1596
|
+
`;
|
|
1597
|
+
case "message_stop":
|
|
1598
|
+
if (event.usage) {
|
|
1599
|
+
return `data: ${JSON.stringify({
|
|
1600
|
+
type: "response.completed",
|
|
1601
|
+
response: {
|
|
1602
|
+
usage: {
|
|
1603
|
+
input_tokens: event.usage.inputTokens,
|
|
1604
|
+
output_tokens: event.usage.outputTokens
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
})}
|
|
1608
|
+
|
|
1609
|
+
`;
|
|
1610
|
+
}
|
|
1611
|
+
return `data: ${JSON.stringify({
|
|
1612
|
+
type: "response.completed"
|
|
1613
|
+
})}
|
|
1614
|
+
|
|
1615
|
+
`;
|
|
1616
|
+
case "error":
|
|
1617
|
+
return `data: ${JSON.stringify({
|
|
1618
|
+
type: "error",
|
|
1619
|
+
error: { message: event.message }
|
|
1620
|
+
})}
|
|
1621
|
+
|
|
1622
|
+
`;
|
|
1623
|
+
default:
|
|
1624
|
+
return "";
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
var openaiResponsesAdapter = new OpenAIResponsesAdapter;
|
|
1629
|
+
registerAdapter(openaiResponsesAdapter);
|
|
1630
|
+
|
|
1631
|
+
// src/adapters/google.ts
|
|
1632
|
+
function googleRoleToStandard(role) {
|
|
1633
|
+
if (role === "model")
|
|
1634
|
+
return "assistant";
|
|
1635
|
+
return role ?? "user";
|
|
1636
|
+
}
|
|
1637
|
+
function standardRoleToGoogle(role) {
|
|
1638
|
+
if (role === "assistant")
|
|
1639
|
+
return "model";
|
|
1640
|
+
return "user";
|
|
1641
|
+
}
|
|
1642
|
+
function contentsToMessages(contents) {
|
|
1643
|
+
return contents.map((c) => {
|
|
1644
|
+
const text = c.parts.filter((p) => p.text !== undefined).map((p) => p.text).join("");
|
|
1645
|
+
return {
|
|
1646
|
+
role: googleRoleToStandard(c.role),
|
|
1647
|
+
content: text || c.parts
|
|
1648
|
+
};
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
function messagesToContents(messages) {
|
|
1652
|
+
return messages.map((m) => ({
|
|
1653
|
+
role: standardRoleToGoogle(m.role),
|
|
1654
|
+
parts: typeof m.content === "string" ? [{ text: m.content }] : Array.isArray(m.content) ? m.content : [{ text: String(m.content) }]
|
|
1655
|
+
}));
|
|
1656
|
+
}
|
|
1657
|
+
function mapGoogleFinishReason(reason) {
|
|
1658
|
+
if (reason === null)
|
|
1659
|
+
return null;
|
|
1660
|
+
switch (reason) {
|
|
1661
|
+
case "STOP":
|
|
1662
|
+
return "stop";
|
|
1663
|
+
case "MAX_TOKENS":
|
|
1664
|
+
return "max_tokens";
|
|
1665
|
+
case "SAFETY":
|
|
1666
|
+
return "content_filter";
|
|
1667
|
+
default:
|
|
1668
|
+
return reason.toLowerCase();
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
function mapStopReasonToGoogle(reason) {
|
|
1672
|
+
if (reason === null)
|
|
1673
|
+
return "STOP";
|
|
1674
|
+
switch (reason) {
|
|
1675
|
+
case "stop":
|
|
1676
|
+
return "STOP";
|
|
1677
|
+
case "max_tokens":
|
|
1678
|
+
return "MAX_TOKENS";
|
|
1679
|
+
case "content_filter":
|
|
1680
|
+
return "SAFETY";
|
|
1681
|
+
default:
|
|
1682
|
+
return reason.toUpperCase();
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
class GoogleGenerativeAIAdapter {
|
|
1687
|
+
apiType = "google-generative-ai";
|
|
1688
|
+
parseRequest(body) {
|
|
1689
|
+
const raw = body;
|
|
1690
|
+
const model = raw.model ?? "";
|
|
1691
|
+
const contents = raw.contents ?? [];
|
|
1692
|
+
const messages = contentsToMessages(contents);
|
|
1693
|
+
let system;
|
|
1694
|
+
if (raw.systemInstruction?.parts) {
|
|
1695
|
+
system = raw.systemInstruction.parts.filter((p) => p.text !== undefined).map((p) => p.text).join("");
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
model,
|
|
1699
|
+
messages,
|
|
1700
|
+
system,
|
|
1701
|
+
stream: raw.stream !== false,
|
|
1702
|
+
maxTokens: raw.generationConfig?.maxOutputTokens,
|
|
1703
|
+
rawBody: raw
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
1707
|
+
const endpoint = parsed.stream ? `${baseUrl}/v1beta/models/${targetModel}:streamGenerateContent?alt=sse` : `${baseUrl}/v1beta/models/${targetModel}:generateContent`;
|
|
1708
|
+
const contents = messagesToContents(parsed.messages);
|
|
1709
|
+
const requestBody = {
|
|
1710
|
+
...parsed.rawBody,
|
|
1711
|
+
contents
|
|
1712
|
+
};
|
|
1713
|
+
delete requestBody.model;
|
|
1714
|
+
delete requestBody.stream;
|
|
1715
|
+
if (parsed.system) {
|
|
1716
|
+
requestBody.systemInstruction = {
|
|
1717
|
+
parts: [{ text: parsed.system }]
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
if (parsed.maxTokens !== undefined) {
|
|
1721
|
+
requestBody.generationConfig = {
|
|
1722
|
+
...requestBody.generationConfig,
|
|
1723
|
+
maxOutputTokens: parsed.maxTokens
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
return {
|
|
1727
|
+
url: endpoint,
|
|
1728
|
+
method: "POST",
|
|
1729
|
+
headers: {
|
|
1730
|
+
"Content-Type": "application/json",
|
|
1731
|
+
[auth.headerName || "x-goog-api-key"]: auth.headerValue || auth.apiKey
|
|
1732
|
+
},
|
|
1733
|
+
body: JSON.stringify(requestBody)
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
modifyMessages(rawBody, compressedMessages) {
|
|
1737
|
+
return {
|
|
1738
|
+
...rawBody,
|
|
1739
|
+
contents: messagesToContents(compressedMessages)
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
parseResponse(body) {
|
|
1743
|
+
const raw = body;
|
|
1744
|
+
const candidates = raw.candidates;
|
|
1745
|
+
const candidate = candidates?.[0];
|
|
1746
|
+
const content = candidate?.content;
|
|
1747
|
+
const text = content?.parts?.filter((p) => p.text !== undefined).map((p) => p.text).join("") ?? "";
|
|
1748
|
+
const finishReason = candidate?.finishReason;
|
|
1749
|
+
const usageMeta = raw.usageMetadata;
|
|
1750
|
+
return {
|
|
1751
|
+
id: raw.id ?? `google-${Date.now()}`,
|
|
1752
|
+
model: raw.modelVersion ?? "",
|
|
1753
|
+
content: text,
|
|
1754
|
+
role: "assistant",
|
|
1755
|
+
stopReason: mapGoogleFinishReason(finishReason ?? null),
|
|
1756
|
+
usage: usageMeta ? {
|
|
1757
|
+
inputTokens: usageMeta.promptTokenCount ?? 0,
|
|
1758
|
+
outputTokens: usageMeta.candidatesTokenCount ?? 0
|
|
1759
|
+
} : undefined
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
buildResponse(parsed) {
|
|
1763
|
+
const result = {
|
|
1764
|
+
candidates: [
|
|
1765
|
+
{
|
|
1766
|
+
content: {
|
|
1767
|
+
parts: [{ text: parsed.content }],
|
|
1768
|
+
role: "model"
|
|
1769
|
+
},
|
|
1770
|
+
finishReason: mapStopReasonToGoogle(parsed.stopReason)
|
|
1771
|
+
}
|
|
1772
|
+
]
|
|
1773
|
+
};
|
|
1774
|
+
if (parsed.usage) {
|
|
1775
|
+
result.usageMetadata = {
|
|
1776
|
+
promptTokenCount: parsed.usage.inputTokens,
|
|
1777
|
+
candidatesTokenCount: parsed.usage.outputTokens
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
return result;
|
|
1781
|
+
}
|
|
1782
|
+
parseStreamChunk(chunk) {
|
|
1783
|
+
const events = [];
|
|
1784
|
+
const lines = chunk.split(`
|
|
1785
|
+
`);
|
|
1786
|
+
for (const line of lines) {
|
|
1787
|
+
const trimmed = line.trim();
|
|
1788
|
+
if (!trimmed.startsWith("data:"))
|
|
1789
|
+
continue;
|
|
1790
|
+
const jsonStr = trimmed.slice(5).trim();
|
|
1791
|
+
if (jsonStr === "" || jsonStr === "[DONE]")
|
|
1792
|
+
continue;
|
|
1793
|
+
let parsed;
|
|
1794
|
+
try {
|
|
1795
|
+
parsed = JSON.parse(jsonStr);
|
|
1796
|
+
} catch {
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
const candidates = parsed.candidates;
|
|
1800
|
+
const candidate = candidates?.[0];
|
|
1801
|
+
if (!candidate)
|
|
1802
|
+
continue;
|
|
1803
|
+
const content = candidate.content;
|
|
1804
|
+
const text = content?.parts?.filter((p) => p.text !== undefined).map((p) => p.text).join("") ?? "";
|
|
1805
|
+
const finishReason = candidate.finishReason;
|
|
1806
|
+
if (content?.role === "model" && text !== "") {
|
|
1807
|
+
events.push({
|
|
1808
|
+
type: "message_start",
|
|
1809
|
+
id: parsed.id ?? `google-${Date.now()}`,
|
|
1810
|
+
model: parsed.modelVersion ?? ""
|
|
1811
|
+
});
|
|
1812
|
+
events.push({ type: "content_delta", text, index: 0 });
|
|
1813
|
+
} else if (text !== "") {
|
|
1814
|
+
events.push({ type: "content_delta", text, index: 0 });
|
|
1815
|
+
}
|
|
1816
|
+
if (finishReason && finishReason !== "FINISH_REASON_UNSPECIFIED") {
|
|
1817
|
+
const usageMeta = parsed.usageMetadata;
|
|
1818
|
+
events.push({ type: "content_stop", index: 0 });
|
|
1819
|
+
events.push({
|
|
1820
|
+
type: "message_stop",
|
|
1821
|
+
usage: usageMeta ? {
|
|
1822
|
+
inputTokens: usageMeta.promptTokenCount ?? 0,
|
|
1823
|
+
outputTokens: usageMeta.candidatesTokenCount ?? 0
|
|
1824
|
+
} : undefined
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return events;
|
|
1829
|
+
}
|
|
1830
|
+
buildStreamChunk(event) {
|
|
1831
|
+
switch (event.type) {
|
|
1832
|
+
case "message_start":
|
|
1833
|
+
return `data: ${JSON.stringify({
|
|
1834
|
+
candidates: [
|
|
1835
|
+
{
|
|
1836
|
+
content: { parts: [{ text: "" }], role: "model" }
|
|
1837
|
+
}
|
|
1838
|
+
]
|
|
1839
|
+
})}
|
|
1840
|
+
|
|
1841
|
+
`;
|
|
1842
|
+
case "content_delta":
|
|
1843
|
+
return `data: ${JSON.stringify({
|
|
1844
|
+
candidates: [
|
|
1845
|
+
{
|
|
1846
|
+
content: { parts: [{ text: event.text }], role: "model" }
|
|
1847
|
+
}
|
|
1848
|
+
]
|
|
1849
|
+
})}
|
|
1850
|
+
|
|
1851
|
+
`;
|
|
1852
|
+
case "content_stop":
|
|
1853
|
+
return "";
|
|
1854
|
+
case "message_stop":
|
|
1855
|
+
return `data: ${JSON.stringify({
|
|
1856
|
+
candidates: [
|
|
1857
|
+
{
|
|
1858
|
+
content: { parts: [{ text: "" }], role: "model" },
|
|
1859
|
+
finishReason: "STOP"
|
|
1860
|
+
}
|
|
1861
|
+
],
|
|
1862
|
+
...event.usage ? {
|
|
1863
|
+
usageMetadata: {
|
|
1864
|
+
promptTokenCount: event.usage.inputTokens,
|
|
1865
|
+
candidatesTokenCount: event.usage.outputTokens
|
|
1866
|
+
}
|
|
1867
|
+
} : {}
|
|
1868
|
+
})}
|
|
1869
|
+
|
|
1870
|
+
`;
|
|
1871
|
+
case "error":
|
|
1872
|
+
return `data: ${JSON.stringify({
|
|
1873
|
+
error: { message: event.message }
|
|
1874
|
+
})}
|
|
1875
|
+
|
|
1876
|
+
`;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
var googleAdapter = new GoogleGenerativeAIAdapter;
|
|
1881
|
+
registerAdapter(googleAdapter);
|
|
1882
|
+
|
|
1883
|
+
// src/adapters/ollama.ts
|
|
1884
|
+
class OllamaAdapter {
|
|
1885
|
+
apiType = "ollama";
|
|
1886
|
+
parseRequest(body) {
|
|
1887
|
+
const raw = body;
|
|
1888
|
+
const model = raw.model ?? "";
|
|
1889
|
+
const messages = raw.messages ?? [];
|
|
1890
|
+
const stream = raw.stream !== false;
|
|
1891
|
+
let system;
|
|
1892
|
+
const filteredMessages = [];
|
|
1893
|
+
for (const msg of messages) {
|
|
1894
|
+
if (msg.role === "system" && typeof msg.content === "string") {
|
|
1895
|
+
system = msg.content;
|
|
1896
|
+
} else {
|
|
1897
|
+
filteredMessages.push(msg);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return {
|
|
1901
|
+
model,
|
|
1902
|
+
messages: filteredMessages,
|
|
1903
|
+
system,
|
|
1904
|
+
stream,
|
|
1905
|
+
maxTokens: raw.options?.num_predict,
|
|
1906
|
+
rawBody: raw
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
buildUpstreamRequest(parsed, targetModel, baseUrl, _auth) {
|
|
1910
|
+
const messages = [];
|
|
1911
|
+
if (parsed.system) {
|
|
1912
|
+
messages.push({ role: "system", content: parsed.system });
|
|
1913
|
+
}
|
|
1914
|
+
messages.push(...parsed.messages);
|
|
1915
|
+
const requestBody = {
|
|
1916
|
+
...parsed.rawBody,
|
|
1917
|
+
model: targetModel,
|
|
1918
|
+
messages,
|
|
1919
|
+
stream: parsed.stream
|
|
1920
|
+
};
|
|
1921
|
+
if (parsed.maxTokens !== undefined) {
|
|
1922
|
+
requestBody.options = {
|
|
1923
|
+
...requestBody.options,
|
|
1924
|
+
num_predict: parsed.maxTokens
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
url: `${baseUrl}/api/chat`,
|
|
1929
|
+
method: "POST",
|
|
1930
|
+
headers: {
|
|
1931
|
+
"Content-Type": "application/json"
|
|
1932
|
+
},
|
|
1933
|
+
body: JSON.stringify(requestBody)
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
modifyMessages(rawBody, compressedMessages) {
|
|
1937
|
+
return {
|
|
1938
|
+
...rawBody,
|
|
1939
|
+
messages: compressedMessages
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
parseResponse(body) {
|
|
1943
|
+
const raw = body;
|
|
1944
|
+
const message = raw.message;
|
|
1945
|
+
const model = raw.model ?? "";
|
|
1946
|
+
return {
|
|
1947
|
+
id: `ollama-${Date.now()}`,
|
|
1948
|
+
model,
|
|
1949
|
+
content: message?.content ?? "",
|
|
1950
|
+
role: "assistant",
|
|
1951
|
+
stopReason: raw.done === true ? "stop" : null,
|
|
1952
|
+
usage: raw.prompt_eval_count !== undefined || raw.eval_count !== undefined ? {
|
|
1953
|
+
inputTokens: raw.prompt_eval_count ?? 0,
|
|
1954
|
+
outputTokens: raw.eval_count ?? 0
|
|
1955
|
+
} : undefined
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
buildResponse(parsed) {
|
|
1959
|
+
const result = {
|
|
1960
|
+
model: parsed.model,
|
|
1961
|
+
message: {
|
|
1962
|
+
role: "assistant",
|
|
1963
|
+
content: parsed.content
|
|
1964
|
+
},
|
|
1965
|
+
done: parsed.stopReason === "stop"
|
|
1966
|
+
};
|
|
1967
|
+
if (parsed.usage) {
|
|
1968
|
+
result.prompt_eval_count = parsed.usage.inputTokens;
|
|
1969
|
+
result.eval_count = parsed.usage.outputTokens;
|
|
1970
|
+
}
|
|
1971
|
+
return result;
|
|
1972
|
+
}
|
|
1973
|
+
parseStreamChunk(chunk) {
|
|
1974
|
+
const events = [];
|
|
1975
|
+
const lines = chunk.split(`
|
|
1976
|
+
`);
|
|
1977
|
+
for (const line of lines) {
|
|
1978
|
+
const trimmed = line.trim();
|
|
1979
|
+
if (trimmed === "")
|
|
1980
|
+
continue;
|
|
1981
|
+
let parsed;
|
|
1982
|
+
try {
|
|
1983
|
+
parsed = JSON.parse(trimmed);
|
|
1984
|
+
} catch {
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
const message = parsed.message;
|
|
1988
|
+
const done = parsed.done === true;
|
|
1989
|
+
if (message?.role === "assistant" && !done) {
|
|
1990
|
+
if (events.length === 0 && message.content !== undefined) {
|
|
1991
|
+
events.push({
|
|
1992
|
+
type: "message_start",
|
|
1993
|
+
id: `ollama-${Date.now()}`,
|
|
1994
|
+
model: parsed.model ?? ""
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
if (message.content !== undefined) {
|
|
1998
|
+
events.push({
|
|
1999
|
+
type: "content_delta",
|
|
2000
|
+
text: message.content,
|
|
2001
|
+
index: 0
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (done) {
|
|
2006
|
+
events.push({ type: "content_stop", index: 0 });
|
|
2007
|
+
events.push({
|
|
2008
|
+
type: "message_stop",
|
|
2009
|
+
usage: parsed.prompt_eval_count !== undefined || parsed.eval_count !== undefined ? {
|
|
2010
|
+
inputTokens: parsed.prompt_eval_count ?? 0,
|
|
2011
|
+
outputTokens: parsed.eval_count ?? 0
|
|
2012
|
+
} : undefined
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return events;
|
|
2017
|
+
}
|
|
2018
|
+
buildStreamChunk(event) {
|
|
2019
|
+
switch (event.type) {
|
|
2020
|
+
case "message_start":
|
|
2021
|
+
return `${JSON.stringify({
|
|
2022
|
+
model: event.model,
|
|
2023
|
+
message: { role: "assistant", content: "" },
|
|
2024
|
+
done: false
|
|
2025
|
+
})}
|
|
2026
|
+
`;
|
|
2027
|
+
case "content_delta":
|
|
2028
|
+
return `${JSON.stringify({
|
|
2029
|
+
message: { role: "assistant", content: event.text },
|
|
2030
|
+
done: false
|
|
2031
|
+
})}
|
|
2032
|
+
`;
|
|
2033
|
+
case "content_stop":
|
|
2034
|
+
return "";
|
|
2035
|
+
case "message_stop":
|
|
2036
|
+
return `${JSON.stringify({
|
|
2037
|
+
done: true,
|
|
2038
|
+
...event.usage ? {
|
|
2039
|
+
prompt_eval_count: event.usage.inputTokens,
|
|
2040
|
+
eval_count: event.usage.outputTokens
|
|
2041
|
+
} : {}
|
|
2042
|
+
})}
|
|
2043
|
+
`;
|
|
2044
|
+
case "error":
|
|
2045
|
+
return `${JSON.stringify({
|
|
2046
|
+
error: event.message,
|
|
2047
|
+
done: true
|
|
2048
|
+
})}
|
|
2049
|
+
`;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
var ollamaAdapter = new OllamaAdapter;
|
|
2054
|
+
registerAdapter(ollamaAdapter);
|
|
2055
|
+
|
|
2056
|
+
// src/utils/aws-sigv4.ts
|
|
2057
|
+
var import_node_crypto = require("node:crypto");
|
|
2058
|
+
var SERVICE = "bedrock";
|
|
2059
|
+
function sha256(data) {
|
|
2060
|
+
return import_node_crypto.createHash("sha256").update(data).digest("hex");
|
|
2061
|
+
}
|
|
2062
|
+
function hmacSha256(key, data) {
|
|
2063
|
+
return import_node_crypto.createHmac("sha256", key).update(data).digest();
|
|
2064
|
+
}
|
|
2065
|
+
function hmacSha256Hex(key, data) {
|
|
2066
|
+
return import_node_crypto.createHmac("sha256", key).update(data).digest("hex");
|
|
2067
|
+
}
|
|
2068
|
+
function awsUriEncode(str) {
|
|
2069
|
+
return encodeURIComponent(str).replace(/!/g, "%21").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/\*/g, "%2A");
|
|
2070
|
+
}
|
|
2071
|
+
function buildCanonicalUri(pathname) {
|
|
2072
|
+
if (pathname === "" || pathname === "/")
|
|
2073
|
+
return "/";
|
|
2074
|
+
const segments = pathname.split("/");
|
|
2075
|
+
return segments.map((segment) => segment === "" ? "" : awsUriEncode(awsUriEncode(segment))).join("/");
|
|
2076
|
+
}
|
|
2077
|
+
function formatDateStamp(date) {
|
|
2078
|
+
return date.toISOString().slice(0, 10).replace(/-/g, "");
|
|
2079
|
+
}
|
|
2080
|
+
function formatAmzDate(date) {
|
|
2081
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
2082
|
+
}
|
|
2083
|
+
function deriveSigningKey(secretKey, dateStamp, region, service) {
|
|
2084
|
+
const kDate = hmacSha256(`AWS4${secretKey}`, dateStamp);
|
|
2085
|
+
const kRegion = hmacSha256(kDate, region);
|
|
2086
|
+
const kService = hmacSha256(kRegion, service);
|
|
2087
|
+
return hmacSha256(kService, "aws4_request");
|
|
2088
|
+
}
|
|
2089
|
+
function signRequest(request, credentials, now) {
|
|
2090
|
+
const date = now ?? new Date;
|
|
2091
|
+
const dateStamp = formatDateStamp(date);
|
|
2092
|
+
const amzDate = formatAmzDate(date);
|
|
2093
|
+
const url = new URL(request.url);
|
|
2094
|
+
const canonicalUri = buildCanonicalUri(url.pathname);
|
|
2095
|
+
const sortedParams = [...url.searchParams.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
2096
|
+
const canonicalQueryString = sortedParams.map(([k, v]) => `${awsUriEncode(k)}=${awsUriEncode(v)}`).join("&");
|
|
2097
|
+
const payloadHash = sha256(request.body);
|
|
2098
|
+
const headersToSign = {};
|
|
2099
|
+
for (const [k, v] of Object.entries(request.headers)) {
|
|
2100
|
+
headersToSign[k.toLowerCase()] = v.trim();
|
|
2101
|
+
}
|
|
2102
|
+
headersToSign["host"] = url.host;
|
|
2103
|
+
headersToSign["x-amz-date"] = amzDate;
|
|
2104
|
+
headersToSign["x-amz-content-sha256"] = payloadHash;
|
|
2105
|
+
if (credentials.sessionToken) {
|
|
2106
|
+
headersToSign["x-amz-security-token"] = credentials.sessionToken;
|
|
2107
|
+
}
|
|
2108
|
+
const sortedHeaderKeys = Object.keys(headersToSign).sort();
|
|
2109
|
+
const canonicalHeaders = sortedHeaderKeys.map((k) => `${k}:${headersToSign[k]}`).join(`
|
|
2110
|
+
`) + `
|
|
2111
|
+
`;
|
|
2112
|
+
const signedHeaders = sortedHeaderKeys.join(";");
|
|
2113
|
+
const canonicalRequest = [
|
|
2114
|
+
request.method,
|
|
2115
|
+
canonicalUri,
|
|
2116
|
+
canonicalQueryString,
|
|
2117
|
+
canonicalHeaders,
|
|
2118
|
+
signedHeaders,
|
|
2119
|
+
payloadHash
|
|
2120
|
+
].join(`
|
|
2121
|
+
`);
|
|
2122
|
+
const credentialScope = `${dateStamp}/${credentials.region}/${SERVICE}/aws4_request`;
|
|
2123
|
+
const stringToSign = [
|
|
2124
|
+
"AWS4-HMAC-SHA256",
|
|
2125
|
+
amzDate,
|
|
2126
|
+
credentialScope,
|
|
2127
|
+
sha256(canonicalRequest)
|
|
2128
|
+
].join(`
|
|
2129
|
+
`);
|
|
2130
|
+
const signingKey = deriveSigningKey(credentials.secretAccessKey, dateStamp, credentials.region, SERVICE);
|
|
2131
|
+
const signature = hmacSha256Hex(signingKey, stringToSign);
|
|
2132
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${credentialScope}, ` + `SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
2133
|
+
const result = {
|
|
2134
|
+
Authorization: authorization,
|
|
2135
|
+
"x-amz-date": amzDate,
|
|
2136
|
+
"x-amz-content-sha256": payloadHash
|
|
2137
|
+
};
|
|
2138
|
+
if (credentials.sessionToken) {
|
|
2139
|
+
result["x-amz-security-token"] = credentials.sessionToken;
|
|
2140
|
+
}
|
|
2141
|
+
return result;
|
|
2142
|
+
}
|
|
2143
|
+
function extractRegionFromUrl(url) {
|
|
2144
|
+
const match = url.match(/\.([a-z0-9-]+)\.amazonaws\.com/);
|
|
2145
|
+
return match?.[1];
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/adapters/bedrock.ts
|
|
2149
|
+
function bedrockMessagesToStandard(messages) {
|
|
2150
|
+
return messages.map((m) => {
|
|
2151
|
+
if (typeof m.content === "string") {
|
|
2152
|
+
return { role: m.role, content: m.content };
|
|
2153
|
+
}
|
|
2154
|
+
const textParts = m.content.filter((b) => b.text !== undefined);
|
|
2155
|
+
if (textParts.length === 1) {
|
|
2156
|
+
return { role: m.role, content: textParts[0].text };
|
|
2157
|
+
}
|
|
2158
|
+
return { role: m.role, content: m.content };
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
function standardMessagesToBedrock(messages) {
|
|
2162
|
+
return messages.map((m) => {
|
|
2163
|
+
if (typeof m.content === "string") {
|
|
2164
|
+
return { role: m.role, content: [{ text: m.content }] };
|
|
2165
|
+
}
|
|
2166
|
+
if (Array.isArray(m.content)) {
|
|
2167
|
+
return { role: m.role, content: m.content };
|
|
2168
|
+
}
|
|
2169
|
+
return { role: m.role, content: [{ text: String(m.content) }] };
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
function mapBedrockStopReason(reason) {
|
|
2173
|
+
if (reason === null)
|
|
2174
|
+
return null;
|
|
2175
|
+
switch (reason) {
|
|
2176
|
+
case "end_turn":
|
|
2177
|
+
return "stop";
|
|
2178
|
+
case "max_tokens":
|
|
2179
|
+
return "max_tokens";
|
|
2180
|
+
case "content_filtered":
|
|
2181
|
+
return "content_filter";
|
|
2182
|
+
default:
|
|
2183
|
+
return reason;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function mapStopReasonToBedrock(reason) {
|
|
2187
|
+
if (reason === null)
|
|
2188
|
+
return "end_turn";
|
|
2189
|
+
switch (reason) {
|
|
2190
|
+
case "stop":
|
|
2191
|
+
return "end_turn";
|
|
2192
|
+
case "max_tokens":
|
|
2193
|
+
return "max_tokens";
|
|
2194
|
+
case "content_filter":
|
|
2195
|
+
return "content_filtered";
|
|
2196
|
+
default:
|
|
2197
|
+
return reason;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
class BedrockAdapter {
|
|
2202
|
+
apiType = "bedrock-converse-stream";
|
|
2203
|
+
parseRequest(body) {
|
|
2204
|
+
const raw = body;
|
|
2205
|
+
const model = raw.modelId ?? "";
|
|
2206
|
+
const messages = raw.messages ? bedrockMessagesToStandard(raw.messages) : [];
|
|
2207
|
+
let system;
|
|
2208
|
+
if (raw.system && raw.system.length > 0) {
|
|
2209
|
+
if (raw.system.length === 1) {
|
|
2210
|
+
system = raw.system[0].text;
|
|
2211
|
+
} else {
|
|
2212
|
+
system = raw.system.map((s) => ({ type: "text", text: s.text }));
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return {
|
|
2216
|
+
model,
|
|
2217
|
+
messages,
|
|
2218
|
+
system,
|
|
2219
|
+
stream: true,
|
|
2220
|
+
maxTokens: raw.inferenceConfig?.maxTokens,
|
|
2221
|
+
rawBody: raw
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
2225
|
+
const bedrockMessages = standardMessagesToBedrock(parsed.messages);
|
|
2226
|
+
const requestBody = {
|
|
2227
|
+
...parsed.rawBody,
|
|
2228
|
+
messages: bedrockMessages
|
|
2229
|
+
};
|
|
2230
|
+
delete requestBody.modelId;
|
|
2231
|
+
if (parsed.system) {
|
|
2232
|
+
if (typeof parsed.system === "string") {
|
|
2233
|
+
requestBody.system = [{ text: parsed.system }];
|
|
2234
|
+
} else {
|
|
2235
|
+
requestBody.system = parsed.system.map((s) => ({ text: s.text }));
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
if (parsed.maxTokens !== undefined) {
|
|
2239
|
+
requestBody.inferenceConfig = {
|
|
2240
|
+
...requestBody.inferenceConfig,
|
|
2241
|
+
maxTokens: parsed.maxTokens
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
const url = `${baseUrl}/model/${targetModel}/converse-stream`;
|
|
2245
|
+
const body = JSON.stringify(requestBody);
|
|
2246
|
+
const headers = {
|
|
2247
|
+
"Content-Type": "application/json"
|
|
2248
|
+
};
|
|
2249
|
+
if (auth.awsAccessKeyId && auth.awsSecretKey && auth.awsRegion) {
|
|
2250
|
+
const sigv4Headers = signRequest({ method: "POST", url, headers, body }, {
|
|
2251
|
+
accessKeyId: auth.awsAccessKeyId,
|
|
2252
|
+
secretAccessKey: auth.awsSecretKey,
|
|
2253
|
+
sessionToken: auth.awsSessionToken,
|
|
2254
|
+
region: auth.awsRegion
|
|
2255
|
+
});
|
|
2256
|
+
Object.assign(headers, sigv4Headers);
|
|
2257
|
+
} else if (auth.apiKey) {
|
|
2258
|
+
headers[auth.headerName || "Authorization"] = auth.headerValue || auth.apiKey;
|
|
2259
|
+
}
|
|
2260
|
+
return { url, method: "POST", headers, body };
|
|
2261
|
+
}
|
|
2262
|
+
modifyMessages(rawBody, compressedMessages) {
|
|
2263
|
+
return {
|
|
2264
|
+
...rawBody,
|
|
2265
|
+
messages: standardMessagesToBedrock(compressedMessages)
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
parseResponse(body) {
|
|
2269
|
+
const raw = body;
|
|
2270
|
+
const output = raw.output;
|
|
2271
|
+
const message = output?.message;
|
|
2272
|
+
const text = message?.content?.filter((b) => b.text !== undefined).map((b) => b.text).join("") ?? "";
|
|
2273
|
+
const stopReason = raw.stopReason;
|
|
2274
|
+
const usage = raw.usage;
|
|
2275
|
+
return {
|
|
2276
|
+
id: raw.requestId ?? `bedrock-${Date.now()}`,
|
|
2277
|
+
model: raw.modelId ?? "",
|
|
2278
|
+
content: text,
|
|
2279
|
+
role: "assistant",
|
|
2280
|
+
stopReason: mapBedrockStopReason(stopReason ?? null),
|
|
2281
|
+
usage: usage ? {
|
|
2282
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
2283
|
+
outputTokens: usage.outputTokens ?? 0
|
|
2284
|
+
} : undefined
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
buildResponse(parsed) {
|
|
2288
|
+
const result = {
|
|
2289
|
+
output: {
|
|
2290
|
+
message: {
|
|
2291
|
+
role: "assistant",
|
|
2292
|
+
content: [{ text: parsed.content }]
|
|
2293
|
+
}
|
|
2294
|
+
},
|
|
2295
|
+
stopReason: mapStopReasonToBedrock(parsed.stopReason)
|
|
2296
|
+
};
|
|
2297
|
+
if (parsed.usage) {
|
|
2298
|
+
result.usage = {
|
|
2299
|
+
inputTokens: parsed.usage.inputTokens,
|
|
2300
|
+
outputTokens: parsed.usage.outputTokens
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
return result;
|
|
2304
|
+
}
|
|
2305
|
+
parseStreamChunk(chunk) {
|
|
2306
|
+
const events = [];
|
|
2307
|
+
const lines = chunk.split(`
|
|
2308
|
+
`);
|
|
2309
|
+
for (const line of lines) {
|
|
2310
|
+
const trimmed = line.trim();
|
|
2311
|
+
if (trimmed === "")
|
|
2312
|
+
continue;
|
|
2313
|
+
let parsed;
|
|
2314
|
+
try {
|
|
2315
|
+
parsed = JSON.parse(trimmed);
|
|
2316
|
+
} catch {
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
if (parsed.messageStart !== undefined) {
|
|
2320
|
+
const start = parsed.messageStart;
|
|
2321
|
+
events.push({
|
|
2322
|
+
type: "message_start",
|
|
2323
|
+
id: `bedrock-${Date.now()}`,
|
|
2324
|
+
model: ""
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
if (parsed.contentBlockDelta !== undefined) {
|
|
2328
|
+
const delta = parsed.contentBlockDelta;
|
|
2329
|
+
events.push({
|
|
2330
|
+
type: "content_delta",
|
|
2331
|
+
text: delta.delta?.text ?? "",
|
|
2332
|
+
index: delta.contentBlockIndex ?? 0
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
if (parsed.contentBlockStop !== undefined) {
|
|
2336
|
+
const stop = parsed.contentBlockStop;
|
|
2337
|
+
events.push({
|
|
2338
|
+
type: "content_stop",
|
|
2339
|
+
index: stop.contentBlockIndex ?? 0
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
if (parsed.messageStop !== undefined) {
|
|
2343
|
+
const stop = parsed.messageStop;
|
|
2344
|
+
events.push({
|
|
2345
|
+
type: "message_stop",
|
|
2346
|
+
usage: undefined
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
if (parsed.metadata !== undefined) {
|
|
2350
|
+
const meta = parsed.metadata;
|
|
2351
|
+
if (meta.usage) {
|
|
2352
|
+
events.push({
|
|
2353
|
+
type: "message_stop",
|
|
2354
|
+
usage: {
|
|
2355
|
+
inputTokens: meta.usage.inputTokens ?? 0,
|
|
2356
|
+
outputTokens: meta.usage.outputTokens ?? 0
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return events;
|
|
2363
|
+
}
|
|
2364
|
+
buildStreamChunk(event) {
|
|
2365
|
+
switch (event.type) {
|
|
2366
|
+
case "message_start":
|
|
2367
|
+
return `${JSON.stringify({
|
|
2368
|
+
messageStart: { role: "assistant" }
|
|
2369
|
+
})}
|
|
2370
|
+
`;
|
|
2371
|
+
case "content_delta":
|
|
2372
|
+
return `${JSON.stringify({
|
|
2373
|
+
contentBlockDelta: {
|
|
2374
|
+
delta: { text: event.text },
|
|
2375
|
+
contentBlockIndex: event.index
|
|
2376
|
+
}
|
|
2377
|
+
})}
|
|
2378
|
+
`;
|
|
2379
|
+
case "content_stop":
|
|
2380
|
+
return `${JSON.stringify({
|
|
2381
|
+
contentBlockStop: { contentBlockIndex: event.index }
|
|
2382
|
+
})}
|
|
2383
|
+
`;
|
|
2384
|
+
case "message_stop":
|
|
2385
|
+
if (event.usage) {
|
|
2386
|
+
return `${JSON.stringify({
|
|
2387
|
+
messageStop: { stopReason: "end_turn" }
|
|
2388
|
+
})}
|
|
2389
|
+
${JSON.stringify({
|
|
2390
|
+
metadata: {
|
|
2391
|
+
usage: {
|
|
2392
|
+
inputTokens: event.usage.inputTokens,
|
|
2393
|
+
outputTokens: event.usage.outputTokens
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
})}
|
|
2397
|
+
`;
|
|
2398
|
+
}
|
|
2399
|
+
return `${JSON.stringify({
|
|
2400
|
+
messageStop: { stopReason: "end_turn" }
|
|
2401
|
+
})}
|
|
2402
|
+
`;
|
|
2403
|
+
case "error":
|
|
2404
|
+
return `${JSON.stringify({
|
|
2405
|
+
error: { message: event.message }
|
|
2406
|
+
})}
|
|
2407
|
+
`;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
var bedrockAdapter = new BedrockAdapter;
|
|
2412
|
+
registerAdapter(bedrockAdapter);
|
|
2413
|
+
|
|
2414
|
+
// src/adapters/openai-codex.ts
|
|
2415
|
+
class OpenAICodexAdapter {
|
|
2416
|
+
apiType = "openai-codex-responses";
|
|
2417
|
+
parseRequest(body) {
|
|
2418
|
+
return parseOpenAIBody(body);
|
|
2419
|
+
}
|
|
2420
|
+
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
2421
|
+
const { rawBody } = parsed;
|
|
2422
|
+
const upstreamBody = { ...rawBody };
|
|
2423
|
+
upstreamBody.model = targetModel;
|
|
2424
|
+
upstreamBody.stream = true;
|
|
2425
|
+
upstreamBody.store = false;
|
|
2426
|
+
if (!upstreamBody.instructions) {
|
|
2427
|
+
upstreamBody.instructions = upstreamBody.system ?? "You are a helpful assistant.";
|
|
2428
|
+
}
|
|
2429
|
+
delete upstreamBody.system;
|
|
2430
|
+
if (!upstreamBody.input && upstreamBody.messages) {
|
|
2431
|
+
upstreamBody.input = upstreamBody.messages;
|
|
2432
|
+
delete upstreamBody.messages;
|
|
2433
|
+
}
|
|
2434
|
+
delete upstreamBody.max_tokens;
|
|
2435
|
+
delete upstreamBody.max_output_tokens;
|
|
2436
|
+
if (upstreamBody.tools) {
|
|
2437
|
+
upstreamBody.tools = toOpenAITools(upstreamBody.tools);
|
|
2438
|
+
}
|
|
2439
|
+
return {
|
|
2440
|
+
url: `${baseUrl}/codex/responses`,
|
|
2441
|
+
method: "POST",
|
|
2442
|
+
headers: {
|
|
2443
|
+
"Content-Type": "application/json",
|
|
2444
|
+
Authorization: `Bearer ${auth.apiKey}`
|
|
2445
|
+
},
|
|
2446
|
+
body: JSON.stringify(upstreamBody)
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
modifyMessages(rawBody, compressedMessages) {
|
|
2450
|
+
return openaiResponsesAdapter.modifyMessages(rawBody, compressedMessages);
|
|
2451
|
+
}
|
|
2452
|
+
parseResponse(body) {
|
|
2453
|
+
return openaiResponsesAdapter.parseResponse(body);
|
|
2454
|
+
}
|
|
2455
|
+
buildResponse(parsed) {
|
|
2456
|
+
return openaiResponsesAdapter.buildResponse(parsed);
|
|
2457
|
+
}
|
|
2458
|
+
parseStreamChunk(chunk) {
|
|
2459
|
+
return openaiResponsesAdapter.parseStreamChunk(chunk);
|
|
2460
|
+
}
|
|
2461
|
+
buildStreamChunk(event) {
|
|
2462
|
+
return openaiResponsesAdapter.buildStreamChunk(event);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
var openaiCodexAdapter = new OpenAICodexAdapter;
|
|
2466
|
+
registerAdapter(openaiCodexAdapter);
|
|
2467
|
+
|
|
2468
|
+
// src/compression/compaction-detector.ts
|
|
2469
|
+
var COMPACTION_PATTERNS = [
|
|
2470
|
+
"merge these partial summaries into a single cohesive summary",
|
|
2471
|
+
"preserve all opaque identifiers exactly as written",
|
|
2472
|
+
"your task is to create a detailed summary of the conversation so far",
|
|
2473
|
+
"do not use any tools. you must respond with only the <summary>",
|
|
2474
|
+
"important: do not use any tools",
|
|
2475
|
+
"summarize the conversation",
|
|
2476
|
+
"create a summary of our conversation",
|
|
2477
|
+
"compact the conversation"
|
|
2478
|
+
];
|
|
2479
|
+
function extractTextFromContent(content) {
|
|
2480
|
+
if (typeof content === "string")
|
|
2481
|
+
return content;
|
|
2482
|
+
if (Array.isArray(content)) {
|
|
2483
|
+
return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(`
|
|
2484
|
+
`);
|
|
2485
|
+
}
|
|
2486
|
+
return "";
|
|
2487
|
+
}
|
|
2488
|
+
function detectCompaction(headers, messages) {
|
|
2489
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2490
|
+
if (key.toLowerCase() === "x-request-compaction" && value === "true") {
|
|
2491
|
+
return { isCompaction: true, detectedBy: "header", confidence: 1 };
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
let lastUserMessage;
|
|
2495
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
2496
|
+
if (messages[i].role === "user") {
|
|
2497
|
+
lastUserMessage = messages[i];
|
|
2498
|
+
break;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (lastUserMessage) {
|
|
2502
|
+
const text = extractTextFromContent(lastUserMessage.content).toLowerCase();
|
|
2503
|
+
for (const pattern of COMPACTION_PATTERNS) {
|
|
2504
|
+
if (text.includes(pattern)) {
|
|
2505
|
+
return { isCompaction: true, detectedBy: "prompt_pattern", confidence: 0.95 };
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
return { isCompaction: false, detectedBy: "none", confidence: 0 };
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
// src/utils/token-estimator.ts
|
|
2513
|
+
var TOKENS_PER_CJK = 2.5;
|
|
2514
|
+
var CHARS_PER_ASCII_TOKEN = 4;
|
|
2515
|
+
var MESSAGE_OVERHEAD = 4;
|
|
2516
|
+
function isCJK(charCode) {
|
|
2517
|
+
return charCode >= 12288 && charCode <= 40959 || charCode >= 44032 && charCode <= 55215 || charCode >= 63744 && charCode <= 64255;
|
|
2518
|
+
}
|
|
2519
|
+
function estimateTokens(text) {
|
|
2520
|
+
if (text.length === 0)
|
|
2521
|
+
return 0;
|
|
2522
|
+
let asciiSegmentLength = 0;
|
|
2523
|
+
let tokenCount = 0;
|
|
2524
|
+
for (let i = 0;i < text.length; i++) {
|
|
2525
|
+
const code = text.charCodeAt(i);
|
|
2526
|
+
if (isCJK(code)) {
|
|
2527
|
+
tokenCount += asciiSegmentLength / CHARS_PER_ASCII_TOKEN;
|
|
2528
|
+
asciiSegmentLength = 0;
|
|
2529
|
+
tokenCount += TOKENS_PER_CJK;
|
|
2530
|
+
} else {
|
|
2531
|
+
asciiSegmentLength++;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
tokenCount += asciiSegmentLength / CHARS_PER_ASCII_TOKEN;
|
|
2535
|
+
return Math.ceil(tokenCount);
|
|
2536
|
+
}
|
|
2537
|
+
function estimateMessagesTokens(messages) {
|
|
2538
|
+
let total = 0;
|
|
2539
|
+
for (const message of messages) {
|
|
2540
|
+
total += MESSAGE_OVERHEAD;
|
|
2541
|
+
if (typeof message.content === "string") {
|
|
2542
|
+
total += estimateTokens(message.content);
|
|
2543
|
+
} else if (Array.isArray(message.content)) {
|
|
2544
|
+
for (const block of message.content) {
|
|
2545
|
+
if (block.type === "text" && block.text !== undefined) {
|
|
2546
|
+
total += estimateTokens(block.text);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
return total;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// src/compression/synthetic-response.ts
|
|
2555
|
+
function messageContentToString(content) {
|
|
2556
|
+
if (typeof content === "string")
|
|
2557
|
+
return content;
|
|
2558
|
+
if (Array.isArray(content)) {
|
|
2559
|
+
return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(`
|
|
2560
|
+
`);
|
|
2561
|
+
}
|
|
2562
|
+
return JSON.stringify(content);
|
|
2563
|
+
}
|
|
2564
|
+
function formatRecentMessages(messages) {
|
|
2565
|
+
return messages.map((m) => `[${m.role}]: ${messageContentToString(m.content)}`).join(`
|
|
2566
|
+
|
|
2567
|
+
`);
|
|
2568
|
+
}
|
|
2569
|
+
function buildSyntheticSummaryResponse(summary, recentMessages, model) {
|
|
2570
|
+
const recentText = formatRecentMessages(recentMessages);
|
|
2571
|
+
const content = `<summary>
|
|
2572
|
+
${summary}
|
|
2573
|
+
</summary>
|
|
2574
|
+
|
|
2575
|
+
<recent_messages>
|
|
2576
|
+
${recentText}
|
|
2577
|
+
</recent_messages>`;
|
|
2578
|
+
return {
|
|
2579
|
+
id: `msg_precomputed_${Date.now()}`,
|
|
2580
|
+
model,
|
|
2581
|
+
content,
|
|
2582
|
+
role: "assistant",
|
|
2583
|
+
stopReason: "end_turn",
|
|
2584
|
+
usage: { inputTokens: 0, outputTokens: estimateTokens(content) }
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
function buildSyntheticHttpResponse(parsed, adapter) {
|
|
2588
|
+
const body = adapter.buildResponse ? adapter.buildResponse(parsed) : {
|
|
2589
|
+
id: parsed.id,
|
|
2590
|
+
type: "message",
|
|
2591
|
+
role: "assistant",
|
|
2592
|
+
model: parsed.model,
|
|
2593
|
+
content: [{ type: "text", text: parsed.content }],
|
|
2594
|
+
stop_reason: parsed.stopReason,
|
|
2595
|
+
usage: parsed.usage ? {
|
|
2596
|
+
input_tokens: parsed.usage.inputTokens,
|
|
2597
|
+
output_tokens: parsed.usage.outputTokens
|
|
2598
|
+
} : undefined
|
|
2599
|
+
};
|
|
2600
|
+
return new Response(JSON.stringify(body), {
|
|
2601
|
+
status: 200,
|
|
2602
|
+
headers: {
|
|
2603
|
+
"content-type": "application/json",
|
|
2604
|
+
"x-synthetic-response": "true"
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// src/routing/local-classifier.ts
|
|
2610
|
+
var import_transformers = require("@huggingface/transformers");
|
|
2611
|
+
var CAT_L = "L";
|
|
2612
|
+
var CAT_M = "M";
|
|
2613
|
+
var CAT_H = "H";
|
|
2614
|
+
var CAT_Q = "Q";
|
|
2615
|
+
var TIER_MAP = {
|
|
2616
|
+
L: "LIGHT",
|
|
2617
|
+
M: "MEDIUM",
|
|
2618
|
+
H: "HEAVY"
|
|
2619
|
+
};
|
|
2620
|
+
var MODEL_ID = "Xenova/multilingual-e5-small";
|
|
2621
|
+
var E5_PREFIX = "query: ";
|
|
2622
|
+
var BATCH_SIZE = 32;
|
|
2623
|
+
var TRAINING_LIGHT = [
|
|
2624
|
+
"안녕하세요",
|
|
2625
|
+
"안녕",
|
|
2626
|
+
"안녕히 가세요",
|
|
2627
|
+
"안녕히 계세요",
|
|
2628
|
+
"반갑습니다",
|
|
2629
|
+
"잘 지내시죠",
|
|
2630
|
+
"오랜만이에요",
|
|
2631
|
+
"고마워",
|
|
2632
|
+
"감사합니다",
|
|
2633
|
+
"고맙습니다",
|
|
2634
|
+
"네 고마워요",
|
|
2635
|
+
"정말 감사합니다",
|
|
2636
|
+
"도와줘서 고마워",
|
|
2637
|
+
"네",
|
|
2638
|
+
"예",
|
|
2639
|
+
"아니요",
|
|
2640
|
+
"좋아요",
|
|
2641
|
+
"알겠습니다",
|
|
2642
|
+
"확인했습니다",
|
|
2643
|
+
"그래요",
|
|
2644
|
+
"맞아요",
|
|
2645
|
+
"아 네",
|
|
2646
|
+
"Python이 뭐야?",
|
|
2647
|
+
"JavaScript가 뭐야?",
|
|
2648
|
+
"오늘 날씨 어때?",
|
|
2649
|
+
"지금 몇 시야?",
|
|
2650
|
+
"이거 뭐야?",
|
|
2651
|
+
"TypeScript가 뭐예요?",
|
|
2652
|
+
"API가 뭐야?",
|
|
2653
|
+
"HTML이 뭐야?",
|
|
2654
|
+
"CSS가 뭐야?",
|
|
2655
|
+
"Hello",
|
|
2656
|
+
"Hi",
|
|
2657
|
+
"Hey there",
|
|
2658
|
+
"Good morning",
|
|
2659
|
+
"Good afternoon",
|
|
2660
|
+
"How are you",
|
|
2661
|
+
"What's up",
|
|
2662
|
+
"Thanks",
|
|
2663
|
+
"Thank you",
|
|
2664
|
+
"Got it",
|
|
2665
|
+
"OK",
|
|
2666
|
+
"Sounds good",
|
|
2667
|
+
"I see",
|
|
2668
|
+
"Understood",
|
|
2669
|
+
"Great thanks",
|
|
2670
|
+
"What is Python?",
|
|
2671
|
+
"What time is it?",
|
|
2672
|
+
"What's the weather?",
|
|
2673
|
+
"Who is Einstein?",
|
|
2674
|
+
"Where is Seoul?",
|
|
2675
|
+
"How old are you?",
|
|
2676
|
+
"yes",
|
|
2677
|
+
"no",
|
|
2678
|
+
"maybe",
|
|
2679
|
+
"sure",
|
|
2680
|
+
"please",
|
|
2681
|
+
"done",
|
|
2682
|
+
"ok",
|
|
2683
|
+
"cool",
|
|
2684
|
+
"nice",
|
|
2685
|
+
"awesome"
|
|
2686
|
+
];
|
|
2687
|
+
var TRAINING_MEDIUM = [
|
|
2688
|
+
"Write a quicksort function in TypeScript",
|
|
2689
|
+
"Implement a binary search tree with insert and delete",
|
|
2690
|
+
"Create a REST API endpoint for user authentication",
|
|
2691
|
+
"Write a function to merge two sorted arrays",
|
|
2692
|
+
"Implement a linked list in Python",
|
|
2693
|
+
"Write a unit test for the calculator module",
|
|
2694
|
+
"Create a simple Express.js middleware for logging",
|
|
2695
|
+
"Write a regex to validate email addresses",
|
|
2696
|
+
"Implement a LRU cache with get and put operations",
|
|
2697
|
+
"Create a React component for a todo list",
|
|
2698
|
+
"Write a SQL query to join two tables",
|
|
2699
|
+
"Implement a basic JWT authentication flow",
|
|
2700
|
+
"Write a function to parse CSV files",
|
|
2701
|
+
"Create a simple WebSocket server",
|
|
2702
|
+
"Implement bubble sort in Java",
|
|
2703
|
+
"Write a Python script to read a JSON file",
|
|
2704
|
+
"Create a Docker compose file for a web app",
|
|
2705
|
+
"Write a Git pre-commit hook",
|
|
2706
|
+
"REST API에 로그인 엔드포인트 추가해줘",
|
|
2707
|
+
"이 함수에 에러 핸들링 추가해줘",
|
|
2708
|
+
"TypeScript로 이벤트 이미터 만들어줘",
|
|
2709
|
+
"데이터베이스 마이그레이션 스크립트 작성해줘",
|
|
2710
|
+
"React 컴포넌트에 상태 관리 추가해줘",
|
|
2711
|
+
"Express 라우터에 CORS 미들웨어 추가해줘",
|
|
2712
|
+
"테스트 코드 작성해줘",
|
|
2713
|
+
"이 코드 리팩토링해줘",
|
|
2714
|
+
"Explain the difference between let and const in JavaScript",
|
|
2715
|
+
"What's the difference between SQL and NoSQL databases",
|
|
2716
|
+
"Explain how async await works in Python",
|
|
2717
|
+
"Describe the MVC architecture pattern",
|
|
2718
|
+
"Explain what Docker containers are",
|
|
2719
|
+
"REST와 GraphQL의 차이점을 설명해줘",
|
|
2720
|
+
"이벤트 루프가 어떻게 동작하는지 설명해줘",
|
|
2721
|
+
"클로저가 뭐야? 설명해줘",
|
|
2722
|
+
"Set up a Node.js project with TypeScript and ESLint",
|
|
2723
|
+
"Create a basic CI/CD pipeline using GitHub Actions",
|
|
2724
|
+
"Configure Nginx as a reverse proxy for a Node.js app",
|
|
2725
|
+
`이 함수를 리팩토링해줘:
|
|
2726
|
+
function processUsers(data) {
|
|
2727
|
+
var result = [];
|
|
2728
|
+
for (var i = 0; i < data.length; i++) {
|
|
2729
|
+
if (data[i].active == true && data[i].age > 18) {
|
|
2730
|
+
var name = data[i].firstName + ' ' + data[i].lastName;
|
|
2731
|
+
var obj = { name: name, email: data[i].email, role: data[i].isAdmin ? 'admin' : 'user' };
|
|
2732
|
+
if (data[i].department !== null && data[i].department !== undefined) {
|
|
2733
|
+
obj.department = data[i].department.name;
|
|
2734
|
+
obj.manager = data[i].department.manager ? data[i].department.manager.name : 'N/A';
|
|
2735
|
+
}
|
|
2736
|
+
result.push(obj);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
result.sort(function(a, b) { return a.name > b.name ? 1 : -1; });
|
|
2740
|
+
return result;
|
|
2741
|
+
}`,
|
|
2742
|
+
`Refactor this code to use modern JavaScript:
|
|
2743
|
+
function getItems(list) {
|
|
2744
|
+
var items = [];
|
|
2745
|
+
for (var i = 0; i < list.length; i++) {
|
|
2746
|
+
if (list[i].active === true) {
|
|
2747
|
+
items.push(list[i].name);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return items;
|
|
2751
|
+
}`
|
|
2752
|
+
];
|
|
2753
|
+
var TRAINING_HEAVY = [
|
|
2754
|
+
"Design a distributed consensus algorithm for a multi-region database with strong consistency and Byzantine fault tolerance",
|
|
2755
|
+
"Explain the theoretical foundations of quantum computing and how quantum entanglement can be used for cryptographic key distribution",
|
|
2756
|
+
"Analyze the trade-offs between eventual consistency and strong consistency in distributed systems, including CAP theorem implications",
|
|
2757
|
+
"Design a fault-tolerant microservices architecture for a real-time trading platform handling millions of transactions per second",
|
|
2758
|
+
"Propose a novel approach to solving the traveling salesman problem that improves upon current approximation algorithms",
|
|
2759
|
+
"Design a machine learning pipeline for real-time fraud detection in financial transactions with sub-millisecond latency requirements",
|
|
2760
|
+
"Compare and contrast different consensus protocols (Paxos, Raft, PBFT) and recommend the best one for a blockchain-based supply chain system",
|
|
2761
|
+
"Architect a system that can handle 10 million concurrent WebSocket connections with horizontal scaling",
|
|
2762
|
+
"Design a real-time data streaming architecture combining Kafka, Flink, and a time-series database for IoT sensor data",
|
|
2763
|
+
"메모리 릭이 발생하는데 프로파일러에서 이벤트 루프 블로킹과 GC 지연이 동시에 나타나. 마이크로서비스 간 gRPC 연결 풀링도 의심되는 상황인데 원인 분석 방법을 단계별로 설명해줘",
|
|
2764
|
+
"대규모 분산 시스템에서 파티션 톨런스와 일관성을 동시에 보장하는 방법을 설계해줘",
|
|
2765
|
+
"실시간 추천 시스템을 위한 아키텍처를 설계해줘. 1초 이내에 개인화된 추천을 제공해야 해",
|
|
2766
|
+
"카프카 기반 이벤트 드리븐 아키텍처에서 순서 보장과 정확히 한 번 처리를 어떻게 보장할 수 있을까?",
|
|
2767
|
+
"마이크로서비스 간의 분산 트랜잭션을 사가 패턴으로 구현하는 방법을 단계별로 설명해줘",
|
|
2768
|
+
"Debug a memory leak in a production Node.js application where the heap grows indefinitely but garbage collection logs show normal behavior",
|
|
2769
|
+
"Investigate why our Kubernetes pods are being OOMKilled despite having memory limits set to 4GB and actual usage reported as 2GB",
|
|
2770
|
+
"Find the root cause of intermittent 500ms latency spikes in our PostgreSQL queries that happen every 15 minutes",
|
|
2771
|
+
"Design a multi-tenant SaaS platform with shared infrastructure but isolated data, supporting custom domains and white-labeling",
|
|
2772
|
+
"Implement a distributed task scheduler that guarantees at-least-once execution with idempotency support across multiple data centers"
|
|
2773
|
+
];
|
|
2774
|
+
var TRAINING_Q = [
|
|
2775
|
+
"아까 그거 다시 해줘",
|
|
2776
|
+
"그거 좀 더 자세히 설명해줘",
|
|
2777
|
+
"아까 말한 거 그대로 해줘",
|
|
2778
|
+
"이거 수정해줘",
|
|
2779
|
+
"저거 어디 있지",
|
|
2780
|
+
"그거 어떻게 됐어",
|
|
2781
|
+
"위에꺼 다시 한번",
|
|
2782
|
+
"그거 그대로 해줘",
|
|
2783
|
+
"아까 한 거 다시",
|
|
2784
|
+
"그 코드 다시 보여줘",
|
|
2785
|
+
"저번에 한 거 기억나?",
|
|
2786
|
+
"그 부분 수정해줘",
|
|
2787
|
+
"Do that again",
|
|
2788
|
+
"What about the thing we discussed earlier",
|
|
2789
|
+
"Show me that again",
|
|
2790
|
+
"Can you fix that",
|
|
2791
|
+
"Change it like I said before",
|
|
2792
|
+
"Continue from where we left off",
|
|
2793
|
+
"That thing from earlier, do it again",
|
|
2794
|
+
"Remember what we were working on",
|
|
2795
|
+
"Go back to the previous one",
|
|
2796
|
+
"Make it like the other one",
|
|
2797
|
+
"The same thing but different",
|
|
2798
|
+
"Update the one from before",
|
|
2799
|
+
"그거 해줘",
|
|
2800
|
+
"이거 해줘",
|
|
2801
|
+
"저거 어때",
|
|
2802
|
+
"How about this one",
|
|
2803
|
+
"What about that",
|
|
2804
|
+
"Try the other approach",
|
|
2805
|
+
"Use the one I mentioned",
|
|
2806
|
+
"Fix the issue",
|
|
2807
|
+
"그냥 그거",
|
|
2808
|
+
"이건 어때",
|
|
2809
|
+
"Make it better",
|
|
2810
|
+
"Change it",
|
|
2811
|
+
"이거 수정해"
|
|
2812
|
+
];
|
|
2813
|
+
var Q_PATTERNS = [
|
|
2814
|
+
/^(아까|그거|저거|이거|그|위에|아래|저번|이전|전에).*(다시|해줘|해|보여|설명|수정|변경|삭제|추가|해봐)/,
|
|
2815
|
+
/^(그거|저거|이거|그|이|저)(만|만큼|대로|처럼|같이)?\s*(해줘|해|놔|둬|봐|어때|어떻게)/,
|
|
2816
|
+
/^(그거|저거|이거)\s*$/,
|
|
2817
|
+
/(아까|저번에|전에|위에서|앞에서|이전에).*(그|그거|그것|그때|했던|말한)/,
|
|
2818
|
+
/^(이거|저거|그거)(\s*.*)?$/
|
|
2819
|
+
];
|
|
2820
|
+
var DEICTIC_WORDS = new Set(["그거", "저거", "이거", "그것", "이것", "저것", "아까", "저번"]);
|
|
2821
|
+
function matchesQPattern(text) {
|
|
2822
|
+
const trimmed = text.trim();
|
|
2823
|
+
for (const pattern of Q_PATTERNS) {
|
|
2824
|
+
if (pattern.test(trimmed))
|
|
2825
|
+
return true;
|
|
2826
|
+
}
|
|
2827
|
+
if (trimmed.length < 20) {
|
|
2828
|
+
for (const word of DEICTIC_WORDS) {
|
|
2829
|
+
if (trimmed.includes(word))
|
|
2830
|
+
return true;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return false;
|
|
2834
|
+
}
|
|
2835
|
+
var CODE_PATTERN = /[{}();]|function |const |let |var |class |import |export |=>|\bdef \b|\bfn\b/;
|
|
2836
|
+
var TECH_TERMS = /\b(implement|create|design|architect|debug|refactor|migrate|deploy|build|write|develop)\b/i;
|
|
2837
|
+
function isLikelyLight(text) {
|
|
2838
|
+
const trimmed = text.trim();
|
|
2839
|
+
if (trimmed.length <= 20 && !CODE_PATTERN.test(trimmed) && !TECH_TERMS.test(trimmed)) {
|
|
2840
|
+
return true;
|
|
2841
|
+
}
|
|
2842
|
+
return false;
|
|
2843
|
+
}
|
|
2844
|
+
var extractorPromise = null;
|
|
2845
|
+
function getExtractor() {
|
|
2846
|
+
if (!extractorPromise) {
|
|
2847
|
+
console.log("[clawmux] Loading embedding model...");
|
|
2848
|
+
extractorPromise = import_transformers.pipeline("feature-extraction", MODEL_ID).then((pipe) => {
|
|
2849
|
+
console.log("[clawmux] Embedding model loaded");
|
|
2850
|
+
return pipe;
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
return extractorPromise;
|
|
2854
|
+
}
|
|
2855
|
+
var centroidsPromise = null;
|
|
2856
|
+
async function computeMeanEmbedding(texts) {
|
|
2857
|
+
const extractor = await getExtractor();
|
|
2858
|
+
const allEmbeddings = [];
|
|
2859
|
+
for (let i = 0;i < texts.length; i += BATCH_SIZE) {
|
|
2860
|
+
const batch = texts.slice(i, i + BATCH_SIZE).map((t) => E5_PREFIX + t);
|
|
2861
|
+
const output = await extractor(batch, { pooling: "mean", normalize: true });
|
|
2862
|
+
const list = output.tolist();
|
|
2863
|
+
for (const emb of list) {
|
|
2864
|
+
allEmbeddings.push(emb);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
if (allEmbeddings.length === 0)
|
|
2868
|
+
return [];
|
|
2869
|
+
const dim = allEmbeddings[0].length;
|
|
2870
|
+
const mean = new Array(dim).fill(0);
|
|
2871
|
+
for (const emb of allEmbeddings) {
|
|
2872
|
+
for (let j = 0;j < dim; j++) {
|
|
2873
|
+
mean[j] += emb[j] / allEmbeddings.length;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
const magnitude = Math.sqrt(mean.reduce((sum, v) => sum + v * v, 0));
|
|
2877
|
+
if (magnitude > 0) {
|
|
2878
|
+
for (let j = 0;j < dim; j++)
|
|
2879
|
+
mean[j] /= magnitude;
|
|
2880
|
+
}
|
|
2881
|
+
return mean;
|
|
2882
|
+
}
|
|
2883
|
+
function getCentroids() {
|
|
2884
|
+
if (!centroidsPromise) {
|
|
2885
|
+
centroidsPromise = (async () => {
|
|
2886
|
+
console.log("[clawmux] Computing category centroids...");
|
|
2887
|
+
const [cL, cM, cH, cQ] = await Promise.all([
|
|
2888
|
+
computeMeanEmbedding(TRAINING_LIGHT),
|
|
2889
|
+
computeMeanEmbedding(TRAINING_MEDIUM),
|
|
2890
|
+
computeMeanEmbedding(TRAINING_HEAVY),
|
|
2891
|
+
computeMeanEmbedding(TRAINING_Q)
|
|
2892
|
+
]);
|
|
2893
|
+
console.log(`[clawmux] Centroids ready: L=${TRAINING_LIGHT.length} M=${TRAINING_MEDIUM.length} ` + `H=${TRAINING_HEAVY.length} Q=${TRAINING_Q.length} samples`);
|
|
2894
|
+
return { [CAT_L]: cL, [CAT_M]: cM, [CAT_H]: cH, [CAT_Q]: cQ };
|
|
2895
|
+
})();
|
|
2896
|
+
}
|
|
2897
|
+
return centroidsPromise;
|
|
2898
|
+
}
|
|
2899
|
+
function cosineSimilarity(a, b) {
|
|
2900
|
+
let dot = 0;
|
|
2901
|
+
let magA = 0;
|
|
2902
|
+
let magB = 0;
|
|
2903
|
+
for (let i = 0;i < a.length; i++) {
|
|
2904
|
+
dot += a[i] * b[i];
|
|
2905
|
+
magA += a[i] * a[i];
|
|
2906
|
+
magB += b[i] * b[i];
|
|
2907
|
+
}
|
|
2908
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
2909
|
+
return denom > 0 ? dot / denom : 0;
|
|
2910
|
+
}
|
|
2911
|
+
async function classifyLocal(messages, config) {
|
|
2912
|
+
const userText = extractLastUserText(messages);
|
|
2913
|
+
if (!userText) {
|
|
2914
|
+
return {
|
|
2915
|
+
tier: "MEDIUM",
|
|
2916
|
+
confidence: 0,
|
|
2917
|
+
reasoning: "No user message found",
|
|
2918
|
+
error: "No user message found in request"
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
const centroids = await getCentroids();
|
|
2922
|
+
const extractor = await getExtractor();
|
|
2923
|
+
const output = await extractor([E5_PREFIX + userText], { pooling: "mean", normalize: true });
|
|
2924
|
+
const inputEmb = output.tolist()[0];
|
|
2925
|
+
let bestCat = CAT_M;
|
|
2926
|
+
let bestSim = -Infinity;
|
|
2927
|
+
for (const [cat, centroid] of Object.entries(centroids)) {
|
|
2928
|
+
const sim = cosineSimilarity(inputEmb, centroid);
|
|
2929
|
+
if (sim > bestSim) {
|
|
2930
|
+
bestSim = sim;
|
|
2931
|
+
bestCat = cat;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
if (isLikelyLight(userText) && bestCat !== CAT_Q) {
|
|
2935
|
+
bestCat = CAT_L;
|
|
2936
|
+
bestSim = Math.max(bestSim, 0.7);
|
|
2937
|
+
}
|
|
2938
|
+
const heuristicQ = matchesQPattern(userText);
|
|
2939
|
+
if (bestCat === CAT_Q || heuristicQ) {
|
|
2940
|
+
const contextText = buildContextText(messages, userText, config?.contextMessages ?? 10);
|
|
2941
|
+
const ctxOutput = await extractor([E5_PREFIX + contextText], { pooling: "mean", normalize: true });
|
|
2942
|
+
const contextEmb = ctxOutput.tolist()[0];
|
|
2943
|
+
let reBestCat = CAT_M;
|
|
2944
|
+
let reBestSim = -Infinity;
|
|
2945
|
+
for (const [cat, centroid] of Object.entries(centroids)) {
|
|
2946
|
+
if (cat === CAT_Q)
|
|
2947
|
+
continue;
|
|
2948
|
+
const sim = cosineSimilarity(contextEmb, centroid);
|
|
2949
|
+
if (sim > reBestSim) {
|
|
2950
|
+
reBestSim = sim;
|
|
2951
|
+
reBestCat = cat;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
const tier2 = TIER_MAP[reBestCat] ?? "MEDIUM";
|
|
2955
|
+
return {
|
|
2956
|
+
tier: tier2,
|
|
2957
|
+
confidence: reBestSim,
|
|
2958
|
+
reasoning: `Re-classified with context (initial: Q, heuristic: ${heuristicQ})`
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
const tier = TIER_MAP[bestCat] ?? "MEDIUM";
|
|
2962
|
+
return { tier, confidence: bestSim };
|
|
2963
|
+
}
|
|
2964
|
+
function extractLastUserText(messages) {
|
|
2965
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
2966
|
+
const msg = messages[i];
|
|
2967
|
+
if (msg.role !== "user")
|
|
2968
|
+
continue;
|
|
2969
|
+
if (typeof msg.content === "string") {
|
|
2970
|
+
return msg.content;
|
|
2971
|
+
}
|
|
2972
|
+
if (Array.isArray(msg.content)) {
|
|
2973
|
+
const parts = [];
|
|
2974
|
+
for (const block of msg.content) {
|
|
2975
|
+
if (block.type === "text" && block.text) {
|
|
2976
|
+
parts.push(block.text);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
if (parts.length > 0)
|
|
2980
|
+
return parts.join(" ");
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
function buildContextText(allMessages, currentText, contextCount) {
|
|
2986
|
+
const relevantMessages = allMessages.filter((m) => m.role === "user" || m.role === "assistant");
|
|
2987
|
+
const lastN = relevantMessages.slice(-contextCount);
|
|
2988
|
+
const parts = [];
|
|
2989
|
+
for (const msg of lastN) {
|
|
2990
|
+
let text;
|
|
2991
|
+
if (typeof msg.content === "string") {
|
|
2992
|
+
text = msg.content;
|
|
2993
|
+
} else if (Array.isArray(msg.content)) {
|
|
2994
|
+
text = msg.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join(" ");
|
|
2995
|
+
} else {
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2998
|
+
parts.push(`[${msg.role}]: ${text}`);
|
|
2999
|
+
}
|
|
3000
|
+
const lastPart = parts[parts.length - 1];
|
|
3001
|
+
if (!lastPart || !lastPart.includes(currentText)) {
|
|
3002
|
+
parts.push(`[user]: ${currentText}`);
|
|
3003
|
+
}
|
|
3004
|
+
return parts.join(`
|
|
3005
|
+
`);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// src/openclaw/auth-resolver.ts
|
|
3009
|
+
var PROVIDER_ENV_VARS = {
|
|
3010
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
3011
|
+
openai: "OPENAI_API_KEY",
|
|
3012
|
+
google: "GEMINI_API_KEY",
|
|
3013
|
+
gemini: "GEMINI_API_KEY",
|
|
3014
|
+
zai: "ZAI_API_KEY",
|
|
3015
|
+
aws: "AWS_ACCESS_KEY_ID",
|
|
3016
|
+
bedrock: "AWS_ACCESS_KEY_ID"
|
|
3017
|
+
};
|
|
3018
|
+
function getEnvFallback(provider) {
|
|
3019
|
+
const exact = PROVIDER_ENV_VARS[provider];
|
|
3020
|
+
if (exact)
|
|
3021
|
+
return process.env[exact];
|
|
3022
|
+
for (const [key, envVar] of Object.entries(PROVIDER_ENV_VARS)) {
|
|
3023
|
+
if (provider.startsWith(key)) {
|
|
3024
|
+
return process.env[envVar];
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
function formatAuth(apiKey, providerConfig) {
|
|
3030
|
+
const api = providerConfig?.api ?? "";
|
|
3031
|
+
if (api === "anthropic-messages") {
|
|
3032
|
+
return { apiKey, headerName: "x-api-key", headerValue: apiKey };
|
|
3033
|
+
}
|
|
3034
|
+
if (api === "openai-completions" || api === "openai-responses") {
|
|
3035
|
+
return { apiKey, headerName: "Authorization", headerValue: `Bearer ${apiKey}` };
|
|
3036
|
+
}
|
|
3037
|
+
if (api === "google-generative-ai") {
|
|
3038
|
+
return { apiKey, headerName: "x-goog-api-key", headerValue: apiKey };
|
|
3039
|
+
}
|
|
3040
|
+
if (api === "bedrock-converse-stream") {
|
|
3041
|
+
const secretKey = process.env.AWS_SECRET_ACCESS_KEY ?? "";
|
|
3042
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
3043
|
+
const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? extractRegionFromUrl(providerConfig?.baseUrl ?? "") ?? "us-east-1";
|
|
3044
|
+
return {
|
|
3045
|
+
apiKey,
|
|
3046
|
+
headerName: "Authorization",
|
|
3047
|
+
headerValue: "",
|
|
3048
|
+
awsAccessKeyId: apiKey,
|
|
3049
|
+
awsSecretKey: secretKey,
|
|
3050
|
+
awsSessionToken: sessionToken,
|
|
3051
|
+
awsRegion: region
|
|
164
3052
|
};
|
|
165
3053
|
}
|
|
3054
|
+
return { apiKey, headerName: "Authorization", headerValue: `Bearer ${apiKey}` };
|
|
166
3055
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
3056
|
+
var NO_AUTH_APIS = new Set(["ollama"]);
|
|
3057
|
+
function resolveApiKey(provider, openclawConfig, authProfiles) {
|
|
3058
|
+
const providerConfig = getProviderConfig(provider, openclawConfig);
|
|
3059
|
+
const api = providerConfig?.api ?? "";
|
|
3060
|
+
if (NO_AUTH_APIS.has(api)) {
|
|
3061
|
+
return { apiKey: "ollama-local", headerName: "", headerValue: "" };
|
|
3062
|
+
}
|
|
3063
|
+
for (const profile of authProfiles) {
|
|
3064
|
+
if (profile.provider === provider) {
|
|
3065
|
+
const key = profile.apiKey ?? profile.token;
|
|
3066
|
+
if (key) {
|
|
3067
|
+
const resolved = resolveEnvVar(key);
|
|
3068
|
+
if (resolved) {
|
|
3069
|
+
return formatAuth(resolved, providerConfig);
|
|
3070
|
+
}
|
|
179
3071
|
}
|
|
180
|
-
return handler(req, null);
|
|
181
3072
|
}
|
|
182
3073
|
}
|
|
183
|
-
|
|
3074
|
+
if (providerConfig?.apiKey) {
|
|
3075
|
+
const resolved = resolveEnvVar(providerConfig.apiKey);
|
|
3076
|
+
if (resolved) {
|
|
3077
|
+
return formatAuth(resolved, providerConfig);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
const envKey = getEnvFallback(provider);
|
|
3081
|
+
if (envKey) {
|
|
3082
|
+
return formatAuth(envKey, providerConfig);
|
|
3083
|
+
}
|
|
3084
|
+
return;
|
|
184
3085
|
}
|
|
185
3086
|
|
|
186
|
-
// src/
|
|
187
|
-
var
|
|
3087
|
+
// src/adapters/stream-transformer.ts
|
|
3088
|
+
var encoder = new TextEncoder;
|
|
3089
|
+
var decoder = new TextDecoder;
|
|
3090
|
+
function createStreamTranslator(sourceAdapter, targetAdapter) {
|
|
3091
|
+
if (sourceAdapter.apiType === targetAdapter.apiType) {
|
|
3092
|
+
return new TransformStream;
|
|
3093
|
+
}
|
|
3094
|
+
let buffer = "";
|
|
3095
|
+
let messageStarted = false;
|
|
3096
|
+
return new TransformStream({
|
|
3097
|
+
transform(chunk, controller) {
|
|
3098
|
+
if (!sourceAdapter.parseStreamChunk || !targetAdapter.buildStreamChunk) {
|
|
3099
|
+
controller.enqueue(chunk);
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
3103
|
+
let delimiterIndex;
|
|
3104
|
+
while ((delimiterIndex = buffer.indexOf(`
|
|
188
3105
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
3106
|
+
`)) !== -1) {
|
|
3107
|
+
const frame = buffer.slice(0, delimiterIndex);
|
|
3108
|
+
buffer = buffer.slice(delimiterIndex + 2);
|
|
3109
|
+
if (frame.trim() === "")
|
|
3110
|
+
continue;
|
|
3111
|
+
const events = sourceAdapter.parseStreamChunk(frame);
|
|
3112
|
+
for (const event of events) {
|
|
3113
|
+
if (event.type === "message_start") {
|
|
3114
|
+
messageStarted = true;
|
|
3115
|
+
} else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
|
|
3116
|
+
messageStarted = true;
|
|
3117
|
+
const synthetic = targetAdapter.buildStreamChunk({
|
|
3118
|
+
type: "message_start",
|
|
3119
|
+
id: "",
|
|
3120
|
+
model: ""
|
|
3121
|
+
});
|
|
3122
|
+
if (synthetic)
|
|
3123
|
+
controller.enqueue(encoder.encode(synthetic));
|
|
3124
|
+
}
|
|
3125
|
+
const translated = targetAdapter.buildStreamChunk(event);
|
|
3126
|
+
controller.enqueue(encoder.encode(translated));
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
},
|
|
3130
|
+
flush(controller) {
|
|
3131
|
+
if (buffer.trim() !== "" && sourceAdapter.parseStreamChunk && targetAdapter.buildStreamChunk) {
|
|
3132
|
+
const events = sourceAdapter.parseStreamChunk(buffer);
|
|
3133
|
+
for (const event of events) {
|
|
3134
|
+
if (event.type === "message_start") {
|
|
3135
|
+
messageStarted = true;
|
|
3136
|
+
} else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
|
|
3137
|
+
messageStarted = true;
|
|
3138
|
+
const synthetic = targetAdapter.buildStreamChunk({
|
|
3139
|
+
type: "message_start",
|
|
3140
|
+
id: "",
|
|
3141
|
+
model: ""
|
|
3142
|
+
});
|
|
3143
|
+
if (synthetic)
|
|
3144
|
+
controller.enqueue(encoder.encode(synthetic));
|
|
3145
|
+
}
|
|
3146
|
+
const translated = targetAdapter.buildStreamChunk(event);
|
|
3147
|
+
controller.enqueue(encoder.encode(translated));
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
function getStreamContentType(adapter) {
|
|
3154
|
+
switch (adapter.apiType) {
|
|
3155
|
+
case "anthropic-messages":
|
|
3156
|
+
case "openai-completions":
|
|
3157
|
+
case "openai-responses":
|
|
3158
|
+
return "text/event-stream";
|
|
3159
|
+
case "google-generative-ai":
|
|
3160
|
+
return "application/json";
|
|
3161
|
+
case "ollama":
|
|
3162
|
+
return "application/x-ndjson";
|
|
3163
|
+
case "bedrock-converse-stream":
|
|
3164
|
+
return "application/vnd.amazon.eventstream";
|
|
3165
|
+
default:
|
|
3166
|
+
return "text/event-stream";
|
|
193
3167
|
}
|
|
194
|
-
return createNodeServer(config);
|
|
195
3168
|
}
|
|
196
|
-
function
|
|
197
|
-
|
|
3169
|
+
async function translateResponse(sourceAdapter, targetAdapter, upstreamResponse, streaming) {
|
|
3170
|
+
if (sourceAdapter.apiType === targetAdapter.apiType) {
|
|
3171
|
+
return upstreamResponse;
|
|
3172
|
+
}
|
|
3173
|
+
if (!streaming) {
|
|
3174
|
+
return translateNonStreamingResponse(sourceAdapter, targetAdapter, upstreamResponse);
|
|
3175
|
+
}
|
|
3176
|
+
return translateStreamingResponse(sourceAdapter, targetAdapter, upstreamResponse);
|
|
3177
|
+
}
|
|
3178
|
+
async function translateNonStreamingResponse(sourceAdapter, targetAdapter, upstreamResponse) {
|
|
3179
|
+
if (!sourceAdapter.parseResponse || !targetAdapter.buildResponse) {
|
|
3180
|
+
return upstreamResponse;
|
|
3181
|
+
}
|
|
3182
|
+
const body = await upstreamResponse.json();
|
|
3183
|
+
const parsed = sourceAdapter.parseResponse(body);
|
|
3184
|
+
const translated = targetAdapter.buildResponse(parsed);
|
|
3185
|
+
const headers = copyRelevantHeaders(upstreamResponse.headers);
|
|
3186
|
+
headers.set("content-type", "application/json");
|
|
3187
|
+
return new Response(JSON.stringify(translated), {
|
|
3188
|
+
status: upstreamResponse.status,
|
|
3189
|
+
headers
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
function translateStreamingResponse(sourceAdapter, targetAdapter, upstreamResponse) {
|
|
3193
|
+
if (!upstreamResponse.body) {
|
|
3194
|
+
return upstreamResponse;
|
|
3195
|
+
}
|
|
3196
|
+
if (!sourceAdapter.parseStreamChunk || !targetAdapter.buildStreamChunk) {
|
|
3197
|
+
return upstreamResponse;
|
|
3198
|
+
}
|
|
3199
|
+
const translator = createStreamTranslator(sourceAdapter, targetAdapter);
|
|
3200
|
+
const translatedBody = upstreamResponse.body.pipeThrough(translator);
|
|
3201
|
+
const headers = copyRelevantHeaders(upstreamResponse.headers);
|
|
3202
|
+
headers.set("content-type", getStreamContentType(targetAdapter));
|
|
3203
|
+
return new Response(translatedBody, {
|
|
3204
|
+
status: upstreamResponse.status,
|
|
3205
|
+
headers
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
function copyRelevantHeaders(source) {
|
|
3209
|
+
const headers = new Headers;
|
|
3210
|
+
const passthrough = [
|
|
3211
|
+
"cache-control",
|
|
3212
|
+
"x-request-id",
|
|
3213
|
+
"x-ratelimit-limit",
|
|
3214
|
+
"x-ratelimit-remaining",
|
|
3215
|
+
"x-ratelimit-reset"
|
|
3216
|
+
];
|
|
3217
|
+
for (const name of passthrough) {
|
|
3218
|
+
const value = source.get(name);
|
|
3219
|
+
if (value !== null) {
|
|
3220
|
+
headers.set(name, value);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
return headers;
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
// src/compression/session-store.ts
|
|
3227
|
+
function djb2Hash(str) {
|
|
3228
|
+
let hash = 5381;
|
|
3229
|
+
for (let i = 0;i < str.length; i++) {
|
|
3230
|
+
hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
|
|
3231
|
+
}
|
|
3232
|
+
return hash >>> 0;
|
|
3233
|
+
}
|
|
3234
|
+
function generateSessionId(messages) {
|
|
3235
|
+
const firstUserMessage = messages.find((m) => m.role === "user");
|
|
3236
|
+
if (!firstUserMessage)
|
|
3237
|
+
return "empty-session";
|
|
3238
|
+
const content = typeof firstUserMessage.content === "string" ? firstUserMessage.content : JSON.stringify(firstUserMessage.content);
|
|
3239
|
+
return `session-${djb2Hash(content)}`;
|
|
3240
|
+
}
|
|
3241
|
+
function createSessionStore(maxSessions = 500) {
|
|
3242
|
+
const store = new Map;
|
|
3243
|
+
function evictLru() {
|
|
3244
|
+
if (store.size < maxSessions)
|
|
3245
|
+
return;
|
|
3246
|
+
let oldestId = "";
|
|
3247
|
+
let oldestAccess = Infinity;
|
|
3248
|
+
for (const [id, session] of store) {
|
|
3249
|
+
if (session.lastAccess < oldestAccess) {
|
|
3250
|
+
oldestAccess = session.lastAccess;
|
|
3251
|
+
oldestId = id;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
if (oldestId)
|
|
3255
|
+
store.delete(oldestId);
|
|
3256
|
+
}
|
|
198
3257
|
return {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
hostname: config.host,
|
|
206
|
-
fetch: dispatch
|
|
207
|
-
});
|
|
3258
|
+
get(id) {
|
|
3259
|
+
const session = store.get(id);
|
|
3260
|
+
if (session) {
|
|
3261
|
+
session.lastAccess = Date.now();
|
|
3262
|
+
}
|
|
3263
|
+
return session;
|
|
208
3264
|
},
|
|
209
|
-
|
|
210
|
-
|
|
3265
|
+
getOrCreate(id, messages) {
|
|
3266
|
+
const existing = store.get(id);
|
|
3267
|
+
if (existing) {
|
|
3268
|
+
existing.lastAccess = Date.now();
|
|
3269
|
+
return existing;
|
|
3270
|
+
}
|
|
3271
|
+
evictLru();
|
|
3272
|
+
const session = {
|
|
3273
|
+
id,
|
|
3274
|
+
messages,
|
|
3275
|
+
tokenCount: 0,
|
|
3276
|
+
compressionState: "idle",
|
|
3277
|
+
lastAccess: Date.now()
|
|
3278
|
+
};
|
|
3279
|
+
store.set(id, session);
|
|
3280
|
+
return session;
|
|
3281
|
+
},
|
|
3282
|
+
set(id, session) {
|
|
3283
|
+
if (!store.has(id)) {
|
|
3284
|
+
evictLru();
|
|
3285
|
+
}
|
|
3286
|
+
if (session.lastAccess === 0) {
|
|
3287
|
+
session.lastAccess = Date.now();
|
|
3288
|
+
}
|
|
3289
|
+
store.set(id, session);
|
|
3290
|
+
},
|
|
3291
|
+
update(id, updates) {
|
|
3292
|
+
const session = store.get(id);
|
|
3293
|
+
if (!session)
|
|
211
3294
|
return;
|
|
212
|
-
|
|
213
|
-
|
|
3295
|
+
Object.assign(session, updates);
|
|
3296
|
+
session.lastAccess = Date.now();
|
|
3297
|
+
return session;
|
|
3298
|
+
},
|
|
3299
|
+
delete(id) {
|
|
3300
|
+
return store.delete(id);
|
|
3301
|
+
},
|
|
3302
|
+
size() {
|
|
3303
|
+
return store.size;
|
|
3304
|
+
},
|
|
3305
|
+
has(id) {
|
|
3306
|
+
return store.has(id);
|
|
214
3307
|
}
|
|
215
3308
|
};
|
|
216
3309
|
}
|
|
217
|
-
|
|
218
|
-
|
|
3310
|
+
|
|
3311
|
+
// src/compression/worker.ts
|
|
3312
|
+
var SUMMARY_PREFIX = "[Summary of previous conversation]";
|
|
3313
|
+
function messageContentToString2(content) {
|
|
3314
|
+
if (typeof content === "string")
|
|
3315
|
+
return content;
|
|
3316
|
+
if (Array.isArray(content)) {
|
|
3317
|
+
return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(`
|
|
3318
|
+
`);
|
|
3319
|
+
}
|
|
3320
|
+
return JSON.stringify(content);
|
|
3321
|
+
}
|
|
3322
|
+
function estimateMessageTokens(msg) {
|
|
3323
|
+
const MESSAGE_OVERHEAD2 = 4;
|
|
3324
|
+
return MESSAGE_OVERHEAD2 + estimateTokens(messageContentToString2(msg.content));
|
|
3325
|
+
}
|
|
3326
|
+
function buildCompressionPrompt(messages, targetTokens) {
|
|
3327
|
+
const conversationText = messages.map((m) => `${m.role}: ${messageContentToString2(m.content)}`).join(`
|
|
3328
|
+
|
|
3329
|
+
`);
|
|
3330
|
+
return [
|
|
3331
|
+
{
|
|
3332
|
+
role: "system",
|
|
3333
|
+
content: [
|
|
3334
|
+
"You are a conversation summarizer. Produce a concise summary of the conversation below.",
|
|
3335
|
+
`Target length: approximately ${targetTokens} tokens.`,
|
|
3336
|
+
"Preserve: key decisions, code snippets, technical details, and action items.",
|
|
3337
|
+
"Format: plain text paragraphs. Start with the most important context."
|
|
3338
|
+
].join(`
|
|
3339
|
+
`)
|
|
3340
|
+
},
|
|
3341
|
+
{
|
|
3342
|
+
role: "user",
|
|
3343
|
+
content: conversationText
|
|
3344
|
+
}
|
|
3345
|
+
];
|
|
3346
|
+
}
|
|
3347
|
+
function buildCompressedMessages(summary) {
|
|
3348
|
+
return [
|
|
3349
|
+
{
|
|
3350
|
+
role: "user",
|
|
3351
|
+
content: `${SUMMARY_PREFIX}
|
|
3352
|
+
${summary}`
|
|
3353
|
+
},
|
|
3354
|
+
{
|
|
3355
|
+
role: "assistant",
|
|
3356
|
+
content: "Understood. I have the context from our previous conversation. How can I help you continue?"
|
|
3357
|
+
}
|
|
3358
|
+
];
|
|
3359
|
+
}
|
|
3360
|
+
function truncateToFit(messages, targetTokens) {
|
|
3361
|
+
const result = [];
|
|
3362
|
+
let usedTokens = 0;
|
|
3363
|
+
const firstMsg = messages[0];
|
|
3364
|
+
const firstContent = firstMsg ? messageContentToString2(firstMsg.content) : "";
|
|
3365
|
+
const hasSystemPrefix = firstMsg?.role === "user" && firstContent.startsWith(SUMMARY_PREFIX);
|
|
3366
|
+
if (hasSystemPrefix && firstMsg) {
|
|
3367
|
+
const tokens = estimateMessageTokens(firstMsg);
|
|
3368
|
+
result.push(firstMsg);
|
|
3369
|
+
usedTokens += tokens;
|
|
3370
|
+
}
|
|
3371
|
+
const tail = [];
|
|
3372
|
+
const startIdx = hasSystemPrefix ? 1 : 0;
|
|
3373
|
+
for (let i = messages.length - 1;i >= startIdx; i--) {
|
|
3374
|
+
const msg = messages[i];
|
|
3375
|
+
const tokens = estimateMessageTokens(msg);
|
|
3376
|
+
if (usedTokens + tokens > targetTokens)
|
|
3377
|
+
break;
|
|
3378
|
+
tail.unshift(msg);
|
|
3379
|
+
usedTokens += tokens;
|
|
3380
|
+
}
|
|
3381
|
+
return [...result, ...tail];
|
|
3382
|
+
}
|
|
3383
|
+
function createCompressionWorker(config) {
|
|
3384
|
+
let activeJobs = 0;
|
|
3385
|
+
let completedJobs = 0;
|
|
3386
|
+
let failedJobs = 0;
|
|
3387
|
+
function shouldCompress(session) {
|
|
3388
|
+
const thresholdTokens = config.threshold * config.contextWindow;
|
|
3389
|
+
return session.tokenCount >= thresholdTokens && session.compressionState === "idle";
|
|
3390
|
+
}
|
|
3391
|
+
function triggerCompression(session, sessionStore, makeApiCall) {
|
|
3392
|
+
if (activeJobs >= config.maxConcurrent)
|
|
3393
|
+
return;
|
|
3394
|
+
session.compressionState = "computing";
|
|
3395
|
+
session.snapshotIndex = session.messages.length;
|
|
3396
|
+
sessionStore.update(session.id, {
|
|
3397
|
+
compressionState: "computing",
|
|
3398
|
+
snapshotIndex: session.messages.length
|
|
3399
|
+
});
|
|
3400
|
+
activeJobs++;
|
|
3401
|
+
const targetTokens = config.targetRatio * config.contextWindow;
|
|
3402
|
+
const promptMessages = buildCompressionPrompt(session.messages, targetTokens);
|
|
3403
|
+
const sessionId = session.id;
|
|
3404
|
+
const originalMessages = [...session.messages];
|
|
3405
|
+
const jobPromise = Promise.race([
|
|
3406
|
+
makeApiCall(config.compressionModel, promptMessages),
|
|
3407
|
+
new Promise((_resolve, reject) => {
|
|
3408
|
+
setTimeout(() => reject(new Error("compression_timeout")), config.timeoutMs);
|
|
3409
|
+
})
|
|
3410
|
+
]);
|
|
3411
|
+
jobPromise.then((summaryText) => {
|
|
3412
|
+
const compressed = buildCompressedMessages(summaryText);
|
|
3413
|
+
sessionStore.update(sessionId, {
|
|
3414
|
+
compressionState: "ready",
|
|
3415
|
+
compressedSummary: summaryText,
|
|
3416
|
+
compressedMessages: compressed
|
|
3417
|
+
});
|
|
3418
|
+
const current = sessionStore.get(sessionId);
|
|
3419
|
+
if (current) {
|
|
3420
|
+
session.compressionState = current.compressionState;
|
|
3421
|
+
session.compressedSummary = current.compressedSummary;
|
|
3422
|
+
session.compressedMessages = current.compressedMessages;
|
|
3423
|
+
}
|
|
3424
|
+
activeJobs--;
|
|
3425
|
+
completedJobs++;
|
|
3426
|
+
}).catch((error) => {
|
|
3427
|
+
if (error.message === "compression_timeout") {
|
|
3428
|
+
const truncated = truncateToFit(originalMessages, targetTokens);
|
|
3429
|
+
sessionStore.update(sessionId, {
|
|
3430
|
+
compressionState: "ready",
|
|
3431
|
+
compressedMessages: truncated
|
|
3432
|
+
});
|
|
3433
|
+
const current = sessionStore.get(sessionId);
|
|
3434
|
+
if (current) {
|
|
3435
|
+
session.compressionState = current.compressionState;
|
|
3436
|
+
session.compressedMessages = current.compressedMessages;
|
|
3437
|
+
}
|
|
3438
|
+
activeJobs--;
|
|
3439
|
+
completedJobs++;
|
|
3440
|
+
} else {
|
|
3441
|
+
sessionStore.update(sessionId, { compressionState: "idle" });
|
|
3442
|
+
session.compressionState = "idle";
|
|
3443
|
+
activeJobs--;
|
|
3444
|
+
failedJobs++;
|
|
3445
|
+
console.error(`[CompressionWorker] Job failed for session ${sessionId}:`, error.message);
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
function applyCompression(session) {
|
|
3450
|
+
if (session.compressionState !== "ready" || !session.compressedMessages) {
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
const compressed = session.compressedMessages;
|
|
3454
|
+
const snapshotIdx = session.snapshotIndex ?? session.messages.length - 3;
|
|
3455
|
+
const postSnapshotMessages = session.messages.slice(snapshotIdx);
|
|
3456
|
+
const combined = [...compressed, ...postSnapshotMessages];
|
|
3457
|
+
session.compressionState = "idle";
|
|
3458
|
+
session.compressedMessages = undefined;
|
|
3459
|
+
session.compressedSummary = undefined;
|
|
3460
|
+
session.snapshotIndex = undefined;
|
|
3461
|
+
return combined;
|
|
3462
|
+
}
|
|
3463
|
+
function getStats() {
|
|
3464
|
+
return { activeJobs, completedJobs, failedJobs };
|
|
3465
|
+
}
|
|
219
3466
|
return {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
3467
|
+
shouldCompress,
|
|
3468
|
+
triggerCompression,
|
|
3469
|
+
applyCompression,
|
|
3470
|
+
getStats
|
|
3471
|
+
};
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// src/proxy/compression-integration.ts
|
|
3475
|
+
var HARD_CEILING_RATIO = 0.9;
|
|
3476
|
+
function messagesToTokenMessages(messages) {
|
|
3477
|
+
return messages.map((m) => ({
|
|
3478
|
+
role: m.role,
|
|
3479
|
+
content: m.content
|
|
3480
|
+
}));
|
|
3481
|
+
}
|
|
3482
|
+
function extractResponseText(responseBody) {
|
|
3483
|
+
try {
|
|
3484
|
+
const parsed = JSON.parse(responseBody);
|
|
3485
|
+
if (Array.isArray(parsed.content)) {
|
|
3486
|
+
const textBlocks = parsed.content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
|
|
3487
|
+
if (textBlocks.length > 0)
|
|
3488
|
+
return textBlocks.join(`
|
|
3489
|
+
`);
|
|
3490
|
+
}
|
|
3491
|
+
if (Array.isArray(parsed.choices)) {
|
|
3492
|
+
const choices = parsed.choices;
|
|
3493
|
+
const first = choices[0];
|
|
3494
|
+
if (first) {
|
|
3495
|
+
const message = first.message;
|
|
3496
|
+
if (message && typeof message.content === "string") {
|
|
3497
|
+
return message.content;
|
|
234
3498
|
}
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
return JSON.stringify(parsed);
|
|
3502
|
+
} catch {
|
|
3503
|
+
return responseBody;
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
function createMakeApiCall(adapter, compressionModel, baseUrl, auth) {
|
|
3507
|
+
const actualModelId = compressionModel.includes("/") ? compressionModel.split("/").slice(1).join("/") : compressionModel;
|
|
3508
|
+
return async (model, messages) => {
|
|
3509
|
+
const syntheticParsed = {
|
|
3510
|
+
model,
|
|
3511
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
3512
|
+
stream: false,
|
|
3513
|
+
maxTokens: 4096,
|
|
3514
|
+
rawBody: {
|
|
3515
|
+
model,
|
|
3516
|
+
messages,
|
|
3517
|
+
stream: false,
|
|
3518
|
+
max_tokens: 4096
|
|
3519
|
+
}
|
|
3520
|
+
};
|
|
3521
|
+
const upstream = adapter.buildUpstreamRequest(syntheticParsed, actualModelId, baseUrl, auth);
|
|
3522
|
+
const response = await fetch(upstream.url, {
|
|
3523
|
+
method: upstream.method,
|
|
3524
|
+
headers: upstream.headers,
|
|
3525
|
+
body: upstream.body
|
|
3526
|
+
});
|
|
3527
|
+
const body = await response.text();
|
|
3528
|
+
if (!response.ok) {
|
|
3529
|
+
throw new Error(`Compression API call failed: ${response.status} ${body}`);
|
|
3530
|
+
}
|
|
3531
|
+
return extractResponseText(body);
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
function createCompressionMiddleware(config) {
|
|
3535
|
+
const contextWindow = config.resolvedContextWindow;
|
|
3536
|
+
const sessionStore = createSessionStore(config.maxSessions ?? 500);
|
|
3537
|
+
const worker = createCompressionWorker({
|
|
3538
|
+
threshold: config.threshold,
|
|
3539
|
+
targetRatio: config.targetRatio,
|
|
3540
|
+
compressionModel: config.compressionModel,
|
|
3541
|
+
contextWindow,
|
|
3542
|
+
maxConcurrent: 2,
|
|
3543
|
+
timeoutMs: 60000
|
|
3544
|
+
});
|
|
3545
|
+
function beforeForward(parsed, adapter) {
|
|
3546
|
+
const messages = parsed.messages;
|
|
3547
|
+
if (messages.length <= 1) {
|
|
3548
|
+
return { messages, wasCompressed: false };
|
|
3549
|
+
}
|
|
3550
|
+
const sessionId = generateSessionId(messages);
|
|
3551
|
+
const session = sessionStore.getOrCreate(sessionId, messages);
|
|
3552
|
+
const compressed = worker.applyCompression(session);
|
|
3553
|
+
if (compressed) {
|
|
3554
|
+
sessionStore.update(sessionId, {
|
|
3555
|
+
messages: compressed,
|
|
3556
|
+
compressionState: "idle",
|
|
3557
|
+
compressedMessages: undefined,
|
|
3558
|
+
compressedSummary: undefined,
|
|
3559
|
+
snapshotIndex: undefined
|
|
235
3560
|
});
|
|
236
|
-
|
|
237
|
-
|
|
3561
|
+
const originalTokens = estimateMessagesTokens(messagesToTokenMessages(messages));
|
|
3562
|
+
const compressedTokens = estimateMessagesTokens(messagesToTokenMessages(compressed));
|
|
3563
|
+
if (config.statsTracker) {
|
|
3564
|
+
config.statsTracker.recordCompression(originalTokens, compressedTokens);
|
|
3565
|
+
}
|
|
3566
|
+
console.log(`[compression] Applied compression: ${originalTokens} → ${compressedTokens} tokens (${((1 - compressedTokens / originalTokens) * 100).toFixed(0)}% reduction)`);
|
|
3567
|
+
return { messages: compressed, wasCompressed: true };
|
|
3568
|
+
}
|
|
3569
|
+
const tokenCount = estimateMessagesTokens(messagesToTokenMessages(messages));
|
|
3570
|
+
const hardCeilingTokens = HARD_CEILING_RATIO * contextWindow;
|
|
3571
|
+
if (tokenCount >= hardCeilingTokens) {
|
|
3572
|
+
const targetTokens = config.targetRatio * contextWindow;
|
|
3573
|
+
const truncated = truncateToFit(messages, targetTokens);
|
|
3574
|
+
console.log(`[compression] Hard ceiling hit (${tokenCount} tokens >= ${Math.round(hardCeilingTokens)}), truncating to ${Math.round(targetTokens)} tokens`);
|
|
3575
|
+
return { messages: truncated, wasCompressed: true };
|
|
3576
|
+
}
|
|
3577
|
+
return { messages, wasCompressed: false };
|
|
3578
|
+
}
|
|
3579
|
+
function afterResponse(parsed, adapter, baseUrl, auth) {
|
|
3580
|
+
const messages = parsed.messages;
|
|
3581
|
+
if (messages.length <= 1)
|
|
3582
|
+
return;
|
|
3583
|
+
const sessionId = generateSessionId(messages);
|
|
3584
|
+
const session = sessionStore.getOrCreate(sessionId, messages);
|
|
3585
|
+
const tokenCount = estimateMessagesTokens(messagesToTokenMessages(messages));
|
|
3586
|
+
sessionStore.update(sessionId, {
|
|
3587
|
+
messages: [...messages],
|
|
3588
|
+
tokenCount
|
|
3589
|
+
});
|
|
3590
|
+
const updatedSession = sessionStore.get(sessionId);
|
|
3591
|
+
if (!updatedSession)
|
|
3592
|
+
return;
|
|
3593
|
+
if (worker.shouldCompress(updatedSession)) {
|
|
3594
|
+
const makeApiCall = createMakeApiCall(adapter, config.compressionModel, baseUrl, auth);
|
|
3595
|
+
worker.triggerCompression(updatedSession, sessionStore, makeApiCall);
|
|
3596
|
+
console.log(`[compression] Triggered background compression for session ${sessionId} (${tokenCount} tokens)`);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
function getSummaryForSession(messages) {
|
|
3600
|
+
if (messages.length <= 1)
|
|
3601
|
+
return;
|
|
3602
|
+
const sessionId = generateSessionId(messages);
|
|
3603
|
+
const session = sessionStore.get(sessionId);
|
|
3604
|
+
if (!session)
|
|
3605
|
+
return;
|
|
3606
|
+
if (session.compressionState === "ready" && session.compressedSummary) {
|
|
3607
|
+
const recentMessages = session.messages.slice(-3);
|
|
3608
|
+
const summary = session.compressedSummary;
|
|
3609
|
+
sessionStore.update(sessionId, {
|
|
3610
|
+
compressionState: "idle",
|
|
3611
|
+
compressedMessages: undefined,
|
|
3612
|
+
compressedSummary: undefined
|
|
238
3613
|
});
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
3614
|
+
return { summary, recentMessages };
|
|
3615
|
+
}
|
|
3616
|
+
return;
|
|
3617
|
+
}
|
|
3618
|
+
return {
|
|
3619
|
+
beforeForward,
|
|
3620
|
+
afterResponse,
|
|
3621
|
+
getSessionStore: () => sessionStore,
|
|
3622
|
+
getWorker: () => worker,
|
|
3623
|
+
getSummaryForSession
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
// src/proxy/pipeline.ts
|
|
3628
|
+
registerAdapter(new AnthropicAdapter);
|
|
3629
|
+
async function collectCodexStream(sourceAdapter, response) {
|
|
3630
|
+
const reader = response.body.getReader();
|
|
3631
|
+
const decoder2 = new TextDecoder;
|
|
3632
|
+
let buffer = "";
|
|
3633
|
+
let id = "";
|
|
3634
|
+
let model = "";
|
|
3635
|
+
const textParts = [];
|
|
3636
|
+
let usage;
|
|
3637
|
+
for (;; ) {
|
|
3638
|
+
const { done, value } = await reader.read();
|
|
3639
|
+
if (done)
|
|
3640
|
+
break;
|
|
3641
|
+
buffer += decoder2.decode(value, { stream: true });
|
|
3642
|
+
let idx;
|
|
3643
|
+
while ((idx = buffer.indexOf(`
|
|
3644
|
+
|
|
3645
|
+
`)) !== -1) {
|
|
3646
|
+
const frame = buffer.slice(0, idx);
|
|
3647
|
+
buffer = buffer.slice(idx + 2);
|
|
3648
|
+
if (!frame.trim() || !sourceAdapter.parseStreamChunk)
|
|
3649
|
+
continue;
|
|
3650
|
+
for (const event of sourceAdapter.parseStreamChunk(frame)) {
|
|
3651
|
+
if (event.type === "message_start") {
|
|
3652
|
+
id = event.id ?? "";
|
|
3653
|
+
model = event.model ?? "";
|
|
3654
|
+
} else if (event.type === "content_delta") {
|
|
3655
|
+
textParts.push(event.text ?? "");
|
|
3656
|
+
} else if (event.type === "message_stop" && event.usage) {
|
|
3657
|
+
usage = event.usage;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
return {
|
|
3663
|
+
id,
|
|
3664
|
+
model,
|
|
3665
|
+
content: textParts.join(""),
|
|
3666
|
+
role: "assistant",
|
|
3667
|
+
stopReason: "completed",
|
|
3668
|
+
usage
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
function findProviderForModel(modelString, openclawConfig) {
|
|
3672
|
+
const providers = openclawConfig.models?.providers;
|
|
3673
|
+
if (!providers)
|
|
3674
|
+
return;
|
|
3675
|
+
const [providerName, modelId] = modelString.split("/", 2);
|
|
3676
|
+
if (!providerName || !modelId)
|
|
3677
|
+
return;
|
|
3678
|
+
const providerConfig = providers[providerName];
|
|
3679
|
+
if (!providerConfig)
|
|
3680
|
+
return;
|
|
3681
|
+
return {
|
|
3682
|
+
providerName,
|
|
3683
|
+
baseUrl: providerConfig.baseUrl ?? "",
|
|
3684
|
+
apiType: providerConfig.api ?? ""
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
function jsonErrorResponse(message, status) {
|
|
3688
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
3689
|
+
status,
|
|
3690
|
+
headers: { "content-type": "application/json" }
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3693
|
+
async function handleApiRequest(req, body, apiType, config, openclawConfig, authProfiles, compressionMiddleware) {
|
|
3694
|
+
const adapter = getAdapter(apiType);
|
|
3695
|
+
if (!adapter) {
|
|
3696
|
+
return jsonErrorResponse(`Unknown API type: ${apiType}`, 500);
|
|
3697
|
+
}
|
|
3698
|
+
const parsed = adapter.parseRequest(body);
|
|
3699
|
+
const compactionHeaders = {};
|
|
3700
|
+
req.headers.forEach((value, key) => {
|
|
3701
|
+
compactionHeaders[key] = value;
|
|
3702
|
+
});
|
|
3703
|
+
const compaction = detectCompaction(compactionHeaders, parsed.messages);
|
|
3704
|
+
if (compaction.isCompaction && compressionMiddleware) {
|
|
3705
|
+
const summaryData = compressionMiddleware.getSummaryForSession(parsed.messages);
|
|
3706
|
+
if (summaryData) {
|
|
3707
|
+
const syntheticParsed = buildSyntheticSummaryResponse(summaryData.summary, summaryData.recentMessages, parsed.model);
|
|
3708
|
+
console.log(`[clawmux] Compaction detected (${compaction.detectedBy}) → returning synthetic response`);
|
|
3709
|
+
return buildSyntheticHttpResponse(syntheticParsed, adapter);
|
|
3710
|
+
}
|
|
3711
|
+
console.log(`[clawmux] Compaction detected but no summary available, forwarding to upstream`);
|
|
3712
|
+
}
|
|
3713
|
+
let effectiveParsed = parsed;
|
|
3714
|
+
if (compressionMiddleware) {
|
|
3715
|
+
const { messages: compressedMessages, wasCompressed } = compressionMiddleware.beforeForward(parsed, adapter);
|
|
3716
|
+
if (wasCompressed) {
|
|
3717
|
+
const modifiedRawBody = adapter.modifyMessages(parsed.rawBody, compressedMessages);
|
|
3718
|
+
effectiveParsed = {
|
|
3719
|
+
...parsed,
|
|
3720
|
+
messages: compressedMessages,
|
|
3721
|
+
rawBody: modifiedRawBody
|
|
3722
|
+
};
|
|
246
3723
|
}
|
|
3724
|
+
}
|
|
3725
|
+
const messages = effectiveParsed.messages;
|
|
3726
|
+
const classification = await classifyLocal(messages);
|
|
3727
|
+
const decision = {
|
|
3728
|
+
tier: classification.tier,
|
|
3729
|
+
model: config.routing.models[classification.tier],
|
|
3730
|
+
confidence: classification.confidence,
|
|
3731
|
+
overrideReason: classification.reasoning
|
|
3732
|
+
};
|
|
3733
|
+
const lookup = findProviderForModel(decision.model, openclawConfig);
|
|
3734
|
+
let providerName;
|
|
3735
|
+
let baseUrl;
|
|
3736
|
+
let targetApiType;
|
|
3737
|
+
if (lookup) {
|
|
3738
|
+
providerName = lookup.providerName;
|
|
3739
|
+
baseUrl = lookup.baseUrl;
|
|
3740
|
+
targetApiType = lookup.apiType;
|
|
3741
|
+
} else {
|
|
3742
|
+
const reqUrl = new URL(req.url);
|
|
3743
|
+
baseUrl = `${reqUrl.protocol}//${reqUrl.host}`;
|
|
3744
|
+
providerName = apiType.split("-")[0];
|
|
3745
|
+
targetApiType = apiType;
|
|
3746
|
+
}
|
|
3747
|
+
const auth = resolveApiKey(providerName, openclawConfig, authProfiles);
|
|
3748
|
+
if (!auth) {
|
|
3749
|
+
return jsonErrorResponse(`No auth credentials found for provider: ${providerName}`, 502);
|
|
3750
|
+
}
|
|
3751
|
+
const authInfo = {
|
|
3752
|
+
apiKey: auth.apiKey,
|
|
3753
|
+
headerName: auth.headerName,
|
|
3754
|
+
headerValue: auth.headerValue,
|
|
3755
|
+
awsAccessKeyId: auth.awsAccessKeyId,
|
|
3756
|
+
awsSecretKey: auth.awsSecretKey,
|
|
3757
|
+
awsSessionToken: auth.awsSessionToken,
|
|
3758
|
+
awsRegion: auth.awsRegion
|
|
247
3759
|
};
|
|
3760
|
+
const actualModelId = decision.model.split("/").slice(1).join("/");
|
|
3761
|
+
const isCrossProvider = targetApiType !== "" && targetApiType !== apiType;
|
|
3762
|
+
const targetAdapter = isCrossProvider ? getAdapter(targetApiType) : undefined;
|
|
3763
|
+
const requestAdapter = targetAdapter ?? adapter;
|
|
3764
|
+
const upstream = requestAdapter.buildUpstreamRequest(effectiveParsed, actualModelId, baseUrl, authInfo);
|
|
3765
|
+
let upstreamResponse;
|
|
3766
|
+
try {
|
|
3767
|
+
upstreamResponse = await fetch(upstream.url, {
|
|
3768
|
+
method: upstream.method,
|
|
3769
|
+
headers: upstream.headers,
|
|
3770
|
+
body: upstream.body
|
|
3771
|
+
});
|
|
3772
|
+
} catch (err) {
|
|
3773
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3774
|
+
return jsonErrorResponse(`Upstream request failed: ${message}`, 502);
|
|
3775
|
+
}
|
|
3776
|
+
console.log(`[clawmux] [llm] ${decision.tier} → ${decision.model} | conf=${classification.confidence.toFixed(2)}${classification.reasoning ? ` | ${classification.reasoning}` : ""}`);
|
|
3777
|
+
if (compressionMiddleware && upstreamResponse.ok) {
|
|
3778
|
+
compressionMiddleware.afterResponse(parsed, adapter, baseUrl, authInfo);
|
|
3779
|
+
}
|
|
3780
|
+
const isCodexUpstream = targetApiType === "openai-codex-responses";
|
|
3781
|
+
if (isCodexUpstream && upstreamResponse.ok && upstreamResponse.body) {
|
|
3782
|
+
if (effectiveParsed.stream) {
|
|
3783
|
+
return translateResponse(requestAdapter, adapter, upstreamResponse, true);
|
|
3784
|
+
}
|
|
3785
|
+
const collected = await collectCodexStream(requestAdapter, upstreamResponse);
|
|
3786
|
+
const translated = adapter.buildResponse(collected);
|
|
3787
|
+
return new Response(JSON.stringify(translated), {
|
|
3788
|
+
status: 200,
|
|
3789
|
+
headers: { "content-type": "application/json" }
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
if (targetAdapter && upstreamResponse.ok) {
|
|
3793
|
+
return translateResponse(targetAdapter, adapter, upstreamResponse, effectiveParsed.stream);
|
|
3794
|
+
}
|
|
3795
|
+
return new Response(upstreamResponse.body, {
|
|
3796
|
+
status: upstreamResponse.status,
|
|
3797
|
+
statusText: upstreamResponse.statusText,
|
|
3798
|
+
headers: upstreamResponse.headers
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
function createResolvedCompressionMiddleware(config, openclawConfig, piAiCatalog, statsTracker) {
|
|
3802
|
+
const contextWindows = config.routing.contextWindows ?? {};
|
|
3803
|
+
const resolvedContextWindow = resolveCompressionContextWindow(config.routing.models, contextWindows, openclawConfig, piAiCatalog);
|
|
3804
|
+
const tiers = ["LIGHT", "MEDIUM", "HEAVY"];
|
|
3805
|
+
for (const tier of tiers) {
|
|
3806
|
+
const modelKey = config.routing.models[tier];
|
|
3807
|
+
if (modelKey) {
|
|
3808
|
+
const window = resolveContextWindow(modelKey, contextWindows, openclawConfig, piAiCatalog);
|
|
3809
|
+
console.log(`[clawmux] ${tier} → ${modelKey} contextWindow=${window}`);
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
console.log(`[clawmux] Compression contextWindow=${resolvedContextWindow} (minimum across tiers)`);
|
|
3813
|
+
return createCompressionMiddleware({
|
|
3814
|
+
threshold: config.compression.threshold,
|
|
3815
|
+
targetRatio: config.compression.targetRatio ?? 0.6,
|
|
3816
|
+
compressionModel: config.compression.model,
|
|
3817
|
+
resolvedContextWindow,
|
|
3818
|
+
statsTracker
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3821
|
+
var ROUTE_MAPPINGS = [
|
|
3822
|
+
{ apiType: "anthropic-messages", key: "/v1/messages" },
|
|
3823
|
+
{ apiType: "openai-completions", key: "/v1/chat/completions" },
|
|
3824
|
+
{ apiType: "openai-responses", key: "/v1/responses" },
|
|
3825
|
+
{ apiType: "google-generative-ai", key: "/v1beta/models/*" },
|
|
3826
|
+
{ apiType: "ollama", key: "/api/chat" },
|
|
3827
|
+
{ apiType: "bedrock-converse-stream", key: "/model/*/converse-stream" }
|
|
3828
|
+
];
|
|
3829
|
+
function setupPipelineRoutes(config, openclawConfig, authProfiles, compressionMiddleware) {
|
|
3830
|
+
for (const mapping of ROUTE_MAPPINGS) {
|
|
3831
|
+
setRouteHandler(mapping.key, (req, body) => handleApiRequest(req, body, mapping.apiType, config, openclawConfig, authProfiles, compressionMiddleware));
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
// src/index.ts
|
|
3836
|
+
var import_node_path4 = require("node:path");
|
|
3837
|
+
async function bootstrap(portOverride) {
|
|
3838
|
+
const configPath = process.env.CLAWMUX_CONFIG ? import_node_path4.resolve(process.env.CLAWMUX_CONFIG) : import_node_path4.resolve(process.cwd(), "clawmux.json");
|
|
3839
|
+
const result = await loadConfig(configPath);
|
|
3840
|
+
if (!result.valid) {
|
|
3841
|
+
console.error("[clawmux] Config errors:");
|
|
3842
|
+
for (const err of result.errors)
|
|
3843
|
+
console.error(` - ${err}`);
|
|
3844
|
+
process.exit(1);
|
|
3845
|
+
}
|
|
3846
|
+
const config = result.config;
|
|
3847
|
+
const openclawConfig = await readOpenClawConfig();
|
|
3848
|
+
const authProfiles = await readAuthProfiles();
|
|
3849
|
+
const piAiCatalog = await loadPiAiCatalog();
|
|
3850
|
+
const compressionMiddleware = createResolvedCompressionMiddleware(config, openclawConfig, piAiCatalog);
|
|
3851
|
+
setupPipelineRoutes(config, openclawConfig, authProfiles, compressionMiddleware);
|
|
3852
|
+
const port = portOverride ?? parseInt(process.env.CLAWMUX_PORT ?? "3456", 10);
|
|
3853
|
+
const server = createServer({ port, host: "127.0.0.1" });
|
|
3854
|
+
server.start();
|
|
3855
|
+
console.log(`[clawmux] Proxy server running on http://127.0.0.1:${port}`);
|
|
3856
|
+
const watcher = createConfigWatcher(configPath, (newConfig) => {
|
|
3857
|
+
console.log("[clawmux] Config reloaded, updating routes...");
|
|
3858
|
+
clearCustomHandlers();
|
|
3859
|
+
const newCompression = createResolvedCompressionMiddleware(newConfig, openclawConfig, piAiCatalog);
|
|
3860
|
+
setupPipelineRoutes(newConfig, openclawConfig, authProfiles, newCompression);
|
|
3861
|
+
});
|
|
3862
|
+
watcher.start();
|
|
3863
|
+
}
|
|
3864
|
+
if (typeof Bun !== "undefined" && Bun.main === "/home/runner/work/ClawMux/ClawMux/src/index.ts") {
|
|
3865
|
+
bootstrap().catch((err) => {
|
|
3866
|
+
console.error(`[clawmux] Fatal: ${err.message}`);
|
|
3867
|
+
process.exit(1);
|
|
3868
|
+
});
|
|
248
3869
|
}
|
|
249
3870
|
|
|
250
3871
|
// src/utils/logger.ts
|
|
251
|
-
var
|
|
252
|
-
var
|
|
253
|
-
var LOG_DIR =
|
|
3872
|
+
var import_node_fs2 = require("node:fs");
|
|
3873
|
+
var import_node_path5 = require("node:path");
|
|
3874
|
+
var LOG_DIR = import_node_path5.join(process.env.HOME ?? "/root", ".openclaw", "clawmux");
|
|
254
3875
|
var MAX_DAYS = 7;
|
|
255
3876
|
var fileStream = null;
|
|
256
3877
|
var currentDate = "";
|
|
@@ -259,7 +3880,7 @@ function todayString() {
|
|
|
259
3880
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
260
3881
|
}
|
|
261
3882
|
function logPath(date) {
|
|
262
|
-
return
|
|
3883
|
+
return import_node_path5.join(LOG_DIR, `${date}.log`);
|
|
263
3884
|
}
|
|
264
3885
|
function rotateIfNeeded() {
|
|
265
3886
|
const today = todayString();
|
|
@@ -269,15 +3890,15 @@ function rotateIfNeeded() {
|
|
|
269
3890
|
fileStream.end();
|
|
270
3891
|
}
|
|
271
3892
|
currentDate = today;
|
|
272
|
-
fileStream =
|
|
3893
|
+
fileStream = import_node_fs2.createWriteStream(logPath(today), { flags: "a" });
|
|
273
3894
|
purgeOldLogs();
|
|
274
3895
|
}
|
|
275
3896
|
function purgeOldLogs() {
|
|
276
3897
|
try {
|
|
277
|
-
const files =
|
|
3898
|
+
const files = import_node_fs2.readdirSync(LOG_DIR).filter((f) => f.endsWith(".log")).sort();
|
|
278
3899
|
while (files.length > MAX_DAYS) {
|
|
279
3900
|
const oldest = files.shift();
|
|
280
|
-
|
|
3901
|
+
import_node_fs2.unlinkSync(import_node_path5.join(LOG_DIR, oldest));
|
|
281
3902
|
}
|
|
282
3903
|
} catch (_) {}
|
|
283
3904
|
}
|
|
@@ -294,7 +3915,7 @@ function writeLine(level, args) {
|
|
|
294
3915
|
}
|
|
295
3916
|
}
|
|
296
3917
|
function initLogger() {
|
|
297
|
-
|
|
3918
|
+
import_node_fs2.mkdirSync(LOG_DIR, { recursive: true });
|
|
298
3919
|
rotateIfNeeded();
|
|
299
3920
|
const origLog = console.log.bind(console);
|
|
300
3921
|
const origError = console.error.bind(console);
|
|
@@ -317,7 +3938,7 @@ function getLogDir() {
|
|
|
317
3938
|
}
|
|
318
3939
|
|
|
319
3940
|
// src/cli.ts
|
|
320
|
-
var VERSION2 = process.env.npm_package_version ?? "0.3.
|
|
3941
|
+
var VERSION2 = process.env.npm_package_version ?? "0.3.2";
|
|
321
3942
|
var SERVICE_NAME = "clawmux";
|
|
322
3943
|
var HELP = `Usage: clawmux <command>
|
|
323
3944
|
|
|
@@ -339,10 +3960,20 @@ Environment:
|
|
|
339
3960
|
CLAWMUX_PORT Server port override
|
|
340
3961
|
OPENCLAW_CONFIG_PATH Path to openclaw.json`;
|
|
341
3962
|
var PROVIDER_KEY = "clawmux";
|
|
342
|
-
var
|
|
3963
|
+
var PROVIDER_API_FALLBACK = "openai-responses";
|
|
3964
|
+
function resolveProviderApi(mediumModel, openclawProviders) {
|
|
3965
|
+
const providerName = mediumModel.split("/")[0];
|
|
3966
|
+
if (!providerName)
|
|
3967
|
+
return PROVIDER_API_FALLBACK;
|
|
3968
|
+
const providerConfig = openclawProviders[providerName];
|
|
3969
|
+
const api = providerConfig?.["api"];
|
|
3970
|
+
if (typeof api === "string" && api.length > 0)
|
|
3971
|
+
return api;
|
|
3972
|
+
return PROVIDER_API_FALLBACK;
|
|
3973
|
+
}
|
|
343
3974
|
async function fileExistsLocal(path) {
|
|
344
3975
|
try {
|
|
345
|
-
await
|
|
3976
|
+
await import_promises6.access(path);
|
|
346
3977
|
return true;
|
|
347
3978
|
} catch {
|
|
348
3979
|
return false;
|
|
@@ -367,13 +3998,13 @@ function resolveClawmuxBin() {
|
|
|
367
3998
|
return detectPackageManager() === "bunx" ? "bunx clawmux" : "npx clawmux";
|
|
368
3999
|
}
|
|
369
4000
|
}
|
|
370
|
-
function
|
|
4001
|
+
function getHomeDir2() {
|
|
371
4002
|
return process.env.HOME ?? "/root";
|
|
372
4003
|
}
|
|
373
|
-
var SYSTEMD_DIR =
|
|
374
|
-
var SYSTEMD_PATH =
|
|
375
|
-
var LAUNCHD_DIR =
|
|
376
|
-
var LAUNCHD_PATH =
|
|
4004
|
+
var SYSTEMD_DIR = import_node_path6.join(getHomeDir2(), ".config", "systemd", "user");
|
|
4005
|
+
var SYSTEMD_PATH = import_node_path6.join(SYSTEMD_DIR, `${SERVICE_NAME}.service`);
|
|
4006
|
+
var LAUNCHD_DIR = import_node_path6.join(getHomeDir2(), "Library", "LaunchAgents");
|
|
4007
|
+
var LAUNCHD_PATH = import_node_path6.join(LAUNCHD_DIR, `com.${SERVICE_NAME}.plist`);
|
|
377
4008
|
function buildSystemdUnit(bin, port, workDir) {
|
|
378
4009
|
return `[Unit]
|
|
379
4010
|
Description=ClawMux - Smart model routing proxy
|
|
@@ -392,7 +4023,7 @@ WantedBy=default.target
|
|
|
392
4023
|
`;
|
|
393
4024
|
}
|
|
394
4025
|
function buildLaunchdPlist(bin, port, workDir) {
|
|
395
|
-
const logDir =
|
|
4026
|
+
const logDir = import_node_path6.join(getHomeDir2(), ".openclaw", "clawmux");
|
|
396
4027
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
397
4028
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
398
4029
|
<plist version="1.0">
|
|
@@ -431,8 +4062,8 @@ async function installService(port, workDir) {
|
|
|
431
4062
|
const bin = resolveClawmuxBin();
|
|
432
4063
|
const os = import_node_os.platform();
|
|
433
4064
|
if (os === "linux") {
|
|
434
|
-
await
|
|
435
|
-
await
|
|
4065
|
+
await import_promises6.mkdir(SYSTEMD_DIR, { recursive: true });
|
|
4066
|
+
await import_promises6.writeFile(SYSTEMD_PATH, buildSystemdUnit(bin, port, workDir));
|
|
436
4067
|
try {
|
|
437
4068
|
import_node_child_process.execSync("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
438
4069
|
import_node_child_process.execSync(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "pipe" });
|
|
@@ -446,10 +4077,10 @@ async function installService(port, workDir) {
|
|
|
446
4077
|
console.warn(" You can start manually: clawmux start");
|
|
447
4078
|
}
|
|
448
4079
|
} else if (os === "darwin") {
|
|
449
|
-
await
|
|
450
|
-
const logDir =
|
|
451
|
-
await
|
|
452
|
-
await
|
|
4080
|
+
await import_promises6.mkdir(LAUNCHD_DIR, { recursive: true });
|
|
4081
|
+
const logDir = import_node_path6.join(getHomeDir2(), ".openclaw", "clawmux");
|
|
4082
|
+
await import_promises6.mkdir(logDir, { recursive: true });
|
|
4083
|
+
await import_promises6.writeFile(LAUNCHD_PATH, buildLaunchdPlist(bin, port, workDir));
|
|
453
4084
|
try {
|
|
454
4085
|
import_node_child_process.execSync(`launchctl load -w ${LAUNCHD_PATH}`, { stdio: "pipe" });
|
|
455
4086
|
console.log("[info] launchd service installed and started");
|
|
@@ -512,7 +4143,7 @@ async function removeService() {
|
|
|
512
4143
|
import_node_child_process.execSync(`systemctl --user disable ${SERVICE_NAME}`, { stdio: "pipe" });
|
|
513
4144
|
} catch (_) {}
|
|
514
4145
|
if (await fileExistsLocal(SYSTEMD_PATH)) {
|
|
515
|
-
await
|
|
4146
|
+
await import_promises6.unlink(SYSTEMD_PATH);
|
|
516
4147
|
import_node_child_process.execSync("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
517
4148
|
console.log("[info] systemd service removed");
|
|
518
4149
|
}
|
|
@@ -521,7 +4152,7 @@ async function removeService() {
|
|
|
521
4152
|
import_node_child_process.execSync(`launchctl unload ${LAUNCHD_PATH}`, { stdio: "pipe" });
|
|
522
4153
|
} catch (_) {}
|
|
523
4154
|
if (await fileExistsLocal(LAUNCHD_PATH)) {
|
|
524
|
-
await
|
|
4155
|
+
await import_promises6.unlink(LAUNCHD_PATH);
|
|
525
4156
|
console.log("[info] launchd plist removed");
|
|
526
4157
|
}
|
|
527
4158
|
}
|
|
@@ -596,8 +4227,8 @@ async function update() {
|
|
|
596
4227
|
async function init() {
|
|
597
4228
|
const args = process.argv.slice(2);
|
|
598
4229
|
const noService = args.includes("--no-service");
|
|
599
|
-
const homeDir =
|
|
600
|
-
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ??
|
|
4230
|
+
const homeDir = getHomeDir2();
|
|
4231
|
+
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path6.join(homeDir, ".openclaw", "openclaw.json");
|
|
601
4232
|
if (!await fileExistsLocal(openclawConfigPath)) {
|
|
602
4233
|
console.error(`[error] OpenClaw config not found at ${openclawConfigPath}`);
|
|
603
4234
|
console.error("Set OPENCLAW_CONFIG_PATH or ensure ~/.openclaw/openclaw.json exists");
|
|
@@ -605,25 +4236,25 @@ async function init() {
|
|
|
605
4236
|
}
|
|
606
4237
|
console.log(`[info] Using OpenClaw config: ${openclawConfigPath}`);
|
|
607
4238
|
const backupPath = `${openclawConfigPath}.bak.${Date.now()}`;
|
|
608
|
-
await
|
|
4239
|
+
await import_promises6.copyFile(openclawConfigPath, backupPath);
|
|
609
4240
|
console.log(`[info] Backup created: ${backupPath}`);
|
|
610
|
-
const clawmuxJsonPath =
|
|
611
|
-
const examplePath =
|
|
4241
|
+
const clawmuxJsonPath = import_node_path6.join(process.cwd(), "clawmux.json");
|
|
4242
|
+
const examplePath = import_node_path6.join(process.cwd(), "clawmux.example.json");
|
|
612
4243
|
if (!await fileExistsLocal(clawmuxJsonPath)) {
|
|
613
4244
|
if (await fileExistsLocal(examplePath)) {
|
|
614
|
-
await
|
|
4245
|
+
await import_promises6.copyFile(examplePath, clawmuxJsonPath);
|
|
615
4246
|
console.log("[info] Created clawmux.json from clawmux.example.json");
|
|
616
4247
|
} else {
|
|
617
4248
|
const defaultConfig = {
|
|
618
4249
|
compression: { threshold: 0.75, model: "" },
|
|
619
4250
|
routing: { models: { LIGHT: "", MEDIUM: "", HEAVY: "" } }
|
|
620
4251
|
};
|
|
621
|
-
await
|
|
4252
|
+
await import_promises6.writeFile(clawmuxJsonPath, JSON.stringify(defaultConfig, null, 2) + `
|
|
622
4253
|
`);
|
|
623
4254
|
console.log("[info] Created default clawmux.json (configure models before use)");
|
|
624
4255
|
}
|
|
625
4256
|
}
|
|
626
|
-
const raw = await
|
|
4257
|
+
const raw = await import_promises6.readFile(openclawConfigPath, "utf-8");
|
|
627
4258
|
const config = JSON.parse(raw);
|
|
628
4259
|
if (!config.models)
|
|
629
4260
|
config.models = {};
|
|
@@ -631,17 +4262,41 @@ async function init() {
|
|
|
631
4262
|
if (!models.providers)
|
|
632
4263
|
models.providers = {};
|
|
633
4264
|
const providers = models.providers;
|
|
4265
|
+
let providerApi = PROVIDER_API_FALLBACK;
|
|
4266
|
+
try {
|
|
4267
|
+
const clawmuxRaw = await import_promises6.readFile(clawmuxJsonPath, "utf-8");
|
|
4268
|
+
const clawmuxConfig = JSON.parse(clawmuxRaw);
|
|
4269
|
+
const routing = clawmuxConfig["routing"];
|
|
4270
|
+
const routingModels = routing?.["models"];
|
|
4271
|
+
const mediumModel = routingModels?.["MEDIUM"];
|
|
4272
|
+
if (typeof mediumModel === "string" && mediumModel.length > 0) {
|
|
4273
|
+
providerApi = resolveProviderApi(mediumModel, providers);
|
|
4274
|
+
console.log(`[info] MEDIUM model: ${mediumModel} → provider api: ${providerApi}`);
|
|
4275
|
+
} else {
|
|
4276
|
+
console.log(`[info] MEDIUM model not configured yet, using default api: ${providerApi}`);
|
|
4277
|
+
}
|
|
4278
|
+
} catch {
|
|
4279
|
+
console.log(`[info] clawmux.json not readable, using default api: ${providerApi}`);
|
|
4280
|
+
}
|
|
634
4281
|
if (providers[PROVIDER_KEY]) {
|
|
635
|
-
|
|
4282
|
+
const existing = providers[PROVIDER_KEY];
|
|
4283
|
+
if (existing["api"] !== providerApi) {
|
|
4284
|
+
existing["api"] = providerApi;
|
|
4285
|
+
await import_promises6.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
|
|
4286
|
+
`);
|
|
4287
|
+
console.log(` updated ${PROVIDER_KEY} provider api → ${providerApi}`);
|
|
4288
|
+
} else {
|
|
4289
|
+
console.log(` skip ${PROVIDER_KEY} (already exists, api=${providerApi})`);
|
|
4290
|
+
}
|
|
636
4291
|
} else {
|
|
637
4292
|
providers[PROVIDER_KEY] = {
|
|
638
4293
|
baseUrl: "http://localhost:3456",
|
|
639
|
-
api:
|
|
4294
|
+
api: providerApi,
|
|
640
4295
|
models: [{ id: "auto", name: "ClawMux Auto Router" }]
|
|
641
4296
|
};
|
|
642
|
-
await
|
|
4297
|
+
await import_promises6.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
|
|
643
4298
|
`);
|
|
644
|
-
console.log(` added ${PROVIDER_KEY} provider to openclaw.json`);
|
|
4299
|
+
console.log(` added ${PROVIDER_KEY} provider to openclaw.json (api=${providerApi})`);
|
|
645
4300
|
}
|
|
646
4301
|
const port = process.env.CLAWMUX_PORT ?? "3456";
|
|
647
4302
|
if (!noService) {
|
|
@@ -662,7 +4317,7 @@ Next steps:`);
|
|
|
662
4317
|
console.log(" 3. Select a provider: openclaw provider clawmux-openai");
|
|
663
4318
|
console.log(" 4. Start chatting: openclaw chat");
|
|
664
4319
|
}
|
|
665
|
-
function start() {
|
|
4320
|
+
async function start() {
|
|
666
4321
|
const args = process.argv.slice(2);
|
|
667
4322
|
let port = parseInt(process.env.CLAWMUX_PORT ?? "3456", 10);
|
|
668
4323
|
const portIdx = args.indexOf("--port") !== -1 ? args.indexOf("--port") : args.indexOf("-p");
|
|
@@ -670,20 +4325,18 @@ function start() {
|
|
|
670
4325
|
port = parseInt(args[portIdx + 1], 10);
|
|
671
4326
|
}
|
|
672
4327
|
initLogger();
|
|
673
|
-
|
|
674
|
-
server.start();
|
|
675
|
-
console.log(`[clawmux] Proxy server running on http://127.0.0.1:${port}`);
|
|
4328
|
+
await bootstrap(port);
|
|
676
4329
|
console.log(`[clawmux] Logs: ${getLogDir()}`);
|
|
677
4330
|
checkForUpdate();
|
|
678
4331
|
}
|
|
679
4332
|
async function uninstall() {
|
|
680
4333
|
await removeService();
|
|
681
|
-
const homeDir =
|
|
682
|
-
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ??
|
|
4334
|
+
const homeDir = getHomeDir2();
|
|
4335
|
+
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path6.join(homeDir, ".openclaw", "openclaw.json");
|
|
683
4336
|
if (await fileExistsLocal(openclawConfigPath)) {
|
|
684
4337
|
const backupPath = `${openclawConfigPath}.bak.${Date.now()}`;
|
|
685
|
-
await
|
|
686
|
-
const raw = await
|
|
4338
|
+
await import_promises6.copyFile(openclawConfigPath, backupPath);
|
|
4339
|
+
const raw = await import_promises6.readFile(openclawConfigPath, "utf-8");
|
|
687
4340
|
const config = JSON.parse(raw);
|
|
688
4341
|
const models = config.models ?? {};
|
|
689
4342
|
const providers = models.providers ?? {};
|
|
@@ -695,7 +4348,7 @@ async function uninstall() {
|
|
|
695
4348
|
}
|
|
696
4349
|
}
|
|
697
4350
|
if (removed > 0) {
|
|
698
|
-
await
|
|
4351
|
+
await import_promises6.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
|
|
699
4352
|
`);
|
|
700
4353
|
console.log(`[info] Removed ${removed} ClawMux provider(s) from openclaw.json`);
|
|
701
4354
|
}
|
|
@@ -711,7 +4364,10 @@ switch (command) {
|
|
|
711
4364
|
});
|
|
712
4365
|
break;
|
|
713
4366
|
case "start":
|
|
714
|
-
start()
|
|
4367
|
+
start().catch((err) => {
|
|
4368
|
+
console.error(`[error] ${err.message}`);
|
|
4369
|
+
process.exit(1);
|
|
4370
|
+
});
|
|
715
4371
|
break;
|
|
716
4372
|
case "stop":
|
|
717
4373
|
stopService();
|