@exulu/backend 1.58.0 → 1.60.0
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/dist/catalog-EOKGOHTY.js +10 -0
- package/dist/{chunk-RVLZ5EL3.js → chunk-23YNGK3V.js} +645 -66
- package/dist/chunk-YS27XOXI.js +62 -0
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-K4W6OJ3G.js → convert-exulu-tools-to-ai-sdk-tools-PLLM2CJL.js} +1 -1
- package/dist/index.cjs +2826 -1239
- package/dist/index.d.cts +13 -14
- package/dist/index.d.ts +13 -14
- package/dist/index.js +2031 -1141
- package/ee/python/.litellm/config.yaml.example +64 -0
- package/ee/python/requirements.txt +15 -0
- package/ee/python/setup.sh +13 -0
- package/ee/workers.ts +15 -29
- package/package.json +3 -1
|
@@ -5,6 +5,32 @@ import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2, S3Service
|
|
|
5
5
|
import { tool } from "ai";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
|
+
// src/utils/sanitize-name.ts
|
|
9
|
+
var sanitizeName = (name) => {
|
|
10
|
+
return name.toLowerCase().replace(/ /g, "_")?.trim();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/exulu/tool.ts
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
15
|
+
|
|
16
|
+
// src/exulu/app/singleton.ts
|
|
17
|
+
var instance = null;
|
|
18
|
+
var exuluApp = {
|
|
19
|
+
get: () => {
|
|
20
|
+
if (!instance) {
|
|
21
|
+
throw new Error("ExuluApp not initialized");
|
|
22
|
+
}
|
|
23
|
+
return instance;
|
|
24
|
+
},
|
|
25
|
+
set: (app) => {
|
|
26
|
+
instance = app;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/exulu/resolve-model.ts
|
|
31
|
+
import CryptoJS from "crypto-js";
|
|
32
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
33
|
+
|
|
8
34
|
// src/postgres/client.ts
|
|
9
35
|
import Knex from "knex";
|
|
10
36
|
import "knex";
|
|
@@ -72,26 +98,20 @@ async function postgresClient() {
|
|
|
72
98
|
database: dbName,
|
|
73
99
|
password: process.env.POSTGRES_DB_PASSWORD,
|
|
74
100
|
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
101
|
+
// TCP keepalive prevents idle sockets from being silently dropped by
|
|
102
|
+
// intermediate network devices (NAT, firewalls) between us and Hetzner.
|
|
103
|
+
keepAlive: true,
|
|
104
|
+
keepAliveInitialDelayMillis: 1e4,
|
|
75
105
|
connectionTimeoutMillis: 3e4,
|
|
76
|
-
// Increased from 10s to 30s to handle connection spikes
|
|
77
|
-
// PostgreSQL statement timeout (in milliseconds) - kills queries that run too long
|
|
78
|
-
// This prevents runaway queries from blocking connections
|
|
79
106
|
statement_timeout: 18e5,
|
|
80
|
-
// 30 minutes - should be longer than max job timeout (1200s = 20m)
|
|
81
|
-
// Connection idle timeout - how long pg client waits before timing out
|
|
82
107
|
query_timeout: 18e5
|
|
83
|
-
// 30 minutes
|
|
84
108
|
},
|
|
85
109
|
pool: {
|
|
86
110
|
min: 10,
|
|
87
|
-
// Minimum connections always ready
|
|
88
111
|
max: 300,
|
|
89
|
-
// Increased to support high worker concurrency (250+ concurrent jobs)
|
|
90
112
|
acquireTimeoutMillis: 12e4,
|
|
91
|
-
// 2 minutes - increased to handle high contention during bursts
|
|
92
113
|
createTimeoutMillis: 3e4,
|
|
93
|
-
idleTimeoutMillis:
|
|
94
|
-
// Keep connections alive for reuse
|
|
114
|
+
idleTimeoutMillis: 3e4,
|
|
95
115
|
reapIntervalMillis: 1e3,
|
|
96
116
|
createRetryIntervalMillis: 200,
|
|
97
117
|
// Enable propagateCreateError to properly handle connection creation failures
|
|
@@ -127,30 +147,531 @@ async function postgresClient() {
|
|
|
127
147
|
};
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
// src/
|
|
131
|
-
|
|
150
|
+
// src/utils/check-record-access.ts
|
|
151
|
+
var checkRecordAccessCache = /* @__PURE__ */ new Map();
|
|
152
|
+
var checkRecordAccess = async (record, request, user) => {
|
|
153
|
+
const setRecordAccessCache = (hasAccess2) => {
|
|
154
|
+
checkRecordAccessCache.set(`${record.id}-${request}-${user?.id}`, {
|
|
155
|
+
hasAccess: hasAccess2,
|
|
156
|
+
expiresAt: new Date(Date.now() + 1e3 * 60 * 1)
|
|
157
|
+
// 1 minute
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const cachedAccess = checkRecordAccessCache.get(`${record.id}-${request}-${user?.id}`);
|
|
161
|
+
if (cachedAccess && cachedAccess.expiresAt > /* @__PURE__ */ new Date()) {
|
|
162
|
+
return cachedAccess.hasAccess;
|
|
163
|
+
}
|
|
164
|
+
const isPublic = record.rights_mode === "public";
|
|
165
|
+
const byUsers = record.rights_mode === "users";
|
|
166
|
+
const byRoles = record.rights_mode === "roles";
|
|
167
|
+
const createdBy = typeof record.created_by === "string" ? record.created_by : record.created_by?.toString();
|
|
168
|
+
const isCreator = user ? createdBy === user.id.toString() : false;
|
|
169
|
+
const isAdmin = user ? user.super_admin : false;
|
|
170
|
+
const isApi = user ? user.type === "api" : false;
|
|
171
|
+
const isAdminApi = isApi && (!user.scope_mode || user.scope_mode === "admin");
|
|
172
|
+
const isAgentsScopedApi = isApi && user.scope_mode === "agents" && request === "read" && Array.isArray(user.agent_ids) && user.agent_ids.includes(String(record.id));
|
|
173
|
+
let hasAccess = "none";
|
|
174
|
+
if (isPublic || isCreator || isAdmin || isAdminApi || isAgentsScopedApi) {
|
|
175
|
+
setRecordAccessCache(true);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (byUsers) {
|
|
179
|
+
if (!user) {
|
|
180
|
+
setRecordAccessCache(false);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
hasAccess = record.RBAC?.users?.find((x) => x.id === user.id)?.rights || "none";
|
|
184
|
+
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
185
|
+
console.error(
|
|
186
|
+
`[EXULU] Your current user ${user.id} does not have access to this record, current access type is: ${hasAccess}.`
|
|
187
|
+
);
|
|
188
|
+
setRecordAccessCache(false);
|
|
189
|
+
return false;
|
|
190
|
+
} else {
|
|
191
|
+
setRecordAccessCache(true);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (byRoles) {
|
|
196
|
+
if (!user) {
|
|
197
|
+
setRecordAccessCache(false);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
hasAccess = record.RBAC?.roles?.find((x) => x.id === user.role?.id)?.rights || "none";
|
|
201
|
+
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
202
|
+
console.error(
|
|
203
|
+
`[EXULU] Your current role ${user.role?.name} does not have access to this record, current access type is: ${hasAccess}.`
|
|
204
|
+
);
|
|
205
|
+
setRecordAccessCache(false);
|
|
206
|
+
return false;
|
|
207
|
+
} else {
|
|
208
|
+
setRecordAccessCache(true);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
setRecordAccessCache(false);
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
132
215
|
|
|
133
|
-
// src/
|
|
134
|
-
|
|
135
|
-
|
|
216
|
+
// src/exulu/litellm/supervisor.ts
|
|
217
|
+
import { spawn } from "child_process";
|
|
218
|
+
import { existsSync } from "fs";
|
|
219
|
+
import { resolve } from "path";
|
|
220
|
+
var MAX_CRASHES = 5;
|
|
221
|
+
var INITIAL_BACKOFF_MS = 1e3;
|
|
222
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
223
|
+
var READY_TIMEOUT_MS = 3e4;
|
|
224
|
+
var READY_POLL_INTERVAL_MS = 200;
|
|
225
|
+
var SHUTDOWN_GRACE_MS = 5e3;
|
|
226
|
+
var internal = {
|
|
227
|
+
child: void 0,
|
|
228
|
+
state: "idle",
|
|
229
|
+
crashCount: 0,
|
|
230
|
+
backoffMs: INITIAL_BACKOFF_MS,
|
|
231
|
+
readyPromise: void 0,
|
|
232
|
+
shutdownRequested: false
|
|
233
|
+
};
|
|
234
|
+
var isLiteLLMEnabled = () => process.env.EXULU_USE_LITELLM === "true";
|
|
235
|
+
var resolveConfig = (packageRoot) => {
|
|
236
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
237
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
238
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
239
|
+
const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve(packageRoot, "./config.litellm.yaml");
|
|
240
|
+
const venvBin = resolve(packageRoot, "ee/python/.venv/bin");
|
|
241
|
+
const venvPython = resolve(venvBin, "python");
|
|
242
|
+
const litellmBin = resolve(venvBin, "litellm");
|
|
243
|
+
return { host, port, masterKey, configPath, venvBin, venvPython, litellmBin };
|
|
244
|
+
};
|
|
245
|
+
var log = (line) => console.log(`[EXULU-LITELLM] ${line}`);
|
|
246
|
+
var pollHealth = async (host, port) => {
|
|
247
|
+
const url = `http://${host}:${port}/health/liveliness`;
|
|
248
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
249
|
+
while (Date.now() < deadline) {
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch(url, { method: "GET" });
|
|
252
|
+
if (res.ok) return;
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
|
|
256
|
+
}
|
|
257
|
+
throw new Error(
|
|
258
|
+
`LiteLLM did not become ready at ${url} within ${READY_TIMEOUT_MS}ms`
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
var spawnLiteLLM = (cfg) => {
|
|
262
|
+
log(
|
|
263
|
+
`Spawning LiteLLM: ${cfg.litellmBin} --config ${cfg.configPath} --port ${cfg.port} --host ${cfg.host}`
|
|
264
|
+
);
|
|
265
|
+
const { DEBUG: _debug, ...rest } = process.env;
|
|
266
|
+
const childEnv = { ...rest, DEBUG: "false" };
|
|
267
|
+
const child = spawn(
|
|
268
|
+
cfg.litellmBin,
|
|
269
|
+
[
|
|
270
|
+
"--config",
|
|
271
|
+
cfg.configPath,
|
|
272
|
+
"--port",
|
|
273
|
+
cfg.port,
|
|
274
|
+
"--host",
|
|
275
|
+
cfg.host
|
|
276
|
+
],
|
|
277
|
+
{
|
|
278
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
279
|
+
env: childEnv
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
child.stdout?.on("data", (chunk) => {
|
|
283
|
+
chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(l));
|
|
284
|
+
});
|
|
285
|
+
child.stderr?.on("data", (chunk) => {
|
|
286
|
+
chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(`stderr: ${l}`));
|
|
287
|
+
});
|
|
288
|
+
return child;
|
|
289
|
+
};
|
|
290
|
+
var supervise = async (cfg) => {
|
|
291
|
+
while (!internal.shutdownRequested && internal.crashCount < MAX_CRASHES) {
|
|
292
|
+
internal.state = internal.crashCount === 0 ? "starting" : "respawning";
|
|
293
|
+
internal.child = spawnLiteLLM(cfg);
|
|
294
|
+
const exitPromise = new Promise((resolveFn) => {
|
|
295
|
+
internal.child.on("exit", (code2) => resolveFn(code2));
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
await Promise.race([
|
|
299
|
+
pollHealth(cfg.host, cfg.port).then(() => "ready"),
|
|
300
|
+
exitPromise.then((code2) => ({ exited: code2 }))
|
|
301
|
+
]);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
log(`Readiness probe failed: ${err.message}`);
|
|
304
|
+
try {
|
|
305
|
+
internal.child?.kill("SIGTERM");
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!internal.child?.killed && internal.child?.exitCode === null) {
|
|
310
|
+
internal.state = "ready";
|
|
311
|
+
internal.crashCount = 0;
|
|
312
|
+
internal.backoffMs = INITIAL_BACKOFF_MS;
|
|
313
|
+
log("LiteLLM is ready.");
|
|
314
|
+
}
|
|
315
|
+
const code = await exitPromise;
|
|
316
|
+
internal.state = "respawning";
|
|
317
|
+
internal.child = void 0;
|
|
318
|
+
if (internal.shutdownRequested) {
|
|
319
|
+
log("Child exited during shutdown; supervisor stopping.");
|
|
320
|
+
internal.state = "stopped";
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
internal.crashCount += 1;
|
|
324
|
+
log(
|
|
325
|
+
`LiteLLM exited (code=${code}). Crash ${internal.crashCount}/${MAX_CRASHES}. Respawning in ${internal.backoffMs}ms.`
|
|
326
|
+
);
|
|
327
|
+
if (internal.crashCount >= MAX_CRASHES) {
|
|
328
|
+
log(
|
|
329
|
+
"LiteLLM keeps crashing \u2014 fix the config and restart Exulu. Giving up on respawn."
|
|
330
|
+
);
|
|
331
|
+
internal.state = "given_up";
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
await new Promise((r) => setTimeout(r, internal.backoffMs));
|
|
335
|
+
internal.backoffMs = Math.min(internal.backoffMs * 2, MAX_BACKOFF_MS);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
var _packageRoot;
|
|
339
|
+
var setLiteLLMPackageRoot = (root) => {
|
|
340
|
+
_packageRoot = root;
|
|
341
|
+
};
|
|
342
|
+
var startLiteLLMSupervisor = async (options = {}) => {
|
|
343
|
+
if (!isLiteLLMEnabled()) return;
|
|
344
|
+
if (internal.readyPromise) {
|
|
345
|
+
return internal.readyPromise;
|
|
346
|
+
}
|
|
347
|
+
if (!process.env.LITELLM_MASTER_KEY) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
"EXULU_USE_LITELLM is true but LITELLM_MASTER_KEY is not set. Set LITELLM_MASTER_KEY to a strong secret and restart Exulu."
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const packageRoot = options.packageRoot ?? _packageRoot;
|
|
353
|
+
if (!packageRoot) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
"LiteLLM supervisor: package root not set. Call setLiteLLMPackageRoot() from the boot path before starting the supervisor."
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const cfg = resolveConfig(packageRoot);
|
|
359
|
+
if (!existsSync(cfg.configPath)) {
|
|
360
|
+
log(
|
|
361
|
+
`LiteLLM config not found at ${cfg.configPath}. Copy ee/python/.litellm/config.yaml.example to that path, edit it, and restart Exulu. LiteLLM will NOT be started until then.`
|
|
362
|
+
);
|
|
363
|
+
internal.state = "given_up";
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!existsSync(cfg.litellmBin)) {
|
|
367
|
+
log(
|
|
368
|
+
`LiteLLM binary not found at ${cfg.litellmBin}. The Python venv may not be set up. Run setupPythonEnvironment() from @exulu/backend, then restart.`
|
|
369
|
+
);
|
|
370
|
+
internal.state = "given_up";
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
internal.readyPromise = (async () => {
|
|
374
|
+
supervise(cfg);
|
|
375
|
+
const deadline = Date.now() + READY_TIMEOUT_MS + 5e3;
|
|
376
|
+
while (Date.now() < deadline) {
|
|
377
|
+
if (internal.state === "ready") return;
|
|
378
|
+
if (internal.state === "given_up") {
|
|
379
|
+
throw new Error("LiteLLM supervisor gave up before becoming ready.");
|
|
380
|
+
}
|
|
381
|
+
await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
|
|
382
|
+
}
|
|
383
|
+
throw new Error("Timed out waiting for LiteLLM supervisor readiness.");
|
|
384
|
+
})();
|
|
385
|
+
registerShutdownHandlers();
|
|
386
|
+
return internal.readyPromise;
|
|
387
|
+
};
|
|
388
|
+
var waitForLiteLLMReady = async () => {
|
|
389
|
+
if (!isLiteLLMEnabled()) return;
|
|
390
|
+
if (!internal.readyPromise) {
|
|
391
|
+
await startLiteLLMSupervisor();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
return internal.readyPromise;
|
|
395
|
+
};
|
|
396
|
+
var stopLiteLLM = (signal = "SIGTERM") => {
|
|
397
|
+
internal.shutdownRequested = true;
|
|
398
|
+
const child = internal.child;
|
|
399
|
+
if (!child) return;
|
|
400
|
+
try {
|
|
401
|
+
child.kill(signal);
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
setTimeout(() => {
|
|
405
|
+
try {
|
|
406
|
+
if (!child.killed && child.exitCode === null) {
|
|
407
|
+
child.kill("SIGKILL");
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
}, SHUTDOWN_GRACE_MS).unref();
|
|
412
|
+
};
|
|
413
|
+
var shutdownHandlersRegistered = false;
|
|
414
|
+
var registerShutdownHandlers = () => {
|
|
415
|
+
if (shutdownHandlersRegistered) return;
|
|
416
|
+
shutdownHandlersRegistered = true;
|
|
417
|
+
process.on("SIGINT", () => stopLiteLLM("SIGTERM"));
|
|
418
|
+
process.on("SIGTERM", () => stopLiteLLM("SIGTERM"));
|
|
419
|
+
process.on("exit", () => stopLiteLLM("SIGTERM"));
|
|
136
420
|
};
|
|
137
421
|
|
|
138
|
-
// src/exulu/
|
|
139
|
-
|
|
422
|
+
// src/exulu/tags.ts
|
|
423
|
+
var MAX_LEN = 63;
|
|
424
|
+
function sanitizeTagValue(raw) {
|
|
425
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
426
|
+
const s = String(raw).normalize("NFKC").toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
427
|
+
return s.slice(0, MAX_LEN);
|
|
428
|
+
}
|
|
429
|
+
function buildTags(input) {
|
|
430
|
+
const candidates = [];
|
|
431
|
+
if (input.user_id) {
|
|
432
|
+
candidates.push("user_id_" + input.user_id);
|
|
433
|
+
}
|
|
434
|
+
if (input.user_name) {
|
|
435
|
+
candidates.push("user_name_" + input.user_name);
|
|
436
|
+
}
|
|
437
|
+
if (input.role_id) {
|
|
438
|
+
candidates.push("role_id_" + input.role_id);
|
|
439
|
+
}
|
|
440
|
+
if (input.role_name) {
|
|
441
|
+
candidates.push("role_name_" + input.role_name);
|
|
442
|
+
}
|
|
443
|
+
if (input.project_id) {
|
|
444
|
+
candidates.push("project_id_" + input.project_id);
|
|
445
|
+
}
|
|
446
|
+
if (input.project_name) {
|
|
447
|
+
candidates.push("project_name_" + input.project_name);
|
|
448
|
+
}
|
|
449
|
+
if (input.agent_id) {
|
|
450
|
+
candidates.push("agent_id_" + input.agent_id);
|
|
451
|
+
}
|
|
452
|
+
if (input.agent_name) {
|
|
453
|
+
candidates.push("agent_name_" + input.agent_name);
|
|
454
|
+
}
|
|
455
|
+
console.log("[EXULU] Candidates", candidates);
|
|
456
|
+
const out = [];
|
|
457
|
+
for (const candidate of candidates) {
|
|
458
|
+
if (candidate === void 0 || candidate === null) continue;
|
|
459
|
+
const value = sanitizeTagValue(candidate);
|
|
460
|
+
console.log("[EXULU] Sanitized tag value", value);
|
|
461
|
+
if (value === void 0 || value === "") continue;
|
|
462
|
+
out.push(value);
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
function decodeBody(body) {
|
|
467
|
+
if (typeof body === "string") return body;
|
|
468
|
+
if (body instanceof Uint8Array) return new TextDecoder().decode(body);
|
|
469
|
+
return void 0;
|
|
470
|
+
}
|
|
471
|
+
function stripContentLengthHeader(headers) {
|
|
472
|
+
if (!headers) return headers;
|
|
473
|
+
if (typeof headers.delete === "function") {
|
|
474
|
+
const clone = new globalThis.Headers(headers);
|
|
475
|
+
clone.delete("content-length");
|
|
476
|
+
return clone;
|
|
477
|
+
}
|
|
478
|
+
if (Array.isArray(headers)) {
|
|
479
|
+
return headers.filter(
|
|
480
|
+
([k]) => k.toLowerCase() !== "content-length"
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
const out = {};
|
|
484
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
485
|
+
if (k.toLowerCase() === "content-length") continue;
|
|
486
|
+
out[k] = v;
|
|
487
|
+
}
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
function createTaggedFetch(tags) {
|
|
491
|
+
const labeled = async (input, init) => {
|
|
492
|
+
try {
|
|
493
|
+
if (!init || !init.body) return globalThis.fetch(input, init);
|
|
494
|
+
const method = (init.method ?? "POST").toUpperCase();
|
|
495
|
+
if (method !== "POST" && method !== "PUT" && method !== "PATCH") {
|
|
496
|
+
return globalThis.fetch(input, init);
|
|
497
|
+
}
|
|
498
|
+
const decoded = decodeBody(init.body);
|
|
499
|
+
if (decoded === void 0) {
|
|
500
|
+
console.warn("[vertex-labels] unsupported body type, forwarding unchanged");
|
|
501
|
+
return globalThis.fetch(input, init);
|
|
502
|
+
}
|
|
503
|
+
let parsed;
|
|
504
|
+
try {
|
|
505
|
+
parsed = JSON.parse(decoded);
|
|
506
|
+
} catch {
|
|
507
|
+
console.warn("[vertex-labels] body is not JSON, forwarding unchanged");
|
|
508
|
+
return globalThis.fetch(input, init);
|
|
509
|
+
}
|
|
510
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
511
|
+
return globalThis.fetch(input, init);
|
|
512
|
+
}
|
|
513
|
+
const existing = parsed.metadata ?? {};
|
|
514
|
+
parsed.metadata = { ...existing, tags };
|
|
515
|
+
console.log("[EXULU] tags", parsed.metadata);
|
|
516
|
+
const nextInit = {
|
|
517
|
+
...init,
|
|
518
|
+
body: JSON.stringify(parsed),
|
|
519
|
+
headers: stripContentLengthHeader(init.headers)
|
|
520
|
+
};
|
|
521
|
+
return globalThis.fetch(input, nextInit);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
console.warn("[vertex-labels] label injection failed, forwarding unchanged", err);
|
|
524
|
+
return globalThis.fetch(input, init);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
return labeled;
|
|
528
|
+
}
|
|
140
529
|
|
|
141
|
-
// src/exulu/
|
|
142
|
-
var
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
530
|
+
// src/exulu/resolve-model.ts
|
|
531
|
+
var LITELLM_PROVIDER_SENTINEL = new Proxy(
|
|
532
|
+
{},
|
|
533
|
+
{
|
|
534
|
+
get(_target, prop) {
|
|
535
|
+
if (prop === "id") return "litellm";
|
|
536
|
+
if (prop === Symbol.toPrimitive || prop === "toString") {
|
|
537
|
+
return () => "[LiteLLMProviderSentinel]";
|
|
538
|
+
}
|
|
539
|
+
console.error(`ExuluProvider.${String(prop)} is not available in LiteLLM mode. `, new Error().stack);
|
|
540
|
+
throw new Error(
|
|
541
|
+
`ExuluProvider.${String(prop)} is not available in LiteLLM mode. Code paths that depend on the in-code provider catalog must check isLiteLLMEnabled() and degrade.`
|
|
542
|
+
);
|
|
147
543
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
var ResolveModelError = class extends Error {
|
|
547
|
+
constructor(code, message) {
|
|
548
|
+
super(message);
|
|
549
|
+
this.code = code;
|
|
550
|
+
this.name = "ResolveModelError";
|
|
152
551
|
}
|
|
153
552
|
};
|
|
553
|
+
var _litellmProvider;
|
|
554
|
+
var getLiteLLMProvider = ({
|
|
555
|
+
user,
|
|
556
|
+
role,
|
|
557
|
+
project,
|
|
558
|
+
agent
|
|
559
|
+
}) => {
|
|
560
|
+
if (_litellmProvider) return _litellmProvider;
|
|
561
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
562
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
563
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
564
|
+
const tags = buildTags({
|
|
565
|
+
user,
|
|
566
|
+
role,
|
|
567
|
+
project,
|
|
568
|
+
agent
|
|
569
|
+
});
|
|
570
|
+
if (!masterKey) {
|
|
571
|
+
throw new ResolveModelError(
|
|
572
|
+
"LITELLM_NOT_CONFIGURED",
|
|
573
|
+
"LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
_litellmProvider = createOpenAICompatible({
|
|
577
|
+
name: "litellm",
|
|
578
|
+
baseURL: `http://${host}:${port}/v1`,
|
|
579
|
+
apiKey: masterKey,
|
|
580
|
+
fetch: createTaggedFetch(tags)
|
|
581
|
+
});
|
|
582
|
+
return _litellmProvider;
|
|
583
|
+
};
|
|
584
|
+
async function resolveModel(input) {
|
|
585
|
+
const { modelId, user, providers, agent, project, rbacBypass } = input;
|
|
586
|
+
const rbacRequest = input.rbacRequest ?? "read";
|
|
587
|
+
if (isLiteLLMEnabled()) {
|
|
588
|
+
try {
|
|
589
|
+
await waitForLiteLLMReady();
|
|
590
|
+
} catch (err) {
|
|
591
|
+
throw new ResolveModelError(
|
|
592
|
+
"LITELLM_NOT_READY",
|
|
593
|
+
`LiteLLM is not ready: ${err.message}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const litellm = getLiteLLMProvider({
|
|
597
|
+
user: user?.id,
|
|
598
|
+
role: user?.role?.id,
|
|
599
|
+
project: project?.id,
|
|
600
|
+
agent: agent?.id
|
|
601
|
+
});
|
|
602
|
+
const languageModel2 = litellm(modelId);
|
|
603
|
+
const syntheticModel = {
|
|
604
|
+
id: modelId,
|
|
605
|
+
name: modelId,
|
|
606
|
+
provider: modelId,
|
|
607
|
+
active: true,
|
|
608
|
+
rights_mode: "public",
|
|
609
|
+
created_by: "litellm"
|
|
610
|
+
};
|
|
611
|
+
return {
|
|
612
|
+
languageModel: languageModel2,
|
|
613
|
+
model: syntheticModel,
|
|
614
|
+
exuluProvider: LITELLM_PROVIDER_SENTINEL,
|
|
615
|
+
apiKey: void 0
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const { db: db2 } = await postgresClient();
|
|
619
|
+
const model = await db2.from("models").where({ id: modelId }).first();
|
|
620
|
+
if (!model) {
|
|
621
|
+
throw new ResolveModelError("MODEL_NOT_FOUND", `Model ${modelId} not found`);
|
|
622
|
+
}
|
|
623
|
+
if (!model.active) {
|
|
624
|
+
throw new ResolveModelError("MODEL_INACTIVE", `Model ${model.name} is inactive`);
|
|
625
|
+
}
|
|
626
|
+
if (!rbacBypass) {
|
|
627
|
+
const ok = await checkRecordAccess(model, rbacRequest, user);
|
|
628
|
+
if (!ok) {
|
|
629
|
+
throw new ResolveModelError(
|
|
630
|
+
"MODEL_FORBIDDEN",
|
|
631
|
+
`No ${rbacRequest} access to model ${model.name}`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const exuluProvider = providers.find((p) => p.id === model.provider);
|
|
636
|
+
if (!exuluProvider) {
|
|
637
|
+
throw new ResolveModelError(
|
|
638
|
+
"PROVIDER_NOT_FOUND",
|
|
639
|
+
`ExuluProvider ${model.provider} (referenced by model ${model.name}) not registered in this instance`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
if (!exuluProvider.config?.model?.create) {
|
|
643
|
+
throw new ResolveModelError(
|
|
644
|
+
"PROVIDER_NO_MODEL",
|
|
645
|
+
`ExuluProvider ${exuluProvider.id} has no model.create()`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
let apiKey;
|
|
649
|
+
if (model.authvariable) {
|
|
650
|
+
const variable = await db2.from("variables").where({ name: model.authvariable }).first();
|
|
651
|
+
if (!variable) {
|
|
652
|
+
throw new ResolveModelError(
|
|
653
|
+
"AUTH_VAR_NOT_FOUND",
|
|
654
|
+
`Auth variable ${model.authvariable} (referenced by model ${model.name}) not found`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (!variable.encrypted) {
|
|
658
|
+
throw new ResolveModelError(
|
|
659
|
+
"AUTH_VAR_NOT_ENCRYPTED",
|
|
660
|
+
`Auth variable ${model.authvariable} must be encrypted`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
664
|
+
apiKey = bytes.toString(CryptoJS.enc.Utf8);
|
|
665
|
+
}
|
|
666
|
+
const languageModel = exuluProvider.config.model.create({
|
|
667
|
+
apiKey,
|
|
668
|
+
user: user?.id,
|
|
669
|
+
role: user?.role?.id,
|
|
670
|
+
project: project?.id,
|
|
671
|
+
agent: agent?.id
|
|
672
|
+
});
|
|
673
|
+
return { languageModel, model, exuluProvider, apiKey };
|
|
674
|
+
}
|
|
154
675
|
|
|
155
676
|
// src/exulu/tool.ts
|
|
156
677
|
var ExuluTool = class {
|
|
@@ -211,29 +732,19 @@ var ExuluTool = class {
|
|
|
211
732
|
if (!agent) {
|
|
212
733
|
throw new Error("Agent not found.");
|
|
213
734
|
}
|
|
214
|
-
const { db: db2 } = await postgresClient();
|
|
215
735
|
let providerapikey;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
providerapikey =
|
|
226
|
-
if (!variable.encrypted) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
"Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys."
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
if (variable.encrypted) {
|
|
232
|
-
const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
233
|
-
providerapikey = bytes.toString(CryptoJS.enc.Utf8);
|
|
234
|
-
}
|
|
736
|
+
if (agent.model) {
|
|
737
|
+
const providers = exuluApp.get().providers;
|
|
738
|
+
const resolved = await resolveModel({
|
|
739
|
+
modelId: agent.model,
|
|
740
|
+
user,
|
|
741
|
+
providers,
|
|
742
|
+
agent: { id: agent.id },
|
|
743
|
+
rbacBypass: true
|
|
744
|
+
});
|
|
745
|
+
providerapikey = resolved.apiKey;
|
|
235
746
|
}
|
|
236
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
747
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-PLLM2CJL.js");
|
|
237
748
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
238
749
|
[this],
|
|
239
750
|
[],
|
|
@@ -560,7 +1071,7 @@ async function withRetry(generateFn, maxRetries = 3) {
|
|
|
560
1071
|
if (attempt === maxRetries) {
|
|
561
1072
|
throw error;
|
|
562
1073
|
}
|
|
563
|
-
await new Promise((
|
|
1074
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * 1e3));
|
|
564
1075
|
}
|
|
565
1076
|
}
|
|
566
1077
|
throw lastError;
|
|
@@ -929,7 +1440,7 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
|
|
|
929
1440
|
if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
|
|
930
1441
|
if (attempt < maxRetries) {
|
|
931
1442
|
const backoffMs = Math.pow(2, attempt) * 1e3;
|
|
932
|
-
await new Promise((
|
|
1443
|
+
await new Promise((resolve3) => setTimeout(resolve3, backoffMs));
|
|
933
1444
|
s3Client = void 0;
|
|
934
1445
|
getS3Client(config);
|
|
935
1446
|
continue;
|
|
@@ -2279,6 +2790,10 @@ var agentMessagesSchema = {
|
|
|
2279
2790
|
{
|
|
2280
2791
|
name: "session",
|
|
2281
2792
|
type: "text"
|
|
2793
|
+
},
|
|
2794
|
+
{
|
|
2795
|
+
name: "model",
|
|
2796
|
+
type: "text"
|
|
2282
2797
|
}
|
|
2283
2798
|
]
|
|
2284
2799
|
};
|
|
@@ -2459,6 +2974,11 @@ var agentsSchema = {
|
|
|
2459
2974
|
name: "feedback",
|
|
2460
2975
|
type: "boolean"
|
|
2461
2976
|
},
|
|
2977
|
+
{
|
|
2978
|
+
name: "suggestions_enabled",
|
|
2979
|
+
type: "boolean",
|
|
2980
|
+
default: false
|
|
2981
|
+
},
|
|
2462
2982
|
{
|
|
2463
2983
|
name: "description",
|
|
2464
2984
|
type: "text"
|
|
@@ -2477,11 +2997,7 @@ var agentsSchema = {
|
|
|
2477
2997
|
// allows selecting a exulu context as native memory for the agent
|
|
2478
2998
|
},
|
|
2479
2999
|
{
|
|
2480
|
-
name: "
|
|
2481
|
-
type: "text"
|
|
2482
|
-
},
|
|
2483
|
-
{
|
|
2484
|
-
name: "provider",
|
|
3000
|
+
name: "model",
|
|
2485
3001
|
type: "text"
|
|
2486
3002
|
},
|
|
2487
3003
|
{
|
|
@@ -2511,6 +3027,59 @@ var agentsSchema = {
|
|
|
2511
3027
|
}
|
|
2512
3028
|
]
|
|
2513
3029
|
};
|
|
3030
|
+
var modelsSchema = {
|
|
3031
|
+
type: "models",
|
|
3032
|
+
name: {
|
|
3033
|
+
plural: "models",
|
|
3034
|
+
singular: "model"
|
|
3035
|
+
},
|
|
3036
|
+
RBAC: true,
|
|
3037
|
+
fields: [
|
|
3038
|
+
{
|
|
3039
|
+
name: "name",
|
|
3040
|
+
type: "text",
|
|
3041
|
+
required: true
|
|
3042
|
+
},
|
|
3043
|
+
{
|
|
3044
|
+
name: "description",
|
|
3045
|
+
type: "text"
|
|
3046
|
+
},
|
|
3047
|
+
{
|
|
3048
|
+
name: "provider",
|
|
3049
|
+
type: "text",
|
|
3050
|
+
required: true
|
|
3051
|
+
},
|
|
3052
|
+
{
|
|
3053
|
+
name: "authvariable",
|
|
3054
|
+
type: "text"
|
|
3055
|
+
},
|
|
3056
|
+
{
|
|
3057
|
+
name: "active",
|
|
3058
|
+
type: "boolean",
|
|
3059
|
+
default: true
|
|
3060
|
+
},
|
|
3061
|
+
{
|
|
3062
|
+
name: "requests_per_window",
|
|
3063
|
+
type: "number"
|
|
3064
|
+
},
|
|
3065
|
+
{
|
|
3066
|
+
name: "window_seconds",
|
|
3067
|
+
type: "number"
|
|
3068
|
+
},
|
|
3069
|
+
{
|
|
3070
|
+
name: "token_budget",
|
|
3071
|
+
type: "number"
|
|
3072
|
+
},
|
|
3073
|
+
{
|
|
3074
|
+
name: "cost_budget_usd",
|
|
3075
|
+
type: "number"
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
name: "budget_window",
|
|
3079
|
+
type: "text"
|
|
3080
|
+
}
|
|
3081
|
+
]
|
|
3082
|
+
};
|
|
2514
3083
|
var usersSchema = {
|
|
2515
3084
|
type: "users",
|
|
2516
3085
|
name: {
|
|
@@ -2806,6 +3375,7 @@ var coreSchemas = {
|
|
|
2806
3375
|
agentsSchema: () => addCoreFields(agentsSchema),
|
|
2807
3376
|
agentMessagesSchema: () => addCoreFields(agentMessagesSchema),
|
|
2808
3377
|
agentSessionsSchema: () => addCoreFields(agentSessionsSchema),
|
|
3378
|
+
modelsSchema: () => addCoreFields(modelsSchema),
|
|
2809
3379
|
projectsSchema: () => addCoreFields(projectsSchema),
|
|
2810
3380
|
usersSchema: () => addCoreFields(usersSchema),
|
|
2811
3381
|
skillsSchema: () => addCoreFields(skillsSchema),
|
|
@@ -5667,14 +6237,14 @@ import {
|
|
|
5667
6237
|
SandboxManager
|
|
5668
6238
|
} from "@anthropic-ai/sandbox-runtime";
|
|
5669
6239
|
import { mkdir as mkdir2, rm, writeFile as writeFile2, readFile as fsReadFile, readdir, stat } from "fs/promises";
|
|
5670
|
-
import { existsSync as
|
|
5671
|
-
import { join as join3, dirname, resolve, relative, posix } from "path";
|
|
5672
|
-
import { exec as exec2, spawn } from "child_process";
|
|
6240
|
+
import { existsSync as existsSync3 } from "fs";
|
|
6241
|
+
import { join as join3, dirname, resolve as resolve2, relative, posix } from "path";
|
|
6242
|
+
import { exec as exec2, spawn as spawn2 } from "child_process";
|
|
5673
6243
|
import { promisify as promisify2 } from "util";
|
|
5674
6244
|
|
|
5675
6245
|
// src/exulu/system-dependencies.ts
|
|
5676
6246
|
import { exec } from "child_process";
|
|
5677
|
-
import { existsSync } from "fs";
|
|
6247
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5678
6248
|
import { join as join2 } from "path";
|
|
5679
6249
|
import { promisify } from "util";
|
|
5680
6250
|
var execAsync = promisify(exec);
|
|
@@ -5740,7 +6310,7 @@ async function probeDependency(dep) {
|
|
|
5740
6310
|
case "npm-global": {
|
|
5741
6311
|
const root = await getNpmGlobalRoot();
|
|
5742
6312
|
if (!root) return false;
|
|
5743
|
-
return
|
|
6313
|
+
return existsSync2(join2(root, dep.check.packageName));
|
|
5744
6314
|
}
|
|
5745
6315
|
}
|
|
5746
6316
|
}
|
|
@@ -5840,7 +6410,7 @@ async function downloadSkill(skill, skillsDirectory, config) {
|
|
|
5840
6410
|
}
|
|
5841
6411
|
}
|
|
5842
6412
|
function isArtifactPath(absPath, sessionDir) {
|
|
5843
|
-
const resolved =
|
|
6413
|
+
const resolved = resolve2(absPath);
|
|
5844
6414
|
const rel = relative(sessionDir, resolved);
|
|
5845
6415
|
if (!rel || rel.startsWith("..")) return false;
|
|
5846
6416
|
const first = rel.split("/")[0];
|
|
@@ -5899,7 +6469,7 @@ async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config) {
|
|
|
5899
6469
|
async function downloadKeyIntoSandbox(opts) {
|
|
5900
6470
|
const { sessionId, userId, fullS3Key, config } = opts;
|
|
5901
6471
|
const sessionDir = join3("/tmp", "exulu-sessions", sessionId);
|
|
5902
|
-
if (!
|
|
6472
|
+
if (!existsSync3(sessionDir)) {
|
|
5903
6473
|
return { written: false };
|
|
5904
6474
|
}
|
|
5905
6475
|
const userPrefix = `user_${userId}/sessions/${sessionId}/`;
|
|
@@ -5933,7 +6503,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
5933
6503
|
return cached.handle;
|
|
5934
6504
|
}
|
|
5935
6505
|
const sessionDir = join3("/tmp", "exulu-sessions", sessionId);
|
|
5936
|
-
const dirExisted =
|
|
6506
|
+
const dirExisted = existsSync3(sessionDir);
|
|
5937
6507
|
await mkdir2(sessionDir, { recursive: true });
|
|
5938
6508
|
const skillsDirectory = join3(sessionDir, "skills");
|
|
5939
6509
|
const installedSkills = /* @__PURE__ */ new Map();
|
|
@@ -6040,7 +6610,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6040
6610
|
if (!persistenceEnabled || !isArtifactPath(absPath, sessionDir)) {
|
|
6041
6611
|
return {};
|
|
6042
6612
|
}
|
|
6043
|
-
const rel = relative(sessionDir,
|
|
6613
|
+
const rel = relative(sessionDir, resolve2(absPath));
|
|
6044
6614
|
const s3Key = artifactS3Key(sessionId, rel);
|
|
6045
6615
|
const out = {};
|
|
6046
6616
|
try {
|
|
@@ -6085,7 +6655,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6085
6655
|
sessionSandboxConfig
|
|
6086
6656
|
);
|
|
6087
6657
|
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
6088
|
-
const child =
|
|
6658
|
+
const child = spawn2("/bin/bash", ["-c", wrapped]);
|
|
6089
6659
|
let stderr = "";
|
|
6090
6660
|
child.stderr.on("data", (chunk) => {
|
|
6091
6661
|
stderr += chunk.toString();
|
|
@@ -6595,9 +7165,18 @@ export {
|
|
|
6595
7165
|
postgresClient,
|
|
6596
7166
|
authentication,
|
|
6597
7167
|
STATISTICS_TYPE_ENUM,
|
|
7168
|
+
isLiteLLMEnabled,
|
|
7169
|
+
setLiteLLMPackageRoot,
|
|
7170
|
+
startLiteLLMSupervisor,
|
|
7171
|
+
waitForLiteLLMReady,
|
|
6598
7172
|
sanitizeName,
|
|
7173
|
+
checkRecordAccess,
|
|
6599
7174
|
checkLicense,
|
|
6600
7175
|
exuluApp,
|
|
7176
|
+
buildTags,
|
|
7177
|
+
createTaggedFetch,
|
|
7178
|
+
ResolveModelError,
|
|
7179
|
+
resolveModel,
|
|
6601
7180
|
updateStatistic,
|
|
6602
7181
|
createProjectItemsRetrievalTool,
|
|
6603
7182
|
sanitizeToolName,
|