elsium-ai 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/README.md +73 -42
- package/dist/index.d.ts +14 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1864 -333
- package/package.json +12 -11
package/dist/index.js
CHANGED
|
@@ -491,7 +491,16 @@ function envBool(name, fallback) {
|
|
|
491
491
|
const raw = getEnvVar(name);
|
|
492
492
|
if (raw !== undefined) {
|
|
493
493
|
const normalized = raw.toLowerCase();
|
|
494
|
-
|
|
494
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes")
|
|
495
|
+
return true;
|
|
496
|
+
if (normalized === "false" || normalized === "0" || normalized === "no")
|
|
497
|
+
return false;
|
|
498
|
+
throw new ElsiumError({
|
|
499
|
+
code: "CONFIG_ERROR",
|
|
500
|
+
message: `Environment variable ${name} has unrecognized boolean value: ${raw}. Expected one of: true, false, 1, 0, yes, no`,
|
|
501
|
+
retryable: false,
|
|
502
|
+
metadata: { variable: name, value: raw }
|
|
503
|
+
});
|
|
495
504
|
}
|
|
496
505
|
if (fallback !== undefined)
|
|
497
506
|
return fallback;
|
|
@@ -502,6 +511,243 @@ function envBool(name, fallback) {
|
|
|
502
511
|
metadata: { variable: name }
|
|
503
512
|
});
|
|
504
513
|
}
|
|
514
|
+
// ../core/src/schema.ts
|
|
515
|
+
var log = createLogger();
|
|
516
|
+
function zodDefKind(def) {
|
|
517
|
+
return typeof def.type === "string" ? def.type : def.typeName;
|
|
518
|
+
}
|
|
519
|
+
function zodObjectToJsonSchema(schema, convert) {
|
|
520
|
+
const shape = typeof schema.shape === "function" ? schema.shape() : schema.shape;
|
|
521
|
+
const properties = {};
|
|
522
|
+
const required = [];
|
|
523
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
524
|
+
const fieldSchema = value;
|
|
525
|
+
properties[key] = convert(fieldSchema);
|
|
526
|
+
const fieldDef = fieldSchema._def;
|
|
527
|
+
const fieldKind = zodDefKind(fieldDef);
|
|
528
|
+
if (fieldKind !== "optional" && fieldKind !== "ZodOptional" && fieldKind !== "default" && fieldKind !== "ZodDefault") {
|
|
529
|
+
required.push(key);
|
|
530
|
+
}
|
|
531
|
+
if (fieldDef.description) {
|
|
532
|
+
properties[key].description = fieldDef.description;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return { type: "object", properties, required };
|
|
536
|
+
}
|
|
537
|
+
function zodToJsonSchema(schema) {
|
|
538
|
+
if (!("_def" in schema))
|
|
539
|
+
return { type: "object" };
|
|
540
|
+
const def = schema._def;
|
|
541
|
+
const kind = zodDefKind(def);
|
|
542
|
+
switch (kind) {
|
|
543
|
+
case "object":
|
|
544
|
+
case "ZodObject":
|
|
545
|
+
return zodObjectToJsonSchema(def, zodToJsonSchema);
|
|
546
|
+
case "string":
|
|
547
|
+
case "ZodString":
|
|
548
|
+
return { type: "string" };
|
|
549
|
+
case "number":
|
|
550
|
+
case "ZodNumber":
|
|
551
|
+
return { type: "number" };
|
|
552
|
+
case "boolean":
|
|
553
|
+
case "ZodBoolean":
|
|
554
|
+
return { type: "boolean" };
|
|
555
|
+
case "array":
|
|
556
|
+
case "ZodArray":
|
|
557
|
+
return {
|
|
558
|
+
type: "array",
|
|
559
|
+
items: zodToJsonSchema(def.element ?? def.type)
|
|
560
|
+
};
|
|
561
|
+
case "enum":
|
|
562
|
+
case "ZodEnum": {
|
|
563
|
+
const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
|
|
564
|
+
return { type: "string", enum: values };
|
|
565
|
+
}
|
|
566
|
+
case "optional":
|
|
567
|
+
case "ZodOptional":
|
|
568
|
+
return zodToJsonSchema(def.innerType);
|
|
569
|
+
case "default":
|
|
570
|
+
case "ZodDefault":
|
|
571
|
+
return zodToJsonSchema(def.innerType);
|
|
572
|
+
case "nullable":
|
|
573
|
+
case "ZodNullable": {
|
|
574
|
+
const inner = zodToJsonSchema(def.innerType);
|
|
575
|
+
return { ...inner, nullable: true };
|
|
576
|
+
}
|
|
577
|
+
case "ZodLiteral":
|
|
578
|
+
return { type: typeof def.value, const: def.value };
|
|
579
|
+
case "ZodUnion": {
|
|
580
|
+
const options = def.options.map(zodToJsonSchema);
|
|
581
|
+
return { anyOf: options };
|
|
582
|
+
}
|
|
583
|
+
case "ZodRecord":
|
|
584
|
+
return {
|
|
585
|
+
type: "object",
|
|
586
|
+
additionalProperties: def.valueType ? zodToJsonSchema(def.valueType) : { type: "string" }
|
|
587
|
+
};
|
|
588
|
+
case "ZodTuple": {
|
|
589
|
+
const items = (def.items ?? []).map(zodToJsonSchema);
|
|
590
|
+
return { type: "array", prefixItems: items, minItems: items.length, maxItems: items.length };
|
|
591
|
+
}
|
|
592
|
+
case "ZodDate":
|
|
593
|
+
return { type: "string", format: "date-time" };
|
|
594
|
+
default:
|
|
595
|
+
log.warn(`zodToJsonSchema: unsupported type ${kind}, defaulting to string`);
|
|
596
|
+
return { type: "string" };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// ../core/src/registry.ts
|
|
600
|
+
var log2 = createLogger();
|
|
601
|
+
var BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
602
|
+
function createRegistry(label) {
|
|
603
|
+
const entries = new Map;
|
|
604
|
+
return {
|
|
605
|
+
register(name, factory) {
|
|
606
|
+
if (BLOCKED_KEYS.has(name)) {
|
|
607
|
+
log2.warn(`Registry(${label}): rejected blocked key "${name}"`);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
entries.set(name, factory);
|
|
611
|
+
log2.debug(`Registry(${label}): registered "${name}"`);
|
|
612
|
+
},
|
|
613
|
+
get(name) {
|
|
614
|
+
if (BLOCKED_KEYS.has(name))
|
|
615
|
+
return;
|
|
616
|
+
return entries.get(name);
|
|
617
|
+
},
|
|
618
|
+
list() {
|
|
619
|
+
return Array.from(entries.keys());
|
|
620
|
+
},
|
|
621
|
+
has(name) {
|
|
622
|
+
if (BLOCKED_KEYS.has(name))
|
|
623
|
+
return false;
|
|
624
|
+
return entries.has(name);
|
|
625
|
+
},
|
|
626
|
+
unregister(name) {
|
|
627
|
+
if (BLOCKED_KEYS.has(name))
|
|
628
|
+
return false;
|
|
629
|
+
const deleted = entries.delete(name);
|
|
630
|
+
if (deleted) {
|
|
631
|
+
log2.debug(`Registry(${label}): unregistered "${name}"`);
|
|
632
|
+
}
|
|
633
|
+
return deleted;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
// ../core/src/tokens.ts
|
|
638
|
+
var MODEL_RATIOS = {
|
|
639
|
+
cl100k_base: 4,
|
|
640
|
+
"gpt-4o": 4,
|
|
641
|
+
"gpt-4o-mini": 4,
|
|
642
|
+
o1: 4,
|
|
643
|
+
"o1-mini": 4,
|
|
644
|
+
"o3-mini": 4,
|
|
645
|
+
"claude-opus-4-6": 3.5,
|
|
646
|
+
"claude-sonnet-4-6": 3.5,
|
|
647
|
+
"claude-haiku-4-5-20251001": 3.5,
|
|
648
|
+
"gemini-2.0-flash": 4,
|
|
649
|
+
"gemini-2.0-flash-lite": 4,
|
|
650
|
+
"gemini-2.5-pro-preview-05-06": 4,
|
|
651
|
+
"gemini-2.5-flash-preview-04-17": 4
|
|
652
|
+
};
|
|
653
|
+
function getRatio(model) {
|
|
654
|
+
if (!model)
|
|
655
|
+
return 4;
|
|
656
|
+
if (MODEL_RATIOS[model])
|
|
657
|
+
return MODEL_RATIOS[model];
|
|
658
|
+
if (model.startsWith("claude"))
|
|
659
|
+
return 3.5;
|
|
660
|
+
if (model.startsWith("gemini"))
|
|
661
|
+
return 4;
|
|
662
|
+
if (model.startsWith("gpt") || model.startsWith("o1") || model.startsWith("o3"))
|
|
663
|
+
return 4;
|
|
664
|
+
return 4;
|
|
665
|
+
}
|
|
666
|
+
function extractMessageText(msg) {
|
|
667
|
+
if (typeof msg.content === "string")
|
|
668
|
+
return msg.content;
|
|
669
|
+
return msg.content.filter((p) => p.type === "text" && ("text" in p)).map((p) => p.text).join("");
|
|
670
|
+
}
|
|
671
|
+
function countTokens(text, model) {
|
|
672
|
+
const ratio = getRatio(model);
|
|
673
|
+
return Math.ceil(text.length / ratio) + 4;
|
|
674
|
+
}
|
|
675
|
+
function createContextManager(config) {
|
|
676
|
+
const { maxTokens, strategy, reserveTokens = 0 } = config;
|
|
677
|
+
const budget = maxTokens - reserveTokens;
|
|
678
|
+
function estimateMessageTokens(msg) {
|
|
679
|
+
return countTokens(extractMessageText(msg)) + 4;
|
|
680
|
+
}
|
|
681
|
+
function estimateTokens(messages) {
|
|
682
|
+
return messages.reduce((sum, msg) => sum + estimateMessageTokens(msg), 0);
|
|
683
|
+
}
|
|
684
|
+
async function fitTruncate(messages, system) {
|
|
685
|
+
let available = budget;
|
|
686
|
+
if (system)
|
|
687
|
+
available -= countTokens(system);
|
|
688
|
+
const result = [];
|
|
689
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
690
|
+
const tokens = estimateMessageTokens(messages[i]);
|
|
691
|
+
if (available - tokens < 0 && result.length > 0)
|
|
692
|
+
break;
|
|
693
|
+
available -= tokens;
|
|
694
|
+
result.unshift(messages[i]);
|
|
695
|
+
}
|
|
696
|
+
return result;
|
|
697
|
+
}
|
|
698
|
+
async function fitSummarize(messages, system) {
|
|
699
|
+
if (!config.summarizer)
|
|
700
|
+
return fitTruncate(messages, system);
|
|
701
|
+
let available = budget;
|
|
702
|
+
if (system)
|
|
703
|
+
available -= countTokens(system);
|
|
704
|
+
const total = estimateTokens(messages);
|
|
705
|
+
if (total <= available)
|
|
706
|
+
return messages;
|
|
707
|
+
const keepCount = Math.max(1, Math.floor(messages.length / 3));
|
|
708
|
+
const toSummarize = messages.slice(0, messages.length - keepCount);
|
|
709
|
+
const toKeep = messages.slice(messages.length - keepCount);
|
|
710
|
+
const summary = await config.summarizer(toSummarize);
|
|
711
|
+
const summaryMsg = {
|
|
712
|
+
role: "system",
|
|
713
|
+
content: `Previous conversation summary: ${summary}`
|
|
714
|
+
};
|
|
715
|
+
return [summaryMsg, ...toKeep];
|
|
716
|
+
}
|
|
717
|
+
async function fitSlidingWindow(messages, system) {
|
|
718
|
+
let available = budget;
|
|
719
|
+
if (system)
|
|
720
|
+
available -= countTokens(system);
|
|
721
|
+
const result = [];
|
|
722
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
723
|
+
const tokens = estimateMessageTokens(messages[i]);
|
|
724
|
+
if (available - tokens < 0 && result.length > 0)
|
|
725
|
+
break;
|
|
726
|
+
available -= tokens;
|
|
727
|
+
result.unshift(messages[i]);
|
|
728
|
+
}
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
estimateTokens,
|
|
733
|
+
async fit(messages, system) {
|
|
734
|
+
const total = estimateTokens(messages);
|
|
735
|
+
let available = budget;
|
|
736
|
+
if (system)
|
|
737
|
+
available -= countTokens(system);
|
|
738
|
+
if (total <= available)
|
|
739
|
+
return messages;
|
|
740
|
+
switch (strategy) {
|
|
741
|
+
case "truncate":
|
|
742
|
+
return fitTruncate(messages, system);
|
|
743
|
+
case "summarize":
|
|
744
|
+
return fitSummarize(messages, system);
|
|
745
|
+
case "sliding-window":
|
|
746
|
+
return fitSlidingWindow(messages, system);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
505
751
|
// ../core/src/circuit-breaker.ts
|
|
506
752
|
function defaultShouldCount(error) {
|
|
507
753
|
if (error && typeof error === "object" && "retryable" in error) {
|
|
@@ -630,6 +876,101 @@ function createCircuitBreaker(config) {
|
|
|
630
876
|
}
|
|
631
877
|
};
|
|
632
878
|
}
|
|
879
|
+
// ../core/src/shutdown.ts
|
|
880
|
+
function createShutdownManager(config) {
|
|
881
|
+
const drainTimeoutMs = config?.drainTimeoutMs ?? 30000;
|
|
882
|
+
const signals = config?.signals ?? ["SIGTERM", "SIGINT"];
|
|
883
|
+
if (drainTimeoutMs < 0 || !Number.isFinite(drainTimeoutMs)) {
|
|
884
|
+
throw new ElsiumError({
|
|
885
|
+
code: "CONFIG_ERROR",
|
|
886
|
+
message: "drainTimeoutMs must be >= 0 and finite",
|
|
887
|
+
retryable: false
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
let shuttingDown = false;
|
|
891
|
+
let inFlightCount = 0;
|
|
892
|
+
let drainResolve = null;
|
|
893
|
+
let shutdownPromise = null;
|
|
894
|
+
const signalHandlers = [];
|
|
895
|
+
function checkDrained() {
|
|
896
|
+
if (inFlightCount === 0 && drainResolve) {
|
|
897
|
+
drainResolve();
|
|
898
|
+
drainResolve = null;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async function shutdown() {
|
|
902
|
+
if (shutdownPromise)
|
|
903
|
+
return shutdownPromise;
|
|
904
|
+
shuttingDown = true;
|
|
905
|
+
shutdownPromise = (async () => {
|
|
906
|
+
config?.onDrainStart?.();
|
|
907
|
+
if (inFlightCount === 0) {
|
|
908
|
+
config?.onDrainComplete?.();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const drainPromise = new Promise((resolve) => {
|
|
912
|
+
drainResolve = resolve;
|
|
913
|
+
});
|
|
914
|
+
let drainTimer;
|
|
915
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
916
|
+
drainTimer = setTimeout(() => resolve("timeout"), drainTimeoutMs);
|
|
917
|
+
});
|
|
918
|
+
const result = await Promise.race([
|
|
919
|
+
drainPromise.then(() => "drained"),
|
|
920
|
+
timeoutPromise
|
|
921
|
+
]);
|
|
922
|
+
if (drainTimer !== undefined)
|
|
923
|
+
clearTimeout(drainTimer);
|
|
924
|
+
if (result === "timeout") {
|
|
925
|
+
config?.onForceShutdown?.();
|
|
926
|
+
} else {
|
|
927
|
+
config?.onDrainComplete?.();
|
|
928
|
+
}
|
|
929
|
+
})();
|
|
930
|
+
return shutdownPromise;
|
|
931
|
+
}
|
|
932
|
+
const manager = {
|
|
933
|
+
get inFlight() {
|
|
934
|
+
return inFlightCount;
|
|
935
|
+
},
|
|
936
|
+
get isShuttingDown() {
|
|
937
|
+
return shuttingDown;
|
|
938
|
+
},
|
|
939
|
+
async trackOperation(fn) {
|
|
940
|
+
if (shuttingDown) {
|
|
941
|
+
throw new ElsiumError({
|
|
942
|
+
code: "VALIDATION_ERROR",
|
|
943
|
+
message: "Server is shutting down, not accepting new operations",
|
|
944
|
+
retryable: true
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
inFlightCount++;
|
|
948
|
+
try {
|
|
949
|
+
return await fn();
|
|
950
|
+
} finally {
|
|
951
|
+
inFlightCount--;
|
|
952
|
+
checkDrained();
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
shutdown,
|
|
956
|
+
dispose() {
|
|
957
|
+
for (const { signal, handler } of signalHandlers) {
|
|
958
|
+
process.removeListener(signal, handler);
|
|
959
|
+
}
|
|
960
|
+
signalHandlers.length = 0;
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
if (typeof process !== "undefined" && process.on) {
|
|
964
|
+
for (const signal of signals) {
|
|
965
|
+
const handler = () => {
|
|
966
|
+
manager.shutdown();
|
|
967
|
+
};
|
|
968
|
+
signalHandlers.push({ signal, handler });
|
|
969
|
+
process.on(signal, handler);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return manager;
|
|
973
|
+
}
|
|
633
974
|
// ../gateway/src/provider.ts
|
|
634
975
|
var providerRegistry = new Map;
|
|
635
976
|
var metadataRegistry = new Map;
|
|
@@ -659,16 +1000,16 @@ function composeMiddleware(middlewares) {
|
|
|
659
1000
|
};
|
|
660
1001
|
}
|
|
661
1002
|
function loggingMiddleware(logger) {
|
|
662
|
-
const
|
|
1003
|
+
const log3 = logger ?? createLogger({ level: "info" });
|
|
663
1004
|
return async (ctx, next) => {
|
|
664
|
-
|
|
1005
|
+
log3.info("LLM request", {
|
|
665
1006
|
provider: ctx.provider,
|
|
666
1007
|
model: ctx.model,
|
|
667
1008
|
traceId: ctx.traceId,
|
|
668
1009
|
messageCount: ctx.request.messages.length
|
|
669
1010
|
});
|
|
670
1011
|
const response = await next(ctx);
|
|
671
|
-
|
|
1012
|
+
log3.info("LLM response", {
|
|
672
1013
|
provider: ctx.provider,
|
|
673
1014
|
model: ctx.model,
|
|
674
1015
|
traceId: ctx.traceId,
|
|
@@ -804,7 +1145,7 @@ function xrayMiddleware(options = {}) {
|
|
|
804
1145
|
}
|
|
805
1146
|
|
|
806
1147
|
// ../gateway/src/pricing.ts
|
|
807
|
-
var
|
|
1148
|
+
var log3 = createLogger();
|
|
808
1149
|
var PRICING = {
|
|
809
1150
|
"claude-opus-4-6": { inputPerMillion: 15, outputPerMillion: 75 },
|
|
810
1151
|
"claude-sonnet-4-6": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
@@ -842,7 +1183,7 @@ function resolveModelName(model) {
|
|
|
842
1183
|
function calculateCost(model, usage) {
|
|
843
1184
|
const pricing = PRICING[resolveModelName(model)];
|
|
844
1185
|
if (!pricing) {
|
|
845
|
-
|
|
1186
|
+
log3.warn(`Unknown model "${model}" — cost will be reported as $0. Register pricing with registerPricing().`);
|
|
846
1187
|
return {
|
|
847
1188
|
inputCost: 0,
|
|
848
1189
|
outputCost: 0,
|
|
@@ -936,15 +1277,33 @@ function createAnthropicProvider(config) {
|
|
|
936
1277
|
if (part.type === "text")
|
|
937
1278
|
return { type: "text", text: part.text };
|
|
938
1279
|
if (part.type === "image" && part.source?.type === "base64") {
|
|
1280
|
+
const src = part.source;
|
|
939
1281
|
return {
|
|
940
1282
|
type: "image",
|
|
941
1283
|
source: {
|
|
942
1284
|
type: "base64",
|
|
943
|
-
media_type:
|
|
944
|
-
data:
|
|
1285
|
+
media_type: src.mediaType,
|
|
1286
|
+
data: src.data
|
|
945
1287
|
}
|
|
946
1288
|
};
|
|
947
1289
|
}
|
|
1290
|
+
if (part.type === "document" && part.source) {
|
|
1291
|
+
if (part.source.type === "base64") {
|
|
1292
|
+
const src = part.source;
|
|
1293
|
+
return {
|
|
1294
|
+
type: "document",
|
|
1295
|
+
source: {
|
|
1296
|
+
type: "base64",
|
|
1297
|
+
media_type: src.mediaType,
|
|
1298
|
+
data: src.data
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
return { type: "text", text: "[document: url source not supported by Anthropic]" };
|
|
1303
|
+
}
|
|
1304
|
+
if (part.type === "audio") {
|
|
1305
|
+
return { type: "text", text: "[audio content not supported by this provider]" };
|
|
1306
|
+
}
|
|
948
1307
|
return { type: "text", text: "[unsupported content]" };
|
|
949
1308
|
}
|
|
950
1309
|
function formatMultipartContent(msg, role) {
|
|
@@ -1053,6 +1412,16 @@ function createAnthropicProvider(config) {
|
|
|
1053
1412
|
const tools = formatTools(req.tools);
|
|
1054
1413
|
if (tools)
|
|
1055
1414
|
body.tools = tools;
|
|
1415
|
+
if (req.schema) {
|
|
1416
|
+
const jsonSchema = zodToJsonSchema(req.schema);
|
|
1417
|
+
const structuredTool = {
|
|
1418
|
+
name: "_structured_output",
|
|
1419
|
+
description: "Return structured output matching the required schema",
|
|
1420
|
+
input_schema: jsonSchema
|
|
1421
|
+
};
|
|
1422
|
+
body.tools = [...tools ?? [], structuredTool];
|
|
1423
|
+
body.tool_choice = { type: "tool", name: "_structured_output" };
|
|
1424
|
+
}
|
|
1056
1425
|
const startTime = performance.now();
|
|
1057
1426
|
const raw = await retry(async () => {
|
|
1058
1427
|
const controller = new AbortController;
|
|
@@ -1262,7 +1631,31 @@ function createGoogleProvider(config) {
|
|
|
1262
1631
|
return { role, parts };
|
|
1263
1632
|
}
|
|
1264
1633
|
function formatGeminiMultipartContent(msg, role) {
|
|
1265
|
-
const parts =
|
|
1634
|
+
const parts = [];
|
|
1635
|
+
for (const p of msg.content) {
|
|
1636
|
+
if (p.type === "text") {
|
|
1637
|
+
parts.push({ text: p.text });
|
|
1638
|
+
} else if (p.type === "image") {
|
|
1639
|
+
const img = p;
|
|
1640
|
+
if (img.source.type === "base64") {
|
|
1641
|
+
parts.push({ inlineData: { mimeType: img.source.mediaType, data: img.source.data } });
|
|
1642
|
+
} else {
|
|
1643
|
+
parts.push({ fileData: { mimeType: "image/jpeg", fileUri: img.source.url } });
|
|
1644
|
+
}
|
|
1645
|
+
} else if (p.type === "audio" || p.type === "document") {
|
|
1646
|
+
const media = p;
|
|
1647
|
+
if (media.source.type === "base64") {
|
|
1648
|
+
parts.push({
|
|
1649
|
+
inlineData: { mimeType: media.source.mediaType, data: media.source.data }
|
|
1650
|
+
});
|
|
1651
|
+
} else {
|
|
1652
|
+
const urlSource = media.source;
|
|
1653
|
+
parts.push({
|
|
1654
|
+
fileData: { mimeType: "application/octet-stream", fileUri: urlSource.url }
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1266
1659
|
return { role, parts };
|
|
1267
1660
|
}
|
|
1268
1661
|
function formatMessages(messages) {
|
|
@@ -1359,6 +1752,10 @@ function createGoogleProvider(config) {
|
|
|
1359
1752
|
config2.topP = req.topP;
|
|
1360
1753
|
if (req.stopSequences?.length)
|
|
1361
1754
|
config2.stopSequences = req.stopSequences;
|
|
1755
|
+
if (req.schema) {
|
|
1756
|
+
config2.responseMimeType = "application/json";
|
|
1757
|
+
config2.responseSchema = zodToJsonSchema(req.schema);
|
|
1758
|
+
}
|
|
1362
1759
|
return config2;
|
|
1363
1760
|
}
|
|
1364
1761
|
function buildRequestBody(req) {
|
|
@@ -1437,7 +1834,8 @@ async function handleGoogleErrorResponse(response) {
|
|
|
1437
1834
|
throw ElsiumError.authError("google");
|
|
1438
1835
|
}
|
|
1439
1836
|
if (response.status === 429) {
|
|
1440
|
-
|
|
1837
|
+
const retryAfter = response.headers.get("retry-after");
|
|
1838
|
+
throw ElsiumError.rateLimit("google", retryAfter ? Number.parseInt(retryAfter) * 1000 : undefined);
|
|
1441
1839
|
}
|
|
1442
1840
|
throw ElsiumError.providerError(`Google API error ${response.status}: ${errorBody}`, {
|
|
1443
1841
|
provider: "google",
|
|
@@ -1623,6 +2021,43 @@ function createOpenAIProvider(config) {
|
|
|
1623
2021
|
}
|
|
1624
2022
|
return openaiMsg;
|
|
1625
2023
|
}
|
|
2024
|
+
function formatUserContent(msg) {
|
|
2025
|
+
if (typeof msg.content === "string")
|
|
2026
|
+
return msg.content;
|
|
2027
|
+
const parts = [];
|
|
2028
|
+
for (const part of msg.content) {
|
|
2029
|
+
if (part.type === "text") {
|
|
2030
|
+
parts.push({ type: "text", text: part.text });
|
|
2031
|
+
} else if (part.type === "image") {
|
|
2032
|
+
if (part.source.type === "base64") {
|
|
2033
|
+
const url = `data:${part.source.mediaType};base64,${part.source.data}`;
|
|
2034
|
+
parts.push({ type: "image_url", image_url: { url } });
|
|
2035
|
+
} else {
|
|
2036
|
+
parts.push({ type: "image_url", image_url: { url: part.source.url } });
|
|
2037
|
+
}
|
|
2038
|
+
} else if (part.type === "audio") {
|
|
2039
|
+
if (part.source.type === "base64") {
|
|
2040
|
+
const format = part.source.mediaType.split("/")[1] ?? "wav";
|
|
2041
|
+
parts.push({
|
|
2042
|
+
type: "input_audio",
|
|
2043
|
+
input_audio: { data: part.source.data, format }
|
|
2044
|
+
});
|
|
2045
|
+
} else {
|
|
2046
|
+
parts.push({ type: "text", text: "[audio: url source requires file upload]" });
|
|
2047
|
+
}
|
|
2048
|
+
} else if (part.type === "document") {
|
|
2049
|
+
if (part.source.type === "base64") {
|
|
2050
|
+
parts.push({
|
|
2051
|
+
type: "text",
|
|
2052
|
+
text: `[document: ${part.source.mediaType} content attached as base64]`
|
|
2053
|
+
});
|
|
2054
|
+
} else {
|
|
2055
|
+
parts.push({ type: "text", text: `[document: ${part.source.url}]` });
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return parts;
|
|
2060
|
+
}
|
|
1626
2061
|
function formatMessages(messages) {
|
|
1627
2062
|
const formatted = [];
|
|
1628
2063
|
for (const msg of messages) {
|
|
@@ -1638,7 +2073,7 @@ function createOpenAIProvider(config) {
|
|
|
1638
2073
|
formatted.push(formatAssistantMessage(msg));
|
|
1639
2074
|
continue;
|
|
1640
2075
|
}
|
|
1641
|
-
formatted.push({ role: "user", content:
|
|
2076
|
+
formatted.push({ role: "user", content: formatUserContent(msg) });
|
|
1642
2077
|
}
|
|
1643
2078
|
return formatted;
|
|
1644
2079
|
}
|
|
@@ -1715,6 +2150,17 @@ function createOpenAIProvider(config) {
|
|
|
1715
2150
|
const tools = formatTools(req.tools);
|
|
1716
2151
|
if (tools)
|
|
1717
2152
|
body.tools = tools;
|
|
2153
|
+
if (req.schema) {
|
|
2154
|
+
const jsonSchema = zodToJsonSchema(req.schema);
|
|
2155
|
+
body.response_format = {
|
|
2156
|
+
type: "json_schema",
|
|
2157
|
+
json_schema: {
|
|
2158
|
+
name: "structured_output",
|
|
2159
|
+
strict: true,
|
|
2160
|
+
schema: jsonSchema
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
1718
2164
|
const startTime = performance.now();
|
|
1719
2165
|
const raw = await retry(async () => {
|
|
1720
2166
|
const controller = new AbortController;
|
|
@@ -1877,7 +2323,7 @@ var PROVIDER_FACTORIES = {
|
|
|
1877
2323
|
function registerProviderFactory(name, factory) {
|
|
1878
2324
|
PROVIDER_FACTORIES[name] = factory;
|
|
1879
2325
|
}
|
|
1880
|
-
function
|
|
2326
|
+
function validateGatewayConfig(config) {
|
|
1881
2327
|
const factory = PROVIDER_FACTORIES[config.provider];
|
|
1882
2328
|
if (!factory) {
|
|
1883
2329
|
throw new ElsiumError({
|
|
@@ -1886,21 +2332,92 @@ function gateway(config) {
|
|
|
1886
2332
|
retryable: false
|
|
1887
2333
|
});
|
|
1888
2334
|
}
|
|
2335
|
+
if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
|
|
2336
|
+
throw new ElsiumError({
|
|
2337
|
+
code: "CONFIG_ERROR",
|
|
2338
|
+
message: "apiKey must be a non-empty string",
|
|
2339
|
+
retryable: false
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
if (config.timeout !== undefined && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
|
|
2343
|
+
throw new ElsiumError({
|
|
2344
|
+
code: "CONFIG_ERROR",
|
|
2345
|
+
message: "timeout must be a positive finite number",
|
|
2346
|
+
retryable: false
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
if (config.maxRetries !== undefined && (!Number.isFinite(config.maxRetries) || !Number.isInteger(config.maxRetries) || config.maxRetries < 0)) {
|
|
2350
|
+
throw new ElsiumError({
|
|
2351
|
+
code: "CONFIG_ERROR",
|
|
2352
|
+
message: "maxRetries must be a non-negative finite integer",
|
|
2353
|
+
retryable: false
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
return factory;
|
|
2357
|
+
}
|
|
2358
|
+
function autoRegisterProvider(provider) {
|
|
2359
|
+
if (!provider.metadata)
|
|
2360
|
+
return;
|
|
2361
|
+
registerProviderMetadata(provider.name, provider.metadata);
|
|
2362
|
+
if (!provider.metadata.pricing)
|
|
2363
|
+
return;
|
|
2364
|
+
for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
|
|
2365
|
+
registerPricing(model, pricing);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function validateRequestLimits(request, maxMessages, maxInputTokens) {
|
|
2369
|
+
if (request.messages.length > maxMessages) {
|
|
2370
|
+
throw ElsiumError.validation(`Message count ${request.messages.length} exceeds limit of ${maxMessages}`);
|
|
2371
|
+
}
|
|
2372
|
+
let estimatedTokens = 0;
|
|
2373
|
+
for (const msg of request.messages) {
|
|
2374
|
+
const text = typeof msg.content === "string" ? msg.content : msg.content.map((p) => p.type === "text" ? p.text : "").join("");
|
|
2375
|
+
estimatedTokens += Math.ceil(text.length / 4);
|
|
2376
|
+
}
|
|
2377
|
+
if (estimatedTokens > maxInputTokens) {
|
|
2378
|
+
throw ElsiumError.validation(`Estimated input tokens (~${estimatedTokens}) exceeds limit of ${maxInputTokens}`);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
function buildMiddlewareContext(req, providerName, defaultModel, metadata) {
|
|
2382
|
+
return {
|
|
2383
|
+
request: req,
|
|
2384
|
+
provider: providerName,
|
|
2385
|
+
model: req.model ?? defaultModel,
|
|
2386
|
+
traceId: generateTraceId(),
|
|
2387
|
+
startTime: performance.now(),
|
|
2388
|
+
metadata
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
async function accumulateStreamEvents(stream, emit) {
|
|
2392
|
+
let textContent = "";
|
|
2393
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
2394
|
+
let stopReason = "end_turn";
|
|
2395
|
+
let id = "";
|
|
2396
|
+
for await (const event of stream) {
|
|
2397
|
+
emit(event);
|
|
2398
|
+
if (event.type === "text_delta") {
|
|
2399
|
+
textContent += event.text;
|
|
2400
|
+
} else if (event.type === "message_end") {
|
|
2401
|
+
usage = event.usage;
|
|
2402
|
+
stopReason = event.stopReason;
|
|
2403
|
+
} else if (event.type === "message_start") {
|
|
2404
|
+
id = event.id;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return { textContent, usage, stopReason, id };
|
|
2408
|
+
}
|
|
2409
|
+
function gateway(config) {
|
|
2410
|
+
const factory = validateGatewayConfig(config);
|
|
1889
2411
|
const provider = factory({
|
|
1890
2412
|
apiKey: config.apiKey,
|
|
1891
2413
|
baseUrl: config.baseUrl,
|
|
1892
2414
|
timeout: config.timeout,
|
|
1893
2415
|
maxRetries: config.maxRetries
|
|
1894
2416
|
});
|
|
1895
|
-
|
|
1896
|
-
registerProviderMetadata(provider.name, provider.metadata);
|
|
1897
|
-
if (provider.metadata.pricing) {
|
|
1898
|
-
for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
|
|
1899
|
-
registerPricing(model, pricing);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
2417
|
+
autoRegisterProvider(provider);
|
|
1903
2418
|
const defaultModel = config.model ?? provider.defaultModel;
|
|
2419
|
+
const maxMessages = config.maxMessages ?? 1000;
|
|
2420
|
+
const maxInputTokens = config.maxInputTokens ?? 1e6;
|
|
1904
2421
|
let xrayStore = null;
|
|
1905
2422
|
const allMiddleware = [...config.middleware ?? []];
|
|
1906
2423
|
if (config.xray) {
|
|
@@ -1915,14 +2432,7 @@ function gateway(config) {
|
|
|
1915
2432
|
if (!composedMiddleware) {
|
|
1916
2433
|
return provider.complete(req);
|
|
1917
2434
|
}
|
|
1918
|
-
const ctx = {
|
|
1919
|
-
request: req,
|
|
1920
|
-
provider: provider.name,
|
|
1921
|
-
model: req.model ?? defaultModel,
|
|
1922
|
-
traceId: generateTraceId(),
|
|
1923
|
-
startTime: performance.now(),
|
|
1924
|
-
metadata: request.metadata ?? {}
|
|
1925
|
-
};
|
|
2435
|
+
const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
|
|
1926
2436
|
return composedMiddleware(ctx, async (c) => provider.complete(c.request));
|
|
1927
2437
|
}
|
|
1928
2438
|
return {
|
|
@@ -1934,34 +2444,27 @@ function gateway(config) {
|
|
|
1934
2444
|
return xrayStore?.callHistory(limit) ?? [];
|
|
1935
2445
|
},
|
|
1936
2446
|
async complete(request) {
|
|
2447
|
+
validateRequestLimits(request, maxMessages, maxInputTokens);
|
|
1937
2448
|
return executeWithMiddleware(request);
|
|
1938
2449
|
},
|
|
1939
2450
|
stream(request) {
|
|
2451
|
+
validateRequestLimits(request, maxMessages, maxInputTokens);
|
|
1940
2452
|
const req = { ...request, model: request.model ?? defaultModel };
|
|
1941
2453
|
if (composedMiddleware) {
|
|
1942
|
-
const ctx = {
|
|
1943
|
-
request: req,
|
|
1944
|
-
provider: provider.name,
|
|
1945
|
-
model: req.model ?? defaultModel,
|
|
1946
|
-
traceId: generateTraceId(),
|
|
1947
|
-
startTime: performance.now(),
|
|
1948
|
-
metadata: request.metadata ?? {}
|
|
1949
|
-
};
|
|
2454
|
+
const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
|
|
1950
2455
|
return createStream(async (emit) => {
|
|
1951
2456
|
await composedMiddleware(ctx, async (c) => {
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
emit(event);
|
|
1955
|
-
}
|
|
2457
|
+
const result = await accumulateStreamEvents(provider.stream(c.request), emit);
|
|
2458
|
+
const latencyMs = Math.round(performance.now() - ctx.startTime);
|
|
1956
2459
|
return {
|
|
1957
|
-
id:
|
|
1958
|
-
message: { role: "assistant", content:
|
|
1959
|
-
usage:
|
|
1960
|
-
cost:
|
|
2460
|
+
id: result.id,
|
|
2461
|
+
message: { role: "assistant", content: result.textContent },
|
|
2462
|
+
usage: result.usage,
|
|
2463
|
+
cost: calculateCost(c.model, result.usage),
|
|
1961
2464
|
model: c.model,
|
|
1962
2465
|
provider: provider.name,
|
|
1963
|
-
stopReason:
|
|
1964
|
-
latencyMs
|
|
2466
|
+
stopReason: result.stopReason,
|
|
2467
|
+
latencyMs,
|
|
1965
2468
|
traceId: ctx.traceId
|
|
1966
2469
|
};
|
|
1967
2470
|
});
|
|
@@ -1971,107 +2474,46 @@ function gateway(config) {
|
|
|
1971
2474
|
},
|
|
1972
2475
|
async generate(request) {
|
|
1973
2476
|
const { schema, ...rest } = request;
|
|
1974
|
-
const jsonSchema =
|
|
1975
|
-
const systemPrompt = [
|
|
1976
|
-
rest.system ?? "",
|
|
1977
|
-
"You MUST respond with valid JSON matching this schema:",
|
|
1978
|
-
JSON.stringify(jsonSchema, null, 2),
|
|
1979
|
-
"Respond ONLY with the JSON object, no markdown or explanation."
|
|
1980
|
-
].filter(Boolean).join(`
|
|
1981
|
-
|
|
1982
|
-
`);
|
|
2477
|
+
const jsonSchema = zodToJsonSchema(schema);
|
|
1983
2478
|
const response = await executeWithMiddleware({
|
|
1984
2479
|
...rest,
|
|
1985
|
-
|
|
2480
|
+
schema,
|
|
2481
|
+
system: [
|
|
2482
|
+
rest.system ?? "",
|
|
2483
|
+
"You MUST respond with valid JSON matching this schema:",
|
|
2484
|
+
JSON.stringify(jsonSchema, null, 2),
|
|
2485
|
+
"Respond ONLY with the JSON object, no markdown or explanation."
|
|
2486
|
+
].filter(Boolean).join(`
|
|
2487
|
+
|
|
2488
|
+
`)
|
|
1986
2489
|
});
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
}
|
|
2490
|
+
let parsed;
|
|
2491
|
+
if (response.stopReason === "tool_use" && response.message.toolCalls?.length) {
|
|
2492
|
+
const structuredCall = response.message.toolCalls.find((tc) => tc.name === "_structured_output");
|
|
2493
|
+
if (structuredCall) {
|
|
2494
|
+
parsed = structuredCall.arguments;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
if (parsed === undefined) {
|
|
2498
|
+
const text = typeof response.message.content === "string" ? response.message.content : "";
|
|
2499
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
2500
|
+
if (!jsonMatch) {
|
|
2501
|
+
throw ElsiumError.validation("LLM response did not contain valid JSON", {
|
|
2502
|
+
response: text
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
1993
2506
|
}
|
|
1994
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
1995
2507
|
const result = schema.safeParse(parsed);
|
|
1996
2508
|
if (!result.success) {
|
|
1997
2509
|
throw ElsiumError.validation("LLM response did not match schema", {
|
|
1998
|
-
errors: result.error.issues
|
|
1999
|
-
response: text
|
|
2510
|
+
errors: result.error.issues
|
|
2000
2511
|
});
|
|
2001
2512
|
}
|
|
2002
2513
|
return { data: result.data, response };
|
|
2003
2514
|
}
|
|
2004
2515
|
};
|
|
2005
2516
|
}
|
|
2006
|
-
function schemaToJsonSchema(schema) {
|
|
2007
|
-
try {
|
|
2008
|
-
if ("_def" in schema) {
|
|
2009
|
-
const def = schema._def;
|
|
2010
|
-
const result = convertZodDef(def);
|
|
2011
|
-
if (result)
|
|
2012
|
-
return result;
|
|
2013
|
-
}
|
|
2014
|
-
} catch {}
|
|
2015
|
-
return { type: "string" };
|
|
2016
|
-
}
|
|
2017
|
-
function zodDefKind(def) {
|
|
2018
|
-
return typeof def.type === "string" ? def.type : def.typeName;
|
|
2019
|
-
}
|
|
2020
|
-
function convertZodDef(def) {
|
|
2021
|
-
const kind = zodDefKind(def);
|
|
2022
|
-
switch (kind) {
|
|
2023
|
-
case "object":
|
|
2024
|
-
case "ZodObject":
|
|
2025
|
-
return convertZodObject(def);
|
|
2026
|
-
case "string":
|
|
2027
|
-
case "ZodString":
|
|
2028
|
-
return { type: "string" };
|
|
2029
|
-
case "number":
|
|
2030
|
-
case "ZodNumber":
|
|
2031
|
-
return { type: "number" };
|
|
2032
|
-
case "boolean":
|
|
2033
|
-
case "ZodBoolean":
|
|
2034
|
-
return { type: "boolean" };
|
|
2035
|
-
case "array":
|
|
2036
|
-
case "ZodArray":
|
|
2037
|
-
return convertZodArray(def);
|
|
2038
|
-
case "enum":
|
|
2039
|
-
case "ZodEnum": {
|
|
2040
|
-
const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
|
|
2041
|
-
return { type: "string", enum: values };
|
|
2042
|
-
}
|
|
2043
|
-
case "optional":
|
|
2044
|
-
case "ZodOptional":
|
|
2045
|
-
return convertZodOptional(def);
|
|
2046
|
-
default:
|
|
2047
|
-
return null;
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
function convertZodObject(def) {
|
|
2051
|
-
if (!def.shape)
|
|
2052
|
-
return null;
|
|
2053
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
2054
|
-
const properties = {};
|
|
2055
|
-
const required = [];
|
|
2056
|
-
for (const [key, value] of Object.entries(shape)) {
|
|
2057
|
-
properties[key] = schemaToJsonSchema(value);
|
|
2058
|
-
const valDef = value._def;
|
|
2059
|
-
const valKind = zodDefKind(valDef);
|
|
2060
|
-
if (valKind !== "optional" && valKind !== "ZodOptional") {
|
|
2061
|
-
required.push(key);
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
return { type: "object", properties, required };
|
|
2065
|
-
}
|
|
2066
|
-
function convertZodArray(def) {
|
|
2067
|
-
return {
|
|
2068
|
-
type: "array",
|
|
2069
|
-
items: schemaToJsonSchema(def.element ?? def.type)
|
|
2070
|
-
};
|
|
2071
|
-
}
|
|
2072
|
-
function convertZodOptional(def) {
|
|
2073
|
-
return schemaToJsonSchema(def.innerType ?? def.innerType);
|
|
2074
|
-
}
|
|
2075
2517
|
// ../gateway/src/security.ts
|
|
2076
2518
|
var INJECTION_PATTERNS = [
|
|
2077
2519
|
{
|
|
@@ -2245,75 +2687,396 @@ function redactSecrets(text, piiTypes) {
|
|
|
2245
2687
|
redacted = piiResult.redacted;
|
|
2246
2688
|
found.push(...piiResult.found);
|
|
2247
2689
|
}
|
|
2248
|
-
return { redacted, found };
|
|
2249
|
-
}
|
|
2250
|
-
function checkBlockedPatterns(text, patterns) {
|
|
2690
|
+
return { redacted, found };
|
|
2691
|
+
}
|
|
2692
|
+
function checkBlockedPatterns(text, patterns) {
|
|
2693
|
+
const violations = [];
|
|
2694
|
+
for (const pattern of patterns) {
|
|
2695
|
+
pattern.lastIndex = 0;
|
|
2696
|
+
if (pattern.test(text)) {
|
|
2697
|
+
violations.push({
|
|
2698
|
+
type: "blocked_pattern",
|
|
2699
|
+
detail: `Blocked pattern matched: ${pattern.source}`,
|
|
2700
|
+
severity: "medium"
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return violations;
|
|
2705
|
+
}
|
|
2706
|
+
function scanMessageForViolations(text, config) {
|
|
2707
|
+
const violations = [];
|
|
2708
|
+
if (config.promptInjection !== false) {
|
|
2709
|
+
violations.push(...detectPromptInjection(text));
|
|
2710
|
+
}
|
|
2711
|
+
if (config.jailbreakDetection) {
|
|
2712
|
+
violations.push(...detectJailbreak(text));
|
|
2713
|
+
}
|
|
2714
|
+
if (config.blockedPatterns?.length) {
|
|
2715
|
+
violations.push(...checkBlockedPatterns(text, config.blockedPatterns));
|
|
2716
|
+
}
|
|
2717
|
+
return violations;
|
|
2718
|
+
}
|
|
2719
|
+
function reportAndThrow(violations, config) {
|
|
2720
|
+
for (const v of violations) {
|
|
2721
|
+
config.onViolation?.(v);
|
|
2722
|
+
}
|
|
2723
|
+
throw ElsiumError.validation(`Security violation detected: ${violations.map((v) => v.detail).join("; ")}`);
|
|
2724
|
+
}
|
|
2725
|
+
function redactResponseSecrets(response, config) {
|
|
2726
|
+
const responseText = extractText(response.message.content);
|
|
2727
|
+
if (!responseText)
|
|
2728
|
+
return response;
|
|
2729
|
+
const { redacted, found } = redactSecrets(responseText, config.piiTypes);
|
|
2730
|
+
if (found.length === 0)
|
|
2731
|
+
return response;
|
|
2732
|
+
for (const v of found) {
|
|
2733
|
+
config.onViolation?.(v);
|
|
2734
|
+
}
|
|
2735
|
+
return {
|
|
2736
|
+
...response,
|
|
2737
|
+
message: {
|
|
2738
|
+
...response.message,
|
|
2739
|
+
content: redacted
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
function securityMiddleware(config) {
|
|
2744
|
+
return async (ctx, next) => {
|
|
2745
|
+
if (ctx.request.system) {
|
|
2746
|
+
const sysViolations = scanMessageForViolations(ctx.request.system, config);
|
|
2747
|
+
if (sysViolations.length > 0) {
|
|
2748
|
+
reportAndThrow(sysViolations, config);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
for (const message of ctx.request.messages) {
|
|
2752
|
+
const text = extractText(message.content);
|
|
2753
|
+
if (!text)
|
|
2754
|
+
continue;
|
|
2755
|
+
const violations = scanMessageForViolations(text, config);
|
|
2756
|
+
if (violations.length > 0) {
|
|
2757
|
+
reportAndThrow(violations, config);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
const response = await next(ctx);
|
|
2761
|
+
if (config.secretRedaction !== false) {
|
|
2762
|
+
return redactResponseSecrets(response, config);
|
|
2763
|
+
}
|
|
2764
|
+
return response;
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
// ../gateway/src/cache.ts
|
|
2768
|
+
import { createHash } from "node:crypto";
|
|
2769
|
+
var log4 = createLogger();
|
|
2770
|
+
function createInMemoryCache(maxSize = 1000) {
|
|
2771
|
+
const cache = new Map;
|
|
2772
|
+
function evict() {
|
|
2773
|
+
if (cache.size <= maxSize)
|
|
2774
|
+
return;
|
|
2775
|
+
const firstKey = cache.keys().next().value;
|
|
2776
|
+
if (firstKey !== undefined)
|
|
2777
|
+
cache.delete(firstKey);
|
|
2778
|
+
}
|
|
2779
|
+
return {
|
|
2780
|
+
async get(key) {
|
|
2781
|
+
const entry = cache.get(key);
|
|
2782
|
+
if (!entry)
|
|
2783
|
+
return null;
|
|
2784
|
+
if (Date.now() > entry.expiresAt) {
|
|
2785
|
+
cache.delete(key);
|
|
2786
|
+
return null;
|
|
2787
|
+
}
|
|
2788
|
+
cache.delete(key);
|
|
2789
|
+
cache.set(key, entry);
|
|
2790
|
+
return entry.value;
|
|
2791
|
+
},
|
|
2792
|
+
async set(key, value, ttlMs) {
|
|
2793
|
+
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
2794
|
+
evict();
|
|
2795
|
+
},
|
|
2796
|
+
async delete(key) {
|
|
2797
|
+
cache.delete(key);
|
|
2798
|
+
},
|
|
2799
|
+
async clear() {
|
|
2800
|
+
cache.clear();
|
|
2801
|
+
}
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
function defaultCacheKey(ctx) {
|
|
2805
|
+
const data = JSON.stringify({
|
|
2806
|
+
provider: ctx.provider,
|
|
2807
|
+
model: ctx.model,
|
|
2808
|
+
messages: ctx.request.messages,
|
|
2809
|
+
system: ctx.request.system,
|
|
2810
|
+
temperature: ctx.request.temperature
|
|
2811
|
+
});
|
|
2812
|
+
return createHash("sha256").update(data).digest("hex");
|
|
2813
|
+
}
|
|
2814
|
+
function defaultShouldCache(_ctx, response) {
|
|
2815
|
+
const temp = _ctx.request.temperature;
|
|
2816
|
+
if (temp !== undefined && temp !== 0)
|
|
2817
|
+
return false;
|
|
2818
|
+
return response.stopReason === "end_turn";
|
|
2819
|
+
}
|
|
2820
|
+
function cacheMiddleware(config) {
|
|
2821
|
+
const ttlMs = config?.ttlMs ?? 3600000;
|
|
2822
|
+
const adapter = config?.adapter ?? createInMemoryCache(config?.maxSize ?? 1000);
|
|
2823
|
+
const keyFn = config?.keyFn ?? defaultCacheKey;
|
|
2824
|
+
const shouldCache = config?.shouldCache ?? defaultShouldCache;
|
|
2825
|
+
let hits = 0;
|
|
2826
|
+
let misses = 0;
|
|
2827
|
+
const middleware = async (ctx, next) => {
|
|
2828
|
+
if (ctx.request.stream) {
|
|
2829
|
+
return next(ctx);
|
|
2830
|
+
}
|
|
2831
|
+
const key = keyFn(ctx);
|
|
2832
|
+
const cached = await adapter.get(key);
|
|
2833
|
+
if (cached) {
|
|
2834
|
+
hits++;
|
|
2835
|
+
log4.debug("Cache hit", { key: key.slice(0, 8), provider: ctx.provider });
|
|
2836
|
+
return cached;
|
|
2837
|
+
}
|
|
2838
|
+
misses++;
|
|
2839
|
+
const response = await next(ctx);
|
|
2840
|
+
if (shouldCache(ctx, response)) {
|
|
2841
|
+
await adapter.set(key, response, ttlMs);
|
|
2842
|
+
}
|
|
2843
|
+
return response;
|
|
2844
|
+
};
|
|
2845
|
+
return Object.assign(middleware, {
|
|
2846
|
+
adapter,
|
|
2847
|
+
stats() {
|
|
2848
|
+
const total = hits + misses;
|
|
2849
|
+
return {
|
|
2850
|
+
hits,
|
|
2851
|
+
misses,
|
|
2852
|
+
size: 0,
|
|
2853
|
+
hitRate: total > 0 ? hits / total : 0
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
// ../gateway/src/output-guardrails.ts
|
|
2859
|
+
var log5 = createLogger();
|
|
2860
|
+
var PII_PATTERNS2 = [
|
|
2861
|
+
{
|
|
2862
|
+
pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
2863
|
+
label: "email",
|
|
2864
|
+
replacement: "[REDACTED_EMAIL]"
|
|
2865
|
+
},
|
|
2866
|
+
{
|
|
2867
|
+
pattern: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
|
|
2868
|
+
label: "phone",
|
|
2869
|
+
replacement: "[REDACTED_PHONE]"
|
|
2870
|
+
},
|
|
2871
|
+
{
|
|
2872
|
+
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
2873
|
+
label: "ssn",
|
|
2874
|
+
replacement: "[REDACTED_SSN]"
|
|
2875
|
+
},
|
|
2876
|
+
{
|
|
2877
|
+
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
|
|
2878
|
+
label: "credit_card",
|
|
2879
|
+
replacement: "[REDACTED_CC]"
|
|
2880
|
+
}
|
|
2881
|
+
];
|
|
2882
|
+
var SECRET_PATTERNS2 = [
|
|
2883
|
+
{
|
|
2884
|
+
pattern: /\bsk-[A-Za-z0-9]{20,}\b/g,
|
|
2885
|
+
label: "api_key",
|
|
2886
|
+
replacement: "[REDACTED_API_KEY]"
|
|
2887
|
+
},
|
|
2888
|
+
{
|
|
2889
|
+
pattern: /\bpk-[A-Za-z0-9]{20,}\b/g,
|
|
2890
|
+
label: "api_key",
|
|
2891
|
+
replacement: "[REDACTED_API_KEY]"
|
|
2892
|
+
},
|
|
2893
|
+
{
|
|
2894
|
+
pattern: /\bAKIA[A-Z0-9]{16}\b/g,
|
|
2895
|
+
label: "aws_key",
|
|
2896
|
+
replacement: "[REDACTED_AWS_KEY]"
|
|
2897
|
+
}
|
|
2898
|
+
];
|
|
2899
|
+
function detectPII(text) {
|
|
2251
2900
|
const violations = [];
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2901
|
+
const normalized = text.normalize("NFKC");
|
|
2902
|
+
for (const { pattern, label } of [...PII_PATTERNS2, ...SECRET_PATTERNS2]) {
|
|
2903
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
2904
|
+
if (regex.test(normalized)) {
|
|
2255
2905
|
violations.push({
|
|
2256
|
-
type: "
|
|
2257
|
-
detail: `
|
|
2258
|
-
|
|
2906
|
+
type: "pii",
|
|
2907
|
+
detail: `Detected ${label} in output`,
|
|
2908
|
+
pattern: label
|
|
2259
2909
|
});
|
|
2260
2910
|
}
|
|
2261
2911
|
}
|
|
2262
2912
|
return violations;
|
|
2263
2913
|
}
|
|
2264
|
-
function
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
if (config.jailbreakDetection) {
|
|
2270
|
-
violations.push(...detectJailbreak(text));
|
|
2914
|
+
function redactPII(text) {
|
|
2915
|
+
let result = text;
|
|
2916
|
+
for (const { pattern, replacement } of [...PII_PATTERNS2, ...SECRET_PATTERNS2]) {
|
|
2917
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
2918
|
+
result = result.replace(regex, replacement);
|
|
2271
2919
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2920
|
+
return result;
|
|
2921
|
+
}
|
|
2922
|
+
function checkContentPolicy(text, policy) {
|
|
2923
|
+
const violations = [];
|
|
2924
|
+
if (policy.maxResponseLength && text.length > policy.maxResponseLength) {
|
|
2925
|
+
violations.push({
|
|
2926
|
+
type: "content_policy",
|
|
2927
|
+
detail: `Response length ${text.length} exceeds max ${policy.maxResponseLength}`
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
if (policy.blockedPatterns) {
|
|
2931
|
+
for (const pattern of policy.blockedPatterns) {
|
|
2932
|
+
if (pattern.test(text)) {
|
|
2933
|
+
violations.push({
|
|
2934
|
+
type: "content_policy",
|
|
2935
|
+
detail: `Response matches blocked pattern: ${pattern.source}`,
|
|
2936
|
+
pattern: pattern.source
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2274
2940
|
}
|
|
2275
2941
|
return violations;
|
|
2276
2942
|
}
|
|
2277
|
-
function
|
|
2278
|
-
|
|
2279
|
-
|
|
2943
|
+
function checkCustomRules(text, rules) {
|
|
2944
|
+
const violations = [];
|
|
2945
|
+
for (const rule of rules) {
|
|
2946
|
+
if (rule.pattern.test(text)) {
|
|
2947
|
+
violations.push({
|
|
2948
|
+
type: "custom_rule",
|
|
2949
|
+
detail: rule.message ?? `Output matched custom rule: ${rule.name}`,
|
|
2950
|
+
pattern: rule.pattern.source
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2280
2953
|
}
|
|
2281
|
-
|
|
2954
|
+
return violations;
|
|
2282
2955
|
}
|
|
2283
|
-
function
|
|
2284
|
-
const
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
...
|
|
2297
|
-
|
|
2956
|
+
function outputGuardrailMiddleware(config) {
|
|
2957
|
+
const mode = config.onViolation ?? "block";
|
|
2958
|
+
return async (ctx, next) => {
|
|
2959
|
+
const response = await next(ctx);
|
|
2960
|
+
const text = extractText(response.message.content);
|
|
2961
|
+
const violations = [];
|
|
2962
|
+
if (config.piiDetection) {
|
|
2963
|
+
violations.push(...detectPII(text));
|
|
2964
|
+
}
|
|
2965
|
+
if (config.contentPolicy) {
|
|
2966
|
+
violations.push(...checkContentPolicy(text, config.contentPolicy));
|
|
2967
|
+
}
|
|
2968
|
+
if (config.customRules?.length) {
|
|
2969
|
+
violations.push(...checkCustomRules(text, config.customRules));
|
|
2970
|
+
}
|
|
2971
|
+
if (violations.length === 0)
|
|
2972
|
+
return response;
|
|
2973
|
+
for (const v of violations) {
|
|
2974
|
+
config.onViolationCallback?.(v);
|
|
2975
|
+
}
|
|
2976
|
+
switch (mode) {
|
|
2977
|
+
case "block":
|
|
2978
|
+
throw ElsiumError.validation(`Output guardrail violation: ${violations.map((v) => v.detail).join("; ")}`, { violations });
|
|
2979
|
+
case "redact": {
|
|
2980
|
+
let redacted = text;
|
|
2981
|
+
if (config.piiDetection) {
|
|
2982
|
+
redacted = redactPII(redacted);
|
|
2983
|
+
}
|
|
2984
|
+
return {
|
|
2985
|
+
...response,
|
|
2986
|
+
message: { ...response.message, content: redacted }
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
case "warn":
|
|
2990
|
+
log5.warn("Output guardrail violations detected", { violations });
|
|
2991
|
+
return response;
|
|
2298
2992
|
}
|
|
2299
2993
|
};
|
|
2300
2994
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2995
|
+
// ../gateway/src/batch.ts
|
|
2996
|
+
var log6 = createLogger();
|
|
2997
|
+
function createBatch(gateway2, config) {
|
|
2998
|
+
const concurrency = config?.concurrency ?? 5;
|
|
2999
|
+
const retryPerItem = config?.retryPerItem ?? 0;
|
|
3000
|
+
return {
|
|
3001
|
+
async execute(requests) {
|
|
3002
|
+
const startTime = performance.now();
|
|
3003
|
+
const results = new Array(requests.length);
|
|
3004
|
+
let completed = 0;
|
|
3005
|
+
let totalSucceeded = 0;
|
|
3006
|
+
let totalFailed = 0;
|
|
3007
|
+
let running = 0;
|
|
3008
|
+
let nextIndex = 0;
|
|
3009
|
+
const signal = config?.signal;
|
|
3010
|
+
async function processItem(index) {
|
|
3011
|
+
if (signal?.aborted) {
|
|
3012
|
+
results[index] = {
|
|
3013
|
+
index,
|
|
3014
|
+
success: false,
|
|
3015
|
+
error: "Batch cancelled"
|
|
3016
|
+
};
|
|
3017
|
+
totalFailed++;
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
let lastError;
|
|
3021
|
+
for (let attempt = 0;attempt <= retryPerItem; attempt++) {
|
|
3022
|
+
try {
|
|
3023
|
+
const response = await gateway2.complete(requests[index]);
|
|
3024
|
+
results[index] = { index, success: true, response };
|
|
3025
|
+
totalSucceeded++;
|
|
3026
|
+
return;
|
|
3027
|
+
} catch (err2) {
|
|
3028
|
+
lastError = err2 instanceof Error ? err2.message : String(err2);
|
|
3029
|
+
if (attempt < retryPerItem && err2 instanceof ElsiumError && err2.retryable) {
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
break;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
results[index] = { index, success: false, error: lastError };
|
|
3036
|
+
totalFailed++;
|
|
2310
3037
|
}
|
|
3038
|
+
return new Promise((resolve) => {
|
|
3039
|
+
function scheduleNext() {
|
|
3040
|
+
while (running < concurrency && nextIndex < requests.length) {
|
|
3041
|
+
if (signal?.aborted) {
|
|
3042
|
+
for (let i = nextIndex;i < requests.length; i++) {
|
|
3043
|
+
results[i] = { index: i, success: false, error: "Batch cancelled" };
|
|
3044
|
+
totalFailed++;
|
|
3045
|
+
}
|
|
3046
|
+
nextIndex = requests.length;
|
|
3047
|
+
break;
|
|
3048
|
+
}
|
|
3049
|
+
const idx = nextIndex++;
|
|
3050
|
+
running++;
|
|
3051
|
+
processItem(idx).then(() => {
|
|
3052
|
+
running--;
|
|
3053
|
+
completed++;
|
|
3054
|
+
config?.onProgress?.(completed, requests.length);
|
|
3055
|
+
if (completed === requests.length) {
|
|
3056
|
+
resolve({
|
|
3057
|
+
results,
|
|
3058
|
+
totalSucceeded,
|
|
3059
|
+
totalFailed,
|
|
3060
|
+
totalDurationMs: Math.round(performance.now() - startTime)
|
|
3061
|
+
});
|
|
3062
|
+
} else {
|
|
3063
|
+
scheduleNext();
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
if (requests.length === 0) {
|
|
3069
|
+
resolve({
|
|
3070
|
+
results: [],
|
|
3071
|
+
totalSucceeded: 0,
|
|
3072
|
+
totalFailed: 0,
|
|
3073
|
+
totalDurationMs: 0
|
|
3074
|
+
});
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
scheduleNext();
|
|
3078
|
+
});
|
|
2311
3079
|
}
|
|
2312
|
-
const response = await next(ctx);
|
|
2313
|
-
if (config.secretRedaction !== false) {
|
|
2314
|
-
return redactResponseSecrets(response, config);
|
|
2315
|
-
}
|
|
2316
|
-
return response;
|
|
2317
3080
|
};
|
|
2318
3081
|
}
|
|
2319
3082
|
// ../gateway/src/router.ts
|
|
@@ -2544,12 +3307,26 @@ function createProviderMesh(config) {
|
|
|
2544
3307
|
const available = sortedProviders.find((e) => isProviderAvailable(e.name));
|
|
2545
3308
|
const entry = available ?? sortedProviders[0];
|
|
2546
3309
|
const gw = getGateway(entry.name);
|
|
2547
|
-
|
|
3310
|
+
let resolvedStream = null;
|
|
3311
|
+
callWithCircuitBreaker(entry.name, () => {
|
|
3312
|
+
resolvedStream = gw.stream({ ...request, model: request.model ?? entry.model });
|
|
3313
|
+
return Promise.resolve(resolvedStream);
|
|
3314
|
+
}).catch(() => {});
|
|
3315
|
+
if (resolvedStream === null) {
|
|
3316
|
+
const err2 = new ElsiumError({
|
|
3317
|
+
code: "PROVIDER_ERROR",
|
|
3318
|
+
message: "Circuit breaker is open",
|
|
3319
|
+
retryable: true
|
|
3320
|
+
});
|
|
3321
|
+
return new ElsiumStream(async function* () {
|
|
3322
|
+
yield { type: "error", error: err2 };
|
|
3323
|
+
}());
|
|
3324
|
+
}
|
|
3325
|
+
return resolvedStream;
|
|
2548
3326
|
}
|
|
2549
3327
|
};
|
|
2550
3328
|
}
|
|
2551
3329
|
// ../tools/src/define.ts
|
|
2552
|
-
var log2 = createLogger();
|
|
2553
3330
|
function defineTool(config) {
|
|
2554
3331
|
const { name, description, input, output, handler, timeoutMs = 30000 } = config;
|
|
2555
3332
|
return {
|
|
@@ -2624,91 +3401,20 @@ function defineTool(config) {
|
|
|
2624
3401
|
}
|
|
2625
3402
|
};
|
|
2626
3403
|
}
|
|
2627
|
-
function zodDefKind2(def) {
|
|
2628
|
-
return typeof def.type === "string" ? def.type : def.typeName;
|
|
2629
|
-
}
|
|
2630
|
-
function zodObjectToJsonSchema(def) {
|
|
2631
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
2632
|
-
const properties = {};
|
|
2633
|
-
const required = [];
|
|
2634
|
-
for (const [key, value] of Object.entries(shape)) {
|
|
2635
|
-
const fieldSchema = value;
|
|
2636
|
-
properties[key] = zodToJsonSchema(fieldSchema);
|
|
2637
|
-
const fieldDef = fieldSchema._def;
|
|
2638
|
-
const fieldKind = zodDefKind2(fieldDef);
|
|
2639
|
-
if (fieldKind !== "optional" && fieldKind !== "ZodOptional" && fieldKind !== "default" && fieldKind !== "ZodDefault") {
|
|
2640
|
-
required.push(key);
|
|
2641
|
-
}
|
|
2642
|
-
if (fieldDef.description) {
|
|
2643
|
-
properties[key].description = fieldDef.description;
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
return { type: "object", properties, required };
|
|
2647
|
-
}
|
|
2648
|
-
function zodToJsonSchema(schema) {
|
|
2649
|
-
if (!("_def" in schema))
|
|
2650
|
-
return { type: "object" };
|
|
2651
|
-
const def = schema._def;
|
|
2652
|
-
const kind = zodDefKind2(def);
|
|
2653
|
-
switch (kind) {
|
|
2654
|
-
case "object":
|
|
2655
|
-
case "ZodObject":
|
|
2656
|
-
return zodObjectToJsonSchema(def);
|
|
2657
|
-
case "string":
|
|
2658
|
-
case "ZodString":
|
|
2659
|
-
return { type: "string" };
|
|
2660
|
-
case "number":
|
|
2661
|
-
case "ZodNumber":
|
|
2662
|
-
return { type: "number" };
|
|
2663
|
-
case "boolean":
|
|
2664
|
-
case "ZodBoolean":
|
|
2665
|
-
return { type: "boolean" };
|
|
2666
|
-
case "array":
|
|
2667
|
-
case "ZodArray":
|
|
2668
|
-
return {
|
|
2669
|
-
type: "array",
|
|
2670
|
-
items: zodToJsonSchema(def.element ?? def.type)
|
|
2671
|
-
};
|
|
2672
|
-
case "enum":
|
|
2673
|
-
case "ZodEnum": {
|
|
2674
|
-
const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
|
|
2675
|
-
return { type: "string", enum: values };
|
|
2676
|
-
}
|
|
2677
|
-
case "optional":
|
|
2678
|
-
case "ZodOptional":
|
|
2679
|
-
return zodToJsonSchema(def.innerType);
|
|
2680
|
-
case "default":
|
|
2681
|
-
case "ZodDefault":
|
|
2682
|
-
return zodToJsonSchema(def.innerType);
|
|
2683
|
-
case "nullable":
|
|
2684
|
-
case "ZodNullable": {
|
|
2685
|
-
const inner = zodToJsonSchema(def.innerType);
|
|
2686
|
-
return { ...inner, nullable: true };
|
|
2687
|
-
}
|
|
2688
|
-
case "ZodLiteral":
|
|
2689
|
-
return { type: typeof def.value, const: def.value };
|
|
2690
|
-
case "ZodUnion": {
|
|
2691
|
-
const options = def.options.map(zodToJsonSchema);
|
|
2692
|
-
return { anyOf: options };
|
|
2693
|
-
}
|
|
2694
|
-
case "ZodRecord":
|
|
2695
|
-
return {
|
|
2696
|
-
type: "object",
|
|
2697
|
-
additionalProperties: def.valueType ? zodToJsonSchema(def.valueType) : { type: "string" }
|
|
2698
|
-
};
|
|
2699
|
-
case "ZodTuple": {
|
|
2700
|
-
const items = (def.items ?? []).map(zodToJsonSchema);
|
|
2701
|
-
return { type: "array", prefixItems: items, minItems: items.length, maxItems: items.length };
|
|
2702
|
-
}
|
|
2703
|
-
case "ZodDate":
|
|
2704
|
-
return { type: "string", format: "date-time" };
|
|
2705
|
-
default:
|
|
2706
|
-
log2.warn(`zodToJsonSchema: unsupported type ${kind}, defaulting to string`);
|
|
2707
|
-
return { type: "string" };
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
3404
|
// ../tools/src/toolkit.ts
|
|
2711
3405
|
function createToolkit(name, tools) {
|
|
3406
|
+
const seen = new Set;
|
|
3407
|
+
for (const tool of tools) {
|
|
3408
|
+
if (seen.has(tool.name)) {
|
|
3409
|
+
throw new ElsiumError({
|
|
3410
|
+
code: "CONFIG_ERROR",
|
|
3411
|
+
message: `Duplicate tool name "${tool.name}" in toolkit "${name}"`,
|
|
3412
|
+
retryable: false,
|
|
3413
|
+
metadata: { toolkit: name, tool: tool.name }
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
seen.add(tool.name);
|
|
3417
|
+
}
|
|
2712
3418
|
const toolMap = new Map(tools.map((t) => [t.name, t]));
|
|
2713
3419
|
return {
|
|
2714
3420
|
name,
|
|
@@ -6727,7 +7433,26 @@ var coerce = {
|
|
|
6727
7433
|
};
|
|
6728
7434
|
var NEVER = INVALID;
|
|
6729
7435
|
// ../tools/src/builtin.ts
|
|
6730
|
-
var BLOCKED_HOSTS = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|0\.0\.0\.0|0+\.0+\.0+\.0+|\[
|
|
7436
|
+
var BLOCKED_HOSTS = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|0\.0\.0\.0|0+\.0+\.0+\.0+|\[?::1\]?|\[?::ffff:127\.\d+\.\d+\.\d+\]?|\[?::ffff:10\.\d+\.\d+\.\d+\]?|\[?::ffff:192\.168\.\d+\.\d+\]?|\[?::ffff:172\.(1[6-9]|2\d|3[01])\.\d+\.\d+\]?|::ffff:127\.\d+\.\d+\.\d+|0177\.\d+\.\d+\.\d+|2130706433|\[?::\]?|\[?f[cd][0-9a-f]{2}:|\[?fe80:)/i;
|
|
7437
|
+
var BLOCKED_HEADER_NAMES = new Set([
|
|
7438
|
+
"cookie",
|
|
7439
|
+
"set-cookie",
|
|
7440
|
+
"authorization",
|
|
7441
|
+
"proxy-authorization",
|
|
7442
|
+
"host",
|
|
7443
|
+
"x-api-key",
|
|
7444
|
+
"x-forwarded-for",
|
|
7445
|
+
"x-real-ip"
|
|
7446
|
+
]);
|
|
7447
|
+
function sanitizeHeaders(headers) {
|
|
7448
|
+
const safe = {};
|
|
7449
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
7450
|
+
if (!BLOCKED_HEADER_NAMES.has(key.toLowerCase())) {
|
|
7451
|
+
safe[key] = value;
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
return safe;
|
|
7455
|
+
}
|
|
6731
7456
|
function validateUrl(urlString) {
|
|
6732
7457
|
const parsed = new URL(urlString);
|
|
6733
7458
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
@@ -6752,8 +7477,9 @@ var httpFetchTool = defineTool({
|
|
|
6752
7477
|
timeoutMs: 15000,
|
|
6753
7478
|
handler: async ({ url, headers }, context) => {
|
|
6754
7479
|
validateUrl(url);
|
|
7480
|
+
const safeHeaders = headers ? sanitizeHeaders(headers) : undefined;
|
|
6755
7481
|
const response = await fetch(url, {
|
|
6756
|
-
headers,
|
|
7482
|
+
headers: safeHeaders,
|
|
6757
7483
|
signal: context.signal,
|
|
6758
7484
|
redirect: "manual"
|
|
6759
7485
|
});
|
|
@@ -7228,15 +7954,33 @@ function createMemory(config) {
|
|
|
7228
7954
|
case "unlimited":
|
|
7229
7955
|
break;
|
|
7230
7956
|
}
|
|
7957
|
+
if (config.store && config.agentId) {
|
|
7958
|
+
config.store.save(config.agentId, [...messages]).catch(() => {});
|
|
7959
|
+
}
|
|
7231
7960
|
},
|
|
7232
7961
|
getMessages() {
|
|
7233
7962
|
return [...messages];
|
|
7234
7963
|
},
|
|
7235
7964
|
clear() {
|
|
7236
7965
|
messages.length = 0;
|
|
7966
|
+
if (config.store && config.agentId) {
|
|
7967
|
+
config.store.clear(config.agentId).catch(() => {});
|
|
7968
|
+
}
|
|
7237
7969
|
},
|
|
7238
7970
|
getTokenEstimate() {
|
|
7239
7971
|
return messages.reduce((sum, m) => sum + estimateTokens(m), 0);
|
|
7972
|
+
},
|
|
7973
|
+
async loadFromStore() {
|
|
7974
|
+
if (!config.store || !config.agentId)
|
|
7975
|
+
return;
|
|
7976
|
+
const stored = await config.store.load(config.agentId);
|
|
7977
|
+
messages.length = 0;
|
|
7978
|
+
messages.push(...stored);
|
|
7979
|
+
},
|
|
7980
|
+
async saveToStore() {
|
|
7981
|
+
if (!config.store || !config.agentId)
|
|
7982
|
+
return;
|
|
7983
|
+
await config.store.save(config.agentId, [...messages]);
|
|
7240
7984
|
}
|
|
7241
7985
|
};
|
|
7242
7986
|
}
|
|
@@ -7282,7 +8026,7 @@ var JAILBREAK_PATTERNS2 = [
|
|
|
7282
8026
|
detail: "Opposite mode jailbreak attempt"
|
|
7283
8027
|
}
|
|
7284
8028
|
];
|
|
7285
|
-
var
|
|
8029
|
+
var SECRET_PATTERNS3 = [
|
|
7286
8030
|
{
|
|
7287
8031
|
pattern: /\bsk-[a-zA-Z0-9_-]{20,}\b/g,
|
|
7288
8032
|
detail: "API secret key detected",
|
|
@@ -7360,7 +8104,7 @@ function createAgentSecurity(config) {
|
|
|
7360
8104
|
const violations = [];
|
|
7361
8105
|
let redactedOutput = output;
|
|
7362
8106
|
if (config.redactSecrets !== false) {
|
|
7363
|
-
for (const { pattern, detail, replacement } of
|
|
8107
|
+
for (const { pattern, detail, replacement } of SECRET_PATTERNS3) {
|
|
7364
8108
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
7365
8109
|
if (regex.test(redactedOutput)) {
|
|
7366
8110
|
violations.push({ type: "secret_detected", detail, severity: "medium" });
|
|
@@ -7421,9 +8165,9 @@ Only respond with JSON, nothing else.`
|
|
|
7421
8165
|
if (!parsed) {
|
|
7422
8166
|
return null;
|
|
7423
8167
|
}
|
|
7424
|
-
const score = parsed.score
|
|
8168
|
+
const score = typeof parsed.score === "number" ? parsed.score : 0.5;
|
|
7425
8169
|
const threshold = config.hallucination?.threshold ?? 0.7;
|
|
7426
|
-
const claims = parsed.hallucinated_claims
|
|
8170
|
+
const claims = Array.isArray(parsed.hallucinated_claims) ? parsed.hallucinated_claims : [];
|
|
7427
8171
|
return {
|
|
7428
8172
|
passed: score >= threshold,
|
|
7429
8173
|
score,
|
|
@@ -7486,12 +8230,12 @@ Only respond with JSON, nothing else.`
|
|
|
7486
8230
|
if (!parsed) {
|
|
7487
8231
|
return null;
|
|
7488
8232
|
}
|
|
7489
|
-
const score = parsed.score
|
|
8233
|
+
const score = typeof parsed.score === "number" ? parsed.score : 0.5;
|
|
7490
8234
|
const threshold = config.relevance?.threshold ?? 0.5;
|
|
7491
8235
|
return {
|
|
7492
8236
|
passed: score >= threshold,
|
|
7493
8237
|
score,
|
|
7494
|
-
reason: parsed.reason
|
|
8238
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : score >= threshold ? "Output is relevant" : "Output lacks relevance"
|
|
7495
8239
|
};
|
|
7496
8240
|
}
|
|
7497
8241
|
function checkRelevanceHeuristic(input, output) {
|
|
@@ -7543,8 +8287,8 @@ Only respond with JSON, nothing else.`
|
|
|
7543
8287
|
if (!parsed) {
|
|
7544
8288
|
return null;
|
|
7545
8289
|
}
|
|
7546
|
-
const score = parsed.score
|
|
7547
|
-
const claims = parsed.ungrounded_claims
|
|
8290
|
+
const score = typeof parsed.score === "number" ? parsed.score : 0.5;
|
|
8291
|
+
const claims = Array.isArray(parsed.ungrounded_claims) ? parsed.ungrounded_claims : [];
|
|
7548
8292
|
return {
|
|
7549
8293
|
passed: score >= 0.7,
|
|
7550
8294
|
score,
|
|
@@ -7605,16 +8349,23 @@ Only respond with JSON, nothing else.`
|
|
|
7605
8349
|
}
|
|
7606
8350
|
|
|
7607
8351
|
// ../agents/src/state-machine.ts
|
|
8352
|
+
async function safeHook(fn) {
|
|
8353
|
+
if (!fn)
|
|
8354
|
+
return;
|
|
8355
|
+
try {
|
|
8356
|
+
await fn();
|
|
8357
|
+
} catch (_) {}
|
|
8358
|
+
}
|
|
7608
8359
|
function executeStateMachine(baseConfig, stateConfig, deps, input, options) {
|
|
7609
8360
|
return runStateMachine(baseConfig, stateConfig, deps, input, options ?? {});
|
|
7610
8361
|
}
|
|
7611
|
-
function handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, signal) {
|
|
8362
|
+
function handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, signal, hooks, approvalGate, approvalConfig) {
|
|
7612
8363
|
const toolCalls = response.message.toolCalls;
|
|
7613
8364
|
if (!toolCalls?.length || response.stopReason !== "tool_use") {
|
|
7614
8365
|
return null;
|
|
7615
8366
|
}
|
|
7616
8367
|
return (async () => {
|
|
7617
|
-
const toolResults = await executeToolCalls(toolCalls, toolMap, toolCallHistory, signal);
|
|
8368
|
+
const toolResults = await executeToolCalls(toolCalls, toolMap, toolCallHistory, signal, hooks, approvalGate, approvalConfig);
|
|
7618
8369
|
const toolMessage = {
|
|
7619
8370
|
role: "tool",
|
|
7620
8371
|
content: "",
|
|
@@ -7718,6 +8469,7 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
|
|
|
7718
8469
|
});
|
|
7719
8470
|
}
|
|
7720
8471
|
const agentSecurity = baseConfig.guardrails?.security ? createAgentSecurity(baseConfig.guardrails.security) : null;
|
|
8472
|
+
const approvalGate = baseConfig.guardrails?.approval ? createApprovalGate(baseConfig.guardrails.approval) : null;
|
|
7721
8473
|
const maxTokenBudget = guardrails?.maxTokenBudget ?? 500000;
|
|
7722
8474
|
const outputValidator = guardrails?.outputValidator ?? (() => true);
|
|
7723
8475
|
const conversationMessages = [{ role: "user", content: input }];
|
|
@@ -7735,7 +8487,8 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
|
|
|
7735
8487
|
checkTokenBudget(totalInputTokens + totalOutputTokens, maxTokenBudget);
|
|
7736
8488
|
response = applyOutputGuardrails(response, outputValidator, agentSecurity);
|
|
7737
8489
|
conversationMessages.push(response.message);
|
|
7738
|
-
|
|
8490
|
+
await safeHook(() => baseConfig.hooks?.onMessage?.(response.message));
|
|
8491
|
+
const toolCallAction = handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, options.signal, baseConfig.hooks, approvalGate, baseConfig.guardrails);
|
|
7739
8492
|
if (toolCallAction) {
|
|
7740
8493
|
await toolCallAction;
|
|
7741
8494
|
continue;
|
|
@@ -7756,9 +8509,30 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
|
|
|
7756
8509
|
metadata: { iterations, maxIterations, lastState: currentStateName }
|
|
7757
8510
|
});
|
|
7758
8511
|
}
|
|
7759
|
-
async function executeToolCalls(toolCalls, toolMap, history, signal) {
|
|
8512
|
+
async function executeToolCalls(toolCalls, toolMap, history, signal, hooks, approvalGate, approvalConfig) {
|
|
7760
8513
|
const results = [];
|
|
7761
8514
|
for (const tc of toolCalls) {
|
|
8515
|
+
await safeHook(() => hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
|
|
8516
|
+
if (approvalGate && shouldRequireApproval(approvalConfig?.approval?.requireApprovalFor, {
|
|
8517
|
+
toolName: tc.name
|
|
8518
|
+
})) {
|
|
8519
|
+
const decision = await approvalGate.requestApproval("tool_call", `Execute tool: ${tc.name}`, {
|
|
8520
|
+
toolName: tc.name,
|
|
8521
|
+
arguments: tc.arguments
|
|
8522
|
+
});
|
|
8523
|
+
if (!decision.approved) {
|
|
8524
|
+
const deniedResult = {
|
|
8525
|
+
success: false,
|
|
8526
|
+
error: `Tool call denied: ${decision.reason ?? "Approval denied"}`,
|
|
8527
|
+
toolCallId: tc.id,
|
|
8528
|
+
durationMs: 0
|
|
8529
|
+
};
|
|
8530
|
+
await safeHook(() => hooks?.onToolResult?.(deniedResult));
|
|
8531
|
+
history.push({ name: tc.name, arguments: tc.arguments, result: deniedResult });
|
|
8532
|
+
results.push(formatToolResult(deniedResult));
|
|
8533
|
+
continue;
|
|
8534
|
+
}
|
|
8535
|
+
}
|
|
7762
8536
|
const tool = toolMap.get(tc.name);
|
|
7763
8537
|
if (!tool) {
|
|
7764
8538
|
const errorResult = {
|
|
@@ -7767,11 +8541,13 @@ async function executeToolCalls(toolCalls, toolMap, history, signal) {
|
|
|
7767
8541
|
toolCallId: tc.id,
|
|
7768
8542
|
durationMs: 0
|
|
7769
8543
|
};
|
|
8544
|
+
await safeHook(() => hooks?.onToolResult?.(errorResult));
|
|
7770
8545
|
history.push({ name: tc.name, arguments: tc.arguments, result: errorResult });
|
|
7771
8546
|
results.push(formatToolResult(errorResult));
|
|
7772
8547
|
continue;
|
|
7773
8548
|
}
|
|
7774
8549
|
const result = await tool.execute(tc.arguments, { toolCallId: tc.id, signal });
|
|
8550
|
+
await safeHook(() => hooks?.onToolResult?.(result));
|
|
7775
8551
|
history.push({ name: tc.name, arguments: tc.arguments, result });
|
|
7776
8552
|
results.push(formatToolResult(result));
|
|
7777
8553
|
}
|
|
@@ -7779,7 +8555,7 @@ async function executeToolCalls(toolCalls, toolMap, history, signal) {
|
|
|
7779
8555
|
}
|
|
7780
8556
|
|
|
7781
8557
|
// ../agents/src/agent.ts
|
|
7782
|
-
async function
|
|
8558
|
+
async function safeHook2(fn) {
|
|
7783
8559
|
if (!fn)
|
|
7784
8560
|
return;
|
|
7785
8561
|
try {
|
|
@@ -7885,7 +8661,7 @@ function defineAgent(config, deps) {
|
|
|
7885
8661
|
traceId,
|
|
7886
8662
|
confidence
|
|
7887
8663
|
};
|
|
7888
|
-
await
|
|
8664
|
+
await safeHook2(() => config.hooks?.onComplete?.(agentResult));
|
|
7889
8665
|
return { action: "return", result: agentResult };
|
|
7890
8666
|
}
|
|
7891
8667
|
function checkBudget(totalInputTokens, totalOutputTokens) {
|
|
@@ -7943,7 +8719,7 @@ function defineAgent(config, deps) {
|
|
|
7943
8719
|
totalInputTokens += response.usage.inputTokens;
|
|
7944
8720
|
totalOutputTokens += response.usage.outputTokens;
|
|
7945
8721
|
totalCost += response.cost.totalCost;
|
|
7946
|
-
await
|
|
8722
|
+
await safeHook2(() => config.hooks?.onMessage?.(response.message));
|
|
7947
8723
|
conversationMessages.push(response.message);
|
|
7948
8724
|
if (!response.message.toolCalls?.length || response.stopReason !== "tool_use") {
|
|
7949
8725
|
const result = await handleNonToolResponse(response, messages, iterations, totalInputTokens, totalOutputTokens, totalCost, toolCallHistory, traceId, conversationMessages);
|
|
@@ -7971,7 +8747,7 @@ function defineAgent(config, deps) {
|
|
|
7971
8747
|
async function executeToolCalls2(toolCalls, history, options = {}) {
|
|
7972
8748
|
const results = [];
|
|
7973
8749
|
for (const tc of toolCalls) {
|
|
7974
|
-
await
|
|
8750
|
+
await safeHook2(() => config.hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
|
|
7975
8751
|
if (approvalGate && shouldRequireApproval(config.guardrails?.approval?.requireApprovalFor, {
|
|
7976
8752
|
toolName: tc.name
|
|
7977
8753
|
})) {
|
|
@@ -7983,7 +8759,7 @@ function defineAgent(config, deps) {
|
|
|
7983
8759
|
toolCallId: tc.id,
|
|
7984
8760
|
durationMs: 0
|
|
7985
8761
|
};
|
|
7986
|
-
await
|
|
8762
|
+
await safeHook2(() => config.hooks?.onToolResult?.(deniedResult));
|
|
7987
8763
|
history.push({ name: tc.name, arguments: tc.arguments, result: deniedResult });
|
|
7988
8764
|
results.push(formatToolResult(deniedResult));
|
|
7989
8765
|
continue;
|
|
@@ -7997,7 +8773,7 @@ function defineAgent(config, deps) {
|
|
|
7997
8773
|
toolCallId: tc.id,
|
|
7998
8774
|
durationMs: 0
|
|
7999
8775
|
};
|
|
8000
|
-
await
|
|
8776
|
+
await safeHook2(() => config.hooks?.onToolResult?.(errorResult));
|
|
8001
8777
|
history.push({ name: tc.name, arguments: tc.arguments, result: errorResult });
|
|
8002
8778
|
results.push(formatToolResult(errorResult));
|
|
8003
8779
|
continue;
|
|
@@ -8006,7 +8782,7 @@ function defineAgent(config, deps) {
|
|
|
8006
8782
|
toolCallId: tc.id,
|
|
8007
8783
|
signal: options.signal
|
|
8008
8784
|
});
|
|
8009
|
-
await
|
|
8785
|
+
await safeHook2(() => config.hooks?.onToolResult?.(result));
|
|
8010
8786
|
history.push({ name: tc.name, arguments: tc.arguments, result });
|
|
8011
8787
|
results.push(formatToolResult(result));
|
|
8012
8788
|
}
|
|
@@ -8029,15 +8805,113 @@ function defineAgent(config, deps) {
|
|
|
8029
8805
|
continue;
|
|
8030
8806
|
validateInputText(extractText(msg.content));
|
|
8031
8807
|
}
|
|
8032
|
-
if (config.states && config.initialState) {
|
|
8033
|
-
const inputText = messages.filter((m) => m.role === "user").map((m) => extractText(m.content)).join(`
|
|
8034
|
-
`);
|
|
8035
|
-
return executeStateMachine(config, { states: config.states, initialState: config.initialState }, deps, inputText || "", options);
|
|
8808
|
+
if (config.states && config.initialState) {
|
|
8809
|
+
const inputText = messages.filter((m) => m.role === "user").map((m) => extractText(m.content)).join(`
|
|
8810
|
+
`);
|
|
8811
|
+
return executeStateMachine(config, { states: config.states, initialState: config.initialState }, deps, inputText || "", options);
|
|
8812
|
+
}
|
|
8813
|
+
return executeLoop(messages, options);
|
|
8814
|
+
},
|
|
8815
|
+
resetMemory() {
|
|
8816
|
+
memory.clear();
|
|
8817
|
+
}
|
|
8818
|
+
};
|
|
8819
|
+
}
|
|
8820
|
+
// ../agents/src/stores/memory-store.ts
|
|
8821
|
+
function createInMemoryMemoryStore() {
|
|
8822
|
+
const store = new Map;
|
|
8823
|
+
return {
|
|
8824
|
+
async load(agentId) {
|
|
8825
|
+
return [...store.get(agentId) ?? []];
|
|
8826
|
+
},
|
|
8827
|
+
async save(agentId, messages) {
|
|
8828
|
+
store.set(agentId, [...messages]);
|
|
8829
|
+
},
|
|
8830
|
+
async clear(agentId) {
|
|
8831
|
+
store.delete(agentId);
|
|
8832
|
+
}
|
|
8833
|
+
};
|
|
8834
|
+
}
|
|
8835
|
+
// ../agents/src/stores/sqlite-store.ts
|
|
8836
|
+
import { createRequire } from "node:module";
|
|
8837
|
+
var require2 = createRequire(import.meta.url);
|
|
8838
|
+
var log7 = createLogger();
|
|
8839
|
+
var BLOCKED_KEYS2 = new Set(["__proto__", "constructor", "prototype"]);
|
|
8840
|
+
var TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
8841
|
+
function createSqliteMemoryStore(config) {
|
|
8842
|
+
const { path, tableName = "agent_memory" } = config;
|
|
8843
|
+
if (BLOCKED_KEYS2.has(tableName)) {
|
|
8844
|
+
throw new Error(`Invalid table name: ${tableName}`);
|
|
8845
|
+
}
|
|
8846
|
+
if (!TABLE_NAME_PATTERN.test(tableName)) {
|
|
8847
|
+
throw new Error(`Invalid table name format: ${tableName}`);
|
|
8848
|
+
}
|
|
8849
|
+
let db = null;
|
|
8850
|
+
let initPromise = null;
|
|
8851
|
+
async function getDb() {
|
|
8852
|
+
if (db)
|
|
8853
|
+
return db;
|
|
8854
|
+
if (initPromise)
|
|
8855
|
+
return initPromise;
|
|
8856
|
+
initPromise = (async () => {
|
|
8857
|
+
try {
|
|
8858
|
+
const Database = require2("better-sqlite3");
|
|
8859
|
+
db = new Database(path);
|
|
8860
|
+
db.exec(`
|
|
8861
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
8862
|
+
agent_id TEXT NOT NULL,
|
|
8863
|
+
idx INTEGER NOT NULL,
|
|
8864
|
+
role TEXT NOT NULL,
|
|
8865
|
+
content TEXT NOT NULL,
|
|
8866
|
+
metadata TEXT,
|
|
8867
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
8868
|
+
PRIMARY KEY (agent_id, idx)
|
|
8869
|
+
)
|
|
8870
|
+
`);
|
|
8871
|
+
return db;
|
|
8872
|
+
} catch (err2) {
|
|
8873
|
+
initPromise = null;
|
|
8874
|
+
log7.error("Failed to initialize SQLite memory store", {
|
|
8875
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
8876
|
+
});
|
|
8877
|
+
throw new Error("better-sqlite3 is required for SQLite memory store. Install it as a dependency.");
|
|
8878
|
+
}
|
|
8879
|
+
})();
|
|
8880
|
+
return initPromise;
|
|
8881
|
+
}
|
|
8882
|
+
return {
|
|
8883
|
+
async load(agentId) {
|
|
8884
|
+
if (BLOCKED_KEYS2.has(agentId))
|
|
8885
|
+
return [];
|
|
8886
|
+
const database = await getDb();
|
|
8887
|
+
const rows = database.prepare(`SELECT role, content, metadata FROM ${tableName} WHERE agent_id = ? ORDER BY idx`).all(agentId);
|
|
8888
|
+
return rows.map((row) => {
|
|
8889
|
+
const msg = {
|
|
8890
|
+
role: row.role,
|
|
8891
|
+
content: JSON.parse(row.content)
|
|
8892
|
+
};
|
|
8893
|
+
if (row.metadata) {
|
|
8894
|
+
msg.metadata = JSON.parse(row.metadata);
|
|
8895
|
+
}
|
|
8896
|
+
return msg;
|
|
8897
|
+
});
|
|
8898
|
+
},
|
|
8899
|
+
async save(agentId, messages) {
|
|
8900
|
+
if (BLOCKED_KEYS2.has(agentId))
|
|
8901
|
+
return;
|
|
8902
|
+
const database = await getDb();
|
|
8903
|
+
database.prepare(`DELETE FROM ${tableName} WHERE agent_id = ?`).run(agentId);
|
|
8904
|
+
const insert = database.prepare(`INSERT INTO ${tableName} (agent_id, idx, role, content, metadata) VALUES (?, ?, ?, ?, ?)`);
|
|
8905
|
+
for (let i = 0;i < messages.length; i++) {
|
|
8906
|
+
const msg = messages[i];
|
|
8907
|
+
insert.run(agentId, i, msg.role, JSON.stringify(msg.content), msg.metadata ? JSON.stringify(msg.metadata) : null);
|
|
8036
8908
|
}
|
|
8037
|
-
return executeLoop(messages, options);
|
|
8038
8909
|
},
|
|
8039
|
-
|
|
8040
|
-
|
|
8910
|
+
async clear(agentId) {
|
|
8911
|
+
if (BLOCKED_KEYS2.has(agentId))
|
|
8912
|
+
return;
|
|
8913
|
+
const database = await getDb();
|
|
8914
|
+
database.prepare(`DELETE FROM ${tableName} WHERE agent_id = ?`).run(agentId);
|
|
8041
8915
|
}
|
|
8042
8916
|
};
|
|
8043
8917
|
}
|
|
@@ -8538,7 +9412,11 @@ function createMockEmbeddings(dims = 128) {
|
|
|
8538
9412
|
}
|
|
8539
9413
|
};
|
|
8540
9414
|
}
|
|
9415
|
+
var embeddingProviderRegistry = createRegistry("embeddingProvider");
|
|
8541
9416
|
function getEmbeddingProvider(config) {
|
|
9417
|
+
const registered = embeddingProviderRegistry.get(config.provider);
|
|
9418
|
+
if (registered)
|
|
9419
|
+
return registered(config);
|
|
8542
9420
|
switch (config.provider) {
|
|
8543
9421
|
case "openai":
|
|
8544
9422
|
return createOpenAIEmbeddings(config);
|
|
@@ -8547,12 +9425,13 @@ function getEmbeddingProvider(config) {
|
|
|
8547
9425
|
default:
|
|
8548
9426
|
throw new ElsiumError({
|
|
8549
9427
|
code: "CONFIG_ERROR",
|
|
8550
|
-
message: `Unknown embedding provider: ${config.provider}`,
|
|
9428
|
+
message: `Unknown embedding provider: ${config.provider}. Available: openai, mock${embeddingProviderRegistry.list().length ? `, ${embeddingProviderRegistry.list().join(", ")}` : ""}`,
|
|
8551
9429
|
retryable: false
|
|
8552
9430
|
});
|
|
8553
9431
|
}
|
|
8554
9432
|
}
|
|
8555
9433
|
// ../rag/src/vectorstore.ts
|
|
9434
|
+
var vectorStoreRegistry = createRegistry("vectorStore");
|
|
8556
9435
|
function cosineSimilarity(a, b) {
|
|
8557
9436
|
if (a.length !== b.length)
|
|
8558
9437
|
return 0;
|
|
@@ -8686,6 +9565,123 @@ function rag(config) {
|
|
|
8686
9565
|
}
|
|
8687
9566
|
};
|
|
8688
9567
|
}
|
|
9568
|
+
// ../rag/src/stores/pgvector.ts
|
|
9569
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
9570
|
+
var require3 = createRequire2(import.meta.url);
|
|
9571
|
+
var log8 = createLogger();
|
|
9572
|
+
var BLOCKED_KEYS3 = new Set(["__proto__", "constructor", "prototype"]);
|
|
9573
|
+
var TABLE_NAME_PATTERN2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
9574
|
+
function createPgVectorStore(config) {
|
|
9575
|
+
const { connectionString, tableName = "vector_chunks", dimensions = 1536 } = config;
|
|
9576
|
+
if (BLOCKED_KEYS3.has(tableName)) {
|
|
9577
|
+
throw new Error(`Invalid table name: ${tableName}`);
|
|
9578
|
+
}
|
|
9579
|
+
if (!TABLE_NAME_PATTERN2.test(tableName)) {
|
|
9580
|
+
throw new Error(`Invalid table name format: ${tableName}`);
|
|
9581
|
+
}
|
|
9582
|
+
let client = null;
|
|
9583
|
+
let initialized = false;
|
|
9584
|
+
async function getClient() {
|
|
9585
|
+
if (client)
|
|
9586
|
+
return client;
|
|
9587
|
+
try {
|
|
9588
|
+
const pg = require3("pg");
|
|
9589
|
+
client = new pg.Client({ connectionString });
|
|
9590
|
+
await client.connect();
|
|
9591
|
+
if (!initialized) {
|
|
9592
|
+
await client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
9593
|
+
await client.query(`
|
|
9594
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
9595
|
+
id TEXT PRIMARY KEY,
|
|
9596
|
+
content TEXT NOT NULL,
|
|
9597
|
+
document_id TEXT NOT NULL,
|
|
9598
|
+
chunk_index INTEGER NOT NULL,
|
|
9599
|
+
metadata JSONB DEFAULT '{}',
|
|
9600
|
+
embedding vector(${dimensions})
|
|
9601
|
+
)
|
|
9602
|
+
`);
|
|
9603
|
+
initialized = true;
|
|
9604
|
+
}
|
|
9605
|
+
return client;
|
|
9606
|
+
} catch (err2) {
|
|
9607
|
+
log8.error("Failed to initialize PgVector store", {
|
|
9608
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
9609
|
+
});
|
|
9610
|
+
throw new Error("pg is required for PgVector store. Install it as a dependency.");
|
|
9611
|
+
}
|
|
9612
|
+
}
|
|
9613
|
+
return {
|
|
9614
|
+
name: "pgvector",
|
|
9615
|
+
async upsert(chunks) {
|
|
9616
|
+
const pg = await getClient();
|
|
9617
|
+
for (const chunk of chunks) {
|
|
9618
|
+
if (BLOCKED_KEYS3.has(chunk.id))
|
|
9619
|
+
continue;
|
|
9620
|
+
const embedding = `[${chunk.embedding.values.join(",")}]`;
|
|
9621
|
+
await pg.query(`INSERT INTO ${tableName} (id, content, document_id, chunk_index, metadata, embedding)
|
|
9622
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
9623
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
9624
|
+
content = EXCLUDED.content,
|
|
9625
|
+
document_id = EXCLUDED.document_id,
|
|
9626
|
+
chunk_index = EXCLUDED.chunk_index,
|
|
9627
|
+
metadata = EXCLUDED.metadata,
|
|
9628
|
+
embedding = EXCLUDED.embedding`, [
|
|
9629
|
+
chunk.id,
|
|
9630
|
+
chunk.content,
|
|
9631
|
+
chunk.documentId,
|
|
9632
|
+
chunk.index,
|
|
9633
|
+
JSON.stringify(chunk.metadata),
|
|
9634
|
+
embedding
|
|
9635
|
+
]);
|
|
9636
|
+
}
|
|
9637
|
+
},
|
|
9638
|
+
async query(embedding, options) {
|
|
9639
|
+
const pg = await getClient();
|
|
9640
|
+
const topK = options?.topK ?? 5;
|
|
9641
|
+
const minScore = options?.minScore ?? 0;
|
|
9642
|
+
const embeddingStr = `[${embedding.values.join(",")}]`;
|
|
9643
|
+
const result = await pg.query(`SELECT id, content, document_id, chunk_index, metadata,
|
|
9644
|
+
1 - (embedding <=> $1::vector) as score
|
|
9645
|
+
FROM ${tableName}
|
|
9646
|
+
WHERE 1 - (embedding <=> $1::vector) >= $2
|
|
9647
|
+
ORDER BY embedding <=> $1::vector
|
|
9648
|
+
LIMIT $3`, [embeddingStr, minScore, topK]);
|
|
9649
|
+
return result.rows.map((row) => ({
|
|
9650
|
+
chunk: {
|
|
9651
|
+
id: row.id,
|
|
9652
|
+
content: row.content,
|
|
9653
|
+
documentId: row.document_id,
|
|
9654
|
+
index: row.chunk_index,
|
|
9655
|
+
metadata: {
|
|
9656
|
+
startChar: 0,
|
|
9657
|
+
endChar: 0,
|
|
9658
|
+
tokenEstimate: 0,
|
|
9659
|
+
...row.metadata ?? {}
|
|
9660
|
+
}
|
|
9661
|
+
},
|
|
9662
|
+
score: row.score,
|
|
9663
|
+
distance: 1 - row.score
|
|
9664
|
+
}));
|
|
9665
|
+
},
|
|
9666
|
+
async delete(ids) {
|
|
9667
|
+
const pg = await getClient();
|
|
9668
|
+
const filtered = ids.filter((id) => !BLOCKED_KEYS3.has(id));
|
|
9669
|
+
if (filtered.length === 0)
|
|
9670
|
+
return;
|
|
9671
|
+
const placeholders = filtered.map((_, i) => `$${i + 1}`).join(", ");
|
|
9672
|
+
await pg.query(`DELETE FROM ${tableName} WHERE id IN (${placeholders})`, filtered);
|
|
9673
|
+
},
|
|
9674
|
+
async clear() {
|
|
9675
|
+
const pg = await getClient();
|
|
9676
|
+
await pg.query(`DELETE FROM ${tableName}`);
|
|
9677
|
+
},
|
|
9678
|
+
async count() {
|
|
9679
|
+
const pg = await getClient();
|
|
9680
|
+
const result = await pg.query(`SELECT COUNT(*)::int as count FROM ${tableName}`);
|
|
9681
|
+
return result.rows[0]?.count ?? 0;
|
|
9682
|
+
}
|
|
9683
|
+
};
|
|
9684
|
+
}
|
|
8689
9685
|
// ../workflows/src/step.ts
|
|
8690
9686
|
function step(name, config) {
|
|
8691
9687
|
return { name, ...config };
|
|
@@ -9267,7 +10263,8 @@ function createCostEngine(config = {}) {
|
|
|
9267
10263
|
};
|
|
9268
10264
|
}
|
|
9269
10265
|
// ../observe/src/tracer.ts
|
|
9270
|
-
|
|
10266
|
+
import { writeFileSync } from "node:fs";
|
|
10267
|
+
var log9 = createLogger();
|
|
9271
10268
|
function observe(config = {}) {
|
|
9272
10269
|
const {
|
|
9273
10270
|
output = ["console"],
|
|
@@ -9282,7 +10279,21 @@ function observe(config = {}) {
|
|
|
9282
10279
|
for (const out of output) {
|
|
9283
10280
|
if (out === "console") {
|
|
9284
10281
|
handlers.push(consoleHandler);
|
|
9285
|
-
} else if (out === "json-file") {
|
|
10282
|
+
} else if (out === "json-file") {
|
|
10283
|
+
exporters.push({
|
|
10284
|
+
name: "json-file",
|
|
10285
|
+
export(spansToExport) {
|
|
10286
|
+
const filename = `.elsium/traces-${Date.now()}.json`;
|
|
10287
|
+
try {
|
|
10288
|
+
writeFileSync(filename, JSON.stringify(spansToExport, null, 2));
|
|
10289
|
+
} catch (err2) {
|
|
10290
|
+
log9.error("Failed to write trace file", {
|
|
10291
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
10292
|
+
});
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
});
|
|
10296
|
+
} else {
|
|
9286
10297
|
exporters.push(out);
|
|
9287
10298
|
}
|
|
9288
10299
|
}
|
|
@@ -9351,7 +10362,7 @@ function observe(config = {}) {
|
|
|
9351
10362
|
function consoleHandler(span) {
|
|
9352
10363
|
const duration = span.durationMs !== undefined ? `${span.durationMs}ms` : "running";
|
|
9353
10364
|
const status = span.status === "error" ? "[ERROR]" : span.status === "ok" ? "[OK]" : "[...]";
|
|
9354
|
-
|
|
10365
|
+
log9.info("span", {
|
|
9355
10366
|
trace: span.traceId,
|
|
9356
10367
|
span: span.name,
|
|
9357
10368
|
kind: span.kind,
|
|
@@ -9449,8 +10460,86 @@ function createMetrics(options) {
|
|
|
9449
10460
|
}
|
|
9450
10461
|
};
|
|
9451
10462
|
}
|
|
10463
|
+
// ../observe/src/experiment.ts
|
|
10464
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
10465
|
+
var log10 = createLogger();
|
|
10466
|
+
function createExperiment(config) {
|
|
10467
|
+
const { name, variants } = config;
|
|
10468
|
+
if (variants.length === 0) {
|
|
10469
|
+
throw new Error("Experiment must have at least one variant");
|
|
10470
|
+
}
|
|
10471
|
+
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
|
|
10472
|
+
const stats = {};
|
|
10473
|
+
for (const v of variants) {
|
|
10474
|
+
stats[v.name] = { assignments: 0, metrics: {} };
|
|
10475
|
+
}
|
|
10476
|
+
function hashAssign(userId) {
|
|
10477
|
+
const hash = createHash2("sha256").update(`${name}:${userId}`).digest();
|
|
10478
|
+
const value = hash.readUInt32BE(0) % 1e4 / 1e4;
|
|
10479
|
+
return pickVariant(value);
|
|
10480
|
+
}
|
|
10481
|
+
function randomAssign() {
|
|
10482
|
+
const value = Math.random();
|
|
10483
|
+
return pickVariant(value);
|
|
10484
|
+
}
|
|
10485
|
+
function pickVariant(value) {
|
|
10486
|
+
let cumulative = 0;
|
|
10487
|
+
for (const v of variants) {
|
|
10488
|
+
cumulative += v.weight / totalWeight;
|
|
10489
|
+
if (value < cumulative)
|
|
10490
|
+
return v;
|
|
10491
|
+
}
|
|
10492
|
+
return variants[variants.length - 1];
|
|
10493
|
+
}
|
|
10494
|
+
return {
|
|
10495
|
+
assign(userId) {
|
|
10496
|
+
const variant = userId ? hashAssign(userId) : randomAssign();
|
|
10497
|
+
const s = stats[variant.name];
|
|
10498
|
+
if (s)
|
|
10499
|
+
s.assignments++;
|
|
10500
|
+
log10.debug("Experiment assignment", {
|
|
10501
|
+
experiment: name,
|
|
10502
|
+
variant: variant.name,
|
|
10503
|
+
userId
|
|
10504
|
+
});
|
|
10505
|
+
return variant;
|
|
10506
|
+
},
|
|
10507
|
+
record(variant, metrics) {
|
|
10508
|
+
const s = stats[variant];
|
|
10509
|
+
if (!s)
|
|
10510
|
+
return;
|
|
10511
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
10512
|
+
if (!s.metrics[key]) {
|
|
10513
|
+
s.metrics[key] = { sum: 0, count: 0 };
|
|
10514
|
+
}
|
|
10515
|
+
s.metrics[key].sum += value;
|
|
10516
|
+
s.metrics[key].count++;
|
|
10517
|
+
}
|
|
10518
|
+
},
|
|
10519
|
+
results() {
|
|
10520
|
+
let totalAssignments = 0;
|
|
10521
|
+
const variantResults = {};
|
|
10522
|
+
for (const [vName, s] of Object.entries(stats)) {
|
|
10523
|
+
totalAssignments += s.assignments;
|
|
10524
|
+
const metricsResult = {};
|
|
10525
|
+
for (const [key, m] of Object.entries(s.metrics)) {
|
|
10526
|
+
metricsResult[key] = {
|
|
10527
|
+
sum: m.sum,
|
|
10528
|
+
count: m.count,
|
|
10529
|
+
avg: m.count > 0 ? m.sum / m.count : 0
|
|
10530
|
+
};
|
|
10531
|
+
}
|
|
10532
|
+
variantResults[vName] = {
|
|
10533
|
+
assignments: s.assignments,
|
|
10534
|
+
metrics: metricsResult
|
|
10535
|
+
};
|
|
10536
|
+
}
|
|
10537
|
+
return { name, totalAssignments, variants: variantResults };
|
|
10538
|
+
}
|
|
10539
|
+
};
|
|
10540
|
+
}
|
|
9452
10541
|
// ../observe/src/otel.ts
|
|
9453
|
-
var
|
|
10542
|
+
var log11 = createLogger();
|
|
9454
10543
|
var SPAN_KIND_MAP = {
|
|
9455
10544
|
llm: 3,
|
|
9456
10545
|
tool: 1,
|
|
@@ -9591,10 +10680,10 @@ function createOTLPExporter(config) {
|
|
|
9591
10680
|
body: JSON.stringify(payload)
|
|
9592
10681
|
});
|
|
9593
10682
|
if (!response.ok) {
|
|
9594
|
-
|
|
10683
|
+
log11.error(`OTLP export failed: ${response.status} ${response.statusText}`);
|
|
9595
10684
|
}
|
|
9596
10685
|
} catch (err2) {
|
|
9597
|
-
|
|
10686
|
+
log11.error("OTLP export error", { error: err2 instanceof Error ? err2.message : String(err2) });
|
|
9598
10687
|
}
|
|
9599
10688
|
}
|
|
9600
10689
|
function startAutoFlush() {
|
|
@@ -9617,6 +10706,16 @@ function createOTLPExporter(config) {
|
|
|
9617
10706
|
} else {
|
|
9618
10707
|
startAutoFlush();
|
|
9619
10708
|
}
|
|
10709
|
+
},
|
|
10710
|
+
async shutdown() {
|
|
10711
|
+
if (flushTimer) {
|
|
10712
|
+
clearInterval(flushTimer);
|
|
10713
|
+
flushTimer = null;
|
|
10714
|
+
}
|
|
10715
|
+
if (buffer.length > 0) {
|
|
10716
|
+
const batch = buffer.splice(0, buffer.length);
|
|
10717
|
+
await sendBatch(batch);
|
|
10718
|
+
}
|
|
9620
10719
|
}
|
|
9621
10720
|
};
|
|
9622
10721
|
}
|
|
@@ -11692,7 +12791,7 @@ var Hono2 = class extends Hono {
|
|
|
11692
12791
|
// ../app/src/middleware.ts
|
|
11693
12792
|
import { timingSafeEqual } from "node:crypto";
|
|
11694
12793
|
function corsMiddleware(config = true) {
|
|
11695
|
-
const opts = typeof config === "boolean" ? { origin:
|
|
12794
|
+
const opts = typeof config === "boolean" ? { origin: "*", methods: ["GET", "POST", "OPTIONS"] } : config;
|
|
11696
12795
|
return async (c, next) => {
|
|
11697
12796
|
const requestOrigin = c.req.header("Origin") ?? "";
|
|
11698
12797
|
let allowedOrigin;
|
|
@@ -11769,8 +12868,203 @@ function rateLimitMiddleware(config) {
|
|
|
11769
12868
|
await next();
|
|
11770
12869
|
};
|
|
11771
12870
|
}
|
|
12871
|
+
function requestIdMiddleware() {
|
|
12872
|
+
return async (c, next) => {
|
|
12873
|
+
const raw2 = c.req.header("X-Request-ID");
|
|
12874
|
+
const id = raw2 && /^[\w\-.:]{1,128}$/.test(raw2) ? raw2 : generateId("req");
|
|
12875
|
+
c.set("requestId", id);
|
|
12876
|
+
await next();
|
|
12877
|
+
c.res.headers.set("X-Request-ID", id);
|
|
12878
|
+
};
|
|
12879
|
+
}
|
|
12880
|
+
function requestLoggerMiddleware(logger) {
|
|
12881
|
+
const log12 = logger ?? createLogger();
|
|
12882
|
+
return async (c, next) => {
|
|
12883
|
+
const start = Date.now();
|
|
12884
|
+
await next();
|
|
12885
|
+
const duration = Date.now() - start;
|
|
12886
|
+
log12.info(`${c.req.method} ${c.req.path}`, {
|
|
12887
|
+
method: c.req.method,
|
|
12888
|
+
path: c.req.path,
|
|
12889
|
+
status: c.res.status,
|
|
12890
|
+
durationMs: duration,
|
|
12891
|
+
requestId: c.get("requestId")
|
|
12892
|
+
});
|
|
12893
|
+
};
|
|
12894
|
+
}
|
|
12895
|
+
|
|
12896
|
+
// ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/utils/stream.js
|
|
12897
|
+
var StreamingApi = class {
|
|
12898
|
+
writer;
|
|
12899
|
+
encoder;
|
|
12900
|
+
writable;
|
|
12901
|
+
abortSubscribers = [];
|
|
12902
|
+
responseReadable;
|
|
12903
|
+
aborted = false;
|
|
12904
|
+
closed = false;
|
|
12905
|
+
constructor(writable, _readable) {
|
|
12906
|
+
this.writable = writable;
|
|
12907
|
+
this.writer = writable.getWriter();
|
|
12908
|
+
this.encoder = new TextEncoder;
|
|
12909
|
+
const reader = _readable.getReader();
|
|
12910
|
+
this.abortSubscribers.push(async () => {
|
|
12911
|
+
await reader.cancel();
|
|
12912
|
+
});
|
|
12913
|
+
this.responseReadable = new ReadableStream({
|
|
12914
|
+
async pull(controller) {
|
|
12915
|
+
const { done, value } = await reader.read();
|
|
12916
|
+
done ? controller.close() : controller.enqueue(value);
|
|
12917
|
+
},
|
|
12918
|
+
cancel: () => {
|
|
12919
|
+
this.abort();
|
|
12920
|
+
}
|
|
12921
|
+
});
|
|
12922
|
+
}
|
|
12923
|
+
async write(input) {
|
|
12924
|
+
try {
|
|
12925
|
+
if (typeof input === "string") {
|
|
12926
|
+
input = this.encoder.encode(input);
|
|
12927
|
+
}
|
|
12928
|
+
await this.writer.write(input);
|
|
12929
|
+
} catch {}
|
|
12930
|
+
return this;
|
|
12931
|
+
}
|
|
12932
|
+
async writeln(input) {
|
|
12933
|
+
await this.write(input + `
|
|
12934
|
+
`);
|
|
12935
|
+
return this;
|
|
12936
|
+
}
|
|
12937
|
+
sleep(ms) {
|
|
12938
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
12939
|
+
}
|
|
12940
|
+
async close() {
|
|
12941
|
+
try {
|
|
12942
|
+
await this.writer.close();
|
|
12943
|
+
} catch {}
|
|
12944
|
+
this.closed = true;
|
|
12945
|
+
}
|
|
12946
|
+
async pipe(body) {
|
|
12947
|
+
this.writer.releaseLock();
|
|
12948
|
+
await body.pipeTo(this.writable, { preventClose: true });
|
|
12949
|
+
this.writer = this.writable.getWriter();
|
|
12950
|
+
}
|
|
12951
|
+
onAbort(listener) {
|
|
12952
|
+
this.abortSubscribers.push(listener);
|
|
12953
|
+
}
|
|
12954
|
+
abort() {
|
|
12955
|
+
if (!this.aborted) {
|
|
12956
|
+
this.aborted = true;
|
|
12957
|
+
this.abortSubscribers.forEach((subscriber) => subscriber());
|
|
12958
|
+
}
|
|
12959
|
+
}
|
|
12960
|
+
};
|
|
12961
|
+
|
|
12962
|
+
// ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/helper/streaming/utils.js
|
|
12963
|
+
var isOldBunVersion = () => {
|
|
12964
|
+
const version = typeof Bun !== "undefined" ? Bun.version : undefined;
|
|
12965
|
+
if (version === undefined) {
|
|
12966
|
+
return false;
|
|
12967
|
+
}
|
|
12968
|
+
const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
|
|
12969
|
+
isOldBunVersion = () => result;
|
|
12970
|
+
return result;
|
|
12971
|
+
};
|
|
12972
|
+
|
|
12973
|
+
// ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/helper/streaming/stream.js
|
|
12974
|
+
var contextStash = /* @__PURE__ */ new WeakMap;
|
|
12975
|
+
var stream = (c, cb, onError) => {
|
|
12976
|
+
const { readable, writable } = new TransformStream;
|
|
12977
|
+
const stream2 = new StreamingApi(writable, readable);
|
|
12978
|
+
if (isOldBunVersion()) {
|
|
12979
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
12980
|
+
if (!stream2.closed) {
|
|
12981
|
+
stream2.abort();
|
|
12982
|
+
}
|
|
12983
|
+
});
|
|
12984
|
+
}
|
|
12985
|
+
contextStash.set(stream2.responseReadable, c);
|
|
12986
|
+
(async () => {
|
|
12987
|
+
try {
|
|
12988
|
+
await cb(stream2);
|
|
12989
|
+
} catch (e) {
|
|
12990
|
+
if (e === undefined) {} else if (e instanceof Error && onError) {
|
|
12991
|
+
await onError(e, stream2);
|
|
12992
|
+
} else {
|
|
12993
|
+
console.error(e);
|
|
12994
|
+
}
|
|
12995
|
+
} finally {
|
|
12996
|
+
stream2.close();
|
|
12997
|
+
}
|
|
12998
|
+
})();
|
|
12999
|
+
return c.newResponse(stream2.responseReadable);
|
|
13000
|
+
};
|
|
13001
|
+
|
|
13002
|
+
// ../app/src/sse.ts
|
|
13003
|
+
function sseHeaders() {
|
|
13004
|
+
return {
|
|
13005
|
+
"Content-Type": "text/event-stream",
|
|
13006
|
+
"Cache-Control": "no-cache",
|
|
13007
|
+
Connection: "keep-alive",
|
|
13008
|
+
"X-Accel-Buffering": "no"
|
|
13009
|
+
};
|
|
13010
|
+
}
|
|
13011
|
+
function formatSSE(event, data) {
|
|
13012
|
+
const json = JSON.stringify(data);
|
|
13013
|
+
if (event === "message") {
|
|
13014
|
+
return `data: ${json}
|
|
13015
|
+
|
|
13016
|
+
`;
|
|
13017
|
+
}
|
|
13018
|
+
return `event: ${event}
|
|
13019
|
+
data: ${json}
|
|
13020
|
+
|
|
13021
|
+
`;
|
|
13022
|
+
}
|
|
13023
|
+
function streamResponse(c, source) {
|
|
13024
|
+
const headers = sseHeaders();
|
|
13025
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
13026
|
+
c.header(key, value);
|
|
13027
|
+
}
|
|
13028
|
+
return stream(c, async (s) => {
|
|
13029
|
+
try {
|
|
13030
|
+
for await (const event of source) {
|
|
13031
|
+
const sseData = formatSSE("message", event);
|
|
13032
|
+
await s.write(sseData);
|
|
13033
|
+
}
|
|
13034
|
+
} catch (err2) {
|
|
13035
|
+
const errorEvent = {
|
|
13036
|
+
type: "error",
|
|
13037
|
+
error: err2 instanceof Error ? err2 : new Error(String(err2))
|
|
13038
|
+
};
|
|
13039
|
+
const sseData = formatSSE("error", {
|
|
13040
|
+
type: "error",
|
|
13041
|
+
message: errorEvent.error.message
|
|
13042
|
+
});
|
|
13043
|
+
await s.write(sseData);
|
|
13044
|
+
}
|
|
13045
|
+
});
|
|
13046
|
+
}
|
|
11772
13047
|
|
|
11773
13048
|
// ../app/src/routes.ts
|
|
13049
|
+
function parseJsonBody(raw2) {
|
|
13050
|
+
try {
|
|
13051
|
+
return { ok: true, data: JSON.parse(raw2) };
|
|
13052
|
+
} catch {
|
|
13053
|
+
return { ok: false };
|
|
13054
|
+
}
|
|
13055
|
+
}
|
|
13056
|
+
function elsiumErrorResponse(c, err2, fallbackMessage) {
|
|
13057
|
+
if (err2 instanceof ElsiumError) {
|
|
13058
|
+
return c.json({ error: err2.message, code: err2.code }, err2.statusCode ?? 500);
|
|
13059
|
+
}
|
|
13060
|
+
return c.json({ error: fallbackMessage }, 500);
|
|
13061
|
+
}
|
|
13062
|
+
function resolveAgent(name, agents, defaultAgent) {
|
|
13063
|
+
const agent = name ? agents.get(name) : defaultAgent;
|
|
13064
|
+
if (agent)
|
|
13065
|
+
return { agent };
|
|
13066
|
+
return { error: name ? `Agent "${name}" not found` : "No default agent configured" };
|
|
13067
|
+
}
|
|
11774
13068
|
function createRoutes(deps) {
|
|
11775
13069
|
const app = new Hono2;
|
|
11776
13070
|
let totalRequests = 0;
|
|
@@ -11811,17 +13105,32 @@ function createRoutes(deps) {
|
|
|
11811
13105
|
if (rawText.length > MAX_BODY_SIZE) {
|
|
11812
13106
|
return c.json({ error: "Request body too large (max 1MB)" }, 413);
|
|
11813
13107
|
}
|
|
11814
|
-
const
|
|
13108
|
+
const parsed = parseJsonBody(rawText);
|
|
13109
|
+
if (!parsed.ok) {
|
|
13110
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
13111
|
+
}
|
|
13112
|
+
const body = parsed.data;
|
|
11815
13113
|
if (!body.message) {
|
|
11816
13114
|
return c.json({ error: "message is required" }, 400);
|
|
11817
13115
|
}
|
|
11818
|
-
const
|
|
11819
|
-
if (
|
|
11820
|
-
return c.json({
|
|
11821
|
-
|
|
11822
|
-
|
|
13116
|
+
const resolved = resolveAgent(body.agent, deps.agents, deps.defaultAgent);
|
|
13117
|
+
if ("error" in resolved) {
|
|
13118
|
+
return c.json({ error: resolved.error }, 404);
|
|
13119
|
+
}
|
|
13120
|
+
if (body.stream) {
|
|
13121
|
+
const stream2 = deps.gateway.stream({
|
|
13122
|
+
messages: [{ role: "user", content: body.message }],
|
|
13123
|
+
system: resolved.agent.config.system,
|
|
13124
|
+
model: resolved.agent.config.model
|
|
13125
|
+
});
|
|
13126
|
+
return streamResponse(c, stream2);
|
|
13127
|
+
}
|
|
13128
|
+
let result;
|
|
13129
|
+
try {
|
|
13130
|
+
result = await resolved.agent.run(body.message);
|
|
13131
|
+
} catch (err2) {
|
|
13132
|
+
return elsiumErrorResponse(c, err2, "Agent execution failed");
|
|
11823
13133
|
}
|
|
11824
|
-
const result = await agent.run(body.message);
|
|
11825
13134
|
deps.tracer?.trackLLMCall({
|
|
11826
13135
|
model: "unknown",
|
|
11827
13136
|
inputTokens: result.usage.totalInputTokens,
|
|
@@ -11838,7 +13147,7 @@ function createRoutes(deps) {
|
|
|
11838
13147
|
totalTokens: result.usage.totalTokens,
|
|
11839
13148
|
cost: result.usage.totalCost
|
|
11840
13149
|
},
|
|
11841
|
-
model: agent.config.model ?? "default",
|
|
13150
|
+
model: resolved.agent.config.model ?? "default",
|
|
11842
13151
|
traceId: result.traceId
|
|
11843
13152
|
};
|
|
11844
13153
|
return c.json(response);
|
|
@@ -11850,7 +13159,11 @@ function createRoutes(deps) {
|
|
|
11850
13159
|
if (rawText.length > MAX_BODY_SIZE) {
|
|
11851
13160
|
return c.json({ error: "Request body too large (max 1MB)" }, 413);
|
|
11852
13161
|
}
|
|
11853
|
-
const
|
|
13162
|
+
const parsed = parseJsonBody(rawText);
|
|
13163
|
+
if (!parsed.ok) {
|
|
13164
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
13165
|
+
}
|
|
13166
|
+
const body = parsed.data;
|
|
11854
13167
|
if (!body.messages?.length) {
|
|
11855
13168
|
return c.json({ error: "messages array is required" }, 400);
|
|
11856
13169
|
}
|
|
@@ -11858,13 +13171,28 @@ function createRoutes(deps) {
|
|
|
11858
13171
|
role: m.role,
|
|
11859
13172
|
content: m.content
|
|
11860
13173
|
}));
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
13174
|
+
if (body.stream) {
|
|
13175
|
+
const stream2 = deps.gateway.stream({
|
|
13176
|
+
messages,
|
|
13177
|
+
model: body.model,
|
|
13178
|
+
system: body.system,
|
|
13179
|
+
maxTokens: body.maxTokens,
|
|
13180
|
+
temperature: body.temperature
|
|
13181
|
+
});
|
|
13182
|
+
return streamResponse(c, stream2);
|
|
13183
|
+
}
|
|
13184
|
+
let response;
|
|
13185
|
+
try {
|
|
13186
|
+
response = await deps.gateway.complete({
|
|
13187
|
+
messages,
|
|
13188
|
+
model: body.model,
|
|
13189
|
+
system: body.system,
|
|
13190
|
+
maxTokens: body.maxTokens,
|
|
13191
|
+
temperature: body.temperature
|
|
13192
|
+
});
|
|
13193
|
+
} catch (err2) {
|
|
13194
|
+
return elsiumErrorResponse(c, err2, "Completion failed");
|
|
13195
|
+
}
|
|
11868
13196
|
deps.tracer?.trackLLMCall({
|
|
11869
13197
|
model: response.model,
|
|
11870
13198
|
inputTokens: response.usage.inputTokens,
|
|
@@ -11893,9 +13221,18 @@ function createRoutes(deps) {
|
|
|
11893
13221
|
}
|
|
11894
13222
|
|
|
11895
13223
|
// ../app/src/app.ts
|
|
11896
|
-
var
|
|
13224
|
+
var log12 = createLogger();
|
|
11897
13225
|
function createApp(config) {
|
|
11898
13226
|
const app = new Hono2;
|
|
13227
|
+
app.onError((err2, c) => {
|
|
13228
|
+
const statusCode = err2 instanceof ElsiumError ? err2.statusCode ?? 500 : 500;
|
|
13229
|
+
const code = err2 instanceof ElsiumError ? err2.code : "UNKNOWN";
|
|
13230
|
+
log12.error("Unhandled error", { error: err2.message, code, path: c.req.path });
|
|
13231
|
+
return c.json({ error: err2.message, code }, statusCode);
|
|
13232
|
+
});
|
|
13233
|
+
app.notFound((c) => {
|
|
13234
|
+
return c.json({ error: "Not found" }, 404);
|
|
13235
|
+
});
|
|
11899
13236
|
const providerNames = Object.keys(config.gateway.providers);
|
|
11900
13237
|
const primaryProvider = providerNames[0];
|
|
11901
13238
|
const primaryConfig = config.gateway.providers[primaryProvider];
|
|
@@ -11910,6 +13247,8 @@ function createApp(config) {
|
|
|
11910
13247
|
costTracking: config.observe?.costTracking ?? true
|
|
11911
13248
|
});
|
|
11912
13249
|
const serverConfig = config.server ?? {};
|
|
13250
|
+
app.use("*", requestIdMiddleware());
|
|
13251
|
+
app.use("*", requestLoggerMiddleware(log12));
|
|
11913
13252
|
if (serverConfig.cors) {
|
|
11914
13253
|
app.use("*", corsMiddleware(serverConfig.cors));
|
|
11915
13254
|
}
|
|
@@ -11932,7 +13271,7 @@ function createApp(config) {
|
|
|
11932
13271
|
defaultAgent,
|
|
11933
13272
|
tracer,
|
|
11934
13273
|
startTime: Date.now(),
|
|
11935
|
-
version: "0.
|
|
13274
|
+
version: config.version ?? "0.2.2",
|
|
11936
13275
|
providers: providerNames
|
|
11937
13276
|
});
|
|
11938
13277
|
app.route("/", routes);
|
|
@@ -11948,13 +13287,25 @@ function createApp(config) {
|
|
|
11948
13287
|
port: listenPort,
|
|
11949
13288
|
hostname
|
|
11950
13289
|
});
|
|
11951
|
-
|
|
13290
|
+
let shutdownManager;
|
|
13291
|
+
if (serverConfig.gracefulShutdown) {
|
|
13292
|
+
const drainTimeoutMs = typeof serverConfig.gracefulShutdown === "object" ? serverConfig.gracefulShutdown.drainTimeoutMs : undefined;
|
|
13293
|
+
shutdownManager = createShutdownManager({
|
|
13294
|
+
drainTimeoutMs,
|
|
13295
|
+
onDrainStart: () => log12.info("Draining connections..."),
|
|
13296
|
+
onDrainComplete: () => log12.info("Drain complete")
|
|
13297
|
+
});
|
|
13298
|
+
}
|
|
13299
|
+
log12.info("ElsiumAI server started", {
|
|
11952
13300
|
url: `http://${hostname}:${listenPort}`,
|
|
11953
13301
|
routes: ["POST /chat", "POST /complete", "GET /health", "GET /metrics", "GET /agents"]
|
|
11954
13302
|
});
|
|
11955
13303
|
return {
|
|
11956
13304
|
port: listenPort,
|
|
11957
|
-
stop: () => {
|
|
13305
|
+
stop: async () => {
|
|
13306
|
+
if (shutdownManager) {
|
|
13307
|
+
await shutdownManager.shutdown();
|
|
13308
|
+
}
|
|
11958
13309
|
server.close();
|
|
11959
13310
|
}
|
|
11960
13311
|
};
|
|
@@ -11962,7 +13313,54 @@ function createApp(config) {
|
|
|
11962
13313
|
};
|
|
11963
13314
|
}
|
|
11964
13315
|
// ../app/src/rbac.ts
|
|
11965
|
-
var
|
|
13316
|
+
var log13 = createLogger();
|
|
13317
|
+
// ../app/src/tenant.ts
|
|
13318
|
+
var log14 = createLogger();
|
|
13319
|
+
function tenantMiddleware(config) {
|
|
13320
|
+
const { extractTenant, onUnknownTenant = "reject", defaultTenant } = config;
|
|
13321
|
+
return async (c, next) => {
|
|
13322
|
+
const tenant = extractTenant(c);
|
|
13323
|
+
if (!tenant) {
|
|
13324
|
+
if (onUnknownTenant === "default" && defaultTenant) {
|
|
13325
|
+
c.set("tenant", defaultTenant);
|
|
13326
|
+
log14.debug("Using default tenant", { tenantId: defaultTenant.tenantId });
|
|
13327
|
+
} else {
|
|
13328
|
+
return c.json({ error: "Tenant identification required" }, 401);
|
|
13329
|
+
}
|
|
13330
|
+
} else {
|
|
13331
|
+
c.set("tenant", tenant);
|
|
13332
|
+
log14.debug("Tenant identified", { tenantId: tenant.tenantId });
|
|
13333
|
+
}
|
|
13334
|
+
await next();
|
|
13335
|
+
};
|
|
13336
|
+
}
|
|
13337
|
+
function tenantRateLimitMiddleware() {
|
|
13338
|
+
const windows = new Map;
|
|
13339
|
+
return async (c, next) => {
|
|
13340
|
+
const tenant = c.get("tenant");
|
|
13341
|
+
if (!tenant?.limits?.maxRequestsPerMinute) {
|
|
13342
|
+
await next();
|
|
13343
|
+
return;
|
|
13344
|
+
}
|
|
13345
|
+
const limit = tenant.limits.maxRequestsPerMinute;
|
|
13346
|
+
const now = Date.now();
|
|
13347
|
+
const windowMs = 60000;
|
|
13348
|
+
const key = tenant.tenantId;
|
|
13349
|
+
let entry = windows.get(key);
|
|
13350
|
+
if (!entry || now - entry.windowStart > windowMs) {
|
|
13351
|
+
entry = { count: 0, windowStart: now };
|
|
13352
|
+
windows.set(key, entry);
|
|
13353
|
+
}
|
|
13354
|
+
entry.count++;
|
|
13355
|
+
if (entry.count > limit) {
|
|
13356
|
+
return c.json({
|
|
13357
|
+
error: "Rate limit exceeded",
|
|
13358
|
+
retryAfterMs: windowMs - (now - entry.windowStart)
|
|
13359
|
+
}, 429);
|
|
13360
|
+
}
|
|
13361
|
+
await next();
|
|
13362
|
+
};
|
|
13363
|
+
}
|
|
11966
13364
|
// ../mcp/src/client.ts
|
|
11967
13365
|
import { spawn } from "node:child_process";
|
|
11968
13366
|
function createMCPClient(config) {
|
|
@@ -12174,7 +13572,7 @@ function createMCPClient(config) {
|
|
|
12174
13572
|
};
|
|
12175
13573
|
}
|
|
12176
13574
|
// ../mcp/src/server.ts
|
|
12177
|
-
var
|
|
13575
|
+
var log15 = createLogger();
|
|
12178
13576
|
function createMCPServer(config) {
|
|
12179
13577
|
let running = false;
|
|
12180
13578
|
const toolMap = new Map(config.tools.map((t) => [t.name, t]));
|
|
@@ -12309,7 +13707,7 @@ function createMCPServer(config) {
|
|
|
12309
13707
|
pendingChunks.shift();
|
|
12310
13708
|
buffer += chunk;
|
|
12311
13709
|
if (buffer.length > MAX_BUFFER_SIZE2) {
|
|
12312
|
-
|
|
13710
|
+
log15.error("MCP server: buffer size limit exceeded, resetting");
|
|
12313
13711
|
buffer = "";
|
|
12314
13712
|
continue;
|
|
12315
13713
|
}
|
|
@@ -12344,6 +13742,119 @@ function createMCPServer(config) {
|
|
|
12344
13742
|
}
|
|
12345
13743
|
};
|
|
12346
13744
|
}
|
|
13745
|
+
// ../client/dist/index.js
|
|
13746
|
+
async function* parseSSEStream(response) {
|
|
13747
|
+
if (!response.body) {
|
|
13748
|
+
throw new Error("Response body is null");
|
|
13749
|
+
}
|
|
13750
|
+
const reader = response.body.getReader();
|
|
13751
|
+
const decoder = new TextDecoder;
|
|
13752
|
+
let buffer = "";
|
|
13753
|
+
try {
|
|
13754
|
+
while (true) {
|
|
13755
|
+
const { done, value } = await reader.read();
|
|
13756
|
+
if (done)
|
|
13757
|
+
break;
|
|
13758
|
+
buffer += decoder.decode(value, { stream: true });
|
|
13759
|
+
const lines = buffer.split(`
|
|
13760
|
+
`);
|
|
13761
|
+
buffer = lines.pop() ?? "";
|
|
13762
|
+
for (const line of lines) {
|
|
13763
|
+
if (line.startsWith("event: error")) {
|
|
13764
|
+
continue;
|
|
13765
|
+
}
|
|
13766
|
+
if (!line.startsWith("data: "))
|
|
13767
|
+
continue;
|
|
13768
|
+
const data = line.slice(6).trim();
|
|
13769
|
+
if (!data || data === "[DONE]")
|
|
13770
|
+
continue;
|
|
13771
|
+
try {
|
|
13772
|
+
const event = JSON.parse(data);
|
|
13773
|
+
yield event;
|
|
13774
|
+
} catch {}
|
|
13775
|
+
}
|
|
13776
|
+
}
|
|
13777
|
+
} finally {
|
|
13778
|
+
reader.releaseLock();
|
|
13779
|
+
}
|
|
13780
|
+
}
|
|
13781
|
+
function createClient(config) {
|
|
13782
|
+
const { baseUrl, apiKey, timeout = 30000 } = config;
|
|
13783
|
+
function headers() {
|
|
13784
|
+
const h = {
|
|
13785
|
+
"Content-Type": "application/json"
|
|
13786
|
+
};
|
|
13787
|
+
if (apiKey) {
|
|
13788
|
+
h.Authorization = `Bearer ${apiKey}`;
|
|
13789
|
+
}
|
|
13790
|
+
return h;
|
|
13791
|
+
}
|
|
13792
|
+
async function request(method, path, body) {
|
|
13793
|
+
const controller = new AbortController;
|
|
13794
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
13795
|
+
try {
|
|
13796
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
13797
|
+
method,
|
|
13798
|
+
headers: headers(),
|
|
13799
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
13800
|
+
signal: controller.signal
|
|
13801
|
+
});
|
|
13802
|
+
if (!response.ok) {
|
|
13803
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
13804
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
13805
|
+
}
|
|
13806
|
+
return await response.json();
|
|
13807
|
+
} finally {
|
|
13808
|
+
clearTimeout(timer);
|
|
13809
|
+
}
|
|
13810
|
+
}
|
|
13811
|
+
async function streamRequest(path, body) {
|
|
13812
|
+
const controller = new AbortController;
|
|
13813
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
13814
|
+
try {
|
|
13815
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
13816
|
+
method: "POST",
|
|
13817
|
+
headers: headers(),
|
|
13818
|
+
body: JSON.stringify(body),
|
|
13819
|
+
signal: controller.signal
|
|
13820
|
+
});
|
|
13821
|
+
if (!response.ok) {
|
|
13822
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
13823
|
+
clearTimeout(timer);
|
|
13824
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
13825
|
+
}
|
|
13826
|
+
return response;
|
|
13827
|
+
} catch (err2) {
|
|
13828
|
+
clearTimeout(timer);
|
|
13829
|
+
throw err2;
|
|
13830
|
+
}
|
|
13831
|
+
}
|
|
13832
|
+
return {
|
|
13833
|
+
async chat(req) {
|
|
13834
|
+
return request("POST", "/chat", { ...req, stream: false });
|
|
13835
|
+
},
|
|
13836
|
+
async* chatStream(req) {
|
|
13837
|
+
const response = await streamRequest("/chat", { ...req, stream: true });
|
|
13838
|
+
yield* parseSSEStream(response);
|
|
13839
|
+
},
|
|
13840
|
+
async complete(req) {
|
|
13841
|
+
return request("POST", "/complete", { ...req, stream: false });
|
|
13842
|
+
},
|
|
13843
|
+
async* completeStream(req) {
|
|
13844
|
+
const response = await streamRequest("/complete", { ...req, stream: true });
|
|
13845
|
+
yield* parseSSEStream(response);
|
|
13846
|
+
},
|
|
13847
|
+
async health() {
|
|
13848
|
+
return request("GET", "/health");
|
|
13849
|
+
},
|
|
13850
|
+
async metrics() {
|
|
13851
|
+
return request("GET", "/metrics");
|
|
13852
|
+
},
|
|
13853
|
+
async agents() {
|
|
13854
|
+
return request("GET", "/agents");
|
|
13855
|
+
}
|
|
13856
|
+
};
|
|
13857
|
+
}
|
|
12347
13858
|
// ../testing/src/mock-provider.ts
|
|
12348
13859
|
function mockProvider(options = {}) {
|
|
12349
13860
|
const { responses = [], defaultResponse, onRequest } = options;
|
|
@@ -12452,10 +13963,10 @@ function mockProvider(options = {}) {
|
|
|
12452
13963
|
};
|
|
12453
13964
|
}
|
|
12454
13965
|
// ../testing/src/fixtures.ts
|
|
12455
|
-
import { createHash } from "node:crypto";
|
|
13966
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
12456
13967
|
function hashMessages(messages) {
|
|
12457
13968
|
const content = messages.map((m) => `${m.role}:${m.content}`).join("|");
|
|
12458
|
-
return
|
|
13969
|
+
return createHash3("sha256").update(content).digest("hex").slice(0, 16);
|
|
12459
13970
|
}
|
|
12460
13971
|
function createFixture(name, entries) {
|
|
12461
13972
|
return {
|
|
@@ -12965,7 +14476,7 @@ function createPromptRegistry() {
|
|
|
12965
14476
|
};
|
|
12966
14477
|
}
|
|
12967
14478
|
// ../testing/src/regression.ts
|
|
12968
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14479
|
+
import { mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12969
14480
|
import { dirname } from "node:path";
|
|
12970
14481
|
function makeEmptyResult(name) {
|
|
12971
14482
|
return {
|
|
@@ -13050,7 +14561,7 @@ function createRegressionSuite(name) {
|
|
|
13050
14561
|
};
|
|
13051
14562
|
}
|
|
13052
14563
|
mkdirSync(dirname(path), { recursive: true });
|
|
13053
|
-
|
|
14564
|
+
writeFileSync2(path, JSON.stringify(baseline, null, 2));
|
|
13054
14565
|
},
|
|
13055
14566
|
addCase(input, output, score) {
|
|
13056
14567
|
if (!baseline) {
|
|
@@ -13121,7 +14632,9 @@ function createReplayPlayer(entriesOrJson) {
|
|
|
13121
14632
|
};
|
|
13122
14633
|
}
|
|
13123
14634
|
export {
|
|
14635
|
+
zodToJsonSchema,
|
|
13124
14636
|
xrayMiddleware,
|
|
14637
|
+
vectorStoreRegistry,
|
|
13125
14638
|
unwrapOr,
|
|
13126
14639
|
unwrap,
|
|
13127
14640
|
tryCatchSync,
|
|
@@ -13129,7 +14642,11 @@ export {
|
|
|
13129
14642
|
toTraceparent,
|
|
13130
14643
|
toOTelSpan,
|
|
13131
14644
|
toOTelExportRequest,
|
|
14645
|
+
tenantRateLimitMiddleware,
|
|
14646
|
+
tenantMiddleware,
|
|
14647
|
+
streamResponse,
|
|
13132
14648
|
step,
|
|
14649
|
+
sseHeaders,
|
|
13133
14650
|
sleep,
|
|
13134
14651
|
securityMiddleware,
|
|
13135
14652
|
runSupervisor,
|
|
@@ -13142,6 +14659,7 @@ export {
|
|
|
13142
14659
|
redactSecrets,
|
|
13143
14660
|
rag,
|
|
13144
14661
|
parseTraceparent,
|
|
14662
|
+
outputGuardrailMiddleware,
|
|
13145
14663
|
ok,
|
|
13146
14664
|
observe,
|
|
13147
14665
|
mockProvider,
|
|
@@ -13157,6 +14675,7 @@ export {
|
|
|
13157
14675
|
gateway,
|
|
13158
14676
|
formatToolResultAsText,
|
|
13159
14677
|
formatToolResult,
|
|
14678
|
+
formatSSE,
|
|
13160
14679
|
formatEvalReport,
|
|
13161
14680
|
extractTraceContext,
|
|
13162
14681
|
extractText,
|
|
@@ -13165,6 +14684,7 @@ export {
|
|
|
13165
14684
|
envNumber,
|
|
13166
14685
|
envBool,
|
|
13167
14686
|
env,
|
|
14687
|
+
embeddingProviderRegistry,
|
|
13168
14688
|
detectPromptInjection,
|
|
13169
14689
|
detectJailbreak,
|
|
13170
14690
|
defineWorkflow,
|
|
@@ -13176,15 +14696,18 @@ export {
|
|
|
13176
14696
|
currentTimeTool,
|
|
13177
14697
|
createToolkit,
|
|
13178
14698
|
createStream,
|
|
14699
|
+
createSqliteMemoryStore,
|
|
13179
14700
|
createSpan,
|
|
13180
14701
|
createSnapshotStore,
|
|
13181
14702
|
createSemanticValidator,
|
|
13182
14703
|
createReplayRecorder,
|
|
13183
14704
|
createReplayPlayer,
|
|
13184
14705
|
createRegressionSuite,
|
|
14706
|
+
createRegistry,
|
|
13185
14707
|
createRecorder,
|
|
13186
14708
|
createProviderMesh,
|
|
13187
14709
|
createPromptRegistry,
|
|
14710
|
+
createPgVectorStore,
|
|
13188
14711
|
createOpenAIProvider,
|
|
13189
14712
|
createOpenAIEmbeddings,
|
|
13190
14713
|
createOTLPExporter,
|
|
@@ -13195,17 +14718,25 @@ export {
|
|
|
13195
14718
|
createMCPClient,
|
|
13196
14719
|
createLogger,
|
|
13197
14720
|
createInMemoryStore,
|
|
14721
|
+
createInMemoryMemoryStore,
|
|
14722
|
+
createInMemoryCache,
|
|
13198
14723
|
createGoogleProvider,
|
|
13199
14724
|
createFixture,
|
|
14725
|
+
createExperiment,
|
|
13200
14726
|
createCostEngine,
|
|
14727
|
+
createContextManager,
|
|
13201
14728
|
createConfidenceScorer,
|
|
14729
|
+
createClient,
|
|
14730
|
+
createBatch,
|
|
13202
14731
|
createApp,
|
|
13203
14732
|
createAnthropicProvider,
|
|
13204
14733
|
createAgentSecurity,
|
|
14734
|
+
countTokens,
|
|
13205
14735
|
costTrackingMiddleware,
|
|
13206
14736
|
composeMiddleware,
|
|
13207
14737
|
checkBlockedPatterns,
|
|
13208
14738
|
calculatorTool,
|
|
13209
14739
|
calculateCost,
|
|
14740
|
+
cacheMiddleware,
|
|
13210
14741
|
ElsiumError
|
|
13211
14742
|
};
|