@exulu/backend 1.57.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.
@@ -0,0 +1,64 @@
1
+ # LiteLLM Proxy configuration for Exulu.
2
+ #
3
+ # This file is read by the LiteLLM process Exulu spawns when EXULU_USE_LITELLM=true.
4
+ # Documentation: https://docs.litellm.ai/docs/proxy/configs
5
+ #
6
+ # To use:
7
+ # 1. Copy this file to config.yaml in the same directory.
8
+ # 2. Edit model_list to add the providers you want to expose.
9
+ # 3. Set the referenced env vars (LITELLM_MASTER_KEY, GOOGLE_VERTEX_PROJECT, etc.).
10
+ # 4. Set EXULU_USE_LITELLM=true and (re)start Exulu.
11
+
12
+ general_settings:
13
+ master_key: os.environ/LITELLM_MASTER_KEY
14
+
15
+ # Database mode (optional). Set `database_url` only if you want LiteLLM's
16
+ # user/key/budget/spend tracking tables. When set, LiteLLM creates ~60
17
+ # tables in the target database (all prefixed `LiteLLM_`).
18
+ #
19
+ # ⚠ ALWAYS use a DEDICATED Postgres database for LiteLLM — never share it
20
+ # with Exulu's main database. LiteLLM's schema sync drops any tables in
21
+ # the public schema that aren't part of its schema; sharing a database
22
+ # with Exulu has destroyed application data in the past.
23
+ #
24
+ # Exulu's ExuluDatabase.init runs `prisma db push` against this URL
25
+ # automatically at boot, but only after verifying:
26
+ # 1. database_url is NOT the same as Exulu's own Postgres (warns + skips)
27
+ # 2. the target database contains no non-LiteLLM_* tables (warns + skips)
28
+ # 3. the push itself runs without --accept-data-loss (Prisma refuses any
29
+ # schema change that would drop data)
30
+ # Re-running on an already-set-up database is a no-op.
31
+ #
32
+ # database_url: "postgresql://user:pass@host:5432/litellm_dedicated"
33
+
34
+ model_list:
35
+ # Each entry's `model_name` is what Exulu agents will reference in their
36
+ # `model` field after toggling LiteLLM on. Pick stable, human-friendly names.
37
+
38
+ - model_name: vertex-flash
39
+ litellm_params:
40
+ model: vertex_ai/gemini-2.5-flash
41
+ vertex_project: os.environ/GOOGLE_VERTEX_PROJECT
42
+ vertex_location: europe-west1
43
+ vertex_credentials: os.environ/GOOGLE_VERTEX_CREDENTIALS_JSON
44
+
45
+ - model_name: vertex-pro
46
+ litellm_params:
47
+ model: vertex_ai/gemini-2.5-pro
48
+ vertex_project: os.environ/GOOGLE_VERTEX_PROJECT
49
+ vertex_location: europe-west1
50
+ vertex_credentials: os.environ/GOOGLE_VERTEX_CREDENTIALS_JSON
51
+
52
+ - model_name: claude-haiku
53
+ litellm_params:
54
+ model: anthropic/claude-haiku-4-5
55
+ api_key: os.environ/ANTHROPIC_API_KEY
56
+
57
+ - model_name: gpt-5
58
+ litellm_params:
59
+ model: openai/gpt-5
60
+ api_key: os.environ/OPENAI_API_KEY
61
+
62
+ # Per-model rate limits and budgets are configured here in LiteLLM-native form.
63
+ # See https://docs.litellm.ai/docs/proxy/users for budget configuration and
64
+ # https://docs.litellm.ai/docs/proxy/rate_limit_tiers for rate limiting.
@@ -630,7 +630,7 @@ async function processDocument(
630
630
  source: filePath,
631
631
  }
632
632
 
633
- const stripped = filePath.split('.').pop()?.trim();
633
+ const stripped = filePath.split('.').pop()?.trim().toLowerCase();
634
634
  let result: ProcessorOutput;
