@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.
Files changed (190) hide show
  1. package/dist/ApiConverter.cjs +2 -2
  2. package/dist/ApiConverter.js +1 -1
  3. package/dist/{CompletionService-DdWaAxq0.d.ts → CompletionService-C3BX0Ovy.d.ts} +4 -3
  4. package/dist/{CompletionService-D3a47vdU.d.cts → CompletionService-kNA17cfI.d.cts} +4 -3
  5. package/dist/auth/GeminiCodeAssistProjectResolver.cjs +6 -5
  6. package/dist/auth/GeminiCodeAssistProjectResolver.js +4 -3
  7. package/dist/{chunk-HRRYG2AX.js → chunk-23S5OPIO.js} +10 -1
  8. package/dist/chunk-25GXJCEZ.cjs +40 -0
  9. package/dist/chunk-2JYRIADS.js +40 -0
  10. package/dist/{chunk-4XMYPW46.cjs → chunk-4VLRHHFE.cjs} +75 -33
  11. package/dist/{chunk-VACWOIRE.cjs → chunk-4VZX5F4T.cjs} +39 -15
  12. package/dist/{chunk-C6OV7GCC.js → chunk-6CL5HTPQ.js} +44 -2
  13. package/dist/chunk-6QQG7YG3.cjs +153 -0
  14. package/dist/{chunk-3Q6Z6JTO.cjs → chunk-6VUQPOV3.cjs} +277 -86
  15. package/dist/chunk-7AK7B36B.cjs +44 -0
  16. package/dist/{chunk-5ZQBEOMD.js → chunk-AT7B7SOZ.js} +25 -2
  17. package/dist/{chunk-IK3KE3AU.js → chunk-DE2SRBFZ.js} +1 -1
  18. package/dist/chunk-DLXZEZWO.cjs +285 -0
  19. package/dist/{chunk-UHGVBFU3.js → chunk-EBLFCNPY.js} +38 -14
  20. package/dist/chunk-EJLH6NFT.cjs +166 -0
  21. package/dist/chunk-HVBBDBS3.cjs +350 -0
  22. package/dist/{chunk-46ZIFWYG.cjs → chunk-IIRPUVPH.cjs} +7 -1
  23. package/dist/{chunk-JWVTXXPX.cjs → chunk-KBM2W6UY.cjs} +8 -4
  24. package/dist/{chunk-PG7PS2NN.cjs → chunk-KCVJWJST.cjs} +14 -2
  25. package/dist/chunk-LB6EBS6Y.js +69 -0
  26. package/dist/{chunk-IWQAX3CS.js → chunk-LVUCAWMK.js} +1 -1
  27. package/dist/chunk-MKF46GNZ.js +350 -0
  28. package/dist/{chunk-HYN75H6G.cjs → chunk-MUHWHQVM.cjs} +1 -1
  29. package/dist/{chunk-MFZ5437A.js → chunk-N5T4M4GU.js} +1 -1
  30. package/dist/{chunk-BODMAL6G.js → chunk-OEWZUX4G.js} +262 -71
  31. package/dist/chunk-OF3BECU2.cjs +69 -0
  32. package/dist/{chunk-DDMRUL76.cjs → chunk-OKMJLEXX.cjs} +5 -5
  33. package/dist/chunk-OPRS4IQZ.js +153 -0
  34. package/dist/{chunk-D3TDKWRP.js → chunk-SIZQQD6T.js} +14 -2
  35. package/dist/{chunk-CGKLE6IW.cjs → chunk-SQYVORNP.cjs} +1219 -510
  36. package/dist/{chunk-6QZGDLL4.js → chunk-TOVPMMHM.js} +5 -1
  37. package/dist/{chunk-FH2P4OCD.js → chunk-UDCC7EMR.js} +976 -267
  38. package/dist/{chunk-ZLX3KGXX.js → chunk-UX7VWA46.js} +135 -23
  39. package/dist/{chunk-K52THW2C.cjs → chunk-V4TXMEJS.cjs} +5 -5
  40. package/dist/chunk-VRFBB5DB.js +44 -0
  41. package/dist/chunk-WB6PH56J.js +166 -0
  42. package/dist/{chunk-G2UTNSGU.js → chunk-WNKWAEUR.js} +7 -1
  43. package/dist/{chunk-SUEGJ75D.cjs → chunk-WYTEQ4Q5.cjs} +25 -2
  44. package/dist/{chunk-5ENKBSWO.js → chunk-YJVHCHQX.js} +42 -6
  45. package/dist/{chunk-VQY5W4YW.cjs → chunk-YK57NMIF.cjs} +44 -8
  46. package/dist/{chunk-KFI44N2R.cjs → chunk-YNJNTCSQ.cjs} +10 -1
  47. package/dist/completion/ApiKeyPoolService.cjs +12 -0
  48. package/dist/completion/ApiKeyPoolService.d.cts +6 -0
  49. package/dist/completion/ApiKeyPoolService.d.ts +6 -0
  50. package/dist/completion/ApiKeyPoolService.js +12 -0
  51. package/dist/completion/CompletionService.cjs +25 -23
  52. package/dist/completion/CompletionService.d.cts +2 -1
  53. package/dist/completion/CompletionService.d.ts +2 -1
  54. package/dist/completion/CompletionService.js +24 -22
  55. package/dist/completion.cjs +25 -23
  56. package/dist/completion.d.cts +2 -1
  57. package/dist/completion.d.ts +2 -1
  58. package/dist/completion.js +24 -22
  59. package/dist/index.cjs +49 -26
  60. package/dist/index.d.cts +8 -8
  61. package/dist/index.d.ts +8 -8
  62. package/dist/index.js +52 -29
  63. package/dist/{keyPolicy-Cp0FntZl.d.ts → keyPolicy-zdWTu3CQ.d.cts} +1 -1
  64. package/dist/{keyPolicy-Cp0FntZl.d.cts → keyPolicy-zdWTu3CQ.d.ts} +1 -1
  65. package/dist/outbound-api/quotaWarn.d.cts +1 -1
  66. package/dist/outbound-api/quotaWarn.d.ts +1 -1
  67. package/dist/outbound-api/routeResolver.cjs +2 -2
  68. package/dist/outbound-api/routeResolver.d.cts +4 -3
  69. package/dist/outbound-api/routeResolver.d.ts +4 -3
  70. package/dist/outbound-api/routeResolver.js +1 -1
  71. package/dist/outbound-api/subscriptionRegistryPort.d.cts +1 -0
  72. package/dist/outbound-api/subscriptionRegistryPort.d.ts +1 -0
  73. package/dist/outbound-api/types.d.cts +3 -2
  74. package/dist/outbound-api/types.d.ts +3 -2
  75. package/dist/outbound-api.cjs +47 -24
  76. package/dist/outbound-api.d.cts +127 -85
  77. package/dist/outbound-api.d.ts +127 -85
  78. package/dist/outbound-api.js +50 -27
  79. package/dist/pipeline/AccountAllowanceScheduling.cjs +15 -0
  80. package/dist/pipeline/AccountAllowanceScheduling.d.cts +90 -0
  81. package/dist/pipeline/AccountAllowanceScheduling.d.ts +90 -0
  82. package/dist/pipeline/AccountAllowanceScheduling.js +15 -0
  83. package/dist/pipeline/AccountAllowanceStore.cjs +20 -0
  84. package/dist/pipeline/AccountAllowanceStore.d.cts +77 -0
  85. package/dist/pipeline/AccountAllowanceStore.d.ts +77 -0
  86. package/dist/pipeline/AccountAllowanceStore.js +20 -0
  87. package/dist/pipeline/AuthSource.d.cts +11 -2
  88. package/dist/pipeline/AuthSource.d.ts +11 -2
  89. package/dist/pipeline/BoundAccountSelectionError.cjs +10 -0
  90. package/dist/pipeline/BoundAccountSelectionError.d.cts +28 -0
  91. package/dist/pipeline/BoundAccountSelectionError.d.ts +28 -0
  92. package/dist/pipeline/BoundAccountSelectionError.js +10 -0
  93. package/dist/pipeline/LlmConfigProviderAuth.d.cts +1 -0
  94. package/dist/pipeline/LlmConfigProviderAuth.d.ts +1 -0
  95. package/dist/pipeline/SubscriptionAccountHealth.cjs +4 -2
  96. package/dist/pipeline/SubscriptionAccountHealth.d.cts +34 -6
  97. package/dist/pipeline/SubscriptionAccountHealth.d.ts +34 -6
  98. package/dist/pipeline/SubscriptionAccountHealth.js +3 -1
  99. package/dist/pipeline/SubscriptionAuthSource.cjs +5 -2
  100. package/dist/pipeline/SubscriptionAuthSource.d.cts +1 -0
  101. package/dist/pipeline/SubscriptionAuthSource.d.ts +1 -0
  102. package/dist/pipeline/SubscriptionAuthSource.js +4 -1
  103. package/dist/pipeline/SubscriptionAuthStrategy.d.cts +8 -3
  104. package/dist/pipeline/SubscriptionAuthStrategy.d.ts +8 -3
  105. package/dist/pipeline/executeProviderCall.cjs +2 -2
  106. package/dist/pipeline/executeProviderCall.d.cts +8 -3
  107. package/dist/pipeline/executeProviderCall.d.ts +8 -3
  108. package/dist/pipeline/executeProviderCall.js +1 -1
  109. package/dist/pipeline/resolveProviderChain.d.cts +2 -3
  110. package/dist/pipeline/resolveProviderChain.d.ts +2 -3
  111. package/dist/pipeline/resolveSubscriptionChain.d.cts +1 -0
  112. package/dist/pipeline/resolveSubscriptionChain.d.ts +1 -0
  113. package/dist/pipeline/upstreamFetch.cjs +3 -2
  114. package/dist/pipeline/upstreamFetch.js +2 -1
  115. package/dist/ports/pricing-store.d.cts +4 -3
  116. package/dist/ports/pricing-store.d.ts +4 -3
  117. package/dist/ports.d.cts +4 -3
  118. package/dist/ports.d.ts +4 -3
  119. package/dist/provider-proxy/ProviderProxy.cjs +25 -23
  120. package/dist/provider-proxy/ProviderProxy.d.cts +1 -0
  121. package/dist/provider-proxy/ProviderProxy.d.ts +1 -0
  122. package/dist/provider-proxy/ProviderProxy.js +24 -22
  123. package/dist/provider-proxy/ingress/providerProxyShared.cjs +27 -23
  124. package/dist/provider-proxy/ingress/providerProxyShared.d.cts +34 -5
  125. package/dist/provider-proxy/ingress/providerProxyShared.d.ts +34 -5
  126. package/dist/provider-proxy/ingress/providerProxyShared.js +26 -22
  127. package/dist/provider-proxy/matchText.cjs +4 -2
  128. package/dist/provider-proxy/matchText.d.cts +41 -6
  129. package/dist/provider-proxy/matchText.d.ts +41 -6
  130. package/dist/provider-proxy/matchText.js +3 -1
  131. package/dist/provider-proxy/types.d.cts +30 -2
  132. package/dist/provider-proxy/types.d.ts +30 -2
  133. package/dist/provider-proxy.cjs +25 -23
  134. package/dist/provider-proxy.d.cts +1 -0
  135. package/dist/provider-proxy.d.ts +1 -0
  136. package/dist/provider-proxy.js +24 -22
  137. package/dist/{routeResolver-C4T7yClx.d.ts → routeResolver-CHTv_Bs5.d.ts} +1 -1
  138. package/dist/{routeResolver-CB3tvJKy.d.cts → routeResolver-kepxnfcC.d.cts} +1 -1
  139. package/dist/transformer/TransformerChainExecutor.cjs +2 -2
  140. package/dist/transformer/TransformerChainExecutor.js +1 -1
  141. package/dist/transformer/transformers/AnthropicTransformer.cjs +2 -2
  142. package/dist/transformer/transformers/AnthropicTransformer.js +1 -1
  143. package/dist/transformer/transformers/GeminiCodeAssistTransformer.cjs +3 -3
  144. package/dist/transformer/transformers/GeminiCodeAssistTransformer.js +2 -2
  145. package/dist/transformer/transformers/GeminiTransformer.cjs +3 -3
  146. package/dist/transformer/transformers/GeminiTransformer.js +2 -2
  147. package/dist/transformer/transformers/OpenAIResponseTransformer.cjs +2 -2
  148. package/dist/transformer/transformers/OpenAIResponseTransformer.d.cts +1 -1
  149. package/dist/transformer/transformers/OpenAIResponseTransformer.d.ts +1 -1
  150. package/dist/transformer/transformers/OpenAIResponseTransformer.js +1 -1
  151. package/dist/transformer/transformers/OpenAITransformer.cjs +6 -0
  152. package/dist/transformer/transformers/OpenAITransformer.d.cts +68 -0
  153. package/dist/transformer/transformers/OpenAITransformer.d.ts +68 -0
  154. package/dist/transformer/transformers/OpenAITransformer.js +6 -0
  155. package/dist/transformer/transformers.cjs +8 -14
  156. package/dist/transformer/transformers.d.cts +12 -48
  157. package/dist/transformer/transformers.d.ts +12 -48
  158. package/dist/transformer/transformers.js +12 -18
  159. package/dist/transformer/types.d.cts +17 -0
  160. package/dist/transformer/types.d.ts +17 -0
  161. package/dist/transformer.cjs +10 -11
  162. package/dist/transformer.d.cts +1 -2
  163. package/dist/transformer.d.ts +1 -2
  164. package/dist/transformer.js +9 -10
  165. package/dist/{types-B8Arpxn0.d.cts → types-H3wGZSOo.d.cts} +134 -7
  166. package/dist/{types-lgPYQuZS.d.ts → types-hsoKrVQS.d.ts} +134 -7
  167. package/dist/usage/pricing-engine.cjs +2 -2
  168. package/dist/usage/pricing-engine.d.cts +9 -1
  169. package/dist/usage/pricing-engine.d.ts +9 -1
  170. package/dist/usage/pricing-engine.js +1 -1
  171. package/dist/usage.cjs +2 -2
  172. package/dist/usage.js +1 -1
  173. package/package.json +1 -1
  174. package/dist/chunk-7FEBOPJO.cjs +0 -50
  175. package/dist/chunk-DWKQZ2SK.cjs +0 -254
  176. package/dist/chunk-IAXWICZU.js +0 -69
  177. package/dist/chunk-IB3FDI4O.js +0 -50
  178. package/dist/chunk-OQWKZCXP.js +0 -242
  179. package/dist/chunk-QWBWGODS.cjs +0 -69
  180. package/dist/chunk-YMFAJFQT.cjs +0 -173
  181. package/dist/chunk-ZCIQ3L3R.cjs +0 -242
  182. package/dist/chunk-ZJZAOKI7.js +0 -254
  183. package/dist/transformer/transformers/OpenCodeGoTransformer.cjs +0 -6
  184. package/dist/transformer/transformers/OpenCodeGoTransformer.d.cts +0 -51
  185. package/dist/transformer/transformers/OpenCodeGoTransformer.d.ts +0 -51
  186. package/dist/transformer/transformers/OpenCodeGoTransformer.js +0 -6
  187. package/dist/transformer/transformers/ReasoningTransformer.cjs +0 -6
  188. package/dist/transformer/transformers/ReasoningTransformer.d.cts +0 -47
  189. package/dist/transformer/transformers/ReasoningTransformer.d.ts +0 -47
  190. 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-G2UTNSGU.js";
