@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
package/dist/http.js ADDED
@@ -0,0 +1,666 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { request as httpRequest } from "node:http";
3
+ import { request as httpsRequest } from "node:https";
4
+ import { connect as netConnect, isIP } from "node:net";
5
+ import * as tls from "node:tls";
6
+ // Caps a single gateway response at 10 MB; real chat completions are far smaller.
7
+ export const MAX_RESPONSE_BYTES = 10_000_000;
8
+ export class OutboundHttpEgressError extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.name = "OutboundHttpEgressError";
13
+ this.code = code;
14
+ }
15
+ }
16
+ const FORWARDED_CREDENTIAL_HEADERS = new Set([
17
+ "authorization",
18
+ "x-litellm-key",
19
+ "x-api-key",
20
+ "api-key",
21
+ ]);
22
+ function headersFromNode(headers) {
23
+ const out = new Headers();
24
+ for (const [name, value] of Object.entries(headers)) {
25
+ if (Array.isArray(value)) {
26
+ for (const item of value)
27
+ out.append(name, item);
28
+ }
29
+ else if (value !== undefined) {
30
+ out.set(name, value);
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+ function headersToRecord(headers) {
36
+ const out = {};
37
+ const normalized = new Headers(headers);
38
+ normalized.forEach((value, key) => {
39
+ out[key] = value;
40
+ });
41
+ return out;
42
+ }
43
+ function hasForwardedCredentialHeader(headers) {
44
+ return Object.keys(headers).some((name) => FORWARDED_CREDENTIAL_HEADERS.has(name.toLowerCase()));
45
+ }
46
+ function isRecord(value) {
47
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48
+ }
49
+ export function isMissingIssuerError(error) {
50
+ const cause = isRecord(error) ? error.cause : undefined;
51
+ const candidates = [error, cause];
52
+ return candidates.some((item) => {
53
+ if (!isRecord(item))
54
+ return false;
55
+ return item.code === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY";
56
+ });
57
+ }
58
+ const RECOVERABLE_TLS_TRUST_ERROR_CODES = new Set([
59
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
60
+ "SELF_SIGNED_CERT_IN_CHAIN",
61
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
62
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
63
+ ]);
64
+ export function isRecoverableTlsTrustError(error) {
65
+ const cause = isRecord(error) ? error.cause : undefined;
66
+ const candidates = [error, cause];
67
+ return candidates.some((item) => {
68
+ if (!isRecord(item) || typeof item.code !== "string")
69
+ return false;
70
+ return RECOVERABLE_TLS_TRUST_ERROR_CODES.has(item.code);
71
+ });
72
+ }
73
+ function usesHttps(url) {
74
+ try {
75
+ return new URL(url).protocol === "https:";
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ function readCertificateFile(path) {
82
+ try {
83
+ return [readFileSync(path, "utf8")];
84
+ }
85
+ catch {
86
+ return [];
87
+ }
88
+ }
89
+ // One-time set of paths we have already warned about so the warning fires once per path.
90
+ const warnedCaBundlePaths = new Set();
91
+ function extraCaCertificates(caBundlePath) {
92
+ const paths = [process.env.NODE_EXTRA_CA_CERTS, caBundlePath].filter((path) => path !== undefined && path.trim().length > 0);
93
+ return paths.flatMap((path) => {
94
+ const certs = readCertificateFile(path);
95
+ // Warn once when a configured path yields no certificates so the operator
96
+ // can tell the file is missing or unreadable without throwing at startup.
97
+ if (certs.length === 0 && !warnedCaBundlePaths.has(path)) {
98
+ warnedCaBundlePaths.add(path);
99
+ // eslint-disable-next-line no-console
100
+ console.warn(`[keiko-model-gateway] CA bundle at ${path} could not be read or is empty`);
101
+ }
102
+ return certs;
103
+ });
104
+ }
105
+ function nodeCaCertificates(source) {
106
+ const getter = tls.getCACertificates;
107
+ if (typeof getter !== "function") {
108
+ return [];
109
+ }
110
+ try {
111
+ return getter(source);
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ }
117
+ export function gatewayTrustedCaCertificates(caBundlePath) {
118
+ return Array.from(new Set([
119
+ ...nodeCaCertificates("default"),
120
+ ...tls.rootCertificates,
121
+ ...nodeCaCertificates("system"),
122
+ ...nodeCaCertificates("extra"),
123
+ ...extraCaCertificates(caBundlePath),
124
+ ]));
125
+ }
126
+ // Exposed for tests to reset the one-time warning set between runs.
127
+ export function _resetWarnedCaBundlePaths() {
128
+ warnedCaBundlePaths.clear();
129
+ }
130
+ function bodyToString(body) {
131
+ if (body === undefined || body === null) {
132
+ return undefined;
133
+ }
134
+ if (typeof body === "string") {
135
+ return body;
136
+ }
137
+ if (body instanceof URLSearchParams) {
138
+ return body.toString();
139
+ }
140
+ throw new TypeError("gateway HTTP fallback supports string request bodies only");
141
+ }
142
+ // Converts a Node IncomingMessage into a streaming web Response, enforcing the
143
+ // byte cap inline and destroying the request when the consumer cancels. Unlike a
144
+ // Buffer.concat-on-end approach this delivers SSE chunks incrementally (#152), so
145
+ // the CA-bundle fallback streams tokens instead of buffering the whole response.
146
+ export function streamingResponseFromNode(res, onCancel, maxBytes = MAX_RESPONSE_BYTES) {
147
+ let total = 0;
148
+ let done = false;
149
+ const body = new ReadableStream({
150
+ start(controller) {
151
+ res.on("data", (chunk) => {
152
+ if (done)
153
+ return;
154
+ total += chunk.length;
155
+ if (total > maxBytes) {
156
+ done = true;
157
+ controller.error(new Error("gateway response exceeded the size limit"));
158
+ onCancel();
159
+ return;
160
+ }
161
+ controller.enqueue(new Uint8Array(chunk));
162
+ });
163
+ res.on("end", () => {
164
+ if (done)
165
+ return;
166
+ done = true;
167
+ controller.close();
168
+ });
169
+ res.on("error", (error) => {
170
+ if (done)
171
+ return;
172
+ done = true;
173
+ controller.error(error);
174
+ });
175
+ },
176
+ cancel() {
177
+ done = true;
178
+ onCancel();
179
+ },
180
+ });
181
+ return new Response(body, {
182
+ status: res.statusCode ?? 500,
183
+ statusText: res.statusMessage ?? "",
184
+ headers: headersFromNode(res.headers),
185
+ });
186
+ }
187
+ // Composes a caller-supplied AbortSignal with an AbortSignal.timeout so both
188
+ // cancellation and deadline are observed. Returns undefined when neither is set.
189
+ function composeSignal(callerSignal, timeoutMs) {
190
+ const timeoutSignal = timeoutMs !== undefined ? AbortSignal.timeout(timeoutMs) : undefined;
191
+ if (callerSignal != null && timeoutSignal !== undefined) {
192
+ return AbortSignal.any([callerSignal, timeoutSignal]);
193
+ }
194
+ if (callerSignal != null)
195
+ return callerSignal;
196
+ return timeoutSignal;
197
+ }
198
+ function fetchWithCaBundle(url, init, egress, maxResponseBytes) {
199
+ const body = bodyToString(init.body);
200
+ const headers = headersToRecord(init.headers);
201
+ const cap = maxResponseBytes ?? MAX_RESPONSE_BYTES;
202
+ return new Promise((resolve, reject) => {
203
+ const req = httpsRequest(url, {
204
+ method: init.method ?? "GET",
205
+ headers,
206
+ ca: [...gatewayTrustedCaCertificates(egress?.caBundlePath)],
207
+ signal: init.signal ?? undefined,
208
+ }, (res) => {
209
+ resolve(streamingResponseFromNode(res, () => req.destroy(), cap));
210
+ });
211
+ req.on("error", reject);
212
+ req.end(body);
213
+ });
214
+ }
215
+ function normalizeHost(hostname) {
216
+ return hostname.toLowerCase().replace(/^\[/u, "").replace(/\]$/u, "");
217
+ }
218
+ function tlsServerName(hostname) {
219
+ const normalized = normalizeHost(hostname);
220
+ return isIP(normalized) === 0 ? normalized : undefined;
221
+ }
222
+ function defaultPort(protocol) {
223
+ return protocol === "https:" ? "443" : "80";
224
+ }
225
+ function targetPort(url) {
226
+ return url.port.length > 0 ? url.port : defaultPort(url.protocol);
227
+ }
228
+ // Returns the Host header value for a target URL: omit the port when it is the
229
+ // default for the scheme (443 for https, 80 for http) so the value matches what
230
+ // undici sends directly and satisfies SigV4 pre-signed S3 URLs behind a proxy.
231
+ function hostHeader(url) {
232
+ const isDefaultPort = (url.protocol === "https:" && (url.port === "" || url.port === "443")) ||
233
+ (url.protocol === "http:" && (url.port === "" || url.port === "80"));
234
+ return isDefaultPort ? url.hostname : `${url.hostname}:${url.port}`;
235
+ }
236
+ function noProxyRuleMatches(rule, host, hostPort) {
237
+ if (rule.length === 0)
238
+ return false;
239
+ if (rule === "*")
240
+ return true;
241
+ if (rule.includes(":") && normalizeHost(rule) === hostPort)
242
+ return true;
243
+ const domain = rule.startsWith(".") ? rule.slice(1) : rule;
244
+ if (host === domain)
245
+ return true;
246
+ return host.endsWith(`.${domain}`);
247
+ }
248
+ function noProxyMatches(url, rules) {
249
+ if (rules === undefined || rules.length === 0)
250
+ return false;
251
+ const host = normalizeHost(url.hostname);
252
+ const hostPort = `${host}:${targetPort(url)}`;
253
+ for (const rawRule of rules) {
254
+ const rule = rawRule.trim().toLowerCase();
255
+ if (noProxyRuleMatches(rule, host, hostPort))
256
+ return true;
257
+ }
258
+ return false;
259
+ }
260
+ function parseProxyUrl(raw) {
261
+ let url;
262
+ try {
263
+ url = new URL(raw);
264
+ }
265
+ catch {
266
+ throw new OutboundHttpEgressError("PROXY_EGRESS_FAILED", "Configured proxy URL is invalid.");
267
+ }
268
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
269
+ throw new OutboundHttpEgressError("PROXY_EGRESS_FAILED", "Configured proxy URL uses an unsupported scheme.");
270
+ }
271
+ if (url.username !== "" || url.password !== "") {
272
+ throw new OutboundHttpEgressError("PROXY_AUTH_REQUIRED", "Proxy credentials must not be embedded in the proxy URL.");
273
+ }
274
+ return url;
275
+ }
276
+ function proxyForTarget(target, egress) {
277
+ if (egress === undefined || noProxyMatches(target, egress.noProxy))
278
+ return undefined;
279
+ if (target.protocol === "https:")
280
+ return egress.httpsProxy ?? egress.httpProxy;
281
+ if (target.protocol === "http:")
282
+ return egress.httpProxy;
283
+ return undefined;
284
+ }
285
+ function proxyPort(proxy) {
286
+ if (proxy.port.length > 0)
287
+ return Number(proxy.port);
288
+ return proxy.protocol === "https:" ? 443 : 80;
289
+ }
290
+ const PROXY_UNREACHABLE_CODES = new Set([
291
+ "ECONNREFUSED",
292
+ "ENOTFOUND",
293
+ "EAI_AGAIN",
294
+ "ETIMEDOUT",
295
+ "EHOSTUNREACH",
296
+ "ENETUNREACH",
297
+ ]);
298
+ const ABORT_ERROR_NAMES = new Set(["AbortError", "TimeoutError"]);
299
+ function mapProxyError(error) {
300
+ if (error instanceof OutboundHttpEgressError)
301
+ return error;
302
+ if (isRecoverableTlsTrustError(error)) {
303
+ return tlsCaFailureError();
304
+ }
305
+ if (error instanceof Error) {
306
+ const code = isRecord(error) ? error.code : undefined;
307
+ if ((typeof code === "string" && PROXY_UNREACHABLE_CODES.has(code)) ||
308
+ ABORT_ERROR_NAMES.has(error.name)) {
309
+ return new OutboundHttpEgressError("PROXY_UNREACHABLE", "Configured proxy is unreachable.");
310
+ }
311
+ return error;
312
+ }
313
+ return new OutboundHttpEgressError("PROXY_EGRESS_FAILED", "Outbound egress failed.");
314
+ }
315
+ function tlsCaFailureError() {
316
+ return new OutboundHttpEgressError("TLS_CA_FAILURE", "TLS certificate verification failed for outbound egress.");
317
+ }
318
+ const PROXY_UNREACHABLE_ERROR = new OutboundHttpEgressError("PROXY_UNREACHABLE", "Configured proxy is unreachable.");
319
+ function attachAbortGuard(signal, onAbort) {
320
+ if (signal.aborted) {
321
+ onAbort();
322
+ return () => undefined;
323
+ }
324
+ signal.addEventListener("abort", onAbort, { once: true });
325
+ return () => {
326
+ signal.removeEventListener("abort", onAbort);
327
+ };
328
+ }
329
+ function openProxySocket(proxy, ca, signal) {
330
+ const host = proxy.hostname;
331
+ const port = proxyPort(proxy);
332
+ return new Promise((resolve, reject) => {
333
+ let settled = false;
334
+ const settle = (fn) => {
335
+ if (settled)
336
+ return;
337
+ settled = true;
338
+ cleanup();
339
+ fn();
340
+ };
341
+ const onConnect = () => {
342
+ settle(() => {
343
+ resolve(socket);
344
+ });
345
+ };
346
+ const onError = (error) => {
347
+ settle(() => {
348
+ reject(mapProxyError(error));
349
+ });
350
+ };
351
+ const onAbort = () => {
352
+ socket.destroy();
353
+ settle(() => {
354
+ reject(PROXY_UNREACHABLE_ERROR);
355
+ });
356
+ };
357
+ let removeAbort = () => undefined;
358
+ const cleanup = () => {
359
+ socket.off("error", onError);
360
+ removeAbort();
361
+ };
362
+ const socket = proxy.protocol === "https:"
363
+ ? tls.connect({ host, port, servername: tlsServerName(host), ca: [...ca] }, onConnect)
364
+ : netConnect({ host, port }, onConnect);
365
+ socket.once("error", onError);
366
+ if (signal !== undefined) {
367
+ removeAbort = attachAbortGuard(signal, onAbort);
368
+ }
369
+ });
370
+ }
371
+ function readConnectHeader(socket, signal) {
372
+ return new Promise((resolve, reject) => {
373
+ const chunks = [];
374
+ let settled = false;
375
+ let removeAbort = () => undefined;
376
+ const settle = (fn) => {
377
+ if (settled)
378
+ return;
379
+ settled = true;
380
+ socket.off("data", onData);
381
+ socket.off("error", onError);
382
+ removeAbort();
383
+ fn();
384
+ };
385
+ const onData = (chunk) => {
386
+ chunks.push(chunk);
387
+ const buffer = Buffer.concat(chunks);
388
+ const headerEnd = buffer.indexOf("\r\n\r\n");
389
+ if (headerEnd === -1)
390
+ return;
391
+ const rest = buffer.subarray(headerEnd + 4);
392
+ if (rest.length > 0)
393
+ socket.unshift(rest);
394
+ settle(() => {
395
+ resolve(buffer.subarray(0, headerEnd).toString("latin1"));
396
+ });
397
+ };
398
+ const onError = (error) => {
399
+ settle(() => {
400
+ reject(mapProxyError(error));
401
+ });
402
+ };
403
+ const onAbort = () => {
404
+ socket.destroy();
405
+ settle(() => {
406
+ reject(PROXY_UNREACHABLE_ERROR);
407
+ });
408
+ };
409
+ socket.on("data", onData);
410
+ socket.once("error", onError);
411
+ if (signal !== undefined) {
412
+ removeAbort = attachAbortGuard(signal, onAbort);
413
+ }
414
+ });
415
+ }
416
+ function connectStatus(header) {
417
+ const match = /^HTTP\/\d(?:\.\d)?\s+(\d{3})/iu.exec(header);
418
+ return match === null ? 0 : Number(match[1]);
419
+ }
420
+ function startTargetTls(target, socket, ca, signal) {
421
+ return new Promise((resolve, reject) => {
422
+ let settled = false;
423
+ const settle = (fn) => {
424
+ if (settled)
425
+ return;
426
+ settled = true;
427
+ cleanup();
428
+ fn();
429
+ };
430
+ const onError = (error) => {
431
+ settle(() => {
432
+ reject(mapProxyError(error));
433
+ });
434
+ };
435
+ const onAbort = () => {
436
+ tlsSocket.destroy();
437
+ settle(() => {
438
+ reject(new DOMException("The operation was aborted.", "AbortError"));
439
+ });
440
+ };
441
+ const cleanup = () => {
442
+ tlsSocket.off("error", onError);
443
+ signal?.removeEventListener("abort", onAbort);
444
+ };
445
+ const tlsSocket = tls.connect({ socket, servername: tlsServerName(target.hostname), ca: [...ca] }, () => {
446
+ settle(() => {
447
+ resolve(tlsSocket);
448
+ });
449
+ });
450
+ tlsSocket.once("error", onError);
451
+ tlsSocket.resume();
452
+ if (signal !== undefined) {
453
+ if (signal.aborted) {
454
+ onAbort();
455
+ return;
456
+ }
457
+ signal.addEventListener("abort", onAbort, { once: true });
458
+ }
459
+ });
460
+ }
461
+ async function createTlsTunnel(target, proxy, ca, signal) {
462
+ const socket = await openProxySocket(proxy, ca, signal);
463
+ const authority = `${target.hostname}:${targetPort(target)}`;
464
+ socket.write(`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n\r\n`);
465
+ const status = connectStatus(await readConnectHeader(socket, signal));
466
+ if (status === 407) {
467
+ socket.destroy();
468
+ throw new OutboundHttpEgressError("PROXY_AUTH_REQUIRED", "The configured proxy requires authentication.");
469
+ }
470
+ if (status < 200 || status >= 300) {
471
+ socket.destroy();
472
+ throw new OutboundHttpEgressError(status === 403 ? "PROXY_BLOCKED_BY_POLICY" : "PROXY_EGRESS_FAILED", "The configured proxy rejected outbound egress.");
473
+ }
474
+ socket.resume();
475
+ return startTargetTls(target, socket, ca, signal);
476
+ }
477
+ function responseFromClientRequest(start) {
478
+ return new Promise((resolve, reject) => {
479
+ start(resolve, reject);
480
+ });
481
+ }
482
+ function fetchHttpViaProxy(target, init, proxy, ca, maxResponseBytes) {
483
+ const body = bodyToString(init.body);
484
+ const headers = headersToRecord(init.headers);
485
+ if (hasForwardedCredentialHeader(headers)) {
486
+ throw new OutboundHttpEgressError("PROXY_BLOCKED_BY_POLICY", "Refusing to forward credential headers to a plaintext HTTP target through the configured proxy.");
487
+ }
488
+ // Ensure Host header omits the default port (fixes SigV4 pre-signed S3 URLs).
489
+ if (!Object.prototype.hasOwnProperty.call(headers, "host")) {
490
+ headers.host = hostHeader(target);
491
+ }
492
+ const request = proxy.protocol === "https:" ? httpsRequest : httpRequest;
493
+ const cap = maxResponseBytes ?? MAX_RESPONSE_BYTES;
494
+ return responseFromClientRequest((resolve, reject) => {
495
+ const req = request({
496
+ protocol: proxy.protocol,
497
+ hostname: proxy.hostname,
498
+ port: proxyPort(proxy),
499
+ method: init.method ?? "GET",
500
+ path: target.href,
501
+ headers,
502
+ ca: proxy.protocol === "https:" ? [...ca] : undefined,
503
+ signal: init.signal ?? undefined,
504
+ }, (res) => {
505
+ resolve(streamingResponseFromNode(res, () => {
506
+ req.destroy();
507
+ }, cap));
508
+ });
509
+ req.on("error", (error) => {
510
+ reject(mapProxyError(error));
511
+ });
512
+ req.end(body);
513
+ });
514
+ }
515
+ async function fetchHttpsViaProxy(target, init, proxy, ca, maxResponseBytes) {
516
+ const body = bodyToString(init.body);
517
+ const headers = headersToRecord(init.headers);
518
+ if (!Object.prototype.hasOwnProperty.call(headers, "connection")) {
519
+ headers.connection = "close";
520
+ }
521
+ // Ensure Host header omits :443 so it matches what undici sends directly and
522
+ // satisfies SigV4 pre-signed S3 URLs behind a proxy.
523
+ if (!Object.prototype.hasOwnProperty.call(headers, "host")) {
524
+ headers.host = hostHeader(target);
525
+ }
526
+ const socket = await createTlsTunnel(target, proxy, ca, init.signal ?? undefined);
527
+ const cap = maxResponseBytes ?? MAX_RESPONSE_BYTES;
528
+ return responseFromClientRequest((resolve, reject) => {
529
+ const req = httpRequest({
530
+ method: init.method ?? "GET",
531
+ hostname: target.hostname,
532
+ port: Number(targetPort(target)),
533
+ path: `${target.pathname}${target.search}`,
534
+ headers,
535
+ signal: init.signal ?? undefined,
536
+ createConnection: () => socket,
537
+ }, (res) => {
538
+ resolve(streamingResponseFromNode(res, () => {
539
+ req.destroy();
540
+ }, cap));
541
+ });
542
+ req.on("error", (error) => {
543
+ reject(mapProxyError(error));
544
+ });
545
+ req.end(body);
546
+ });
547
+ }
548
+ function fetchViaProxy(target, init, proxyRaw, egress, maxResponseBytes) {
549
+ const proxy = parseProxyUrl(proxyRaw);
550
+ const ca = gatewayTrustedCaCertificates(egress?.caBundlePath);
551
+ return target.protocol === "https:"
552
+ ? fetchHttpsViaProxy(target, init, proxy, ca, maxResponseBytes)
553
+ : fetchHttpViaProxy(target, init, proxy, ca, maxResponseBytes);
554
+ }
555
+ // Extracted from gatewayFetch to keep its cyclomatic complexity within the limit.
556
+ async function fetchDirectWithCaFallback(url, init, doFetch, useCaFallback, egress, maxResponseBytes) {
557
+ try {
558
+ return await doFetch(url, init);
559
+ }
560
+ catch (error) {
561
+ if (useCaFallback && usesHttps(url) && isRecoverableTlsTrustError(error)) {
562
+ try {
563
+ return await fetchWithCaBundle(url, init, egress, maxResponseBytes);
564
+ }
565
+ catch (fallbackError) {
566
+ if (isRecoverableTlsTrustError(fallbackError)) {
567
+ throw tlsCaFailureError();
568
+ }
569
+ throw fallbackError;
570
+ }
571
+ }
572
+ if (usesHttps(url) && isRecoverableTlsTrustError(error)) {
573
+ throw tlsCaFailureError();
574
+ }
575
+ throw error;
576
+ }
577
+ }
578
+ export async function gatewayFetch(url, options = {}) {
579
+ const { fetchImpl, useCaFallback = fetchImpl === undefined, egress, timeoutMs, maxResponseBytes, ...rest } = options;
580
+ // Compose caller signal + optional timeout into a single signal for all paths.
581
+ const composedSignal = composeSignal(rest.signal, timeoutMs);
582
+ const init = composedSignal !== undefined ? { ...rest, signal: composedSignal } : rest;
583
+ const doFetch = fetchImpl ?? globalThis.fetch;
584
+ const target = new URL(url);
585
+ const proxy = fetchImpl === undefined ? proxyForTarget(target, egress) : undefined;
586
+ if (proxy !== undefined) {
587
+ return fetchViaProxy(target, init, proxy, egress, maxResponseBytes);
588
+ }
589
+ return fetchDirectWithCaFallback(url, init, doFetch, useCaFallback, egress, maxResponseBytes);
590
+ }
591
+ export async function readJsonCapped(response, maxBytes = MAX_RESPONSE_BYTES) {
592
+ if (response.body === null) {
593
+ return response.json();
594
+ }
595
+ const reader = response.body.getReader();
596
+ const decoder = new TextDecoder();
597
+ const parts = [];
598
+ let total = 0;
599
+ for (;;) {
600
+ const { done, value } = await reader.read();
601
+ if (done)
602
+ break;
603
+ total += value.byteLength;
604
+ if (total > maxBytes) {
605
+ await reader.cancel();
606
+ throw new Error("response body exceeded the size limit");
607
+ }
608
+ parts.push(decoder.decode(value, { stream: true }));
609
+ }
610
+ parts.push(decoder.decode());
611
+ return JSON.parse(parts.join(""));
612
+ }
613
+ // Splits an SSE buffer on newlines, keeping the trailing partial line (no newline yet)
614
+ // for the next read. Returns the complete lines and the leftover remainder so a
615
+ // `data: {...}` payload split across two reads is never parsed half-formed.
616
+ function splitSseBuffer(buffer) {
617
+ const segments = buffer.split("\n");
618
+ const rest = segments.pop() ?? "";
619
+ return { lines: segments, rest };
620
+ }
621
+ function parseSseLine(rawLine) {
622
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
623
+ if (line.length === 0 || !line.startsWith("data:")) {
624
+ return { kind: "skip" };
625
+ }
626
+ const payload = line.slice("data:".length).trimStart();
627
+ if (payload === "[DONE]") {
628
+ return { kind: "done" };
629
+ }
630
+ return { kind: "value", value: JSON.parse(payload) };
631
+ }
632
+ // Reads a Server-Sent-Events response as a stream of parsed JSON `data:` payloads.
633
+ // Incomplete lines are buffered across reads; `data: [DONE]` terminates; cumulative
634
+ // bytes are capped exactly like readJsonCapped. A null body yields nothing.
635
+ export async function* readSseStream(response, maxBytes = MAX_RESPONSE_BYTES) {
636
+ if (response.body === null) {
637
+ return;
638
+ }
639
+ const reader = response.body.getReader();
640
+ const decoder = new TextDecoder();
641
+ let buffer = "";
642
+ let total = 0;
643
+ for (;;) {
644
+ const { done, value } = await reader.read();
645
+ if (done)
646
+ break;
647
+ total += value.byteLength;
648
+ if (total > maxBytes) {
649
+ await reader.cancel();
650
+ throw new Error("response body exceeded the size limit");
651
+ }
652
+ buffer += decoder.decode(value, { stream: true });
653
+ const { lines, rest } = splitSseBuffer(buffer);
654
+ buffer = rest;
655
+ for (const line of lines) {
656
+ const result = parseSseLine(line);
657
+ if (result.kind === "done")
658
+ return;
659
+ if (result.kind === "value")
660
+ yield result.value;
661
+ }
662
+ }
663
+ const tail = parseSseLine(buffer + decoder.decode());
664
+ if (tail.kind === "value")
665
+ yield tail.value;
666
+ }
@@ -0,0 +1,16 @@
1
+ export { KEIKO_MODEL_GATEWAY_VERSION } from "./version.js";
2
+ export type { CircuitBreakerConfig, CircuitBreakerStatus, CircuitState, ChatMessage, Clock, CostClass, FinishReason, GatewayConfig, GatewayRequest, GatewayStreamChunk, InfillingAlignment, LatencyClass, ModelCapability, CompletionInteractionMode, CompletionDegradeReason, CompletionModelSelection, ModelKind, ModelProviderConfig, NormalizedResponse, NormalizedToolCall, OutboundHttpEgressConfig, ProviderAdapter, ResponseFormat, StreamDelta, StreamEvent, ToolDefinition, UsageMetadata, } from "./types.js";
3
+ export { CAPABILITY_REGISTRY, createDefaultChatCapability, explainConversationIneligibility, findCapability, INFILLING_ALIGNMENTS, isAlignedInfillingModel, isAsYouTypeCompletionModel, isConversationEligibleModel, listCapabilities, modelSupportsInfilling, resolveCostClass, selectCheapest, selectCompletionModelFromCapabilities, type CapabilityQuery, type CompletionSelectionOptions, type ConversationIneligibilityReason, } from "./capabilities.js";
4
+ export { CAPABILITY_DATA } from "./capabilities.data.js";
5
+ export { apiKeyHeaderValue, DEFAULT_API_KEY_HEADER_NAME, loadConfigFromFile, loadEgressConfigFromFile, normalizeApiKeyHeaderName, parseGatewayConfig, resolveOutboundHttpEgressConfig, toSafeObject, validateBaseUrl, type EnvSource, type SafeGatewayConfig, type SafeProviderConfig, } from "./config.js";
6
+ export { Gateway, type GatewayDeps } from "./gateway.js";
7
+ export { assertConfiguredModel, findConfiguredCapability, listConfiguredCapabilities, selectCompletionModel, selectConfiguredModel, type ConfiguredCapabilityProvider, type ConfiguredCapabilitySource, type ModelSelectionQuery, } from "./model-selection.js";
8
+ export { assertCompatibleEmbeddingIdentity, verifyEmbeddingCapability, type EmbeddingCapabilityCheck, type EmbeddingFailureReason, type EmbeddingIdentityWarning, type EmbeddingProbeOptions, type OpenAIEmbeddingAdapter, } from "./embedding.js";
9
+ export { requestOpenAIEmbedding, requestOpenAIEmbeddingBatch, type OpenAIEmbeddingBatchOutcome, type OpenAIEmbeddingBatchRequest, type OpenAIEmbeddingErrorKind, type OpenAIEmbeddingOutcome, type OpenAIEmbeddingRequest, type OpenAIEmbeddingSuccess, } from "./openai-embedding-adapter.js";
10
+ export { redact } from "@oscharko-dev/keiko-security";
11
+ export { AuthenticationError, CancelledError, CircuitOpenError, ConfigInvalidError, ContextOverflowError, ERROR_CODES, GatewayEgressError, GatewayError, MalformedToolCallError, ModelRefusalError, ProviderError, RateLimitError, TimeoutError, TransportError, UnknownModelError, type ErrorCode, type GatewayEgressErrorCode, } from "@oscharko-dev/keiko-security/errors/gateway";
12
+ export * as QualityIntelligence from "./qualityIntelligence/index.js";
13
+ export { QualityIntelligenceSafeErrorException, createInMemoryReplayCache, deriveReplayCacheKey, dispatchQualityIntelligenceRequest, isCacheable, type QualityIntelligenceBudgetState, type QualityIntelligenceCancellationHandle, type QualityIntelligenceReplayCachePort, type QualityIntelligenceSafeError, type QualityIntelligenceSafeErrorCode, } from "./qualityIntelligence/index.js";
14
+ export type { QualityIntelligenceDispatcherArgs, QualityIntelligenceDispatcherResult, } from "./qualityIntelligence/dispatcher.js";
15
+ export * as PromptEnhancer from "./promptEnhancer/index.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAE3D,YAAY,EACV,oBAAoB,EACpB,oBAAoB,EACpB,YAAY,EACZ,WAAW,EACX,KAAK,EACL,SAAS,EACT,YAAY,EACZ,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,yBAAyB,EACzB,uBAAuB,EACvB,wBAAwB,EACxB,SAAS,EACT,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,WAAW,EACX,WAAW,EACX,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAC3B,gCAAgC,EAChC,cAAc,EACd,oBAAoB,EACpB,uBAAuB,EACvB,0BAA0B,EAC1B,2BAA2B,EAC3B,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,cAAc,EACd,qCAAqC,EACrC,KAAK,eAAe,EACpB,KAAK,0BAA0B,EAC/B,KAAK,+BAA+B,GACrC,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,kBAAkB,EAClB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,+BAA+B,EAC/B,YAAY,EACZ,eAAe,EACf,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,GACzB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,iCAAiC,EACjC,yBAAyB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;AAEtD,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,KAAK,SAAS,EACd,KAAK,sBAAsB,GAC5B,MAAM,6CAA6C,CAAC;AAKrD,OAAO,KAAK,mBAAmB,MAAM,gCAAgC,CAAC;AAGtE,OAAO,EACL,qCAAqC,EACrC,yBAAyB,EACzB,oBAAoB,EACpB,kCAAkC,EAClC,WAAW,EACX,KAAK,8BAA8B,EACnC,KAAK,qCAAqC,EAC1C,KAAK,kCAAkC,EACvC,KAAK,4BAA4B,EACjC,KAAK,gCAAgC,GACtC,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,iCAAiC,EACjC,mCAAmC,GACpC,MAAM,qCAAqC,CAAC;AAM7C,OAAO,KAAK,cAAc,MAAM,2BAA2B,CAAC"}