635
635
  switch (stripped) {
636
636
  case 'txt':
@@ -1017,7 +1017,7 @@ export async function documentProcessor({
1017
1017
  supportedTypes = ['pdf', 'docx', 'doc', 'txt', 'md', 'jpg', 'jpeg', 'png', 'gif', 'webp'];
1018
1018
  break;
1019
1019
  case "officeparser":
1020
- supportedTypes = [];
1020
+ supportedTypes = ['docx', 'pptx', 'xlsx', 'odt', 'odp', 'ods', 'pdf', 'rtf', 'csv', 'md', 'html'];
1021
1021
  break;
1022
1022
  case "liteparse":
1023
1023
  supportedTypes = ['pdf', 'doc', 'docx', 'docm', 'odt', 'rtf', 'ppt', 'pptx', 'pptm', 'odp', 'xls', 'xlsx', 'xlsm', 'ods', 'csv', 'tsv'];
@@ -1027,8 +1027,8 @@ export async function documentProcessor({
1027
1027
  break;
1028
1028
  }
1029
1029
 
1030
- if (!supportedTypes.includes(fileType)) {
1031
- throw new Error(`[EXULU] Unsupported file type: ${fileType} for Exulu document processor, the ${config?.processor.name} processor only supports the following file types: ${supportedTypes.join(', ')}.`);
1030
+ if (!supportedTypes.includes(fileType.toLowerCase())) {
1031
+ throw new Error(`[EXULU] Unsupported file type: ${fileType.toLowerCase()} for Exulu document processor, the ${config?.processor.name} processor only supports the following file types: ${supportedTypes.join(', ')}.`);
1032
1032
  }
1033
1033
 
1034
1034
  // Process document with VLM validation enabled
@@ -1043,7 +1043,6 @@ export async function documentProcessor({
1043
1043
 
1044
1044
  return content.json;
1045
1045
 
1046
-
1047
1046
  } catch (error) {
1048
1047
  console.error('Error during chunking:', error);
1049
1048
  throw error;
@@ -2,3 +2,19 @@ docling
2
2
  transformers
3
3
  pyinstaller
4
4
  docling-hierarchical-pdf
5
+ defusedxml
6
+ # LiteLLM proxy — only used when EXULU_USE_LITELLM=true. Always installed so
7
+ # the dep is ready when the env var is flipped. Pinned to a tested version;
8
+ # upgrade deliberately.
9
+ litellm[proxy]==1.85.1
10
+ # Prisma Python client — required by LiteLLM proxy whenever a `database_url`
11
+ # is set in config.litellm.yaml (used for user/budget tracking, key
12
+ # management, etc.). NOT included in litellm[proxy] in 1.85.1, hence the
13
+ # separate pin. setup.sh runs `prisma generate` against LiteLLM's bundled
14
+ # schema after pip install so the Python client module is materialized.
15
+ prisma==0.15.0
16
+ # Vertex AI SDK — required by LiteLLM when routing to `vertex_ai/*` models
17
+ # (the `vertexai` Python module lives inside google-cloud-aiplatform).
18
+ # NOT included in litellm[proxy]; without it, requests to Vertex models
19
+ # fail with "No module named 'vertexai'".
20
+ google-cloud-aiplatform>=1.38
@@ -203,6 +203,19 @@ pip install -r "$REQUIREMENTS_FILE"
203
203
 
204
204
  print_success "All dependencies installed successfully"
205
205
 
206
+ # Step 6.5: Generate Prisma client for LiteLLM database mode.
207
+ # LiteLLM's PrismaClient does `from prisma import Prisma`, which only works
208
+ # after `prisma generate` has materialized the Python client against
209
+ # LiteLLM's bundled schema. Skip silently if LiteLLM isn't installed or its
210
+ # schema isn't where we expect — database mode is opt-in via config.litellm.yaml.
211
+ LITELLM_PROXY_DIR=$(find "$VENV_DIR/lib" -path "*/litellm/proxy" -type d 2>/dev/null | head -1)
212
+ if [ -n "$LITELLM_PROXY_DIR" ] && [ -f "$LITELLM_PROXY_DIR/schema.prisma" ]; then
213
+ print_info "Generating Prisma client for LiteLLM..."
214
+ (cd "$LITELLM_PROXY_DIR" && PATH="$VENV_DIR/bin:$PATH" "$VENV_DIR/bin/prisma" generate > /dev/null 2>&1) \
215
+ && print_success "Prisma client generated for LiteLLM" \
216
+ || print_warning "Prisma generate failed; LiteLLM database mode (database_url in config.litellm.yaml) may not work until you run 'cd $LITELLM_PROXY_DIR && PATH=$VENV_DIR/bin:\$PATH $VENV_DIR/bin/prisma generate'"
217
+ fi
218
+
206
219
  # Step 7: Validate installation
207
220
  echo ""
208
221
  echo "Step 7: Validating installation..."
package/ee/workers.ts CHANGED
@@ -10,6 +10,7 @@ import { getTableName, type ExuluContext } from "@SRC/exulu/context.ts";
10
10
  import type { ExuluReranker } from "@SRC/exulu/reranker.ts";
11
11
  import type { ExuluEval } from "@SRC/exulu/evals.ts";
12
12
  import type { ExuluTool } from "@SRC/exulu/tool.ts";
13
+ import { resolveModel } from "@SRC/exulu/resolve-model.ts";
13
14
  import { postgresClient } from "@SRC/postgres/client";
14
15
  import type { BullMqJobData } from "@EE/queues/decorator.ts";
15
16
  import { type Tracer } from "@opentelemetry/api";
@@ -405,6 +406,9 @@ export const createWorkers = async (
405
406
  // not part of the database, so remove it here before
406
407
  // we upadte the item in the db.
407
408
  delete processorResult.field;
409
+ // fts is a generated column (tsvector GENERATED ALWAYS AS ... STORED)
410
+ // and Postgres rejects any explicit update to it.
411
+ delete processorResult.fts;
408
412
 
409
413
  // Memory optimization: For large processor results (e.g., documents),
410
414
  // extract only the fields we need for the database update to avoid
@@ -1376,37 +1380,21 @@ export const processUiMessagesFlow = async ({
1376
1380
  enabledTools?.map((x) => x.name + " (" + x.id + ")"),
1377
1381
  );
1378
1382
 
1379
- // Get the variable name from user's anthropic_token field
1380
- const variableName = agent.providerapikey;
1381
-
1382
- // Look up the variable from the variables table
1383
- const { db } = await postgresClient();
1384
-
1385
- let providerapikey: string | undefined;
1386
-
1387
- if (variableName) {
1388
- const variable = await db.from("variables").where({ name: variableName }).first();
1389
- if (!variable) {
1390
- throw new Error(
1391
- `Provider API key variable not found for agent ${agent.name} (${agent.id}).`,
1392
- );
1393
- }
1394
-
1395
- // Get the API key from the variable (decrypt if encrypted)
1396
- providerapikey = variable.value;
1397
-
1398
- if (!variable.encrypted) {
1399
- throw new Error(
1400
- `Provider API key variable not encrypted for agent ${agent.name} (${agent.id}), for security reasons you are only allowed to use encrypted variables for provider API keys.`,
1401
- );
1402
- }
1403
-
1404
- if (variable.encrypted) {
1405
- const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
1406
- providerapikey = bytes.toString(CryptoJS.enc.Utf8);
1407
- }
1383
+ if (!agent.model) {
1384
+ throw new Error(
1385
+ `Agent ${agent.name} (${agent.id}) has no model configured.`,
1386
+ );
1408
1387
  }
1409
1388
 
1389
+ const resolved = await resolveModel({
1390
+ modelId: agent.model,
1391
+ user,
1392
+ providers,
1393
+ agent: { id: agent.id },
1394
+ });
1395
+ const providerapikey = resolved.apiKey;
1396
+ const resolvedLanguageModel = resolved.languageModel;
1397
+
1410
1398
  // Remove placeholder agent response before sending
1411
1399
  const messagesWithoutPlaceholder = inputMessages.filter(
1412
1400
  (message) => (message.metadata as any)?.type !== "placeholder",
@@ -1509,6 +1497,7 @@ export const processUiMessagesFlow = async ({
1509
1497
  message: currentMessage,
1510
1498
  currentTools: enabledTools,
1511
1499
  allExuluTools: tools,
1500
+ languageModel: resolvedLanguageModel,
1512
1501
  providerapikey,
1513
1502
  toolConfigs: agent.tools,
1514
1503
  exuluConfig: config,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "1.57.0",
4
+ "version": "1.59.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -63,6 +63,7 @@
63
63
  "@types/express": "^5.0.1",
64
64
  "@types/graphql-type-json": "^0.3.5",
65
65
  "@types/jest": "^29.5.14",
66
+ "@types/multer": "^2.1.0",
66
67
  "@types/node": "^22.14.0",
67
68
  "@types/pg": "^8.15.1",
68
69
  "@types/supertest": "^6.0.2",
@@ -88,8 +89,8 @@
88
89
  "dependencies": {
89
90
  "@ai-sdk/anthropic": "^3.0.23",
90
91
  "@ai-sdk/azure": "^3.0.53",
91
- "@ai-sdk/cerebras": "^2.0.29",
92
- "@ai-sdk/google-vertex": "^4.0.28",
92
+ "@ai-sdk/cerebras": "^2.0.51",
93
+ "@ai-sdk/google-vertex": "^4.0.136",
93
94
  "@ai-sdk/openai": "^3.0.18",
94
95
  "@ai-sdk/openai-compatible": "^2.0.37",
95
96
  "@anthropic-ai/sandbox-runtime": "^0.0.49",
@@ -139,6 +140,7 @@
139
140
  "knex": "^3.1.0",
140
141
  "link": "^2.1.1",
141
142
  "mammoth": "^1.11.0",
143
+ "multer": "^2.1.1",
142
144
  "natural": "^8.1.0",
143
145
  "nodemailer": "^8.0.7",
144
146
  "officeparser": "^5.2.2",