@exulu/backend 3.3.0 → 3.4.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/index.cjs CHANGED
@@ -1233,6 +1233,53 @@ var init_table_names = __esm({
1233
1233
  }
1234
1234
  });
1235
1235
 
1236
+ // src/exulu/litellm/env.ts
1237
+ function litellmBase() {
1238
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
1239
+ const port = process.env.LITELLM_PORT ?? "4000";
1240
+ const masterKey = process.env.LITELLM_MASTER_KEY;
1241
+ if (!masterKey) {
1242
+ throw new LiteLLMAdminError("LITELLM_MASTER_KEY is not configured.");
1243
+ }
1244
+ return { url: `http://${host}:${port}`, masterKey };
1245
+ }
1246
+ function resolveLiteLLMTarget() {
1247
+ const rawBase = process.env.LITELLM_BASE_URL;
1248
+ if (rawBase && rawBase.trim().length > 0) {
1249
+ const apiKey = process.env.EXULU_API_KEY;
1250
+ if (!apiKey) {
1251
+ throw new Error("EXULU_API_KEY is required when LITELLM_BASE_URL is set (remote LiteLLM client mode).");
1252
+ }
1253
+ return {
1254
+ baseUrl: rawBase.trim().replace(/\/+$/, ""),
1255
+ authHeaders: { "exulu-api-key": apiKey },
1256
+ remote: true
1257
+ };
1258
+ }
1259
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
1260
+ const port = process.env.LITELLM_PORT ?? "4000";
1261
+ const masterKey = process.env.LITELLM_MASTER_KEY;
1262
+ return {
1263
+ baseUrl: `http://${host}:${port}`,
1264
+ authHeaders: masterKey ? { Authorization: `Bearer ${masterKey}` } : {},
1265
+ remote: false
1266
+ };
1267
+ }
1268
+ var LiteLLMAdminError;
1269
+ var init_env = __esm({
1270
+ "src/exulu/litellm/env.ts"() {
1271
+ "use strict";
1272
+ init_cjs_shims();
1273
+ LiteLLMAdminError = class extends Error {
1274
+ constructor(message, status) {
1275
+ super(message);
1276
+ this.status = status;
1277
+ this.name = "LiteLLMAdminError";
1278
+ }
1279
+ };
1280
+ }
1281
+ });
1282
+
1236
1283
  // src/exulu/litellm/supervisor.ts
1237
1284
  var import_node_child_process, import_node_fs3, import_node_path2, LITELLM_UI_PATH, MAX_CRASHES, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, READY_TIMEOUT_MS, WAIT_TIMEOUT_MS, READY_POLL_INTERVAL_MS, SHUTDOWN_GRACE_MS, internal, isLiteLLMEnabled, resolveConfig, log2, pollHealth, spawnLiteLLM, supervise, _packageRoot, _clientMode, setLiteLLMPackageRoot, enableLiteLLMClientMode, startLiteLLMSupervisor, waitForLiteLLMReady, stopLiteLLM, shutdownHandlersRegistered, registerShutdownHandlers, getSupervisorState;
