@oscharko-dev/keiko-model-gateway 0.2.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.
Files changed (116) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/capabilities.d.ts +26 -0
  3. package/dist/capabilities.d.ts.map +1 -0
  4. package/dist/capabilities.data.d.ts +3 -0
  5. package/dist/capabilities.data.d.ts.map +1 -0
  6. package/dist/capabilities.data.js +5 -0
  7. package/dist/capabilities.js +169 -0
  8. package/dist/config.d.ts +34 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +733 -0
  11. package/dist/embedding.d.ts +38 -0
  12. package/dist/embedding.d.ts.map +1 -0
  13. package/dist/embedding.js +118 -0
  14. package/dist/gateway.d.ts +23 -0
  15. package/dist/gateway.d.ts.map +1 -0
  16. package/dist/gateway.js +144 -0
  17. package/dist/http.d.ts +24 -0
  18. package/dist/http.d.ts.map +1 -0
  19. package/dist/http.js +666 -0
  20. package/dist/index.d.ts +16 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +34 -0
  23. package/dist/model-selection.d.ts +22 -0
  24. package/dist/model-selection.d.ts.map +1 -0
  25. package/dist/model-selection.js +59 -0
  26. package/dist/normalize.d.ts +9 -0
  27. package/dist/normalize.d.ts.map +1 -0
  28. package/dist/normalize.js +114 -0
  29. package/dist/openai-adapter.d.ts +22 -0
  30. package/dist/openai-adapter.d.ts.map +1 -0
  31. package/dist/openai-adapter.js +382 -0
  32. package/dist/openai-embedding-adapter.d.ts +46 -0
  33. package/dist/openai-embedding-adapter.d.ts.map +1 -0
  34. package/dist/openai-embedding-adapter.js +271 -0
  35. package/dist/promptEnhancer/__tests__/_support.d.ts +15 -0
  36. package/dist/promptEnhancer/__tests__/_support.d.ts.map +1 -0
  37. package/dist/promptEnhancer/__tests__/_support.js +28 -0
  38. package/dist/promptEnhancer/__tests__/fixtures.d.ts +8 -0
  39. package/dist/promptEnhancer/__tests__/fixtures.d.ts.map +1 -0
  40. package/dist/promptEnhancer/__tests__/fixtures.js +58 -0
  41. package/dist/promptEnhancer/__tests__/grounding-fixtures.d.ts +11 -0
  42. package/dist/promptEnhancer/__tests__/grounding-fixtures.d.ts.map +1 -0
  43. package/dist/promptEnhancer/__tests__/grounding-fixtures.js +84 -0
  44. package/dist/promptEnhancer/candidates.d.ts +32 -0
  45. package/dist/promptEnhancer/candidates.d.ts.map +1 -0
  46. package/dist/promptEnhancer/candidates.js +109 -0
  47. package/dist/promptEnhancer/critic.d.ts +22 -0
  48. package/dist/promptEnhancer/critic.d.ts.map +1 -0
  49. package/dist/promptEnhancer/critic.js +237 -0
  50. package/dist/promptEnhancer/generator.d.ts +15 -0
  51. package/dist/promptEnhancer/generator.d.ts.map +1 -0
  52. package/dist/promptEnhancer/generator.js +424 -0
  53. package/dist/promptEnhancer/index.d.ts +16 -0
  54. package/dist/promptEnhancer/index.d.ts.map +1 -0
  55. package/dist/promptEnhancer/index.js +15 -0
  56. package/dist/promptEnhancer/optimize.d.ts +27 -0
  57. package/dist/promptEnhancer/optimize.d.ts.map +1 -0
  58. package/dist/promptEnhancer/optimize.js +203 -0
  59. package/dist/promptEnhancer/planner.d.ts +36 -0
  60. package/dist/promptEnhancer/planner.d.ts.map +1 -0
  61. package/dist/promptEnhancer/planner.js +55 -0
  62. package/dist/promptEnhancer/profiles.d.ts +20 -0
  63. package/dist/promptEnhancer/profiles.d.ts.map +1 -0
  64. package/dist/promptEnhancer/profiles.js +126 -0
  65. package/dist/promptEnhancer/rendering.d.ts +15 -0
  66. package/dist/promptEnhancer/rendering.d.ts.map +1 -0
  67. package/dist/promptEnhancer/rendering.js +72 -0
  68. package/dist/promptEnhancer/validate.d.ts +31 -0
  69. package/dist/promptEnhancer/validate.d.ts.map +1 -0
  70. package/dist/promptEnhancer/validate.js +144 -0
  71. package/dist/qualityIntelligence/budget.d.ts +10 -0
  72. package/dist/qualityIntelligence/budget.d.ts.map +1 -0
  73. package/dist/qualityIntelligence/budget.js +38 -0
  74. package/dist/qualityIntelligence/cancellation.d.ts +7 -0
  75. package/dist/qualityIntelligence/cancellation.d.ts.map +1 -0
  76. package/dist/qualityIntelligence/cancellation.js +58 -0
  77. package/dist/qualityIntelligence/capabilityGate.d.ts +13 -0
  78. package/dist/qualityIntelligence/capabilityGate.d.ts.map +1 -0
  79. package/dist/qualityIntelligence/capabilityGate.js +51 -0
  80. package/dist/qualityIntelligence/capabilityMapping.d.ts +4 -0
  81. package/dist/qualityIntelligence/capabilityMapping.d.ts.map +1 -0
  82. package/dist/qualityIntelligence/capabilityMapping.js +21 -0
  83. package/dist/qualityIntelligence/circuitBreaker.d.ts +26 -0
  84. package/dist/qualityIntelligence/circuitBreaker.d.ts.map +1 -0
  85. package/dist/qualityIntelligence/circuitBreaker.js +78 -0
  86. package/dist/qualityIntelligence/dispatcher.d.ts +38 -0
  87. package/dist/qualityIntelligence/dispatcher.d.ts.map +1 -0
  88. package/dist/qualityIntelligence/dispatcher.js +116 -0
  89. package/dist/qualityIntelligence/index.d.ts +20 -0
  90. package/dist/qualityIntelligence/index.d.ts.map +1 -0
  91. package/dist/qualityIntelligence/index.js +15 -0
  92. package/dist/qualityIntelligence/promptSegmentation.d.ts +13 -0
  93. package/dist/qualityIntelligence/promptSegmentation.d.ts.map +1 -0
  94. package/dist/qualityIntelligence/promptSegmentation.js +70 -0
  95. package/dist/qualityIntelligence/replayCache.d.ts +11 -0
  96. package/dist/qualityIntelligence/replayCache.d.ts.map +1 -0
  97. package/dist/qualityIntelligence/replayCache.js +72 -0
  98. package/dist/qualityIntelligence/routing.d.ts +11 -0
  99. package/dist/qualityIntelligence/routing.d.ts.map +1 -0
  100. package/dist/qualityIntelligence/routing.js +25 -0
  101. package/dist/qualityIntelligence/safeError.d.ts +38 -0
  102. package/dist/qualityIntelligence/safeError.d.ts.map +1 -0
  103. package/dist/qualityIntelligence/safeError.js +63 -0
  104. package/dist/qualityIntelligence/taskProfiles.d.ts +15 -0
  105. package/dist/qualityIntelligence/taskProfiles.d.ts.map +1 -0
  106. package/dist/qualityIntelligence/taskProfiles.js +101 -0
  107. package/dist/resilience.d.ts +26 -0
  108. package/dist/resilience.d.ts.map +1 -0
  109. package/dist/resilience.js +182 -0
  110. package/dist/types.d.ts +59 -0
  111. package/dist/types.d.ts.map +1 -0
  112. package/dist/types.js +6 -0
  113. package/dist/version.d.ts +2 -0
  114. package/dist/version.d.ts.map +1 -0
  115. package/dist/version.js +3 -0
  116. package/package.json +47 -0
