@omnicross/core 0.1.4 → 0.1.6

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