@elsium-ai/app 0.3.0 → 0.4.1

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 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
@@ -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;AACtE,YAAY,EAAE,sBAAsB,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 { system, messages } = formatMessages(req.messages);
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 controller = new AbortController;
986
- const timer = setTimeout(() => controller.abort(), timeout);
987
- try {
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 { system, messages } = formatMessages(req.messages);
1005
- const model = req.model ?? "claude-sonnet-4-6";
1006
- const body = {
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
- const controller = new AbortController;
1022
- const timer = setTimeout(() => controller.abort(), timeout);
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
- } finally {
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 msg.content) {
1194
- if (p.type === "text") {
1195
- parts.push({ text: p.text });
1196
- } else if (p.type === "image") {
1197
- const img = p;
1198
- if (img.source.type === "base64") {
1199
- parts.push({ inlineData: { mimeType: img.source.mediaType, data: img.source.data } });
1200
- } else {
1201
- parts.push({ fileData: { mimeType: "image/jpeg", fileUri: img.source.url } });
1202
- }
1203
- } else if (p.type === "audio" || p.type === "document") {
1204
- const media = p;
1205
- if (media.source.type === "base64") {
1206
- parts.push({
1207
- inlineData: { mimeType: media.source.mediaType, data: media.source.data }
1208
- });
1209
- } else {
1210
- const urlSource = media.source;
1211
- parts.push({
1212
- fileData: { mimeType: "application/octet-stream", fileUri: urlSource.url }
1213
- });
1214
- }
1215
- }
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
- if (part.type === "text") {
1588
- parts.push({ type: "text", text: part.text });
1589
- } else if (part.type === "image") {
1590
- if (part.source.type === "base64") {
1591
- const url = `data:${part.source.mediaType};base64,${part.source.data}`;
1592
- parts.push({ type: "image_url", image_url: { url } });
1593
- } else {
1594
- parts.push({ type: "image_url", image_url: { url: part.source.url } });
1595
- }
1596
- } else if (part.type === "audio") {
1597
- if (part.source.type === "base64") {
1598
- const format = part.source.mediaType.split("/")[1] ?? "wav";
1599
- parts.push({
1600
- type: "input_audio",
1601
- input_audio: { data: part.source.data, format }
1602
- });
1603
- } else {
1604
- parts.push({ type: "text", text: "[audio: url source requires file upload]" });
1605
- }
1606
- } else if (part.type === "document") {
1607
- if (part.source.type === "base64") {
1608
- parts.push({
1609
- type: "text",
1610
- text: `[document: ${part.source.mediaType} content attached as base64]`
1611
- });
1612
- } else {
1613
- parts.push({ type: "text", text: `[document: ${part.source.url}]` });
1614
- }
1615
- }
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 messages = formatMessages(req.messages);
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 messages = formatMessages(req.messages);
1744
- const model = req.model ?? "gpt-4o";
1745
- if (req.system) {
1746
- messages.unshift({ role: "system", content: req.system });
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: ${Object.keys(PROVIDER_FACTORIES).join(", ")}`,
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 ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
2073
+ const ctx2 = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
2010
2074
  return createStream(async (emit) => {
2011
- await composedMiddleware(ctx, async (c) => {
2075
+ await composedMiddleware(ctx2, async (c) => {
2012
2076
  const result = await accumulateStreamEvents(provider.stream(c.request), emit);
2013
- const latencyMs = Math.round(performance.now() - ctx.startTime);
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: ctx.traceId
2087
+ traceId: ctx2.traceId
2024
2088
  };
2025
2089
  });
2026
2090
  });
2027
2091
  }
2028
- return provider.stream(req);
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
- let parsed;
2046
- if (response.stopReason === "tool_use" && response.message.toolCalls?.length) {
2047
- const structuredCall = response.message.toolCalls.find((tc) => tc.name === "_structured_output");
2048
- if (structuredCall) {
2049
- parsed = structuredCall.arguments;
2050
- }
2051
- }
2052
- if (parsed === undefined) {
2053
- const text = typeof response.message.content === "string" ? response.message.content : "";
2054
- const jsonMatch = text.match(/\{[\s\S]*\}/);
2055
- if (!jsonMatch) {
2056
- throw ElsiumError.validation("LLM response did not contain valid JSON", {
2057
- response: text
2058
- });
2059
- }
2060
- parsed = JSON.parse(jsonMatch[0]);
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.9/node_modules/@hono/node-server/dist/index.mjs
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.3/node_modules/hono/dist/compose.js
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.3/node_modules/hono/dist/request/constants.js
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.3/node_modules/hono/dist/utils/body.js
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.3/node_modules/hono/dist/utils/url.js
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.3/node_modules/hono/dist/request.js
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.3/node_modules/hono/dist/utils/html.js
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.3/node_modules/hono/dist/context.js
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.3/node_modules/hono/dist/router.js
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.3/node_modules/hono/dist/utils/constants.js
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.3/node_modules/hono/dist/hono-base.js
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.3/node_modules/hono/dist/router/reg-exp-router/matcher.js
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.3/node_modules/hono/dist/router/reg-exp-router/node.js
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.3/node_modules/hono/dist/router/reg-exp-router/trie.js
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.3/node_modules/hono/dist/router/reg-exp-router/router.js
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.3/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
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.3/node_modules/hono/dist/router/smart-router/router.js
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.3/node_modules/hono/dist/router/trie-router/node.js
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.3/node_modules/hono/dist/router/trie-router/router.js
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.3/node_modules/hono/dist/hono.js
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.3/node_modules/hono/dist/utils/stream.js
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.3/node_modules/hono/dist/helper/streaming/utils.js
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.3/node_modules/hono/dist/helper/streaming/stream.js
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
- if (rawText.length > MAX_BODY_SIZE) {
4671
- return c.json({ error: "Request body too large (max 1MB)" }, 413);
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
- const content = typeof result.message.content === "string" ? result.message.content : "";
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
- if (rawText.length > MAX_BODY_SIZE) {
4725
- return c.json({ error: "Request body too large (max 1MB)" }, 413);
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,
@@ -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;AAmC3B,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,CA6LnD"}
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
@@ -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;AAIzC,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"}
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.0",
3
+ "version": "0.4.1",
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.3.0",
30
- "@elsium-ai/gateway": "^0.3.0",
31
- "@elsium-ai/agents": "^0.3.0",
32
- "@elsium-ai/tools": "^0.3.0",
33
- "@elsium-ai/observe": "^0.3.0",
34
- "@elsium-ai/rag": "^0.3.0",
35
- "@elsium-ai/workflows": "^0.3.0",
36
- "@hono/node-server": "^1.13.0",
37
- "hono": "^4.7.0",
29
+ "@elsium-ai/core": "^0.4.1",
30
+ "@elsium-ai/gateway": "^0.4.1",
31
+ "@elsium-ai/agents": "^0.4.1",
32
+ "@elsium-ai/tools": "^0.4.1",
33
+ "@elsium-ai/observe": "^0.4.1",
34
+ "@elsium-ai/rag": "^0.4.1",
35
+ "@elsium-ai/workflows": "^0.4.1",
36
+ "@hono/node-server": "^1.19.10",
37
+ "hono": "^4.12.4",
38
38
  "zod": "^3.24.0"
39
39
  },
40
40
  "devDependencies": {