@@ -0,0 +1,38 @@
1
+ import type { EmbeddingModelIdentity, EmbeddingVectorMetric } from "@oscharko-dev/keiko-contracts";
2
+ import type { OpenAIEmbeddingBatchOutcome, OpenAIEmbeddingBatchRequest, OpenAIEmbeddingRequest, OpenAIEmbeddingOutcome } from "./openai-embedding-adapter.js";
3
+ import type { OutboundHttpEgressConfig } from "./types.js";
4
+ export type EmbeddingFailureReason = "missing-credentials" | "unavailable" | "wrong-header" | "rate-limited" | "dimension-mismatch" | "timeout" | "cancelled" | "unsupported-model" | "proxy-unreachable" | "proxy-auth-required" | "proxy-egress-failed" | "proxy-blocked-by-policy" | "tls-ca-failure" | "invalid-response" | "incompatible-with-stored-identity";
5
+ export interface EmbeddingIdentityWarning {
6
+ readonly code: "model-revision-changed";
7
+ readonly safeMessage: string;
8
+ readonly previousRevision?: string;
9
+ readonly currentRevision?: string;
10
+ }
11
+ export type EmbeddingCapabilityCheck = {
12
+ readonly ok: true;
13
+ readonly identity: EmbeddingModelIdentity;
14
+ readonly warning?: EmbeddingIdentityWarning;
15
+ } | {
16
+ readonly ok: false;
17
+ readonly reason: EmbeddingFailureReason;
18
+ readonly safeMessage: string;
19
+ };
20
+ export interface EmbeddingProbeOptions {
21
+ readonly modelId: string;
22
+ readonly provider: string;
23
+ readonly vectorMetric: EmbeddingVectorMetric;
24
+ readonly expectedDimensions?: number;
25
+ readonly signal?: AbortSignal;
26
+ readonly timeoutMs?: number;
27
+ }
28
+ export interface OpenAIEmbeddingAdapter {
29
+ readonly endpoint: string;
30
+ readonly apiKey: string;
31
+ readonly apiKeyHeaderName?: string;
32
+ readonly egress?: OutboundHttpEgressConfig | undefined;
33
+ readonly request: (input: OpenAIEmbeddingRequest) => Promise<OpenAIEmbeddingOutcome>;
34
+ readonly requestBatch?: (input: OpenAIEmbeddingBatchRequest) => Promise<OpenAIEmbeddingBatchOutcome>;
35
+ }
36
+ export declare function verifyEmbeddingCapability(adapter: OpenAIEmbeddingAdapter, options: EmbeddingProbeOptions): Promise<EmbeddingCapabilityCheck>;
37
+ export declare function assertCompatibleEmbeddingIdentity(stored: EmbeddingModelIdentity, current: EmbeddingModelIdentity): EmbeddingCapabilityCheck;
38
+ //# sourceMappingURL=embedding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding.d.ts","sourceRoot":"","sources":["../src/embedding.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACnG,OAAO,KAAK,EACV,2BAA2B,EAC3B,2BAA2B,EAE3B,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,aAAa,GACb,cAAc,GACd,cAAc,GACd,oBAAoB,GACpB,SAAS,GACT,WAAW,GACX,mBAAmB,GACnB,mBAAmB,GACnB,qBAAqB,GACrB,qBAAqB,GACrB,yBAAyB,GACzB,gBAAgB,GAChB,kBAAkB,GAClB,mCAAmC,CAAC;AAExC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,MAAM,MAAM,wBAAwB,GAChC;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,wBAAwB,CAAC;CAC7C,GACD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAElG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,wBAAwB,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAIrF,QAAQ,CAAC,YAAY,CAAC,EAAE,CACtB,KAAK,EAAE,2BAA2B,KAC/B,OAAO,CAAC,2BAA2B,CAAC,CAAC;CAC3C;AAkED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,wBAAwB,CAAC,CAiCnC;AA4BD,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,sBAAsB,EAC9B,OAAO,EAAE,sBAAsB,GAC9B,wBAAwB,CAW1B"}
@@ -0,0 +1,118 @@
1
+ // Embedding capability verification + identity compatibility for the Local
2
+ // Knowledge Connector. Decides whether a configured embedding model is reachable
3
+ // and whether its vector space remains compatible with a previously stored
4
+ // embedding identity. Every failure path produces a `safeMessage` string that
5
+ // MUST NOT contain API keys, full URLs, raw provider bodies, IPs, or the
6
+ // user-supplied input. Probes are pure-ish: I/O is encapsulated in the injected
7
+ // adapter function.
8
+ const PROBE_INPUT = "ping";
9
+ const SAFE_MESSAGES = Object.freeze({
10
+ "missing-credentials": "model gateway credentials are not configured",
11
+ unavailable: "model gateway is not reachable",
12
+ "wrong-header": "model gateway rejected the request — check API key configuration",
13
+ "rate-limited": "model gateway is rate-limited — retry after the configured backoff",
14
+ "dimension-mismatch": "embedding vector dimensions do not match the expected value",
15
+ timeout: "embedding probe timed out",
16
+ cancelled: "embedding probe was cancelled by the caller",
17
+ "unsupported-model": "embedding model is not available on the configured gateway",
18
+ "proxy-unreachable": "configured proxy is unreachable",
19
+ "proxy-auth-required": "configured proxy requires authentication",
20
+ "proxy-egress-failed": "configured proxy failed outbound egress",
21
+ "proxy-blocked-by-policy": "configured proxy blocked outbound egress",
22
+ "tls-ca-failure": "TLS certificate verification failed for outbound egress",
23
+ "invalid-response": "embedding response was malformed",
24
+ "incompatible-with-stored-identity": "embedding model identity changed — existing capsules are no longer compatible",
25
+ });
26
+ const ADAPTER_FAILURE_REASONS = Object.freeze({
27
+ "wrong-header": "wrong-header",
28
+ "rate-limited": "rate-limited",
29
+ "unsupported-model": "unsupported-model",
30
+ timeout: "timeout",
31
+ cancelled: "cancelled",
32
+ "invalid-response": "invalid-response",
33
+ "proxy-unreachable": "proxy-unreachable",
34
+ "proxy-auth-required": "proxy-auth-required",
35
+ "proxy-egress-failed": "proxy-egress-failed",
36
+ "proxy-blocked-by-policy": "proxy-blocked-by-policy",
37
+ "tls-ca-failure": "tls-ca-failure",
38
+ transport: "unavailable",
39
+ });
40
+ function fail(reason) {
41
+ return { ok: false, reason, safeMessage: SAFE_MESSAGES[reason] };
42
+ }
43
+ function reasonFromAdapter(kind) {
44
+ return ADAPTER_FAILURE_REASONS[kind];
45
+ }
46
+ function hasCredentials(adapter) {
47
+ return adapter.apiKey.trim().length > 0;
48
+ }
49
+ function buildIdentity(options, detectedModelId, detectedDimensions, modelRevision) {
50
+ return {
51
+ provider: options.provider,
52
+ modelId: detectedModelId,
53
+ vectorDimensions: detectedDimensions,
54
+ vectorMetric: options.vectorMetric,
55
+ ...(modelRevision !== undefined ? { modelRevision } : {}),
56
+ };
57
+ }
58
+ export async function verifyEmbeddingCapability(adapter, options) {
59
+ if (!hasCredentials(adapter)) {
60
+ return fail("missing-credentials");
61
+ }
62
+ const request = {
63
+ endpoint: adapter.endpoint,
64
+ apiKey: adapter.apiKey,
65
+ ...(adapter.apiKeyHeaderName !== undefined
66
+ ? { apiKeyHeaderName: adapter.apiKeyHeaderName }
67
+ : {}),
68
+ ...(adapter.egress !== undefined ? { egress: adapter.egress } : {}),
69
+ modelId: options.modelId,
70
+ input: PROBE_INPUT,
71
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
72
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
73
+ };
74
+ const outcome = await adapter.request(request);
75
+ if (!outcome.ok) {
76
+ return fail(reasonFromAdapter(outcome.kind));
77
+ }
78
+ const detected = outcome.value.vector.length;
79
+ if (detected === 0) {
80
+ return fail("invalid-response");
81
+ }
82
+ if (options.expectedDimensions !== undefined && options.expectedDimensions !== detected) {
83
+ return fail("dimension-mismatch");
84
+ }
85
+ return {
86
+ ok: true,
87
+ identity: buildIdentity(options, outcome.value.modelId, detected, outcome.value.modelRevision),
88
+ };
89
+ }
90
+ function structuralFieldsEqual(a, b) {
91
+ return (a.provider === b.provider &&
92
+ a.modelId === b.modelId &&
93
+ a.vectorDimensions === b.vectorDimensions &&
94
+ a.vectorMetric === b.vectorMetric);
95
+ }
96
+ function revisionDiffers(stored, current) {
97
+ return stored.modelRevision !== current.modelRevision;
98
+ }
99
+ function buildRevisionWarning(stored, current) {
100
+ return {
101
+ code: "model-revision-changed",
102
+ safeMessage: "embedding model revision changed — capsules remain compatible but should be re-validated",
103
+ ...(stored.modelRevision !== undefined ? { previousRevision: stored.modelRevision } : {}),
104
+ ...(current.modelRevision !== undefined ? { currentRevision: current.modelRevision } : {}),
105
+ };
106
+ }
107
+ export function assertCompatibleEmbeddingIdentity(stored, current) {
108
+ if (!structuralFieldsEqual(stored, current)) {
109
+ return fail("incompatible-with-stored-identity");
110
+ }
111
+ if (revisionDiffers(stored, current)) {
112
+ // Return the CURRENT identity (not stored) so the caller can persist the new revision
113
+ // and avoid a permanent warning on every subsequent compatibility check. The warning
114
+ // carries the previous revision for diagnostics. #192 Copilot finding.
115
+ return { ok: true, identity: current, warning: buildRevisionWarning(stored, current) };
116
+ }
117
+ return { ok: true, identity: stored };
118
+ }
@@ -0,0 +1,23 @@
1
+ import type { Clock, CircuitBreakerStatus, GatewayConfig, GatewayRequest, GatewayStreamChunk, NormalizedResponse, ProviderAdapter } from "./types.js";
2
+ export interface GatewayDeps {
3
+ readonly adapter?: ProviderAdapter | undefined;
4
+ readonly clock?: Clock | undefined;
5
+ }
6
+ export declare class Gateway {
7
+ private readonly config;
8
+ private readonly clock;
9
+ private readonly adapter;
10
+ private readonly providers;
11
+ private readonly breakers;
12
+ constructor(config: GatewayConfig, deps?: GatewayDeps);
13
+ chat(request: GatewayRequest): Promise<NormalizedResponse>;
14
+ chatStream(request: GatewayRequest): AsyncGenerator<GatewayStreamChunk>;
15
+ private streamFrom;
16
+ private enrich;
17
+ circuitStatus(modelId: string): CircuitBreakerStatus;
18
+ private invoke;
19
+ private route;
20
+ private breakerFor;
21
+ private adapterFor;
22
+ }
23
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,KAAK,EACL,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,kBAAkB,EAGlB,kBAAkB,EAClB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;CACpC;AAOD,qBAAa,OAAO;IAOhB,OAAO,CAAC,QAAQ,CAAC,MAAM;IANzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2C;IACrE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;gBAG3C,MAAM,EAAE,aAAa,EACtC,IAAI,GAAE,WAAgB;IAOlB,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA+BzD,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAAC,kBAAkB,CAAC;YAuB/D,UAAU;IAczB,OAAO,CAAC,MAAM;IAiBd,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,oBAAoB;YAYtC,MAAM;IAoBpB,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,UAAU;CAUnB"}
@@ -0,0 +1,144 @@
1
+ // Orchestrator: routes a request through the capability registry, then through the
2
+ // circuit breaker, bounded retry, and the provider adapter. Usage metadata
3
+ // (request id, latency, cost class) is owned by the gateway, not the provider, so
4
+ // the audit ledger (issue #10) has a reliable typed target on every response.
5
+ import { randomUUID } from "node:crypto";
6
+ import { CancelledError, UnknownModelError } from "@oscharko-dev/keiko-security/errors/gateway";
7
+ import { findConfiguredCapability } from "./model-selection.js";
8
+ import { OpenAiAdapter } from "./openai-adapter.js";
9
+ import { CircuitBreaker, executeWithRetry, systemClock } from "./resilience.js";
10
+ export class Gateway {
11
+ config;
12
+ clock;
13
+ adapter;
14
+ providers;
15
+ breakers = new Map();
16
+ constructor(config, deps = {}) {
17
+ this.config = config;
18
+ this.clock = deps.clock ?? systemClock;
19
+ this.adapter = deps.adapter;
20
+ this.providers = new Map(config.providers.map((p) => [p.modelId, p]));
21
+ }
22
+ async chat(request) {
23
+ const route = this.route(request.modelId);
24
+ const breaker = this.breakerFor(route.provider);
25
+ const requestId = randomUUID();
26
+ const start = this.clock.now();
27
+ const adapter = this.adapterFor(requestId, route.capability);
28
+ const result = await executeWithRetry((attemptTimeoutMs) => this.invoke(breaker, adapter, request, {
29
+ ...route.provider,
30
+ ...(attemptTimeoutMs === undefined ? {} : { timeoutMs: attemptTimeoutMs }),
31
+ }), route.provider, this.clock, request.cancellationSignal);
32
+ return {
33
+ ...result,
34
+ usage: {
35
+ ...result.usage,
36
+ requestId,
37
+ latencyMs: Math.max(1, this.clock.now() - start),
38
+ costClass: route.capability.costClass,
39
+ },
40
+ };
41
+ }
42
+ // Streaming counterpart of chat(). Routes identically and guards with the circuit
43
+ // breaker, but is NOT wrapped in executeWithRetry: a mid-stream retry would replay
44
+ // already-emitted tokens. An adapter without a streaming variant falls back to a
45
+ // single delta+done synthesised from its buffered call().
46
+ async *chatStream(request) {
47
+ const route = this.route(request.modelId);
48
+ const breaker = this.breakerFor(route.provider);
49
+ breaker.assertAllowed();
50
+ const requestId = randomUUID();
51
+ const start = this.clock.now();
52
+ const adapter = this.adapterFor(requestId, route.capability);
53
+ try {
54
+ for await (const chunk of this.streamFrom(adapter, request, route.provider)) {
55
+ yield chunk.type === "done"
56
+ ? { type: "done", response: this.enrich(chunk.response, requestId, start, route) }
57
+ : chunk;
58
+ }
59
+ breaker.recordSuccess();
60
+ }
61
+ catch (error) {
62
+ // A client-initiated cancel is not a provider fault — skip the breaker.
63
+ if (!(error instanceof CancelledError)) {
64
+ breaker.recordFailure();
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ async *streamFrom(adapter, request, provider) {
70
+ if (adapter.callStream !== undefined) {
71
+ yield* adapter.callStream(request, provider);
72
+ return;
73
+ }
74
+ const response = await adapter.call(request, provider);
75
+ yield { type: "delta", token: response.content };
76
+ yield { type: "done", response };
77
+ }
78
+ enrich(response, requestId, start, route) {
79
+ return {
80
+ ...response,
81
+ usage: {
82
+ ...response.usage,
83
+ requestId,
84
+ latencyMs: Math.max(1, this.clock.now() - start),
85
+ costClass: route.capability.costClass,
86
+ },
87
+ };
88
+ }
89
+ circuitStatus(modelId) {
90
+ const breaker = this.breakers.get(modelId);
91
+ return (breaker?.status(modelId) ?? {
92
+ modelId,
93
+ state: "closed",
94
+ consecutiveFailures: 0,
95
+ openedAt: null,
96
+ });
97
+ }
98
+ async invoke(breaker, adapter, request, provider) {
99
+ breaker.assertAllowed();
100
+ try {
101
+ const response = await adapter.call(request, provider);
102
+ breaker.recordSuccess();
103
+ return response;
104
+ }
105
+ catch (error) {
106
+ // A client-initiated cancel is not a provider fault — skip the breaker.
107
+ if (!(error instanceof CancelledError)) {
108
+ breaker.recordFailure();
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+ route(modelId) {
114
+ const provider = this.providers.get(modelId);
115
+ if (provider === undefined) {
116
+ throw new UnknownModelError(`no provider configured for model '${modelId}'`);
117
+ }
118
+ const capability = findConfiguredCapability(this.config, modelId);
119
+ if (capability === undefined) {
120
+ throw new UnknownModelError(`model '${modelId}' has no capability metadata`);
121
+ }
122
+ if (capability.kind !== "chat") {
123
+ throw new UnknownModelError(`model '${modelId}' has kind '${capability.kind}'; the chat path requires a chat model`);
124
+ }
125
+ return { provider, capability };
126
+ }
127
+ breakerFor(provider) {
128
+ const existing = this.breakers.get(provider.modelId);
129
+ if (existing !== undefined) {
130
+ return existing;
131
+ }
132
+ const breaker = new CircuitBreaker(provider.modelId, this.config.circuitBreaker, this.clock);
133
+ this.breakers.set(provider.modelId, breaker);
134
+ return breaker;
135
+ }
136
+ adapterFor(requestId, capability) {
137
+ return (this.adapter ??
138
+ new OpenAiAdapter({
139
+ requestId,
140
+ costClass: capability.costClass,
141
+ now: this.clock.now,
142
+ }));
143
+ }
144
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { OutboundHttpEgressConfig } from "./types.js";
2
+ export type { OutboundHttpEgressConfig } from "./types.js";
3
+ export declare const MAX_RESPONSE_BYTES = 10000000;
4
+ export interface GatewayFetchOptions extends RequestInit {
5
+ readonly fetchImpl?: typeof fetch | undefined;
6
+ readonly useCaFallback?: boolean | undefined;
7
+ readonly egress?: OutboundHttpEgressConfig | undefined;
8
+ readonly timeoutMs?: number | undefined;
9
+ readonly maxResponseBytes?: number | undefined;
10
+ }
11
+ export type OutboundHttpEgressErrorCode = "PROXY_UNREACHABLE" | "PROXY_AUTH_REQUIRED" | "PROXY_EGRESS_FAILED" | "PROXY_BLOCKED_BY_POLICY" | "TLS_CA_FAILURE";
12
+ export declare class OutboundHttpEgressError extends Error {
13
+ readonly code: OutboundHttpEgressErrorCode;
14
+ constructor(code: OutboundHttpEgressErrorCode, message: string);
15
+ }
16
+ export declare function isMissingIssuerError(error: unknown): boolean;
17
+ export declare function isRecoverableTlsTrustError(error: unknown): boolean;
18
+ export declare function gatewayTrustedCaCertificates(caBundlePath?: string): readonly string[];
19
+ export declare function _resetWarnedCaBundlePaths(): void;
20
+ export declare function streamingResponseFromNode(res: import("node:http").IncomingMessage, onCancel: () => void, maxBytes?: number): Response;
21
+ export declare function gatewayFetch(url: string, options?: GatewayFetchOptions): Promise<Response>;
22
+ export declare function readJsonCapped(response: Response, maxBytes?: number): Promise<unknown>;
23
+ export declare function readSseStream(response: Response, maxBytes?: number): AsyncGenerator;
24
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE3D,YAAY,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,eAAO,MAAM,kBAAkB,WAAa,CAAC;AAE7C,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,wBAAwB,GAAG,SAAS,CAAC;IAIvD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAExC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,MAAM,2BAA2B,GACnC,mBAAmB,GACnB,qBAAqB,GACrB,qBAAqB,GACrB,yBAAyB,GACzB,gBAAgB,CAAC;AAErB,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;gBAE/B,IAAI,EAAE,2BAA2B,EAAE,OAAO,EAAE,MAAM;CAK/D;AAsCD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO5D;AASD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAOlE;AAoDD,wBAAgB,4BAA4B,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAUrF;AAGD,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAmBD,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EACxC,QAAQ,EAAE,MAAM,IAAI,EACpB,QAAQ,GAAE,MAA2B,GACpC,QAAQ,CAqCV;AAufD,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,CAAC,CAoBnB;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,GAAE,MAA2B,GACpC,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAoCD,wBAAuB,aAAa,CAClC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,GAAE,MAA2B,GACpC,cAAc,CA2BhB"}