@elsium-ai/gateway 0.6.0 → 0.8.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 +45 -0
- package/dist/gateway.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +239 -50
- package/dist/providers/openai-compatible.d.ts +10 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -0
- package/dist/router.d.ts +7 -0
- package/dist/router.d.ts.map +1 -1
- package/package.json +2 -2
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/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,iBAAiB,EACjB,WAAW,EACX,UAAU,EAEV,cAAc,EAEd,gBAAgB,EAChB,QAAQ,EACR,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAEN,KAAK,YAAY,EAIjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAI5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,iBAAiB,EACjB,WAAW,EACX,UAAU,EAEV,cAAc,EAEd,gBAAgB,EAChB,QAAQ,EACR,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAEN,KAAK,YAAY,EAIjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAI5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAY7C,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,EAAE,CAAA;IACzB,gBAAgB,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACrC,IAAI,CAAC,EAAE,OAAO,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC1D,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,YAAY,CAAA;IAChD,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG;QAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC;QAC3E,IAAI,EAAE,CAAC,CAAA;QACP,QAAQ,EAAE,WAAW,CAAA;KACrB,CAAC,CAAA;IACF,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAA;IAC9B,QAAQ,IAAI,QAAQ,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAA;CACvC;AA4BD,wBAAgB,uBAAuB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,WAAW,GAC9C,IAAI,CAGN;AAwJD,wBAAgB,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAkItD"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { registerProvider, getProviderFactory, listProviders, registerProviderMe
|
|
|
5
5
|
export { createAnthropicProvider } from './providers/anthropic';
|
|
6
6
|
export { createOpenAIProvider } from './providers/openai';
|
|
7
7
|
export { createGoogleProvider } from './providers/google';
|
|
8
|
+
export { createOpenAICompatibleProvider } from './providers/openai-compatible';
|
|
9
|
+
export type { OpenAICompatibleConfig } from './providers/openai-compatible';
|
|
8
10
|
export { composeMiddleware, composeStreamMiddleware, loggingMiddleware, costTrackingMiddleware, xrayMiddleware, } from './middleware';
|
|
9
11
|
export type { XRayStore } from './middleware';
|
|
10
12
|
export { securityMiddleware, detectPromptInjection, detectJailbreak, redactSecrets, checkBlockedPatterns, classifyContent, } from './security';
|
|
@@ -19,5 +21,5 @@ export { calculateCost, registerPricing, estimateCost } from './pricing';
|
|
|
19
21
|
export { createBatch } from './batch';
|
|
20
22
|
export type { BatchConfig, BatchResult, BatchResultItem } from './batch';
|
|
21
23
|
export { createProviderMesh } from './router';
|
|
22
|
-
export type { ProviderMeshConfig, ProviderEntry, RoutingStrategy, ProviderMesh } from './router';
|
|
24
|
+
export type { ProviderMeshConfig, ProviderEntry, RoutingStrategy, ProviderMesh, MeshAuditLogger, } from './router';
|
|
23
25
|
//# 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,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;
|
|
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;AACzD,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAA;AAC9E,YAAY,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AAG3E,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
|
@@ -1991,11 +1991,44 @@ async function processOpenAISSEStream(body, emit) {
|
|
|
1991
1991
|
}
|
|
1992
1992
|
}
|
|
1993
1993
|
|
|
1994
|
+
// src/providers/openai-compatible.ts
|
|
1995
|
+
function createOpenAICompatibleProvider(config) {
|
|
1996
|
+
const providerName = config.name ?? "openai-compatible";
|
|
1997
|
+
const model = config.defaultModel ?? "default";
|
|
1998
|
+
const inner = createOpenAIProvider({
|
|
1999
|
+
apiKey: config.apiKey,
|
|
2000
|
+
baseUrl: config.baseUrl,
|
|
2001
|
+
timeout: config.timeout,
|
|
2002
|
+
maxRetries: config.maxRetries
|
|
2003
|
+
});
|
|
2004
|
+
const metadata = {
|
|
2005
|
+
baseUrl: `${config.baseUrl}/v1/chat/completions`,
|
|
2006
|
+
capabilities: config.capabilities ?? ["tools", "streaming", "system"],
|
|
2007
|
+
authStyle: "bearer"
|
|
2008
|
+
};
|
|
2009
|
+
return {
|
|
2010
|
+
name: providerName,
|
|
2011
|
+
defaultModel: model,
|
|
2012
|
+
metadata,
|
|
2013
|
+
async complete(request) {
|
|
2014
|
+
const response = await inner.complete(request);
|
|
2015
|
+
return { ...response, provider: providerName };
|
|
2016
|
+
},
|
|
2017
|
+
stream(request) {
|
|
2018
|
+
return inner.stream(request);
|
|
2019
|
+
},
|
|
2020
|
+
async listModels() {
|
|
2021
|
+
return inner.listModels();
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1994
2026
|
// src/gateway.ts
|
|
1995
2027
|
var PROVIDER_FACTORIES = {
|
|
1996
2028
|
anthropic: createAnthropicProvider,
|
|
1997
2029
|
openai: createOpenAIProvider,
|
|
1998
|
-
google: createGoogleProvider
|
|
2030
|
+
google: createGoogleProvider,
|
|
2031
|
+
"openai-compatible": (cfg) => createOpenAICompatibleProvider({ ...cfg, baseUrl: cfg.baseUrl ?? "" })
|
|
1999
2032
|
};
|
|
2000
2033
|
registerProviderMetadata("anthropic", {
|
|
2001
2034
|
baseUrl: "https://api.anthropic.com/v1/messages",
|
|
@@ -2989,6 +3022,7 @@ function createProviderMesh(config) {
|
|
|
2989
3022
|
const sortedProviders = [...config.providers];
|
|
2990
3023
|
const gateways = new Map;
|
|
2991
3024
|
const circuitBreakers = new Map;
|
|
3025
|
+
const audit = config.audit;
|
|
2992
3026
|
for (const entry of sortedProviders) {
|
|
2993
3027
|
const gw = gateway({
|
|
2994
3028
|
provider: entry.name,
|
|
@@ -2999,7 +3033,19 @@ function createProviderMesh(config) {
|
|
|
2999
3033
|
gateways.set(entry.name, gw);
|
|
3000
3034
|
if (config.circuitBreaker) {
|
|
3001
3035
|
const cbConfig = typeof config.circuitBreaker === "boolean" ? {} : config.circuitBreaker;
|
|
3002
|
-
|
|
3036
|
+
const providerName = entry.name;
|
|
3037
|
+
const wrappedConfig = {
|
|
3038
|
+
...cbConfig,
|
|
3039
|
+
onStateChange(from, to) {
|
|
3040
|
+
cbConfig.onStateChange?.(from, to);
|
|
3041
|
+
audit?.log("circuit_breaker_state_change", {
|
|
3042
|
+
provider: providerName,
|
|
3043
|
+
fromState: from,
|
|
3044
|
+
toState: to
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
circuitBreakers.set(entry.name, createCircuitBreaker(wrappedConfig));
|
|
3003
3049
|
}
|
|
3004
3050
|
}
|
|
3005
3051
|
function callWithCircuitBreaker(providerName, fn) {
|
|
@@ -3021,23 +3067,41 @@ function createProviderMesh(config) {
|
|
|
3021
3067
|
}
|
|
3022
3068
|
return gw;
|
|
3023
3069
|
}
|
|
3024
|
-
|
|
3070
|
+
function attemptProvider(entry, request) {
|
|
3071
|
+
const gw = getGateway(entry.name);
|
|
3072
|
+
return callWithCircuitBreaker(entry.name, () => gw.complete({ ...request, model: request.model ?? entry.model }));
|
|
3073
|
+
}
|
|
3074
|
+
function logFailover(fromProvider, toProvider, reason) {
|
|
3075
|
+
audit?.log("provider_failover", {
|
|
3076
|
+
fromProvider,
|
|
3077
|
+
toProvider,
|
|
3078
|
+
strategy: config.strategy,
|
|
3079
|
+
reason
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
function toError2(err2) {
|
|
3083
|
+
return err2 instanceof Error ? err2 : new Error(String(err2));
|
|
3084
|
+
}
|
|
3085
|
+
async function tryProvidersWithAudit(providers, request, errorMessage) {
|
|
3025
3086
|
let lastError = null;
|
|
3026
|
-
|
|
3087
|
+
let failedProvider = null;
|
|
3088
|
+
for (const entry of providers) {
|
|
3027
3089
|
if (!isProviderAvailable(entry.name))
|
|
3028
3090
|
continue;
|
|
3029
3091
|
try {
|
|
3030
|
-
const
|
|
3031
|
-
|
|
3092
|
+
const response = await attemptProvider(entry, request);
|
|
3093
|
+
if (failedProvider)
|
|
3094
|
+
logFailover(failedProvider, entry.name, lastError?.message);
|
|
3095
|
+
return response;
|
|
3032
3096
|
} catch (err2) {
|
|
3033
|
-
|
|
3097
|
+
failedProvider = entry.name;
|
|
3098
|
+
lastError = toError2(err2);
|
|
3034
3099
|
}
|
|
3035
3100
|
}
|
|
3036
|
-
throw lastError ?? new ElsiumError({
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
});
|
|
3101
|
+
throw lastError ?? new ElsiumError({ code: "PROVIDER_ERROR", message: errorMessage, retryable: false });
|
|
3102
|
+
}
|
|
3103
|
+
async function fallbackComplete(request) {
|
|
3104
|
+
return tryProvidersWithAudit(sortedProviders, request, "All providers failed");
|
|
3041
3105
|
}
|
|
3042
3106
|
async function costOptimizedComplete(request) {
|
|
3043
3107
|
const optimizer = config.costOptimizer;
|
|
@@ -3050,7 +3114,13 @@ function createProviderMesh(config) {
|
|
|
3050
3114
|
const gw = getGateway(target.provider);
|
|
3051
3115
|
try {
|
|
3052
3116
|
return await gw.complete({ ...request, model: target.model });
|
|
3053
|
-
} catch {
|
|
3117
|
+
} catch (err2) {
|
|
3118
|
+
audit?.log("provider_failover", {
|
|
3119
|
+
fromProvider: target.provider,
|
|
3120
|
+
toProvider: "fallback-chain",
|
|
3121
|
+
strategy: "cost-optimized",
|
|
3122
|
+
reason: err2 instanceof Error ? err2.message : String(err2)
|
|
3123
|
+
});
|
|
3054
3124
|
return fallbackComplete(request);
|
|
3055
3125
|
}
|
|
3056
3126
|
}
|
|
@@ -3094,31 +3164,13 @@ function createProviderMesh(config) {
|
|
|
3094
3164
|
return capabilities.every((c) => providerCaps.includes(c));
|
|
3095
3165
|
});
|
|
3096
3166
|
}
|
|
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
3167
|
async function capabilityAwareComplete(request) {
|
|
3116
3168
|
const capabilities = detectRequiredCapabilities(request);
|
|
3117
3169
|
const capable = filterCapableProviders(capabilities);
|
|
3118
3170
|
if (capable.length === 0) {
|
|
3119
3171
|
return fallbackComplete(request);
|
|
3120
3172
|
}
|
|
3121
|
-
return
|
|
3173
|
+
return tryProvidersWithAudit(capable, request, "No capable provider succeeded");
|
|
3122
3174
|
}
|
|
3123
3175
|
function defaultCapabilities(provider) {
|
|
3124
3176
|
const meta = getProviderMetadata(provider);
|
|
@@ -3135,6 +3187,149 @@ function createProviderMesh(config) {
|
|
|
3135
3187
|
return ["streaming"];
|
|
3136
3188
|
}
|
|
3137
3189
|
}
|
|
3190
|
+
function errorStream(message) {
|
|
3191
|
+
return new ElsiumStream(async function* () {
|
|
3192
|
+
yield {
|
|
3193
|
+
type: "error",
|
|
3194
|
+
error: new ElsiumError({
|
|
3195
|
+
code: "PROVIDER_ERROR",
|
|
3196
|
+
message,
|
|
3197
|
+
retryable: false
|
|
3198
|
+
})
|
|
3199
|
+
};
|
|
3200
|
+
}());
|
|
3201
|
+
}
|
|
3202
|
+
function logStreamFailover(provider, error) {
|
|
3203
|
+
audit?.log("provider_failover", {
|
|
3204
|
+
fromProvider: provider,
|
|
3205
|
+
toProvider: "next",
|
|
3206
|
+
strategy: config.strategy,
|
|
3207
|
+
reason: error?.message
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
async function tryStreamProvider(entry, request, emit) {
|
|
3211
|
+
const gw = getGateway(entry.name);
|
|
3212
|
+
const providerStream = await callWithCircuitBreaker(entry.name, async () => gw.stream({ ...request, model: request.model ?? entry.model }));
|
|
3213
|
+
let hasEmittedContent = false;
|
|
3214
|
+
for await (const event of providerStream) {
|
|
3215
|
+
if (event.type === "error") {
|
|
3216
|
+
const err2 = event.error instanceof Error ? event.error : new Error(String(event.error));
|
|
3217
|
+
if (hasEmittedContent) {
|
|
3218
|
+
emit(event);
|
|
3219
|
+
return { success: true };
|
|
3220
|
+
}
|
|
3221
|
+
return { success: false, error: err2 };
|
|
3222
|
+
}
|
|
3223
|
+
hasEmittedContent = true;
|
|
3224
|
+
emit(event);
|
|
3225
|
+
}
|
|
3226
|
+
return { success: true };
|
|
3227
|
+
}
|
|
3228
|
+
async function runStreamFallbackLoop(available, request, emit) {
|
|
3229
|
+
let lastError = null;
|
|
3230
|
+
let failedProvider = null;
|
|
3231
|
+
for (const entry of available) {
|
|
3232
|
+
try {
|
|
3233
|
+
const result = await tryStreamProvider(entry, request, emit);
|
|
3234
|
+
if (result.success) {
|
|
3235
|
+
if (failedProvider)
|
|
3236
|
+
logFailover(failedProvider, entry.name, lastError?.message);
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
lastError = result.error ?? null;
|
|
3240
|
+
failedProvider = entry.name;
|
|
3241
|
+
logStreamFailover(entry.name, result.error);
|
|
3242
|
+
} catch (err2) {
|
|
3243
|
+
failedProvider = entry.name;
|
|
3244
|
+
lastError = toError2(err2);
|
|
3245
|
+
logStreamFailover(entry.name, lastError);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
emit({
|
|
3249
|
+
type: "error",
|
|
3250
|
+
error: lastError ?? new ElsiumError({
|
|
3251
|
+
code: "PROVIDER_ERROR",
|
|
3252
|
+
message: "All providers failed during streaming",
|
|
3253
|
+
retryable: false
|
|
3254
|
+
})
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
function streamWithFallback(providers, request) {
|
|
3258
|
+
const available = providers.filter((e) => isProviderAvailable(e.name));
|
|
3259
|
+
if (available.length === 0) {
|
|
3260
|
+
return errorStream("All providers unavailable");
|
|
3261
|
+
}
|
|
3262
|
+
return createStream(async (emit) => {
|
|
3263
|
+
await runStreamFallbackLoop(available, request, emit);
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
function streamCostOptimized(request) {
|
|
3267
|
+
const optimizer = config.costOptimizer;
|
|
3268
|
+
if (!optimizer) {
|
|
3269
|
+
return streamWithFallback(sortedProviders, request);
|
|
3270
|
+
}
|
|
3271
|
+
const complexity = estimateComplexity(request);
|
|
3272
|
+
const threshold = optimizer.complexityThreshold ?? 0.5;
|
|
3273
|
+
const target = complexity < threshold ? optimizer.simpleModel : optimizer.complexModel;
|
|
3274
|
+
return createStream(async (emit) => {
|
|
3275
|
+
try {
|
|
3276
|
+
const gw = getGateway(target.provider);
|
|
3277
|
+
const providerStream = gw.stream({ ...request, model: target.model });
|
|
3278
|
+
for await (const event of providerStream) {
|
|
3279
|
+
emit(event);
|
|
3280
|
+
}
|
|
3281
|
+
} catch {
|
|
3282
|
+
const fallbackStream = streamWithFallback(sortedProviders, request);
|
|
3283
|
+
for await (const event of fallbackStream) {
|
|
3284
|
+
emit(event);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
function streamLatencyOptimized(request) {
|
|
3290
|
+
const available = sortedProviders.filter((e) => isProviderAvailable(e.name));
|
|
3291
|
+
if (available.length === 0) {
|
|
3292
|
+
return errorStream("All providers unavailable");
|
|
3293
|
+
}
|
|
3294
|
+
return createStream(async (emit) => {
|
|
3295
|
+
const controller = new AbortController;
|
|
3296
|
+
const racePromises = available.map(async (entry) => {
|
|
3297
|
+
const gw = getGateway(entry.name);
|
|
3298
|
+
return callWithCircuitBreaker(entry.name, async () => ({
|
|
3299
|
+
entry,
|
|
3300
|
+
stream: gw.stream({
|
|
3301
|
+
...request,
|
|
3302
|
+
model: request.model ?? entry.model,
|
|
3303
|
+
signal: controller.signal
|
|
3304
|
+
})
|
|
3305
|
+
}));
|
|
3306
|
+
});
|
|
3307
|
+
try {
|
|
3308
|
+
const winner = await Promise.any(racePromises);
|
|
3309
|
+
controller.abort();
|
|
3310
|
+
for await (const event of winner.stream) {
|
|
3311
|
+
emit(event);
|
|
3312
|
+
}
|
|
3313
|
+
} catch {
|
|
3314
|
+
emit({
|
|
3315
|
+
type: "error",
|
|
3316
|
+
error: new ElsiumError({
|
|
3317
|
+
code: "PROVIDER_ERROR",
|
|
3318
|
+
message: "All providers failed during streaming",
|
|
3319
|
+
retryable: false
|
|
3320
|
+
})
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
function streamCapabilityAware(request) {
|
|
3326
|
+
const capabilities = detectRequiredCapabilities(request);
|
|
3327
|
+
const capable = filterCapableProviders(capabilities);
|
|
3328
|
+
if (capable.length === 0) {
|
|
3329
|
+
return streamWithFallback(sortedProviders, request);
|
|
3330
|
+
}
|
|
3331
|
+
return streamWithFallback(capable, request);
|
|
3332
|
+
}
|
|
3138
3333
|
return {
|
|
3139
3334
|
providers: sortedProviders.map((p) => p.name),
|
|
3140
3335
|
strategy: config.strategy,
|
|
@@ -3153,25 +3348,18 @@ function createProviderMesh(config) {
|
|
|
3153
3348
|
}
|
|
3154
3349
|
},
|
|
3155
3350
|
stream(request) {
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
message: "Circuit breaker is open",
|
|
3168
|
-
retryable: true
|
|
3169
|
-
});
|
|
3170
|
-
return new ElsiumStream(async function* () {
|
|
3171
|
-
yield { type: "error", error: err2 };
|
|
3172
|
-
}());
|
|
3351
|
+
switch (config.strategy) {
|
|
3352
|
+
case "fallback":
|
|
3353
|
+
return streamWithFallback(sortedProviders, request);
|
|
3354
|
+
case "cost-optimized":
|
|
3355
|
+
return streamCostOptimized(request);
|
|
3356
|
+
case "latency-optimized":
|
|
3357
|
+
return streamLatencyOptimized(request);
|
|
3358
|
+
case "capability-aware":
|
|
3359
|
+
return streamCapabilityAware(request);
|
|
3360
|
+
default:
|
|
3361
|
+
return streamWithFallback(sortedProviders, request);
|
|
3173
3362
|
}
|
|
3174
|
-
return resolvedStream;
|
|
3175
3363
|
}
|
|
3176
3364
|
};
|
|
3177
3365
|
}
|
|
@@ -3194,6 +3382,7 @@ export {
|
|
|
3194
3382
|
detectJailbreak,
|
|
3195
3383
|
createProviderMesh,
|
|
3196
3384
|
createOpenAIProvider,
|
|
3385
|
+
createOpenAICompatibleProvider,
|
|
3197
3386
|
createInMemoryCache,
|
|
3198
3387
|
createGoogleProvider,
|
|
3199
3388
|
createBulkhead,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProviderConfig } from '@elsium-ai/core';
|
|
2
|
+
import type { LLMProvider } from '../provider';
|
|
3
|
+
export interface OpenAICompatibleConfig extends ProviderConfig {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
defaultModel?: string;
|
|
7
|
+
capabilities?: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function createOpenAICompatibleProvider(config: OpenAICompatibleConfig): LLMProvider;
|
|
10
|
+
//# sourceMappingURL=openai-compatible.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-compatible.d.ts","sourceRoot":"","sources":["../../src/providers/openai-compatible.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,aAAa,CAAA;AAGhE,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC7D,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,sBAAsB,GAAG,WAAW,CAmC1F"}
|
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>;
|
package/dist/router.d.ts.map
CHANGED
|
@@ -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,
|
|
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,EAGZ,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,CAob3E"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elsium-ai/gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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.
|
|
29
|
+
"@elsium-ai/core": "^0.8.0",
|
|
30
30
|
"zod": "^3.24.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|