@omnicross/core 0.1.5 → 0.1.7

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 (231) 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 +8 -6
  6. package/dist/auth/GeminiCodeAssistProjectResolver.js +6 -4
  7. package/dist/{chunk-3Z5BQZFM.cjs → chunk-235MXAGX.cjs} +3 -1
  8. package/dist/{chunk-HRRYG2AX.js → chunk-23S5OPIO.js} +10 -1
  9. package/dist/chunk-25GXJCEZ.cjs +40 -0
  10. package/dist/chunk-2E42SAW3.cjs +323 -0
  11. package/dist/chunk-2JYRIADS.js +40 -0
  12. package/dist/chunk-2Z4XM3H5.cjs +44 -0
  13. package/dist/{chunk-BODMAL6G.js → chunk-32O35CIF.js} +271 -91
  14. package/dist/chunk-3SCOLIPT.cjs +63 -0
  15. package/dist/{chunk-SYQF7OIR.js → chunk-3WD4HTR2.js} +2 -0
  16. package/dist/{chunk-CGKLE6IW.cjs → chunk-4BB343BI.cjs} +1489 -520
  17. package/dist/{chunk-VACWOIRE.cjs → chunk-4VZX5F4T.cjs} +39 -15
  18. package/dist/{chunk-5ZQBEOMD.js → chunk-5NJLQ6T6.js} +46 -12
  19. package/dist/chunk-5RSZYQJH.cjs +62 -0
  20. package/dist/chunk-6QQG7YG3.cjs +153 -0
  21. package/dist/{chunk-IFXJ2BG7.js → chunk-7C7DET6S.js} +51 -6
  22. package/dist/{chunk-IK3KE3AU.js → chunk-DE2SRBFZ.js} +1 -1
  23. package/dist/{chunk-FH2P4OCD.js → chunk-E3COOKUP.js} +1240 -271
  24. package/dist/{chunk-UHGVBFU3.js → chunk-EBLFCNPY.js} +38 -14
  25. package/dist/chunk-FU7EAR73.js +62 -0
  26. package/dist/chunk-FXR6N3SR.js +323 -0
  27. package/dist/{chunk-SUEGJ75D.cjs → chunk-GDIXXKF2.cjs} +46 -12
  28. package/dist/chunk-GFIZ6LQI.js +170 -0
  29. package/dist/chunk-GL6XM527.js +61 -0
  30. package/dist/chunk-HVBBDBS3.cjs +350 -0
  31. package/dist/{chunk-46ZIFWYG.cjs → chunk-IIRPUVPH.cjs} +7 -1
  32. package/dist/chunk-J3QQ6JS5.cjs +170 -0
  33. package/dist/{chunk-JWVTXXPX.cjs → chunk-KBM2W6UY.cjs} +8 -4
  34. package/dist/chunk-LB6EBS6Y.js +69 -0
  35. package/dist/{chunk-IWQAX3CS.js → chunk-LVUCAWMK.js} +1 -1
  36. package/dist/chunk-MKF46GNZ.js +350 -0
  37. package/dist/{chunk-HYN75H6G.cjs → chunk-MUHWHQVM.cjs} +1 -1
  38. package/dist/chunk-MYYR3HZL.js +44 -0
  39. package/dist/{chunk-MFZ5437A.js → chunk-N5T4M4GU.js} +1 -1
  40. package/dist/{chunk-5ENKBSWO.js → chunk-N7OOJI5V.js} +88 -8
  41. package/dist/chunk-NKVZ6Z3Y.cjs +61 -0
  42. package/dist/chunk-OF3BECU2.cjs +69 -0
  43. package/dist/{chunk-SNYBEXLB.cjs → chunk-OJGQKHN5.cjs} +3 -1
  44. package/dist/{chunk-DDMRUL76.cjs → chunk-OKMJLEXX.cjs} +5 -5
  45. package/dist/chunk-OPRS4IQZ.js +153 -0
  46. package/dist/chunk-OTZC4TNH.cjs +79 -0
  47. package/dist/{chunk-3Q6Z6JTO.cjs → chunk-OVQBNVSV.cjs} +287 -107
  48. package/dist/{chunk-4XMYPW46.cjs → chunk-OXHNSZ53.cjs} +84 -44
  49. package/dist/{chunk-C6OV7GCC.js → chunk-PEGTIPB3.js} +52 -12
  50. package/dist/chunk-PO3XB56Z.js +79 -0
  51. package/dist/{chunk-D3TDKWRP.js → chunk-RJ5ZGNI3.js} +54 -5
  52. package/dist/{chunk-PG7PS2NN.cjs → chunk-RJUONLUQ.cjs} +59 -10
  53. package/dist/{chunk-BPKCU575.js → chunk-SOO562NM.js} +3 -1
  54. package/dist/{chunk-6QZGDLL4.js → chunk-TOVPMMHM.js} +5 -1
  55. package/dist/{chunk-K52THW2C.cjs → chunk-V4TXMEJS.cjs} +5 -5
  56. package/dist/{chunk-G2UTNSGU.js → chunk-WNKWAEUR.js} +7 -1
  57. package/dist/{chunk-UNWKG6EI.cjs → chunk-XPIOJQS2.cjs} +51 -6
  58. package/dist/chunk-XXFYLL7R.js +63 -0
  59. package/dist/{chunk-KFI44N2R.cjs → chunk-YNJNTCSQ.cjs} +10 -1
  60. package/dist/{chunk-VQY5W4YW.cjs → chunk-YWL7YZWS.cjs} +90 -10
  61. package/dist/completion/ApiKeyPoolService.cjs +12 -0
  62. package/dist/completion/ApiKeyPoolService.d.cts +6 -0
  63. package/dist/completion/ApiKeyPoolService.d.ts +6 -0
  64. package/dist/completion/ApiKeyPoolService.js +12 -0
  65. package/dist/completion/CompletionService.cjs +30 -24
  66. package/dist/completion/CompletionService.d.cts +3 -1
  67. package/dist/completion/CompletionService.d.ts +3 -1
  68. package/dist/completion/CompletionService.js +29 -23
  69. package/dist/completion.cjs +32 -26
  70. package/dist/completion.d.cts +3 -1
  71. package/dist/completion.d.ts +3 -1
  72. package/dist/completion.js +31 -25
  73. package/dist/index.cjs +56 -29
  74. package/dist/index.d.cts +8 -8
  75. package/dist/index.d.ts +8 -8
  76. package/dist/index.js +61 -34
  77. package/dist/{keyPolicy-Cp0FntZl.d.cts → keyPolicy-zdWTu3CQ.d.cts} +1 -1
  78. package/dist/{keyPolicy-Cp0FntZl.d.ts → keyPolicy-zdWTu3CQ.d.ts} +1 -1
  79. package/dist/outbound-api/auditCapture.cjs +2 -2
  80. package/dist/outbound-api/auditCapture.d.cts +3 -4
  81. package/dist/outbound-api/auditCapture.d.ts +3 -4
  82. package/dist/outbound-api/auditCapture.js +1 -1
  83. package/dist/outbound-api/quotaWarn.d.cts +1 -1
  84. package/dist/outbound-api/quotaWarn.d.ts +1 -1
  85. package/dist/outbound-api/routeResolver.cjs +2 -2
  86. package/dist/outbound-api/routeResolver.d.cts +5 -3
  87. package/dist/outbound-api/routeResolver.d.ts +5 -3
  88. package/dist/outbound-api/routeResolver.js +1 -1
  89. package/dist/outbound-api/subscriptionRegistryPort.d.cts +2 -0
  90. package/dist/outbound-api/subscriptionRegistryPort.d.ts +2 -0
  91. package/dist/outbound-api/types.d.cts +4 -2
  92. package/dist/outbound-api/types.d.ts +4 -2
  93. package/dist/outbound-api.cjs +52 -25
  94. package/dist/outbound-api.d.cts +131 -87
  95. package/dist/outbound-api.d.ts +131 -87
  96. package/dist/outbound-api.js +55 -28
  97. package/dist/pipeline/AccountAllowanceScheduling.cjs +15 -0
  98. package/dist/pipeline/AccountAllowanceScheduling.d.cts +91 -0
  99. package/dist/pipeline/AccountAllowanceScheduling.d.ts +91 -0
  100. package/dist/pipeline/AccountAllowanceScheduling.js +15 -0
  101. package/dist/pipeline/AccountAllowanceStore.cjs +20 -0
  102. package/dist/pipeline/AccountAllowanceStore.d.cts +77 -0
  103. package/dist/pipeline/AccountAllowanceStore.d.ts +77 -0
  104. package/dist/pipeline/AccountAllowanceStore.js +20 -0
  105. package/dist/pipeline/AccountRouteActivity.cjs +10 -0
  106. package/dist/pipeline/AccountRouteActivity.d.cts +61 -0
  107. package/dist/pipeline/AccountRouteActivity.d.ts +61 -0
  108. package/dist/pipeline/AccountRouteActivity.js +10 -0
  109. package/dist/pipeline/AuthSource.d.cts +11 -2
  110. package/dist/pipeline/AuthSource.d.ts +11 -2
  111. package/dist/pipeline/BoundAccountSelectionError.cjs +10 -0
  112. package/dist/pipeline/BoundAccountSelectionError.d.cts +28 -0
  113. package/dist/pipeline/BoundAccountSelectionError.d.ts +28 -0
  114. package/dist/pipeline/BoundAccountSelectionError.js +10 -0
  115. package/dist/pipeline/LlmConfigProviderAuth.d.cts +1 -0
  116. package/dist/pipeline/LlmConfigProviderAuth.d.ts +1 -0
  117. package/dist/pipeline/ServerOverloadCounter.cjs +16 -0
  118. package/dist/pipeline/ServerOverloadCounter.d.cts +77 -0
  119. package/dist/pipeline/ServerOverloadCounter.d.ts +77 -0
  120. package/dist/pipeline/ServerOverloadCounter.js +16 -0
  121. package/dist/pipeline/SubscriptionAccountHealth.cjs +6 -2
  122. package/dist/pipeline/SubscriptionAccountHealth.d.cts +65 -8
  123. package/dist/pipeline/SubscriptionAccountHealth.d.ts +65 -8
  124. package/dist/pipeline/SubscriptionAccountHealth.js +5 -1
  125. package/dist/pipeline/SubscriptionAuthSource.cjs +5 -2
  126. package/dist/pipeline/SubscriptionAuthSource.d.cts +1 -0
  127. package/dist/pipeline/SubscriptionAuthSource.d.ts +1 -0
  128. package/dist/pipeline/SubscriptionAuthSource.js +4 -1
  129. package/dist/pipeline/SubscriptionAuthStrategy.d.cts +8 -3
  130. package/dist/pipeline/SubscriptionAuthStrategy.d.ts +8 -3
  131. package/dist/pipeline/executeProviderCall.cjs +2 -2
  132. package/dist/pipeline/executeProviderCall.d.cts +8 -3
  133. package/dist/pipeline/executeProviderCall.d.ts +8 -3
  134. package/dist/pipeline/executeProviderCall.js +1 -1
  135. package/dist/pipeline/resolveProviderChain.d.cts +2 -3
  136. package/dist/pipeline/resolveProviderChain.d.ts +2 -3
  137. package/dist/pipeline/resolveSubscriptionChain.d.cts +1 -0
  138. package/dist/pipeline/resolveSubscriptionChain.d.ts +1 -0
  139. package/dist/pipeline/upstreamFetch.cjs +5 -3
  140. package/dist/pipeline/upstreamFetch.d.cts +35 -6
  141. package/dist/pipeline/upstreamFetch.d.ts +35 -6
  142. package/dist/pipeline/upstreamFetch.js +4 -2
  143. package/dist/pipeline/upstreamTrace.cjs +4 -2
  144. package/dist/pipeline/upstreamTrace.d.cts +22 -8
  145. package/dist/pipeline/upstreamTrace.d.ts +22 -8
  146. package/dist/pipeline/upstreamTrace.js +3 -1
  147. package/dist/ports/pricing-store.d.cts +4 -3
  148. package/dist/ports/pricing-store.d.ts +4 -3
  149. package/dist/ports.d.cts +4 -3
  150. package/dist/ports.d.ts +4 -3
  151. package/dist/provider-proxy/ProviderProxy.cjs +30 -24
  152. package/dist/provider-proxy/ProviderProxy.d.cts +2 -0
  153. package/dist/provider-proxy/ProviderProxy.d.ts +2 -0
  154. package/dist/provider-proxy/ProviderProxy.js +29 -23
  155. package/dist/provider-proxy/identity/codexCliHeaders.cjs +10 -0
  156. package/dist/provider-proxy/identity/codexCliHeaders.d.cts +45 -0
  157. package/dist/provider-proxy/identity/codexCliHeaders.d.ts +45 -0
  158. package/dist/provider-proxy/identity/codexCliHeaders.js +10 -0
  159. package/dist/provider-proxy/ingress/providerProxyShared.cjs +32 -24
  160. package/dist/provider-proxy/ingress/providerProxyShared.d.cts +44 -5
  161. package/dist/provider-proxy/ingress/providerProxyShared.d.ts +44 -5
  162. package/dist/provider-proxy/ingress/providerProxyShared.js +31 -23
  163. package/dist/provider-proxy/matchText.cjs +4 -2
  164. package/dist/provider-proxy/matchText.d.cts +39 -6
  165. package/dist/provider-proxy/matchText.d.ts +39 -6
  166. package/dist/provider-proxy/matchText.js +3 -1
  167. package/dist/provider-proxy/types.d.cts +35 -2
  168. package/dist/provider-proxy/types.d.ts +35 -2
  169. package/dist/provider-proxy.cjs +30 -24
  170. package/dist/provider-proxy.d.cts +2 -0
  171. package/dist/provider-proxy.d.ts +2 -0
  172. package/dist/provider-proxy.js +29 -23
  173. package/dist/{routeResolver-C4T7yClx.d.ts → routeResolver-BxOfunbm.d.ts} +1 -1
  174. package/dist/{routeResolver-CB3tvJKy.d.cts → routeResolver-D5OzD0iM.d.cts} +1 -1
  175. package/dist/transformer/TransformerChainExecutor.cjs +2 -2
  176. package/dist/transformer/TransformerChainExecutor.js +1 -1
  177. package/dist/transformer/transformers/AnthropicTransformer.cjs +3 -2
  178. package/dist/transformer/transformers/AnthropicTransformer.js +2 -1
  179. package/dist/transformer/transformers/GeminiCodeAssistTransformer.cjs +3 -3
  180. package/dist/transformer/transformers/GeminiCodeAssistTransformer.js +2 -2
  181. package/dist/transformer/transformers/GeminiTransformer.cjs +3 -3
  182. package/dist/transformer/transformers/GeminiTransformer.js +2 -2
  183. package/dist/transformer/transformers/OpenAIResponseTransformer.cjs +3 -2
  184. package/dist/transformer/transformers/OpenAIResponseTransformer.d.cts +1 -1
  185. package/dist/transformer/transformers/OpenAIResponseTransformer.d.ts +1 -1
  186. package/dist/transformer/transformers/OpenAIResponseTransformer.js +2 -1
  187. package/dist/transformer/transformers/OpenAITransformer.cjs +6 -0
  188. package/dist/transformer/transformers/OpenAITransformer.d.cts +68 -0
  189. package/dist/transformer/transformers/OpenAITransformer.d.ts +68 -0
  190. package/dist/transformer/transformers/OpenAITransformer.js +6 -0
  191. package/dist/transformer/transformers.cjs +9 -14
  192. package/dist/transformer/transformers.d.cts +12 -48
  193. package/dist/transformer/transformers.d.ts +12 -48
  194. package/dist/transformer/transformers.js +11 -16
  195. package/dist/transformer/types.d.cts +17 -0
  196. package/dist/transformer/types.d.ts +17 -0
  197. package/dist/transformer.cjs +12 -12
  198. package/dist/transformer.d.cts +1 -2
  199. package/dist/transformer.d.ts +1 -2
  200. package/dist/transformer.js +12 -12
  201. package/dist/{types-lgPYQuZS.d.ts → types-CqZD77rS.d.ts} +173 -7
  202. package/dist/{types-B8Arpxn0.d.cts → types-gjJ5lJ5n.d.cts} +173 -7
  203. package/dist/usage/pricing-engine.cjs +2 -2
  204. package/dist/usage/pricing-engine.d.cts +29 -1
  205. package/dist/usage/pricing-engine.d.ts +29 -1
  206. package/dist/usage/pricing-engine.js +1 -1
  207. package/dist/usage/usage-recorder.cjs +2 -2
  208. package/dist/usage/usage-recorder.d.cts +5 -1
  209. package/dist/usage/usage-recorder.d.ts +5 -1
  210. package/dist/usage/usage-recorder.js +1 -1
  211. package/dist/usage.cjs +3 -3
  212. package/dist/usage.js +2 -2
  213. package/package.json +1 -1
  214. package/dist/chunk-7FEBOPJO.cjs +0 -50
  215. package/dist/chunk-DWKQZ2SK.cjs +0 -254
  216. package/dist/chunk-IAXWICZU.js +0 -69
  217. package/dist/chunk-IB3FDI4O.js +0 -50
  218. package/dist/chunk-OQWKZCXP.js +0 -242
  219. package/dist/chunk-QWBWGODS.cjs +0 -69
  220. package/dist/chunk-YMFAJFQT.cjs +0 -173
  221. package/dist/chunk-ZCIQ3L3R.cjs +0 -242
  222. package/dist/chunk-ZJZAOKI7.js +0 -254
  223. package/dist/chunk-ZLX3KGXX.js +0 -173
  224. package/dist/transformer/transformers/OpenCodeGoTransformer.cjs +0 -6
  225. package/dist/transformer/transformers/OpenCodeGoTransformer.d.cts +0 -51
  226. package/dist/transformer/transformers/OpenCodeGoTransformer.d.ts +0 -51
  227. package/dist/transformer/transformers/OpenCodeGoTransformer.js +0 -6
  228. package/dist/transformer/transformers/ReasoningTransformer.cjs +0 -6
  229. package/dist/transformer/transformers/ReasoningTransformer.d.cts +0 -47
  230. package/dist/transformer/transformers/ReasoningTransformer.d.ts +0 -47
  231. package/dist/transformer/transformers/ReasoningTransformer.js +0 -6
