@omnicross/core 0.1.4 → 0.1.6
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/dist/ApiConverter.cjs +2 -2
- package/dist/ApiConverter.js +1 -1
- package/dist/{CompletionService-DdWaAxq0.d.ts → CompletionService-C3BX0Ovy.d.ts} +4 -3
- package/dist/{CompletionService-D3a47vdU.d.cts → CompletionService-kNA17cfI.d.cts} +4 -3
- package/dist/auth/GeminiCodeAssistProjectResolver.cjs +7 -5
- package/dist/auth/GeminiCodeAssistProjectResolver.js +5 -3
- package/dist/{chunk-HRRYG2AX.js → chunk-23S5OPIO.js} +10 -1
- package/dist/chunk-25GXJCEZ.cjs +40 -0
- package/dist/chunk-2JYRIADS.js +40 -0
- package/dist/chunk-3Z5BQZFM.cjs +52 -0
- package/dist/{chunk-4VAVYUZS.cjs → chunk-4VLRHHFE.cjs} +82 -40
- package/dist/{chunk-VACWOIRE.cjs → chunk-4VZX5F4T.cjs} +39 -15
- package/dist/{chunk-FTQ7IQUC.js → chunk-6CL5HTPQ.js} +52 -10
- package/dist/chunk-6QQG7YG3.cjs +153 -0
- package/dist/{chunk-SU2GCLAC.cjs → chunk-6VUQPOV3.cjs} +359 -60
- package/dist/chunk-7AK7B36B.cjs +44 -0
- package/dist/{chunk-5ZQBEOMD.js → chunk-AT7B7SOZ.js} +25 -2
- package/dist/{chunk-IK3KE3AU.js → chunk-DE2SRBFZ.js} +1 -1
- package/dist/chunk-DLXZEZWO.cjs +285 -0
- package/dist/{chunk-UHGVBFU3.js → chunk-EBLFCNPY.js} +38 -14
- package/dist/chunk-EJLH6NFT.cjs +166 -0
- package/dist/chunk-HVBBDBS3.cjs +350 -0
- package/dist/{chunk-7Y47UCF2.js → chunk-IFXJ2BG7.js} +5 -38
- package/dist/{chunk-46ZIFWYG.cjs → chunk-IIRPUVPH.cjs} +7 -1
- package/dist/{chunk-YGXJGRFI.cjs → chunk-KBM2W6UY.cjs} +111 -24
- package/dist/{chunk-OCEUVQHW.cjs → chunk-KCVJWJST.cjs} +73 -3
- package/dist/chunk-LB6EBS6Y.js +69 -0
- package/dist/{chunk-C7PSERMH.js → chunk-LVUCAWMK.js} +1 -1
- package/dist/chunk-MKF46GNZ.js +350 -0
- package/dist/{chunk-HYN75H6G.cjs → chunk-MUHWHQVM.cjs} +1 -1
- package/dist/{chunk-Z7Y2NPHY.js → chunk-N5T4M4GU.js} +1 -1
- package/dist/{chunk-TIER4SUC.js → chunk-OEWZUX4G.js} +346 -47
- package/dist/chunk-OF3BECU2.cjs +69 -0
- package/dist/{chunk-RSJ3TWJQ.cjs → chunk-OKMJLEXX.cjs} +5 -5
- package/dist/chunk-OPRS4IQZ.js +153 -0
- package/dist/{chunk-XK5CIRMQ.js → chunk-SIZQQD6T.js} +72 -2
- package/dist/{chunk-DKPXN34V.cjs → chunk-SQYVORNP.cjs} +1334 -510
- package/dist/chunk-SYQF7OIR.js +52 -0
- package/dist/{chunk-S4WBV25P.js → chunk-TOVPMMHM.js} +111 -24
- package/dist/{chunk-QYTT6GE3.js → chunk-UDCC7EMR.js} +1093 -269
- package/dist/{chunk-E3UCVHLP.cjs → chunk-UNWKG6EI.cjs} +5 -38
- package/dist/{chunk-ZLX3KGXX.js → chunk-UX7VWA46.js} +135 -23
- package/dist/{chunk-7YN22L2H.cjs → chunk-V4TXMEJS.cjs} +5 -5
- package/dist/chunk-VRFBB5DB.js +44 -0
- package/dist/chunk-WB6PH56J.js +166 -0
- package/dist/{chunk-G2UTNSGU.js → chunk-WNKWAEUR.js} +7 -1
- package/dist/{chunk-SUEGJ75D.cjs → chunk-WYTEQ4Q5.cjs} +25 -2
- package/dist/{chunk-5ENKBSWO.js → chunk-YJVHCHQX.js} +42 -6
- package/dist/{chunk-VQY5W4YW.cjs → chunk-YK57NMIF.cjs} +44 -8
- package/dist/{chunk-KFI44N2R.cjs → chunk-YNJNTCSQ.cjs} +10 -1
- package/dist/completion/ApiKeyPoolService.cjs +12 -0
- package/dist/completion/ApiKeyPoolService.d.cts +6 -0
- package/dist/completion/ApiKeyPoolService.d.ts +6 -0
- package/dist/completion/ApiKeyPoolService.js +12 -0
- package/dist/completion/CompletionService.cjs +27 -24
- package/dist/completion/CompletionService.d.cts +2 -1
- package/dist/completion/CompletionService.d.ts +2 -1
- package/dist/completion/CompletionService.js +26 -23
- package/dist/completion.cjs +27 -24
- package/dist/completion.d.cts +2 -1
- package/dist/completion.d.ts +2 -1
- package/dist/completion.js +26 -23
- package/dist/index.cjs +53 -29
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +56 -32
- package/dist/{keyPolicy-Cp0FntZl.d.cts → keyPolicy-zdWTu3CQ.d.cts} +1 -1
- package/dist/{keyPolicy-Cp0FntZl.d.ts → keyPolicy-zdWTu3CQ.d.ts} +1 -1
- package/dist/outbound-api/auditCapture.cjs +2 -2
- package/dist/outbound-api/auditCapture.d.cts +5 -3
- package/dist/outbound-api/auditCapture.d.ts +5 -3
- package/dist/outbound-api/auditCapture.js +1 -1
- package/dist/outbound-api/quotaWarn.d.cts +1 -1
- package/dist/outbound-api/quotaWarn.d.ts +1 -1
- package/dist/outbound-api/routeResolver.cjs +2 -2
- package/dist/outbound-api/routeResolver.d.cts +4 -3
- package/dist/outbound-api/routeResolver.d.ts +4 -3
- package/dist/outbound-api/routeResolver.js +1 -1
- package/dist/outbound-api/subscriptionRegistryPort.d.cts +1 -0
- package/dist/outbound-api/subscriptionRegistryPort.d.ts +1 -0
- package/dist/outbound-api/types.d.cts +3 -2
- package/dist/outbound-api/types.d.ts +3 -2
- package/dist/outbound-api.cjs +49 -25
- package/dist/outbound-api.d.cts +127 -85
- package/dist/outbound-api.d.ts +127 -85
- package/dist/outbound-api.js +52 -28
- package/dist/pipeline/AccountAllowanceScheduling.cjs +15 -0
- package/dist/pipeline/AccountAllowanceScheduling.d.cts +90 -0
- package/dist/pipeline/AccountAllowanceScheduling.d.ts +90 -0
- package/dist/pipeline/AccountAllowanceScheduling.js +15 -0
- package/dist/pipeline/AccountAllowanceStore.cjs +20 -0
- package/dist/pipeline/AccountAllowanceStore.d.cts +77 -0
- package/dist/pipeline/AccountAllowanceStore.d.ts +77 -0
- package/dist/pipeline/AccountAllowanceStore.js +20 -0
- package/dist/pipeline/AuthSource.d.cts +11 -2
- package/dist/pipeline/AuthSource.d.ts +11 -2
- package/dist/pipeline/BoundAccountSelectionError.cjs +10 -0
- package/dist/pipeline/BoundAccountSelectionError.d.cts +28 -0
- package/dist/pipeline/BoundAccountSelectionError.d.ts +28 -0
- package/dist/pipeline/BoundAccountSelectionError.js +10 -0
- package/dist/pipeline/LlmConfigProviderAuth.d.cts +1 -0
- package/dist/pipeline/LlmConfigProviderAuth.d.ts +1 -0
- package/dist/pipeline/SubscriptionAccountHealth.cjs +4 -2
- package/dist/pipeline/SubscriptionAccountHealth.d.cts +34 -6
- package/dist/pipeline/SubscriptionAccountHealth.d.ts +34 -6
- package/dist/pipeline/SubscriptionAccountHealth.js +3 -1
- package/dist/pipeline/SubscriptionAuthSource.cjs +5 -2
- package/dist/pipeline/SubscriptionAuthSource.d.cts +1 -0
- package/dist/pipeline/SubscriptionAuthSource.d.ts +1 -0
- package/dist/pipeline/SubscriptionAuthSource.js +4 -1
- package/dist/pipeline/SubscriptionAuthStrategy.d.cts +8 -3
- package/dist/pipeline/SubscriptionAuthStrategy.d.ts +8 -3
- package/dist/pipeline/executeProviderCall.cjs +2 -2
- package/dist/pipeline/executeProviderCall.d.cts +8 -3
- package/dist/pipeline/executeProviderCall.d.ts +8 -3
- package/dist/pipeline/executeProviderCall.js +1 -1
- package/dist/pipeline/resolveProviderChain.d.cts +2 -3
- package/dist/pipeline/resolveProviderChain.d.ts +2 -3
- package/dist/pipeline/resolveSubscriptionChain.d.cts +1 -0
- package/dist/pipeline/resolveSubscriptionChain.d.ts +1 -0
- package/dist/pipeline/upstreamFetch.cjs +4 -2
- package/dist/pipeline/upstreamFetch.d.cts +6 -0
- package/dist/pipeline/upstreamFetch.d.ts +6 -0
- package/dist/pipeline/upstreamFetch.js +3 -1
- package/dist/pipeline/upstreamTrace.cjs +14 -0
- package/dist/pipeline/upstreamTrace.d.cts +85 -0
- package/dist/pipeline/upstreamTrace.d.ts +85 -0
- package/dist/pipeline/upstreamTrace.js +14 -0
- package/dist/ports/pricing-store.d.cts +4 -3
- package/dist/ports/pricing-store.d.ts +4 -3
- package/dist/ports.d.cts +4 -3
- package/dist/ports.d.ts +4 -3
- package/dist/provider-proxy/ProviderProxy.cjs +27 -24
- package/dist/provider-proxy/ProviderProxy.d.cts +1 -0
- package/dist/provider-proxy/ProviderProxy.d.ts +1 -0
- package/dist/provider-proxy/ProviderProxy.js +26 -23
- package/dist/provider-proxy/ingress/providerProxyShared.cjs +31 -24
- package/dist/provider-proxy/ingress/providerProxyShared.d.cts +50 -5
- package/dist/provider-proxy/ingress/providerProxyShared.d.ts +50 -5
- package/dist/provider-proxy/ingress/providerProxyShared.js +30 -23
- package/dist/provider-proxy/matchText.cjs +4 -2
- package/dist/provider-proxy/matchText.d.cts +41 -6
- package/dist/provider-proxy/matchText.d.ts +41 -6
- package/dist/provider-proxy/matchText.js +3 -1
- package/dist/provider-proxy/types.d.cts +30 -2
- package/dist/provider-proxy/types.d.ts +30 -2
- package/dist/provider-proxy.cjs +27 -24
- package/dist/provider-proxy.d.cts +1 -0
- package/dist/provider-proxy.d.ts +1 -0
- package/dist/provider-proxy.js +26 -23
- package/dist/{routeResolver-C4T7yClx.d.ts → routeResolver-CHTv_Bs5.d.ts} +1 -1
- package/dist/{routeResolver-CB3tvJKy.d.cts → routeResolver-kepxnfcC.d.cts} +1 -1
- package/dist/transformer/TransformerChainExecutor.cjs +2 -2
- package/dist/transformer/TransformerChainExecutor.js +1 -1
- package/dist/transformer/transformers/AnthropicTransformer.cjs +2 -2
- package/dist/transformer/transformers/AnthropicTransformer.js +1 -1
- package/dist/transformer/transformers/GeminiCodeAssistTransformer.cjs +3 -3
- package/dist/transformer/transformers/GeminiCodeAssistTransformer.js +2 -2
- package/dist/transformer/transformers/GeminiTransformer.cjs +3 -3
- package/dist/transformer/transformers/GeminiTransformer.js +2 -2
- package/dist/transformer/transformers/OpenAIResponseTransformer.cjs +2 -2
- package/dist/transformer/transformers/OpenAIResponseTransformer.d.cts +1 -1
- package/dist/transformer/transformers/OpenAIResponseTransformer.d.ts +1 -1
- package/dist/transformer/transformers/OpenAIResponseTransformer.js +1 -1
- package/dist/transformer/transformers/OpenAITransformer.cjs +6 -0
- package/dist/transformer/transformers/OpenAITransformer.d.cts +68 -0
- package/dist/transformer/transformers/OpenAITransformer.d.ts +68 -0
- package/dist/transformer/transformers/OpenAITransformer.js +6 -0
- package/dist/transformer/transformers.cjs +8 -14
- package/dist/transformer/transformers.d.cts +12 -48
- package/dist/transformer/transformers.d.ts +12 -48
- package/dist/transformer/transformers.js +11 -17
- package/dist/transformer/types.d.cts +17 -0
- package/dist/transformer/types.d.ts +17 -0
- package/dist/transformer.cjs +11 -12
- package/dist/transformer.d.cts +1 -2
- package/dist/transformer.d.ts +1 -2
- package/dist/transformer.js +11 -12
- package/dist/{types-B8Arpxn0.d.cts → types-H3wGZSOo.d.cts} +134 -7
- package/dist/{types-lgPYQuZS.d.ts → types-hsoKrVQS.d.ts} +134 -7
- package/dist/usage/pricing-engine.cjs +2 -2
- package/dist/usage/pricing-engine.d.cts +9 -1
- package/dist/usage/pricing-engine.d.ts +9 -1
- package/dist/usage/pricing-engine.js +1 -1
- package/dist/usage.cjs +2 -2
- package/dist/usage.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-C72S6TTV.cjs +0 -49
- package/dist/chunk-DWKQZ2SK.cjs +0 -254
- package/dist/chunk-IAXWICZU.js +0 -69
- package/dist/chunk-QWBWGODS.cjs +0 -69
- package/dist/chunk-RGTR7CIO.cjs +0 -235
- package/dist/chunk-VPBAUQ2M.js +0 -235
- package/dist/chunk-YMFAJFQT.cjs +0 -173
- package/dist/chunk-Z7LUNECE.js +0 -49
- package/dist/chunk-ZJZAOKI7.js +0 -254
- package/dist/transformer/transformers/OpenCodeGoTransformer.cjs +0 -6
- package/dist/transformer/transformers/OpenCodeGoTransformer.d.cts +0 -51
- package/dist/transformer/transformers/OpenCodeGoTransformer.d.ts +0 -51
- package/dist/transformer/transformers/OpenCodeGoTransformer.js +0 -6
- package/dist/transformer/transformers/ReasoningTransformer.cjs +0 -6
- package/dist/transformer/transformers/ReasoningTransformer.d.cts +0 -47
- package/dist/transformer/transformers/ReasoningTransformer.d.ts +0 -47
- package/dist/transformer/transformers/ReasoningTransformer.js +0 -6
|
@@ -1,21 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectMatchText,
|
|
3
|
+
deriveGatewaySessionKey,
|
|
4
|
+
deriveSubscriptionSessionKey
|
|
5
|
+
} from "./chunk-WB6PH56J.js";
|
|
1
6
|
import {
|
|
2
7
|
streamSSEResponse
|
|
3
8
|
} from "./chunk-VVEHS2LI.js";
|
|
4
9
|
import {
|
|
5
|
-
|
|
6
|
-
} from "./chunk-
|
|
10
|
+
TransformerChainExecutor
|
|
11
|
+
} from "./chunk-WNKWAEUR.js";
|
|
12
|
+
import {
|
|
13
|
+
injectExtendedContextBeta
|
|
14
|
+
} from "./chunk-MZNPGW5Q.js";
|
|
7
15
|
import {
|
|
8
16
|
GeminiTransformer
|
|
9
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-LVUCAWMK.js";
|
|
10
18
|
import {
|
|
11
19
|
OpenAIResponseTransformer
|
|
12
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-OEWZUX4G.js";
|
|
13
21
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
AnthropicTransformer,
|
|
23
|
+
resolveAnthropicMaxTokens
|
|
24
|
+
} from "./chunk-6CL5HTPQ.js";
|
|
16
25
|
import {
|
|
17
|
-
|
|
18
|
-
} from "./chunk-
|
|
26
|
+
getGeminiCodeAssistResolver
|
|
27
|
+
} from "./chunk-DRWHTIIZ.js";
|
|
19
28
|
import {
|
|
20
29
|
getSharedIdentityStore
|
|
21
30
|
} from "./chunk-XTEI64OU.js";
|
|
@@ -24,18 +33,31 @@ import {
|
|
|
24
33
|
captureCallerIdentity
|
|
25
34
|
} from "./chunk-XX6NQJMA.js";
|
|
26
35
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
resolveSubscriptionChain
|
|
37
|
+
} from "./chunk-5ERIZW3D.js";
|
|
38
|
+
import {
|
|
39
|
+
getSharedAccountHealth,
|
|
40
|
+
resolveResetSeconds
|
|
41
|
+
} from "./chunk-YJVHCHQX.js";
|
|
42
|
+
import {
|
|
43
|
+
SubscriptionAuthSource
|
|
44
|
+
} from "./chunk-23S5OPIO.js";
|
|
45
|
+
import {
|
|
46
|
+
serializeError
|
|
47
|
+
} from "./chunk-AVYJ4YBN.js";
|
|
30
48
|
import {
|
|
31
49
|
emitWebhookEvent
|
|
32
50
|
} from "./chunk-OE6TDIWW.js";
|
|
33
51
|
import {
|
|
34
|
-
|
|
35
|
-
} from "./chunk-
|
|
52
|
+
isAccountAllowanceExhaustedError
|
|
53
|
+
} from "./chunk-OPRS4IQZ.js";
|
|
54
|
+
import {
|
|
55
|
+
boundAccountSelectionMessage,
|
|
56
|
+
isBoundAccountSelectionError
|
|
57
|
+
} from "./chunk-2JYRIADS.js";
|
|
36
58
|
import {
|
|
37
59
|
executeProviderCall
|
|
38
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-DE2SRBFZ.js";
|
|
39
61
|
import {
|
|
40
62
|
LlmConfigProviderAuth,
|
|
41
63
|
getProviderHeaders
|
|
@@ -43,19 +65,6 @@ import {
|
|
|
43
65
|
import {
|
|
44
66
|
resolveProviderChain
|
|
45
67
|
} from "./chunk-JWC6APFQ.js";
|
|
46
|
-
import {
|
|
47
|
-
resolveSubscriptionChain
|
|
48
|
-
} from "./chunk-5ERIZW3D.js";
|
|
49
|
-
import {
|
|
50
|
-
getSharedAccountHealth,
|
|
51
|
-
resolveResetSeconds
|
|
52
|
-
} from "./chunk-5ENKBSWO.js";
|
|
53
|
-
import {
|
|
54
|
-
SubscriptionAuthSource
|
|
55
|
-
} from "./chunk-HRRYG2AX.js";
|
|
56
|
-
import {
|
|
57
|
-
serializeError
|
|
58
|
-
} from "./chunk-AVYJ4YBN.js";
|
|
59
68
|
import {
|
|
60
69
|
beginBillingCapture
|
|
61
70
|
} from "./chunk-WLYIIEAF.js";
|
|
@@ -64,15 +73,16 @@ import {
|
|
|
64
73
|
markQuotaWarnedOnce
|
|
65
74
|
} from "./chunk-3QFXFCHG.js";
|
|
66
75
|
import {
|
|
76
|
+
detectModelKind,
|
|
67
77
|
detectRequestRole,
|
|
68
78
|
endpointToIngressFormat,
|
|
69
79
|
extractRequestedModel,
|
|
70
80
|
isKindMappedEndpoint,
|
|
71
81
|
modelKindsForEndpoint,
|
|
72
82
|
parseModelRef,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} from "./chunk-
|
|
83
|
+
resolvePrefixTarget,
|
|
84
|
+
resolveRoute
|
|
85
|
+
} from "./chunk-EBLFCNPY.js";
|
|
76
86
|
import {
|
|
77
87
|
buildProviderApiUrl,
|
|
78
88
|
resolveApiFormat,
|
|
@@ -80,14 +90,14 @@ import {
|
|
|
80
90
|
} from "./chunk-H5JUT3KV.js";
|
|
81
91
|
import {
|
|
82
92
|
beginAuditCapture
|
|
83
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-IFXJ2BG7.js";
|
|
84
94
|
import {
|
|
85
95
|
convertAnthropicToOpenAI,
|
|
86
96
|
convertOpenAIToAnthropic
|
|
87
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-AT7B7SOZ.js";
|
|
88
98
|
import {
|
|
89
99
|
fetchUpstream
|
|
90
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-SIZQQD6T.js";
|
|
91
101
|
import {
|
|
92
102
|
applyAugmentation
|
|
93
103
|
} from "./chunk-Y7FO65VT.js";
|
|
@@ -486,23 +496,36 @@ function readAnthropicUsage(usage) {
|
|
|
486
496
|
reasoningTokens: 0
|
|
487
497
|
};
|
|
488
498
|
}
|
|
499
|
+
function emitAnthropicUsageRecord(recorder, rawUsage, attribution) {
|
|
500
|
+
const tapped = readAnthropicUsage(rawUsage);
|
|
501
|
+
if (!tapped) return;
|
|
502
|
+
recorder.record({
|
|
503
|
+
messageId: null,
|
|
504
|
+
parentMessageId: null,
|
|
505
|
+
sessionId: attribution.sessionId,
|
|
506
|
+
providerId: attribution.providerId,
|
|
507
|
+
model: attribution.model,
|
|
508
|
+
apiKeyId: attribution.apiKeyId,
|
|
509
|
+
auditResponse: attribution.auditResponse,
|
|
510
|
+
engineOrigin: "codex-ingress",
|
|
511
|
+
usage: tapped,
|
|
512
|
+
rawUsage
|
|
513
|
+
});
|
|
514
|
+
}
|
|
489
515
|
function recordAnthropicNonStreamUsage(recorder, bodyText, attribution) {
|
|
490
516
|
try {
|
|
491
517
|
const parsed = JSON.parse(bodyText);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
rawUsage: parsed.usage,
|
|
504
|
-
auditResponse: attribution.auditResponse
|
|
505
|
-
});
|
|
518
|
+
emitAnthropicUsageRecord(
|
|
519
|
+
recorder,
|
|
520
|
+
parsed.usage,
|
|
521
|
+
attribution
|
|
522
|
+
);
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function recordAnthropicStreamUsage(recorder, rawUsage, attribution) {
|
|
527
|
+
try {
|
|
528
|
+
emitAnthropicUsageRecord(recorder, rawUsage, attribution);
|
|
506
529
|
} catch {
|
|
507
530
|
}
|
|
508
531
|
}
|
|
@@ -590,7 +613,18 @@ function rewriteSseLine(line, rewriteModel) {
|
|
|
590
613
|
return line;
|
|
591
614
|
}
|
|
592
615
|
}
|
|
593
|
-
|
|
616
|
+
function tapSseDataLine(line, onEvent) {
|
|
617
|
+
const leadingStripped = line.replace(/^\s+/, "");
|
|
618
|
+
if (!leadingStripped.startsWith("data:")) return;
|
|
619
|
+
const payload = leadingStripped.slice(5).trim();
|
|
620
|
+
if (!payload || payload === "[DONE]") return;
|
|
621
|
+
try {
|
|
622
|
+
onEvent(JSON.parse(payload));
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async function relaySseBody(res, body, options) {
|
|
627
|
+
const { rewriteModel, onSseEvent } = options;
|
|
594
628
|
const reader = body.getReader();
|
|
595
629
|
const decoder = new TextDecoder();
|
|
596
630
|
let buffer = "";
|
|
@@ -604,21 +638,26 @@ async function relayStreamWithModelRewrite(res, body, rewriteModel) {
|
|
|
604
638
|
while ((nlIndex = buffer.indexOf("\n")) !== -1) {
|
|
605
639
|
const linePart = buffer.slice(0, nlIndex + 1);
|
|
606
640
|
buffer = buffer.slice(nlIndex + 1);
|
|
607
|
-
|
|
641
|
+
if (onSseEvent) tapSseDataLine(linePart, onSseEvent);
|
|
642
|
+
out += rewriteModel ? rewriteSseLine(linePart, rewriteModel) : linePart;
|
|
608
643
|
}
|
|
609
644
|
if (out) res.write(out);
|
|
610
645
|
}
|
|
611
646
|
const tail = buffer + decoder.decode();
|
|
612
|
-
if (tail)
|
|
647
|
+
if (tail) {
|
|
648
|
+
if (onSseEvent) tapSseDataLine(tail, onSseEvent);
|
|
649
|
+
res.write(rewriteModel ? rewriteSseLine(tail, rewriteModel) : tail);
|
|
650
|
+
}
|
|
613
651
|
} finally {
|
|
614
652
|
reader.releaseLock();
|
|
615
653
|
res.end();
|
|
616
654
|
}
|
|
617
655
|
}
|
|
618
|
-
async function relayResponse(res, providerResponse, isStream, rewriteModel) {
|
|
656
|
+
async function relayResponse(res, providerResponse, isStream, rewriteModel, usageTap) {
|
|
619
657
|
const contentType = providerResponse.headers.get("Content-Type") ?? "";
|
|
620
658
|
const status = providerResponse.status && providerResponse.status >= 100 ? providerResponse.status : 200;
|
|
621
|
-
|
|
659
|
+
const upstreamIsSse = contentType.includes("text/event-stream");
|
|
660
|
+
if (upstreamIsSse || isStream && status < 400) {
|
|
622
661
|
res.writeHead(status, {
|
|
623
662
|
"Content-Type": "text/event-stream",
|
|
624
663
|
"Cache-Control": "no-cache",
|
|
@@ -628,8 +667,16 @@ async function relayResponse(res, providerResponse, isStream, rewriteModel) {
|
|
|
628
667
|
res.end();
|
|
629
668
|
return null;
|
|
630
669
|
}
|
|
631
|
-
if (rewriteModel) {
|
|
632
|
-
|
|
670
|
+
if (rewriteModel || usageTap) {
|
|
671
|
+
let capturedUsage = null;
|
|
672
|
+
await relaySseBody(res, providerResponse.body, {
|
|
673
|
+
rewriteModel,
|
|
674
|
+
onSseEvent: usageTap ? (event) => {
|
|
675
|
+
const raw = usageTap.extractUsage(event);
|
|
676
|
+
if (raw) capturedUsage = raw;
|
|
677
|
+
} : void 0
|
|
678
|
+
});
|
|
679
|
+
if (capturedUsage) usageTap.onUsage(capturedUsage);
|
|
633
680
|
return null;
|
|
634
681
|
}
|
|
635
682
|
const reader = providerResponse.body.getReader();
|
|
@@ -651,11 +698,121 @@ async function relayResponse(res, providerResponse, isStream, rewriteModel) {
|
|
|
651
698
|
res.end(clientText);
|
|
652
699
|
return bodyText;
|
|
653
700
|
}
|
|
701
|
+
async function aggregateAnthropicSseToJsonBody(response, rewriteModel) {
|
|
702
|
+
const text = await response.text();
|
|
703
|
+
const message = {
|
|
704
|
+
type: "message",
|
|
705
|
+
role: "assistant",
|
|
706
|
+
content: [],
|
|
707
|
+
stop_reason: null,
|
|
708
|
+
stop_sequence: null,
|
|
709
|
+
usage: {}
|
|
710
|
+
};
|
|
711
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
712
|
+
for (const raw of text.split("\n")) {
|
|
713
|
+
const line = raw.trim();
|
|
714
|
+
if (!line.startsWith("data:")) continue;
|
|
715
|
+
const payload = line.slice(5).trim();
|
|
716
|
+
if (!payload || payload === "[DONE]") continue;
|
|
717
|
+
let ev;
|
|
718
|
+
try {
|
|
719
|
+
ev = JSON.parse(payload);
|
|
720
|
+
} catch {
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
switch (ev["type"]) {
|
|
724
|
+
case "message_start": {
|
|
725
|
+
const m = ev["message"] ?? {};
|
|
726
|
+
if (typeof m["id"] === "string") message["id"] = m["id"];
|
|
727
|
+
if (typeof m["model"] === "string") message["model"] = m["model"];
|
|
728
|
+
if (m["usage"]) message["usage"] = { ...m["usage"] };
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case "content_block_start": {
|
|
732
|
+
const idx = Number(ev["index"]);
|
|
733
|
+
const cb = ev["content_block"] ?? {};
|
|
734
|
+
blocks.set(idx, {
|
|
735
|
+
type: typeof cb["type"] === "string" ? cb["type"] : "text",
|
|
736
|
+
text: typeof cb["text"] === "string" ? cb["text"] : "",
|
|
737
|
+
id: typeof cb["id"] === "string" ? cb["id"] : void 0,
|
|
738
|
+
name: typeof cb["name"] === "string" ? cb["name"] : void 0,
|
|
739
|
+
inputJson: ""
|
|
740
|
+
});
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
case "content_block_delta": {
|
|
744
|
+
const b = blocks.get(Number(ev["index"]));
|
|
745
|
+
if (!b) break;
|
|
746
|
+
const d = ev["delta"] ?? {};
|
|
747
|
+
if (d["type"] === "text_delta" && typeof d["text"] === "string") b.text += d["text"];
|
|
748
|
+
else if (d["type"] === "input_json_delta" && typeof d["partial_json"] === "string")
|
|
749
|
+
b.inputJson += d["partial_json"];
|
|
750
|
+
else if (d["type"] === "thinking_delta" && typeof d["thinking"] === "string")
|
|
751
|
+
b.text += d["thinking"];
|
|
752
|
+
else if (d["type"] === "signature_delta" && typeof d["signature"] === "string")
|
|
753
|
+
b.signature = (b.signature || "") + d["signature"];
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case "message_delta": {
|
|
757
|
+
const d = ev["delta"] ?? {};
|
|
758
|
+
if (d["stop_reason"] !== void 0) message["stop_reason"] = d["stop_reason"];
|
|
759
|
+
if (d["stop_sequence"] !== void 0) message["stop_sequence"] = d["stop_sequence"];
|
|
760
|
+
if (ev["usage"])
|
|
761
|
+
message["usage"] = { ...message["usage"], ...ev["usage"] };
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
default:
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
message["content"] = [...blocks.keys()].sort((a, b) => a - b).map((idx) => {
|
|
769
|
+
const blk = blocks.get(idx);
|
|
770
|
+
if (!blk) return void 0;
|
|
771
|
+
if (blk.type === "tool_use") {
|
|
772
|
+
let input = {};
|
|
773
|
+
try {
|
|
774
|
+
input = blk.inputJson ? JSON.parse(blk.inputJson) : {};
|
|
775
|
+
} catch {
|
|
776
|
+
input = {};
|
|
777
|
+
}
|
|
778
|
+
return { type: "tool_use", id: blk.id ?? "", name: blk.name ?? "", input };
|
|
779
|
+
}
|
|
780
|
+
if (blk.type === "thinking") {
|
|
781
|
+
const out = { type: "thinking", thinking: blk.text };
|
|
782
|
+
if (blk.signature) out.signature = blk.signature;
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
return { type: blk.type || "text", text: blk.text };
|
|
786
|
+
}).filter((b) => b !== void 0);
|
|
787
|
+
if (rewriteModel) message["model"] = rewriteModel;
|
|
788
|
+
return JSON.stringify(message);
|
|
789
|
+
}
|
|
654
790
|
function writeError(res, status, message) {
|
|
655
791
|
if (res.headersSent) return;
|
|
656
792
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
657
793
|
res.end(JSON.stringify({ error: { type: "provider_proxy_error", message } }));
|
|
658
794
|
}
|
|
795
|
+
function writeBoundAccountError(res, error) {
|
|
796
|
+
if (res.headersSent) return;
|
|
797
|
+
const headers = { "Content-Type": "application/json" };
|
|
798
|
+
if (error.status === 429 && error.resumeAt) {
|
|
799
|
+
const resumeMs = Date.parse(error.resumeAt);
|
|
800
|
+
if (Number.isFinite(resumeMs)) {
|
|
801
|
+
headers["Retry-After"] = String(Math.max(1, Math.ceil((resumeMs - Date.now()) / 1e3)));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
res.writeHead(error.status, headers);
|
|
805
|
+
res.end(
|
|
806
|
+
JSON.stringify({
|
|
807
|
+
error: {
|
|
808
|
+
type: "provider_proxy_error",
|
|
809
|
+
code: error.code,
|
|
810
|
+
reason: error.reason,
|
|
811
|
+
message: boundAccountSelectionMessage(error.reason)
|
|
812
|
+
}
|
|
813
|
+
})
|
|
814
|
+
);
|
|
815
|
+
}
|
|
659
816
|
function resolveApiKey(apiKey) {
|
|
660
817
|
if (!apiKey) return "";
|
|
661
818
|
if (apiKey.startsWith("$")) {
|
|
@@ -663,7 +820,12 @@ function resolveApiKey(apiKey) {
|
|
|
663
820
|
}
|
|
664
821
|
return apiKey;
|
|
665
822
|
}
|
|
666
|
-
async function resolvePoolBoundKey(deps, providerId, provider, sessionId) {
|
|
823
|
+
async function resolvePoolBoundKey(deps, providerId, provider, sessionId, preferredKeyId, boundKeyFallbackPolicy) {
|
|
824
|
+
if (preferredKeyId) {
|
|
825
|
+
const preferred = await deps.apiKeyPool?.getKeyById(providerId, preferredKeyId, sessionId);
|
|
826
|
+
if (preferred) return preferred;
|
|
827
|
+
if (boundKeyFallbackPolicy !== "pool") return "";
|
|
828
|
+
}
|
|
667
829
|
if (deps.apiKeyPool && sessionId) {
|
|
668
830
|
const poolKey = await deps.apiKeyPool.getKeyForSession(providerId, sessionId);
|
|
669
831
|
if (poolKey) return poolKey;
|
|
@@ -718,6 +880,8 @@ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isS
|
|
|
718
880
|
return {
|
|
719
881
|
auth,
|
|
720
882
|
preferredAccountId: route.preferredAccountId,
|
|
883
|
+
preferredAccountGroup: route.preferredAccountGroup,
|
|
884
|
+
boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
|
|
721
885
|
chain,
|
|
722
886
|
transformerProvider,
|
|
723
887
|
resolvedModel,
|
|
@@ -792,6 +956,8 @@ async function runPipeline(anthropicBody, plan, reportSelection) {
|
|
|
792
956
|
model: resolvedModel,
|
|
793
957
|
sessionKey: plan.sessionKey,
|
|
794
958
|
preferredAccountId: plan.preferredAccountId,
|
|
959
|
+
preferredAccountGroup: plan.preferredAccountGroup,
|
|
960
|
+
boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
|
|
795
961
|
reportSelection: (accountId, isActive) => {
|
|
796
962
|
proxyAccountId = accountId;
|
|
797
963
|
reportSelection?.(accountId, isActive);
|
|
@@ -838,6 +1004,8 @@ async function runSubscriptionSameFormatFetch(rawBody, plan, reportSelection, op
|
|
|
838
1004
|
model: plan.resolvedModel,
|
|
839
1005
|
sessionKey: plan.sessionKey,
|
|
840
1006
|
preferredAccountId: plan.preferredAccountId,
|
|
1007
|
+
preferredAccountGroup: plan.preferredAccountGroup,
|
|
1008
|
+
boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
|
|
841
1009
|
reportSelection: (accountId, isActive, remapped) => {
|
|
842
1010
|
proxyAccountId = accountId;
|
|
843
1011
|
remappedModel = remapped;
|
|
@@ -1026,21 +1194,61 @@ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {
|
|
|
1026
1194
|
if (!plan) return;
|
|
1027
1195
|
anthropicBody.model = plan.resolvedModel;
|
|
1028
1196
|
const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry(anthropicBody, rawBody, plan, route, deps, options) : await runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options);
|
|
1029
|
-
const
|
|
1197
|
+
const upstreamResponse = providerResponse.response;
|
|
1198
|
+
const upstreamSse = (upstreamResponse.headers.get("content-type") ?? "").includes(
|
|
1199
|
+
"text/event-stream"
|
|
1200
|
+
);
|
|
1201
|
+
const usageAttribution = {
|
|
1202
|
+
sessionId: route.sessionId,
|
|
1203
|
+
providerId: route.providerId ?? "anthropic",
|
|
1204
|
+
model: plan.resolvedModel,
|
|
1205
|
+
apiKeyId: route.apiKeyId ?? null,
|
|
1206
|
+
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1207
|
+
auditResponse: res
|
|
1208
|
+
};
|
|
1209
|
+
const usageRecorder = deps.usageRecorder;
|
|
1210
|
+
let anthropicInputUsage;
|
|
1211
|
+
const usageTap = usageRecorder ? {
|
|
1212
|
+
extractUsage(event) {
|
|
1213
|
+
if (event["type"] === "message_start") {
|
|
1214
|
+
const msg = event["message"];
|
|
1215
|
+
const u = msg?.["usage"];
|
|
1216
|
+
if (u) anthropicInputUsage = u;
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
if (event["type"] === "message_delta") {
|
|
1220
|
+
const outputUsage = event["usage"];
|
|
1221
|
+
if (!outputUsage) return null;
|
|
1222
|
+
return { ...anthropicInputUsage, ...outputUsage };
|
|
1223
|
+
}
|
|
1224
|
+
return null;
|
|
1225
|
+
},
|
|
1226
|
+
onUsage(rawUsage) {
|
|
1227
|
+
recordAnthropicStreamUsage(usageRecorder, rawUsage, usageAttribution);
|
|
1228
|
+
}
|
|
1229
|
+
} : void 0;
|
|
1230
|
+
let bodyText;
|
|
1231
|
+
if (!isStream && upstreamSse) {
|
|
1232
|
+
bodyText = await aggregateAnthropicSseToJsonBody(upstreamResponse, route.requestedModel);
|
|
1233
|
+
res.writeHead(
|
|
1234
|
+
upstreamResponse.status && upstreamResponse.status >= 100 ? upstreamResponse.status : 200,
|
|
1235
|
+
{ "Content-Type": "application/json" }
|
|
1236
|
+
);
|
|
1237
|
+
res.end(bodyText);
|
|
1238
|
+
} else {
|
|
1239
|
+
bodyText = await relayResponse(res, upstreamResponse, isStream, route.requestedModel, usageTap);
|
|
1240
|
+
}
|
|
1030
1241
|
if (bodyText && deps.usageRecorder) {
|
|
1031
|
-
recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText,
|
|
1032
|
-
sessionId: route.sessionId,
|
|
1033
|
-
providerId: route.providerId ?? "anthropic",
|
|
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
|
|
1038
|
-
});
|
|
1242
|
+
recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
|
|
1039
1243
|
}
|
|
1040
1244
|
} catch (err) {
|
|
1245
|
+
if (isBoundAccountSelectionError(err)) {
|
|
1246
|
+
writeBoundAccountError(res, err);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1041
1249
|
const errMsg = serializeError(err);
|
|
1042
1250
|
console.error("[ProviderProxy:anthropic] Pipeline error:", errMsg);
|
|
1043
|
-
writeError(res, 502, errMsg);
|
|
1251
|
+
writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
|
|
1044
1252
|
}
|
|
1045
1253
|
}
|
|
1046
1254
|
async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
|
|
@@ -1054,7 +1262,14 @@ async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
|
|
|
1054
1262
|
writeError(res, 502, `Provider not found: ${providerId}`);
|
|
1055
1263
|
return null;
|
|
1056
1264
|
}
|
|
1057
|
-
const apiKey = await resolvePoolBoundKey(
|
|
1265
|
+
const apiKey = await resolvePoolBoundKey(
|
|
1266
|
+
deps,
|
|
1267
|
+
providerId,
|
|
1268
|
+
provider,
|
|
1269
|
+
route.sessionId,
|
|
1270
|
+
route.preferredKeyId,
|
|
1271
|
+
route.boundKeyFallbackPolicy
|
|
1272
|
+
);
|
|
1058
1273
|
if (!apiKey) {
|
|
1059
1274
|
writeError(res, 502, "API key not configured");
|
|
1060
1275
|
return null;
|
|
@@ -1167,11 +1382,31 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
|
|
|
1167
1382
|
writeError(res, 502, "Anthropic route is missing a providerId");
|
|
1168
1383
|
return;
|
|
1169
1384
|
}
|
|
1385
|
+
let delegatedApiKey = hints.apiKey;
|
|
1386
|
+
if (route.authMode === "byo" && route.providerId) {
|
|
1387
|
+
const provider = await deps.llmConfig.getProvider(route.providerId);
|
|
1388
|
+
if (!provider) {
|
|
1389
|
+
writeError(res, 502, `Provider not found: ${route.providerId}`);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
delegatedApiKey = await resolvePoolBoundKey(
|
|
1393
|
+
deps,
|
|
1394
|
+
route.providerId,
|
|
1395
|
+
provider,
|
|
1396
|
+
route.sessionId,
|
|
1397
|
+
route.preferredKeyId,
|
|
1398
|
+
route.boundKeyFallbackPolicy
|
|
1399
|
+
);
|
|
1400
|
+
if (!delegatedApiKey) {
|
|
1401
|
+
writeError(res, 502, "API key not configured");
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1170
1405
|
const handler = handlerFactory({
|
|
1171
1406
|
llmConfig: deps.llmConfig,
|
|
1172
1407
|
providerId: route.providerId ?? "",
|
|
1173
1408
|
model: route.model,
|
|
1174
|
-
apiKey:
|
|
1409
|
+
apiKey: delegatedApiKey,
|
|
1175
1410
|
backgroundTaskModel: route.backgroundTaskModel,
|
|
1176
1411
|
isOfficialProvider: hints.isOfficialProvider,
|
|
1177
1412
|
thinkingLevel: hints.thinkingLevel,
|
|
@@ -1180,6 +1415,11 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
|
|
|
1180
1415
|
passThroughAuthToken: hints.passThroughAuthToken ?? null,
|
|
1181
1416
|
resolvePassThroughAuthToken: hints.resolvePassThroughAuthToken ?? null,
|
|
1182
1417
|
subscriptionProfile: hints.subscriptionProfile ?? null,
|
|
1418
|
+
preferredAccountId: hints.preferredAccountId,
|
|
1419
|
+
preferredAccountGroup: hints.preferredAccountGroup,
|
|
1420
|
+
boundAccountFallbackPolicy: hints.boundAccountFallbackPolicy,
|
|
1421
|
+
preferredKeyId: route.preferredKeyId,
|
|
1422
|
+
boundKeyFallbackPolicy: route.boundKeyFallbackPolicy,
|
|
1183
1423
|
maxConcurrency: hints.maxConcurrency,
|
|
1184
1424
|
webSearchService: hints.webSearchService ?? null,
|
|
1185
1425
|
onRetry: hints.onRetry,
|
|
@@ -1206,23 +1446,36 @@ function readGeminiUsage(usage) {
|
|
|
1206
1446
|
reasoningTokens
|
|
1207
1447
|
};
|
|
1208
1448
|
}
|
|
1449
|
+
function emitGeminiUsageRecord(recorder, rawUsageMetadata, attribution) {
|
|
1450
|
+
const tapped = readGeminiUsage(rawUsageMetadata);
|
|
1451
|
+
if (!tapped) return;
|
|
1452
|
+
recorder.record({
|
|
1453
|
+
messageId: null,
|
|
1454
|
+
parentMessageId: null,
|
|
1455
|
+
sessionId: attribution.sessionId,
|
|
1456
|
+
providerId: attribution.providerId,
|
|
1457
|
+
model: attribution.model,
|
|
1458
|
+
apiKeyId: attribution.apiKeyId,
|
|
1459
|
+
auditResponse: attribution.auditResponse,
|
|
1460
|
+
engineOrigin: "codex-ingress",
|
|
1461
|
+
usage: tapped,
|
|
1462
|
+
rawUsage: rawUsageMetadata
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1209
1465
|
function recordGeminiNonStreamUsage(recorder, bodyText, attribution) {
|
|
1210
1466
|
try {
|
|
1211
1467
|
const parsed = JSON.parse(bodyText);
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
usage: tapped,
|
|
1224
|
-
rawUsage: parsed.usageMetadata
|
|
1225
|
-
});
|
|
1468
|
+
emitGeminiUsageRecord(
|
|
1469
|
+
recorder,
|
|
1470
|
+
parsed.usageMetadata,
|
|
1471
|
+
attribution
|
|
1472
|
+
);
|
|
1473
|
+
} catch {
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
function recordGeminiStreamUsage(recorder, rawUsageMetadata, attribution) {
|
|
1477
|
+
try {
|
|
1478
|
+
emitGeminiUsageRecord(recorder, rawUsageMetadata, attribution);
|
|
1226
1479
|
} catch {
|
|
1227
1480
|
}
|
|
1228
1481
|
}
|
|
@@ -1263,16 +1516,32 @@ async function handleGeminiGenerateContentRequest(res, rawBody, url, route, deps
|
|
|
1263
1516
|
const plan = await buildByoPlan2(res, route, deps, resolvedModel, isStream);
|
|
1264
1517
|
if (!plan) return;
|
|
1265
1518
|
const providerResponse = await runPipelineWithPoolReporting2(geminiBody, plan);
|
|
1266
|
-
const
|
|
1519
|
+
const usageAttribution = {
|
|
1520
|
+
sessionId: route.sessionId,
|
|
1521
|
+
providerId: route.providerId ?? "gemini",
|
|
1522
|
+
model: resolvedModel,
|
|
1523
|
+
apiKeyId: route.apiKeyId ?? null,
|
|
1524
|
+
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1525
|
+
auditResponse: res
|
|
1526
|
+
};
|
|
1527
|
+
const usageRecorder = deps.usageRecorder;
|
|
1528
|
+
const usageTap = usageRecorder ? {
|
|
1529
|
+
extractUsage(event) {
|
|
1530
|
+
return event["usageMetadata"] ?? null;
|
|
1531
|
+
},
|
|
1532
|
+
onUsage(rawUsage) {
|
|
1533
|
+
recordGeminiStreamUsage(usageRecorder, rawUsage, usageAttribution);
|
|
1534
|
+
}
|
|
1535
|
+
} : void 0;
|
|
1536
|
+
const bodyText = await relayResponse(
|
|
1537
|
+
res,
|
|
1538
|
+
providerResponse.response,
|
|
1539
|
+
isStream,
|
|
1540
|
+
void 0,
|
|
1541
|
+
usageTap
|
|
1542
|
+
);
|
|
1267
1543
|
if (bodyText && deps.usageRecorder) {
|
|
1268
|
-
recordGeminiNonStreamUsage(deps.usageRecorder, bodyText,
|
|
1269
|
-
sessionId: route.sessionId,
|
|
1270
|
-
providerId: route.providerId ?? "gemini",
|
|
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
|
|
1275
|
-
});
|
|
1544
|
+
recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
|
|
1276
1545
|
}
|
|
1277
1546
|
} catch (err) {
|
|
1278
1547
|
const errMsg = serializeError(err);
|
|
@@ -1291,7 +1560,14 @@ async function buildByoPlan2(res, route, deps, resolvedModel, isStream) {
|
|
|
1291
1560
|
writeError(res, 502, `Provider not found: ${providerId}`);
|
|
1292
1561
|
return null;
|
|
1293
1562
|
}
|
|
1294
|
-
const apiKey = await resolvePoolBoundKey(
|
|
1563
|
+
const apiKey = await resolvePoolBoundKey(
|
|
1564
|
+
deps,
|
|
1565
|
+
providerId,
|
|
1566
|
+
provider,
|
|
1567
|
+
route.sessionId,
|
|
1568
|
+
route.preferredKeyId,
|
|
1569
|
+
route.boundKeyFallbackPolicy
|
|
1570
|
+
);
|
|
1295
1571
|
if (!apiKey) {
|
|
1296
1572
|
writeError(res, 502, "API key not configured");
|
|
1297
1573
|
return null;
|
|
@@ -1391,23 +1667,36 @@ function readChatCompletionsUsage(usage) {
|
|
|
1391
1667
|
reasoningTokens
|
|
1392
1668
|
};
|
|
1393
1669
|
}
|
|
1670
|
+
function emitChatCompletionsUsageRecord(recorder, rawUsage, attribution) {
|
|
1671
|
+
const tapped = readChatCompletionsUsage(rawUsage);
|
|
1672
|
+
if (!tapped) return;
|
|
1673
|
+
recorder.record({
|
|
1674
|
+
messageId: null,
|
|
1675
|
+
parentMessageId: null,
|
|
1676
|
+
sessionId: attribution.sessionId,
|
|
1677
|
+
providerId: attribution.providerId,
|
|
1678
|
+
model: attribution.model,
|
|
1679
|
+
apiKeyId: attribution.apiKeyId,
|
|
1680
|
+
auditResponse: attribution.auditResponse,
|
|
1681
|
+
engineOrigin: "codex-ingress",
|
|
1682
|
+
usage: tapped,
|
|
1683
|
+
rawUsage
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1394
1686
|
function recordChatCompletionsNonStreamUsage(recorder, bodyText, attribution) {
|
|
1395
1687
|
try {
|
|
1396
1688
|
const parsed = JSON.parse(bodyText);
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
usage: tapped,
|
|
1409
|
-
rawUsage: parsed.usage
|
|
1410
|
-
});
|
|
1689
|
+
emitChatCompletionsUsageRecord(
|
|
1690
|
+
recorder,
|
|
1691
|
+
parsed.usage,
|
|
1692
|
+
attribution
|
|
1693
|
+
);
|
|
1694
|
+
} catch {
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function recordChatCompletionsStreamUsage(recorder, rawUsage, attribution) {
|
|
1698
|
+
try {
|
|
1699
|
+
emitChatCompletionsUsageRecord(recorder, rawUsage, attribution);
|
|
1411
1700
|
} catch {
|
|
1412
1701
|
}
|
|
1413
1702
|
}
|
|
@@ -1435,21 +1724,41 @@ async function handleOpenAIChatRequest(res, rawBody, route, deps) {
|
|
|
1435
1724
|
const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan3(res, route, deps, resolvedModel, isStream);
|
|
1436
1725
|
if (!plan) return;
|
|
1437
1726
|
const providerResponse = plan.isSubscription ? await runPipelineWithSubscriptionRetry2(chatBody, plan) : await runPipelineWithPoolReporting3(chatBody, plan);
|
|
1438
|
-
const
|
|
1727
|
+
const usageAttribution = {
|
|
1728
|
+
sessionId: route.sessionId,
|
|
1729
|
+
providerId: route.providerId ?? "openai",
|
|
1730
|
+
model: resolvedModel,
|
|
1731
|
+
apiKeyId: route.apiKeyId ?? null,
|
|
1732
|
+
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1733
|
+
auditResponse: res
|
|
1734
|
+
};
|
|
1735
|
+
const usageRecorder = deps.usageRecorder;
|
|
1736
|
+
const usageTap = usageRecorder ? {
|
|
1737
|
+
extractUsage(event) {
|
|
1738
|
+
return event["usage"] ?? null;
|
|
1739
|
+
},
|
|
1740
|
+
onUsage(rawUsage) {
|
|
1741
|
+
recordChatCompletionsStreamUsage(usageRecorder, rawUsage, usageAttribution);
|
|
1742
|
+
}
|
|
1743
|
+
} : void 0;
|
|
1744
|
+
const bodyText = await relayResponse(
|
|
1745
|
+
res,
|
|
1746
|
+
providerResponse.response,
|
|
1747
|
+
isStream,
|
|
1748
|
+
void 0,
|
|
1749
|
+
usageTap
|
|
1750
|
+
);
|
|
1439
1751
|
if (bodyText && deps.usageRecorder) {
|
|
1440
|
-
recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText,
|
|
1441
|
-
sessionId: route.sessionId,
|
|
1442
|
-
providerId: route.providerId ?? "openai",
|
|
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
|
|
1447
|
-
});
|
|
1752
|
+
recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
|
|
1448
1753
|
}
|
|
1449
1754
|
} catch (err) {
|
|
1755
|
+
if (isBoundAccountSelectionError(err)) {
|
|
1756
|
+
writeBoundAccountError(res, err);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1450
1759
|
const errMsg = serializeError(err);
|
|
1451
1760
|
console.error("[ProviderProxy:chat] Pipeline error:", errMsg);
|
|
1452
|
-
writeError(res, 502, errMsg);
|
|
1761
|
+
writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
|
|
1453
1762
|
}
|
|
1454
1763
|
}
|
|
1455
1764
|
async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
|
|
@@ -1463,7 +1772,14 @@ async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
|
|
|
1463
1772
|
writeError(res, 502, `Provider not found: ${providerId}`);
|
|
1464
1773
|
return null;
|
|
1465
1774
|
}
|
|
1466
|
-
const apiKey = await resolvePoolBoundKey(
|
|
1775
|
+
const apiKey = await resolvePoolBoundKey(
|
|
1776
|
+
deps,
|
|
1777
|
+
providerId,
|
|
1778
|
+
provider,
|
|
1779
|
+
route.sessionId,
|
|
1780
|
+
route.preferredKeyId,
|
|
1781
|
+
route.boundKeyFallbackPolicy
|
|
1782
|
+
);
|
|
1467
1783
|
if (!apiKey) {
|
|
1468
1784
|
writeError(res, 502, "API key not configured");
|
|
1469
1785
|
return null;
|
|
@@ -1529,6 +1845,9 @@ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream)
|
|
|
1529
1845
|
};
|
|
1530
1846
|
return {
|
|
1531
1847
|
auth,
|
|
1848
|
+
preferredAccountId: route.preferredAccountId,
|
|
1849
|
+
preferredAccountGroup: route.preferredAccountGroup,
|
|
1850
|
+
boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
|
|
1532
1851
|
chain,
|
|
1533
1852
|
transformerProvider,
|
|
1534
1853
|
resolvedModel,
|
|
@@ -1547,6 +1866,9 @@ async function runPipeline3(chatBody, plan) {
|
|
|
1547
1866
|
await auth.applyHeaders(authHeaders, {
|
|
1548
1867
|
upstreamUrl,
|
|
1549
1868
|
model: resolvedModel,
|
|
1869
|
+
preferredAccountId: plan.preferredAccountId,
|
|
1870
|
+
preferredAccountGroup: plan.preferredAccountGroup,
|
|
1871
|
+
boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
|
|
1550
1872
|
reportSelection: (accountId) => {
|
|
1551
1873
|
proxyAccountId = accountId;
|
|
1552
1874
|
}
|
|
@@ -1625,23 +1947,36 @@ function readResponsesUsage(usage) {
|
|
|
1625
1947
|
reasoningTokens
|
|
1626
1948
|
};
|
|
1627
1949
|
}
|
|
1950
|
+
function emitResponsesUsageRecord(recorder, rawUsage, attribution) {
|
|
1951
|
+
const tapped = readResponsesUsage(rawUsage);
|
|
1952
|
+
if (!tapped) return;
|
|
1953
|
+
recorder.record({
|
|
1954
|
+
messageId: null,
|
|
1955
|
+
parentMessageId: null,
|
|
1956
|
+
sessionId: attribution.sessionId,
|
|
1957
|
+
providerId: attribution.providerId,
|
|
1958
|
+
model: attribution.model,
|
|
1959
|
+
apiKeyId: attribution.apiKeyId,
|
|
1960
|
+
auditResponse: attribution.auditResponse,
|
|
1961
|
+
engineOrigin: "codex-ingress",
|
|
1962
|
+
usage: tapped,
|
|
1963
|
+
rawUsage
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1628
1966
|
function recordResponsesNonStreamUsage(recorder, bodyText, attribution) {
|
|
1629
1967
|
try {
|
|
1630
1968
|
const parsed = JSON.parse(bodyText);
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
usage: tapped,
|
|
1643
|
-
rawUsage: parsed.usage
|
|
1644
|
-
});
|
|
1969
|
+
emitResponsesUsageRecord(
|
|
1970
|
+
recorder,
|
|
1971
|
+
parsed.usage,
|
|
1972
|
+
attribution
|
|
1973
|
+
);
|
|
1974
|
+
} catch {
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
function recordResponsesStreamUsage(recorder, rawUsage, attribution) {
|
|
1978
|
+
try {
|
|
1979
|
+
emitResponsesUsageRecord(recorder, rawUsage, attribution);
|
|
1645
1980
|
} catch {
|
|
1646
1981
|
}
|
|
1647
1982
|
}
|
|
@@ -1652,7 +1987,7 @@ function isOpenAIResponsesRequest(method, url) {
|
|
|
1652
1987
|
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
1653
1988
|
return path.endsWith("/responses");
|
|
1654
1989
|
}
|
|
1655
|
-
async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
|
|
1990
|
+
async function handleOpenAIResponsesRequest(res, rawBody, route, deps, requestHeaders = {}) {
|
|
1656
1991
|
let responsesBody;
|
|
1657
1992
|
try {
|
|
1658
1993
|
responsesBody = JSON.parse(rawBody);
|
|
@@ -1661,27 +1996,58 @@ async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
|
|
|
1661
1996
|
return;
|
|
1662
1997
|
}
|
|
1663
1998
|
const isStream = responsesBody.stream === true;
|
|
1999
|
+
const derivedSession = deriveGatewaySessionKey(responsesBody, requestHeaders, {
|
|
2000
|
+
endpoint: "responses",
|
|
2001
|
+
// A verified outbound key is the strongest route-local fallback. Resident
|
|
2002
|
+
// proxy callers without one still get a stable run/session bucket; the
|
|
2003
|
+
// final `anonymous` fallback is handled inside the shared utility.
|
|
2004
|
+
fallbackKey: route.apiKeyId ?? route.sessionId ?? void 0
|
|
2005
|
+
});
|
|
2006
|
+
const sessionKey = derivedSession.key;
|
|
1664
2007
|
const resolvedModel = route.model;
|
|
1665
2008
|
responsesBody.model = resolvedModel;
|
|
1666
2009
|
try {
|
|
1667
|
-
const plan = route.authMode === "subscription" ? await buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
|
|
2010
|
+
const plan = route.authMode === "subscription" ? await buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream, sessionKey) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
|
|
1668
2011
|
if (!plan) return;
|
|
1669
2012
|
const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry3(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
|
|
1670
|
-
const
|
|
2013
|
+
const usageAttribution = {
|
|
2014
|
+
sessionId: route.sessionId,
|
|
2015
|
+
providerId: route.providerId ?? "codex",
|
|
2016
|
+
model: resolvedModel,
|
|
2017
|
+
apiKeyId: route.apiKeyId ?? null,
|
|
2018
|
+
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
2019
|
+
auditResponse: res
|
|
2020
|
+
};
|
|
2021
|
+
const usageRecorder = deps.usageRecorder;
|
|
2022
|
+
const usageTap = usageRecorder ? {
|
|
2023
|
+
extractUsage(event) {
|
|
2024
|
+
if (event["type"] !== "response.completed") return null;
|
|
2025
|
+
const response = event["response"];
|
|
2026
|
+
const usage = response?.["usage"];
|
|
2027
|
+
return usage ?? null;
|
|
2028
|
+
},
|
|
2029
|
+
onUsage(rawUsage) {
|
|
2030
|
+
recordResponsesStreamUsage(usageRecorder, rawUsage, usageAttribution);
|
|
2031
|
+
}
|
|
2032
|
+
} : void 0;
|
|
2033
|
+
const bodyText = await relayResponse(
|
|
2034
|
+
res,
|
|
2035
|
+
providerResponse.response,
|
|
2036
|
+
isStream,
|
|
2037
|
+
route.requestedModel,
|
|
2038
|
+
usageTap
|
|
2039
|
+
);
|
|
1671
2040
|
if (bodyText && deps.usageRecorder) {
|
|
1672
|
-
recordResponsesNonStreamUsage(deps.usageRecorder, bodyText,
|
|
1673
|
-
sessionId: route.sessionId,
|
|
1674
|
-
providerId: route.providerId ?? "codex",
|
|
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
|
|
1679
|
-
});
|
|
2041
|
+
recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
|
|
1680
2042
|
}
|
|
1681
2043
|
} catch (err) {
|
|
2044
|
+
if (isBoundAccountSelectionError(err)) {
|
|
2045
|
+
writeBoundAccountError(res, err);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
1682
2048
|
const errMsg = serializeError(err);
|
|
1683
2049
|
console.error("[ProviderProxy:responses] Pipeline error:", errMsg);
|
|
1684
|
-
writeError(res, 502, errMsg);
|
|
2050
|
+
writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
|
|
1685
2051
|
}
|
|
1686
2052
|
}
|
|
1687
2053
|
async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
|
|
@@ -1695,7 +2061,14 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
|
|
|
1695
2061
|
writeError(res, 502, `Provider not found: ${providerId}`);
|
|
1696
2062
|
return null;
|
|
1697
2063
|
}
|
|
1698
|
-
const apiKey = await resolvePoolBoundKey(
|
|
2064
|
+
const apiKey = await resolvePoolBoundKey(
|
|
2065
|
+
deps,
|
|
2066
|
+
providerId,
|
|
2067
|
+
provider,
|
|
2068
|
+
route.sessionId,
|
|
2069
|
+
route.preferredKeyId,
|
|
2070
|
+
route.boundKeyFallbackPolicy
|
|
2071
|
+
);
|
|
1699
2072
|
if (!apiKey) {
|
|
1700
2073
|
writeError(res, 502, "API key not configured");
|
|
1701
2074
|
return null;
|
|
@@ -1726,7 +2099,7 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
|
|
|
1726
2099
|
proxyProviderId: "byo"
|
|
1727
2100
|
};
|
|
1728
2101
|
}
|
|
1729
|
-
async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) {
|
|
2102
|
+
async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream, sessionKey) {
|
|
1730
2103
|
const profile = route.subscriptionProfile;
|
|
1731
2104
|
if (!profile) {
|
|
1732
2105
|
writeError(res, 502, "Subscription mode requires a codex subscription profile");
|
|
@@ -1751,15 +2124,27 @@ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream)
|
|
|
1751
2124
|
};
|
|
1752
2125
|
if (profile.authStrategy.providerId === "gemini") {
|
|
1753
2126
|
try {
|
|
1754
|
-
transformerProvider.geminiProject = await resolveGeminiCodeAssistProject(profile
|
|
2127
|
+
transformerProvider.geminiProject = await resolveGeminiCodeAssistProject(profile, {
|
|
2128
|
+
sessionKey,
|
|
2129
|
+
preferredAccountId: route.preferredAccountId,
|
|
2130
|
+
preferredAccountGroup: route.preferredAccountGroup,
|
|
2131
|
+
boundAccountFallbackPolicy: route.boundAccountFallbackPolicy
|
|
2132
|
+
});
|
|
1755
2133
|
} catch (err) {
|
|
2134
|
+
if (isBoundAccountSelectionError(err)) {
|
|
2135
|
+
writeBoundAccountError(res, err);
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
1756
2138
|
writeError(res, 502, serializeError(err));
|
|
1757
2139
|
return null;
|
|
1758
2140
|
}
|
|
1759
2141
|
}
|
|
1760
2142
|
return {
|
|
1761
2143
|
auth,
|
|
2144
|
+
sessionKey,
|
|
1762
2145
|
preferredAccountId: route.preferredAccountId,
|
|
2146
|
+
preferredAccountGroup: route.preferredAccountGroup,
|
|
2147
|
+
boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
|
|
1763
2148
|
chain,
|
|
1764
2149
|
transformerProvider,
|
|
1765
2150
|
resolvedModel,
|
|
@@ -1772,9 +2157,9 @@ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream)
|
|
|
1772
2157
|
proxyProviderId: profile.authStrategy.providerId
|
|
1773
2158
|
};
|
|
1774
2159
|
}
|
|
1775
|
-
async function resolveGeminiCodeAssistProject(profile) {
|
|
2160
|
+
async function resolveGeminiCodeAssistProject(profile, hints) {
|
|
1776
2161
|
const probe = {};
|
|
1777
|
-
await profile.authStrategy.applyHeaders(probe);
|
|
2162
|
+
await profile.authStrategy.applyHeaders(probe, hints);
|
|
1778
2163
|
const bearer = probe.Authorization ?? probe.authorization ?? "";
|
|
1779
2164
|
const accessToken = bearer.replace(/^Bearer\s+/i, "").trim();
|
|
1780
2165
|
if (!accessToken) return void 0;
|
|
@@ -1785,13 +2170,16 @@ async function resolveGeminiCodeAssistProject(profile) {
|
|
|
1785
2170
|
async function runPipeline4(responsesBody, plan) {
|
|
1786
2171
|
const executor = getSharedExecutor();
|
|
1787
2172
|
const endpointTransformer = getResponsesEndpointTransformer();
|
|
1788
|
-
const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
|
|
2173
|
+
const { auth, sessionKey, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
|
|
1789
2174
|
let proxyAccountId;
|
|
1790
2175
|
const authHeaders = {};
|
|
1791
2176
|
await auth.applyHeaders(authHeaders, {
|
|
1792
2177
|
upstreamUrl,
|
|
1793
2178
|
model: resolvedModel,
|
|
2179
|
+
sessionKey,
|
|
1794
2180
|
preferredAccountId: plan.preferredAccountId,
|
|
2181
|
+
preferredAccountGroup: plan.preferredAccountGroup,
|
|
2182
|
+
boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
|
|
1795
2183
|
reportSelection: (accountId) => {
|
|
1796
2184
|
proxyAccountId = accountId;
|
|
1797
2185
|
}
|
|
@@ -1844,7 +2232,7 @@ async function runPipelineWithPoolReporting4(responsesBody, plan) {
|
|
|
1844
2232
|
async function runPipelineWithSubscriptionRetry3(responsesBody, plan) {
|
|
1845
2233
|
const first = await runPipeline4(responsesBody, plan);
|
|
1846
2234
|
if (first.rawStatus !== 401) return first;
|
|
1847
|
-
const refreshed = await plan.auth.onUnauthorized?.();
|
|
2235
|
+
const refreshed = await plan.auth.onUnauthorized?.(plan.sessionKey);
|
|
1848
2236
|
if (!refreshed) {
|
|
1849
2237
|
console.warn("[ProviderProxy:responses] 401 not recoverable (onUnauthorized returned false)");
|
|
1850
2238
|
return first;
|
|
@@ -1883,7 +2271,7 @@ async function routeRequest(req, res, routes, deps) {
|
|
|
1883
2271
|
}
|
|
1884
2272
|
if (isOpenAIResponsesRequest(method, url)) {
|
|
1885
2273
|
const rawBody = await readBody(req);
|
|
1886
|
-
await handleOpenAIResponsesRequest(res, rawBody, route, deps);
|
|
2274
|
+
await handleOpenAIResponsesRequest(res, rawBody, route, deps, req.headers);
|
|
1887
2275
|
return;
|
|
1888
2276
|
}
|
|
1889
2277
|
if (isOpenAIChatRequest(method, url)) {
|
|
@@ -2093,6 +2481,38 @@ var DEFAULT_ACCOUNT_PROBE = {
|
|
|
2093
2481
|
historySize: 10,
|
|
2094
2482
|
staggerMs: 500
|
|
2095
2483
|
};
|
|
2484
|
+
var DEFAULT_ALLOWANCE_SCHEDULING = {
|
|
2485
|
+
enabled: false,
|
|
2486
|
+
demoteAtPercent: 80,
|
|
2487
|
+
pauseAtPercent: 98,
|
|
2488
|
+
priorityPenalty: 100
|
|
2489
|
+
};
|
|
2490
|
+
function normalizeAllowanceScheduling(raw) {
|
|
2491
|
+
const value = raw?.allowanceScheduling;
|
|
2492
|
+
const demoteAtPercent = clampNumber(
|
|
2493
|
+
value?.demoteAtPercent,
|
|
2494
|
+
0,
|
|
2495
|
+
100,
|
|
2496
|
+
DEFAULT_ALLOWANCE_SCHEDULING.demoteAtPercent
|
|
2497
|
+
);
|
|
2498
|
+
const requestedPause = clampNumber(
|
|
2499
|
+
value?.pauseAtPercent,
|
|
2500
|
+
0,
|
|
2501
|
+
100,
|
|
2502
|
+
DEFAULT_ALLOWANCE_SCHEDULING.pauseAtPercent
|
|
2503
|
+
);
|
|
2504
|
+
return {
|
|
2505
|
+
enabled: value?.enabled === true,
|
|
2506
|
+
demoteAtPercent,
|
|
2507
|
+
pauseAtPercent: Math.max(demoteAtPercent, requestedPause),
|
|
2508
|
+
priorityPenalty: Math.trunc(clampNumber(
|
|
2509
|
+
value?.priorityPenalty,
|
|
2510
|
+
1,
|
|
2511
|
+
1e3,
|
|
2512
|
+
DEFAULT_ALLOWANCE_SCHEDULING.priorityPenalty
|
|
2513
|
+
))
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2096
2516
|
function normalizeAccountProbe(raw) {
|
|
2097
2517
|
const ap = raw?.accountProbe;
|
|
2098
2518
|
return {
|
|
@@ -2287,7 +2707,8 @@ function normalizePrefixTargets(raw) {
|
|
|
2287
2707
|
function normalizeEndpointConfig(e) {
|
|
2288
2708
|
const endpoint = e.endpoint;
|
|
2289
2709
|
const useSubscription = e.useSubscription === true;
|
|
2290
|
-
const boundAccountId = typeof e.boundAccountId === "string" && e.boundAccountId.trim() !== "" ? e.boundAccountId : void 0;
|
|
2710
|
+
const boundAccountId = typeof e.boundAccountId === "string" && e.boundAccountId.trim() !== "" ? e.boundAccountId.trim() : void 0;
|
|
2711
|
+
const boundAccountFallbackPolicy = boundAccountId ? e.boundAccountFallbackPolicy === "pool" ? "pool" : "strict" : void 0;
|
|
2291
2712
|
const boundKeyId = typeof e.boundKeyId === "string" && e.boundKeyId.trim() !== "" ? e.boundKeyId : void 0;
|
|
2292
2713
|
let config;
|
|
2293
2714
|
if (isKindMappedEndpoint(endpoint)) {
|
|
@@ -2317,21 +2738,186 @@ function normalizeEndpointConfig(e) {
|
|
|
2317
2738
|
config.backgroundModelIds = e.backgroundModelIds;
|
|
2318
2739
|
}
|
|
2319
2740
|
}
|
|
2320
|
-
if (boundAccountId)
|
|
2741
|
+
if (boundAccountId) {
|
|
2742
|
+
config.boundAccountId = boundAccountId;
|
|
2743
|
+
config.boundAccountFallbackPolicy = boundAccountFallbackPolicy;
|
|
2744
|
+
}
|
|
2321
2745
|
if (boundKeyId) config.boundKeyId = boundKeyId;
|
|
2322
2746
|
return config;
|
|
2323
2747
|
}
|
|
2748
|
+
function normalizeBindingTarget(raw) {
|
|
2749
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2750
|
+
const target = raw;
|
|
2751
|
+
const providerId = typeof target.providerId === "string" ? target.providerId.trim() : "";
|
|
2752
|
+
if (!providerId) return null;
|
|
2753
|
+
if (target.kind === "account") {
|
|
2754
|
+
const accountId = typeof target.accountId === "string" ? target.accountId.trim() : "";
|
|
2755
|
+
return accountId ? { kind: "account", providerId, accountId } : null;
|
|
2756
|
+
}
|
|
2757
|
+
if (target.kind === "account-group") {
|
|
2758
|
+
const group = typeof target.group === "string" ? target.group.trim() : "";
|
|
2759
|
+
return group ? { kind: "account-group", providerId, group } : null;
|
|
2760
|
+
}
|
|
2761
|
+
if (target.kind === "account-pool") return { kind: "account-pool", providerId };
|
|
2762
|
+
if (target.kind === "provider") {
|
|
2763
|
+
const keyId = typeof target.keyId === "string" ? target.keyId.trim() : "";
|
|
2764
|
+
return keyId ? { kind: "provider", providerId, keyId } : { kind: "provider", providerId };
|
|
2765
|
+
}
|
|
2766
|
+
return null;
|
|
2767
|
+
}
|
|
2768
|
+
function normalizeGatewayModelMappings(raw) {
|
|
2769
|
+
if (!Array.isArray(raw)) return [];
|
|
2770
|
+
const mappings = [];
|
|
2771
|
+
for (const entry of raw) {
|
|
2772
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2773
|
+
const value = entry;
|
|
2774
|
+
const source = typeof value.source === "string" ? value.source.trim() : "";
|
|
2775
|
+
const target = typeof value.target === "string" ? value.target.trim() : "";
|
|
2776
|
+
if (!source || !target) continue;
|
|
2777
|
+
mappings.push({ source, target });
|
|
2778
|
+
}
|
|
2779
|
+
return mappings;
|
|
2780
|
+
}
|
|
2781
|
+
function hasConfiguredLegacyModels(config) {
|
|
2782
|
+
return Object.values(config.modelMap ?? {}).some((value) => typeof value === "string" && value.trim()) || Boolean(config.models?.some((value) => typeof value === "string" && value.trim())) || Boolean(config.defaultModel?.trim()) || Boolean(config.backgroundModel?.trim()) || Boolean(Object.values(config.prefixTargets ?? {}).some((value) => value?.trim()));
|
|
2783
|
+
}
|
|
2784
|
+
function splitModelRef(ref) {
|
|
2785
|
+
if (typeof ref !== "string") return null;
|
|
2786
|
+
const idx = ref.indexOf(",");
|
|
2787
|
+
if (idx <= 0) return null;
|
|
2788
|
+
const providerId = ref.slice(0, idx).trim();
|
|
2789
|
+
const modelId = ref.slice(idx + 1).trim();
|
|
2790
|
+
return providerId && modelId ? { providerId, modelId } : null;
|
|
2791
|
+
}
|
|
2792
|
+
function legacyTarget(config, providerId) {
|
|
2793
|
+
if (config.useSubscription) {
|
|
2794
|
+
if (config.boundAccountId) {
|
|
2795
|
+
return { kind: "account", providerId, accountId: config.boundAccountId };
|
|
2796
|
+
}
|
|
2797
|
+
if (config.boundAccountGroup) {
|
|
2798
|
+
return { kind: "account-group", providerId, group: config.boundAccountGroup };
|
|
2799
|
+
}
|
|
2800
|
+
return { kind: "account-pool", providerId };
|
|
2801
|
+
}
|
|
2802
|
+
return config.boundKeyId ? { kind: "provider", providerId, keyId: config.boundKeyId } : { kind: "provider", providerId };
|
|
2803
|
+
}
|
|
2804
|
+
function legacyEndpointToBindings(config) {
|
|
2805
|
+
const fallback = config.boundAccountId && config.boundAccountFallbackPolicy === "strict" ? "fail" : "next";
|
|
2806
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
2807
|
+
const routeFor = (providerId) => {
|
|
2808
|
+
const existing = byProvider.get(providerId);
|
|
2809
|
+
if (existing) return existing;
|
|
2810
|
+
const created = {
|
|
2811
|
+
id: `legacy-${config.endpoint}-${providerId}`,
|
|
2812
|
+
name: `${config.endpoint} \xB7 ${providerId}`,
|
|
2813
|
+
enabled: true,
|
|
2814
|
+
keyScope: "all",
|
|
2815
|
+
endpoint: config.endpoint,
|
|
2816
|
+
target: legacyTarget(config, providerId),
|
|
2817
|
+
fallback,
|
|
2818
|
+
modelMode: "mapped"
|
|
2819
|
+
};
|
|
2820
|
+
byProvider.set(providerId, created);
|
|
2821
|
+
return created;
|
|
2822
|
+
};
|
|
2823
|
+
for (const [kind, ref] of Object.entries(config.modelMap ?? {})) {
|
|
2824
|
+
const parsed = splitModelRef(ref);
|
|
2825
|
+
if (!parsed) continue;
|
|
2826
|
+
const route = routeFor(parsed.providerId);
|
|
2827
|
+
route.modelMap = { ...route.modelMap, [kind]: ref };
|
|
2828
|
+
}
|
|
2829
|
+
for (const ref of config.models ?? []) {
|
|
2830
|
+
const parsed = splitModelRef(ref);
|
|
2831
|
+
if (!parsed) continue;
|
|
2832
|
+
const route = routeFor(parsed.providerId);
|
|
2833
|
+
route.models = [...route.models ?? [], ref];
|
|
2834
|
+
}
|
|
2835
|
+
for (const [prefix, ref] of Object.entries(config.prefixTargets ?? {})) {
|
|
2836
|
+
const parsed = splitModelRef(ref);
|
|
2837
|
+
if (!parsed) continue;
|
|
2838
|
+
const route = routeFor(parsed.providerId);
|
|
2839
|
+
route.dispatchMode = "prefix";
|
|
2840
|
+
route.prefixTargets = { ...route.prefixTargets, [prefix]: ref };
|
|
2841
|
+
}
|
|
2842
|
+
for (const [field, ref] of [
|
|
2843
|
+
["defaultModel", config.defaultModel],
|
|
2844
|
+
["backgroundModel", config.backgroundModel]
|
|
2845
|
+
]) {
|
|
2846
|
+
const parsed = splitModelRef(ref);
|
|
2847
|
+
if (!parsed) continue;
|
|
2848
|
+
const route = routeFor(parsed.providerId);
|
|
2849
|
+
route[field] = ref;
|
|
2850
|
+
if (config.backgroundModelIds?.length) {
|
|
2851
|
+
route.backgroundModelIds = [...config.backgroundModelIds];
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
return [...byProvider.values()];
|
|
2855
|
+
}
|
|
2856
|
+
function legacyEndpointsToBindings(endpoints) {
|
|
2857
|
+
return endpoints.filter((endpoint) => hasConfiguredLegacyModels(endpoint)).flatMap((endpoint) => legacyEndpointToBindings(endpoint));
|
|
2858
|
+
}
|
|
2859
|
+
function normalizeGatewayBindings(raw) {
|
|
2860
|
+
if (!Array.isArray(raw)) return [];
|
|
2861
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2862
|
+
const bindings = [];
|
|
2863
|
+
for (const entry of raw) {
|
|
2864
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2865
|
+
const value = entry;
|
|
2866
|
+
const id = typeof value.id === "string" ? value.id.trim() : "";
|
|
2867
|
+
const name = typeof value.name === "string" ? value.name.trim() : "";
|
|
2868
|
+
const endpoint = value.endpoint;
|
|
2869
|
+
const target = normalizeBindingTarget(value.target);
|
|
2870
|
+
if (!id || seen.has(id) || !name || !ALL_ENDPOINTS.includes(endpoint) || !target) continue;
|
|
2871
|
+
seen.add(id);
|
|
2872
|
+
const normalizedRoute = normalizeEndpointConfig({
|
|
2873
|
+
...value,
|
|
2874
|
+
endpoint,
|
|
2875
|
+
useSubscription: target.kind !== "provider"
|
|
2876
|
+
});
|
|
2877
|
+
const apiKeyIds = Array.isArray(value.apiKeyIds) ? [...new Set(value.apiKeyIds.filter((key) => typeof key === "string" && key.trim() !== "").map((key) => key.trim()))] : [];
|
|
2878
|
+
const keyScope = value.keyScope === "selected" ? "selected" : value.keyScope === "all" ? "all" : apiKeyIds.length > 0 ? "selected" : "all";
|
|
2879
|
+
const modelMappings = normalizeGatewayModelMappings(value.modelMappings);
|
|
2880
|
+
const modelMode = value.modelMode === "passthrough" ? "passthrough" : value.modelMode === "mapped" ? "mapped" : modelMappings.length > 0 || hasConfiguredLegacyModels(normalizedRoute) ? "mapped" : "passthrough";
|
|
2881
|
+
const priority = typeof value.priority === "number" && Number.isFinite(value.priority) ? Math.max(0, Math.min(1e4, Math.round(value.priority))) : void 0;
|
|
2882
|
+
const binding = {
|
|
2883
|
+
id,
|
|
2884
|
+
name,
|
|
2885
|
+
enabled: value.enabled !== false,
|
|
2886
|
+
keyScope,
|
|
2887
|
+
endpoint,
|
|
2888
|
+
target,
|
|
2889
|
+
// Legacy `'global'` meant "yield to the removed global endpoint fallback";
|
|
2890
|
+
// it now yields to the next matching route, which is the same default.
|
|
2891
|
+
fallback: value.fallback === "fail" ? "fail" : "next",
|
|
2892
|
+
modelMode
|
|
2893
|
+
};
|
|
2894
|
+
if (apiKeyIds.length > 0) binding.apiKeyIds = apiKeyIds;
|
|
2895
|
+
if (modelMappings.length > 0) binding.modelMappings = modelMappings;
|
|
2896
|
+
if (priority !== void 0) binding.priority = priority;
|
|
2897
|
+
if (normalizedRoute.modelMap) binding.modelMap = normalizedRoute.modelMap;
|
|
2898
|
+
if (normalizedRoute.models) binding.models = normalizedRoute.models;
|
|
2899
|
+
if (normalizedRoute.dispatchMode) binding.dispatchMode = normalizedRoute.dispatchMode;
|
|
2900
|
+
if (normalizedRoute.prefixTargets) binding.prefixTargets = normalizedRoute.prefixTargets;
|
|
2901
|
+
if (normalizedRoute.defaultModel !== void 0) binding.defaultModel = normalizedRoute.defaultModel;
|
|
2902
|
+
if (normalizedRoute.backgroundModel !== void 0) binding.backgroundModel = normalizedRoute.backgroundModel;
|
|
2903
|
+
if (normalizedRoute.backgroundModelIds) binding.backgroundModelIds = normalizedRoute.backgroundModelIds;
|
|
2904
|
+
bindings.push(binding);
|
|
2905
|
+
}
|
|
2906
|
+
return bindings;
|
|
2907
|
+
}
|
|
2324
2908
|
function defaultServerConfig() {
|
|
2325
2909
|
const queues = normalizeQueueSegments(void 0);
|
|
2326
2910
|
return {
|
|
2327
2911
|
enabled: false,
|
|
2328
2912
|
networkBinding: false,
|
|
2329
2913
|
endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
|
|
2914
|
+
bindings: [],
|
|
2330
2915
|
port: DEFAULT_OUTBOUND_PORT,
|
|
2331
2916
|
userMessageQueue: queues.userMessageQueue,
|
|
2332
2917
|
concurrencyQueue: queues.concurrencyQueue,
|
|
2333
2918
|
accountHealth: normalizeAccountHealth(void 0),
|
|
2334
2919
|
accountProbe: normalizeAccountProbe(void 0),
|
|
2920
|
+
allowanceScheduling: normalizeAllowanceScheduling(void 0),
|
|
2335
2921
|
audit: normalizeAudit(void 0),
|
|
2336
2922
|
billing: normalizeBilling(void 0),
|
|
2337
2923
|
fingerprint: normalizeFingerprint(void 0),
|
|
@@ -2348,17 +2934,20 @@ function normalizeServerConfig(raw) {
|
|
|
2348
2934
|
}
|
|
2349
2935
|
}
|
|
2350
2936
|
const queues = normalizeQueueSegments(raw);
|
|
2937
|
+
const legacyEndpoints = ALL_ENDPOINTS.map((ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep));
|
|
2938
|
+
const persistedBindings = normalizeGatewayBindings(raw.bindings);
|
|
2939
|
+
const bindings = persistedBindings.length > 0 ? persistedBindings : normalizeGatewayBindings(legacyEndpointsToBindings(legacyEndpoints));
|
|
2351
2940
|
const config = {
|
|
2352
2941
|
enabled: raw.enabled === true,
|
|
2353
2942
|
networkBinding: raw.networkBinding === true,
|
|
2354
|
-
endpoints: ALL_ENDPOINTS.map(
|
|
2355
|
-
|
|
2356
|
-
),
|
|
2943
|
+
endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
|
|
2944
|
+
bindings,
|
|
2357
2945
|
port: raw.port ?? base.port,
|
|
2358
2946
|
userMessageQueue: queues.userMessageQueue,
|
|
2359
2947
|
concurrencyQueue: queues.concurrencyQueue,
|
|
2360
2948
|
accountHealth: normalizeAccountHealth(raw),
|
|
2361
2949
|
accountProbe: normalizeAccountProbe(raw),
|
|
2950
|
+
allowanceScheduling: normalizeAllowanceScheduling(raw),
|
|
2362
2951
|
audit: normalizeAudit(raw),
|
|
2363
2952
|
billing: normalizeBilling(raw),
|
|
2364
2953
|
fingerprint: normalizeFingerprint(raw),
|
|
@@ -2384,11 +2973,13 @@ function mergeServerConfig(current, patch) {
|
|
|
2384
2973
|
enabled: patch.enabled ?? current.enabled,
|
|
2385
2974
|
networkBinding: patch.networkBinding ?? current.networkBinding,
|
|
2386
2975
|
endpoints: patch.endpoints ?? current.endpoints,
|
|
2976
|
+
bindings: patch.bindings ?? current.bindings,
|
|
2387
2977
|
port: patch.port ?? current.port,
|
|
2388
2978
|
userMessageQueue: patch.userMessageQueue ?? current.userMessageQueue,
|
|
2389
2979
|
concurrencyQueue: patch.concurrencyQueue ?? current.concurrencyQueue,
|
|
2390
2980
|
accountHealth: patch.accountHealth ?? current.accountHealth,
|
|
2391
2981
|
accountProbe: patch.accountProbe ?? current.accountProbe,
|
|
2982
|
+
allowanceScheduling: patch.allowanceScheduling ?? current.allowanceScheduling,
|
|
2392
2983
|
audit: patch.audit ?? current.audit,
|
|
2393
2984
|
billing: patch.billing ?? current.billing,
|
|
2394
2985
|
// Proxy is layer-replaced (not deep-merged): a PUT carrying `proxy` swaps the
|
|
@@ -2406,6 +2997,167 @@ function mergeServerConfig(current, patch) {
|
|
|
2406
2997
|
});
|
|
2407
2998
|
}
|
|
2408
2999
|
|
|
3000
|
+
// src/outbound-api/gatewayBindingResolver.ts
|
|
3001
|
+
var MESSAGE_FALLBACK_KINDS = ["sonnet", "opus", "haiku", "fable"];
|
|
3002
|
+
function nonBlank(value) {
|
|
3003
|
+
return typeof value === "string" && value.trim() !== "";
|
|
3004
|
+
}
|
|
3005
|
+
function gatewayBindingAllowsKey(binding, apiKeyId) {
|
|
3006
|
+
const scope = binding.keyScope ?? (binding.apiKeyIds?.length ? "selected" : "all");
|
|
3007
|
+
return scope === "all" || Boolean(binding.apiKeyIds?.includes(apiKeyId));
|
|
3008
|
+
}
|
|
3009
|
+
function wildcardMatches(pattern, value) {
|
|
3010
|
+
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join(".*");
|
|
3011
|
+
return new RegExp(`^${escaped}$`, "iu").test(value);
|
|
3012
|
+
}
|
|
3013
|
+
function resolveGatewayModelMapping(mappings, requestedModel) {
|
|
3014
|
+
if (!nonBlank(requestedModel)) return void 0;
|
|
3015
|
+
const wanted = requestedModel.trim();
|
|
3016
|
+
const exact = (mappings ?? []).find(
|
|
3017
|
+
(mapping) => nonBlank(mapping.source) && !mapping.source.includes("*") && mapping.source.trim().toLocaleLowerCase() === wanted.toLocaleLowerCase()
|
|
3018
|
+
);
|
|
3019
|
+
if (exact && nonBlank(exact.target)) return exact.target.trim();
|
|
3020
|
+
const wildcard = (mappings ?? []).find(
|
|
3021
|
+
(mapping) => nonBlank(mapping.source) && mapping.source.includes("*") && wildcardMatches(mapping.source.trim(), wanted)
|
|
3022
|
+
);
|
|
3023
|
+
return wildcard && nonBlank(wildcard.target) ? wildcard.target.trim() : void 0;
|
|
3024
|
+
}
|
|
3025
|
+
function routeCanServe(binding, requestedModel, role) {
|
|
3026
|
+
if (binding.modelMode === "passthrough") return nonBlank(requestedModel);
|
|
3027
|
+
if (binding.modelMappings?.length) {
|
|
3028
|
+
return nonBlank(resolveGatewayModelMapping(binding.modelMappings, requestedModel));
|
|
3029
|
+
}
|
|
3030
|
+
if (isKindMappedEndpoint(binding.endpoint)) {
|
|
3031
|
+
const map = binding.modelMap ?? {};
|
|
3032
|
+
const kind = detectModelKind(binding.endpoint, requestedModel);
|
|
3033
|
+
if (kind && nonBlank(map[kind])) return true;
|
|
3034
|
+
if (binding.endpoint === "messages") {
|
|
3035
|
+
return MESSAGE_FALLBACK_KINDS.some((candidate) => nonBlank(map[candidate]));
|
|
3036
|
+
}
|
|
3037
|
+
return nonBlank(map.codex);
|
|
3038
|
+
}
|
|
3039
|
+
if (binding.endpoint === "chat" && binding.dispatchMode === "prefix") {
|
|
3040
|
+
return resolvePrefixTarget(binding.prefixTargets, requestedModel) !== null;
|
|
3041
|
+
}
|
|
3042
|
+
if (binding.endpoint === "chat") {
|
|
3043
|
+
if (!requestedModel) return false;
|
|
3044
|
+
const wanted = requestedModel.trim().toLowerCase();
|
|
3045
|
+
return (binding.models ?? []).some((ref) => modelIdOf(ref)?.toLowerCase() === wanted);
|
|
3046
|
+
}
|
|
3047
|
+
return role === "background" ? nonBlank(binding.backgroundModel) : nonBlank(binding.defaultModel);
|
|
3048
|
+
}
|
|
3049
|
+
function modelIdOf(ref) {
|
|
3050
|
+
if (!nonBlank(ref)) return void 0;
|
|
3051
|
+
const trimmed = ref.trim();
|
|
3052
|
+
const comma = trimmed.indexOf(",");
|
|
3053
|
+
const modelId = comma >= 0 ? trimmed.slice(comma + 1).trim() : trimmed;
|
|
3054
|
+
return modelId || void 0;
|
|
3055
|
+
}
|
|
3056
|
+
function targetProviderId(target) {
|
|
3057
|
+
return target.providerId.trim();
|
|
3058
|
+
}
|
|
3059
|
+
function targetRef(target, ref) {
|
|
3060
|
+
const modelId = modelIdOf(ref) ?? "";
|
|
3061
|
+
return modelId ? `${targetProviderId(target)},${modelId}` : "";
|
|
3062
|
+
}
|
|
3063
|
+
function targetRefs(target, refs) {
|
|
3064
|
+
return (refs ?? []).map((ref) => targetRef(target, ref)).filter(nonBlank);
|
|
3065
|
+
}
|
|
3066
|
+
function targetPrefixRefs(target, refs) {
|
|
3067
|
+
if (!refs) return void 0;
|
|
3068
|
+
const mapped = {};
|
|
3069
|
+
for (const kind of ["claude", "gpt", "gemini"]) {
|
|
3070
|
+
const ref = targetRef(target, refs[kind]);
|
|
3071
|
+
if (ref) mapped[kind] = ref;
|
|
3072
|
+
}
|
|
3073
|
+
return mapped.claude || mapped.gpt || mapped.gemini ? mapped : void 0;
|
|
3074
|
+
}
|
|
3075
|
+
function applySingleModel(config, binding, targetModel) {
|
|
3076
|
+
const ref = targetRef(binding.target, targetModel);
|
|
3077
|
+
if (!ref) return;
|
|
3078
|
+
if (isKindMappedEndpoint(binding.endpoint)) {
|
|
3079
|
+
config.modelMap = Object.fromEntries(
|
|
3080
|
+
modelKindsForEndpoint(binding.endpoint).map((kind) => [kind, ref])
|
|
3081
|
+
);
|
|
3082
|
+
} else if (binding.endpoint === "chat") {
|
|
3083
|
+
config.models = [ref];
|
|
3084
|
+
config.dispatchMode = "list";
|
|
3085
|
+
} else {
|
|
3086
|
+
config.defaultModel = ref;
|
|
3087
|
+
config.backgroundModel = ref;
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
function gatewayBindingToEndpointConfig(binding, requestedModel) {
|
|
3091
|
+
const target = binding.target;
|
|
3092
|
+
const config = {
|
|
3093
|
+
endpoint: binding.endpoint,
|
|
3094
|
+
useSubscription: target.kind !== "provider"
|
|
3095
|
+
};
|
|
3096
|
+
const usesGenericModelHandling = binding.modelMode === "passthrough" || Boolean(binding.modelMappings?.length);
|
|
3097
|
+
const dynamicModel = binding.modelMode === "passthrough" ? requestedModel : resolveGatewayModelMapping(binding.modelMappings, requestedModel);
|
|
3098
|
+
if (usesGenericModelHandling) {
|
|
3099
|
+
if (nonBlank(dynamicModel)) applySingleModel(config, binding, dynamicModel);
|
|
3100
|
+
} else if (isKindMappedEndpoint(binding.endpoint)) {
|
|
3101
|
+
config.modelMap = Object.fromEntries(
|
|
3102
|
+
Object.entries(binding.modelMap ?? {}).map(([kind, ref]) => [kind, targetRef(target, ref)])
|
|
3103
|
+
);
|
|
3104
|
+
} else if (binding.endpoint === "chat") {
|
|
3105
|
+
config.models = targetRefs(target, binding.models);
|
|
3106
|
+
if (binding.dispatchMode === "prefix") {
|
|
3107
|
+
config.dispatchMode = "prefix";
|
|
3108
|
+
config.prefixTargets = targetPrefixRefs(target, binding.prefixTargets);
|
|
3109
|
+
}
|
|
3110
|
+
} else {
|
|
3111
|
+
config.defaultModel = targetRef(target, binding.defaultModel);
|
|
3112
|
+
config.backgroundModel = targetRef(target, binding.backgroundModel);
|
|
3113
|
+
if (binding.backgroundModelIds) config.backgroundModelIds = [...binding.backgroundModelIds];
|
|
3114
|
+
}
|
|
3115
|
+
if (target.kind === "account") {
|
|
3116
|
+
config.boundAccountId = target.accountId;
|
|
3117
|
+
config.boundAccountFallbackPolicy = binding.fallback === "next" ? "pool" : "strict";
|
|
3118
|
+
} else if (target.kind === "account-group") {
|
|
3119
|
+
config.boundAccountGroup = target.group;
|
|
3120
|
+
config.boundAccountFallbackPolicy = binding.fallback === "next" ? "pool" : "strict";
|
|
3121
|
+
} else if (target.kind === "provider" && target.keyId) {
|
|
3122
|
+
config.boundKeyId = target.keyId;
|
|
3123
|
+
config.boundKeyFallbackPolicy = binding.fallback === "next" ? "pool" : "strict";
|
|
3124
|
+
}
|
|
3125
|
+
return config;
|
|
3126
|
+
}
|
|
3127
|
+
function candidateGatewayBindings(bindings, apiKeyId, endpoint) {
|
|
3128
|
+
const candidates = (bindings ?? []).filter(
|
|
3129
|
+
(binding) => binding.enabled && binding.endpoint === endpoint && gatewayBindingAllowsKey(binding, apiKeyId)
|
|
3130
|
+
);
|
|
3131
|
+
const scopeOf = (binding) => binding.keyScope ?? (binding.apiKeyIds?.length ? "selected" : "all");
|
|
3132
|
+
const scoped = candidates.filter((binding) => scopeOf(binding) === "selected");
|
|
3133
|
+
const pool = scoped.length > 0 ? scoped : candidates.filter((binding) => scopeOf(binding) === "all");
|
|
3134
|
+
return pool.sort(
|
|
3135
|
+
(left, right) => (left.priority ?? 100) - (right.priority ?? 100) || left.id.localeCompare(right.id)
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
function candidateBackgroundModelIds(bindings, apiKeyId, endpoint) {
|
|
3139
|
+
const ids = [
|
|
3140
|
+
...new Set(
|
|
3141
|
+
candidateGatewayBindings(bindings, apiKeyId, endpoint).flatMap(
|
|
3142
|
+
(binding) => binding.backgroundModelIds ?? []
|
|
3143
|
+
)
|
|
3144
|
+
)
|
|
3145
|
+
];
|
|
3146
|
+
return ids.length > 0 ? ids : void 0;
|
|
3147
|
+
}
|
|
3148
|
+
function resolveGatewayBinding(input) {
|
|
3149
|
+
const candidates = candidateGatewayBindings(input.bindings, input.apiKeyId, input.endpoint);
|
|
3150
|
+
const binding = candidates.find(
|
|
3151
|
+
(candidate) => candidate.fallback === "fail" || routeCanServe(candidate, input.requestedModel, input.role)
|
|
3152
|
+
) ?? candidates[0];
|
|
3153
|
+
if (!binding) return { source: "none" };
|
|
3154
|
+
return {
|
|
3155
|
+
source: "binding",
|
|
3156
|
+
binding,
|
|
3157
|
+
config: gatewayBindingToEndpointConfig(binding, input.requestedModel)
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
|
|
2409
3161
|
// src/outbound-api/outboundApiKeyAuth.ts
|
|
2410
3162
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
2411
3163
|
|
|
@@ -2498,6 +3250,27 @@ async function createNamedKey(db, name) {
|
|
|
2498
3250
|
plaintextOnce: secret
|
|
2499
3251
|
};
|
|
2500
3252
|
}
|
|
3253
|
+
async function createIntegrationKey(db, name, allowedEndpoints = ["responses", "messages"]) {
|
|
3254
|
+
const secret = generateSecret();
|
|
3255
|
+
const id = newKeyId();
|
|
3256
|
+
const prefix = keyPrefix(secret);
|
|
3257
|
+
const row = await db.outboundApiKeysCreate({
|
|
3258
|
+
id,
|
|
3259
|
+
name,
|
|
3260
|
+
keyHash: hashKey(secret),
|
|
3261
|
+
keyPrefix: prefix,
|
|
3262
|
+
kind: "integration",
|
|
3263
|
+
allowedEndpoints: [...new Set(allowedEndpoints)],
|
|
3264
|
+
loopbackOnly: true
|
|
3265
|
+
});
|
|
3266
|
+
return {
|
|
3267
|
+
id: row.id,
|
|
3268
|
+
name: row.name,
|
|
3269
|
+
keyPrefix: row.keyPrefix,
|
|
3270
|
+
createdAt: row.createdAt,
|
|
3271
|
+
plaintextOnce: secret
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
2501
3274
|
function extractCostLimits(row) {
|
|
2502
3275
|
const limits = {};
|
|
2503
3276
|
if (row.dailyCostLimitUsd != null) limits.dailyUsd = row.dailyCostLimitUsd;
|
|
@@ -2520,7 +3293,12 @@ function extractModelRestriction(row) {
|
|
|
2520
3293
|
};
|
|
2521
3294
|
}
|
|
2522
3295
|
function toVerifiedKey(row) {
|
|
2523
|
-
const key = {
|
|
3296
|
+
const key = {
|
|
3297
|
+
id: row.id,
|
|
3298
|
+
kind: row.kind,
|
|
3299
|
+
allowedEndpoints: row.allowedEndpoints,
|
|
3300
|
+
loopbackOnly: row.loopbackOnly
|
|
3301
|
+
};
|
|
2524
3302
|
if (row.maxConcurrency !== void 0 && row.maxConcurrency !== null) {
|
|
2525
3303
|
key.maxConcurrency = row.maxConcurrency;
|
|
2526
3304
|
}
|
|
@@ -3164,10 +3942,10 @@ function getFallbackRedeemLimiter() {
|
|
|
3164
3942
|
fallbackRedeemLimiter ??= new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
|
|
3165
3943
|
return fallbackRedeemLimiter;
|
|
3166
3944
|
}
|
|
3167
|
-
function writeJsonError(res, status, message, headers = {}) {
|
|
3945
|
+
function writeJsonError(res, status, message, headers = {}, details = {}) {
|
|
3168
3946
|
if (res.headersSent) return;
|
|
3169
3947
|
res.writeHead(status, { "Content-Type": "application/json", ...headers });
|
|
3170
|
-
res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
|
|
3948
|
+
res.end(JSON.stringify({ error: { type: "outbound_api_error", message, ...details } }));
|
|
3171
3949
|
}
|
|
3172
3950
|
function writeCostLimitError(res, scope, limitUsd, spentUsd) {
|
|
3173
3951
|
if (res.headersSent) return;
|
|
@@ -3211,6 +3989,11 @@ function extractPresentedKey(req) {
|
|
|
3211
3989
|
const g = Array.isArray(goog) ? goog[0] : goog;
|
|
3212
3990
|
return g?.trim() || void 0;
|
|
3213
3991
|
}
|
|
3992
|
+
function isLoopbackPeer(address) {
|
|
3993
|
+
if (!address) return false;
|
|
3994
|
+
const normalized = address.toLowerCase().split("%")[0];
|
|
3995
|
+
return normalized === "::1" || normalized === "127.0.0.1" || normalized.startsWith("127.") || normalized.startsWith("::ffff:127.");
|
|
3996
|
+
}
|
|
3214
3997
|
function selectEndpoint(method, url) {
|
|
3215
3998
|
if (method !== "POST" || !url) return null;
|
|
3216
3999
|
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
@@ -3234,9 +4017,33 @@ function isModelsListRequest(url) {
|
|
|
3234
4017
|
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
3235
4018
|
return path.endsWith("/models");
|
|
3236
4019
|
}
|
|
3237
|
-
function
|
|
3238
|
-
const
|
|
3239
|
-
const
|
|
4020
|
+
function writeModelsList(res, config, apiKeyId, allowedEndpoints) {
|
|
4021
|
+
const refs = [];
|
|
4022
|
+
const endpointConfigs = [];
|
|
4023
|
+
for (const endpoint of ["chat", "responses", "messages", "gemini"]) {
|
|
4024
|
+
if (allowedEndpoints && !allowedEndpoints.includes(endpoint)) continue;
|
|
4025
|
+
endpointConfigs.push(
|
|
4026
|
+
...candidateGatewayBindings(config.bindings, apiKeyId, endpoint).map(
|
|
4027
|
+
(binding) => gatewayBindingToEndpointConfig(binding)
|
|
4028
|
+
)
|
|
4029
|
+
);
|
|
4030
|
+
}
|
|
4031
|
+
for (const endpoint of endpointConfigs) {
|
|
4032
|
+
if (allowedEndpoints && !allowedEndpoints.includes(endpoint.endpoint)) continue;
|
|
4033
|
+
if (endpoint.endpoint === "chat") refs.push(...endpoint.models ?? []);
|
|
4034
|
+
else if (endpoint.endpoint === "messages" || endpoint.endpoint === "responses") {
|
|
4035
|
+
refs.push(...Object.values(endpoint.modelMap ?? {}));
|
|
4036
|
+
} else {
|
|
4037
|
+
if (endpoint.defaultModel) refs.push(endpoint.defaultModel);
|
|
4038
|
+
if (endpoint.backgroundModel) refs.push(endpoint.backgroundModel);
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4042
|
+
const data = refs.map((ref) => parseModelRef(ref)).filter((p) => p !== null).filter((p) => {
|
|
4043
|
+
if (seen.has(p.modelId)) return false;
|
|
4044
|
+
seen.add(p.modelId);
|
|
4045
|
+
return true;
|
|
4046
|
+
}).map((p) => ({ id: p.modelId, object: "model", owned_by: "omnicross" }));
|
|
3240
4047
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3241
4048
|
res.end(JSON.stringify({ object: "list", data }));
|
|
3242
4049
|
}
|
|
@@ -3270,7 +4077,15 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3270
4077
|
const verified = verification.key;
|
|
3271
4078
|
if (audit) audit.keyId = verified.id;
|
|
3272
4079
|
if (billing) billing.keyId = verified.id;
|
|
4080
|
+
if (verified.loopbackOnly && !isLoopbackPeer(req.socket?.remoteAddress)) {
|
|
4081
|
+
writeJsonError(res, 403, "This integration key is restricted to loopback clients");
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
3273
4084
|
if (isRedeemRequest(req.method, req.url)) {
|
|
4085
|
+
if (verified.kind === "integration") {
|
|
4086
|
+
writeJsonError(res, 403, "Integration keys cannot redeem vouchers");
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
3274
4089
|
await handleVoucherRedeem(
|
|
3275
4090
|
req,
|
|
3276
4091
|
res,
|
|
@@ -3292,7 +4107,11 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3292
4107
|
return;
|
|
3293
4108
|
}
|
|
3294
4109
|
if (req.method === "GET" && isModelsListRequest(req.url)) {
|
|
3295
|
-
|
|
4110
|
+
if (verified.allowedEndpoints && verified.allowedEndpoints.length === 0) {
|
|
4111
|
+
writeJsonError(res, 403, "API key is not allowed to access this endpoint");
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
writeModelsList(res, config, verified.id, verified.allowedEndpoints);
|
|
3296
4115
|
return;
|
|
3297
4116
|
}
|
|
3298
4117
|
const endpoint = selectEndpoint(req.method, req.url);
|
|
@@ -3300,9 +4119,12 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3300
4119
|
writeJsonError(res, 404, `Unsupported: ${req.method} ${req.url}`);
|
|
3301
4120
|
return;
|
|
3302
4121
|
}
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
4122
|
+
if (verified.allowedEndpoints && !verified.allowedEndpoints.includes(endpoint)) {
|
|
4123
|
+
writeJsonError(res, 403, "API key is not allowed to access this endpoint");
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
4126
|
+
if (candidateGatewayBindings(config.bindings, verified.id, endpoint).length === 0) {
|
|
4127
|
+
writeJsonError(res, 503, `endpoint '${endpoint}' has no downstream route for this key`);
|
|
3306
4128
|
return;
|
|
3307
4129
|
}
|
|
3308
4130
|
if (verified.costLimits && deps.keySpendTracker) {
|
|
@@ -3375,23 +4197,40 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3375
4197
|
const urlModel = extractGeminiModelFromUrl(req.url);
|
|
3376
4198
|
if (urlModel) parsedBody["model"] = urlModel;
|
|
3377
4199
|
}
|
|
3378
|
-
const
|
|
4200
|
+
const requestedModel = extractRequestedModel(ingressFormat, parsedBody);
|
|
4201
|
+
const role = isKindMappedEndpoint(endpoint) || endpoint === "chat" ? void 0 : detectRequestRole(ingressFormat, parsedBody, {
|
|
4202
|
+
// Role detection precedes the route pick (the pick consumes the
|
|
4203
|
+
// role), so the hint is the union across the candidate routes.
|
|
4204
|
+
backgroundModelIds: candidateBackgroundModelIds(config.bindings, verified.id, endpoint)
|
|
4205
|
+
});
|
|
4206
|
+
const bindingResolution = resolveGatewayBinding({
|
|
4207
|
+
bindings: config.bindings,
|
|
4208
|
+
apiKeyId: verified.id,
|
|
4209
|
+
endpoint,
|
|
4210
|
+
requestedModel,
|
|
4211
|
+
role
|
|
4212
|
+
});
|
|
4213
|
+
if (bindingResolution.source === "none") {
|
|
4214
|
+
writeJsonError(res, 503, `endpoint '${endpoint}' has no downstream route for this key`);
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
const effectiveEndpointConfig = bindingResolution.config;
|
|
4218
|
+
const bindingAffinitySuffix = `:${bindingResolution.binding.id}`;
|
|
4219
|
+
const sessionId = deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}${bindingAffinitySuffix}` : null;
|
|
3379
4220
|
const resolved = isKindMappedEndpoint(endpoint) || endpoint === "chat" ? await resolveRoute({
|
|
3380
|
-
config:
|
|
4221
|
+
config: effectiveEndpointConfig,
|
|
3381
4222
|
ingressFormat,
|
|
3382
4223
|
llmConfig: deps.llmConfig,
|
|
3383
4224
|
sessionId,
|
|
3384
4225
|
// Capture the ORIGINAL requested id BEFORE any downstream swap; for
|
|
3385
4226
|
// kind-mapped endpoints it selects the kind AND is stamped onto
|
|
3386
4227
|
// `route.requestedModel`; for chat it is matched against the list.
|
|
3387
|
-
requestedModel
|
|
4228
|
+
requestedModel,
|
|
3388
4229
|
// Attribution: stamp the verified named-key id onto the route.
|
|
3389
4230
|
apiKeyId: verified.id
|
|
3390
4231
|
}) : await resolveRoute({
|
|
3391
|
-
config:
|
|
3392
|
-
role
|
|
3393
|
-
backgroundModelIds: endpointConfig.backgroundModelIds
|
|
3394
|
-
}),
|
|
4232
|
+
config: effectiveEndpointConfig,
|
|
4233
|
+
role,
|
|
3395
4234
|
ingressFormat,
|
|
3396
4235
|
llmConfig: deps.llmConfig,
|
|
3397
4236
|
sessionId,
|
|
@@ -3452,12 +4291,29 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3452
4291
|
const replay = makeReplayRequest(req, rawBody);
|
|
3453
4292
|
await routeRequest(replay, res, routeMap, deps.proxyDeps);
|
|
3454
4293
|
} catch (err) {
|
|
4294
|
+
if (isBoundAccountSelectionError(err)) {
|
|
4295
|
+
const headers = {};
|
|
4296
|
+
if (err.status === 429 && err.resumeAt) {
|
|
4297
|
+
const resumeMs = Date.parse(err.resumeAt);
|
|
4298
|
+
if (Number.isFinite(resumeMs)) {
|
|
4299
|
+
headers["Retry-After"] = String(Math.max(1, Math.ceil((resumeMs - Date.now()) / 1e3)));
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
writeJsonError(
|
|
4303
|
+
res,
|
|
4304
|
+
err.status,
|
|
4305
|
+
boundAccountSelectionMessage(err.reason),
|
|
4306
|
+
headers,
|
|
4307
|
+
{ code: err.code, reason: err.reason }
|
|
4308
|
+
);
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
3455
4311
|
const message = serializeError(err);
|
|
3456
4312
|
if (deps.logger) deps.logger.error("[OutboundApi] dispatch error:", message);
|
|
3457
4313
|
else console.error("[OutboundApi] dispatch error:", message);
|
|
3458
4314
|
emitWebhookEvent({ kind: "server.error", at: Date.now(), message });
|
|
3459
4315
|
if (audit) audit.error = message;
|
|
3460
|
-
writeJsonError(res, 502, message);
|
|
4316
|
+
writeJsonError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, message);
|
|
3461
4317
|
} finally {
|
|
3462
4318
|
routeMap.removeRoute(token);
|
|
3463
4319
|
if (releaseSerial) releaseSerial();
|
|
@@ -3481,16 +4337,6 @@ function makeReplayRequest(req, rawBody) {
|
|
|
3481
4337
|
var DEFAULT_OUTBOUND_PORT = 8765;
|
|
3482
4338
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
3483
4339
|
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
|
-
};
|
|
3494
4340
|
var OutboundApiServer = class {
|
|
3495
4341
|
constructor(deps, onPortChange) {
|
|
3496
4342
|
this.deps = deps;
|
|
@@ -3502,6 +4348,7 @@ var OutboundApiServer = class {
|
|
|
3502
4348
|
boundPort = 0;
|
|
3503
4349
|
boundAddr = LOOPBACK_ADDR;
|
|
3504
4350
|
endpoints = [];
|
|
4351
|
+
bindings = [];
|
|
3505
4352
|
userMessageQueue;
|
|
3506
4353
|
concurrencyQueue;
|
|
3507
4354
|
voucherConfig;
|
|
@@ -3528,6 +4375,7 @@ var OutboundApiServer = class {
|
|
|
3528
4375
|
*/
|
|
3529
4376
|
async applyConfig(input) {
|
|
3530
4377
|
this.endpoints = input.endpoints;
|
|
4378
|
+
this.bindings = input.bindings ?? [];
|
|
3531
4379
|
this.userMessageQueue = input.userMessageQueue;
|
|
3532
4380
|
this.concurrencyQueue = input.concurrencyQueue;
|
|
3533
4381
|
this.voucherConfig = input.voucher;
|
|
@@ -3537,16 +4385,6 @@ var OutboundApiServer = class {
|
|
|
3537
4385
|
await this.stop();
|
|
3538
4386
|
return;
|
|
3539
4387
|
}
|
|
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
|
-
}
|
|
3550
4388
|
const running = this.server !== null;
|
|
3551
4389
|
const bindChanged = running && (this.boundAddr !== wantAddr || this.boundPort !== wantPort);
|
|
3552
4390
|
if (running && !bindChanged) {
|
|
@@ -3595,6 +4433,7 @@ var OutboundApiServer = class {
|
|
|
3595
4433
|
}
|
|
3596
4434
|
/** Per-request handler. Auth is enforced on EVERY request (incl. loopback). */
|
|
3597
4435
|
onRequest(req, res) {
|
|
4436
|
+
if (req.url) req.url = normalizeDoubledEndpointPath(req.url);
|
|
3598
4437
|
if (this.deps.healthReportProvider && this.tryServeHealth(req, res)) return;
|
|
3599
4438
|
handleOutboundRequest(
|
|
3600
4439
|
req,
|
|
@@ -3602,6 +4441,7 @@ var OutboundApiServer = class {
|
|
|
3602
4441
|
this.deps,
|
|
3603
4442
|
{
|
|
3604
4443
|
endpoints: this.endpoints,
|
|
4444
|
+
bindings: this.bindings,
|
|
3605
4445
|
userMessageQueue: this.userMessageQueue,
|
|
3606
4446
|
concurrencyQueue: this.concurrencyQueue,
|
|
3607
4447
|
voucher: this.voucherConfig
|
|
@@ -3717,6 +4557,16 @@ function firstLanIPv4() {
|
|
|
3717
4557
|
}
|
|
3718
4558
|
return null;
|
|
3719
4559
|
}
|
|
4560
|
+
function normalizeDoubledEndpointPath(url) {
|
|
4561
|
+
const queryIdx = url.indexOf("?");
|
|
4562
|
+
const path = queryIdx === -1 ? url : url.slice(0, queryIdx);
|
|
4563
|
+
const query = queryIdx === -1 ? "" : url.slice(queryIdx);
|
|
4564
|
+
const collapsed = path.replace(
|
|
4565
|
+
/^(.*?)(\/v1\/(?:messages|responses|chat\/completions))\2$/,
|
|
4566
|
+
"$1$2"
|
|
4567
|
+
);
|
|
4568
|
+
return collapsed + query;
|
|
4569
|
+
}
|
|
3720
4570
|
|
|
3721
4571
|
// src/outbound-api/keySpendTracker.ts
|
|
3722
4572
|
function startOfLocalDay(ts) {
|
|
@@ -3823,7 +4673,7 @@ async function callOpenAICompletion(provider, apiKey, options, logger) {
|
|
|
3823
4673
|
model: options.model,
|
|
3824
4674
|
messages: options.messages.map((m) => convertMessageToOpenAI(m)),
|
|
3825
4675
|
// Only set max_tokens if explicitly provided, otherwise let API use its default
|
|
3826
|
-
...options.maxTokens ? { max_tokens: options.maxTokens } : {},
|
|
4676
|
+
...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
|
|
3827
4677
|
temperature: options.temperature,
|
|
3828
4678
|
stream: false
|
|
3829
4679
|
// For now, non-streaming only
|
|
@@ -3874,7 +4724,7 @@ async function callAnthropicCompletion(provider, apiKey, options, logger) {
|
|
|
3874
4724
|
const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
|
|
3875
4725
|
const anthropicRequest2 = {
|
|
3876
4726
|
model: options.model,
|
|
3877
|
-
max_tokens: options.maxTokens
|
|
4727
|
+
max_tokens: options.maxTokens ?? 16384,
|
|
3878
4728
|
temperature: options.temperature,
|
|
3879
4729
|
...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
|
|
3880
4730
|
messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
|
|
@@ -3920,7 +4770,7 @@ async function callAnthropicCompletion(provider, apiKey, options, logger) {
|
|
|
3920
4770
|
content: m.content
|
|
3921
4771
|
})),
|
|
3922
4772
|
// Anthropic requires max_tokens; use 16384 default if not explicitly set
|
|
3923
|
-
max_tokens: options.maxTokens
|
|
4773
|
+
max_tokens: options.maxTokens ?? 16384,
|
|
3924
4774
|
temperature: options.temperature,
|
|
3925
4775
|
stream: false
|
|
3926
4776
|
};
|
|
@@ -3975,7 +4825,7 @@ async function callGeminiCompletion(provider, apiKey, options, logger) {
|
|
|
3975
4825
|
const request = {
|
|
3976
4826
|
contents,
|
|
3977
4827
|
generationConfig: {
|
|
3978
|
-
...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
|
|
4828
|
+
...options.maxTokens !== void 0 ? { maxOutputTokens: options.maxTokens } : {},
|
|
3979
4829
|
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
3980
4830
|
}
|
|
3981
4831
|
};
|
|
@@ -4044,7 +4894,7 @@ async function callOpenAIResponseCompletion(provider, apiKey, options, logger) {
|
|
|
4044
4894
|
model: options.model,
|
|
4045
4895
|
input,
|
|
4046
4896
|
stream: false,
|
|
4047
|
-
...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
|
|
4897
|
+
...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
|
|
4048
4898
|
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
4049
4899
|
};
|
|
4050
4900
|
if (options.thinkLevel && options.thinkLevel !== "none") {
|
|
@@ -4109,7 +4959,7 @@ async function streamOpenAICompletion(provider, apiKey, options, messageId, call
|
|
|
4109
4959
|
model: options.model,
|
|
4110
4960
|
messages: options.messages.map((m) => convertMessageToOpenAI(m)),
|
|
4111
4961
|
// Only set max_tokens if explicitly provided, otherwise let API use its default
|
|
4112
|
-
...options.maxTokens ? { max_tokens: options.maxTokens } : {},
|
|
4962
|
+
...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
|
|
4113
4963
|
temperature: options.temperature,
|
|
4114
4964
|
stream: true
|
|
4115
4965
|
};
|
|
@@ -4168,7 +5018,7 @@ async function streamOpenAICompletion(provider, apiKey, options, messageId, call
|
|
|
4168
5018
|
async function streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, logger) {
|
|
4169
5019
|
const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
|
|
4170
5020
|
const MAX_TOKENS_FOR_THINKING = 16384;
|
|
4171
|
-
let effectiveMaxTokens = options.maxTokens
|
|
5021
|
+
let effectiveMaxTokens = options.maxTokens ?? 16384;
|
|
4172
5022
|
const thinkingMaxTokens = options.thinkLevel && options.thinkLevel !== "none" ? Math.min(effectiveMaxTokens, MAX_TOKENS_FOR_THINKING) : effectiveMaxTokens;
|
|
4173
5023
|
const thinkingConfig = options.thinkLevel && options.thinkLevel !== "none" ? buildAnthropicThinking(options.model, options.thinkLevel, thinkingMaxTokens) : void 0;
|
|
4174
5024
|
if (thinkingConfig) {
|
|
@@ -4280,7 +5130,7 @@ async function streamGeminiCompletion(provider, apiKey, options, messageId, call
|
|
|
4280
5130
|
const request = {
|
|
4281
5131
|
contents,
|
|
4282
5132
|
generationConfig: {
|
|
4283
|
-
...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
|
|
5133
|
+
...options.maxTokens !== void 0 ? { maxOutputTokens: options.maxTokens } : {},
|
|
4284
5134
|
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
4285
5135
|
}
|
|
4286
5136
|
};
|
|
@@ -4344,7 +5194,7 @@ async function streamOpenAIResponseCompletion(provider, apiKey, options, message
|
|
|
4344
5194
|
model: options.model,
|
|
4345
5195
|
input,
|
|
4346
5196
|
stream: true,
|
|
4347
|
-
...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
|
|
5197
|
+
...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
|
|
4348
5198
|
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
4349
5199
|
};
|
|
4350
5200
|
if (options.thinkLevel && options.thinkLevel !== "none") {
|
|
@@ -4401,7 +5251,7 @@ import {
|
|
|
4401
5251
|
getClaudeMaxTokens,
|
|
4402
5252
|
isReasoningModel
|
|
4403
5253
|
} from "@omnicross/contracts/thinking-config";
|
|
4404
|
-
async function resolveEffectiveMaxTokens(llmConfig,
|
|
5254
|
+
async function resolveEffectiveMaxTokens(llmConfig, _getProvider, logger, _providerId, _modelId, sessionMaxTokens) {
|
|
4405
5255
|
if (sessionMaxTokens !== void 0 && sessionMaxTokens > 0) {
|
|
4406
5256
|
logger.debug("Using session maxTokens", { sessionMaxTokens });
|
|
4407
5257
|
return sessionMaxTokens;
|
|
@@ -4415,45 +5265,6 @@ async function resolveEffectiveMaxTokens(llmConfig, getProvider, logger, provide
|
|
|
4415
5265
|
} catch (err) {
|
|
4416
5266
|
logger.warn("Failed to get global params", err instanceof Error ? err : void 0);
|
|
4417
5267
|
}
|
|
4418
|
-
const MAX_TOKENS_CAP = 131072;
|
|
4419
|
-
const provider = await getProvider(providerId);
|
|
4420
|
-
if (provider) {
|
|
4421
|
-
const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
|
|
4422
|
-
if (modelConfig?.maxTokens && modelConfig.maxTokens > 0) {
|
|
4423
|
-
const cappedMaxTokens = Math.min(modelConfig.maxTokens, MAX_TOKENS_CAP);
|
|
4424
|
-
logger.debug("Using model config maxTokens", {
|
|
4425
|
-
maxTokens: modelConfig.maxTokens,
|
|
4426
|
-
cappedMaxTokens
|
|
4427
|
-
});
|
|
4428
|
-
return cappedMaxTokens;
|
|
4429
|
-
}
|
|
4430
|
-
if (provider.modelGroups) {
|
|
4431
|
-
for (const group of provider.modelGroups) {
|
|
4432
|
-
const model = group.models?.find((m) => m.id === modelId);
|
|
4433
|
-
if (model?.maxTokens && model.maxTokens > 0) {
|
|
4434
|
-
const cappedMaxTokens = Math.min(model.maxTokens, MAX_TOKENS_CAP);
|
|
4435
|
-
logger.debug("Using modelGroup model maxTokens", {
|
|
4436
|
-
maxTokens: model.maxTokens,
|
|
4437
|
-
cappedMaxTokens
|
|
4438
|
-
});
|
|
4439
|
-
return cappedMaxTokens;
|
|
4440
|
-
}
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
|
-
}
|
|
4444
|
-
try {
|
|
4445
|
-
const discoveredMaxTokens = await llmConfig.getDiscoveredModelMaxTokens(providerId, modelId);
|
|
4446
|
-
if (discoveredMaxTokens && discoveredMaxTokens > 0) {
|
|
4447
|
-
const cappedMaxTokens = Math.min(discoveredMaxTokens, MAX_TOKENS_CAP);
|
|
4448
|
-
logger.debug("Using discovered model maxTokens", {
|
|
4449
|
-
discoveredMaxTokens,
|
|
4450
|
-
cappedMaxTokens
|
|
4451
|
-
});
|
|
4452
|
-
return cappedMaxTokens;
|
|
4453
|
-
}
|
|
4454
|
-
} catch (err) {
|
|
4455
|
-
logger.warn("Failed to get discovered model maxTokens", err instanceof Error ? err : void 0);
|
|
4456
|
-
}
|
|
4457
5268
|
logger.debug("No maxTokens configured, returning undefined");
|
|
4458
5269
|
return void 0;
|
|
4459
5270
|
}
|
|
@@ -4556,7 +5367,8 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
|
|
|
4556
5367
|
}
|
|
4557
5368
|
requestBody = {
|
|
4558
5369
|
contents,
|
|
4559
|
-
tools: options.tools
|
|
5370
|
+
tools: options.tools,
|
|
5371
|
+
...options.maxTokens !== void 0 ? { generationConfig: { maxOutputTokens: options.maxTokens } } : {}
|
|
4560
5372
|
};
|
|
4561
5373
|
url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
|
|
4562
5374
|
} else if (apiFormat === "anthropic") {
|
|
@@ -4566,7 +5378,10 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
|
|
|
4566
5378
|
role: m.role,
|
|
4567
5379
|
content: m.content
|
|
4568
5380
|
})),
|
|
4569
|
-
|
|
5381
|
+
// Native Anthropic Messages requires this field. Resolve an absent
|
|
5382
|
+
// caller cap to the model's real ceiling rather than a small constant —
|
|
5383
|
+
// see `resolveAnthropicMaxTokens`.
|
|
5384
|
+
max_tokens: resolveAnthropicMaxTokens(actualModel, options.maxTokens),
|
|
4570
5385
|
temperature: options.temperature ?? 0.7,
|
|
4571
5386
|
stream: true,
|
|
4572
5387
|
tools: options.tools
|
|
@@ -4583,7 +5398,7 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
|
|
|
4583
5398
|
role: m.role,
|
|
4584
5399
|
content: m.content
|
|
4585
5400
|
})),
|
|
4586
|
-
max_tokens: options.maxTokens
|
|
5401
|
+
...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
|
|
4587
5402
|
temperature: options.temperature ?? 0.7,
|
|
4588
5403
|
stream: true,
|
|
4589
5404
|
tools: options.tools
|
|
@@ -5080,6 +5895,18 @@ function getSharedExecutor2() {
|
|
|
5080
5895
|
async function resolveChainWithMain(llmConfig, providerId, model) {
|
|
5081
5896
|
return resolveProviderChain(llmConfig, providerId, model);
|
|
5082
5897
|
}
|
|
5898
|
+
function buildUnifiedCompletionRequest(actualModel, options, stream) {
|
|
5899
|
+
return {
|
|
5900
|
+
model: actualModel,
|
|
5901
|
+
messages: options.messages.map((m) => ({
|
|
5902
|
+
role: m.role,
|
|
5903
|
+
content: m.content
|
|
5904
|
+
})),
|
|
5905
|
+
...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
|
|
5906
|
+
temperature: options.temperature,
|
|
5907
|
+
stream
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5083
5910
|
async function completeWithTransformers(options, llmConfig, getProvider, resolveApiKey2, completeFallback, logger, recording) {
|
|
5084
5911
|
try {
|
|
5085
5912
|
const routedInfo = await llmConfig.resolveRoutedModel(
|
|
@@ -5108,16 +5935,11 @@ async function completeWithTransformers(options, llmConfig, getProvider, resolve
|
|
|
5108
5935
|
if (!hasTransformers) {
|
|
5109
5936
|
return completeFallback(options);
|
|
5110
5937
|
}
|
|
5111
|
-
const unifiedRequest =
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
})),
|
|
5117
|
-
max_tokens: options.maxTokens || 4096,
|
|
5118
|
-
temperature: options.temperature,
|
|
5119
|
-
stream: options.stream ?? false
|
|
5120
|
-
};
|
|
5938
|
+
const unifiedRequest = buildUnifiedCompletionRequest(
|
|
5939
|
+
actualModel,
|
|
5940
|
+
options,
|
|
5941
|
+
options.stream ?? false
|
|
5942
|
+
);
|
|
5121
5943
|
const transformerProvider = {
|
|
5122
5944
|
name: provider.name,
|
|
5123
5945
|
baseUrl: provider.api_base_url,
|
|
@@ -5247,16 +6069,7 @@ async function completeStreamWithTransformers(options, callbacks, llmConfig, get
|
|
|
5247
6069
|
}
|
|
5248
6070
|
const messageId = `msg_${Date.now()}`;
|
|
5249
6071
|
callbacks.onStart?.(messageId);
|
|
5250
|
-
const unifiedRequest =
|
|
5251
|
-
model: actualModel,
|
|
5252
|
-
messages: options.messages.map((m) => ({
|
|
5253
|
-
role: m.role,
|
|
5254
|
-
content: m.content
|
|
5255
|
-
})),
|
|
5256
|
-
max_tokens: options.maxTokens || 4096,
|
|
5257
|
-
temperature: options.temperature,
|
|
5258
|
-
stream: true
|
|
5259
|
-
};
|
|
6072
|
+
const unifiedRequest = buildUnifiedCompletionRequest(actualModel, options, true);
|
|
5260
6073
|
const transformerProvider = {
|
|
5261
6074
|
name: provider.name,
|
|
5262
6075
|
baseUrl: provider.api_base_url,
|
|
@@ -5791,9 +6604,10 @@ ${description}`;
|
|
|
5791
6604
|
* Resolve effective max_tokens value with priority:
|
|
5792
6605
|
* 1. Session settings (if provided)
|
|
5793
6606
|
* 2. Global model parameters (if enabled)
|
|
5794
|
-
* 3.
|
|
5795
|
-
*
|
|
5796
|
-
*
|
|
6607
|
+
* 3. undefined - let API use its default
|
|
6608
|
+
*
|
|
6609
|
+
* Provider and discovery maxTokens values are capability metadata, not
|
|
6610
|
+
* implicit per-request output limits.
|
|
5797
6611
|
*/
|
|
5798
6612
|
async resolveEffectiveMaxTokens(providerId, modelId, sessionMaxTokens) {
|
|
5799
6613
|
return resolveEffectiveMaxTokens(
|
|
@@ -5904,7 +6718,9 @@ export {
|
|
|
5904
6718
|
getGeminiEndpointTransformer,
|
|
5905
6719
|
readBody,
|
|
5906
6720
|
relayResponse,
|
|
6721
|
+
aggregateAnthropicSseToJsonBody,
|
|
5907
6722
|
writeError,
|
|
6723
|
+
writeBoundAccountError,
|
|
5908
6724
|
resolvePoolBoundKey,
|
|
5909
6725
|
extractRouteToken,
|
|
5910
6726
|
isLoopbackAddress,
|
|
@@ -5916,6 +6732,8 @@ export {
|
|
|
5916
6732
|
DEFAULT_USER_MESSAGE_QUEUE,
|
|
5917
6733
|
DEFAULT_CONCURRENCY_QUEUE,
|
|
5918
6734
|
DEFAULT_ACCOUNT_PROBE,
|
|
6735
|
+
DEFAULT_ALLOWANCE_SCHEDULING,
|
|
6736
|
+
normalizeAllowanceScheduling,
|
|
5919
6737
|
normalizeAccountProbe,
|
|
5920
6738
|
normalizeAudit,
|
|
5921
6739
|
normalizeBilling,
|
|
@@ -5928,16 +6746,23 @@ export {
|
|
|
5928
6746
|
normalizeWebhookSegment,
|
|
5929
6747
|
normalizeQueueSegments,
|
|
5930
6748
|
normalizePrefixTargets,
|
|
6749
|
+
legacyEndpointsToBindings,
|
|
6750
|
+
normalizeGatewayBindings,
|
|
5931
6751
|
defaultServerConfig,
|
|
5932
6752
|
normalizeServerConfig,
|
|
5933
6753
|
loadServerConfig,
|
|
5934
6754
|
saveServerConfig,
|
|
5935
6755
|
mergeServerConfig,
|
|
6756
|
+
gatewayBindingToEndpointConfig,
|
|
6757
|
+
candidateGatewayBindings,
|
|
6758
|
+
candidateBackgroundModelIds,
|
|
6759
|
+
resolveGatewayBinding,
|
|
5936
6760
|
computeKeyExpiry,
|
|
5937
6761
|
checkKeyQuota,
|
|
5938
6762
|
randomBase62,
|
|
5939
6763
|
hashKey,
|
|
5940
6764
|
createNamedKey,
|
|
6765
|
+
createIntegrationKey,
|
|
5941
6766
|
verifyKey,
|
|
5942
6767
|
verifyPresentedKey,
|
|
5943
6768
|
ConcurrencyQueueFullError,
|
|
@@ -5959,7 +6784,6 @@ export {
|
|
|
5959
6784
|
isRedeemRequest,
|
|
5960
6785
|
handleVoucherRedeem,
|
|
5961
6786
|
DEFAULT_OUTBOUND_PORT,
|
|
5962
|
-
OutboundApiConfigError,
|
|
5963
6787
|
OutboundApiServer,
|
|
5964
6788
|
formatUrls,
|
|
5965
6789
|
startOfLocalDay,
|