@exulu/backend 1.58.0 → 1.59.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,530 @@ 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, ...envWithoutDebug } = process.env;
266
+ const child = spawn(
267
+ cfg.litellmBin,
268
+ [
269
+ "--config",
270
+ cfg.configPath,
271
+ "--port",
272
+ cfg.port,
273
+ "--host",
274
+ cfg.host
275
+ ],
276
+ {
277
+ stdio: ["ignore", "pipe", "pipe"],
278
+ env: envWithoutDebug
279
+ }
280
+ );
281
+ child.stdout?.on("data", (chunk) => {
282
+ chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(l));
283
+ });
284
+ child.stderr?.on("data", (chunk) => {
285
+ chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(`stderr: ${l}`));
286
+ });
287
+ return child;
288
+ };
289
+ var supervise = async (cfg) => {
290
+ while (!internal.shutdownRequested && internal.crashCount < MAX_CRASHES) {
291
+ internal.state = internal.crashCount === 0 ? "starting" : "respawning";
292
+ internal.child = spawnLiteLLM(cfg);
293
+ const exitPromise = new Promise((resolveFn) => {
294
+ internal.child.on("exit", (code2) => resolveFn(code2));
295
+ });
296
+ try {
297
+ await Promise.race([
298
+ pollHealth(cfg.host, cfg.port).then(() => "ready"),
299
+ exitPromise.then((code2) => ({ exited: code2 }))
300
+ ]);
301
+ } catch (err) {
302
+ log(`Readiness probe failed: ${err.message}`);
303
+ try {
304
+ internal.child?.kill("SIGTERM");
305
+ } catch {
306
+ }
307
+ }
308
+ if (!internal.child?.killed && internal.child?.exitCode === null) {
309
+ internal.state = "ready";
310
+ internal.crashCount = 0;
311
+ internal.backoffMs = INITIAL_BACKOFF_MS;
312
+ log("LiteLLM is ready.");
313
+ }
314
+ const code = await exitPromise;
315
+ internal.state = "respawning";
316
+ internal.child = void 0;
317
+ if (internal.shutdownRequested) {
318
+ log("Child exited during shutdown; supervisor stopping.");
319
+ internal.state = "stopped";
320
+ return;
321
+ }
322
+ internal.crashCount += 1;
323
+ log(
324
+ `LiteLLM exited (code=${code}). Crash ${internal.crashCount}/${MAX_CRASHES}. Respawning in ${internal.backoffMs}ms.`
325
+ );
326
+ if (internal.crashCount >= MAX_CRASHES) {
327
+ log(
328
+ "LiteLLM keeps crashing \u2014 fix the config and restart Exulu. Giving up on respawn."
329
+ );
330
+ internal.state = "given_up";
331
+ return;
332
+ }
333
+ await new Promise((r) => setTimeout(r, internal.backoffMs));
334
+ internal.backoffMs = Math.min(internal.backoffMs * 2, MAX_BACKOFF_MS);
335
+ }
336
+ };
337
+ var _packageRoot;
338
+ var setLiteLLMPackageRoot = (root) => {
339
+ _packageRoot = root;
340
+ };
341
+ var startLiteLLMSupervisor = async (options = {}) => {
342
+ if (!isLiteLLMEnabled()) return;
343
+ if (internal.readyPromise) {
344
+ return internal.readyPromise;
345
+ }
346
+ if (!process.env.LITELLM_MASTER_KEY) {
347
+ throw new Error(
348
+ "EXULU_USE_LITELLM is true but LITELLM_MASTER_KEY is not set. Set LITELLM_MASTER_KEY to a strong secret and restart Exulu."
349
+ );
350
+ }
351
+ const packageRoot = options.packageRoot ?? _packageRoot;
352
+ if (!packageRoot) {
353
+ throw new Error(
354
+ "LiteLLM supervisor: package root not set. Call setLiteLLMPackageRoot() from the boot path before starting the supervisor."
355
+ );
356
+ }
357
+ const cfg = resolveConfig(packageRoot);
358
+ if (!existsSync(cfg.configPath)) {
359
+ log(
360
+ `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.`
361
+ );
362
+ internal.state = "given_up";
363
+ return;
364
+ }
365
+ if (!existsSync(cfg.litellmBin)) {
366
+ log(
367
+ `LiteLLM binary not found at ${cfg.litellmBin}. The Python venv may not be set up. Run setupPythonEnvironment() from @exulu/backend, then restart.`
368
+ );
369
+ internal.state = "given_up";
370
+ return;
371
+ }
372
+ internal.readyPromise = (async () => {
373
+ supervise(cfg);
374
+ const deadline = Date.now() + READY_TIMEOUT_MS + 5e3;
375
+ while (Date.now() < deadline) {
376
+ if (internal.state === "ready") return;
377
+ if (internal.state === "given_up") {
378
+ throw new Error("LiteLLM supervisor gave up before becoming ready.");
379
+ }
380
+ await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
381
+ }
382
+ throw new Error("Timed out waiting for LiteLLM supervisor readiness.");
383
+ })();
384
+ registerShutdownHandlers();
385
+ return internal.readyPromise;
386
+ };
387
+ var waitForLiteLLMReady = async () => {
388
+ if (!isLiteLLMEnabled()) return;
389
+ if (!internal.readyPromise) {
390
+ await startLiteLLMSupervisor();
391
+ return;
392
+ }
393
+ return internal.readyPromise;
394
+ };
395
+ var stopLiteLLM = (signal = "SIGTERM") => {
396
+ internal.shutdownRequested = true;
397
+ const child = internal.child;
398
+ if (!child) return;
399
+ try {
400
+ child.kill(signal);
401
+ } catch {
402
+ }
403
+ setTimeout(() => {
404
+ try {
405
+ if (!child.killed && child.exitCode === null) {
406
+ child.kill("SIGKILL");
407
+ }
408
+ } catch {
409
+ }
410
+ }, SHUTDOWN_GRACE_MS).unref();
411
+ };
412
+ var shutdownHandlersRegistered = false;
413
+ var registerShutdownHandlers = () => {
414
+ if (shutdownHandlersRegistered) return;
415
+ shutdownHandlersRegistered = true;
416
+ process.on("SIGINT", () => stopLiteLLM("SIGTERM"));
417
+ process.on("SIGTERM", () => stopLiteLLM("SIGTERM"));
418
+ process.on("exit", () => stopLiteLLM("SIGTERM"));
136
419
  };