@@ -2,20 +2,29 @@ import {
2
2
  streamSSEResponse
3
3
  } from "./chunk-VVEHS2LI.js";
4
4
  import {
5
- TransformerChainExecutor
6
- } from "./chunk-G2UTNSGU.js";
7
- import {
8
- AnthropicTransformer
9
- } from "./chunk-C6OV7GCC.js";
5
+ AnthropicTransformer,
6
+ resolveAnthropicMaxTokens
7
+ } from "./chunk-PEGTIPB3.js";
10
8
  import {
11
9
  GeminiTransformer
12
- } from "./chunk-IWQAX3CS.js";
10
+ } from "./chunk-LVUCAWMK.js";
13
11
  import {
14
12
  OpenAIResponseTransformer
15
- } from "./chunk-BODMAL6G.js";
13
+ } from "./chunk-32O35CIF.js";
14
+ import {
15
+ TransformerChainExecutor
16
+ } from "./chunk-WNKWAEUR.js";
16
17
  import {
17
18
  injectExtendedContextBeta
18
19
  } from "./chunk-MZNPGW5Q.js";
20
+ import {
21
+ DEFAULT_CODEX_CLI_HEADERS,
22
+ NEVER_FORWARD_HEADERS,
23
+ codexAcceptHeader,
24
+ extractCodexClientHeaders,
25
+ fillMissingHeaders,
26
+ flattenHeaderValue
27
+ } from "./chunk-XXFYLL7R.js";
19
28
  import {
20
29
  getSharedIdentityStore
21
30
  } from "./chunk-XTEI64OU.js";
@@ -25,37 +34,48 @@ import {
25
34
  } from "./chunk-XX6NQJMA.js";
26
35
  import {
27
36
  collectMatchText,
37
+ deriveGatewaySessionKey,
28
38
  deriveSubscriptionSessionKey
29
- } from "./chunk-IAXWICZU.js";
39
+ } from "./chunk-GFIZ6LQI.js";
30
40
  import {
31
41
  emitWebhookEvent
32
42
  } from "./chunk-OE6TDIWW.js";
33
43
  import {
34
44
  getGeminiCodeAssistResolver
35
45
  } from "./chunk-DRWHTIIZ.js";
36
- import {
37
- executeProviderCall
38
- } from "./chunk-IK3KE3AU.js";
39
- import {
40
- LlmConfigProviderAuth,
41
- getProviderHeaders
42
- } from "./chunk-ZY6KU6P3.js";
43
46
  import {
44
47
  resolveProviderChain
45
48
  } from "./chunk-JWC6APFQ.js";
46
49
  import {
47
50
  resolveSubscriptionChain
48
51
  } from "./chunk-5ERIZW3D.js";
52
+ import {
53
+ getSharedOverloadCounter
54
+ } from "./chunk-PO3XB56Z.js";
49
55
  import {
50
56
  getSharedAccountHealth,
51
57
  resolveResetSeconds
52
- } from "./chunk-5ENKBSWO.js";
58
+ } from "./chunk-N7OOJI5V.js";
53
59
  import {
54
60
  SubscriptionAuthSource
55
- } from "./chunk-HRRYG2AX.js";
61
+ } from "./chunk-23S5OPIO.js";
56
62
  import {
57
63
  serializeError
58
64
  } from "./chunk-AVYJ4YBN.js";
65
+ import {
66
+ isAccountAllowanceExhaustedError
67
+ } from "./chunk-OPRS4IQZ.js";
68
+ import {
69
+ boundAccountSelectionMessage,
70
+ isBoundAccountSelectionError
71
+ } from "./chunk-2JYRIADS.js";
72
+ import {
73
+ executeProviderCall
74
+ } from "./chunk-DE2SRBFZ.js";
75
+ import {
76
+ LlmConfigProviderAuth,
77
+ getProviderHeaders
78
+ } from "./chunk-ZY6KU6P3.js";
59
79
  import {
60
80
  beginBillingCapture
61
81
  } from "./chunk-WLYIIEAF.js";
@@ -64,15 +84,16 @@ import {
64
84
  markQuotaWarnedOnce
65
85
  } from "./chunk-3QFXFCHG.js";
66
86
  import {
87
+ detectModelKind,
67
88
  detectRequestRole,
68
89
  endpointToIngressFormat,
69
90
  extractRequestedModel,
70
91
  isKindMappedEndpoint,
71
92
  modelKindsForEndpoint,
72
93
  parseModelRef,
73
- resolveRoute,
74
- validateServerModelConfig
75
- } from "./chunk-UHGVBFU3.js";
94
+ resolvePrefixTarget,
95
+ resolveRoute
96
+ } from "./chunk-EBLFCNPY.js";
76
97
  import {
77
98
  buildProviderApiUrl,
78
99
  resolveApiFormat,
@@ -80,14 +101,20 @@ import {
80
101
  } from "./chunk-H5JUT3KV.js";
81
102
  import {
82
103
  beginAuditCapture
83
- } from "./chunk-IFXJ2BG7.js";
104
+ } from "./chunk-7C7DET6S.js";
84
105
  import {
85
106
  convertAnthropicToOpenAI,
86
107
  convertOpenAIToAnthropic
87
- } from "./chunk-5ZQBEOMD.js";
108
+ } from "./chunk-5NJLQ6T6.js";
88
109
  import {
89
110
  fetchUpstream
90
- } from "./chunk-D3TDKWRP.js";
111
+ } from "./chunk-RJ5ZGNI3.js";
112
+ import {
113
+ getSharedAccountAllowanceStore
114
+ } from "./chunk-MKF46GNZ.js";
115
+ import {
116
+ getSharedAccountRouteActivity
117
+ } from "./chunk-GL6XM527.js";
91
118
  import {
92
119
  applyAugmentation
93
120
  } from "./chunk-Y7FO65VT.js";
@@ -469,6 +496,66 @@ var ProviderProxyRouteMap = class {
469
496
  }
470
497
  };
471
498
 
499
+ // src/provider-proxy/identity/claudeCodeHeaders.ts
500
+ var DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
501
+ var OAUTH_BETA = "oauth-2025-04-20";
502
+ var CLAUDE_CODE_BETA = "claude-code-20250219";
503
+ var INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
504
+ var TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
505
+ var DEFAULT_CLAUDE_CODE_HEADERS = Object.freeze({
506
+ "user-agent": "claude-cli/1.0.119 (external, cli)",
507
+ "x-app": "cli",
508
+ accept: "application/json",
509
+ "accept-language": "*",
510
+ "sec-fetch-mode": "cors",
511
+ "anthropic-dangerous-direct-browser-access": "true",
512
+ "x-stainless-lang": "js",
513
+ "x-stainless-package-version": "0.55.1",
514
+ "x-stainless-os": "Windows",
515
+ "x-stainless-arch": "x64",
516
+ "x-stainless-runtime": "node",
517
+ "x-stainless-runtime-version": "v20.19.2",
518
+ "x-stainless-retry-count": "0",
519
+ "x-stainless-timeout": "60"
520
+ });
521
+ var FORCED_ACCEPT_ENCODING = "identity";
522
+ var CLAUDE_FORWARD_ALLOWLIST = /* @__PURE__ */ new Set([
523
+ "accept",
524
+ "accept-language",
525
+ "sec-fetch-mode",
526
+ "user-agent",
527
+ "x-app",
528
+ "anthropic-dangerous-direct-browser-access",
529
+ "anthropic-version"
530
+ ]);
531
+ var STAINLESS_PREFIX = "x-stainless-";
532
+ function extractClaudeClientHeaders(reqHeaders) {
533
+ const out = {};
534
+ for (const [rawKey, rawVal] of Object.entries(reqHeaders)) {
535
+ const key = rawKey.toLowerCase();
536
+ if (NEVER_FORWARD_HEADERS.has(key)) continue;
537
+ if (!CLAUDE_FORWARD_ALLOWLIST.has(key) && !key.startsWith(STAINLESS_PREFIX)) continue;
538
+ const value = flattenHeaderValue(rawVal);
539
+ if (value !== void 0) out[key] = value;
540
+ }
541
+ return out;
542
+ }
543
+ function buildAnthropicBeta(model, callerBeta) {
544
+ const isHaiku = typeof model === "string" && model.toLowerCase().includes("haiku");
545
+ const baseline = isHaiku ? [OAUTH_BETA, INTERLEAVED_THINKING_BETA] : [CLAUDE_CODE_BETA, OAUTH_BETA, INTERLEAVED_THINKING_BETA, TOOL_STREAMING_BETA];
546
+ const seen = /* @__PURE__ */ new Set();
547
+ const out = [];
548
+ const add = (flag) => {
549
+ const trimmed = flag.trim();
550
+ if (!trimmed || seen.has(trimmed)) return;
551
+ seen.add(trimmed);
552
+ out.push(trimmed);
553
+ };
554
+ baseline.forEach(add);
555
+ if (callerBeta) callerBeta.split(",").forEach(add);
556
+ return out.join(",");
557
+ }
558
+
472
559
  // src/provider-proxy/usage/recordAnthropicUsage.ts
473
560
  function readAnthropicUsage(usage) {
474
561
  if (!usage) return null;
@@ -486,23 +573,36 @@ function readAnthropicUsage(usage) {
486
573
  reasoningTokens: 0
487
574
  };
488
575
  }
576
+ function emitAnthropicUsageRecord(recorder, rawUsage, attribution) {
577
+ const tapped = readAnthropicUsage(rawUsage);
578
+ if (!tapped) return;
579
+ recorder.record({
580
+ messageId: null,
581
+ parentMessageId: null,
582
+ sessionId: attribution.sessionId,
583
+ providerId: attribution.providerId,
584
+ model: attribution.model,
585
+ apiKeyId: attribution.apiKeyId,
586
+ auditResponse: attribution.auditResponse,
587
+ engineOrigin: "codex-ingress",
588
+ usage: tapped,
589
+ rawUsage
590
+ });
591
+ }
489
592
  function recordAnthropicNonStreamUsage(recorder, bodyText, attribution) {
490
593
  try {
491
594
  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
- });
595
+ emitAnthropicUsageRecord(
596
+ recorder,
597
+ parsed.usage,
598
+ attribution
599
+ );
600
+ } catch {
601
+ }
602
+ }
603
+ function recordAnthropicStreamUsage(recorder, rawUsage, attribution) {
604
+ try {
605
+ emitAnthropicUsageRecord(recorder, rawUsage, attribution);
506
606
  } catch {
507
607
  }
508
608
  }
@@ -590,7 +690,18 @@ function rewriteSseLine(line, rewriteModel) {
590
690
  return line;
591
691
  }
592
692
  }
593
- async function relayStreamWithModelRewrite(res, body, rewriteModel) {
693
+ function tapSseDataLine(line, onEvent) {
694
+ const leadingStripped = line.replace(/^\s+/, "");
695
+ if (!leadingStripped.startsWith("data:")) return;
696
+ const payload = leadingStripped.slice(5).trim();
697
+ if (!payload || payload === "[DONE]") return;
698
+ try {
699
+ onEvent(JSON.parse(payload));
700
+ } catch {
701
+ }
702
+ }
703
+ async function relaySseBody(res, body, options) {
704
+ const { rewriteModel, onSseEvent } = options;
594
705
  const reader = body.getReader();
595
706
  const decoder = new TextDecoder();
596
707
  let buffer = "";
@@ -604,21 +715,26 @@ async function relayStreamWithModelRewrite(res, body, rewriteModel) {
604
715
  while ((nlIndex = buffer.indexOf("\n")) !== -1) {
605
716
  const linePart = buffer.slice(0, nlIndex + 1);
606
717
  buffer = buffer.slice(nlIndex + 1);
607
- out += rewriteSseLine(linePart, rewriteModel);
718
+ if (onSseEvent) tapSseDataLine(linePart, onSseEvent);
719
+ out += rewriteModel ? rewriteSseLine(linePart, rewriteModel) : linePart;
608
720
  }
609
721
  if (out) res.write(out);
610
722
  }
611
723
  const tail = buffer + decoder.decode();
612
- if (tail) res.write(rewriteSseLine(tail, rewriteModel));
724
+ if (tail) {
725
+ if (onSseEvent) tapSseDataLine(tail, onSseEvent);
726
+ res.write(rewriteModel ? rewriteSseLine(tail, rewriteModel) : tail);
727
+ }
613
728
  } finally {
614
729
  reader.releaseLock();
615
730
  res.end();
616
731
  }
617
732
  }
