@omnicross/core 0.1.2 → 0.1.4

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 (184) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +57 -57
  3. package/README.md +15 -15
  4. package/dist/auth/GeminiCodeAssistProjectResolver.cjs +6 -1
  5. package/dist/auth/GeminiCodeAssistProjectResolver.js +6 -1
  6. package/dist/chunk-3QFXFCHG.js +33 -0
  7. package/dist/chunk-4IH4EL7M.cjs +9 -0
  8. package/dist/chunk-4YINUUKQ.cjs +33 -0
  9. package/dist/chunk-5ENKBSWO.js +311 -0
  10. package/dist/chunk-6RPZADX3.cjs +92 -0
  11. package/dist/chunk-7Y47UCF2.js +142 -0
  12. package/dist/chunk-AEOZIDEB.cjs +32 -0
  13. package/dist/chunk-APYG5QD7.cjs +50 -0
  14. package/dist/{chunk-MNYKI4CI.js → chunk-BPKCU575.js} +29 -0
  15. package/dist/{chunk-SN3YWBX7.cjs → chunk-DKPXN34V.cjs} +2108 -538
  16. package/dist/{chunk-4NBS6KPV.cjs → chunk-DQ5VAYZ6.cjs} +7 -8
  17. package/dist/chunk-E3UCVHLP.cjs +142 -0
  18. package/dist/chunk-FCR77GYM.cjs +33 -0
  19. package/dist/{chunk-74TMJA7Z.js → chunk-HRRYG2AX.js} +8 -4
  20. package/dist/chunk-HUZZZ3JL.js +32 -0
  21. package/dist/{chunk-KUU2RNG6.cjs → chunk-HYN75H6G.cjs} +3 -0
  22. package/dist/{chunk-QOCNX236.js → chunk-IAXWICZU.js} +24 -1
  23. package/dist/{chunk-JO5NLTLY.js → chunk-IK3KE3AU.js} +3 -0
  24. package/dist/{chunk-V5KPWNYX.cjs → chunk-JMJBSACD.cjs} +3 -3
  25. package/dist/chunk-K5NM7VAH.js +33 -0
  26. package/dist/{chunk-N3V2J5ZO.cjs → chunk-KFI44N2R.cjs} +8 -4
  27. package/dist/chunk-L74VDN4A.cjs +32 -0
  28. package/dist/chunk-OCEUVQHW.cjs +112 -0
  29. package/dist/chunk-OE6TDIWW.js +22 -0
  30. package/dist/chunk-OZFM4X3S.js +18 -0
  31. package/dist/{chunk-LKZJEL6E.cjs → chunk-Q27JY5RG.cjs} +6 -4
  32. package/dist/{chunk-SPVWWUHX.cjs → chunk-QWBWGODS.cjs} +24 -1
  33. package/dist/{chunk-2FEVTJWG.js → chunk-QYTT6GE3.js} +1929 -359
  34. package/dist/{chunk-UYPEN5XE.cjs → chunk-RGTR7CIO.cjs} +6 -6
  35. package/dist/chunk-RPII3E6Z.cjs +145 -0
  36. package/dist/{chunk-XBSYYZIY.cjs → chunk-SNYBEXLB.cjs} +31 -2
  37. package/dist/{chunk-2DCNB7DF.cjs → chunk-SU2GCLAC.cjs} +30 -10
  38. package/dist/{chunk-5HTVET6E.js → chunk-TIER4SUC.js} +24 -4
  39. package/dist/chunk-TKJMZBAH.js +32 -0
  40. package/dist/{chunk-ZSVQT3PW.js → chunk-UBWEBKRC.js} +5 -6
  41. package/dist/chunk-UHGVBFU3.js +385 -0
  42. package/dist/chunk-UIBD2YY5.cjs +22 -0
  43. package/dist/chunk-UNEFIWXI.cjs +32 -0
  44. package/dist/chunk-VACWOIRE.cjs +385 -0
  45. package/dist/{chunk-O466Y272.js → chunk-VPBAUQ2M.js} +7 -7
  46. package/dist/chunk-VQY5W4YW.cjs +311 -0
  47. package/dist/chunk-WLYIIEAF.js +50 -0
  48. package/dist/chunk-WNY4WMRF.js +32 -0
  49. package/dist/chunk-XEKD23J5.js +9 -0
  50. package/dist/chunk-XK5CIRMQ.js +112 -0
  51. package/dist/chunk-XSVNC2UD.cjs +18 -0
  52. package/dist/chunk-XTEI64OU.js +145 -0
  53. package/dist/chunk-XX6NQJMA.js +92 -0
  54. package/dist/{chunk-745DV5FL.js → chunk-Y7FO65VT.js} +3 -3
  55. package/dist/{chunk-PXUJF5HS.js → chunk-ZY6KU6P3.js} +4 -2
  56. package/dist/completion/CompletionService.cjs +30 -17
  57. package/dist/completion/CompletionService.js +29 -16
  58. package/dist/completion/NativeSearchInjector.cjs +3 -3
  59. package/dist/completion/NativeSearchInjector.js +2 -2
  60. package/dist/completion/openrouter-headers.cjs +3 -6
  61. package/dist/completion/openrouter-headers.d.cts +9 -12
  62. package/dist/completion/openrouter-headers.d.ts +9 -12
  63. package/dist/completion/openrouter-headers.js +2 -5
  64. package/dist/completion/openrouter-models.cjs +4 -3
  65. package/dist/completion/openrouter-models.js +2 -1
  66. package/dist/completion.cjs +30 -17
  67. package/dist/completion.js +29 -16
  68. package/dist/index.cjs +151 -20
  69. package/dist/index.d.cts +44 -10
  70. package/dist/index.d.ts +44 -10
  71. package/dist/index.js +160 -29
  72. package/dist/keyPolicy-Cp0FntZl.d.cts +90 -0
  73. package/dist/keyPolicy-Cp0FntZl.d.ts +90 -0
  74. package/dist/outbound-api/auditCapture.cjs +9 -0
  75. package/dist/outbound-api/auditCapture.d.cts +52 -0
  76. package/dist/outbound-api/auditCapture.d.ts +52 -0
  77. package/dist/outbound-api/auditCapture.js +9 -0
  78. package/dist/outbound-api/auditRedact.cjs +8 -0
  79. package/dist/outbound-api/auditRedact.d.cts +30 -0
  80. package/dist/outbound-api/auditRedact.d.ts +30 -0
  81. package/dist/outbound-api/auditRedact.js +8 -0
  82. package/dist/outbound-api/billingCapture.cjs +8 -0
  83. package/dist/outbound-api/billingCapture.d.cts +61 -0
  84. package/dist/outbound-api/billingCapture.d.ts +61 -0
  85. package/dist/outbound-api/billingCapture.js +8 -0
  86. package/dist/outbound-api/quotaWarn.cjs +12 -0
  87. package/dist/outbound-api/quotaWarn.d.cts +43 -0
  88. package/dist/outbound-api/quotaWarn.d.ts +43 -0
  89. package/dist/outbound-api/quotaWarn.js +12 -0
  90. package/dist/outbound-api/routeResolver.cjs +5 -2
  91. package/dist/outbound-api/routeResolver.d.cts +9 -2
  92. package/dist/outbound-api/routeResolver.d.ts +9 -2
  93. package/dist/outbound-api/routeResolver.js +4 -1
  94. package/dist/outbound-api/types.cjs +6 -1
  95. package/dist/outbound-api/types.d.cts +12 -144
  96. package/dist/outbound-api/types.d.ts +12 -144
  97. package/dist/outbound-api/types.js +6 -0
  98. package/dist/outbound-api.cjs +144 -17
  99. package/dist/outbound-api.d.cts +655 -25
  100. package/dist/outbound-api.d.ts +655 -25
  101. package/dist/outbound-api.js +148 -21
  102. package/dist/pipeline/AuthSource.d.cts +29 -1
  103. package/dist/pipeline/AuthSource.d.ts +29 -1
  104. package/dist/pipeline/LlmConfigProviderAuth.cjs +3 -3
  105. package/dist/pipeline/LlmConfigProviderAuth.js +2 -2
  106. package/dist/pipeline/SubscriptionAccountHealth.cjs +22 -0
  107. package/dist/pipeline/SubscriptionAccountHealth.d.cts +235 -0
  108. package/dist/pipeline/SubscriptionAccountHealth.d.ts +235 -0
  109. package/dist/pipeline/SubscriptionAccountHealth.js +22 -0
  110. package/dist/pipeline/SubscriptionAuthSource.cjs +2 -2
  111. package/dist/pipeline/SubscriptionAuthSource.d.cts +3 -2
  112. package/dist/pipeline/SubscriptionAuthSource.d.ts +3 -2
  113. package/dist/pipeline/SubscriptionAuthSource.js +1 -1
  114. package/dist/pipeline/SubscriptionAuthStrategy.d.cts +30 -1
  115. package/dist/pipeline/SubscriptionAuthStrategy.d.ts +30 -1
  116. package/dist/pipeline/auditSink.cjs +14 -0
  117. package/dist/pipeline/auditSink.d.cts +48 -0
  118. package/dist/pipeline/auditSink.d.ts +48 -0
  119. package/dist/pipeline/auditSink.js +14 -0
  120. package/dist/pipeline/auditUsageStash.cjs +10 -0
  121. package/dist/pipeline/auditUsageStash.d.cts +37 -0
  122. package/dist/pipeline/auditUsageStash.d.ts +37 -0
  123. package/dist/pipeline/auditUsageStash.js +10 -0
  124. package/dist/pipeline/billingEmit.cjs +14 -0
  125. package/dist/pipeline/billingEmit.d.cts +49 -0
  126. package/dist/pipeline/billingEmit.d.ts +49 -0
  127. package/dist/pipeline/billingEmit.js +14 -0
  128. package/dist/pipeline/executeProviderCall.cjs +2 -2
  129. package/dist/pipeline/executeProviderCall.js +1 -1
  130. package/dist/pipeline/upstreamFetch.cjs +14 -0
  131. package/dist/pipeline/upstreamFetch.d.cts +83 -0
  132. package/dist/pipeline/upstreamFetch.d.ts +83 -0
  133. package/dist/pipeline/upstreamFetch.js +14 -0
  134. package/dist/pipeline/webhookEmit.cjs +10 -0
  135. package/dist/pipeline/webhookEmit.d.cts +36 -0
  136. package/dist/pipeline/webhookEmit.d.ts +36 -0
  137. package/dist/pipeline/webhookEmit.js +10 -0
  138. package/dist/ports/usage-event-store.d.cts +26 -1
  139. package/dist/ports/usage-event-store.d.ts +26 -1
  140. package/dist/ports.d.cts +8 -1
  141. package/dist/ports.d.ts +8 -1
  142. package/dist/provider-proxy/ProviderProxy.cjs +30 -17
  143. package/dist/provider-proxy/ProviderProxy.js +29 -16
  144. package/dist/provider-proxy/identity/SubscriptionIdentityStore.cjs +15 -0
  145. package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.cts +121 -0
  146. package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.ts +121 -0
  147. package/dist/provider-proxy/identity/SubscriptionIdentityStore.js +15 -0
  148. package/dist/provider-proxy/identity/fingerprintHeaders.cjs +20 -0
  149. package/dist/provider-proxy/identity/fingerprintHeaders.d.cts +89 -0
  150. package/dist/provider-proxy/identity/fingerprintHeaders.d.ts +89 -0
  151. package/dist/provider-proxy/identity/fingerprintHeaders.js +20 -0
  152. package/dist/provider-proxy/ingress/providerProxyShared.cjs +30 -17
  153. package/dist/provider-proxy/ingress/providerProxyShared.d.cts +11 -2
  154. package/dist/provider-proxy/ingress/providerProxyShared.d.ts +11 -2
  155. package/dist/provider-proxy/ingress/providerProxyShared.js +29 -16
  156. package/dist/provider-proxy/matchText.cjs +4 -2
  157. package/dist/provider-proxy/matchText.d.cts +14 -1
  158. package/dist/provider-proxy/matchText.d.ts +14 -1
  159. package/dist/provider-proxy/matchText.js +3 -1
  160. package/dist/provider-proxy/types.d.cts +30 -0
  161. package/dist/provider-proxy/types.d.ts +30 -0
  162. package/dist/provider-proxy.cjs +30 -17
  163. package/dist/provider-proxy.js +29 -16
  164. package/dist/{routeResolver-HE-ZO0fO.d.ts → routeResolver-C4T7yClx.d.ts} +36 -10
  165. package/dist/{routeResolver-BrbK6ja9.d.cts → routeResolver-CB3tvJKy.d.cts} +36 -10
  166. package/dist/transformer/transformers/OpenAIResponseTransformer.cjs +2 -2
  167. package/dist/transformer/transformers/OpenAIResponseTransformer.js +1 -1
  168. package/dist/transformer/transformers.cjs +7 -7
  169. package/dist/transformer/transformers.js +8 -8
  170. package/dist/transformer.cjs +9 -9
  171. package/dist/transformer.js +8 -8
  172. package/dist/types-B8Arpxn0.d.cts +754 -0
  173. package/dist/types-lgPYQuZS.d.ts +754 -0
  174. package/dist/usage/usage-recorder.cjs +3 -2
  175. package/dist/usage/usage-recorder.d.cts +19 -1
  176. package/dist/usage/usage-recorder.d.ts +19 -1
  177. package/dist/usage/usage-recorder.js +2 -1
  178. package/dist/usage.cjs +3 -2
  179. package/dist/usage.js +2 -1
  180. package/package.json +64 -62
  181. package/dist/chunk-3MEACFK3.js +0 -193
  182. package/dist/chunk-6VIXXLMX.cjs +0 -14
  183. package/dist/chunk-E3WHL7CO.js +0 -14
  184. package/dist/chunk-G2FUJNA2.cjs +0 -193
@@ -1,9 +1,15 @@
1
- import {
2
- collectMatchText
3
- } from "./chunk-QOCNX236.js";
4
1
  import {
5
2
  streamSSEResponse
6
3
  } from "./chunk-VVEHS2LI.js";
4
+ import {
5
+ AnthropicTransformer
6
+ } from "./chunk-FTQ7IQUC.js";
7
+ import {
8
+ GeminiTransformer
9
+ } from "./chunk-C7PSERMH.js";
10
+ import {
11
+ OpenAIResponseTransformer
12
+ } from "./chunk-TIER4SUC.js";
7
13
  import {
8
14
  TransformerChainExecutor
9
15
  } from "./chunk-G2UTNSGU.js";
@@ -11,58 +17,87 @@ import {
11
17
  injectExtendedContextBeta
12
18
  } from "./chunk-MZNPGW5Q.js";
13
19
  import {
14
- GeminiTransformer
15
- } from "./chunk-C7PSERMH.js";
20
+ getSharedIdentityStore
21
+ } from "./chunk-XTEI64OU.js";
16
22
  import {
17
- OpenAIResponseTransformer
18
- } from "./chunk-5HTVET6E.js";
23
+ applyFingerprint,
24
+ captureCallerIdentity
25
+ } from "./chunk-XX6NQJMA.js";
19
26
  import {
20
- AnthropicTransformer
21
- } from "./chunk-FTQ7IQUC.js";
27
+ collectMatchText,
28
+ deriveSubscriptionSessionKey
29
+ } from "./chunk-IAXWICZU.js";
30
+ import {
31
+ emitWebhookEvent
32
+ } from "./chunk-OE6TDIWW.js";
22
33
  import {
23
34
  getGeminiCodeAssistResolver
24
35
  } from "./chunk-DRWHTIIZ.js";
25
36
  import {
26
37
  executeProviderCall
27
- } from "./chunk-JO5NLTLY.js";
38
+ } from "./chunk-IK3KE3AU.js";
28
39
  import {
29
40
  LlmConfigProviderAuth,
30
41
  getProviderHeaders
31
- } from "./chunk-PXUJF5HS.js";
42
+ } from "./chunk-ZY6KU6P3.js";
32
43
  import {
33
44
  resolveProviderChain
34
45
  } from "./chunk-JWC6APFQ.js";
35
46
  import {
36
47
  resolveSubscriptionChain
37
48
  } from "./chunk-5ERIZW3D.js";
49
+ import {
50
+ getSharedAccountHealth,
51
+ resolveResetSeconds
52
+ } from "./chunk-5ENKBSWO.js";
38
53
  import {
39
54
  SubscriptionAuthSource
40
- } from "./chunk-74TMJA7Z.js";
55
+ } from "./chunk-HRRYG2AX.js";
41
56
  import {
42
57
  serializeError
43
58
  } from "./chunk-AVYJ4YBN.js";
59
+ import {
60
+ beginBillingCapture
61
+ } from "./chunk-WLYIIEAF.js";
62
+ import {
63
+ computeQuotaWarnings,
64
+ markQuotaWarnedOnce
65
+ } from "./chunk-3QFXFCHG.js";
66
+ import {
67
+ detectRequestRole,
68
+ endpointToIngressFormat,
69
+ extractRequestedModel,
70
+ isKindMappedEndpoint,
71
+ modelKindsForEndpoint,
72
+ parseModelRef,
73
+ resolveRoute,
74
+ validateServerModelConfig
75
+ } from "./chunk-UHGVBFU3.js";
44
76
  import {
45
77
  buildProviderApiUrl,
46
78
  resolveApiFormat,
47
79
  resolveProviderEndpoint
48
80
  } from "./chunk-H5JUT3KV.js";
49
81
  import {
50
- resolveRoute
51
- } from "./chunk-3MEACFK3.js";
82
+ beginAuditCapture
83
+ } from "./chunk-7Y47UCF2.js";
52
84
  import {
53
85
  convertAnthropicToOpenAI,
54
86
  convertOpenAIToAnthropic
55
87
  } from "./chunk-5ZQBEOMD.js";
56
88
  import {
57
- applyAugmentation
58
- } from "./chunk-745DV5FL.js";
89
+ fetchUpstream
90
+ } from "./chunk-XK5CIRMQ.js";
59
91
  import {
60
- OPENROUTER_APP_HEADERS,
61
- isOpenRouterProvider
62
- } from "./chunk-E3WHL7CO.js";
92
+ applyAugmentation
93
+ } from "./chunk-Y7FO65VT.js";
63
94
  import {
64
95
  NATIVE_SEARCH_TOOL_NAMES
65
96
  } from "./chunk-FMOBD2OQ.js";
97
+ import {
98
+ OPENROUTER_APP_HEADERS,
99
+ isOpenRouterProvider
100
+ } from "./chunk-HUZZZ3JL.js";
66
101
 
67
102
  // src/completion/StreamEventBuffer.ts
68
103
  var TERMINAL_TYPES = /* @__PURE__ */ new Set(["done", "error", "abort"]);
