@elsium-ai/app 0.3.0 → 0.4.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 +98 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +349 -238
- package/dist/routes.d.ts.map +1 -1
- package/dist/tenant.d.ts +3 -0
- package/dist/tenant.d.ts.map +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -332,6 +332,104 @@ app.use('*', rateLimitMiddleware({
|
|
|
332
332
|
|
|
333
333
|
---
|
|
334
334
|
|
|
335
|
+
## SSE Utilities
|
|
336
|
+
|
|
337
|
+
Helper functions for building Server-Sent Events responses in Hono handlers.
|
|
338
|
+
|
|
339
|
+
### `sseHeaders`
|
|
340
|
+
|
|
341
|
+
A constant object containing the standard HTTP headers for SSE responses.
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const sseHeaders: Record<string, string>
|
|
345
|
+
// { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### `formatSSE`
|
|
349
|
+
|
|
350
|
+
Formats an event name and data payload into the SSE wire format.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
function formatSSE(event: string, data: unknown): string
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
| Parameter | Type | Description |
|
|
357
|
+
| --- | --- | --- |
|
|
358
|
+
| `event` | `string` | The SSE event name. |
|
|
359
|
+
| `data` | `unknown` | The data payload (will be JSON-stringified). |
|
|
360
|
+
|
|
361
|
+
**Returns:** A formatted SSE string (e.g., `event: text_delta\ndata: {"text":"Hello"}\n\n`).
|
|
362
|
+
|
|
363
|
+
### `streamResponse`
|
|
364
|
+
|
|
365
|
+
Converts a `ReadableStream` into a Hono `Response` with the correct SSE headers.
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
function streamResponse(stream: ReadableStream): Response
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
| Parameter | Type | Description |
|
|
372
|
+
| --- | --- | --- |
|
|
373
|
+
| `stream` | `ReadableStream` | The stream to send as the response body. |
|
|
374
|
+
|
|
375
|
+
**Returns:** A `Response` object with SSE headers.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { sseHeaders, formatSSE, streamResponse } from '@elsium-ai/app'
|
|
379
|
+
|
|
380
|
+
// In a Hono route handler
|
|
381
|
+
app.post('/my-stream', (c) => {
|
|
382
|
+
const stream = new ReadableStream({
|
|
383
|
+
start(controller) {
|
|
384
|
+
controller.enqueue(new TextEncoder().encode(formatSSE('text_delta', { text: 'Hello' })))
|
|
385
|
+
controller.enqueue(new TextEncoder().encode(formatSSE('message_end', { done: true })))
|
|
386
|
+
controller.close()
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
return streamResponse(stream)
|
|
390
|
+
})
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Tenant Budget Middleware
|
|
396
|
+
|
|
397
|
+
### `tenantBudgetMiddleware`
|
|
398
|
+
|
|
399
|
+
Creates a Hono middleware that enforces per-tenant token and cost budgets using sliding windows. Each tenant is identified from the request context and tracked independently.
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
function tenantBudgetMiddleware(config?: {
|
|
403
|
+
windowMs?: number
|
|
404
|
+
maxTokensPerWindow?: number
|
|
405
|
+
maxCostPerWindow?: number
|
|
406
|
+
}): (c: Context, next: Next) => Promise<Response | void>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
| Parameter | Type | Default | Description |
|
|
410
|
+
| --- | --- | --- | --- |
|
|
411
|
+
| `config.windowMs` | `number` | `60_000` | Sliding window duration in milliseconds. |
|
|
412
|
+
| `config.maxTokensPerWindow` | `number` | `undefined` | Maximum tokens allowed per tenant per window. |
|
|
413
|
+
| `config.maxCostPerWindow` | `number` | `undefined` | Maximum cost (USD) allowed per tenant per window. |
|
|
414
|
+
|
|
415
|
+
**Responses on failure:**
|
|
416
|
+
- `429` with `{ error: 'Tenant budget exceeded' }` when the tenant's usage exceeds the configured limits.
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { tenantBudgetMiddleware } from '@elsium-ai/app'
|
|
420
|
+
import { Hono } from 'hono'
|
|
421
|
+
|
|
422
|
+
const app = new Hono()
|
|
423
|
+
|
|
424
|
+
app.use('*', tenantBudgetMiddleware({
|
|
425
|
+
windowMs: 60_000,
|
|
426
|
+
maxTokensPerWindow: 100_000,
|
|
427
|
+
maxCostPerWindow: 1.0,
|
|
428
|
+
}))
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
335
433
|
## Routes
|
|
336
434
|
|
|
337
435
|
### `createRoutes`
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,6 @@ export { createRoutes } from './routes';
|
|
|
7
7
|
export type { RoutesDeps } from './routes';
|
|
8
8
|
export { createRBAC } from './rbac';
|
|
9
9
|
export type { Permission, Role, RBACConfig, RBAC } from './rbac';
|
|
10
|
-
export { tenantMiddleware, tenantRateLimitMiddleware } from './tenant';
|
|
10
|
+
export { tenantMiddleware, tenantRateLimitMiddleware, tenantBudgetMiddleware } from './tenant';
|
|
11
11
|
export type { TenantMiddlewareConfig } from './tenant';
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,YAAY,EACX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU,EACV,eAAe,EACf,WAAW,EACX,YAAY,EACZ,eAAe,EACf,cAAc,EACd,eAAe,EACf,eAAe,EACf,mBAAmB,GACnB,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAG7D,OAAO,EACN,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,GACvB,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1C,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAGhE,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,YAAY,EACX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU,EACV,eAAe,EACf,WAAW,EACX,YAAY,EACZ,eAAe,EACf,cAAc,EACd,eAAe,EACf,eAAe,EACf,mBAAmB,GACnB,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAG7D,OAAO,EACN,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,GACvB,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1C,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAGhE,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAC9F,YAAY,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -575,6 +575,12 @@ function createShutdownManager(config) {
|
|
|
575
575
|
// ../gateway/src/provider.ts
|
|
576
576
|
var providerRegistry = new Map;
|
|
577
577
|
var metadataRegistry = new Map;
|
|
578
|
+
function getProviderFactory(name) {
|
|
579
|
+
return providerRegistry.get(name);
|
|
580
|
+
}
|
|
581
|
+
function listProviders() {
|
|
582
|
+
return Array.from(providerRegistry.keys());
|
|
583
|
+
}
|
|
578
584
|
function registerProviderMetadata(name, metadata) {
|
|
579
585
|
metadataRegistry.set(name, metadata);
|
|
580
586
|
}
|
|
@@ -600,6 +606,17 @@ function composeMiddleware(middlewares) {
|
|
|
600
606
|
return dispatch(0);
|
|
601
607
|
};
|
|
602
608
|
}
|
|
609
|
+
function composeStreamMiddleware(middlewares) {
|
|
610
|
+
return (ctx, source, finalNext) => {
|
|
611
|
+
function dispatch(i, currentCtx, currentSource) {
|
|
612
|
+
if (i >= middlewares.length) {
|
|
613
|
+
return finalNext(currentCtx, currentSource);
|
|
614
|
+
}
|
|
615
|
+
return middlewares[i](currentCtx, currentSource, (c, s) => dispatch(i + 1, c, s));
|
|
616
|
+
}
|
|
617
|
+
return dispatch(0, ctx, source);
|
|
618
|
+
};
|
|
619
|
+
}
|
|
603
620
|
var SENSITIVE_HEADERS = ["x-api-key", "authorization", "api-key"];
|
|
604
621
|
function redactHeaders(headers) {
|
|
605
622
|
const redacted = {};
|
|
@@ -904,6 +921,52 @@ function createAnthropicProvider(config) {
|
|
|
904
921
|
input_schema: t.inputSchema
|
|
905
922
|
}));
|
|
906
923
|
}
|
|
924
|
+
function buildOptionalParams(req) {
|
|
925
|
+
const params = {};
|
|
926
|
+
if (req.temperature !== undefined)
|
|
927
|
+
params.temperature = req.temperature;
|
|
928
|
+
if (req.topP !== undefined)
|
|
929
|
+
params.top_p = req.topP;
|
|
930
|
+
if (req.stopSequences?.length)
|
|
931
|
+
params.stop_sequences = req.stopSequences;
|
|
932
|
+
return params;
|
|
933
|
+
}
|
|
934
|
+
function applyStructuredOutput(body, req, tools) {
|
|
935
|
+
if (!req.schema)
|
|
936
|
+
return;
|
|
937
|
+
const jsonSchema = zodToJsonSchema(req.schema);
|
|
938
|
+
const structuredTool = {
|
|
939
|
+
name: "_structured_output",
|
|
940
|
+
description: "Return structured output matching the required schema",
|
|
941
|
+
input_schema: jsonSchema
|
|
942
|
+
};
|
|
943
|
+
body.tools = [...tools ?? [], structuredTool];
|
|
944
|
+
body.tool_choice = { type: "tool", name: "_structured_output" };
|
|
945
|
+
}
|
|
946
|
+
function buildRequestBody(req) {
|
|
947
|
+
const { system, messages } = formatMessages(req.messages);
|
|
948
|
+
const model = req.model ?? "claude-sonnet-4-6";
|
|
949
|
+
const body = {
|
|
950
|
+
model,
|
|
951
|
+
messages,
|
|
952
|
+
max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
953
|
+
...system || req.system ? { system: req.system ?? system } : {},
|
|
954
|
+
...buildOptionalParams(req),
|
|
955
|
+
...buildSeedMetadata(req)
|
|
956
|
+
};
|
|
957
|
+
const tools = formatTools(req.tools);
|
|
958
|
+
if (tools)
|
|
959
|
+
body.tools = tools;
|
|
960
|
+
applyStructuredOutput(body, req, tools);
|
|
961
|
+
return body;
|
|
962
|
+
}
|
|
963
|
+
function executeWithTimeout(fn, reqSignal) {
|
|
964
|
+
const controller = new AbortController;
|
|
965
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
966
|
+
const signals = [controller.signal, reqSignal].filter(Boolean);
|
|
967
|
+
const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
968
|
+
return fn(mergedSignal).finally(() => clearTimeout(timer));
|
|
969
|
+
}
|
|
907
970
|
function extractContentBlocks(content) {
|
|
908
971
|
const toolCalls = [];
|
|
909
972
|
const textParts = [];
|
|
@@ -955,44 +1018,12 @@ function createAnthropicProvider(config) {
|
|
|
955
1018
|
authStyle: "x-api-key"
|
|
956
1019
|
},
|
|
957
1020
|
async complete(req) {
|
|
958
|
-
const
|
|
959
|
-
const model = req.model ?? "claude-sonnet-4-6";
|
|
960
|
-
const body = {
|
|
961
|
-
model,
|
|
962
|
-
messages,
|
|
963
|
-
max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
964
|
-
...system || req.system ? { system: req.system ?? system } : {},
|
|
965
|
-
...req.temperature !== undefined ? { temperature: req.temperature } : {},
|
|
966
|
-
...req.topP !== undefined ? { top_p: req.topP } : {},
|
|
967
|
-
...req.stopSequences?.length ? { stop_sequences: req.stopSequences } : {},
|
|
968
|
-
...buildSeedMetadata(req)
|
|
969
|
-
};
|
|
970
|
-
const tools = formatTools(req.tools);
|
|
971
|
-
if (tools)
|
|
972
|
-
body.tools = tools;
|
|
973
|
-
if (req.schema) {
|
|
974
|
-
const jsonSchema = zodToJsonSchema(req.schema);
|
|
975
|
-
const structuredTool = {
|
|
976
|
-
name: "_structured_output",
|
|
977
|
-
description: "Return structured output matching the required schema",
|
|
978
|
-
input_schema: jsonSchema
|
|
979
|
-
};
|
|
980
|
-
body.tools = [...tools ?? [], structuredTool];
|
|
981
|
-
body.tool_choice = { type: "tool", name: "_structured_output" };
|
|
982
|
-
}
|
|
1021
|
+
const body = buildRequestBody(req);
|
|
983
1022
|
const startTime = performance.now();
|
|
984
|
-
const raw = await retry(async () => {
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const signals = [controller.signal, req.signal].filter(Boolean);
|
|
989
|
-
const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
990
|
-
const resp = await request("/messages", body, mergedSignal);
|
|
991
|
-
return await resp.json();
|
|
992
|
-
} finally {
|
|
993
|
-
clearTimeout(timer);
|
|
994
|
-
}
|
|
995
|
-
}, {
|
|
1023
|
+
const raw = await retry(() => executeWithTimeout(async (signal) => {
|
|
1024
|
+
const resp = await request("/messages", body, signal);
|
|
1025
|
+
return await resp.json();
|
|
1026
|
+
}, req.signal), {
|
|
996
1027
|
maxRetries,
|
|
997
1028
|
baseDelayMs: 1000,
|
|
998
1029
|
shouldRetry: (e) => e instanceof ElsiumError && e.retryable
|
|
@@ -1001,29 +1032,12 @@ function createAnthropicProvider(config) {
|
|
|
1001
1032
|
return parseResponse(raw, latencyMs);
|
|
1002
1033
|
},
|
|
1003
1034
|
stream(req) {
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1007
|
-
model,
|
|
1008
|
-
messages,
|
|
1009
|
-
max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
1010
|
-
stream: true,
|
|
1011
|
-
...system || req.system ? { system: req.system ?? system } : {},
|
|
1012
|
-
...req.temperature !== undefined ? { temperature: req.temperature } : {},
|
|
1013
|
-
...req.topP !== undefined ? { top_p: req.topP } : {},
|
|
1014
|
-
...req.stopSequences?.length ? { stop_sequences: req.stopSequences } : {},
|
|
1015
|
-
...buildSeedMetadata(req)
|
|
1016
|
-
};
|
|
1017
|
-
const tools = formatTools(req.tools);
|
|
1018
|
-
if (tools)
|
|
1019
|
-
body.tools = tools;
|
|
1035
|
+
const body = buildRequestBody(req);
|
|
1036
|
+
body.stream = true;
|
|
1037
|
+
const model = body.model ?? "claude-sonnet-4-6";
|
|
1020
1038
|
return createStream(async (emit) => {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
try {
|
|
1024
|
-
const signals = [controller.signal, req.signal].filter(Boolean);
|
|
1025
|
-
const mergedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
1026
|
-
const resp = await request("/messages", body, mergedSignal);
|
|
1039
|
+
await executeWithTimeout(async (signal) => {
|
|
1040
|
+
const resp = await request("/messages", body, signal);
|
|
1027
1041
|
if (!resp.body)
|
|
1028
1042
|
throw new ElsiumError({
|
|
1029
1043
|
code: "STREAM_ERROR",
|
|
@@ -1032,9 +1046,7 @@ function createAnthropicProvider(config) {
|
|
|
1032
1046
|
retryable: false
|
|
1033
1047
|
});
|
|
1034
1048
|
await processAnthropicSSEStream(resp.body, model, emit);
|
|
1035
|
-
}
|
|
1036
|
-
clearTimeout(timer);
|
|
1037
|
-
}
|
|
1049
|
+
}, req.signal);
|
|
1038
1050
|
});
|
|
1039
1051
|
},
|
|
1040
1052
|
async listModels() {
|
|
@@ -1188,31 +1200,38 @@ function createGoogleProvider(config) {
|
|
|
1188
1200
|
}
|
|
1189
1201
|
return { role, parts };
|
|
1190
1202
|
}
|
|
1203
|
+
function convertGeminiImagePart(p) {
|
|
1204
|
+
if (p.source.type === "base64") {
|
|
1205
|
+
return { inlineData: { mimeType: p.source.mediaType, data: p.source.data } };
|
|
1206
|
+
}
|
|
1207
|
+
return { fileData: { mimeType: "image/jpeg", fileUri: p.source.url } };
|
|
1208
|
+
}
|
|
1209
|
+
function convertGeminiMediaPart(p) {
|
|
1210
|
+
if (p.source.type === "base64") {
|
|
1211
|
+
return { inlineData: { mimeType: p.source.mediaType, data: p.source.data } };
|
|
1212
|
+
}
|
|
1213
|
+
const urlSource = p.source;
|
|
1214
|
+
return { fileData: { mimeType: "application/octet-stream", fileUri: urlSource.url } };
|
|
1215
|
+
}
|
|
1216
|
+
function convertGeminiContentPart(p) {
|
|
1217
|
+
if (p.type === "text") {
|
|
1218
|
+
return { text: p.text };
|
|
1219
|
+
}
|
|
1220
|
+
if (p.type === "image") {
|
|
1221
|
+
return convertGeminiImagePart(p);
|
|
1222
|
+
}
|
|
1223
|
+
if (p.type === "audio" || p.type === "document") {
|
|
1224
|
+
return convertGeminiMediaPart(p);
|
|
1225
|
+
}
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1191
1228
|
function formatGeminiMultipartContent(msg, role) {
|
|
1229
|
+
const content = msg.content;
|
|
1192
1230
|
const parts = [];
|
|
1193
|
-
for (const p of
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
const img = p;
|
|
1198
|
-
if (img.source.type === "base64") {
|
|
1199
|
-
parts.push({ inlineData: { mimeType: img.source.mediaType, data: img.source.data } });
|
|
1200
|
-
} else {
|
|
1201
|
-
parts.push({ fileData: { mimeType: "image/jpeg", fileUri: img.source.url } });
|
|
1202
|
-
}
|
|
1203
|
-
} else if (p.type === "audio" || p.type === "document") {
|
|
1204
|
-
const media = p;
|
|
1205
|
-
if (media.source.type === "base64") {
|
|
1206
|
-
parts.push({
|
|
1207
|
-
inlineData: { mimeType: media.source.mediaType, data: media.source.data }
|
|
1208
|
-
});
|
|
1209
|
-
} else {
|
|
1210
|
-
const urlSource = media.source;
|
|
1211
|
-
parts.push({
|
|
1212
|
-
fileData: { mimeType: "application/octet-stream", fileUri: urlSource.url }
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1231
|
+
for (const p of content) {
|
|
1232
|
+
const converted = convertGeminiContentPart(p);
|
|
1233
|
+
if (converted)
|
|
1234
|
+
parts.push(converted);
|
|
1216
1235
|
}
|
|
1217
1236
|
return { role, parts };
|
|
1218
1237
|
}
|
|
@@ -1579,40 +1598,48 @@ function createOpenAIProvider(config) {
|
|
|
1579
1598
|
}
|
|
1580
1599
|
return openaiMsg;
|
|
1581
1600
|
}
|
|
1601
|
+
function convertImagePart(part) {
|
|
1602
|
+
if (part.source.type === "base64") {
|
|
1603
|
+
const url = `data:${part.source.mediaType};base64,${part.source.data}`;
|
|
1604
|
+
return { type: "image_url", image_url: { url } };
|
|
1605
|
+
}
|
|
1606
|
+
return { type: "image_url", image_url: { url: part.source.url } };
|
|
1607
|
+
}
|
|
1608
|
+
function convertAudioPart(part) {
|
|
1609
|
+
if (part.source.type === "base64") {
|
|
1610
|
+
const format = part.source.mediaType.split("/")[1] ?? "wav";
|
|
1611
|
+
return { type: "input_audio", input_audio: { data: part.source.data, format } };
|
|
1612
|
+
}
|
|
1613
|
+
return { type: "text", text: "[audio: url source requires file upload]" };
|
|
1614
|
+
}
|
|
1615
|
+
function convertDocumentPart(part) {
|
|
1616
|
+
if (part.source.type === "base64") {
|
|
1617
|
+
return {
|
|
1618
|
+
type: "text",
|
|
1619
|
+
text: `[document: ${part.source.mediaType} content attached as base64]`
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
return { type: "text", text: `[document: ${part.source.url}]` };
|
|
1623
|
+
}
|
|
1624
|
+
function convertContentPart(part) {
|
|
1625
|
+
if (part.type === "text")
|
|
1626
|
+
return { type: "text", text: part.text };
|
|
1627
|
+
if (part.type === "image")
|
|
1628
|
+
return convertImagePart(part);
|
|
1629
|
+
if (part.type === "audio")
|
|
1630
|
+
return convertAudioPart(part);
|
|
1631
|
+
if (part.type === "document")
|
|
1632
|
+
return convertDocumentPart(part);
|
|
1633
|
+
return null;
|
|
1634
|
+
}
|
|
1582
1635
|
function formatUserContent(msg) {
|
|
1583
1636
|
if (typeof msg.content === "string")
|
|
1584
1637
|
return msg.content;
|
|
1585
1638
|
const parts = [];
|
|
1586
1639
|
for (const part of msg.content) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
if (part.source.type === "base64") {
|
|
1591
|
-
const url = `data:${part.source.mediaType};base64,${part.source.data}`;
|
|
1592
|
-
parts.push({ type: "image_url", image_url: { url } });
|
|
1593
|
-
} else {
|
|
1594
|
-
parts.push({ type: "image_url", image_url: { url: part.source.url } });
|
|
1595
|
-
}
|
|
1596
|
-
} else if (part.type === "audio") {
|
|
1597
|
-
if (part.source.type === "base64") {
|
|
1598
|
-
const format = part.source.mediaType.split("/")[1] ?? "wav";
|
|
1599
|
-
parts.push({
|
|
1600
|
-
type: "input_audio",
|
|
1601
|
-
input_audio: { data: part.source.data, format }
|
|
1602
|
-
});
|
|
1603
|
-
} else {
|
|
1604
|
-
parts.push({ type: "text", text: "[audio: url source requires file upload]" });
|
|
1605
|
-
}
|
|
1606
|
-
} else if (part.type === "document") {
|
|
1607
|
-
if (part.source.type === "base64") {
|
|
1608
|
-
parts.push({
|
|
1609
|
-
type: "text",
|
|
1610
|
-
text: `[document: ${part.source.mediaType} content attached as base64]`
|
|
1611
|
-
});
|
|
1612
|
-
} else {
|
|
1613
|
-
parts.push({ type: "text", text: `[document: ${part.source.url}]` });
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1640
|
+
const converted = convertContentPart(part);
|
|
1641
|
+
if (converted)
|
|
1642
|
+
parts.push(converted);
|
|
1616
1643
|
}
|
|
1617
1644
|
return parts;
|
|
1618
1645
|
}
|
|
@@ -1647,6 +1674,49 @@ function createOpenAIProvider(config) {
|
|
|
1647
1674
|
}
|
|
1648
1675
|
}));
|
|
1649
1676
|
}
|
|
1677
|
+
function buildOptionalParams(req) {
|
|
1678
|
+
const params = {};
|
|
1679
|
+
if (req.temperature !== undefined)
|
|
1680
|
+
params.temperature = req.temperature;
|
|
1681
|
+
if (req.seed !== undefined)
|
|
1682
|
+
params.seed = req.seed;
|
|
1683
|
+
if (req.topP !== undefined)
|
|
1684
|
+
params.top_p = req.topP;
|
|
1685
|
+
if (req.stopSequences?.length)
|
|
1686
|
+
params.stop = req.stopSequences;
|
|
1687
|
+
return params;
|
|
1688
|
+
}
|
|
1689
|
+
function applyResponseFormat(body, req) {
|
|
1690
|
+
if (!req.schema)
|
|
1691
|
+
return;
|
|
1692
|
+
const jsonSchema = zodToJsonSchema(req.schema);
|
|
1693
|
+
body.response_format = {
|
|
1694
|
+
type: "json_schema",
|
|
1695
|
+
json_schema: {
|
|
1696
|
+
name: "structured_output",
|
|
1697
|
+
strict: true,
|
|
1698
|
+
schema: jsonSchema
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function buildRequestBody(req) {
|
|
1703
|
+
const messages = formatMessages(req.messages);
|
|
1704
|
+
const model = req.model ?? "gpt-4o";
|
|
1705
|
+
if (req.system) {
|
|
1706
|
+
messages.unshift({ role: "system", content: req.system });
|
|
1707
|
+
}
|
|
1708
|
+
const body = {
|
|
1709
|
+
model,
|
|
1710
|
+
messages,
|
|
1711
|
+
max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS2,
|
|
1712
|
+
...buildOptionalParams(req)
|
|
1713
|
+
};
|
|
1714
|
+
const tools = formatTools(req.tools);
|
|
1715
|
+
if (tools)
|
|
1716
|
+
body.tools = tools;
|
|
1717
|
+
applyResponseFormat(body, req);
|
|
1718
|
+
return body;
|
|
1719
|
+
}
|
|
1650
1720
|
function parseResponse(raw, latencyMs) {
|
|
1651
1721
|
const traceId = generateTraceId();
|
|
1652
1722
|
const choice = raw.choices[0];
|
|
@@ -1691,34 +1761,7 @@ function createOpenAIProvider(config) {
|
|
|
1691
1761
|
authStyle: "bearer"
|
|
1692
1762
|
},
|
|
1693
1763
|
async complete(req) {
|
|
1694
|
-
const
|
|
1695
|
-
const model = req.model ?? "gpt-4o";
|
|
1696
|
-
if (req.system) {
|
|
1697
|
-
messages.unshift({ role: "system", content: req.system });
|
|
1698
|
-
}
|
|
1699
|
-
const body = {
|
|
1700
|
-
model,
|
|
1701
|
-
messages,
|
|
1702
|
-
max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS2,
|
|
1703
|
-
...req.temperature !== undefined ? { temperature: req.temperature } : {},
|
|
1704
|
-
...req.seed !== undefined ? { seed: req.seed } : {},
|
|
1705
|
-
...req.topP !== undefined ? { top_p: req.topP } : {},
|
|
1706
|
-
...req.stopSequences?.length ? { stop: req.stopSequences } : {}
|
|
1707
|
-
};
|
|
1708
|
-
const tools = formatTools(req.tools);
|
|
1709
|
-
if (tools)
|
|
1710
|
-
body.tools = tools;
|
|
1711
|
-
if (req.schema) {
|
|
1712
|
-
const jsonSchema = zodToJsonSchema(req.schema);
|
|
1713
|
-
body.response_format = {
|
|
1714
|
-
type: "json_schema",
|
|
1715
|
-
json_schema: {
|
|
1716
|
-
name: "structured_output",
|
|
1717
|
-
strict: true,
|
|
1718
|
-
schema: jsonSchema
|
|
1719
|
-
}
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1764
|
+
const body = buildRequestBody(req);
|
|
1722
1765
|
const startTime = performance.now();
|
|
1723
1766
|
const raw = await retry(async () => {
|
|
1724
1767
|
const controller = new AbortController;
|
|
@@ -1740,25 +1783,10 @@ function createOpenAIProvider(config) {
|
|
|
1740
1783
|
return parseResponse(raw, latencyMs);
|
|
1741
1784
|
},
|
|
1742
1785
|
stream(req) {
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1748
|
-
const body = {
|
|
1749
|
-
model,
|
|
1750
|
-
messages,
|
|
1751
|
-
max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS2,
|
|
1752
|
-
stream: true,
|
|
1753
|
-
stream_options: { include_usage: true },
|
|
1754
|
-
...req.temperature !== undefined ? { temperature: req.temperature } : {},
|
|
1755
|
-
...req.seed !== undefined ? { seed: req.seed } : {},
|
|
1756
|
-
...req.topP !== undefined ? { top_p: req.topP } : {},
|
|
1757
|
-
...req.stopSequences?.length ? { stop: req.stopSequences } : {}
|
|
1758
|
-
};
|
|
1759
|
-
const tools = formatTools(req.tools);
|
|
1760
|
-
if (tools)
|
|
1761
|
-
body.tools = tools;
|
|
1786
|
+
const body = buildRequestBody(req);
|
|
1787
|
+
body.stream = true;
|
|
1788
|
+
body.stream_options = { include_usage: true };
|
|
1789
|
+
const model = body.model ?? "gpt-4o";
|
|
1762
1790
|
return createStream(async (emit) => {
|
|
1763
1791
|
const controller = new AbortController;
|
|
1764
1792
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
@@ -1878,12 +1906,29 @@ var PROVIDER_FACTORIES = {
|
|
|
1878
1906
|
openai: createOpenAIProvider,
|
|
1879
1907
|
google: createGoogleProvider
|
|
1880
1908
|
};
|
|
1909
|
+
registerProviderMetadata("anthropic", {
|
|
1910
|
+
baseUrl: "https://api.anthropic.com/v1/messages",
|
|
1911
|
+
capabilities: ["tools", "vision", "streaming", "system"],
|
|
1912
|
+
authStyle: "x-api-key"
|
|
1913
|
+
});
|
|
1914
|
+
registerProviderMetadata("openai", {
|
|
1915
|
+
baseUrl: "https://api.openai.com/v1/chat/completions",
|
|
1916
|
+
capabilities: ["tools", "vision", "streaming", "system", "json_mode"],
|
|
1917
|
+
authStyle: "bearer"
|
|
1918
|
+
});
|
|
1919
|
+
registerProviderMetadata("google", {
|
|
1920
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
|
|
1921
|
+
capabilities: ["tools", "vision", "streaming", "system"],
|
|
1922
|
+
authStyle: "bearer"
|
|
1923
|
+
});
|
|
1881
1924
|
function validateGatewayConfig(config) {
|
|
1882
|
-
const factory = PROVIDER_FACTORIES[config.provider];
|
|
1925
|
+
const factory = PROVIDER_FACTORIES[config.provider] ?? getProviderFactory(config.provider);
|
|
1883
1926
|
if (!factory) {
|
|
1927
|
+
const available = [...Object.keys(PROVIDER_FACTORIES), ...listProviders()];
|
|
1928
|
+
const unique = [...new Set(available)];
|
|
1884
1929
|
throw new ElsiumError({
|
|
1885
1930
|
code: "CONFIG_ERROR",
|
|
1886
|
-
message: `Unknown provider: ${config.provider}. Available: ${
|
|
1931
|
+
message: `Unknown provider: ${config.provider}. Available: ${unique.join(", ")}`,
|
|
1887
1932
|
retryable: false
|
|
1888
1933
|
});
|
|
1889
1934
|
}
|
|
@@ -1961,6 +2006,24 @@ async function accumulateStreamEvents(stream, emit) {
|
|
|
1961
2006
|
}
|
|
1962
2007
|
return { textContent, usage, stopReason, id };
|
|
1963
2008
|
}
|
|
2009
|
+
function extractFromToolCalls(response) {
|
|
2010
|
+
if (response.stopReason !== "tool_use" || !response.message.toolCalls?.length) {
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const structuredCall = response.message.toolCalls.find((tc) => tc.name === "_structured_output");
|
|
2014
|
+
return structuredCall?.arguments;
|
|
2015
|
+
}
|
|
2016
|
+
function extractJsonFromText(response) {
|
|
2017
|
+
let text = typeof response.message.content === "string" ? response.message.content : "";
|
|
2018
|
+
text = text.replace(/^```(?:json)?\s*\n?([\s\S]*?)\n?\s*```$/gm, "$1").trim();
|
|
2019
|
+
const jsonMatch = text.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
|
|
2020
|
+
if (!jsonMatch) {
|
|
2021
|
+
throw ElsiumError.validation("LLM response did not contain valid JSON", {
|
|
2022
|
+
response: text
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
return JSON.parse(jsonMatch[0]);
|
|
2026
|
+
}
|
|
1964
2027
|
function gateway(config) {
|
|
1965
2028
|
const factory = validateGatewayConfig(config);
|
|
1966
2029
|
const provider = factory({
|
|
@@ -1982,6 +2045,7 @@ function gateway(config) {
|
|
|
1982
2045
|
allMiddleware.push(xm);
|
|
1983
2046
|
}
|
|
1984
2047
|
const composedMiddleware = allMiddleware.length ? composeMiddleware(allMiddleware) : null;
|
|
2048
|
+
const composedStreamMiddleware = config.streamMiddleware?.length ? composeStreamMiddleware(config.streamMiddleware) : null;
|
|
1985
2049
|
async function executeWithMiddleware(request) {
|
|
1986
2050
|
const req = { ...request, model: request.model ?? defaultModel };
|
|
1987
2051
|
if (!composedMiddleware) {
|
|
@@ -2006,11 +2070,11 @@ function gateway(config) {
|
|
|
2006
2070
|
validateRequestLimits(request, maxMessages, maxInputTokens);
|
|
2007
2071
|
const req = { ...request, model: request.model ?? defaultModel };
|
|
2008
2072
|
if (composedMiddleware) {
|
|
2009
|
-
const
|
|
2073
|
+
const ctx2 = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
|
|
2010
2074
|
return createStream(async (emit) => {
|
|
2011
|
-
await composedMiddleware(
|
|
2075
|
+
await composedMiddleware(ctx2, async (c) => {
|
|
2012
2076
|
const result = await accumulateStreamEvents(provider.stream(c.request), emit);
|
|
2013
|
-
const latencyMs = Math.round(performance.now() -
|
|
2077
|
+
const latencyMs = Math.round(performance.now() - ctx2.startTime);
|
|
2014
2078
|
return {
|
|
2015
2079
|
id: result.id,
|
|
2016
2080
|
message: { role: "assistant", content: result.textContent },
|
|
@@ -2020,12 +2084,21 @@ function gateway(config) {
|
|
|
2020
2084
|
provider: provider.name,
|
|
2021
2085
|
stopReason: result.stopReason,
|
|
2022
2086
|
latencyMs,
|
|
2023
|
-
traceId:
|
|
2087
|
+
traceId: ctx2.traceId
|
|
2024
2088
|
};
|
|
2025
2089
|
});
|
|
2026
2090
|
});
|
|
2027
2091
|
}
|
|
2028
|
-
|
|
2092
|
+
const rawStream = provider.stream(req);
|
|
2093
|
+
if (!composedStreamMiddleware)
|
|
2094
|
+
return rawStream;
|
|
2095
|
+
const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
|
|
2096
|
+
return createStream(async (emit) => {
|
|
2097
|
+
const processed = composedStreamMiddleware(ctx, rawStream, (_c, s) => s);
|
|
2098
|
+
for await (const event of processed) {
|
|
2099
|
+
emit(event);
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2029
2102
|
},
|
|
2030
2103
|
async generate(request) {
|
|
2031
2104
|
const { schema, ...rest } = request;
|
|
@@ -2042,23 +2115,7 @@ function gateway(config) {
|
|
|
2042
2115
|
|
|
2043
2116
|
`)
|
|
2044
2117
|
});
|
|
2045
|
-
|
|
2046
|
-
if (response.stopReason === "tool_use" && response.message.toolCalls?.length) {
|
|
2047
|
-
const structuredCall = response.message.toolCalls.find((tc) => tc.name === "_structured_output");
|
|
2048
|
-
if (structuredCall) {
|
|
2049
|
-
parsed = structuredCall.arguments;
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
if (parsed === undefined) {
|
|
2053
|
-
const text = typeof response.message.content === "string" ? response.message.content : "";
|
|
2054
|
-
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
2055
|
-
if (!jsonMatch) {
|
|
2056
|
-
throw ElsiumError.validation("LLM response did not contain valid JSON", {
|
|
2057
|
-
response: text
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
parsed = JSON.parse(jsonMatch[0]);
|
|
2061
|
-
}
|
|
2118
|
+
const parsed = extractFromToolCalls(response) ?? extractJsonFromText(response);
|
|
2062
2119
|
const result = schema.safeParse(parsed);
|
|
2063
2120
|
if (!result.success) {
|
|
2064
2121
|
throw ElsiumError.validation("LLM response did not match schema", {
|
|
@@ -2284,7 +2341,7 @@ function createNoopSpan(name, kind) {
|
|
|
2284
2341
|
var log8 = createLogger();
|
|
2285
2342
|
// ../observe/src/otel.ts
|
|
2286
2343
|
var log9 = createLogger();
|
|
2287
|
-
// ../../node_modules/.bun/@hono+node-server@1.19.
|
|
2344
|
+
// ../../node_modules/.bun/@hono+node-server@1.19.10/node_modules/@hono/node-server/dist/index.mjs
|
|
2288
2345
|
import { createServer as createServerHTTP } from "http";
|
|
2289
2346
|
import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
|
|
2290
2347
|
import { Http2ServerRequest } from "http2";
|
|
@@ -2818,7 +2875,7 @@ var serve = (options, listeningListener) => {
|
|
|
2818
2875
|
return server;
|
|
2819
2876
|
};
|
|
2820
2877
|
|
|
2821
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
2878
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/compose.js
|
|
2822
2879
|
var compose = (middleware, onError, onNotFound) => {
|
|
2823
2880
|
return (context, next) => {
|
|
2824
2881
|
let index = -1;
|
|
@@ -2862,10 +2919,10 @@ var compose = (middleware, onError, onNotFound) => {
|
|
|
2862
2919
|
};
|
|
2863
2920
|
};
|
|
2864
2921
|
|
|
2865
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
2922
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/request/constants.js
|
|
2866
2923
|
var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
|
|
2867
2924
|
|
|
2868
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
2925
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/utils/body.js
|
|
2869
2926
|
var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
|
|
2870
2927
|
const { all = false, dot = false } = options;
|
|
2871
2928
|
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers;
|
|
@@ -2933,7 +2990,7 @@ var handleParsingNestedValues = (form, key, value) => {
|
|
|
2933
2990
|
});
|
|
2934
2991
|
};
|
|
2935
2992
|
|
|
2936
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
2993
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/utils/url.js
|
|
2937
2994
|
var splitPath = (path) => {
|
|
2938
2995
|
const paths = path.split("/");
|
|
2939
2996
|
if (paths[0] === "") {
|
|
@@ -3133,7 +3190,7 @@ var getQueryParams = (url, key) => {
|
|
|
3133
3190
|
};
|
|
3134
3191
|
var decodeURIComponent_ = decodeURIComponent;
|
|
3135
3192
|
|
|
3136
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3193
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/request.js
|
|
3137
3194
|
var tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
|
|
3138
3195
|
var HonoRequest = class {
|
|
3139
3196
|
raw;
|
|
@@ -3244,7 +3301,7 @@ var HonoRequest = class {
|
|
|
3244
3301
|
}
|
|
3245
3302
|
};
|
|
3246
3303
|
|
|
3247
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3304
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/utils/html.js
|
|
3248
3305
|
var HtmlEscapedCallbackPhase = {
|
|
3249
3306
|
Stringify: 1,
|
|
3250
3307
|
BeforeStream: 2,
|
|
@@ -3282,7 +3339,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) =>
|
|
|
3282
3339
|
}
|
|
3283
3340
|
};
|
|
3284
3341
|
|
|
3285
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3342
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/context.js
|
|
3286
3343
|
var TEXT_PLAIN = "text/plain; charset=UTF-8";
|
|
3287
3344
|
var setDefaultContentType = (contentType, headers) => {
|
|
3288
3345
|
return {
|
|
@@ -3449,7 +3506,7 @@ var Context = class {
|
|
|
3449
3506
|
};
|
|
3450
3507
|
};
|
|
3451
3508
|
|
|
3452
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3509
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router.js
|
|
3453
3510
|
var METHOD_NAME_ALL = "ALL";
|
|
3454
3511
|
var METHOD_NAME_ALL_LOWERCASE = "all";
|
|
3455
3512
|
var METHODS = ["get", "post", "put", "delete", "options", "patch"];
|
|
@@ -3457,10 +3514,10 @@ var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is
|
|
|
3457
3514
|
var UnsupportedPathError = class extends Error {
|
|
3458
3515
|
};
|
|
3459
3516
|
|
|
3460
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3517
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/utils/constants.js
|
|
3461
3518
|
var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
|
|
3462
3519
|
|
|
3463
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3520
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/hono-base.js
|
|
3464
3521
|
var notFoundHandler = (c) => {
|
|
3465
3522
|
return c.text("404 Not Found", 404);
|
|
3466
3523
|
};
|
|
@@ -3679,7 +3736,7 @@ var Hono = class _Hono {
|
|
|
3679
3736
|
};
|
|
3680
3737
|
};
|
|
3681
3738
|
|
|
3682
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3739
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router/reg-exp-router/matcher.js
|
|
3683
3740
|
var emptyParam = [];
|
|
3684
3741
|
function match(method, path) {
|
|
3685
3742
|
const matchers = this.buildAllMatchers();
|
|
@@ -3700,7 +3757,7 @@ function match(method, path) {
|
|
|
3700
3757
|
return match2(method, path);
|
|
3701
3758
|
}
|
|
3702
3759
|
|
|
3703
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3760
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router/reg-exp-router/node.js
|
|
3704
3761
|
var LABEL_REG_EXP_STR = "[^/]+";
|
|
3705
3762
|
var ONLY_WILDCARD_REG_EXP_STR = ".*";
|
|
3706
3763
|
var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
|
|
@@ -3804,7 +3861,7 @@ var Node = class _Node {
|
|
|
3804
3861
|
}
|
|
3805
3862
|
};
|
|
3806
3863
|
|
|
3807
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3864
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router/reg-exp-router/trie.js
|
|
3808
3865
|
var Trie = class {
|
|
3809
3866
|
#context = { varIndex: 0 };
|
|
3810
3867
|
#root = new Node;
|
|
@@ -3860,7 +3917,7 @@ var Trie = class {
|
|
|
3860
3917
|
}
|
|
3861
3918
|
};
|
|
3862
3919
|
|
|
3863
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
3920
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router/reg-exp-router/router.js
|
|
3864
3921
|
var nullMatcher = [/^$/, [], /* @__PURE__ */ Object.create(null)];
|
|
3865
3922
|
var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
|
|
3866
3923
|
function buildWildcardRegExp(path) {
|
|
@@ -4025,7 +4082,7 @@ var RegExpRouter = class {
|
|
|
4025
4082
|
}
|
|
4026
4083
|
};
|
|
4027
4084
|
|
|
4028
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
4085
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
|
|
4029
4086
|
var PreparedRegExpRouter = class {
|
|
4030
4087
|
name = "PreparedRegExpRouter";
|
|
4031
4088
|
#matchers;
|
|
@@ -4097,7 +4154,7 @@ var PreparedRegExpRouter = class {
|
|
|
4097
4154
|
match = match;
|
|
4098
4155
|
};
|
|
4099
4156
|
|
|
4100
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
4157
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router/smart-router/router.js
|
|
4101
4158
|
var SmartRouter = class {
|
|
4102
4159
|
name = "SmartRouter";
|
|
4103
4160
|
#routers = [];
|
|
@@ -4152,7 +4209,7 @@ var SmartRouter = class {
|
|
|
4152
4209
|
}
|
|
4153
4210
|
};
|
|
4154
4211
|
|
|
4155
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
4212
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router/trie-router/node.js
|
|
4156
4213
|
var emptyParams = /* @__PURE__ */ Object.create(null);
|
|
4157
4214
|
var hasChildren = (children) => {
|
|
4158
4215
|
for (const _ in children) {
|
|
@@ -4321,7 +4378,7 @@ var Node2 = class _Node2 {
|
|
|
4321
4378
|
}
|
|
4322
4379
|
};
|
|
4323
4380
|
|
|
4324
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
4381
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/router/trie-router/router.js
|
|
4325
4382
|
var TrieRouter = class {
|
|
4326
4383
|
name = "TrieRouter";
|
|
4327
4384
|
#node;
|
|
@@ -4343,7 +4400,7 @@ var TrieRouter = class {
|
|
|
4343
4400
|
}
|
|
4344
4401
|
};
|
|
4345
4402
|
|
|
4346
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
4403
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/hono.js
|
|
4347
4404
|
var Hono2 = class extends Hono {
|
|
4348
4405
|
constructor(options = {}) {
|
|
4349
4406
|
super(options);
|
|
@@ -4458,7 +4515,7 @@ function requestLoggerMiddleware(logger) {
|
|
|
4458
4515
|
};
|
|
4459
4516
|
}
|
|
4460
4517
|
|
|
4461
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
4518
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/utils/stream.js
|
|
4462
4519
|
var StreamingApi = class {
|
|
4463
4520
|
writer;
|
|
4464
4521
|
encoder;
|
|
@@ -4524,7 +4581,7 @@ var StreamingApi = class {
|
|
|
4524
4581
|
}
|
|
4525
4582
|
};
|
|
4526
4583
|
|
|
4527
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
4584
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/helper/streaming/utils.js
|
|
4528
4585
|
var isOldBunVersion = () => {
|
|
4529
4586
|
const version = typeof Bun !== "undefined" ? Bun.version : undefined;
|
|
4530
4587
|
if (version === undefined) {
|
|
@@ -4535,7 +4592,7 @@ var isOldBunVersion = () => {
|
|
|
4535
4592
|
return result;
|
|
4536
4593
|
};
|
|
4537
4594
|
|
|
4538
|
-
// ../../node_modules/.bun/hono@4.12.
|
|
4595
|
+
// ../../node_modules/.bun/hono@4.12.5/node_modules/hono/dist/helper/streaming/stream.js
|
|
4539
4596
|
var contextStash = /* @__PURE__ */ new WeakMap;
|
|
4540
4597
|
var stream = (c, cb, onError) => {
|
|
4541
4598
|
const { readable, writable } = new TransformStream;
|
|
@@ -4630,6 +4687,31 @@ function resolveAgent(name, agents, defaultAgent) {
|
|
|
4630
4687
|
return { agent };
|
|
4631
4688
|
return { error: name ? `Agent "${name}" not found` : "No default agent configured" };
|
|
4632
4689
|
}
|
|
4690
|
+
var MAX_BODY_SIZE = 1048576;
|
|
4691
|
+
function parseRequestBody(c, rawText) {
|
|
4692
|
+
if (rawText.length > MAX_BODY_SIZE) {
|
|
4693
|
+
return { ok: false, response: c.json({ error: "Request body too large (max 1MB)" }, 413) };
|
|
4694
|
+
}
|
|
4695
|
+
const parsed = parseJsonBody(rawText);
|
|
4696
|
+
if (!parsed.ok) {
|
|
4697
|
+
return { ok: false, response: c.json({ error: "Invalid JSON in request body" }, 400) };
|
|
4698
|
+
}
|
|
4699
|
+
return { ok: true, data: parsed.data };
|
|
4700
|
+
}
|
|
4701
|
+
function buildChatResponse(result, model) {
|
|
4702
|
+
const content = typeof result.message.content === "string" ? result.message.content : "";
|
|
4703
|
+
return {
|
|
4704
|
+
message: content,
|
|
4705
|
+
usage: {
|
|
4706
|
+
inputTokens: result.usage.totalInputTokens,
|
|
4707
|
+
outputTokens: result.usage.totalOutputTokens,
|
|
4708
|
+
totalTokens: result.usage.totalTokens,
|
|
4709
|
+
cost: result.usage.totalCost
|
|
4710
|
+
},
|
|
4711
|
+
model: model ?? "default",
|
|
4712
|
+
traceId: result.traceId
|
|
4713
|
+
};
|
|
4714
|
+
}
|
|
4633
4715
|
function createRoutes(deps) {
|
|
4634
4716
|
const app = new Hono2;
|
|
4635
4717
|
let totalRequests = 0;
|
|
@@ -4665,15 +4747,10 @@ function createRoutes(deps) {
|
|
|
4665
4747
|
});
|
|
4666
4748
|
app.post("/chat", async (c) => {
|
|
4667
4749
|
totalRequests++;
|
|
4668
|
-
const MAX_BODY_SIZE = 1048576;
|
|
4669
4750
|
const rawText = await c.req.text();
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
const parsed = parseJsonBody(rawText);
|
|
4674
|
-
if (!parsed.ok) {
|
|
4675
|
-
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
4676
|
-
}
|
|
4751
|
+
const parsed = parseRequestBody(c, rawText);
|
|
4752
|
+
if (!parsed.ok)
|
|
4753
|
+
return parsed.response;
|
|
4677
4754
|
const body = parsed.data;
|
|
4678
4755
|
if (!body.message) {
|
|
4679
4756
|
return c.json({ error: "message is required" }, 400);
|
|
@@ -4697,37 +4774,20 @@ function createRoutes(deps) {
|
|
|
4697
4774
|
return elsiumErrorResponse(c, err2, "Agent execution failed");
|
|
4698
4775
|
}
|
|
4699
4776
|
deps.tracer?.trackLLMCall({
|
|
4700
|
-
model: "unknown",
|
|
4777
|
+
model: resolved.agent.config.model ?? "unknown",
|
|
4701
4778
|
inputTokens: result.usage.totalInputTokens,
|
|
4702
4779
|
outputTokens: result.usage.totalOutputTokens,
|
|
4703
4780
|
cost: result.usage.totalCost,
|
|
4704
4781
|
latencyMs: 0
|
|
4705
4782
|
});
|
|
4706
|
-
|
|
4707
|
-
const response = {
|
|
4708
|
-
message: content,
|
|
4709
|
-
usage: {
|
|
4710
|
-
inputTokens: result.usage.totalInputTokens,
|
|
4711
|
-
outputTokens: result.usage.totalOutputTokens,
|
|
4712
|
-
totalTokens: result.usage.totalTokens,
|
|
4713
|
-
cost: result.usage.totalCost
|
|
4714
|
-
},
|
|
4715
|
-
model: resolved.agent.config.model ?? "default",
|
|
4716
|
-
traceId: result.traceId
|
|
4717
|
-
};
|
|
4718
|
-
return c.json(response);
|
|
4783
|
+
return c.json(buildChatResponse(result, resolved.agent.config.model));
|
|
4719
4784
|
});
|
|
4720
4785
|
app.post("/complete", async (c) => {
|
|
4721
4786
|
totalRequests++;
|
|
4722
|
-
const MAX_BODY_SIZE = 1048576;
|
|
4723
4787
|
const rawText = await c.req.text();
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
const parsed = parseJsonBody(rawText);
|
|
4728
|
-
if (!parsed.ok) {
|
|
4729
|
-
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
4730
|
-
}
|
|
4788
|
+
const parsed = parseRequestBody(c, rawText);
|
|
4789
|
+
if (!parsed.ok)
|
|
4790
|
+
return parsed.response;
|
|
4731
4791
|
const body = parsed.data;
|
|
4732
4792
|
if (!body.messages?.length) {
|
|
4733
4793
|
return c.json({ error: "messages array is required" }, 400);
|
|
@@ -4974,6 +5034,7 @@ function createRBAC(config) {
|
|
|
4974
5034
|
}
|
|
4975
5035
|
// src/tenant.ts
|
|
4976
5036
|
var log12 = createLogger();
|
|
5037
|
+
var tenantUsage = new Map;
|
|
4977
5038
|
function tenantMiddleware(config) {
|
|
4978
5039
|
const { extractTenant, onUnknownTenant = "reject", defaultTenant } = config;
|
|
4979
5040
|
return async (c, next) => {
|
|
@@ -5019,9 +5080,59 @@ function tenantRateLimitMiddleware() {
|
|
|
5019
5080
|
await next();
|
|
5020
5081
|
};
|
|
5021
5082
|
}
|
|
5083
|
+
function getOrCreateUsage(tenantId) {
|
|
5084
|
+
let usage = tenantUsage.get(tenantId);
|
|
5085
|
+
if (!usage) {
|
|
5086
|
+
const now = Date.now();
|
|
5087
|
+
usage = {
|
|
5088
|
+
minute: { tokens: 0, cost: 0, windowStart: now },
|
|
5089
|
+
day: { tokens: 0, cost: 0, windowStart: now }
|
|
5090
|
+
};
|
|
5091
|
+
tenantUsage.set(tenantId, usage);
|
|
5092
|
+
}
|
|
5093
|
+
return usage;
|
|
5094
|
+
}
|
|
5095
|
+
function resetWindowIfExpired(window, durationMs) {
|
|
5096
|
+
const now = Date.now();
|
|
5097
|
+
if (now - window.windowStart > durationMs) {
|
|
5098
|
+
window.tokens = 0;
|
|
5099
|
+
window.cost = 0;
|
|
5100
|
+
window.windowStart = now;
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
function tenantBudgetMiddleware() {
|
|
5104
|
+
return async (c, next) => {
|
|
5105
|
+
const tenant = c.get("tenant");
|
|
5106
|
+
if (!tenant?.limits) {
|
|
5107
|
+
await next();
|
|
5108
|
+
return;
|
|
5109
|
+
}
|
|
5110
|
+
const usage = getOrCreateUsage(tenant.tenantId);
|
|
5111
|
+
resetWindowIfExpired(usage.minute, 60000);
|
|
5112
|
+
resetWindowIfExpired(usage.day, 86400000);
|
|
5113
|
+
if (tenant.limits.maxTokensPerMinute && usage.minute.tokens >= tenant.limits.maxTokensPerMinute) {
|
|
5114
|
+
return c.json({ error: "Token rate limit exceeded", retryAfterMs: 60000 }, 429);
|
|
5115
|
+
}
|
|
5116
|
+
if (tenant.limits.maxCostPerDay && usage.day.cost >= tenant.limits.maxCostPerDay) {
|
|
5117
|
+
return c.json({ error: "Daily cost limit exceeded" }, 429);
|
|
5118
|
+
}
|
|
5119
|
+
await next();
|
|
5120
|
+
const tokenCount = Number(c.res.headers.get("x-token-count")) || 0;
|
|
5121
|
+
const cost = Number(c.res.headers.get("x-cost")) || 0;
|
|
5122
|
+
if (tokenCount > 0) {
|
|
5123
|
+
usage.minute.tokens += tokenCount;
|
|
5124
|
+
usage.day.tokens += tokenCount;
|
|
5125
|
+
}
|
|
5126
|
+
if (cost > 0) {
|
|
5127
|
+
usage.minute.cost += cost;
|
|
5128
|
+
usage.day.cost += cost;
|
|
5129
|
+
}
|
|
5130
|
+
};
|
|
5131
|
+
}
|
|
5022
5132
|
export {
|
|
5023
5133
|
tenantRateLimitMiddleware,
|
|
5024
5134
|
tenantMiddleware,
|
|
5135
|
+
tenantBudgetMiddleware,
|
|
5025
5136
|
streamResponse,
|
|
5026
5137
|
sseHeaders,
|
|
5027
5138
|
requestLoggerMiddleware,
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAE9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAEhD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAE9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAEhD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAqE3B,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC1B,YAAY,CAAC,EAAE,KAAK,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAkKnD"}
|
package/dist/tenant.d.ts
CHANGED
|
@@ -12,4 +12,7 @@ export declare function tenantRateLimitMiddleware(): (c: Context, next: Next) =>
|
|
|
12
12
|
error: string;
|
|
13
13
|
retryAfterMs: number;
|
|
14
14
|
}, 429, "json">) | undefined>;
|
|
15
|
+
export declare function tenantBudgetMiddleware(): (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
|
|
16
|
+
error: string;
|
|
17
|
+
}, 429, "json">) | undefined>;
|
|
15
18
|
//# sourceMappingURL=tenant.d.ts.map
|
package/dist/tenant.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAczC,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,aAAa,GAAG,IAAI,CAAA;IACnD,eAAe,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACtC,aAAa,CAAC,EAAE,aAAa,CAAA;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,IAGhD,GAAG,OAAO,EAAE,MAAM,IAAI;;8BAiBpC;AAOD,wBAAgB,yBAAyB,KAG1B,GAAG,OAAO,EAAE,MAAM,IAAI;;;8BAiCpC;AA0BD,wBAAgB,sBAAsB,KACvB,GAAG,OAAO,EAAE,MAAM,IAAI;;8BAyCpC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elsium-ai/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "App bootstrap, HTTP server, and API routes for ElsiumAI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Utrera <ebutrera9103@gmail.com>",
|
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
"dev": "bun --watch src/index.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@elsium-ai/core": "^0.
|
|
30
|
-
"@elsium-ai/gateway": "^0.
|
|
31
|
-
"@elsium-ai/agents": "^0.
|
|
32
|
-
"@elsium-ai/tools": "^0.
|
|
33
|
-
"@elsium-ai/observe": "^0.
|
|
34
|
-
"@elsium-ai/rag": "^0.
|
|
35
|
-
"@elsium-ai/workflows": "^0.
|
|
36
|
-
"@hono/node-server": "^1.
|
|
37
|
-
"hono": "^4.
|
|
29
|
+
"@elsium-ai/core": "^0.4.0",
|
|
30
|
+
"@elsium-ai/gateway": "^0.4.0",
|
|
31
|
+
"@elsium-ai/agents": "^0.4.0",
|
|
32
|
+
"@elsium-ai/tools": "^0.4.0",
|
|
33
|
+
"@elsium-ai/observe": "^0.4.0",
|
|
34
|
+
"@elsium-ai/rag": "^0.4.0",
|
|
35
|
+
"@elsium-ai/workflows": "^0.4.0",
|
|
36
|
+
"@hono/node-server": "^1.19.10",
|
|
37
|
+
"hono": "^4.12.4",
|
|
38
38
|
"zod": "^3.24.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|