1238
1285
  var init_supervisor = __esm({
@@ -1242,6 +1289,7 @@ var init_supervisor = __esm({
1242
1289
  import_node_child_process = require("child_process");
1243
1290
  import_node_fs3 = require("fs");
1244
1291
  import_node_path2 = require("path");
1292
+ init_env();
1245
1293
  LITELLM_UI_PATH = "/litellm-admin";
1246
1294
  MAX_CRASHES = 5;
1247
1295
  INITIAL_BACKOFF_MS = 1e3;
@@ -1426,12 +1474,11 @@ var init_supervisor = __esm({
1426
1474
  if (!isLiteLLMEnabled()) return;
1427
1475
  if (_clientMode) {
1428
1476
  if (internal.state === "ready") return;
1429
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
1430
- const port = process.env.LITELLM_PORT ?? "4000";
1431
- const url = `http://${host}:${port}/health/liveliness`;
1477
+ const { baseUrl, authHeaders, remote } = resolveLiteLLMTarget();
1478
+ const url = remote ? `${baseUrl}/v1/models` : `${baseUrl}/health/liveliness`;
1432
1479
  let res;
1433
1480
  try {
1434
- res = await fetch(url, { method: "GET" });
1481
+ res = await fetch(url, { method: "GET", headers: remote ? authHeaders : {} });
1435
1482
  } catch (err) {
1436
1483
  throw new Error(
1437
1484
  `LiteLLM proxy not reachable at ${url} (is the Exulu server process running?): ${err.message}`
@@ -1625,31 +1672,6 @@ var init_tags = __esm({
1625
1672
  }
1626
1673
  });
1627
1674
 
1628
- // src/exulu/litellm/env.ts
1629
- function litellmBase() {
1630
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
1631
- const port = process.env.LITELLM_PORT ?? "4000";
1632
- const masterKey = process.env.LITELLM_MASTER_KEY;
1633
- if (!masterKey) {
1634
- throw new LiteLLMAdminError("LITELLM_MASTER_KEY is not configured.");
1635
- }
1636
- return { url: `http://${host}:${port}`, masterKey };
1637
- }
1638
- var LiteLLMAdminError;
1639
- var init_env = __esm({
1640
- "src/exulu/litellm/env.ts"() {
1641
- "use strict";
1642
- init_cjs_shims();
1643
- LiteLLMAdminError = class extends Error {
1644
- constructor(message, status) {
1645
- super(message);
1646
- this.status = status;
1647
- this.name = "LiteLLMAdminError";
1648
- }
1649
- };
1650
- }
1651
- });
1652
-
1653
1675
  // src/exulu/litellm/admin-client.ts
1654
1676
  async function call(path4, body) {
1655
1677
  const { url, masterKey } = litellmBase();
@@ -2285,6 +2307,7 @@ var init_resolve_model = __esm({
2285
2307
  init_supervisor();
2286
2308
  init_tags();
2287
2309
  init_budget_service();
2310
+ init_env();
2288
2311
  ResolveModelError = class extends Error {
2289
2312
  constructor(code, message) {
2290
2313
  super(message);
@@ -2300,9 +2323,7 @@ var init_resolve_model = __esm({
2300
2323
  team,
2301
2324
  routine
2302
2325
  }) => {
2303
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
2304
- const port = process.env.LITELLM_PORT ?? "4000";
2305
- const masterKey = process.env.LITELLM_MASTER_KEY;
2326
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
2306
2327
  const tags = buildTags({
2307
2328
  user_id: user?.id,
2308
2329
  role_id: role?.id,
@@ -2317,16 +2338,13 @@ var init_resolve_model = __esm({
2317
2338
  routine_id: routine?.id,
2318
2339
  routine_name: routine?.name
2319
2340
  });
2320
- if (!masterKey) {
2321
- throw new ResolveModelError(
2322
- "LITELLM_NOT_CONFIGURED",
2323
- "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
2324
- );
2325
- }
2326
2341
  return (0, import_openai_compatible.createOpenAICompatible)({
2327
2342
  name: "litellm",
2328
- baseURL: `http://${host}:${port}/v1`,
2329
- apiKey: masterKey,
2343
+ baseURL: `${baseUrl}/v1`,
2344
+ // createOpenAICompatible requires a non-empty apiKey; real auth for remote mode is the
2345
+ // exulu-api-key header (added via `headers`). The passthrough strips/ignores Authorization.
2346
+ apiKey: process.env.LITELLM_MASTER_KEY ?? process.env.EXULU_API_KEY ?? "x",
2347
+ headers: authHeaders,
2330
2348
  fetch: createTaggedFetch(tags),
2331
2349
  // Without this flag the openai-compatible provider strips any
2332
2350
  // responseFormat.schema before sending and warns
@@ -3320,6 +3338,9 @@ var init_check_item_write_access = __esm({
3320
3338
  if (record.rights_mode === "private") {
3321
3339
  return record.created_by != null && String(record.created_by) === String(user.id);
3322
3340
  }
3341
+ if (record.created_by != null && String(record.created_by) === String(user.id)) {
3342
+ return true;
3343
+ }
3323
3344
  const validRightsModes = ["users", "roles", "teams"];
3324
3345
  if (!validRightsModes.includes(record.rights_mode)) {
3325
3346
  return false;
@@ -4963,6 +4984,7 @@ var init_catalog = __esm({
4963
4984
  "src/exulu/litellm/catalog.ts"() {
4964
4985
  "use strict";
4965
4986
  init_cjs_shims();
4987
+ init_env();
4966
4988
  CACHE_TTL_MS = 3e4;
4967
4989
  __resetLiteLLMCatalogCacheForTesting = () => {
4968
4990
  _cache = void 0;
@@ -4972,14 +4994,18 @@ var init_catalog = __esm({
4972
4994
  if (_cache && _cache.expiresAt > Date.now()) {
4973
4995
  return _cache.items;
4974
4996
  }
4975
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
4976
- const port = process.env.LITELLM_PORT ?? "4000";
4977
- const masterKey = process.env.LITELLM_MASTER_KEY;
4978
- if (!masterKey) return [];
4997
+ let baseUrl;
4998
+ let authHeaders;
4999
+ try {
5000
+ ({ baseUrl, authHeaders } = resolveLiteLLMTarget());
5001
+ } catch {
5002
+ return [];
5003
+ }
5004
+ if (Object.keys(authHeaders).length === 0) return [];
4979
5005
  try {
4980
- const res = await fetch(`http://${host}:${port}/model/info`, {
5006
+ const res = await fetch(`${baseUrl}/model/info`, {
4981
5007
  method: "GET",
4982
- headers: { Authorization: `Bearer ${masterKey}` }
5008
+ headers: authHeaders
4983
5009
  });
4984
5010
  if (!res.ok) {
4985
5011
  console.error(
@@ -6564,15 +6590,7 @@ async function resolveReranker(input) {
6564
6590
  `LiteLLM is not ready: ${err.message}`
6565
6591
  );
6566
6592
  }
6567
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
6568
- const port = process.env.LITELLM_PORT ?? "4000";
6569
- const masterKey = process.env.LITELLM_MASTER_KEY;
6570
- if (!masterKey) {
6571
- throw new ResolveRerankerError(
6572
- "LITELLM_NOT_CONFIGURED",
6573
- "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
6574
- );
6575
- }
6593
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
6576
6594
  const resolvedUserId = user?.id ?? userId;
6577
6595
  if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
6578
6596
  const role = user?.role;
@@ -6592,7 +6610,7 @@ async function resolveReranker(input) {
6592
6610
  routine_name: routine?.name,
6593
6611
  context_name: contextName
6594
6612
  });
6595
- const endpoint = `http://${host}:${port}/v1/rerank`;
6613
+ const endpoint = `${baseUrl}/v1/rerank`;
6596
6614
  const rerank2 = async (query, chunks, opts) => {
6597
6615
  try {
6598
6616
  if (chunks.length === 0) return [];
@@ -6602,7 +6620,7 @@ async function resolveReranker(input) {
6602
6620
  const res = await fetch(endpoint, {
6603
6621
  method: "POST",
6604
6622
  headers: {
6605
- Authorization: `Bearer ${masterKey}`,
6623
+ ...authHeaders,
6606
6624
  "Content-Type": "application/json"
6607
6625
  },
6608
6626
  body: JSON.stringify({
@@ -6643,7 +6661,7 @@ async function resolveReranker(input) {
6643
6661
  };
6644
6662
  return { model, rerank: rerank2 };
6645
6663
  }
6646
- var import_fs2, ResolveRerankerError;
6664
+ var ResolveRerankerError;
6647
6665
  var init_resolve_reranker = __esm({
6648
6666
  "src/exulu/resolve-reranker.ts"() {
6649
6667
  "use strict";
@@ -6651,7 +6669,7 @@ var init_resolve_reranker = __esm({
6651
6669
  init_supervisor();
6652
6670
  init_budget_service();
6653
6671
  init_tags();
6654
- import_fs2 = require("fs");
6672
+ init_env();
6655
6673
  ResolveRerankerError = class extends Error {
6656
6674
  constructor(code, message) {
6657
6675
  super(message);
@@ -10019,6 +10037,7 @@ init_cjs_shims();
10019
10037
  init_supervisor();
10020
10038
  init_budget_service();
10021
10039
  init_tags();
10040
+ init_env();
10022
10041
  var ResolveEmbedderError = class extends Error {
10023
10042
  constructor(code, message) {
10024
10043
  super(message);
@@ -10042,15 +10061,7 @@ async function resolveEmbedder(input) {
10042
10061
  `LiteLLM is not ready: ${err.message}`
10043
10062
  );
10044
10063
  }
10045
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
10046
- const port = process.env.LITELLM_PORT ?? "4000";
10047
- const masterKey = process.env.LITELLM_MASTER_KEY;
10048
- if (!masterKey) {
10049
- throw new ResolveEmbedderError(
10050
- "LITELLM_NOT_CONFIGURED",
10051
- "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
10052
- );
10053
- }
10064
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
10054
10065
  const resolvedUserId = user?.id ?? userId;
10055
10066
  if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
10056
10067
  const { dimensionality, maxChunkSize, maxBatchSize } = getEmbeddingModelInfo(model);
@@ -10071,12 +10082,12 @@ async function resolveEmbedder(input) {
10071
10082
  routine_name: routine?.name,
10072
10083
  context_name: contextName
10073
10084
  });
10074
- const endpoint = `http://${host}:${port}/v1/embeddings`;
10085
+ const endpoint = `${baseUrl}/v1/embeddings`;
10075
10086
  const embedBatch = async (batch) => {
10076
10087
  const res = await fetch(endpoint, {
10077
10088
  method: "POST",
10078
10089
  headers: {
10079
- Authorization: `Bearer ${masterKey}`,
10090
+ ...authHeaders,
10080
10091
  "Content-Type": "application/json"
10081
10092
  },
10082
10093
  body: JSON.stringify({
@@ -15120,20 +15131,25 @@ async function encryptString(string) {
15120
15131
  const hash = await import_bcryptjs2.default.hash(string, SALT_ROUNDS);
15121
15132
  return hash;
15122
15133
  }
15123
- var generateApiKey = async (name, email) => {
15134
+ var generateApiKey = async (name, email, options) => {
15124
15135
  const { db: db2 } = await postgresClient();
15125
15136
  email = String(email).trim().toLowerCase();
15137
+ const superAdmin = options?.superAdmin ?? true;
15138
+ const roleName = options?.roleName ?? "admin";
15139
+ const rolePermissions = options?.rolePermissions ?? {
15140
+ agents: "write",
15141
+ workflows: "write",
15142
+ variables: "write",
15143
+ users: "write"
15144
+ };
15126
15145
  console.log("[EXULU] Inserting default user and admin role.");
15127
- const existingRole = await db2.from("roles").where({ name: "admin" }).first();
15146
+ const existingRole = await db2.from("roles").where({ name: roleName }).first();
15128
15147
  let roleId;
15129
15148
  if (!existingRole) {
15130
- console.log("[EXULU] Creating default admin role.");
15149
+ console.log(`[EXULU] Creating default ${roleName} role.`);
15131
15150
  const role = await db2.from("roles").insert({
15132
- name: "admin",
15133
- agents: "write",
15134
- workflows: "write",
15135
- variables: "write",
15136
- users: "write"
15151
+ name: roleName,
15152
+ ...rolePermissions
15137
15153
  }).returning("id");
15138
15154
  roleId = role[0].id;
15139
15155
  } else {
@@ -15149,7 +15165,7 @@ var generateApiKey = async (name, email) => {
15149
15165
  await db2.from("users").insert({
15150
15166
  name,
15151
15167
  email,
15152
- super_admin: true,
15168
+ super_admin: superAdmin,
15153
15169
  createdAt: /* @__PURE__ */ new Date(),
15154
15170
  updatedAt: /* @__PURE__ */ new Date(),
15155
15171
  type: "api",
@@ -15735,6 +15751,9 @@ function createMutations(table, contexts, tools, config) {
15735
15751
  }
15736
15752
  throw new Error("Only the creator can edit this private record");
15737
15753
  }
15754
+ if (record.created_by != null && String(record.created_by) === String(user.id)) {
15755
+ return true;
15756
+ }
15738
15757
  if (record.rights_mode === "users") {
15739
15758
  const rbacRecord = await db2.from("rbac").where({
15740
15759
  entity: table.name.singular,
@@ -19681,7 +19700,7 @@ var mapRoutineRunRow = (row, routineById) => {
19681
19700
 
19682
19701
  // src/graphql/schemas/index.ts
19683
19702
  init_entitlements();
19684
- var import_fs3 = require("fs");
19703
+ var import_fs2 = require("fs");
19685
19704
 
19686
19705
  // src/exulu/transcription/service.ts
19687
19706
  init_cjs_shims();
@@ -23636,7 +23655,7 @@ var import_utils5 = require("@apollo/utils.keyvaluecache");
23636
23655
  var import_body_parser = __toESM(require("body-parser"), 1);
23637
23656
  var import_crypto_js7 = require("crypto-js");
23638
23657
  var import_openai = require("openai");
23639
- var import_fs4 = __toESM(require("fs"), 1);
23658
+ var import_fs3 = __toESM(require("fs"), 1);
23640
23659
  var import_node_crypto15 = require("crypto");
23641
23660
  var import_api2 = require("@opentelemetry/api");
23642
23661
  var import_jszip3 = __toESM(require("jszip"), 1);
@@ -23855,6 +23874,7 @@ ${summary}` }],
23855
23874
 
23856
23875
  // src/exulu/transcribe.ts
23857
23876
  init_cjs_shims();
23877
+ init_env();
23858
23878
  var TranscriptionError = class extends Error {
23859
23879
  constructor(upstreamStatus, message) {
23860
23880
  super(message);
@@ -23863,11 +23883,8 @@ var TranscriptionError = class extends Error {
23863
23883
  }
23864
23884
  };
23865
23885
  async function transcribeAudio(args) {
23866
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
23867
- const port = process.env.LITELLM_PORT ?? "4000";
23868
- const masterKey = process.env.LITELLM_MASTER_KEY;
23886
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
23869
23887
  const model = process.env.TRANSCRIPTION_MODEL;
23870
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
23871
23888
  if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
23872
23889
  const form = new FormData();
23873
23890
  form.append(
@@ -23877,9 +23894,9 @@ async function transcribeAudio(args) {
23877
23894
  );
23878
23895
  form.append("model", model);
23879
23896
  if (args.language) form.append("language", args.language);
23880
- const res = await fetch(`http://${host}:${port}/v1/audio/transcriptions`, {
23897
+ const res = await fetch(`${baseUrl}/v1/audio/transcriptions`, {
23881
23898
  method: "POST",
23882
- headers: { Authorization: `Bearer ${masterKey}` },
23899
+ headers: { ...authHeaders },
23883
23900
  body: form
23884
23901
  });
23885
23902
  if (!res.ok) {
@@ -23895,6 +23912,7 @@ async function transcribeAudio(args) {
23895
23912
 
23896
23913
  // src/exulu/speech.ts
23897
23914
  init_cjs_shims();
23915
+ init_env();
23898
23916
  var SpeechError = class extends Error {
23899
23917
  constructor(upstreamStatus, message) {
23900
23918
  super(message);
@@ -23903,12 +23921,9 @@ var SpeechError = class extends Error {
23903
23921
  }
23904
23922
  };
23905
23923
  async function synthesizeSpeech(args) {
23906
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
23907
- const port = process.env.LITELLM_PORT ?? "4000";
23908
- const masterKey = process.env.LITELLM_MASTER_KEY;
23924
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
23909
23925
  const model = process.env.TTS_MODEL;
23910
23926
  const voice = process.env.TTS_VOICE;
23911
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
23912
23927
  if (!model) throw new Error("TTS_MODEL is not set");
23913
23928
  if (!voice) throw new Error("TTS_VOICE is not set");
23914
23929
  const body = {
@@ -23917,10 +23932,10 @@ async function synthesizeSpeech(args) {
23917
23932
  voice,
23918
23933
  response_format: "mp3"
23919
23934
  };
23920
- const res = await fetch(`http://${host}:${port}/v1/audio/speech`, {
23935
+ const res = await fetch(`${baseUrl}/v1/audio/speech`, {
23921
23936
  method: "POST",
23922
23937
  headers: {
23923
- Authorization: `Bearer ${masterKey}`,
23938
+ ...authHeaders,
23924
23939
  "Content-Type": "application/json"
23925
23940
  },
23926
23941
  body: JSON.stringify(body)
@@ -23938,6 +23953,7 @@ async function synthesizeSpeech(args) {
23938
23953
 
23939
23954
  // src/exulu/image-generation.ts
23940
23955
  init_cjs_shims();
23956
+ init_env();
23941
23957
  var ImageGenerationError = class extends Error {
23942
23958
  constructor(upstreamStatus, message) {
23943
23959
  super(message);
@@ -23945,13 +23961,6 @@ var ImageGenerationError = class extends Error {
23945
23961
  this.name = "ImageGenerationError";
23946
23962
  }
23947
23963
  };
23948
- var resolveProxyConfig = () => {
23949
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
23950
- const port = process.env.LITELLM_PORT ?? "4000";
23951
- const masterKey = process.env.LITELLM_MASTER_KEY;
23952
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
23953
- return { host, port, masterKey };
23954
- };
23955
23964
  var normalizeDataEntries = async (data) => {
23956
23965
  const out = [];
23957
23966
  for (const entry of data) {
@@ -23988,7 +23997,7 @@ var normalizeDataEntries = async (data) => {
23988
23997
  async function generateImage(args) {
23989
23998
  if (!args.model) throw new Error("model is required");
23990
23999
  if (!args.prompt) throw new Error("prompt is required");
23991
- const cfg = resolveProxyConfig();
24000
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
23992
24001
  const body = {
23993
24002
  model: args.model,
23994
24003
  prompt: args.prompt
@@ -23996,10 +24005,10 @@ async function generateImage(args) {
23996
24005
  if (args.size) body.size = args.size;
23997
24006
  if (args.quality) body.quality = args.quality;
23998
24007
  if (args.n) body.n = args.n;
23999
- const res = await fetch(`http://${cfg.host}:${cfg.port}/v1/images/generations`, {
24008
+ const res = await fetch(`${baseUrl}/v1/images/generations`, {
24000
24009
  method: "POST",
24001
24010
  headers: {
24002
- Authorization: `Bearer ${cfg.masterKey}`,
24011
+ ...authHeaders,
24003
24012
  "Content-Type": "application/json"
24004
24013
  },
24005
24014
  body: JSON.stringify(body),
@@ -24027,7 +24036,7 @@ async function editImage(args) {
24027
24036
  if (!args.references || args.references.length === 0) {
24028
24037
  throw new Error("at least one reference image is required");
24029
24038
  }
24030
- const cfg = resolveProxyConfig();
24039
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
24031
24040
  const form = new FormData();
24032
24041
  form.append("model", args.model);
24033
24042
  form.append("prompt", args.prompt);
@@ -24049,9 +24058,9 @@ async function editImage(args) {
24049
24058
  args.mask.filename
24050
24059
  );
24051
24060
  }
24052
- const res = await fetch(`http://${cfg.host}:${cfg.port}/v1/images/edits`, {
24061
+ const res = await fetch(`${baseUrl}/v1/images/edits`, {
24053
24062
  method: "POST",
24054
- headers: { Authorization: `Bearer ${cfg.masterKey}` },
24063
+ headers: { ...authHeaders },
24055
24064
  body: form,
24056
24065
  signal: args.signal
24057
24066
  });
@@ -24201,6 +24210,12 @@ var import_node_path9 = require("path");
24201
24210
  init_tags();
24202
24211
  init_admin_client();
24203
24212
  init_env();
24213
+
24214
+ // src/exulu/litellm/passthrough-allowlist.ts
24215
+ init_cjs_shims();
24216
+ var isLiteLLMPassthroughPathAllowed = (path4) => path4.startsWith("/v1/") || path4 === "/model/info";
24217
+
24218
+ // src/exulu/routes.ts
24204
24219
  init_activity_client();
24205
24220
  init_budget_service();
24206
24221
 
@@ -24901,7 +24916,7 @@ var REQUEST_SIZE_LIMIT = "50mb";
24901
24916
  var getExuluVersionNumber = async () => {
24902
24917
  try {
24903
24918
  const path4 = process.cwd();
24904
- const packageJson = import_fs4.default.readFileSync(path4 + "/package.json", "utf8");
24919
+ const packageJson = import_fs3.default.readFileSync(path4 + "/package.json", "utf8");
24905
24920
  const packageData = JSON.parse(packageJson);
24906
24921
  const exuluVersion = packageData.dependencies["@exulu/backend"];
24907
24922
  console.log(`[EXULU] Installed exulu-backend version: ${exuluVersion}`);
@@ -26453,7 +26468,7 @@ ${style.markdown}` : params.prompt;
26453
26468
  return;
26454
26469
  }
26455
26470
  const user = authenticationResult.user;
26456
- if (!req.path.startsWith("/v1/")) {
26471
+ if (!isLiteLLMPassthroughPathAllowed(req.path)) {
26457
26472
  res.status(403).json({
26458
26473
  detail: `Path ${req.path} is not exposed through the Exulu LiteLLM proxy.`
26459
26474
  });
@@ -29883,7 +29898,7 @@ init_cjs_shims();
29883
29898
  var import_child_process = require("child_process");
29884
29899
  var import_util = require("util");
29885
29900
  var import_path3 = require("path");
29886
- var import_fs5 = require("fs");
29901
+ var import_fs4 = require("fs");
29887
29902
  var import_url = require("url");
29888
29903
  var execAsync4 = (0, import_util.promisify)(import_child_process.exec);
29889
29904
  function getPackageRoot() {
@@ -29893,9 +29908,9 @@ function getPackageRoot() {
29893
29908
  const maxAttempts = 10;
29894
29909
  while (attempts < maxAttempts) {
29895
29910
  const packageJsonPath = (0, import_path3.join)(currentDir, "package.json");
29896
- if ((0, import_fs5.existsSync)(packageJsonPath)) {
29911
+ if ((0, import_fs4.existsSync)(packageJsonPath)) {
29897
29912
  try {
29898
- const packageJson = JSON.parse((0, import_fs5.readFileSync)(packageJsonPath, "utf-8"));
29913
+ const packageJson = JSON.parse((0, import_fs4.readFileSync)(packageJsonPath, "utf-8"));
29899
29914
  if (packageJson.name === "@exulu/backend") {
29900
29915
  return currentDir;
29901
29916
  }
@@ -29922,7 +29937,7 @@ function isPythonEnvironmentSetup(packageRoot) {
29922
29937
  const root = packageRoot ?? getPackageRoot();
29923
29938
  const venvPath = getVenvPath(root);
29924
29939
  const pythonPath = (0, import_path3.join)(venvPath, "bin", "python");
29925
- return (0, import_fs5.existsSync)(venvPath) && (0, import_fs5.existsSync)(pythonPath);
29940
+ return (0, import_fs4.existsSync)(venvPath) && (0, import_fs4.existsSync)(pythonPath);
29926
29941
  }
29927
29942
  async function setupPythonEnvironment(options = {}) {
29928
29943
  const {
@@ -29943,7 +29958,7 @@ async function setupPythonEnvironment(options = {}) {
29943
29958
  };
29944
29959
  }
29945
29960
  const setupScriptPath = getSetupScriptPath(packageRoot);
29946
- if (!(0, import_fs5.existsSync)(setupScriptPath)) {
29961
+ if (!(0, import_fs4.existsSync)(setupScriptPath)) {
29947
29962
  return {
29948
29963
  success: false,
29949
29964
  message: `Setup script not found at: ${setupScriptPath}`,
@@ -30025,13 +30040,13 @@ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
30025
30040
  const root = packageRoot ?? getPackageRoot();
30026
30041
  const venvPath = getVenvPath(root);
30027
30042
  const pythonPath = (0, import_path3.join)(venvPath, "bin", "python");
30028
- if (!(0, import_fs5.existsSync)(venvPath)) {
30043
+ if (!(0, import_fs4.existsSync)(venvPath)) {
30029
30044
  return {
30030
30045
  valid: false,
30031
30046
  message: getPythonSetupInstructions()
30032
30047
  };
30033
30048
  }
30034
- if (!(0, import_fs5.existsSync)(pythonPath)) {
30049
+ if (!(0, import_fs4.existsSync)(pythonPath)) {
30035
30050
  return {
30036
30051
  valid: false,
30037
30052
  message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
@@ -32581,7 +32596,7 @@ var MarkdownChunker = class {
32581
32596
 
32582
32597
  // ee/python/documents/processing/doc_processor.ts
32583
32598
  init_cjs_shims();
32584
- var fs5 = __toESM(require("fs"), 1);
32599
+ var fs4 = __toESM(require("fs"), 1);
32585
32600
  var path3 = __toESM(require("path"), 1);
32586
32601
  var import_ai14 = require("ai");
32587
32602
  var import_zod27 = require("zod");
@@ -32599,7 +32614,7 @@ init_cjs_shims();
32599
32614
  var import_child_process2 = require("child_process");
32600
32615
  var import_util3 = require("util");
32601
32616
  var import_path4 = require("path");
32602
- var import_fs6 = require("fs");
32617
+ var import_fs5 = require("fs");
32603
32618
  var import_url2 = require("url");
32604
32619
  var execAsync5 = (0, import_util3.promisify)(import_child_process2.exec);
32605
32620
  function getPackageRoot2() {
@@ -32609,9 +32624,9 @@ function getPackageRoot2() {
32609
32624
  const maxAttempts = 10;
32610
32625
  while (attempts < maxAttempts) {
32611
32626
  const packageJsonPath = (0, import_path4.join)(currentDir, "package.json");
32612
- if ((0, import_fs6.existsSync)(packageJsonPath)) {
32627
+ if ((0, import_fs5.existsSync)(packageJsonPath)) {
32613
32628
  try {
32614
- const packageJson = JSON.parse((0, import_fs6.readFileSync)(packageJsonPath, "utf-8"));
32629
+ const packageJson = JSON.parse((0, import_fs5.readFileSync)(packageJsonPath, "utf-8"));
32615
32630
  if (packageJson.name === "@exulu/backend") {
32616
32631
  return currentDir;
32617
32632
  }
@@ -32673,7 +32688,7 @@ async function executePythonScript(config) {
32673
32688
  await validatePythonEnvironmentForExecution(packageRoot);
32674
32689
  }
32675
32690
  const resolvedScriptPath = (0, import_path4.resolve)(packageRoot, scriptPath);
32676
- if (!(0, import_fs6.existsSync)(resolvedScriptPath)) {
32691
+ if (!(0, import_fs5.existsSync)(resolvedScriptPath)) {
32677
32692
  throw new PythonExecutionError(
32678
32693
  `Python script not found: ${resolvedScriptPath}`,
32679
32694
  "",
@@ -32743,6 +32758,7 @@ init_cjs_shims();
32743
32758
  init_supervisor();
32744
32759
  init_budget_service();
32745
32760
  init_tags();
32761
+ init_env();
32746
32762
  var ResolveOcrError = class extends Error {
32747
32763
  constructor(code, message) {
32748
32764
  super(message);
@@ -32766,15 +32782,7 @@ async function resolveOcr(input) {
32766
32782
  `LiteLLM is not ready: ${err.message}`
32767
32783
  );
32768
32784
  }
32769
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
32770
- const port = process.env.LITELLM_PORT ?? "4000";
32771
- const masterKey = process.env.LITELLM_MASTER_KEY;
32772
- if (!masterKey) {
32773
- throw new ResolveOcrError(
32774
- "LITELLM_NOT_CONFIGURED",
32775
- "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
32776
- );
32777
- }
32785
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
32778
32786
  const resolvedUserId = user?.id ?? userId;
32779
32787
  if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
32780
32788
  const role = user?.role;
@@ -32794,12 +32802,12 @@ async function resolveOcr(input) {
32794
32802
  routine_name: routine?.name,
32795
32803
  context_name: contextName
32796
32804
  });
32797
- const endpoint = `http://${host}:${port}/v1/ocr`;
32805
+ const endpoint = `${baseUrl}/v1/ocr`;
32798
32806
  const ocr = async (document2, opts) => {
32799
32807
  const res = await fetch(endpoint, {
32800
32808
  method: "POST",
32801
32809
  headers: {
32802
- Authorization: `Bearer ${masterKey}`,
32810
+ ...authHeaders,
32803
32811
  "Content-Type": "application/json"
32804
32812
  },
32805
32813
  body: JSON.stringify({
@@ -32877,9 +32885,9 @@ async function resolveVlmModel(config) {
32877
32885
  }
32878
32886
  async function processImage(buffer, paths, config, verbose = false) {
32879
32887
  try {
32880
- await fs5.promises.mkdir(paths.images, { recursive: true });
32888
+ await fs4.promises.mkdir(paths.images, { recursive: true });
32881
32889
  const imagePath = path3.join(paths.images, "1.png");
32882
- await fs5.promises.writeFile(imagePath, buffer);
32890
+ await fs4.promises.writeFile(imagePath, buffer);
32883
32891
  console.log(`[EXULU] Image saved to: ${imagePath}`);
32884
32892
  let json = [{
32885
32893
  page: 1,
@@ -32897,7 +32905,7 @@ async function processImage(buffer, paths, config, verbose = false) {
32897
32905
  verbose,
32898
32906
  config.vlm.concurrency
32899
32907
  );
32900
- await fs5.promises.writeFile(
32908
+ await fs4.promises.writeFile(
32901
32909
  paths.json,
32902
32910
  JSON.stringify(json, null, 2),
32903
32911
  "utf-8"
@@ -32908,14 +32916,14 @@ async function processImage(buffer, paths, config, verbose = false) {
32908
32916
  } else {
32909
32917
  console.log("[EXULU] No VLM configured, image saved without content extraction");
32910
32918
  console.log("[EXULU] Note: Enable VLM in config to extract text/content from images");
32911
- await fs5.promises.writeFile(
32919
+ await fs4.promises.writeFile(
32912
32920
  paths.json,
32913
32921
  JSON.stringify(json, null, 2),
32914
32922
  "utf-8"
32915
32923
  );
32916
32924
  }
32917
32925
  const markdown = json.map((p) => p.vlm_corrected_text ?? p.content).join("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
32918
- await fs5.promises.writeFile(paths.markdown, markdown, "utf-8");
32926
+ await fs4.promises.writeFile(paths.markdown, markdown, "utf-8");
32919
32927
  return {
32920
32928
  markdown,
32921
32929
  json
@@ -32968,7 +32976,7 @@ function reconstructHeadings(correctedText, headingsHierarchy) {
32968
32976
  return result;
32969
32977
  }
32970
32978
  async function validatePageWithVLM(page, imagePath, model) {
32971
- const imageBuffer = await fs5.promises.readFile(imagePath);
32979
+ const imageBuffer = await fs4.promises.readFile(imagePath);
32972
32980
  const imageBase64 = imageBuffer.toString("base64");
32973
32981
  const mimeType = "image/png";
32974
32982
  const prompt = `You are a document validation assistant. Your task is to analyze a page image and correct the output of an OCR/parsing pipeline. The content may include tables, technical diagrams, schematics, and structured text.
@@ -33303,7 +33311,7 @@ ${setupResult.output || ""}`);
33303
33311
  if (!result.success) {
33304
33312
  throw new Error(`Document processing failed: ${result.stderr}`);
33305
33313
  }
33306
- const jsonContent = await fs5.promises.readFile(paths.json, "utf-8");
33314
+ const jsonContent = await fs4.promises.readFile(paths.json, "utf-8");
33307
33315
  json = JSON.parse(jsonContent);
33308
33316
  } else if (config?.processor.name === "officeparser") {
33309
33317
  const text = await (0, import_officeparser3.parseOfficeAsync)(buffer, {
@@ -33336,7 +33344,7 @@ ${setupResult.output || ""}`);
33336
33344
  await new Promise((resolve8) => setImmediate(resolve8));
33337
33345
  await new Promise((resolve8) => setTimeout(resolve8, Math.floor(Math.random() * 1e3) + 200));
33338
33346
  console.log(`[EXULU] OCR chunk ${i + 1}/${pdfChunks.length}: pages ${chunk.start_page}\u2013${chunk.end_page - 1}`);
33339
- const chunkBuffer = await fs5.promises.readFile(chunk.path);
33347
+ const chunkBuffer = await fs4.promises.readFile(chunk.path);
33340
33348
  const chunkBase64 = chunkBuffer.toString("base64");
33341
33349
  const chunkResponse = await withRetry(async () => {
33342
33350
  return await resolved.ocr({
@@ -33353,9 +33361,9 @@ ${setupResult.output || ""}`);
33353
33361
  );
33354
33362
  const parser = new import_liteparse.LiteParse();
33355
33363
  const screenshots = await parser.screenshot(paths.source, void 0);
33356
- await fs5.promises.mkdir(paths.images, { recursive: true });
33364
+ await fs4.promises.mkdir(paths.images, { recursive: true });
33357
33365
  for (const screenshot of screenshots) {
33358
- await fs5.promises.writeFile(
33366
+ await fs4.promises.writeFile(
33359
33367
  path3.join(
33360
33368
  paths.images,
33361
33369
  `${screenshot.pageNum}.png`
@@ -33370,15 +33378,15 @@ ${setupResult.output || ""}`);
33370
33378
  image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
33371
33379
  headings: []
33372
33380
  }));
33373
- fs5.writeFileSync(paths.json, JSON.stringify(json, null, 2));
33381
+ fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
33374
33382
  } else if (config?.processor.name === "liteparse") {
33375
33383
  const parser = new import_liteparse.LiteParse();
33376
33384
  const result = await parser.parse(paths.source);
33377
33385
  const screenshots = await parser.screenshot(paths.source, void 0);
33378
33386
  console.log(`[EXULU] Liteparse screenshots: ${JSON.stringify(screenshots)}`);
33379
- await fs5.promises.mkdir(paths.images, { recursive: true });
33387
+ await fs4.promises.mkdir(paths.images, { recursive: true });
33380
33388
  for (const screenshot of screenshots) {
33381
- await fs5.promises.writeFile(path3.join(paths.images, `${screenshot.pageNum}.png`), screenshot.imageBuffer);
33389
+ await fs4.promises.writeFile(path3.join(paths.images, `${screenshot.pageNum}.png`), screenshot.imageBuffer);
33382
33390
  screenshot.imagePath = path3.join(paths.images, `${screenshot.pageNum}.png`);
33383
33391
  }
33384
33392
  json = result.pages.map((page) => ({
@@ -33386,7 +33394,7 @@ ${setupResult.output || ""}`);
33386
33394
  content: page.text,
33387
33395
  image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
33388
33396
  }));
33389
- fs5.writeFileSync(paths.json, JSON.stringify(json, null, 2));
33397
+ fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
33390
33398
  }
33391
33399
  console.log(`[EXULU]
33392
33400
  \u2713 Document processing completed successfully`);
@@ -33418,13 +33426,13 @@ ${setupResult.output || ""}`);
33418
33426
  console.log(`[EXULU] Corrected: ${page.vlm_corrected_text.substring(0, 150)}...`);
33419
33427
  });
33420
33428
  }
33421
- await fs5.promises.writeFile(
33429
+ await fs4.promises.writeFile(
33422
33430
  paths.json,
33423
33431
  JSON.stringify(json, null, 2),
33424
33432
  "utf-8"
33425
33433
  );
33426
33434
  }
33427
- const markdownStream = fs5.createWriteStream(paths.markdown, { encoding: "utf-8" });
33435
+ const markdownStream = fs4.createWriteStream(paths.markdown, { encoding: "utf-8" });
33428
33436
  for (let i = 0; i < json.length; i++) {
33429
33437
  const p = json[i];
33430
33438
  if (!p) continue;
@@ -33440,7 +33448,7 @@ ${setupResult.output || ""}`);
33440
33448
  });
33441
33449
  console.log(`[EXULU] Validated output saved to: ${paths.json}`);
33442
33450
  console.log(`[EXULU] Validated markdown saved to: ${paths.markdown}`);
33443
- const markdown = await fs5.promises.readFile(paths.markdown, "utf-8");
33451
+ const markdown = await fs4.promises.readFile(paths.markdown, "utf-8");
33444
33452
  const processedJson = json.map((e) => {
33445
33453
  const finalContent = e.vlm_corrected_text ?? e.content;
33446
33454
  return {
@@ -33471,7 +33479,7 @@ var loadFile = async (file, name, tempDir) => {
33471
33479
  let buffer;
33472
33480
  if (Buffer.isBuffer(file)) {
33473
33481
  filePath = path3.join(tempDir, `${UUID}.${fileType}`);
33474
- await fs5.promises.writeFile(filePath, file);
33482
+ await fs4.promises.writeFile(filePath, file);
33475
33483
  buffer = file;
33476
33484
  } else {
33477
33485
  filePath = filePath.trim();
@@ -33479,11 +33487,11 @@ var loadFile = async (file, name, tempDir) => {
33479
33487
  const response = await fetch(filePath);
33480
33488
  const array = await response.arrayBuffer();
33481
33489
  const tempFilePath = path3.join(tempDir, `${UUID}.${fileType}`);
33482
- await fs5.promises.writeFile(tempFilePath, Buffer.from(array));
33490
+ await fs4.promises.writeFile(tempFilePath, Buffer.from(array));
33483
33491
  buffer = Buffer.from(array);
33484
33492
  filePath = tempFilePath;
33485
33493
  } else {
33486
- buffer = await fs5.promises.readFile(file);
33494
+ buffer = await fs4.promises.readFile(file);
33487
33495
  }
33488
33496
  }
33489
33497
  return { filePath, fileType, buffer };
@@ -33501,9 +33509,9 @@ async function documentProcessor({
33501
33509
  const tempDir = path3.join(process.cwd(), "temp", uuid);
33502
33510
  const localFilesAndFoldersToDelete = [tempDir];
33503
33511
  console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
33504
- await fs5.promises.mkdir(tempDir, { recursive: true });
33512
+ await fs4.promises.mkdir(tempDir, { recursive: true });
33505
33513
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
33506
- await fs5.promises.writeFile(path3.join(tempDir, "created_at.txt"), timestamp);
33514
+ await fs4.promises.writeFile(path3.join(tempDir, "created_at.txt"), timestamp);
33507
33515
  try {
33508
33516
  const {
33509
33517
  filePath,
@@ -33544,7 +33552,7 @@ async function documentProcessor({
33544
33552
  if (config?.debugging?.deleteTempFiles !== false) {
33545
33553
  for (const file2 of localFilesAndFoldersToDelete) {
33546
33554
  try {
33547
- await fs5.promises.rm(file2, { recursive: true });
33555
+ await fs4.promises.rm(file2, { recursive: true });
33548
33556
  console.log(`[EXULU] Deleted file or folder: ${file2}`);
33549
33557
  } catch (error) {
33550
33558
  console.error(`[EXULU] Error deleting file or folder: ${file2}`, error);