@@ -462,10 +497,11 @@ function recordAnthropicNonStreamUsage(recorder, bodyText, attribution) {
462
497
  sessionId: attribution.sessionId,
463
498
  providerId: attribution.providerId,
464
499
  model: attribution.model,
465
- apiKeyId: null,
500
+ apiKeyId: attribution.apiKeyId,
466
501
  engineOrigin: "codex-ingress",
467
502
  usage: tapped,
468
- rawUsage: parsed.usage
503
+ rawUsage: parsed.usage,
504
+ auditResponse: attribution.auditResponse
469
505
  });
470
506
  } catch {
471
507
  }
@@ -502,7 +538,84 @@ function readBody(req) {
502
538
  req.on("error", reject);
503
539
  });
504
540
  }
505
- async function relayResponse(res, providerResponse, isStream) {
541
+ function rewriteJsonModel(bodyText, rewriteModel) {
542
+ try {
543
+ const obj = JSON.parse(bodyText);
544
+ if (!obj || typeof obj !== "object") return bodyText;
545
+ let changed = false;
546
+ if (typeof obj.model === "string") {
547
+ obj.model = rewriteModel;
548
+ changed = true;
549
+ }
550
+ const nested = obj.response;
551
+ if (nested && typeof nested === "object" && typeof nested.model === "string") {
552
+ nested.model = rewriteModel;
553
+ changed = true;
554
+ }
555
+ return changed ? JSON.stringify(obj) : bodyText;
556
+ } catch {
557
+ return bodyText;
558
+ }
559
+ }
560
+ function rewriteSseLine(line, rewriteModel) {
561
+ if (!line.startsWith("data:") || !line.includes('"model"')) return line;
562
+ const nl = /\r?\n$/.exec(line);
563
+ const newline = nl ? nl[0] : "";
564
+ const withoutNl = newline ? line.slice(0, line.length - newline.length) : line;
565
+ const afterData = withoutNl.slice("data:".length);
566
+ const leadingSpace = afterData.startsWith(" ") ? " " : "";
567
+ const payload = afterData.slice(leadingSpace.length);
568
+ if (payload === "" || payload === "[DONE]") return line;
569
+ try {
570
+ const obj = JSON.parse(payload);
571
+ if (!obj || typeof obj !== "object") return line;
572
+ const type = typeof obj.type === "string" ? obj.type : "";
573
+ let changed = false;
574
+ if (type === "message_start") {
575
+ const message = obj.message;
576
+ if (message && typeof message === "object" && typeof message.model === "string") {
577
+ message.model = rewriteModel;
578
+ changed = true;
579
+ }
580
+ } else if (type === "response.created" || type === "response.in_progress" || type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
581
+ const response = obj.response;
582
+ if (response && typeof response === "object" && typeof response.model === "string") {
583
+ response.model = rewriteModel;
584
+ changed = true;
585
+ }
586
+ }
587
+ if (!changed) return line;
588
+ return `data:${leadingSpace}${JSON.stringify(obj)}${newline}`;
589
+ } catch {
590
+ return line;
591
+ }
592
+ }
593
+ async function relayStreamWithModelRewrite(res, body, rewriteModel) {
594
+ const reader = body.getReader();
595
+ const decoder = new TextDecoder();
596
+ let buffer = "";
597
+ try {
598
+ for (; ; ) {
599
+ const { done, value } = await reader.read();
600
+ if (done) break;
601
+ buffer += decoder.decode(value, { stream: true });
602
+ let out = "";
603
+ let nlIndex;
604
+ while ((nlIndex = buffer.indexOf("\n")) !== -1) {
605
+ const linePart = buffer.slice(0, nlIndex + 1);
606
+ buffer = buffer.slice(nlIndex + 1);
607
+ out += rewriteSseLine(linePart, rewriteModel);
608
+ }
609
+ if (out) res.write(out);
610
+ }
611
+ const tail = buffer + decoder.decode();
612
+ if (tail) res.write(rewriteSseLine(tail, rewriteModel));
613
+ } finally {
614
+ reader.releaseLock();
615
+ res.end();
616
+ }
617
+ }
618
+ async function relayResponse(res, providerResponse, isStream, rewriteModel) {
506
619
  const contentType = providerResponse.headers.get("Content-Type") ?? "";
507
620
  const status = providerResponse.status && providerResponse.status >= 100 ? providerResponse.status : 200;
508
621
  if (isStream || contentType.includes("text/event-stream")) {
@@ -515,6 +628,10 @@ async function relayResponse(res, providerResponse, isStream) {
515
628
  res.end();
516
629
  return null;
517
630
  }
631
+ if (rewriteModel) {
632
+ await relayStreamWithModelRewrite(res, providerResponse.body, rewriteModel);
633
+ return null;
634
+ }
518
635
  const reader = providerResponse.body.getReader();
519
636
  try {
520
637
  for (; ; ) {
@@ -529,8 +646,9 @@ async function relayResponse(res, providerResponse, isStream) {
529
646
  return null;
530
647
  }
531
648
  const bodyText = await providerResponse.text();
649
+ const clientText = rewriteModel ? rewriteJsonModel(bodyText, rewriteModel) : bodyText;
532
650
  res.writeHead(status, { "Content-Type": contentType.includes("json") ? contentType : "application/json" });
533
- res.end(bodyText);
651
+ res.end(clientText);
534
652
  return bodyText;
535
653
  }
536
654
  function writeError(res, status, message) {
@@ -568,14 +686,15 @@ async function buildSubscriptionPlan(res, route, deps, anthropicBody, routeModel
568
686
  resolvedModel = mapped.resolvedModel;
569
687
  scenario = mapped.scenario;
570
688
  }
571
- const plan = buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario);
689
+ const sessionKey = deriveSubscriptionSessionKey(anthropicBody);
690
+ const plan = buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario, sessionKey);
572
691
  if (!plan) {
573
692
  writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
574
693
  return null;
575
694
  }
576
695
  return plan;
577
696
  }
578
- function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario) {
697
+ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario, sessionKey) {
579
698
  const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel, route.subscriptionConfig);
580
699
  if (!upstreamUrl) return null;
581
700
  const auth = route.auth ?? new SubscriptionAuthSource(profile);
@@ -598,6 +717,7 @@ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isS
598
717
  };
599
718
  return {
600
719
  auth,
720
+ preferredAccountId: route.preferredAccountId,
601
721
  chain,
602
722
  transformerProvider,
603
723
  resolvedModel,
@@ -622,7 +742,8 @@ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isS
622
742
  upstreamUrl,
623
743
  sameFormat,
624
744
  isSubscription: true,
625
- scenario
745
+ scenario,
746
+ sessionKey
626
747
  };
627
748
  }
628
749
  function usesResponsesChain(names) {
@@ -657,12 +778,25 @@ function collectText(value) {
657
778
  }
658
779
  return 0;
659
780
  }
660
- async function runPipeline(anthropicBody, plan) {
781
+ function proxyProviderId(plan) {
782
+ return plan.isSubscription ? plan.transformerProvider.name : "byo";
783
+ }
784
+ async function runPipeline(anthropicBody, plan, reportSelection) {
661
785
  const executor = getSharedExecutor();
662
786
  const endpointTransformer = getAnthropicEndpointTransformer();
663
787
  const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
788
+ let proxyAccountId;
664
789
  const authHeaders = {};
665
- await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
790
+ await auth.applyHeaders(authHeaders, {
791
+ upstreamUrl,
792
+ model: resolvedModel,
793
+ sessionKey: plan.sessionKey,
794
+ preferredAccountId: plan.preferredAccountId,
795
+ reportSelection: (accountId, isActive) => {
796
+ proxyAccountId = accountId;
797
+ reportSelection?.(accountId, isActive);
798
+ }
799
+ });
666
800
  let rawStatus = null;
667
801
  const { response } = await executeProviderCall({
668
802
  executor,
@@ -682,7 +816,11 @@ async function runPipeline(anthropicBody, plan) {
682
816
  },
683
817
  fetchFn: (url, headers, body) => {
684
818
  console.info(`[ProviderProxy:anthropic] -> ${url} model=${resolvedModel} stream=${isStream}`);
685
- return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
819
+ return fetchUpstream(
820
+ url,
821
+ { method: "POST", headers, body: JSON.stringify(body) },
822
+ { providerId: proxyProviderId(plan), accountId: proxyAccountId }
823
+ ).then((r) => {
686
824
  rawStatus = r.status;
687
825
  return r;
688
826
  });
@@ -691,29 +829,96 @@ async function runPipeline(anthropicBody, plan) {
691
829
  });
692
830
  return { response, rawStatus };
693
831
  }
694
- async function runSubscriptionSameFormatFetch(rawBody, plan) {
832
+ async function runSubscriptionSameFormatFetch(rawBody, plan, reportSelection, options = {}) {
833
+ let proxyAccountId;
834
+ let remappedModel;
695
835
  const headers = { "content-type": "application/json" };
696
836
  await plan.auth.applyHeaders(headers, {
697
837
  upstreamUrl: plan.upstreamUrl,
698
- model: plan.resolvedModel
838
+ model: plan.resolvedModel,
839
+ sessionKey: plan.sessionKey,
840
+ preferredAccountId: plan.preferredAccountId,
841
+ reportSelection: (accountId, isActive, remapped) => {
842
+ proxyAccountId = accountId;
843
+ remappedModel = remapped;
844
+ reportSelection?.(accountId, isActive, remapped);
845
+ }
699
846
  });
847
+ if (plan.isSubscription && plan.transformerProvider.name === "claude") {
848
+ applyFingerprint(
849
+ getSharedIdentityStore(),
850
+ headers,
851
+ plan.transformerProvider.name,
852
+ proxyAccountId,
853
+ options.callerIdentity
854
+ );
855
+ }
856
+ const applyRemap = remappedModel !== void 0 && outboundRemapApplies(plan);
857
+ const outboundBody = applyRemap ? rewriteBodyModel(rawBody, remappedModel) : rawBody;
858
+ const outboundModel = applyRemap ? remappedModel : plan.resolvedModel;
700
859
  console.info(
701
- `[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${plan.resolvedModel} stream=${plan.isStream}`
860
+ `[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${outboundModel} stream=${plan.isStream}`
861
+ );
862
+ const response = await fetchUpstream(
863
+ plan.upstreamUrl,
864
+ { method: "POST", headers, body: outboundBody },
865
+ { providerId: proxyProviderId(plan), accountId: proxyAccountId }
702
866
  );
703
- const response = await fetch(plan.upstreamUrl, { method: "POST", headers, body: rawBody });
704
867
  return { response, rawStatus: response.status };
705
868
  }
706
- async function runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) {
707
- const runOnce = () => plan.sameFormat ? runSubscriptionSameFormatFetch(relayBody, plan) : runPipeline(anthropicBody, plan);
869
+ function outboundRemapApplies(plan) {
870
+ return plan.isSubscription && plan.transformerProvider.name === "claude";
871
+ }
872
+ function rewriteBodyModel(rawBody, model) {
873
+ try {
874
+ const parsed = JSON.parse(rawBody);
875
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return rawBody;
876
+ parsed.model = model;
877
+ return JSON.stringify(parsed);
878
+ } catch {
879
+ return rawBody;
880
+ }
881
+ }
882
+ async function runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan, options = {}) {
883
+ let selectedAccountId;
884
+ const reportSelection = (accountId) => {
885
+ selectedAccountId = accountId;
886
+ };
887
+ const runOnce = () => plan.sameFormat ? runSubscriptionSameFormatFetch(relayBody, plan, reportSelection, options) : runPipeline(anthropicBody, plan, reportSelection);
708
888
  const first = await runOnce();
709
- if (first.rawStatus !== 401) return first;
710
- const refreshed = await plan.auth.onUnauthorized?.();
711
- if (!refreshed) {
712
- console.warn("[ProviderProxy:anthropic] 401 not recoverable (onUnauthorized returned false)");
713
- return first;
889
+ let result = first;
890
+ if (first.rawStatus === 401) {
891
+ const refreshed = await plan.auth.onUnauthorized?.(plan.sessionKey);
892
+ if (refreshed) {
893
+ console.info("[ProviderProxy:anthropic] 401 \u2192 token refreshed; retrying once");
894
+ result = await runOnce();
895
+ } else {
896
+ console.warn("[ProviderProxy:anthropic] 401 not recoverable (onUnauthorized returned false)");
897
+ }
898
+ }
899
+ await markSubscriptionHealth(plan, selectedAccountId, result);
900
+ return result;
901
+ }
902
+ async function readBoundedBody(response, max = 2048) {
903
+ try {
904
+ const text = await response.clone().text();
905
+ return text.slice(0, max);
906
+ } catch {
907
+ return void 0;
714
908
  }
715
- console.info("[ProviderProxy:anthropic] 401 \u2192 token refreshed; retrying once");
716
- return runOnce();
909
+ }
910
+ async function markSubscriptionHealth(plan, selectedAccountId, result) {
911
+ if (!plan.isSubscription || selectedAccountId === void 0) return;
912
+ const providerId = plan.transformerProvider.name;
913
+ const status = result.rawStatus;
914
+ const { resetHeaderSeconds, retryAfterSeconds } = resolveResetSeconds(providerId, result.response.headers);
915
+ const bodyText = status === 403 ? await readBoundedBody(result.response) : void 0;
916
+ getSharedAccountHealth().recordUpstreamOutcome(providerId, selectedAccountId, {
917
+ status,
918
+ resetHeaderSeconds,
919
+ retryAfterSeconds,
920
+ bodyText
921
+ });
717
922
  }
718
923
  function isFallbackEligibleStatus(rawStatus) {
719
924
  return rawStatus === null || rawStatus >= 500 || rawStatus === 429;
@@ -728,9 +933,9 @@ function recordBreakerOutcome(profile, modelId, rawStatus) {
728
933
  if (outcome === "neutral") return;
729
934
  profile?.recordModelOutcome?.(modelId, outcome === "success");
730
935
  }
731
- async function runSubscriptionAttemptOutcome(anthropicBody, relayBody, plan) {
936
+ async function runSubscriptionAttemptOutcome(anthropicBody, relayBody, plan, options = {}) {
732
937
  try {
733
- return { kind: "result", result: await runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) };
938
+ return { kind: "result", result: await runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan, options) };
734
939
  } catch (error) {
735
940
  return { kind: "thrown", error };
736
941
  }
@@ -742,11 +947,11 @@ function settleOutcome(outcome) {
742
947
  if (outcome.kind === "thrown") throw outcome.error;
743
948
  return outcome.result;
744
949
  }
745
- async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialPlan, route, deps) {
950
+ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialPlan, route, deps, options = {}) {
746
951
  const profile = route.subscriptionProfile;
747
952
  const scenario = initialPlan.scenario;
748
953
  if (!profile?.nextFallback || scenario === void 0) {
749
- const loneOutcome = await runSubscriptionAttemptOutcome(anthropicBody, rawBody, initialPlan);
954
+ const loneOutcome = await runSubscriptionAttemptOutcome(anthropicBody, rawBody, initialPlan, options);
750
955
  recordBreakerOutcome(profile, initialPlan.resolvedModel, outcomeStatus(loneOutcome));
751
956
  return settleOutcome(loneOutcome);
752
957
  }
@@ -757,7 +962,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
757
962
  if (profile.allowModel && !profile.allowModel(initialPlan.resolvedModel)) {
758
963
  attempted.push(initialPlan.resolvedModel);
759
964
  const firstAdmitting = profile.nextFallback(scenario, attempted, route.subscriptionConfig);
760
- const gatedPlan = firstAdmitting ? buildSubscriptionIterationPlan(profile, route, deps, firstAdmitting.modelId, initialPlan.isStream, scenario) : null;
965
+ const gatedPlan = firstAdmitting ? buildSubscriptionIterationPlan(profile, route, deps, firstAdmitting.modelId, initialPlan.isStream, scenario, initialPlan.sessionKey) : null;
761
966
  if (firstAdmitting && gatedPlan) {
762
967
  console.warn(
763
968
  `[ProviderProxy:anthropic] subscription primary ${initialPlan.resolvedModel} circuit open -> first admitting fallback ${firstAdmitting.modelId}`
@@ -772,7 +977,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
772
977
  );
773
978
  }
774
979
  }
775
- let outcome = await runSubscriptionAttemptOutcome(firstBodyObj, firstRelayBody, plan);
980
+ let outcome = await runSubscriptionAttemptOutcome(firstBodyObj, firstRelayBody, plan, options);
776
981
  recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
777
982
  if (!attempted.includes(plan.resolvedModel)) attempted.push(plan.resolvedModel);
778
983
  while (attempted.length < MAX_FALLBACK_ATTEMPTS) {
@@ -785,7 +990,8 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
785
990
  deps,
786
991
  next.modelId,
787
992
  plan.isStream,
788
- scenario
993
+ scenario,
994
+ initialPlan.sessionKey
789
995
  );
790
996
  if (!nextPlan) return settleOutcome(outcome);
791
997
  const sinceLabel = outcome.kind === "thrown" ? "thrown error" : `status ${String(outcome.result.rawStatus)}`;
@@ -796,7 +1002,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
796
1002
  const fallbackRelayBody = nextPlan.sameFormat ? JSON.stringify(fallbackBodyObj) : rawBody;
797
1003
  plan = nextPlan;
798
1004
  attempted.push(next.modelId);
799
- outcome = await runSubscriptionAttemptOutcome(fallbackBodyObj, fallbackRelayBody, plan);
1005
+ outcome = await runSubscriptionAttemptOutcome(fallbackBodyObj, fallbackRelayBody, plan, options);
800
1006
  recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
801
1007
  }
802
1008
  return settleOutcome(outcome);
@@ -819,13 +1025,16 @@ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {
819
1025
  const plan = route.authMode === "subscription" ? await buildSubscriptionPlan(res, route, deps, anthropicBody, resolvedModel, isStream) : await buildByoPlan(res, route, deps, resolvedModel, isStream);
820
1026
  if (!plan) return;
821
1027
  anthropicBody.model = plan.resolvedModel;
822
- const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry(anthropicBody, rawBody, plan, route, deps) : await runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options);
823
- const bodyText = await relayResponse(res, providerResponse.response, isStream);
1028
+ const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry(anthropicBody, rawBody, plan, route, deps, options) : await runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options);
1029
+ const bodyText = await relayResponse(res, providerResponse.response, isStream, route.requestedModel);
824
1030
  if (bodyText && deps.usageRecorder) {
825
1031
  recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, {
826
1032
  sessionId: route.sessionId,
827
1033
  providerId: route.providerId ?? "anthropic",
828
- model: plan.resolvedModel
1034
+ model: plan.resolvedModel,
1035
+ apiKeyId: route.apiKeyId ?? null,
1036
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1037
+ auditResponse: res
829
1038
  });
830
1039
  }
831
1040
  } catch (err) {
@@ -881,7 +1090,7 @@ async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
881
1090
  extendedContextEnabled: route.anthropicSdkHints?.extendedContext?.enabled ?? false
882
1091
  };
883
1092
  }
