@exulu/backend 3.3.1 → 3.5.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.js CHANGED
@@ -14,7 +14,6 @@ import {
14
14
  ExuluTool,
15
15
  KB_EDITOR_TOOL_ID,
16
16
  LITELLM_UI_PATH,
17
- LiteLLMAdminError,
18
17
  OAUTH_CALLBACK_PATH,
19
18
  PreviewRenderError,
20
19
  ResolveModelError,
@@ -88,10 +87,12 @@ import {
88
87
  verifyCredentialNonce,
89
88
  waitForLiteLLMReady,
90
89
  withRetry
91
- } from "./chunk-5FTX543Z.js";
90
+ } from "./chunk-7AZH4ETH.js";
92
91
  import {
93
- findLiteLLMModel
94
- } from "./chunk-7CCMW3IW.js";
92
+ LiteLLMAdminError,
93
+ findLiteLLMModel,
94
+ resolveLiteLLMTarget
95
+ } from "./chunk-4PDWNVNT.js";
95
96
 
96
97
  // src/index.ts
97
98
  import "dotenv/config";
@@ -1216,15 +1217,7 @@ async function resolveEmbedder(input) {
1216
1217
  `LiteLLM is not ready: ${err.message}`
1217
1218
  );
1218
1219
  }
1219
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
1220
- const port = process.env.LITELLM_PORT ?? "4000";
1221
- const masterKey = process.env.LITELLM_MASTER_KEY;
1222
- if (!masterKey) {
1223
- throw new ResolveEmbedderError(
1224
- "LITELLM_NOT_CONFIGURED",
1225
- "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
1226
- );
1227
- }
1220
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
1228
1221
  const resolvedUserId = user?.id ?? userId;
1229
1222
  if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
1230
1223
  const { dimensionality, maxChunkSize, maxBatchSize } = getEmbeddingModelInfo(model);
@@ -1245,12 +1238,12 @@ async function resolveEmbedder(input) {
1245
1238
  routine_name: routine?.name,
1246
1239
  context_name: contextName
1247
1240
  });
