@elsium-ai/gateway 0.6.0 → 0.7.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 CHANGED
@@ -919,6 +919,14 @@ interface ProviderEntry {
919
919
  ### `ProviderMeshConfig`
920
920
 
921
921
  ```ts
922
+ interface MeshAuditLogger {
923
+ log(
924
+ type: string,
925
+ data: Record<string, unknown>,
926
+ options?: { actor?: string; traceId?: string },
927
+ ): void
928
+ }
929
+
922
930
  interface ProviderMeshConfig {
923
931
  providers: ProviderEntry[]
924
932
  strategy: RoutingStrategy
@@ -928,6 +936,7 @@ interface ProviderMeshConfig {
928
936
  complexityThreshold?: number
929
937
  }
930
938
  circuitBreaker?: CircuitBreakerConfig | boolean
939
+ audit?: MeshAuditLogger
931
940
  }
932
941
  ```
933
942
 
@@ -937,6 +946,7 @@ interface ProviderMeshConfig {
937
946
  | `strategy` | `RoutingStrategy` | Routing strategy to use. |
938
947
  | `costOptimizer` | `CostOptimizerConfig` | Configuration for the `cost-optimized` strategy. |
939
948
  | `circuitBreaker` | `CircuitBreakerConfig \| boolean` | Enable circuit breakers per provider. Pass `true` for defaults or an object to configure thresholds. |
949
+ | `audit` | `MeshAuditLogger` | Optional audit logger. When provided, the mesh logs `provider_failover` and `circuit_breaker_state_change` events. Compatible with `AuditTrail` from `@elsium-ai/observe`. |
940
950
 
941
951
  ### `ProviderMesh`
942
952
 
@@ -1042,6 +1052,41 @@ const response = await mesh.complete({
1042
1052
  })
1043
1053
  ```
1044
1054
 
1055
+ #### Failover Audit Trail
1056
+
1057
+ Pass an audit trail to the mesh to get tamper-evident records of every provider failover and circuit breaker state change:
1058
+
1059
+ ```ts
1060
+ import { createProviderMesh } from '@elsium-ai/gateway'
1061
+ import { createAuditTrail } from '@elsium-ai/observe'
1062
+
1063
+ const audit = createAuditTrail({
1064
+ hashChain: true,
1065
+ batch: { size: 500, intervalMs: 100 },
1066
+ })
1067
+
1068
+ const mesh = createProviderMesh({
1069
+ providers: [
1070
+ { name: 'anthropic', config: { apiKey: process.env.ANTHROPIC_API_KEY! }, model: 'claude-sonnet-4-6' },
1071
+ { name: 'openai', config: { apiKey: process.env.OPENAI_API_KEY! }, model: 'gpt-4o' },
1072
+ ],
1073
+ strategy: 'fallback',
1074
+ circuitBreaker: { failureThreshold: 5, resetTimeoutMs: 30_000 },
1075
+ audit,
1076
+ })
1077
+
1078
+ // If Anthropic fails and OpenAI succeeds, the audit trail records:
1079
+ // { type: 'provider_failover', data: { fromProvider: 'anthropic', toProvider: 'openai', strategy: 'fallback', reason: '...' } }
1080
+ //
1081
+ // If the circuit breaker trips:
1082
+ // { type: 'circuit_breaker_state_change', data: { provider: 'anthropic', fromState: 'closed', toState: 'open' } }
1083
+
1084
+ const failovers = await audit.query({ type: 'provider_failover' })
1085
+ const breakerEvents = await audit.query({ type: ['circuit_breaker_state_change'] })
1086
+ ```
1087
+
1088
+ The `MeshAuditLogger` interface is intentionally minimal — any object with a `log(type, data, options?)` method works, so you can use `AuditTrail` from `@elsium-ai/observe` or your own logger.
1089
+
1045
1090
  ---
1046
1091
 
1047
1092
  ## Part of ElsiumAI
package/dist/index.d.ts CHANGED
@@ -19,5 +19,5 @@ export { calculateCost, registerPricing, estimateCost } from './pricing';
19
19
  export { createBatch } from './batch';
20
20
  export type { BatchConfig, BatchResult, BatchResultItem } from './batch';
21
21
  export { createProviderMesh } from './router';
22
- export type { ProviderMeshConfig, ProviderEntry, RoutingStrategy, ProviderMesh } from './router';
22
+ export type { ProviderMeshConfig, ProviderEntry, RoutingStrategy, ProviderMesh, MeshAuditLogger, } from './router';
23
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AAC5D,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGvD,YAAY,EACX,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,SAAS,GACT,MAAM,YAAY,CAAA;AACnB,OAAO,EACN,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,wBAAwB,EACxB,mBAAmB,GACnB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAGzD,OAAO,EACN,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,GACd,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAG7C,OAAO,EACN,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,eAAe,GACf,MAAM,YAAY,CAAA;AACnB,YAAY,EACX,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,GACpB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/D,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAG1D,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAC9D,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAG9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EACX,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,GACf,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAGxE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAGxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AAC5D,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGvD,YAAY,EACX,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,SAAS,GACT,MAAM,YAAY,CAAA;AACnB,OAAO,EACN,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,wBAAwB,EACxB,mBAAmB,GACnB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAGzD,OAAO,EACN,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,GACd,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAG7C,OAAO,EACN,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,eAAe,GACf,MAAM,YAAY,CAAA;AACnB,YAAY,EACX,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,GACpB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/D,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAG1D,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAC9D,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAG9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EACX,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,GACf,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAGxE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAGxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAC7C,YAAY,EACX,kBAAkB,EAClB,aAAa,EACb,eAAe,EACf,YAAY,EACZ,eAAe,GACf,MAAM,UAAU,CAAA"}
package/dist/index.js CHANGED
@@ -2989,6 +2989,7 @@ function createProviderMesh(config) {
2989
2989
  const sortedProviders = [...config.providers];
2990
2990
  const gateways = new Map;
2991
2991
  const circuitBreakers = new Map;
2992
+ const audit = config.audit;
2992
2993
  for (const entry of sortedProviders) {
2993
2994
  const gw = gateway({
2994
2995
  provider: entry.name,
@@ -2999,7 +3000,19 @@ function createProviderMesh(config) {
2999
3000
  gateways.set(entry.name, gw);
3000
3001
  if (config.circuitBreaker) {
3001
3002
  const cbConfig = typeof config.circuitBreaker === "boolean" ? {} : config.circuitBreaker;
3002
- circuitBreakers.set(entry.name, createCircuitBreaker(cbConfig));
3003
+ const providerName = entry.name;
3004
+ const wrappedConfig = {
3005
+ ...cbConfig,
3006
+ onStateChange(from, to) {
3007
+ cbConfig.onStateChange?.(from, to);
3008
+ audit?.log("circuit_breaker_state_change", {
3009
+ provider: providerName,
3010
+ fromState: from,
3011
+ toState: to
3012
+ });
3013
+ }
3014
+ };
3015
+ circuitBreakers.set(entry.name, createCircuitBreaker(wrappedConfig));
3003
3016
  }
3004
3017
  }
3005
3018
  function callWithCircuitBreaker(providerName, fn) {
@@ -3021,23 +3034,41 @@ function createProviderMesh(config) {
3021
3034
  }
3022
3035
  return gw;
3023
3036
  }
3024
- async function fallbackComplete(request) {
3037
+ function attemptProvider(entry, request) {
3038
+ const gw = getGateway(entry.name);
3039
+ return callWithCircuitBreaker(entry.name, () => gw.complete({ ...request, model: request.model ?? entry.model }));
3040
+ }
3041
+ function logFailover(fromProvider, toProvider, reason) {
3042
+ audit?.log("provider_failover", {
3043
+ fromProvider,
3044
+ toProvider,
3045
+ strategy: config.strategy,
3046
+ reason
3047
+ });
3048
+ }
3049
+ function toError2(err2) {
3050
+ return err2 instanceof Error ? err2 : new Error(String(err2));
3051
+ }
3052
+ async function tryProvidersWithAudit(providers, request, errorMessage) {
3025
3053
  let lastError = null;
3026
- for (const entry of sortedProviders) {
3054
+ let failedProvider = null;
3055
+ for (const entry of providers) {
3027
3056
  if (!isProviderAvailable(entry.name))
3028
3057
  continue;
3029
3058
  try {
3030
- const gw = getGateway(entry.name);
3031
- return await callWithCircuitBreaker(entry.name, () => gw.complete({ ...request, model: request.model ?? entry.model }));
3059
+ const response = await attemptProvider(entry, request);
3060
+ if (failedProvider)
3061
+ logFailover(failedProvider, entry.name, lastError?.message);
3062
+ return response;
3032
3063
  } catch (err2) {
3033
- lastError = err2 instanceof Error ? err2 : new Error(String(err2));
3064
+ failedProvider = entry.name;
3065
+ lastError = toError2(err2);
3034
3066
  }
3035
3067
  }
3036
- throw lastError ?? new ElsiumError({
3037
- code: "PROVIDER_ERROR",
3038
- message: "All providers failed",
3039
- retryable: false
3040
- });
3068
+ throw lastError ?? new ElsiumError({ code: "PROVIDER_ERROR", message: errorMessage, retryable: false });
3069
+ }
3070
+ async function fallbackComplete(request) {
3071
+ return tryProvidersWithAudit(sortedProviders, request, "All providers failed");
3041
3072
  }
3042
3073
  async function costOptimizedComplete(request) {
3043
3074
  const optimizer = config.costOptimizer;
@@ -3050,7 +3081,13 @@ function createProviderMesh(config) {
3050
3081
  const gw = getGateway(target.provider);
3051
3082
  try {
3052
3083
  return await gw.complete({ ...request, model: target.model });
3053
- } catch {
3084
+ } catch (err2) {
3085
+ audit?.log("provider_failover", {
3086
+ fromProvider: target.provider,
3087
+ toProvider: "fallback-chain",
3088
+ strategy: "cost-optimized",
3089
+ reason: err2 instanceof Error ? err2.message : String(err2)
3090
+ });
3054
3091
  return fallbackComplete(request);
3055
3092
  }
3056
3093
  }
@@ -3094,31 +3131,13 @@ function createProviderMesh(config) {
3094
3131
  return capabilities.every((c) => providerCaps.includes(c));
3095
3132
  });
3096
3133
  }
3097
- async function tryProviders(providers, request) {
3098
- let lastError = null;
3099
- for (const entry of providers) {
3100
- if (!isProviderAvailable(entry.name))
3101
- continue;
3102
- try {
3103
- const gw = getGateway(entry.name);
3104
- return await callWithCircuitBreaker(entry.name, () => gw.complete({ ...request, model: request.model ?? entry.model }));
3105
- } catch (err2) {
3106
- lastError = err2 instanceof Error ? err2 : new Error(String(err2));
3107
- }
3108
- }
3109
- throw lastError ?? new ElsiumError({
3110
- code: "PROVIDER_ERROR",
3111
- message: "No capable provider succeeded",
3112
- retryable: false
3113
- });
3114
- }
3115
3134
  async function capabilityAwareComplete(request) {
3116
3135
  const capabilities = detectRequiredCapabilities(request);
3117
3136
  const capable = filterCapableProviders(capabilities);
3118
3137
  if (capable.length === 0) {
3119
3138
  return fallbackComplete(request);
3120
3139
  }
3121
- return tryProviders(capable, request);
3140
+ return tryProvidersWithAudit(capable, request, "No capable provider succeeded");
3122
3141
  }
3123
3142
  function defaultCapabilities(provider) {
3124
3143
  const meta = getProviderMetadata(provider);
package/dist/router.d.ts CHANGED
@@ -21,11 +21,18 @@ export interface CostOptimizerConfig {
21
21
  };
22
22
  complexityThreshold?: number;
23
23
  }
24
+ export interface MeshAuditLogger {
25
+ log(type: string, data: Record<string, unknown>, options?: {
26
+ actor?: string;
27
+ traceId?: string;
28
+ }): void;
29
+ }
24
30
  export interface ProviderMeshConfig {
25
31
  providers: ProviderEntry[];
26
32
  strategy: RoutingStrategy;
27
33
  costOptimizer?: CostOptimizerConfig;
28
34
  circuitBreaker?: CircuitBreakerConfig | boolean;
35
+ audit?: MeshAuditLogger;
29
36
  }
30
37
  export interface ProviderMesh {
31
38
  complete(request: CompletionRequest): Promise<LLMResponse>;
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,EAEN,KAAK,oBAAoB,EAEzB,YAAY,EAEZ,MAAM,iBAAiB,CAAA;AAKxB,MAAM,MAAM,eAAe,GACxB,UAAU,GACV,gBAAgB,GAChB,mBAAmB,GACnB,kBAAkB,CAAA;AAErB,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,WAAW,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAChD,YAAY,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACjD,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,aAAa,EAAE,CAAA;IAC1B,QAAQ,EAAE,eAAe,CAAA;IACzB,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,cAAc,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAA;CAC/C;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC1D,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,YAAY,CAAA;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAA;CAClC;AAoDD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAgQ3E"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,EAEN,KAAK,oBAAoB,EAEzB,YAAY,EAEZ,MAAM,iBAAiB,CAAA;AAKxB,MAAM,MAAM,eAAe,GACxB,UAAU,GACV,gBAAgB,GAChB,mBAAmB,GACnB,kBAAkB,CAAA;AAErB,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,WAAW,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAChD,YAAY,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACjD,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,eAAe;IAC/B,GAAG,CACF,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5C,IAAI,CAAA;CACP;AAED,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,aAAa,EAAE,CAAA;IAC1B,QAAQ,EAAE,eAAe,CAAA;IACzB,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,cAAc,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAA;IAC/C,KAAK,CAAC,EAAE,eAAe,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC1D,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,YAAY,CAAA;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAA;CAClC;AAoDD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAgR3E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elsium-ai/gateway",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Multi-provider LLM gateway for ElsiumAI",
5
5
  "license": "MIT",
6
6
  "author": "Eric Utrera <ebutrera9103@gmail.com>",
@@ -26,7 +26,7 @@
26
26
  "dev": "bun --watch src/index.ts"
27
27
  },
28
28
  "dependencies": {
29
- "@elsium-ai/core": "^0.6.0",
29
+ "@elsium-ai/core": "^0.7.0",
30
30
  "zod": "^3.24.0"
31
31
  },
32
32
  "devDependencies": {