elsium-ai 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +465 -81
- package/package.json +11 -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;
|
|
@@ -630,6 +639,101 @@ function createCircuitBreaker(config) {
|
|
|
630
639
|
}
|
|
631
640
|
};
|
|
632
641
|
}
|
|
642
|
+
// ../core/src/shutdown.ts
|
|
643
|
+
function createShutdownManager(config) {
|
|
644
|
+
const drainTimeoutMs = config?.drainTimeoutMs ?? 30000;
|
|
645
|
+
const signals = config?.signals ?? ["SIGTERM", "SIGINT"];
|
|
646
|
+
if (drainTimeoutMs < 0 || !Number.isFinite(drainTimeoutMs)) {
|
|
647
|
+
throw new ElsiumError({
|
|
648
|
+
code: "CONFIG_ERROR",
|
|
649
|
+
message: "drainTimeoutMs must be >= 0 and finite",
|
|
650
|
+
retryable: false
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
let shuttingDown = false;
|
|
654
|
+
let inFlightCount = 0;
|
|
655
|
+
let drainResolve = null;
|
|
656
|
+
let shutdownPromise = null;
|
|
657
|
+
const signalHandlers = [];
|
|
658
|
+
function checkDrained() {
|
|
659
|
+
if (inFlightCount === 0 && drainResolve) {
|
|
660
|
+
drainResolve();
|
|
661
|
+
drainResolve = null;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async function shutdown() {
|
|
665
|
+
if (shutdownPromise)
|
|
666
|
+
return shutdownPromise;
|
|
667
|
+
shuttingDown = true;
|
|
668
|
+
shutdownPromise = (async () => {
|
|
669
|
+
config?.onDrainStart?.();
|
|
670
|
+
if (inFlightCount === 0) {
|
|
671
|
+
config?.onDrainComplete?.();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const drainPromise = new Promise((resolve) => {
|
|
675
|
+
drainResolve = resolve;
|
|
676
|
+
});
|
|
677
|
+
let drainTimer;
|
|
678
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
679
|
+
drainTimer = setTimeout(() => resolve("timeout"), drainTimeoutMs);
|
|
680
|
+
});
|
|
681
|
+
const result = await Promise.race([
|
|
682
|
+
drainPromise.then(() => "drained"),
|
|
683
|
+
timeoutPromise
|
|
684
|
+
]);
|
|
685
|
+
if (drainTimer !== undefined)
|
|
686
|
+
clearTimeout(drainTimer);
|
|
687
|
+
if (result === "timeout") {
|
|
688
|
+
config?.onForceShutdown?.();
|
|
689
|
+
} else {
|
|
690
|
+
config?.onDrainComplete?.();
|
|
691
|
+
}
|
|
692
|
+
})();
|
|
693
|
+
return shutdownPromise;
|
|
694
|
+
}
|
|
695
|
+
const manager = {
|
|
696
|
+
get inFlight() {
|
|
697
|
+
return inFlightCount;
|
|
698
|
+
},
|
|
699
|
+
get isShuttingDown() {
|
|
700
|
+
return shuttingDown;
|
|
701
|
+
},
|
|
702
|
+
async trackOperation(fn) {
|
|
703
|
+
if (shuttingDown) {
|
|
704
|
+
throw new ElsiumError({
|
|
705
|
+
code: "VALIDATION_ERROR",
|
|
706
|
+
message: "Server is shutting down, not accepting new operations",
|
|
707
|
+
retryable: true
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
inFlightCount++;
|
|
711
|
+
try {
|
|
712
|
+
return await fn();
|
|
713
|
+
} finally {
|
|
714
|
+
inFlightCount--;
|
|
715
|
+
checkDrained();
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
shutdown,
|
|
719
|
+
dispose() {
|
|
720
|
+
for (const { signal, handler } of signalHandlers) {
|
|
721
|
+
process.removeListener(signal, handler);
|
|
722
|
+
}
|
|
723
|
+
signalHandlers.length = 0;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
if (typeof process !== "undefined" && process.on) {
|
|
727
|
+
for (const signal of signals) {
|
|
728
|
+
const handler = () => {
|
|
729
|
+
manager.shutdown();
|
|
730
|
+
};
|
|
731
|
+
signalHandlers.push({ signal, handler });
|
|
732
|
+
process.on(signal, handler);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return manager;
|
|
736
|
+
}
|
|
633
737
|
// ../gateway/src/provider.ts
|
|
634
738
|
var providerRegistry = new Map;
|
|
635
739
|
var metadataRegistry = new Map;
|
|
@@ -1262,7 +1366,19 @@ function createGoogleProvider(config) {
|
|
|
1262
1366
|
return { role, parts };
|
|
1263
1367
|
}
|
|
1264
1368
|
function formatGeminiMultipartContent(msg, role) {
|
|
1265
|
-
const parts =
|
|
1369
|
+
const parts = [];
|
|
1370
|
+
for (const p of msg.content) {
|
|
1371
|
+
if (p.type === "text") {
|
|
1372
|
+
parts.push({ text: p.text });
|
|
1373
|
+
} else if (p.type === "image") {
|
|
1374
|
+
const img = p;
|
|
1375
|
+
if (img.source.type === "base64") {
|
|
1376
|
+
parts.push({ inlineData: { mimeType: img.source.mediaType, data: img.source.data } });
|
|
1377
|
+
} else {
|
|
1378
|
+
parts.push({ fileData: { mimeType: "image/jpeg", fileUri: img.source.url } });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1266
1382
|
return { role, parts };
|
|
1267
1383
|
}
|
|
1268
1384
|
function formatMessages(messages) {
|
|
@@ -1437,7 +1553,8 @@ async function handleGoogleErrorResponse(response) {
|
|
|
1437
1553
|
throw ElsiumError.authError("google");
|
|
1438
1554
|
}
|
|
1439
1555
|
if (response.status === 429) {
|
|
1440
|
-
|
|
1556
|
+
const retryAfter = response.headers.get("retry-after");
|
|
1557
|
+
throw ElsiumError.rateLimit("google", retryAfter ? Number.parseInt(retryAfter) * 1000 : undefined);
|
|
1441
1558
|
}
|
|
1442
1559
|
throw ElsiumError.providerError(`Google API error ${response.status}: ${errorBody}`, {
|
|
1443
1560
|
provider: "google",
|
|
@@ -1623,6 +1740,24 @@ function createOpenAIProvider(config) {
|
|
|
1623
1740
|
}
|
|
1624
1741
|
return openaiMsg;
|
|
1625
1742
|
}
|
|
1743
|
+
function formatUserContent(msg) {
|
|
1744
|
+
if (typeof msg.content === "string")
|
|
1745
|
+
return msg.content;
|
|
1746
|
+
const parts = [];
|
|
1747
|
+
for (const part of msg.content) {
|
|
1748
|
+
if (part.type === "text") {
|
|
1749
|
+
parts.push({ type: "text", text: part.text });
|
|
1750
|
+
} else if (part.type === "image") {
|
|
1751
|
+
if (part.source.type === "base64") {
|
|
1752
|
+
const url = `data:${part.source.mediaType};base64,${part.source.data}`;
|
|
1753
|
+
parts.push({ type: "image_url", image_url: { url } });
|
|
1754
|
+
} else {
|
|
1755
|
+
parts.push({ type: "image_url", image_url: { url: part.source.url } });
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return parts;
|
|
1760
|
+
}
|
|
1626
1761
|
function formatMessages(messages) {
|
|
1627
1762
|
const formatted = [];
|
|
1628
1763
|
for (const msg of messages) {
|
|
@@ -1638,7 +1773,7 @@ function createOpenAIProvider(config) {
|
|
|
1638
1773
|
formatted.push(formatAssistantMessage(msg));
|
|
1639
1774
|
continue;
|
|
1640
1775
|
}
|
|
1641
|
-
formatted.push({ role: "user", content:
|
|
1776
|
+
formatted.push({ role: "user", content: formatUserContent(msg) });
|
|
1642
1777
|
}
|
|
1643
1778
|
return formatted;
|
|
1644
1779
|
}
|
|
@@ -1877,7 +2012,7 @@ var PROVIDER_FACTORIES = {
|
|
|
1877
2012
|
function registerProviderFactory(name, factory) {
|
|
1878
2013
|
PROVIDER_FACTORIES[name] = factory;
|
|
1879
2014
|
}
|
|
1880
|
-
function
|
|
2015
|
+
function validateGatewayConfig(config) {
|
|
1881
2016
|
const factory = PROVIDER_FACTORIES[config.provider];
|
|
1882
2017
|
if (!factory) {
|
|
1883
2018
|
throw new ElsiumError({
|
|
@@ -1886,21 +2021,92 @@ function gateway(config) {
|
|
|
1886
2021
|
retryable: false
|
|
1887
2022
|
});
|
|
1888
2023
|
}
|
|
2024
|
+
if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
|
|
2025
|
+
throw new ElsiumError({
|
|
2026
|
+
code: "CONFIG_ERROR",
|
|
2027
|
+
message: "apiKey must be a non-empty string",
|
|
2028
|
+
retryable: false
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
if (config.timeout !== undefined && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
|
|
2032
|
+
throw new ElsiumError({
|
|
2033
|
+
code: "CONFIG_ERROR",
|
|
2034
|
+
message: "timeout must be a positive finite number",
|
|
2035
|
+
retryable: false
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
if (config.maxRetries !== undefined && (!Number.isFinite(config.maxRetries) || !Number.isInteger(config.maxRetries) || config.maxRetries < 0)) {
|
|
2039
|
+
throw new ElsiumError({
|
|
2040
|
+
code: "CONFIG_ERROR",
|
|
2041
|
+
message: "maxRetries must be a non-negative finite integer",
|
|
2042
|
+
retryable: false
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
return factory;
|
|
2046
|
+
}
|
|
2047
|
+
function autoRegisterProvider(provider) {
|
|
2048
|
+
if (!provider.metadata)
|
|
2049
|
+
return;
|
|
2050
|
+
registerProviderMetadata(provider.name, provider.metadata);
|
|
2051
|
+
if (!provider.metadata.pricing)
|
|
2052
|
+
return;
|
|
2053
|
+
for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
|
|
2054
|
+
registerPricing(model, pricing);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
function validateRequestLimits(request, maxMessages, maxInputTokens) {
|
|
2058
|
+
if (request.messages.length > maxMessages) {
|
|
2059
|
+
throw ElsiumError.validation(`Message count ${request.messages.length} exceeds limit of ${maxMessages}`);
|
|
2060
|
+
}
|
|
2061
|
+
let estimatedTokens = 0;
|
|
2062
|
+
for (const msg of request.messages) {
|
|
2063
|
+
const text = typeof msg.content === "string" ? msg.content : msg.content.map((p) => p.type === "text" ? p.text : "").join("");
|
|
2064
|
+
estimatedTokens += Math.ceil(text.length / 4);
|
|
2065
|
+
}
|
|
2066
|
+
if (estimatedTokens > maxInputTokens) {
|
|
2067
|
+
throw ElsiumError.validation(`Estimated input tokens (~${estimatedTokens}) exceeds limit of ${maxInputTokens}`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
function buildMiddlewareContext(req, providerName, defaultModel, metadata) {
|
|
2071
|
+
return {
|
|
2072
|
+
request: req,
|
|
2073
|
+
provider: providerName,
|
|
2074
|
+
model: req.model ?? defaultModel,
|
|
2075
|
+
traceId: generateTraceId(),
|
|
2076
|
+
startTime: performance.now(),
|
|
2077
|
+
metadata
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
async function accumulateStreamEvents(stream, emit) {
|
|
2081
|
+
let textContent = "";
|
|
2082
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
2083
|
+
let stopReason = "end_turn";
|
|
2084
|
+
let id = "";
|
|
2085
|
+
for await (const event of stream) {
|
|
2086
|
+
emit(event);
|
|
2087
|
+
if (event.type === "text_delta") {
|
|
2088
|
+
textContent += event.text;
|
|
2089
|
+
} else if (event.type === "message_end") {
|
|
2090
|
+
usage = event.usage;
|
|
2091
|
+
stopReason = event.stopReason;
|
|
2092
|
+
} else if (event.type === "message_start") {
|
|
2093
|
+
id = event.id;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return { textContent, usage, stopReason, id };
|
|
2097
|
+
}
|
|
2098
|
+
function gateway(config) {
|
|
2099
|
+
const factory = validateGatewayConfig(config);
|
|
1889
2100
|
const provider = factory({
|
|
1890
2101
|
apiKey: config.apiKey,
|
|
1891
2102
|
baseUrl: config.baseUrl,
|
|
1892
2103
|
timeout: config.timeout,
|
|
1893
2104
|
maxRetries: config.maxRetries
|
|
1894
2105
|
});
|
|
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
|
-
}
|
|
2106
|
+
autoRegisterProvider(provider);
|
|
1903
2107
|
const defaultModel = config.model ?? provider.defaultModel;
|
|
2108
|
+
const maxMessages = config.maxMessages ?? 1000;
|
|
2109
|
+
const maxInputTokens = config.maxInputTokens ?? 1e6;
|
|
1904
2110
|
let xrayStore = null;
|
|
1905
2111
|
const allMiddleware = [...config.middleware ?? []];
|
|
1906
2112
|
if (config.xray) {
|
|
@@ -1915,14 +2121,7 @@ function gateway(config) {
|
|
|
1915
2121
|
if (!composedMiddleware) {
|
|
1916
2122
|
return provider.complete(req);
|
|
1917
2123
|
}
|
|
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
|
-
};
|
|
2124
|
+
const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
|
|
1926
2125
|
return composedMiddleware(ctx, async (c) => provider.complete(c.request));
|
|
1927
2126
|
}
|
|
1928
2127
|
return {
|
|
@@ -1934,34 +2133,27 @@ function gateway(config) {
|
|
|
1934
2133
|
return xrayStore?.callHistory(limit) ?? [];
|
|
1935
2134
|
},
|
|
1936
2135
|
async complete(request) {
|
|
2136
|
+
validateRequestLimits(request, maxMessages, maxInputTokens);
|
|
1937
2137
|
return executeWithMiddleware(request);
|
|
1938
2138
|
},
|
|
1939
2139
|
stream(request) {
|
|
2140
|
+
validateRequestLimits(request, maxMessages, maxInputTokens);
|
|
1940
2141
|
const req = { ...request, model: request.model ?? defaultModel };
|
|
1941
2142
|
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
|
-
};
|
|
2143
|
+
const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
|
|
1950
2144
|
return createStream(async (emit) => {
|
|
1951
2145
|
await composedMiddleware(ctx, async (c) => {
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
emit(event);
|
|
1955
|
-
}
|
|
2146
|
+
const result = await accumulateStreamEvents(provider.stream(c.request), emit);
|
|
2147
|
+
const latencyMs = Math.round(performance.now() - ctx.startTime);
|
|
1956
2148
|
return {
|
|
1957
|
-
id:
|
|
1958
|
-
message: { role: "assistant", content:
|
|
1959
|
-
usage:
|
|
1960
|
-
cost:
|
|
2149
|
+
id: result.id,
|
|
2150
|
+
message: { role: "assistant", content: result.textContent },
|
|
2151
|
+
usage: result.usage,
|
|
2152
|
+
cost: calculateCost(c.model, result.usage),
|
|
1961
2153
|
model: c.model,
|
|
1962
2154
|
provider: provider.name,
|
|
1963
|
-
stopReason:
|
|
1964
|
-
latencyMs
|
|
2155
|
+
stopReason: result.stopReason,
|
|
2156
|
+
latencyMs,
|
|
1965
2157
|
traceId: ctx.traceId
|
|
1966
2158
|
};
|
|
1967
2159
|
});
|
|
@@ -2300,6 +2492,12 @@ function redactResponseSecrets(response, config) {
|
|
|
2300
2492
|
}
|
|
2301
2493
|
function securityMiddleware(config) {
|
|
2302
2494
|
return async (ctx, next) => {
|
|
2495
|
+
if (ctx.request.system) {
|
|
2496
|
+
const sysViolations = scanMessageForViolations(ctx.request.system, config);
|
|
2497
|
+
if (sysViolations.length > 0) {
|
|
2498
|
+
reportAndThrow(sysViolations, config);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2303
2501
|
for (const message of ctx.request.messages) {
|
|
2304
2502
|
const text = extractText(message.content);
|
|
2305
2503
|
if (!text)
|
|
@@ -2544,7 +2742,22 @@ function createProviderMesh(config) {
|
|
|
2544
2742
|
const available = sortedProviders.find((e) => isProviderAvailable(e.name));
|
|
2545
2743
|
const entry = available ?? sortedProviders[0];
|
|
2546
2744
|
const gw = getGateway(entry.name);
|
|
2547
|
-
|
|
2745
|
+
let resolvedStream = null;
|
|
2746
|
+
callWithCircuitBreaker(entry.name, () => {
|
|
2747
|
+
resolvedStream = gw.stream({ ...request, model: request.model ?? entry.model });
|
|
2748
|
+
return Promise.resolve(resolvedStream);
|
|
2749
|
+
}).catch(() => {});
|
|
2750
|
+
if (resolvedStream === null) {
|
|
2751
|
+
const err2 = new ElsiumError({
|
|
2752
|
+
code: "PROVIDER_ERROR",
|
|
2753
|
+
message: "Circuit breaker is open",
|
|
2754
|
+
retryable: true
|
|
2755
|
+
});
|
|
2756
|
+
return new ElsiumStream(async function* () {
|
|
2757
|
+
yield { type: "error", error: err2 };
|
|
2758
|
+
}());
|
|
2759
|
+
}
|
|
2760
|
+
return resolvedStream;
|
|
2548
2761
|
}
|
|
2549
2762
|
};
|
|
2550
2763
|
}
|
|
@@ -2709,6 +2922,18 @@ function zodToJsonSchema(schema) {
|
|
|
2709
2922
|
}
|
|
2710
2923
|
// ../tools/src/toolkit.ts
|
|
2711
2924
|
function createToolkit(name, tools) {
|
|
2925
|
+
const seen = new Set;
|
|
2926
|
+
for (const tool of tools) {
|
|
2927
|
+
if (seen.has(tool.name)) {
|
|
2928
|
+
throw new ElsiumError({
|
|
2929
|
+
code: "CONFIG_ERROR",
|
|
2930
|
+
message: `Duplicate tool name "${tool.name}" in toolkit "${name}"`,
|
|
2931
|
+
retryable: false,
|
|
2932
|
+
metadata: { toolkit: name, tool: tool.name }
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
seen.add(tool.name);
|
|
2936
|
+
}
|
|
2712
2937
|
const toolMap = new Map(tools.map((t) => [t.name, t]));
|
|
2713
2938
|
return {
|
|
2714
2939
|
name,
|
|
@@ -6727,7 +6952,26 @@ var coerce = {
|
|
|
6727
6952
|
};
|
|
6728
6953
|
var NEVER = INVALID;
|
|
6729
6954
|
// ../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+|\[
|
|
6955
|
+
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;
|
|
6956
|
+
var BLOCKED_HEADER_NAMES = new Set([
|
|
6957
|
+
"cookie",
|
|
6958
|
+
"set-cookie",
|
|
6959
|
+
"authorization",
|
|
6960
|
+
"proxy-authorization",
|
|
6961
|
+
"host",
|
|
6962
|
+
"x-api-key",
|
|
6963
|
+
"x-forwarded-for",
|
|
6964
|
+
"x-real-ip"
|
|
6965
|
+
]);
|
|
6966
|
+
function sanitizeHeaders(headers) {
|
|
6967
|
+
const safe = {};
|
|
6968
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
6969
|
+
if (!BLOCKED_HEADER_NAMES.has(key.toLowerCase())) {
|
|
6970
|
+
safe[key] = value;
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
return safe;
|
|
6974
|
+
}
|
|
6731
6975
|
function validateUrl(urlString) {
|
|
6732
6976
|
const parsed = new URL(urlString);
|
|
6733
6977
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
@@ -6752,8 +6996,9 @@ var httpFetchTool = defineTool({
|
|
|
6752
6996
|
timeoutMs: 15000,
|
|
6753
6997
|
handler: async ({ url, headers }, context) => {
|
|
6754
6998
|
validateUrl(url);
|
|
6999
|
+
const safeHeaders = headers ? sanitizeHeaders(headers) : undefined;
|
|
6755
7000
|
const response = await fetch(url, {
|
|
6756
|
-
headers,
|
|
7001
|
+
headers: safeHeaders,
|
|
6757
7002
|
signal: context.signal,
|
|
6758
7003
|
redirect: "manual"
|
|
6759
7004
|
});
|
|
@@ -7421,9 +7666,9 @@ Only respond with JSON, nothing else.`
|
|
|
7421
7666
|
if (!parsed) {
|
|
7422
7667
|
return null;
|
|
7423
7668
|
}
|
|
7424
|
-
const score = parsed.score
|
|
7669
|
+
const score = typeof parsed.score === "number" ? parsed.score : 0.5;
|
|
7425
7670
|
const threshold = config.hallucination?.threshold ?? 0.7;
|
|
7426
|
-
const claims = parsed.hallucinated_claims
|
|
7671
|
+
const claims = Array.isArray(parsed.hallucinated_claims) ? parsed.hallucinated_claims : [];
|
|
7427
7672
|
return {
|
|
7428
7673
|
passed: score >= threshold,
|
|
7429
7674
|
score,
|
|
@@ -7486,12 +7731,12 @@ Only respond with JSON, nothing else.`
|
|
|
7486
7731
|
if (!parsed) {
|
|
7487
7732
|
return null;
|
|
7488
7733
|
}
|
|
7489
|
-
const score = parsed.score
|
|
7734
|
+
const score = typeof parsed.score === "number" ? parsed.score : 0.5;
|
|
7490
7735
|
const threshold = config.relevance?.threshold ?? 0.5;
|
|
7491
7736
|
return {
|
|
7492
7737
|
passed: score >= threshold,
|
|
7493
7738
|
score,
|
|
7494
|
-
reason: parsed.reason
|
|
7739
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : score >= threshold ? "Output is relevant" : "Output lacks relevance"
|
|
7495
7740
|
};
|
|
7496
7741
|
}
|
|
7497
7742
|
function checkRelevanceHeuristic(input, output) {
|
|
@@ -7543,8 +7788,8 @@ Only respond with JSON, nothing else.`
|
|
|
7543
7788
|
if (!parsed) {
|
|
7544
7789
|
return null;
|
|
7545
7790
|
}
|
|
7546
|
-
const score = parsed.score
|
|
7547
|
-
const claims = parsed.ungrounded_claims
|
|
7791
|
+
const score = typeof parsed.score === "number" ? parsed.score : 0.5;
|
|
7792
|
+
const claims = Array.isArray(parsed.ungrounded_claims) ? parsed.ungrounded_claims : [];
|
|
7548
7793
|
return {
|
|
7549
7794
|
passed: score >= 0.7,
|
|
7550
7795
|
score,
|
|
@@ -7605,16 +7850,23 @@ Only respond with JSON, nothing else.`
|
|
|
7605
7850
|
}
|
|
7606
7851
|
|
|
7607
7852
|
// ../agents/src/state-machine.ts
|
|
7853
|
+
async function safeHook(fn) {
|
|
7854
|
+
if (!fn)
|
|
7855
|
+
return;
|
|
7856
|
+
try {
|
|
7857
|
+
await fn();
|
|
7858
|
+
} catch (_) {}
|
|
7859
|
+
}
|
|
7608
7860
|
function executeStateMachine(baseConfig, stateConfig, deps, input, options) {
|
|
7609
7861
|
return runStateMachine(baseConfig, stateConfig, deps, input, options ?? {});
|
|
7610
7862
|
}
|
|
7611
|
-
function handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, signal) {
|
|
7863
|
+
function handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, signal, hooks, approvalGate, approvalConfig) {
|
|
7612
7864
|
const toolCalls = response.message.toolCalls;
|
|
7613
7865
|
if (!toolCalls?.length || response.stopReason !== "tool_use") {
|
|
7614
7866
|
return null;
|
|
7615
7867
|
}
|
|
7616
7868
|
return (async () => {
|
|
7617
|
-
const toolResults = await executeToolCalls(toolCalls, toolMap, toolCallHistory, signal);
|
|
7869
|
+
const toolResults = await executeToolCalls(toolCalls, toolMap, toolCallHistory, signal, hooks, approvalGate, approvalConfig);
|
|
7618
7870
|
const toolMessage = {
|
|
7619
7871
|
role: "tool",
|
|
7620
7872
|
content: "",
|
|
@@ -7718,6 +7970,7 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
|
|
|
7718
7970
|
});
|
|
7719
7971
|
}
|
|
7720
7972
|
const agentSecurity = baseConfig.guardrails?.security ? createAgentSecurity(baseConfig.guardrails.security) : null;
|
|
7973
|
+
const approvalGate = baseConfig.guardrails?.approval ? createApprovalGate(baseConfig.guardrails.approval) : null;
|
|
7721
7974
|
const maxTokenBudget = guardrails?.maxTokenBudget ?? 500000;
|
|
7722
7975
|
const outputValidator = guardrails?.outputValidator ?? (() => true);
|
|
7723
7976
|
const conversationMessages = [{ role: "user", content: input }];
|
|
@@ -7735,7 +7988,8 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
|
|
|
7735
7988
|
checkTokenBudget(totalInputTokens + totalOutputTokens, maxTokenBudget);
|
|
7736
7989
|
response = applyOutputGuardrails(response, outputValidator, agentSecurity);
|
|
7737
7990
|
conversationMessages.push(response.message);
|
|
7738
|
-
|
|
7991
|
+
await safeHook(() => baseConfig.hooks?.onMessage?.(response.message));
|
|
7992
|
+
const toolCallAction = handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, options.signal, baseConfig.hooks, approvalGate, baseConfig.guardrails);
|
|
7739
7993
|
if (toolCallAction) {
|
|
7740
7994
|
await toolCallAction;
|
|
7741
7995
|
continue;
|
|
@@ -7756,9 +8010,30 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
|
|
|
7756
8010
|
metadata: { iterations, maxIterations, lastState: currentStateName }
|
|
7757
8011
|
});
|
|
7758
8012
|
}
|
|
7759
|
-
async function executeToolCalls(toolCalls, toolMap, history, signal) {
|
|
8013
|
+
async function executeToolCalls(toolCalls, toolMap, history, signal, hooks, approvalGate, approvalConfig) {
|
|
7760
8014
|
const results = [];
|
|
7761
8015
|
for (const tc of toolCalls) {
|
|
8016
|
+
await safeHook(() => hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
|
|
8017
|
+
if (approvalGate && shouldRequireApproval(approvalConfig?.approval?.requireApprovalFor, {
|
|
8018
|
+
toolName: tc.name
|
|
8019
|
+
})) {
|
|
8020
|
+
const decision = await approvalGate.requestApproval("tool_call", `Execute tool: ${tc.name}`, {
|
|
8021
|
+
toolName: tc.name,
|
|
8022
|
+
arguments: tc.arguments
|
|
8023
|
+
});
|
|
8024
|
+
if (!decision.approved) {
|
|
8025
|
+
const deniedResult = {
|
|
8026
|
+
success: false,
|
|
8027
|
+
error: `Tool call denied: ${decision.reason ?? "Approval denied"}`,
|
|
8028
|
+
toolCallId: tc.id,
|
|
8029
|
+
durationMs: 0
|
|
8030
|
+
};
|
|
8031
|
+
await safeHook(() => hooks?.onToolResult?.(deniedResult));
|
|
8032
|
+
history.push({ name: tc.name, arguments: tc.arguments, result: deniedResult });
|
|
8033
|
+
results.push(formatToolResult(deniedResult));
|
|
8034
|
+
continue;
|
|
8035
|
+
}
|
|
8036
|
+
}
|
|
7762
8037
|
const tool = toolMap.get(tc.name);
|
|
7763
8038
|
if (!tool) {
|
|
7764
8039
|
const errorResult = {
|
|
@@ -7767,11 +8042,13 @@ async function executeToolCalls(toolCalls, toolMap, history, signal) {
|
|
|
7767
8042
|
toolCallId: tc.id,
|
|
7768
8043
|
durationMs: 0
|
|
7769
8044
|
};
|
|
8045
|
+
await safeHook(() => hooks?.onToolResult?.(errorResult));
|
|
7770
8046
|
history.push({ name: tc.name, arguments: tc.arguments, result: errorResult });
|
|
7771
8047
|
results.push(formatToolResult(errorResult));
|
|
7772
8048
|
continue;
|
|
7773
8049
|
}
|
|
7774
8050
|
const result = await tool.execute(tc.arguments, { toolCallId: tc.id, signal });
|
|
8051
|
+
await safeHook(() => hooks?.onToolResult?.(result));
|
|
7775
8052
|
history.push({ name: tc.name, arguments: tc.arguments, result });
|
|
7776
8053
|
results.push(formatToolResult(result));
|
|
7777
8054
|
}
|
|
@@ -7779,7 +8056,7 @@ async function executeToolCalls(toolCalls, toolMap, history, signal) {
|
|
|
7779
8056
|
}
|
|
7780
8057
|
|
|
7781
8058
|
// ../agents/src/agent.ts
|
|
7782
|
-
async function
|
|
8059
|
+
async function safeHook2(fn) {
|
|
7783
8060
|
if (!fn)
|
|
7784
8061
|
return;
|
|
7785
8062
|
try {
|
|
@@ -7885,7 +8162,7 @@ function defineAgent(config, deps) {
|
|
|
7885
8162
|
traceId,
|
|
7886
8163
|
confidence
|
|
7887
8164
|
};
|
|
7888
|
-
await
|
|
8165
|
+
await safeHook2(() => config.hooks?.onComplete?.(agentResult));
|
|
7889
8166
|
return { action: "return", result: agentResult };
|
|
7890
8167
|
}
|
|
7891
8168
|
function checkBudget(totalInputTokens, totalOutputTokens) {
|
|
@@ -7943,7 +8220,7 @@ function defineAgent(config, deps) {
|
|
|
7943
8220
|
totalInputTokens += response.usage.inputTokens;
|
|
7944
8221
|
totalOutputTokens += response.usage.outputTokens;
|
|
7945
8222
|
totalCost += response.cost.totalCost;
|
|
7946
|
-
await
|
|
8223
|
+
await safeHook2(() => config.hooks?.onMessage?.(response.message));
|
|
7947
8224
|
conversationMessages.push(response.message);
|
|
7948
8225
|
if (!response.message.toolCalls?.length || response.stopReason !== "tool_use") {
|
|
7949
8226
|
const result = await handleNonToolResponse(response, messages, iterations, totalInputTokens, totalOutputTokens, totalCost, toolCallHistory, traceId, conversationMessages);
|
|
@@ -7971,7 +8248,7 @@ function defineAgent(config, deps) {
|
|
|
7971
8248
|
async function executeToolCalls2(toolCalls, history, options = {}) {
|
|
7972
8249
|
const results = [];
|
|
7973
8250
|
for (const tc of toolCalls) {
|
|
7974
|
-
await
|
|
8251
|
+
await safeHook2(() => config.hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
|
|
7975
8252
|
if (approvalGate && shouldRequireApproval(config.guardrails?.approval?.requireApprovalFor, {
|
|
7976
8253
|
toolName: tc.name
|
|
7977
8254
|
})) {
|
|
@@ -7983,7 +8260,7 @@ function defineAgent(config, deps) {
|
|
|
7983
8260
|
toolCallId: tc.id,
|
|
7984
8261
|
durationMs: 0
|
|
7985
8262
|
};
|
|
7986
|
-
await
|
|
8263
|
+
await safeHook2(() => config.hooks?.onToolResult?.(deniedResult));
|
|
7987
8264
|
history.push({ name: tc.name, arguments: tc.arguments, result: deniedResult });
|
|
7988
8265
|
results.push(formatToolResult(deniedResult));
|
|
7989
8266
|
continue;
|
|
@@ -7997,7 +8274,7 @@ function defineAgent(config, deps) {
|
|
|
7997
8274
|
toolCallId: tc.id,
|
|
7998
8275
|
durationMs: 0
|
|
7999
8276
|
};
|
|
8000
|
-
await
|
|
8277
|
+
await safeHook2(() => config.hooks?.onToolResult?.(errorResult));
|
|
8001
8278
|
history.push({ name: tc.name, arguments: tc.arguments, result: errorResult });
|
|
8002
8279
|
results.push(formatToolResult(errorResult));
|
|
8003
8280
|
continue;
|
|
@@ -8006,7 +8283,7 @@ function defineAgent(config, deps) {
|
|
|
8006
8283
|
toolCallId: tc.id,
|
|
8007
8284
|
signal: options.signal
|
|
8008
8285
|
});
|
|
8009
|
-
await
|
|
8286
|
+
await safeHook2(() => config.hooks?.onToolResult?.(result));
|
|
8010
8287
|
history.push({ name: tc.name, arguments: tc.arguments, result });
|
|
8011
8288
|
results.push(formatToolResult(result));
|
|
8012
8289
|
}
|
|
@@ -9267,6 +9544,7 @@ function createCostEngine(config = {}) {
|
|
|
9267
9544
|
};
|
|
9268
9545
|
}
|
|
9269
9546
|
// ../observe/src/tracer.ts
|
|
9547
|
+
import { writeFileSync } from "node:fs";
|
|
9270
9548
|
var log3 = createLogger();
|
|
9271
9549
|
function observe(config = {}) {
|
|
9272
9550
|
const {
|
|
@@ -9282,7 +9560,21 @@ function observe(config = {}) {
|
|
|
9282
9560
|
for (const out of output) {
|
|
9283
9561
|
if (out === "console") {
|
|
9284
9562
|
handlers.push(consoleHandler);
|
|
9285
|
-
} else if (out === "json-file") {
|
|
9563
|
+
} else if (out === "json-file") {
|
|
9564
|
+
exporters.push({
|
|
9565
|
+
name: "json-file",
|
|
9566
|
+
export(spansToExport) {
|
|
9567
|
+
const filename = `.elsium/traces-${Date.now()}.json`;
|
|
9568
|
+
try {
|
|
9569
|
+
writeFileSync(filename, JSON.stringify(spansToExport, null, 2));
|
|
9570
|
+
} catch (err2) {
|
|
9571
|
+
log3.error("Failed to write trace file", {
|
|
9572
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
9573
|
+
});
|
|
9574
|
+
}
|
|
9575
|
+
}
|
|
9576
|
+
});
|
|
9577
|
+
} else {
|
|
9286
9578
|
exporters.push(out);
|
|
9287
9579
|
}
|
|
9288
9580
|
}
|
|
@@ -9617,6 +9909,16 @@ function createOTLPExporter(config) {
|
|
|
9617
9909
|
} else {
|
|
9618
9910
|
startAutoFlush();
|
|
9619
9911
|
}
|
|
9912
|
+
},
|
|
9913
|
+
async shutdown() {
|
|
9914
|
+
if (flushTimer) {
|
|
9915
|
+
clearInterval(flushTimer);
|
|
9916
|
+
flushTimer = null;
|
|
9917
|
+
}
|
|
9918
|
+
if (buffer.length > 0) {
|
|
9919
|
+
const batch = buffer.splice(0, buffer.length);
|
|
9920
|
+
await sendBatch(batch);
|
|
9921
|
+
}
|
|
9620
9922
|
}
|
|
9621
9923
|
};
|
|
9622
9924
|
}
|
|
@@ -11692,7 +11994,7 @@ var Hono2 = class extends Hono {
|
|
|
11692
11994
|
// ../app/src/middleware.ts
|
|
11693
11995
|
import { timingSafeEqual } from "node:crypto";
|
|
11694
11996
|
function corsMiddleware(config = true) {
|
|
11695
|
-
const opts = typeof config === "boolean" ? { origin:
|
|
11997
|
+
const opts = typeof config === "boolean" ? { origin: "*", methods: ["GET", "POST", "OPTIONS"] } : config;
|
|
11696
11998
|
return async (c, next) => {
|
|
11697
11999
|
const requestOrigin = c.req.header("Origin") ?? "";
|
|
11698
12000
|
let allowedOrigin;
|
|
@@ -11769,8 +12071,51 @@ function rateLimitMiddleware(config) {
|
|
|
11769
12071
|
await next();
|
|
11770
12072
|
};
|
|
11771
12073
|
}
|
|
12074
|
+
function requestIdMiddleware() {
|
|
12075
|
+
return async (c, next) => {
|
|
12076
|
+
const raw2 = c.req.header("X-Request-ID");
|
|
12077
|
+
const id = raw2 && /^[\w\-.:]{1,128}$/.test(raw2) ? raw2 : generateId("req");
|
|
12078
|
+
c.set("requestId", id);
|
|
12079
|
+
await next();
|
|
12080
|
+
c.res.headers.set("X-Request-ID", id);
|
|
12081
|
+
};
|
|
12082
|
+
}
|
|
12083
|
+
function requestLoggerMiddleware(logger) {
|
|
12084
|
+
const log5 = logger ?? createLogger();
|
|
12085
|
+
return async (c, next) => {
|
|
12086
|
+
const start = Date.now();
|
|
12087
|
+
await next();
|
|
12088
|
+
const duration = Date.now() - start;
|
|
12089
|
+
log5.info(`${c.req.method} ${c.req.path}`, {
|
|
12090
|
+
method: c.req.method,
|
|
12091
|
+
path: c.req.path,
|
|
12092
|
+
status: c.res.status,
|
|
12093
|
+
durationMs: duration,
|
|
12094
|
+
requestId: c.get("requestId")
|
|
12095
|
+
});
|
|
12096
|
+
};
|
|
12097
|
+
}
|
|
11772
12098
|
|
|
11773
12099
|
// ../app/src/routes.ts
|
|
12100
|
+
function parseJsonBody(raw2) {
|
|
12101
|
+
try {
|
|
12102
|
+
return { ok: true, data: JSON.parse(raw2) };
|
|
12103
|
+
} catch {
|
|
12104
|
+
return { ok: false };
|
|
12105
|
+
}
|
|
12106
|
+
}
|
|
12107
|
+
function elsiumErrorResponse(c, err2, fallbackMessage) {
|
|
12108
|
+
if (err2 instanceof ElsiumError) {
|
|
12109
|
+
return c.json({ error: err2.message, code: err2.code }, err2.statusCode ?? 500);
|
|
12110
|
+
}
|
|
12111
|
+
return c.json({ error: fallbackMessage }, 500);
|
|
12112
|
+
}
|
|
12113
|
+
function resolveAgent(name, agents, defaultAgent) {
|
|
12114
|
+
const agent = name ? agents.get(name) : defaultAgent;
|
|
12115
|
+
if (agent)
|
|
12116
|
+
return { agent };
|
|
12117
|
+
return { error: name ? `Agent "${name}" not found` : "No default agent configured" };
|
|
12118
|
+
}
|
|
11774
12119
|
function createRoutes(deps) {
|
|
11775
12120
|
const app = new Hono2;
|
|
11776
12121
|
let totalRequests = 0;
|
|
@@ -11811,17 +12156,24 @@ function createRoutes(deps) {
|
|
|
11811
12156
|
if (rawText.length > MAX_BODY_SIZE) {
|
|
11812
12157
|
return c.json({ error: "Request body too large (max 1MB)" }, 413);
|
|
11813
12158
|
}
|
|
11814
|
-
const
|
|
12159
|
+
const parsed = parseJsonBody(rawText);
|
|
12160
|
+
if (!parsed.ok) {
|
|
12161
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
12162
|
+
}
|
|
12163
|
+
const body = parsed.data;
|
|
11815
12164
|
if (!body.message) {
|
|
11816
12165
|
return c.json({ error: "message is required" }, 400);
|
|
11817
12166
|
}
|
|
11818
|
-
const
|
|
11819
|
-
if (
|
|
11820
|
-
return c.json({
|
|
11821
|
-
|
|
11822
|
-
|
|
12167
|
+
const resolved = resolveAgent(body.agent, deps.agents, deps.defaultAgent);
|
|
12168
|
+
if ("error" in resolved) {
|
|
12169
|
+
return c.json({ error: resolved.error }, 404);
|
|
12170
|
+
}
|
|
12171
|
+
let result;
|
|
12172
|
+
try {
|
|
12173
|
+
result = await resolved.agent.run(body.message);
|
|
12174
|
+
} catch (err2) {
|
|
12175
|
+
return elsiumErrorResponse(c, err2, "Agent execution failed");
|
|
11823
12176
|
}
|
|
11824
|
-
const result = await agent.run(body.message);
|
|
11825
12177
|
deps.tracer?.trackLLMCall({
|
|
11826
12178
|
model: "unknown",
|
|
11827
12179
|
inputTokens: result.usage.totalInputTokens,
|
|
@@ -11838,7 +12190,7 @@ function createRoutes(deps) {
|
|
|
11838
12190
|
totalTokens: result.usage.totalTokens,
|
|
11839
12191
|
cost: result.usage.totalCost
|
|
11840
12192
|
},
|
|
11841
|
-
model: agent.config.model ?? "default",
|
|
12193
|
+
model: resolved.agent.config.model ?? "default",
|
|
11842
12194
|
traceId: result.traceId
|
|
11843
12195
|
};
|
|
11844
12196
|
return c.json(response);
|
|
@@ -11850,7 +12202,11 @@ function createRoutes(deps) {
|
|
|
11850
12202
|
if (rawText.length > MAX_BODY_SIZE) {
|
|
11851
12203
|
return c.json({ error: "Request body too large (max 1MB)" }, 413);
|
|
11852
12204
|
}
|
|
11853
|
-
const
|
|
12205
|
+
const parsed = parseJsonBody(rawText);
|
|
12206
|
+
if (!parsed.ok) {
|
|
12207
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
12208
|
+
}
|
|
12209
|
+
const body = parsed.data;
|
|
11854
12210
|
if (!body.messages?.length) {
|
|
11855
12211
|
return c.json({ error: "messages array is required" }, 400);
|
|
11856
12212
|
}
|
|
@@ -11858,13 +12214,18 @@ function createRoutes(deps) {
|
|
|
11858
12214
|
role: m.role,
|
|
11859
12215
|
content: m.content
|
|
11860
12216
|
}));
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
12217
|
+
let response;
|
|
12218
|
+
try {
|
|
12219
|
+
response = await deps.gateway.complete({
|
|
12220
|
+
messages,
|
|
12221
|
+
model: body.model,
|
|
12222
|
+
system: body.system,
|
|
12223
|
+
maxTokens: body.maxTokens,
|
|
12224
|
+
temperature: body.temperature
|
|
12225
|
+
});
|
|
12226
|
+
} catch (err2) {
|
|
12227
|
+
return elsiumErrorResponse(c, err2, "Completion failed");
|
|
12228
|
+
}
|
|
11868
12229
|
deps.tracer?.trackLLMCall({
|
|
11869
12230
|
model: response.model,
|
|
11870
12231
|
inputTokens: response.usage.inputTokens,
|
|
@@ -11896,6 +12257,15 @@ function createRoutes(deps) {
|
|
|
11896
12257
|
var log5 = createLogger();
|
|
11897
12258
|
function createApp(config) {
|
|
11898
12259
|
const app = new Hono2;
|
|
12260
|
+
app.onError((err2, c) => {
|
|
12261
|
+
const statusCode = err2 instanceof ElsiumError ? err2.statusCode ?? 500 : 500;
|
|
12262
|
+
const code = err2 instanceof ElsiumError ? err2.code : "UNKNOWN";
|
|
12263
|
+
log5.error("Unhandled error", { error: err2.message, code, path: c.req.path });
|
|
12264
|
+
return c.json({ error: err2.message, code }, statusCode);
|
|
12265
|
+
});
|
|
12266
|
+
app.notFound((c) => {
|
|
12267
|
+
return c.json({ error: "Not found" }, 404);
|
|
12268
|
+
});
|
|
11899
12269
|
const providerNames = Object.keys(config.gateway.providers);
|
|
11900
12270
|
const primaryProvider = providerNames[0];
|
|
11901
12271
|
const primaryConfig = config.gateway.providers[primaryProvider];
|
|
@@ -11910,6 +12280,8 @@ function createApp(config) {
|
|
|
11910
12280
|
costTracking: config.observe?.costTracking ?? true
|
|
11911
12281
|
});
|
|
11912
12282
|
const serverConfig = config.server ?? {};
|
|
12283
|
+
app.use("*", requestIdMiddleware());
|
|
12284
|
+
app.use("*", requestLoggerMiddleware(log5));
|
|
11913
12285
|
if (serverConfig.cors) {
|
|
11914
12286
|
app.use("*", corsMiddleware(serverConfig.cors));
|
|
11915
12287
|
}
|
|
@@ -11932,7 +12304,7 @@ function createApp(config) {
|
|
|
11932
12304
|
defaultAgent,
|
|
11933
12305
|
tracer,
|
|
11934
12306
|
startTime: Date.now(),
|
|
11935
|
-
version: "0.
|
|
12307
|
+
version: config.version ?? "0.2.2",
|
|
11936
12308
|
providers: providerNames
|
|
11937
12309
|
});
|
|
11938
12310
|
app.route("/", routes);
|
|
@@ -11948,13 +12320,25 @@ function createApp(config) {
|
|
|
11948
12320
|
port: listenPort,
|
|
11949
12321
|
hostname
|
|
11950
12322
|
});
|
|
12323
|
+
let shutdownManager;
|
|
12324
|
+
if (serverConfig.gracefulShutdown) {
|
|
12325
|
+
const drainTimeoutMs = typeof serverConfig.gracefulShutdown === "object" ? serverConfig.gracefulShutdown.drainTimeoutMs : undefined;
|
|
12326
|
+
shutdownManager = createShutdownManager({
|
|
12327
|
+
drainTimeoutMs,
|
|
12328
|
+
onDrainStart: () => log5.info("Draining connections..."),
|
|
12329
|
+
onDrainComplete: () => log5.info("Drain complete")
|
|
12330
|
+
});
|
|
12331
|
+
}
|
|
11951
12332
|
log5.info("ElsiumAI server started", {
|
|
11952
12333
|
url: `http://${hostname}:${listenPort}`,
|
|
11953
12334
|
routes: ["POST /chat", "POST /complete", "GET /health", "GET /metrics", "GET /agents"]
|
|
11954
12335
|
});
|
|
11955
12336
|
return {
|
|
11956
12337
|
port: listenPort,
|
|
11957
|
-
stop: () => {
|
|
12338
|
+
stop: async () => {
|
|
12339
|
+
if (shutdownManager) {
|
|
12340
|
+
await shutdownManager.shutdown();
|
|
12341
|
+
}
|
|
11958
12342
|
server.close();
|
|
11959
12343
|
}
|
|
11960
12344
|
};
|
|
@@ -12965,7 +13349,7 @@ function createPromptRegistry() {
|
|
|
12965
13349
|
};
|
|
12966
13350
|
}
|
|
12967
13351
|
// ../testing/src/regression.ts
|
|
12968
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13352
|
+
import { mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12969
13353
|
import { dirname } from "node:path";
|
|
12970
13354
|
function makeEmptyResult(name) {
|
|
12971
13355
|
return {
|
|
@@ -13050,7 +13434,7 @@ function createRegressionSuite(name) {
|
|
|
13050
13434
|
};
|
|
13051
13435
|
}
|
|
13052
13436
|
mkdirSync(dirname(path), { recursive: true });
|
|
13053
|
-
|
|
13437
|
+
writeFileSync2(path, JSON.stringify(baseline, null, 2));
|
|
13054
13438
|
},
|
|
13055
13439
|
addCase(input, output, score) {
|
|
13056
13440
|
if (!baseline) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elsium-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "ElsiumAI — A high-performance, TypeScript-first AI framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Utrera <ebutrera9103@gmail.com>",
|
|
@@ -25,16 +25,16 @@
|
|
|
25
25
|
"build": "bun build ./src/index.ts --outdir ./dist --target node && bun x tsc -p tsconfig.build.json --emitDeclarationOnly"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@elsium-ai/core": "^0.2.
|
|
29
|
-
"@elsium-ai/gateway": "^0.2.
|
|
30
|
-
"@elsium-ai/agents": "^0.2.
|
|
31
|
-
"@elsium-ai/tools": "^0.2.
|
|
32
|
-
"@elsium-ai/rag": "^0.2.
|
|
33
|
-
"@elsium-ai/workflows": "^0.2.
|
|
34
|
-
"@elsium-ai/observe": "^0.2.
|
|
35
|
-
"@elsium-ai/app": "^0.2.
|
|
36
|
-
"@elsium-ai/testing": "^0.2.
|
|
37
|
-
"@elsium-ai/mcp": "^0.2.
|
|
28
|
+
"@elsium-ai/core": "^0.2.3",
|
|
29
|
+
"@elsium-ai/gateway": "^0.2.3",
|
|
30
|
+
"@elsium-ai/agents": "^0.2.3",
|
|
31
|
+
"@elsium-ai/tools": "^0.2.3",
|
|
32
|
+
"@elsium-ai/rag": "^0.2.3",
|
|
33
|
+
"@elsium-ai/workflows": "^0.2.3",
|
|
34
|
+
"@elsium-ai/observe": "^0.2.3",
|
|
35
|
+
"@elsium-ai/app": "^0.2.3",
|
|
36
|
+
"@elsium-ai/testing": "^0.2.3",
|
|
37
|
+
"@elsium-ai/mcp": "^0.2.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"typescript": "^5.7.0"
|