@elsium-ai/app 0.2.2 → 0.3.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/app.d.ts +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +770 -157
- package/dist/middleware.d.ts +3 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/sse.d.ts +6 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/tenant.d.ts +15 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -389,6 +389,189 @@ function createLogger(options = {}) {
|
|
|
389
389
|
}
|
|
390
390
|
};
|
|
391
391
|
}
|
|
392
|
+
// ../core/src/schema.ts
|
|
393
|
+
var log = createLogger();
|
|
394
|
+
function zodDefKind(def) {
|
|
395
|
+
return typeof def.type === "string" ? def.type : def.typeName;
|
|
396
|
+
}
|
|
397
|
+
function zodObjectToJsonSchema(schema, convert) {
|
|
398
|
+
const shape = typeof schema.shape === "function" ? schema.shape() : schema.shape;
|
|
399
|
+
const properties = {};
|
|
400
|
+
const required = [];
|
|
401
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
402
|
+
const fieldSchema = value;
|
|
403
|
+
properties[key] = convert(fieldSchema);
|
|
404
|
+
const fieldDef = fieldSchema._def;
|
|
405
|
+
const fieldKind = zodDefKind(fieldDef);
|
|
406
|
+
if (fieldKind !== "optional" && fieldKind !== "ZodOptional" && fieldKind !== "default" && fieldKind !== "ZodDefault") {
|
|
407
|
+
required.push(key);
|
|
408
|
+
}
|
|
409
|
+
if (fieldDef.description) {
|
|
410
|
+
properties[key].description = fieldDef.description;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return { type: "object", properties, required };
|
|
414
|
+
}
|
|
415
|
+
function zodToJsonSchema(schema) {
|
|
416
|
+
if (!("_def" in schema))
|
|
417
|
+
return { type: "object" };
|
|
418
|
+
const def = schema._def;
|
|
419
|
+
const kind = zodDefKind(def);
|
|
420
|
+
switch (kind) {
|
|
421
|
+
case "object":
|
|
422
|
+
case "ZodObject":
|
|
423
|
+
return zodObjectToJsonSchema(def, zodToJsonSchema);
|
|
424
|
+
case "string":
|
|
425
|
+
case "ZodString":
|
|
426
|
+
return { type: "string" };
|
|
427
|
+
case "number":
|
|
428
|
+
case "ZodNumber":
|
|
429
|
+
return { type: "number" };
|
|
430
|
+
case "boolean":
|
|
431
|
+
case "ZodBoolean":
|
|
432
|
+
return { type: "boolean" };
|
|
433
|
+
case "array":
|
|
434
|
+
case "ZodArray":
|
|
435
|
+
return {
|
|
436
|
+
type: "array",
|
|
437
|
+
items: zodToJsonSchema(def.element ?? def.type)
|
|
438
|
+
};
|
|
439
|
+
case "enum":
|
|
440
|
+
case "ZodEnum": {
|
|
441
|
+
const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
|
|
442
|
+
return { type: "string", enum: values };
|
|
443
|
+
}
|
|
444
|
+
case "optional":
|
|
445
|
+
case "ZodOptional":
|
|
446
|
+
return zodToJsonSchema(def.innerType);
|
|
447
|
+
case "default":
|
|
448
|
+
case "ZodDefault":
|
|
449
|
+
return zodToJsonSchema(def.innerType);
|
|
450
|
+
case "nullable":
|
|
451
|
+
case "ZodNullable": {
|
|
452
|
+
const inner = zodToJsonSchema(def.innerType);
|
|
453
|
+
return { ...inner, nullable: true };
|
|
454
|
+
}
|
|
455
|
+
case "ZodLiteral":
|
|
456
|
+
return { type: typeof def.value, const: def.value };
|
|
457
|
+
case "ZodUnion": {
|
|
458
|
+
const options = def.options.map(zodToJsonSchema);
|
|
459
|
+
return { anyOf: options };
|
|
460
|
+
}
|
|
461
|
+
case "ZodRecord":
|
|
462
|
+
return {
|
|
463
|
+
type: "object",
|
|
464
|
+
additionalProperties: def.valueType ? zodToJsonSchema(def.valueType) : { type: "string" }
|
|
465
|
+
};
|
|
466
|
+
case "ZodTuple": {
|
|
467
|
+
const items = (def.items ?? []).map(zodToJsonSchema);
|
|
468
|
+
return { type: "array", prefixItems: items, minItems: items.length, maxItems: items.length };
|
|
469
|
+
}
|
|
470
|
+
case "ZodDate":
|
|
471
|
+
return { type: "string", format: "date-time" };
|
|
472
|
+
default:
|
|
473
|
+
log.warn(`zodToJsonSchema: unsupported type ${kind}, defaulting to string`);
|
|
474
|
+
return { type: "string" };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// ../core/src/registry.ts
|
|
478
|
+
var log2 = createLogger();
|
|
479
|
+
var BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
480
|
+
// ../core/src/shutdown.ts
|
|
481
|
+
function createShutdownManager(config) {
|
|
482
|
+
const drainTimeoutMs = config?.drainTimeoutMs ?? 30000;
|
|
483
|
+
const signals = config?.signals ?? ["SIGTERM", "SIGINT"];
|
|
484
|
+
if (drainTimeoutMs < 0 || !Number.isFinite(drainTimeoutMs)) {
|
|
485
|
+
throw new ElsiumError({
|
|
486
|
+
code: "CONFIG_ERROR",
|
|
487
|
+
message: "drainTimeoutMs must be >= 0 and finite",
|
|
488
|
+
retryable: false
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
let shuttingDown = false;
|
|
492
|
+
let inFlightCount = 0;
|
|
493
|
+
let drainResolve = null;
|
|
494
|
+
let shutdownPromise = null;
|
|
495
|
+
const signalHandlers = [];
|
|
496
|
+
function checkDrained() {
|
|
497
|
+
if (inFlightCount === 0 && drainResolve) {
|
|
498
|
+
drainResolve();
|
|
499
|
+
drainResolve = null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function shutdown() {
|
|
503
|
+
if (shutdownPromise)
|
|
504
|
+
return shutdownPromise;
|
|
505
|
+
shuttingDown = true;
|
|
506
|
+
shutdownPromise = (async () => {
|
|
507
|
+
config?.onDrainStart?.();
|
|
508
|
+
if (inFlightCount === 0) {
|
|
509
|
+
config?.onDrainComplete?.();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const drainPromise = new Promise((resolve) => {
|
|
513
|
+
drainResolve = resolve;
|
|
514
|
+
});
|
|
515
|
+
let drainTimer;
|
|
516
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
517
|
+
drainTimer = setTimeout(() => resolve("timeout"), drainTimeoutMs);
|
|
518
|
+
});
|
|
519
|
+
const result = await Promise.race([
|
|
520
|
+
drainPromise.then(() => "drained"),
|
|
521
|
+
timeoutPromise
|
|
522
|
+
]);
|
|
523
|
+
if (drainTimer !== undefined)
|
|
524
|
+
clearTimeout(drainTimer);
|
|
525
|
+
if (result === "timeout") {
|
|
526
|
+
config?.onForceShutdown?.();
|
|
527
|
+
} else {
|
|
528
|
+
config?.onDrainComplete?.();
|
|
529
|
+
}
|
|
530
|
+
})();
|
|
531
|
+
return shutdownPromise;
|
|
532
|
+
}
|
|
533
|
+
const manager = {
|
|
534
|
+
get inFlight() {
|
|
535
|
+
return inFlightCount;
|
|
536
|
+
},
|
|
537
|
+
get isShuttingDown() {
|
|
538
|
+
return shuttingDown;
|
|
539
|
+
},
|
|
540
|
+
async trackOperation(fn) {
|
|
541
|
+
if (shuttingDown) {
|
|
542
|
+
throw new ElsiumError({
|
|
543
|
+
code: "VALIDATION_ERROR",
|
|
544
|
+
message: "Server is shutting down, not accepting new operations",
|
|
545
|
+
retryable: true
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
inFlightCount++;
|
|
549
|
+
try {
|
|
550
|
+
return await fn();
|
|
551
|
+
} finally {
|
|
552
|
+
inFlightCount--;
|
|
553
|
+
checkDrained();
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
shutdown,
|
|
557
|
+
dispose() {
|
|
558
|
+
for (const { signal, handler } of signalHandlers) {
|
|
559
|
+
process.removeListener(signal, handler);
|
|
560
|
+
}
|
|
561
|
+
signalHandlers.length = 0;
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
if (typeof process !== "undefined" && process.on) {
|
|
565
|
+
for (const signal of signals) {
|
|
566
|
+
const handler = () => {
|
|
567
|
+
manager.shutdown();
|
|
568
|
+
};
|
|
569
|
+
signalHandlers.push({ signal, handler });
|
|
570
|
+
process.on(signal, handler);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return manager;
|
|
574
|
+
}
|
|
392
575
|
// ../gateway/src/provider.ts
|
|
393
576
|
var providerRegistry = new Map;
|
|
394
577
|
var metadataRegistry = new Map;
|
|
@@ -520,7 +703,7 @@ function xrayMiddleware(options = {}) {
|
|
|
520
703
|
}
|
|
521
704
|
|
|
522
705
|
// ../gateway/src/pricing.ts
|
|
523
|
-
var
|
|
706
|
+
var log3 = createLogger();
|
|
524
707
|
var PRICING = {
|
|
525
708
|
"claude-opus-4-6": { inputPerMillion: 15, outputPerMillion: 75 },
|
|
526
709
|
"claude-sonnet-4-6": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
@@ -558,7 +741,7 @@ function resolveModelName(model) {
|
|
|
558
741
|
function calculateCost(model, usage) {
|
|
559
742
|
const pricing = PRICING[resolveModelName(model)];
|
|
560
743
|
if (!pricing) {
|
|
561
|
-
|
|
744
|
+
log3.warn(`Unknown model "${model}" — cost will be reported as $0. Register pricing with registerPricing().`);
|
|
562
745
|
return {
|
|
563
746
|
inputCost: 0,
|
|
564
747
|
outputCost: 0,
|
|
@@ -652,15 +835,33 @@ function createAnthropicProvider(config) {
|
|
|
652
835
|
if (part.type === "text")
|
|
653
836
|
return { type: "text", text: part.text };
|
|
654
837
|
if (part.type === "image" && part.source?.type === "base64") {
|
|
838
|
+
const src = part.source;
|
|
655
839
|
return {
|
|
656
840
|
type: "image",
|
|
657
841
|
source: {
|
|
658
842
|
type: "base64",
|
|
659
|
-
media_type:
|
|
660
|
-
data:
|
|
843
|
+
media_type: src.mediaType,
|
|
844
|
+
data: src.data
|
|
661
845
|
}
|
|
662
846
|
};
|
|
663
847
|
}
|
|
848
|
+
if (part.type === "document" && part.source) {
|
|
849
|
+
if (part.source.type === "base64") {
|
|
850
|
+
const src = part.source;
|
|
851
|
+
return {
|
|
852
|
+
type: "document",
|
|
853
|
+
source: {
|
|
854
|
+
type: "base64",
|
|
855
|
+
media_type: src.mediaType,
|
|
856
|
+
data: src.data
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return { type: "text", text: "[document: url source not supported by Anthropic]" };
|
|
861
|
+
}
|
|
862
|
+
if (part.type === "audio") {
|
|
863
|
+
return { type: "text", text: "[audio content not supported by this provider]" };
|
|
864
|
+
}
|
|
664
865
|
return { type: "text", text: "[unsupported content]" };
|
|
665
866
|
}
|
|
666
867
|
function formatMultipartContent(msg, role) {
|
|
@@ -769,6 +970,16 @@ function createAnthropicProvider(config) {
|
|
|
769
970
|
const tools = formatTools(req.tools);
|
|
770
971
|
if (tools)
|
|
771
972
|
body.tools = tools;
|
|
973
|
+
if (req.schema) {
|
|
974
|
+
const jsonSchema = zodToJsonSchema(req.schema);
|
|
975
|
+
const structuredTool = {
|
|
976
|
+
name: "_structured_output",
|
|
977
|
+
description: "Return structured output matching the required schema",
|
|
978
|
+
input_schema: jsonSchema
|
|
979
|
+
};
|
|
980
|
+
body.tools = [...tools ?? [], structuredTool];
|
|
981
|
+
body.tool_choice = { type: "tool", name: "_structured_output" };
|
|
982
|
+
}
|
|
772
983
|
const startTime = performance.now();
|
|
773
984
|
const raw = await retry(async () => {
|
|
774
985
|
const controller = new AbortController;
|
|
@@ -978,7 +1189,31 @@ function createGoogleProvider(config) {
|
|
|
978
1189
|
return { role, parts };
|
|
979
1190
|
}
|
|
980
1191
|
function formatGeminiMultipartContent(msg, role) {
|
|
981
|
-
const parts =
|
|
1192
|
+
const parts = [];
|
|
1193
|
+
for (const p of msg.content) {
|
|
1194
|
+
if (p.type === "text") {
|
|
1195
|
+
parts.push({ text: p.text });
|
|
1196
|
+
} else if (p.type === "image") {
|
|
1197
|
+
const img = p;
|
|
1198
|
+
if (img.source.type === "base64") {
|
|
1199
|
+
parts.push({ inlineData: { mimeType: img.source.mediaType, data: img.source.data } });
|
|
1200
|
+
} else {
|
|
1201
|
+
parts.push({ fileData: { mimeType: "image/jpeg", fileUri: img.source.url } });
|
|
1202
|
+
}
|
|
1203
|
+
} else if (p.type === "audio" || p.type === "document") {
|
|
1204
|
+
const media = p;
|
|
1205
|
+
if (media.source.type === "base64") {
|
|
1206
|
+
parts.push({
|
|
1207
|
+
inlineData: { mimeType: media.source.mediaType, data: media.source.data }
|
|
1208
|
+
});
|
|
1209
|
+
} else {
|
|
1210
|
+
const urlSource = media.source;
|
|
1211
|
+
parts.push({
|
|
1212
|
+
fileData: { mimeType: "application/octet-stream", fileUri: urlSource.url }
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
982
1217
|
return { role, parts };
|
|
983
1218
|
}
|
|
984
1219
|
function formatMessages(messages) {
|
|
@@ -1075,6 +1310,10 @@ function createGoogleProvider(config) {
|
|
|
1075
1310
|
config2.topP = req.topP;
|
|
1076
1311
|
if (req.stopSequences?.length)
|
|
1077
1312
|
config2.stopSequences = req.stopSequences;
|
|
1313
|
+
if (req.schema) {
|
|
1314
|
+
config2.responseMimeType = "application/json";
|
|
1315
|
+
config2.responseSchema = zodToJsonSchema(req.schema);
|
|
1316
|
+
}
|
|
1078
1317
|
return config2;
|
|
1079
1318
|
}
|
|
1080
1319
|
function buildRequestBody(req) {
|
|
@@ -1153,7 +1392,8 @@ async function handleGoogleErrorResponse(response) {
|
|
|
1153
1392
|
throw ElsiumError.authError("google");
|
|
1154
1393
|
}
|
|
1155
1394
|
if (response.status === 429) {
|
|
1156
|
-
|
|
1395
|
+
const retryAfter = response.headers.get("retry-after");
|
|
1396
|
+
throw ElsiumError.rateLimit("google", retryAfter ? Number.parseInt(retryAfter) * 1000 : undefined);
|
|
1157
1397
|
}
|
|
1158
1398
|
throw ElsiumError.providerError(`Google API error ${response.status}: ${errorBody}`, {
|
|
1159
1399
|
provider: "google",
|
|
@@ -1339,6 +1579,43 @@ function createOpenAIProvider(config) {
|
|
|
1339
1579
|
}
|
|
1340
1580
|
return openaiMsg;
|
|
1341
1581
|
}
|
|
1582
|
+
function formatUserContent(msg) {
|
|
1583
|
+
if (typeof msg.content === "string")
|
|
1584
|
+
return msg.content;
|
|
1585
|
+
const parts = [];
|
|
1586
|
+
for (const part of msg.content) {
|
|
1587
|
+
if (part.type === "text") {
|
|
1588
|
+
parts.push({ type: "text", text: part.text });
|
|
1589
|
+
} else if (part.type === "image") {
|
|
1590
|
+
if (part.source.type === "base64") {
|
|
1591
|
+
const url = `data:${part.source.mediaType};base64,${part.source.data}`;
|
|
1592
|
+
parts.push({ type: "image_url", image_url: { url } });
|
|
1593
|
+
} else {
|
|
1594
|
+
parts.push({ type: "image_url", image_url: { url: part.source.url } });
|
|
1595
|
+
}
|
|
1596
|
+
} else if (part.type === "audio") {
|
|
1597
|
+
if (part.source.type === "base64") {
|
|
1598
|
+
const format = part.source.mediaType.split("/")[1] ?? "wav";
|
|
1599
|
+
parts.push({
|
|
1600
|
+
type: "input_audio",
|
|
1601
|
+
input_audio: { data: part.source.data, format }
|
|
1602
|
+
});
|
|
1603
|
+
} else {
|
|
1604
|
+
parts.push({ type: "text", text: "[audio: url source requires file upload]" });
|
|
1605
|
+
}
|
|
1606
|
+
} else if (part.type === "document") {
|
|
1607
|
+
if (part.source.type === "base64") {
|
|
1608
|
+
parts.push({
|
|
1609
|
+
type: "text",
|
|
1610
|
+
text: `[document: ${part.source.mediaType} content attached as base64]`
|
|
1611
|
+
});
|
|
1612
|
+
} else {
|
|
1613
|
+
parts.push({ type: "text", text: `[document: ${part.source.url}]` });
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return parts;
|
|
1618
|
+
}
|
|
1342
1619
|
function formatMessages(messages) {
|
|
1343
1620
|
const formatted = [];
|
|
1344
1621
|
for (const msg of messages) {
|
|
@@ -1354,7 +1631,7 @@ function createOpenAIProvider(config) {
|
|
|
1354
1631
|
formatted.push(formatAssistantMessage(msg));
|
|
1355
1632
|
continue;
|
|
1356
1633
|
}
|
|
1357
|
-
formatted.push({ role: "user", content:
|
|
1634
|
+
formatted.push({ role: "user", content: formatUserContent(msg) });
|
|
1358
1635
|
}
|
|
1359
1636
|
return formatted;
|
|
1360
1637
|
}
|
|
@@ -1431,6 +1708,17 @@ function createOpenAIProvider(config) {
|
|
|
1431
1708
|
const tools = formatTools(req.tools);
|
|
1432
1709
|
if (tools)
|
|
1433
1710
|
body.tools = tools;
|
|
1711
|
+
if (req.schema) {
|
|
1712
|
+
const jsonSchema = zodToJsonSchema(req.schema);
|
|
1713
|
+
body.response_format = {
|
|
1714
|
+
type: "json_schema",
|
|
1715
|
+
json_schema: {
|
|
1716
|
+
name: "structured_output",
|
|
1717
|
+
strict: true,
|
|
1718
|
+
schema: jsonSchema
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1434
1722
|
const startTime = performance.now();
|
|
1435
1723
|
const raw = await retry(async () => {
|
|
1436
1724
|
const controller = new AbortController;
|
|
@@ -1590,7 +1878,7 @@ var PROVIDER_FACTORIES = {
|
|
|
1590
1878
|
openai: createOpenAIProvider,
|
|
1591
1879
|
google: createGoogleProvider
|
|
1592
1880
|
};
|
|
1593
|
-
function
|
|
1881
|
+
function validateGatewayConfig(config) {
|
|
1594
1882
|
const factory = PROVIDER_FACTORIES[config.provider];
|
|
1595
1883
|
if (!factory) {
|
|
1596
1884
|
throw new ElsiumError({
|
|
@@ -1599,21 +1887,92 @@ function gateway(config) {
|
|
|
1599
1887
|
retryable: false
|
|
1600
1888
|
});
|
|
1601
1889
|
}
|
|
1890
|
+
if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
|
|
1891
|
+
throw new ElsiumError({
|
|
1892
|
+
code: "CONFIG_ERROR",
|
|
1893
|
+
message: "apiKey must be a non-empty string",
|
|
1894
|
+
retryable: false
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
if (config.timeout !== undefined && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
|
|
1898
|
+
throw new ElsiumError({
|
|
1899
|
+
code: "CONFIG_ERROR",
|
|
1900
|
+
message: "timeout must be a positive finite number",
|
|
1901
|
+
retryable: false
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
if (config.maxRetries !== undefined && (!Number.isFinite(config.maxRetries) || !Number.isInteger(config.maxRetries) || config.maxRetries < 0)) {
|
|
1905
|
+
throw new ElsiumError({
|
|
1906
|
+
code: "CONFIG_ERROR",
|
|
1907
|
+
message: "maxRetries must be a non-negative finite integer",
|
|
1908
|
+
retryable: false
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
return factory;
|
|
1912
|
+
}
|
|
1913
|
+
function autoRegisterProvider(provider) {
|
|
1914
|
+
if (!provider.metadata)
|
|
1915
|
+
return;
|
|
1916
|
+
registerProviderMetadata(provider.name, provider.metadata);
|
|
1917
|
+
if (!provider.metadata.pricing)
|
|
1918
|
+
return;
|
|
1919
|
+
for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
|
|
1920
|
+
registerPricing(model, pricing);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
function validateRequestLimits(request, maxMessages, maxInputTokens) {
|
|
1924
|
+
if (request.messages.length > maxMessages) {
|
|
1925
|
+
throw ElsiumError.validation(`Message count ${request.messages.length} exceeds limit of ${maxMessages}`);
|
|
1926
|
+
}
|
|
1927
|
+
let estimatedTokens = 0;
|
|
1928
|
+
for (const msg of request.messages) {
|
|
1929
|
+
const text = typeof msg.content === "string" ? msg.content : msg.content.map((p) => p.type === "text" ? p.text : "").join("");
|
|
1930
|
+
estimatedTokens += Math.ceil(text.length / 4);
|
|
1931
|
+
}
|
|
1932
|
+
if (estimatedTokens > maxInputTokens) {
|
|
1933
|
+
throw ElsiumError.validation(`Estimated input tokens (~${estimatedTokens}) exceeds limit of ${maxInputTokens}`);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function buildMiddlewareContext(req, providerName, defaultModel, metadata) {
|
|
1937
|
+
return {
|
|
1938
|
+
request: req,
|
|
1939
|
+
provider: providerName,
|
|
1940
|
+
model: req.model ?? defaultModel,
|
|
1941
|
+
traceId: generateTraceId(),
|
|
1942
|
+
startTime: performance.now(),
|
|
1943
|
+
metadata
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
async function accumulateStreamEvents(stream, emit) {
|
|
1947
|
+
let textContent = "";
|
|
1948
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
1949
|
+
let stopReason = "end_turn";
|
|
1950
|
+
let id = "";
|
|
1951
|
+
for await (const event of stream) {
|
|
1952
|
+
emit(event);
|
|
1953
|
+
if (event.type === "text_delta") {
|
|
1954
|
+
textContent += event.text;
|
|
1955
|
+
} else if (event.type === "message_end") {
|
|
1956
|
+
usage = event.usage;
|
|
1957
|
+
stopReason = event.stopReason;
|
|
1958
|
+
} else if (event.type === "message_start") {
|
|
1959
|
+
id = event.id;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return { textContent, usage, stopReason, id };
|
|
1963
|
+
}
|
|
1964
|
+
function gateway(config) {
|
|
1965
|
+
const factory = validateGatewayConfig(config);
|
|
1602
1966
|
const provider = factory({
|
|
1603
1967
|
apiKey: config.apiKey,
|
|
1604
1968
|
baseUrl: config.baseUrl,
|
|
1605
1969
|
timeout: config.timeout,
|
|
1606
1970
|
maxRetries: config.maxRetries
|
|
1607
1971
|
});
|
|
1608
|
-
|
|
1609
|
-
registerProviderMetadata(provider.name, provider.metadata);
|
|
1610
|
-
if (provider.metadata.pricing) {
|
|
1611
|
-
for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
|
|
1612
|
-
registerPricing(model, pricing);
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1972
|
+
autoRegisterProvider(provider);
|
|
1616
1973
|
const defaultModel = config.model ?? provider.defaultModel;
|
|
1974
|
+
const maxMessages = config.maxMessages ?? 1000;
|
|
1975
|
+
const maxInputTokens = config.maxInputTokens ?? 1e6;
|
|
1617
1976
|
let xrayStore = null;
|
|
1618
1977
|
const allMiddleware = [...config.middleware ?? []];
|
|
1619
1978
|
if (config.xray) {
|
|
@@ -1628,14 +1987,7 @@ function gateway(config) {
|
|
|
1628
1987
|
if (!composedMiddleware) {
|
|
1629
1988
|
return provider.complete(req);
|
|
1630
1989
|
}
|
|
1631
|
-
const ctx = {
|
|
1632
|
-
request: req,
|
|
1633
|
-
provider: provider.name,
|
|
1634
|
-
model: req.model ?? defaultModel,
|
|
1635
|
-
traceId: generateTraceId(),
|
|
1636
|
-
startTime: performance.now(),
|
|
1637
|
-
metadata: request.metadata ?? {}
|
|
1638
|
-
};
|
|
1990
|
+
const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
|
|
1639
1991
|
return composedMiddleware(ctx, async (c) => provider.complete(c.request));
|
|
1640
1992
|
}
|
|
1641
1993
|
return {
|
|
@@ -1647,34 +1999,27 @@ function gateway(config) {
|
|
|
1647
1999
|
return xrayStore?.callHistory(limit) ?? [];
|
|
1648
2000
|
},
|
|
1649
2001
|
async complete(request) {
|
|
2002
|
+
validateRequestLimits(request, maxMessages, maxInputTokens);
|
|
1650
2003
|
return executeWithMiddleware(request);
|
|
1651
2004
|
},
|
|
1652
2005
|
stream(request) {
|
|
2006
|
+
validateRequestLimits(request, maxMessages, maxInputTokens);
|
|
1653
2007
|
const req = { ...request, model: request.model ?? defaultModel };
|
|
1654
2008
|
if (composedMiddleware) {
|
|
1655
|
-
const ctx = {
|
|
1656
|
-
request: req,
|
|
1657
|
-
provider: provider.name,
|
|
1658
|
-
model: req.model ?? defaultModel,
|
|
1659
|
-
traceId: generateTraceId(),
|
|
1660
|
-
startTime: performance.now(),
|
|
1661
|
-
metadata: request.metadata ?? {}
|
|
1662
|
-
};
|
|
2009
|
+
const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
|
|
1663
2010
|
return createStream(async (emit) => {
|
|
1664
2011
|
await composedMiddleware(ctx, async (c) => {
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
emit(event);
|
|
1668
|
-
}
|
|
2012
|
+
const result = await accumulateStreamEvents(provider.stream(c.request), emit);
|
|
2013
|
+
const latencyMs = Math.round(performance.now() - ctx.startTime);
|
|
1669
2014
|
return {
|
|
1670
|
-
id:
|
|
1671
|
-
message: { role: "assistant", content:
|
|
1672
|
-
usage:
|
|
1673
|
-
cost:
|
|
2015
|
+
id: result.id,
|
|
2016
|
+
message: { role: "assistant", content: result.textContent },
|
|
2017
|
+
usage: result.usage,
|
|
2018
|
+
cost: calculateCost(c.model, result.usage),
|
|
1674
2019
|
model: c.model,
|
|
1675
2020
|
provider: provider.name,
|
|
1676
|
-
stopReason:
|
|
1677
|
-
latencyMs
|
|
2021
|
+
stopReason: result.stopReason,
|
|
2022
|
+
latencyMs,
|
|
1678
2023
|
traceId: ctx.traceId
|
|
1679
2024
|
};
|
|
1680
2025
|
});
|
|
@@ -1684,107 +2029,52 @@ function gateway(config) {
|
|
|
1684
2029
|
},
|
|
1685
2030
|
async generate(request) {
|
|
1686
2031
|
const { schema, ...rest } = request;
|
|
1687
|
-
const jsonSchema =
|
|
1688
|
-
const systemPrompt = [
|
|
1689
|
-
rest.system ?? "",
|
|
1690
|
-
"You MUST respond with valid JSON matching this schema:",
|
|
1691
|
-
JSON.stringify(jsonSchema, null, 2),
|
|
1692
|
-
"Respond ONLY with the JSON object, no markdown or explanation."
|
|
1693
|
-
].filter(Boolean).join(`
|
|
1694
|
-
|
|
1695
|
-
`);
|
|
2032
|
+
const jsonSchema = zodToJsonSchema(schema);
|
|
1696
2033
|
const response = await executeWithMiddleware({
|
|
1697
2034
|
...rest,
|
|
1698
|
-
|
|
2035
|
+
schema,
|
|
2036
|
+
system: [
|
|
2037
|
+
rest.system ?? "",
|
|
2038
|
+
"You MUST respond with valid JSON matching this schema:",
|
|
2039
|
+
JSON.stringify(jsonSchema, null, 2),
|
|
2040
|
+
"Respond ONLY with the JSON object, no markdown or explanation."
|
|
2041
|
+
].filter(Boolean).join(`
|
|
2042
|
+
|
|
2043
|
+
`)
|
|
1699
2044
|
});
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
}
|
|
2045
|
+
let parsed;
|
|
2046
|
+
if (response.stopReason === "tool_use" && response.message.toolCalls?.length) {
|
|
2047
|
+
const structuredCall = response.message.toolCalls.find((tc) => tc.name === "_structured_output");
|
|
2048
|
+
if (structuredCall) {
|
|
2049
|
+
parsed = structuredCall.arguments;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (parsed === undefined) {
|
|
2053
|
+
const text = typeof response.message.content === "string" ? response.message.content : "";
|
|
2054
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
2055
|
+
if (!jsonMatch) {
|
|
2056
|
+
throw ElsiumError.validation("LLM response did not contain valid JSON", {
|
|
2057
|
+
response: text
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
1706
2061
|
}
|
|
1707
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
1708
2062
|
const result = schema.safeParse(parsed);
|
|
1709
2063
|
if (!result.success) {
|
|
1710
2064
|
throw ElsiumError.validation("LLM response did not match schema", {
|
|
1711
|
-
errors: result.error.issues
|
|
1712
|
-
response: text
|
|
2065
|
+
errors: result.error.issues
|
|
1713
2066
|
});
|
|
1714
2067
|
}
|
|
1715
2068
|
return { data: result.data, response };
|
|
1716
2069
|
}
|
|
1717
2070
|
};
|
|
1718
2071
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
return result;
|
|
1726
|
-
}
|
|
1727
|
-
} catch {}
|
|
1728
|
-
return { type: "string" };
|
|
1729
|
-
}
|
|
1730
|
-
function zodDefKind(def) {
|
|
1731
|
-
return typeof def.type === "string" ? def.type : def.typeName;
|
|
1732
|
-
}
|
|
1733
|
-
function convertZodDef(def) {
|
|
1734
|
-
const kind = zodDefKind(def);
|
|
1735
|
-
switch (kind) {
|
|
1736
|
-
case "object":
|
|
1737
|
-
case "ZodObject":
|
|
1738
|
-
return convertZodObject(def);
|
|
1739
|
-
case "string":
|
|
1740
|
-
case "ZodString":
|
|
1741
|
-
return { type: "string" };
|
|
1742
|
-
case "number":
|
|
1743
|
-
case "ZodNumber":
|
|
1744
|
-
return { type: "number" };
|
|
1745
|
-
case "boolean":
|
|
1746
|
-
case "ZodBoolean":
|
|
1747
|
-
return { type: "boolean" };
|
|
1748
|
-
case "array":
|
|
1749
|
-
case "ZodArray":
|
|
1750
|
-
return convertZodArray(def);
|
|
1751
|
-
case "enum":
|
|
1752
|
-
case "ZodEnum": {
|
|
1753
|
-
const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
|
|
1754
|
-
return { type: "string", enum: values };
|
|
1755
|
-
}
|
|
1756
|
-
case "optional":
|
|
1757
|
-
case "ZodOptional":
|
|
1758
|
-
return convertZodOptional(def);
|
|
1759
|
-
default:
|
|
1760
|
-
return null;
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
function convertZodObject(def) {
|
|
1764
|
-
if (!def.shape)
|
|
1765
|
-
return null;
|
|
1766
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
1767
|
-
const properties = {};
|
|
1768
|
-
const required = [];
|
|
1769
|
-
for (const [key, value] of Object.entries(shape)) {
|
|
1770
|
-
properties[key] = schemaToJsonSchema(value);
|
|
1771
|
-
const valDef = value._def;
|
|
1772
|
-
const valKind = zodDefKind(valDef);
|
|
1773
|
-
if (valKind !== "optional" && valKind !== "ZodOptional") {
|
|
1774
|
-
required.push(key);
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
return { type: "object", properties, required };
|
|
1778
|
-
}
|
|
1779
|
-
function convertZodArray(def) {
|
|
1780
|
-
return {
|
|
1781
|
-
type: "array",
|
|
1782
|
-
items: schemaToJsonSchema(def.element ?? def.type)
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
function convertZodOptional(def) {
|
|
1786
|
-
return schemaToJsonSchema(def.innerType ?? def.innerType);
|
|
1787
|
-
}
|
|
2072
|
+
// ../gateway/src/cache.ts
|
|
2073
|
+
var log4 = createLogger();
|
|
2074
|
+
// ../gateway/src/output-guardrails.ts
|
|
2075
|
+
var log5 = createLogger();
|
|
2076
|
+
// ../gateway/src/batch.ts
|
|
2077
|
+
var log6 = createLogger();
|
|
1788
2078
|
// ../observe/src/span.ts
|
|
1789
2079
|
function createSpan(name, options = {}) {
|
|
1790
2080
|
const id = generateId("spn");
|
|
@@ -1854,7 +2144,8 @@ function createSpan(name, options = {}) {
|
|
|
1854
2144
|
return span;
|
|
1855
2145
|
}
|
|
1856
2146
|
// ../observe/src/tracer.ts
|
|
1857
|
-
|
|
2147
|
+
import { writeFileSync } from "node:fs";
|
|
2148
|
+
var log7 = createLogger();
|
|
1858
2149
|
function observe(config = {}) {
|
|
1859
2150
|
const {
|
|
1860
2151
|
output = ["console"],
|
|
@@ -1869,7 +2160,21 @@ function observe(config = {}) {
|
|
|
1869
2160
|
for (const out of output) {
|
|
1870
2161
|
if (out === "console") {
|
|
1871
2162
|
handlers.push(consoleHandler);
|
|
1872
|
-
} else if (out === "json-file") {
|
|
2163
|
+
} else if (out === "json-file") {
|
|
2164
|
+
exporters.push({
|
|
2165
|
+
name: "json-file",
|
|
2166
|
+
export(spansToExport) {
|
|
2167
|
+
const filename = `.elsium/traces-${Date.now()}.json`;
|
|
2168
|
+
try {
|
|
2169
|
+
writeFileSync(filename, JSON.stringify(spansToExport, null, 2));
|
|
2170
|
+
} catch (err2) {
|
|
2171
|
+
log7.error("Failed to write trace file", {
|
|
2172
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
});
|
|
2177
|
+
} else {
|
|
1873
2178
|
exporters.push(out);
|
|
1874
2179
|
}
|
|
1875
2180
|
}
|
|
@@ -1938,7 +2243,7 @@ function observe(config = {}) {
|
|
|
1938
2243
|
function consoleHandler(span) {
|
|
1939
2244
|
const duration = span.durationMs !== undefined ? `${span.durationMs}ms` : "running";
|
|
1940
2245
|
const status = span.status === "error" ? "[ERROR]" : span.status === "ok" ? "[OK]" : "[...]";
|
|
1941
|
-
|
|
2246
|
+
log7.info("span", {
|
|
1942
2247
|
trace: span.traceId,
|
|
1943
2248
|
span: span.name,
|
|
1944
2249
|
kind: span.kind,
|
|
@@ -1975,8 +2280,10 @@ function createNoopSpan(name, kind) {
|
|
|
1975
2280
|
}
|
|
1976
2281
|
};
|
|
1977
2282
|
}
|
|
2283
|
+
// ../observe/src/experiment.ts
|
|
2284
|
+
var log8 = createLogger();
|
|
1978
2285
|
// ../observe/src/otel.ts
|
|
1979
|
-
var
|
|
2286
|
+
var log9 = createLogger();
|
|
1980
2287
|
// ../../node_modules/.bun/@hono+node-server@1.19.9/node_modules/@hono/node-server/dist/index.mjs
|
|
1981
2288
|
import { createServer as createServerHTTP } from "http";
|
|
1982
2289
|
import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
|
|
@@ -4049,7 +4356,7 @@ var Hono2 = class extends Hono {
|
|
|
4049
4356
|
// src/middleware.ts
|
|
4050
4357
|
import { timingSafeEqual } from "node:crypto";
|
|
4051
4358
|
function corsMiddleware(config = true) {
|
|
4052
|
-
const opts = typeof config === "boolean" ? { origin:
|
|
4359
|
+
const opts = typeof config === "boolean" ? { origin: "*", methods: ["GET", "POST", "OPTIONS"] } : config;
|
|
4053
4360
|
return async (c, next) => {
|
|
4054
4361
|
const requestOrigin = c.req.header("Origin") ?? "";
|
|
4055
4362
|
let allowedOrigin;
|
|
@@ -4126,8 +4433,203 @@ function rateLimitMiddleware(config) {
|
|
|
4126
4433
|
await next();
|
|
4127
4434
|
};
|
|
4128
4435
|
}
|
|
4436
|
+
function requestIdMiddleware() {
|
|
4437
|
+
return async (c, next) => {
|
|
4438
|
+
const raw2 = c.req.header("X-Request-ID");
|
|
4439
|
+
const id = raw2 && /^[\w\-.:]{1,128}$/.test(raw2) ? raw2 : generateId("req");
|
|
4440
|
+
c.set("requestId", id);
|
|
4441
|
+
await next();
|
|
4442
|
+
c.res.headers.set("X-Request-ID", id);
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
function requestLoggerMiddleware(logger) {
|
|
4446
|
+
const log10 = logger ?? createLogger();
|
|
4447
|
+
return async (c, next) => {
|
|
4448
|
+
const start = Date.now();
|
|
4449
|
+
await next();
|
|
4450
|
+
const duration = Date.now() - start;
|
|
4451
|
+
log10.info(`${c.req.method} ${c.req.path}`, {
|
|
4452
|
+
method: c.req.method,
|
|
4453
|
+
path: c.req.path,
|
|
4454
|
+
status: c.res.status,
|
|
4455
|
+
durationMs: duration,
|
|
4456
|
+
requestId: c.get("requestId")
|
|
4457
|
+
});
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
// ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/utils/stream.js
|
|
4462
|
+
var StreamingApi = class {
|
|
4463
|
+
writer;
|
|
4464
|
+
encoder;
|
|
4465
|
+
writable;
|
|
4466
|
+
abortSubscribers = [];
|
|
4467
|
+
responseReadable;
|
|
4468
|
+
aborted = false;
|
|
4469
|
+
closed = false;
|
|
4470
|
+
constructor(writable, _readable) {
|
|
4471
|
+
this.writable = writable;
|
|
4472
|
+
this.writer = writable.getWriter();
|
|
4473
|
+
this.encoder = new TextEncoder;
|
|
4474
|
+
const reader = _readable.getReader();
|
|
4475
|
+
this.abortSubscribers.push(async () => {
|
|
4476
|
+
await reader.cancel();
|
|
4477
|
+
});
|
|
4478
|
+
this.responseReadable = new ReadableStream({
|
|
4479
|
+
async pull(controller) {
|
|
4480
|
+
const { done, value } = await reader.read();
|
|
4481
|
+
done ? controller.close() : controller.enqueue(value);
|
|
4482
|
+
},
|
|
4483
|
+
cancel: () => {
|
|
4484
|
+
this.abort();
|
|
4485
|
+
}
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
async write(input) {
|
|
4489
|
+
try {
|
|
4490
|
+
if (typeof input === "string") {
|
|
4491
|
+
input = this.encoder.encode(input);
|
|
4492
|
+
}
|
|
4493
|
+
await this.writer.write(input);
|
|
4494
|
+
} catch {}
|
|
4495
|
+
return this;
|
|
4496
|
+
}
|
|
4497
|
+
async writeln(input) {
|
|
4498
|
+
await this.write(input + `
|
|
4499
|
+
`);
|
|
4500
|
+
return this;
|
|
4501
|
+
}
|
|
4502
|
+
sleep(ms) {
|
|
4503
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
4504
|
+
}
|
|
4505
|
+
async close() {
|
|
4506
|
+
try {
|
|
4507
|
+
await this.writer.close();
|
|
4508
|
+
} catch {}
|
|
4509
|
+
this.closed = true;
|
|
4510
|
+
}
|
|
4511
|
+
async pipe(body) {
|
|
4512
|
+
this.writer.releaseLock();
|
|
4513
|
+
await body.pipeTo(this.writable, { preventClose: true });
|
|
4514
|
+
this.writer = this.writable.getWriter();
|
|
4515
|
+
}
|
|
4516
|
+
onAbort(listener) {
|
|
4517
|
+
this.abortSubscribers.push(listener);
|
|
4518
|
+
}
|
|
4519
|
+
abort() {
|
|
4520
|
+
if (!this.aborted) {
|
|
4521
|
+
this.aborted = true;
|
|
4522
|
+
this.abortSubscribers.forEach((subscriber) => subscriber());
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
};
|
|
4526
|
+
|
|
4527
|
+
// ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/helper/streaming/utils.js
|
|
4528
|
+
var isOldBunVersion = () => {
|
|
4529
|
+
const version = typeof Bun !== "undefined" ? Bun.version : undefined;
|
|
4530
|
+
if (version === undefined) {
|
|
4531
|
+
return false;
|
|
4532
|
+
}
|
|
4533
|
+
const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
|
|
4534
|
+
isOldBunVersion = () => result;
|
|
4535
|
+
return result;
|
|
4536
|
+
};
|
|
4537
|
+
|
|
4538
|
+
// ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/helper/streaming/stream.js
|
|
4539
|
+
var contextStash = /* @__PURE__ */ new WeakMap;
|
|
4540
|
+
var stream = (c, cb, onError) => {
|
|
4541
|
+
const { readable, writable } = new TransformStream;
|
|
4542
|
+
const stream2 = new StreamingApi(writable, readable);
|
|
4543
|
+
if (isOldBunVersion()) {
|
|
4544
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
4545
|
+
if (!stream2.closed) {
|
|
4546
|
+
stream2.abort();
|
|
4547
|
+
}
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
contextStash.set(stream2.responseReadable, c);
|
|
4551
|
+
(async () => {
|
|
4552
|
+
try {
|
|
4553
|
+
await cb(stream2);
|
|
4554
|
+
} catch (e) {
|
|
4555
|
+
if (e === undefined) {} else if (e instanceof Error && onError) {
|
|
4556
|
+
await onError(e, stream2);
|
|
4557
|
+
} else {
|
|
4558
|
+
console.error(e);
|
|
4559
|
+
}
|
|
4560
|
+
} finally {
|
|
4561
|
+
stream2.close();
|
|
4562
|
+
}
|
|
4563
|
+
})();
|
|
4564
|
+
return c.newResponse(stream2.responseReadable);
|
|
4565
|
+
};
|
|
4566
|
+
|
|
4567
|
+
// src/sse.ts
|
|
4568
|
+
function sseHeaders() {
|
|
4569
|
+
return {
|
|
4570
|
+
"Content-Type": "text/event-stream",
|
|
4571
|
+
"Cache-Control": "no-cache",
|
|
4572
|
+
Connection: "keep-alive",
|
|
4573
|
+
"X-Accel-Buffering": "no"
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
function formatSSE(event, data) {
|
|
4577
|
+
const json = JSON.stringify(data);
|
|
4578
|
+
if (event === "message") {
|
|
4579
|
+
return `data: ${json}
|
|
4580
|
+
|
|
4581
|
+
`;
|
|
4582
|
+
}
|
|
4583
|
+
return `event: ${event}
|
|
4584
|
+
data: ${json}
|
|
4585
|
+
|
|
4586
|
+
`;
|
|
4587
|
+
}
|
|
4588
|
+
function streamResponse(c, source) {
|
|
4589
|
+
const headers = sseHeaders();
|
|
4590
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4591
|
+
c.header(key, value);
|
|
4592
|
+
}
|
|
4593
|
+
return stream(c, async (s) => {
|
|
4594
|
+
try {
|
|
4595
|
+
for await (const event of source) {
|
|
4596
|
+
const sseData = formatSSE("message", event);
|
|
4597
|
+
await s.write(sseData);
|
|
4598
|
+
}
|
|
4599
|
+
} catch (err2) {
|
|
4600
|
+
const errorEvent = {
|
|
4601
|
+
type: "error",
|
|
4602
|
+
error: err2 instanceof Error ? err2 : new Error(String(err2))
|
|
4603
|
+
};
|
|
4604
|
+
const sseData = formatSSE("error", {
|
|
4605
|
+
type: "error",
|
|
4606
|
+
message: errorEvent.error.message
|
|
4607
|
+
});
|
|
4608
|
+
await s.write(sseData);
|
|
4609
|
+
}
|
|
4610
|
+
});
|
|
4611
|
+
}
|
|
4129
4612
|
|
|
4130
4613
|
// src/routes.ts
|
|
4614
|
+
function parseJsonBody(raw2) {
|
|
4615
|
+
try {
|
|
4616
|
+
return { ok: true, data: JSON.parse(raw2) };
|
|
4617
|
+
} catch {
|
|
4618
|
+
return { ok: false };
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
function elsiumErrorResponse(c, err2, fallbackMessage) {
|
|
4622
|
+
if (err2 instanceof ElsiumError) {
|
|
4623
|
+
return c.json({ error: err2.message, code: err2.code }, err2.statusCode ?? 500);
|
|
4624
|
+
}
|
|
4625
|
+
return c.json({ error: fallbackMessage }, 500);
|
|
4626
|
+
}
|
|
4627
|
+
function resolveAgent(name, agents, defaultAgent) {
|
|
4628
|
+
const agent = name ? agents.get(name) : defaultAgent;
|
|
4629
|
+
if (agent)
|
|
4630
|
+
return { agent };
|
|
4631
|
+
return { error: name ? `Agent "${name}" not found` : "No default agent configured" };
|
|
4632
|
+
}
|
|
4131
4633
|
function createRoutes(deps) {
|
|
4132
4634
|
const app = new Hono2;
|
|
4133
4635
|
let totalRequests = 0;
|
|
@@ -4168,17 +4670,32 @@ function createRoutes(deps) {
|
|
|
4168
4670
|
if (rawText.length > MAX_BODY_SIZE) {
|
|
4169
4671
|
return c.json({ error: "Request body too large (max 1MB)" }, 413);
|
|
4170
4672
|
}
|
|
4171
|
-
const
|
|
4673
|
+
const parsed = parseJsonBody(rawText);
|
|
4674
|
+
if (!parsed.ok) {
|
|
4675
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
4676
|
+
}
|
|
4677
|
+
const body = parsed.data;
|
|
4172
4678
|
if (!body.message) {
|
|
4173
4679
|
return c.json({ error: "message is required" }, 400);
|
|
4174
4680
|
}
|
|
4175
|
-
const
|
|
4176
|
-
if (
|
|
4177
|
-
return c.json({
|
|
4178
|
-
|
|
4179
|
-
|
|
4681
|
+
const resolved = resolveAgent(body.agent, deps.agents, deps.defaultAgent);
|
|
4682
|
+
if ("error" in resolved) {
|
|
4683
|
+
return c.json({ error: resolved.error }, 404);
|
|
4684
|
+
}
|
|
4685
|
+
if (body.stream) {
|
|
4686
|
+
const stream2 = deps.gateway.stream({
|
|
4687
|
+
messages: [{ role: "user", content: body.message }],
|
|
4688
|
+
system: resolved.agent.config.system,
|
|
4689
|
+
model: resolved.agent.config.model
|
|
4690
|
+
});
|
|
4691
|
+
return streamResponse(c, stream2);
|
|
4692
|
+
}
|
|
4693
|
+
let result;
|
|
4694
|
+
try {
|
|
4695
|
+
result = await resolved.agent.run(body.message);
|
|
4696
|
+
} catch (err2) {
|
|
4697
|
+
return elsiumErrorResponse(c, err2, "Agent execution failed");
|
|
4180
4698
|
}
|
|
4181
|
-
const result = await agent.run(body.message);
|
|
4182
4699
|
deps.tracer?.trackLLMCall({
|
|
4183
4700
|
model: "unknown",
|
|
4184
4701
|
inputTokens: result.usage.totalInputTokens,
|
|
@@ -4195,7 +4712,7 @@ function createRoutes(deps) {
|
|
|
4195
4712
|
totalTokens: result.usage.totalTokens,
|
|
4196
4713
|
cost: result.usage.totalCost
|
|
4197
4714
|
},
|
|
4198
|
-
model: agent.config.model ?? "default",
|
|
4715
|
+
model: resolved.agent.config.model ?? "default",
|
|
4199
4716
|
traceId: result.traceId
|
|
4200
4717
|
};
|
|
4201
4718
|
return c.json(response);
|
|
@@ -4207,7 +4724,11 @@ function createRoutes(deps) {
|
|
|
4207
4724
|
if (rawText.length > MAX_BODY_SIZE) {
|
|
4208
4725
|
return c.json({ error: "Request body too large (max 1MB)" }, 413);
|
|
4209
4726
|
}
|
|
4210
|
-
const
|
|
4727
|
+
const parsed = parseJsonBody(rawText);
|
|
4728
|
+
if (!parsed.ok) {
|
|
4729
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
4730
|
+
}
|
|
4731
|
+
const body = parsed.data;
|
|
4211
4732
|
if (!body.messages?.length) {
|
|
4212
4733
|
return c.json({ error: "messages array is required" }, 400);
|
|
4213
4734
|
}
|
|
@@ -4215,13 +4736,28 @@ function createRoutes(deps) {
|
|
|
4215
4736
|
role: m.role,
|
|
4216
4737
|
content: m.content
|
|
4217
4738
|
}));
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4739
|
+
if (body.stream) {
|
|
4740
|
+
const stream2 = deps.gateway.stream({
|
|
4741
|
+
messages,
|
|
4742
|
+
model: body.model,
|
|
4743
|
+
system: body.system,
|
|
4744
|
+
maxTokens: body.maxTokens,
|
|
4745
|
+
temperature: body.temperature
|
|
4746
|
+
});
|
|
4747
|
+
return streamResponse(c, stream2);
|
|
4748
|
+
}
|
|
4749
|
+
let response;
|
|
4750
|
+
try {
|
|
4751
|
+
response = await deps.gateway.complete({
|
|
4752
|
+
messages,
|
|
4753
|
+
model: body.model,
|
|
4754
|
+
system: body.system,
|
|
4755
|
+
maxTokens: body.maxTokens,
|
|
4756
|
+
temperature: body.temperature
|
|
4757
|
+
});
|
|
4758
|
+
} catch (err2) {
|
|
4759
|
+
return elsiumErrorResponse(c, err2, "Completion failed");
|
|
4760
|
+
}
|
|
4225
4761
|
deps.tracer?.trackLLMCall({
|
|
4226
4762
|
model: response.model,
|
|
4227
4763
|
inputTokens: response.usage.inputTokens,
|
|
@@ -4250,9 +4786,18 @@ function createRoutes(deps) {
|
|
|
4250
4786
|
}
|
|
4251
4787
|
|
|
4252
4788
|
// src/app.ts
|
|
4253
|
-
var
|
|
4789
|
+
var log10 = createLogger();
|
|
4254
4790
|
function createApp(config) {
|
|
4255
4791
|
const app = new Hono2;
|
|
4792
|
+
app.onError((err2, c) => {
|
|
4793
|
+
const statusCode = err2 instanceof ElsiumError ? err2.statusCode ?? 500 : 500;
|
|
4794
|
+
const code = err2 instanceof ElsiumError ? err2.code : "UNKNOWN";
|
|
4795
|
+
log10.error("Unhandled error", { error: err2.message, code, path: c.req.path });
|
|
4796
|
+
return c.json({ error: err2.message, code }, statusCode);
|
|
4797
|
+
});
|
|
4798
|
+
app.notFound((c) => {
|
|
4799
|
+
return c.json({ error: "Not found" }, 404);
|
|
4800
|
+
});
|
|
4256
4801
|
const providerNames = Object.keys(config.gateway.providers);
|
|
4257
4802
|
const primaryProvider = providerNames[0];
|
|
4258
4803
|
const primaryConfig = config.gateway.providers[primaryProvider];
|
|
@@ -4267,6 +4812,8 @@ function createApp(config) {
|
|
|
4267
4812
|
costTracking: config.observe?.costTracking ?? true
|
|
4268
4813
|
});
|
|
4269
4814
|
const serverConfig = config.server ?? {};
|
|
4815
|
+
app.use("*", requestIdMiddleware());
|
|
4816
|
+
app.use("*", requestLoggerMiddleware(log10));
|
|
4270
4817
|
if (serverConfig.cors) {
|
|
4271
4818
|
app.use("*", corsMiddleware(serverConfig.cors));
|
|
4272
4819
|
}
|
|
@@ -4289,7 +4836,7 @@ function createApp(config) {
|
|
|
4289
4836
|
defaultAgent,
|
|
4290
4837
|
tracer,
|
|
4291
4838
|
startTime: Date.now(),
|
|
4292
|
-
version: "0.
|
|
4839
|
+
version: config.version ?? "0.2.2",
|
|
4293
4840
|
providers: providerNames
|
|
4294
4841
|
});
|
|
4295
4842
|
app.route("/", routes);
|
|
@@ -4305,13 +4852,25 @@ function createApp(config) {
|
|
|
4305
4852
|
port: listenPort,
|
|
4306
4853
|
hostname
|
|
4307
4854
|
});
|
|
4308
|
-
|
|
4855
|
+
let shutdownManager;
|
|
4856
|
+
if (serverConfig.gracefulShutdown) {
|
|
4857
|
+
const drainTimeoutMs = typeof serverConfig.gracefulShutdown === "object" ? serverConfig.gracefulShutdown.drainTimeoutMs : undefined;
|
|
4858
|
+
shutdownManager = createShutdownManager({
|
|
4859
|
+
drainTimeoutMs,
|
|
4860
|
+
onDrainStart: () => log10.info("Draining connections..."),
|
|
4861
|
+
onDrainComplete: () => log10.info("Drain complete")
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4864
|
+
log10.info("ElsiumAI server started", {
|
|
4309
4865
|
url: `http://${hostname}:${listenPort}`,
|
|
4310
4866
|
routes: ["POST /chat", "POST /complete", "GET /health", "GET /metrics", "GET /agents"]
|
|
4311
4867
|
});
|
|
4312
4868
|
return {
|
|
4313
4869
|
port: listenPort,
|
|
4314
|
-
stop: () => {
|
|
4870
|
+
stop: async () => {
|
|
4871
|
+
if (shutdownManager) {
|
|
4872
|
+
await shutdownManager.shutdown();
|
|
4873
|
+
}
|
|
4315
4874
|
server.close();
|
|
4316
4875
|
}
|
|
4317
4876
|
};
|
|
@@ -4319,7 +4878,7 @@ function createApp(config) {
|
|
|
4319
4878
|
};
|
|
4320
4879
|
}
|
|
4321
4880
|
// src/rbac.ts
|
|
4322
|
-
var
|
|
4881
|
+
var log11 = createLogger();
|
|
4323
4882
|
var BUILT_IN_ROLES = [
|
|
4324
4883
|
{
|
|
4325
4884
|
name: "admin",
|
|
@@ -4357,7 +4916,7 @@ function matchPermission(granted, required) {
|
|
|
4357
4916
|
}
|
|
4358
4917
|
function createRBAC(config) {
|
|
4359
4918
|
if (config.trustRoleHeader === true) {
|
|
4360
|
-
|
|
4919
|
+
log11.warn("RBAC: trustRoleHeader is enabled — any client can self-assign roles via the X-Role header. Only use this in development or behind a trusted reverse proxy.");
|
|
4361
4920
|
}
|
|
4362
4921
|
const roleMap = new Map;
|
|
4363
4922
|
for (const role of BUILT_IN_ROLES) {
|
|
@@ -4413,8 +4972,62 @@ function createRBAC(config) {
|
|
|
4413
4972
|
}
|
|
4414
4973
|
};
|
|
4415
4974
|
}
|
|
4975
|
+
// src/tenant.ts
|
|
4976
|
+
var log12 = createLogger();
|
|
4977
|
+
function tenantMiddleware(config) {
|
|
4978
|
+
const { extractTenant, onUnknownTenant = "reject", defaultTenant } = config;
|
|
4979
|
+
return async (c, next) => {
|
|
4980
|
+
const tenant = extractTenant(c);
|
|
4981
|
+
if (!tenant) {
|
|
4982
|
+
if (onUnknownTenant === "default" && defaultTenant) {
|
|
4983
|
+
c.set("tenant", defaultTenant);
|
|
4984
|
+
log12.debug("Using default tenant", { tenantId: defaultTenant.tenantId });
|
|
4985
|
+
} else {
|
|
4986
|
+
return c.json({ error: "Tenant identification required" }, 401);
|
|
4987
|
+
}
|
|
4988
|
+
} else {
|
|
4989
|
+
c.set("tenant", tenant);
|
|
4990
|
+
log12.debug("Tenant identified", { tenantId: tenant.tenantId });
|
|
4991
|
+
}
|
|
4992
|
+
await next();
|
|
4993
|
+
};
|
|
4994
|
+
}
|
|
4995
|
+
function tenantRateLimitMiddleware() {
|
|
4996
|
+
const windows = new Map;
|
|
4997
|
+
return async (c, next) => {
|
|
4998
|
+
const tenant = c.get("tenant");
|
|
4999
|
+
if (!tenant?.limits?.maxRequestsPerMinute) {
|
|
5000
|
+
await next();
|
|
5001
|
+
return;
|
|
5002
|
+
}
|
|
5003
|
+
const limit = tenant.limits.maxRequestsPerMinute;
|
|
5004
|
+
const now = Date.now();
|
|
5005
|
+
const windowMs = 60000;
|
|
5006
|
+
const key = tenant.tenantId;
|
|
5007
|
+
let entry = windows.get(key);
|
|
5008
|
+
if (!entry || now - entry.windowStart > windowMs) {
|
|
5009
|
+
entry = { count: 0, windowStart: now };
|
|
5010
|
+
windows.set(key, entry);
|
|
5011
|
+
}
|
|
5012
|
+
entry.count++;
|
|
5013
|
+
if (entry.count > limit) {
|
|
5014
|
+
return c.json({
|
|
5015
|
+
error: "Rate limit exceeded",
|
|
5016
|
+
retryAfterMs: windowMs - (now - entry.windowStart)
|
|
5017
|
+
}, 429);
|
|
5018
|
+
}
|
|
5019
|
+
await next();
|
|
5020
|
+
};
|
|
5021
|
+
}
|
|
4416
5022
|
export {
|
|
5023
|
+
tenantRateLimitMiddleware,
|
|
5024
|
+
tenantMiddleware,
|
|
5025
|
+
streamResponse,
|
|
5026
|
+
sseHeaders,
|
|
5027
|
+
requestLoggerMiddleware,
|
|
5028
|
+
requestIdMiddleware,
|
|
4417
5029
|
rateLimitMiddleware,
|
|
5030
|
+
formatSSE,
|
|
4418
5031
|
createRoutes,
|
|
4419
5032
|
createRBAC,
|
|
4420
5033
|
createApp,
|