137
420
 
138
- // src/exulu/tool.ts
139
- import { randomUUID } from "crypto";
421
+ // src/exulu/tags.ts
422
+ var MAX_LEN = 63;
423
+ function sanitizeTagValue(raw) {
424
+ if (raw === void 0 || raw === null) return void 0;
425
+ const s = String(raw).normalize("NFKC").toLowerCase().replace(/[^a-z0-9_-]/g, "-");
426
+ return s.slice(0, MAX_LEN);
427
+ }
428
+ function buildTags(input) {
429
+ const candidates = [];
430
+ if (input.user_id) {
431
+ candidates.push("user_id_" + input.user_id);
432
+ }
433
+ if (input.user_name) {
434
+ candidates.push("user_name_" + input.user_name);
435
+ }
436
+ if (input.role_id) {
437
+ candidates.push("role_id_" + input.role_id);
438
+ }
439
+ if (input.role_name) {
440
+ candidates.push("role_name_" + input.role_name);
441
+ }
442
+ if (input.project_id) {
443
+ candidates.push("project_id_" + input.project_id);
444
+ }
445
+ if (input.project_name) {
446
+ candidates.push("project_name_" + input.project_name);
447
+ }
448
+ if (input.agent_id) {
449
+ candidates.push("agent_id_" + input.agent_id);
450
+ }
451
+ if (input.agent_name) {
452
+ candidates.push("agent_name_" + input.agent_name);
453
+ }
454
+ console.log("[EXULU] Candidates", candidates);
455
+ const out = [];
456
+ for (const candidate of candidates) {
457
+ if (candidate === void 0 || candidate === null) continue;
458
+ const value = sanitizeTagValue(candidate);
459
+ console.log("[EXULU] Sanitized tag value", value);
460
+ if (value === void 0 || value === "") continue;
461
+ out.push(value);
462
+ }
463
+ return out;
464
+ }
465
+ function decodeBody(body) {
466
+ if (typeof body === "string") return body;
467
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
468
+ return void 0;
469
+ }
470
+ function stripContentLengthHeader(headers) {
471
+ if (!headers) return headers;
472
+ if (typeof headers.delete === "function") {
473
+ const clone = new globalThis.Headers(headers);
474
+ clone.delete("content-length");
475
+ return clone;
476
+ }
477
+ if (Array.isArray(headers)) {
478
+ return headers.filter(
479
+ ([k]) => k.toLowerCase() !== "content-length"
480
+ );
481
+ }
482
+ const out = {};
483
+ for (const [k, v] of Object.entries(headers)) {
484
+ if (k.toLowerCase() === "content-length") continue;
485
+ out[k] = v;
486
+ }
487
+ return out;
488
+ }
489
+ function createTaggedFetch(tags) {
490
+ const labeled = async (input, init) => {
491
+ try {
492
+ if (!init || !init.body) return globalThis.fetch(input, init);
493
+ const method = (init.method ?? "POST").toUpperCase();
494
+ if (method !== "POST" && method !== "PUT" && method !== "PATCH") {
495
+ return globalThis.fetch(input, init);
496
+ }
497
+ const decoded = decodeBody(init.body);
498
+ if (decoded === void 0) {
499
+ console.warn("[vertex-labels] unsupported body type, forwarding unchanged");
500
+ return globalThis.fetch(input, init);
501
+ }
502
+ let parsed;
503
+ try {
504
+ parsed = JSON.parse(decoded);
505
+ } catch {
506
+ console.warn("[vertex-labels] body is not JSON, forwarding unchanged");
507
+ return globalThis.fetch(input, init);
508
+ }
509
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
510
+ return globalThis.fetch(input, init);
511
+ }
512
+ const existing = parsed.metadata ?? {};
513
+ parsed.metadata = { ...existing, tags };
514
+ console.log("[EXULU] tags", parsed.metadata);
515
+ const nextInit = {
516
+ ...init,
517
+ body: JSON.stringify(parsed),
518
+ headers: stripContentLengthHeader(init.headers)
519
+ };
520
+ return globalThis.fetch(input, nextInit);
521
+ } catch (err) {
522
+ console.warn("[vertex-labels] label injection failed, forwarding unchanged", err);
523
+ return globalThis.fetch(input, init);
524
+ }
525
+ };
526
+ return labeled;
527
+ }
140
528
 
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");
529
+ // src/exulu/resolve-model.ts
530
+ var LITELLM_PROVIDER_SENTINEL = new Proxy(
531
+ {},
532
+ {
533
+ get(_target, prop) {
534
+ if (prop === "id") return "litellm";
535
+ if (prop === Symbol.toPrimitive || prop === "toString") {
536
+ return () => "[LiteLLMProviderSentinel]";
537
+ }
538
+ console.error(`ExuluProvider.${String(prop)} is not available in LiteLLM mode. `, new Error().stack);
539
+ throw new Error(
540
+ `ExuluProvider.${String(prop)} is not available in LiteLLM mode. Code paths that depend on the in-code provider catalog must check isLiteLLMEnabled() and degrade.`
541
+ );
147
542
  }
148
- return instance;
149
- },
150
- set: (app) => {
151
- instance = app;
543
+ }
544
+ );
545
+ var ResolveModelError = class extends Error {
546
+ constructor(code, message) {
547
+ super(message);
548
+ this.code = code;
549
+ this.name = "ResolveModelError";
152
550
  }
153
551
  };
