@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.
@@ -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: 6e4,
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/exulu/tool.ts
131
- import CryptoJS from "crypto-js";
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/utils/sanitize-name.ts
134
- var sanitizeName = (name) => {
135
- return name.toLowerCase().replace(/ /g, "_")?.trim();
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/tool.ts
139
- import { randomUUID } from "crypto";
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/app/singleton.ts
142
- var instance = null;
143
- var exuluApp = {
144
- get: () => {
145
- if (!instance) {
146
- throw new Error("ExuluApp not initialized");
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
- return instance;
149
- },
150
- set: (app) => {
151
- instance = app;
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
- const variableName = agent.providerapikey;
217
- if (variableName) {
218
- console.log("[EXULU] provider api key variable name", variableName);
219
- const variable = await db2.from("variables").where({ name: variableName }).first();
220
- if (!variable) {
221
- throw new Error(
222
- "Provider API key variable not found for " + agent.name + " (" + agent.id + ")."
223
- );
224
- }
225
- providerapikey = variable.value;
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-K4W6OJ3G.js");
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((resolve2) => setTimeout(resolve2, Math.pow(2, attempt) * 1e3));
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((resolve2) => setTimeout(resolve2, backoffMs));
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: "providerapikey",
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 existsSync2 } from "fs";
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 existsSync(join2(root, dep.check.packageName));
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 = resolve(absPath);
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 (!existsSync2(sessionDir)) {
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 = existsSync2(sessionDir);
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, resolve(absPath));
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 = spawn("/bin/bash", ["-c", wrapped]);
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,