@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/{catalog-UGTDNMDM.js → catalog-3QLU2G7R.js} +1 -1
- package/dist/{chunk-7CCMW3IW.js → chunk-4PDWNVNT.js} +67 -9
- package/dist/{chunk-5FTX543Z.js → chunk-7AZH4ETH.js} +31 -58
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-WQWYMU7G.js → convert-exulu-tools-to-ai-sdk-tools-TZ2UKWR4.js} +2 -2
- package/dist/index.cjs +233 -162
- package/dist/index.js +99 -62
- package/package.json +1 -1
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-
|
|
90
|
+
} from "./chunk-7AZH4ETH.js";
|
|
92
91
|
import {
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
6207
|
+
const existingRole = await db.from("roles").where({ name: roleName }).first();
|
|
6207
6208
|
let roleId;
|
|
6208
6209
|
if (!existingRole) {
|
|
6209
|
-
console.log(
|
|
6210
|
+
console.log(`[EXULU] Creating default ${roleName} role.`);
|
|
6210
6211
|
const role = await db.from("roles").insert({
|
|
6211
|
-
name:
|
|
6212
|
-
|
|
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:
|
|
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-
|
|
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
|
|
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(
|
|
14861
|
+
const res = await fetch(`${target.baseUrl}/v1/audio/transcriptions`, {
|
|
14846
14862
|
method: "POST",
|
|
14847
|
-
headers: {
|
|
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
|
|
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(
|
|
14933
|
+
const res = await fetch(`${baseUrl}/v1/audio/speech`, {
|
|
14885
14934
|
method: "POST",
|
|
14886
14935
|
headers: {
|
|
14887
|
-
|
|
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
|
|
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(
|
|
15004
|
+
const res = await fetch(`${baseUrl}/v1/images/generations`, {
|
|
14963
15005
|
method: "POST",
|
|
14964
15006
|
headers: {
|
|
14965
|
-
|
|
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
|
|
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(
|
|
15057
|
+
const res = await fetch(`${baseUrl}/v1/images/edits`, {
|
|
15016
15058
|
method: "POST",
|
|
15017
|
-
headers: {
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
23468
|
+
...authHeaders,
|
|
23432
23469
|
"Content-Type": "application/json"
|
|
23433
23470
|
},
|
|
23434
23471
|
body: JSON.stringify({
|