552
+ var _litellmProvider;
553
+ var getLiteLLMProvider = ({
554
+ user,
555
+ role,
556
+ project,
557
+ agent
558
+ }) => {
559
+ if (_litellmProvider) return _litellmProvider;
560
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
561
+ const port = process.env.LITELLM_PORT ?? "4000";
562
+ const masterKey = process.env.LITELLM_MASTER_KEY;
563
+ const tags = buildTags({
564
+ user,
565
+ role,
566
+ project,
567
+ agent
568
+ });
569
+ if (!masterKey) {
570
+ throw new ResolveModelError(
571
+ "LITELLM_NOT_CONFIGURED",
572
+ "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
573
+ );
574
+ }
575
+ _litellmProvider = createOpenAICompatible({
576
+ name: "litellm",
577
+ baseURL: `http://${host}:${port}/v1`,
578
+ apiKey: masterKey,
579
+ fetch: createTaggedFetch(tags)
580
+ });
581
+ return _litellmProvider;
582
+ };
583
+ async function resolveModel(input) {
584
+ const { modelId, user, providers, agent, project, rbacBypass } = input;
585
+ const rbacRequest = input.rbacRequest ?? "read";
586
+ if (isLiteLLMEnabled()) {
587
+ try {
588
+ await waitForLiteLLMReady();
589
+ } catch (err) {
590
+ throw new ResolveModelError(
591
+ "LITELLM_NOT_READY",
592
+ `LiteLLM is not ready: ${err.message}`
593
+ );
594
+ }
595
+ const litellm = getLiteLLMProvider({
596
+ user: user?.id,
597
+ role: user?.role?.id,
598
+ project: project?.id,
599
+ agent: agent?.id
600
+ });
601
+ const languageModel2 = litellm(modelId);
602
+ const syntheticModel = {
603
+ id: modelId,
604
+ name: modelId,
605
+ provider: modelId,
606
+ active: true,
607
+ rights_mode: "public",
608
+ created_by: "litellm"
609
+ };
610
+ return {
611
+ languageModel: languageModel2,
612
+ model: syntheticModel,
613
+ exuluProvider: LITELLM_PROVIDER_SENTINEL,
614
+ apiKey: void 0
615
+ };
616
+ }
617
+ const { db: db2 } = await postgresClient();
618
+ const model = await db2.from("models").where({ id: modelId }).first();
619
+ if (!model) {
620
+ throw new ResolveModelError("MODEL_NOT_FOUND", `Model ${modelId} not found`);
621
+ }
622
+ if (!model.active) {
623
+ throw new ResolveModelError("MODEL_INACTIVE", `Model ${model.name} is inactive`);
624
+ }
625
+ if (!rbacBypass) {
626
+ const ok = await checkRecordAccess(model, rbacRequest, user);
627
+ if (!ok) {
628
+ throw new ResolveModelError(
629
+ "MODEL_FORBIDDEN",
630
+ `No ${rbacRequest} access to model ${model.name}`
631
+ );
632
+ }
633
+ }
634
+ const exuluProvider = providers.find((p) => p.id === model.provider);
635
+ if (!exuluProvider) {
636
+ throw new ResolveModelError(
637
+ "PROVIDER_NOT_FOUND",
638
+ `ExuluProvider ${model.provider} (referenced by model ${model.name}) not registered in this instance`
639
+ );
640
+ }
641
+ if (!exuluProvider.config?.model?.create) {
642
+ throw new ResolveModelError(
643
+ "PROVIDER_NO_MODEL",
644
+ `ExuluProvider ${exuluProvider.id} has no model.create()`
645
+ );
646
+ }
647
+ let apiKey;
648
+ if (model.authvariable) {
649
+ const variable = await db2.from("variables").where({ name: model.authvariable }).first();
650
+ if (!variable) {
651
+ throw new ResolveModelError(
652
+ "AUTH_VAR_NOT_FOUND",
653
+ `Auth variable ${model.authvariable} (referenced by model ${model.name}) not found`
654
+ );
655
+ }
656
+ if (!variable.encrypted) {
657
+ throw new ResolveModelError(
658
+ "AUTH_VAR_NOT_ENCRYPTED",
659
+ `Auth variable ${model.authvariable} must be encrypted`
660
+ );
661
+ }
662
+ const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
663
+ apiKey = bytes.toString(CryptoJS.enc.Utf8);
664
+ }
665
+ const languageModel = exuluProvider.config.model.create({
666
+ apiKey,
667
+ user: user?.id,
668
+ role: user?.role?.id,
669
+ project: project?.id,
670
+ agent: agent?.id
671
+ });
672
+ return { languageModel, model, exuluProvider, apiKey };
673
+ }
154
674
 
