@omnicross/core 0.1.5 → 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 +6 -5
- package/dist/auth/GeminiCodeAssistProjectResolver.js +4 -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-4XMYPW46.cjs → chunk-4VLRHHFE.cjs} +75 -33
- package/dist/{chunk-VACWOIRE.cjs → chunk-4VZX5F4T.cjs} +39 -15
- package/dist/{chunk-C6OV7GCC.js → chunk-6CL5HTPQ.js} +44 -2
- package/dist/chunk-6QQG7YG3.cjs +153 -0
- package/dist/{chunk-3Q6Z6JTO.cjs → chunk-6VUQPOV3.cjs} +277 -86
- 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-46ZIFWYG.cjs → chunk-IIRPUVPH.cjs} +7 -1
- package/dist/{chunk-JWVTXXPX.cjs → chunk-KBM2W6UY.cjs} +8 -4
- package/dist/{chunk-PG7PS2NN.cjs → chunk-KCVJWJST.cjs} +14 -2
- package/dist/chunk-LB6EBS6Y.js +69 -0
- package/dist/{chunk-IWQAX3CS.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-MFZ5437A.js → chunk-N5T4M4GU.js} +1 -1
- package/dist/{chunk-BODMAL6G.js → chunk-OEWZUX4G.js} +262 -71
- package/dist/chunk-OF3BECU2.cjs +69 -0
- package/dist/{chunk-DDMRUL76.cjs → chunk-OKMJLEXX.cjs} +5 -5
- package/dist/chunk-OPRS4IQZ.js +153 -0
- package/dist/{chunk-D3TDKWRP.js → chunk-SIZQQD6T.js} +14 -2
- package/dist/{chunk-CGKLE6IW.cjs → chunk-SQYVORNP.cjs} +1219 -510
- package/dist/{chunk-6QZGDLL4.js → chunk-TOVPMMHM.js} +5 -1
- package/dist/{chunk-FH2P4OCD.js → chunk-UDCC7EMR.js} +976 -267
- package/dist/{chunk-ZLX3KGXX.js → chunk-UX7VWA46.js} +135 -23
- package/dist/{chunk-K52THW2C.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 +25 -23
- package/dist/completion/CompletionService.d.cts +2 -1
- package/dist/completion/CompletionService.d.ts +2 -1
- package/dist/completion/CompletionService.js +24 -22
- package/dist/completion.cjs +25 -23
- package/dist/completion.d.cts +2 -1
- package/dist/completion.d.ts +2 -1
- package/dist/completion.js +24 -22
- package/dist/index.cjs +49 -26
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +52 -29
- package/dist/{keyPolicy-Cp0FntZl.d.ts → keyPolicy-zdWTu3CQ.d.cts} +1 -1
- package/dist/{keyPolicy-Cp0FntZl.d.cts → keyPolicy-zdWTu3CQ.d.ts} +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 +47 -24
- package/dist/outbound-api.d.cts +127 -85
- package/dist/outbound-api.d.ts +127 -85
- package/dist/outbound-api.js +50 -27
- 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 +3 -2
- package/dist/pipeline/upstreamFetch.js +2 -1
- 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 +25 -23
- package/dist/provider-proxy/ProviderProxy.d.cts +1 -0
- package/dist/provider-proxy/ProviderProxy.d.ts +1 -0
- package/dist/provider-proxy/ProviderProxy.js +24 -22
- package/dist/provider-proxy/ingress/providerProxyShared.cjs +27 -23
- package/dist/provider-proxy/ingress/providerProxyShared.d.cts +34 -5
- package/dist/provider-proxy/ingress/providerProxyShared.d.ts +34 -5
- package/dist/provider-proxy/ingress/providerProxyShared.js +26 -22
- 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 +25 -23
- package/dist/provider-proxy.d.cts +1 -0
- package/dist/provider-proxy.d.ts +1 -0
- package/dist/provider-proxy.js +24 -22
- 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 +12 -18
- package/dist/transformer/types.d.cts +17 -0
- package/dist/transformer/types.d.ts +17 -0
- package/dist/transformer.cjs +10 -11
- package/dist/transformer.d.cts +1 -2
- package/dist/transformer.d.ts +1 -2
- package/dist/transformer.js +9 -10
- 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-7FEBOPJO.cjs +0 -50
- package/dist/chunk-DWKQZ2SK.cjs +0 -254
- package/dist/chunk-IAXWICZU.js +0 -69
- package/dist/chunk-IB3FDI4O.js +0 -50
- package/dist/chunk-OQWKZCXP.js +0 -242
- package/dist/chunk-QWBWGODS.cjs +0 -69
- package/dist/chunk-YMFAJFQT.cjs +0 -173
- package/dist/chunk-ZCIQ3L3R.cjs +0 -242
- 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
10
|
TransformerChainExecutor
|
|
6
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-WNKWAEUR.js";
|
|
7
12
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
13
|
+
injectExtendedContextBeta
|
|
14
|
+
} from "./chunk-MZNPGW5Q.js";
|
|
10
15
|
import {
|
|
11
16
|
GeminiTransformer
|
|
12
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-LVUCAWMK.js";
|
|
13
18
|
import {
|
|
14
19
|
OpenAIResponseTransformer
|
|
15
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-OEWZUX4G.js";
|
|
16
21
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
AnthropicTransformer,
|
|
23
|
+
resolveAnthropicMaxTokens
|
|
24
|
+
} from "./chunk-6CL5HTPQ.js";
|
|
25
|
+
import {
|
|
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,
|
|
@@ -84,10 +94,10 @@ import {
|
|
|
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();
|
|
@@ -745,6 +792,27 @@ function writeError(res, status, message) {
|
|
|
745
792
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
746
793
|
res.end(JSON.stringify({ error: { type: "provider_proxy_error", message } }));
|
|
747
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
|
+
}
|
|
748
816
|
function resolveApiKey(apiKey) {
|
|
749
817
|
if (!apiKey) return "";
|
|
750
818
|
if (apiKey.startsWith("$")) {
|
|
@@ -752,7 +820,12 @@ function resolveApiKey(apiKey) {
|
|
|
752
820
|
}
|
|
753
821
|
return apiKey;
|
|
754
822
|
}
|
|
755
|
-
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
|
+
}
|
|
756
829
|
if (deps.apiKeyPool && sessionId) {
|
|
757
830
|
const poolKey = await deps.apiKeyPool.getKeyForSession(providerId, sessionId);
|
|
758
831
|
if (poolKey) return poolKey;
|
|
@@ -807,6 +880,8 @@ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isS
|
|
|
807
880
|
return {
|
|
808
881
|
auth,
|
|
809
882
|
preferredAccountId: route.preferredAccountId,
|
|
883
|
+
preferredAccountGroup: route.preferredAccountGroup,
|
|
884
|
+
boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
|
|
810
885
|
chain,
|
|
811
886
|
transformerProvider,
|
|
812
887
|
resolvedModel,
|
|
@@ -881,6 +956,8 @@ async function runPipeline(anthropicBody, plan, reportSelection) {
|
|
|
881
956
|
model: resolvedModel,
|
|
882
957
|
sessionKey: plan.sessionKey,
|
|
883
958
|
preferredAccountId: plan.preferredAccountId,
|
|
959
|
+
preferredAccountGroup: plan.preferredAccountGroup,
|
|
960
|
+
boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
|
|
884
961
|
reportSelection: (accountId, isActive) => {
|
|
885
962
|
proxyAccountId = accountId;
|
|
886
963
|
reportSelection?.(accountId, isActive);
|
|
@@ -927,6 +1004,8 @@ async function runSubscriptionSameFormatFetch(rawBody, plan, reportSelection, op
|
|
|
927
1004
|
model: plan.resolvedModel,
|
|
928
1005
|
sessionKey: plan.sessionKey,
|
|
929
1006
|
preferredAccountId: plan.preferredAccountId,
|
|
1007
|
+
preferredAccountGroup: plan.preferredAccountGroup,
|
|
1008
|
+
boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
|
|
930
1009
|
reportSelection: (accountId, isActive, remapped) => {
|
|
931
1010
|
proxyAccountId = accountId;
|
|
932
1011
|
remappedModel = remapped;
|
|
@@ -1119,6 +1198,35 @@ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {
|
|
|
1119
1198
|
const upstreamSse = (upstreamResponse.headers.get("content-type") ?? "").includes(
|
|
1120
1199
|
"text/event-stream"
|
|
1121
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;
|
|
1122
1230
|
let bodyText;
|
|
1123
1231
|
if (!isStream && upstreamSse) {
|
|
1124
1232
|
bodyText = await aggregateAnthropicSseToJsonBody(upstreamResponse, route.requestedModel);
|
|
@@ -1128,22 +1236,19 @@ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {
|
|
|
1128
1236
|
);
|
|
1129
1237
|
res.end(bodyText);
|
|
1130
1238
|
} else {
|
|
1131
|
-
bodyText = await relayResponse(res, upstreamResponse, isStream, route.requestedModel);
|
|
1239
|
+
bodyText = await relayResponse(res, upstreamResponse, isStream, route.requestedModel, usageTap);
|
|
1132
1240
|
}
|
|
1133
1241
|
if (bodyText && deps.usageRecorder) {
|
|
1134
|
-
recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText,
|
|
1135
|
-
sessionId: route.sessionId,
|
|
1136
|
-
providerId: route.providerId ?? "anthropic",
|
|
1137
|
-
model: plan.resolvedModel,
|
|
1138
|
-
apiKeyId: route.apiKeyId ?? null,
|
|
1139
|
-
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1140
|
-
auditResponse: res
|
|
1141
|
-
});
|
|
1242
|
+
recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
|
|
1142
1243
|
}
|
|
1143
1244
|
} catch (err) {
|
|
1245
|
+
if (isBoundAccountSelectionError(err)) {
|
|
1246
|
+
writeBoundAccountError(res, err);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1144
1249
|
const errMsg = serializeError(err);
|
|
1145
1250
|
console.error("[ProviderProxy:anthropic] Pipeline error:", errMsg);
|
|
1146
|
-
writeError(res, 502, errMsg);
|
|
1251
|
+
writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
|
|
1147
1252
|
}
|
|
1148
1253
|
}
|
|
1149
1254
|
async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
|
|
@@ -1157,7 +1262,14 @@ async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
|
|
|
1157
1262
|
writeError(res, 502, `Provider not found: ${providerId}`);
|
|
1158
1263
|
return null;
|
|
1159
1264
|
}
|
|
1160
|
-
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
|
+
);
|
|
1161
1273
|
if (!apiKey) {
|
|
1162
1274
|
writeError(res, 502, "API key not configured");
|
|
1163
1275
|
return null;
|
|
@@ -1270,11 +1382,31 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
|
|
|
1270
1382
|
writeError(res, 502, "Anthropic route is missing a providerId");
|
|
1271
1383
|
return;
|
|
1272
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
|
+
}
|
|
1273
1405
|
const handler = handlerFactory({
|
|
1274
1406
|
llmConfig: deps.llmConfig,
|
|
1275
1407
|
providerId: route.providerId ?? "",
|
|
1276
1408
|
model: route.model,
|
|
1277
|
-
apiKey:
|
|
1409
|
+
apiKey: delegatedApiKey,
|
|
1278
1410
|
backgroundTaskModel: route.backgroundTaskModel,
|
|
1279
1411
|
isOfficialProvider: hints.isOfficialProvider,
|
|
1280
1412
|
thinkingLevel: hints.thinkingLevel,
|
|
@@ -1283,6 +1415,11 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
|
|
|
1283
1415
|
passThroughAuthToken: hints.passThroughAuthToken ?? null,
|
|
1284
1416
|
resolvePassThroughAuthToken: hints.resolvePassThroughAuthToken ?? null,
|
|
1285
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,
|
|
1286
1423
|
maxConcurrency: hints.maxConcurrency,
|
|
1287
1424
|
webSearchService: hints.webSearchService ?? null,
|
|
1288
1425
|
onRetry: hints.onRetry,
|
|
@@ -1309,23 +1446,36 @@ function readGeminiUsage(usage) {
|
|
|
1309
1446
|
reasoningTokens
|
|
1310
1447
|
};
|
|
1311
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
|
+
}
|
|
1312
1465
|
function recordGeminiNonStreamUsage(recorder, bodyText, attribution) {
|
|
1313
1466
|
try {
|
|
1314
1467
|
const parsed = JSON.parse(bodyText);
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
usage: tapped,
|
|
1327
|
-
rawUsage: parsed.usageMetadata
|
|
1328
|
-
});
|
|
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);
|
|
1329
1479
|
} catch {
|
|
1330
1480
|
}
|
|
1331
1481
|
}
|
|
@@ -1366,16 +1516,32 @@ async function handleGeminiGenerateContentRequest(res, rawBody, url, route, deps
|
|
|
1366
1516
|
const plan = await buildByoPlan2(res, route, deps, resolvedModel, isStream);
|
|
1367
1517
|
if (!plan) return;
|
|
1368
1518
|
const providerResponse = await runPipelineWithPoolReporting2(geminiBody, plan);
|
|
1369
|
-
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
|
+
);
|
|
1370
1543
|
if (bodyText && deps.usageRecorder) {
|
|
1371
|
-
recordGeminiNonStreamUsage(deps.usageRecorder, bodyText,
|
|
1372
|
-
sessionId: route.sessionId,
|
|
1373
|
-
providerId: route.providerId ?? "gemini",
|
|
1374
|
-
model: resolvedModel,
|
|
1375
|
-
apiKeyId: route.apiKeyId ?? null,
|
|
1376
|
-
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1377
|
-
auditResponse: res
|
|
1378
|
-
});
|
|
1544
|
+
recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
|
|
1379
1545
|
}
|
|
1380
1546
|
} catch (err) {
|
|
1381
1547
|
const errMsg = serializeError(err);
|
|
@@ -1394,7 +1560,14 @@ async function buildByoPlan2(res, route, deps, resolvedModel, isStream) {
|
|
|
1394
1560
|
writeError(res, 502, `Provider not found: ${providerId}`);
|
|
1395
1561
|
return null;
|
|
1396
1562
|
}
|
|
1397
|
-
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
|
+
);
|
|
1398
1571
|
if (!apiKey) {
|
|
1399
1572
|
writeError(res, 502, "API key not configured");
|
|
1400
1573
|
return null;
|
|
@@ -1494,23 +1667,36 @@ function readChatCompletionsUsage(usage) {
|
|
|
1494
1667
|
reasoningTokens
|
|
1495
1668
|
};
|
|
1496
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
|
+
}
|
|
1497
1686
|
function recordChatCompletionsNonStreamUsage(recorder, bodyText, attribution) {
|
|
1498
1687
|
try {
|
|
1499
1688
|
const parsed = JSON.parse(bodyText);
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
usage: tapped,
|
|
1512
|
-
rawUsage: parsed.usage
|
|
1513
|
-
});
|
|
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);
|
|
1514
1700
|
} catch {
|
|
1515
1701
|
}
|
|
1516
1702
|
}
|
|
@@ -1538,21 +1724,41 @@ async function handleOpenAIChatRequest(res, rawBody, route, deps) {
|
|
|
1538
1724
|
const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan3(res, route, deps, resolvedModel, isStream);
|
|
1539
1725
|
if (!plan) return;
|
|
1540
1726
|
const providerResponse = plan.isSubscription ? await runPipelineWithSubscriptionRetry2(chatBody, plan) : await runPipelineWithPoolReporting3(chatBody, plan);
|
|
1541
|
-
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
|
+
);
|
|
1542
1751
|
if (bodyText && deps.usageRecorder) {
|
|
1543
|
-
recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText,
|
|
1544
|
-
sessionId: route.sessionId,
|
|
1545
|
-
providerId: route.providerId ?? "openai",
|
|
1546
|
-
model: resolvedModel,
|
|
1547
|
-
apiKeyId: route.apiKeyId ?? null,
|
|
1548
|
-
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1549
|
-
auditResponse: res
|
|
1550
|
-
});
|
|
1752
|
+
recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
|
|
1551
1753
|
}
|
|
1552
1754
|
} catch (err) {
|
|
1755
|
+
if (isBoundAccountSelectionError(err)) {
|
|
1756
|
+
writeBoundAccountError(res, err);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1553
1759
|
const errMsg = serializeError(err);
|
|
1554
1760
|
console.error("[ProviderProxy:chat] Pipeline error:", errMsg);
|
|
1555
|
-
writeError(res, 502, errMsg);
|
|
1761
|
+
writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
|
|
1556
1762
|
}
|
|
1557
1763
|
}
|
|
1558
1764
|
async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
|
|
@@ -1566,7 +1772,14 @@ async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
|
|
|
1566
1772
|
writeError(res, 502, `Provider not found: ${providerId}`);
|
|
1567
1773
|
return null;
|
|
1568
1774
|
}
|
|
1569
|
-
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
|
+
);
|
|
1570
1783
|
if (!apiKey) {
|
|
1571
1784
|
writeError(res, 502, "API key not configured");
|
|
1572
1785
|
return null;
|
|
@@ -1632,6 +1845,9 @@ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream)
|
|
|
1632
1845
|
};
|
|
1633
1846
|
return {
|
|
1634
1847
|
auth,
|
|
1848
|
+
preferredAccountId: route.preferredAccountId,
|
|
1849
|
+
preferredAccountGroup: route.preferredAccountGroup,
|
|
1850
|
+
boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
|
|
1635
1851
|
chain,
|
|
1636
1852
|
transformerProvider,
|
|
1637
1853
|
resolvedModel,
|
|
@@ -1650,6 +1866,9 @@ async function runPipeline3(chatBody, plan) {
|
|
|
1650
1866
|
await auth.applyHeaders(authHeaders, {
|
|
1651
1867
|
upstreamUrl,
|
|
1652
1868
|
model: resolvedModel,
|
|
1869
|
+
preferredAccountId: plan.preferredAccountId,
|
|
1870
|
+
preferredAccountGroup: plan.preferredAccountGroup,
|
|
1871
|
+
boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
|
|
1653
1872
|
reportSelection: (accountId) => {
|
|
1654
1873
|
proxyAccountId = accountId;
|
|
1655
1874
|
}
|
|
@@ -1728,23 +1947,36 @@ function readResponsesUsage(usage) {
|
|
|
1728
1947
|
reasoningTokens
|
|
1729
1948
|
};
|
|
1730
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
|
+
}
|
|
1731
1966
|
function recordResponsesNonStreamUsage(recorder, bodyText, attribution) {
|
|
1732
1967
|
try {
|
|
1733
1968
|
const parsed = JSON.parse(bodyText);
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
usage: tapped,
|
|
1746
|
-
rawUsage: parsed.usage
|
|
1747
|
-
});
|
|
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);
|
|
1748
1980
|
} catch {
|
|
1749
1981
|
}
|
|
1750
1982
|
}
|
|
@@ -1755,7 +1987,7 @@ function isOpenAIResponsesRequest(method, url) {
|
|
|
1755
1987
|
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
1756
1988
|
return path.endsWith("/responses");
|
|
1757
1989
|
}
|
|
1758
|
-
async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
|
|
1990
|
+
async function handleOpenAIResponsesRequest(res, rawBody, route, deps, requestHeaders = {}) {
|
|
1759
1991
|
let responsesBody;
|
|
1760
1992
|
try {
|
|
1761
1993
|
responsesBody = JSON.parse(rawBody);
|
|
@@ -1764,27 +1996,58 @@ async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
|
|
|
1764
1996
|
return;
|
|
1765
1997
|
}
|
|
1766
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;
|
|
1767
2007
|
const resolvedModel = route.model;
|
|
1768
2008
|
responsesBody.model = resolvedModel;
|
|
1769
2009
|
try {
|
|
1770
|
-
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);
|
|
1771
2011
|
if (!plan) return;
|
|
1772
2012
|
const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry3(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
|
|
1773
|
-
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
|
+
);
|
|
1774
2040
|
if (bodyText && deps.usageRecorder) {
|
|
1775
|
-
recordResponsesNonStreamUsage(deps.usageRecorder, bodyText,
|
|
1776
|
-
sessionId: route.sessionId,
|
|
1777
|
-
providerId: route.providerId ?? "codex",
|
|
1778
|
-
model: resolvedModel,
|
|
1779
|
-
apiKeyId: route.apiKeyId ?? null,
|
|
1780
|
-
// request-audit-log: correlate this request's tokens/cost to its audit record.
|
|
1781
|
-
auditResponse: res
|
|
1782
|
-
});
|
|
2041
|
+
recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
|
|
1783
2042
|
}
|
|
1784
2043
|
} catch (err) {
|
|
2044
|
+
if (isBoundAccountSelectionError(err)) {
|
|
2045
|
+
writeBoundAccountError(res, err);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
1785
2048
|
const errMsg = serializeError(err);
|
|
1786
2049
|
console.error("[ProviderProxy:responses] Pipeline error:", errMsg);
|
|
1787
|
-
writeError(res, 502, errMsg);
|
|
2050
|
+
writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
|
|
1788
2051
|
}
|
|
1789
2052
|
}
|
|
1790
2053
|
async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
|
|
@@ -1798,7 +2061,14 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
|
|
|
1798
2061
|
writeError(res, 502, `Provider not found: ${providerId}`);
|
|
1799
2062
|
return null;
|
|
1800
2063
|
}
|
|
1801
|
-
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
|
+
);
|
|
1802
2072
|
if (!apiKey) {
|
|
1803
2073
|
writeError(res, 502, "API key not configured");
|
|
1804
2074
|
return null;
|
|
@@ -1829,7 +2099,7 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
|
|
|
1829
2099
|
proxyProviderId: "byo"
|
|
1830
2100
|
};
|
|
1831
2101
|
}
|
|
1832
|
-
async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) {
|
|
2102
|
+
async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream, sessionKey) {
|
|
1833
2103
|
const profile = route.subscriptionProfile;
|
|
1834
2104
|
if (!profile) {
|
|
1835
2105
|
writeError(res, 502, "Subscription mode requires a codex subscription profile");
|
|
@@ -1854,15 +2124,27 @@ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream)
|
|
|
1854
2124
|
};
|
|
1855
2125
|
if (profile.authStrategy.providerId === "gemini") {
|
|
1856
2126
|
try {
|
|
1857
|
-
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
|
+
});
|
|
1858
2133
|
} catch (err) {
|
|
2134
|
+
if (isBoundAccountSelectionError(err)) {
|
|
2135
|
+
writeBoundAccountError(res, err);
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
1859
2138
|
writeError(res, 502, serializeError(err));
|
|
1860
2139
|
return null;
|
|
1861
2140
|
}
|
|
1862
2141
|
}
|
|
1863
2142
|
return {
|
|
1864
2143
|
auth,
|
|
2144
|
+
sessionKey,
|
|
1865
2145
|
preferredAccountId: route.preferredAccountId,
|
|
2146
|
+
preferredAccountGroup: route.preferredAccountGroup,
|
|
2147
|
+
boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
|
|
1866
2148
|
chain,
|
|
1867
2149
|
transformerProvider,
|
|
1868
2150
|
resolvedModel,
|
|
@@ -1875,9 +2157,9 @@ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream)
|
|
|
1875
2157
|
proxyProviderId: profile.authStrategy.providerId
|
|
1876
2158
|
};
|
|
1877
2159
|
}
|
|
1878
|
-
async function resolveGeminiCodeAssistProject(profile) {
|
|
2160
|
+
async function resolveGeminiCodeAssistProject(profile, hints) {
|
|
1879
2161
|
const probe = {};
|
|
1880
|
-
await profile.authStrategy.applyHeaders(probe);
|
|
2162
|
+
await profile.authStrategy.applyHeaders(probe, hints);
|
|
1881
2163
|
const bearer = probe.Authorization ?? probe.authorization ?? "";
|
|
1882
2164
|
const accessToken = bearer.replace(/^Bearer\s+/i, "").trim();
|
|
1883
2165
|
if (!accessToken) return void 0;
|
|
@@ -1888,13 +2170,16 @@ async function resolveGeminiCodeAssistProject(profile) {
|
|
|
1888
2170
|
async function runPipeline4(responsesBody, plan) {
|
|
1889
2171
|
const executor = getSharedExecutor();
|
|
1890
2172
|
const endpointTransformer = getResponsesEndpointTransformer();
|
|
1891
|
-
const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
|
|
2173
|
+
const { auth, sessionKey, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
|
|
1892
2174
|
let proxyAccountId;
|
|
1893
2175
|
const authHeaders = {};
|
|
1894
2176
|
await auth.applyHeaders(authHeaders, {
|
|
1895
2177
|
upstreamUrl,
|
|
1896
2178
|
model: resolvedModel,
|
|
2179
|
+
sessionKey,
|
|
1897
2180
|
preferredAccountId: plan.preferredAccountId,
|
|
2181
|
+
preferredAccountGroup: plan.preferredAccountGroup,
|
|
2182
|
+
boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
|
|
1898
2183
|
reportSelection: (accountId) => {
|
|
1899
2184
|
proxyAccountId = accountId;
|
|
1900
2185
|
}
|
|
@@ -1947,7 +2232,7 @@ async function runPipelineWithPoolReporting4(responsesBody, plan) {
|
|
|
1947
2232
|
async function runPipelineWithSubscriptionRetry3(responsesBody, plan) {
|
|
1948
2233
|
const first = await runPipeline4(responsesBody, plan);
|
|
1949
2234
|
if (first.rawStatus !== 401) return first;
|
|
1950
|
-
const refreshed = await plan.auth.onUnauthorized?.();
|
|
2235
|
+
const refreshed = await plan.auth.onUnauthorized?.(plan.sessionKey);
|
|
1951
2236
|
if (!refreshed) {
|
|
1952
2237
|
console.warn("[ProviderProxy:responses] 401 not recoverable (onUnauthorized returned false)");
|
|
1953
2238
|
return first;
|
|
@@ -1986,7 +2271,7 @@ async function routeRequest(req, res, routes, deps) {
|
|
|
1986
2271
|
}
|
|
1987
2272
|
if (isOpenAIResponsesRequest(method, url)) {
|
|
1988
2273
|
const rawBody = await readBody(req);
|
|
1989
|
-
await handleOpenAIResponsesRequest(res, rawBody, route, deps);
|
|
2274
|
+
await handleOpenAIResponsesRequest(res, rawBody, route, deps, req.headers);
|
|
1990
2275
|
return;
|
|
1991
2276
|
}
|
|
1992
2277
|
if (isOpenAIChatRequest(method, url)) {
|
|
@@ -2196,6 +2481,38 @@ var DEFAULT_ACCOUNT_PROBE = {
|
|
|
2196
2481
|
historySize: 10,
|
|
2197
2482
|
staggerMs: 500
|
|
2198
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
|
+
}
|
|
2199
2516
|
function normalizeAccountProbe(raw) {
|
|
2200
2517
|
const ap = raw?.accountProbe;
|
|
2201
2518
|
return {
|
|
@@ -2390,7 +2707,8 @@ function normalizePrefixTargets(raw) {
|
|
|
2390
2707
|
function normalizeEndpointConfig(e) {
|
|
2391
2708
|
const endpoint = e.endpoint;
|
|
2392
2709
|
const useSubscription = e.useSubscription === true;
|
|
2393
|
-
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;
|
|
2394
2712
|
const boundKeyId = typeof e.boundKeyId === "string" && e.boundKeyId.trim() !== "" ? e.boundKeyId : void 0;
|
|
2395
2713
|
let config;
|
|
2396
2714
|
if (isKindMappedEndpoint(endpoint)) {
|
|
@@ -2420,21 +2738,186 @@ function normalizeEndpointConfig(e) {
|
|
|
2420
2738
|
config.backgroundModelIds = e.backgroundModelIds;
|
|
2421
2739
|
}
|
|
2422
2740
|
}
|
|
2423
|
-
if (boundAccountId)
|
|
2741
|
+
if (boundAccountId) {
|
|
2742
|
+
config.boundAccountId = boundAccountId;
|
|
2743
|
+
config.boundAccountFallbackPolicy = boundAccountFallbackPolicy;
|
|
2744
|
+
}
|
|
2424
2745
|
if (boundKeyId) config.boundKeyId = boundKeyId;
|
|
2425
2746
|
return config;
|
|
2426
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
|
+
}
|
|
2427
2908
|
function defaultServerConfig() {
|
|
2428
2909
|
const queues = normalizeQueueSegments(void 0);
|
|
2429
2910
|
return {
|
|
2430
2911
|
enabled: false,
|
|
2431
2912
|
networkBinding: false,
|
|
2432
2913
|
endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
|
|
2914
|
+
bindings: [],
|
|
2433
2915
|
port: DEFAULT_OUTBOUND_PORT,
|
|
2434
2916
|
userMessageQueue: queues.userMessageQueue,
|
|
2435
2917
|
concurrencyQueue: queues.concurrencyQueue,
|
|
2436
2918
|
accountHealth: normalizeAccountHealth(void 0),
|
|
2437
2919
|
accountProbe: normalizeAccountProbe(void 0),
|
|
2920
|
+
allowanceScheduling: normalizeAllowanceScheduling(void 0),
|
|
2438
2921
|
audit: normalizeAudit(void 0),
|
|
2439
2922
|
billing: normalizeBilling(void 0),
|
|
2440
2923
|
fingerprint: normalizeFingerprint(void 0),
|
|
@@ -2451,17 +2934,20 @@ function normalizeServerConfig(raw) {
|
|
|
2451
2934
|
}
|
|
2452
2935
|
}
|
|
2453
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));
|
|
2454
2940
|
const config = {
|
|
2455
2941
|
enabled: raw.enabled === true,
|
|
2456
2942
|
networkBinding: raw.networkBinding === true,
|
|
2457
|
-
endpoints: ALL_ENDPOINTS.map(
|
|
2458
|
-
|
|
2459
|
-
),
|
|
2943
|
+
endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
|
|
2944
|
+
bindings,
|
|
2460
2945
|
port: raw.port ?? base.port,
|
|
2461
2946
|
userMessageQueue: queues.userMessageQueue,
|
|
2462
2947
|
concurrencyQueue: queues.concurrencyQueue,
|
|
2463
2948
|
accountHealth: normalizeAccountHealth(raw),
|
|
2464
2949
|
accountProbe: normalizeAccountProbe(raw),
|
|
2950
|
+
allowanceScheduling: normalizeAllowanceScheduling(raw),
|
|
2465
2951
|
audit: normalizeAudit(raw),
|
|
2466
2952
|
billing: normalizeBilling(raw),
|
|
2467
2953
|
fingerprint: normalizeFingerprint(raw),
|
|
@@ -2487,11 +2973,13 @@ function mergeServerConfig(current, patch) {
|
|
|
2487
2973
|
enabled: patch.enabled ?? current.enabled,
|
|
2488
2974
|
networkBinding: patch.networkBinding ?? current.networkBinding,
|
|
2489
2975
|
endpoints: patch.endpoints ?? current.endpoints,
|
|
2976
|
+
bindings: patch.bindings ?? current.bindings,
|
|
2490
2977
|
port: patch.port ?? current.port,
|
|
2491
2978
|
userMessageQueue: patch.userMessageQueue ?? current.userMessageQueue,
|
|
2492
2979
|
concurrencyQueue: patch.concurrencyQueue ?? current.concurrencyQueue,
|
|
2493
2980
|
accountHealth: patch.accountHealth ?? current.accountHealth,
|
|
2494
2981
|
accountProbe: patch.accountProbe ?? current.accountProbe,
|
|
2982
|
+
allowanceScheduling: patch.allowanceScheduling ?? current.allowanceScheduling,
|
|
2495
2983
|
audit: patch.audit ?? current.audit,
|
|
2496
2984
|
billing: patch.billing ?? current.billing,
|
|
2497
2985
|
// Proxy is layer-replaced (not deep-merged): a PUT carrying `proxy` swaps the
|
|
@@ -2509,6 +2997,167 @@ function mergeServerConfig(current, patch) {
|
|
|
2509
2997
|
});
|
|
2510
2998
|
}
|
|
2511
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
|
+
|
|
2512
3161
|
// src/outbound-api/outboundApiKeyAuth.ts
|
|
2513
3162
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
2514
3163
|
|
|
@@ -2601,6 +3250,27 @@ async function createNamedKey(db, name) {
|
|
|
2601
3250
|
plaintextOnce: secret
|
|
2602
3251
|
};
|
|
2603
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
|
+
}
|
|
2604
3274
|
function extractCostLimits(row) {
|
|
2605
3275
|
const limits = {};
|
|
2606
3276
|
if (row.dailyCostLimitUsd != null) limits.dailyUsd = row.dailyCostLimitUsd;
|
|
@@ -2623,7 +3293,12 @@ function extractModelRestriction(row) {
|
|
|
2623
3293
|
};
|
|
2624
3294
|
}
|
|
2625
3295
|
function toVerifiedKey(row) {
|
|
2626
|
-
const key = {
|
|
3296
|
+
const key = {
|
|
3297
|
+
id: row.id,
|
|
3298
|
+
kind: row.kind,
|
|
3299
|
+
allowedEndpoints: row.allowedEndpoints,
|
|
3300
|
+
loopbackOnly: row.loopbackOnly
|
|
3301
|
+
};
|
|
2627
3302
|
if (row.maxConcurrency !== void 0 && row.maxConcurrency !== null) {
|
|
2628
3303
|
key.maxConcurrency = row.maxConcurrency;
|
|
2629
3304
|
}
|
|
@@ -3267,10 +3942,10 @@ function getFallbackRedeemLimiter() {
|
|
|
3267
3942
|
fallbackRedeemLimiter ??= new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
|
|
3268
3943
|
return fallbackRedeemLimiter;
|
|
3269
3944
|
}
|
|
3270
|
-
function writeJsonError(res, status, message, headers = {}) {
|
|
3945
|
+
function writeJsonError(res, status, message, headers = {}, details = {}) {
|
|
3271
3946
|
if (res.headersSent) return;
|
|
3272
3947
|
res.writeHead(status, { "Content-Type": "application/json", ...headers });
|
|
3273
|
-
res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
|
|
3948
|
+
res.end(JSON.stringify({ error: { type: "outbound_api_error", message, ...details } }));
|
|
3274
3949
|
}
|
|
3275
3950
|
function writeCostLimitError(res, scope, limitUsd, spentUsd) {
|
|
3276
3951
|
if (res.headersSent) return;
|
|
@@ -3314,6 +3989,11 @@ function extractPresentedKey(req) {
|
|
|
3314
3989
|
const g = Array.isArray(goog) ? goog[0] : goog;
|
|
3315
3990
|
return g?.trim() || void 0;
|
|
3316
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
|
+
}
|
|
3317
3997
|
function selectEndpoint(method, url) {
|
|
3318
3998
|
if (method !== "POST" || !url) return null;
|
|
3319
3999
|
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
@@ -3337,9 +4017,33 @@ function isModelsListRequest(url) {
|
|
|
3337
4017
|
const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
|
|
3338
4018
|
return path.endsWith("/models");
|
|
3339
4019
|
}
|
|
3340
|
-
function
|
|
3341
|
-
const
|
|
3342
|
-
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" }));
|
|
3343
4047
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3344
4048
|
res.end(JSON.stringify({ object: "list", data }));
|
|
3345
4049
|
}
|
|
@@ -3373,7 +4077,15 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3373
4077
|
const verified = verification.key;
|
|
3374
4078
|
if (audit) audit.keyId = verified.id;
|
|
3375
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
|
+
}
|
|
3376
4084
|
if (isRedeemRequest(req.method, req.url)) {
|
|
4085
|
+
if (verified.kind === "integration") {
|
|
4086
|
+
writeJsonError(res, 403, "Integration keys cannot redeem vouchers");
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
3377
4089
|
await handleVoucherRedeem(
|
|
3378
4090
|
req,
|
|
3379
4091
|
res,
|
|
@@ -3395,7 +4107,11 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3395
4107
|
return;
|
|
3396
4108
|
}
|
|
3397
4109
|
if (req.method === "GET" && isModelsListRequest(req.url)) {
|
|
3398
|
-
|
|
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);
|
|
3399
4115
|
return;
|
|
3400
4116
|
}
|
|
3401
4117
|
const endpoint = selectEndpoint(req.method, req.url);
|
|
@@ -3403,9 +4119,12 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3403
4119
|
writeJsonError(res, 404, `Unsupported: ${req.method} ${req.url}`);
|
|
3404
4120
|
return;
|
|
3405
4121
|
}
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
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`);
|
|
3409
4128
|
return;
|
|
3410
4129
|
}
|
|
3411
4130
|
if (verified.costLimits && deps.keySpendTracker) {
|
|
@@ -3478,23 +4197,40 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3478
4197
|
const urlModel = extractGeminiModelFromUrl(req.url);
|
|
3479
4198
|
if (urlModel) parsedBody["model"] = urlModel;
|
|
3480
4199
|
}
|
|
3481
|
-
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;
|
|
3482
4220
|
const resolved = isKindMappedEndpoint(endpoint) || endpoint === "chat" ? await resolveRoute({
|
|
3483
|
-
config:
|
|
4221
|
+
config: effectiveEndpointConfig,
|
|
3484
4222
|
ingressFormat,
|
|
3485
4223
|
llmConfig: deps.llmConfig,
|
|
3486
4224
|
sessionId,
|
|
3487
4225
|
// Capture the ORIGINAL requested id BEFORE any downstream swap; for
|
|
3488
4226
|
// kind-mapped endpoints it selects the kind AND is stamped onto
|
|
3489
4227
|
// `route.requestedModel`; for chat it is matched against the list.
|
|
3490
|
-
requestedModel
|
|
4228
|
+
requestedModel,
|
|
3491
4229
|
// Attribution: stamp the verified named-key id onto the route.
|
|
3492
4230
|
apiKeyId: verified.id
|
|
3493
4231
|
}) : await resolveRoute({
|
|
3494
|
-
config:
|
|
3495
|
-
role
|
|
3496
|
-
backgroundModelIds: endpointConfig.backgroundModelIds
|
|
3497
|
-
}),
|
|
4232
|
+
config: effectiveEndpointConfig,
|
|
4233
|
+
role,
|
|
3498
4234
|
ingressFormat,
|
|
3499
4235
|
llmConfig: deps.llmConfig,
|
|
3500
4236
|
sessionId,
|
|
@@ -3555,12 +4291,29 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
|
|
|
3555
4291
|
const replay = makeReplayRequest(req, rawBody);
|
|
3556
4292
|
await routeRequest(replay, res, routeMap, deps.proxyDeps);
|
|
3557
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
|
+
}
|
|
3558
4311
|
const message = serializeError(err);
|
|
3559
4312
|
if (deps.logger) deps.logger.error("[OutboundApi] dispatch error:", message);
|
|
3560
4313
|
else console.error("[OutboundApi] dispatch error:", message);
|
|
3561
4314
|
emitWebhookEvent({ kind: "server.error", at: Date.now(), message });
|
|
3562
4315
|
if (audit) audit.error = message;
|
|
3563
|
-
writeJsonError(res, 502, message);
|
|
4316
|
+
writeJsonError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, message);
|
|
3564
4317
|
} finally {
|
|
3565
4318
|
routeMap.removeRoute(token);
|
|
3566
4319
|
if (releaseSerial) releaseSerial();
|
|
@@ -3584,16 +4337,6 @@ function makeReplayRequest(req, rawBody) {
|
|
|
3584
4337
|
var DEFAULT_OUTBOUND_PORT = 8765;
|
|
3585
4338
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
3586
4339
|
var LAN_ADDR = "0.0.0.0";
|
|
3587
|
-
var OutboundApiConfigError = class extends Error {
|
|
3588
|
-
missing;
|
|
3589
|
-
constructor(missing) {
|
|
3590
|
-
super(
|
|
3591
|
-
"Outbound API server cannot start: incomplete model-kind configuration \u2014 " + missing.map((m) => `${m.endpoint} missing [${m.missingKinds.join(", ")}]`).join("; ")
|
|
3592
|
-
);
|
|
3593
|
-
this.name = "OutboundApiConfigError";
|
|
3594
|
-
this.missing = missing;
|
|
3595
|
-
}
|
|
3596
|
-
};
|
|
3597
4340
|
var OutboundApiServer = class {
|
|
3598
4341
|
constructor(deps, onPortChange) {
|
|
3599
4342
|
this.deps = deps;
|
|
@@ -3605,6 +4348,7 @@ var OutboundApiServer = class {
|
|
|
3605
4348
|
boundPort = 0;
|
|
3606
4349
|
boundAddr = LOOPBACK_ADDR;
|
|
3607
4350
|
endpoints = [];
|
|
4351
|
+
bindings = [];
|
|
3608
4352
|
userMessageQueue;
|
|
3609
4353
|
concurrencyQueue;
|
|
3610
4354
|
voucherConfig;
|
|
@@ -3631,6 +4375,7 @@ var OutboundApiServer = class {
|
|
|
3631
4375
|
*/
|
|
3632
4376
|
async applyConfig(input) {
|
|
3633
4377
|
this.endpoints = input.endpoints;
|
|
4378
|
+
this.bindings = input.bindings ?? [];
|
|
3634
4379
|
this.userMessageQueue = input.userMessageQueue;
|
|
3635
4380
|
this.concurrencyQueue = input.concurrencyQueue;
|
|
3636
4381
|
this.voucherConfig = input.voucher;
|
|
@@ -3640,16 +4385,6 @@ var OutboundApiServer = class {
|
|
|
3640
4385
|
await this.stop();
|
|
3641
4386
|
return;
|
|
3642
4387
|
}
|
|
3643
|
-
const missing = validateServerModelConfig({
|
|
3644
|
-
enabled: true,
|
|
3645
|
-
networkBinding: input.networkBinding,
|
|
3646
|
-
endpoints: input.endpoints,
|
|
3647
|
-
port: input.port
|
|
3648
|
-
});
|
|
3649
|
-
if (missing.length > 0) {
|
|
3650
|
-
await this.stop();
|
|
3651
|
-
throw new OutboundApiConfigError(missing);
|
|
3652
|
-
}
|
|
3653
4388
|
const running = this.server !== null;
|
|
3654
4389
|
const bindChanged = running && (this.boundAddr !== wantAddr || this.boundPort !== wantPort);
|
|
3655
4390
|
if (running && !bindChanged) {
|
|
@@ -3706,6 +4441,7 @@ var OutboundApiServer = class {
|
|
|
3706
4441
|
this.deps,
|
|
3707
4442
|
{
|
|
3708
4443
|
endpoints: this.endpoints,
|
|
4444
|
+
bindings: this.bindings,
|
|
3709
4445
|
userMessageQueue: this.userMessageQueue,
|
|
3710
4446
|
concurrencyQueue: this.concurrencyQueue,
|
|
3711
4447
|
voucher: this.voucherConfig
|
|
@@ -3937,7 +4673,7 @@ async function callOpenAICompletion(provider, apiKey, options, logger) {
|
|
|
3937
4673
|
model: options.model,
|
|
3938
4674
|
messages: options.messages.map((m) => convertMessageToOpenAI(m)),
|
|
3939
4675
|
// Only set max_tokens if explicitly provided, otherwise let API use its default
|
|
3940
|
-
...options.maxTokens ? { max_tokens: options.maxTokens } : {},
|
|
4676
|
+
...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
|
|
3941
4677
|
temperature: options.temperature,
|
|
3942
4678
|
stream: false
|
|
3943
4679
|
// For now, non-streaming only
|
|
@@ -3988,7 +4724,7 @@ async function callAnthropicCompletion(provider, apiKey, options, logger) {
|
|
|
3988
4724
|
const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
|
|
3989
4725
|
const anthropicRequest2 = {
|
|
3990
4726
|
model: options.model,
|
|
3991
|
-
max_tokens: options.maxTokens
|
|
4727
|
+
max_tokens: options.maxTokens ?? 16384,
|
|
3992
4728
|
temperature: options.temperature,
|
|
3993
4729
|
...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
|
|
3994
4730
|
messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
|
|
@@ -4034,7 +4770,7 @@ async function callAnthropicCompletion(provider, apiKey, options, logger) {
|
|
|
4034
4770
|
content: m.content
|
|
4035
4771
|
})),
|
|
4036
4772
|
// Anthropic requires max_tokens; use 16384 default if not explicitly set
|
|
4037
|
-
max_tokens: options.maxTokens
|
|
4773
|
+
max_tokens: options.maxTokens ?? 16384,
|
|
4038
4774
|
temperature: options.temperature,
|
|
4039
4775
|
stream: false
|
|
4040
4776
|
};
|
|
@@ -4089,7 +4825,7 @@ async function callGeminiCompletion(provider, apiKey, options, logger) {
|
|
|
4089
4825
|
const request = {
|
|
4090
4826
|
contents,
|
|
4091
4827
|
generationConfig: {
|
|
4092
|
-
...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
|
|
4828
|
+
...options.maxTokens !== void 0 ? { maxOutputTokens: options.maxTokens } : {},
|
|
4093
4829
|
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
4094
4830
|
}
|
|
4095
4831
|
};
|
|
@@ -4158,7 +4894,7 @@ async function callOpenAIResponseCompletion(provider, apiKey, options, logger) {
|
|
|
4158
4894
|
model: options.model,
|
|
4159
4895
|
input,
|
|
4160
4896
|
stream: false,
|
|
4161
|
-
...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
|
|
4897
|
+
...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
|
|
4162
4898
|
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
4163
4899
|
};
|
|
4164
4900
|
if (options.thinkLevel && options.thinkLevel !== "none") {
|
|
@@ -4223,7 +4959,7 @@ async function streamOpenAICompletion(provider, apiKey, options, messageId, call
|
|
|
4223
4959
|
model: options.model,
|
|
4224
4960
|
messages: options.messages.map((m) => convertMessageToOpenAI(m)),
|
|
4225
4961
|
// Only set max_tokens if explicitly provided, otherwise let API use its default
|
|
4226
|
-
...options.maxTokens ? { max_tokens: options.maxTokens } : {},
|
|
4962
|
+
...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
|
|
4227
4963
|
temperature: options.temperature,
|
|
4228
4964
|
stream: true
|
|
4229
4965
|
};
|
|
@@ -4282,7 +5018,7 @@ async function streamOpenAICompletion(provider, apiKey, options, messageId, call
|
|
|
4282
5018
|
async function streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, logger) {
|
|
4283
5019
|
const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
|
|
4284
5020
|
const MAX_TOKENS_FOR_THINKING = 16384;
|
|
4285
|
-
let effectiveMaxTokens = options.maxTokens
|
|
5021
|
+
let effectiveMaxTokens = options.maxTokens ?? 16384;
|
|
4286
5022
|
const thinkingMaxTokens = options.thinkLevel && options.thinkLevel !== "none" ? Math.min(effectiveMaxTokens, MAX_TOKENS_FOR_THINKING) : effectiveMaxTokens;
|
|
4287
5023
|
const thinkingConfig = options.thinkLevel && options.thinkLevel !== "none" ? buildAnthropicThinking(options.model, options.thinkLevel, thinkingMaxTokens) : void 0;
|
|
4288
5024
|
if (thinkingConfig) {
|
|
@@ -4394,7 +5130,7 @@ async function streamGeminiCompletion(provider, apiKey, options, messageId, call
|
|
|
4394
5130
|
const request = {
|
|
4395
5131
|
contents,
|
|
4396
5132
|
generationConfig: {
|
|
4397
|
-
...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
|
|
5133
|
+
...options.maxTokens !== void 0 ? { maxOutputTokens: options.maxTokens } : {},
|
|
4398
5134
|
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
4399
5135
|
}
|
|
4400
5136
|
};
|
|
@@ -4458,7 +5194,7 @@ async function streamOpenAIResponseCompletion(provider, apiKey, options, message
|
|
|
4458
5194
|
model: options.model,
|
|
4459
5195
|
input,
|
|
4460
5196
|
stream: true,
|
|
4461
|
-
...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
|
|
5197
|
+
...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
|
|
4462
5198
|
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
4463
5199
|
};
|
|
4464
5200
|
if (options.thinkLevel && options.thinkLevel !== "none") {
|
|
@@ -4515,7 +5251,7 @@ import {
|
|
|
4515
5251
|
getClaudeMaxTokens,
|
|
4516
5252
|
isReasoningModel
|
|
4517
5253
|
} from "@omnicross/contracts/thinking-config";
|
|
4518
|
-
async function resolveEffectiveMaxTokens(llmConfig,
|
|
5254
|
+
async function resolveEffectiveMaxTokens(llmConfig, _getProvider, logger, _providerId, _modelId, sessionMaxTokens) {
|
|
4519
5255
|
if (sessionMaxTokens !== void 0 && sessionMaxTokens > 0) {
|
|
4520
5256
|
logger.debug("Using session maxTokens", { sessionMaxTokens });
|
|
4521
5257
|
return sessionMaxTokens;
|
|
@@ -4529,45 +5265,6 @@ async function resolveEffectiveMaxTokens(llmConfig, getProvider, logger, provide
|
|
|
4529
5265
|
} catch (err) {
|
|
4530
5266
|
logger.warn("Failed to get global params", err instanceof Error ? err : void 0);
|
|
4531
5267
|
}
|
|
4532
|
-
const MAX_TOKENS_CAP = 131072;
|
|
4533
|
-
const provider = await getProvider(providerId);
|
|
4534
|
-
if (provider) {
|
|
4535
|
-
const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
|
|
4536
|
-
if (modelConfig?.maxTokens && modelConfig.maxTokens > 0) {
|
|
4537
|
-
const cappedMaxTokens = Math.min(modelConfig.maxTokens, MAX_TOKENS_CAP);
|
|
4538
|
-
logger.debug("Using model config maxTokens", {
|
|
4539
|
-
maxTokens: modelConfig.maxTokens,
|
|
4540
|
-
cappedMaxTokens
|
|
4541
|
-
});
|
|
4542
|
-
return cappedMaxTokens;
|
|
4543
|
-
}
|
|
4544
|
-
if (provider.modelGroups) {
|
|
4545
|
-
for (const group of provider.modelGroups) {
|
|
4546
|
-
const model = group.models?.find((m) => m.id === modelId);
|
|
4547
|
-
if (model?.maxTokens && model.maxTokens > 0) {
|
|
4548
|
-
const cappedMaxTokens = Math.min(model.maxTokens, MAX_TOKENS_CAP);
|
|
4549
|
-
logger.debug("Using modelGroup model maxTokens", {
|
|
4550
|
-
maxTokens: model.maxTokens,
|
|
4551
|
-
cappedMaxTokens
|
|
4552
|
-
});
|
|
4553
|
-
return cappedMaxTokens;
|
|
4554
|
-
}
|
|
4555
|
-
}
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
try {
|
|
4559
|
-
const discoveredMaxTokens = await llmConfig.getDiscoveredModelMaxTokens(providerId, modelId);
|
|
4560
|
-
if (discoveredMaxTokens && discoveredMaxTokens > 0) {
|
|
4561
|
-
const cappedMaxTokens = Math.min(discoveredMaxTokens, MAX_TOKENS_CAP);
|
|
4562
|
-
logger.debug("Using discovered model maxTokens", {
|
|
4563
|
-
discoveredMaxTokens,
|
|
4564
|
-
cappedMaxTokens
|
|
4565
|
-
});
|
|
4566
|
-
return cappedMaxTokens;
|
|
4567
|
-
}
|
|
4568
|
-
} catch (err) {
|
|
4569
|
-
logger.warn("Failed to get discovered model maxTokens", err instanceof Error ? err : void 0);
|
|
4570
|
-
}
|
|
4571
5268
|
logger.debug("No maxTokens configured, returning undefined");
|
|
4572
5269
|
return void 0;
|
|
4573
5270
|
}
|
|
@@ -4670,7 +5367,8 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
|
|
|
4670
5367
|
}
|
|
4671
5368
|
requestBody = {
|
|
4672
5369
|
contents,
|
|
4673
|
-
tools: options.tools
|
|
5370
|
+
tools: options.tools,
|
|
5371
|
+
...options.maxTokens !== void 0 ? { generationConfig: { maxOutputTokens: options.maxTokens } } : {}
|
|
4674
5372
|
};
|
|
4675
5373
|
url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
|
|
4676
5374
|
} else if (apiFormat === "anthropic") {
|
|
@@ -4680,7 +5378,10 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
|
|
|
4680
5378
|
role: m.role,
|
|
4681
5379
|
content: m.content
|
|
4682
5380
|
})),
|
|
4683
|
-
|
|
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),
|
|
4684
5385
|
temperature: options.temperature ?? 0.7,
|
|
4685
5386
|
stream: true,
|
|
4686
5387
|
tools: options.tools
|
|
@@ -4697,7 +5398,7 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
|
|
|
4697
5398
|
role: m.role,
|
|
4698
5399
|
content: m.content
|
|
4699
5400
|
})),
|
|
4700
|
-
max_tokens: options.maxTokens
|
|
5401
|
+
...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
|
|
4701
5402
|
temperature: options.temperature ?? 0.7,
|
|
4702
5403
|
stream: true,
|
|
4703
5404
|
tools: options.tools
|
|
@@ -5194,6 +5895,18 @@ function getSharedExecutor2() {
|
|
|
5194
5895
|
async function resolveChainWithMain(llmConfig, providerId, model) {
|
|
5195
5896
|
return resolveProviderChain(llmConfig, providerId, model);
|
|
5196
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
|
+
}
|
|
5197
5910
|
async function completeWithTransformers(options, llmConfig, getProvider, resolveApiKey2, completeFallback, logger, recording) {
|
|
5198
5911
|
try {
|
|
5199
5912
|
const routedInfo = await llmConfig.resolveRoutedModel(
|
|
@@ -5222,16 +5935,11 @@ async function completeWithTransformers(options, llmConfig, getProvider, resolve
|
|
|
5222
5935
|
if (!hasTransformers) {
|
|
5223
5936
|
return completeFallback(options);
|
|
5224
5937
|
}
|
|
5225
|
-
const unifiedRequest =
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
})),
|
|
5231
|
-
max_tokens: options.maxTokens || 4096,
|
|
5232
|
-
temperature: options.temperature,
|
|
5233
|
-
stream: options.stream ?? false
|
|
5234
|
-
};
|
|
5938
|
+
const unifiedRequest = buildUnifiedCompletionRequest(
|
|
5939
|
+
actualModel,
|
|
5940
|
+
options,
|
|
5941
|
+
options.stream ?? false
|
|
5942
|
+
);
|
|
5235
5943
|
const transformerProvider = {
|
|
5236
5944
|
name: provider.name,
|
|
5237
5945
|
baseUrl: provider.api_base_url,
|
|
@@ -5361,16 +6069,7 @@ async function completeStreamWithTransformers(options, callbacks, llmConfig, get
|
|
|
5361
6069
|
}
|
|
5362
6070
|
const messageId = `msg_${Date.now()}`;
|
|
5363
6071
|
callbacks.onStart?.(messageId);
|
|
5364
|
-
const unifiedRequest =
|
|
5365
|
-
model: actualModel,
|
|
5366
|
-
messages: options.messages.map((m) => ({
|
|
5367
|
-
role: m.role,
|
|
5368
|
-
content: m.content
|
|
5369
|
-
})),
|
|
5370
|
-
max_tokens: options.maxTokens || 4096,
|
|
5371
|
-
temperature: options.temperature,
|
|
5372
|
-
stream: true
|
|
5373
|
-
};
|
|
6072
|
+
const unifiedRequest = buildUnifiedCompletionRequest(actualModel, options, true);
|
|
5374
6073
|
const transformerProvider = {
|
|
5375
6074
|
name: provider.name,
|
|
5376
6075
|
baseUrl: provider.api_base_url,
|
|
@@ -5905,9 +6604,10 @@ ${description}`;
|
|
|
5905
6604
|
* Resolve effective max_tokens value with priority:
|
|
5906
6605
|
* 1. Session settings (if provided)
|
|
5907
6606
|
* 2. Global model parameters (if enabled)
|
|
5908
|
-
* 3.
|
|
5909
|
-
*
|
|
5910
|
-
*
|
|
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.
|
|
5911
6611
|
*/
|
|
5912
6612
|
async resolveEffectiveMaxTokens(providerId, modelId, sessionMaxTokens) {
|
|
5913
6613
|
return resolveEffectiveMaxTokens(
|
|
@@ -6020,6 +6720,7 @@ export {
|
|
|
6020
6720
|
relayResponse,
|
|
6021
6721
|
aggregateAnthropicSseToJsonBody,
|
|
6022
6722
|
writeError,
|
|
6723
|
+
writeBoundAccountError,
|
|
6023
6724
|
resolvePoolBoundKey,
|
|
6024
6725
|
extractRouteToken,
|
|
6025
6726
|
isLoopbackAddress,
|
|
@@ -6031,6 +6732,8 @@ export {
|
|
|
6031
6732
|
DEFAULT_USER_MESSAGE_QUEUE,
|
|
6032
6733
|
DEFAULT_CONCURRENCY_QUEUE,
|
|
6033
6734
|
DEFAULT_ACCOUNT_PROBE,
|
|
6735
|
+
DEFAULT_ALLOWANCE_SCHEDULING,
|
|
6736
|
+
normalizeAllowanceScheduling,
|
|
6034
6737
|
normalizeAccountProbe,
|
|
6035
6738
|
normalizeAudit,
|
|
6036
6739
|
normalizeBilling,
|
|
@@ -6043,16 +6746,23 @@ export {
|
|
|
6043
6746
|
normalizeWebhookSegment,
|
|
6044
6747
|
normalizeQueueSegments,
|
|
6045
6748
|
normalizePrefixTargets,
|
|
6749
|
+
legacyEndpointsToBindings,
|
|
6750
|
+
normalizeGatewayBindings,
|
|
6046
6751
|
defaultServerConfig,
|
|
6047
6752
|
normalizeServerConfig,
|
|
6048
6753
|
loadServerConfig,
|
|
6049
6754
|
saveServerConfig,
|
|
6050
6755
|
mergeServerConfig,
|
|
6756
|
+
gatewayBindingToEndpointConfig,
|
|
6757
|
+
candidateGatewayBindings,
|
|
6758
|
+
candidateBackgroundModelIds,
|
|
6759
|
+
resolveGatewayBinding,
|
|
6051
6760
|
computeKeyExpiry,
|
|
6052
6761
|
checkKeyQuota,
|
|
6053
6762
|
randomBase62,
|
|
6054
6763
|
hashKey,
|
|
6055
6764
|
createNamedKey,
|
|
6765
|
+
createIntegrationKey,
|
|
6056
6766
|
verifyKey,
|
|
6057
6767
|
verifyPresentedKey,
|
|
6058
6768
|
ConcurrencyQueueFullError,
|
|
@@ -6074,7 +6784,6 @@ export {
|
|
|
6074
6784
|
isRedeemRequest,
|
|
6075
6785
|
handleVoucherRedeem,
|
|
6076
6786
|
DEFAULT_OUTBOUND_PORT,
|
|
6077
|
-
OutboundApiConfigError,
|
|
6078
6787
|
OutboundApiServer,
|
|
6079
6788
|
formatUrls,
|
|
6080
6789
|
startOfLocalDay,
|