@elsium-ai/core 0.1.6
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/circuit-breaker.d.ts +17 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/dedup.d.ts +14 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +987 -0
- package/dist/logger.d.ts +22 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/policy.d.ts +37 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/result.d.ts +18 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/shutdown.d.ts +16 -0
- package/dist/shutdown.d.ts.map +1 -0
- package/dist/stream.d.ts +26 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/types.d.ts +160 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/errors.ts
|
|
3
|
+
class ElsiumError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
provider;
|
|
6
|
+
model;
|
|
7
|
+
statusCode;
|
|
8
|
+
retryable;
|
|
9
|
+
retryAfterMs;
|
|
10
|
+
cause;
|
|
11
|
+
metadata;
|
|
12
|
+
constructor(details) {
|
|
13
|
+
super(details.message);
|
|
14
|
+
this.name = "ElsiumError";
|
|
15
|
+
this.code = details.code;
|
|
16
|
+
this.provider = details.provider;
|
|
17
|
+
this.model = details.model;
|
|
18
|
+
this.statusCode = details.statusCode;
|
|
19
|
+
this.retryable = details.retryable;
|
|
20
|
+
this.retryAfterMs = details.retryAfterMs;
|
|
21
|
+
this.cause = details.cause;
|
|
22
|
+
this.metadata = details.metadata;
|
|
23
|
+
}
|
|
24
|
+
toJSON() {
|
|
25
|
+
return {
|
|
26
|
+
name: this.name,
|
|
27
|
+
code: this.code,
|
|
28
|
+
message: this.message,
|
|
29
|
+
provider: this.provider,
|
|
30
|
+
model: this.model,
|
|
31
|
+
statusCode: this.statusCode,
|
|
32
|
+
retryable: this.retryable,
|
|
33
|
+
retryAfterMs: this.retryAfterMs,
|
|
34
|
+
metadata: this.metadata
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
static providerError(message, opts) {
|
|
38
|
+
return new ElsiumError({
|
|
39
|
+
code: "PROVIDER_ERROR",
|
|
40
|
+
message,
|
|
41
|
+
provider: opts.provider,
|
|
42
|
+
statusCode: opts.statusCode,
|
|
43
|
+
retryable: opts.retryable ?? false,
|
|
44
|
+
cause: opts.cause
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
static rateLimit(provider, retryAfterMs) {
|
|
48
|
+
return new ElsiumError({
|
|
49
|
+
code: "RATE_LIMIT",
|
|
50
|
+
message: `Rate limited by ${provider}`,
|
|
51
|
+
provider,
|
|
52
|
+
statusCode: 429,
|
|
53
|
+
retryable: true,
|
|
54
|
+
retryAfterMs
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
static authError(provider) {
|
|
58
|
+
return new ElsiumError({
|
|
59
|
+
code: "AUTH_ERROR",
|
|
60
|
+
message: `Authentication failed for ${provider}. Check your API key.`,
|
|
61
|
+
provider,
|
|
62
|
+
statusCode: 401,
|
|
63
|
+
retryable: false
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
static timeout(provider, timeoutMs) {
|
|
67
|
+
return new ElsiumError({
|
|
68
|
+
code: "TIMEOUT",
|
|
69
|
+
message: `Request to ${provider} timed out after ${timeoutMs}ms`,
|
|
70
|
+
provider,
|
|
71
|
+
retryable: true
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
static validation(message, metadata) {
|
|
75
|
+
return new ElsiumError({
|
|
76
|
+
code: "VALIDATION_ERROR",
|
|
77
|
+
message,
|
|
78
|
+
retryable: false,
|
|
79
|
+
metadata
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
static budgetExceeded(spent, budget) {
|
|
83
|
+
return new ElsiumError({
|
|
84
|
+
code: "BUDGET_EXCEEDED",
|
|
85
|
+
message: `Token budget exceeded: spent ${spent}, budget ${budget}`,
|
|
86
|
+
retryable: false,
|
|
87
|
+
metadata: { spent, budget }
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// src/result.ts
|
|
92
|
+
function ok(value) {
|
|
93
|
+
return { ok: true, value };
|
|
94
|
+
}
|
|
95
|
+
function err(error) {
|
|
96
|
+
return { ok: false, error };
|
|
97
|
+
}
|
|
98
|
+
function isOk(result) {
|
|
99
|
+
return result.ok;
|
|
100
|
+
}
|
|
101
|
+
function isErr(result) {
|
|
102
|
+
return !result.ok;
|
|
103
|
+
}
|
|
104
|
+
function unwrap(result) {
|
|
105
|
+
if (result.ok)
|
|
106
|
+
return result.value;
|
|
107
|
+
throw result.error instanceof Error ? result.error : new Error(String(result.error));
|
|
108
|
+
}
|
|
109
|
+
function unwrapOr(result, fallback) {
|
|
110
|
+
return result.ok ? result.value : fallback;
|
|
111
|
+
}
|
|
112
|
+
async function tryCatch(fn) {
|
|
113
|
+
try {
|
|
114
|
+
return ok(await fn());
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return err(e instanceof Error ? e : new Error(String(e)));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function tryCatchSync(fn) {
|
|
120
|
+
try {
|
|
121
|
+
return ok(fn());
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return err(e instanceof Error ? e : new Error(String(e)));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// src/utils.ts
|
|
127
|
+
import { randomBytes } from "crypto";
|
|
128
|
+
function cryptoHex(bytes) {
|
|
129
|
+
return randomBytes(bytes).toString("hex");
|
|
130
|
+
}
|
|
131
|
+
function generateId(prefix = "els") {
|
|
132
|
+
const timestamp = Date.now().toString(36);
|
|
133
|
+
const random = cryptoHex(4);
|
|
134
|
+
return `${prefix}_${timestamp}_${random}`;
|
|
135
|
+
}
|
|
136
|
+
function generateTraceId() {
|
|
137
|
+
const timestamp = Date.now().toString(36);
|
|
138
|
+
const random = cryptoHex(6);
|
|
139
|
+
return `trc_${timestamp}_${random}`;
|
|
140
|
+
}
|
|
141
|
+
function extractText(content) {
|
|
142
|
+
if (typeof content === "string")
|
|
143
|
+
return content;
|
|
144
|
+
return content.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("");
|
|
145
|
+
}
|
|
146
|
+
async function sleep(ms) {
|
|
147
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
148
|
+
}
|
|
149
|
+
function getRetryDelay(error, attempt, baseDelayMs, maxDelayMs) {
|
|
150
|
+
if (error && typeof error === "object" && "retryAfterMs" in error && typeof error.retryAfterMs === "number") {
|
|
151
|
+
return error.retryAfterMs;
|
|
152
|
+
}
|
|
153
|
+
return Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
154
|
+
}
|
|
155
|
+
function retry(fn, options = {}) {
|
|
156
|
+
const {
|
|
157
|
+
maxRetries = 3,
|
|
158
|
+
baseDelayMs = 1000,
|
|
159
|
+
maxDelayMs = 30000,
|
|
160
|
+
shouldRetry = (error) => {
|
|
161
|
+
if (error && typeof error === "object" && "retryable" in error) {
|
|
162
|
+
return error.retryable === true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
} = options;
|
|
167
|
+
return (async () => {
|
|
168
|
+
let lastError;
|
|
169
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
170
|
+
try {
|
|
171
|
+
return await fn();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
lastError = error;
|
|
174
|
+
if (attempt === maxRetries || !shouldRetry(error)) {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
const delay = getRetryDelay(error, attempt, baseDelayMs, maxDelayMs);
|
|
178
|
+
const jitter = delay * (0.5 + Math.random() * 0.5);
|
|
179
|
+
await sleep(jitter);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
throw lastError;
|
|
183
|
+
})();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/stream.ts
|
|
187
|
+
function shouldEmitCheckpoint(lastCheckpointTime, intervalMs, textLength) {
|
|
188
|
+
const elapsed = Date.now() - lastCheckpointTime;
|
|
189
|
+
return elapsed >= intervalMs && textLength > 0;
|
|
190
|
+
}
|
|
191
|
+
function createCheckpoint(textAccumulator, eventIndex, now) {
|
|
192
|
+
return {
|
|
193
|
+
id: generateId("ckpt"),
|
|
194
|
+
timestamp: now,
|
|
195
|
+
text: textAccumulator,
|
|
196
|
+
tokensSoFar: Math.ceil(textAccumulator.length / 1.5),
|
|
197
|
+
eventIndex
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function toError(err2) {
|
|
201
|
+
return err2 instanceof Error ? err2 : new Error(String(err2));
|
|
202
|
+
}
|
|
203
|
+
function* emitErrorEvent(err2, textAccumulator, onPartialRecovery) {
|
|
204
|
+
const error = toError(err2);
|
|
205
|
+
if (textAccumulator.length > 0) {
|
|
206
|
+
onPartialRecovery?.(textAccumulator, error);
|
|
207
|
+
yield { type: "recovery", partialText: textAccumulator, error };
|
|
208
|
+
} else {
|
|
209
|
+
yield { type: "error", error };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class ElsiumStream {
|
|
214
|
+
source;
|
|
215
|
+
iterating = false;
|
|
216
|
+
constructor(source) {
|
|
217
|
+
this.source = source;
|
|
218
|
+
}
|
|
219
|
+
async* [Symbol.asyncIterator]() {
|
|
220
|
+
if (this.iterating) {
|
|
221
|
+
throw new Error("ElsiumStream supports only a single consumer");
|
|
222
|
+
}
|
|
223
|
+
this.iterating = true;
|
|
224
|
+
yield* this.source;
|
|
225
|
+
}
|
|
226
|
+
text() {
|
|
227
|
+
const source = this.source;
|
|
228
|
+
return {
|
|
229
|
+
async* [Symbol.asyncIterator]() {
|
|
230
|
+
for await (const event of source) {
|
|
231
|
+
if (event.type === "text_delta") {
|
|
232
|
+
yield event.text;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
async toText() {
|
|
239
|
+
const parts = [];
|
|
240
|
+
for await (const text of this.text()) {
|
|
241
|
+
parts.push(text);
|
|
242
|
+
}
|
|
243
|
+
return parts.join("");
|
|
244
|
+
}
|
|
245
|
+
async toTextWithTimeout(timeoutMs) {
|
|
246
|
+
const parts = [];
|
|
247
|
+
const deadline = Date.now() + timeoutMs;
|
|
248
|
+
const iterator = this.source[Symbol.asyncIterator]();
|
|
249
|
+
try {
|
|
250
|
+
while (true) {
|
|
251
|
+
const remaining = deadline - Date.now();
|
|
252
|
+
if (remaining <= 0)
|
|
253
|
+
break;
|
|
254
|
+
let timer;
|
|
255
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
256
|
+
timer = setTimeout(() => resolve({ value: undefined, done: true }), remaining);
|
|
257
|
+
});
|
|
258
|
+
const result = await Promise.race([iterator.next(), timeoutPromise]);
|
|
259
|
+
if (timer !== undefined)
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
if (result.done)
|
|
262
|
+
break;
|
|
263
|
+
const event = result.value;
|
|
264
|
+
if (event.type === "text_delta") {
|
|
265
|
+
parts.push(event.text);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch (err2) {
|
|
269
|
+
if (parts.length === 0)
|
|
270
|
+
throw err2;
|
|
271
|
+
} finally {
|
|
272
|
+
await iterator.return?.();
|
|
273
|
+
}
|
|
274
|
+
return parts.join("");
|
|
275
|
+
}
|
|
276
|
+
async toResponse() {
|
|
277
|
+
const parts = [];
|
|
278
|
+
let usage = null;
|
|
279
|
+
let stopReason = null;
|
|
280
|
+
for await (const event of this.source) {
|
|
281
|
+
switch (event.type) {
|
|
282
|
+
case "text_delta":
|
|
283
|
+
parts.push(event.text);
|
|
284
|
+
break;
|
|
285
|
+
case "message_end":
|
|
286
|
+
usage = event.usage;
|
|
287
|
+
stopReason = event.stopReason;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { text: parts.join(""), usage, stopReason };
|
|
292
|
+
}
|
|
293
|
+
pipe(transform) {
|
|
294
|
+
return new ElsiumStream(transform(this.source));
|
|
295
|
+
}
|
|
296
|
+
resilient(options = {}) {
|
|
297
|
+
const { checkpointIntervalMs = 1000, onCheckpoint, onPartialRecovery } = options;
|
|
298
|
+
const source = this.source;
|
|
299
|
+
const resilientSource = {
|
|
300
|
+
async* [Symbol.asyncIterator]() {
|
|
301
|
+
let lastCheckpointTime = Date.now();
|
|
302
|
+
let textAccumulator = "";
|
|
303
|
+
let eventIndex = 0;
|
|
304
|
+
try {
|
|
305
|
+
for await (const event of source) {
|
|
306
|
+
eventIndex++;
|
|
307
|
+
if (event.type === "text_delta") {
|
|
308
|
+
textAccumulator += event.text;
|
|
309
|
+
}
|
|
310
|
+
yield event;
|
|
311
|
+
if (shouldEmitCheckpoint(lastCheckpointTime, checkpointIntervalMs, textAccumulator.length)) {
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
const checkpoint = createCheckpoint(textAccumulator, eventIndex, now);
|
|
314
|
+
onCheckpoint?.(checkpoint);
|
|
315
|
+
yield { type: "checkpoint", checkpoint };
|
|
316
|
+
lastCheckpointTime = now;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch (err2) {
|
|
320
|
+
yield* emitErrorEvent(err2, textAccumulator, onPartialRecovery);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
return new ElsiumStream(resilientSource);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
var MAX_BUFFER_SIZE = 1e4;
|
|
328
|
+
function createStream(executor) {
|
|
329
|
+
let resolve = null;
|
|
330
|
+
const buffer = [];
|
|
331
|
+
let done = false;
|
|
332
|
+
let error = null;
|
|
333
|
+
let dropped = 0;
|
|
334
|
+
const source = {
|
|
335
|
+
[Symbol.asyncIterator]() {
|
|
336
|
+
return {
|
|
337
|
+
next() {
|
|
338
|
+
if (buffer.length > 0) {
|
|
339
|
+
const value = buffer.shift();
|
|
340
|
+
return Promise.resolve({ value, done: false });
|
|
341
|
+
}
|
|
342
|
+
if (done) {
|
|
343
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
344
|
+
}
|
|
345
|
+
if (error) {
|
|
346
|
+
return Promise.reject(error);
|
|
347
|
+
}
|
|
348
|
+
return new Promise((r) => {
|
|
349
|
+
resolve = r;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const emit = (event) => {
|
|
356
|
+
if (resolve) {
|
|
357
|
+
const r = resolve;
|
|
358
|
+
resolve = null;
|
|
359
|
+
r({ value: event, done: false });
|
|
360
|
+
} else {
|
|
361
|
+
if (buffer.length < MAX_BUFFER_SIZE) {
|
|
362
|
+
buffer.push(event);
|
|
363
|
+
} else {
|
|
364
|
+
dropped++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
executor(emit).then(() => {
|
|
369
|
+
if (dropped > 0) {
|
|
370
|
+
emit({
|
|
371
|
+
type: "error",
|
|
372
|
+
error: new Error(`Stream buffer overflow: ${dropped} events dropped`)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
done = true;
|
|
376
|
+
if (resolve) {
|
|
377
|
+
const r = resolve;
|
|
378
|
+
resolve = null;
|
|
379
|
+
r({ value: undefined, done: true });
|
|
380
|
+
}
|
|
381
|
+
}).catch((e) => {
|
|
382
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
383
|
+
if (resolve) {
|
|
384
|
+
resolve({ value: { type: "error", error }, done: false });
|
|
385
|
+
resolve = null;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
return new ElsiumStream(source);
|
|
389
|
+
}
|
|
390
|
+
// src/logger.ts
|
|
391
|
+
var LOG_LEVELS = {
|
|
392
|
+
debug: 0,
|
|
393
|
+
info: 1,
|
|
394
|
+
warn: 2,
|
|
395
|
+
error: 3
|
|
396
|
+
};
|
|
397
|
+
function createLogger(options = {}) {
|
|
398
|
+
const { level = "info", pretty = false, context = {} } = options;
|
|
399
|
+
const minLevel = LOG_LEVELS[level];
|
|
400
|
+
function log(logLevel, message, data) {
|
|
401
|
+
if (LOG_LEVELS[logLevel] < minLevel)
|
|
402
|
+
return;
|
|
403
|
+
const entry = {
|
|
404
|
+
...context,
|
|
405
|
+
level: logLevel,
|
|
406
|
+
message,
|
|
407
|
+
timestamp: new Date().toISOString(),
|
|
408
|
+
...data ? { data } : {}
|
|
409
|
+
};
|
|
410
|
+
const output = pretty ? JSON.stringify(entry, null, 2) : JSON.stringify(entry);
|
|
411
|
+
if (logLevel === "error") {
|
|
412
|
+
console.error(output);
|
|
413
|
+
} else if (logLevel === "warn") {
|
|
414
|
+
console.warn(output);
|
|
415
|
+
} else {
|
|
416
|
+
console.log(output);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
debug: (msg, data) => log("debug", msg, data),
|
|
421
|
+
info: (msg, data) => log("info", msg, data),
|
|
422
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
423
|
+
error: (msg, data) => log("error", msg, data),
|
|
424
|
+
child(childContext) {
|
|
425
|
+
return createLogger({
|
|
426
|
+
level,
|
|
427
|
+
pretty,
|
|
428
|
+
context: { ...context, ...childContext }
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// src/config.ts
|
|
434
|
+
function getEnvVar(name) {
|
|
435
|
+
const value = process.env[name];
|
|
436
|
+
if (value === undefined || value === "undefined")
|
|
437
|
+
return;
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
function env(name, fallback) {
|
|
441
|
+
const value = getEnvVar(name);
|
|
442
|
+
if (value !== undefined)
|
|
443
|
+
return value;
|
|
444
|
+
if (fallback !== undefined)
|
|
445
|
+
return fallback;
|
|
446
|
+
throw new ElsiumError({
|
|
447
|
+
code: "CONFIG_ERROR",
|
|
448
|
+
message: `Missing required environment variable: ${name}`,
|
|
449
|
+
retryable: false,
|
|
450
|
+
metadata: { variable: name }
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
function envNumber(name, fallback) {
|
|
454
|
+
const raw = getEnvVar(name);
|
|
455
|
+
if (raw !== undefined) {
|
|
456
|
+
const parsed = Number(raw);
|
|
457
|
+
if (!Number.isFinite(parsed)) {
|
|
458
|
+
throw new ElsiumError({
|
|
459
|
+
code: "CONFIG_ERROR",
|
|
460
|
+
message: `Environment variable ${name} is not a valid finite number: ${raw}`,
|
|
461
|
+
retryable: false,
|
|
462
|
+
metadata: { variable: name, value: raw }
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return parsed;
|
|
466
|
+
}
|
|
467
|
+
if (fallback !== undefined)
|
|
468
|
+
return fallback;
|
|
469
|
+
throw new ElsiumError({
|
|
470
|
+
code: "CONFIG_ERROR",
|
|
471
|
+
message: `Missing required environment variable: ${name}`,
|
|
472
|
+
retryable: false,
|
|
473
|
+
metadata: { variable: name }
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function envBool(name, fallback) {
|
|
477
|
+
const raw = getEnvVar(name);
|
|
478
|
+
if (raw !== undefined) {
|
|
479
|
+
const normalized = raw.toLowerCase();
|
|
480
|
+
return normalized === "true" || normalized === "1" || normalized === "yes";
|
|
481
|
+
}
|
|
482
|
+
if (fallback !== undefined)
|
|
483
|
+
return fallback;
|
|
484
|
+
throw new ElsiumError({
|
|
485
|
+
code: "CONFIG_ERROR",
|
|
486
|
+
message: `Missing required environment variable: ${name}`,
|
|
487
|
+
retryable: false,
|
|
488
|
+
metadata: { variable: name }
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
// src/circuit-breaker.ts
|
|
492
|
+
function defaultShouldCount(error) {
|
|
493
|
+
if (error && typeof error === "object" && "retryable" in error) {
|
|
494
|
+
return error.retryable === true;
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
function createCircuitBreaker(config) {
|
|
499
|
+
const failureThreshold = config?.failureThreshold ?? 5;
|
|
500
|
+
const resetTimeoutMs = config?.resetTimeoutMs ?? 30000;
|
|
501
|
+
const halfOpenMaxAttempts = config?.halfOpenMaxAttempts ?? 3;
|
|
502
|
+
const windowMs = config?.windowMs ?? 60000;
|
|
503
|
+
if (failureThreshold < 1 || !Number.isFinite(failureThreshold)) {
|
|
504
|
+
throw new ElsiumError({
|
|
505
|
+
code: "CONFIG_ERROR",
|
|
506
|
+
message: "failureThreshold must be >= 1",
|
|
507
|
+
retryable: false
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (resetTimeoutMs < 0 || !Number.isFinite(resetTimeoutMs)) {
|
|
511
|
+
throw new ElsiumError({
|
|
512
|
+
code: "CONFIG_ERROR",
|
|
513
|
+
message: "resetTimeoutMs must be >= 0 and finite",
|
|
514
|
+
retryable: false
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (halfOpenMaxAttempts < 1 || !Number.isFinite(halfOpenMaxAttempts)) {
|
|
518
|
+
throw new ElsiumError({
|
|
519
|
+
code: "CONFIG_ERROR",
|
|
520
|
+
message: "halfOpenMaxAttempts must be >= 1",
|
|
521
|
+
retryable: false
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (windowMs < 0 || !Number.isFinite(windowMs)) {
|
|
525
|
+
throw new ElsiumError({
|
|
526
|
+
code: "CONFIG_ERROR",
|
|
527
|
+
message: "windowMs must be >= 0 and finite",
|
|
528
|
+
retryable: false
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const onStateChange = config?.onStateChange;
|
|
532
|
+
const shouldCount = config?.shouldCount ?? defaultShouldCount;
|
|
533
|
+
let currentState = "closed";
|
|
534
|
+
let failureTimestamps = [];
|
|
535
|
+
let lastOpenedAt = 0;
|
|
536
|
+
let halfOpenAttempts = 0;
|
|
537
|
+
let halfOpenInFlight = 0;
|
|
538
|
+
function transition(to) {
|
|
539
|
+
if (currentState === to)
|
|
540
|
+
return;
|
|
541
|
+
const from = currentState;
|
|
542
|
+
currentState = to;
|
|
543
|
+
onStateChange?.(from, to);
|
|
544
|
+
}
|
|
545
|
+
function recordFailure() {
|
|
546
|
+
const now = Date.now();
|
|
547
|
+
failureTimestamps.push(now);
|
|
548
|
+
failureTimestamps = failureTimestamps.filter((t) => now - t < windowMs);
|
|
549
|
+
if (failureTimestamps.length >= failureThreshold) {
|
|
550
|
+
lastOpenedAt = now;
|
|
551
|
+
halfOpenAttempts = 0;
|
|
552
|
+
transition("open");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function recordSuccess() {
|
|
556
|
+
if (currentState === "half-open") {
|
|
557
|
+
failureTimestamps = [];
|
|
558
|
+
halfOpenAttempts = 0;
|
|
559
|
+
halfOpenInFlight = 0;
|
|
560
|
+
transition("closed");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
get state() {
|
|
565
|
+
if (currentState === "open" && Date.now() - lastOpenedAt >= resetTimeoutMs) {
|
|
566
|
+
transition("half-open");
|
|
567
|
+
}
|
|
568
|
+
return currentState;
|
|
569
|
+
},
|
|
570
|
+
get failureCount() {
|
|
571
|
+
const now = Date.now();
|
|
572
|
+
return failureTimestamps.filter((t) => now - t < windowMs).length;
|
|
573
|
+
},
|
|
574
|
+
async execute(fn) {
|
|
575
|
+
const state = this.state;
|
|
576
|
+
if (state === "open") {
|
|
577
|
+
throw new ElsiumError({
|
|
578
|
+
code: "PROVIDER_ERROR",
|
|
579
|
+
message: "Circuit breaker is open",
|
|
580
|
+
retryable: true
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (state === "half-open" && halfOpenInFlight >= halfOpenMaxAttempts) {
|
|
584
|
+
lastOpenedAt = Date.now();
|
|
585
|
+
transition("open");
|
|
586
|
+
throw new ElsiumError({
|
|
587
|
+
code: "PROVIDER_ERROR",
|
|
588
|
+
message: "Circuit breaker is open",
|
|
589
|
+
retryable: true
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (state === "half-open") {
|
|
593
|
+
halfOpenAttempts++;
|
|
594
|
+
halfOpenInFlight++;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const result = await fn();
|
|
598
|
+
recordSuccess();
|
|
599
|
+
return result;
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (shouldCount(error)) {
|
|
602
|
+
recordFailure();
|
|
603
|
+
}
|
|
604
|
+
throw error;
|
|
605
|
+
} finally {
|
|
606
|
+
if (state === "half-open") {
|
|
607
|
+
halfOpenInFlight = Math.max(0, halfOpenInFlight - 1);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
reset() {
|
|
612
|
+
failureTimestamps = [];
|
|
613
|
+
halfOpenAttempts = 0;
|
|
614
|
+
halfOpenInFlight = 0;
|
|
615
|
+
transition("closed");
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
// src/dedup.ts
|
|
620
|
+
import { createHash } from "crypto";
|
|
621
|
+
function createDedup(config) {
|
|
622
|
+
const ttlMs = config?.ttlMs ?? 5000;
|
|
623
|
+
const maxEntries = config?.maxEntries ?? 1000;
|
|
624
|
+
if (ttlMs < 0 || !Number.isFinite(ttlMs)) {
|
|
625
|
+
throw new ElsiumError({
|
|
626
|
+
code: "CONFIG_ERROR",
|
|
627
|
+
message: "ttlMs must be >= 0 and finite",
|
|
628
|
+
retryable: false
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (maxEntries < 1 || !Number.isFinite(maxEntries)) {
|
|
632
|
+
throw new ElsiumError({
|
|
633
|
+
code: "CONFIG_ERROR",
|
|
634
|
+
message: "maxEntries must be >= 1",
|
|
635
|
+
retryable: false
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
const inFlight = new Map;
|
|
639
|
+
const cache = new Map;
|
|
640
|
+
function evictExpired() {
|
|
641
|
+
const now = Date.now();
|
|
642
|
+
for (const [key, entry] of cache) {
|
|
643
|
+
if (now >= entry.expiresAt) {
|
|
644
|
+
cache.delete(key);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function enforceSizeLimit() {
|
|
649
|
+
if (cache.size <= maxEntries)
|
|
650
|
+
return;
|
|
651
|
+
const oldest = cache.keys().next().value;
|
|
652
|
+
if (oldest !== undefined)
|
|
653
|
+
cache.delete(oldest);
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
async deduplicate(key, fn) {
|
|
657
|
+
const cached = cache.get(key);
|
|
658
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
659
|
+
return cached.value;
|
|
660
|
+
}
|
|
661
|
+
const existing = inFlight.get(key);
|
|
662
|
+
if (existing) {
|
|
663
|
+
return existing;
|
|
664
|
+
}
|
|
665
|
+
const promise = fn().then((result) => {
|
|
666
|
+
inFlight.delete(key);
|
|
667
|
+
cache.set(key, { value: result, expiresAt: Date.now() + ttlMs });
|
|
668
|
+
enforceSizeLimit();
|
|
669
|
+
return result;
|
|
670
|
+
}, (error) => {
|
|
671
|
+
inFlight.delete(key);
|
|
672
|
+
throw error;
|
|
673
|
+
});
|
|
674
|
+
inFlight.set(key, promise);
|
|
675
|
+
return promise;
|
|
676
|
+
},
|
|
677
|
+
hashRequest(request) {
|
|
678
|
+
const sorted = JSON.stringify(request, (_key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value);
|
|
679
|
+
return createHash("sha256").update(sorted).digest("hex").slice(0, 16);
|
|
680
|
+
},
|
|
681
|
+
get size() {
|
|
682
|
+
evictExpired();
|
|
683
|
+
return cache.size + inFlight.size;
|
|
684
|
+
},
|
|
685
|
+
clear() {
|
|
686
|
+
inFlight.clear();
|
|
687
|
+
cache.clear();
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function dedupMiddleware(config) {
|
|
692
|
+
const dedup = createDedup(config);
|
|
693
|
+
return async (ctx, next) => {
|
|
694
|
+
const key = dedup.hashRequest({
|
|
695
|
+
messages: ctx.request.messages,
|
|
696
|
+
model: ctx.model,
|
|
697
|
+
provider: ctx.provider,
|
|
698
|
+
system: ctx.request.system,
|
|
699
|
+
temperature: ctx.request.temperature,
|
|
700
|
+
seed: ctx.request.seed,
|
|
701
|
+
maxTokens: ctx.request.maxTokens,
|
|
702
|
+
topP: ctx.request.topP,
|
|
703
|
+
stopSequences: ctx.request.stopSequences,
|
|
704
|
+
tools: ctx.request.tools
|
|
705
|
+
});
|
|
706
|
+
return dedup.deduplicate(key, () => next(ctx));
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
// src/policy.ts
|
|
710
|
+
function createPolicySet(policies) {
|
|
711
|
+
const policyList = [...policies];
|
|
712
|
+
return {
|
|
713
|
+
evaluate(ctx) {
|
|
714
|
+
const denials = [];
|
|
715
|
+
for (const policy of policyList) {
|
|
716
|
+
const mode = policy.mode ?? "all-must-pass";
|
|
717
|
+
const results = policy.rules.map((rule) => rule(ctx));
|
|
718
|
+
if (mode === "all-must-pass") {
|
|
719
|
+
const denied = results.filter((r) => r.decision === "deny");
|
|
720
|
+
denials.push(...denied);
|
|
721
|
+
} else {
|
|
722
|
+
const anyAllowed = results.some((r) => r.decision === "allow");
|
|
723
|
+
if (!anyAllowed && results.length > 0) {
|
|
724
|
+
denials.push(results[0]);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return denials;
|
|
729
|
+
},
|
|
730
|
+
addPolicy(policy) {
|
|
731
|
+
policyList.push(policy);
|
|
732
|
+
},
|
|
733
|
+
removePolicy(name) {
|
|
734
|
+
const idx = policyList.findIndex((p) => p.name === name);
|
|
735
|
+
if (idx !== -1)
|
|
736
|
+
policyList.splice(idx, 1);
|
|
737
|
+
},
|
|
738
|
+
get policies() {
|
|
739
|
+
return policyList.map((p) => p.name);
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function policyMiddleware(policySet) {
|
|
744
|
+
return async (ctx, next) => {
|
|
745
|
+
const requestContent = ctx.request.messages.map((m) => extractText(m.content)).join(`
|
|
746
|
+
`);
|
|
747
|
+
const tokenCount = Math.ceil(requestContent.length / 4);
|
|
748
|
+
const policyCtx = {
|
|
749
|
+
model: ctx.model,
|
|
750
|
+
provider: ctx.provider,
|
|
751
|
+
metadata: ctx.metadata,
|
|
752
|
+
requestContent,
|
|
753
|
+
tokenCount
|
|
754
|
+
};
|
|
755
|
+
const denials = policySet.evaluate(policyCtx);
|
|
756
|
+
if (denials.length > 0) {
|
|
757
|
+
throw ElsiumError.validation(`Policy denied: ${denials.map((d) => `[${d.policyName}] ${d.reason}`).join("; ")}`);
|
|
758
|
+
}
|
|
759
|
+
return next(ctx);
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function modelAccessPolicy(allowedModels) {
|
|
763
|
+
return {
|
|
764
|
+
name: "model-access",
|
|
765
|
+
description: "Restricts access to specific models",
|
|
766
|
+
rules: [
|
|
767
|
+
(ctx) => {
|
|
768
|
+
if (!ctx.model)
|
|
769
|
+
return { decision: "allow", reason: "No model specified", policyName: "model-access" };
|
|
770
|
+
const model = ctx.model;
|
|
771
|
+
const allowed = allowedModels.some((m) => {
|
|
772
|
+
if (m.endsWith("*"))
|
|
773
|
+
return model.startsWith(m.slice(0, -1));
|
|
774
|
+
return model === m;
|
|
775
|
+
});
|
|
776
|
+
return {
|
|
777
|
+
decision: allowed ? "allow" : "deny",
|
|
778
|
+
reason: allowed ? "Model is allowed" : `Model "${ctx.model}" is not in allowed list`,
|
|
779
|
+
policyName: "model-access"
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
]
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function tokenLimitPolicy(maxTokens) {
|
|
786
|
+
if (!Number.isFinite(maxTokens) || maxTokens < 0) {
|
|
787
|
+
throw ElsiumError.validation("tokenLimitPolicy: maxTokens must be >= 0 and finite");
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
name: "token-limit",
|
|
791
|
+
description: `Limits requests to ${maxTokens} tokens`,
|
|
792
|
+
rules: [
|
|
793
|
+
(ctx) => {
|
|
794
|
+
if (ctx.tokenCount === undefined) {
|
|
795
|
+
return {
|
|
796
|
+
decision: "allow",
|
|
797
|
+
reason: "No token count available",
|
|
798
|
+
policyName: "token-limit"
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const allowed = ctx.tokenCount <= maxTokens;
|
|
802
|
+
return {
|
|
803
|
+
decision: allowed ? "allow" : "deny",
|
|
804
|
+
reason: allowed ? "Within token limit" : `Token count ${ctx.tokenCount} exceeds limit ${maxTokens}`,
|
|
805
|
+
policyName: "token-limit"
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
]
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function costLimitPolicy(maxCost) {
|
|
812
|
+
if (!Number.isFinite(maxCost) || maxCost < 0) {
|
|
813
|
+
throw ElsiumError.validation("costLimitPolicy: maxCost must be >= 0 and finite");
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
name: "cost-limit",
|
|
817
|
+
description: `Limits requests to $${maxCost}`,
|
|
818
|
+
rules: [
|
|
819
|
+
(ctx) => {
|
|
820
|
+
if (ctx.costEstimate === undefined) {
|
|
821
|
+
return {
|
|
822
|
+
decision: "allow",
|
|
823
|
+
reason: "No cost estimate available",
|
|
824
|
+
policyName: "cost-limit"
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const allowed = ctx.costEstimate <= maxCost;
|
|
828
|
+
return {
|
|
829
|
+
decision: allowed ? "allow" : "deny",
|
|
830
|
+
reason: allowed ? "Within cost limit" : `Cost $${ctx.costEstimate} exceeds limit $${maxCost}`,
|
|
831
|
+
policyName: "cost-limit"
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
]
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function contentPolicy(blockedPatterns) {
|
|
838
|
+
return {
|
|
839
|
+
name: "content-policy",
|
|
840
|
+
description: "Blocks requests matching content patterns",
|
|
841
|
+
rules: [
|
|
842
|
+
(ctx) => {
|
|
843
|
+
if (!ctx.requestContent) {
|
|
844
|
+
return { decision: "allow", reason: "No content to check", policyName: "content-policy" };
|
|
845
|
+
}
|
|
846
|
+
for (const pattern of blockedPatterns) {
|
|
847
|
+
pattern.lastIndex = 0;
|
|
848
|
+
if (pattern.test(ctx.requestContent)) {
|
|
849
|
+
return {
|
|
850
|
+
decision: "deny",
|
|
851
|
+
reason: `Content matches blocked pattern: ${pattern.source}`,
|
|
852
|
+
policyName: "content-policy"
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return { decision: "allow", reason: "Content is clean", policyName: "content-policy" };
|
|
857
|
+
}
|
|
858
|
+
]
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
// src/shutdown.ts
|
|
862
|
+
function createShutdownManager(config) {
|
|
863
|
+
const drainTimeoutMs = config?.drainTimeoutMs ?? 30000;
|
|
864
|
+
const signals = config?.signals ?? ["SIGTERM", "SIGINT"];
|
|
865
|
+
if (drainTimeoutMs < 0 || !Number.isFinite(drainTimeoutMs)) {
|
|
866
|
+
throw new ElsiumError({
|
|
867
|
+
code: "CONFIG_ERROR",
|
|
868
|
+
message: "drainTimeoutMs must be >= 0 and finite",
|
|
869
|
+
retryable: false
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
let shuttingDown = false;
|
|
873
|
+
let inFlightCount = 0;
|
|
874
|
+
let drainResolve = null;
|
|
875
|
+
let shutdownPromise = null;
|
|
876
|
+
const signalHandlers = [];
|
|
877
|
+
function checkDrained() {
|
|
878
|
+
if (inFlightCount === 0 && drainResolve) {
|
|
879
|
+
drainResolve();
|
|
880
|
+
drainResolve = null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function shutdown() {
|
|
884
|
+
if (shutdownPromise)
|
|
885
|
+
return shutdownPromise;
|
|
886
|
+
shuttingDown = true;
|
|
887
|
+
shutdownPromise = (async () => {
|
|
888
|
+
config?.onDrainStart?.();
|
|
889
|
+
if (inFlightCount === 0) {
|
|
890
|
+
config?.onDrainComplete?.();
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const drainPromise = new Promise((resolve) => {
|
|
894
|
+
drainResolve = resolve;
|
|
895
|
+
});
|
|
896
|
+
let drainTimer;
|
|
897
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
898
|
+
drainTimer = setTimeout(() => resolve("timeout"), drainTimeoutMs);
|
|
899
|
+
});
|
|
900
|
+
const result = await Promise.race([
|
|
901
|
+
drainPromise.then(() => "drained"),
|
|
902
|
+
timeoutPromise
|
|
903
|
+
]);
|
|
904
|
+
if (drainTimer !== undefined)
|
|
905
|
+
clearTimeout(drainTimer);
|
|
906
|
+
if (result === "timeout") {
|
|
907
|
+
config?.onForceShutdown?.();
|
|
908
|
+
} else {
|
|
909
|
+
config?.onDrainComplete?.();
|
|
910
|
+
}
|
|
911
|
+
})();
|
|
912
|
+
return shutdownPromise;
|
|
913
|
+
}
|
|
914
|
+
const manager = {
|
|
915
|
+
get inFlight() {
|
|
916
|
+
return inFlightCount;
|
|
917
|
+
},
|
|
918
|
+
get isShuttingDown() {
|
|
919
|
+
return shuttingDown;
|
|
920
|
+
},
|
|
921
|
+
async trackOperation(fn) {
|
|
922
|
+
if (shuttingDown) {
|
|
923
|
+
throw new ElsiumError({
|
|
924
|
+
code: "VALIDATION_ERROR",
|
|
925
|
+
message: "Server is shutting down, not accepting new operations",
|
|
926
|
+
retryable: true
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
inFlightCount++;
|
|
930
|
+
try {
|
|
931
|
+
return await fn();
|
|
932
|
+
} finally {
|
|
933
|
+
inFlightCount--;
|
|
934
|
+
checkDrained();
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
shutdown,
|
|
938
|
+
dispose() {
|
|
939
|
+
for (const { signal, handler } of signalHandlers) {
|
|
940
|
+
process.removeListener(signal, handler);
|
|
941
|
+
}
|
|
942
|
+
signalHandlers.length = 0;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
if (typeof process !== "undefined" && process.on) {
|
|
946
|
+
for (const signal of signals) {
|
|
947
|
+
const handler = () => {
|
|
948
|
+
manager.shutdown();
|
|
949
|
+
};
|
|
950
|
+
signalHandlers.push({ signal, handler });
|
|
951
|
+
process.on(signal, handler);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return manager;
|
|
955
|
+
}
|
|
956
|
+
export {
|
|
957
|
+
unwrapOr,
|
|
958
|
+
unwrap,
|
|
959
|
+
tryCatchSync,
|
|
960
|
+
tryCatch,
|
|
961
|
+
tokenLimitPolicy,
|
|
962
|
+
sleep,
|
|
963
|
+
retry,
|
|
964
|
+
policyMiddleware,
|
|
965
|
+
ok,
|
|
966
|
+
modelAccessPolicy,
|
|
967
|
+
isOk,
|
|
968
|
+
isErr,
|
|
969
|
+
generateTraceId,
|
|
970
|
+
generateId,
|
|
971
|
+
extractText,
|
|
972
|
+
err,
|
|
973
|
+
envNumber,
|
|
974
|
+
envBool,
|
|
975
|
+
env,
|
|
976
|
+
dedupMiddleware,
|
|
977
|
+
createStream,
|
|
978
|
+
createShutdownManager,
|
|
979
|
+
createPolicySet,
|
|
980
|
+
createLogger,
|
|
981
|
+
createDedup,
|
|
982
|
+
createCircuitBreaker,
|
|
983
|
+
costLimitPolicy,
|
|
984
|
+
contentPolicy,
|
|
985
|
+
ElsiumStream,
|
|
986
|
+
ElsiumError
|
|
987
|
+
};
|