155
675
  // src/exulu/tool.ts
156
676
  var ExuluTool = class {
@@ -211,29 +731,19 @@ var ExuluTool = class {
211
731
  if (!agent) {
212
732
  throw new Error("Agent not found.");
213
733
  }
214
- const { db: db2 } = await postgresClient();
215
734
  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
- }
735
+ if (agent.model) {
736
+ const providers = exuluApp.get().providers;
737
+ const resolved = await resolveModel({
738
+ modelId: agent.model,
739
+ user,
740
+ providers,
741
+ agent: { id: agent.id },
742
+ rbacBypass: true
743
+ });
744
+ providerapikey = resolved.apiKey;
235
745
  }
236
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-K4W6OJ3G.js");
746
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-ZEECMX43.js");
237
747
  const tools = await convertExuluToolsToAiSdkTools2(
238
748
  [this],
239
749
  [],
@@ -560,7 +1070,7 @@ async function withRetry(generateFn, maxRetries = 3) {
560
1070
  if (attempt === maxRetries) {
561
1071
  throw error;
562
1072
  }
563
- await new Promise((resolve2) => setTimeout(resolve2, Math.pow(2, attempt) * 1e3));
1073
+ await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * 1e3));
564
1074
  }
565
1075
  }
566
1076
  throw lastError;