618
- async function relayResponse(res, providerResponse, isStream, rewriteModel) {
733
+ async function relayResponse(res, providerResponse, isStream, rewriteModel, usageTap, onSseEvent) {
619
734
  const contentType = providerResponse.headers.get("Content-Type") ?? "";
620
735
  const status = providerResponse.status && providerResponse.status >= 100 ? providerResponse.status : 200;
621
- if (isStream || contentType.includes("text/event-stream")) {
736
+ const upstreamIsSse = contentType.includes("text/event-stream");
737
+ if (upstreamIsSse || isStream && status < 400) {
622
738
  res.writeHead(status, {
623
739
  "Content-Type": "text/event-stream",
624
740
  "Cache-Control": "no-cache",
@@ -628,8 +744,19 @@ async function relayResponse(res, providerResponse, isStream, rewriteModel) {
628
744
  res.end();
629
745
  return null;
630
746
  }
631
- if (rewriteModel) {
632
- await relayStreamWithModelRewrite(res, providerResponse.body, rewriteModel);
747
+ if (rewriteModel || usageTap || onSseEvent) {
748
+ let capturedUsage = null;
749
+ await relaySseBody(res, providerResponse.body, {
750
+ rewriteModel,
751
+ onSseEvent: onSseEvent || usageTap ? (event) => {
752
+ onSseEvent?.(event);
753
+ if (usageTap) {
754
+ const raw = usageTap.extractUsage(event);
755
+ if (raw) capturedUsage = raw;
756
+ }
757
+ } : void 0
758
+ });
759
+ if (capturedUsage) usageTap.onUsage(capturedUsage);
633
760
  return null;
634
761
  }
635
762
  const reader = providerResponse.body.getReader();
@@ -745,6 +872,27 @@ function writeError(res, status, message) {
745
872
  res.writeHead(status, { "Content-Type": "application/json" });
746
873
  res.end(JSON.stringify({ error: { type: "provider_proxy_error", message } }));
747
874
  }
875
+ function writeBoundAccountError(res, error) {
876
+ if (res.headersSent) return;
877
+ const headers = { "Content-Type": "application/json" };
878
+ if (error.status === 429 && error.resumeAt) {
879
+ const resumeMs = Date.parse(error.resumeAt);
880
+ if (Number.isFinite(resumeMs)) {
881
+ headers["Retry-After"] = String(Math.max(1, Math.ceil((resumeMs - Date.now()) / 1e3)));
882
+ }
883
+ }
884
+ res.writeHead(error.status, headers);
885
+ res.end(
886
+ JSON.stringify({
887
+ error: {
888
+ type: "provider_proxy_error",
889
+ code: error.code,
890
+ reason: error.reason,
891
+ message: boundAccountSelectionMessage(error.reason)
892
+ }
893
+ })
894
+ );
895
+ }
748
896
  function resolveApiKey(apiKey) {
749
897
  if (!apiKey) return "";
750
898
  if (apiKey.startsWith("$")) {
@@ -752,7 +900,12 @@ function resolveApiKey(apiKey) {
752
900
  }
753
901
  return apiKey;
754
902
  }
755
- async function resolvePoolBoundKey(deps, providerId, provider, sessionId) {
903
+ async function resolvePoolBoundKey(deps, providerId, provider, sessionId, preferredKeyId, boundKeyFallbackPolicy) {
904
+ if (preferredKeyId) {
905
+ const preferred = await deps.apiKeyPool?.getKeyById(providerId, preferredKeyId, sessionId);
906
+ if (preferred) return preferred;
907
+ if (boundKeyFallbackPolicy !== "pool") return "";
908
+ }
756
909
  if (deps.apiKeyPool && sessionId) {
757
910
  const poolKey = await deps.apiKeyPool.getKeyForSession(providerId, sessionId);
758
911
  if (poolKey) return poolKey;
@@ -807,6 +960,8 @@ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isS
807
960
  return {
808
961
  auth,
809
962
  preferredAccountId: route.preferredAccountId,
963
+ preferredAccountGroup: route.preferredAccountGroup,
964
+ boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
810
965
  chain,
811
966
  transformerProvider,
812
967
  resolvedModel,
@@ -881,6 +1036,8 @@ async function runPipeline(anthropicBody, plan, reportSelection) {
881
1036
  model: resolvedModel,
882
1037
  sessionKey: plan.sessionKey,
883
1038
  preferredAccountId: plan.preferredAccountId,
1039
+ preferredAccountGroup: plan.preferredAccountGroup,
1040
+ boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
884
1041
  reportSelection: (accountId, isActive) => {
885
1042
  proxyAccountId = accountId;
886
1043
  reportSelection?.(accountId, isActive);
@@ -908,7 +1065,16 @@ async function runPipeline(anthropicBody, plan, reportSelection) {
908
1065
  return fetchUpstream(
909
1066
  url,
910
1067
  { method: "POST", headers, body: JSON.stringify(body) },
911
- { providerId: proxyProviderId(plan), accountId: proxyAccountId }
1068
+ {
1069
+ providerId: proxyProviderId(plan),
1070
+ accountId: proxyAccountId,
1071
+ routeActivity: plan.isSubscription ? {
1072
+ endpoint: "messages",
1073
+ sessionKey: plan.sessionKey,
1074
+ sessionSource: plan.sessionKey ? "content-fingerprint" : "none",
1075
+ model: resolvedModel
1076
+ } : void 0
1077
+ }
912
1078
  ).then((r) => {
913
1079
  rawStatus = r.status;
914
1080
  return r;
@@ -927,13 +1093,16 @@ async function runSubscriptionSameFormatFetch(rawBody, plan, reportSelection, op
927
1093
  model: plan.resolvedModel,
928
1094
  sessionKey: plan.sessionKey,
929
1095
  preferredAccountId: plan.preferredAccountId,
1096
+ preferredAccountGroup: plan.preferredAccountGroup,
1097
+ boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
930
1098
  reportSelection: (accountId, isActive, remapped) => {
931
1099
  proxyAccountId = accountId;
932
1100
  remappedModel = remapped;
933
1101
  reportSelection?.(accountId, isActive, remapped);
934
1102
  }
935
1103
  });
936
- if (plan.isSubscription && plan.transformerProvider.name === "claude") {
1104
+ const isClaudeSubscription = plan.isSubscription && plan.transformerProvider.name === "claude";
1105
+ if (isClaudeSubscription) {
937
1106
  applyFingerprint(
938
1107
  getSharedIdentityStore(),
939
1108
  headers,
@@ -942,16 +1111,36 @@ async function runSubscriptionSameFormatFetch(rawBody, plan, reportSelection, op
942
1111
  options.callerIdentity
943
1112
  );
944
1113
  }
1114
+ if (isClaudeSubscription) {
1115
+ fillMissingHeaders(headers, options.callerClientHeaders ?? {});
1116
+ fillMissingHeaders(headers, DEFAULT_CLAUDE_CODE_HEADERS);
1117
+ }
1118
+ fillMissingHeaders(headers, { "anthropic-version": DEFAULT_ANTHROPIC_VERSION });
1119
+ headers["accept-encoding"] = FORCED_ACCEPT_ENCODING;
945
1120
  const applyRemap = remappedModel !== void 0 && outboundRemapApplies(plan);
946
1121
  const outboundBody = applyRemap ? rewriteBodyModel(rawBody, remappedModel) : rawBody;
947
1122
  const outboundModel = applyRemap ? remappedModel : plan.resolvedModel;
1123
+ if (isClaudeSubscription) {
1124
+ headers["anthropic-beta"] = buildAnthropicBeta(outboundModel, options.callerAnthropicBeta);
1125
+ } else if (options.callerAnthropicBeta) {
1126
+ fillMissingHeaders(headers, { "anthropic-beta": options.callerAnthropicBeta });
1127
+ }
948
1128
  console.info(
949
1129
  `[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${outboundModel} stream=${plan.isStream}`
950
1130
  );
951
1131
  const response = await fetchUpstream(
952
1132
  plan.upstreamUrl,
953
1133
  { method: "POST", headers, body: outboundBody },
954
- { providerId: proxyProviderId(plan), accountId: proxyAccountId }
1134
+ {
1135
+ providerId: proxyProviderId(plan),
1136
+ accountId: proxyAccountId,
1137
+ routeActivity: plan.isSubscription ? {
1138
+ endpoint: "messages",
1139
+ sessionKey: plan.sessionKey,
1140
+ sessionSource: plan.sessionKey ? "content-fingerprint" : "none",
1141
+ model: outboundModel
1142
+ } : void 0
1143
+ }
955
1144
  );
956
1145
  return { response, rawStatus: response.status };
957
1146
  }
@@ -1119,6 +1308,35 @@ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {
1119
1308
  const upstreamSse = (upstreamResponse.headers.get("content-type") ?? "").includes(
1120
1309
  "text/event-stream"
1121
1310
  );
1311
+ const usageAttribution = {
1312
+ sessionId: route.sessionId,
1313
+ providerId: route.providerId ?? "anthropic",
1314
+ model: plan.resolvedModel,
1315
+ apiKeyId: route.apiKeyId ?? null,
1316
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1317
+ auditResponse: res
1318
+ };
1319
+ const usageRecorder = deps.usageRecorder;
1320
+ let anthropicInputUsage;
1321
+ const usageTap = usageRecorder ? {
1322
+ extractUsage(event) {
1323
+ if (event["type"] === "message_start") {
1324
+ const msg = event["message"];
1325
+ const u = msg?.["usage"];
1326
+ if (u) anthropicInputUsage = u;
1327
+ return null;
1328
+ }
1329
+ if (event["type"] === "message_delta") {
1330
+ const outputUsage = event["usage"];
1331
+ if (!outputUsage) return null;
1332
+ return { ...anthropicInputUsage, ...outputUsage };
1333
+ }
1334
+ return null;
1335
+ },
1336
+ onUsage(rawUsage) {
1337
+ recordAnthropicStreamUsage(usageRecorder, rawUsage, usageAttribution);
1338
+ }
1339
+ } : void 0;
1122
1340
  let bodyText;
1123
1341
  if (!isStream && upstreamSse) {
1124
1342
  bodyText = await aggregateAnthropicSseToJsonBody(upstreamResponse, route.requestedModel);
@@ -1128,22 +1346,19 @@ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {
1128
1346
  );
1129
1347
  res.end(bodyText);
1130
1348
  } else {
1131
- bodyText = await relayResponse(res, upstreamResponse, isStream, route.requestedModel);
1349
+ bodyText = await relayResponse(res, upstreamResponse, isStream, route.requestedModel, usageTap);
1132
1350
  }
1133
1351
  if (bodyText && deps.usageRecorder) {
1134
- recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, {
1135
- sessionId: route.sessionId,
1136
- providerId: route.providerId ?? "anthropic",
1137
- model: plan.resolvedModel,
1138
- apiKeyId: route.apiKeyId ?? null,
1139
- // request-audit-log: correlate this request's tokens/cost to its audit record.
1140
- auditResponse: res
1141
- });
1352
+ recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
1142
1353
  }
1143
1354
  } catch (err) {
1355
+ if (isBoundAccountSelectionError(err)) {
1356
+ writeBoundAccountError(res, err);
1357
+ return;
1358
+ }
1144
1359
  const errMsg = serializeError(err);
1145
1360
  console.error("[ProviderProxy:anthropic] Pipeline error:", errMsg);
1146
- writeError(res, 502, errMsg);
1361
+ writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
1147
1362
  }
1148
1363
  }
1149
1364
  async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
@@ -1157,7 +1372,14 @@ async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
1157
1372
  writeError(res, 502, `Provider not found: ${providerId}`);
1158
1373
  return null;
1159
1374
  }
1160
- const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
1375
+ const apiKey = await resolvePoolBoundKey(
1376
+ deps,
1377
+ providerId,
1378
+ provider,
1379
+ route.sessionId,
1380
+ route.preferredKeyId,
1381
+ route.boundKeyFallbackPolicy
1382
+ );
1161
1383
  if (!apiKey) {
1162
1384
  writeError(res, 502, "API key not configured");
1163
1385
  return null;
@@ -1258,7 +1480,12 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
1258
1480
  const callerBetaRaw = req.headers["anthropic-beta"];
1259
1481
  const callerAnthropicBeta = Array.isArray(callerBetaRaw) ? callerBetaRaw.join(",") : callerBetaRaw;
1260
1482
  const callerIdentity = captureCallerIdentity(getSharedIdentityStore(), req.headers);
1261
- await handleAnthropicMessagesByo(res, rawBody, route, deps, { callerAnthropicBeta, callerIdentity });
1483
+ const callerClientHeaders = extractClaudeClientHeaders(req.headers);
1484
+ await handleAnthropicMessagesByo(res, rawBody, route, deps, {
1485
+ callerAnthropicBeta,
1486
+ callerIdentity,
1487
+ callerClientHeaders
1488
+ });
1262
1489
  return;
1263
1490
  }
1264
1491
  const hints = route.anthropicSdkHints;
@@ -1270,11 +1497,31 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
1270
1497
  writeError(res, 502, "Anthropic route is missing a providerId");
1271
1498
  return;
1272
1499
  }
1500
+ let delegatedApiKey = hints.apiKey;
1501
+ if (route.authMode === "byo" && route.providerId) {
1502
+ const provider = await deps.llmConfig.getProvider(route.providerId);
1503
+ if (!provider) {
1504
+ writeError(res, 502, `Provider not found: ${route.providerId}`);
1505
+ return;
1506
+ }
1507
+ delegatedApiKey = await resolvePoolBoundKey(
1508
+ deps,
1509
+ route.providerId,
1510
+ provider,
1511
+ route.sessionId,
1512
+ route.preferredKeyId,
1513
+ route.boundKeyFallbackPolicy
1514
+ );
1515
+ if (!delegatedApiKey) {
1516
+ writeError(res, 502, "API key not configured");
1517
+ return;
1518
+ }
1519
+ }
1273
1520
  const handler = handlerFactory({
1274
1521
  llmConfig: deps.llmConfig,
1275
1522
  providerId: route.providerId ?? "",
1276
1523
  model: route.model,
1277
- apiKey: hints.apiKey,
1524
+ apiKey: delegatedApiKey,
1278
1525
  backgroundTaskModel: route.backgroundTaskModel,
1279
1526
  isOfficialProvider: hints.isOfficialProvider,
1280
1527
  thinkingLevel: hints.thinkingLevel,
@@ -1283,6 +1530,11 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
1283
1530
  passThroughAuthToken: hints.passThroughAuthToken ?? null,
1284
1531
  resolvePassThroughAuthToken: hints.resolvePassThroughAuthToken ?? null,
1285
1532
  subscriptionProfile: hints.subscriptionProfile ?? null,
1533
+ preferredAccountId: hints.preferredAccountId,
1534
+ preferredAccountGroup: hints.preferredAccountGroup,
1535
+ boundAccountFallbackPolicy: hints.boundAccountFallbackPolicy,
1536
+ preferredKeyId: route.preferredKeyId,
1537
+ boundKeyFallbackPolicy: route.boundKeyFallbackPolicy,
1286
1538
  maxConcurrency: hints.maxConcurrency,
1287
1539
  webSearchService: hints.webSearchService ?? null,
1288
1540
  onRetry: hints.onRetry,
@@ -1309,23 +1561,36 @@ function readGeminiUsage(usage) {
1309
1561
  reasoningTokens
1310
1562
  };
1311
1563
  }
1564
+ function emitGeminiUsageRecord(recorder, rawUsageMetadata, attribution) {
1565
+ const tapped = readGeminiUsage(rawUsageMetadata);
1566
+ if (!tapped) return;
1567
+ recorder.record({
1568
+ messageId: null,
1569
+ parentMessageId: null,
1570
+ sessionId: attribution.sessionId,
1571
+ providerId: attribution.providerId,
1572
+ model: attribution.model,
1573
+ apiKeyId: attribution.apiKeyId,
1574
+ auditResponse: attribution.auditResponse,
1575
+ engineOrigin: "codex-ingress",
1576
+ usage: tapped,
1577
+ rawUsage: rawUsageMetadata
1578
+ });
1579
+ }
1312
1580
  function recordGeminiNonStreamUsage(recorder, bodyText, attribution) {
1313
1581
  try {
1314
1582
  const parsed = JSON.parse(bodyText);
1315
- const tapped = readGeminiUsage(parsed.usageMetadata);
1316
- if (!tapped) return;
1317
- recorder.record({
1318
- messageId: null,
1319
- parentMessageId: null,
1320
- sessionId: attribution.sessionId,
1321
- providerId: attribution.providerId,
1322
- model: attribution.model,
1323
- apiKeyId: attribution.apiKeyId,
1324
- auditResponse: attribution.auditResponse,
1325
- engineOrigin: "codex-ingress",
1326
- usage: tapped,
1327
- rawUsage: parsed.usageMetadata
1328
- });
1583
+ emitGeminiUsageRecord(
1584
+ recorder,
1585
+ parsed.usageMetadata,
1586
+ attribution
1587
+ );
1588
+ } catch {
1589
+ }
1590
+ }
1591
+ function recordGeminiStreamUsage(recorder, rawUsageMetadata, attribution) {
1592
+ try {
1593
+ emitGeminiUsageRecord(recorder, rawUsageMetadata, attribution);
1329
1594
  } catch {
1330
1595
  }
1331
1596
  }
@@ -1366,16 +1631,32 @@ async function handleGeminiGenerateContentRequest(res, rawBody, url, route, deps
1366
1631
  const plan = await buildByoPlan2(res, route, deps, resolvedModel, isStream);
1367
1632
  if (!plan) return;
1368
1633
  const providerResponse = await runPipelineWithPoolReporting2(geminiBody, plan);
1369
- const bodyText = await relayResponse(res, providerResponse.response, isStream);
1634
+ const usageAttribution = {
1635
+ sessionId: route.sessionId,
1636
+ providerId: route.providerId ?? "gemini",
1637
+ model: resolvedModel,
1638
+ apiKeyId: route.apiKeyId ?? null,
1639
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1640
+ auditResponse: res
1641
+ };
1642
+ const usageRecorder = deps.usageRecorder;
1643
+ const usageTap = usageRecorder ? {
1644
+ extractUsage(event) {
1645
+ return event["usageMetadata"] ?? null;
1646
+ },
1647
+ onUsage(rawUsage) {
1648
+ recordGeminiStreamUsage(usageRecorder, rawUsage, usageAttribution);
1649
+ }
1650
+ } : void 0;
1651
+ const bodyText = await relayResponse(
1652
+ res,
1653
+ providerResponse.response,
1654
+ isStream,
1655
+ void 0,
1656
+ usageTap
1657
+ );
1370
1658
  if (bodyText && deps.usageRecorder) {
1371
- recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, {
1372
- sessionId: route.sessionId,
1373
- providerId: route.providerId ?? "gemini",
1374
- model: resolvedModel,
1375
- apiKeyId: route.apiKeyId ?? null,
1376
- // request-audit-log: correlate this request's tokens/cost to its audit record.
1377
- auditResponse: res
1378
- });
1659
+ recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
1379
1660
  }
1380
1661
  } catch (err) {
1381
1662
  const errMsg = serializeError(err);
@@ -1394,7 +1675,14 @@ async function buildByoPlan2(res, route, deps, resolvedModel, isStream) {
1394
1675
  writeError(res, 502, `Provider not found: ${providerId}`);
1395
1676
  return null;
1396
1677
  }
1397
- const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
1678
+ const apiKey = await resolvePoolBoundKey(
1679
+ deps,
1680
+ providerId,
1681
+ provider,
1682
+ route.sessionId,
1683
+ route.preferredKeyId,
1684
+ route.boundKeyFallbackPolicy
1685
+ );
1398
1686
  if (!apiKey) {
1399
1687
  writeError(res, 502, "API key not configured");
1400
1688
  return null;
@@ -1494,23 +1782,36 @@ function readChatCompletionsUsage(usage) {
1494
1782
  reasoningTokens
1495
1783
  };
1496
1784
  }
1785
+ function emitChatCompletionsUsageRecord(recorder, rawUsage, attribution) {
1786
+ const tapped = readChatCompletionsUsage(rawUsage);
1787
+ if (!tapped) return;
1788
+ recorder.record({
1789
+ messageId: null,
1790
+ parentMessageId: null,
1791
+ sessionId: attribution.sessionId,
1792
+ providerId: attribution.providerId,
1793
+ model: attribution.model,
1794
+ apiKeyId: attribution.apiKeyId,
1795
+ auditResponse: attribution.auditResponse,
1796
+ engineOrigin: "codex-ingress",
1797
+ usage: tapped,
1798
+ rawUsage
1799
+ });
1800
+ }
1497
1801
  function recordChatCompletionsNonStreamUsage(recorder, bodyText, attribution) {
1498
1802
  try {
1499
1803
  const parsed = JSON.parse(bodyText);
1500
- const tapped = readChatCompletionsUsage(parsed.usage);
1501
- if (!tapped) return;
1502
- recorder.record({
1503
- messageId: null,
1504
- parentMessageId: null,
1505
- sessionId: attribution.sessionId,
1506
- providerId: attribution.providerId,
1507
- model: attribution.model,
1508
- apiKeyId: attribution.apiKeyId,
1509
- auditResponse: attribution.auditResponse,
1510
- engineOrigin: "codex-ingress",
1511
- usage: tapped,
1512
- rawUsage: parsed.usage
1513
- });
1804
+ emitChatCompletionsUsageRecord(
1805
+ recorder,
1806
+ parsed.usage,
1807
+ attribution
1808
+ );
1809
+ } catch {
1810
+ }
1811
+ }
1812
+ function recordChatCompletionsStreamUsage(recorder, rawUsage, attribution) {
1813
+ try {
1814
+ emitChatCompletionsUsageRecord(recorder, rawUsage, attribution);
1514
1815
  } catch {
1515
1816
  }
1516
1817
  }
@@ -1538,21 +1839,41 @@ async function handleOpenAIChatRequest(res, rawBody, route, deps) {
1538
1839
  const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan3(res, route, deps, resolvedModel, isStream);
1539
1840
  if (!plan) return;
1540
1841
  const providerResponse = plan.isSubscription ? await runPipelineWithSubscriptionRetry2(chatBody, plan) : await runPipelineWithPoolReporting3(chatBody, plan);
1541
- const bodyText = await relayResponse(res, providerResponse.response, isStream);
1842
+ const usageAttribution = {
1843
+ sessionId: route.sessionId,
1844
+ providerId: route.providerId ?? "openai",
1845
+ model: resolvedModel,
1846
+ apiKeyId: route.apiKeyId ?? null,
1847
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1848
+ auditResponse: res
1849
+ };
1850
+ const usageRecorder = deps.usageRecorder;
1851
+ const usageTap = usageRecorder ? {
1852
+ extractUsage(event) {
1853
+ return event["usage"] ?? null;
1854
+ },
1855
+ onUsage(rawUsage) {
1856
+ recordChatCompletionsStreamUsage(usageRecorder, rawUsage, usageAttribution);
1857
+ }
1858
+ } : void 0;
1859
+ const bodyText = await relayResponse(
1860
+ res,
1861
+ providerResponse.response,
1862
+ isStream,
1863
+ void 0,
1864
+ usageTap
1865
+ );
1542
1866
  if (bodyText && deps.usageRecorder) {
1543
- recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, {
1544
- sessionId: route.sessionId,
1545
- providerId: route.providerId ?? "openai",
1546
- model: resolvedModel,
1547
- apiKeyId: route.apiKeyId ?? null,
1548
- // request-audit-log: correlate this request's tokens/cost to its audit record.
1549
- auditResponse: res
1550
- });
1867
+ recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
1551
1868
  }
1552
1869
  } catch (err) {
1870
+ if (isBoundAccountSelectionError(err)) {
1871
+ writeBoundAccountError(res, err);
1872
+ return;
1873
+ }
1553
1874
  const errMsg = serializeError(err);
1554
1875
  console.error("[ProviderProxy:chat] Pipeline error:", errMsg);
1555
- writeError(res, 502, errMsg);
1876
+ writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
1556
1877
  }
1557
1878
  }
1558
1879
  async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
@@ -1566,7 +1887,14 @@ async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
1566
1887
  writeError(res, 502, `Provider not found: ${providerId}`);
1567
1888
  return null;
1568
1889
  }
1569
- const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
1890
+ const apiKey = await resolvePoolBoundKey(
1891
+ deps,
1892
+ providerId,
1893
+ provider,
1894
+ route.sessionId,
1895
+ route.preferredKeyId,
1896
+ route.boundKeyFallbackPolicy
1897
+ );
1570
1898
  if (!apiKey) {
1571
1899
  writeError(res, 502, "API key not configured");
1572
1900
  return null;
@@ -1632,6 +1960,9 @@ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream)
1632
1960
  };
1633
1961
  return {
1634
1962
  auth,
1963
+ preferredAccountId: route.preferredAccountId,
1964
+ preferredAccountGroup: route.preferredAccountGroup,
1965
+ boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
1635
1966
  chain,
1636
1967
  transformerProvider,
1637
1968
  resolvedModel,
@@ -1650,6 +1981,9 @@ async function runPipeline3(chatBody, plan) {
1650
1981
  await auth.applyHeaders(authHeaders, {
1651
1982
  upstreamUrl,
1652
1983
  model: resolvedModel,
1984
+ preferredAccountId: plan.preferredAccountId,
1985
+ preferredAccountGroup: plan.preferredAccountGroup,
1986
+ boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
1653
1987
  reportSelection: (accountId) => {
1654
1988
  proxyAccountId = accountId;
1655
1989
  }
@@ -1728,34 +2062,109 @@ function readResponsesUsage(usage) {
1728
2062
  reasoningTokens
1729
2063
  };
1730
2064
  }
2065
+ function emitResponsesUsageRecord(recorder, rawUsage, attribution) {
2066
+ const tapped = readResponsesUsage(rawUsage);
2067
+ if (!tapped) return;
2068
+ recorder.record({
2069
+ messageId: null,
2070
+ parentMessageId: null,
2071
+ sessionId: attribution.sessionId,
2072
+ providerId: attribution.providerId,
2073
+ model: attribution.model,
2074
+ apiKeyId: attribution.apiKeyId,
2075
+ cacheKeySource: attribution.cacheKeySource,
2076
+ cacheKeyInjected: attribution.cacheKeyInjected,
2077
+ auditResponse: attribution.auditResponse,
2078
+ engineOrigin: "codex-ingress",
2079
+ usage: tapped,
2080
+ rawUsage
2081
+ });
2082
+ }
1731
2083
  function recordResponsesNonStreamUsage(recorder, bodyText, attribution) {
1732
2084
  try {
1733
2085
  const parsed = JSON.parse(bodyText);
1734
- const tapped = readResponsesUsage(parsed.usage);
1735
- if (!tapped) return;
1736
- recorder.record({
1737
- messageId: null,
1738
- parentMessageId: null,
1739
- sessionId: attribution.sessionId,
1740
- providerId: attribution.providerId,
1741
- model: attribution.model,
1742
- apiKeyId: attribution.apiKeyId,
1743
- auditResponse: attribution.auditResponse,
1744
- engineOrigin: "codex-ingress",
1745
- usage: tapped,
1746
- rawUsage: parsed.usage
1747
- });
2086
+ emitResponsesUsageRecord(
2087
+ recorder,
2088
+ parsed.usage,
2089
+ attribution
2090
+ );
2091
+ } catch {
2092
+ }
2093
+ }
2094
+ function recordResponsesStreamUsage(recorder, rawUsage, attribution) {
2095
+ try {
2096
+ emitResponsesUsageRecord(recorder, rawUsage, attribution);
1748
2097
  } catch {
1749
2098
  }
1750
2099
  }
1751
2100
 
2101
+ // src/provider-proxy/ingress/codexUsageLimitDetection.ts
2102
+ var PROVIDER_ID = "codex";
2103
+ var DEFAULT_QUOTA_COOLDOWN_MS = 5 * 6e4;
2104
+ var QUOTA_EXHAUSTED_PERCENT = 90;
2105
+ function parseTryAgainDeadline(bodyText) {
2106
+ const match = bodyText.match(/try again (?:at|on)\s+(.+?)(?:[.\n]|$)/i);
2107
+ if (!match) return void 0;
2108
+ const cleaned = match[1].replace(/(\d+)(st|nd|rd|th)/giu, "$1").trim();
2109
+ const ms = Date.parse(cleaned);
2110
+ return Number.isFinite(ms) ? ms : void 0;
2111
+ }
2112
+ function deadlineFromAllowanceSnapshot(accountId, now) {
2113
+ const snapshot = getSharedAccountAllowanceStore().get(PROVIDER_ID, accountId, now);
2114
+ if (!snapshot) return void 0;
2115
+ const candidates = snapshot.windows.filter(
2116
+ (w) => typeof w.usedPercent === "number" && w.usedPercent >= QUOTA_EXHAUSTED_PERCENT && typeof w.resetsAt === "string"
2117
+ ).map((w) => ({ ms: Date.parse(w.resetsAt), usedPercent: w.usedPercent })).filter((x) => Number.isFinite(x.ms) && x.ms > now).sort((a, b) => b.usedPercent - a.usedPercent);
2118
+ return candidates[0]?.ms;
2119
+ }
2120
+ function resolveCodexQuotaDeadline(accountId, bodyText, now = Date.now()) {
2121
+ const parsed = parseTryAgainDeadline(bodyText);
2122
+ const parsedDeadline = parsed !== void 0 && parsed > now ? parsed : void 0;
2123
+ return deadlineFromAllowanceSnapshot(accountId, now) ?? parsedDeadline ?? now + DEFAULT_QUOTA_COOLDOWN_MS;
2124
+ }
2125
+ function markCodexUsageLimitExhaustion(accountId, bodyText, now = Date.now()) {
2126
+ if (!accountId) return;
2127
+ const deadline = resolveCodexQuotaDeadline(accountId, bodyText, now);
2128
+ getSharedAccountHealth().markQuotaExhausted(PROVIDER_ID, accountId, deadline, now);
2129
+ }
2130
+
1752
2131
  // src/provider-proxy/ingress/openaiResponsesIngress.ts
2132
+ var SERVER_OVERLOADED_CODES = /* @__PURE__ */ new Set(["server_is_overloaded", "slow_down"]);
2133
+ function isCodexServerOverloadEvent(event) {
2134
+ if (event["type"] !== "response.failed") return false;
2135
+ const error = event["response"]?.["error"];
2136
+ const code = error?.["code"];
2137
+ return typeof code === "string" && SERVER_OVERLOADED_CODES.has(code);
2138
+ }
1753
2139
  function isOpenAIResponsesRequest(method, url) {
1754
2140
  if (method !== "POST" || !url) return false;
1755
2141
  const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
1756
2142
  return path.endsWith("/responses");
1757
2143
  }
1758
- async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
2144
+ var SAFE_INJECTED_CACHE_KEY_SOURCES = /* @__PURE__ */ new Set([
2145
+ "session-header",
2146
+ "thread-header",
2147
+ "body-session-id",
2148
+ "body-thread-id",
2149
+ "content-fingerprint"
2150
+ ]);
2151
+ function isSafeInjectedCacheKeySource(source) {
2152
+ return SAFE_INJECTED_CACHE_KEY_SOURCES.has(source);
2153
+ }
2154
+ function ensureCodexPromptCacheKey(body, proxyProviderId2, derivedSession) {
2155
+ if (typeof body.prompt_cache_key === "string" && body.prompt_cache_key.trim()) {
2156
+ return { cacheKeySource: "client", cacheKeyInjected: false };
2157
+ }
2158
+ if (proxyProviderId2 !== "codex" || !isSafeInjectedCacheKeySource(derivedSession.source)) {
2159
+ return { cacheKeySource: "none", cacheKeyInjected: false };
2160
+ }
2161
+ body.prompt_cache_key = `omnicross:${derivedSession.source}:${derivedSession.key}`;
2162
+ return {
2163
+ cacheKeySource: derivedSession.source,
2164
+ cacheKeyInjected: true
2165
+ };
2166
+ }
2167
+ async function handleOpenAIResponsesRequest(res, rawBody, route, deps, requestHeaders = {}) {
1759
2168
  let responsesBody;
1760
2169
  try {
1761
2170
  responsesBody = JSON.parse(rawBody);
@@ -1764,27 +2173,96 @@ async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
1764
2173
  return;
1765
2174
  }
1766
2175
  const isStream = responsesBody.stream === true;
2176
+ const derivedSession = deriveGatewaySessionKey(responsesBody, requestHeaders, {
2177
+ endpoint: "responses",
2178
+ // A verified outbound key is the strongest route-local fallback. Resident
2179
+ // proxy callers without one still get a stable run/session bucket; the
2180
+ // final `anonymous` fallback is handled inside the shared utility.
2181
+ fallbackKey: route.apiKeyId ?? route.sessionId ?? void 0
2182
+ });
2183
+ const sessionKey = derivedSession.key;
1767
2184
  const resolvedModel = route.model;
1768
2185
  responsesBody.model = resolvedModel;
1769
2186
  try {
1770
- const plan = route.authMode === "subscription" ? await buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
2187
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan3(
2188
+ res,
2189
+ route,
2190
+ deps,
2191
+ resolvedModel,
2192
+ isStream,
2193
+ sessionKey,
2194
+ derivedSession.source,
2195
+ requestHeaders
2196
+ ) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
1771
2197
  if (!plan) return;
1772
- const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry3(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
1773
- const bodyText = await relayResponse(res, providerResponse.response, isStream, route.requestedModel);
2198
+ const cacheKeyAttribution = ensureCodexPromptCacheKey(
2199
+ responsesBody,
2200
+ plan.proxyProviderId,
2201
+ derivedSession
2202
+ );
2203
+ let providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry3(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
2204
+ providerResponse = await retryAroundCodexUsageLimit(responsesBody, plan, providerResponse);
2205
+ const usageAttribution = {
2206
+ sessionId: route.sessionId,
2207
+ providerId: route.providerId ?? "codex",
2208
+ model: resolvedModel,
2209
+ apiKeyId: route.apiKeyId ?? null,
2210
+ cacheKeySource: cacheKeyAttribution.cacheKeySource,
2211
+ cacheKeyInjected: cacheKeyAttribution.cacheKeyInjected,
2212
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
2213
+ auditResponse: res
2214
+ };
2215
+ const usageRecorder = deps.usageRecorder;
2216
+ const usageTap = usageRecorder ? {
2217
+ extractUsage(event) {
2218
+ if (event["type"] !== "response.completed") return null;
2219
+ const response = event["response"];
2220
+ const usage = response?.["usage"];
2221
+ return usage ?? null;
2222
+ },
2223
+ onUsage(rawUsage) {
2224
+ recordResponsesStreamUsage(usageRecorder, rawUsage, usageAttribution);
2225
+ }
2226
+ } : void 0;
2227
+ const overloadActivityId = providerResponse.activityRecordId;
2228
+ const overloadAccountId = providerResponse.accountId;
2229
+ const overloadProviderId = plan.proxyProviderId;
2230
+ let overloadRecorded = false;
2231
+ const overloadDetector = (event) => {
2232
+ if (overloadRecorded || !isCodexServerOverloadEvent(event)) return;
2233
+ overloadRecorded = true;
2234
+ if (overloadActivityId) {
2235
+ getSharedAccountRouteActivity().amend(overloadActivityId, {
2236
+ streamError: "server_overloaded"
2237
+ });
2238
+ }
2239
+ if (overloadAccountId) {
2240
+ getSharedOverloadCounter().recordOverload({
2241
+ providerId: overloadProviderId,
2242
+ accountId: overloadAccountId,
2243
+ endpoint: "responses"
2244
+ });
2245
+ }
2246
+ };
2247
+ const bodyText = await relayResponse(
2248
+ res,
2249
+ providerResponse.response,
2250
+ isStream,
2251
+ route.requestedModel,
2252
+ usageTap,
2253
+ overloadDetector
2254
+ );
1774
2255
  if (bodyText && deps.usageRecorder) {
1775
- recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, {
1776
- sessionId: route.sessionId,
1777
- providerId: route.providerId ?? "codex",
1778
- model: resolvedModel,
1779
- apiKeyId: route.apiKeyId ?? null,
1780
- // request-audit-log: correlate this request's tokens/cost to its audit record.
1781
- auditResponse: res
1782
- });
2256
+ recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, usageAttribution);
1783
2257
  }
1784
2258
  } catch (err) {
2259
+ if (isBoundAccountSelectionError(err)) {
2260
+ writeBoundAccountError(res, err);
2261
+ return;
2262
+ }
1785
2263
  const errMsg = serializeError(err);
1786
2264
  console.error("[ProviderProxy:responses] Pipeline error:", errMsg);
1787
- writeError(res, 502, errMsg);
2265
+ writeError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, errMsg);
1788
2266
  }
1789
2267
  }
1790
2268
  async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
@@ -1798,7 +2276,14 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
1798
2276
  writeError(res, 502, `Provider not found: ${providerId}`);
1799
2277
  return null;
1800
2278
  }
1801
- const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
2279
+ const apiKey = await resolvePoolBoundKey(
2280
+ deps,
2281
+ providerId,
2282
+ provider,
2283
+ route.sessionId,
2284
+ route.preferredKeyId,
2285
+ route.boundKeyFallbackPolicy
2286
+ );
1802
2287
  if (!apiKey) {
1803
2288
  writeError(res, 502, "API key not configured");
1804
2289
  return null;
@@ -1829,7 +2314,7 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
1829
2314
  proxyProviderId: "byo"
1830
2315
  };
1831
2316
  }
1832
- async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) {
2317
+ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream, sessionKey, sessionSource, requestHeaders) {
1833
2318
  const profile = route.subscriptionProfile;
1834
2319
  if (!profile) {
1835
2320
  writeError(res, 502, "Subscription mode requires a codex subscription profile");
@@ -1854,15 +2339,28 @@ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream)
1854
2339
  };
1855
2340
  if (profile.authStrategy.providerId === "gemini") {
1856
2341
  try {
1857
- transformerProvider.geminiProject = await resolveGeminiCodeAssistProject(profile);
2342
+ transformerProvider.geminiProject = await resolveGeminiCodeAssistProject(profile, {
2343
+ sessionKey,
2344
+ preferredAccountId: route.preferredAccountId,
2345
+ preferredAccountGroup: route.preferredAccountGroup,
2346
+ boundAccountFallbackPolicy: route.boundAccountFallbackPolicy
2347
+ });
1858
2348
  } catch (err) {
2349
+ if (isBoundAccountSelectionError(err)) {
2350
+ writeBoundAccountError(res, err);
2351
+ return null;
2352
+ }
1859
2353
  writeError(res, 502, serializeError(err));
1860
2354
  return null;
1861
2355
  }
1862
2356
  }
1863
2357
  return {
1864
2358
  auth,
2359
+ sessionKey,
2360
+ sessionSource,
1865
2361
  preferredAccountId: route.preferredAccountId,
2362
+ preferredAccountGroup: route.preferredAccountGroup,
2363
+ boundAccountFallbackPolicy: route.boundAccountFallbackPolicy,
1866
2364
  chain,
1867
2365
  transformerProvider,
1868
2366
  resolvedModel,
@@ -1872,12 +2370,15 @@ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream)
1872
2370
  // profile's `resolveUpstreamUrl` for non-Code-Assist subscriptions.
1873
2371
  resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
1874
2372
  upstreamUrl,
1875
- proxyProviderId: profile.authStrategy.providerId
2373
+ proxyProviderId: profile.authStrategy.providerId,
2374
+ // Only the codex backend understands these; other subscription providers
2375
+ // (gemini Code Assist) share this relay and must not receive them.
2376
+ callerClientHeaders: profile.authStrategy.providerId === "codex" ? extractCodexClientHeaders(requestHeaders) : void 0
1876
2377
  };
1877
2378
  }
1878
- async function resolveGeminiCodeAssistProject(profile) {
2379
+ async function resolveGeminiCodeAssistProject(profile, hints) {
1879
2380
  const probe = {};
1880
- await profile.authStrategy.applyHeaders(probe);
2381
+ await profile.authStrategy.applyHeaders(probe, hints);
1881
2382
  const bearer = probe.Authorization ?? probe.authorization ?? "";
1882
2383
  const accessToken = bearer.replace(/^Bearer\s+/i, "").trim();
1883
2384
  if (!accessToken) return void 0;
@@ -1888,13 +2389,17 @@ async function resolveGeminiCodeAssistProject(profile) {
1888
2389
  async function runPipeline4(responsesBody, plan) {
1889
2390
  const executor = getSharedExecutor();
1890
2391
  const endpointTransformer = getResponsesEndpointTransformer();
1891
- const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
2392
+ const { auth, sessionKey, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
1892
2393
  let proxyAccountId;
2394
+ let activityRecordId;
1893
2395
  const authHeaders = {};
1894
2396
  await auth.applyHeaders(authHeaders, {
1895
2397
  upstreamUrl,
1896
2398
  model: resolvedModel,
2399
+ sessionKey,
1897
2400
  preferredAccountId: plan.preferredAccountId,
2401
+ preferredAccountGroup: plan.preferredAccountGroup,
2402
+ boundAccountFallbackPolicy: plan.boundAccountFallbackPolicy,
1898
2403
  reportSelection: (accountId) => {
1899
2404
  proxyAccountId = accountId;
1900
2405
  }
@@ -1914,6 +2419,11 @@ async function runPipeline4(responsesBody, plan) {
1914
2419
  if (value !== void 0 && !(key in headers)) headers[key] = value;
1915
2420
  }
1916
2421
  }
2422
+ if (plan.proxyProviderId === "codex") {
2423
+ fillMissingHeaders(headers, plan.callerClientHeaders ?? {});
2424
+ fillMissingHeaders(headers, DEFAULT_CODEX_CLI_HEADERS);
2425
+ fillMissingHeaders(headers, { accept: codexAcceptHeader(isStream) });
2426
+ }
1917
2427
  return headers;
1918
2428
  },
1919
2429
  fetchFn: (url, headers, body) => {
@@ -1921,7 +2431,19 @@ async function runPipeline4(responsesBody, plan) {
1921
2431
  return fetchUpstream(
1922
2432
  url,
1923
2433
  { method: "POST", headers, body: JSON.stringify(body) },
1924
- { providerId: plan.proxyProviderId, accountId: proxyAccountId }
2434
+ {
2435
+ providerId: plan.proxyProviderId,
2436
+ accountId: proxyAccountId,
2437
+ routeActivity: plan.proxyProviderId === "byo" ? void 0 : {
2438
+ endpoint: "responses",
2439
+ sessionKey,
2440
+ sessionSource: plan.sessionSource ?? "none",
2441
+ model: resolvedModel,
2442
+ onRecorded: (record) => {
2443
+ activityRecordId = record.id;
2444
+ }
2445
+ }
2446
+ }
1925
2447
  ).then((r) => {
1926
2448
  rawStatus = r.status;
1927
2449
  return r;
@@ -1929,7 +2451,7 @@ async function runPipeline4(responsesBody, plan) {
1929
2451
  },
1930
2452
  runResponseChain: true
1931
2453
  });
1932
- return { response, rawStatus };
2454
+ return { response, rawStatus, accountId: proxyAccountId, activityRecordId };
1933
2455
  }
1934
2456
  async function runPipelineWithPoolReporting4(responsesBody, plan) {
1935
2457
  const first = await runPipeline4(responsesBody, plan);
@@ -1947,7 +2469,7 @@ async function runPipelineWithPoolReporting4(responsesBody, plan) {
1947
2469
  async function runPipelineWithSubscriptionRetry3(responsesBody, plan) {
1948
2470
  const first = await runPipeline4(responsesBody, plan);
1949
2471
  if (first.rawStatus !== 401) return first;
1950
- const refreshed = await plan.auth.onUnauthorized?.();
2472
+ const refreshed = await plan.auth.onUnauthorized?.(plan.sessionKey);
1951
2473
  if (!refreshed) {
1952
2474
  console.warn("[ProviderProxy:responses] 401 not recoverable (onUnauthorized returned false)");
1953
2475
  return first;
@@ -1955,6 +2477,22 @@ async function runPipelineWithSubscriptionRetry3(responsesBody, plan) {
1955
2477
  console.log("[ProviderProxy:responses] 401 \u2192 token refreshed; retrying once");
1956
2478
  return runPipeline4(responsesBody, plan);
1957
2479
  }
2480
+ var MAX_QUOTA_RETRIES = 3;
2481
+ function isCodexAccountQuota429(plan, result) {
2482
+ return plan.proxyProviderId === "codex" && !!result.accountId && result.rawStatus === 429;
2483
+ }
2484
+ async function retryAroundCodexUsageLimit(responsesBody, plan, first, runAttempt = runPipelineWithSubscriptionRetry3) {
2485
+ if (!isCodexAccountQuota429(plan, first)) return first;
2486
+ let current = first;
2487
+ for (let attempt = 0; attempt < MAX_QUOTA_RETRIES; attempt++) {
2488
+ const errorBody = await current.response.text().catch(() => "");
2489
+ markCodexUsageLimitExhaustion(current.accountId, errorBody);
2490
+ const next = await runAttempt(responsesBody, plan);
2491
+ if (!isCodexAccountQuota429(plan, next)) return next;
2492
+ current = next;
2493
+ }
2494
+ return current;
2495
+ }
1958
2496
 
1959
2497
  // src/provider-proxy/providerProxyRouter.ts
1960
2498
  function extractRouteToken(authHeader) {
@@ -1986,7 +2524,7 @@ async function routeRequest(req, res, routes, deps) {
1986
2524
  }
1987
2525
  if (isOpenAIResponsesRequest(method, url)) {
1988
2526
  const rawBody = await readBody(req);
1989
- await handleOpenAIResponsesRequest(res, rawBody, route, deps);
2527
+ await handleOpenAIResponsesRequest(res, rawBody, route, deps, req.headers);
1990
2528
  return;
1991
2529
  }
1992
2530
  if (isOpenAIChatRequest(method, url)) {
@@ -2196,6 +2734,38 @@ var DEFAULT_ACCOUNT_PROBE = {
2196
2734
  historySize: 10,
2197
2735
  staggerMs: 500
2198
2736
  };
2737
+ var DEFAULT_ALLOWANCE_SCHEDULING = {
2738
+ enabled: false,
2739
+ demoteAtPercent: 80,
2740
+ pauseAtPercent: 98,
2741
+ priorityPenalty: 100
2742
+ };
2743
+ function normalizeAllowanceScheduling(raw) {
2744
+ const value = raw?.allowanceScheduling;
2745
+ const demoteAtPercent = clampNumber(
2746
+ value?.demoteAtPercent,
2747
+ 0,
2748
+ 100,
2749
+ DEFAULT_ALLOWANCE_SCHEDULING.demoteAtPercent
2750
+ );
2751
+ const requestedPause = clampNumber(
2752
+ value?.pauseAtPercent,
2753
+ 0,
2754
+ 100,
2755
+ DEFAULT_ALLOWANCE_SCHEDULING.pauseAtPercent
2756
+ );
2757
+ return {
2758
+ enabled: value?.enabled === true,
2759
+ demoteAtPercent,
2760
+ pauseAtPercent: Math.max(demoteAtPercent, requestedPause),
2761
+ priorityPenalty: Math.trunc(clampNumber(
2762
+ value?.priorityPenalty,
2763
+ 1,
2764
+ 1e3,
2765
+ DEFAULT_ALLOWANCE_SCHEDULING.priorityPenalty
2766
+ ))
2767
+ };
2768
+ }
2199
2769
  function normalizeAccountProbe(raw) {
2200
2770
  const ap = raw?.accountProbe;
2201
2771
  return {
@@ -2214,15 +2784,20 @@ function normalizeAudit(raw) {
2214
2784
  return {
2215
2785
  enabled: a?.enabled === true,
2216
2786
  captureBodies: a?.captureBodies === true,
2217
- maxBodyBytes: Math.trunc(
2218
- clampNumber(a?.maxBodyBytes, 256, 1048576, DEFAULT_AUDIT_CONFIG.maxBodyBytes)
2219
- ),
2787
+ maxBodyBytes: normalizeMaxBodyBytes(a?.maxBodyBytes),
2220
2788
  retentionDays: Math.trunc(
2221
2789
  clampNumber(a?.retentionDays, 1, 365, DEFAULT_AUDIT_CONFIG.retentionDays)
2222
2790
  ),
2223
2791
  trustForwardedFor: a?.trustForwardedFor === true
2224
2792
  };
2225
2793
  }
2794
+ function normalizeMaxBodyBytes(value) {
2795
+ if (value === -1) return -1;
2796
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
2797
+ return DEFAULT_AUDIT_CONFIG.maxBodyBytes;
2798
+ }
2799
+ return Math.trunc(Math.min(1048576, value));
2800
+ }
2226
2801
  function normalizeBilling(raw) {
2227
2802
  const b = raw?.billing;
2228
2803
  const config = {
@@ -2390,7 +2965,8 @@ function normalizePrefixTargets(raw) {
2390
2965
  function normalizeEndpointConfig(e) {
2391
2966
  const endpoint = e.endpoint;
2392
2967
  const useSubscription = e.useSubscription === true;
2393
- const boundAccountId = typeof e.boundAccountId === "string" && e.boundAccountId.trim() !== "" ? e.boundAccountId : void 0;
2968
+ const boundAccountId = typeof e.boundAccountId === "string" && e.boundAccountId.trim() !== "" ? e.boundAccountId.trim() : void 0;
2969
+ const boundAccountFallbackPolicy = boundAccountId ? e.boundAccountFallbackPolicy === "pool" ? "pool" : "strict" : void 0;
2394
2970
  const boundKeyId = typeof e.boundKeyId === "string" && e.boundKeyId.trim() !== "" ? e.boundKeyId : void 0;
2395
2971
  let config;
2396
2972
  if (isKindMappedEndpoint(endpoint)) {
@@ -2420,21 +2996,186 @@ function normalizeEndpointConfig(e) {
2420
2996
  config.backgroundModelIds = e.backgroundModelIds;
2421
2997
  }
2422
2998
  }
2423
- if (boundAccountId) config.boundAccountId = boundAccountId;
2999
+ if (boundAccountId) {
3000
+ config.boundAccountId = boundAccountId;
3001
+ config.boundAccountFallbackPolicy = boundAccountFallbackPolicy;
3002
+ }
2424
3003
  if (boundKeyId) config.boundKeyId = boundKeyId;
2425
3004
  return config;
2426
3005
  }
3006
+ function normalizeBindingTarget(raw) {
3007
+ if (!raw || typeof raw !== "object") return null;
3008
+ const target = raw;
3009
+ const providerId = typeof target.providerId === "string" ? target.providerId.trim() : "";
3010
+ if (!providerId) return null;
3011
+ if (target.kind === "account") {
3012
+ const accountId = typeof target.accountId === "string" ? target.accountId.trim() : "";
3013
+ return accountId ? { kind: "account", providerId, accountId } : null;
3014
+ }
3015
+ if (target.kind === "account-group") {
3016
+ const group = typeof target.group === "string" ? target.group.trim() : "";
3017
+ return group ? { kind: "account-group", providerId, group } : null;
3018
+ }
3019
+ if (target.kind === "account-pool") return { kind: "account-pool", providerId };
3020
+ if (target.kind === "provider") {
3021
+ const keyId = typeof target.keyId === "string" ? target.keyId.trim() : "";
3022
+ return keyId ? { kind: "provider", providerId, keyId } : { kind: "provider", providerId };
3023
+ }
3024
+ return null;
3025
+ }
3026
+ function normalizeGatewayModelMappings(raw) {
3027
+ if (!Array.isArray(raw)) return [];
3028
+ const mappings = [];
3029
+ for (const entry of raw) {
3030
+ if (!entry || typeof entry !== "object") continue;
3031
+ const value = entry;
3032
+ const source = typeof value.source === "string" ? value.source.trim() : "";
3033
+ const target = typeof value.target === "string" ? value.target.trim() : "";
3034
+ if (!source || !target) continue;
3035
+ mappings.push({ source, target });
3036
+ }
3037
+ return mappings;
3038
+ }
3039
+ function hasConfiguredLegacyModels(config) {
3040
+ 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()));
3041
+ }
3042
+ function splitModelRef(ref) {
3043
+ if (typeof ref !== "string") return null;
3044
+ const idx = ref.indexOf(",");
3045
+ if (idx <= 0) return null;
3046
+ const providerId = ref.slice(0, idx).trim();
3047
+ const modelId = ref.slice(idx + 1).trim();
3048
+ return providerId && modelId ? { providerId, modelId } : null;
3049
+ }
3050
+ function legacyTarget(config, providerId) {
3051
+ if (config.useSubscription) {
3052
+ if (config.boundAccountId) {
3053
+ return { kind: "account", providerId, accountId: config.boundAccountId };
3054
+ }
3055
+ if (config.boundAccountGroup) {
3056
+ return { kind: "account-group", providerId, group: config.boundAccountGroup };
3057
+ }
3058
+ return { kind: "account-pool", providerId };
3059
+ }
3060
+ return config.boundKeyId ? { kind: "provider", providerId, keyId: config.boundKeyId } : { kind: "provider", providerId };
3061
+ }
3062
+ function legacyEndpointToBindings(config) {
3063
+ const fallback = config.boundAccountId && config.boundAccountFallbackPolicy === "strict" ? "fail" : "next";
3064
+ const byProvider = /* @__PURE__ */ new Map();
3065
+ const routeFor = (providerId) => {
3066
+ const existing = byProvider.get(providerId);
3067
+ if (existing) return existing;
3068
+ const created = {
3069
+ id: `legacy-${config.endpoint}-${providerId}`,
3070
+ name: `${config.endpoint} \xB7 ${providerId}`,
3071
+ enabled: true,
3072
+ keyScope: "all",
3073
+ endpoint: config.endpoint,
3074
+ target: legacyTarget(config, providerId),
3075
+ fallback,
3076
+ modelMode: "mapped"
3077
+ };
3078
+ byProvider.set(providerId, created);
3079
+ return created;
3080
+ };
3081
+ for (const [kind, ref] of Object.entries(config.modelMap ?? {})) {
3082
+ const parsed = splitModelRef(ref);
3083
+ if (!parsed) continue;
3084
+ const route = routeFor(parsed.providerId);
3085
+ route.modelMap = { ...route.modelMap, [kind]: ref };
3086
+ }
3087
+ for (const ref of config.models ?? []) {
3088
+ const parsed = splitModelRef(ref);
3089
+ if (!parsed) continue;
3090
+ const route = routeFor(parsed.providerId);
3091
+ route.models = [...route.models ?? [], ref];
3092
+ }
3093
+ for (const [prefix, ref] of Object.entries(config.prefixTargets ?? {})) {
3094
+ const parsed = splitModelRef(ref);
3095
+ if (!parsed) continue;
3096
+ const route = routeFor(parsed.providerId);
3097
+ route.dispatchMode = "prefix";
3098
+ route.prefixTargets = { ...route.prefixTargets, [prefix]: ref };
3099
+ }
3100
+ for (const [field, ref] of [
3101
+ ["defaultModel", config.defaultModel],
3102
+ ["backgroundModel", config.backgroundModel]
3103
+ ]) {
3104
+ const parsed = splitModelRef(ref);
3105
+ if (!parsed) continue;
3106
+ const route = routeFor(parsed.providerId);
3107
+ route[field] = ref;
3108
+ if (config.backgroundModelIds?.length) {
3109
+ route.backgroundModelIds = [...config.backgroundModelIds];
3110
+ }
3111
+ }
3112
+ return [...byProvider.values()];
3113
+ }
3114
+ function legacyEndpointsToBindings(endpoints) {
3115
+ return endpoints.filter((endpoint) => hasConfiguredLegacyModels(endpoint)).flatMap((endpoint) => legacyEndpointToBindings(endpoint));
3116
+ }
3117
+ function normalizeGatewayBindings(raw) {
3118
+ if (!Array.isArray(raw)) return [];
3119
+ const seen = /* @__PURE__ */ new Set();
3120
+ const bindings = [];
3121
+ for (const entry of raw) {
3122
+ if (!entry || typeof entry !== "object") continue;
3123
+ const value = entry;
3124
+ const id = typeof value.id === "string" ? value.id.trim() : "";
3125
+ const name = typeof value.name === "string" ? value.name.trim() : "";
3126
+ const endpoint = value.endpoint;
3127
+ const target = normalizeBindingTarget(value.target);
3128
+ if (!id || seen.has(id) || !name || !ALL_ENDPOINTS.includes(endpoint) || !target) continue;
3129
+ seen.add(id);
3130
+ const normalizedRoute = normalizeEndpointConfig({
3131
+ ...value,
3132
+ endpoint,
3133
+ useSubscription: target.kind !== "provider"
3134
+ });
3135
+ const apiKeyIds = Array.isArray(value.apiKeyIds) ? [...new Set(value.apiKeyIds.filter((key) => typeof key === "string" && key.trim() !== "").map((key) => key.trim()))] : [];
3136
+ const keyScope = value.keyScope === "selected" ? "selected" : value.keyScope === "all" ? "all" : apiKeyIds.length > 0 ? "selected" : "all";
3137
+ const modelMappings = normalizeGatewayModelMappings(value.modelMappings);
3138
+ const modelMode = value.modelMode === "passthrough" ? "passthrough" : value.modelMode === "mapped" ? "mapped" : modelMappings.length > 0 || hasConfiguredLegacyModels(normalizedRoute) ? "mapped" : "passthrough";
3139
+ const priority = typeof value.priority === "number" && Number.isFinite(value.priority) ? Math.max(0, Math.min(1e4, Math.round(value.priority))) : void 0;
3140
+ const binding = {
3141
+ id,
3142
+ name,
3143
+ enabled: value.enabled !== false,
3144
+ keyScope,
3145
+ endpoint,
3146
+ target,
3147
+ // Legacy `'global'` meant "yield to the removed global endpoint fallback";
3148
+ // it now yields to the next matching route, which is the same default.
3149
+ fallback: value.fallback === "fail" ? "fail" : "next",
3150
+ modelMode
3151
+ };
3152
+ if (apiKeyIds.length > 0) binding.apiKeyIds = apiKeyIds;
3153
+ if (modelMappings.length > 0) binding.modelMappings = modelMappings;
3154
+ if (priority !== void 0) binding.priority = priority;
3155
+ if (normalizedRoute.modelMap) binding.modelMap = normalizedRoute.modelMap;
3156
+ if (normalizedRoute.models) binding.models = normalizedRoute.models;
3157
+ if (normalizedRoute.dispatchMode) binding.dispatchMode = normalizedRoute.dispatchMode;
3158
+ if (normalizedRoute.prefixTargets) binding.prefixTargets = normalizedRoute.prefixTargets;
3159
+ if (normalizedRoute.defaultModel !== void 0) binding.defaultModel = normalizedRoute.defaultModel;
3160
+ if (normalizedRoute.backgroundModel !== void 0) binding.backgroundModel = normalizedRoute.backgroundModel;
3161
+ if (normalizedRoute.backgroundModelIds) binding.backgroundModelIds = normalizedRoute.backgroundModelIds;
3162
+ bindings.push(binding);
3163
+ }
3164
+ return bindings;
3165
+ }
2427
3166
  function defaultServerConfig() {
2428
3167
  const queues = normalizeQueueSegments(void 0);
2429
3168
  return {
2430
3169
  enabled: false,
2431
3170
  networkBinding: false,
2432
3171
  endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
3172
+ bindings: [],
2433
3173
  port: DEFAULT_OUTBOUND_PORT,
2434
3174
  userMessageQueue: queues.userMessageQueue,
2435
3175
  concurrencyQueue: queues.concurrencyQueue,
2436
3176
  accountHealth: normalizeAccountHealth(void 0),
2437
3177
  accountProbe: normalizeAccountProbe(void 0),
3178
+ allowanceScheduling: normalizeAllowanceScheduling(void 0),
2438
3179
  audit: normalizeAudit(void 0),
2439
3180
  billing: normalizeBilling(void 0),
2440
3181
  fingerprint: normalizeFingerprint(void 0),
@@ -2451,17 +3192,20 @@ function normalizeServerConfig(raw) {
2451
3192
  }
2452
3193
  }
2453
3194
  const queues = normalizeQueueSegments(raw);
3195
+ const legacyEndpoints = ALL_ENDPOINTS.map((ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep));
3196
+ const persistedBindings = normalizeGatewayBindings(raw.bindings);
3197
+ const bindings = persistedBindings.length > 0 ? persistedBindings : normalizeGatewayBindings(legacyEndpointsToBindings(legacyEndpoints));
2454
3198
  const config = {
2455
3199
  enabled: raw.enabled === true,
2456
3200
  networkBinding: raw.networkBinding === true,
2457
- endpoints: ALL_ENDPOINTS.map(
2458
- (ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep)
2459
- ),
3201
+ endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
3202
+ bindings,
2460
3203
  port: raw.port ?? base.port,
2461
3204
  userMessageQueue: queues.userMessageQueue,
2462
3205
  concurrencyQueue: queues.concurrencyQueue,
2463
3206
  accountHealth: normalizeAccountHealth(raw),
2464
3207
  accountProbe: normalizeAccountProbe(raw),
3208
+ allowanceScheduling: normalizeAllowanceScheduling(raw),
2465
3209
  audit: normalizeAudit(raw),
2466
3210
  billing: normalizeBilling(raw),
2467
3211
  fingerprint: normalizeFingerprint(raw),
@@ -2487,11 +3231,13 @@ function mergeServerConfig(current, patch) {
2487
3231
  enabled: patch.enabled ?? current.enabled,
2488
3232
  networkBinding: patch.networkBinding ?? current.networkBinding,
2489
3233
  endpoints: patch.endpoints ?? current.endpoints,
3234
+ bindings: patch.bindings ?? current.bindings,
2490
3235
  port: patch.port ?? current.port,
2491
3236
  userMessageQueue: patch.userMessageQueue ?? current.userMessageQueue,
2492
3237
  concurrencyQueue: patch.concurrencyQueue ?? current.concurrencyQueue,
2493
3238
  accountHealth: patch.accountHealth ?? current.accountHealth,
2494
3239
  accountProbe: patch.accountProbe ?? current.accountProbe,
3240
+ allowanceScheduling: patch.allowanceScheduling ?? current.allowanceScheduling,
2495
3241
  audit: patch.audit ?? current.audit,
2496
3242
  billing: patch.billing ?? current.billing,
2497
3243
  // Proxy is layer-replaced (not deep-merged): a PUT carrying `proxy` swaps the
@@ -2509,6 +3255,167 @@ function mergeServerConfig(current, patch) {
2509
3255
  });
2510
3256
  }
2511
3257
 
3258
+ // src/outbound-api/gatewayBindingResolver.ts
3259
+ var MESSAGE_FALLBACK_KINDS = ["sonnet", "opus", "haiku", "fable"];
3260
+ function nonBlank(value) {
3261
+ return typeof value === "string" && value.trim() !== "";
3262
+ }
3263
+ function gatewayBindingAllowsKey(binding, apiKeyId) {
3264
+ const scope = binding.keyScope ?? (binding.apiKeyIds?.length ? "selected" : "all");
3265
+ return scope === "all" || Boolean(binding.apiKeyIds?.includes(apiKeyId));
3266
+ }
3267
+ function wildcardMatches(pattern, value) {
3268
+ const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join(".*");
3269
+ return new RegExp(`^${escaped}$`, "iu").test(value);
3270
+ }
3271
+ function resolveGatewayModelMapping(mappings, requestedModel) {
3272
+ if (!nonBlank(requestedModel)) return void 0;
3273
+ const wanted = requestedModel.trim();
3274
+ const exact = (mappings ?? []).find(
3275
+ (mapping) => nonBlank(mapping.source) && !mapping.source.includes("*") && mapping.source.trim().toLocaleLowerCase() === wanted.toLocaleLowerCase()
3276
+ );
3277
+ if (exact && nonBlank(exact.target)) return exact.target.trim();
3278
+ const wildcard = (mappings ?? []).find(
3279
+ (mapping) => nonBlank(mapping.source) && mapping.source.includes("*") && wildcardMatches(mapping.source.trim(), wanted)
3280
+ );
3281
+ return wildcard && nonBlank(wildcard.target) ? wildcard.target.trim() : void 0;
3282
+ }
3283
+ function routeCanServe(binding, requestedModel, role) {
3284
+ if (binding.modelMode === "passthrough") return nonBlank(requestedModel);
3285
+ if (binding.modelMappings?.length) {
3286
+ return nonBlank(resolveGatewayModelMapping(binding.modelMappings, requestedModel));
3287
+ }
3288
+ if (isKindMappedEndpoint(binding.endpoint)) {
3289
+ const map = binding.modelMap ?? {};
3290
+ const kind = detectModelKind(binding.endpoint, requestedModel);
3291
+ if (kind && nonBlank(map[kind])) return true;
3292
+ if (binding.endpoint === "messages") {
3293
+ return MESSAGE_FALLBACK_KINDS.some((candidate) => nonBlank(map[candidate]));
3294
+ }
3295
+ return nonBlank(map.codex);
3296
+ }
3297
+ if (binding.endpoint === "chat" && binding.dispatchMode === "prefix") {
3298
+ return resolvePrefixTarget(binding.prefixTargets, requestedModel) !== null;
3299
+ }
3300
+ if (binding.endpoint === "chat") {
3301
+ if (!requestedModel) return false;
3302
+ const wanted = requestedModel.trim().toLowerCase();
3303
+ return (binding.models ?? []).some((ref) => modelIdOf(ref)?.toLowerCase() === wanted);
3304
+ }
3305
+ return role === "background" ? nonBlank(binding.backgroundModel) : nonBlank(binding.defaultModel);
3306
+ }
3307
+ function modelIdOf(ref) {
3308
+ if (!nonBlank(ref)) return void 0;
3309
+ const trimmed = ref.trim();
3310
+ const comma = trimmed.indexOf(",");
3311
+ const modelId = comma >= 0 ? trimmed.slice(comma + 1).trim() : trimmed;
3312
+ return modelId || void 0;
3313
+ }
3314
+ function targetProviderId(target) {
3315
+ return target.providerId.trim();
3316
+ }
3317
+ function targetRef(target, ref) {
3318
+ const modelId = modelIdOf(ref) ?? "";
3319
+ return modelId ? `${targetProviderId(target)},${modelId}` : "";
3320
+ }
3321
+ function targetRefs(target, refs) {
3322
+ return (refs ?? []).map((ref) => targetRef(target, ref)).filter(nonBlank);
3323
+ }
3324
+ function targetPrefixRefs(target, refs) {
3325
+ if (!refs) return void 0;
3326
+ const mapped = {};
3327
+ for (const kind of ["claude", "gpt", "gemini"]) {
3328
+ const ref = targetRef(target, refs[kind]);
3329
+ if (ref) mapped[kind] = ref;
3330
+ }
3331
+ return mapped.claude || mapped.gpt || mapped.gemini ? mapped : void 0;
3332
+ }
3333
+ function applySingleModel(config, binding, targetModel) {
3334
+ const ref = targetRef(binding.target, targetModel);
3335
+ if (!ref) return;
3336
+ if (isKindMappedEndpoint(binding.endpoint)) {
3337
+ config.modelMap = Object.fromEntries(
3338
+ modelKindsForEndpoint(binding.endpoint).map((kind) => [kind, ref])
3339
+ );
3340
+ } else if (binding.endpoint === "chat") {
3341
+ config.models = [ref];
3342
+ config.dispatchMode = "list";
3343
+ } else {
3344
+ config.defaultModel = ref;
3345
+ config.backgroundModel = ref;
3346
+ }
3347
+ }
3348
+ function gatewayBindingToEndpointConfig(binding, requestedModel) {
3349
+ const target = binding.target;
3350
+ const config = {
3351
+ endpoint: binding.endpoint,
3352
+ useSubscription: target.kind !== "provider"
3353
+ };
3354
+ const usesGenericModelHandling = binding.modelMode === "passthrough" || Boolean(binding.modelMappings?.length);
3355
+ const dynamicModel = binding.modelMode === "passthrough" ? requestedModel : resolveGatewayModelMapping(binding.modelMappings, requestedModel);
3356
+ if (usesGenericModelHandling) {
3357
+ if (nonBlank(dynamicModel)) applySingleModel(config, binding, dynamicModel);
3358
+ } else if (isKindMappedEndpoint(binding.endpoint)) {
3359
+ config.modelMap = Object.fromEntries(
3360
+ Object.entries(binding.modelMap ?? {}).map(([kind, ref]) => [kind, targetRef(target, ref)])
3361
+ );
3362
+ } else if (binding.endpoint === "chat") {
3363
+ config.models = targetRefs(target, binding.models);
3364
+ if (binding.dispatchMode === "prefix") {
3365
+ config.dispatchMode = "prefix";
3366
+ config.prefixTargets = targetPrefixRefs(target, binding.prefixTargets);
3367
+ }
3368
+ } else {
3369
+ config.defaultModel = targetRef(target, binding.defaultModel);
3370
+ config.backgroundModel = targetRef(target, binding.backgroundModel);
3371
+ if (binding.backgroundModelIds) config.backgroundModelIds = [...binding.backgroundModelIds];
3372
+ }
3373
+ if (target.kind === "account") {
3374
+ config.boundAccountId = target.accountId;
3375
+ config.boundAccountFallbackPolicy = binding.fallback === "next" ? "pool" : "strict";
3376
+ } else if (target.kind === "account-group") {
3377
+ config.boundAccountGroup = target.group;
3378
+ config.boundAccountFallbackPolicy = binding.fallback === "next" ? "pool" : "strict";
3379
+ } else if (target.kind === "provider" && target.keyId) {
3380
+ config.boundKeyId = target.keyId;
3381
+ config.boundKeyFallbackPolicy = binding.fallback === "next" ? "pool" : "strict";
3382
+ }
3383
+ return config;
3384
+ }
3385
+ function candidateGatewayBindings(bindings, apiKeyId, endpoint) {
3386
+ const candidates = (bindings ?? []).filter(
3387
+ (binding) => binding.enabled && binding.endpoint === endpoint && gatewayBindingAllowsKey(binding, apiKeyId)
3388
+ );
3389
+ const scopeOf = (binding) => binding.keyScope ?? (binding.apiKeyIds?.length ? "selected" : "all");
3390
+ const scoped = candidates.filter((binding) => scopeOf(binding) === "selected");
3391
+ const pool = scoped.length > 0 ? scoped : candidates.filter((binding) => scopeOf(binding) === "all");
3392
+ return pool.sort(
3393
+ (left, right) => (left.priority ?? 100) - (right.priority ?? 100) || left.id.localeCompare(right.id)
3394
+ );
3395
+ }
3396
+ function candidateBackgroundModelIds(bindings, apiKeyId, endpoint) {
3397
+ const ids = [
3398
+ ...new Set(
3399
+ candidateGatewayBindings(bindings, apiKeyId, endpoint).flatMap(
3400
+ (binding) => binding.backgroundModelIds ?? []
3401
+ )
3402
+ )
3403
+ ];
3404
+ return ids.length > 0 ? ids : void 0;
3405
+ }
3406
+ function resolveGatewayBinding(input) {
3407
+ const candidates = candidateGatewayBindings(input.bindings, input.apiKeyId, input.endpoint);
3408
+ const binding = candidates.find(
3409
+ (candidate) => candidate.fallback === "fail" || routeCanServe(candidate, input.requestedModel, input.role)
3410
+ ) ?? candidates[0];
3411
+ if (!binding) return { source: "none" };
3412
+ return {
3413
+ source: "binding",
3414
+ binding,
3415
+ config: gatewayBindingToEndpointConfig(binding, input.requestedModel)
3416
+ };
3417
+ }
3418
+
2512
3419
  // src/outbound-api/outboundApiKeyAuth.ts
2513
3420
  import { createHash, randomBytes as randomBytes2 } from "crypto";
2514
3421
 
@@ -2591,7 +3498,30 @@ async function createNamedKey(db, name) {
2591
3498
  id,
2592
3499
  name,
2593
3500
  keyHash: hashKey(secret),
2594
- keyPrefix: prefix
3501
+ keyPrefix: prefix,
3502
+ plaintext: secret
3503
+ });
3504
+ return {
3505
+ id: row.id,
3506
+ name: row.name,
3507
+ keyPrefix: row.keyPrefix,
3508
+ createdAt: row.createdAt,
3509
+ plaintextOnce: secret
3510
+ };
3511
+ }
3512
+ async function createIntegrationKey(db, name, allowedEndpoints = ["responses", "messages"]) {
3513
+ const secret = generateSecret();
3514
+ const id = newKeyId();
3515
+ const prefix = keyPrefix(secret);
3516
+ const row = await db.outboundApiKeysCreate({
3517
+ id,
3518
+ name,
3519
+ keyHash: hashKey(secret),
3520
+ keyPrefix: prefix,
3521
+ kind: "integration",
3522
+ allowedEndpoints: [...new Set(allowedEndpoints)],
3523
+ loopbackOnly: true,
3524
+ plaintext: secret
2595
3525
  });
2596
3526
  return {
2597
3527
  id: row.id,
@@ -2623,7 +3553,12 @@ function extractModelRestriction(row) {
2623
3553
  };
2624
3554
  }
2625
3555
  function toVerifiedKey(row) {
2626
- const key = { id: row.id };
3556
+ const key = {
3557
+ id: row.id,
3558
+ kind: row.kind,
3559
+ allowedEndpoints: row.allowedEndpoints,
3560
+ loopbackOnly: row.loopbackOnly
3561
+ };
2627
3562
  if (row.maxConcurrency !== void 0 && row.maxConcurrency !== null) {
2628
3563
  key.maxConcurrency = row.maxConcurrency;
2629
3564
  }
@@ -3267,10 +4202,10 @@ function getFallbackRedeemLimiter() {
3267
4202
  fallbackRedeemLimiter ??= new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
3268
4203
  return fallbackRedeemLimiter;
3269
4204
  }
3270
- function writeJsonError(res, status, message, headers = {}) {
4205
+ function writeJsonError(res, status, message, headers = {}, details = {}) {
3271
4206
  if (res.headersSent) return;
3272
4207
  res.writeHead(status, { "Content-Type": "application/json", ...headers });
3273
- res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
4208
+ res.end(JSON.stringify({ error: { type: "outbound_api_error", message, ...details } }));
3274
4209
  }
3275
4210
  function writeCostLimitError(res, scope, limitUsd, spentUsd) {
3276
4211
  if (res.headersSent) return;
@@ -3314,6 +4249,11 @@ function extractPresentedKey(req) {
3314
4249
  const g = Array.isArray(goog) ? goog[0] : goog;
3315
4250
  return g?.trim() || void 0;
3316
4251
  }
4252
+ function isLoopbackPeer(address) {
4253
+ if (!address) return false;
4254
+ const normalized = address.toLowerCase().split("%")[0];
4255
+ return normalized === "::1" || normalized === "127.0.0.1" || normalized.startsWith("127.") || normalized.startsWith("::ffff:127.");
4256
+ }
3317
4257
  function selectEndpoint(method, url) {
3318
4258
  if (method !== "POST" || !url) return null;
3319
4259
  const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
@@ -3337,9 +4277,33 @@ function isModelsListRequest(url) {
3337
4277
  const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
3338
4278
  return path.endsWith("/models");
3339
4279
  }
3340
- function writeChatModelsList(res, config) {
3341
- const chat = config.endpoints.find((e) => e.endpoint === "chat");
3342
- const data = (chat?.models ?? []).map((ref) => parseModelRef(ref)).filter((p) => p !== null).map((p) => ({ id: p.modelId, object: "model", owned_by: "omnicross" }));
4280
+ function writeModelsList(res, config, apiKeyId, allowedEndpoints) {
4281
+ const refs = [];
4282
+ const endpointConfigs = [];
4283
+ for (const endpoint of ["chat", "responses", "messages", "gemini"]) {
4284
+ if (allowedEndpoints && !allowedEndpoints.includes(endpoint)) continue;
4285
+ endpointConfigs.push(
4286
+ ...candidateGatewayBindings(config.bindings, apiKeyId, endpoint).map(
4287
+ (binding) => gatewayBindingToEndpointConfig(binding)
4288
+ )
4289
+ );
4290
+ }
4291
+ for (const endpoint of endpointConfigs) {
4292
+ if (allowedEndpoints && !allowedEndpoints.includes(endpoint.endpoint)) continue;
4293
+ if (endpoint.endpoint === "chat") refs.push(...endpoint.models ?? []);
4294
+ else if (endpoint.endpoint === "messages" || endpoint.endpoint === "responses") {
4295
+ refs.push(...Object.values(endpoint.modelMap ?? {}));
4296
+ } else {
4297
+ if (endpoint.defaultModel) refs.push(endpoint.defaultModel);
4298
+ if (endpoint.backgroundModel) refs.push(endpoint.backgroundModel);
4299
+ }
4300
+ }
4301
+ const seen = /* @__PURE__ */ new Set();
4302
+ const data = refs.map((ref) => parseModelRef(ref)).filter((p) => p !== null).filter((p) => {
4303
+ if (seen.has(p.modelId)) return false;
4304
+ seen.add(p.modelId);
4305
+ return true;
4306
+ }).map((p) => ({ id: p.modelId, object: "model", owned_by: "omnicross" }));
3343
4307
  res.writeHead(200, { "Content-Type": "application/json" });
3344
4308
  res.end(JSON.stringify({ object: "list", data }));
3345
4309
  }
@@ -3373,7 +4337,15 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
3373
4337
  const verified = verification.key;
3374
4338
  if (audit) audit.keyId = verified.id;
3375
4339
  if (billing) billing.keyId = verified.id;
4340
+ if (verified.loopbackOnly && !isLoopbackPeer(req.socket?.remoteAddress)) {
4341
+ writeJsonError(res, 403, "This integration key is restricted to loopback clients");
4342
+ return;
4343
+ }
3376
4344
  if (isRedeemRequest(req.method, req.url)) {
4345
+ if (verified.kind === "integration") {
4346
+ writeJsonError(res, 403, "Integration keys cannot redeem vouchers");
4347
+ return;
4348
+ }
3377
4349
  await handleVoucherRedeem(
3378
4350
  req,
3379
4351
  res,
@@ -3395,7 +4367,11 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
3395
4367
  return;
3396
4368
  }
3397
4369
  if (req.method === "GET" && isModelsListRequest(req.url)) {
3398
- writeChatModelsList(res, config);
4370
+ if (verified.allowedEndpoints && verified.allowedEndpoints.length === 0) {
4371
+ writeJsonError(res, 403, "API key is not allowed to access this endpoint");
4372
+ return;
4373
+ }
4374
+ writeModelsList(res, config, verified.id, verified.allowedEndpoints);
3399
4375
  return;
3400
4376
  }
3401
4377
  const endpoint = selectEndpoint(req.method, req.url);
@@ -3403,9 +4379,12 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
3403
4379
  writeJsonError(res, 404, `Unsupported: ${req.method} ${req.url}`);
3404
4380
  return;
3405
4381
  }
3406
- const endpointConfig = config.endpoints.find((e) => e.endpoint === endpoint);
3407
- if (!endpointConfig) {
3408
- writeJsonError(res, 503, `endpoint '${endpoint}' is not configured`);
4382
+ if (verified.allowedEndpoints && !verified.allowedEndpoints.includes(endpoint)) {
4383
+ writeJsonError(res, 403, "API key is not allowed to access this endpoint");
4384
+ return;
4385
+ }
4386
+ if (candidateGatewayBindings(config.bindings, verified.id, endpoint).length === 0) {
4387
+ writeJsonError(res, 503, `endpoint '${endpoint}' has no downstream route for this key`);
3409
4388
  return;
3410
4389
  }
3411
4390
  if (verified.costLimits && deps.keySpendTracker) {
@@ -3478,23 +4457,40 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
3478
4457
  const urlModel = extractGeminiModelFromUrl(req.url);
3479
4458
  if (urlModel) parsedBody["model"] = urlModel;
3480
4459
  }
3481
- const sessionId = deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}` : null;
4460
+ const requestedModel = extractRequestedModel(ingressFormat, parsedBody);
4461
+ const role = isKindMappedEndpoint(endpoint) || endpoint === "chat" ? void 0 : detectRequestRole(ingressFormat, parsedBody, {
4462
+ // Role detection precedes the route pick (the pick consumes the
4463
+ // role), so the hint is the union across the candidate routes.
4464
+ backgroundModelIds: candidateBackgroundModelIds(config.bindings, verified.id, endpoint)
4465
+ });
4466
+ const bindingResolution = resolveGatewayBinding({
4467
+ bindings: config.bindings,
4468
+ apiKeyId: verified.id,
4469
+ endpoint,
4470
+ requestedModel,
4471
+ role
4472
+ });
4473
+ if (bindingResolution.source === "none") {
4474
+ writeJsonError(res, 503, `endpoint '${endpoint}' has no downstream route for this key`);
4475
+ return;
4476
+ }
4477
+ const effectiveEndpointConfig = bindingResolution.config;
4478
+ const bindingAffinitySuffix = `:${bindingResolution.binding.id}`;
4479
+ const sessionId = deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}${bindingAffinitySuffix}` : null;
3482
4480
  const resolved = isKindMappedEndpoint(endpoint) || endpoint === "chat" ? await resolveRoute({
3483
- config: endpointConfig,
4481
+ config: effectiveEndpointConfig,
3484
4482
  ingressFormat,
3485
4483
  llmConfig: deps.llmConfig,
3486
4484
  sessionId,
3487
4485
  // Capture the ORIGINAL requested id BEFORE any downstream swap; for
3488
4486
  // kind-mapped endpoints it selects the kind AND is stamped onto
3489
4487
  // `route.requestedModel`; for chat it is matched against the list.
3490
- requestedModel: extractRequestedModel(ingressFormat, parsedBody),
4488
+ requestedModel,
3491
4489
  // Attribution: stamp the verified named-key id onto the route.
3492
4490
  apiKeyId: verified.id
3493
4491
  }) : await resolveRoute({
3494
- config: endpointConfig,
3495
- role: detectRequestRole(ingressFormat, parsedBody, {
3496
- backgroundModelIds: endpointConfig.backgroundModelIds
3497
- }),
4492
+ config: effectiveEndpointConfig,
4493
+ role,
3498
4494
  ingressFormat,
3499
4495
  llmConfig: deps.llmConfig,
3500
4496
  sessionId,
@@ -3555,12 +4551,29 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
3555
4551
  const replay = makeReplayRequest(req, rawBody);
3556
4552
  await routeRequest(replay, res, routeMap, deps.proxyDeps);
3557
4553
  } catch (err) {
4554
+ if (isBoundAccountSelectionError(err)) {
4555
+ const headers = {};
4556
+ if (err.status === 429 && err.resumeAt) {
4557
+ const resumeMs = Date.parse(err.resumeAt);
4558
+ if (Number.isFinite(resumeMs)) {
4559
+ headers["Retry-After"] = String(Math.max(1, Math.ceil((resumeMs - Date.now()) / 1e3)));
4560
+ }
4561
+ }
4562
+ writeJsonError(
4563
+ res,
4564
+ err.status,
4565
+ boundAccountSelectionMessage(err.reason),
4566
+ headers,
4567
+ { code: err.code, reason: err.reason }
4568
+ );
4569
+ return;
4570
+ }
3558
4571
  const message = serializeError(err);
3559
4572
  if (deps.logger) deps.logger.error("[OutboundApi] dispatch error:", message);
3560
4573
  else console.error("[OutboundApi] dispatch error:", message);
3561
4574
  emitWebhookEvent({ kind: "server.error", at: Date.now(), message });
3562
4575
  if (audit) audit.error = message;
3563
- writeJsonError(res, 502, message);
4576
+ writeJsonError(res, isAccountAllowanceExhaustedError(err) ? 429 : 502, message);
3564
4577
  } finally {
3565
4578
  routeMap.removeRoute(token);
3566
4579
  if (releaseSerial) releaseSerial();
@@ -3584,16 +4597,6 @@ function makeReplayRequest(req, rawBody) {
3584
4597
  var DEFAULT_OUTBOUND_PORT = 8765;
3585
4598
  var LOOPBACK_ADDR = "127.0.0.1";
3586
4599
  var LAN_ADDR = "0.0.0.0";
3587
- var OutboundApiConfigError = class extends Error {
3588
- missing;
3589
- constructor(missing) {
3590
- super(
3591
- "Outbound API server cannot start: incomplete model-kind configuration \u2014 " + missing.map((m) => `${m.endpoint} missing [${m.missingKinds.join(", ")}]`).join("; ")
3592
- );
3593
- this.name = "OutboundApiConfigError";
3594
- this.missing = missing;
3595
- }
3596
- };
3597
4600
  var OutboundApiServer = class {
3598
4601
  constructor(deps, onPortChange) {
3599
4602
  this.deps = deps;
@@ -3605,6 +4608,7 @@ var OutboundApiServer = class {
3605
4608
  boundPort = 0;
3606
4609
  boundAddr = LOOPBACK_ADDR;
3607
4610
  endpoints = [];
4611
+ bindings = [];
3608
4612
  userMessageQueue;
3609
4613
  concurrencyQueue;
3610
4614
  voucherConfig;
@@ -3631,6 +4635,7 @@ var OutboundApiServer = class {
3631
4635
  */
3632
4636
  async applyConfig(input) {
3633
4637
  this.endpoints = input.endpoints;
4638
+ this.bindings = input.bindings ?? [];
3634
4639
  this.userMessageQueue = input.userMessageQueue;
3635
4640
  this.concurrencyQueue = input.concurrencyQueue;
3636
4641
  this.voucherConfig = input.voucher;
@@ -3640,16 +4645,6 @@ var OutboundApiServer = class {
3640
4645
  await this.stop();
3641
4646
  return;
3642
4647
  }
3643
- const missing = validateServerModelConfig({
3644
- enabled: true,
3645
- networkBinding: input.networkBinding,
3646
- endpoints: input.endpoints,
3647
- port: input.port
3648
- });
3649
- if (missing.length > 0) {
3650
- await this.stop();
3651
- throw new OutboundApiConfigError(missing);
3652
- }
3653
4648
  const running = this.server !== null;
3654
4649
  const bindChanged = running && (this.boundAddr !== wantAddr || this.boundPort !== wantPort);
3655
4650
  if (running && !bindChanged) {
@@ -3706,6 +4701,7 @@ var OutboundApiServer = class {
3706
4701
  this.deps,
3707
4702
  {
3708
4703
  endpoints: this.endpoints,
4704
+ bindings: this.bindings,
3709
4705
  userMessageQueue: this.userMessageQueue,
3710
4706
  concurrencyQueue: this.concurrencyQueue,
3711
4707
  voucher: this.voucherConfig
@@ -3937,7 +4933,7 @@ async function callOpenAICompletion(provider, apiKey, options, logger) {
3937
4933
  model: options.model,
3938
4934
  messages: options.messages.map((m) => convertMessageToOpenAI(m)),
3939
4935
  // Only set max_tokens if explicitly provided, otherwise let API use its default
3940
- ...options.maxTokens ? { max_tokens: options.maxTokens } : {},
4936
+ ...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
3941
4937
  temperature: options.temperature,
3942
4938
  stream: false
3943
4939
  // For now, non-streaming only
@@ -3988,7 +4984,7 @@ async function callAnthropicCompletion(provider, apiKey, options, logger) {
3988
4984
  const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
3989
4985
  const anthropicRequest2 = {
3990
4986
  model: options.model,
3991
- max_tokens: options.maxTokens || 16384,
4987
+ max_tokens: options.maxTokens ?? 16384,
3992
4988
  temperature: options.temperature,
3993
4989
  ...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
3994
4990
  messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
@@ -4034,7 +5030,7 @@ async function callAnthropicCompletion(provider, apiKey, options, logger) {
4034
5030
  content: m.content
4035
5031
  })),
4036
5032
  // Anthropic requires max_tokens; use 16384 default if not explicitly set
4037
- max_tokens: options.maxTokens || 16384,
5033
+ max_tokens: options.maxTokens ?? 16384,
4038
5034
  temperature: options.temperature,
4039
5035
  stream: false
4040
5036
  };
@@ -4089,7 +5085,7 @@ async function callGeminiCompletion(provider, apiKey, options, logger) {
4089
5085
  const request = {
4090
5086
  contents,
4091
5087
  generationConfig: {
4092
- ...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
5088
+ ...options.maxTokens !== void 0 ? { maxOutputTokens: options.maxTokens } : {},
4093
5089
  ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
4094
5090
  }
4095
5091
  };
@@ -4158,7 +5154,7 @@ async function callOpenAIResponseCompletion(provider, apiKey, options, logger) {
4158
5154
  model: options.model,
4159
5155
  input,
4160
5156
  stream: false,
4161
- ...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
5157
+ ...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
4162
5158
  ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
4163
5159
  };
4164
5160
  if (options.thinkLevel && options.thinkLevel !== "none") {
@@ -4223,7 +5219,7 @@ async function streamOpenAICompletion(provider, apiKey, options, messageId, call
4223
5219
  model: options.model,
4224
5220
  messages: options.messages.map((m) => convertMessageToOpenAI(m)),
4225
5221
  // Only set max_tokens if explicitly provided, otherwise let API use its default
4226
- ...options.maxTokens ? { max_tokens: options.maxTokens } : {},
5222
+ ...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
4227
5223
  temperature: options.temperature,
4228
5224
  stream: true
4229
5225
  };
@@ -4282,7 +5278,7 @@ async function streamOpenAICompletion(provider, apiKey, options, messageId, call
4282
5278
  async function streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, logger) {
4283
5279
  const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
4284
5280
  const MAX_TOKENS_FOR_THINKING = 16384;
4285
- let effectiveMaxTokens = options.maxTokens || 16384;
5281
+ let effectiveMaxTokens = options.maxTokens ?? 16384;
4286
5282
  const thinkingMaxTokens = options.thinkLevel && options.thinkLevel !== "none" ? Math.min(effectiveMaxTokens, MAX_TOKENS_FOR_THINKING) : effectiveMaxTokens;
4287
5283
  const thinkingConfig = options.thinkLevel && options.thinkLevel !== "none" ? buildAnthropicThinking(options.model, options.thinkLevel, thinkingMaxTokens) : void 0;
4288
5284
  if (thinkingConfig) {
@@ -4394,7 +5390,7 @@ async function streamGeminiCompletion(provider, apiKey, options, messageId, call
4394
5390
  const request = {
4395
5391
  contents,
4396
5392
  generationConfig: {
4397
- ...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
5393
+ ...options.maxTokens !== void 0 ? { maxOutputTokens: options.maxTokens } : {},
4398
5394
  ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
4399
5395
  }
4400
5396
  };
@@ -4458,7 +5454,7 @@ async function streamOpenAIResponseCompletion(provider, apiKey, options, message
4458
5454
  model: options.model,
4459
5455
  input,
4460
5456
  stream: true,
4461
- ...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
5457
+ ...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
4462
5458
  ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
4463
5459
  };
4464
5460
  if (options.thinkLevel && options.thinkLevel !== "none") {
@@ -4515,7 +5511,7 @@ import {
4515
5511
  getClaudeMaxTokens,
4516
5512
  isReasoningModel
4517
5513
  } from "@omnicross/contracts/thinking-config";
4518
- async function resolveEffectiveMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens) {
5514
+ async function resolveEffectiveMaxTokens(llmConfig, _getProvider, logger, _providerId, _modelId, sessionMaxTokens) {
4519
5515
  if (sessionMaxTokens !== void 0 && sessionMaxTokens > 0) {
4520
5516
  logger.debug("Using session maxTokens", { sessionMaxTokens });
4521
5517
  return sessionMaxTokens;
@@ -4529,45 +5525,6 @@ async function resolveEffectiveMaxTokens(llmConfig, getProvider, logger, provide
4529
5525
  } catch (err) {
4530
5526
  logger.warn("Failed to get global params", err instanceof Error ? err : void 0);
4531
5527
  }
4532
- const MAX_TOKENS_CAP = 131072;
4533
- const provider = await getProvider(providerId);
4534
- if (provider) {
4535
- const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
4536
- if (modelConfig?.maxTokens && modelConfig.maxTokens > 0) {
4537
- const cappedMaxTokens = Math.min(modelConfig.maxTokens, MAX_TOKENS_CAP);
4538
- logger.debug("Using model config maxTokens", {
4539
- maxTokens: modelConfig.maxTokens,
4540
- cappedMaxTokens
4541
- });
4542
- return cappedMaxTokens;
4543
- }
4544
- if (provider.modelGroups) {
4545
- for (const group of provider.modelGroups) {
4546
- const model = group.models?.find((m) => m.id === modelId);
4547
- if (model?.maxTokens && model.maxTokens > 0) {
4548
- const cappedMaxTokens = Math.min(model.maxTokens, MAX_TOKENS_CAP);
4549
- logger.debug("Using modelGroup model maxTokens", {
4550
- maxTokens: model.maxTokens,
4551
- cappedMaxTokens
4552
- });
4553
- return cappedMaxTokens;
4554
- }
4555
- }
4556
- }
4557
- }
4558
- try {
4559
- const discoveredMaxTokens = await llmConfig.getDiscoveredModelMaxTokens(providerId, modelId);
4560
- if (discoveredMaxTokens && discoveredMaxTokens > 0) {
4561
- const cappedMaxTokens = Math.min(discoveredMaxTokens, MAX_TOKENS_CAP);
4562
- logger.debug("Using discovered model maxTokens", {
4563
- discoveredMaxTokens,
4564
- cappedMaxTokens
4565
- });
4566
- return cappedMaxTokens;
4567
- }
4568
- } catch (err) {
4569
- logger.warn("Failed to get discovered model maxTokens", err instanceof Error ? err : void 0);
4570
- }
4571
5528
  logger.debug("No maxTokens configured, returning undefined");
4572
5529
  return void 0;
4573
5530
  }
@@ -4670,7 +5627,8 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
4670
5627
  }
4671
5628
  requestBody = {
4672
5629
  contents,
4673
- tools: options.tools
5630
+ tools: options.tools,
5631
+ ...options.maxTokens !== void 0 ? { generationConfig: { maxOutputTokens: options.maxTokens } } : {}
4674
5632
  };
4675
5633
  url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
4676
5634
  } else if (apiFormat === "anthropic") {
@@ -4680,7 +5638,10 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
4680
5638
  role: m.role,
4681
5639
  content: m.content
4682
5640
  })),
4683
- max_tokens: options.maxTokens || 4096,
5641
+ // Native Anthropic Messages requires this field. Resolve an absent
5642
+ // caller cap to the model's real ceiling rather than a small constant —
5643
+ // see `resolveAnthropicMaxTokens`.
5644
+ max_tokens: resolveAnthropicMaxTokens(actualModel, options.maxTokens),
4684
5645
  temperature: options.temperature ?? 0.7,
4685
5646
  stream: true,
4686
5647
  tools: options.tools
@@ -4697,7 +5658,7 @@ function buildToolRequest(apiFormat, conversationMessages, actualModel, options,
4697
5658
  role: m.role,
4698
5659
  content: m.content
4699
5660
  })),
4700
- max_tokens: options.maxTokens || 4096,
5661
+ ...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
4701
5662
  temperature: options.temperature ?? 0.7,
4702
5663
  stream: true,
4703
5664
  tools: options.tools
@@ -5194,6 +6155,18 @@ function getSharedExecutor2() {
5194
6155
  async function resolveChainWithMain(llmConfig, providerId, model) {
5195
6156
  return resolveProviderChain(llmConfig, providerId, model);
5196
6157
  }
6158
+ function buildUnifiedCompletionRequest(actualModel, options, stream) {
6159
+ return {
6160
+ model: actualModel,
6161
+ messages: options.messages.map((m) => ({
6162
+ role: m.role,
6163
+ content: m.content
6164
+ })),
6165
+ ...options.maxTokens !== void 0 ? { max_tokens: options.maxTokens } : {},
6166
+ temperature: options.temperature,
6167
+ stream
6168
+ };
6169
+ }
5197
6170
  async function completeWithTransformers(options, llmConfig, getProvider, resolveApiKey2, completeFallback, logger, recording) {
5198
6171
  try {
5199
6172
  const routedInfo = await llmConfig.resolveRoutedModel(
@@ -5222,16 +6195,11 @@ async function completeWithTransformers(options, llmConfig, getProvider, resolve
5222
6195
  if (!hasTransformers) {
5223
6196
  return completeFallback(options);
5224
6197
  }
5225
- const unifiedRequest = {
5226
- model: actualModel,
5227
- messages: options.messages.map((m) => ({
5228
- role: m.role,
5229
- content: m.content
5230
- })),
5231
- max_tokens: options.maxTokens || 4096,
5232
- temperature: options.temperature,
5233
- stream: options.stream ?? false
5234
- };
6198
+ const unifiedRequest = buildUnifiedCompletionRequest(
6199
+ actualModel,
6200
+ options,
6201
+ options.stream ?? false
6202
+ );
5235
6203
  const transformerProvider = {
5236
6204
  name: provider.name,
5237
6205
  baseUrl: provider.api_base_url,
@@ -5361,16 +6329,7 @@ async function completeStreamWithTransformers(options, callbacks, llmConfig, get
5361
6329
  }
5362
6330
  const messageId = `msg_${Date.now()}`;
5363
6331
  callbacks.onStart?.(messageId);
5364
- const unifiedRequest = {
5365
- model: actualModel,
5366
- messages: options.messages.map((m) => ({
5367
- role: m.role,
5368
- content: m.content
5369
- })),
5370
- max_tokens: options.maxTokens || 4096,
5371
- temperature: options.temperature,
5372
- stream: true
5373
- };
6332
+ const unifiedRequest = buildUnifiedCompletionRequest(actualModel, options, true);
5374
6333
  const transformerProvider = {
5375
6334
  name: provider.name,
5376
6335
  baseUrl: provider.api_base_url,
@@ -5905,9 +6864,10 @@ ${description}`;
5905
6864
  * Resolve effective max_tokens value with priority:
5906
6865
  * 1. Session settings (if provided)
5907
6866
  * 2. Global model parameters (if enabled)
5908
- * 3. Model's maxTokens from provider config
5909
- * 4. Discovered models cache (from API)
5910
- * 5. undefined - let API use its default
6867
+ * 3. undefined - let API use its default
6868
+ *
6869
+ * Provider and discovery maxTokens values are capability metadata, not
6870
+ * implicit per-request output limits.
5911
6871
  */
5912
6872
  async resolveEffectiveMaxTokens(providerId, modelId, sessionMaxTokens) {
5913
6873
  return resolveEffectiveMaxTokens(
@@ -6020,6 +6980,7 @@ export {
6020
6980
  relayResponse,
6021
6981
  aggregateAnthropicSseToJsonBody,
6022
6982
  writeError,
6983
+ writeBoundAccountError,
6023
6984
  resolvePoolBoundKey,
6024
6985
  extractRouteToken,
6025
6986
  isLoopbackAddress,
@@ -6031,6 +6992,8 @@ export {
6031
6992
  DEFAULT_USER_MESSAGE_QUEUE,
6032
6993
  DEFAULT_CONCURRENCY_QUEUE,
6033
6994
  DEFAULT_ACCOUNT_PROBE,
6995
+ DEFAULT_ALLOWANCE_SCHEDULING,
6996
+ normalizeAllowanceScheduling,
6034
6997
  normalizeAccountProbe,
6035
6998
  normalizeAudit,
6036
6999
  normalizeBilling,
@@ -6043,16 +7006,23 @@ export {
6043
7006
  normalizeWebhookSegment,
6044
7007
  normalizeQueueSegments,
6045
7008
  normalizePrefixTargets,
7009
+ legacyEndpointsToBindings,
7010
+ normalizeGatewayBindings,
6046
7011
  defaultServerConfig,
6047
7012
  normalizeServerConfig,
6048
7013
  loadServerConfig,
6049
7014
  saveServerConfig,
6050
7015
  mergeServerConfig,
7016
+ gatewayBindingToEndpointConfig,
7017
+ candidateGatewayBindings,
7018
+ candidateBackgroundModelIds,
7019
+ resolveGatewayBinding,
6051
7020
  computeKeyExpiry,
6052
7021
  checkKeyQuota,
6053
7022
  randomBase62,
6054
7023
  hashKey,
6055
7024
  createNamedKey,
7025
+ createIntegrationKey,
6056
7026
  verifyKey,
6057
7027
  verifyPresentedKey,
6058
7028
  ConcurrencyQueueFullError,
@@ -6074,7 +7044,6 @@ export {
6074
7044
  isRedeemRequest,
6075
7045
  handleVoucherRedeem,
6076
7046
  DEFAULT_OUTBOUND_PORT,
6077
- OutboundApiConfigError,
6078
7047
  OutboundApiServer,
6079
7048
  formatUrls,
6080
7049
  startOfLocalDay,