884
- async function runSameFormatFetch(rawBody, plan, options, keyOverride) {
1093
+ async function runSameFormatFetch(bodyToSend, plan, options, keyOverride) {
885
1094
  const { provider, apiKey, resolvedModel, isStream, extendedContextEnabled } = plan;
886
1095
  if (!provider) {
887
1096
  throw new Error("[ProviderProxy:anthropic] same-format BYO path requires a provider row");
@@ -902,11 +1111,26 @@ async function runSameFormatFetch(rawBody, plan, options, keyOverride) {
902
1111
  injectExtendedContextBeta(headers, resolvedModel, extendedContextEnabled ?? false);
903
1112
  const url = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
904
1113
  console.info(`[ProviderProxy:anthropic] (same-format) -> ${url} model=${resolvedModel} stream=${isStream}`);
905
- const response = await fetch(url, { method: "POST", headers, body: rawBody });
1114
+ const response = await fetchUpstream(
1115
+ url,
1116
+ { method: "POST", headers, body: bodyToSend },
1117
+ { providerId: "byo" }
1118
+ );
906
1119
  return { response, rawStatus: response.status };
907
1120
  }
1121
+ function resolveSameFormatBody(anthropicBody, rawBody, plan) {
1122
+ let originalModel;
1123
+ try {
1124
+ const m = JSON.parse(rawBody).model;
1125
+ originalModel = typeof m === "string" ? m : void 0;
1126
+ } catch {
1127
+ originalModel = void 0;
1128
+ }
1129
+ return plan.resolvedModel !== originalModel ? JSON.stringify(anthropicBody) : rawBody;
1130
+ }
908
1131
  async function runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options) {
909
- const runOnce = (keyOverride) => plan.sameFormat ? runSameFormatFetch(rawBody, plan, options, keyOverride) : runPipeline(anthropicBody, plan);
1132
+ const sameFormatBody = plan.sameFormat ? resolveSameFormatBody(anthropicBody, rawBody, plan) : rawBody;
1133
+ const runOnce = (keyOverride) => plan.sameFormat ? runSameFormatFetch(sameFormatBody, plan, options, keyOverride) : runPipeline(anthropicBody, plan);
910
1134
  const first = await runOnce();
911
1135
  const outcome = await plan.auth.onResult?.(first.rawStatus);
912
1136
  if (outcome?.rebound) {
@@ -930,7 +1154,8 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
930
1154
  const rawBody = await readBody(req);
931
1155
  const callerBetaRaw = req.headers["anthropic-beta"];
932
1156
  const callerAnthropicBeta = Array.isArray(callerBetaRaw) ? callerBetaRaw.join(",") : callerBetaRaw;
933
- await handleAnthropicMessagesByo(res, rawBody, route, deps, { callerAnthropicBeta });
1157
+ const callerIdentity = captureCallerIdentity(getSharedIdentityStore(), req.headers);
1158
+ await handleAnthropicMessagesByo(res, rawBody, route, deps, { callerAnthropicBeta, callerIdentity });
934
1159
  return;
935
1160
  }
936
1161
  const hints = route.anthropicSdkHints;
@@ -992,7 +1217,8 @@ function recordGeminiNonStreamUsage(recorder, bodyText, attribution) {
992
1217
  sessionId: attribution.sessionId,
993
1218
  providerId: attribution.providerId,
994
1219
  model: attribution.model,
995
- apiKeyId: null,
1220
+ apiKeyId: attribution.apiKeyId,
1221
+ auditResponse: attribution.auditResponse,
996
1222
  engineOrigin: "codex-ingress",
997
1223
  usage: tapped,
998
1224
  rawUsage: parsed.usageMetadata
@@ -1042,7 +1268,10 @@ async function handleGeminiGenerateContentRequest(res, rawBody, url, route, deps
1042
1268
  recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, {
1043
1269
  sessionId: route.sessionId,
1044
1270
  providerId: route.providerId ?? "gemini",
1045
- model: resolvedModel
1271
+ model: resolvedModel,
1272
+ apiKeyId: route.apiKeyId ?? null,
1273
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1274
+ auditResponse: res
1046
1275
  });
1047
1276
  }
1048
1277
  } catch (err) {
@@ -1117,7 +1346,11 @@ async function runPipeline2(geminiBody, plan) {
1117
1346
  },
1118
1347
  fetchFn: (url, headers, body) => {
1119
1348
  console.log(`[ProviderProxy:gemini] -> ${url} model=${resolvedModel} stream=${isStream}`);
1120
- return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1349
+ return fetchUpstream(
1350
+ url,
1351
+ { method: "POST", headers, body: JSON.stringify(body) },
1352
+ { providerId: "byo" }
1353
+ ).then((r) => {
1121
1354
  rawStatus = r.status;
1122
1355
  return r;
1123
1356
  });
@@ -1169,7 +1402,8 @@ function recordChatCompletionsNonStreamUsage(recorder, bodyText, attribution) {
1169
1402
  sessionId: attribution.sessionId,
1170
1403
  providerId: attribution.providerId,
1171
1404
  model: attribution.model,
1172
- apiKeyId: null,
1405
+ apiKeyId: attribution.apiKeyId,
1406
+ auditResponse: attribution.auditResponse,
1173
1407
  engineOrigin: "codex-ingress",
1174
1408
  usage: tapped,
1175
1409
  rawUsage: parsed.usage
@@ -1184,6 +1418,8 @@ function isOpenAIChatRequest(method, url) {
1184
1418
  const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
1185
1419
  return path.endsWith("/chat/completions");
1186
1420
  }
1421
+ var CHAT_BRIDGE_SUBSCRIPTION_PROVIDERS = /* @__PURE__ */ new Set(["claude"]);
1422
+ var IDENTITY_ENDPOINT_TRANSFORMER = { name: "identity-openai-chat" };
1187
1423
  async function handleOpenAIChatRequest(res, rawBody, route, deps) {
1188
1424
  let chatBody;
1189
1425
  try {
@@ -1192,23 +1428,22 @@ async function handleOpenAIChatRequest(res, rawBody, route, deps) {
1192
1428
  writeError(res, 400, "Invalid JSON in request body");
1193
1429
  return;
1194
1430
  }
1195
- if (route.authMode === "subscription") {
1196
- writeError(res, 502, "OpenAI Chat Completions ingress does not support subscription auth");
1197
- return;
1198
- }
1199
1431
  const isStream = chatBody.stream === true;
1200
1432
  const resolvedModel = route.model;
1201
1433
  chatBody.model = resolvedModel;
1202
1434
  try {
1203
- const plan = await buildByoPlan3(res, route, deps, resolvedModel, isStream);
1435
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan3(res, route, deps, resolvedModel, isStream);
1204
1436
  if (!plan) return;
1205
- const providerResponse = await runPipelineWithPoolReporting3(chatBody, plan);
1437
+ const providerResponse = plan.isSubscription ? await runPipelineWithSubscriptionRetry2(chatBody, plan) : await runPipelineWithPoolReporting3(chatBody, plan);
1206
1438
  const bodyText = await relayResponse(res, providerResponse.response, isStream);
1207
1439
  if (bodyText && deps.usageRecorder) {
1208
1440
  recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, {
1209
1441
  sessionId: route.sessionId,
1210
1442
  providerId: route.providerId ?? "openai",
1211
- model: resolvedModel
1443
+ model: resolvedModel,
1444
+ apiKeyId: route.apiKeyId ?? null,
1445
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1446
+ auditResponse: res
1212
1447
  });
1213
1448
  }
1214
1449
  } catch (err) {
@@ -1255,14 +1490,67 @@ async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
1255
1490
  resolvedModel,
1256
1491
  isStream,
1257
1492
  resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
1258
- upstreamUrl: byoUrl
1493
+ upstreamUrl: byoUrl,
1494
+ proxyProviderId: "byo",
1495
+ isSubscription: false
1496
+ };
1497
+ }
1498
+ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) {
1499
+ const profile = route.subscriptionProfile;
1500
+ if (!profile) {
1501
+ writeError(res, 502, "Subscription mode requires a subscription profile");
1502
+ return null;
1503
+ }
1504
+ const providerId = profile.authStrategy.providerId;
1505
+ if (!CHAT_BRIDGE_SUBSCRIPTION_PROVIDERS.has(providerId)) {
1506
+ writeError(
1507
+ res,
1508
+ 501,
1509
+ `the OpenAI-chat\u2192subscription bridge currently supports claude only; subscription provider '${providerId}' is not implemented on this endpoint yet (use the /v1/responses endpoint, a Claude subscription, or a BYO provider)`
1510
+ );
1511
+ return null;
1512
+ }
1513
+ const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel);
1514
+ if (!upstreamUrl) {
1515
+ writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
1516
+ return null;
1517
+ }
1518
+ const auth = route.auth ?? new SubscriptionAuthSource(profile);
1519
+ const chain = resolveSubscriptionChain(
1520
+ profile,
1521
+ deps.llmConfig.getTransformerService(),
1522
+ IDENTITY_ENDPOINT_TRANSFORMER
1523
+ );
1524
+ const transformerProvider = {
1525
+ name: providerId,
1526
+ baseUrl: upstreamUrl,
1527
+ apiKey: "",
1528
+ models: [resolvedModel]
1529
+ };
1530
+ return {
1531
+ auth,
1532
+ chain,
1533
+ transformerProvider,
1534
+ resolvedModel,
1535
+ isStream,
1536
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
1537
+ upstreamUrl,
1538
+ proxyProviderId: providerId,
1539
+ isSubscription: true
1259
1540
  };
1260
1541
  }
1261
1542
  async function runPipeline3(chatBody, plan) {
1262
1543
  const executor = getSharedExecutor();
1263
1544
  const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
1545
+ let proxyAccountId;
1264
1546
  const authHeaders = {};
1265
- await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
1547
+ await auth.applyHeaders(authHeaders, {
1548
+ upstreamUrl,
1549
+ model: resolvedModel,
1550
+ reportSelection: (accountId) => {
1551
+ proxyAccountId = accountId;
1552
+ }
1553
+ });
1266
1554
  let rawStatus = null;
1267
1555
  const { response } = await executeProviderCall({
1268
1556
  executor,
@@ -1281,7 +1569,11 @@ async function runPipeline3(chatBody, plan) {
1281
1569
  },
1282
1570
  fetchFn: (url, headers, body) => {
1283
1571
  console.log(`[ProviderProxy:chat] -> ${url} model=${resolvedModel} stream=${isStream}`);
1284
- return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1572
+ return fetchUpstream(
1573
+ url,
1574
+ { method: "POST", headers, body: JSON.stringify(body) },
1575
+ { providerId: plan.proxyProviderId, accountId: proxyAccountId }
1576
+ ).then((r) => {
1285
1577
  rawStatus = r.status;
1286
1578
  return r;
1287
1579
  });
@@ -1303,6 +1595,17 @@ async function runPipelineWithPoolReporting3(chatBody, plan) {
1303
1595
  }
1304
1596
  return first;
1305
1597
  }
1598
+ async function runPipelineWithSubscriptionRetry2(chatBody, plan) {
1599
+ const first = await runPipeline3(chatBody, plan);
1600
+ if (first.rawStatus !== 401) return first;
1601
+ const refreshed = await plan.auth.onUnauthorized?.();
1602
+ if (!refreshed) {
1603
+ console.warn("[ProviderProxy:chat] 401 not recoverable (onUnauthorized returned false)");
1604
+ return first;
1605
+ }
1606
+ console.log("[ProviderProxy:chat] 401 \u2192 token refreshed; retrying once");
1607
+ return runPipeline3(chatBody, plan);
1608
+ }
1306
1609
 
1307
1610
  // src/provider-proxy/usage/recordResponsesUsage.ts
1308
1611
  function readResponsesUsage(usage) {
@@ -1333,7 +1636,8 @@ function recordResponsesNonStreamUsage(recorder, bodyText, attribution) {
1333
1636
  sessionId: attribution.sessionId,
1334
1637
  providerId: attribution.providerId,
1335
1638
  model: attribution.model,
1336
- apiKeyId: null,
1639
+ apiKeyId: attribution.apiKeyId,
1640
+ auditResponse: attribution.auditResponse,
1337
1641
  engineOrigin: "codex-ingress",
1338
1642
  usage: tapped,
1339
1643
  rawUsage: parsed.usage
@@ -1360,15 +1664,18 @@ async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
1360
1664
  const resolvedModel = route.model;
1361
1665
  responsesBody.model = resolvedModel;
1362
1666
  try {
1363
- const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
1667
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
1364
1668
  if (!plan) return;
1365
- const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry2(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
1366
- const bodyText = await relayResponse(res, providerResponse.response, isStream);
1669
+ const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry3(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
1670
+ const bodyText = await relayResponse(res, providerResponse.response, isStream, route.requestedModel);
1367
1671
  if (bodyText && deps.usageRecorder) {
1368
1672
  recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, {
1369
1673
  sessionId: route.sessionId,
1370
1674
  providerId: route.providerId ?? "codex",
1371
- model: resolvedModel
1675
+ model: resolvedModel,
1676
+ apiKeyId: route.apiKeyId ?? null,
1677
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1678
+ auditResponse: res
1372
1679
  });
1373
1680
  }
1374
1681
  } catch (err) {
@@ -1415,10 +1722,11 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
1415
1722
  resolvedModel,
1416
1723
  isStream,
1417
1724
  resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
1418
- upstreamUrl: byoUrl
1725
+ upstreamUrl: byoUrl,
1726
+ proxyProviderId: "byo"
1419
1727
  };
1420
1728
  }
1421
- async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) {
1729
+ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) {
1422
1730
  const profile = route.subscriptionProfile;
1423
1731
  if (!profile) {
1424
1732
  writeError(res, 502, "Subscription mode requires a codex subscription profile");
@@ -1451,6 +1759,7 @@ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream)
1451
1759
  }
1452
1760
  return {
1453
1761
  auth,
1762
+ preferredAccountId: route.preferredAccountId,
1454
1763
  chain,
1455
1764
  transformerProvider,
1456
1765
  resolvedModel,
@@ -1459,7 +1768,8 @@ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream)
1459
1768
  // stream vs non-stream colon-method URL in `config.url`); fall back to the
1460
1769
  // profile's `resolveUpstreamUrl` for non-Code-Assist subscriptions.
1461
1770
  resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
1462
- upstreamUrl
1771
+ upstreamUrl,
1772
+ proxyProviderId: profile.authStrategy.providerId
1463
1773
  };
1464
1774
  }
1465
1775
  async function resolveGeminiCodeAssistProject(profile) {
@@ -1476,8 +1786,16 @@ async function runPipeline4(responsesBody, plan) {
1476
1786
  const executor = getSharedExecutor();
1477
1787
  const endpointTransformer = getResponsesEndpointTransformer();
1478
1788
  const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
1789
+ let proxyAccountId;
1479
1790
  const authHeaders = {};
1480
- await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
1791
+ await auth.applyHeaders(authHeaders, {
1792
+ upstreamUrl,
1793
+ model: resolvedModel,
1794
+ preferredAccountId: plan.preferredAccountId,
1795
+ reportSelection: (accountId) => {
1796
+ proxyAccountId = accountId;
1797
+ }
1798
+ });
1481
1799
  let rawStatus = null;
1482
1800
  const { response } = await executeProviderCall({
1483
1801
  executor,
@@ -1497,7 +1815,11 @@ async function runPipeline4(responsesBody, plan) {
1497
1815
  },
1498
1816
  fetchFn: (url, headers, body) => {
1499
1817
  console.log(`[ProviderProxy:responses] -> ${url} model=${resolvedModel} stream=${isStream}`);
1500
- return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1818
+ return fetchUpstream(
1819
+ url,
1820
+ { method: "POST", headers, body: JSON.stringify(body) },
1821
+ { providerId: plan.proxyProviderId, accountId: proxyAccountId }
1822
+ ).then((r) => {
1501
1823
  rawStatus = r.status;
1502
1824
  return r;
1503
1825
  });
@@ -1519,7 +1841,7 @@ async function runPipelineWithPoolReporting4(responsesBody, plan) {
1519
1841
  }
1520
1842
  return first;
1521
1843
  }
1522
- async function runPipelineWithSubscriptionRetry2(responsesBody, plan) {
1844
+ async function runPipelineWithSubscriptionRetry3(responsesBody, plan) {
1523
1845
  const first = await runPipeline4(responsesBody, plan);
1524
1846
  if (first.rawStatus !== 401) return first;
1525
1847
  const refreshed = await plan.auth.onUnauthorized?.();
@@ -1696,12 +2018,439 @@ function __resetProviderProxyForTests() {
1696
2018
  // src/outbound-api/OutboundApiServer.ts
1697
2019
  import http2 from "http";
1698
2020
  import { networkInterfaces } from "os";
2021
+ import { healthHttpStatus } from "@omnicross/contracts/health-logging-types";
2022
+
2023
+ // src/outbound-api/keyedMutex.ts
2024
+ var KeyedMutex = class {
2025
+ /** key → the tail of the pending-op chain (resolves when the last op frees). */
2026
+ tails = /* @__PURE__ */ new Map();
2027
+ /**
2028
+ * Run `fn` exclusively for `key`: it starts only after every previously
2029
+ * enqueued op for the SAME key has settled, and the next waiter starts only
2030
+ * after `fn` settles. Returns `fn`'s result (or rejection). Never lets one op's
2031
+ * failure wedge the queue (waiters proceed regardless).
2032
+ */
2033
+ async runExclusive(key, fn) {
2034
+ const prev = this.tails.get(key) ?? Promise.resolve();
2035
+ let release2;
2036
+ const gate = new Promise((resolve) => {
2037
+ release2 = resolve;
2038
+ });
2039
+ const chain = prev.then(() => gate);
2040
+ this.tails.set(key, chain);
2041
+ await prev.catch(() => void 0);
2042
+ try {
2043
+ return await fn();
2044
+ } finally {
2045
+ release2();
2046
+ if (this.tails.get(key) === chain) this.tails.delete(key);
2047
+ }
2048
+ }
2049
+ };
1699
2050
 
1700
2051
  // src/outbound-api/outboundApiRouter.ts
1701
2052
  import { Readable } from "stream";
1702
2053
 
2054
+ // src/outbound-api/apiServerConfig.ts
2055
+ import { DEFAULT_AUDIT_CONFIG } from "@omnicross/contracts/audit-types";
2056
+ import { DEFAULT_BILLING_CONFIG } from "@omnicross/contracts/billing-types";
2057
+ import {
2058
+ WEBHOOK_DESTINATION_TYPES,
2059
+ WEBHOOK_EVENT_KINDS
2060
+ } from "@omnicross/contracts/webhook-types";
2061
+ var OUTBOUND_API_SERVER_CONFIG_KEY = "outboundApiServer.config";
2062
+ var DEFAULT_USER_MESSAGE_QUEUE = {
2063
+ enabled: false,
2064
+ delayMs: 200,
2065
+ waitTimeoutMs: 6e4
2066
+ };
2067
+ var DEFAULT_CONCURRENCY_QUEUE = {
2068
+ maxQueueSizeFactor: 2,
2069
+ minQueueSize: 4,
2070
+ waitTimeoutMs: 6e4
2071
+ };
2072
+ var DEFAULT_ACCOUNT_HEALTH = {
2073
+ overloadCooldownEnabled: true,
2074
+ overloadCooldownMs: 10 * 6e4
2075
+ };
2076
+ function normalizeAccountHealth(raw) {
2077
+ const ah = raw?.accountHealth;
2078
+ return {
2079
+ overloadCooldownEnabled: ah?.overloadCooldownEnabled !== false,
2080
+ overloadCooldownMs: clampNumber(
2081
+ ah?.overloadCooldownMs,
2082
+ 6e4,
2083
+ 36e5,
2084
+ DEFAULT_ACCOUNT_HEALTH.overloadCooldownMs
2085
+ )
2086
+ };
2087
+ }
2088
+ var DEFAULT_ACCOUNT_PROBE = {
2089
+ enabled: false,
2090
+ intervalMs: 15 * 6e4,
2091
+ onlyMultiAccount: true,
2092
+ timeoutMs: 5e3,
2093
+ historySize: 10,
2094
+ staggerMs: 500
2095
+ };
2096
+ function normalizeAccountProbe(raw) {
2097
+ const ap = raw?.accountProbe;
2098
+ return {
2099
+ enabled: ap?.enabled === true,
2100
+ intervalMs: clampNumber(ap?.intervalMs, 6e4, 864e5, DEFAULT_ACCOUNT_PROBE.intervalMs),
2101
+ onlyMultiAccount: ap?.onlyMultiAccount !== false,
2102
+ timeoutMs: clampNumber(ap?.timeoutMs, 1e3, 6e4, DEFAULT_ACCOUNT_PROBE.timeoutMs),
2103
+ historySize: Math.trunc(
2104
+ clampNumber(ap?.historySize, 1, 200, DEFAULT_ACCOUNT_PROBE.historySize)
2105
+ ),
2106
+ staggerMs: clampNumber(ap?.staggerMs, 0, 6e4, DEFAULT_ACCOUNT_PROBE.staggerMs)
2107
+ };
2108
+ }
2109
+ function normalizeAudit(raw) {
2110
+ const a = raw?.audit;
2111
+ return {
2112
+ enabled: a?.enabled === true,
2113
+ captureBodies: a?.captureBodies === true,
2114
+ maxBodyBytes: Math.trunc(
2115
+ clampNumber(a?.maxBodyBytes, 256, 1048576, DEFAULT_AUDIT_CONFIG.maxBodyBytes)
2116
+ ),
2117
+ retentionDays: Math.trunc(
2118
+ clampNumber(a?.retentionDays, 1, 365, DEFAULT_AUDIT_CONFIG.retentionDays)
2119
+ ),
2120
+ trustForwardedFor: a?.trustForwardedFor === true
2121
+ };
2122
+ }
2123
+ function normalizeBilling(raw) {
2124
+ const b = raw?.billing;
2125
+ const config = {
2126
+ enabled: b?.enabled === true,
2127
+ maxRetryAgeMs: Math.trunc(
2128
+ clampNumber(b?.maxRetryAgeMs, 6e4, 2592e6, DEFAULT_BILLING_CONFIG.maxRetryAgeMs)
2129
+ )
2130
+ };
2131
+ if (typeof b?.endpoint === "string" && b.endpoint.trim()) config.endpoint = b.endpoint.trim();
2132
+ if (typeof b?.secret === "string" && b.secret.length > 0) config.secret = b.secret;
2133
+ return config;
2134
+ }
2135
+ var DEFAULT_FINGERPRINT = { enabled: false };
2136
+ function normalizeFingerprint(raw) {
2137
+ const f = raw?.fingerprint;
2138
+ const out = { enabled: f?.enabled === true };
2139
+ if (typeof f?.ua === "string" && f.ua.trim().length > 0) out.ua = f.ua.trim();
2140
+ return out;
2141
+ }
2142
+ function normalizeVoucher(raw) {
2143
+ return { enabled: raw?.voucher?.enabled === true };
2144
+ }
2145
+ var PROXY_TYPES = ["http", "https", "socks5"];
2146
+ function normalizeProxyConfig(raw) {
2147
+ if (!raw || typeof raw !== "object") return void 0;
2148
+ const r = raw;
2149
+ if (typeof r["url"] === "string" && r["url"].trim().length > 0) {
2150
+ return { url: r["url"].trim() };
2151
+ }
2152
+ const type = r["type"];
2153
+ const host = r["host"];
2154
+ const port = r["port"];
2155
+ if (typeof type === "string" && PROXY_TYPES.includes(type) && typeof host === "string" && host.trim().length > 0 && typeof port === "number" && Number.isFinite(port) && port >= 1 && port <= 65535) {
2156
+ const out = {
2157
+ type,
2158
+ host: host.trim(),
2159
+ port: Math.trunc(port)
2160
+ };
2161
+ if (typeof r["username"] === "string" && r["username"].length > 0) out.username = r["username"];
2162
+ if (typeof r["password"] === "string" && r["password"].length > 0) out.password = r["password"];
2163
+ return out;
2164
+ }
2165
+ return void 0;
2166
+ }
2167
+ function normalizeProxySegment(raw) {
2168
+ if (!raw || typeof raw !== "object") return void 0;
2169
+ const r = raw;
2170
+ const out = {};
2171
+ const global = normalizeProxyConfig(r["global"]);
2172
+ if (global) out.global = global;
2173
+ if (r["byProvider"] && typeof r["byProvider"] === "object") {
2174
+ const byProvider = {};
2175
+ for (const [key, value] of Object.entries(r["byProvider"])) {
2176
+ if (!key.trim()) continue;
2177
+ const cfg = normalizeProxyConfig(value);
2178
+ if (cfg) byProvider[key] = cfg;
2179
+ }
2180
+ if (Object.keys(byProvider).length > 0) out.byProvider = byProvider;
2181
+ }
2182
+ return out.global || out.byProvider ? out : void 0;
2183
+ }
2184
+ function normalizeWebhookDestination(raw) {
2185
+ if (!raw || typeof raw !== "object") return void 0;
2186
+ const r = raw;
2187
+ const id = typeof r["id"] === "string" ? r["id"].trim() : "";
2188
+ const url = typeof r["url"] === "string" ? r["url"].trim() : "";
2189
+ const type = r["type"];
2190
+ if (!id || !url || typeof type !== "string" || !WEBHOOK_DESTINATION_TYPES.includes(type)) {
2191
+ return void 0;
2192
+ }
2193
+ const out = {
2194
+ id,
2195
+ type,
2196
+ url,
2197
+ enabled: r["enabled"] !== false
2198
+ };
2199
+ if (typeof r["secret"] === "string" && r["secret"].length > 0) out.secret = r["secret"];
2200
+ if (Array.isArray(r["events"])) {
2201
+ const events = r["events"].filter(
2202
+ (e) => typeof e === "string" && WEBHOOK_EVENT_KINDS.includes(e)
2203
+ );
2204
+ if (events.length > 0) out.events = events;
2205
+ }
2206
+ return out;
2207
+ }
2208
+ function normalizeWebhookSegment(raw) {
2209
+ if (!raw || typeof raw !== "object") return void 0;
2210
+ const r = raw;
2211
+ const destinations = [];
2212
+ if (Array.isArray(r["destinations"])) {
2213
+ for (const entry of r["destinations"]) {
2214
+ const d = normalizeWebhookDestination(entry);
2215
+ if (d) destinations.push(d);
2216
+ }
2217
+ }
2218
+ const enabled = r["enabled"] === true;
2219
+ if (!enabled && destinations.length === 0) return void 0;
2220
+ return { enabled, destinations };
2221
+ }
2222
+ function clampNumber(value, min, max, fallback) {
2223
+ const n = typeof value === "number" && Number.isFinite(value) ? value : fallback;
2224
+ return Math.min(max, Math.max(min, n));
2225
+ }
2226
+ function normalizeQueueSegments(raw) {
2227
+ const umq = raw?.userMessageQueue;
2228
+ const cq = raw?.concurrencyQueue;
2229
+ return {
2230
+ userMessageQueue: {
2231
+ enabled: umq?.enabled === true,
2232
+ delayMs: clampNumber(umq?.delayMs, 0, 1e4, DEFAULT_USER_MESSAGE_QUEUE.delayMs),
2233
+ waitTimeoutMs: clampNumber(
2234
+ umq?.waitTimeoutMs,
2235
+ 1e3,
2236
+ 3e5,
2237
+ DEFAULT_USER_MESSAGE_QUEUE.waitTimeoutMs
2238
+ )
2239
+ },
2240
+ concurrencyQueue: {
2241
+ maxQueueSizeFactor: clampNumber(
2242
+ cq?.maxQueueSizeFactor,
2243
+ 1,
2244
+ 10,
2245
+ DEFAULT_CONCURRENCY_QUEUE.maxQueueSizeFactor
2246
+ ),
2247
+ minQueueSize: clampNumber(cq?.minQueueSize, 1, 100, DEFAULT_CONCURRENCY_QUEUE.minQueueSize),
2248
+ waitTimeoutMs: clampNumber(
2249
+ cq?.waitTimeoutMs,
2250
+ 1e3,
2251
+ 3e5,
2252
+ DEFAULT_CONCURRENCY_QUEUE.waitTimeoutMs
2253
+ )
2254
+ }
2255
+ };
2256
+ }
2257
+ var ALL_ENDPOINTS = ["chat", "responses", "messages", "gemini"];
2258
+ function blankModelMap(endpoint) {
2259
+ const modelMap = {};
2260
+ for (const kind of modelKindsForEndpoint(endpoint)) modelMap[kind] = "";
2261
+ return modelMap;
2262
+ }
2263
+ function defaultEndpointConfig(endpoint) {
2264
+ if (isKindMappedEndpoint(endpoint)) {
2265
+ return { endpoint, modelMap: blankModelMap(endpoint), useSubscription: false };
2266
+ }
2267
+ if (endpoint === "chat") {
2268
+ return { endpoint, models: [], useSubscription: false };
2269
+ }
2270
+ return {
2271
+ endpoint,
2272
+ defaultModel: "",
2273
+ backgroundModel: "",
2274
+ useSubscription: false
2275
+ };
2276
+ }
2277
+ function normalizePrefixTargets(raw) {
2278
+ if (!raw || typeof raw !== "object") return void 0;
2279
+ const r = raw;
2280
+ const out = {};
2281
+ for (const key of ["claude", "gpt", "gemini"]) {
2282
+ const v = r[key];
2283
+ if (typeof v === "string" && v.trim() !== "") out[key] = v.trim();
2284
+ }
2285
+ return out.claude || out.gpt || out.gemini ? out : void 0;
2286
+ }
2287
+ function normalizeEndpointConfig(e) {
2288
+ const endpoint = e.endpoint;
2289
+ const useSubscription = e.useSubscription === true;
2290
+ const boundAccountId = typeof e.boundAccountId === "string" && e.boundAccountId.trim() !== "" ? e.boundAccountId : void 0;
2291
+ const boundKeyId = typeof e.boundKeyId === "string" && e.boundKeyId.trim() !== "" ? e.boundKeyId : void 0;
2292
+ let config;
2293
+ if (isKindMappedEndpoint(endpoint)) {
2294
+ const rawMap = e.modelMap && typeof e.modelMap === "object" ? e.modelMap : {};
2295
+ const modelMap = {};
2296
+ for (const kind of modelKindsForEndpoint(endpoint)) {
2297
+ const v = rawMap[kind];
2298
+ modelMap[kind] = typeof v === "string" ? v : "";
2299
+ }
2300
+ config = { endpoint, modelMap, useSubscription };
2301
+ } else if (endpoint === "chat") {
2302
+ const models = Array.isArray(e.models) ? e.models.filter((m) => typeof m === "string" && m.trim() !== "") : [];
2303
+ config = { endpoint, models, useSubscription };
2304
+ if (e.dispatchMode === "prefix") {
2305
+ config.dispatchMode = "prefix";
2306
+ const targets = normalizePrefixTargets(e.prefixTargets);
2307
+ if (targets) config.prefixTargets = targets;
2308
+ }
2309
+ } else {
2310
+ config = {
2311
+ endpoint,
2312
+ defaultModel: typeof e.defaultModel === "string" ? e.defaultModel : "",
2313
+ backgroundModel: typeof e.backgroundModel === "string" ? e.backgroundModel : "",
2314
+ useSubscription
2315
+ };
2316
+ if (Array.isArray(e.backgroundModelIds)) {
2317
+ config.backgroundModelIds = e.backgroundModelIds;
2318
+ }
2319
+ }
2320
+ if (boundAccountId) config.boundAccountId = boundAccountId;
2321
+ if (boundKeyId) config.boundKeyId = boundKeyId;
2322
+ return config;
2323
+ }
2324
+ function defaultServerConfig() {
2325
+ const queues = normalizeQueueSegments(void 0);
2326
+ return {
2327
+ enabled: false,
2328
+ networkBinding: false,
2329
+ endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
2330
+ port: DEFAULT_OUTBOUND_PORT,
2331
+ userMessageQueue: queues.userMessageQueue,
2332
+ concurrencyQueue: queues.concurrencyQueue,
2333
+ accountHealth: normalizeAccountHealth(void 0),
2334
+ accountProbe: normalizeAccountProbe(void 0),
2335
+ audit: normalizeAudit(void 0),
2336
+ billing: normalizeBilling(void 0),
2337
+ fingerprint: normalizeFingerprint(void 0),
2338
+ voucher: normalizeVoucher(void 0)
2339
+ };
2340
+ }
2341
+ function normalizeServerConfig(raw) {
2342
+ const base = defaultServerConfig();
2343
+ if (!raw) return base;
2344
+ const byEndpoint = /* @__PURE__ */ new Map();
2345
+ for (const e of raw.endpoints ?? []) {
2346
+ if (e && ALL_ENDPOINTS.includes(e.endpoint)) {
2347
+ byEndpoint.set(e.endpoint, normalizeEndpointConfig(e));
2348
+ }
2349
+ }
2350
+ const queues = normalizeQueueSegments(raw);
2351
+ const config = {
2352
+ enabled: raw.enabled === true,
2353
+ networkBinding: raw.networkBinding === true,
2354
+ endpoints: ALL_ENDPOINTS.map(
2355
+ (ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep)
2356
+ ),
2357
+ port: raw.port ?? base.port,
2358
+ userMessageQueue: queues.userMessageQueue,
2359
+ concurrencyQueue: queues.concurrencyQueue,
2360
+ accountHealth: normalizeAccountHealth(raw),
2361
+ accountProbe: normalizeAccountProbe(raw),
2362
+ audit: normalizeAudit(raw),
2363
+ billing: normalizeBilling(raw),
2364
+ fingerprint: normalizeFingerprint(raw),
2365
+ voucher: normalizeVoucher(raw)
2366
+ };
2367
+ const proxy = normalizeProxySegment(raw.proxy);
2368
+ if (proxy) config.proxy = proxy;
2369
+ const webhook = normalizeWebhookSegment(raw.webhook);
2370
+ if (webhook) config.webhook = webhook;
2371
+ return config;
2372
+ }
2373
+ async function loadServerConfig(store) {
2374
+ const raw = await store.get(
2375
+ OUTBOUND_API_SERVER_CONFIG_KEY
2376
+ );
2377
+ return normalizeServerConfig(raw);
2378
+ }
2379
+ async function saveServerConfig(store, config) {
2380
+ await store.set(OUTBOUND_API_SERVER_CONFIG_KEY, config);
2381
+ }
2382
+ function mergeServerConfig(current, patch) {
2383
+ return normalizeServerConfig({
2384
+ enabled: patch.enabled ?? current.enabled,
2385
+ networkBinding: patch.networkBinding ?? current.networkBinding,
2386
+ endpoints: patch.endpoints ?? current.endpoints,
2387
+ port: patch.port ?? current.port,
2388
+ userMessageQueue: patch.userMessageQueue ?? current.userMessageQueue,
2389
+ concurrencyQueue: patch.concurrencyQueue ?? current.concurrencyQueue,
2390
+ accountHealth: patch.accountHealth ?? current.accountHealth,
2391
+ accountProbe: patch.accountProbe ?? current.accountProbe,
2392
+ audit: patch.audit ?? current.audit,
2393
+ billing: patch.billing ?? current.billing,
2394
+ // Proxy is layer-replaced (not deep-merged): a PUT carrying `proxy` swaps the
2395
+ // whole segment; omitting it keeps the current one. `undefined` on both ⇒ absent.
2396
+ proxy: patch.proxy ?? current.proxy,
2397
+ // Webhook is layer-replaced too (a PUT carrying `webhook` swaps the whole
2398
+ // segment; omitting it keeps the current one). `undefined` on both ⇒ absent.
2399
+ webhook: patch.webhook ?? current.webhook,
2400
+ // Fingerprint is always-filled (normalizeFingerprint synthesizes a default);
2401
+ // a PUT carrying it replaces the segment, else the current one is kept.
2402
+ fingerprint: patch.fingerprint ?? current.fingerprint,
2403
+ // Voucher is always-filled (normalizeVoucher synthesizes a default); a PUT
2404
+ // carrying it replaces the segment, else the current one is kept.
2405
+ voucher: patch.voucher ?? current.voucher
2406
+ });
2407
+ }
2408
+
1703
2409
  // src/outbound-api/outboundApiKeyAuth.ts
1704
2410
  import { createHash, randomBytes as randomBytes2 } from "crypto";
2411
+
2412
+ // src/outbound-api/keyPolicy.ts
2413
+ var DAY_MS = 864e5;
2414
+ function computeKeyExpiry(row, now) {
2415
+ const mode = row.activationMode ?? "fixed";
2416
+ if (mode === "activation") {
2417
+ if (row.activatedAt == null) return { expired: false, needsActivation: true };
2418
+ if (row.activationDays == null) return { expired: false };
2419
+ const effectiveExpiresAt = row.activatedAt + row.activationDays * DAY_MS;
2420
+ return { expired: now >= effectiveExpiresAt, effectiveExpiresAt };
2421
+ }
2422
+ if (row.expiresAt == null) return { expired: false };
2423
+ return { expired: now >= row.expiresAt, effectiveExpiresAt: row.expiresAt };
2424
+ }
2425
+ function checkKeyQuota(limits, spend) {
2426
+ if (!limits) return { allowed: true };
2427
+ if (limits.dailyUsd != null && spend.dailyUsd >= limits.dailyUsd) {
2428
+ return { allowed: false, scope: "daily", limitUsd: limits.dailyUsd, spentUsd: spend.dailyUsd };
2429
+ }
2430
+ if (limits.weeklyUsd != null && spend.weeklyUsd >= limits.weeklyUsd) {
2431
+ return { allowed: false, scope: "weekly", limitUsd: limits.weeklyUsd, spentUsd: spend.weeklyUsd };
2432
+ }
2433
+ if (limits.totalUsd != null && spend.totalUsd >= limits.totalUsd) {
2434
+ return { allowed: false, scope: "total", limitUsd: limits.totalUsd, spentUsd: spend.totalUsd };
2435
+ }
2436
+ return { allowed: true };
2437
+ }
2438
+ function canonicalModelId(value) {
2439
+ const idx = value.indexOf(",");
2440
+ const bare = idx >= 0 ? value.slice(idx + 1) : value;
2441
+ return bare.trim().toLowerCase();
2442
+ }
2443
+ function checkModelAllowed(restriction, resolvedModelId) {
2444
+ if (!restriction) return { allowed: true };
2445
+ const target = canonicalModelId(resolvedModelId);
2446
+ const listed = restriction.models.some((m) => canonicalModelId(m) === target);
2447
+ if (restriction.mode === "allowlist") {
2448
+ return listed ? { allowed: true } : { allowed: false, model: resolvedModelId };
2449
+ }
2450
+ return listed ? { allowed: false, model: resolvedModelId } : { allowed: true };
2451
+ }
2452
+
2453
+ // src/outbound-api/outboundApiKeyAuth.ts
1705
2454
  var SECRET_BYTES = 32;
1706
2455
  var PREFIX_LEN = 14;
1707
2456
  var KEY_PREFIX = "sk-omnicross-";
@@ -1749,143 +2498,706 @@ async function createNamedKey(db, name) {
1749
2498
  plaintextOnce: secret
1750
2499
  };
1751
2500
  }
1752
- async function verifyPresentedKey(db, presentedKey) {
1753
- if (!presentedKey) return null;
2501
+ function extractCostLimits(row) {
2502
+ const limits = {};
2503
+ if (row.dailyCostLimitUsd != null) limits.dailyUsd = row.dailyCostLimitUsd;
2504
+ if (row.totalCostLimitUsd != null) limits.totalUsd = row.totalCostLimitUsd;
2505
+ if (row.weeklyCostLimitUsd != null) limits.weeklyUsd = row.weeklyCostLimitUsd;
2506
+ return limits.dailyUsd != null || limits.totalUsd != null || limits.weeklyUsd != null ? limits : void 0;
2507
+ }
2508
+ function extractRateLimit(row) {
2509
+ if (row.rateLimitMaxRequests == null && row.rateLimitWindowMs == null) return void 0;
2510
+ const override = {};
2511
+ if (row.rateLimitMaxRequests != null) override.maxRequests = row.rateLimitMaxRequests;
2512
+ if (row.rateLimitWindowMs != null) override.windowMs = row.rateLimitWindowMs;
2513
+ return override;
2514
+ }
2515
+ function extractModelRestriction(row) {
2516
+ if (row.enableModelRestriction !== true) return void 0;
2517
+ return {
2518
+ mode: row.restrictionMode ?? "blacklist",
2519
+ models: row.restrictedModels ?? []
2520
+ };
2521
+ }
2522
+ function toVerifiedKey(row) {
2523
+ const key = { id: row.id };
2524
+ if (row.maxConcurrency !== void 0 && row.maxConcurrency !== null) {
2525
+ key.maxConcurrency = row.maxConcurrency;
2526
+ }
2527
+ const costLimits = extractCostLimits(row);
2528
+ if (costLimits) key.costLimits = costLimits;
2529
+ const rateLimit = extractRateLimit(row);
2530
+ if (rateLimit) key.rateLimit = rateLimit;
2531
+ const modelRestriction = extractModelRestriction(row);
2532
+ if (modelRestriction) key.modelRestriction = modelRestriction;
2533
+ return key;
2534
+ }
2535
+ async function verifyKey(db, presentedKey, now = Date.now()) {
2536
+ if (!presentedKey) return { status: "invalid" };
1754
2537
  const trimmed = presentedKey.trim();
1755
- if (!trimmed) return null;
2538
+ if (!trimmed) return { status: "invalid" };
1756
2539
  const row = await db.outboundApiKeysGetByHash(hashKey(trimmed));
1757
- if (!row) return null;
1758
- if (!row.enabled || row.revokedAt !== null) return null;
2540
+ if (!row) return { status: "invalid" };
2541
+ if (!row.enabled || row.revokedAt !== null) return { status: "invalid" };
2542
+ const expiry = computeKeyExpiry(row, now);
2543
+ if (expiry.expired) return { status: "expired" };
1759
2544
  void db.outboundApiKeysTouchLastUsed(row.id).catch(() => {
1760
2545
  });
1761
- return { id: row.id };
2546
+ if (expiry.needsActivation) {
2547
+ void db.outboundApiKeysMarkActivated(row.id, now).catch(() => {
2548
+ });
2549
+ }
2550
+ return { status: "ok", key: toVerifiedKey(row) };
2551
+ }
2552
+ async function verifyPresentedKey(db, presentedKey) {
2553
+ const result = await verifyKey(db, presentedKey);
2554
+ return result.status === "ok" ? result.key : null;
1762
2555
  }
1763
2556
 
1764
- // src/outbound-api/roleDetection.ts
1765
- import { normalizeModelId } from "@omnicross/contracts/canonical-models";
1766
- var BACKGROUND_TIER_TOKENS = /* @__PURE__ */ new Set([
1767
- "haiku",
1768
- "mini",
1769
- "flash",
1770
- "small",
1771
- "lite",
1772
- "nano",
1773
- "8b"
1774
- ]);
1775
- function modelTokens(normalizedId) {
1776
- return normalizedId.split(/[-._:/\s]+/).filter(Boolean);
1777
- }
1778
- function isBackgroundTierModel(rawModelId) {
1779
- if (!rawModelId) return false;
1780
- const tokens = modelTokens(normalizeModelId(rawModelId));
1781
- return tokens.some((tok) => BACKGROUND_TIER_TOKENS.has(tok));
1782
- }
1783
- function extractRequestedModel(ingressFormat, body) {
1784
- const model = body["model"];
1785
- return typeof model === "string" && model ? model : void 0;
1786
- }
1787
- function anthropicHasVision(body) {
1788
- const messages = body["messages"];
1789
- if (!Array.isArray(messages)) return false;
1790
- for (const msg of messages) {
1791
- const content = msg?.content;
1792
- if (!Array.isArray(content)) continue;
1793
- for (const part of content) {
1794
- if (part?.type === "image") return true;
2557
+ // src/outbound-api/outboundConcurrencyGate.ts
2558
+ var ConcurrencyQueueFullError = class extends Error {
2559
+ constructor(apiKeyId, maxQueueSize) {
2560
+ super(`concurrency queue full (max ${maxQueueSize}) for key '${apiKeyId}'`);
2561
+ this.apiKeyId = apiKeyId;
2562
+ this.maxQueueSize = maxQueueSize;
2563
+ this.name = "ConcurrencyQueueFullError";
2564
+ }
2565
+ apiKeyId;
2566
+ maxQueueSize;
2567
+ code = "concurrency_queue_full";
2568
+ };
2569
+ var ConcurrencyWaitTimeoutError = class extends Error {
2570
+ constructor(apiKeyId, waitTimeoutMs) {
2571
+ super(`concurrency wait timed out after ${waitTimeoutMs}ms for key '${apiKeyId}'`);
2572
+ this.apiKeyId = apiKeyId;
2573
+ this.waitTimeoutMs = waitTimeoutMs;
2574
+ this.name = "ConcurrencyWaitTimeoutError";
2575
+ }
2576
+ apiKeyId;
2577
+ waitTimeoutMs;
2578
+ code = "concurrency_wait_timeout";
2579
+ };
2580
+ var ConcurrencyWaitCancelledError = class extends Error {
2581
+ constructor(apiKeyId) {
2582
+ super(`concurrency wait cancelled for key '${apiKeyId}'`);
2583
+ this.apiKeyId = apiKeyId;
2584
+ this.name = "ConcurrencyWaitCancelledError";
2585
+ }
2586
+ apiKeyId;
2587
+ code = "concurrency_wait_cancelled";
2588
+ };
2589
+ function isConcurrencyRejection(err) {
2590
+ return err instanceof ConcurrencyQueueFullError || err instanceof ConcurrencyWaitTimeoutError || err instanceof ConcurrencyWaitCancelledError;
2591
+ }
2592
+ var OutboundConcurrencyGate = class {
2593
+ states = /* @__PURE__ */ new Map();
2594
+ ensure(apiKeyId) {
2595
+ let state = this.states.get(apiKeyId);
2596
+ if (!state) {
2597
+ state = { active: 0, waiters: [] };
2598
+ this.states.set(apiKeyId, state);
2599
+ }
2600
+ return state;
2601
+ }
2602
+ /** Delete a key's state once it is fully idle (no active, no waiters). */
2603
+ gc(apiKeyId) {
2604
+ const state = this.states.get(apiKeyId);
2605
+ if (state && state.active === 0 && state.waiters.length === 0) {
2606
+ this.states.delete(apiKeyId);
2607
+ }
2608
+ }
2609
+ /**
2610
+ * Acquire a concurrency slot for `apiKeyId` under `limit`. Grants immediately
2611
+ * when `active < limit`; else enqueues FIFO while under the per-key queue cap;
2612
+ * else rejects queue-full. `limit <= 0` = unlimited (always granted).
2613
+ */
2614
+ acquire(apiKeyId, limit, options) {
2615
+ const state = this.ensure(apiKeyId);
2616
+ const cap = limit > 0 ? limit : Infinity;
2617
+ if (state.active < cap) {
2618
+ state.active += 1;
2619
+ const slot = this.makeSlot(apiKeyId);
2620
+ return { granted: Promise.resolve(slot), cancel: slot.cancel };
2621
+ }
2622
+ const maxQueueSize = Math.max(
2623
+ Number.isFinite(cap) ? cap * options.maxQueueSizeFactor : Infinity,
2624
+ options.minQueueSize
2625
+ );
2626
+ if (state.waiters.length >= maxQueueSize) {
2627
+ const err = new ConcurrencyQueueFullError(apiKeyId, maxQueueSize);
2628
+ return { granted: Promise.reject(err), cancel: () => {
2629
+ } };
2630
+ }
2631
+ let settled = false;
2632
+ let granted = false;
2633
+ let resolveGrant;
2634
+ let rejectGrant;
2635
+ const grantedPromise = new Promise((resolve, reject) => {
2636
+ resolveGrant = resolve;
2637
+ rejectGrant = reject;
2638
+ });
2639
+ const waiter = {
2640
+ cap,
2641
+ settled: false,
2642
+ timeoutHandle: null,
2643
+ grant: () => {
2644
+ if (settled) return;
2645
+ settled = true;
2646
+ granted = true;
2647
+ waiter.settled = true;
2648
+ if (waiter.timeoutHandle) clearTimeout(waiter.timeoutHandle);
2649
+ state.active += 1;
2650
+ resolveGrant(this.makeSlot(apiKeyId));
2651
+ },
2652
+ fail: (err) => {
2653
+ if (settled) return;
2654
+ settled = true;
2655
+ waiter.settled = true;
2656
+ if (waiter.timeoutHandle) clearTimeout(waiter.timeoutHandle);
2657
+ const idx = state.waiters.indexOf(waiter);
2658
+ if (idx >= 0) state.waiters.splice(idx, 1);
2659
+ rejectGrant(err);
2660
+ this.gc(apiKeyId);
2661
+ }
2662
+ };
2663
+ waiter.timeoutHandle = setTimeout(() => {
2664
+ waiter.fail(new ConcurrencyWaitTimeoutError(apiKeyId, options.waitTimeoutMs));
2665
+ }, options.waitTimeoutMs);
2666
+ state.waiters.push(waiter);
2667
+ const cancel = () => {
2668
+ if (settled || granted) return;
2669
+ waiter.fail(new ConcurrencyWaitCancelledError(apiKeyId));
2670
+ };
2671
+ return { granted: grantedPromise, cancel };
2672
+ }
2673
+ /** Build an idempotent granted slot (`release` decrements; `cancel` is a no-op). */
2674
+ makeSlot(apiKeyId) {
2675
+ let released = false;
2676
+ const release2 = () => {
2677
+ if (released) return;
2678
+ released = true;
2679
+ const state = this.states.get(apiKeyId);
2680
+ if (!state) return;
2681
+ state.active = Math.max(0, state.active - 1);
2682
+ this.dispatchNext(apiKeyId);
2683
+ this.gc(apiKeyId);
2684
+ };
2685
+ return { release: release2, cancel: () => {
2686
+ } };
2687
+ }
2688
+ /** After a release freed a slot, grant the FIFO head if it fits under its cap. */
2689
+ dispatchNext(apiKeyId) {
2690
+ const state = this.states.get(apiKeyId);
2691
+ if (!state || state.waiters.length === 0) return;
2692
+ const head = state.waiters[0];
2693
+ if (state.active < head.cap) {
2694
+ state.waiters.shift();
2695
+ head.grant();
1795
2696
  }
1796
2697
  }
2698
+ /** Snapshot for observability — only keys with active slots or waiters. */
2699
+ getStatus() {
2700
+ const out = [];
2701
+ for (const [apiKeyId, state] of this.states) {
2702
+ if (state.active > 0 || state.waiters.length > 0) {
2703
+ out.push({ apiKeyId, active: state.active, waiting: state.waiters.length });
2704
+ }
2705
+ }
2706
+ return out;
2707
+ }
2708
+ /** Drop all state + pending timers (tests / teardown). */
2709
+ reset() {
2710
+ for (const state of this.states.values()) {
2711
+ for (const w of state.waiters) {
2712
+ if (w.timeoutHandle) clearTimeout(w.timeoutHandle);
2713
+ }
2714
+ }
2715
+ this.states.clear();
2716
+ }
2717
+ };
2718
+
2719
+ // src/outbound-api/outboundRateLimiter.ts
2720
+ var DEFAULT_WINDOW_MS = 6e4;
2721
+ var DEFAULT_MAX_REQUESTS = 60;
2722
+ var OutboundRateLimiter = class {
2723
+ windowMs;
2724
+ maxRequests;
2725
+ /** apiKeyId → ascending request timestamps within the current window. */
2726
+ hits = /* @__PURE__ */ new Map();
2727
+ constructor(options = {}) {
2728
+ this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
2729
+ this.maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
2730
+ }
2731
+ /**
2732
+ * Record a request for `apiKeyId` and decide whether it is allowed. Prunes
2733
+ * timestamps older than the window first; when allowed, the request's
2734
+ * timestamp is appended.
2735
+ *
2736
+ * `override` (outbound-key-policy) supplies a PER-KEY window/max for this
2737
+ * bucket, superseding the instance defaults for THAT key. Absent ⇒ the
2738
+ * instance default 60/60s (byte-identical to before this change). An effective
2739
+ * `maxRequests` of `0` means UNLIMITED — the request is allowed and NOT
2740
+ * recorded (no bucket growth).
2741
+ */
2742
+ check(apiKeyId, now = Date.now(), override) {
2743
+ const maxRequests = override?.maxRequests ?? this.maxRequests;
2744
+ const windowMs = override?.windowMs ?? this.windowMs;
2745
+ if (maxRequests === 0) return { allowed: true, retryAfterSeconds: 0 };
2746
+ const cutoff = now - windowMs;
2747
+ const timestamps = (this.hits.get(apiKeyId) ?? []).filter((ts) => ts > cutoff);
2748
+ if (timestamps.length >= maxRequests) {
2749
+ if (timestamps.length === 0) {
2750
+ this.hits.delete(apiKeyId);
2751
+ return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(windowMs / 1e3)) };
2752
+ }
2753
+ const oldest = timestamps[0];
2754
+ const retryAfterMs = Math.max(0, oldest + windowMs - now);
2755
+ this.hits.set(apiKeyId, timestamps);
2756
+ return {
2757
+ allowed: false,
2758
+ retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1e3))
2759
+ };
2760
+ }
2761
+ timestamps.push(now);
2762
+ this.hits.set(apiKeyId, timestamps);
2763
+ return { allowed: true, retryAfterSeconds: 0 };
2764
+ }
2765
+ /** Drop all recorded state (tests / teardown). */
2766
+ reset() {
2767
+ this.hits.clear();
2768
+ }
2769
+ };
2770
+
2771
+ // src/outbound-api/userMessageDetection.ts
2772
+ function asArray(v) {
2773
+ return Array.isArray(v) ? v : [];
2774
+ }
2775
+ function asObject(v) {
2776
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
2777
+ }
2778
+ function last(arr) {
2779
+ return arr.length > 0 ? arr[arr.length - 1] : void 0;
2780
+ }
2781
+ function isUserMessageAnthropic(body) {
2782
+ const messages = asArray(body["messages"]);
2783
+ const lastMsg = asObject(last(messages));
2784
+ if (!lastMsg || lastMsg["role"] !== "user") return false;
2785
+ const content = lastMsg["content"];
2786
+ if (typeof content === "string") return true;
2787
+ if (Array.isArray(content)) {
2788
+ const hasToolResult = content.some(
2789
+ (block) => asObject(block)?.["type"] === "tool_result"
2790
+ );
2791
+ return !hasToolResult;
2792
+ }
2793
+ return false;
2794
+ }
2795
+ function isUserMessageResponses(body) {
2796
+ const input = body["input"];
2797
+ if (typeof input === "string") return input.length > 0;
2798
+ if (Array.isArray(input)) {
2799
+ const lastItem = asObject(last(input));
2800
+ if (!lastItem) return false;
2801
+ if (lastItem["type"] === "function_call_output") return false;
2802
+ return lastItem["role"] === "user";
2803
+ }
1797
2804
  return false;
1798
2805
  }
1799
- function openaiChatHasVision(body) {
1800
- const messages = body["messages"];
1801
- if (!Array.isArray(messages)) return false;
1802
- for (const msg of messages) {
1803
- const content = msg?.content;
1804
- if (!Array.isArray(content)) continue;
1805
- for (const part of content) {
1806
- if (part?.type === "image_url") return true;
1807
- }
1808
- }
1809
- return false;
2806
+ function isUserMessageChat(body) {
2807
+ const messages = asArray(body["messages"]);
2808
+ const lastMsg = asObject(last(messages));
2809
+ if (!lastMsg) return false;
2810
+ return lastMsg["role"] === "user";
2811
+ }
2812
+ function isUserMessageGemini(body) {
2813
+ const contents = asArray(body["contents"]);
2814
+ const lastContent = asObject(last(contents));
2815
+ if (!lastContent || lastContent["role"] !== "user") return false;
2816
+ const parts = asArray(lastContent["parts"]);
2817
+ const hasFunctionResponse = parts.some(
2818
+ (part) => asObject(part)?.["functionResponse"] !== void 0
2819
+ );
2820
+ return !hasFunctionResponse;
2821
+ }
2822
+ function isUserMessageRequest(endpoint, parsedBody) {
2823
+ const body = asObject(parsedBody);
2824
+ if (!body) return false;
2825
+ switch (endpoint) {
2826
+ case "messages":
2827
+ return isUserMessageAnthropic(body);
2828
+ case "responses":
2829
+ return isUserMessageResponses(body);
2830
+ case "chat":
2831
+ return isUserMessageChat(body);
2832
+ case "gemini":
2833
+ return isUserMessageGemini(body);
2834
+ default:
2835
+ return false;
2836
+ }
2837
+ }
2838
+
2839
+ // src/outbound-api/userMessageSerialQueue.ts
2840
+ var SerialQueueTimeoutError = class extends Error {
2841
+ constructor(providerId, waitTimeoutMs) {
2842
+ super(`user-message serial queue wait timed out after ${waitTimeoutMs}ms for '${providerId}'`);
2843
+ this.providerId = providerId;
2844
+ this.waitTimeoutMs = waitTimeoutMs;
2845
+ this.name = "SerialQueueTimeoutError";
2846
+ }
2847
+ providerId;
2848
+ waitTimeoutMs;
2849
+ code = "serial_queue_timeout";
2850
+ };
2851
+ function isSerialQueueTimeout(err) {
2852
+ return err instanceof SerialQueueTimeoutError;
2853
+ }
2854
+ var UserMessageSerialQueue = class {
2855
+ states = /* @__PURE__ */ new Map();
2856
+ ensure(providerId) {
2857
+ let state = this.states.get(providerId);
2858
+ if (!state) {
2859
+ state = { holding: false, lastReleaseTs: null, waiters: [], dispatchHandle: null };
2860
+ this.states.set(providerId, state);
2861
+ }
2862
+ return state;
2863
+ }
2864
+ /**
2865
+ * Acquire the serial slot for `providerId`. Resolves immediately when the slot
2866
+ * is free, no one is queued, and the `delayMs` gap since the last release has
2867
+ * elapsed; otherwise enqueues FIFO and resolves in order (or rejects with a
2868
+ * {@link SerialQueueTimeoutError} after `waitTimeoutMs`).
2869
+ */
2870
+ acquire(providerId, options) {
2871
+ const { waitTimeoutMs, delayMs } = options;
2872
+ const now = options.now ?? Date.now();
2873
+ const state = this.ensure(providerId);
2874
+ const spacingElapsed = state.lastReleaseTs === null || now - state.lastReleaseTs >= delayMs;
2875
+ if (!state.holding && state.waiters.length === 0 && spacingElapsed) {
2876
+ state.holding = true;
2877
+ return Promise.resolve(this.makeSlot(providerId));
2878
+ }
2879
+ return new Promise((resolve, reject) => {
2880
+ const waiter = {
2881
+ delayMs,
2882
+ resolve,
2883
+ reject,
2884
+ timeoutHandle: null,
2885
+ done: false
2886
+ };
2887
+ waiter.timeoutHandle = setTimeout(() => {
2888
+ if (waiter.done) return;
2889
+ waiter.done = true;
2890
+ const idx = state.waiters.indexOf(waiter);
2891
+ if (idx >= 0) state.waiters.splice(idx, 1);
2892
+ reject(new SerialQueueTimeoutError(providerId, waitTimeoutMs));
2893
+ }, waitTimeoutMs);
2894
+ state.waiters.push(waiter);
2895
+ this.maybeDispatch(providerId);
2896
+ });
2897
+ }
2898
+ /** Build an idempotent release for a granted slot. */
2899
+ makeSlot(providerId) {
2900
+ let released = false;
2901
+ return {
2902
+ release: () => {
2903
+ if (released) return;
2904
+ released = true;
2905
+ const state = this.states.get(providerId);
2906
+ if (!state) return;
2907
+ state.holding = false;
2908
+ state.lastReleaseTs = Date.now();
2909
+ this.maybeDispatch(providerId);
2910
+ }
2911
+ };
2912
+ }
2913
+ /**
2914
+ * Grant the head waiter when the slot is free and the head's `delayMs` gap has
2915
+ * elapsed; otherwise schedule a dispatch for the residual gap. No-op while the
2916
+ * slot is held (release re-invokes this) or the queue is empty.
2917
+ */
2918
+ maybeDispatch(providerId) {
2919
+ const state = this.states.get(providerId);
2920
+ if (!state || state.holding || state.waiters.length === 0) return;
2921
+ const head = state.waiters[0];
2922
+ const now = Date.now();
2923
+ const elapsed = state.lastReleaseTs === null ? Infinity : now - state.lastReleaseTs;
2924
+ const residual = Math.max(0, head.delayMs - elapsed);
2925
+ if (residual <= 0) {
2926
+ this.grantHead(providerId);
2927
+ return;
2928
+ }
2929
+ if (state.dispatchHandle !== null) return;
2930
+ state.dispatchHandle = setTimeout(() => {
2931
+ state.dispatchHandle = null;
2932
+ this.grantHead(providerId);
2933
+ }, residual);
2934
+ }
2935
+ /** Hand the slot to the FIFO head (if free and someone is waiting). */
2936
+ grantHead(providerId) {
2937
+ const state = this.states.get(providerId);
2938
+ if (!state || state.holding || state.waiters.length === 0) return;
2939
+ if (state.dispatchHandle !== null) {
2940
+ clearTimeout(state.dispatchHandle);
2941
+ state.dispatchHandle = null;
2942
+ }
2943
+ const head = state.waiters.shift();
2944
+ if (!head) return;
2945
+ if (head.timeoutHandle) clearTimeout(head.timeoutHandle);
2946
+ head.done = true;
2947
+ state.holding = true;
2948
+ head.resolve(this.makeSlot(providerId));
2949
+ }
2950
+ /** Snapshot for observability — only keys holding a slot or with waiters. */
2951
+ getStatus() {
2952
+ const out = [];
2953
+ for (const [providerId, state] of this.states) {
2954
+ if (state.holding || state.waiters.length > 0) {
2955
+ out.push({ providerId, holding: state.holding, waiting: state.waiters.length });
2956
+ }
2957
+ }
2958
+ return out;
2959
+ }
2960
+ /** Drop all state + pending timers (tests / teardown). */
2961
+ reset() {
2962
+ for (const state of this.states.values()) {
2963
+ if (state.dispatchHandle) clearTimeout(state.dispatchHandle);
2964
+ for (const w of state.waiters) {
2965
+ if (w.timeoutHandle) clearTimeout(w.timeoutHandle);
2966
+ }
2967
+ }
2968
+ this.states.clear();
2969
+ }
2970
+ };
2971
+
2972
+ // src/outbound-api/voucher.ts
2973
+ import { createHash as createHash2 } from "crypto";
2974
+ var CODE_ENTROPY_CHARS = 32;
2975
+ var CODE_PREFIX = "CC_";
2976
+ var DISPLAY_PREFIX_LEN = 8;
2977
+ var DAY_MS2 = 864e5;
2978
+ function generateVoucherCode() {
2979
+ return CODE_PREFIX + randomBase62(CODE_ENTROPY_CHARS);
2980
+ }
2981
+ function hashVoucherCode(code) {
2982
+ return createHash2("sha256").update(code, "utf8").digest("hex");
2983
+ }
2984
+ function voucherCodePrefix(code) {
2985
+ return code.slice(0, DISPLAY_PREFIX_LEN);
2986
+ }
2987
+ function newVoucherId() {
2988
+ return `vch_${Date.now()}_${randomBase62(8)}`;
2989
+ }
2990
+ function computeVoucherGrant(voucher, currentKey, now) {
2991
+ if (voucher.type === "credit") {
2992
+ const current = currentKey?.totalCostLimitUsd ?? 0;
2993
+ let granted2 = current + (voucher.creditUsd ?? 0);
2994
+ if (voucher.maxTotalCostLimitUsd != null) {
2995
+ granted2 = Math.min(granted2, voucher.maxTotalCostLimitUsd);
2996
+ }
2997
+ return { totalCostLimitUsd: granted2 };
2998
+ }
2999
+ const currentExpiry = currentKey?.expiresAt ?? now;
3000
+ let granted = currentExpiry + (voucher.renewalDays ?? 0) * DAY_MS2;
3001
+ if (voucher.maxExpiryDays != null) {
3002
+ granted = Math.min(granted, now + voucher.maxExpiryDays * DAY_MS2);
3003
+ }
3004
+ return { expiresAt: granted };
3005
+ }
3006
+ function toVoucherInfo(v) {
3007
+ const info = {
3008
+ id: v.id,
3009
+ codePrefix: v.codePrefix,
3010
+ type: v.type,
3011
+ status: v.status,
3012
+ createdAt: v.createdAt
3013
+ };
3014
+ if (v.creditUsd != null) info.creditUsd = v.creditUsd;
3015
+ if (v.renewalDays != null) info.renewalDays = v.renewalDays;
3016
+ if (v.maxTotalCostLimitUsd != null) info.maxTotalCostLimitUsd = v.maxTotalCostLimitUsd;
3017
+ if (v.maxExpiryDays != null) info.maxExpiryDays = v.maxExpiryDays;
3018
+ if (v.redeemedAt != null) info.redeemedAt = v.redeemedAt;
3019
+ if (v.redeemedByKeyId != null) info.redeemedByKeyId = v.redeemedByKeyId;
3020
+ if (v.grantApplied != null) info.grantApplied = v.grantApplied;
3021
+ if (v.grantedTotalCostLimitUsd != null) info.grantedTotalCostLimitUsd = v.grantedTotalCostLimitUsd;
3022
+ if (v.grantedExpiresAt != null) info.grantedExpiresAt = v.grantedExpiresAt;
3023
+ if (v.revokedAt != null) info.revokedAt = v.revokedAt;
3024
+ return info;
3025
+ }
3026
+
3027
+ // src/outbound-api/voucherRedeem.ts
3028
+ function isRedeemRequest(method, url) {
3029
+ if (method !== "POST" || !url) return false;
3030
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
3031
+ return path.endsWith("/redeem");
3032
+ }
3033
+ function writeErr(res, status, message, headers = {}) {
3034
+ if (res.headersSent) return;
3035
+ res.writeHead(status, { "Content-Type": "application/json", ...headers });
3036
+ res.end(JSON.stringify({ error: { type: "voucher_error", message } }));
3037
+ }
3038
+ function writeResult(res, result) {
3039
+ if (res.headersSent) return;
3040
+ res.writeHead(200, { "Content-Type": "application/json" });
3041
+ res.end(JSON.stringify({ ok: true, ...result }));
1810
3042
  }
1811
- function openaiResponsesHasVision(body) {
1812
- const input = body["input"];
1813
- if (!Array.isArray(input)) return false;
1814
- for (const item of input) {
1815
- const content = item?.content;
1816
- if (Array.isArray(content)) {
1817
- for (const part of content) {
1818
- if (part?.type === "input_image") return true;
1819
- }
1820
- }
1821
- if (item?.type === "input_image") return true;
1822
- }
1823
- return false;
3043
+ function readBody2(req) {
3044
+ return new Promise((resolve, reject) => {
3045
+ const chunks = [];
3046
+ req.on("data", (c) => chunks.push(c));
3047
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
3048
+ req.on("error", reject);
3049
+ });
1824
3050
  }
1825
- function geminiHasVision(body) {
1826
- const contents = body["contents"];
1827
- if (!Array.isArray(contents)) return false;
1828
- for (const content of contents) {
1829
- const parts = content?.parts;
1830
- if (!Array.isArray(parts)) continue;
1831
- for (const part of parts) {
1832
- const p = part;
1833
- if (p?.inline_data || p?.inlineData || p?.file_data || p?.fileData) return true;
1834
- }
3051
+ function grantToResult(type, granted) {
3052
+ const result = { type };
3053
+ if (granted.totalCostLimitUsd != null) result.totalCostLimitUsd = granted.totalCostLimitUsd;
3054
+ if (granted.expiresAt != null) result.expiresAt = granted.expiresAt;
3055
+ return result;
3056
+ }
3057
+ function currentResult(type, keyRow) {
3058
+ const result = { type };
3059
+ if (type === "credit") {
3060
+ if (keyRow?.totalCostLimitUsd != null) result.totalCostLimitUsd = keyRow.totalCostLimitUsd;
3061
+ } else if (keyRow?.expiresAt != null) {
3062
+ result.expiresAt = keyRow.expiresAt;
3063
+ }
3064
+ return result;
3065
+ }
3066
+ function recordedAbsolute(v) {
3067
+ const granted = {};
3068
+ if (v.grantedTotalCostLimitUsd != null) granted.totalCostLimitUsd = v.grantedTotalCostLimitUsd;
3069
+ if (v.grantedExpiresAt != null) granted.expiresAt = v.grantedExpiresAt;
3070
+ return granted;
3071
+ }
3072
+ function policyFrom(granted) {
3073
+ const policy = {};
3074
+ if (granted.totalCostLimitUsd != null) policy.totalCostLimitUsd = granted.totalCostLimitUsd;
3075
+ if (granted.expiresAt != null) policy.expiresAt = granted.expiresAt;
3076
+ return policy;
3077
+ }
3078
+ var NOT_REDEEMABLE = "Invalid or non-redeemable code";
3079
+ var fallbackMutex = null;
3080
+ function getFallbackMutex() {
3081
+ fallbackMutex ??= new KeyedMutex();
3082
+ return fallbackMutex;
3083
+ }
3084
+ async function handleVoucherRedeem(req, res, deps, voucherEnabled, redeemLimiter, verifiedKeyId, presentedKey, now, redeemMutex) {
3085
+ if (!voucherEnabled || !deps.voucherDb) {
3086
+ writeErr(res, 403, "Voucher redemption is not enabled");
3087
+ return;
1835
3088
  }
1836
- return false;
1837
- }
1838
- function hasVisionContent(ingressFormat, body) {
1839
- switch (ingressFormat) {
1840
- case "anthropic-messages":
1841
- return anthropicHasVision(body);
1842
- case "openai-chat":
1843
- return openaiChatHasVision(body);
1844
- case "openai-responses":
1845
- return openaiResponsesHasVision(body);
1846
- case "gemini-generatecontent":
1847
- return geminiHasVision(body);
1848
- default:
1849
- return false;
3089
+ const voucherDb = deps.voucherDb;
3090
+ const rl = redeemLimiter.check(verifiedKeyId, now);
3091
+ if (!rl.allowed) {
3092
+ writeErr(res, 429, "Too many redeem attempts", { "Retry-After": String(rl.retryAfterSeconds) });
3093
+ return;
1850
3094
  }
1851
- }
1852
- function detectRequestRole(ingressFormat, body, options) {
1853
- if (hasVisionContent(ingressFormat, body)) return "vision";
1854
- const requestedModel = extractRequestedModel(ingressFormat, body);
1855
- if (requestedModel) {
1856
- const overrides = options?.backgroundModelIds;
1857
- if (overrides && overrides.length > 0) {
1858
- const reqNorm = normalizeModelId(requestedModel);
1859
- const inOverride = overrides.some((id) => {
1860
- if (!id) return false;
1861
- const candidate = id.includes(",") ? id.slice(id.indexOf(",") + 1) : id;
1862
- return normalizeModelId(candidate) === reqNorm || candidate === requestedModel;
1863
- });
1864
- if (inOverride) return "background";
1865
- }
1866
- if (isBackgroundTierModel(requestedModel)) return "background";
3095
+ const raw = await readBody2(req);
3096
+ let code;
3097
+ try {
3098
+ code = raw ? JSON.parse(raw)["code"] : void 0;
3099
+ } catch {
3100
+ writeErr(res, 400, "Invalid JSON in request body");
3101
+ return;
1867
3102
  }
1868
- return "default";
1869
- }
1870
- function endpointToIngressFormat(endpoint) {
1871
- switch (endpoint) {
1872
- case "chat":
1873
- return "openai-chat";
1874
- case "responses":
1875
- return "openai-responses";
1876
- case "messages":
1877
- return "anthropic-messages";
1878
- case "gemini":
1879
- return "gemini-generatecontent";
3103
+ if (typeof code !== "string" || !code.trim()) {
3104
+ writeErr(res, 400, "Missing redemption code");
3105
+ return;
1880
3106
  }
3107
+ const codeHash = hashVoucherCode(code.trim());
3108
+ const keyHash = hashKey(presentedKey.trim());
3109
+ const applyFirstPass = async (voucher, absolute) => {
3110
+ const ok = await deps.db.outboundApiKeysSetPolicy(verifiedKeyId, policyFrom(absolute));
3111
+ if (!ok) {
3112
+ await voucherDb.voucherRevertRedeem(voucher.id, verifiedKeyId);
3113
+ return { ok: false, status: 409, message: "Could not apply the grant \u2014 please retry" };
3114
+ }
3115
+ await voucherDb.voucherMarkGrantApplied(voucher.id);
3116
+ return { ok: true, result: grantToResult(voucher.type, absolute) };
3117
+ };
3118
+ const reconcileOne = async (voucher) => {
3119
+ const ok = await deps.db.outboundApiKeysSetPolicy(verifiedKeyId, policyFrom(recordedAbsolute(voucher)));
3120
+ if (ok) await voucherDb.voucherMarkGrantApplied(voucher.id);
3121
+ };
3122
+ const reconcileStranded = async () => {
3123
+ const all = await voucherDb.voucherList();
3124
+ const stranded = all.filter((v) => v.status === "redeemed" && v.grantApplied !== true && v.redeemedByKeyId === verifiedKeyId).sort((a, b) => (a.redeemedAt ?? 0) - (b.redeemedAt ?? 0));
3125
+ for (const v of stranded) await reconcileOne(v);
3126
+ };
3127
+ const runCritical = async () => {
3128
+ await reconcileStranded();
3129
+ const voucher = await voucherDb.voucherGetByHash(codeHash);
3130
+ if (!voucher || voucher.status === "revoked") {
3131
+ return { ok: false, status: 400, message: NOT_REDEEMABLE };
3132
+ }
3133
+ if (voucher.status === "redeemed") {
3134
+ if (voucher.redeemedByKeyId !== verifiedKeyId) {
3135
+ return { ok: false, status: 400, message: NOT_REDEEMABLE };
3136
+ }
3137
+ if (voucher.grantApplied === true) {
3138
+ const keyRow2 = await deps.db.outboundApiKeysGetByHash(keyHash);
3139
+ return { ok: true, result: currentResult(voucher.type, keyRow2) };
3140
+ }
3141
+ return await applyFirstPass(voucher, recordedAbsolute(voucher));
3142
+ }
3143
+ const keyRow = await deps.db.outboundApiKeysGetByHash(keyHash);
3144
+ const absolute = computeVoucherGrant(voucher, keyRow, now);
3145
+ const won = await voucherDb.voucherRedeemCas(voucher.id, verifiedKeyId, absolute, now);
3146
+ if (!won) {
3147
+ const after = await voucherDb.voucherGetByHash(codeHash);
3148
+ if (after?.status === "redeemed" && after.redeemedByKeyId === verifiedKeyId) {
3149
+ return after.grantApplied === true ? { ok: true, result: currentResult(after.type, await deps.db.outboundApiKeysGetByHash(keyHash)) } : await applyFirstPass(after, recordedAbsolute(after));
3150
+ }
3151
+ return { ok: false, status: 409, message: "Code was already redeemed" };
3152
+ }
3153
+ return await applyFirstPass(voucher, absolute);
3154
+ };
3155
+ const mutex = redeemMutex ?? getFallbackMutex();
3156
+ const outcome = await mutex.runExclusive(verifiedKeyId, runCritical);
3157
+ if (outcome.ok) writeResult(res, outcome.result);
3158
+ else writeErr(res, outcome.status, outcome.message);
1881
3159
  }
1882
3160
 
1883
3161
  // src/outbound-api/outboundApiRouter.ts
3162
+ var fallbackRedeemLimiter = null;
3163
+ function getFallbackRedeemLimiter() {
3164
+ fallbackRedeemLimiter ??= new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
3165
+ return fallbackRedeemLimiter;
3166
+ }
1884
3167
  function writeJsonError(res, status, message, headers = {}) {
1885
3168
  if (res.headersSent) return;
1886
3169
  res.writeHead(status, { "Content-Type": "application/json", ...headers });
1887
3170
  res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
1888
3171
  }
3172
+ function writeCostLimitError(res, scope, limitUsd, spentUsd) {
3173
+ if (res.headersSent) return;
3174
+ res.writeHead(402, { "Content-Type": "application/json" });
3175
+ res.end(
3176
+ JSON.stringify({
3177
+ error: {
3178
+ type: "outbound_api_error",
3179
+ message: `Cost limit reached for this key (${scope}): $${spentUsd.toFixed(4)} of $${limitUsd.toFixed(4)}`
3180
+ },
3181
+ scope,
3182
+ limitUsd,
3183
+ spentUsd
3184
+ })
3185
+ );
3186
+ }
3187
+ function writeModelNotAllowedError(res, model, mode) {
3188
+ if (res.headersSent) return;
3189
+ res.writeHead(403, { "Content-Type": "application/json" });
3190
+ res.end(
3191
+ JSON.stringify({
3192
+ error: {
3193
+ type: "outbound_api_error",
3194
+ message: `Model '${model}' is not permitted for this key (${mode})`
3195
+ },
3196
+ model,
3197
+ mode
3198
+ })
3199
+ );
3200
+ }
1889
3201
  function extractPresentedKey(req) {
1890
3202
  const auth = req.headers["authorization"];
1891
3203
  if (typeof auth === "string" && auth.trim()) {
@@ -1917,7 +3229,18 @@ function extractGeminiModelFromUrl(url) {
1917
3229
  const m = /\/models\/([^/:]+):(?:stream)?[gG]enerateContent$/.exec(path.replace(/\/+$/, ""));
1918
3230
  return m ? decodeURIComponent(m[1]) : void 0;
1919
3231
  }
1920
- function readBody2(req) {
3232
+ function isModelsListRequest(url) {
3233
+ if (!url) return false;
3234
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
3235
+ return path.endsWith("/models");
3236
+ }
3237
+ function writeChatModelsList(res, config) {
3238
+ const chat = config.endpoints.find((e) => e.endpoint === "chat");
3239
+ const data = (chat?.models ?? []).map((ref) => parseModelRef(ref)).filter((p) => p !== null).map((p) => ({ id: p.modelId, object: "model", owned_by: "omnicross" }));
3240
+ res.writeHead(200, { "Content-Type": "application/json" });
3241
+ res.end(JSON.stringify({ object: "list", data }));
3242
+ }
3243
+ function readBody3(req) {
1921
3244
  return new Promise((resolve, reject) => {
1922
3245
  const chunks = [];
1923
3246
  req.on("data", (chunk) => chunks.push(chunk));
@@ -1930,20 +3253,48 @@ function shimAuthHeader(req, routeToken) {
1930
3253
  delete req.headers["x-goog-api-key"];
1931
3254
  delete req.headers["x-api-key"];
1932
3255
  }
1933
- async function handleOutboundRequest(req, res, deps, config, rateLimiter) {
3256
+ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serialQueue, concurrencyGate, redeemLimiter, redeemMutex) {
3257
+ const now = Date.now();
3258
+ const audit = beginAuditCapture(req, res, now);
3259
+ const billing = beginBillingCapture(req, res, now);
1934
3260
  const presented = extractPresentedKey(req);
1935
- const verified = await verifyPresentedKey(deps.db, presented);
1936
- if (!verified) {
1937
- writeJsonError(res, 401, "Invalid or missing API key");
3261
+ const verification = await verifyKey(deps.db, presented, now);
3262
+ if (verification.status !== "ok") {
3263
+ writeJsonError(
3264
+ res,
3265
+ 401,
3266
+ verification.status === "expired" ? "API key has expired" : "Invalid or missing API key"
3267
+ );
3268
+ return;
3269
+ }
3270
+ const verified = verification.key;
3271
+ if (audit) audit.keyId = verified.id;
3272
+ if (billing) billing.keyId = verified.id;
3273
+ if (isRedeemRequest(req.method, req.url)) {
3274
+ await handleVoucherRedeem(
3275
+ req,
3276
+ res,
3277
+ deps,
3278
+ config.voucher?.enabled === true,
3279
+ redeemLimiter ?? getFallbackRedeemLimiter(),
3280
+ verified.id,
3281
+ presented ?? "",
3282
+ now,
3283
+ redeemMutex
3284
+ );
1938
3285
  return;
1939
3286
  }
1940
- const decision = rateLimiter.check(verified.id);
3287
+ const decision = rateLimiter.check(verified.id, now, verified.rateLimit);
1941
3288
  if (!decision.allowed) {
1942
3289
  writeJsonError(res, 429, "Rate limit exceeded", {
1943
3290
  "Retry-After": String(decision.retryAfterSeconds)
1944
3291
  });
1945
3292
  return;
1946
3293
  }
3294
+ if (req.method === "GET" && isModelsListRequest(req.url)) {
3295
+ writeChatModelsList(res, config);
3296
+ return;
3297
+ }
1947
3298
  const endpoint = selectEndpoint(req.method, req.url);
1948
3299
  if (!endpoint) {
1949
3300
  writeJsonError(res, 404, `Unsupported: ${req.method} ${req.url}`);
@@ -1954,53 +3305,165 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter) {
1954
3305
  writeJsonError(res, 503, `endpoint '${endpoint}' is not configured`);
1955
3306
  return;
1956
3307
  }
1957
- const ingressFormat = endpointToIngressFormat(endpoint);
1958
- const rawBody = await readBody2(req);
1959
- let parsedBody = {};
1960
- try {
1961
- parsedBody = rawBody ? JSON.parse(rawBody) : {};
1962
- } catch {
1963
- writeJsonError(res, 400, "Invalid JSON in request body");
1964
- return;
1965
- }
1966
- if (endpoint === "gemini" && typeof parsedBody["model"] !== "string") {
1967
- const urlModel = extractGeminiModelFromUrl(req.url);
1968
- if (urlModel) parsedBody["model"] = urlModel;
3308
+ if (verified.costLimits && deps.keySpendTracker) {
3309
+ const spend = await deps.keySpendTracker.getSpend(verified.id, now);
3310
+ const quota = checkKeyQuota(verified.costLimits, spend);
3311
+ if (!quota.allowed) {
3312
+ emitWebhookEvent({
3313
+ kind: "key.quotaExceeded",
3314
+ at: now,
3315
+ keyId: verified.id,
3316
+ scope: quota.scope,
3317
+ limitUsd: quota.limitUsd,
3318
+ spentUsd: quota.spentUsd
3319
+ });
3320
+ writeCostLimitError(res, quota.scope, quota.limitUsd, quota.spentUsd);
3321
+ return;
3322
+ }
3323
+ for (const warn of computeQuotaWarnings(verified.costLimits, spend)) {
3324
+ if (markQuotaWarnedOnce(verified.id, warn.scope, warn.windowStart)) {
3325
+ emitWebhookEvent({
3326
+ kind: "key.quotaWarning",
3327
+ at: now,
3328
+ keyId: verified.id,
3329
+ scope: warn.scope,
3330
+ limitUsd: warn.limitUsd,
3331
+ spentUsd: warn.spentUsd
3332
+ });
3333
+ }
3334
+ }
1969
3335
  }
1970
- const role = detectRequestRole(ingressFormat, parsedBody, {
1971
- backgroundModelIds: endpointConfig.backgroundModelIds
1972
- });
1973
- const resolved = await resolveRoute({
1974
- config: endpointConfig,
1975
- role,
1976
- ingressFormat,
1977
- llmConfig: deps.llmConfig,
1978
- // pool-seam (omnicross-daemon-parity-poolseam, design D1/D2(a)): synthesize a
1979
- // STABLE per-verified-key sessionId so the BYO ingress can seed a pool session
1980
- // binding and 429/529/401/403 failover actually fires. Synthesize ONLY when
1981
- // the pool is wired — pool-null embedders/tests keep `route.sessionId === null`
1982
- // (byte-identical to pre-seam). The `outbound:` prefix is namespace-isolated
1983
- // from real chat-session ids; `verified.id` is a small operator-controlled set,
1984
- // so `sessionBindings` stays bounded (one binding per named key, not per
1985
- // request — `sessionBindings` has no TTL).
1986
- sessionId: deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}` : null
1987
- });
1988
- if (!resolved.ok) {
1989
- writeJsonError(res, resolved.error.status, resolved.error.message);
1990
- return;
3336
+ const concurrencyLimit = verified.maxConcurrency;
3337
+ let releaseConcurrency = null;
3338
+ if (typeof concurrencyLimit === "number" && concurrencyLimit > 0) {
3339
+ const cq = config.concurrencyQueue ?? DEFAULT_CONCURRENCY_QUEUE;
3340
+ const acquisition = concurrencyGate.acquire(verified.id, concurrencyLimit, {
3341
+ maxQueueSizeFactor: cq.maxQueueSizeFactor,
3342
+ minQueueSize: cq.minQueueSize,
3343
+ waitTimeoutMs: cq.waitTimeoutMs
3344
+ });
3345
+ const cancelOnClose = () => acquisition.cancel();
3346
+ res.once("close", cancelOnClose);
3347
+ let slot;
3348
+ try {
3349
+ slot = await acquisition.granted;
3350
+ } catch (err) {
3351
+ res.removeListener("close", cancelOnClose);
3352
+ if (isConcurrencyRejection(err)) {
3353
+ writeJsonError(res, 429, "Concurrency limit exceeded", { "Retry-After": "5" });
3354
+ return;
3355
+ }
3356
+ throw err;
3357
+ }
3358
+ res.removeListener("close", cancelOnClose);
3359
+ const release2 = () => slot.release();
3360
+ releaseConcurrency = release2;
3361
+ res.once("close", release2);
1991
3362
  }
1992
- const routeMap = deps.providerProxy.getRouteMap();
1993
- const token = routeMap.addRoute(resolved.route);
1994
3363
  try {
1995
- shimAuthHeader(req, token);
1996
- const replay = makeReplayRequest(req, rawBody);
1997
- await routeRequest(replay, res, routeMap, deps.proxyDeps);
1998
- } catch (err) {
1999
- const message = serializeError(err);
2000
- console.error("[OutboundApi] dispatch error:", message);
2001
- writeJsonError(res, 502, message);
3364
+ const ingressFormat = endpointToIngressFormat(endpoint);
3365
+ const rawBody = await readBody3(req);
3366
+ let parsedBody = {};
3367
+ try {
3368
+ parsedBody = rawBody ? JSON.parse(rawBody) : {};
3369
+ } catch {
3370
+ writeJsonError(res, 400, "Invalid JSON in request body");
3371
+ return;
3372
+ }
3373
+ if (audit) audit.setRequestBody(rawBody);
3374
+ if (endpoint === "gemini" && typeof parsedBody["model"] !== "string") {
3375
+ const urlModel = extractGeminiModelFromUrl(req.url);
3376
+ if (urlModel) parsedBody["model"] = urlModel;
3377
+ }
3378
+ const sessionId = deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}` : null;
3379
+ const resolved = isKindMappedEndpoint(endpoint) || endpoint === "chat" ? await resolveRoute({
3380
+ config: endpointConfig,
3381
+ ingressFormat,
3382
+ llmConfig: deps.llmConfig,
3383
+ sessionId,
3384
+ // Capture the ORIGINAL requested id BEFORE any downstream swap; for
3385
+ // kind-mapped endpoints it selects the kind AND is stamped onto
3386
+ // `route.requestedModel`; for chat it is matched against the list.
3387
+ requestedModel: extractRequestedModel(ingressFormat, parsedBody),
3388
+ // Attribution: stamp the verified named-key id onto the route.
3389
+ apiKeyId: verified.id
3390
+ }) : await resolveRoute({
3391
+ config: endpointConfig,
3392
+ role: detectRequestRole(ingressFormat, parsedBody, {
3393
+ backgroundModelIds: endpointConfig.backgroundModelIds
3394
+ }),
3395
+ ingressFormat,
3396
+ llmConfig: deps.llmConfig,
3397
+ sessionId,
3398
+ // Attribution: stamp the verified named-key id onto the route.
3399
+ apiKeyId: verified.id
3400
+ });
3401
+ if (!resolved.ok) {
3402
+ writeJsonError(res, resolved.error.status, resolved.error.message);
3403
+ return;
3404
+ }
3405
+ if (audit) {
3406
+ audit.model = resolved.route.model;
3407
+ audit.provider = resolved.route.providerId;
3408
+ }
3409
+ if (billing) {
3410
+ billing.model = resolved.route.model;
3411
+ billing.provider = resolved.route.providerId;
3412
+ billing.authMode = resolved.route.authMode;
3413
+ }
3414
+ if (verified.modelRestriction) {
3415
+ const modelDecision = checkModelAllowed(verified.modelRestriction, resolved.route.model);
3416
+ if (!modelDecision.allowed) {
3417
+ writeModelNotAllowedError(res, modelDecision.model, verified.modelRestriction.mode);
3418
+ return;
3419
+ }
3420
+ }
3421
+ let releaseSerial = null;
3422
+ const providerKey = resolved.route.providerId;
3423
+ if (config.userMessageQueue?.enabled && providerKey && isUserMessageRequest(endpoint, parsedBody)) {
3424
+ const umq = config.userMessageQueue;
3425
+ try {
3426
+ const serialSlot = await serialQueue.acquire(providerKey, {
3427
+ waitTimeoutMs: umq.waitTimeoutMs,
3428
+ delayMs: umq.delayMs
3429
+ });
3430
+ releaseSerial = () => serialSlot.release();
3431
+ } catch (err) {
3432
+ if (isSerialQueueTimeout(err)) {
3433
+ writeJsonError(res, 503, "User-message serial queue wait timed out");
3434
+ return;
3435
+ }
3436
+ throw err;
3437
+ }
3438
+ }
3439
+ if (releaseSerial) {
3440
+ const original = res.writeHead.bind(res);
3441
+ const releaseOnce = releaseSerial;
3442
+ res.writeHead = ((...args) => {
3443
+ res.writeHead = original;
3444
+ releaseOnce();
3445
+ return original(...args);
3446
+ });
3447
+ }
3448
+ const routeMap = deps.providerProxy.getRouteMap();
3449
+ const token = routeMap.addRoute(resolved.route);
3450
+ try {
3451
+ shimAuthHeader(req, token);
3452
+ const replay = makeReplayRequest(req, rawBody);
3453
+ await routeRequest(replay, res, routeMap, deps.proxyDeps);
3454
+ } catch (err) {
3455
+ const message = serializeError(err);
3456
+ if (deps.logger) deps.logger.error("[OutboundApi] dispatch error:", message);
3457
+ else console.error("[OutboundApi] dispatch error:", message);
3458
+ emitWebhookEvent({ kind: "server.error", at: Date.now(), message });
3459
+ if (audit) audit.error = message;
3460
+ writeJsonError(res, 502, message);
3461
+ } finally {
3462
+ routeMap.removeRoute(token);
3463
+ if (releaseSerial) releaseSerial();
3464
+ }
2002
3465
  } finally {
2003
- routeMap.removeRoute(token);
3466
+ if (releaseConcurrency) releaseConcurrency();
2004
3467
  }
2005
3468
  }
2006
3469
  function makeReplayRequest(req, rawBody) {
@@ -2014,53 +3477,20 @@ function makeReplayRequest(req, rawBody) {
2014
3477
  return readable;
2015
3478
  }
2016
3479
 
2017
- // src/outbound-api/outboundRateLimiter.ts
2018
- var DEFAULT_WINDOW_MS = 6e4;
2019
- var DEFAULT_MAX_REQUESTS = 60;
2020
- var OutboundRateLimiter = class {
2021
- windowMs;
2022
- maxRequests;
2023
- /** apiKeyId → ascending request timestamps within the current window. */
2024
- hits = /* @__PURE__ */ new Map();
2025
- constructor(options = {}) {
2026
- this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
2027
- this.maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
2028
- }
2029
- /**
2030
- * Record a request for `apiKeyId` and decide whether it is allowed. Prunes
2031
- * timestamps older than the window first; when allowed, the request's
2032
- * timestamp is appended.
2033
- */
2034
- check(apiKeyId, now = Date.now()) {
2035
- const cutoff = now - this.windowMs;
2036
- const timestamps = (this.hits.get(apiKeyId) ?? []).filter((ts) => ts > cutoff);
2037
- if (timestamps.length >= this.maxRequests) {
2038
- if (timestamps.length === 0) {
2039
- this.hits.delete(apiKeyId);
2040
- return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(this.windowMs / 1e3)) };
2041
- }
2042
- const oldest = timestamps[0];
2043
- const retryAfterMs = Math.max(0, oldest + this.windowMs - now);
2044
- this.hits.set(apiKeyId, timestamps);
2045
- return {
2046
- allowed: false,
2047
- retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1e3))
2048
- };
2049
- }
2050
- timestamps.push(now);
2051
- this.hits.set(apiKeyId, timestamps);
2052
- return { allowed: true, retryAfterSeconds: 0 };
2053
- }
2054
- /** Drop all recorded state (tests / teardown). */
2055
- reset() {
2056
- this.hits.clear();
2057
- }
2058
- };
2059
-
2060
3480
  // src/outbound-api/OutboundApiServer.ts
2061
3481
  var DEFAULT_OUTBOUND_PORT = 8765;
2062
3482
  var LOOPBACK_ADDR = "127.0.0.1";
2063
3483
  var LAN_ADDR = "0.0.0.0";
3484
+ var OutboundApiConfigError = class extends Error {
3485
+ missing;
3486
+ constructor(missing) {
3487
+ super(
3488
+ "Outbound API server cannot start: incomplete model-kind configuration \u2014 " + missing.map((m) => `${m.endpoint} missing [${m.missingKinds.join(", ")}]`).join("; ")
3489
+ );
3490
+ this.name = "OutboundApiConfigError";
3491
+ this.missing = missing;
3492
+ }
3493
+ };
2064
3494
  var OutboundApiServer = class {
2065
3495
  constructor(deps, onPortChange) {
2066
3496
  this.deps = deps;
@@ -2072,7 +3502,25 @@ var OutboundApiServer = class {
2072
3502
  boundPort = 0;
2073
3503
  boundAddr = LOOPBACK_ADDR;
2074
3504
  endpoints = [];
3505
+ userMessageQueue;
3506
+ concurrencyQueue;
3507
+ voucherConfig;
2075
3508
  rateLimiter = new OutboundRateLimiter();
3509
+ /**
3510
+ * Redeem-attempt limiter (voucher-redemption #9, design D6) — a SEPARATE bucket
3511
+ * from the traffic `rateLimiter`, keyed by the authenticating key id, so
3512
+ * brute-forcing `CC_` codes is throttled (a handful/min) without touching the
3513
+ * per-key request rate. Conservative fixed defaults (10 / 60s).
3514
+ */
3515
+ redeemLimiter = new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
3516
+ /**
3517
+ * Per-key redeem mutex (voucher-redemption #9, MJ1 fix). One instance for the
3518
+ * server's lifetime so concurrent redeem REQUESTS for the same key serialize
3519
+ * (relative grant increments accumulate instead of clobbering a snapshot).
3520
+ */
3521
+ redeemMutex = new KeyedMutex();
3522
+ serialQueue = new UserMessageSerialQueue();
3523
+ concurrencyGate = new OutboundConcurrencyGate();
2076
3524
  /**
2077
3525
  * Apply a config. Restarts the listener ONLY when the bind address or port
2078
3526
  * changes (or when toggling enabled); per-endpoint routing config is updated
@@ -2080,12 +3528,25 @@ var OutboundApiServer = class {
2080
3528
  */
2081
3529
  async applyConfig(input) {
2082
3530
  this.endpoints = input.endpoints;
3531
+ this.userMessageQueue = input.userMessageQueue;
3532
+ this.concurrencyQueue = input.concurrencyQueue;
3533
+ this.voucherConfig = input.voucher;
2083
3534
  const wantAddr = input.networkBinding ? LAN_ADDR : LOOPBACK_ADDR;
2084
3535
  const wantPort = input.port ?? DEFAULT_OUTBOUND_PORT;
2085
3536
  if (!input.enabled) {
2086
3537
  await this.stop();
2087
3538
  return;
2088
3539
  }
3540
+ const missing = validateServerModelConfig({
3541
+ enabled: true,
3542
+ networkBinding: input.networkBinding,
3543
+ endpoints: input.endpoints,
3544
+ port: input.port
3545
+ });
3546
+ if (missing.length > 0) {
3547
+ await this.stop();
3548
+ throw new OutboundApiConfigError(missing);
3549
+ }
2089
3550
  const running = this.server !== null;
2090
3551
  const bindChanged = running && (this.boundAddr !== wantAddr || this.boundPort !== wantPort);
2091
3552
  if (running && !bindChanged) {
@@ -2101,7 +3562,7 @@ var OutboundApiServer = class {
2101
3562
  this.boundAddr = bindAddr;
2102
3563
  this.boundPort = actualPort;
2103
3564
  if (actualPort !== port) this.onPortChange?.(actualPort);
2104
- console.log(`[OutboundApiServer] Listening on ${bindAddr}:${actualPort}`);
3565
+ this.logInfo(`[OutboundApiServer] Listening on ${bindAddr}:${actualPort}`);
2105
3566
  return actualPort;
2106
3567
  }
2107
3568
  /** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
@@ -2123,7 +3584,7 @@ var OutboundApiServer = class {
2123
3584
  const addr = server.address();
2124
3585
  if (addr && typeof addr === "object") {
2125
3586
  server.removeListener("error", onError);
2126
- server.on("error", (e) => console.error("[OutboundApiServer] server error", serializeError(e)));
3587
+ server.on("error", (e) => this.logError("[OutboundApiServer] server error", serializeError(e)));
2127
3588
  this.server = server;
2128
3589
  resolve(addr.port);
2129
3590
  } else {
@@ -2134,21 +3595,58 @@ var OutboundApiServer = class {
2134
3595
  }
2135
3596
  /** Per-request handler. Auth is enforced on EVERY request (incl. loopback). */
2136
3597
  onRequest(req, res) {
3598
+ if (this.deps.healthReportProvider && this.tryServeHealth(req, res)) return;
2137
3599
  handleOutboundRequest(
2138
3600
  req,
2139
3601
  res,
2140
3602
  this.deps,
2141
- { endpoints: this.endpoints },
2142
- this.rateLimiter
3603
+ {
3604
+ endpoints: this.endpoints,
3605
+ userMessageQueue: this.userMessageQueue,
3606
+ concurrencyQueue: this.concurrencyQueue,
3607
+ voucher: this.voucherConfig
3608
+ },
3609
+ this.rateLimiter,
3610
+ this.serialQueue,
3611
+ this.concurrencyGate,
3612
+ this.redeemLimiter,
3613
+ this.redeemMutex
2143
3614
  ).catch((err) => {
2144
3615
  const message = serializeError(err);
2145
- console.error("[OutboundApiServer] unhandled error:", message);
3616
+ this.logError("[OutboundApiServer] unhandled error:", message);
2146
3617
  if (!res.headersSent) {
2147
3618
  res.writeHead(500, { "Content-Type": "application/json" });
2148
3619
  res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
2149
3620
  }
2150
3621
  });
2151
3622
  }
3623
+ /**
3624
+ * Serve `GET|HEAD /health` (+ `/healthz`) from the injected provider, returning
3625
+ * true when it handled the request. 200 when `ok`, else 503; secret-free body.
3626
+ */
3627
+ tryServeHealth(req, res) {
3628
+ const provider = this.deps.healthReportProvider;
3629
+ if (!provider) return false;
3630
+ if (req.method !== "GET" && req.method !== "HEAD") return false;
3631
+ const path = (req.url ?? "/").split("?")[0]?.replace(/\/+$/, "") || "/";
3632
+ if (path !== "/health" && path !== "/healthz") return false;
3633
+ const report = provider();
3634
+ res.writeHead(healthHttpStatus(report.status), { "Content-Type": "application/json" });
3635
+ res.end(req.method === "HEAD" ? void 0 : JSON.stringify(report));
3636
+ return true;
3637
+ }
3638
+ /** Route an info lifecycle line through the injected logger, else `console.log`
3639
+ * (byte-identical legacy fallback when no logger is wired). */
3640
+ logInfo(message) {
3641
+ if (this.deps.logger) this.deps.logger.info(message);
3642
+ else console.log(message);
3643
+ }
3644
+ /** Route an error lifecycle line through the injected logger, else `console.error`. */
3645
+ logError(message, detail) {
3646
+ if (this.deps.logger) this.deps.logger.error(message, detail);
3647
+ else if (detail === void 0) console.error(message);
3648
+ else console.error(message, detail);
3649
+ }
2152
3650
  /** Stop the listener and release the port. */
2153
3651
  async stop() {
2154
3652
  const server = this.server;
@@ -2157,7 +3655,7 @@ var OutboundApiServer = class {
2157
3655
  this.boundPort = 0;
2158
3656
  return new Promise((resolve) => {
2159
3657
  server.close(() => {
2160
- console.log("[OutboundApiServer] Stopped");
3658
+ this.logInfo("[OutboundApiServer] Stopped");
2161
3659
  resolve();
2162
3660
  });
2163
3661
  });
@@ -2189,6 +3687,17 @@ var OutboundApiServer = class {
2189
3687
  lanFormats: lanBase ? formatUrls(lanBase) : null
2190
3688
  };
2191
3689
  }
3690
+ /**
3691
+ * Live queue-occupancy snapshot (only active entries). This getter's name +
3692
+ * shape are FROZEN — `omnicross-uqc-daemon` spreads it into its `/status`
3693
+ * response; the existing {@link getStatus} shape is deliberately NOT changed.
3694
+ */
3695
+ getQueueStatus() {
3696
+ return {
3697
+ serial: this.serialQueue.getStatus(),
3698
+ concurrency: this.concurrencyGate.getStatus()
3699
+ };
3700
+ }
2192
3701
  };
2193
3702
  function formatUrls(base) {
2194
3703
  return {
@@ -2209,66 +3718,88 @@ function firstLanIPv4() {
2209
3718
  return null;
2210
3719
  }
2211
3720
 
2212
- // src/outbound-api/apiServerConfig.ts
2213
- var OUTBOUND_API_SERVER_CONFIG_KEY = "outboundApiServer.config";
2214
- var ALL_ENDPOINTS = ["chat", "responses", "messages", "gemini"];
2215
- function defaultEndpointConfig(endpoint) {
2216
- return {
2217
- endpoint,
2218
- defaultModel: "",
2219
- backgroundModel: "",
2220
- useSubscription: false
2221
- };
2222
- }
2223
- function defaultServerConfig() {
2224
- return {
2225
- enabled: false,
2226
- networkBinding: false,
2227
- endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
2228
- port: DEFAULT_OUTBOUND_PORT
2229
- };
2230
- }
2231
- function normalizeServerConfig(raw) {
2232
- const base = defaultServerConfig();
2233
- if (!raw) return base;
2234
- const byEndpoint = /* @__PURE__ */ new Map();
2235
- for (const e of raw.endpoints ?? []) {
2236
- if (e && ALL_ENDPOINTS.includes(e.endpoint)) {
2237
- byEndpoint.set(e.endpoint, {
2238
- endpoint: e.endpoint,
2239
- defaultModel: e.defaultModel ?? "",
2240
- backgroundModel: e.backgroundModel ?? "",
2241
- visionModel: e.visionModel,
2242
- useSubscription: e.useSubscription === true,
2243
- backgroundModelIds: e.backgroundModelIds
2244
- });
3721
+ // src/outbound-api/keySpendTracker.ts
3722
+ function startOfLocalDay(ts) {
3723
+ const d = new Date(ts);
3724
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
3725
+ }
3726
+ function startOfLocalWeek(ts) {
3727
+ const d = new Date(ts);
3728
+ const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());
3729
+ day.setDate(day.getDate() - day.getDay());
3730
+ return day.getTime();
3731
+ }
3732
+ var KeySpendTracker = class {
3733
+ constructor(seeder) {
3734
+ this.seeder = seeder;
3735
+ }
3736
+ seeder;
3737
+ /** apiKeyId → live accumulated spend (seeded lazily). */
3738
+ spend = /* @__PURE__ */ new Map();
3739
+ /**
3740
+ * Read the key's accumulated spend, seeding from the durable store on a cache
3741
+ * miss and rolling the daily/weekly windows on a hit.
3742
+ */
3743
+ async getSpend(apiKeyId, now) {
3744
+ const existing = this.spend.get(apiKeyId);
3745
+ if (existing) {
3746
+ rollWindows(existing, now);
3747
+ return existing;
3748
+ }
3749
+ const seeded = await this.seed(apiKeyId, now);
3750
+ const existingAfterSeed = this.spend.get(apiKeyId);
3751
+ if (existingAfterSeed) {
3752
+ rollWindows(existingAfterSeed, now);
3753
+ return existingAfterSeed;
2245
3754
  }
3755
+ this.spend.set(apiKeyId, seeded);
3756
+ return seeded;
3757
+ }
3758
+ /**
3759
+ * Increment the aggregate for one recorded request. No-op when the key has not
3760
+ * been seeded yet — a later `getSpend` will lazy-seed from the durable store,
3761
+ * which already includes this record, so it is neither lost nor double-counted.
3762
+ */
3763
+ add(apiKeyId, costUsd, now) {
3764
+ const s = this.spend.get(apiKeyId);
3765
+ if (!s) return;
3766
+ rollWindows(s, now);
3767
+ s.dailyUsd += costUsd;
3768
+ s.weeklyUsd += costUsd;
3769
+ s.totalUsd += costUsd;
3770
+ }
3771
+ /** Drop all cached spend (tests / teardown). */
3772
+ reset() {
3773
+ this.spend.clear();
3774
+ }
3775
+ /**
3776
+ * Derive a fresh spend snapshot for `apiKeyId` from the durable store in a
3777
+ * SINGLE scan (all three windows at once). `endTs` is EXCLUSIVE, so `now + 1`
3778
+ * includes a record stamped at exactly `now`.
3779
+ */
3780
+ async seed(apiKeyId, now) {
3781
+ const dailyWindowStart = startOfLocalDay(now);
3782
+ const weeklyWindowStart = startOfLocalWeek(now);
3783
+ const { totalUsd, dailyUsd, weeklyUsd } = await this.seeder.getSpendByKey({
3784
+ apiKeyId,
3785
+ dayStartTs: dailyWindowStart,
3786
+ weekStartTs: weeklyWindowStart,
3787
+ endTs: now + 1
3788
+ });
3789
+ return { dailyUsd, dailyWindowStart, weeklyUsd, weeklyWindowStart, totalUsd };
3790
+ }
3791
+ };
3792
+ function rollWindows(s, now) {
3793
+ const day = startOfLocalDay(now);
3794
+ if (s.dailyWindowStart !== day) {
3795
+ s.dailyUsd = 0;
3796
+ s.dailyWindowStart = day;
3797
+ }
3798
+ const week = startOfLocalWeek(now);
3799
+ if (s.weeklyWindowStart !== week) {
3800
+ s.weeklyUsd = 0;
3801
+ s.weeklyWindowStart = week;
2246
3802
  }
2247
- return {
2248
- enabled: raw.enabled === true,
2249
- networkBinding: raw.networkBinding === true,
2250
- endpoints: ALL_ENDPOINTS.map(
2251
- (ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep)
2252
- ),
2253
- port: raw.port ?? base.port
2254
- };
2255
- }
2256
- async function loadServerConfig(store) {
2257
- const raw = await store.get(
2258
- OUTBOUND_API_SERVER_CONFIG_KEY
2259
- );
2260
- return normalizeServerConfig(raw);
2261
- }
2262
- async function saveServerConfig(store, config) {
2263
- await store.set(OUTBOUND_API_SERVER_CONFIG_KEY, config);
2264
- }
2265
- function mergeServerConfig(current, patch) {
2266
- return normalizeServerConfig({
2267
- enabled: patch.enabled ?? current.enabled,
2268
- networkBinding: patch.networkBinding ?? current.networkBinding,
2269
- endpoints: patch.endpoints ?? current.endpoints,
2270
- port: patch.port ?? current.port
2271
- });
2272
3803
  }
2273
3804
 
2274
3805
  // src/outbound-api/index.ts
@@ -4380,21 +5911,60 @@ export {
4380
5911
  ProviderProxy,
4381
5912
  getProviderProxy,
4382
5913
  __resetProviderProxyForTests,
5914
+ KeyedMutex,
5915
+ OUTBOUND_API_SERVER_CONFIG_KEY,
5916
+ DEFAULT_USER_MESSAGE_QUEUE,
5917
+ DEFAULT_CONCURRENCY_QUEUE,
5918
+ DEFAULT_ACCOUNT_PROBE,
5919
+ normalizeAccountProbe,
5920
+ normalizeAudit,
5921
+ normalizeBilling,
5922
+ DEFAULT_FINGERPRINT,
5923
+ normalizeFingerprint,
5924
+ normalizeVoucher,
5925
+ normalizeProxyConfig,
5926
+ normalizeProxySegment,
5927
+ normalizeWebhookDestination,
5928
+ normalizeWebhookSegment,
5929
+ normalizeQueueSegments,
5930
+ normalizePrefixTargets,
5931
+ defaultServerConfig,
5932
+ normalizeServerConfig,
5933
+ loadServerConfig,
5934
+ saveServerConfig,
5935
+ mergeServerConfig,
5936
+ computeKeyExpiry,
5937
+ checkKeyQuota,
5938
+ randomBase62,
4383
5939
  hashKey,
4384
5940
  createNamedKey,
5941
+ verifyKey,
4385
5942
  verifyPresentedKey,
4386
- detectRequestRole,
4387
- endpointToIngressFormat,
5943
+ ConcurrencyQueueFullError,
5944
+ ConcurrencyWaitTimeoutError,
5945
+ ConcurrencyWaitCancelledError,
5946
+ isConcurrencyRejection,
5947
+ OutboundConcurrencyGate,
4388
5948
  OutboundRateLimiter,
5949
+ isUserMessageRequest,
5950
+ SerialQueueTimeoutError,
5951
+ isSerialQueueTimeout,
5952
+ UserMessageSerialQueue,
5953
+ generateVoucherCode,
5954
+ hashVoucherCode,
5955
+ voucherCodePrefix,
5956
+ newVoucherId,
5957
+ computeVoucherGrant,
5958
+ toVoucherInfo,
5959
+ isRedeemRequest,
5960
+ handleVoucherRedeem,
4389
5961
  DEFAULT_OUTBOUND_PORT,
5962
+ OutboundApiConfigError,
4390
5963
  OutboundApiServer,
4391
5964
  formatUrls,
4392
- OUTBOUND_API_SERVER_CONFIG_KEY,
4393
- defaultServerConfig,
4394
- normalizeServerConfig,
4395
- loadServerConfig,
4396
- saveServerConfig,
4397
- mergeServerConfig,
5965
+ startOfLocalDay,
5966
+ startOfLocalWeek,
5967
+ KeySpendTracker,
4398
5968
  getOutboundApiServer,
4399
5969
  __resetOutboundApiServerForTests
4400
5970
  };