llmjs2 1.7.1 → 2.0.1
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/README.md +116 -18
- package/dist/index.d.mts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +1242 -0
- package/dist/index.mjs +1211 -0
- package/package.json +32 -58
- package/chain/AGENT_STEP_README.md +0 -102
- package/chain/README.md +0 -257
- package/chain/WORKFLOW_README.md +0 -85
- package/chain/agent-step-example.js +0 -232
- package/chain/docs/AGENT.md +0 -126
- package/chain/docs/GRAPH.md +0 -490
- package/chain/examples.js +0 -314
- package/chain/index.js +0 -31
- package/chain/lib/agent.js +0 -338
- package/chain/lib/flow/agent-step.js +0 -119
- package/chain/lib/flow/edge.js +0 -24
- package/chain/lib/flow/flow.js +0 -76
- package/chain/lib/flow/graph.js +0 -331
- package/chain/lib/flow/index.js +0 -7
- package/chain/lib/flow/step.js +0 -63
- package/chain/lib/memory/in-memory.js +0 -117
- package/chain/lib/memory/index.js +0 -36
- package/chain/lib/memory/lance-memory.js +0 -225
- package/chain/lib/memory/sqlite-memory.js +0 -309
- package/chain/simple-agent-step-example.js +0 -168
- package/chain/workflow-example-usage.js +0 -70
- package/chain/workflow-example.json +0 -59
- package/core/README.md +0 -485
- package/core/cli.js +0 -275
- package/core/config.yaml +0 -149
- package/core/docs/BASIC_USAGE.md +0 -62
- package/core/docs/CLI.md +0 -104
- package/core/docs/GET_STARTED.md +0 -129
- package/core/docs/GUARDRAILS_GUIDE.md +0 -734
- package/core/docs/README.md +0 -47
- package/core/docs/ROUTER_GUIDE.md +0 -199
- package/core/docs/SERVER_MODE.md +0 -358
- package/core/index.js +0 -115
- package/core/logger.js +0 -115
- package/core/providers/ollama.js +0 -128
- package/core/providers/openai.js +0 -112
- package/core/providers/openrouter.js +0 -206
- package/core/router.js +0 -252
- package/core/server.js +0 -203
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
9
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// node_modules/dotenv/lib/main.js
|
|
18
|
+
var require_main = __commonJS({
|
|
19
|
+
"node_modules/dotenv/lib/main.js"(exports, module) {
|
|
20
|
+
"use strict";
|
|
21
|
+
var fs3 = __require("fs");
|
|
22
|
+
var path2 = __require("path");
|
|
23
|
+
var os = __require("os");
|
|
24
|
+
var crypto = __require("crypto");
|
|
25
|
+
var TIPS = [
|
|
26
|
+
"\u25C8 encrypted .env [www.dotenvx.com]",
|
|
27
|
+
"\u25C8 secrets for agents [www.dotenvx.com]",
|
|
28
|
+
"\u2301 auth for agents [www.vestauth.com]",
|
|
29
|
+
"\u2318 custom filepath { path: '/custom/path/.env' }",
|
|
30
|
+
"\u2318 enable debugging { debug: true }",
|
|
31
|
+
"\u2318 override existing { override: true }",
|
|
32
|
+
"\u2318 suppress logs { quiet: true }",
|
|
33
|
+
"\u2318 multiple files { path: ['.env.local', '.env'] }"
|
|
34
|
+
];
|
|
35
|
+
function _getRandomTip() {
|
|
36
|
+
return TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
37
|
+
}
|
|
38
|
+
function parseBoolean(value) {
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
return !["false", "0", "no", "off", ""].includes(value.toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
return Boolean(value);
|
|
43
|
+
}
|
|
44
|
+
function supportsAnsi() {
|
|
45
|
+
return process.stdout.isTTY;
|
|
46
|
+
}
|
|
47
|
+
function dim(text) {
|
|
48
|
+
return supportsAnsi() ? `\x1B[2m${text}\x1B[0m` : text;
|
|
49
|
+
}
|
|
50
|
+
var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
|
|
51
|
+
function parse(src) {
|
|
52
|
+
const obj = {};
|
|
53
|
+
let lines = src.toString();
|
|
54
|
+
lines = lines.replace(/\r\n?/mg, "\n");
|
|
55
|
+
let match;
|
|
56
|
+
while ((match = LINE.exec(lines)) != null) {
|
|
57
|
+
const key = match[1];
|
|
58
|
+
let value = match[2] || "";
|
|
59
|
+
value = value.trim();
|
|
60
|
+
const maybeQuote = value[0];
|
|
61
|
+
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2");
|
|
62
|
+
if (maybeQuote === '"') {
|
|
63
|
+
value = value.replace(/\\n/g, "\n");
|
|
64
|
+
value = value.replace(/\\r/g, "\r");
|
|
65
|
+
}
|
|
66
|
+
obj[key] = value;
|
|
67
|
+
}
|
|
68
|
+
return obj;
|
|
69
|
+
}
|
|
70
|
+
function _parseVault(options) {
|
|
71
|
+
options = options || {};
|
|
72
|
+
const vaultPath = _vaultPath(options);
|
|
73
|
+
options.path = vaultPath;
|
|
74
|
+
const result = DotenvModule.configDotenv(options);
|
|
75
|
+
if (!result.parsed) {
|
|
76
|
+
const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
|
|
77
|
+
err.code = "MISSING_DATA";
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
const keys = _dotenvKey(options).split(",");
|
|
81
|
+
const length = keys.length;
|
|
82
|
+
let decrypted;
|
|
83
|
+
for (let i = 0; i < length; i++) {
|
|
84
|
+
try {
|
|
85
|
+
const key = keys[i].trim();
|
|
86
|
+
const attrs = _instructions(result, key);
|
|
87
|
+
decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
|
|
88
|
+
break;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (i + 1 >= length) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return DotenvModule.parse(decrypted);
|
|
96
|
+
}
|
|
97
|
+
function _warn(message) {
|
|
98
|
+
console.error(`\u26A0 ${message}`);
|
|
99
|
+
}
|
|
100
|
+
function _debug(message) {
|
|
101
|
+
console.log(`\u2506 ${message}`);
|
|
102
|
+
}
|
|
103
|
+
function _log(message) {
|
|
104
|
+
console.log(`\u25C7 ${message}`);
|
|
105
|
+
}
|
|
106
|
+
function _dotenvKey(options) {
|
|
107
|
+
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
|
|
108
|
+
return options.DOTENV_KEY;
|
|
109
|
+
}
|
|
110
|
+
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
111
|
+
return process.env.DOTENV_KEY;
|
|
112
|
+
}
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
function _instructions(result, dotenvKey) {
|
|
116
|
+
let uri;
|
|
117
|
+
try {
|
|
118
|
+
uri = new URL(dotenvKey);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error.code === "ERR_INVALID_URL") {
|
|
121
|
+
const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
|
|
122
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
const key = uri.password;
|
|
128
|
+
if (!key) {
|
|
129
|
+
const err = new Error("INVALID_DOTENV_KEY: Missing key part");
|
|
130
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
const environment = uri.searchParams.get("environment");
|
|
134
|
+
if (!environment) {
|
|
135
|
+
const err = new Error("INVALID_DOTENV_KEY: Missing environment part");
|
|
136
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
|
|
140
|
+
const ciphertext = result.parsed[environmentKey];
|
|
141
|
+
if (!ciphertext) {
|
|
142
|
+
const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
|
|
143
|
+
err.code = "NOT_FOUND_DOTENV_ENVIRONMENT";
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
return { ciphertext, key };
|
|
147
|
+
}
|
|
148
|
+
function _vaultPath(options) {
|
|
149
|
+
let possibleVaultPath = null;
|
|
150
|
+
if (options && options.path && options.path.length > 0) {
|
|
151
|
+
if (Array.isArray(options.path)) {
|
|
152
|
+
for (const filepath of options.path) {
|
|
153
|
+
if (fs3.existsSync(filepath)) {
|
|
154
|
+
possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
possibleVaultPath = path2.resolve(process.cwd(), ".env.vault");
|
|
162
|
+
}
|
|
163
|
+
if (fs3.existsSync(possibleVaultPath)) {
|
|
164
|
+
return possibleVaultPath;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function _resolveHome(envPath) {
|
|
169
|
+
return envPath[0] === "~" ? path2.join(os.homedir(), envPath.slice(1)) : envPath;
|
|
170
|
+
}
|
|
171
|
+
function _configVault(options) {
|
|
172
|
+
const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
173
|
+
const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
174
|
+
if (debug || !quiet) {
|
|
175
|
+
_log("loading env from encrypted .env.vault");
|
|
176
|
+
}
|
|
177
|
+
const parsed = DotenvModule._parseVault(options);
|
|
178
|
+
let processEnv = process.env;
|
|
179
|
+
if (options && options.processEnv != null) {
|
|
180
|
+
processEnv = options.processEnv;
|
|
181
|
+
}
|
|
182
|
+
DotenvModule.populate(processEnv, parsed, options);
|
|
183
|
+
return { parsed };
|
|
184
|
+
}
|
|
185
|
+
function configDotenv(options) {
|
|
186
|
+
const dotenvPath = path2.resolve(process.cwd(), ".env");
|
|
187
|
+
let encoding = "utf8";
|
|
188
|
+
let processEnv = process.env;
|
|
189
|
+
if (options && options.processEnv != null) {
|
|
190
|
+
processEnv = options.processEnv;
|
|
191
|
+
}
|
|
192
|
+
let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
193
|
+
let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
194
|
+
if (options && options.encoding) {
|
|
195
|
+
encoding = options.encoding;
|
|
196
|
+
} else {
|
|
197
|
+
if (debug) {
|
|
198
|
+
_debug("no encoding is specified (UTF-8 is used by default)");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let optionPaths = [dotenvPath];
|
|
202
|
+
if (options && options.path) {
|
|
203
|
+
if (!Array.isArray(options.path)) {
|
|
204
|
+
optionPaths = [_resolveHome(options.path)];
|
|
205
|
+
} else {
|
|
206
|
+
optionPaths = [];
|
|
207
|
+
for (const filepath of options.path) {
|
|
208
|
+
optionPaths.push(_resolveHome(filepath));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
let lastError;
|
|
213
|
+
const parsedAll = {};
|
|
214
|
+
for (const path3 of optionPaths) {
|
|
215
|
+
try {
|
|
216
|
+
const parsed = DotenvModule.parse(fs3.readFileSync(path3, { encoding }));
|
|
217
|
+
DotenvModule.populate(parsedAll, parsed, options);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
if (debug) {
|
|
220
|
+
_debug(`failed to load ${path3} ${e.message}`);
|
|
221
|
+
}
|
|
222
|
+
lastError = e;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const populated = DotenvModule.populate(processEnv, parsedAll, options);
|
|
226
|
+
debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug);
|
|
227
|
+
quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet);
|
|
228
|
+
if (debug || !quiet) {
|
|
229
|
+
const keysCount = Object.keys(populated).length;
|
|
230
|
+
const shortPaths = [];
|
|
231
|
+
for (const filePath of optionPaths) {
|
|
232
|
+
try {
|
|
233
|
+
const relative = path2.relative(process.cwd(), filePath);
|
|
234
|
+
shortPaths.push(relative);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
if (debug) {
|
|
237
|
+
_debug(`failed to load ${filePath} ${e.message}`);
|
|
238
|
+
}
|
|
239
|
+
lastError = e;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
_log(`injected env (${keysCount}) from ${shortPaths.join(",")} ${dim(`// tip: ${_getRandomTip()}`)}`);
|
|
243
|
+
}
|
|
244
|
+
if (lastError) {
|
|
245
|
+
return { parsed: parsedAll, error: lastError };
|
|
246
|
+
} else {
|
|
247
|
+
return { parsed: parsedAll };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function config(options) {
|
|
251
|
+
if (_dotenvKey(options).length === 0) {
|
|
252
|
+
return DotenvModule.configDotenv(options);
|
|
253
|
+
}
|
|
254
|
+
const vaultPath = _vaultPath(options);
|
|
255
|
+
if (!vaultPath) {
|
|
256
|
+
_warn(`you set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}`);
|
|
257
|
+
return DotenvModule.configDotenv(options);
|
|
258
|
+
}
|
|
259
|
+
return DotenvModule._configVault(options);
|
|
260
|
+
}
|
|
261
|
+
function decrypt(encrypted, keyStr) {
|
|
262
|
+
const key = Buffer.from(keyStr.slice(-64), "hex");
|
|
263
|
+
let ciphertext = Buffer.from(encrypted, "base64");
|
|
264
|
+
const nonce = ciphertext.subarray(0, 12);
|
|
265
|
+
const authTag = ciphertext.subarray(-16);
|
|
266
|
+
ciphertext = ciphertext.subarray(12, -16);
|
|
267
|
+
try {
|
|
268
|
+
const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
269
|
+
aesgcm.setAuthTag(authTag);
|
|
270
|
+
return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
const isRange = error instanceof RangeError;
|
|
273
|
+
const invalidKeyLength = error.message === "Invalid key length";
|
|
274
|
+
const decryptionFailed = error.message === "Unsupported state or unable to authenticate data";
|
|
275
|
+
if (isRange || invalidKeyLength) {
|
|
276
|
+
const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)");
|
|
277
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
278
|
+
throw err;
|
|
279
|
+
} else if (decryptionFailed) {
|
|
280
|
+
const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY");
|
|
281
|
+
err.code = "DECRYPTION_FAILED";
|
|
282
|
+
throw err;
|
|
283
|
+
} else {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function populate(processEnv, parsed, options = {}) {
|
|
289
|
+
const debug = Boolean(options && options.debug);
|
|
290
|
+
const override = Boolean(options && options.override);
|
|
291
|
+
const populated = {};
|
|
292
|
+
if (typeof parsed !== "object") {
|
|
293
|
+
const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
|
|
294
|
+
err.code = "OBJECT_REQUIRED";
|
|
295
|
+
throw err;
|
|
296
|
+
}
|
|
297
|
+
for (const key of Object.keys(parsed)) {
|
|
298
|
+
if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
|
299
|
+
if (override === true) {
|
|
300
|
+
processEnv[key] = parsed[key];
|
|
301
|
+
populated[key] = parsed[key];
|
|
302
|
+
}
|
|
303
|
+
if (debug) {
|
|
304
|
+
if (override === true) {
|
|
305
|
+
_debug(`"${key}" is already defined and WAS overwritten`);
|
|
306
|
+
} else {
|
|
307
|
+
_debug(`"${key}" is already defined and was NOT overwritten`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
processEnv[key] = parsed[key];
|
|
312
|
+
populated[key] = parsed[key];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return populated;
|
|
316
|
+
}
|
|
317
|
+
var DotenvModule = {
|
|
318
|
+
configDotenv,
|
|
319
|
+
_configVault,
|
|
320
|
+
_parseVault,
|
|
321
|
+
config,
|
|
322
|
+
decrypt,
|
|
323
|
+
parse,
|
|
324
|
+
populate
|
|
325
|
+
};
|
|
326
|
+
module.exports.configDotenv = DotenvModule.configDotenv;
|
|
327
|
+
module.exports._configVault = DotenvModule._configVault;
|
|
328
|
+
module.exports._parseVault = DotenvModule._parseVault;
|
|
329
|
+
module.exports.config = DotenvModule.config;
|
|
330
|
+
module.exports.decrypt = DotenvModule.decrypt;
|
|
331
|
+
module.exports.parse = DotenvModule.parse;
|
|
332
|
+
module.exports.populate = DotenvModule.populate;
|
|
333
|
+
module.exports = DotenvModule;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// src/config.ts
|
|
338
|
+
function getConfig() {
|
|
339
|
+
return {
|
|
340
|
+
openai: {
|
|
341
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
342
|
+
baseUrl: process.env.OPENAI_BASE_URL || "https://api.openai.com/v1",
|
|
343
|
+
defaultModel: process.env.OPENAI_DEFAULT_MODEL || "gpt-3.5-turbo"
|
|
344
|
+
},
|
|
345
|
+
openrouter: {
|
|
346
|
+
apiKey: process.env.OPEN_ROUTER_API_KEY,
|
|
347
|
+
baseUrl: process.env.OPEN_ROUTER_BASE_URL || "https://openrouter.ai/api/v1/chat/completions",
|
|
348
|
+
defaultModel: process.env.OPEN_ROUTER_DEFAULT_MODEL || "openrouter/free"
|
|
349
|
+
},
|
|
350
|
+
ollama: {
|
|
351
|
+
apiKey: process.env.OLLAMA_API_KEY,
|
|
352
|
+
baseUrl: process.env.OLLAMA_BASE_URL || "http://localhost:1234",
|
|
353
|
+
defaultModel: process.env.OLLAMA_DEFAULT_MODEL || "gemma4:31b-cloud"
|
|
354
|
+
},
|
|
355
|
+
bigmodel: {
|
|
356
|
+
apiKey: process.env.BIGMODEL_API_KEY,
|
|
357
|
+
baseUrl: process.env.BIGMODEL_BASE_URL || "https://open.bigmodel.cn/api/paas/v4/chat/completions",
|
|
358
|
+
defaultModel: process.env.BIGMODEL_DEFAULT_MODEL || "glm-4.7-flash"
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/tools.ts
|
|
364
|
+
function convertToOpenAITools(tools) {
|
|
365
|
+
if (!tools || tools.length === 0) {
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
return tools.map((tool) => {
|
|
369
|
+
const properties = {};
|
|
370
|
+
const required = [];
|
|
371
|
+
for (const [paramName, paramDef] of Object.entries(tool.parameters)) {
|
|
372
|
+
properties[paramName] = {
|
|
373
|
+
type: paramDef.type,
|
|
374
|
+
description: paramDef.description
|
|
375
|
+
};
|
|
376
|
+
if (paramDef.required) {
|
|
377
|
+
required.push(paramName);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
type: "function",
|
|
382
|
+
function: {
|
|
383
|
+
name: tool.name,
|
|
384
|
+
description: tool.description,
|
|
385
|
+
parameters: {
|
|
386
|
+
type: "object",
|
|
387
|
+
properties,
|
|
388
|
+
required: required.length > 0 ? required : void 0
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function convertToOllamaTools(tools) {
|
|
395
|
+
return convertToOpenAITools(tools);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/logger.ts
|
|
399
|
+
import pino from "pino";
|
|
400
|
+
var currentVerboseLevel = "none";
|
|
401
|
+
var getPinoLevel = (level) => {
|
|
402
|
+
switch (level) {
|
|
403
|
+
case "debug":
|
|
404
|
+
return "debug";
|
|
405
|
+
case "info":
|
|
406
|
+
return "info";
|
|
407
|
+
case "none":
|
|
408
|
+
default:
|
|
409
|
+
return "silent";
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
var logger = pino({
|
|
413
|
+
level: getPinoLevel(currentVerboseLevel),
|
|
414
|
+
transport: process.env.NODE_ENV !== "production" ? {
|
|
415
|
+
target: "pino-pretty",
|
|
416
|
+
options: {
|
|
417
|
+
colorize: true,
|
|
418
|
+
translateTime: "SYS:standard"
|
|
419
|
+
}
|
|
420
|
+
} : void 0
|
|
421
|
+
});
|
|
422
|
+
function verbose(level) {
|
|
423
|
+
currentVerboseLevel = level;
|
|
424
|
+
logger.level = getPinoLevel(level);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/utils.ts
|
|
428
|
+
function formatMessages(messages) {
|
|
429
|
+
return messages.map((msg) => {
|
|
430
|
+
if (msg.images && msg.images.length > 0) {
|
|
431
|
+
const content = [{ type: "text", text: msg.content }];
|
|
432
|
+
for (const image of msg.images) {
|
|
433
|
+
content.push({
|
|
434
|
+
type: "image_url",
|
|
435
|
+
image_url: { url: `data:image/jpeg;base64,${image}` }
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
...msg,
|
|
440
|
+
content,
|
|
441
|
+
images: void 0
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return msg;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async function fetchWithRetry(url, options = {}) {
|
|
448
|
+
const { timeout = 3e4, retries = 2, retryDelay = 1e3, ...fetchOptions } = options;
|
|
449
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
450
|
+
const controller = new AbortController();
|
|
451
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
452
|
+
try {
|
|
453
|
+
const response = await fetch(url, {
|
|
454
|
+
...fetchOptions,
|
|
455
|
+
signal: controller.signal
|
|
456
|
+
});
|
|
457
|
+
clearTimeout(id);
|
|
458
|
+
if (!response.ok) {
|
|
459
|
+
const isTransient = response.status >= 500 || response.status === 429;
|
|
460
|
+
if (isTransient && attempt < retries) {
|
|
461
|
+
logger.warn(
|
|
462
|
+
{
|
|
463
|
+
url,
|
|
464
|
+
status: response.status,
|
|
465
|
+
attempt: attempt + 1,
|
|
466
|
+
method: fetchOptions.method,
|
|
467
|
+
body: fetchOptions.body
|
|
468
|
+
},
|
|
469
|
+
"Transient error, retrying..."
|
|
470
|
+
);
|
|
471
|
+
await new Promise((res) => setTimeout(res, retryDelay * Math.pow(2, attempt)));
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return response;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
clearTimeout(id);
|
|
478
|
+
const isAbort = error.name === "AbortError";
|
|
479
|
+
if ((isAbort || error.message.includes("fetch failed")) && attempt < retries) {
|
|
480
|
+
logger.warn(
|
|
481
|
+
{
|
|
482
|
+
url,
|
|
483
|
+
error: error.message,
|
|
484
|
+
attempt: attempt + 1,
|
|
485
|
+
method: fetchOptions.method,
|
|
486
|
+
body: fetchOptions.body
|
|
487
|
+
},
|
|
488
|
+
"Network error or timeout, retrying..."
|
|
489
|
+
);
|
|
490
|
+
await new Promise((res) => setTimeout(res, retryDelay * Math.pow(2, attempt)));
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
throw new Error(`Fetch failed after ${retries} retries`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/providers/openai.ts
|
|
500
|
+
async function openaiCompletion(options, config, modelName) {
|
|
501
|
+
const apiKey = options.apiKey || config.openai.apiKey;
|
|
502
|
+
const baseUrl = options.baseUrl || options.host || config.openai.baseUrl || "https://api.openai.com/v1";
|
|
503
|
+
if (!apiKey) {
|
|
504
|
+
throw new Error("OpenAI API key is required");
|
|
505
|
+
}
|
|
506
|
+
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
507
|
+
const url = normalizedBaseUrl.endsWith("/chat/completions") ? normalizedBaseUrl : `${normalizedBaseUrl}/chat/completions`;
|
|
508
|
+
const tools = convertToOpenAITools(options.tools);
|
|
509
|
+
const messages = formatMessages(options.messages);
|
|
510
|
+
const body = {
|
|
511
|
+
model: modelName,
|
|
512
|
+
messages
|
|
513
|
+
};
|
|
514
|
+
if (tools) {
|
|
515
|
+
body.tools = tools;
|
|
516
|
+
}
|
|
517
|
+
logger.debug({ url, body, provider: "openai" }, "Sending request to OpenAI");
|
|
518
|
+
const response = await fetchWithRetry(url, {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: {
|
|
521
|
+
"Content-Type": "application/json",
|
|
522
|
+
Authorization: `Bearer ${apiKey}`
|
|
523
|
+
},
|
|
524
|
+
body: JSON.stringify(body)
|
|
525
|
+
});
|
|
526
|
+
const rawText = await response.text();
|
|
527
|
+
logger.debug({ response: rawText, provider: "openai" }, "Received raw response from OpenAI");
|
|
528
|
+
if (!response.ok) {
|
|
529
|
+
logger.error(
|
|
530
|
+
{ url, model: modelName, body, provider: "openai" },
|
|
531
|
+
"OpenAI request failed"
|
|
532
|
+
);
|
|
533
|
+
throw new Error(`OpenAI API error (${response.status}): ${rawText}`);
|
|
534
|
+
}
|
|
535
|
+
const data = JSON.parse(rawText);
|
|
536
|
+
return data;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/providers/openrouter.ts
|
|
540
|
+
async function openrouterCompletion(options, config, modelName) {
|
|
541
|
+
const apiKey = options.apiKey || config.openrouter.apiKey;
|
|
542
|
+
const baseUrl = options.baseUrl || options.host || config.openrouter.baseUrl || "https://openrouter.ai/api/v1/chat/completions";
|
|
543
|
+
if (!apiKey) {
|
|
544
|
+
throw new Error("OpenRouter API key is required");
|
|
545
|
+
}
|
|
546
|
+
const url = baseUrl;
|
|
547
|
+
const tools = convertToOpenAITools(options.tools);
|
|
548
|
+
const messages = formatMessages(options.messages);
|
|
549
|
+
const body = {
|
|
550
|
+
model: modelName,
|
|
551
|
+
messages
|
|
552
|
+
};
|
|
553
|
+
if (tools) {
|
|
554
|
+
body.tools = tools;
|
|
555
|
+
}
|
|
556
|
+
logger.debug({ url, body, provider: "openrouter" }, "Sending request to OpenRouter");
|
|
557
|
+
const response = await fetchWithRetry(url, {
|
|
558
|
+
method: "POST",
|
|
559
|
+
headers: {
|
|
560
|
+
"Content-Type": "application/json",
|
|
561
|
+
Authorization: `Bearer ${apiKey}`,
|
|
562
|
+
"HTTP-Referer": "https://github.com/llmjs2",
|
|
563
|
+
// Required by OpenRouter
|
|
564
|
+
"X-Title": "llmjs2"
|
|
565
|
+
// Optional but recommended by OpenRouter
|
|
566
|
+
},
|
|
567
|
+
body: JSON.stringify(body)
|
|
568
|
+
});
|
|
569
|
+
const rawText = await response.text();
|
|
570
|
+
logger.debug({ response: rawText, provider: "openrouter" }, "Received raw response from OpenRouter");
|
|
571
|
+
if (!response.ok) {
|
|
572
|
+
logger.error(
|
|
573
|
+
{ url, model: modelName, body, provider: "openrouter" },
|
|
574
|
+
"OpenRouter request failed"
|
|
575
|
+
);
|
|
576
|
+
throw new Error(`OpenRouter API error (${response.status}): ${rawText}`);
|
|
577
|
+
}
|
|
578
|
+
const data = JSON.parse(rawText);
|
|
579
|
+
return data;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/providers/ollama.ts
|
|
583
|
+
async function ollamaCompletion(options, config, modelName) {
|
|
584
|
+
const baseUrl = options.baseUrl || options.host || config.ollama.baseUrl || "http://localhost:11434";
|
|
585
|
+
const apiKey = options.apiKey || config.ollama.apiKey;
|
|
586
|
+
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
587
|
+
const url = normalizedBaseUrl.endsWith("/api/chat") ? normalizedBaseUrl : `${normalizedBaseUrl}/api/chat`;
|
|
588
|
+
const tools = convertToOllamaTools(options.tools);
|
|
589
|
+
const body = {
|
|
590
|
+
model: modelName,
|
|
591
|
+
messages: options.messages,
|
|
592
|
+
stream: false
|
|
593
|
+
// Ensure we get a single response object
|
|
594
|
+
};
|
|
595
|
+
if (tools) {
|
|
596
|
+
body.tools = tools;
|
|
597
|
+
}
|
|
598
|
+
const headers = {
|
|
599
|
+
"Content-Type": "application/json"
|
|
600
|
+
};
|
|
601
|
+
if (apiKey) {
|
|
602
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
603
|
+
}
|
|
604
|
+
logger.debug({ url, body, provider: "ollama" }, "Sending request to Ollama");
|
|
605
|
+
const response = await fetchWithRetry(url, {
|
|
606
|
+
method: "POST",
|
|
607
|
+
headers,
|
|
608
|
+
body: JSON.stringify(body)
|
|
609
|
+
});
|
|
610
|
+
const rawText = await response.text();
|
|
611
|
+
logger.debug({ response: rawText, provider: "ollama" }, "Received raw response from Ollama");
|
|
612
|
+
if (!response.ok) {
|
|
613
|
+
logger.error(
|
|
614
|
+
{ url, model: modelName, body, provider: "ollama" },
|
|
615
|
+
"Ollama request failed"
|
|
616
|
+
);
|
|
617
|
+
throw new Error(`Ollama API error (${response.status}): ${rawText}`);
|
|
618
|
+
}
|
|
619
|
+
const data = JSON.parse(rawText);
|
|
620
|
+
const normalizedResponse = {
|
|
621
|
+
id: `ollama-${Date.now()}`,
|
|
622
|
+
object: "chat.completion",
|
|
623
|
+
created: Math.floor(Date.now() / 1e3),
|
|
624
|
+
model: data.model,
|
|
625
|
+
choices: [
|
|
626
|
+
{
|
|
627
|
+
index: 0,
|
|
628
|
+
message: data.message,
|
|
629
|
+
finish_reason: data.done ? "stop" : "length"
|
|
630
|
+
}
|
|
631
|
+
],
|
|
632
|
+
usage: {
|
|
633
|
+
prompt_tokens: data.prompt_eval_count || 0,
|
|
634
|
+
completion_tokens: data.eval_count || 0,
|
|
635
|
+
total_tokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
return normalizedResponse;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/providers/bigmodel.ts
|
|
642
|
+
async function bigmodelCompletion(options, config, modelName) {
|
|
643
|
+
const apiKey = options.apiKey || config.bigmodel.apiKey;
|
|
644
|
+
const baseUrl = options.baseUrl || options.host || config.bigmodel.baseUrl || "https://open.bigmodel.cn/api/paas/v4/chat/completions";
|
|
645
|
+
if (!apiKey) {
|
|
646
|
+
throw new Error("BigModel API key is required");
|
|
647
|
+
}
|
|
648
|
+
const url = baseUrl;
|
|
649
|
+
const messages = formatMessages(options.messages);
|
|
650
|
+
const body = {
|
|
651
|
+
model: modelName,
|
|
652
|
+
messages
|
|
653
|
+
};
|
|
654
|
+
const anyOpts = options;
|
|
655
|
+
if (anyOpts.thinking !== void 0) body.thinking = anyOpts.thinking;
|
|
656
|
+
if (anyOpts.max_tokens !== void 0) body.max_tokens = anyOpts.max_tokens;
|
|
657
|
+
if (anyOpts.temperature !== void 0) body.temperature = anyOpts.temperature;
|
|
658
|
+
logger.debug({ url, body, provider: "bigmodel" }, "Sending request to BigModel");
|
|
659
|
+
const response = await fetchWithRetry(url, {
|
|
660
|
+
method: "POST",
|
|
661
|
+
headers: {
|
|
662
|
+
"Content-Type": "application/json",
|
|
663
|
+
Authorization: `Bearer ${apiKey}`
|
|
664
|
+
},
|
|
665
|
+
body: JSON.stringify(body)
|
|
666
|
+
});
|
|
667
|
+
const rawText = await response.text();
|
|
668
|
+
logger.debug({ response: rawText, provider: "bigmodel" }, "Received raw response from BigModel");
|
|
669
|
+
if (!response.ok) {
|
|
670
|
+
logger.error(
|
|
671
|
+
{ url, model: modelName, body, provider: "bigmodel" },
|
|
672
|
+
"BigModel request failed"
|
|
673
|
+
);
|
|
674
|
+
throw new Error(`BigModel API error (${response.status}): ${rawText}`);
|
|
675
|
+
}
|
|
676
|
+
const data = JSON.parse(rawText);
|
|
677
|
+
return data;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/completion.ts
|
|
681
|
+
try {
|
|
682
|
+
const dotenv = require_main();
|
|
683
|
+
if (dotenv && dotenv.config) {
|
|
684
|
+
dotenv.config();
|
|
685
|
+
}
|
|
686
|
+
} catch (e) {
|
|
687
|
+
}
|
|
688
|
+
async function completion(promptOrOptions) {
|
|
689
|
+
const config = getConfig();
|
|
690
|
+
logger.debug({ inputType: typeof promptOrOptions }, "completion() called");
|
|
691
|
+
let options;
|
|
692
|
+
const selectedProvider = config.openai.apiKey ? "openai" : config.openrouter.apiKey ? "openrouter" : config.bigmodel.apiKey ? "bigmodel" : config.ollama.apiKey ? "ollama" : null;
|
|
693
|
+
const getProviderModel = (provider2) => {
|
|
694
|
+
switch (provider2) {
|
|
695
|
+
case "openai":
|
|
696
|
+
return process.env.OPENAI_DEFAULT_MODEL || config.openai.defaultModel || "gpt-4o-mini";
|
|
697
|
+
case "openrouter":
|
|
698
|
+
return process.env.OPEN_ROUTER_DEFAULT_MODEL || config.openrouter.defaultModel || "openai/gpt-4o-mini";
|
|
699
|
+
case "bigmodel":
|
|
700
|
+
return process.env.BIGMODEL_DEFAULT_MODEL || config.bigmodel.defaultModel || "glm-4.7-flash";
|
|
701
|
+
case "ollama":
|
|
702
|
+
return process.env.OLLAMA_DEFAULT_MODEL || config.ollama.defaultModel || "llama3.2";
|
|
703
|
+
default:
|
|
704
|
+
throw new Error(`Unsupported provider: ${provider2}`);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
const resolveModel = (explicitModel) => {
|
|
708
|
+
if (explicitModel) {
|
|
709
|
+
return explicitModel;
|
|
710
|
+
}
|
|
711
|
+
if (process.env.LLM_DEFAULT_MODEL) {
|
|
712
|
+
return process.env.LLM_DEFAULT_MODEL;
|
|
713
|
+
}
|
|
714
|
+
if (!selectedProvider) {
|
|
715
|
+
throw new Error(
|
|
716
|
+
"No model specified. Set LLM_DEFAULT_MODEL / provider default model env, provide `model` in options, or configure one of OPENAI_API_KEY / OPEN_ROUTER_API_KEY / BIGMODEL_API_KEY / OLLAMA_API_KEY."
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
return `${selectedProvider}/${getProviderModel(selectedProvider)}`;
|
|
720
|
+
};
|
|
721
|
+
if (typeof promptOrOptions === "string") {
|
|
722
|
+
options = {
|
|
723
|
+
model: resolveModel(),
|
|
724
|
+
messages: [{ role: "user", content: promptOrOptions }]
|
|
725
|
+
};
|
|
726
|
+
} else {
|
|
727
|
+
options = {
|
|
728
|
+
...promptOrOptions,
|
|
729
|
+
model: resolveModel(promptOrOptions.model)
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (!options.model) {
|
|
733
|
+
throw new Error('No model specified. Provide `model` in options (format: "provider/model_name") or set LLM_DEFAULT_MODEL.');
|
|
734
|
+
}
|
|
735
|
+
const firstSlashIndex = options.model.indexOf("/");
|
|
736
|
+
if (firstSlashIndex === -1) {
|
|
737
|
+
throw new Error('Invalid model format. Expected "provider/model_name"');
|
|
738
|
+
}
|
|
739
|
+
const provider = options.model.substring(0, firstSlashIndex);
|
|
740
|
+
const modelName = options.model.substring(firstSlashIndex + 1);
|
|
741
|
+
logger.info({ provider, model: modelName }, "Dispatching completion request");
|
|
742
|
+
let response;
|
|
743
|
+
switch (provider) {
|
|
744
|
+
case "openai":
|
|
745
|
+
response = await openaiCompletion(options, config, modelName);
|
|
746
|
+
break;
|
|
747
|
+
case "openrouter":
|
|
748
|
+
response = await openrouterCompletion(options, config, modelName);
|
|
749
|
+
break;
|
|
750
|
+
case "bigmodel":
|
|
751
|
+
response = await bigmodelCompletion(options, config, modelName);
|
|
752
|
+
break;
|
|
753
|
+
case "ollama":
|
|
754
|
+
response = await ollamaCompletion(options, config, modelName);
|
|
755
|
+
break;
|
|
756
|
+
default:
|
|
757
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
758
|
+
}
|
|
759
|
+
if (typeof promptOrOptions === "string") {
|
|
760
|
+
const text = response.choices?.[0]?.message?.content || response.message?.content || "";
|
|
761
|
+
logger.debug({ provider, model: modelName, responseType: "text" }, "completion() finished");
|
|
762
|
+
return text;
|
|
763
|
+
}
|
|
764
|
+
logger.debug({ provider, model: modelName, responseType: "object" }, "completion() finished");
|
|
765
|
+
return response;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/router.ts
|
|
769
|
+
import fs from "fs";
|
|
770
|
+
import path from "path";
|
|
771
|
+
import { randomUUID } from "crypto";
|
|
772
|
+
function resolveEnvPlaceholders(input) {
|
|
773
|
+
return input.replace(/\$\{([A-Z0-9_]+)\}/gi, (_match, name) => {
|
|
774
|
+
return process.env[name] ?? "";
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function parseConfigFile(filePath) {
|
|
778
|
+
const resolvedPath = path.resolve(process.cwd(), filePath);
|
|
779
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
780
|
+
if (ext === ".json") {
|
|
781
|
+
const content = fs.readFileSync(resolvedPath, "utf8");
|
|
782
|
+
const interpolated = resolveEnvPlaceholders(content);
|
|
783
|
+
return JSON.parse(interpolated);
|
|
784
|
+
}
|
|
785
|
+
if (ext === ".js" || ext === ".cjs") {
|
|
786
|
+
const loaded = __require(resolvedPath);
|
|
787
|
+
return loaded?.default ?? loaded;
|
|
788
|
+
}
|
|
789
|
+
throw new Error(`Unsupported router config file extension: ${ext}`);
|
|
790
|
+
}
|
|
791
|
+
function validateModelFormat(model, routeName) {
|
|
792
|
+
if (!model || model.indexOf("/") === -1) {
|
|
793
|
+
const suffix = routeName ? ` in route ${routeName}` : "";
|
|
794
|
+
throw new Error(`Invalid model format${suffix}: expected provider/model_name`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function validateConfig(config) {
|
|
798
|
+
const routing = config.routing ?? "default";
|
|
799
|
+
if (!["random", "sequential", "default"].includes(routing)) {
|
|
800
|
+
throw new Error(`Invalid routing strategy: ${routing}`);
|
|
801
|
+
}
|
|
802
|
+
if (!Array.isArray(config.model_list) || config.model_list.length === 0) {
|
|
803
|
+
throw new Error("model_list must contain at least one route");
|
|
804
|
+
}
|
|
805
|
+
config.model_list.forEach((entry) => {
|
|
806
|
+
if (!entry.model_name) {
|
|
807
|
+
throw new Error("Each route must include model_name");
|
|
808
|
+
}
|
|
809
|
+
if (!entry.llm_params || !entry.llm_params.model) {
|
|
810
|
+
throw new Error(`Each route must include llm_params.model (route: ${entry.model_name})`);
|
|
811
|
+
}
|
|
812
|
+
validateModelFormat(entry.llm_params.model, entry.model_name);
|
|
813
|
+
});
|
|
814
|
+
if (config.guardrails && !Array.isArray(config.guardrails)) {
|
|
815
|
+
throw new Error("guardrails must be an array when provided");
|
|
816
|
+
}
|
|
817
|
+
if (Array.isArray(config.guardrails)) {
|
|
818
|
+
const names = /* @__PURE__ */ new Set();
|
|
819
|
+
config.guardrails.forEach((guardrail, index) => {
|
|
820
|
+
if (!guardrail || typeof guardrail !== "object") {
|
|
821
|
+
throw new Error(`Invalid guardrail at index ${index}`);
|
|
822
|
+
}
|
|
823
|
+
if (!guardrail.name || typeof guardrail.name !== "string") {
|
|
824
|
+
throw new Error(`Each guardrail must include a non-empty name (index: ${index})`);
|
|
825
|
+
}
|
|
826
|
+
if (names.has(guardrail.name)) {
|
|
827
|
+
throw new Error(`Duplicate guardrail name: ${guardrail.name}`);
|
|
828
|
+
}
|
|
829
|
+
names.add(guardrail.name);
|
|
830
|
+
if (guardrail.mode !== "pre_call" && guardrail.mode !== "post_call") {
|
|
831
|
+
throw new Error(`Invalid guardrail mode for ${guardrail.name}: ${guardrail.mode}`);
|
|
832
|
+
}
|
|
833
|
+
const codeType = typeof guardrail.code;
|
|
834
|
+
if (codeType !== "string" && codeType !== "function") {
|
|
835
|
+
throw new Error(`Guardrail ${guardrail.name} must provide code as a string or function`);
|
|
836
|
+
}
|
|
837
|
+
if (guardrail.timeout_ms !== void 0) {
|
|
838
|
+
if (!Number.isFinite(guardrail.timeout_ms) || guardrail.timeout_ms <= 0) {
|
|
839
|
+
throw new Error(`Guardrail ${guardrail.name} has invalid timeout_ms: expected a positive number`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
...config,
|
|
846
|
+
routing
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function compileGuardrail(guardrail) {
|
|
850
|
+
const timeoutMs = guardrail.timeout_ms;
|
|
851
|
+
if (typeof guardrail.code === "function") {
|
|
852
|
+
return {
|
|
853
|
+
name: guardrail.name,
|
|
854
|
+
mode: guardrail.mode,
|
|
855
|
+
timeoutMs,
|
|
856
|
+
handler: guardrail.code
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
const fn = new Function(`return (${guardrail.code});`)();
|
|
861
|
+
if (typeof fn !== "function") {
|
|
862
|
+
throw new Error("compiled value is not a function");
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
name: guardrail.name,
|
|
866
|
+
mode: guardrail.mode,
|
|
867
|
+
timeoutMs,
|
|
868
|
+
handler: fn
|
|
869
|
+
};
|
|
870
|
+
} catch (error) {
|
|
871
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
872
|
+
throw new Error(`GuardrailCompileError [${guardrail.name}] (${guardrail.mode}): ${message}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function createProcessId() {
|
|
876
|
+
try {
|
|
877
|
+
return randomUUID();
|
|
878
|
+
} catch (_error) {
|
|
879
|
+
return `${Date.now()}-${Math.floor(Math.random() * 1e5)}`;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function runGuardrail(guardrail, processId, payload) {
|
|
883
|
+
const execute = Promise.resolve().then(() => guardrail.handler(processId, payload));
|
|
884
|
+
try {
|
|
885
|
+
if (!guardrail.timeoutMs) {
|
|
886
|
+
return await execute;
|
|
887
|
+
}
|
|
888
|
+
const timeout = new Promise((_, reject) => {
|
|
889
|
+
setTimeout(() => {
|
|
890
|
+
reject(new Error(`GuardrailTimeoutError [${guardrail.name}] (${guardrail.mode}): exceeded ${guardrail.timeoutMs}ms`));
|
|
891
|
+
}, guardrail.timeoutMs);
|
|
892
|
+
});
|
|
893
|
+
return await Promise.race([execute, timeout]);
|
|
894
|
+
} catch (error) {
|
|
895
|
+
if (error instanceof Error && error.message.startsWith("GuardrailTimeoutError")) {
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
899
|
+
throw new Error(`GuardrailRuntimeError [${guardrail.name}] (${guardrail.mode}): ${message}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function validatePreCallOutput(output, guardrailName) {
|
|
903
|
+
if (!output || typeof output !== "object") {
|
|
904
|
+
throw new Error(`GuardrailValidationError [${guardrailName}] (pre_call): expected object output`);
|
|
905
|
+
}
|
|
906
|
+
if (typeof output.model !== "string" || !output.model) {
|
|
907
|
+
throw new Error(`GuardrailValidationError [${guardrailName}] (pre_call): expected non-empty model`);
|
|
908
|
+
}
|
|
909
|
+
if (!Array.isArray(output.messages)) {
|
|
910
|
+
throw new Error(`GuardrailValidationError [${guardrailName}] (pre_call): expected messages array`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function router(configOrPath) {
|
|
914
|
+
const rawConfig = typeof configOrPath === "string" ? parseConfigFile(configOrPath) : configOrPath;
|
|
915
|
+
const config = validateConfig(rawConfig);
|
|
916
|
+
const compiledGuardrails = (config.guardrails ?? []).filter((guardrail) => guardrail.enabled !== false).map(compileGuardrail);
|
|
917
|
+
const preCallGuardrails = compiledGuardrails.filter((guardrail) => guardrail.mode === "pre_call");
|
|
918
|
+
const postCallGuardrails = compiledGuardrails.filter((guardrail) => guardrail.mode === "post_call");
|
|
919
|
+
let sequentialIndex = 0;
|
|
920
|
+
const pickRoute = () => {
|
|
921
|
+
if (config.routing === "sequential") {
|
|
922
|
+
const index = sequentialIndex % config.model_list.length;
|
|
923
|
+
sequentialIndex += 1;
|
|
924
|
+
return config.model_list[index];
|
|
925
|
+
}
|
|
926
|
+
const randomIndex = Math.floor(Math.random() * config.model_list.length);
|
|
927
|
+
return config.model_list[randomIndex];
|
|
928
|
+
};
|
|
929
|
+
return {
|
|
930
|
+
async completion(options) {
|
|
931
|
+
const processId = createProcessId();
|
|
932
|
+
const route = pickRoute();
|
|
933
|
+
const params = route.llm_params;
|
|
934
|
+
const merged = {
|
|
935
|
+
...options,
|
|
936
|
+
...params,
|
|
937
|
+
messages: options.messages,
|
|
938
|
+
model: options.model || params.model,
|
|
939
|
+
apiKey: options.apiKey || params.api_key || params.apiKey,
|
|
940
|
+
baseUrl: options.baseUrl || params.api_base || params.baseUrl
|
|
941
|
+
};
|
|
942
|
+
let preCallPayload = {
|
|
943
|
+
model: merged.model,
|
|
944
|
+
messages: merged.messages,
|
|
945
|
+
tools: merged.tools,
|
|
946
|
+
metadata: {
|
|
947
|
+
route_name: route.model_name,
|
|
948
|
+
routing: config.routing
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
for (const guardrail of preCallGuardrails) {
|
|
952
|
+
const output = await runGuardrail(guardrail, processId, preCallPayload);
|
|
953
|
+
validatePreCallOutput(output, guardrail.name);
|
|
954
|
+
preCallPayload = output;
|
|
955
|
+
}
|
|
956
|
+
merged.model = preCallPayload.model;
|
|
957
|
+
merged.messages = preCallPayload.messages;
|
|
958
|
+
merged.tools = preCallPayload.tools;
|
|
959
|
+
validateModelFormat(merged.model, route.model_name);
|
|
960
|
+
const response = await completion(merged);
|
|
961
|
+
let postCallPayload = response;
|
|
962
|
+
for (const guardrail of postCallGuardrails) {
|
|
963
|
+
postCallPayload = await runGuardrail(guardrail, processId, postCallPayload);
|
|
964
|
+
}
|
|
965
|
+
return postCallPayload;
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/memory.ts
|
|
971
|
+
var memory_exports = {};
|
|
972
|
+
__export(memory_exports, {
|
|
973
|
+
fileMemory: () => fileMemory,
|
|
974
|
+
inMemory: () => inMemory
|
|
975
|
+
});
|
|
976
|
+
import * as fs2 from "fs/promises";
|
|
977
|
+
function createMemoryView(store) {
|
|
978
|
+
const getThreadMessages = (resourceId, threadId) => {
|
|
979
|
+
return (store.get(resourceId) || []).filter((message) => message.threadId === threadId);
|
|
980
|
+
};
|
|
981
|
+
const projectMessages = (messages, limit) => {
|
|
982
|
+
const visibleMessages = limit === void 0 ? messages : messages.slice(-limit);
|
|
983
|
+
return visibleMessages.map(({ role, content }) => ({ role, content }));
|
|
984
|
+
};
|
|
985
|
+
return {
|
|
986
|
+
async save(resourceId, threadId, role, content) {
|
|
987
|
+
const messages = store.get(resourceId) ?? [];
|
|
988
|
+
messages.push({ role, content, threadId });
|
|
989
|
+
store.set(resourceId, messages);
|
|
990
|
+
},
|
|
991
|
+
async list(resourceId, threadId, limit) {
|
|
992
|
+
return projectMessages(getThreadMessages(resourceId, threadId), limit);
|
|
993
|
+
},
|
|
994
|
+
async search(resourceId, query) {
|
|
995
|
+
const normalizedQuery = query.toLowerCase();
|
|
996
|
+
const messages = store.get(resourceId) || [];
|
|
997
|
+
return messages.filter((message) => message.content.toLowerCase().includes(normalizedQuery)).map(({ content, threadId }) => ({ content, threadId }));
|
|
998
|
+
},
|
|
999
|
+
async clear(resourceId, threadId) {
|
|
1000
|
+
if (!store.has(resourceId)) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const remainingMessages = (store.get(resourceId) || []).filter((message) => message.threadId !== threadId);
|
|
1004
|
+
store.set(resourceId, remainingMessages);
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function inMemory() {
|
|
1009
|
+
return createMemoryView(/* @__PURE__ */ new Map());
|
|
1010
|
+
}
|
|
1011
|
+
function fileMemory(config) {
|
|
1012
|
+
const { path: path2 } = config;
|
|
1013
|
+
async function readData() {
|
|
1014
|
+
try {
|
|
1015
|
+
const data = await fs2.readFile(path2, "utf-8");
|
|
1016
|
+
return JSON.parse(data);
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
if (e?.code === "ENOENT") {
|
|
1019
|
+
return {};
|
|
1020
|
+
}
|
|
1021
|
+
throw e;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
async function writeData(data) {
|
|
1025
|
+
await fs2.writeFile(path2, JSON.stringify(data, null, 2), "utf-8");
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
async save(resourceId, threadId, role, content) {
|
|
1029
|
+
const data = await readData();
|
|
1030
|
+
const messages = data[resourceId] ?? [];
|
|
1031
|
+
messages.push({ role, content, threadId });
|
|
1032
|
+
data[resourceId] = messages;
|
|
1033
|
+
await writeData(data);
|
|
1034
|
+
},
|
|
1035
|
+
async list(resourceId, threadId, limit) {
|
|
1036
|
+
const data = await readData();
|
|
1037
|
+
const messages = (data[resourceId] || []).filter((message) => message.threadId === threadId);
|
|
1038
|
+
const visibleMessages = limit === void 0 ? messages : messages.slice(-limit);
|
|
1039
|
+
return visibleMessages.map(({ role, content }) => ({ role, content }));
|
|
1040
|
+
},
|
|
1041
|
+
async search(resourceId, query) {
|
|
1042
|
+
const data = await readData();
|
|
1043
|
+
const normalizedQuery = query.toLowerCase();
|
|
1044
|
+
const messages = data[resourceId] || [];
|
|
1045
|
+
return messages.filter((message) => message.content.toLowerCase().includes(normalizedQuery)).map(({ content, threadId }) => ({ content, threadId }));
|
|
1046
|
+
},
|
|
1047
|
+
async clear(resourceId, threadId) {
|
|
1048
|
+
const data = await readData();
|
|
1049
|
+
if (!data[resourceId]) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
data[resourceId] = data[resourceId].filter((message) => message.threadId !== threadId);
|
|
1053
|
+
await writeData(data);
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/agent.ts
|
|
1059
|
+
var Agent = class {
|
|
1060
|
+
constructor(config) {
|
|
1061
|
+
this.config = config;
|
|
1062
|
+
if (!config.instruction) throw new Error("MissingInstructionError: instruction field is required.");
|
|
1063
|
+
if (!config.route) throw new Error("MissingRouteError: route is required.");
|
|
1064
|
+
}
|
|
1065
|
+
config;
|
|
1066
|
+
async generate(request) {
|
|
1067
|
+
let userPrompt;
|
|
1068
|
+
let memoryReq;
|
|
1069
|
+
let userMessageContent;
|
|
1070
|
+
let userImages = [];
|
|
1071
|
+
if (typeof request === "string") {
|
|
1072
|
+
userPrompt = request;
|
|
1073
|
+
userMessageContent = userPrompt;
|
|
1074
|
+
} else {
|
|
1075
|
+
if (!request.userPrompt) throw new Error("MissingUserPromptError: Object form of generate() requires userPrompt.");
|
|
1076
|
+
userPrompt = request.userPrompt;
|
|
1077
|
+
memoryReq = request.memory;
|
|
1078
|
+
let textPrompt = userPrompt;
|
|
1079
|
+
if (request.attachments && request.attachments.length > 0) {
|
|
1080
|
+
const { markitdown } = await import("any-markdown");
|
|
1081
|
+
const results = await markitdown(request.attachments);
|
|
1082
|
+
const textAttachments = [];
|
|
1083
|
+
const resultsArray = Array.isArray(results) ? results : [results];
|
|
1084
|
+
for (const result of resultsArray) {
|
|
1085
|
+
if ("error" in result && result.error) {
|
|
1086
|
+
logger.warn(
|
|
1087
|
+
{ code: result.code, message: result.message },
|
|
1088
|
+
"Failed to process attachment"
|
|
1089
|
+
);
|
|
1090
|
+
} else if (result.type === "image") {
|
|
1091
|
+
let base64Data = result.base64;
|
|
1092
|
+
if (base64Data.startsWith("data:")) {
|
|
1093
|
+
base64Data = base64Data.split(",")[1];
|
|
1094
|
+
}
|
|
1095
|
+
userImages.push(base64Data);
|
|
1096
|
+
} else {
|
|
1097
|
+
textAttachments.push(result.markdown);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (textAttachments.length > 0) {
|
|
1101
|
+
for (const attachmentText of textAttachments) {
|
|
1102
|
+
textPrompt += `
|
|
1103
|
+
|
|
1104
|
+
=== Attachment Start ===
|
|
1105
|
+
${attachmentText}
|
|
1106
|
+
=== Attachment End ===`;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
userMessageContent = textPrompt;
|
|
1111
|
+
}
|
|
1112
|
+
const messages = [
|
|
1113
|
+
{ role: "system", content: this.config.instruction }
|
|
1114
|
+
];
|
|
1115
|
+
if (this.config.memory && memoryReq) {
|
|
1116
|
+
const useSession = memoryReq.session ?? this.config.session;
|
|
1117
|
+
const useRelevance = memoryReq.relevance ?? this.config.relevance;
|
|
1118
|
+
if (useSession) {
|
|
1119
|
+
const history = await this.config.memory.list(memoryReq.resourceId, memoryReq.threadId);
|
|
1120
|
+
for (const msg of history) {
|
|
1121
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (useRelevance) {
|
|
1125
|
+
const relevant = await this.config.memory.search(memoryReq.resourceId, userPrompt);
|
|
1126
|
+
if (relevant.length > 0) {
|
|
1127
|
+
messages.push({ role: "system", content: `Relevant past messages:
|
|
1128
|
+
${relevant.map((r) => r.content).join("\n")}` });
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const userMessage = { role: "user", content: userMessageContent };
|
|
1133
|
+
if (userImages.length > 0) {
|
|
1134
|
+
userMessage.images = userImages;
|
|
1135
|
+
}
|
|
1136
|
+
messages.push(userMessage);
|
|
1137
|
+
const tools = this.config.tools;
|
|
1138
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
1139
|
+
if (tools) {
|
|
1140
|
+
for (const t of tools) {
|
|
1141
|
+
toolMap.set(t.name, t);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const toolCallsLog = [];
|
|
1145
|
+
let finalContent = "";
|
|
1146
|
+
while (true) {
|
|
1147
|
+
const response = await this.config.route.completion({ messages, tools });
|
|
1148
|
+
const toolCalls = response.choices?.[0]?.message?.tool_calls || response.message?.tool_calls || response.tool_calls;
|
|
1149
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
1150
|
+
messages.push({
|
|
1151
|
+
role: "assistant",
|
|
1152
|
+
content: response.choices?.[0]?.message?.content || response.message?.content || response.content || "",
|
|
1153
|
+
tool_calls: toolCalls
|
|
1154
|
+
});
|
|
1155
|
+
for (const tc of toolCalls) {
|
|
1156
|
+
const tool = toolMap.get(tc.function.name);
|
|
1157
|
+
let resultStr = "";
|
|
1158
|
+
if (tool) {
|
|
1159
|
+
try {
|
|
1160
|
+
const params = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
|
|
1161
|
+
resultStr = await tool.execute(params);
|
|
1162
|
+
toolCallsLog.push({
|
|
1163
|
+
name: tc.function.name,
|
|
1164
|
+
parameters: params,
|
|
1165
|
+
result: resultStr
|
|
1166
|
+
});
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
console.error(`[Agent] Error executing tool ${tc.function.name}:`, e);
|
|
1169
|
+
resultStr = `ToolExecutionError: ${e.message}`;
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
console.error(`[Agent] Tool not found: ${tc.function.name}`);
|
|
1173
|
+
resultStr = `ToolExecutionError: Tool ${tc.function.name} not found`;
|
|
1174
|
+
}
|
|
1175
|
+
messages.push({
|
|
1176
|
+
role: "tool",
|
|
1177
|
+
content: resultStr,
|
|
1178
|
+
tool_call_id: tc.id
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
} else {
|
|
1182
|
+
finalContent = response.choices?.[0]?.message?.content || response.message?.content || response.content || "";
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (this.config.memory && memoryReq) {
|
|
1187
|
+
await this.config.memory.save(memoryReq.resourceId, memoryReq.threadId, "user", userPrompt);
|
|
1188
|
+
await this.config.memory.save(memoryReq.resourceId, memoryReq.threadId, "assistant", finalContent);
|
|
1189
|
+
}
|
|
1190
|
+
if (toolCallsLog.length > 0) {
|
|
1191
|
+
return {
|
|
1192
|
+
content: finalContent,
|
|
1193
|
+
toolCalls: toolCallsLog
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
return finalContent;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
function createAgent(config) {
|
|
1200
|
+
return new Agent(config);
|
|
1201
|
+
}
|
|
1202
|
+
export {
|
|
1203
|
+
Agent,
|
|
1204
|
+
memory_exports as Memory,
|
|
1205
|
+
completion,
|
|
1206
|
+
createAgent,
|
|
1207
|
+
fileMemory,
|
|
1208
|
+
inMemory,
|
|
1209
|
+
router,
|
|
1210
|
+
verbose
|
|
1211
|
+
};
|