@@ -929,7 +1439,7 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
929
1439
  if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
930
1440
  if (attempt < maxRetries) {
931
1441
  const backoffMs = Math.pow(2, attempt) * 1e3;
932
- await new Promise((resolve2) => setTimeout(resolve2, backoffMs));
1442
+ await new Promise((resolve3) => setTimeout(resolve3, backoffMs));
933
1443
  s3Client = void 0;
934
1444
  getS3Client(config);
935
1445
  continue;
@@ -2279,6 +2789,10 @@ var agentMessagesSchema = {
2279
2789
  {
2280
2790
  name: "session",
2281
2791
  type: "text"
2792
+ },
2793
+ {
2794
+ name: "model",
2795
+ type: "text"
2282
2796
  }
2283
2797
  ]
2284
2798
  };
@@ -2459,6 +2973,11 @@ var agentsSchema = {
2459
2973
  name: "feedback",
2460
2974
  type: "boolean"
2461
2975
  },
2976
+ {
2977
+ name: "suggestions_enabled",
2978
+ type: "boolean",
2979
+ default: false
2980
+ },
2462
2981
  {
2463
2982
  name: "description",
2464
2983
  type: "text"
@@ -2477,11 +2996,7 @@ var agentsSchema = {
2477
2996
  // allows selecting a exulu context as native memory for the agent
2478
2997
  },
2479
2998
  {
2480
- name: "providerapikey",
2481
- type: "text"
2482
- },
2483
- {
2484
- name: "provider",
2999
+ name: "model",
2485
3000
  type: "text"
2486
3001
  },
2487
3002
  {
@@ -2511,6 +3026,59 @@ var agentsSchema = {
2511
3026
  }
2512
3027
  ]
2513
3028
  };
