@omnicross/core 0.1.2 → 0.1.4
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/LICENSE +21 -21
- package/NOTICE +57 -57
- package/README.md +15 -15
- package/dist/auth/GeminiCodeAssistProjectResolver.cjs +6 -1
- package/dist/auth/GeminiCodeAssistProjectResolver.js +6 -1
- package/dist/chunk-3QFXFCHG.js +33 -0
- package/dist/chunk-4IH4EL7M.cjs +9 -0
- package/dist/chunk-4YINUUKQ.cjs +33 -0
- package/dist/chunk-5ENKBSWO.js +311 -0
- package/dist/chunk-6RPZADX3.cjs +92 -0
- package/dist/chunk-7Y47UCF2.js +142 -0
- package/dist/chunk-AEOZIDEB.cjs +32 -0
- package/dist/chunk-APYG5QD7.cjs +50 -0
- package/dist/{chunk-MNYKI4CI.js → chunk-BPKCU575.js} +29 -0
- package/dist/{chunk-SN3YWBX7.cjs → chunk-DKPXN34V.cjs} +2108 -538
- package/dist/{chunk-4NBS6KPV.cjs → chunk-DQ5VAYZ6.cjs} +7 -8
- package/dist/chunk-E3UCVHLP.cjs +142 -0
- package/dist/chunk-FCR77GYM.cjs +33 -0
- package/dist/{chunk-74TMJA7Z.js → chunk-HRRYG2AX.js} +8 -4
- package/dist/chunk-HUZZZ3JL.js +32 -0
- package/dist/{chunk-KUU2RNG6.cjs → chunk-HYN75H6G.cjs} +3 -0
- package/dist/{chunk-QOCNX236.js → chunk-IAXWICZU.js} +24 -1
- package/dist/{chunk-JO5NLTLY.js → chunk-IK3KE3AU.js} +3 -0
- package/dist/{chunk-V5KPWNYX.cjs → chunk-JMJBSACD.cjs} +3 -3
- package/dist/chunk-K5NM7VAH.js +33 -0
- package/dist/{chunk-N3V2J5ZO.cjs → chunk-KFI44N2R.cjs} +8 -4
- package/dist/chunk-L74VDN4A.cjs +32 -0
- package/dist/chunk-OCEUVQHW.cjs +112 -0
- package/dist/chunk-OE6TDIWW.js +22 -0
- package/dist/chunk-OZFM4X3S.js +18 -0
- package/dist/{chunk-LKZJEL6E.cjs → chunk-Q27JY5RG.cjs} +6 -4
- package/dist/{chunk-SPVWWUHX.cjs → chunk-QWBWGODS.cjs} +24 -1
- package/dist/{chunk-2FEVTJWG.js → chunk-QYTT6GE3.js} +1929 -359
- package/dist/{chunk-UYPEN5XE.cjs → chunk-RGTR7CIO.cjs} +6 -6
- package/dist/chunk-RPII3E6Z.cjs +145 -0
- package/dist/{chunk-XBSYYZIY.cjs → chunk-SNYBEXLB.cjs} +31 -2
- package/dist/{chunk-2DCNB7DF.cjs → chunk-SU2GCLAC.cjs} +30 -10
- package/dist/{chunk-5HTVET6E.js → chunk-TIER4SUC.js} +24 -4
- package/dist/chunk-TKJMZBAH.js +32 -0
- package/dist/{chunk-ZSVQT3PW.js → chunk-UBWEBKRC.js} +5 -6
- package/dist/chunk-UHGVBFU3.js +385 -0
- package/dist/chunk-UIBD2YY5.cjs +22 -0
- package/dist/chunk-UNEFIWXI.cjs +32 -0
- package/dist/chunk-VACWOIRE.cjs +385 -0
- package/dist/{chunk-O466Y272.js → chunk-VPBAUQ2M.js} +7 -7
- package/dist/chunk-VQY5W4YW.cjs +311 -0
- package/dist/chunk-WLYIIEAF.js +50 -0
- package/dist/chunk-WNY4WMRF.js +32 -0
- package/dist/chunk-XEKD23J5.js +9 -0
- package/dist/chunk-XK5CIRMQ.js +112 -0
- package/dist/chunk-XSVNC2UD.cjs +18 -0
- package/dist/chunk-XTEI64OU.js +145 -0
- package/dist/chunk-XX6NQJMA.js +92 -0
- package/dist/{chunk-745DV5FL.js → chunk-Y7FO65VT.js} +3 -3
- package/dist/{chunk-PXUJF5HS.js → chunk-ZY6KU6P3.js} +4 -2
- package/dist/completion/CompletionService.cjs +30 -17
- package/dist/completion/CompletionService.js +29 -16
- package/dist/completion/NativeSearchInjector.cjs +3 -3
- package/dist/completion/NativeSearchInjector.js +2 -2
- package/dist/completion/openrouter-headers.cjs +3 -6
- package/dist/completion/openrouter-headers.d.cts +9 -12
- package/dist/completion/openrouter-headers.d.ts +9 -12
- package/dist/completion/openrouter-headers.js +2 -5
- package/dist/completion/openrouter-models.cjs +4 -3
- package/dist/completion/openrouter-models.js +2 -1
- package/dist/completion.cjs +30 -17
- package/dist/completion.js +29 -16
- package/dist/index.cjs +151 -20
- package/dist/index.d.cts +44 -10
- package/dist/index.d.ts +44 -10
- package/dist/index.js +160 -29
- package/dist/keyPolicy-Cp0FntZl.d.cts +90 -0
- package/dist/keyPolicy-Cp0FntZl.d.ts +90 -0
- package/dist/outbound-api/auditCapture.cjs +9 -0
- package/dist/outbound-api/auditCapture.d.cts +52 -0
- package/dist/outbound-api/auditCapture.d.ts +52 -0
- package/dist/outbound-api/auditCapture.js +9 -0
- package/dist/outbound-api/auditRedact.cjs +8 -0
- package/dist/outbound-api/auditRedact.d.cts +30 -0
- package/dist/outbound-api/auditRedact.d.ts +30 -0
- package/dist/outbound-api/auditRedact.js +8 -0
- package/dist/outbound-api/billingCapture.cjs +8 -0
- package/dist/outbound-api/billingCapture.d.cts +61 -0
- package/dist/outbound-api/billingCapture.d.ts +61 -0
- package/dist/outbound-api/billingCapture.js +8 -0
- package/dist/outbound-api/quotaWarn.cjs +12 -0
- package/dist/outbound-api/quotaWarn.d.cts +43 -0
- package/dist/outbound-api/quotaWarn.d.ts +43 -0
- package/dist/outbound-api/quotaWarn.js +12 -0
- package/dist/outbound-api/routeResolver.cjs +5 -2
- package/dist/outbound-api/routeResolver.d.cts +9 -2
- package/dist/outbound-api/routeResolver.d.ts +9 -2
- package/dist/outbound-api/routeResolver.js +4 -1
- package/dist/outbound-api/types.cjs +6 -1
- package/dist/outbound-api/types.d.cts +12 -144
- package/dist/outbound-api/types.d.ts +12 -144
- package/dist/outbound-api/types.js +6 -0
- package/dist/outbound-api.cjs +144 -17
- package/dist/outbound-api.d.cts +655 -25
- package/dist/outbound-api.d.ts +655 -25
- package/dist/outbound-api.js +148 -21
- package/dist/pipeline/AuthSource.d.cts +29 -1
- package/dist/pipeline/AuthSource.d.ts +29 -1
- package/dist/pipeline/LlmConfigProviderAuth.cjs +3 -3
- package/dist/pipeline/LlmConfigProviderAuth.js +2 -2
- package/dist/pipeline/SubscriptionAccountHealth.cjs +22 -0
- package/dist/pipeline/SubscriptionAccountHealth.d.cts +235 -0
- package/dist/pipeline/SubscriptionAccountHealth.d.ts +235 -0
- package/dist/pipeline/SubscriptionAccountHealth.js +22 -0
- package/dist/pipeline/SubscriptionAuthSource.cjs +2 -2
- package/dist/pipeline/SubscriptionAuthSource.d.cts +3 -2
- package/dist/pipeline/SubscriptionAuthSource.d.ts +3 -2
- package/dist/pipeline/SubscriptionAuthSource.js +1 -1
- package/dist/pipeline/SubscriptionAuthStrategy.d.cts +30 -1
- package/dist/pipeline/SubscriptionAuthStrategy.d.ts +30 -1
- package/dist/pipeline/auditSink.cjs +14 -0
- package/dist/pipeline/auditSink.d.cts +48 -0
- package/dist/pipeline/auditSink.d.ts +48 -0
- package/dist/pipeline/auditSink.js +14 -0
- package/dist/pipeline/auditUsageStash.cjs +10 -0
- package/dist/pipeline/auditUsageStash.d.cts +37 -0
- package/dist/pipeline/auditUsageStash.d.ts +37 -0
- package/dist/pipeline/auditUsageStash.js +10 -0
- package/dist/pipeline/billingEmit.cjs +14 -0
- package/dist/pipeline/billingEmit.d.cts +49 -0
- package/dist/pipeline/billingEmit.d.ts +49 -0
- package/dist/pipeline/billingEmit.js +14 -0
- package/dist/pipeline/executeProviderCall.cjs +2 -2
- package/dist/pipeline/executeProviderCall.js +1 -1
- package/dist/pipeline/upstreamFetch.cjs +14 -0
- package/dist/pipeline/upstreamFetch.d.cts +83 -0
- package/dist/pipeline/upstreamFetch.d.ts +83 -0
- package/dist/pipeline/upstreamFetch.js +14 -0
- package/dist/pipeline/webhookEmit.cjs +10 -0
- package/dist/pipeline/webhookEmit.d.cts +36 -0
- package/dist/pipeline/webhookEmit.d.ts +36 -0
- package/dist/pipeline/webhookEmit.js +10 -0
- package/dist/ports/usage-event-store.d.cts +26 -1
- package/dist/ports/usage-event-store.d.ts +26 -1
- package/dist/ports.d.cts +8 -1
- package/dist/ports.d.ts +8 -1
- package/dist/provider-proxy/ProviderProxy.cjs +30 -17
- package/dist/provider-proxy/ProviderProxy.js +29 -16
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.cjs +15 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.cts +121 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.ts +121 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.js +15 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.cjs +20 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.d.cts +89 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.d.ts +89 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.js +20 -0
- package/dist/provider-proxy/ingress/providerProxyShared.cjs +30 -17
- package/dist/provider-proxy/ingress/providerProxyShared.d.cts +11 -2
- package/dist/provider-proxy/ingress/providerProxyShared.d.ts +11 -2
- package/dist/provider-proxy/ingress/providerProxyShared.js +29 -16
- package/dist/provider-proxy/matchText.cjs +4 -2
- package/dist/provider-proxy/matchText.d.cts +14 -1
- package/dist/provider-proxy/matchText.d.ts +14 -1
- package/dist/provider-proxy/matchText.js +3 -1
- package/dist/provider-proxy/types.d.cts +30 -0
- package/dist/provider-proxy/types.d.ts +30 -0
- package/dist/provider-proxy.cjs +30 -17
- package/dist/provider-proxy.js +29 -16
- package/dist/{routeResolver-HE-ZO0fO.d.ts → routeResolver-C4T7yClx.d.ts} +36 -10
- package/dist/{routeResolver-BrbK6ja9.d.cts → routeResolver-CB3tvJKy.d.cts} +36 -10
- package/dist/transformer/transformers/OpenAIResponseTransformer.cjs +2 -2
- package/dist/transformer/transformers/OpenAIResponseTransformer.js +1 -1
- package/dist/transformer/transformers.cjs +7 -7
- package/dist/transformer/transformers.js +8 -8
- package/dist/transformer.cjs +9 -9
- package/dist/transformer.js +8 -8
- package/dist/types-B8Arpxn0.d.cts +754 -0
- package/dist/types-lgPYQuZS.d.ts +754 -0
- package/dist/usage/usage-recorder.cjs +3 -2
- package/dist/usage/usage-recorder.d.cts +19 -1
- package/dist/usage/usage-recorder.d.ts +19 -1
- package/dist/usage/usage-recorder.js +2 -1
- package/dist/usage.cjs +3 -2
- package/dist/usage.js +2 -1
- package/package.json +64 -62
- package/dist/chunk-3MEACFK3.js +0 -193
- package/dist/chunk-6VIXXLMX.cjs +0 -14
- package/dist/chunk-E3WHL7CO.js +0 -14
- package/dist/chunk-G2FUJNA2.cjs +0 -193
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collectMatchText
|
|
3
|
-
} from "./chunk-QOCNX236.js";
|
|
4
1
|
import {
|
|
5
2
|
streamSSEResponse
|
|
6
3
|
} from "./chunk-VVEHS2LI.js";
|
|
4
|
+
import {
|
|
5
|
+
AnthropicTransformer
|
|
6
|
+
} from "./chunk-FTQ7IQUC.js";
|
|
7
|
+
import {
|
|
8
|
+
GeminiTransformer
|
|
9
|
+
} from "./chunk-C7PSERMH.js";
|
|
10
|
+
import {
|
|
11
|
+
OpenAIResponseTransformer
|
|
12
|
+
} from "./chunk-TIER4SUC.js";
|
|
7
13
|
import {
|
|
8
14
|
TransformerChainExecutor
|
|
9
15
|
} from "./chunk-G2UTNSGU.js";
|
|
@@ -11,58 +17,87 @@ import {
|
|
|
11
17
|
injectExtendedContextBeta
|
|
12
18
|
} from "./chunk-MZNPGW5Q.js";
|
|
13
19
|
import {
|
|
14
|
-
|
|
15
|
-
} from "./chunk-
|
|
20
|
+
getSharedIdentityStore
|
|
21
|
+
} from "./chunk-XTEI64OU.js";
|
|
16
22
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
applyFingerprint,
|
|
24
|
+
captureCallerIdentity
|
|
25
|
+
} from "./chunk-XX6NQJMA.js";
|
|
19
26
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
collectMatchText,
|
|
28
|
+
deriveSubscriptionSessionKey
|
|
29
|
+
} from "./chunk-IAXWICZU.js";
|
|
30
|
+
import {
|
|
31
|
+
emitWebhookEvent
|
|
32
|
+
} from "./chunk-OE6TDIWW.js";
|
|
22
33
|
import {
|
|
23
34
|
getGeminiCodeAssistResolver
|
|
24
35
|
} from "./chunk-DRWHTIIZ.js";
|
|
25
36
|
import {
|
|
26
37
|
executeProviderCall
|
|
27
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-IK3KE3AU.js";
|
|
28
39
|
import {
|
|
29
40
|
LlmConfigProviderAuth,
|
|
30
41
|
getProviderHeaders
|
|
31
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-ZY6KU6P3.js";
|
|
32
43
|
import {
|
|
33
44
|
resolveProviderChain
|
|
34
45
|
} from "./chunk-JWC6APFQ.js";
|
|
35
46
|
import {
|
|
36
47
|
resolveSubscriptionChain
|
|
37
48
|
} from "./chunk-5ERIZW3D.js";
|
|
49
|
+
import {
|
|
50
|
+
getSharedAccountHealth,
|
|
51
|
+
resolveResetSeconds
|
|
52
|
+
} from "./chunk-5ENKBSWO.js";
|
|
38
53
|
import {
|
|
39
54
|
SubscriptionAuthSource
|
|
40
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-HRRYG2AX.js";
|
|
41
56
|
import {
|
|
42
57
|
serializeError
|
|
43
58
|
} from "./chunk-AVYJ4YBN.js";
|
|
59
|
+
import {
|
|
60
|
+
beginBillingCapture
|
|
61
|
+
} from "./chunk-WLYIIEAF.js";
|
|
62
|
+
import {
|
|
63
|
+
computeQuotaWarnings,
|
|
64
|
+
markQuotaWarnedOnce
|
|
65
|
+
} from "./chunk-3QFXFCHG.js";
|
|
66
|
+
import {
|
|
67
|
+
detectRequestRole,
|
|
68
|
+
endpointToIngressFormat,
|
|
69
|
+
extractRequestedModel,
|
|
70
|
+
isKindMappedEndpoint,
|
|
71
|
+
modelKindsForEndpoint,
|
|
72
|
+
parseModelRef,
|
|
73
|
+
resolveRoute,
|
|
74
|
+
validateServerModelConfig
|
|
75
|
+
} from "./chunk-UHGVBFU3.js";
|
|
44
76
|
import {
|
|
45
77
|
buildProviderApiUrl,
|
|
46
78
|
resolveApiFormat,
|
|
47
79
|
resolveProviderEndpoint
|
|
48
80
|
} from "./chunk-H5JUT3KV.js";
|
|
49
81
|
import {
|
|
50
|
-
|
|
51
|
-
} from "./chunk-
|
|
82
|
+
beginAuditCapture
|
|
83
|
+
} from "./chunk-7Y47UCF2.js";
|
|
52
84
|
import {
|
|
53
85
|
convertAnthropicToOpenAI,
|
|
54
86
|
convertOpenAIToAnthropic
|
|
55
87
|
} from "./chunk-5ZQBEOMD.js";
|
|
56
88
|
import {
|
|
57
|
-
|
|
58
|
-
} from "./chunk-
|
|
89
|
+
fetchUpstream
|
|
90
|
+
} from "./chunk-XK5CIRMQ.js";
|
|
59
91
|
import {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} from "./chunk-E3WHL7CO.js";
|
|
92
|
+
applyAugmentation
|
|
93
|
+
} from "./chunk-Y7FO65VT.js";
|
|
63
94
|
import {
|
|
64
95
|
NATIVE_SEARCH_TOOL_NAMES
|
|
65
96
|
} from "./chunk-FMOBD2OQ.js";
|
|
97
|
+
import {
|
|
98
|
+
OPENROUTER_APP_HEADERS,
|
|
99
|
+
isOpenRouterProvider
|
|
100
|
+
} from "./chunk-HUZZZ3JL.js";
|
|
66
101
|
|
|
67
102
|
// src/completion/StreamEventBuffer.ts
|
|
68
103
|
var TERMINAL_TYPES = /* @__PURE__ */ new Set(["done", "error", "abort"]);
|
|
@@ -462,10 +497,11 @@ function recordAnthropicNonStreamUsage(recorder, bodyText, attribution) {
|
|
|
462
497
|
sessionId: attribution.sessionId,
|
|
463
498
|
providerId: attribution.providerId,
|
|
464
499
|
model: attribution.model,
|
|
465
|
-
apiKeyId:
|
|
500
|
+
apiKeyId: attribution.apiKeyId,
|
|
466
501
|
engineOrigin: "codex-ingress",
|
|
467
502
|
usage: tapped,
|
|
468
|
-
rawUsage: parsed.usage
|
|
503
|
+
rawUsage: parsed.usage,
|
|
504
|
+
auditResponse: attribution.auditResponse
|
|
469
505
|
});
|
|
470
506
|
} catch {
|
|
471
507
|
}
|
|
@@ -502,7 +538,84 @@ function readBody(req) {
|
|
|
502
538
|
req.on("error", reject);
|
|
503
539
|
});
|
|
504
540
|
}
|
|
505
|
-
|
|
541
|
+
function rewriteJsonModel(bodyText, rewriteModel) {
|
|
542
|
+
try {
|
|
543
|
+
const obj = JSON.parse(bodyText);
|
|
544
|
+
if (!obj || typeof obj !== "object") return bodyText;
|
|
545
|
+
let changed = false;
|
|
546
|
+
if (typeof obj.model === "string") {
|
|
547
|
+
obj.model = rewriteModel;
|
|
548
|
+
changed = true;
|
|
549
|
+
}
|
|
550
|
+
const nested = obj.response;
|
|
551
|
+
if (nested && typeof nested === "object" && typeof nested.model === "string") {
|
|
552
|
+
nested.model = rewriteModel;
|
|
553
|
+
changed = true;
|
|
554
|
+
}
|
|
555
|
+
return changed ? JSON.stringify(obj) : bodyText;
|
|
556
|
+
} catch {
|
|
557
|
+
return bodyText;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function rewriteSseLine(line, rewriteModel) {
|
|
561
|
+
if (!line.startsWith("data:") || !line.includes('"model"')) return line;
|
|
562
|
+
const nl = /\r?\n$/.exec(line);
|
|
563
|
+
const newline = nl ? nl[0] : "";
|
|
564
|
+
const withoutNl = newline ? line.slice(0, line.length - newline.length) : line;
|
|
565
|
+
const afterData = withoutNl.slice("data:".length);
|
|
566
|
+
const leadingSpace = afterData.startsWith(" ") ? " " : "";
|
|
567
|
+
const payload = afterData.slice(leadingSpace.length);
|
|
568
|
+
if (payload === "" || payload === "[DONE]") return line;
|
|
569
|
+
try {
|
|
570
|
+
const obj = JSON.parse(payload);
|
|
571
|
+
if (!obj || typeof obj !== "object") return line;
|
|
572
|
+
const type = typeof obj.type === "string" ? obj.type : "";
|
|
573
|
+
let changed = false;
|
|
574
|
+
if (type === "message_start") {
|
|
575
|
+
const message = obj.message;
|
|
576
|
+
if (message && typeof message === "object" && typeof message.model === "string") {
|
|
577
|
+
message.model = rewriteModel;
|
|
578
|
+
changed = true;
|
|
579
|
+
}
|
|
580
|
+
} else if (type === "response.created" || type === "response.in_progress" || type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
|
|
581
|
+
const response = obj.response;
|
|
582
|
+
if (response && typeof response === "object" && typeof response.model === "string") {
|
|
583
|
+
response.model = rewriteModel;
|
|
584
|
+
changed = true;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (!changed) return line;
|
|
588
|
+
return `data:${leadingSpace}${JSON.stringify(obj)}${newline}`;
|
|
589
|
+
} catch {
|
|
590
|
+
return line;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function relayStreamWithModelRewrite(res, body, rewriteModel) {
|
|
594
|
+
const reader = body.getReader();
|
|
595
|
+
const decoder = new TextDecoder();
|
|
596
|
+
let buffer = "";
|
|
597
|
+
try {
|
|
598
|
+
for (; ; ) {
|
|
599
|
+
const { done, value } = await reader.read();
|
|
600
|
+
if (done) break;
|
|
601
|
+
buffer += decoder.decode(value, { stream: true });
|
|
602
|
+
let out = "";
|
|
603
|
+
let nlIndex;
|
|
604
|
+
while ((nlIndex = buffer.indexOf("\n")) !== -1) {
|
|
605
|
+
const linePart = buffer.slice(0, nlIndex + 1);
|
|
606
|
+
buffer = buffer.slice(nlIndex + 1);
|
|
607
|
+
out += rewriteSseLine(linePart, rewriteModel);
|
|
608
|
+
}
|
|
609
|
+
if (out) res.write(out);
|
|
610
|
+
}
|
|
611
|
+
const tail = buffer + decoder.decode();
|
|
612
|
+
if (tail) res.write(rewriteSseLine(tail, rewriteModel));
|
|
613
|
+
} finally {
|
|
614
|
+
reader.releaseLock();
|
|
615
|
+
res.end();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function relayResponse(res, providerResponse, isStream, rewriteModel) {
|
|
506
619
|
const contentType = providerResponse.headers.get("Content-Type") ?? "";
|
|
507
620
|
const status = providerResponse.status && providerResponse.status >= 100 ? providerResponse.status : 200;
|
|
508
621
|
if (isStream || contentType.includes("text/event-stream")) {
|
|
@@ -515,6 +628,10 @@ async function relayResponse(res, providerResponse, isStream) {
|
|
|
515
628
|
res.end();
|
|
516
629
|
return null;
|
|
517
630
|
}
|
|
631
|
+
if (rewriteModel) {
|
|
632
|
+
await relayStreamWithModelRewrite(res, providerResponse.body, rewriteModel);
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
518
635
|
const reader = providerResponse.body.getReader();
|
|
519
636
|
try {
|
|
520
637
|
for (; ; ) {
|
|
@@ -529,8 +646,9 @@ async function relayResponse(res, providerResponse, isStream) {
|
|
|
529
646
|
return null;
|
|
530
647
|
}
|
|
531
648
|
const bodyText = await providerResponse.text();
|
|
649
|
+
const clientText = rewriteModel ? rewriteJsonModel(bodyText, rewriteModel) : bodyText;
|
|
532
650
|
res.writeHead(status, { "Content-Type": contentType.includes("json") ? contentType : "application/json" });
|
|
533
|
-
res.end(
|
|
651
|
+
res.end(clientText);
|
|
534
652
|
return bodyText;
|
|
535
653
|
}
|
|
536
654
|
function writeError(res, status, message) {
|
|
@@ -568,14 +686,15 @@ async function buildSubscriptionPlan(res, route, deps, anthropicBody, routeModel
|
|
|
568
686
|
resolvedModel = mapped.resolvedModel;
|
|
569
687
|
scenario = mapped.scenario;
|
|
570
688
|
}
|
|
571
|
-
const
|
|
689
|
+
const sessionKey = deriveSubscriptionSessionKey(anthropicBody);
|
|
690
|
+
const plan = buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario, sessionKey);
|
|
572
691
|
if (!plan) {
|
|
573
692
|
writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
|
|
574
693
|
return null;
|
|
575
694
|
}
|
|
576
695
|
return plan;
|
|
577
696
|
}
|
|
578
|
-
function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario) {
|
|
697
|
+
function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario, sessionKey) {
|
|
579
698
|
const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel, route.subscriptionConfig);
|
|
580
699
|
if (!upstreamUrl) return null;
|
|
581
700
|
const auth = route.auth ?? new SubscriptionAuthSource(profile);
|
|
@@ -598,6 +717,7 @@ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isS
|
|
|
598
717
|
};
|
|
599
718
|
return {
|
|
600
719
|
auth,
|
|
720
|
+
preferredAccountId: route.preferredAccountId,
|
|
601
721
|
chain,
|
|
602
722
|
transformerProvider,
|
|
603
723
|
resolvedModel,
|
|
@@ -622,7 +742,8 @@ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isS
|
|
|
622
742
|
upstreamUrl,
|
|
623
743
|
sameFormat,
|
|
624
744
|
isSubscription: true,
|
|
625
|
-
scenario
|
|
745
|
+
scenario,
|
|
746
|
+
sessionKey
|
|
626
747
|
};
|
|
627
748
|
}
|
|
628
749
|
function usesResponsesChain(names) {
|
|
@@ -657,12 +778,25 @@ function collectText(value) {
|
|
|
657
778
|
}
|
|
658
779
|
return 0;
|
|
659
780
|
}
|
|
660
|
-
|
|
781
|
+
function proxyProviderId(plan) {
|
|
782
|
+
return plan.isSubscription ? plan.transformerProvider.name : "byo";
|
|
783
|
+
}
|
|
784
|
+
async function runPipeline(anthropicBody, plan, reportSelection) {
|
|
661
785
|
const executor = getSharedExecutor();
|
|
662
786
|
const endpointTransformer = getAnthropicEndpointTransformer();
|
|
663
787
|
const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
|
|
788
|
+
let proxyAccountId;
|
|
664
789
|
const authHeaders = {};
|
|
665
|
-
await auth.applyHeaders(authHeaders, {
|
|
790
|
+
await auth.applyHeaders(authHeaders, {
|
|
791
|
+
upstreamUrl,
|
|
792
|
+
model: resolvedModel,
|
|
793
|
+
sessionKey: plan.sessionKey,
|
|
794
|
+
preferredAccountId: plan.preferredAccountId,
|
|
795
|
+
reportSelection: (accountId, isActive) => {
|
|
796
|
+
proxyAccountId = accountId;
|
|
797
|
+
reportSelection?.(accountId, isActive);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
666
800
|
let rawStatus = null;
|
|
667
801
|
const { response } = await executeProviderCall({
|
|
668
802
|
executor,
|
|
@@ -682,7 +816,11 @@ async function runPipeline(anthropicBody, plan) {
|
|
|
682
816
|
},
|
|
683
817
|
fetchFn: (url, headers, body) => {
|
|
684
818
|
console.info(`[ProviderProxy:anthropic] -> ${url} model=${resolvedModel} stream=${isStream}`);
|
|
685
|
-
return
|
|
819
|
+
return fetchUpstream(
|
|
820
|
+
url,
|
|
821
|
+
{ method: "POST", headers, body: JSON.stringify(body) },
|
|
822
|
+
{ providerId: proxyProviderId(plan), accountId: proxyAccountId }
|
|
823
|
+
).then((r) => {
|
|
686
824
|
rawStatus = r.status;
|
|
687
825
|
return r;
|
|
688
826
|
});
|
|
@@ -691,29 +829,96 @@ async function runPipeline(anthropicBody, plan) {
|
|
|
691
829
|
});
|
|
692
830
|
return { response, rawStatus };
|
|
693
831
|
}
|
|
694
|
-
async function runSubscriptionSameFormatFetch(rawBody, plan) {
|
|
832
|
+
async function runSubscriptionSameFormatFetch(rawBody, plan, reportSelection, options = {}) {
|
|
833
|
+
let proxyAccountId;
|
|
834
|
+
let remappedModel;
|
|
695
835
|
const headers = { "content-type": "application/json" };
|
|
696
836
|
await plan.auth.applyHeaders(headers, {
|
|
697
837
|
upstreamUrl: plan.upstreamUrl,
|
|
698
|
-
model: plan.resolvedModel
|
|
838
|
+
model: plan.resolvedModel,
|
|
839
|
+
sessionKey: plan.sessionKey,
|
|
840
|
+
preferredAccountId: plan.preferredAccountId,
|
|
841
|
+
reportSelection: (accountId, isActive, remapped) => {
|
|
842
|
+
proxyAccountId = accountId;
|
|
843
|
+
remappedModel = remapped;
|
|
844
|
+
reportSelection?.(accountId, isActive, remapped);
|
|
845
|
+
}
|
|
699
846
|
});
|
|
847
|
+
if (plan.isSubscription && plan.transformerProvider.name === "claude") {
|
|
848
|
+
applyFingerprint(
|
|
849
|
+
getSharedIdentityStore(),
|
|
850
|
+
headers,
|
|
851
|
+
plan.transformerProvider.name,
|
|
852
|
+
proxyAccountId,
|
|
853
|
+
options.callerIdentity
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
const applyRemap = remappedModel !== void 0 && outboundRemapApplies(plan);
|
|
857
|
+
const outboundBody = applyRemap ? rewriteBodyModel(rawBody, remappedModel) : rawBody;
|
|
858
|
+
const outboundModel = applyRemap ? remappedModel : plan.resolvedModel;
|
|
700
859
|
console.info(
|
|
701
|
-
`[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${
|
|
860
|
+
`[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${outboundModel} stream=${plan.isStream}`
|
|
861
|
+
);
|
|
862
|
+
const response = await fetchUpstream(
|
|
863
|
+
plan.upstreamUrl,
|
|
864
|
+
{ method: "POST", headers, body: outboundBody },
|
|
865
|
+
{ providerId: proxyProviderId(plan), accountId: proxyAccountId }
|
|
702
866
|
);
|
|
703
|
-
const response = await fetch(plan.upstreamUrl, { method: "POST", headers, body: rawBody });
|
|
704
867
|
return { response, rawStatus: response.status };
|
|
705
868
|
}
|
|
706
|
-
|
|
707
|
-
|
|
869
|
+
function outboundRemapApplies(plan) {
|
|
870
|
+
return plan.isSubscription && plan.transformerProvider.name === "claude";
|
|
871
|
+
}
|
|
872
|
+
function rewriteBodyModel(rawBody, model) {
|
|
873
|
+
try {
|
|
874
|
+
const parsed = JSON.parse(rawBody);
|
|
875
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return rawBody;
|
|
876
|
+
parsed.model = model;
|
|
877
|
+
return JSON.stringify(parsed);
|
|
878
|
+
} catch {
|
|
879
|
+
return rawBody;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan, options = {}) {
|
|
883
|
+
let selectedAccountId;
|
|
884
|
+
const reportSelection = (accountId) => {
|
|
885
|
+
selectedAccountId = accountId;
|
|
886
|
+
};
|
|
887
|
+
const runOnce = () => plan.sameFormat ? runSubscriptionSameFormatFetch(relayBody, plan, reportSelection, options) : runPipeline(anthropicBody, plan, reportSelection);
|
|
708
888
|
const first = await runOnce();
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
889
|
+
let result = first;
|
|
890
|
+
if (first.rawStatus === 401) {
|
|
891
|
+
const refreshed = await plan.auth.onUnauthorized?.(plan.sessionKey);
|
|
892
|
+
if (refreshed) {
|
|
893
|
+
console.info("[ProviderProxy:anthropic] 401 \u2192 token refreshed; retrying once");
|
|
894
|
+
result = await runOnce();
|
|
895
|
+
} else {
|
|
896
|
+
console.warn("[ProviderProxy:anthropic] 401 not recoverable (onUnauthorized returned false)");
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
await markSubscriptionHealth(plan, selectedAccountId, result);
|
|
900
|
+
return result;
|
|
901
|
+
}
|
|
902
|
+
async function readBoundedBody(response, max = 2048) {
|
|
903
|
+
try {
|
|
904
|
+
const text = await response.clone().text();
|
|
905
|
+
return text.slice(0, max);
|
|
906
|
+
} catch {
|
|
907
|
+
return void 0;
|
|
714
908
|
}
|
|
715
|
-
|
|
716
|
-
|
|
909
|
+
}
|
|
910
|
+
async function markSubscriptionHealth(plan, selectedAccountId, result) {
|
|
911
|
+
if (!plan.isSubscription || selectedAccountId === void 0) return;
|
|
912
|
+
const providerId = plan.transformerProvider.name;
|
|
913
|
+
const status = result.rawStatus;
|
|
914
|
+
const { resetHeaderSeconds, retryAfterSeconds } = resolveResetSeconds(providerId, result.response.headers);
|
|
915
|
+
const bodyText = status === 403 ? await readBoundedBody(result.response) : void 0;
|
|
916
|
+
getSharedAccountHealth().recordUpstreamOutcome(providerId, selectedAccountId, {
|
|
917
|
+
status,
|
|
918
|
+
resetHeaderSeconds,
|
|
919
|
+
retryAfterSeconds,
|
|
920
|
+
bodyText
|
|
921
|
+
});
|
|
717
922
|
}
|
|
718
923
|
function isFallbackEligibleStatus(rawStatus) {
|
|
719
924
|
return rawStatus === null || rawStatus >= 500 || rawStatus === 429;
|
|
@@ -728,9 +933,9 @@ function recordBreakerOutcome(profile, modelId, rawStatus) {
|
|
|
728
933
|
if (outcome === "neutral") return;
|
|
729
934
|
profile?.recordModelOutcome?.(modelId, outcome === "success");
|
|
730
935
|
}
|
|
731
|
-
async function runSubscriptionAttemptOutcome(anthropicBody, relayBody, plan) {
|
|
936
|
+
async function runSubscriptionAttemptOutcome(anthropicBody, relayBody, plan, options = {}) {
|
|
732
937
|
try {
|
|
733
|
-
return { kind: "result", result: await runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) };
|
|
938
|
+
return { kind: "result", result: await runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan, options) };
|
|
734
939
|
} catch (error) {
|
|
735
940
|
return { kind: "thrown", error };
|
|
736
941
|
}
|
|
@@ -742,11 +947,11 @@ function settleOutcome(outcome) {
|
|
|
742
947
|
if (outcome.kind === "thrown") throw outcome.error;
|
|
743
948
|
return outcome.result;
|
|
744
949
|
}
|
|
745
|
-
async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialPlan, route, deps) {
|
|
950
|
+
async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialPlan, route, deps, options = {}) {
|
|
746
951
|
const profile = route.subscriptionProfile;
|
|
747
952
|
const scenario = initialPlan.scenario;
|
|
748
953
|
if (!profile?.nextFallback || scenario === void 0) {
|
|
749
|
-
const loneOutcome = await runSubscriptionAttemptOutcome(anthropicBody, rawBody, initialPlan);
|
|
954
|
+
const loneOutcome = await runSubscriptionAttemptOutcome(anthropicBody, rawBody, initialPlan, options);
|
|
750
955
|
recordBreakerOutcome(profile, initialPlan.resolvedModel, outcomeStatus(loneOutcome));
|
|
751
956
|
return settleOutcome(loneOutcome);
|
|
752
957
|
}
|
|
@@ -757,7 +962,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
|
|
|
757
962
|
if (profile.allowModel && !profile.allowModel(initialPlan.resolvedModel)) {
|
|
758
963
|
attempted.push(initialPlan.resolvedModel);
|
|
759
964
|
const firstAdmitting = profile.nextFallback(scenario, attempted, route.subscriptionConfig);
|
|
760
|
-
const gatedPlan = firstAdmitting ? buildSubscriptionIterationPlan(profile, route, deps, firstAdmitting.modelId, initialPlan.isStream, scenario) : null;
|
|
965
|
+
const gatedPlan = firstAdmitting ? buildSubscriptionIterationPlan(profile, route, deps, firstAdmitting.modelId, initialPlan.isStream, scenario, initialPlan.sessionKey) : null;
|
|
761
966
|
if (firstAdmitting && gatedPlan) {
|
|
762
967
|
console.warn(
|
|
763
968
|
`[ProviderProxy:anthropic] subscription primary ${initialPlan.resolvedModel} circuit open -> first admitting fallback ${firstAdmitting.modelId}`
|
|
@@ -772,7 +977,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
|
|
|
772
977
|
);
|
|
773
978
|
}
|
|
774
979
|
}
|
|
775
|
-
let outcome = await runSubscriptionAttemptOutcome(firstBodyObj, firstRelayBody, plan);
|
|
980
|
+
let outcome = await runSubscriptionAttemptOutcome(firstBodyObj, firstRelayBody, plan, options);
|
|
776
981
|
recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
|
|
777
982
|
if (!attempted.includes(plan.resolvedModel)) attempted.push(plan.resolvedModel);
|
|
778
983
|
while (attempted.length < MAX_FALLBACK_ATTEMPTS) {
|
|
@@ -785,7 +990,8 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
|
|
|
785
990
|
deps,
|
|
786
991
|
next.modelId,
|
|
787
992
|
plan.isStream,
|
|
788
|
-
scenario
|
|
993
|
+
scenario,
|
|
994
|
+
initialPlan.sessionKey
|
|
789
995
|
);
|
|
790
996
|
if (!nextPlan) return settleOutcome(outcome);
|
|
791
997
|
const sinceLabel = outcome.kind === "thrown" ? "thrown error" : `status ${String(outcome.result.rawStatus)}`;
|
|
@@ -796,7 +1002,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
|
|
|
796
1002
|
const fallbackRelayBody = nextPlan.sameFormat ? JSON.stringify(fallbackBodyObj) : rawBody;
|
|
797
1003
|
plan = nextPlan;
|
|
798
1004
|
attempted.push(next.modelId);
|
|
799
|
-
outcome = await runSubscriptionAttemptOutcome(fallbackBodyObj, fallbackRelayBody, plan);
|
|
1005
|
+
outcome = await runSubscriptionAttemptOutcome(fallbackBodyObj, fallbackRelayBody, plan, options);
|
|
800
1006
|
recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
|
|
801
1007
|
}
|
|
802
1008
|
return settleOutcome(outcome);
|
|
@@ -819,13 +1025,16 @@ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {
|
|
|
819
1025
|
const plan = route.authMode === "subscription" ? await buildSubscriptionPlan(res, route, deps, anthropicBody, resolvedModel, isStream) : await buildByoPlan(res, route, deps, resolvedModel, isStream);
|
|
820
1026
|
if (!plan) return;
|
|
821
1027
|
anthropicBody.model = plan.resolvedModel;
|
|
822
|
-
const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry(anthropicBody, rawBody, plan, route, deps) : await runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options);
|
|
823
|
-
const bodyText = await relayResponse(res, providerResponse.response, isStream);
|
|
1028
|
+
const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry(anthropicBody, rawBody, plan, route, deps, options) : await runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options);
|
|
1029
|
+
const bodyText = await relayResponse(res, providerResponse.response, isStream, route.requestedModel);
|
|
824
1030
|
if (bodyText && deps.usageRecorder) {
|
|
825
1031
|
recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, {
|
|
826
1032
|
sessionId: route.sessionId,
|
|
827
1033
|
providerId: route.providerId ?? "anthropic",
|
|
828
|
-
model: plan.resolvedModel
|
|
1034
|
+
model: plan.resolvedModel,
|
|
1035
|
+
apiKeyId: route.apiKeyId ?? null,
|
|
1036
|
+
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1037
|
+
auditResponse: res
|
|
829
1038
|
});
|
|
830
1039
|
}
|
|
831
1040
|
} catch (err) {
|
|
@@ -881,7 +1090,7 @@ async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
|
|
|
881
1090
|
extendedContextEnabled: route.anthropicSdkHints?.extendedContext?.enabled ?? false
|
|
882
1091
|
};
|
|
883
1092
|
}
|
|
884
|
-
async function runSameFormatFetch(
|
|
1093
|
+
async function runSameFormatFetch(bodyToSend, plan, options, keyOverride) {
|
|
885
1094
|
const { provider, apiKey, resolvedModel, isStream, extendedContextEnabled } = plan;
|
|
886
1095
|
if (!provider) {
|
|
887
1096
|
throw new Error("[ProviderProxy:anthropic] same-format BYO path requires a provider row");
|
|
@@ -902,11 +1111,26 @@ async function runSameFormatFetch(rawBody, plan, options, keyOverride) {
|
|
|
902
1111
|
injectExtendedContextBeta(headers, resolvedModel, extendedContextEnabled ?? false);
|
|
903
1112
|
const url = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
|
|
904
1113
|
console.info(`[ProviderProxy:anthropic] (same-format) -> ${url} model=${resolvedModel} stream=${isStream}`);
|
|
905
|
-
const response = await
|
|
1114
|
+
const response = await fetchUpstream(
|
|
1115
|
+
url,
|
|
1116
|
+
{ method: "POST", headers, body: bodyToSend },
|
|
1117
|
+
{ providerId: "byo" }
|
|
1118
|
+
);
|
|
906
1119
|
return { response, rawStatus: response.status };
|
|
907
1120
|
}
|
|
1121
|
+
function resolveSameFormatBody(anthropicBody, rawBody, plan) {
|
|
1122
|
+
let originalModel;
|
|
1123
|
+
try {
|
|
1124
|
+
const m = JSON.parse(rawBody).model;
|
|
1125
|
+
originalModel = typeof m === "string" ? m : void 0;
|
|
1126
|
+
} catch {
|
|
1127
|
+
originalModel = void 0;
|
|
1128
|
+
}
|
|
1129
|
+
return plan.resolvedModel !== originalModel ? JSON.stringify(anthropicBody) : rawBody;
|
|
1130
|
+
}
|
|
908
1131
|
async function runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options) {
|
|
909
|
-
const
|
|
1132
|
+
const sameFormatBody = plan.sameFormat ? resolveSameFormatBody(anthropicBody, rawBody, plan) : rawBody;
|
|
1133
|
+
const runOnce = (keyOverride) => plan.sameFormat ? runSameFormatFetch(sameFormatBody, plan, options, keyOverride) : runPipeline(anthropicBody, plan);
|
|
910
1134
|
const first = await runOnce();
|
|
911
1135
|
const outcome = await plan.auth.onResult?.(first.rawStatus);
|
|
912
1136
|
if (outcome?.rebound) {
|
|
@@ -930,7 +1154,8 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
|
|
|
930
1154
|
const rawBody = await readBody(req);
|
|
931
1155
|
const callerBetaRaw = req.headers["anthropic-beta"];
|
|
932
1156
|
const callerAnthropicBeta = Array.isArray(callerBetaRaw) ? callerBetaRaw.join(",") : callerBetaRaw;
|
|
933
|
-
|
|
1157
|
+
const callerIdentity = captureCallerIdentity(getSharedIdentityStore(), req.headers);
|
|
1158
|
+
await handleAnthropicMessagesByo(res, rawBody, route, deps, { callerAnthropicBeta, callerIdentity });
|
|
934
1159
|
return;
|
|
935
1160
|
}
|
|
936
1161
|
const hints = route.anthropicSdkHints;
|
|
@@ -992,7 +1217,8 @@ function recordGeminiNonStreamUsage(recorder, bodyText, attribution) {
|
|
|
992
1217
|
sessionId: attribution.sessionId,
|
|
993
1218
|
providerId: attribution.providerId,
|
|
994
1219
|
model: attribution.model,
|
|
995
|
-
apiKeyId:
|
|
1220
|
+
apiKeyId: attribution.apiKeyId,
|
|
1221
|
+
auditResponse: attribution.auditResponse,
|
|
996
1222
|
engineOrigin: "codex-ingress",
|
|
997
1223
|
usage: tapped,
|
|
998
1224
|
rawUsage: parsed.usageMetadata
|
|
@@ -1042,7 +1268,10 @@ async function handleGeminiGenerateContentRequest(res, rawBody, url, route, deps
|
|
|
1042
1268
|
recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, {
|
|
1043
1269
|
sessionId: route.sessionId,
|
|
1044
1270
|
providerId: route.providerId ?? "gemini",
|
|
1045
|
-
model: resolvedModel
|
|
1271
|
+
model: resolvedModel,
|
|
1272
|
+
apiKeyId: route.apiKeyId ?? null,
|
|
1273
|
+
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1274
|
+
auditResponse: res
|
|
1046
1275
|
});
|
|
1047
1276
|
}
|
|
1048
1277
|
} catch (err) {
|
|
@@ -1117,7 +1346,11 @@ async function runPipeline2(geminiBody, plan) {
|
|
|
1117
1346
|
},
|
|
1118
1347
|
fetchFn: (url, headers, body) => {
|
|
1119
1348
|
console.log(`[ProviderProxy:gemini] -> ${url} model=${resolvedModel} stream=${isStream}`);
|
|
1120
|
-
return
|
|
1349
|
+
return fetchUpstream(
|
|
1350
|
+
url,
|
|
1351
|
+
{ method: "POST", headers, body: JSON.stringify(body) },
|
|
1352
|
+
{ providerId: "byo" }
|
|
1353
|
+
).then((r) => {
|
|
1121
1354
|
rawStatus = r.status;
|
|
1122
1355
|
return r;
|
|
1123
1356
|
});
|
|
@@ -1169,7 +1402,8 @@ function recordChatCompletionsNonStreamUsage(recorder, bodyText, attribution) {
|
|
|
1169
1402
|
sessionId: attribution.sessionId,
|
|
1170
1403
|
providerId: attribution.providerId,
|
|
1171
1404
|
model: attribution.model,
|
|
1172
|
-
apiKeyId:
|
|
1405
|
+
apiKeyId: attribution.apiKeyId,
|
|
1406
|
+
auditResponse: attribution.auditResponse,
|
|
1173
1407
|
engineOrigin: "codex-ingress",
|
|
1174
1408
|
usage: tapped,
|
|
1175
1409
|
rawUsage: parsed.usage
|
|
@@ -1184,6 +1418,8 @@ function isOpenAIChatRequest(method, url) {
|
|
|
1184
1418
|
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
1185
1419
|
return path.endsWith("/chat/completions");
|
|
1186
1420
|
}
|
|
1421
|
+
var CHAT_BRIDGE_SUBSCRIPTION_PROVIDERS = /* @__PURE__ */ new Set(["claude"]);
|
|
1422
|
+
var IDENTITY_ENDPOINT_TRANSFORMER = { name: "identity-openai-chat" };
|
|
1187
1423
|
async function handleOpenAIChatRequest(res, rawBody, route, deps) {
|
|
1188
1424
|
let chatBody;
|
|
1189
1425
|
try {
|
|
@@ -1192,23 +1428,22 @@ async function handleOpenAIChatRequest(res, rawBody, route, deps) {
|
|
|
1192
1428
|
writeError(res, 400, "Invalid JSON in request body");
|
|
1193
1429
|
return;
|
|
1194
1430
|
}
|
|
1195
|
-
if (route.authMode === "subscription") {
|
|
1196
|
-
writeError(res, 502, "OpenAI Chat Completions ingress does not support subscription auth");
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
1431
|
const isStream = chatBody.stream === true;
|
|
1200
1432
|
const resolvedModel = route.model;
|
|
1201
1433
|
chatBody.model = resolvedModel;
|
|
1202
1434
|
try {
|
|
1203
|
-
const plan = await buildByoPlan3(res, route, deps, resolvedModel, isStream);
|
|
1435
|
+
const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan3(res, route, deps, resolvedModel, isStream);
|
|
1204
1436
|
if (!plan) return;
|
|
1205
|
-
const providerResponse = await runPipelineWithPoolReporting3(chatBody, plan);
|
|
1437
|
+
const providerResponse = plan.isSubscription ? await runPipelineWithSubscriptionRetry2(chatBody, plan) : await runPipelineWithPoolReporting3(chatBody, plan);
|
|
1206
1438
|
const bodyText = await relayResponse(res, providerResponse.response, isStream);
|
|
1207
1439
|
if (bodyText && deps.usageRecorder) {
|
|
1208
1440
|
recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, {
|
|
1209
1441
|
sessionId: route.sessionId,
|
|
1210
1442
|
providerId: route.providerId ?? "openai",
|
|
1211
|
-
model: resolvedModel
|
|
1443
|
+
model: resolvedModel,
|
|
1444
|
+
apiKeyId: route.apiKeyId ?? null,
|
|
1445
|
+
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1446
|
+
auditResponse: res
|
|
1212
1447
|
});
|
|
1213
1448
|
}
|
|
1214
1449
|
} catch (err) {
|
|
@@ -1255,14 +1490,67 @@ async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
|
|
|
1255
1490
|
resolvedModel,
|
|
1256
1491
|
isStream,
|
|
1257
1492
|
resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
|
|
1258
|
-
upstreamUrl: byoUrl
|
|
1493
|
+
upstreamUrl: byoUrl,
|
|
1494
|
+
proxyProviderId: "byo",
|
|
1495
|
+
isSubscription: false
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) {
|
|
1499
|
+
const profile = route.subscriptionProfile;
|
|
1500
|
+
if (!profile) {
|
|
1501
|
+
writeError(res, 502, "Subscription mode requires a subscription profile");
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
const providerId = profile.authStrategy.providerId;
|
|
1505
|
+
if (!CHAT_BRIDGE_SUBSCRIPTION_PROVIDERS.has(providerId)) {
|
|
1506
|
+
writeError(
|
|
1507
|
+
res,
|
|
1508
|
+
501,
|
|
1509
|
+
`the OpenAI-chat\u2192subscription bridge currently supports claude only; subscription provider '${providerId}' is not implemented on this endpoint yet (use the /v1/responses endpoint, a Claude subscription, or a BYO provider)`
|
|
1510
|
+
);
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel);
|
|
1514
|
+
if (!upstreamUrl) {
|
|
1515
|
+
writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
const auth = route.auth ?? new SubscriptionAuthSource(profile);
|
|
1519
|
+
const chain = resolveSubscriptionChain(
|
|
1520
|
+
profile,
|
|
1521
|
+
deps.llmConfig.getTransformerService(),
|
|
1522
|
+
IDENTITY_ENDPOINT_TRANSFORMER
|
|
1523
|
+
);
|
|
1524
|
+
const transformerProvider = {
|
|
1525
|
+
name: providerId,
|
|
1526
|
+
baseUrl: upstreamUrl,
|
|
1527
|
+
apiKey: "",
|
|
1528
|
+
models: [resolvedModel]
|
|
1529
|
+
};
|
|
1530
|
+
return {
|
|
1531
|
+
auth,
|
|
1532
|
+
chain,
|
|
1533
|
+
transformerProvider,
|
|
1534
|
+
resolvedModel,
|
|
1535
|
+
isStream,
|
|
1536
|
+
resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
|
|
1537
|
+
upstreamUrl,
|
|
1538
|
+
proxyProviderId: providerId,
|
|
1539
|
+
isSubscription: true
|
|
1259
1540
|
};
|
|
1260
1541
|
}
|
|
1261
1542
|
async function runPipeline3(chatBody, plan) {
|
|
1262
1543
|
const executor = getSharedExecutor();
|
|
1263
1544
|
const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
|
|
1545
|
+
let proxyAccountId;
|
|
1264
1546
|
const authHeaders = {};
|
|
1265
|
-
await auth.applyHeaders(authHeaders, {
|
|
1547
|
+
await auth.applyHeaders(authHeaders, {
|
|
1548
|
+
upstreamUrl,
|
|
1549
|
+
model: resolvedModel,
|
|
1550
|
+
reportSelection: (accountId) => {
|
|
1551
|
+
proxyAccountId = accountId;
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1266
1554
|
let rawStatus = null;
|
|
1267
1555
|
const { response } = await executeProviderCall({
|
|
1268
1556
|
executor,
|
|
@@ -1281,7 +1569,11 @@ async function runPipeline3(chatBody, plan) {
|
|
|
1281
1569
|
},
|
|
1282
1570
|
fetchFn: (url, headers, body) => {
|
|
1283
1571
|
console.log(`[ProviderProxy:chat] -> ${url} model=${resolvedModel} stream=${isStream}`);
|
|
1284
|
-
return
|
|
1572
|
+
return fetchUpstream(
|
|
1573
|
+
url,
|
|
1574
|
+
{ method: "POST", headers, body: JSON.stringify(body) },
|
|
1575
|
+
{ providerId: plan.proxyProviderId, accountId: proxyAccountId }
|
|
1576
|
+
).then((r) => {
|
|
1285
1577
|
rawStatus = r.status;
|
|
1286
1578
|
return r;
|
|
1287
1579
|
});
|
|
@@ -1303,6 +1595,17 @@ async function runPipelineWithPoolReporting3(chatBody, plan) {
|
|
|
1303
1595
|
}
|
|
1304
1596
|
return first;
|
|
1305
1597
|
}
|
|
1598
|
+
async function runPipelineWithSubscriptionRetry2(chatBody, plan) {
|
|
1599
|
+
const first = await runPipeline3(chatBody, plan);
|
|
1600
|
+
if (first.rawStatus !== 401) return first;
|
|
1601
|
+
const refreshed = await plan.auth.onUnauthorized?.();
|
|
1602
|
+
if (!refreshed) {
|
|
1603
|
+
console.warn("[ProviderProxy:chat] 401 not recoverable (onUnauthorized returned false)");
|
|
1604
|
+
return first;
|
|
1605
|
+
}
|
|
1606
|
+
console.log("[ProviderProxy:chat] 401 \u2192 token refreshed; retrying once");
|
|
1607
|
+
return runPipeline3(chatBody, plan);
|
|
1608
|
+
}
|
|
1306
1609
|
|
|
1307
1610
|
// src/provider-proxy/usage/recordResponsesUsage.ts
|
|
1308
1611
|
function readResponsesUsage(usage) {
|
|
@@ -1333,7 +1636,8 @@ function recordResponsesNonStreamUsage(recorder, bodyText, attribution) {
|
|
|
1333
1636
|
sessionId: attribution.sessionId,
|
|
1334
1637
|
providerId: attribution.providerId,
|
|
1335
1638
|
model: attribution.model,
|
|
1336
|
-
apiKeyId:
|
|
1639
|
+
apiKeyId: attribution.apiKeyId,
|
|
1640
|
+
auditResponse: attribution.auditResponse,
|
|
1337
1641
|
engineOrigin: "codex-ingress",
|
|
1338
1642
|
usage: tapped,
|
|
1339
1643
|
rawUsage: parsed.usage
|
|
@@ -1360,15 +1664,18 @@ async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
|
|
|
1360
1664
|
const resolvedModel = route.model;
|
|
1361
1665
|
responsesBody.model = resolvedModel;
|
|
1362
1666
|
try {
|
|
1363
|
-
const plan = route.authMode === "subscription" ? await
|
|
1667
|
+
const plan = route.authMode === "subscription" ? await buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
|
|
1364
1668
|
if (!plan) return;
|
|
1365
|
-
const providerResponse = route.authMode === "subscription" ? await
|
|
1366
|
-
const bodyText = await relayResponse(res, providerResponse.response, isStream);
|
|
1669
|
+
const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry3(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
|
|
1670
|
+
const bodyText = await relayResponse(res, providerResponse.response, isStream, route.requestedModel);
|
|
1367
1671
|
if (bodyText && deps.usageRecorder) {
|
|
1368
1672
|
recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, {
|
|
1369
1673
|
sessionId: route.sessionId,
|
|
1370
1674
|
providerId: route.providerId ?? "codex",
|
|
1371
|
-
model: resolvedModel
|
|
1675
|
+
model: resolvedModel,
|
|
1676
|
+
apiKeyId: route.apiKeyId ?? null,
|
|
1677
|
+
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1678
|
+
auditResponse: res
|
|
1372
1679
|
});
|
|
1373
1680
|
}
|
|
1374
1681
|
} catch (err) {
|
|
@@ -1415,10 +1722,11 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
|
|
|
1415
1722
|
resolvedModel,
|
|
1416
1723
|
isStream,
|
|
1417
1724
|
resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
|
|
1418
|
-
upstreamUrl: byoUrl
|
|
1725
|
+
upstreamUrl: byoUrl,
|
|
1726
|
+
proxyProviderId: "byo"
|
|
1419
1727
|
};
|
|
1420
1728
|
}
|
|
1421
|
-
async function
|
|
1729
|
+
async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) {
|
|
1422
1730
|
const profile = route.subscriptionProfile;
|
|
1423
1731
|
if (!profile) {
|
|
1424
1732
|
writeError(res, 502, "Subscription mode requires a codex subscription profile");
|
|
@@ -1451,6 +1759,7 @@ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream)
|
|
|
1451
1759
|
}
|
|
1452
1760
|
return {
|
|
1453
1761
|
auth,
|
|
1762
|
+
preferredAccountId: route.preferredAccountId,
|
|
1454
1763
|
chain,
|
|
1455
1764
|
transformerProvider,
|
|
1456
1765
|
resolvedModel,
|
|
@@ -1459,7 +1768,8 @@ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream)
|
|
|
1459
1768
|
// stream vs non-stream colon-method URL in `config.url`); fall back to the
|
|
1460
1769
|
// profile's `resolveUpstreamUrl` for non-Code-Assist subscriptions.
|
|
1461
1770
|
resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
|
|
1462
|
-
upstreamUrl
|
|
1771
|
+
upstreamUrl,
|
|
1772
|
+
proxyProviderId: profile.authStrategy.providerId
|
|
1463
1773
|
};
|
|
1464
1774
|
}
|
|
1465
1775
|
async function resolveGeminiCodeAssistProject(profile) {
|
|
@@ -1476,8 +1786,16 @@ async function runPipeline4(responsesBody, plan) {
|
|
|
1476
1786
|
const executor = getSharedExecutor();
|
|
1477
1787
|
const endpointTransformer = getResponsesEndpointTransformer();
|
|
1478
1788
|
const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
|
|
1789
|
+
let proxyAccountId;
|
|
1479
1790
|
const authHeaders = {};
|
|
1480
|
-
await auth.applyHeaders(authHeaders, {
|
|
1791
|
+
await auth.applyHeaders(authHeaders, {
|
|
1792
|
+
upstreamUrl,
|
|
1793
|
+
model: resolvedModel,
|
|
1794
|
+
preferredAccountId: plan.preferredAccountId,
|
|
1795
|
+
reportSelection: (accountId) => {
|
|
1796
|
+
proxyAccountId = accountId;
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1481
1799
|
let rawStatus = null;
|
|
1482
1800
|
const { response } = await executeProviderCall({
|
|
1483
1801
|
executor,
|
|
@@ -1497,7 +1815,11 @@ async function runPipeline4(responsesBody, plan) {
|
|
|
1497
1815
|
},
|
|
1498
1816
|
fetchFn: (url, headers, body) => {
|
|
1499
1817
|
console.log(`[ProviderProxy:responses] -> ${url} model=${resolvedModel} stream=${isStream}`);
|
|
1500
|
-
return
|
|
1818
|
+
return fetchUpstream(
|
|
1819
|
+
url,
|
|
1820
|
+
{ method: "POST", headers, body: JSON.stringify(body) },
|
|
1821
|
+
{ providerId: plan.proxyProviderId, accountId: proxyAccountId }
|
|
1822
|
+
).then((r) => {
|
|
1501
1823
|
rawStatus = r.status;
|
|
1502
1824
|
return r;
|
|
1503
1825
|
});
|
|
@@ -1519,7 +1841,7 @@ async function runPipelineWithPoolReporting4(responsesBody, plan) {
|
|
|
1519
1841
|
}
|
|
1520
1842
|
return first;
|
|
1521
1843
|
}
|
|
1522
|
-
async function
|
|
1844
|
+
async function runPipelineWithSubscriptionRetry3(responsesBody, plan) {
|
|
1523
1845
|
const first = await runPipeline4(responsesBody, plan);
|
|
1524
1846
|
if (first.rawStatus !== 401) return first;
|
|
1525
1847
|
const refreshed = await plan.auth.onUnauthorized?.();
|
|
@@ -1696,12 +2018,439 @@ function __resetProviderProxyForTests() {
|
|
|
1696
2018
|
// src/outbound-api/OutboundApiServer.ts
|
|
1697
2019
|
import http2 from "http";
|
|
1698
2020
|
import { networkInterfaces } from "os";
|
|
2021
|
+
import { healthHttpStatus } from "@omnicross/contracts/health-logging-types";
|
|
2022
|
+
|
|
2023
|
+
// src/outbound-api/keyedMutex.ts
|
|
2024
|
+
var KeyedMutex = class {
|
|
2025
|
+
/** key → the tail of the pending-op chain (resolves when the last op frees). */
|
|
2026
|
+
tails = /* @__PURE__ */ new Map();
|
|
2027
|
+
/**
|
|
2028
|
+
* Run `fn` exclusively for `key`: it starts only after every previously
|
|
2029
|
+
* enqueued op for the SAME key has settled, and the next waiter starts only
|
|
2030
|
+
* after `fn` settles. Returns `fn`'s result (or rejection). Never lets one op's
|
|
2031
|
+
* failure wedge the queue (waiters proceed regardless).
|
|
2032
|
+
*/
|
|
2033
|
+
async runExclusive(key, fn) {
|
|
2034
|
+
const prev = this.tails.get(key) ?? Promise.resolve();
|
|
2035
|
+
let release2;
|
|
2036
|
+
const gate = new Promise((resolve) => {
|
|
2037
|
+
release2 = resolve;
|
|
2038
|
+
});
|
|
2039
|
+
const chain = prev.then(() => gate);
|
|
2040
|
+
this.tails.set(key, chain);
|
|
2041
|
+
await prev.catch(() => void 0);
|
|
2042
|
+
try {
|
|
2043
|
+
return await fn();
|
|
2044
|
+
} finally {
|
|
2045
|
+
release2();
|
|
2046
|
+
if (this.tails.get(key) === chain) this.tails.delete(key);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
1699
2050
|
|
|
1700
2051
|
// src/outbound-api/outboundApiRouter.ts
|
|
1701
2052
|
import { Readable } from "stream";
|
|
1702
2053
|
|
|
2054
|
+
// src/outbound-api/apiServerConfig.ts
|
|
2055
|
+
import { DEFAULT_AUDIT_CONFIG } from "@omnicross/contracts/audit-types";
|
|
2056
|
+
import { DEFAULT_BILLING_CONFIG } from "@omnicross/contracts/billing-types";
|
|
2057
|
+
import {
|
|
2058
|
+
WEBHOOK_DESTINATION_TYPES,
|
|
2059
|
+
WEBHOOK_EVENT_KINDS
|
|
2060
|
+
} from "@omnicross/contracts/webhook-types";
|
|
2061
|
+
var OUTBOUND_API_SERVER_CONFIG_KEY = "outboundApiServer.config";
|
|
2062
|
+
var DEFAULT_USER_MESSAGE_QUEUE = {
|
|
2063
|
+
enabled: false,
|
|
2064
|
+
delayMs: 200,
|
|
2065
|
+
waitTimeoutMs: 6e4
|
|
2066
|
+
};
|
|
2067
|
+
var DEFAULT_CONCURRENCY_QUEUE = {
|
|
2068
|
+
maxQueueSizeFactor: 2,
|
|
2069
|
+
minQueueSize: 4,
|
|
2070
|
+
waitTimeoutMs: 6e4
|
|
2071
|
+
};
|
|
2072
|
+
var DEFAULT_ACCOUNT_HEALTH = {
|
|
2073
|
+
overloadCooldownEnabled: true,
|
|
2074
|
+
overloadCooldownMs: 10 * 6e4
|
|
2075
|
+
};
|
|
2076
|
+
function normalizeAccountHealth(raw) {
|
|
2077
|
+
const ah = raw?.accountHealth;
|
|
2078
|
+
return {
|
|
2079
|
+
overloadCooldownEnabled: ah?.overloadCooldownEnabled !== false,
|
|
2080
|
+
overloadCooldownMs: clampNumber(
|
|
2081
|
+
ah?.overloadCooldownMs,
|
|
2082
|
+
6e4,
|
|
2083
|
+
36e5,
|
|
2084
|
+
DEFAULT_ACCOUNT_HEALTH.overloadCooldownMs
|
|
2085
|
+
)
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
var DEFAULT_ACCOUNT_PROBE = {
|
|
2089
|
+
enabled: false,
|
|
2090
|
+
intervalMs: 15 * 6e4,
|
|
2091
|
+
onlyMultiAccount: true,
|
|
2092
|
+
timeoutMs: 5e3,
|
|
2093
|
+
historySize: 10,
|
|
2094
|
+
staggerMs: 500
|
|
2095
|
+
};
|
|
2096
|
+
function normalizeAccountProbe(raw) {
|
|
2097
|
+
const ap = raw?.accountProbe;
|
|
2098
|
+
return {
|
|
2099
|
+
enabled: ap?.enabled === true,
|
|
2100
|
+
intervalMs: clampNumber(ap?.intervalMs, 6e4, 864e5, DEFAULT_ACCOUNT_PROBE.intervalMs),
|
|
2101
|
+
onlyMultiAccount: ap?.onlyMultiAccount !== false,
|
|
2102
|
+
timeoutMs: clampNumber(ap?.timeoutMs, 1e3, 6e4, DEFAULT_ACCOUNT_PROBE.timeoutMs),
|
|
2103
|
+
historySize: Math.trunc(
|
|
2104
|
+
clampNumber(ap?.historySize, 1, 200, DEFAULT_ACCOUNT_PROBE.historySize)
|
|
2105
|
+
),
|
|
2106
|
+
staggerMs: clampNumber(ap?.staggerMs, 0, 6e4, DEFAULT_ACCOUNT_PROBE.staggerMs)
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function normalizeAudit(raw) {
|
|
2110
|
+
const a = raw?.audit;
|
|
2111
|
+
return {
|
|
2112
|
+
enabled: a?.enabled === true,
|
|
2113
|
+
captureBodies: a?.captureBodies === true,
|
|
2114
|
+
maxBodyBytes: Math.trunc(
|
|
2115
|
+
clampNumber(a?.maxBodyBytes, 256, 1048576, DEFAULT_AUDIT_CONFIG.maxBodyBytes)
|
|
2116
|
+
),
|
|
2117
|
+
retentionDays: Math.trunc(
|
|
2118
|
+
clampNumber(a?.retentionDays, 1, 365, DEFAULT_AUDIT_CONFIG.retentionDays)
|
|
2119
|
+
),
|
|
2120
|
+
trustForwardedFor: a?.trustForwardedFor === true
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
function normalizeBilling(raw) {
|
|
2124
|
+
const b = raw?.billing;
|
|
2125
|
+
const config = {
|
|
2126
|
+
enabled: b?.enabled === true,
|
|
2127
|
+
maxRetryAgeMs: Math.trunc(
|
|
2128
|
+
clampNumber(b?.maxRetryAgeMs, 6e4, 2592e6, DEFAULT_BILLING_CONFIG.maxRetryAgeMs)
|
|
2129
|
+
)
|
|
2130
|
+
};
|
|
2131
|
+
if (typeof b?.endpoint === "string" && b.endpoint.trim()) config.endpoint = b.endpoint.trim();
|
|
2132
|
+
if (typeof b?.secret === "string" && b.secret.length > 0) config.secret = b.secret;
|
|
2133
|
+
return config;
|
|
2134
|
+
}
|
|
2135
|
+
var DEFAULT_FINGERPRINT = { enabled: false };
|
|
2136
|
+
function normalizeFingerprint(raw) {
|
|
2137
|
+
const f = raw?.fingerprint;
|
|
2138
|
+
const out = { enabled: f?.enabled === true };
|
|
2139
|
+
if (typeof f?.ua === "string" && f.ua.trim().length > 0) out.ua = f.ua.trim();
|
|
2140
|
+
return out;
|
|
2141
|
+
}
|
|
2142
|
+
function normalizeVoucher(raw) {
|
|
2143
|
+
return { enabled: raw?.voucher?.enabled === true };
|
|
2144
|
+
}
|
|
2145
|
+
var PROXY_TYPES = ["http", "https", "socks5"];
|
|
2146
|
+
function normalizeProxyConfig(raw) {
|
|
2147
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
2148
|
+
const r = raw;
|
|
2149
|
+
if (typeof r["url"] === "string" && r["url"].trim().length > 0) {
|
|
2150
|
+
return { url: r["url"].trim() };
|
|
2151
|
+
}
|
|
2152
|
+
const type = r["type"];
|
|
2153
|
+
const host = r["host"];
|
|
2154
|
+
const port = r["port"];
|
|
2155
|
+
if (typeof type === "string" && PROXY_TYPES.includes(type) && typeof host === "string" && host.trim().length > 0 && typeof port === "number" && Number.isFinite(port) && port >= 1 && port <= 65535) {
|
|
2156
|
+
const out = {
|
|
2157
|
+
type,
|
|
2158
|
+
host: host.trim(),
|
|
2159
|
+
port: Math.trunc(port)
|
|
2160
|
+
};
|
|
2161
|
+
if (typeof r["username"] === "string" && r["username"].length > 0) out.username = r["username"];
|
|
2162
|
+
if (typeof r["password"] === "string" && r["password"].length > 0) out.password = r["password"];
|
|
2163
|
+
return out;
|
|
2164
|
+
}
|
|
2165
|
+
return void 0;
|
|
2166
|
+
}
|
|
2167
|
+
function normalizeProxySegment(raw) {
|
|
2168
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
2169
|
+
const r = raw;
|
|
2170
|
+
const out = {};
|
|
2171
|
+
const global = normalizeProxyConfig(r["global"]);
|
|
2172
|
+
if (global) out.global = global;
|
|
2173
|
+
if (r["byProvider"] && typeof r["byProvider"] === "object") {
|
|
2174
|
+
const byProvider = {};
|
|
2175
|
+
for (const [key, value] of Object.entries(r["byProvider"])) {
|
|
2176
|
+
if (!key.trim()) continue;
|
|
2177
|
+
const cfg = normalizeProxyConfig(value);
|
|
2178
|
+
if (cfg) byProvider[key] = cfg;
|
|
2179
|
+
}
|
|
2180
|
+
if (Object.keys(byProvider).length > 0) out.byProvider = byProvider;
|
|
2181
|
+
}
|
|
2182
|
+
return out.global || out.byProvider ? out : void 0;
|
|
2183
|
+
}
|
|
2184
|
+
function normalizeWebhookDestination(raw) {
|
|
2185
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
2186
|
+
const r = raw;
|
|
2187
|
+
const id = typeof r["id"] === "string" ? r["id"].trim() : "";
|
|
2188
|
+
const url = typeof r["url"] === "string" ? r["url"].trim() : "";
|
|
2189
|
+
const type = r["type"];
|
|
2190
|
+
if (!id || !url || typeof type !== "string" || !WEBHOOK_DESTINATION_TYPES.includes(type)) {
|
|
2191
|
+
return void 0;
|
|
2192
|
+
}
|
|
2193
|
+
const out = {
|
|
2194
|
+
id,
|
|
2195
|
+
type,
|
|
2196
|
+
url,
|
|
2197
|
+
enabled: r["enabled"] !== false
|
|
2198
|
+
};
|
|
2199
|
+
if (typeof r["secret"] === "string" && r["secret"].length > 0) out.secret = r["secret"];
|
|
2200
|
+
if (Array.isArray(r["events"])) {
|
|
2201
|
+
const events = r["events"].filter(
|
|
2202
|
+
(e) => typeof e === "string" && WEBHOOK_EVENT_KINDS.includes(e)
|
|
2203
|
+
);
|
|
2204
|
+
if (events.length > 0) out.events = events;
|
|
2205
|
+
}
|
|
2206
|
+
return out;
|
|
2207
|
+
}
|
|
2208
|
+
function normalizeWebhookSegment(raw) {
|
|
2209
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
2210
|
+
const r = raw;
|
|
2211
|
+
const destinations = [];
|
|
2212
|
+
if (Array.isArray(r["destinations"])) {
|
|
2213
|
+
for (const entry of r["destinations"]) {
|
|
2214
|
+
const d = normalizeWebhookDestination(entry);
|
|
2215
|
+
if (d) destinations.push(d);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
const enabled = r["enabled"] === true;
|
|
2219
|
+
if (!enabled && destinations.length === 0) return void 0;
|
|
2220
|
+
return { enabled, destinations };
|
|
2221
|
+
}
|
|
2222
|
+
function clampNumber(value, min, max, fallback) {
|
|
2223
|
+
const n = typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2224
|
+
return Math.min(max, Math.max(min, n));
|
|
2225
|
+
}
|
|
2226
|
+
function normalizeQueueSegments(raw) {
|
|
2227
|
+
const umq = raw?.userMessageQueue;
|
|
2228
|
+
const cq = raw?.concurrencyQueue;
|
|
2229
|
+
return {
|
|
2230
|
+
userMessageQueue: {
|
|
2231
|
+
enabled: umq?.enabled === true,
|
|
2232
|
+
delayMs: clampNumber(umq?.delayMs, 0, 1e4, DEFAULT_USER_MESSAGE_QUEUE.delayMs),
|
|
2233
|
+
waitTimeoutMs: clampNumber(
|
|
2234
|
+
umq?.waitTimeoutMs,
|
|
2235
|
+
1e3,
|
|
2236
|
+
3e5,
|
|
2237
|
+
DEFAULT_USER_MESSAGE_QUEUE.waitTimeoutMs
|
|
2238
|
+
)
|
|
2239
|
+
},
|
|
2240
|
+
concurrencyQueue: {
|
|
2241
|
+
maxQueueSizeFactor: clampNumber(
|
|
2242
|
+
cq?.maxQueueSizeFactor,
|
|
2243
|
+
1,
|
|
2244
|
+
10,
|
|
2245
|
+
DEFAULT_CONCURRENCY_QUEUE.maxQueueSizeFactor
|
|
2246
|
+
),
|
|
2247
|
+
minQueueSize: clampNumber(cq?.minQueueSize, 1, 100, DEFAULT_CONCURRENCY_QUEUE.minQueueSize),
|
|
2248
|
+
waitTimeoutMs: clampNumber(
|
|
2249
|
+
cq?.waitTimeoutMs,
|
|
2250
|
+
1e3,
|
|
2251
|
+
3e5,
|
|
2252
|
+
DEFAULT_CONCURRENCY_QUEUE.waitTimeoutMs
|
|
2253
|
+
)
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
var ALL_ENDPOINTS = ["chat", "responses", "messages", "gemini"];
|
|
2258
|
+
function blankModelMap(endpoint) {
|
|
2259
|
+
const modelMap = {};
|
|
2260
|
+
for (const kind of modelKindsForEndpoint(endpoint)) modelMap[kind] = "";
|
|
2261
|
+
return modelMap;
|
|
2262
|
+
}
|
|
2263
|
+
function defaultEndpointConfig(endpoint) {
|
|
2264
|
+
if (isKindMappedEndpoint(endpoint)) {
|
|
2265
|
+
return { endpoint, modelMap: blankModelMap(endpoint), useSubscription: false };
|
|
2266
|
+
}
|
|
2267
|
+
if (endpoint === "chat") {
|
|
2268
|
+
return { endpoint, models: [], useSubscription: false };
|
|
2269
|
+
}
|
|
2270
|
+
return {
|
|
2271
|
+
endpoint,
|
|
2272
|
+
defaultModel: "",
|
|
2273
|
+
backgroundModel: "",
|
|
2274
|
+
useSubscription: false
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function normalizePrefixTargets(raw) {
|
|
2278
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
2279
|
+
const r = raw;
|
|
2280
|
+
const out = {};
|
|
2281
|
+
for (const key of ["claude", "gpt", "gemini"]) {
|
|
2282
|
+
const v = r[key];
|
|
2283
|
+
if (typeof v === "string" && v.trim() !== "") out[key] = v.trim();
|
|
2284
|
+
}
|
|
2285
|
+
return out.claude || out.gpt || out.gemini ? out : void 0;
|
|
2286
|
+
}
|
|
2287
|
+
function normalizeEndpointConfig(e) {
|
|
2288
|
+
const endpoint = e.endpoint;
|
|
2289
|
+
const useSubscription = e.useSubscription === true;
|
|
2290
|
+
const boundAccountId = typeof e.boundAccountId === "string" && e.boundAccountId.trim() !== "" ? e.boundAccountId : void 0;
|
|
2291
|
+
const boundKeyId = typeof e.boundKeyId === "string" && e.boundKeyId.trim() !== "" ? e.boundKeyId : void 0;
|
|
2292
|
+
let config;
|
|
2293
|
+
if (isKindMappedEndpoint(endpoint)) {
|
|
2294
|
+
const rawMap = e.modelMap && typeof e.modelMap === "object" ? e.modelMap : {};
|
|
2295
|
+
const modelMap = {};
|
|
2296
|
+
for (const kind of modelKindsForEndpoint(endpoint)) {
|
|
2297
|
+
const v = rawMap[kind];
|
|
2298
|
+
modelMap[kind] = typeof v === "string" ? v : "";
|
|
2299
|
+
}
|
|
2300
|
+
config = { endpoint, modelMap, useSubscription };
|
|
2301
|
+
} else if (endpoint === "chat") {
|
|
2302
|
+
const models = Array.isArray(e.models) ? e.models.filter((m) => typeof m === "string" && m.trim() !== "") : [];
|
|
2303
|
+
config = { endpoint, models, useSubscription };
|
|
2304
|
+
if (e.dispatchMode === "prefix") {
|
|
2305
|
+
config.dispatchMode = "prefix";
|
|
2306
|
+
const targets = normalizePrefixTargets(e.prefixTargets);
|
|
2307
|
+
if (targets) config.prefixTargets = targets;
|
|
2308
|
+
}
|
|
2309
|
+
} else {
|
|
2310
|
+
config = {
|
|
2311
|
+
endpoint,
|
|
2312
|
+
defaultModel: typeof e.defaultModel === "string" ? e.defaultModel : "",
|
|
2313
|
+
backgroundModel: typeof e.backgroundModel === "string" ? e.backgroundModel : "",
|
|
2314
|
+
useSubscription
|
|
2315
|
+
};
|
|
2316
|
+
if (Array.isArray(e.backgroundModelIds)) {
|
|
2317
|
+
config.backgroundModelIds = e.backgroundModelIds;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
if (boundAccountId) config.boundAccountId = boundAccountId;
|
|
2321
|
+
if (boundKeyId) config.boundKeyId = boundKeyId;
|
|
2322
|
+
return config;
|
|
2323
|
+
}
|
|
2324
|
+
function defaultServerConfig() {
|
|
2325
|
+
const queues = normalizeQueueSegments(void 0);
|
|
2326
|
+
return {
|
|
2327
|
+
enabled: false,
|
|
2328
|
+
networkBinding: false,
|
|
2329
|
+
endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
|
|
2330
|
+
port: DEFAULT_OUTBOUND_PORT,
|
|
2331
|
+
userMessageQueue: queues.userMessageQueue,
|
|
2332
|
+
concurrencyQueue: queues.concurrencyQueue,
|
|
2333
|
+
accountHealth: normalizeAccountHealth(void 0),
|
|
2334
|
+
accountProbe: normalizeAccountProbe(void 0),
|
|
2335
|
+
audit: normalizeAudit(void 0),
|
|
2336
|
+
billing: normalizeBilling(void 0),
|
|
2337
|
+
fingerprint: normalizeFingerprint(void 0),
|
|
2338
|
+
voucher: normalizeVoucher(void 0)
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function normalizeServerConfig(raw) {
|
|
2342
|
+
const base = defaultServerConfig();
|
|
2343
|
+
if (!raw) return base;
|
|
2344
|
+
const byEndpoint = /* @__PURE__ */ new Map();
|
|
2345
|
+
for (const e of raw.endpoints ?? []) {
|
|
2346
|
+
if (e && ALL_ENDPOINTS.includes(e.endpoint)) {
|
|
2347
|
+
byEndpoint.set(e.endpoint, normalizeEndpointConfig(e));
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const queues = normalizeQueueSegments(raw);
|
|
2351
|
+
const config = {
|
|
2352
|
+
enabled: raw.enabled === true,
|
|
2353
|
+
networkBinding: raw.networkBinding === true,
|
|
2354
|
+
endpoints: ALL_ENDPOINTS.map(
|
|
2355
|
+
(ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep)
|
|
2356
|
+
),
|
|
2357
|
+
port: raw.port ?? base.port,
|
|
2358
|
+
userMessageQueue: queues.userMessageQueue,
|
|
2359
|
+
concurrencyQueue: queues.concurrencyQueue,
|
|
2360
|
+
accountHealth: normalizeAccountHealth(raw),
|
|
2361
|
+
accountProbe: normalizeAccountProbe(raw),
|
|
2362
|
+
audit: normalizeAudit(raw),
|
|
2363
|
+
billing: normalizeBilling(raw),
|
|
2364
|
+
fingerprint: normalizeFingerprint(raw),
|
|
2365
|
+
voucher: normalizeVoucher(raw)
|
|
2366
|
+
};
|
|
2367
|
+
const proxy = normalizeProxySegment(raw.proxy);
|
|
2368
|
+
if (proxy) config.proxy = proxy;
|
|
2369
|
+
const webhook = normalizeWebhookSegment(raw.webhook);
|
|
2370
|
+
if (webhook) config.webhook = webhook;
|
|
2371
|
+
return config;
|
|
2372
|
+
}
|
|
2373
|
+
async function loadServerConfig(store) {
|
|
2374
|
+
const raw = await store.get(
|
|
2375
|
+
OUTBOUND_API_SERVER_CONFIG_KEY
|
|
2376
|
+
);
|
|
2377
|
+
return normalizeServerConfig(raw);
|
|
2378
|
+
}
|
|
2379
|
+
async function saveServerConfig(store, config) {
|
|
2380
|
+
await store.set(OUTBOUND_API_SERVER_CONFIG_KEY, config);
|
|
2381
|
+
}
|
|
2382
|
+
function mergeServerConfig(current, patch) {
|
|
2383
|
+
return normalizeServerConfig({
|
|
2384
|
+
enabled: patch.enabled ?? current.enabled,
|
|
2385
|
+
networkBinding: patch.networkBinding ?? current.networkBinding,
|
|
2386
|
+
endpoints: patch.endpoints ?? current.endpoints,
|
|
2387
|
+
port: patch.port ?? current.port,
|
|
2388
|
+
userMessageQueue: patch.userMessageQueue ?? current.userMessageQueue,
|
|
2389
|
+
concurrencyQueue: patch.concurrencyQueue ?? current.concurrencyQueue,
|
|
2390
|
+
accountHealth: patch.accountHealth ?? current.accountHealth,
|
|
2391
|
+
accountProbe: patch.accountProbe ?? current.accountProbe,
|
|
2392
|
+
audit: patch.audit ?? current.audit,
|
|
2393
|
+
billing: patch.billing ?? current.billing,
|
|
2394
|
+
// Proxy is layer-replaced (not deep-merged): a PUT carrying `proxy` swaps the
|
|
2395
|
+
// whole segment; omitting it keeps the current one. `undefined` on both ⇒ absent.
|
|
2396
|
+
proxy: patch.proxy ?? current.proxy,
|
|
2397
|
+
// Webhook is layer-replaced too (a PUT carrying `webhook` swaps the whole
|
|
2398
|
+
// segment; omitting it keeps the current one). `undefined` on both ⇒ absent.
|
|
2399
|
+
webhook: patch.webhook ?? current.webhook,
|
|
2400
|
+
// Fingerprint is always-filled (normalizeFingerprint synthesizes a default);
|
|
2401
|
+
// a PUT carrying it replaces the segment, else the current one is kept.
|
|
2402
|
+
fingerprint: patch.fingerprint ?? current.fingerprint,
|
|
2403
|
+
// Voucher is always-filled (normalizeVoucher synthesizes a default); a PUT
|
|
2404
|
+
// carrying it replaces the segment, else the current one is kept.
|
|
2405
|
+
voucher: patch.voucher ?? current.voucher
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
|
|
1703
2409
|
// src/outbound-api/outboundApiKeyAuth.ts
|
|
1704
2410
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
2411
|
+
|
|
2412
|
+
// src/outbound-api/keyPolicy.ts
|
|
2413
|
+
var DAY_MS = 864e5;
|
|
2414
|
+
function computeKeyExpiry(row, now) {
|
|
2415
|
+
const mode = row.activationMode ?? "fixed";
|
|
2416
|
+
if (mode === "activation") {
|
|
2417
|
+
if (row.activatedAt == null) return { expired: false, needsActivation: true };
|
|
2418
|
+
if (row.activationDays == null) return { expired: false };
|
|
2419
|
+
const effectiveExpiresAt = row.activatedAt + row.activationDays * DAY_MS;
|
|
2420
|
+
return { expired: now >= effectiveExpiresAt, effectiveExpiresAt };
|
|
2421
|
+
}
|
|
2422
|
+
if (row.expiresAt == null) return { expired: false };
|
|
2423
|
+
return { expired: now >= row.expiresAt, effectiveExpiresAt: row.expiresAt };
|
|
2424
|
+
}
|
|
2425
|
+
function checkKeyQuota(limits, spend) {
|
|
2426
|
+
if (!limits) return { allowed: true };
|
|
2427
|
+
if (limits.dailyUsd != null && spend.dailyUsd >= limits.dailyUsd) {
|
|
2428
|
+
return { allowed: false, scope: "daily", limitUsd: limits.dailyUsd, spentUsd: spend.dailyUsd };
|
|
2429
|
+
}
|
|
2430
|
+
if (limits.weeklyUsd != null && spend.weeklyUsd >= limits.weeklyUsd) {
|
|
2431
|
+
return { allowed: false, scope: "weekly", limitUsd: limits.weeklyUsd, spentUsd: spend.weeklyUsd };
|
|
2432
|
+
}
|
|
2433
|
+
if (limits.totalUsd != null && spend.totalUsd >= limits.totalUsd) {
|
|
2434
|
+
return { allowed: false, scope: "total", limitUsd: limits.totalUsd, spentUsd: spend.totalUsd };
|
|
2435
|
+
}
|
|
2436
|
+
return { allowed: true };
|
|
2437
|
+
}
|
|
2438
|
+
function canonicalModelId(value) {
|
|
2439
|
+
const idx = value.indexOf(",");
|
|
2440
|
+
const bare = idx >= 0 ? value.slice(idx + 1) : value;
|
|
2441
|
+
return bare.trim().toLowerCase();
|
|
2442
|
+
}
|
|
2443
|
+
function checkModelAllowed(restriction, resolvedModelId) {
|
|
2444
|
+
if (!restriction) return { allowed: true };
|
|
2445
|
+
const target = canonicalModelId(resolvedModelId);
|
|
2446
|
+
const listed = restriction.models.some((m) => canonicalModelId(m) === target);
|
|
2447
|
+
if (restriction.mode === "allowlist") {
|
|
2448
|
+
return listed ? { allowed: true } : { allowed: false, model: resolvedModelId };
|
|
2449
|
+
}
|
|
2450
|
+
return listed ? { allowed: false, model: resolvedModelId } : { allowed: true };
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// src/outbound-api/outboundApiKeyAuth.ts
|
|
1705
2454
|
var SECRET_BYTES = 32;
|
|
1706
2455
|
var PREFIX_LEN = 14;
|
|
1707
2456
|
var KEY_PREFIX = "sk-omnicross-";
|
|
@@ -1749,143 +2498,706 @@ async function createNamedKey(db, name) {
|
|
|
1749
2498
|
plaintextOnce: secret
|
|
1750
2499
|
};
|
|
1751
2500
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
2501
|
+
function extractCostLimits(row) {
|
|
2502
|
+
const limits = {};
|
|
2503
|
+
if (row.dailyCostLimitUsd != null) limits.dailyUsd = row.dailyCostLimitUsd;
|
|
2504
|
+
if (row.totalCostLimitUsd != null) limits.totalUsd = row.totalCostLimitUsd;
|
|
2505
|
+
if (row.weeklyCostLimitUsd != null) limits.weeklyUsd = row.weeklyCostLimitUsd;
|
|
2506
|
+
return limits.dailyUsd != null || limits.totalUsd != null || limits.weeklyUsd != null ? limits : void 0;
|
|
2507
|
+
}
|
|
2508
|
+
function extractRateLimit(row) {
|
|
2509
|
+
if (row.rateLimitMaxRequests == null && row.rateLimitWindowMs == null) return void 0;
|
|
2510
|
+
const override = {};
|
|
2511
|
+
if (row.rateLimitMaxRequests != null) override.maxRequests = row.rateLimitMaxRequests;
|
|
2512
|
+
if (row.rateLimitWindowMs != null) override.windowMs = row.rateLimitWindowMs;
|
|
2513
|
+
return override;
|
|
2514
|
+
}
|
|
2515
|
+
function extractModelRestriction(row) {
|
|
2516
|
+
if (row.enableModelRestriction !== true) return void 0;
|
|
2517
|
+
return {
|
|
2518
|
+
mode: row.restrictionMode ?? "blacklist",
|
|
2519
|
+
models: row.restrictedModels ?? []
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
function toVerifiedKey(row) {
|
|
2523
|
+
const key = { id: row.id };
|
|
2524
|
+
if (row.maxConcurrency !== void 0 && row.maxConcurrency !== null) {
|
|
2525
|
+
key.maxConcurrency = row.maxConcurrency;
|
|
2526
|
+
}
|
|
2527
|
+
const costLimits = extractCostLimits(row);
|
|
2528
|
+
if (costLimits) key.costLimits = costLimits;
|
|
2529
|
+
const rateLimit = extractRateLimit(row);
|
|
2530
|
+
if (rateLimit) key.rateLimit = rateLimit;
|
|
2531
|
+
const modelRestriction = extractModelRestriction(row);
|
|
2532
|
+
if (modelRestriction) key.modelRestriction = modelRestriction;
|
|
2533
|
+
return key;
|
|
2534
|
+
}
|
|
2535
|
+
async function verifyKey(db, presentedKey, now = Date.now()) {
|
|
2536
|
+
if (!presentedKey) return { status: "invalid" };
|
|
1754
2537
|
const trimmed = presentedKey.trim();
|
|
1755
|
-
if (!trimmed) return
|
|
2538
|
+
if (!trimmed) return { status: "invalid" };
|
|
1756
2539
|
const row = await db.outboundApiKeysGetByHash(hashKey(trimmed));
|
|
1757
|
-
if (!row) return
|
|
1758
|
-
if (!row.enabled || row.revokedAt !== null) return
|
|
2540
|
+
if (!row) return { status: "invalid" };
|
|
2541
|
+
if (!row.enabled || row.revokedAt !== null) return { status: "invalid" };
|
|
2542
|
+
const expiry = computeKeyExpiry(row, now);
|
|
2543
|
+
if (expiry.expired) return { status: "expired" };
|
|
1759
2544
|
void db.outboundApiKeysTouchLastUsed(row.id).catch(() => {
|
|
1760
2545
|
});
|
|
1761
|
-
|
|
2546
|
+
if (expiry.needsActivation) {
|
|
2547
|
+
void db.outboundApiKeysMarkActivated(row.id, now).catch(() => {
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
return { status: "ok", key: toVerifiedKey(row) };
|
|
2551
|
+
}
|
|
2552
|
+
async function verifyPresentedKey(db, presentedKey) {
|
|
2553
|
+
const result = await verifyKey(db, presentedKey);
|
|
2554
|
+
return result.status === "ok" ? result.key : null;
|
|
1762
2555
|
}
|
|
1763
2556
|
|
|
1764
|
-
// src/outbound-api/
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
2557
|
+
// src/outbound-api/outboundConcurrencyGate.ts
|
|
2558
|
+
var ConcurrencyQueueFullError = class extends Error {
|
|
2559
|
+
constructor(apiKeyId, maxQueueSize) {
|
|
2560
|
+
super(`concurrency queue full (max ${maxQueueSize}) for key '${apiKeyId}'`);
|
|
2561
|
+
this.apiKeyId = apiKeyId;
|
|
2562
|
+
this.maxQueueSize = maxQueueSize;
|
|
2563
|
+
this.name = "ConcurrencyQueueFullError";
|
|
2564
|
+
}
|
|
2565
|
+
apiKeyId;
|
|
2566
|
+
maxQueueSize;
|
|
2567
|
+
code = "concurrency_queue_full";
|
|
2568
|
+
};
|
|
2569
|
+
var ConcurrencyWaitTimeoutError = class extends Error {
|
|
2570
|
+
constructor(apiKeyId, waitTimeoutMs) {
|
|
2571
|
+
super(`concurrency wait timed out after ${waitTimeoutMs}ms for key '${apiKeyId}'`);
|
|
2572
|
+
this.apiKeyId = apiKeyId;
|
|
2573
|
+
this.waitTimeoutMs = waitTimeoutMs;
|
|
2574
|
+
this.name = "ConcurrencyWaitTimeoutError";
|
|
2575
|
+
}
|
|
2576
|
+
apiKeyId;
|
|
2577
|
+
waitTimeoutMs;
|
|
2578
|
+
code = "concurrency_wait_timeout";
|
|
2579
|
+
};
|
|
2580
|
+
var ConcurrencyWaitCancelledError = class extends Error {
|
|
2581
|
+
constructor(apiKeyId) {
|
|
2582
|
+
super(`concurrency wait cancelled for key '${apiKeyId}'`);
|
|
2583
|
+
this.apiKeyId = apiKeyId;
|
|
2584
|
+
this.name = "ConcurrencyWaitCancelledError";
|
|
2585
|
+
}
|
|
2586
|
+
apiKeyId;
|
|
2587
|
+
code = "concurrency_wait_cancelled";
|
|
2588
|
+
};
|
|
2589
|
+
function isConcurrencyRejection(err) {
|
|
2590
|
+
return err instanceof ConcurrencyQueueFullError || err instanceof ConcurrencyWaitTimeoutError || err instanceof ConcurrencyWaitCancelledError;
|
|
2591
|
+
}
|
|
2592
|
+
var OutboundConcurrencyGate = class {
|
|
2593
|
+
states = /* @__PURE__ */ new Map();
|
|
2594
|
+
ensure(apiKeyId) {
|
|
2595
|
+
let state = this.states.get(apiKeyId);
|
|
2596
|
+
if (!state) {
|
|
2597
|
+
state = { active: 0, waiters: [] };
|
|
2598
|
+
this.states.set(apiKeyId, state);
|
|
2599
|
+
}
|
|
2600
|
+
return state;
|
|
2601
|
+
}
|
|
2602
|
+
/** Delete a key's state once it is fully idle (no active, no waiters). */
|
|
2603
|
+
gc(apiKeyId) {
|
|
2604
|
+
const state = this.states.get(apiKeyId);
|
|
2605
|
+
if (state && state.active === 0 && state.waiters.length === 0) {
|
|
2606
|
+
this.states.delete(apiKeyId);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Acquire a concurrency slot for `apiKeyId` under `limit`. Grants immediately
|
|
2611
|
+
* when `active < limit`; else enqueues FIFO while under the per-key queue cap;
|
|
2612
|
+
* else rejects queue-full. `limit <= 0` = unlimited (always granted).
|
|
2613
|
+
*/
|
|
2614
|
+
acquire(apiKeyId, limit, options) {
|
|
2615
|
+
const state = this.ensure(apiKeyId);
|
|
2616
|
+
const cap = limit > 0 ? limit : Infinity;
|
|
2617
|
+
if (state.active < cap) {
|
|
2618
|
+
state.active += 1;
|
|
2619
|
+
const slot = this.makeSlot(apiKeyId);
|
|
2620
|
+
return { granted: Promise.resolve(slot), cancel: slot.cancel };
|
|
2621
|
+
}
|
|
2622
|
+
const maxQueueSize = Math.max(
|
|
2623
|
+
Number.isFinite(cap) ? cap * options.maxQueueSizeFactor : Infinity,
|
|
2624
|
+
options.minQueueSize
|
|
2625
|
+
);
|
|
2626
|
+
if (state.waiters.length >= maxQueueSize) {
|
|
2627
|
+
const err = new ConcurrencyQueueFullError(apiKeyId, maxQueueSize);
|
|
2628
|
+
return { granted: Promise.reject(err), cancel: () => {
|
|
2629
|
+
} };
|
|
2630
|
+
}
|
|
2631
|
+
let settled = false;
|
|
2632
|
+
let granted = false;
|
|
2633
|
+
let resolveGrant;
|
|
2634
|
+
let rejectGrant;
|
|
2635
|
+
const grantedPromise = new Promise((resolve, reject) => {
|
|
2636
|
+
resolveGrant = resolve;
|
|
2637
|
+
rejectGrant = reject;
|
|
2638
|
+
});
|
|
2639
|
+
const waiter = {
|
|
2640
|
+
cap,
|
|
2641
|
+
settled: false,
|
|
2642
|
+
timeoutHandle: null,
|
|
2643
|
+
grant: () => {
|
|
2644
|
+
if (settled) return;
|
|
2645
|
+
settled = true;
|
|
2646
|
+
granted = true;
|
|
2647
|
+
waiter.settled = true;
|
|
2648
|
+
if (waiter.timeoutHandle) clearTimeout(waiter.timeoutHandle);
|
|
2649
|
+
state.active += 1;
|
|
2650
|
+
resolveGrant(this.makeSlot(apiKeyId));
|
|
2651
|
+
},
|
|
2652
|
+
fail: (err) => {
|
|
2653
|
+
if (settled) return;
|
|
2654
|
+
settled = true;
|
|
2655
|
+
waiter.settled = true;
|
|
2656
|
+
if (waiter.timeoutHandle) clearTimeout(waiter.timeoutHandle);
|
|
2657
|
+
const idx = state.waiters.indexOf(waiter);
|
|
2658
|
+
if (idx >= 0) state.waiters.splice(idx, 1);
|
|
2659
|
+
rejectGrant(err);
|
|
2660
|
+
this.gc(apiKeyId);
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
waiter.timeoutHandle = setTimeout(() => {
|
|
2664
|
+
waiter.fail(new ConcurrencyWaitTimeoutError(apiKeyId, options.waitTimeoutMs));
|
|
2665
|
+
}, options.waitTimeoutMs);
|
|
2666
|
+
state.waiters.push(waiter);
|
|
2667
|
+
const cancel = () => {
|
|
2668
|
+
if (settled || granted) return;
|
|
2669
|
+
waiter.fail(new ConcurrencyWaitCancelledError(apiKeyId));
|
|
2670
|
+
};
|
|
2671
|
+
return { granted: grantedPromise, cancel };
|
|
2672
|
+
}
|
|
2673
|
+
/** Build an idempotent granted slot (`release` decrements; `cancel` is a no-op). */
|
|
2674
|
+
makeSlot(apiKeyId) {
|
|
2675
|
+
let released = false;
|
|
2676
|
+
const release2 = () => {
|
|
2677
|
+
if (released) return;
|
|
2678
|
+
released = true;
|
|
2679
|
+
const state = this.states.get(apiKeyId);
|
|
2680
|
+
if (!state) return;
|
|
2681
|
+
state.active = Math.max(0, state.active - 1);
|
|
2682
|
+
this.dispatchNext(apiKeyId);
|
|
2683
|
+
this.gc(apiKeyId);
|
|
2684
|
+
};
|
|
2685
|
+
return { release: release2, cancel: () => {
|
|
2686
|
+
} };
|
|
2687
|
+
}
|
|
2688
|
+
/** After a release freed a slot, grant the FIFO head if it fits under its cap. */
|
|
2689
|
+
dispatchNext(apiKeyId) {
|
|
2690
|
+
const state = this.states.get(apiKeyId);
|
|
2691
|
+
if (!state || state.waiters.length === 0) return;
|
|
2692
|
+
const head = state.waiters[0];
|
|
2693
|
+
if (state.active < head.cap) {
|
|
2694
|
+
state.waiters.shift();
|
|
2695
|
+
head.grant();
|
|
1795
2696
|
}
|
|
1796
2697
|
}
|
|
2698
|
+
/** Snapshot for observability — only keys with active slots or waiters. */
|
|
2699
|
+
getStatus() {
|
|
2700
|
+
const out = [];
|
|
2701
|
+
for (const [apiKeyId, state] of this.states) {
|
|
2702
|
+
if (state.active > 0 || state.waiters.length > 0) {
|
|
2703
|
+
out.push({ apiKeyId, active: state.active, waiting: state.waiters.length });
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return out;
|
|
2707
|
+
}
|
|
2708
|
+
/** Drop all state + pending timers (tests / teardown). */
|
|
2709
|
+
reset() {
|
|
2710
|
+
for (const state of this.states.values()) {
|
|
2711
|
+
for (const w of state.waiters) {
|
|
2712
|
+
if (w.timeoutHandle) clearTimeout(w.timeoutHandle);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
this.states.clear();
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
|
|
2719
|
+
// src/outbound-api/outboundRateLimiter.ts
|
|
2720
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
2721
|
+
var DEFAULT_MAX_REQUESTS = 60;
|
|
2722
|
+
var OutboundRateLimiter = class {
|
|
2723
|
+
windowMs;
|
|
2724
|
+
maxRequests;
|
|
2725
|
+
/** apiKeyId → ascending request timestamps within the current window. */
|
|
2726
|
+
hits = /* @__PURE__ */ new Map();
|
|
2727
|
+
constructor(options = {}) {
|
|
2728
|
+
this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
|
|
2729
|
+
this.maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* Record a request for `apiKeyId` and decide whether it is allowed. Prunes
|
|
2733
|
+
* timestamps older than the window first; when allowed, the request's
|
|
2734
|
+
* timestamp is appended.
|
|
2735
|
+
*
|
|
2736
|
+
* `override` (outbound-key-policy) supplies a PER-KEY window/max for this
|
|
2737
|
+
* bucket, superseding the instance defaults for THAT key. Absent ⇒ the
|
|
2738
|
+
* instance default 60/60s (byte-identical to before this change). An effective
|
|
2739
|
+
* `maxRequests` of `0` means UNLIMITED — the request is allowed and NOT
|
|
2740
|
+
* recorded (no bucket growth).
|
|
2741
|
+
*/
|
|
2742
|
+
check(apiKeyId, now = Date.now(), override) {
|
|
2743
|
+
const maxRequests = override?.maxRequests ?? this.maxRequests;
|
|
2744
|
+
const windowMs = override?.windowMs ?? this.windowMs;
|
|
2745
|
+
if (maxRequests === 0) return { allowed: true, retryAfterSeconds: 0 };
|
|
2746
|
+
const cutoff = now - windowMs;
|
|
2747
|
+
const timestamps = (this.hits.get(apiKeyId) ?? []).filter((ts) => ts > cutoff);
|
|
2748
|
+
if (timestamps.length >= maxRequests) {
|
|
2749
|
+
if (timestamps.length === 0) {
|
|
2750
|
+
this.hits.delete(apiKeyId);
|
|
2751
|
+
return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(windowMs / 1e3)) };
|
|
2752
|
+
}
|
|
2753
|
+
const oldest = timestamps[0];
|
|
2754
|
+
const retryAfterMs = Math.max(0, oldest + windowMs - now);
|
|
2755
|
+
this.hits.set(apiKeyId, timestamps);
|
|
2756
|
+
return {
|
|
2757
|
+
allowed: false,
|
|
2758
|
+
retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1e3))
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
timestamps.push(now);
|
|
2762
|
+
this.hits.set(apiKeyId, timestamps);
|
|
2763
|
+
return { allowed: true, retryAfterSeconds: 0 };
|
|
2764
|
+
}
|
|
2765
|
+
/** Drop all recorded state (tests / teardown). */
|
|
2766
|
+
reset() {
|
|
2767
|
+
this.hits.clear();
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
|
|
2771
|
+
// src/outbound-api/userMessageDetection.ts
|
|
2772
|
+
function asArray(v) {
|
|
2773
|
+
return Array.isArray(v) ? v : [];
|
|
2774
|
+
}
|
|
2775
|
+
function asObject(v) {
|
|
2776
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
2777
|
+
}
|
|
2778
|
+
function last(arr) {
|
|
2779
|
+
return arr.length > 0 ? arr[arr.length - 1] : void 0;
|
|
2780
|
+
}
|
|
2781
|
+
function isUserMessageAnthropic(body) {
|
|
2782
|
+
const messages = asArray(body["messages"]);
|
|
2783
|
+
const lastMsg = asObject(last(messages));
|
|
2784
|
+
if (!lastMsg || lastMsg["role"] !== "user") return false;
|
|
2785
|
+
const content = lastMsg["content"];
|
|
2786
|
+
if (typeof content === "string") return true;
|
|
2787
|
+
if (Array.isArray(content)) {
|
|
2788
|
+
const hasToolResult = content.some(
|
|
2789
|
+
(block) => asObject(block)?.["type"] === "tool_result"
|
|
2790
|
+
);
|
|
2791
|
+
return !hasToolResult;
|
|
2792
|
+
}
|
|
2793
|
+
return false;
|
|
2794
|
+
}
|
|
2795
|
+
function isUserMessageResponses(body) {
|
|
2796
|
+
const input = body["input"];
|
|
2797
|
+
if (typeof input === "string") return input.length > 0;
|
|
2798
|
+
if (Array.isArray(input)) {
|
|
2799
|
+
const lastItem = asObject(last(input));
|
|
2800
|
+
if (!lastItem) return false;
|
|
2801
|
+
if (lastItem["type"] === "function_call_output") return false;
|
|
2802
|
+
return lastItem["role"] === "user";
|
|
2803
|
+
}
|
|
1797
2804
|
return false;
|
|
1798
2805
|
}
|
|
1799
|
-
function
|
|
1800
|
-
const messages = body["messages"];
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2806
|
+
function isUserMessageChat(body) {
|
|
2807
|
+
const messages = asArray(body["messages"]);
|
|
2808
|
+
const lastMsg = asObject(last(messages));
|
|
2809
|
+
if (!lastMsg) return false;
|
|
2810
|
+
return lastMsg["role"] === "user";
|
|
2811
|
+
}
|
|
2812
|
+
function isUserMessageGemini(body) {
|
|
2813
|
+
const contents = asArray(body["contents"]);
|
|
2814
|
+
const lastContent = asObject(last(contents));
|
|
2815
|
+
if (!lastContent || lastContent["role"] !== "user") return false;
|
|
2816
|
+
const parts = asArray(lastContent["parts"]);
|
|
2817
|
+
const hasFunctionResponse = parts.some(
|
|
2818
|
+
(part) => asObject(part)?.["functionResponse"] !== void 0
|
|
2819
|
+
);
|
|
2820
|
+
return !hasFunctionResponse;
|
|
2821
|
+
}
|
|
2822
|
+
function isUserMessageRequest(endpoint, parsedBody) {
|
|
2823
|
+
const body = asObject(parsedBody);
|
|
2824
|
+
if (!body) return false;
|
|
2825
|
+
switch (endpoint) {
|
|
2826
|
+
case "messages":
|
|
2827
|
+
return isUserMessageAnthropic(body);
|
|
2828
|
+
case "responses":
|
|
2829
|
+
return isUserMessageResponses(body);
|
|
2830
|
+
case "chat":
|
|
2831
|
+
return isUserMessageChat(body);
|
|
2832
|
+
case "gemini":
|
|
2833
|
+
return isUserMessageGemini(body);
|
|
2834
|
+
default:
|
|
2835
|
+
return false;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// src/outbound-api/userMessageSerialQueue.ts
|
|
2840
|
+
var SerialQueueTimeoutError = class extends Error {
|
|
2841
|
+
constructor(providerId, waitTimeoutMs) {
|
|
2842
|
+
super(`user-message serial queue wait timed out after ${waitTimeoutMs}ms for '${providerId}'`);
|
|
2843
|
+
this.providerId = providerId;
|
|
2844
|
+
this.waitTimeoutMs = waitTimeoutMs;
|
|
2845
|
+
this.name = "SerialQueueTimeoutError";
|
|
2846
|
+
}
|
|
2847
|
+
providerId;
|
|
2848
|
+
waitTimeoutMs;
|
|
2849
|
+
code = "serial_queue_timeout";
|
|
2850
|
+
};
|
|
2851
|
+
function isSerialQueueTimeout(err) {
|
|
2852
|
+
return err instanceof SerialQueueTimeoutError;
|
|
2853
|
+
}
|
|
2854
|
+
var UserMessageSerialQueue = class {
|
|
2855
|
+
states = /* @__PURE__ */ new Map();
|
|
2856
|
+
ensure(providerId) {
|
|
2857
|
+
let state = this.states.get(providerId);
|
|
2858
|
+
if (!state) {
|
|
2859
|
+
state = { holding: false, lastReleaseTs: null, waiters: [], dispatchHandle: null };
|
|
2860
|
+
this.states.set(providerId, state);
|
|
2861
|
+
}
|
|
2862
|
+
return state;
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Acquire the serial slot for `providerId`. Resolves immediately when the slot
|
|
2866
|
+
* is free, no one is queued, and the `delayMs` gap since the last release has
|
|
2867
|
+
* elapsed; otherwise enqueues FIFO and resolves in order (or rejects with a
|
|
2868
|
+
* {@link SerialQueueTimeoutError} after `waitTimeoutMs`).
|
|
2869
|
+
*/
|
|
2870
|
+
acquire(providerId, options) {
|
|
2871
|
+
const { waitTimeoutMs, delayMs } = options;
|
|
2872
|
+
const now = options.now ?? Date.now();
|
|
2873
|
+
const state = this.ensure(providerId);
|
|
2874
|
+
const spacingElapsed = state.lastReleaseTs === null || now - state.lastReleaseTs >= delayMs;
|
|
2875
|
+
if (!state.holding && state.waiters.length === 0 && spacingElapsed) {
|
|
2876
|
+
state.holding = true;
|
|
2877
|
+
return Promise.resolve(this.makeSlot(providerId));
|
|
2878
|
+
}
|
|
2879
|
+
return new Promise((resolve, reject) => {
|
|
2880
|
+
const waiter = {
|
|
2881
|
+
delayMs,
|
|
2882
|
+
resolve,
|
|
2883
|
+
reject,
|
|
2884
|
+
timeoutHandle: null,
|
|
2885
|
+
done: false
|
|
2886
|
+
};
|
|
2887
|
+
waiter.timeoutHandle = setTimeout(() => {
|
|
2888
|
+
if (waiter.done) return;
|
|
2889
|
+
waiter.done = true;
|
|
2890
|
+
const idx = state.waiters.indexOf(waiter);
|
|
2891
|
+
if (idx >= 0) state.waiters.splice(idx, 1);
|
|
2892
|
+
reject(new SerialQueueTimeoutError(providerId, waitTimeoutMs));
|
|
2893
|
+
}, waitTimeoutMs);
|
|
2894
|
+
state.waiters.push(waiter);
|
|
2895
|
+
this.maybeDispatch(providerId);
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
/** Build an idempotent release for a granted slot. */
|
|
2899
|
+
makeSlot(providerId) {
|
|
2900
|
+
let released = false;
|
|
2901
|
+
return {
|
|
2902
|
+
release: () => {
|
|
2903
|
+
if (released) return;
|
|
2904
|
+
released = true;
|
|
2905
|
+
const state = this.states.get(providerId);
|
|
2906
|
+
if (!state) return;
|
|
2907
|
+
state.holding = false;
|
|
2908
|
+
state.lastReleaseTs = Date.now();
|
|
2909
|
+
this.maybeDispatch(providerId);
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Grant the head waiter when the slot is free and the head's `delayMs` gap has
|
|
2915
|
+
* elapsed; otherwise schedule a dispatch for the residual gap. No-op while the
|
|
2916
|
+
* slot is held (release re-invokes this) or the queue is empty.
|
|
2917
|
+
*/
|
|
2918
|
+
maybeDispatch(providerId) {
|
|
2919
|
+
const state = this.states.get(providerId);
|
|
2920
|
+
if (!state || state.holding || state.waiters.length === 0) return;
|
|
2921
|
+
const head = state.waiters[0];
|
|
2922
|
+
const now = Date.now();
|
|
2923
|
+
const elapsed = state.lastReleaseTs === null ? Infinity : now - state.lastReleaseTs;
|
|
2924
|
+
const residual = Math.max(0, head.delayMs - elapsed);
|
|
2925
|
+
if (residual <= 0) {
|
|
2926
|
+
this.grantHead(providerId);
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (state.dispatchHandle !== null) return;
|
|
2930
|
+
state.dispatchHandle = setTimeout(() => {
|
|
2931
|
+
state.dispatchHandle = null;
|
|
2932
|
+
this.grantHead(providerId);
|
|
2933
|
+
}, residual);
|
|
2934
|
+
}
|
|
2935
|
+
/** Hand the slot to the FIFO head (if free and someone is waiting). */
|
|
2936
|
+
grantHead(providerId) {
|
|
2937
|
+
const state = this.states.get(providerId);
|
|
2938
|
+
if (!state || state.holding || state.waiters.length === 0) return;
|
|
2939
|
+
if (state.dispatchHandle !== null) {
|
|
2940
|
+
clearTimeout(state.dispatchHandle);
|
|
2941
|
+
state.dispatchHandle = null;
|
|
2942
|
+
}
|
|
2943
|
+
const head = state.waiters.shift();
|
|
2944
|
+
if (!head) return;
|
|
2945
|
+
if (head.timeoutHandle) clearTimeout(head.timeoutHandle);
|
|
2946
|
+
head.done = true;
|
|
2947
|
+
state.holding = true;
|
|
2948
|
+
head.resolve(this.makeSlot(providerId));
|
|
2949
|
+
}
|
|
2950
|
+
/** Snapshot for observability — only keys holding a slot or with waiters. */
|
|
2951
|
+
getStatus() {
|
|
2952
|
+
const out = [];
|
|
2953
|
+
for (const [providerId, state] of this.states) {
|
|
2954
|
+
if (state.holding || state.waiters.length > 0) {
|
|
2955
|
+
out.push({ providerId, holding: state.holding, waiting: state.waiters.length });
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
return out;
|
|
2959
|
+
}
|
|
2960
|
+
/** Drop all state + pending timers (tests / teardown). */
|
|
2961
|
+
reset() {
|
|
2962
|
+
for (const state of this.states.values()) {
|
|
2963
|
+
if (state.dispatchHandle) clearTimeout(state.dispatchHandle);
|
|
2964
|
+
for (const w of state.waiters) {
|
|
2965
|
+
if (w.timeoutHandle) clearTimeout(w.timeoutHandle);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
this.states.clear();
|
|
2969
|
+
}
|
|
2970
|
+
};
|
|
2971
|
+
|
|
2972
|
+
// src/outbound-api/voucher.ts
|
|
2973
|
+
import { createHash as createHash2 } from "crypto";
|
|
2974
|
+
var CODE_ENTROPY_CHARS = 32;
|
|
2975
|
+
var CODE_PREFIX = "CC_";
|
|
2976
|
+
var DISPLAY_PREFIX_LEN = 8;
|
|
2977
|
+
var DAY_MS2 = 864e5;
|
|
2978
|
+
function generateVoucherCode() {
|
|
2979
|
+
return CODE_PREFIX + randomBase62(CODE_ENTROPY_CHARS);
|
|
2980
|
+
}
|
|
2981
|
+
function hashVoucherCode(code) {
|
|
2982
|
+
return createHash2("sha256").update(code, "utf8").digest("hex");
|
|
2983
|
+
}
|
|
2984
|
+
function voucherCodePrefix(code) {
|
|
2985
|
+
return code.slice(0, DISPLAY_PREFIX_LEN);
|
|
2986
|
+
}
|
|
2987
|
+
function newVoucherId() {
|
|
2988
|
+
return `vch_${Date.now()}_${randomBase62(8)}`;
|
|
2989
|
+
}
|
|
2990
|
+
function computeVoucherGrant(voucher, currentKey, now) {
|
|
2991
|
+
if (voucher.type === "credit") {
|
|
2992
|
+
const current = currentKey?.totalCostLimitUsd ?? 0;
|
|
2993
|
+
let granted2 = current + (voucher.creditUsd ?? 0);
|
|
2994
|
+
if (voucher.maxTotalCostLimitUsd != null) {
|
|
2995
|
+
granted2 = Math.min(granted2, voucher.maxTotalCostLimitUsd);
|
|
2996
|
+
}
|
|
2997
|
+
return { totalCostLimitUsd: granted2 };
|
|
2998
|
+
}
|
|
2999
|
+
const currentExpiry = currentKey?.expiresAt ?? now;
|
|
3000
|
+
let granted = currentExpiry + (voucher.renewalDays ?? 0) * DAY_MS2;
|
|
3001
|
+
if (voucher.maxExpiryDays != null) {
|
|
3002
|
+
granted = Math.min(granted, now + voucher.maxExpiryDays * DAY_MS2);
|
|
3003
|
+
}
|
|
3004
|
+
return { expiresAt: granted };
|
|
3005
|
+
}
|
|
3006
|
+
function toVoucherInfo(v) {
|
|
3007
|
+
const info = {
|
|
3008
|
+
id: v.id,
|
|
3009
|
+
codePrefix: v.codePrefix,
|
|
3010
|
+
type: v.type,
|
|
3011
|
+
status: v.status,
|
|
3012
|
+
createdAt: v.createdAt
|
|
3013
|
+
};
|
|
3014
|
+
if (v.creditUsd != null) info.creditUsd = v.creditUsd;
|
|
3015
|
+
if (v.renewalDays != null) info.renewalDays = v.renewalDays;
|
|
3016
|
+
if (v.maxTotalCostLimitUsd != null) info.maxTotalCostLimitUsd = v.maxTotalCostLimitUsd;
|
|
3017
|
+
if (v.maxExpiryDays != null) info.maxExpiryDays = v.maxExpiryDays;
|
|
3018
|
+
if (v.redeemedAt != null) info.redeemedAt = v.redeemedAt;
|
|
3019
|
+
if (v.redeemedByKeyId != null) info.redeemedByKeyId = v.redeemedByKeyId;
|
|
3020
|
+
if (v.grantApplied != null) info.grantApplied = v.grantApplied;
|
|
3021
|
+
if (v.grantedTotalCostLimitUsd != null) info.grantedTotalCostLimitUsd = v.grantedTotalCostLimitUsd;
|
|
3022
|
+
if (v.grantedExpiresAt != null) info.grantedExpiresAt = v.grantedExpiresAt;
|
|
3023
|
+
if (v.revokedAt != null) info.revokedAt = v.revokedAt;
|
|
3024
|
+
return info;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/outbound-api/voucherRedeem.ts
|
|
3028
|
+
function isRedeemRequest(method, url) {
|
|
3029
|
+
if (method !== "POST" || !url) return false;
|
|
3030
|
+
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
3031
|
+
return path.endsWith("/redeem");
|
|
3032
|
+
}
|
|
3033
|
+
function writeErr(res, status, message, headers = {}) {
|
|
3034
|
+
if (res.headersSent) return;
|
|
3035
|
+
res.writeHead(status, { "Content-Type": "application/json", ...headers });
|
|
3036
|
+
res.end(JSON.stringify({ error: { type: "voucher_error", message } }));
|
|
3037
|
+
}
|
|
3038
|
+
function writeResult(res, result) {
|
|
3039
|
+
if (res.headersSent) return;
|
|
3040
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3041
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
1810
3042
|
}
|
|
1811
|
-
function
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
if (part?.type === "input_image") return true;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
if (item?.type === "input_image") return true;
|
|
1822
|
-
}
|
|
1823
|
-
return false;
|
|
3043
|
+
function readBody2(req) {
|
|
3044
|
+
return new Promise((resolve, reject) => {
|
|
3045
|
+
const chunks = [];
|
|
3046
|
+
req.on("data", (c) => chunks.push(c));
|
|
3047
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
3048
|
+
req.on("error", reject);
|
|
3049
|
+
});
|
|
1824
3050
|
}
|
|
1825
|
-
function
|
|
1826
|
-
const
|
|
1827
|
-
if (
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
3051
|
+
function grantToResult(type, granted) {
|
|
3052
|
+
const result = { type };
|
|
3053
|
+
if (granted.totalCostLimitUsd != null) result.totalCostLimitUsd = granted.totalCostLimitUsd;
|
|
3054
|
+
if (granted.expiresAt != null) result.expiresAt = granted.expiresAt;
|
|
3055
|
+
return result;
|
|
3056
|
+
}
|
|
3057
|
+
function currentResult(type, keyRow) {
|
|
3058
|
+
const result = { type };
|
|
3059
|
+
if (type === "credit") {
|
|
3060
|
+
if (keyRow?.totalCostLimitUsd != null) result.totalCostLimitUsd = keyRow.totalCostLimitUsd;
|
|
3061
|
+
} else if (keyRow?.expiresAt != null) {
|
|
3062
|
+
result.expiresAt = keyRow.expiresAt;
|
|
3063
|
+
}
|
|
3064
|
+
return result;
|
|
3065
|
+
}
|
|
3066
|
+
function recordedAbsolute(v) {
|
|
3067
|
+
const granted = {};
|
|
3068
|
+
if (v.grantedTotalCostLimitUsd != null) granted.totalCostLimitUsd = v.grantedTotalCostLimitUsd;
|
|
3069
|
+
if (v.grantedExpiresAt != null) granted.expiresAt = v.grantedExpiresAt;
|
|
3070
|
+
return granted;
|
|
3071
|
+
}
|
|
3072
|
+
function policyFrom(granted) {
|
|
3073
|
+
const policy = {};
|
|
3074
|
+
if (granted.totalCostLimitUsd != null) policy.totalCostLimitUsd = granted.totalCostLimitUsd;
|
|
3075
|
+
if (granted.expiresAt != null) policy.expiresAt = granted.expiresAt;
|
|
3076
|
+
return policy;
|
|
3077
|
+
}
|
|
3078
|
+
var NOT_REDEEMABLE = "Invalid or non-redeemable code";
|
|
3079
|
+
var fallbackMutex = null;
|
|
3080
|
+
function getFallbackMutex() {
|
|
3081
|
+
fallbackMutex ??= new KeyedMutex();
|
|
3082
|
+
return fallbackMutex;
|
|
3083
|
+
}
|
|
3084
|
+
async function handleVoucherRedeem(req, res, deps, voucherEnabled, redeemLimiter, verifiedKeyId, presentedKey, now, redeemMutex) {
|
|
3085
|
+
if (!voucherEnabled || !deps.voucherDb) {
|
|
3086
|
+
writeErr(res, 403, "Voucher redemption is not enabled");
|
|
3087
|
+
return;
|
|
1835
3088
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
return anthropicHasVision(body);
|
|
1842
|
-
case "openai-chat":
|
|
1843
|
-
return openaiChatHasVision(body);
|
|
1844
|
-
case "openai-responses":
|
|
1845
|
-
return openaiResponsesHasVision(body);
|
|
1846
|
-
case "gemini-generatecontent":
|
|
1847
|
-
return geminiHasVision(body);
|
|
1848
|
-
default:
|
|
1849
|
-
return false;
|
|
3089
|
+
const voucherDb = deps.voucherDb;
|
|
3090
|
+
const rl = redeemLimiter.check(verifiedKeyId, now);
|
|
3091
|
+
if (!rl.allowed) {
|
|
3092
|
+
writeErr(res, 429, "Too many redeem attempts", { "Retry-After": String(rl.retryAfterSeconds) });
|
|
3093
|
+
return;
|
|
1850
3094
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
const reqNorm = normalizeModelId(requestedModel);
|
|
1859
|
-
const inOverride = overrides.some((id) => {
|
|
1860
|
-
if (!id) return false;
|
|
1861
|
-
const candidate = id.includes(",") ? id.slice(id.indexOf(",") + 1) : id;
|
|
1862
|
-
return normalizeModelId(candidate) === reqNorm || candidate === requestedModel;
|
|
1863
|
-
});
|
|
1864
|
-
if (inOverride) return "background";
|
|
1865
|
-
}
|
|
1866
|
-
if (isBackgroundTierModel(requestedModel)) return "background";
|
|
3095
|
+
const raw = await readBody2(req);
|
|
3096
|
+
let code;
|
|
3097
|
+
try {
|
|
3098
|
+
code = raw ? JSON.parse(raw)["code"] : void 0;
|
|
3099
|
+
} catch {
|
|
3100
|
+
writeErr(res, 400, "Invalid JSON in request body");
|
|
3101
|
+
return;
|
|
1867
3102
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
switch (endpoint) {
|
|
1872
|
-
case "chat":
|
|
1873
|
-
return "openai-chat";
|
|
1874
|
-
case "responses":
|
|
1875
|
-
return "openai-responses";
|
|
1876
|
-
case "messages":
|
|
1877
|
-
return "anthropic-messages";
|
|
1878
|
-
case "gemini":
|
|
1879
|
-
return "gemini-generatecontent";
|
|
3103
|
+
if (typeof code !== "string" || !code.trim()) {
|
|
3104
|
+
writeErr(res, 400, "Missing redemption code");
|
|
3105
|
+
return;
|
|
1880
3106
|
}
|
|
3107
|
+
const codeHash = hashVoucherCode(code.trim());
|
|
3108
|
+
const keyHash = hashKey(presentedKey.trim());
|
|
3109
|
+
const applyFirstPass = async (voucher, absolute) => {
|
|
3110
|
+
const ok = await deps.db.outboundApiKeysSetPolicy(verifiedKeyId, policyFrom(absolute));
|
|
3111
|
+
if (!ok) {
|
|
3112
|
+
await voucherDb.voucherRevertRedeem(voucher.id, verifiedKeyId);
|
|
3113
|
+
return { ok: false, status: 409, message: "Could not apply the grant \u2014 please retry" };
|
|
3114
|
+
}
|
|
3115
|
+
await voucherDb.voucherMarkGrantApplied(voucher.id);
|
|
3116
|
+
return { ok: true, result: grantToResult(voucher.type, absolute) };
|
|
3117
|
+
};
|
|
3118
|
+
const reconcileOne = async (voucher) => {
|
|
3119
|
+
const ok = await deps.db.outboundApiKeysSetPolicy(verifiedKeyId, policyFrom(recordedAbsolute(voucher)));
|
|
3120
|
+
if (ok) await voucherDb.voucherMarkGrantApplied(voucher.id);
|
|
3121
|
+
};
|
|
3122
|
+
const reconcileStranded = async () => {
|
|
3123
|
+
const all = await voucherDb.voucherList();
|
|
3124
|
+
const stranded = all.filter((v) => v.status === "redeemed" && v.grantApplied !== true && v.redeemedByKeyId === verifiedKeyId).sort((a, b) => (a.redeemedAt ?? 0) - (b.redeemedAt ?? 0));
|
|
3125
|
+
for (const v of stranded) await reconcileOne(v);
|
|
3126
|
+
};
|
|
3127
|
+
const runCritical = async () => {
|
|
3128
|
+
await reconcileStranded();
|
|
3129
|
+
const voucher = await voucherDb.voucherGetByHash(codeHash);
|
|
3130
|
+
if (!voucher || voucher.status === "revoked") {
|
|
3131
|
+
return { ok: false, status: 400, message: NOT_REDEEMABLE };
|
|
3132
|
+
}
|
|
3133
|
+
if (voucher.status === "redeemed") {
|
|
3134
|
+
if (voucher.redeemedByKeyId !== verifiedKeyId) {
|
|
3135
|
+
return { ok: false, status: 400, message: NOT_REDEEMABLE };
|
|
3136
|
+
}
|
|
3137
|
+
if (voucher.grantApplied === true) {
|
|
3138
|
+
const keyRow2 = await deps.db.outboundApiKeysGetByHash(keyHash);
|
|
3139
|
+
return { ok: true, result: currentResult(voucher.type, keyRow2) };
|
|
3140
|
+
}
|
|
3141
|
+
return await applyFirstPass(voucher, recordedAbsolute(voucher));
|
|
3142
|
+
}
|
|
3143
|
+
const keyRow = await deps.db.outboundApiKeysGetByHash(keyHash);
|
|
3144
|
+
const absolute = computeVoucherGrant(voucher, keyRow, now);
|
|
3145
|
+
const won = await voucherDb.voucherRedeemCas(voucher.id, verifiedKeyId, absolute, now);
|
|
3146
|
+
if (!won) {
|
|
3147
|
+
const after = await voucherDb.voucherGetByHash(codeHash);
|
|
3148
|
+
if (after?.status === "redeemed" && after.redeemedByKeyId === verifiedKeyId) {
|
|
3149
|
+
return after.grantApplied === true ? { ok: true, result: currentResult(after.type, await deps.db.outboundApiKeysGetByHash(keyHash)) } : await applyFirstPass(after, recordedAbsolute(after));
|
|
3150
|
+
}
|
|
3151
|
+
return { ok: false, status: 409, message: "Code was already redeemed" };
|
|
3152
|
+
}
|
|
3153
|
+
return await applyFirstPass(voucher, absolute);
|
|
3154
|
+
};
|
|
3155
|
+
const mutex = redeemMutex ?? getFallbackMutex();
|
|
3156
|
+
const outcome = await mutex.runExclusive(verifiedKeyId, runCritical);
|
|
3157
|
+
if (outcome.ok) writeResult(res, outcome.result);
|
|
3158
|
+
else writeErr(res, outcome.status, outcome.message);
|
|
1881
3159
|
}
|
|
1882
3160
|
|
|
1883
3161
|
// src/outbound-api/outboundApiRouter.ts
|
|
3162
|
+
var fallbackRedeemLimiter = null;
|
|
3163
|
+
function getFallbackRedeemLimiter() {
|
|
3164
|
+
fallbackRedeemLimiter ??= new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
|
|
3165
|
+
return fallbackRedeemLimiter;
|
|
3166
|
+
}
|
|
1884
3167
|
function writeJsonError(res, status, message, headers = {}) {
|
|
1885
3168
|
if (res.headersSent) return;
|
|
1886
3169
|
res.writeHead(status, { "Content-Type": "application/json", ...headers });
|
|
1887
3170
|
res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
|
|
1888
3171
|
}
|
|
3172
|
+
function writeCostLimitError(res, scope, limitUsd, spentUsd) {
|
|
3173
|
+
if (res.headersSent) return;
|
|
3174
|
+
res.writeHead(402, { "Content-Type": "application/json" });
|
|
3175
|
+
res.end(
|
|
3176
|
+
JSON.stringify({
|
|
3177
|
+
error: {
|
|
3178
|
+
type: "outbound_api_error",
|
|
3179
|
+
message: `Cost limit reached for this key (${scope}): $${spentUsd.toFixed(4)} of $${limitUsd.toFixed(4)}`
|
|
3180
|
+
},
|
|
3181
|
+
scope,
|
|
3182
|
+
limitUsd,
|
|
3183
|
+
spentUsd
|
|
3184
|
+
})
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
function writeModelNotAllowedError(res, model, mode) {
|
|
3188
|
+
if (res.headersSent) return;
|
|
3189
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
3190
|
+
res.end(
|
|
3191
|
+
JSON.stringify({
|
|
3192
|
+
error: {
|
|
3193
|
+
type: "outbound_api_error",
|
|
3194
|
+
message: `Model '${model}' is not permitted for this key (${mode})`
|
|
3195
|
+
},
|
|
3196
|
+
model,
|
|
3197
|
+
mode
|
|
3198
|
+
})
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
1889
3201
|
function extractPresentedKey(req) {
|
|
1890
3202
|
const auth = req.headers["authorization"];
|
|
1891
3203
|
if (typeof auth === "string" && auth.trim()) {
|
|
@@ -1917,7 +3229,18 @@ function extractGeminiModelFromUrl(url) {
|
|
|
1917
3229
|
const m = /\/models\/([^/:]+):(?:stream)?[gG]enerateContent$/.exec(path.replace(/\/+$/, ""));
|
|
1918
3230
|
return m ? decodeURIComponent(m[1]) : void 0;
|
|
1919
3231
|
}
|
|
1920
|
-
function
|
|
3232
|
+
function isModelsListRequest(url) {
|
|
3233
|
+
if (!url) return false;
|
|
3234
|
+
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
3235
|
+
return path.endsWith("/models");
|
|
3236
|
+
}
|
|
3237
|
+
function writeChatModelsList(res, config) {
|
|
3238
|
+
const chat = config.endpoints.find((e) => e.endpoint === "chat");
|
|
3239
|
+
const data = (chat?.models ?? []).map((ref) => parseModelRef(ref)).filter((p) => p !== null).map((p) => ({ id: p.modelId, object: "model", owned_by: "omnicross" }));
|
|
3240
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3241
|
+
res.end(JSON.stringify({ object: "list", data }));
|
|
3242
|
+
}
|
|
3243
|
+
function readBody3(req) {
|
|
1921
3244
|
return new Promise((resolve, reject) => {
|
|
1922
3245
|
const chunks = [];
|
|
1923
3246
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -1930,20 +3253,48 @@ function shimAuthHeader(req, routeToken) {
|
|
|
1930
3253
|
delete req.headers["x-goog-api-key"];
|
|
1931
3254
|
delete req.headers["x-api-key"];
|
|
1932
3255
|
}
|
|
1933
|
-
async function handleOutboundRequest(req, res, deps, config, rateLimiter) {
|
|
3256
|
+
async function handleOutboundRequest(req, res, deps, config, rateLimiter, serialQueue, concurrencyGate, redeemLimiter, redeemMutex) {
|
|
3257
|
+
const now = Date.now();
|
|
3258
|
+
const audit = beginAuditCapture(req, res, now);
|
|
3259
|
+
const billing = beginBillingCapture(req, res, now);
|
|
1934
3260
|
const presented = extractPresentedKey(req);
|
|
1935
|
-
const
|
|
1936
|
-
if (
|
|
1937
|
-
writeJsonError(
|
|
3261
|
+
const verification = await verifyKey(deps.db, presented, now);
|
|
3262
|
+
if (verification.status !== "ok") {
|
|
3263
|
+
writeJsonError(
|
|
3264
|
+
res,
|
|
3265
|
+
401,
|
|
3266
|
+
verification.status === "expired" ? "API key has expired" : "Invalid or missing API key"
|
|
3267
|
+
);
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
const verified = verification.key;
|
|
3271
|
+
if (audit) audit.keyId = verified.id;
|
|
3272
|
+
if (billing) billing.keyId = verified.id;
|
|
3273
|
+
if (isRedeemRequest(req.method, req.url)) {
|
|
3274
|
+
await handleVoucherRedeem(
|
|
3275
|
+
req,
|
|
3276
|
+
res,
|
|
3277
|
+
deps,
|
|
3278
|
+
config.voucher?.enabled === true,
|
|
3279
|
+
redeemLimiter ?? getFallbackRedeemLimiter(),
|
|
3280
|
+
verified.id,
|
|
3281
|
+
presented ?? "",
|
|
3282
|
+
now,
|
|
3283
|
+
redeemMutex
|
|
3284
|
+
);
|
|
1938
3285
|
return;
|
|
1939
3286
|
}
|
|
1940
|
-
const decision = rateLimiter.check(verified.id);
|
|
3287
|
+
const decision = rateLimiter.check(verified.id, now, verified.rateLimit);
|
|
1941
3288
|
if (!decision.allowed) {
|
|
1942
3289
|
writeJsonError(res, 429, "Rate limit exceeded", {
|
|
1943
3290
|
"Retry-After": String(decision.retryAfterSeconds)
|
|
1944
3291
|
});
|
|
1945
3292
|
return;
|
|
1946
3293
|
}
|
|
3294
|
+
if (req.method === "GET" && isModelsListRequest(req.url)) {
|
|
3295
|
+
writeChatModelsList(res, config);
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
1947
3298
|
const endpoint = selectEndpoint(req.method, req.url);
|
|
1948
3299
|
if (!endpoint) {
|
|
1949
3300
|
writeJsonError(res, 404, `Unsupported: ${req.method} ${req.url}`);
|
|
@@ -1954,53 +3305,165 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter) {
|
|
|
1954
3305
|
writeJsonError(res, 503, `endpoint '${endpoint}' is not configured`);
|
|
1955
3306
|
return;
|
|
1956
3307
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
3308
|
+
if (verified.costLimits && deps.keySpendTracker) {
|
|
3309
|
+
const spend = await deps.keySpendTracker.getSpend(verified.id, now);
|
|
3310
|
+
const quota = checkKeyQuota(verified.costLimits, spend);
|
|
3311
|
+
if (!quota.allowed) {
|
|
3312
|
+
emitWebhookEvent({
|
|
3313
|
+
kind: "key.quotaExceeded",
|
|
3314
|
+
at: now,
|
|
3315
|
+
keyId: verified.id,
|
|
3316
|
+
scope: quota.scope,
|
|
3317
|
+
limitUsd: quota.limitUsd,
|
|
3318
|
+
spentUsd: quota.spentUsd
|
|
3319
|
+
});
|
|
3320
|
+
writeCostLimitError(res, quota.scope, quota.limitUsd, quota.spentUsd);
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
for (const warn of computeQuotaWarnings(verified.costLimits, spend)) {
|
|
3324
|
+
if (markQuotaWarnedOnce(verified.id, warn.scope, warn.windowStart)) {
|
|
3325
|
+
emitWebhookEvent({
|
|
3326
|
+
kind: "key.quotaWarning",
|
|
3327
|
+
at: now,
|
|
3328
|
+
keyId: verified.id,
|
|
3329
|
+
scope: warn.scope,
|
|
3330
|
+
limitUsd: warn.limitUsd,
|
|
3331
|
+
spentUsd: warn.spentUsd
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
1969
3335
|
}
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
3336
|
+
const concurrencyLimit = verified.maxConcurrency;
|
|
3337
|
+
let releaseConcurrency = null;
|
|
3338
|
+
if (typeof concurrencyLimit === "number" && concurrencyLimit > 0) {
|
|
3339
|
+
const cq = config.concurrencyQueue ?? DEFAULT_CONCURRENCY_QUEUE;
|
|
3340
|
+
const acquisition = concurrencyGate.acquire(verified.id, concurrencyLimit, {
|
|
3341
|
+
maxQueueSizeFactor: cq.maxQueueSizeFactor,
|
|
3342
|
+
minQueueSize: cq.minQueueSize,
|
|
3343
|
+
waitTimeoutMs: cq.waitTimeoutMs
|
|
3344
|
+
});
|
|
3345
|
+
const cancelOnClose = () => acquisition.cancel();
|
|
3346
|
+
res.once("close", cancelOnClose);
|
|
3347
|
+
let slot;
|
|
3348
|
+
try {
|
|
3349
|
+
slot = await acquisition.granted;
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
res.removeListener("close", cancelOnClose);
|
|
3352
|
+
if (isConcurrencyRejection(err)) {
|
|
3353
|
+
writeJsonError(res, 429, "Concurrency limit exceeded", { "Retry-After": "5" });
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
throw err;
|
|
3357
|
+
}
|
|
3358
|
+
res.removeListener("close", cancelOnClose);
|
|
3359
|
+
const release2 = () => slot.release();
|
|
3360
|
+
releaseConcurrency = release2;
|
|
3361
|
+
res.once("close", release2);
|
|
1991
3362
|
}
|
|
1992
|
-
const routeMap = deps.providerProxy.getRouteMap();
|
|
1993
|
-
const token = routeMap.addRoute(resolved.route);
|
|
1994
3363
|
try {
|
|
1995
|
-
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
3364
|
+
const ingressFormat = endpointToIngressFormat(endpoint);
|
|
3365
|
+
const rawBody = await readBody3(req);
|
|
3366
|
+
let parsedBody = {};
|
|
3367
|
+
try {
|
|
3368
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
3369
|
+
} catch {
|
|
3370
|
+
writeJsonError(res, 400, "Invalid JSON in request body");
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
if (audit) audit.setRequestBody(rawBody);
|
|
3374
|
+
if (endpoint === "gemini" && typeof parsedBody["model"] !== "string") {
|
|
3375
|
+
const urlModel = extractGeminiModelFromUrl(req.url);
|
|
3376
|
+
if (urlModel) parsedBody["model"] = urlModel;
|
|
3377
|
+
}
|
|
3378
|
+
const sessionId = deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}` : null;
|
|
3379
|
+
const resolved = isKindMappedEndpoint(endpoint) || endpoint === "chat" ? await resolveRoute({
|
|
3380
|
+
config: endpointConfig,
|
|
3381
|
+
ingressFormat,
|
|
3382
|
+
llmConfig: deps.llmConfig,
|
|
3383
|
+
sessionId,
|
|
3384
|
+
// Capture the ORIGINAL requested id BEFORE any downstream swap; for
|
|
3385
|
+
// kind-mapped endpoints it selects the kind AND is stamped onto
|
|
3386
|
+
// `route.requestedModel`; for chat it is matched against the list.
|
|
3387
|
+
requestedModel: extractRequestedModel(ingressFormat, parsedBody),
|
|
3388
|
+
// Attribution: stamp the verified named-key id onto the route.
|
|
3389
|
+
apiKeyId: verified.id
|
|
3390
|
+
}) : await resolveRoute({
|
|
3391
|
+
config: endpointConfig,
|
|
3392
|
+
role: detectRequestRole(ingressFormat, parsedBody, {
|
|
3393
|
+
backgroundModelIds: endpointConfig.backgroundModelIds
|
|
3394
|
+
}),
|
|
3395
|
+
ingressFormat,
|
|
3396
|
+
llmConfig: deps.llmConfig,
|
|
3397
|
+
sessionId,
|
|
3398
|
+
// Attribution: stamp the verified named-key id onto the route.
|
|
3399
|
+
apiKeyId: verified.id
|
|
3400
|
+
});
|
|
3401
|
+
if (!resolved.ok) {
|
|
3402
|
+
writeJsonError(res, resolved.error.status, resolved.error.message);
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
if (audit) {
|
|
3406
|
+
audit.model = resolved.route.model;
|
|
3407
|
+
audit.provider = resolved.route.providerId;
|
|
3408
|
+
}
|
|
3409
|
+
if (billing) {
|
|
3410
|
+
billing.model = resolved.route.model;
|
|
3411
|
+
billing.provider = resolved.route.providerId;
|
|
3412
|
+
billing.authMode = resolved.route.authMode;
|
|
3413
|
+
}
|
|
3414
|
+
if (verified.modelRestriction) {
|
|
3415
|
+
const modelDecision = checkModelAllowed(verified.modelRestriction, resolved.route.model);
|
|
3416
|
+
if (!modelDecision.allowed) {
|
|
3417
|
+
writeModelNotAllowedError(res, modelDecision.model, verified.modelRestriction.mode);
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
let releaseSerial = null;
|
|
3422
|
+
const providerKey = resolved.route.providerId;
|
|
3423
|
+
if (config.userMessageQueue?.enabled && providerKey && isUserMessageRequest(endpoint, parsedBody)) {
|
|
3424
|
+
const umq = config.userMessageQueue;
|
|
3425
|
+
try {
|
|
3426
|
+
const serialSlot = await serialQueue.acquire(providerKey, {
|
|
3427
|
+
waitTimeoutMs: umq.waitTimeoutMs,
|
|
3428
|
+
delayMs: umq.delayMs
|
|
3429
|
+
});
|
|
3430
|
+
releaseSerial = () => serialSlot.release();
|
|
3431
|
+
} catch (err) {
|
|
3432
|
+
if (isSerialQueueTimeout(err)) {
|
|
3433
|
+
writeJsonError(res, 503, "User-message serial queue wait timed out");
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
throw err;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
if (releaseSerial) {
|
|
3440
|
+
const original = res.writeHead.bind(res);
|
|
3441
|
+
const releaseOnce = releaseSerial;
|
|
3442
|
+
res.writeHead = ((...args) => {
|
|
3443
|
+
res.writeHead = original;
|
|
3444
|
+
releaseOnce();
|
|
3445
|
+
return original(...args);
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
const routeMap = deps.providerProxy.getRouteMap();
|
|
3449
|
+
const token = routeMap.addRoute(resolved.route);
|
|
3450
|
+
try {
|
|
3451
|
+
shimAuthHeader(req, token);
|
|
3452
|
+
const replay = makeReplayRequest(req, rawBody);
|
|
3453
|
+
await routeRequest(replay, res, routeMap, deps.proxyDeps);
|
|
3454
|
+
} catch (err) {
|
|
3455
|
+
const message = serializeError(err);
|
|
3456
|
+
if (deps.logger) deps.logger.error("[OutboundApi] dispatch error:", message);
|
|
3457
|
+
else console.error("[OutboundApi] dispatch error:", message);
|
|
3458
|
+
emitWebhookEvent({ kind: "server.error", at: Date.now(), message });
|
|
3459
|
+
if (audit) audit.error = message;
|
|
3460
|
+
writeJsonError(res, 502, message);
|
|
3461
|
+
} finally {
|
|
3462
|
+
routeMap.removeRoute(token);
|
|
3463
|
+
if (releaseSerial) releaseSerial();
|
|
3464
|
+
}
|
|
2002
3465
|
} finally {
|
|
2003
|
-
|
|
3466
|
+
if (releaseConcurrency) releaseConcurrency();
|
|
2004
3467
|
}
|
|
2005
3468
|
}
|
|
2006
3469
|
function makeReplayRequest(req, rawBody) {
|
|
@@ -2014,53 +3477,20 @@ function makeReplayRequest(req, rawBody) {
|
|
|
2014
3477
|
return readable;
|
|
2015
3478
|
}
|
|
2016
3479
|
|
|
2017
|
-
// src/outbound-api/outboundRateLimiter.ts
|
|
2018
|
-
var DEFAULT_WINDOW_MS = 6e4;
|
|
2019
|
-
var DEFAULT_MAX_REQUESTS = 60;
|
|
2020
|
-
var OutboundRateLimiter = class {
|
|
2021
|
-
windowMs;
|
|
2022
|
-
maxRequests;
|
|
2023
|
-
/** apiKeyId → ascending request timestamps within the current window. */
|
|
2024
|
-
hits = /* @__PURE__ */ new Map();
|
|
2025
|
-
constructor(options = {}) {
|
|
2026
|
-
this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
|
|
2027
|
-
this.maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
|
|
2028
|
-
}
|
|
2029
|
-
/**
|
|
2030
|
-
* Record a request for `apiKeyId` and decide whether it is allowed. Prunes
|
|
2031
|
-
* timestamps older than the window first; when allowed, the request's
|
|
2032
|
-
* timestamp is appended.
|
|
2033
|
-
*/
|
|
2034
|
-
check(apiKeyId, now = Date.now()) {
|
|
2035
|
-
const cutoff = now - this.windowMs;
|
|
2036
|
-
const timestamps = (this.hits.get(apiKeyId) ?? []).filter((ts) => ts > cutoff);
|
|
2037
|
-
if (timestamps.length >= this.maxRequests) {
|
|
2038
|
-
if (timestamps.length === 0) {
|
|
2039
|
-
this.hits.delete(apiKeyId);
|
|
2040
|
-
return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(this.windowMs / 1e3)) };
|
|
2041
|
-
}
|
|
2042
|
-
const oldest = timestamps[0];
|
|
2043
|
-
const retryAfterMs = Math.max(0, oldest + this.windowMs - now);
|
|
2044
|
-
this.hits.set(apiKeyId, timestamps);
|
|
2045
|
-
return {
|
|
2046
|
-
allowed: false,
|
|
2047
|
-
retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1e3))
|
|
2048
|
-
};
|
|
2049
|
-
}
|
|
2050
|
-
timestamps.push(now);
|
|
2051
|
-
this.hits.set(apiKeyId, timestamps);
|
|
2052
|
-
return { allowed: true, retryAfterSeconds: 0 };
|
|
2053
|
-
}
|
|
2054
|
-
/** Drop all recorded state (tests / teardown). */
|
|
2055
|
-
reset() {
|
|
2056
|
-
this.hits.clear();
|
|
2057
|
-
}
|
|
2058
|
-
};
|
|
2059
|
-
|
|
2060
3480
|
// src/outbound-api/OutboundApiServer.ts
|
|
2061
3481
|
var DEFAULT_OUTBOUND_PORT = 8765;
|
|
2062
3482
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
2063
3483
|
var LAN_ADDR = "0.0.0.0";
|
|
3484
|
+
var OutboundApiConfigError = class extends Error {
|
|
3485
|
+
missing;
|
|
3486
|
+
constructor(missing) {
|
|
3487
|
+
super(
|
|
3488
|
+
"Outbound API server cannot start: incomplete model-kind configuration \u2014 " + missing.map((m) => `${m.endpoint} missing [${m.missingKinds.join(", ")}]`).join("; ")
|
|
3489
|
+
);
|
|
3490
|
+
this.name = "OutboundApiConfigError";
|
|
3491
|
+
this.missing = missing;
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
2064
3494
|
var OutboundApiServer = class {
|
|
2065
3495
|
constructor(deps, onPortChange) {
|
|
2066
3496
|
this.deps = deps;
|
|
@@ -2072,7 +3502,25 @@ var OutboundApiServer = class {
|
|
|
2072
3502
|
boundPort = 0;
|
|
2073
3503
|
boundAddr = LOOPBACK_ADDR;
|
|
2074
3504
|
endpoints = [];
|
|
3505
|
+
userMessageQueue;
|
|
3506
|
+
concurrencyQueue;
|
|
3507
|
+
voucherConfig;
|
|
2075
3508
|
rateLimiter = new OutboundRateLimiter();
|
|
3509
|
+
/**
|
|
3510
|
+
* Redeem-attempt limiter (voucher-redemption #9, design D6) — a SEPARATE bucket
|
|
3511
|
+
* from the traffic `rateLimiter`, keyed by the authenticating key id, so
|
|
3512
|
+
* brute-forcing `CC_` codes is throttled (a handful/min) without touching the
|
|
3513
|
+
* per-key request rate. Conservative fixed defaults (10 / 60s).
|
|
3514
|
+
*/
|
|
3515
|
+
redeemLimiter = new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
|
|
3516
|
+
/**
|
|
3517
|
+
* Per-key redeem mutex (voucher-redemption #9, MJ1 fix). One instance for the
|
|
3518
|
+
* server's lifetime so concurrent redeem REQUESTS for the same key serialize
|
|
3519
|
+
* (relative grant increments accumulate instead of clobbering a snapshot).
|
|
3520
|
+
*/
|
|
3521
|
+
redeemMutex = new KeyedMutex();
|
|
3522
|
+
serialQueue = new UserMessageSerialQueue();
|
|
3523
|
+
concurrencyGate = new OutboundConcurrencyGate();
|
|
2076
3524
|
/**
|
|
2077
3525
|
* Apply a config. Restarts the listener ONLY when the bind address or port
|
|
2078
3526
|
* changes (or when toggling enabled); per-endpoint routing config is updated
|
|
@@ -2080,12 +3528,25 @@ var OutboundApiServer = class {
|
|
|
2080
3528
|
*/
|
|
2081
3529
|
async applyConfig(input) {
|
|
2082
3530
|
this.endpoints = input.endpoints;
|
|
3531
|
+
this.userMessageQueue = input.userMessageQueue;
|
|
3532
|
+
this.concurrencyQueue = input.concurrencyQueue;
|
|
3533
|
+
this.voucherConfig = input.voucher;
|
|
2083
3534
|
const wantAddr = input.networkBinding ? LAN_ADDR : LOOPBACK_ADDR;
|
|
2084
3535
|
const wantPort = input.port ?? DEFAULT_OUTBOUND_PORT;
|
|
2085
3536
|
if (!input.enabled) {
|
|
2086
3537
|
await this.stop();
|
|
2087
3538
|
return;
|
|
2088
3539
|
}
|
|
3540
|
+
const missing = validateServerModelConfig({
|
|
3541
|
+
enabled: true,
|
|
3542
|
+
networkBinding: input.networkBinding,
|
|
3543
|
+
endpoints: input.endpoints,
|
|
3544
|
+
port: input.port
|
|
3545
|
+
});
|
|
3546
|
+
if (missing.length > 0) {
|
|
3547
|
+
await this.stop();
|
|
3548
|
+
throw new OutboundApiConfigError(missing);
|
|
3549
|
+
}
|
|
2089
3550
|
const running = this.server !== null;
|
|
2090
3551
|
const bindChanged = running && (this.boundAddr !== wantAddr || this.boundPort !== wantPort);
|
|
2091
3552
|
if (running && !bindChanged) {
|
|
@@ -2101,7 +3562,7 @@ var OutboundApiServer = class {
|
|
|
2101
3562
|
this.boundAddr = bindAddr;
|
|
2102
3563
|
this.boundPort = actualPort;
|
|
2103
3564
|
if (actualPort !== port) this.onPortChange?.(actualPort);
|
|
2104
|
-
|
|
3565
|
+
this.logInfo(`[OutboundApiServer] Listening on ${bindAddr}:${actualPort}`);
|
|
2105
3566
|
return actualPort;
|
|
2106
3567
|
}
|
|
2107
3568
|
/** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
|
|
@@ -2123,7 +3584,7 @@ var OutboundApiServer = class {
|
|
|
2123
3584
|
const addr = server.address();
|
|
2124
3585
|
if (addr && typeof addr === "object") {
|
|
2125
3586
|
server.removeListener("error", onError);
|
|
2126
|
-
server.on("error", (e) =>
|
|
3587
|
+
server.on("error", (e) => this.logError("[OutboundApiServer] server error", serializeError(e)));
|
|
2127
3588
|
this.server = server;
|
|
2128
3589
|
resolve(addr.port);
|
|
2129
3590
|
} else {
|
|
@@ -2134,21 +3595,58 @@ var OutboundApiServer = class {
|
|
|
2134
3595
|
}
|
|
2135
3596
|
/** Per-request handler. Auth is enforced on EVERY request (incl. loopback). */
|
|
2136
3597
|
onRequest(req, res) {
|
|
3598
|
+
if (this.deps.healthReportProvider && this.tryServeHealth(req, res)) return;
|
|
2137
3599
|
handleOutboundRequest(
|
|
2138
3600
|
req,
|
|
2139
3601
|
res,
|
|
2140
3602
|
this.deps,
|
|
2141
|
-
{
|
|
2142
|
-
|
|
3603
|
+
{
|
|
3604
|
+
endpoints: this.endpoints,
|
|
3605
|
+
userMessageQueue: this.userMessageQueue,
|
|
3606
|
+
concurrencyQueue: this.concurrencyQueue,
|
|
3607
|
+
voucher: this.voucherConfig
|
|
3608
|
+
},
|
|
3609
|
+
this.rateLimiter,
|
|
3610
|
+
this.serialQueue,
|
|
3611
|
+
this.concurrencyGate,
|
|
3612
|
+
this.redeemLimiter,
|
|
3613
|
+
this.redeemMutex
|
|
2143
3614
|
).catch((err) => {
|
|
2144
3615
|
const message = serializeError(err);
|
|
2145
|
-
|
|
3616
|
+
this.logError("[OutboundApiServer] unhandled error:", message);
|
|
2146
3617
|
if (!res.headersSent) {
|
|
2147
3618
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2148
3619
|
res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
|
|
2149
3620
|
}
|
|
2150
3621
|
});
|
|
2151
3622
|
}
|
|
3623
|
+
/**
|
|
3624
|
+
* Serve `GET|HEAD /health` (+ `/healthz`) from the injected provider, returning
|
|
3625
|
+
* true when it handled the request. 200 when `ok`, else 503; secret-free body.
|
|
3626
|
+
*/
|
|
3627
|
+
tryServeHealth(req, res) {
|
|
3628
|
+
const provider = this.deps.healthReportProvider;
|
|
3629
|
+
if (!provider) return false;
|
|
3630
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
3631
|
+
const path = (req.url ?? "/").split("?")[0]?.replace(/\/+$/, "") || "/";
|
|
3632
|
+
if (path !== "/health" && path !== "/healthz") return false;
|
|
3633
|
+
const report = provider();
|
|
3634
|
+
res.writeHead(healthHttpStatus(report.status), { "Content-Type": "application/json" });
|
|
3635
|
+
res.end(req.method === "HEAD" ? void 0 : JSON.stringify(report));
|
|
3636
|
+
return true;
|
|
3637
|
+
}
|
|
3638
|
+
/** Route an info lifecycle line through the injected logger, else `console.log`
|
|
3639
|
+
* (byte-identical legacy fallback when no logger is wired). */
|
|
3640
|
+
logInfo(message) {
|
|
3641
|
+
if (this.deps.logger) this.deps.logger.info(message);
|
|
3642
|
+
else console.log(message);
|
|
3643
|
+
}
|
|
3644
|
+
/** Route an error lifecycle line through the injected logger, else `console.error`. */
|
|
3645
|
+
logError(message, detail) {
|
|
3646
|
+
if (this.deps.logger) this.deps.logger.error(message, detail);
|
|
3647
|
+
else if (detail === void 0) console.error(message);
|
|
3648
|
+
else console.error(message, detail);
|
|
3649
|
+
}
|
|
2152
3650
|
/** Stop the listener and release the port. */
|
|
2153
3651
|
async stop() {
|
|
2154
3652
|
const server = this.server;
|
|
@@ -2157,7 +3655,7 @@ var OutboundApiServer = class {
|
|
|
2157
3655
|
this.boundPort = 0;
|
|
2158
3656
|
return new Promise((resolve) => {
|
|
2159
3657
|
server.close(() => {
|
|
2160
|
-
|
|
3658
|
+
this.logInfo("[OutboundApiServer] Stopped");
|
|
2161
3659
|
resolve();
|
|
2162
3660
|
});
|
|
2163
3661
|
});
|
|
@@ -2189,6 +3687,17 @@ var OutboundApiServer = class {
|
|
|
2189
3687
|
lanFormats: lanBase ? formatUrls(lanBase) : null
|
|
2190
3688
|
};
|
|
2191
3689
|
}
|
|
3690
|
+
/**
|
|
3691
|
+
* Live queue-occupancy snapshot (only active entries). This getter's name +
|
|
3692
|
+
* shape are FROZEN — `omnicross-uqc-daemon` spreads it into its `/status`
|
|
3693
|
+
* response; the existing {@link getStatus} shape is deliberately NOT changed.
|
|
3694
|
+
*/
|
|
3695
|
+
getQueueStatus() {
|
|
3696
|
+
return {
|
|
3697
|
+
serial: this.serialQueue.getStatus(),
|
|
3698
|
+
concurrency: this.concurrencyGate.getStatus()
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
2192
3701
|
};
|
|
2193
3702
|
function formatUrls(base) {
|
|
2194
3703
|
return {
|
|
@@ -2209,66 +3718,88 @@ function firstLanIPv4() {
|
|
|
2209
3718
|
return null;
|
|
2210
3719
|
}
|
|
2211
3720
|
|
|
2212
|
-
// src/outbound-api/
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
if (
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
3721
|
+
// src/outbound-api/keySpendTracker.ts
|
|
3722
|
+
function startOfLocalDay(ts) {
|
|
3723
|
+
const d = new Date(ts);
|
|
3724
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
3725
|
+
}
|
|
3726
|
+
function startOfLocalWeek(ts) {
|
|
3727
|
+
const d = new Date(ts);
|
|
3728
|
+
const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
3729
|
+
day.setDate(day.getDate() - day.getDay());
|
|
3730
|
+
return day.getTime();
|
|
3731
|
+
}
|
|
3732
|
+
var KeySpendTracker = class {
|
|
3733
|
+
constructor(seeder) {
|
|
3734
|
+
this.seeder = seeder;
|
|
3735
|
+
}
|
|
3736
|
+
seeder;
|
|
3737
|
+
/** apiKeyId → live accumulated spend (seeded lazily). */
|
|
3738
|
+
spend = /* @__PURE__ */ new Map();
|
|
3739
|
+
/**
|
|
3740
|
+
* Read the key's accumulated spend, seeding from the durable store on a cache
|
|
3741
|
+
* miss and rolling the daily/weekly windows on a hit.
|
|
3742
|
+
*/
|
|
3743
|
+
async getSpend(apiKeyId, now) {
|
|
3744
|
+
const existing = this.spend.get(apiKeyId);
|
|
3745
|
+
if (existing) {
|
|
3746
|
+
rollWindows(existing, now);
|
|
3747
|
+
return existing;
|
|
3748
|
+
}
|
|
3749
|
+
const seeded = await this.seed(apiKeyId, now);
|
|
3750
|
+
const existingAfterSeed = this.spend.get(apiKeyId);
|
|
3751
|
+
if (existingAfterSeed) {
|
|
3752
|
+
rollWindows(existingAfterSeed, now);
|
|
3753
|
+
return existingAfterSeed;
|
|
2245
3754
|
}
|
|
3755
|
+
this.spend.set(apiKeyId, seeded);
|
|
3756
|
+
return seeded;
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Increment the aggregate for one recorded request. No-op when the key has not
|
|
3760
|
+
* been seeded yet — a later `getSpend` will lazy-seed from the durable store,
|
|
3761
|
+
* which already includes this record, so it is neither lost nor double-counted.
|
|
3762
|
+
*/
|
|
3763
|
+
add(apiKeyId, costUsd, now) {
|
|
3764
|
+
const s = this.spend.get(apiKeyId);
|
|
3765
|
+
if (!s) return;
|
|
3766
|
+
rollWindows(s, now);
|
|
3767
|
+
s.dailyUsd += costUsd;
|
|
3768
|
+
s.weeklyUsd += costUsd;
|
|
3769
|
+
s.totalUsd += costUsd;
|
|
3770
|
+
}
|
|
3771
|
+
/** Drop all cached spend (tests / teardown). */
|
|
3772
|
+
reset() {
|
|
3773
|
+
this.spend.clear();
|
|
3774
|
+
}
|
|
3775
|
+
/**
|
|
3776
|
+
* Derive a fresh spend snapshot for `apiKeyId` from the durable store in a
|
|
3777
|
+
* SINGLE scan (all three windows at once). `endTs` is EXCLUSIVE, so `now + 1`
|
|
3778
|
+
* includes a record stamped at exactly `now`.
|
|
3779
|
+
*/
|
|
3780
|
+
async seed(apiKeyId, now) {
|
|
3781
|
+
const dailyWindowStart = startOfLocalDay(now);
|
|
3782
|
+
const weeklyWindowStart = startOfLocalWeek(now);
|
|
3783
|
+
const { totalUsd, dailyUsd, weeklyUsd } = await this.seeder.getSpendByKey({
|
|
3784
|
+
apiKeyId,
|
|
3785
|
+
dayStartTs: dailyWindowStart,
|
|
3786
|
+
weekStartTs: weeklyWindowStart,
|
|
3787
|
+
endTs: now + 1
|
|
3788
|
+
});
|
|
3789
|
+
return { dailyUsd, dailyWindowStart, weeklyUsd, weeklyWindowStart, totalUsd };
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
function rollWindows(s, now) {
|
|
3793
|
+
const day = startOfLocalDay(now);
|
|
3794
|
+
if (s.dailyWindowStart !== day) {
|
|
3795
|
+
s.dailyUsd = 0;
|
|
3796
|
+
s.dailyWindowStart = day;
|
|
3797
|
+
}
|
|
3798
|
+
const week = startOfLocalWeek(now);
|
|
3799
|
+
if (s.weeklyWindowStart !== week) {
|
|
3800
|
+
s.weeklyUsd = 0;
|
|
3801
|
+
s.weeklyWindowStart = week;
|
|
2246
3802
|
}
|
|
2247
|
-
return {
|
|
2248
|
-
enabled: raw.enabled === true,
|
|
2249
|
-
networkBinding: raw.networkBinding === true,
|
|
2250
|
-
endpoints: ALL_ENDPOINTS.map(
|
|
2251
|
-
(ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep)
|
|
2252
|
-
),
|
|
2253
|
-
port: raw.port ?? base.port
|
|
2254
|
-
};
|
|
2255
|
-
}
|
|
2256
|
-
async function loadServerConfig(store) {
|
|
2257
|
-
const raw = await store.get(
|
|
2258
|
-
OUTBOUND_API_SERVER_CONFIG_KEY
|
|
2259
|
-
);
|
|
2260
|
-
return normalizeServerConfig(raw);
|
|
2261
|
-
}
|
|
2262
|
-
async function saveServerConfig(store, config) {
|
|
2263
|
-
await store.set(OUTBOUND_API_SERVER_CONFIG_KEY, config);
|
|
2264
|
-
}
|
|
2265
|
-
function mergeServerConfig(current, patch) {
|
|
2266
|
-
return normalizeServerConfig({
|
|
2267
|
-
enabled: patch.enabled ?? current.enabled,
|
|
2268
|
-
networkBinding: patch.networkBinding ?? current.networkBinding,
|
|
2269
|
-
endpoints: patch.endpoints ?? current.endpoints,
|
|
2270
|
-
port: patch.port ?? current.port
|
|
2271
|
-
});
|
|
2272
3803
|
}
|
|
2273
3804
|
|
|
2274
3805
|
// src/outbound-api/index.ts
|
|
@@ -4380,21 +5911,60 @@ export {
|
|
|
4380
5911
|
ProviderProxy,
|
|
4381
5912
|
getProviderProxy,
|
|
4382
5913
|
__resetProviderProxyForTests,
|
|
5914
|
+
KeyedMutex,
|
|
5915
|
+
OUTBOUND_API_SERVER_CONFIG_KEY,
|
|
5916
|
+
DEFAULT_USER_MESSAGE_QUEUE,
|
|
5917
|
+
DEFAULT_CONCURRENCY_QUEUE,
|
|
5918
|
+
DEFAULT_ACCOUNT_PROBE,
|
|
5919
|
+
normalizeAccountProbe,
|
|
5920
|
+
normalizeAudit,
|
|
5921
|
+
normalizeBilling,
|
|
5922
|
+
DEFAULT_FINGERPRINT,
|
|
5923
|
+
normalizeFingerprint,
|
|
5924
|
+
normalizeVoucher,
|
|
5925
|
+
normalizeProxyConfig,
|
|
5926
|
+
normalizeProxySegment,
|
|
5927
|
+
normalizeWebhookDestination,
|
|
5928
|
+
normalizeWebhookSegment,
|
|
5929
|
+
normalizeQueueSegments,
|
|
5930
|
+
normalizePrefixTargets,
|
|
5931
|
+
defaultServerConfig,
|
|
5932
|
+
normalizeServerConfig,
|
|
5933
|
+
loadServerConfig,
|
|
5934
|
+
saveServerConfig,
|
|
5935
|
+
mergeServerConfig,
|
|
5936
|
+
computeKeyExpiry,
|
|
5937
|
+
checkKeyQuota,
|
|
5938
|
+
randomBase62,
|
|
4383
5939
|
hashKey,
|
|
4384
5940
|
createNamedKey,
|
|
5941
|
+
verifyKey,
|
|
4385
5942
|
verifyPresentedKey,
|
|
4386
|
-
|
|
4387
|
-
|
|
5943
|
+
ConcurrencyQueueFullError,
|
|
5944
|
+
ConcurrencyWaitTimeoutError,
|
|
5945
|
+
ConcurrencyWaitCancelledError,
|
|
5946
|
+
isConcurrencyRejection,
|
|
5947
|
+
OutboundConcurrencyGate,
|
|
4388
5948
|
OutboundRateLimiter,
|
|
5949
|
+
isUserMessageRequest,
|
|
5950
|
+
SerialQueueTimeoutError,
|
|
5951
|
+
isSerialQueueTimeout,
|
|
5952
|
+
UserMessageSerialQueue,
|
|
5953
|
+
generateVoucherCode,
|
|
5954
|
+
hashVoucherCode,
|
|
5955
|
+
voucherCodePrefix,
|
|
5956
|
+
newVoucherId,
|
|
5957
|
+
computeVoucherGrant,
|
|
5958
|
+
toVoucherInfo,
|
|
5959
|
+
isRedeemRequest,
|
|
5960
|
+
handleVoucherRedeem,
|
|
4389
5961
|
DEFAULT_OUTBOUND_PORT,
|
|
5962
|
+
OutboundApiConfigError,
|
|
4390
5963
|
OutboundApiServer,
|
|
4391
5964
|
formatUrls,
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
loadServerConfig,
|
|
4396
|
-
saveServerConfig,
|
|
4397
|
-
mergeServerConfig,
|
|
5965
|
+
startOfLocalDay,
|
|
5966
|
+
startOfLocalWeek,
|
|
5967
|
+
KeySpendTracker,
|
|
4398
5968
|
getOutboundApiServer,
|
|
4399
5969
|
__resetOutboundApiServerForTests
|
|
4400
5970
|
};
|