11
+ } from "./chunk-WNKWAEUR.js";
7
12
  import {
8
- AnthropicTransformer
9
- } from "./chunk-C6OV7GCC.js";
13
+ injectExtendedContextBeta
14
+ } from "./chunk-MZNPGW5Q.js";
10
15
  import {
11
16
  GeminiTransformer
12
- } from "./chunk-IWQAX3CS.js";
17
+ } from "./chunk-LVUCAWMK.js";
13
18
  import {
14
19
  OpenAIResponseTransformer
15
- } from "./chunk-BODMAL6G.js";
20
+ } from "./chunk-OEWZUX4G.js";
16
21
  import {
17
- injectExtendedContextBeta
18
- } from "./chunk-MZNPGW5Q.js";
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
- collectMatchText,
28
- deriveSubscriptionSessionKey
29
- } from "./chunk-IAXWICZU.js";
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
- getGeminiCodeAssistResolver
35
- } from "./chunk-DRWHTIIZ.js";
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-IK3KE3AU.js";
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
- resolveRoute,
74
- validateServerModelConfig
75
- } from "./chunk-UHGVBFU3.js";
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-5ZQBEOMD.js";
97
+ } from "./chunk-AT7B7SOZ.js";
88
98
  import {
89
99
  fetchUpstream
90
- } from "./chunk-D3TDKWRP.js";
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
- const tapped = readAnthropicUsage(parsed.usage);
493
- if (!tapped) return;
494
- recorder.record({
495
- messageId: null,
496
- parentMessageId: null,
497
- sessionId: attribution.sessionId,
498
- providerId: attribution.providerId,
499
- model: attribution.model,
500
- apiKeyId: attribution.apiKeyId,
501
- engineOrigin: "codex-ingress",
502
- usage: tapped,
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
- async function relayStreamWithModelRewrite(res, body, rewriteModel) {
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
- out += rewriteSseLine(linePart, rewriteModel);
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) res.write(rewriteSseLine(tail, rewriteModel));
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
- if (isStream || contentType.includes("text/event-stream")) {
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
- await relayStreamWithModelRewrite(res, providerResponse.body, rewriteModel);
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(deps, providerId, provider, route.sessionId);
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: hints.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
- const tapped = readGeminiUsage(parsed.usageMetadata);
1316
- if (!tapped) return;
1317
- recorder.record({
1318
- messageId: null,
1319
- parentMessageId: null,
1320
- sessionId: attribution.sessionId,
1321
- providerId: attribution.providerId,
1322
- model: attribution.model,
1323
- apiKeyId: attribution.apiKeyId,
1324
- auditResponse: attribution.auditResponse,
1325
- engineOrigin: "codex-ingress",
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 bodyText = await relayResponse(res, providerResponse.response, isStream);
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(deps, providerId, provider, route.sessionId);
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
- const tapped = readChatCompletionsUsage(parsed.usage);
1501
- if (!tapped) return;
1502
- recorder.record({
1503
- messageId: null,
1504
- parentMessageId: null,
1505
- sessionId: attribution.sessionId,
1506
- providerId: attribution.providerId,
1507
- model: attribution.model,
1508
- apiKeyId: attribution.apiKeyId,
1509
- auditResponse: attribution.auditResponse,
1510
- engineOrigin: "codex-ingress",
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 bodyText = await relayResponse(res, providerResponse.response, isStream);
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(deps, providerId, provider, route.sessionId);
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
- const tapped = readResponsesUsage(parsed.usage);
1735
- if (!tapped) return;
1736
- recorder.record({
1737
- messageId: null,
1738
- parentMessageId: null,
1739
- sessionId: attribution.sessionId,
1740
- providerId: attribution.providerId,
1741
- model: attribution.model,
1742
- apiKeyId: attribution.apiKeyId,
1743
- auditResponse: attribution.auditResponse,
1744
- engineOrigin: "codex-ingress",
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 bodyText = await relayResponse(res, providerResponse.response, isStream, route.requestedModel);
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(deps, providerId, provider, route.sessionId);
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) config.boundAccountId = 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
- (ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep)
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 = { id: row.id };
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 writeChatModelsList(res, config) {
3341
- const chat = config.endpoints.find((e) => e.endpoint === "chat");
3342
- const data = (chat?.models ?? []).map((ref) => parseModelRef(ref)).filter((p) => p !== null).map((p) => ({ id: p.modelId, object: "model", owned_by: "omnicross" }));
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
- writeChatModelsList(res, config);
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
- const endpointConfig = config.endpoints.find((e) => e.endpoint === endpoint);
3407
- if (!endpointConfig) {
3408
- writeJsonError(res, 503, `endpoint '${endpoint}' is not configured`);
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 sessionId = deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}` : null;
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: endpointConfig,
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: extractRequestedModel(ingressFormat, parsedBody),
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: endpointConfig,
3495
- role: detectRequestRole(ingressFormat, parsedBody, {
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 || 16384,
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 || 16384,
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 || 16384;
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, getProvider, logger, providerId, modelId, sessionMaxTokens) {
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
- max_tokens: options.maxTokens || 4096,
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 || 4096,
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
- model: actualModel,
5227
- messages: options.messages.map((m) => ({
5228
- role: m.role,
5229
- content: m.content
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. Model's maxTokens from provider config
5909
- * 4. Discovered models cache (from API)
5910
- * 5. undefined - let API use its default
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,