3029
+ var modelsSchema = {
3030
+ type: "models",
3031
+ name: {
3032
+ plural: "models",
3033
+ singular: "model"
3034
+ },
3035
+ RBAC: true,
3036
+ fields: [
3037
+ {
3038
+ name: "name",
3039
+ type: "text",
3040
+ required: true
3041
+ },
3042
+ {
3043
+ name: "description",
3044
+ type: "text"
3045
+ },
3046
+ {
3047
+ name: "provider",
3048
+ type: "text",
3049
+ required: true
3050
+ },
3051
+ {
3052
+ name: "authvariable",
3053
+ type: "text"
3054
+ },
3055
+ {
3056
+ name: "active",
3057
+ type: "boolean",
3058
+ default: true
3059
+ },
3060
+ {
3061
+ name: "requests_per_window",
3062
+ type: "number"
3063
+ },
3064
+ {
3065
+ name: "window_seconds",
3066
+ type: "number"
3067
+ },
3068
+ {
3069
+ name: "token_budget",
3070
+ type: "number"
3071
+ },
3072
+ {
3073
+ name: "cost_budget_usd",
3074
+ type: "number"
3075
+ },
3076
+ {
3077
+ name: "budget_window",
3078
+ type: "text"
3079
+ }
3080
+ ]
3081
+ };
2514
3082
  var usersSchema = {
2515
3083
  type: "users",
2516
3084
  name: {
@@ -2806,6 +3374,7 @@ var coreSchemas = {
2806
3374
  agentsSchema: () => addCoreFields(agentsSchema),
2807
3375
  agentMessagesSchema: () => addCoreFields(agentMessagesSchema),
2808
3376
  agentSessionsSchema: () => addCoreFields(agentSessionsSchema),
3377
+ modelsSchema: () => addCoreFields(modelsSchema),
2809
3378
  projectsSchema: () => addCoreFields(projectsSchema),
2810
3379
  usersSchema: () => addCoreFields(usersSchema),
2811
3380
  skillsSchema: () => addCoreFields(skillsSchema),
@@ -5667,14 +6236,14 @@ import {
5667
6236
  SandboxManager
5668
6237
  } from "@anthropic-ai/sandbox-runtime";
5669
6238
  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";
6239
+ import { existsSync as existsSync3 } from "fs";
6240
+ import { join as join3, dirname, resolve as resolve2, relative, posix } from "path";
6241
+ import { exec as exec2, spawn as spawn2 } from "child_process";
5673
6242
  import { promisify as promisify2 } from "util";
5674
6243
 
5675
6244
  // src/exulu/system-dependencies.ts
5676
6245
  import { exec } from "child_process";
5677
- import { existsSync } from "fs";
6246
+ import { existsSync as existsSync2 } from "fs";
5678
6247
  import { join as join2 } from "path";
5679
6248
  import { promisify } from "util";
5680
6249
  var execAsync = promisify(exec);
@@ -5740,7 +6309,7 @@ async function probeDependency(dep) {
5740
6309
  case "npm-global": {
5741
6310
  const root = await getNpmGlobalRoot();
5742
6311
  if (!root) return false;
5743
- return existsSync(join2(root, dep.check.packageName));
6312
+ return existsSync2(join2(root, dep.check.packageName));
5744
6313
  }
5745
6314
  }
5746
6315
  }
@@ -5840,7 +6409,7 @@ async function downloadSkill(skill, skillsDirectory, config) {
5840
6409
  }
5841
6410
  }
5842
6411
  function isArtifactPath(absPath, sessionDir) {
5843
- const resolved = resolve(absPath);
6412
+ const resolved = resolve2(absPath);
5844
6413
  const rel = relative(sessionDir, resolved);
5845
6414
  if (!rel || rel.startsWith("..")) return false;
5846
6415
  const first = rel.split("/")[0];
@@ -5899,7 +6468,7 @@ async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config) {
5899
6468
  async function downloadKeyIntoSandbox(opts) {
5900
6469
  const { sessionId, userId, fullS3Key, config } = opts;
5901
6470
  const sessionDir = join3("/tmp", "exulu-sessions", sessionId);
5902
- if (!existsSync2(sessionDir)) {
6471
+ if (!existsSync3(sessionDir)) {
5903
6472
  return { written: false };
5904
6473
  }
5905
6474
  const userPrefix = `user_${userId}/sessions/${sessionId}/`;
@@ -5933,7 +6502,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
5933
6502
  return cached.handle;
5934
6503
  }
5935
6504
  const sessionDir = join3("/tmp", "exulu-sessions", sessionId);
5936
- const dirExisted = existsSync2(sessionDir);
6505
+ const dirExisted = existsSync3(sessionDir);
5937
6506
  await mkdir2(sessionDir, { recursive: true });
5938
6507
  const skillsDirectory = join3(sessionDir, "skills");
5939
6508
  const installedSkills = /* @__PURE__ */ new Map();
@@ -6040,7 +6609,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
6040
6609
  if (!persistenceEnabled || !isArtifactPath(absPath, sessionDir)) {
6041
6610
  return {};
6042
6611
  }
6043
- const rel = relative(sessionDir, resolve(absPath));
6612
+ const rel = relative(sessionDir, resolve2(absPath));
6044
6613
  const s3Key = artifactS3Key(sessionId, rel);
6045
6614
  const out = {};
6046
6615
  try {
@@ -6085,7 +6654,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
6085
6654
  sessionSandboxConfig
6086
6655
  );
6087
6656
  await new Promise((resolveSpawn, rejectSpawn) => {
6088
- const child = spawn("/bin/bash", ["-c", wrapped]);
6657
+ const child = spawn2("/bin/bash", ["-c", wrapped]);
6089
6658
  let stderr = "";
6090
6659
  child.stderr.on("data", (chunk) => {
6091
6660
  stderr += chunk.toString();
@@ -6595,9 +7164,18 @@ export {
6595
7164
  postgresClient,
6596
7165
  authentication,
6597
7166
  STATISTICS_TYPE_ENUM,
7167
+ isLiteLLMEnabled,
7168
+ setLiteLLMPackageRoot,
7169
+ startLiteLLMSupervisor,
7170
+ waitForLiteLLMReady,
6598
7171
  sanitizeName,
7172
+ checkRecordAccess,
6599
7173
  checkLicense,
6600
7174
  exuluApp,
7175
+ buildTags,
7176
+ createTaggedFetch,
7177
+ ResolveModelError,
7178
+ resolveModel,
6601
7179
  updateStatistic,
6602
7180
  createProjectItemsRetrievalTool,
6603
7181
  sanitizeToolName,