1248
- const endpoint = `http://${host}:${port}/v1/embeddings`;
1241
+ const endpoint = `${baseUrl}/v1/embeddings`;
1249
1242
  const embedBatch = async (batch) => {
1250
1243
  const res = await fetch(endpoint, {
1251
1244
  method: "POST",
1252
1245
  headers: {
1253
- Authorization: `Bearer ${masterKey}`,
1246
+ ...authHeaders,
1254
1247
  "Content-Type": "application/json"
1255
1248
  },
1256
1249
  body: JSON.stringify({
@@ -6199,20 +6192,25 @@ async function encryptString(string) {
6199
6192
  const hash = await bcrypt.hash(string, SALT_ROUNDS);
6200
6193
  return hash;
6201
6194
  }
6202
- var generateApiKey = async (name, email) => {
6195
+ var generateApiKey = async (name, email, options) => {
6203
6196
  const { db } = await postgresClient();
6204
6197
  email = String(email).trim().toLowerCase();
6198
+ const superAdmin = options?.superAdmin ?? true;
6199
+ const roleName = options?.roleName ?? "admin";
6200
+ const rolePermissions = options?.rolePermissions ?? {
6201
+ agents: "write",
6202
+ workflows: "write",
6203
+ variables: "write",
6204
+ users: "write"
6205
+ };
6205
6206
  console.log("[EXULU] Inserting default user and admin role.");
6206
- const existingRole = await db.from("roles").where({ name: "admin" }).first();
6207
+ const existingRole = await db.from("roles").where({ name: roleName }).first();
6207
6208
  let roleId;
6208
6209
  if (!existingRole) {
6209
- console.log("[EXULU] Creating default admin role.");
6210
+ console.log(`[EXULU] Creating default ${roleName} role.`);
6210
6211
  const role = await db.from("roles").insert({
6211
- name: "admin",
6212
- agents: "write",
6213
- workflows: "write",
6214
- variables: "write",
6215
- users: "write"
6212
+ name: roleName,
6213
+ ...rolePermissions
6216
6214
  }).returning("id");
6217
6215
  roleId = role[0].id;
6218
6216
  } else {
@@ -6228,7 +6226,7 @@ var generateApiKey = async (name, email) => {
6228
6226
  await db.from("users").insert({
6229
6227
  name,
6230
6228
  email,
6231
- super_admin: true,
6229
+ super_admin: superAdmin,
6232
6230
  createdAt: /* @__PURE__ */ new Date(),
6233
6231
  updatedAt: /* @__PURE__ */ new Date(),
6234
6232
  type: "api",
@@ -6800,6 +6798,9 @@ function createMutations(table, contexts, tools, config) {
6800
6798
  }
6801
6799
  throw new Error("Only the creator can edit this private record");
6802
6800
  }
6801
+ if (record.created_by != null && String(record.created_by) === String(user.id)) {
6802
+ return true;
6803
+ }
6803
6804
  if (record.rights_mode === "users") {
6804
6805
  const rbacRecord = await db.from("rbac").where({
6805
6806
  entity: table.name.singular,
@@ -12866,7 +12867,7 @@ type LiteLLMModel {
12866
12867
  }
12867
12868
  `;
12868
12869
  resolvers.Query["litellmCatalog"] = async () => {
12869
- const { fetchLiteLLMCatalog } = await import("./catalog-UGTDNMDM.js");
12870
+ const { fetchLiteLLMCatalog } = await import("./catalog-3QLU2G7R.js");
12870
12871
  return fetchLiteLLMCatalog();
12871
12872
  };
12872
12873
  resolvers.Query["workflowSchedule"] = async (_, args, context, info) => {
@@ -14820,6 +14821,17 @@ ${summary}` }],
14820
14821
  };
14821
14822
 
14822
14823
  // src/exulu/transcribe.ts
14824
+ var TRANSCRIBE_SYSTEM_PROMPT = "You are a speech-to-text transcription engine. Detect the language actually spoken and transcribe it word-for-word in that same language. Never translate. Output only the transcript text \u2014 no quotes, labels, or commentary. If there is no intelligible speech, output nothing.";
14825
+ function isGeminiChatTranscriptionModel(entry) {
14826
+ const upstream = entry?.upstream_model ?? "";
14827
+ return /^vertex_ai\//i.test(upstream) && /gemini/i.test(upstream);
14828
+ }
14829
+ function cleanTranscript(raw) {
14830
+ let text = (raw ?? "").trim();
14831
+ const wrapped = text.match(/^(["'`])([\s\S]*)\1$/);
14832
+ if (wrapped) text = (wrapped[2] ?? "").trim();
14833
+ return text;
14834
+ }
14823
14835
  var TranscriptionError = class extends Error {
14824
14836
  constructor(upstreamStatus, message) {
14825
14837
  super(message);
@@ -14828,12 +14840,16 @@ var TranscriptionError = class extends Error {
14828
14840
  }
14829
14841
  };
14830
14842
  async function transcribeAudio(args) {
14831
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
14832
- const port = process.env.LITELLM_PORT ?? "4000";
14833
- const masterKey = process.env.LITELLM_MASTER_KEY;
14843
+ const target = resolveLiteLLMTarget();
14834
14844
  const model = process.env.TRANSCRIPTION_MODEL;
14835
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
14836
14845
  if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
14846
+ const entry = await findLiteLLMModel(model).catch(() => void 0);
14847
+ if (isGeminiChatTranscriptionModel(entry)) {
14848
+ return transcribeViaChat(args, target, model);
14849
+ }
14850
+ return transcribeViaAudioEndpoint(args, target, model);
14851
+ }
14852
+ async function transcribeViaAudioEndpoint(args, target, model) {
14837
14853
  const form = new FormData();
14838
14854
  form.append(
14839
14855
  "file",
@@ -14842,9 +14858,9 @@ async function transcribeAudio(args) {
14842
14858
  );
14843
14859
  form.append("model", model);
14844
14860
  if (args.language) form.append("language", args.language);
14845
- const res = await fetch(`http://${host}:${port}/v1/audio/transcriptions`, {
14861
+ const res = await fetch(`${target.baseUrl}/v1/audio/transcriptions`, {
14846
14862
  method: "POST",
14847
- headers: { Authorization: `Bearer ${masterKey}` },
14863
+ headers: { ...target.authHeaders },
14848
14864
  body: form
14849
14865
  });
14850
14866
  if (!res.ok) {
@@ -14857,6 +14873,42 @@ async function transcribeAudio(args) {
14857
14873
  const json = await res.json();
14858
14874
  return { text: typeof json.text === "string" ? json.text : "" };
14859
14875
  }
14876
+ async function transcribeViaChat(args, target, model) {
14877
+ const subtype = args.file.mimetype.replace(/^audio\//, "").split(";")[0];
14878
+ const format = (subtype && subtype.length > 0 ? subtype : "wav").toLowerCase();
14879
+ const body = {
14880
+ model,
14881
+ temperature: 0,
14882
+ reasoning_effort: "disable",
14883
+ messages: [
14884
+ { role: "system", content: TRANSCRIBE_SYSTEM_PROMPT },
14885
+ {
14886
+ role: "user",
14887
+ content: [
14888
+ { type: "text", text: "Transcribe this audio." },
14889
+ {
14890
+ type: "input_audio",
14891
+ input_audio: { data: args.file.buffer.toString("base64"), format }
14892
+ }
14893
+ ]
14894
+ }
14895
+ ]
14896
+ };
14897
+ const res = await fetch(`${target.baseUrl}/v1/chat/completions`, {
14898
+ method: "POST",
14899
+ headers: { ...target.authHeaders, "Content-Type": "application/json" },
14900
+ body: JSON.stringify(body)
14901
+ });
14902
+ if (!res.ok) {
14903
+ const errBody = await res.text().catch(() => "");
14904
+ throw new TranscriptionError(
14905
+ res.status,
14906
+ `LiteLLM transcription failed (status ${res.status}): ${errBody}`.trim()
14907
+ );
14908
+ }
14909
+ const json = await res.json();
14910
+ return { text: cleanTranscript(json.choices?.[0]?.message?.content) };
14911
+ }
14860
14912
 
14861
14913
  // src/exulu/speech.ts
14862
14914
  var SpeechError = class extends Error {
@@ -14867,12 +14919,9 @@ var SpeechError = class extends Error {
14867
14919
  }
14868
14920
  };
14869
14921
  async function synthesizeSpeech(args) {
14870
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
14871
- const port = process.env.LITELLM_PORT ?? "4000";
14872
- const masterKey = process.env.LITELLM_MASTER_KEY;
14922
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
14873
14923
  const model = process.env.TTS_MODEL;
14874
14924
  const voice = process.env.TTS_VOICE;
14875
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
14876
14925
  if (!model) throw new Error("TTS_MODEL is not set");
14877
14926
  if (!voice) throw new Error("TTS_VOICE is not set");
14878
14927
  const body = {
@@ -14881,10 +14930,10 @@ async function synthesizeSpeech(args) {
14881
14930
  voice,
14882
14931
  response_format: "mp3"
14883
14932
  };
14884
- const res = await fetch(`http://${host}:${port}/v1/audio/speech`, {
14933
+ const res = await fetch(`${baseUrl}/v1/audio/speech`, {
14885
14934
  method: "POST",
14886
14935
  headers: {
14887
- Authorization: `Bearer ${masterKey}`,
14936
+ ...authHeaders,
14888
14937
  "Content-Type": "application/json"
14889
14938
  },
14890
14939
  body: JSON.stringify(body)
@@ -14908,13 +14957,6 @@ var ImageGenerationError = class extends Error {
14908
14957
  this.name = "ImageGenerationError";
14909
14958
  }
14910
14959
  };
14911
- var resolveProxyConfig = () => {
14912
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
14913
- const port = process.env.LITELLM_PORT ?? "4000";
14914
- const masterKey = process.env.LITELLM_MASTER_KEY;
14915
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
14916
- return { host, port, masterKey };
14917
- };
14918
14960
  var normalizeDataEntries = async (data) => {
14919
14961
  const out = [];
14920
14962
  for (const entry of data) {
@@ -14951,7 +14993,7 @@ var normalizeDataEntries = async (data) => {
14951
14993
  async function generateImage(args) {
14952
14994
  if (!args.model) throw new Error("model is required");
14953
14995
  if (!args.prompt) throw new Error("prompt is required");
14954
- const cfg = resolveProxyConfig();
14996
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
14955
14997
  const body = {
14956
14998
  model: args.model,
14957
14999
  prompt: args.prompt
@@ -14959,10 +15001,10 @@ async function generateImage(args) {
14959
15001
  if (args.size) body.size = args.size;
14960
15002
  if (args.quality) body.quality = args.quality;
14961
15003
  if (args.n) body.n = args.n;
14962
- const res = await fetch(`http://${cfg.host}:${cfg.port}/v1/images/generations`, {
15004
+ const res = await fetch(`${baseUrl}/v1/images/generations`, {
14963
15005
  method: "POST",
14964
15006
  headers: {
14965
- Authorization: `Bearer ${cfg.masterKey}`,
15007
+ ...authHeaders,
14966
15008
  "Content-Type": "application/json"
14967
15009
  },
14968
15010
  body: JSON.stringify(body),
@@ -14990,7 +15032,7 @@ async function editImage(args) {
14990
15032
  if (!args.references || args.references.length === 0) {
14991
15033
  throw new Error("at least one reference image is required");
14992
15034
  }
14993
- const cfg = resolveProxyConfig();
15035
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
14994
15036
  const form = new FormData();
14995
15037
  form.append("model", args.model);
14996
15038
  form.append("prompt", args.prompt);
@@ -15012,9 +15054,9 @@ async function editImage(args) {
15012
15054
  args.mask.filename
15013
15055
  );
15014
15056
  }
15015
- const res = await fetch(`http://${cfg.host}:${cfg.port}/v1/images/edits`, {
15057
+ const res = await fetch(`${baseUrl}/v1/images/edits`, {
15016
15058
  method: "POST",
15017
- headers: { Authorization: `Bearer ${cfg.masterKey}` },
15059
+ headers: { ...authHeaders },
15018
15060
  body: form,
15019
15061
  signal: args.signal
15020
15062
  });
@@ -15161,6 +15203,9 @@ See docs/superpowers/specs/2026-05-31-in-chat-image-generation-design.md for the
15161
15203
  // src/exulu/routes.ts
15162
15204
  import { resolve as resolvePath } from "path";
15163
15205
 
15206
+ // src/exulu/litellm/passthrough-allowlist.ts
15207
+ var isLiteLLMPassthroughPathAllowed = (path2) => path2.startsWith("/v1/") || path2 === "/model/info";
15208
+
15164
15209
  // src/exulu/litellm/usage-view.ts
15165
15210
  var DAY_MS = 24 * 60 * 60 * 1e3;
15166
15211
  var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
@@ -17377,7 +17422,7 @@ ${style.markdown}` : params.prompt;
17377
17422
  return;
17378
17423
  }
17379
17424
  const user = authenticationResult.user;
17380
- if (!req.path.startsWith("/v1/")) {
17425
+ if (!isLiteLLMPassthroughPathAllowed(req.path)) {
17381
17426
  res.status(403).json({
17382
17427
  detail: `Path ${req.path} is not exposed through the Exulu LiteLLM proxy.`
17383
17428
  });
@@ -23395,15 +23440,7 @@ async function resolveOcr(input) {
23395
23440
  `LiteLLM is not ready: ${err.message}`
23396
23441
  );
23397
23442
  }
23398
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
23399
- const port = process.env.LITELLM_PORT ?? "4000";
23400
- const masterKey = process.env.LITELLM_MASTER_KEY;
23401
- if (!masterKey) {
23402
- throw new ResolveOcrError(
23403
- "LITELLM_NOT_CONFIGURED",
23404
- "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
23405
- );
23406
- }
23443
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
23407
23444
  const resolvedUserId = user?.id ?? userId;
23408
23445
  if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
23409
23446
  const role = user?.role;
@@ -23423,12 +23460,12 @@ async function resolveOcr(input) {
23423
23460
  routine_name: routine?.name,
23424
23461
  context_name: contextName
23425
23462
  });
23426
- const endpoint = `http://${host}:${port}/v1/ocr`;
23463
+ const endpoint = `${baseUrl}/v1/ocr`;
23427
23464
  const ocr = async (document, opts) => {
23428
23465
  const res = await fetch(endpoint, {
23429
23466
  method: "POST",
23430
23467
  headers: {
23431
- Authorization: `Bearer ${masterKey}`,
23468
+ ...authHeaders,
23432
23469
  "Content-Type": "application/json"
23433
23470
  },
23434
23471
  body: JSON.stringify({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "3.3.1",
4
+ "version": "3.5.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {