@omnicross/core 0.1.2 → 0.1.3

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 (131) hide show
  1. package/dist/auth/GeminiCodeAssistProjectResolver.cjs +6 -1
  2. package/dist/auth/GeminiCodeAssistProjectResolver.js +6 -1
  3. package/dist/{chunk-SN3YWBX7.cjs → chunk-2FMI2B6V.cjs} +2329 -533
  4. package/dist/{chunk-XBSYYZIY.cjs → chunk-3QOSDDNI.cjs} +39 -3
  5. package/dist/{chunk-2FEVTJWG.js → chunk-4DECIFU7.js} +2162 -366
  6. package/dist/chunk-4IH4EL7M.cjs +9 -0
  7. package/dist/chunk-5ENKBSWO.js +311 -0
  8. package/dist/chunk-6RPZADX3.cjs +92 -0
  9. package/dist/chunk-AEOZIDEB.cjs +32 -0
  10. package/dist/chunk-BO4RFWBR.js +89 -0
  11. package/dist/{chunk-MNYKI4CI.js → chunk-CWM2ZDU5.js} +36 -0
  12. package/dist/chunk-CZHZGRU7.js +383 -0
  13. package/dist/{chunk-4NBS6KPV.cjs → chunk-DQ5VAYZ6.cjs} +7 -8
  14. package/dist/chunk-HUZZZ3JL.js +32 -0
  15. package/dist/{chunk-QOCNX236.js → chunk-IAXWICZU.js} +24 -1
  16. package/dist/{chunk-V5KPWNYX.cjs → chunk-JMJBSACD.cjs} +3 -3
  17. package/dist/chunk-L74VDN4A.cjs +32 -0
  18. package/dist/{chunk-74TMJA7Z.js → chunk-N5QFKSDO.js} +7 -4
  19. package/dist/{chunk-LKZJEL6E.cjs → chunk-Q27JY5RG.cjs} +6 -4
  20. package/dist/{chunk-SPVWWUHX.cjs → chunk-QWBWGODS.cjs} +24 -1
  21. package/dist/chunk-RPII3E6Z.cjs +145 -0
  22. package/dist/chunk-S6JGLF3G.cjs +89 -0
  23. package/dist/chunk-SVXC36MU.cjs +383 -0
  24. package/dist/chunk-TKJMZBAH.js +32 -0
  25. package/dist/{chunk-ZSVQT3PW.js → chunk-UBWEBKRC.js} +5 -6
  26. package/dist/chunk-UNEFIWXI.cjs +32 -0
  27. package/dist/chunk-VQY5W4YW.cjs +311 -0
  28. package/dist/chunk-WNY4WMRF.js +32 -0
  29. package/dist/chunk-XEKD23J5.js +9 -0
  30. package/dist/chunk-XTEI64OU.js +145 -0
  31. package/dist/chunk-XX6NQJMA.js +92 -0
  32. package/dist/{chunk-N3V2J5ZO.cjs → chunk-Y6OM7EV7.cjs} +7 -4
  33. package/dist/{chunk-745DV5FL.js → chunk-Y7FO65VT.js} +3 -3
  34. package/dist/{chunk-PXUJF5HS.js → chunk-ZY6KU6P3.js} +4 -2
  35. package/dist/completion/CompletionService.cjs +22 -15
  36. package/dist/completion/CompletionService.js +21 -14
  37. package/dist/completion/NativeSearchInjector.cjs +3 -3
  38. package/dist/completion/NativeSearchInjector.js +2 -2
  39. package/dist/completion/openrouter-headers.cjs +3 -6
  40. package/dist/completion/openrouter-headers.d.cts +9 -12
  41. package/dist/completion/openrouter-headers.d.ts +9 -12
  42. package/dist/completion/openrouter-headers.js +2 -5
  43. package/dist/completion/openrouter-models.cjs +4 -3
  44. package/dist/completion/openrouter-models.js +2 -1
  45. package/dist/completion.cjs +22 -15
  46. package/dist/completion.js +21 -14
  47. package/dist/index.cjs +139 -17
  48. package/dist/index.d.cts +40 -10
  49. package/dist/index.d.ts +40 -10
  50. package/dist/index.js +146 -24
  51. package/dist/outbound-api/routeResolver.cjs +5 -2
  52. package/dist/outbound-api/routeResolver.d.cts +8 -2
  53. package/dist/outbound-api/routeResolver.d.ts +8 -2
  54. package/dist/outbound-api/routeResolver.js +4 -1
  55. package/dist/outbound-api/types.cjs +6 -1
  56. package/dist/outbound-api/types.d.cts +11 -144
  57. package/dist/outbound-api/types.d.ts +11 -144
  58. package/dist/outbound-api/types.js +6 -0
  59. package/dist/outbound-api.cjs +133 -15
  60. package/dist/outbound-api.d.cts +786 -25
  61. package/dist/outbound-api.d.ts +786 -25
  62. package/dist/outbound-api.js +137 -19
  63. package/dist/pipeline/AuthSource.d.cts +23 -1
  64. package/dist/pipeline/AuthSource.d.ts +23 -1
  65. package/dist/pipeline/LlmConfigProviderAuth.cjs +3 -3
  66. package/dist/pipeline/LlmConfigProviderAuth.js +2 -2
  67. package/dist/pipeline/SubscriptionAccountHealth.cjs +22 -0
  68. package/dist/pipeline/SubscriptionAccountHealth.d.cts +235 -0
  69. package/dist/pipeline/SubscriptionAccountHealth.d.ts +235 -0
  70. package/dist/pipeline/SubscriptionAccountHealth.js +22 -0
  71. package/dist/pipeline/SubscriptionAuthSource.cjs +2 -2
  72. package/dist/pipeline/SubscriptionAuthSource.d.cts +3 -2
  73. package/dist/pipeline/SubscriptionAuthSource.d.ts +3 -2
  74. package/dist/pipeline/SubscriptionAuthSource.js +1 -1
  75. package/dist/pipeline/SubscriptionAuthStrategy.d.cts +24 -1
  76. package/dist/pipeline/SubscriptionAuthStrategy.d.ts +24 -1
  77. package/dist/pipeline/auditSink.cjs +14 -0
  78. package/dist/pipeline/auditSink.d.cts +48 -0
  79. package/dist/pipeline/auditSink.d.ts +48 -0
  80. package/dist/pipeline/auditSink.js +14 -0
  81. package/dist/pipeline/billingEmit.cjs +14 -0
  82. package/dist/pipeline/billingEmit.d.cts +49 -0
  83. package/dist/pipeline/billingEmit.d.ts +49 -0
  84. package/dist/pipeline/billingEmit.js +14 -0
  85. package/dist/ports/usage-event-store.d.cts +26 -1
  86. package/dist/ports/usage-event-store.d.ts +26 -1
  87. package/dist/ports.d.cts +7 -1
  88. package/dist/ports.d.ts +7 -1
  89. package/dist/provider-proxy/ProviderProxy.cjs +22 -15
  90. package/dist/provider-proxy/ProviderProxy.js +21 -14
  91. package/dist/provider-proxy/identity/SubscriptionIdentityStore.cjs +15 -0
  92. package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.cts +121 -0
  93. package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.ts +121 -0
  94. package/dist/provider-proxy/identity/SubscriptionIdentityStore.js +15 -0
  95. package/dist/provider-proxy/identity/fingerprintHeaders.cjs +20 -0
  96. package/dist/provider-proxy/identity/fingerprintHeaders.d.cts +89 -0
  97. package/dist/provider-proxy/identity/fingerprintHeaders.d.ts +89 -0
  98. package/dist/provider-proxy/identity/fingerprintHeaders.js +20 -0
  99. package/dist/provider-proxy/ingress/providerProxyShared.cjs +22 -15
  100. package/dist/provider-proxy/ingress/providerProxyShared.d.cts +11 -2
  101. package/dist/provider-proxy/ingress/providerProxyShared.d.ts +11 -2
  102. package/dist/provider-proxy/ingress/providerProxyShared.js +21 -14
  103. package/dist/provider-proxy/matchText.cjs +4 -2
  104. package/dist/provider-proxy/matchText.d.cts +14 -1
  105. package/dist/provider-proxy/matchText.d.ts +14 -1
  106. package/dist/provider-proxy/matchText.js +3 -1
  107. package/dist/provider-proxy/types.d.cts +23 -0
  108. package/dist/provider-proxy/types.d.ts +23 -0
  109. package/dist/provider-proxy.cjs +22 -15
  110. package/dist/provider-proxy.js +21 -14
  111. package/dist/{routeResolver-HE-ZO0fO.d.ts → routeResolver-B1NglTfe.d.ts} +36 -10
  112. package/dist/{routeResolver-BrbK6ja9.d.cts → routeResolver-CbG5uBgm.d.cts} +36 -10
  113. package/dist/transformer/transformers.cjs +7 -7
  114. package/dist/transformer/transformers.js +7 -7
  115. package/dist/transformer.cjs +5 -5
  116. package/dist/transformer.js +4 -4
  117. package/dist/types-6OUY_xfA.d.ts +830 -0
  118. package/dist/types-aBFFXoBz.d.cts +830 -0
  119. package/dist/usage/usage-recorder.cjs +2 -2
  120. package/dist/usage/usage-recorder.d.cts +19 -1
  121. package/dist/usage/usage-recorder.d.ts +19 -1
  122. package/dist/usage/usage-recorder.js +1 -1
  123. package/dist/usage.cjs +2 -2
  124. package/dist/usage.js +1 -1
  125. package/package.json +4 -2
  126. package/dist/chunk-3MEACFK3.js +0 -193
  127. package/dist/chunk-6VIXXLMX.cjs +0 -14
  128. package/dist/chunk-E3WHL7CO.js +0 -14
  129. package/dist/chunk-G2FUJNA2.cjs +0 -193
  130. package/dist/{chunk-UYPEN5XE.cjs → chunk-FFALUUZZ.cjs} +5 -5
  131. package/dist/{chunk-O466Y272.js → chunk-MQNDWYPN.js} +6 -6
@@ -1,6 +1,3 @@
1
- import {
2
- collectMatchText
3
- } from "./chunk-QOCNX236.js";
4
1
  import {
5
2
  streamSSEResponse
6
3
  } from "./chunk-VVEHS2LI.js";
@@ -8,8 +5,8 @@ import {
8
5
  TransformerChainExecutor
9
6
  } from "./chunk-G2UTNSGU.js";
10
7
  import {
11
- injectExtendedContextBeta
12
- } from "./chunk-MZNPGW5Q.js";
8
+ AnthropicTransformer
9
+ } from "./chunk-FTQ7IQUC.js";
13
10
  import {
14
11
  GeminiTransformer
15
12
  } from "./chunk-C7PSERMH.js";
@@ -17,52 +14,88 @@ import {
17
14
  OpenAIResponseTransformer
18
15
  } from "./chunk-5HTVET6E.js";
19
16
  import {
20
- AnthropicTransformer
21
- } from "./chunk-FTQ7IQUC.js";
17
+ readAuditUsage
18
+ } from "./chunk-CWM2ZDU5.js";
22
19
  import {
23
- getGeminiCodeAssistResolver
24
- } from "./chunk-DRWHTIIZ.js";
20
+ injectExtendedContextBeta
21
+ } from "./chunk-MZNPGW5Q.js";
25
22
  import {
26
- executeProviderCall
27
- } from "./chunk-JO5NLTLY.js";
23
+ getSharedIdentityStore
24
+ } from "./chunk-XTEI64OU.js";
28
25
  import {
29
- LlmConfigProviderAuth,
30
- getProviderHeaders
31
- } from "./chunk-PXUJF5HS.js";
26
+ applyFingerprint,
27
+ captureCallerIdentity
28
+ } from "./chunk-XX6NQJMA.js";
32
29
  import {
33
- resolveProviderChain
34
- } from "./chunk-JWC6APFQ.js";
30
+ collectMatchText,
31
+ deriveSubscriptionSessionKey
32
+ } from "./chunk-IAXWICZU.js";
35
33
  import {
36
34
  resolveSubscriptionChain
37
35
  } from "./chunk-5ERIZW3D.js";
36
+ import {
37
+ getSharedAccountHealth,
38
+ resolveResetSeconds
39
+ } from "./chunk-5ENKBSWO.js";
38
40
  import {
39
41
  SubscriptionAuthSource
40
- } from "./chunk-74TMJA7Z.js";
42
+ } from "./chunk-N5QFKSDO.js";
41
43
  import {
42
44
  serializeError
43
45
  } from "./chunk-AVYJ4YBN.js";
46
+ import {
47
+ getGeminiCodeAssistResolver
48
+ } from "./chunk-DRWHTIIZ.js";
49
+ import {
50
+ getAuditCaptureConfig,
51
+ recordAudit
52
+ } from "./chunk-WNY4WMRF.js";
53
+ import {
54
+ getBillingCaptureConfig,
55
+ publishBillingEvent
56
+ } from "./chunk-TKJMZBAH.js";
57
+ import {
58
+ executeProviderCall
59
+ } from "./chunk-JO5NLTLY.js";
60
+ import {
61
+ LlmConfigProviderAuth,
62
+ getProviderHeaders
63
+ } from "./chunk-ZY6KU6P3.js";
64
+ import {
65
+ resolveProviderChain
66
+ } from "./chunk-JWC6APFQ.js";
44
67
  import {
45
68
  buildProviderApiUrl,
46
69
  resolveApiFormat,
47
70
  resolveProviderEndpoint
48
71
  } from "./chunk-H5JUT3KV.js";
49
72
  import {
50
- resolveRoute
51
- } from "./chunk-3MEACFK3.js";
73
+ detectRequestRole,
74
+ endpointToIngressFormat,
75
+ extractRequestedModel,
76
+ isKindMappedEndpoint,
77
+ modelKindsForEndpoint,
78
+ parseModelRef,
79
+ resolveRoute,
80
+ validateServerModelConfig
81
+ } from "./chunk-CZHZGRU7.js";
52
82
  import {
53
83
  convertAnthropicToOpenAI,
54
84
  convertOpenAIToAnthropic
55
85
  } from "./chunk-5ZQBEOMD.js";
56
86
  import {
57
- applyAugmentation
58
- } from "./chunk-745DV5FL.js";
87
+ fetchUpstream
88
+ } from "./chunk-BO4RFWBR.js";
59
89
  import {
60
- OPENROUTER_APP_HEADERS,
61
- isOpenRouterProvider
62
- } from "./chunk-E3WHL7CO.js";
90
+ applyAugmentation
91
+ } from "./chunk-Y7FO65VT.js";
63
92
  import {
64
93
  NATIVE_SEARCH_TOOL_NAMES
65
94
  } from "./chunk-FMOBD2OQ.js";
95
+ import {
96
+ OPENROUTER_APP_HEADERS,
97
+ isOpenRouterProvider
98
+ } from "./chunk-HUZZZ3JL.js";
66
99
 
67
100
  // src/completion/StreamEventBuffer.ts
68
101
  var TERMINAL_TYPES = /* @__PURE__ */ new Set(["done", "error", "abort"]);
@@ -462,10 +495,11 @@ function recordAnthropicNonStreamUsage(recorder, bodyText, attribution) {
462
495
  sessionId: attribution.sessionId,
463
496
  providerId: attribution.providerId,
464
497
  model: attribution.model,
465
- apiKeyId: null,
498
+ apiKeyId: attribution.apiKeyId,
466
499
  engineOrigin: "codex-ingress",
467
500
  usage: tapped,
468
- rawUsage: parsed.usage
501
+ rawUsage: parsed.usage,
502
+ auditResponse: attribution.auditResponse
469
503
  });
470
504
  } catch {
471
505
  }
@@ -502,7 +536,84 @@ function readBody(req) {
502
536
  req.on("error", reject);
503
537
  });
504
538
  }
505
- async function relayResponse(res, providerResponse, isStream) {
539
+ function rewriteJsonModel(bodyText, rewriteModel) {
540
+ try {
541
+ const obj = JSON.parse(bodyText);
542
+ if (!obj || typeof obj !== "object") return bodyText;
543
+ let changed = false;
544
+ if (typeof obj.model === "string") {
545
+ obj.model = rewriteModel;
546
+ changed = true;
547
+ }
548
+ const nested = obj.response;
549
+ if (nested && typeof nested === "object" && typeof nested.model === "string") {
550
+ nested.model = rewriteModel;
551
+ changed = true;
552
+ }
553
+ return changed ? JSON.stringify(obj) : bodyText;
554
+ } catch {
555
+ return bodyText;
556
+ }
557
+ }
558
+ function rewriteSseLine(line, rewriteModel) {
559
+ if (!line.startsWith("data:") || !line.includes('"model"')) return line;
560
+ const nl = /\r?\n$/.exec(line);
561
+ const newline = nl ? nl[0] : "";
562
+ const withoutNl = newline ? line.slice(0, line.length - newline.length) : line;
563
+ const afterData = withoutNl.slice("data:".length);
564
+ const leadingSpace = afterData.startsWith(" ") ? " " : "";
565
+ const payload = afterData.slice(leadingSpace.length);
566
+ if (payload === "" || payload === "[DONE]") return line;
567
+ try {
568
+ const obj = JSON.parse(payload);
569
+ if (!obj || typeof obj !== "object") return line;
570
+ const type = typeof obj.type === "string" ? obj.type : "";
571
+ let changed = false;
572
+ if (type === "message_start") {
573
+ const message = obj.message;
574
+ if (message && typeof message === "object" && typeof message.model === "string") {
575
+ message.model = rewriteModel;
576
+ changed = true;
577
+ }
578
+ } else if (type === "response.created" || type === "response.in_progress" || type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
579
+ const response = obj.response;
580
+ if (response && typeof response === "object" && typeof response.model === "string") {
581
+ response.model = rewriteModel;
582
+ changed = true;
583
+ }
584
+ }
585
+ if (!changed) return line;
586
+ return `data:${leadingSpace}${JSON.stringify(obj)}${newline}`;
587
+ } catch {
588
+ return line;
589
+ }
590
+ }
591
+ async function relayStreamWithModelRewrite(res, body, rewriteModel) {
592
+ const reader = body.getReader();
593
+ const decoder = new TextDecoder();
594
+ let buffer = "";
595
+ try {
596
+ for (; ; ) {
597
+ const { done, value } = await reader.read();
598
+ if (done) break;
599
+ buffer += decoder.decode(value, { stream: true });
600
+ let out = "";
601
+ let nlIndex;
602
+ while ((nlIndex = buffer.indexOf("\n")) !== -1) {
603
+ const linePart = buffer.slice(0, nlIndex + 1);
604
+ buffer = buffer.slice(nlIndex + 1);
605
+ out += rewriteSseLine(linePart, rewriteModel);
606
+ }
607
+ if (out) res.write(out);
608
+ }
609
+ const tail = buffer + decoder.decode();
610
+ if (tail) res.write(rewriteSseLine(tail, rewriteModel));
611
+ } finally {
612
+ reader.releaseLock();
613
+ res.end();
614
+ }
615
+ }
616
+ async function relayResponse(res, providerResponse, isStream, rewriteModel) {
506
617
  const contentType = providerResponse.headers.get("Content-Type") ?? "";
507
618
  const status = providerResponse.status && providerResponse.status >= 100 ? providerResponse.status : 200;
508
619
  if (isStream || contentType.includes("text/event-stream")) {
@@ -515,6 +626,10 @@ async function relayResponse(res, providerResponse, isStream) {
515
626
  res.end();
516
627
  return null;
517
628
  }
629
+ if (rewriteModel) {
630
+ await relayStreamWithModelRewrite(res, providerResponse.body, rewriteModel);
631
+ return null;
632
+ }
518
633
  const reader = providerResponse.body.getReader();
519
634
  try {
520
635
  for (; ; ) {
@@ -529,8 +644,9 @@ async function relayResponse(res, providerResponse, isStream) {
529
644
  return null;
530
645
  }
531
646
  const bodyText = await providerResponse.text();
647
+ const clientText = rewriteModel ? rewriteJsonModel(bodyText, rewriteModel) : bodyText;
532
648
  res.writeHead(status, { "Content-Type": contentType.includes("json") ? contentType : "application/json" });
533
- res.end(bodyText);
649
+ res.end(clientText);
534
650
  return bodyText;
535
651
  }
536
652
  function writeError(res, status, message) {
@@ -568,14 +684,15 @@ async function buildSubscriptionPlan(res, route, deps, anthropicBody, routeModel
568
684
  resolvedModel = mapped.resolvedModel;
569
685
  scenario = mapped.scenario;
570
686
  }
571
- const plan = buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario);
687
+ const sessionKey = deriveSubscriptionSessionKey(anthropicBody);
688
+ const plan = buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario, sessionKey);
572
689
  if (!plan) {
573
690
  writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
574
691
  return null;
575
692
  }
576
693
  return plan;
577
694
  }
578
- function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario) {
695
+ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario, sessionKey) {
579
696
  const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel, route.subscriptionConfig);
580
697
  if (!upstreamUrl) return null;
581
698
  const auth = route.auth ?? new SubscriptionAuthSource(profile);
@@ -622,7 +739,8 @@ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isS
622
739
  upstreamUrl,
623
740
  sameFormat,
624
741
  isSubscription: true,
625
- scenario
742
+ scenario,
743
+ sessionKey
626
744
  };
627
745
  }
628
746
  function usesResponsesChain(names) {
@@ -657,12 +775,24 @@ function collectText(value) {
657
775
  }
658
776
  return 0;
659
777
  }
660
- async function runPipeline(anthropicBody, plan) {
778
+ function proxyProviderId(plan) {
779
+ return plan.isSubscription ? plan.transformerProvider.name : "byo";
780
+ }
781
+ async function runPipeline(anthropicBody, plan, reportSelection) {
661
782
  const executor = getSharedExecutor();
662
783
  const endpointTransformer = getAnthropicEndpointTransformer();
663
784
  const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
785
+ let proxyAccountId;
664
786
  const authHeaders = {};
665
- await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
787
+ await auth.applyHeaders(authHeaders, {
788
+ upstreamUrl,
789
+ model: resolvedModel,
790
+ sessionKey: plan.sessionKey,
791
+ reportSelection: (accountId, isActive) => {
792
+ proxyAccountId = accountId;
793
+ reportSelection?.(accountId, isActive);
794
+ }
795
+ });
666
796
  let rawStatus = null;
667
797
  const { response } = await executeProviderCall({
668
798
  executor,
@@ -682,7 +812,11 @@ async function runPipeline(anthropicBody, plan) {
682
812
  },
683
813
  fetchFn: (url, headers, body) => {
684
814
  console.info(`[ProviderProxy:anthropic] -> ${url} model=${resolvedModel} stream=${isStream}`);
685
- return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
815
+ return fetchUpstream(
816
+ url,
817
+ { method: "POST", headers, body: JSON.stringify(body) },
818
+ { providerId: proxyProviderId(plan), accountId: proxyAccountId }
819
+ ).then((r) => {
686
820
  rawStatus = r.status;
687
821
  return r;
688
822
  });
@@ -691,29 +825,95 @@ async function runPipeline(anthropicBody, plan) {
691
825
  });
692
826
  return { response, rawStatus };
693
827
  }
694
- async function runSubscriptionSameFormatFetch(rawBody, plan) {
828
+ async function runSubscriptionSameFormatFetch(rawBody, plan, reportSelection, options = {}) {
829
+ let proxyAccountId;
830
+ let remappedModel;
695
831
  const headers = { "content-type": "application/json" };
696
832
  await plan.auth.applyHeaders(headers, {
697
833
  upstreamUrl: plan.upstreamUrl,
698
- model: plan.resolvedModel
834
+ model: plan.resolvedModel,
835
+ sessionKey: plan.sessionKey,
836
+ reportSelection: (accountId, isActive, remapped) => {
837
+ proxyAccountId = accountId;
838
+ remappedModel = remapped;
839
+ reportSelection?.(accountId, isActive, remapped);
840
+ }
699
841
  });
842
+ if (plan.isSubscription && plan.transformerProvider.name === "claude") {
843
+ applyFingerprint(
844
+ getSharedIdentityStore(),
845
+ headers,
846
+ plan.transformerProvider.name,
847
+ proxyAccountId,
848
+ options.callerIdentity
849
+ );
850
+ }
851
+ const applyRemap = remappedModel !== void 0 && outboundRemapApplies(plan);
852
+ const outboundBody = applyRemap ? rewriteBodyModel(rawBody, remappedModel) : rawBody;
853
+ const outboundModel = applyRemap ? remappedModel : plan.resolvedModel;
700
854
  console.info(
701
- `[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${plan.resolvedModel} stream=${plan.isStream}`
855
+ `[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${outboundModel} stream=${plan.isStream}`
856
+ );
857
+ const response = await fetchUpstream(
858
+ plan.upstreamUrl,
859
+ { method: "POST", headers, body: outboundBody },
860
+ { providerId: proxyProviderId(plan), accountId: proxyAccountId }
702
861
  );
703
- const response = await fetch(plan.upstreamUrl, { method: "POST", headers, body: rawBody });
704
862
  return { response, rawStatus: response.status };
705
863
  }
706
- async function runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) {
707
- const runOnce = () => plan.sameFormat ? runSubscriptionSameFormatFetch(relayBody, plan) : runPipeline(anthropicBody, plan);
864
+ function outboundRemapApplies(plan) {
865
+ return plan.isSubscription && plan.transformerProvider.name === "claude";
866
+ }
867
+ function rewriteBodyModel(rawBody, model) {
868
+ try {
869
+ const parsed = JSON.parse(rawBody);
870
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return rawBody;
871
+ parsed.model = model;
872
+ return JSON.stringify(parsed);
873
+ } catch {
874
+ return rawBody;
875
+ }
876
+ }
877
+ async function runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan, options = {}) {
878
+ let selectedAccountId;
879
+ const reportSelection = (accountId) => {
880
+ selectedAccountId = accountId;
881
+ };
882
+ const runOnce = () => plan.sameFormat ? runSubscriptionSameFormatFetch(relayBody, plan, reportSelection, options) : runPipeline(anthropicBody, plan, reportSelection);
708
883
  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;
884
+ let result = first;
885
+ if (first.rawStatus === 401) {
886
+ const refreshed = await plan.auth.onUnauthorized?.(plan.sessionKey);
887
+ if (refreshed) {
888
+ console.info("[ProviderProxy:anthropic] 401 \u2192 token refreshed; retrying once");
889
+ result = await runOnce();
890
+ } else {
891
+ console.warn("[ProviderProxy:anthropic] 401 not recoverable (onUnauthorized returned false)");
892
+ }
714
893
  }
715
- console.info("[ProviderProxy:anthropic] 401 \u2192 token refreshed; retrying once");
716
- return runOnce();
894
+ await markSubscriptionHealth(plan, selectedAccountId, result);
895
+ return result;
896
+ }
897
+ async function readBoundedBody(response, max = 2048) {
898
+ try {
899
+ const text = await response.clone().text();
900
+ return text.slice(0, max);
901
+ } catch {
902
+ return void 0;
903
+ }
904
+ }
905
+ async function markSubscriptionHealth(plan, selectedAccountId, result) {
906
+ if (!plan.isSubscription || selectedAccountId === void 0) return;
907
+ const providerId = plan.transformerProvider.name;
908
+ const status = result.rawStatus;
909
+ const { resetHeaderSeconds, retryAfterSeconds } = resolveResetSeconds(providerId, result.response.headers);
910
+ const bodyText = status === 403 ? await readBoundedBody(result.response) : void 0;
911
+ getSharedAccountHealth().recordUpstreamOutcome(providerId, selectedAccountId, {
912
+ status,
913
+ resetHeaderSeconds,
914
+ retryAfterSeconds,
915
+ bodyText
916
+ });
717
917
  }
718
918
  function isFallbackEligibleStatus(rawStatus) {
719
919
  return rawStatus === null || rawStatus >= 500 || rawStatus === 429;
@@ -728,9 +928,9 @@ function recordBreakerOutcome(profile, modelId, rawStatus) {
728
928
  if (outcome === "neutral") return;
729
929
  profile?.recordModelOutcome?.(modelId, outcome === "success");
730
930
  }
731
- async function runSubscriptionAttemptOutcome(anthropicBody, relayBody, plan) {
931
+ async function runSubscriptionAttemptOutcome(anthropicBody, relayBody, plan, options = {}) {
732
932
  try {
733
- return { kind: "result", result: await runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) };
933
+ return { kind: "result", result: await runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan, options) };
734
934
  } catch (error) {
735
935
  return { kind: "thrown", error };
736
936
  }
@@ -742,11 +942,11 @@ function settleOutcome(outcome) {
742
942
  if (outcome.kind === "thrown") throw outcome.error;
743
943
  return outcome.result;
744
944
  }
745
- async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialPlan, route, deps) {
945
+ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialPlan, route, deps, options = {}) {
746
946
  const profile = route.subscriptionProfile;
747
947
  const scenario = initialPlan.scenario;
748
948
  if (!profile?.nextFallback || scenario === void 0) {
749
- const loneOutcome = await runSubscriptionAttemptOutcome(anthropicBody, rawBody, initialPlan);
949
+ const loneOutcome = await runSubscriptionAttemptOutcome(anthropicBody, rawBody, initialPlan, options);
750
950
  recordBreakerOutcome(profile, initialPlan.resolvedModel, outcomeStatus(loneOutcome));
751
951
  return settleOutcome(loneOutcome);
752
952
  }
@@ -757,7 +957,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
757
957
  if (profile.allowModel && !profile.allowModel(initialPlan.resolvedModel)) {
758
958
  attempted.push(initialPlan.resolvedModel);
759
959
  const firstAdmitting = profile.nextFallback(scenario, attempted, route.subscriptionConfig);
760
- const gatedPlan = firstAdmitting ? buildSubscriptionIterationPlan(profile, route, deps, firstAdmitting.modelId, initialPlan.isStream, scenario) : null;
960
+ const gatedPlan = firstAdmitting ? buildSubscriptionIterationPlan(profile, route, deps, firstAdmitting.modelId, initialPlan.isStream, scenario, initialPlan.sessionKey) : null;
761
961
  if (firstAdmitting && gatedPlan) {
762
962
  console.warn(
763
963
  `[ProviderProxy:anthropic] subscription primary ${initialPlan.resolvedModel} circuit open -> first admitting fallback ${firstAdmitting.modelId}`
@@ -772,7 +972,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
772
972
  );
773
973
  }
774
974
  }
775
- let outcome = await runSubscriptionAttemptOutcome(firstBodyObj, firstRelayBody, plan);
975
+ let outcome = await runSubscriptionAttemptOutcome(firstBodyObj, firstRelayBody, plan, options);
776
976
  recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
777
977
  if (!attempted.includes(plan.resolvedModel)) attempted.push(plan.resolvedModel);
778
978
  while (attempted.length < MAX_FALLBACK_ATTEMPTS) {
@@ -785,7 +985,8 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
785
985
  deps,
786
986
  next.modelId,
787
987
  plan.isStream,
788
- scenario
988
+ scenario,
989
+ initialPlan.sessionKey
789
990
  );
790
991
  if (!nextPlan) return settleOutcome(outcome);
791
992
  const sinceLabel = outcome.kind === "thrown" ? "thrown error" : `status ${String(outcome.result.rawStatus)}`;
@@ -796,7 +997,7 @@ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialP
796
997
  const fallbackRelayBody = nextPlan.sameFormat ? JSON.stringify(fallbackBodyObj) : rawBody;
797
998
  plan = nextPlan;
798
999
  attempted.push(next.modelId);
799
- outcome = await runSubscriptionAttemptOutcome(fallbackBodyObj, fallbackRelayBody, plan);
1000
+ outcome = await runSubscriptionAttemptOutcome(fallbackBodyObj, fallbackRelayBody, plan, options);
800
1001
  recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
801
1002
  }
802
1003
  return settleOutcome(outcome);
@@ -819,13 +1020,16 @@ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {
819
1020
  const plan = route.authMode === "subscription" ? await buildSubscriptionPlan(res, route, deps, anthropicBody, resolvedModel, isStream) : await buildByoPlan(res, route, deps, resolvedModel, isStream);
820
1021
  if (!plan) return;
821
1022
  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);
1023
+ const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry(anthropicBody, rawBody, plan, route, deps, options) : await runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options);
1024
+ const bodyText = await relayResponse(res, providerResponse.response, isStream, route.requestedModel);
824
1025
  if (bodyText && deps.usageRecorder) {
825
1026
  recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, {
826
1027
  sessionId: route.sessionId,
827
1028
  providerId: route.providerId ?? "anthropic",
828
- model: plan.resolvedModel
1029
+ model: plan.resolvedModel,
1030
+ apiKeyId: route.apiKeyId ?? null,
1031
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1032
+ auditResponse: res
829
1033
  });
830
1034
  }
831
1035
  } catch (err) {
@@ -881,7 +1085,7 @@ async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
881
1085
  extendedContextEnabled: route.anthropicSdkHints?.extendedContext?.enabled ?? false
882
1086
  };
883
1087
  }
884
- async function runSameFormatFetch(rawBody, plan, options, keyOverride) {
1088
+ async function runSameFormatFetch(bodyToSend, plan, options, keyOverride) {
885
1089
  const { provider, apiKey, resolvedModel, isStream, extendedContextEnabled } = plan;
886
1090
  if (!provider) {
887
1091
  throw new Error("[ProviderProxy:anthropic] same-format BYO path requires a provider row");
@@ -902,11 +1106,26 @@ async function runSameFormatFetch(rawBody, plan, options, keyOverride) {
902
1106
  injectExtendedContextBeta(headers, resolvedModel, extendedContextEnabled ?? false);
903
1107
  const url = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
904
1108
  console.info(`[ProviderProxy:anthropic] (same-format) -> ${url} model=${resolvedModel} stream=${isStream}`);
905
- const response = await fetch(url, { method: "POST", headers, body: rawBody });
1109
+ const response = await fetchUpstream(
1110
+ url,
1111
+ { method: "POST", headers, body: bodyToSend },
1112
+ { providerId: "byo" }
1113
+ );
906
1114
  return { response, rawStatus: response.status };
907
1115
  }
1116
+ function resolveSameFormatBody(anthropicBody, rawBody, plan) {
1117
+ let originalModel;
1118
+ try {
1119
+ const m = JSON.parse(rawBody).model;
1120
+ originalModel = typeof m === "string" ? m : void 0;
1121
+ } catch {
1122
+ originalModel = void 0;
1123
+ }
1124
+ return plan.resolvedModel !== originalModel ? JSON.stringify(anthropicBody) : rawBody;
1125
+ }
908
1126
  async function runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options) {
909
- const runOnce = (keyOverride) => plan.sameFormat ? runSameFormatFetch(rawBody, plan, options, keyOverride) : runPipeline(anthropicBody, plan);
1127
+ const sameFormatBody = plan.sameFormat ? resolveSameFormatBody(anthropicBody, rawBody, plan) : rawBody;
1128
+ const runOnce = (keyOverride) => plan.sameFormat ? runSameFormatFetch(sameFormatBody, plan, options, keyOverride) : runPipeline(anthropicBody, plan);
910
1129
  const first = await runOnce();
911
1130
  const outcome = await plan.auth.onResult?.(first.rawStatus);
912
1131
  if (outcome?.rebound) {
@@ -930,7 +1149,8 @@ async function handleAnthropicMessagesRequest(req, res, route, deps) {
930
1149
  const rawBody = await readBody(req);
931
1150
  const callerBetaRaw = req.headers["anthropic-beta"];
932
1151
  const callerAnthropicBeta = Array.isArray(callerBetaRaw) ? callerBetaRaw.join(",") : callerBetaRaw;
933
- await handleAnthropicMessagesByo(res, rawBody, route, deps, { callerAnthropicBeta });
1152
+ const callerIdentity = captureCallerIdentity(getSharedIdentityStore(), req.headers);
1153
+ await handleAnthropicMessagesByo(res, rawBody, route, deps, { callerAnthropicBeta, callerIdentity });
934
1154
  return;
935
1155
  }
936
1156
  const hints = route.anthropicSdkHints;
@@ -992,7 +1212,8 @@ function recordGeminiNonStreamUsage(recorder, bodyText, attribution) {
992
1212
  sessionId: attribution.sessionId,
993
1213
  providerId: attribution.providerId,
994
1214
  model: attribution.model,
995
- apiKeyId: null,
1215
+ apiKeyId: attribution.apiKeyId,
1216
+ auditResponse: attribution.auditResponse,
996
1217
  engineOrigin: "codex-ingress",
997
1218
  usage: tapped,
998
1219
  rawUsage: parsed.usageMetadata
@@ -1042,7 +1263,10 @@ async function handleGeminiGenerateContentRequest(res, rawBody, url, route, deps
1042
1263
  recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, {
1043
1264
  sessionId: route.sessionId,
1044
1265
  providerId: route.providerId ?? "gemini",
1045
- model: resolvedModel
1266
+ model: resolvedModel,
1267
+ apiKeyId: route.apiKeyId ?? null,
1268
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1269
+ auditResponse: res
1046
1270
  });
1047
1271
  }
1048
1272
  } catch (err) {
@@ -1117,7 +1341,11 @@ async function runPipeline2(geminiBody, plan) {
1117
1341
  },
1118
1342
  fetchFn: (url, headers, body) => {
1119
1343
  console.log(`[ProviderProxy:gemini] -> ${url} model=${resolvedModel} stream=${isStream}`);
1120
- return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1344
+ return fetchUpstream(
1345
+ url,
1346
+ { method: "POST", headers, body: JSON.stringify(body) },
1347
+ { providerId: "byo" }
1348
+ ).then((r) => {
1121
1349
  rawStatus = r.status;
1122
1350
  return r;
1123
1351
  });
@@ -1169,7 +1397,8 @@ function recordChatCompletionsNonStreamUsage(recorder, bodyText, attribution) {
1169
1397
  sessionId: attribution.sessionId,
1170
1398
  providerId: attribution.providerId,
1171
1399
  model: attribution.model,
1172
- apiKeyId: null,
1400
+ apiKeyId: attribution.apiKeyId,
1401
+ auditResponse: attribution.auditResponse,
1173
1402
  engineOrigin: "codex-ingress",
1174
1403
  usage: tapped,
1175
1404
  rawUsage: parsed.usage
@@ -1184,6 +1413,8 @@ function isOpenAIChatRequest(method, url) {
1184
1413
  const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
1185
1414
  return path.endsWith("/chat/completions");
1186
1415
  }
1416
+ var CHAT_BRIDGE_SUBSCRIPTION_PROVIDERS = /* @__PURE__ */ new Set(["claude"]);
1417
+ var IDENTITY_ENDPOINT_TRANSFORMER = { name: "identity-openai-chat" };
1187
1418
  async function handleOpenAIChatRequest(res, rawBody, route, deps) {
1188
1419
  let chatBody;
1189
1420
  try {
@@ -1192,23 +1423,22 @@ async function handleOpenAIChatRequest(res, rawBody, route, deps) {
1192
1423
  writeError(res, 400, "Invalid JSON in request body");
1193
1424
  return;
1194
1425
  }
1195
- if (route.authMode === "subscription") {
1196
- writeError(res, 502, "OpenAI Chat Completions ingress does not support subscription auth");
1197
- return;
1198
- }
1199
1426
  const isStream = chatBody.stream === true;
1200
1427
  const resolvedModel = route.model;
1201
1428
  chatBody.model = resolvedModel;
1202
1429
  try {
1203
- const plan = await buildByoPlan3(res, route, deps, resolvedModel, isStream);
1430
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan3(res, route, deps, resolvedModel, isStream);
1204
1431
  if (!plan) return;
1205
- const providerResponse = await runPipelineWithPoolReporting3(chatBody, plan);
1432
+ const providerResponse = plan.isSubscription ? await runPipelineWithSubscriptionRetry2(chatBody, plan) : await runPipelineWithPoolReporting3(chatBody, plan);
1206
1433
  const bodyText = await relayResponse(res, providerResponse.response, isStream);
1207
1434
  if (bodyText && deps.usageRecorder) {
1208
1435
  recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, {
1209
1436
  sessionId: route.sessionId,
1210
1437
  providerId: route.providerId ?? "openai",
1211
- model: resolvedModel
1438
+ model: resolvedModel,
1439
+ apiKeyId: route.apiKeyId ?? null,
1440
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1441
+ auditResponse: res
1212
1442
  });
1213
1443
  }
1214
1444
  } catch (err) {
@@ -1255,14 +1485,67 @@ async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
1255
1485
  resolvedModel,
1256
1486
  isStream,
1257
1487
  resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
1258
- upstreamUrl: byoUrl
1488
+ upstreamUrl: byoUrl,
1489
+ proxyProviderId: "byo",
1490
+ isSubscription: false
1491
+ };
1492
+ }
1493
+ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) {
1494
+ const profile = route.subscriptionProfile;
1495
+ if (!profile) {
1496
+ writeError(res, 502, "Subscription mode requires a subscription profile");
1497
+ return null;
1498
+ }
1499
+ const providerId = profile.authStrategy.providerId;
1500
+ if (!CHAT_BRIDGE_SUBSCRIPTION_PROVIDERS.has(providerId)) {
1501
+ writeError(
1502
+ res,
1503
+ 501,
1504
+ `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)`
1505
+ );
1506
+ return null;
1507
+ }
1508
+ const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel);
1509
+ if (!upstreamUrl) {
1510
+ writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
1511
+ return null;
1512
+ }
1513
+ const auth = route.auth ?? new SubscriptionAuthSource(profile);
1514
+ const chain = resolveSubscriptionChain(
1515
+ profile,
1516
+ deps.llmConfig.getTransformerService(),
1517
+ IDENTITY_ENDPOINT_TRANSFORMER
1518
+ );
1519
+ const transformerProvider = {
1520
+ name: providerId,
1521
+ baseUrl: upstreamUrl,
1522
+ apiKey: "",
1523
+ models: [resolvedModel]
1524
+ };
1525
+ return {
1526
+ auth,
1527
+ chain,
1528
+ transformerProvider,
1529
+ resolvedModel,
1530
+ isStream,
1531
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
1532
+ upstreamUrl,
1533
+ proxyProviderId: providerId,
1534
+ isSubscription: true
1259
1535
  };
1260
1536
  }
1261
1537
  async function runPipeline3(chatBody, plan) {
1262
1538
  const executor = getSharedExecutor();
1263
1539
  const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
1540
+ let proxyAccountId;
1264
1541
  const authHeaders = {};
1265
- await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
1542
+ await auth.applyHeaders(authHeaders, {
1543
+ upstreamUrl,
1544
+ model: resolvedModel,
1545
+ reportSelection: (accountId) => {
1546
+ proxyAccountId = accountId;
1547
+ }
1548
+ });
1266
1549
  let rawStatus = null;
1267
1550
  const { response } = await executeProviderCall({
1268
1551
  executor,
@@ -1281,7 +1564,11 @@ async function runPipeline3(chatBody, plan) {
1281
1564
  },
1282
1565
  fetchFn: (url, headers, body) => {
1283
1566
  console.log(`[ProviderProxy:chat] -> ${url} model=${resolvedModel} stream=${isStream}`);
1284
- return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1567
+ return fetchUpstream(
1568
+ url,
1569
+ { method: "POST", headers, body: JSON.stringify(body) },
1570
+ { providerId: plan.proxyProviderId, accountId: proxyAccountId }
1571
+ ).then((r) => {
1285
1572
  rawStatus = r.status;
1286
1573
  return r;
1287
1574
  });
@@ -1303,6 +1590,17 @@ async function runPipelineWithPoolReporting3(chatBody, plan) {
1303
1590
  }
1304
1591
  return first;
1305
1592
  }
1593
+ async function runPipelineWithSubscriptionRetry2(chatBody, plan) {
1594
+ const first = await runPipeline3(chatBody, plan);
1595
+ if (first.rawStatus !== 401) return first;
1596
+ const refreshed = await plan.auth.onUnauthorized?.();
1597
+ if (!refreshed) {
1598
+ console.warn("[ProviderProxy:chat] 401 not recoverable (onUnauthorized returned false)");
1599
+ return first;
1600
+ }
1601
+ console.log("[ProviderProxy:chat] 401 \u2192 token refreshed; retrying once");
1602
+ return runPipeline3(chatBody, plan);
1603
+ }
1306
1604
 
1307
1605
  // src/provider-proxy/usage/recordResponsesUsage.ts
1308
1606
  function readResponsesUsage(usage) {
@@ -1333,7 +1631,8 @@ function recordResponsesNonStreamUsage(recorder, bodyText, attribution) {
1333
1631
  sessionId: attribution.sessionId,
1334
1632
  providerId: attribution.providerId,
1335
1633
  model: attribution.model,
1336
- apiKeyId: null,
1634
+ apiKeyId: attribution.apiKeyId,
1635
+ auditResponse: attribution.auditResponse,
1337
1636
  engineOrigin: "codex-ingress",
1338
1637
  usage: tapped,
1339
1638
  rawUsage: parsed.usage
@@ -1360,15 +1659,18 @@ async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
1360
1659
  const resolvedModel = route.model;
1361
1660
  responsesBody.model = resolvedModel;
1362
1661
  try {
1363
- const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
1662
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
1364
1663
  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);
1664
+ const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry3(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
1665
+ const bodyText = await relayResponse(res, providerResponse.response, isStream, route.requestedModel);
1367
1666
  if (bodyText && deps.usageRecorder) {
1368
1667
  recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, {
1369
1668
  sessionId: route.sessionId,
1370
1669
  providerId: route.providerId ?? "codex",
1371
- model: resolvedModel
1670
+ model: resolvedModel,
1671
+ apiKeyId: route.apiKeyId ?? null,
1672
+ // request-audit-log: correlate this request's tokens/cost to its audit record.
1673
+ auditResponse: res
1372
1674
  });
1373
1675
  }
1374
1676
  } catch (err) {
@@ -1415,10 +1717,11 @@ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
1415
1717
  resolvedModel,
1416
1718
  isStream,
1417
1719
  resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
1418
- upstreamUrl: byoUrl
1720
+ upstreamUrl: byoUrl,
1721
+ proxyProviderId: "byo"
1419
1722
  };
1420
1723
  }
1421
- async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) {
1724
+ async function buildSubscriptionPlan3(res, route, deps, resolvedModel, isStream) {
1422
1725
  const profile = route.subscriptionProfile;
1423
1726
  if (!profile) {
1424
1727
  writeError(res, 502, "Subscription mode requires a codex subscription profile");
@@ -1459,7 +1762,8 @@ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream)
1459
1762
  // stream vs non-stream colon-method URL in `config.url`); fall back to the
1460
1763
  // profile's `resolveUpstreamUrl` for non-Code-Assist subscriptions.
1461
1764
  resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
1462
- upstreamUrl
1765
+ upstreamUrl,
1766
+ proxyProviderId: profile.authStrategy.providerId
1463
1767
  };
1464
1768
  }
1465
1769
  async function resolveGeminiCodeAssistProject(profile) {
@@ -1476,8 +1780,15 @@ async function runPipeline4(responsesBody, plan) {
1476
1780
  const executor = getSharedExecutor();
1477
1781
  const endpointTransformer = getResponsesEndpointTransformer();
1478
1782
  const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
1783
+ let proxyAccountId;
1479
1784
  const authHeaders = {};
1480
- await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
1785
+ await auth.applyHeaders(authHeaders, {
1786
+ upstreamUrl,
1787
+ model: resolvedModel,
1788
+ reportSelection: (accountId) => {
1789
+ proxyAccountId = accountId;
1790
+ }
1791
+ });
1481
1792
  let rawStatus = null;
1482
1793
  const { response } = await executeProviderCall({
1483
1794
  executor,
@@ -1497,7 +1808,11 @@ async function runPipeline4(responsesBody, plan) {
1497
1808
  },
1498
1809
  fetchFn: (url, headers, body) => {
1499
1810
  console.log(`[ProviderProxy:responses] -> ${url} model=${resolvedModel} stream=${isStream}`);
1500
- return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1811
+ return fetchUpstream(
1812
+ url,
1813
+ { method: "POST", headers, body: JSON.stringify(body) },
1814
+ { providerId: plan.proxyProviderId, accountId: proxyAccountId }
1815
+ ).then((r) => {
1501
1816
  rawStatus = r.status;
1502
1817
  return r;
1503
1818
  });
@@ -1519,7 +1834,7 @@ async function runPipelineWithPoolReporting4(responsesBody, plan) {
1519
1834
  }
1520
1835
  return first;
1521
1836
  }
1522
- async function runPipelineWithSubscriptionRetry2(responsesBody, plan) {
1837
+ async function runPipelineWithSubscriptionRetry3(responsesBody, plan) {
1523
1838
  const first = await runPipeline4(responsesBody, plan);
1524
1839
  if (first.rawStatus !== 401) return first;
1525
1840
  const refreshed = await plan.auth.onUnauthorized?.();
@@ -1696,12 +2011,644 @@ function __resetProviderProxyForTests() {
1696
2011
  // src/outbound-api/OutboundApiServer.ts
1697
2012
  import http2 from "http";
1698
2013
  import { networkInterfaces } from "os";
2014
+ import { healthHttpStatus } from "@omnicross/contracts/health-logging-types";
2015
+
2016
+ // src/outbound-api/keyedMutex.ts
2017
+ var KeyedMutex = class {
2018
+ /** key → the tail of the pending-op chain (resolves when the last op frees). */
2019
+ tails = /* @__PURE__ */ new Map();
2020
+ /**
2021
+ * Run `fn` exclusively for `key`: it starts only after every previously
2022
+ * enqueued op for the SAME key has settled, and the next waiter starts only
2023
+ * after `fn` settles. Returns `fn`'s result (or rejection). Never lets one op's
2024
+ * failure wedge the queue (waiters proceed regardless).
2025
+ */
2026
+ async runExclusive(key, fn) {
2027
+ const prev = this.tails.get(key) ?? Promise.resolve();
2028
+ let release2;
2029
+ const gate = new Promise((resolve) => {
2030
+ release2 = resolve;
2031
+ });
2032
+ const chain = prev.then(() => gate);
2033
+ this.tails.set(key, chain);
2034
+ await prev.catch(() => void 0);
2035
+ try {
2036
+ return await fn();
2037
+ } finally {
2038
+ release2();
2039
+ if (this.tails.get(key) === chain) this.tails.delete(key);
2040
+ }
2041
+ }
2042
+ };
1699
2043
 
1700
2044
  // src/outbound-api/outboundApiRouter.ts
1701
2045
  import { Readable } from "stream";
1702
2046
 
2047
+ // src/pipeline/webhookEmit.ts
2048
+ var sink = null;
2049
+ function emitWebhookEvent(event) {
2050
+ const active = sink;
2051
+ if (!active) return;
2052
+ try {
2053
+ active(event);
2054
+ } catch {
2055
+ }
2056
+ }
2057
+
2058
+ // src/outbound-api/apiServerConfig.ts
2059
+ import { DEFAULT_AUDIT_CONFIG } from "@omnicross/contracts/audit-types";
2060
+ import { DEFAULT_BILLING_CONFIG } from "@omnicross/contracts/billing-types";
2061
+ import {
2062
+ WEBHOOK_DESTINATION_TYPES,
2063
+ WEBHOOK_EVENT_KINDS
2064
+ } from "@omnicross/contracts/webhook-types";
2065
+ var OUTBOUND_API_SERVER_CONFIG_KEY = "outboundApiServer.config";
2066
+ var DEFAULT_USER_MESSAGE_QUEUE = {
2067
+ enabled: false,
2068
+ delayMs: 200,
2069
+ waitTimeoutMs: 6e4
2070
+ };
2071
+ var DEFAULT_CONCURRENCY_QUEUE = {
2072
+ maxQueueSizeFactor: 2,
2073
+ minQueueSize: 4,
2074
+ waitTimeoutMs: 6e4
2075
+ };
2076
+ var DEFAULT_ACCOUNT_HEALTH = {
2077
+ overloadCooldownEnabled: true,
2078
+ overloadCooldownMs: 10 * 6e4
2079
+ };
2080
+ function normalizeAccountHealth(raw) {
2081
+ const ah = raw?.accountHealth;
2082
+ return {
2083
+ overloadCooldownEnabled: ah?.overloadCooldownEnabled !== false,
2084
+ overloadCooldownMs: clampNumber(
2085
+ ah?.overloadCooldownMs,
2086
+ 6e4,
2087
+ 36e5,
2088
+ DEFAULT_ACCOUNT_HEALTH.overloadCooldownMs
2089
+ )
2090
+ };
2091
+ }
2092
+ var DEFAULT_ACCOUNT_PROBE = {
2093
+ enabled: false,
2094
+ intervalMs: 15 * 6e4,
2095
+ onlyMultiAccount: true,
2096
+ timeoutMs: 5e3,
2097
+ historySize: 10,
2098
+ staggerMs: 500
2099
+ };
2100
+ function normalizeAccountProbe(raw) {
2101
+ const ap = raw?.accountProbe;
2102
+ return {
2103
+ enabled: ap?.enabled === true,
2104
+ intervalMs: clampNumber(ap?.intervalMs, 6e4, 864e5, DEFAULT_ACCOUNT_PROBE.intervalMs),
2105
+ onlyMultiAccount: ap?.onlyMultiAccount !== false,
2106
+ timeoutMs: clampNumber(ap?.timeoutMs, 1e3, 6e4, DEFAULT_ACCOUNT_PROBE.timeoutMs),
2107
+ historySize: Math.trunc(
2108
+ clampNumber(ap?.historySize, 1, 200, DEFAULT_ACCOUNT_PROBE.historySize)
2109
+ ),
2110
+ staggerMs: clampNumber(ap?.staggerMs, 0, 6e4, DEFAULT_ACCOUNT_PROBE.staggerMs)
2111
+ };
2112
+ }
2113
+ function normalizeAudit(raw) {
2114
+ const a = raw?.audit;
2115
+ return {
2116
+ enabled: a?.enabled === true,
2117
+ captureBodies: a?.captureBodies === true,
2118
+ maxBodyBytes: Math.trunc(
2119
+ clampNumber(a?.maxBodyBytes, 256, 1048576, DEFAULT_AUDIT_CONFIG.maxBodyBytes)
2120
+ ),
2121
+ retentionDays: Math.trunc(
2122
+ clampNumber(a?.retentionDays, 1, 365, DEFAULT_AUDIT_CONFIG.retentionDays)
2123
+ ),
2124
+ trustForwardedFor: a?.trustForwardedFor === true
2125
+ };
2126
+ }
2127
+ function normalizeBilling(raw) {
2128
+ const b = raw?.billing;
2129
+ const config = {
2130
+ enabled: b?.enabled === true,
2131
+ maxRetryAgeMs: Math.trunc(
2132
+ clampNumber(b?.maxRetryAgeMs, 6e4, 2592e6, DEFAULT_BILLING_CONFIG.maxRetryAgeMs)
2133
+ )
2134
+ };
2135
+ if (typeof b?.endpoint === "string" && b.endpoint.trim()) config.endpoint = b.endpoint.trim();
2136
+ if (typeof b?.secret === "string" && b.secret.length > 0) config.secret = b.secret;
2137
+ return config;
2138
+ }
2139
+ var DEFAULT_FINGERPRINT = { enabled: false };
2140
+ function normalizeFingerprint(raw) {
2141
+ const f = raw?.fingerprint;
2142
+ const out = { enabled: f?.enabled === true };
2143
+ if (typeof f?.ua === "string" && f.ua.trim().length > 0) out.ua = f.ua.trim();
2144
+ return out;
2145
+ }
2146
+ function normalizeVoucher(raw) {
2147
+ return { enabled: raw?.voucher?.enabled === true };
2148
+ }
2149
+ var PROXY_TYPES = ["http", "https", "socks5"];
2150
+ function normalizeProxyConfig(raw) {
2151
+ if (!raw || typeof raw !== "object") return void 0;
2152
+ const r = raw;
2153
+ if (typeof r["url"] === "string" && r["url"].trim().length > 0) {
2154
+ return { url: r["url"].trim() };
2155
+ }
2156
+ const type = r["type"];
2157
+ const host = r["host"];
2158
+ const port = r["port"];
2159
+ 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) {
2160
+ const out = {
2161
+ type,
2162
+ host: host.trim(),
2163
+ port: Math.trunc(port)
2164
+ };
2165
+ if (typeof r["username"] === "string" && r["username"].length > 0) out.username = r["username"];
2166
+ if (typeof r["password"] === "string" && r["password"].length > 0) out.password = r["password"];
2167
+ return out;
2168
+ }
2169
+ return void 0;
2170
+ }
2171
+ function normalizeProxySegment(raw) {
2172
+ if (!raw || typeof raw !== "object") return void 0;
2173
+ const r = raw;
2174
+ const out = {};
2175
+ const global = normalizeProxyConfig(r["global"]);
2176
+ if (global) out.global = global;
2177
+ if (r["byProvider"] && typeof r["byProvider"] === "object") {
2178
+ const byProvider = {};
2179
+ for (const [key, value] of Object.entries(r["byProvider"])) {
2180
+ if (!key.trim()) continue;
2181
+ const cfg = normalizeProxyConfig(value);
2182
+ if (cfg) byProvider[key] = cfg;
2183
+ }
2184
+ if (Object.keys(byProvider).length > 0) out.byProvider = byProvider;
2185
+ }
2186
+ return out.global || out.byProvider ? out : void 0;
2187
+ }
2188
+ function normalizeWebhookDestination(raw) {
2189
+ if (!raw || typeof raw !== "object") return void 0;
2190
+ const r = raw;
2191
+ const id = typeof r["id"] === "string" ? r["id"].trim() : "";
2192
+ const url = typeof r["url"] === "string" ? r["url"].trim() : "";
2193
+ const type = r["type"];
2194
+ if (!id || !url || typeof type !== "string" || !WEBHOOK_DESTINATION_TYPES.includes(type)) {
2195
+ return void 0;
2196
+ }
2197
+ const out = {
2198
+ id,
2199
+ type,
2200
+ url,
2201
+ enabled: r["enabled"] !== false
2202
+ };
2203
+ if (typeof r["secret"] === "string" && r["secret"].length > 0) out.secret = r["secret"];
2204
+ if (Array.isArray(r["events"])) {
2205
+ const events = r["events"].filter(
2206
+ (e) => typeof e === "string" && WEBHOOK_EVENT_KINDS.includes(e)
2207
+ );
2208
+ if (events.length > 0) out.events = events;
2209
+ }
2210
+ return out;
2211
+ }
2212
+ function normalizeWebhookSegment(raw) {
2213
+ if (!raw || typeof raw !== "object") return void 0;
2214
+ const r = raw;
2215
+ const destinations = [];
2216
+ if (Array.isArray(r["destinations"])) {
2217
+ for (const entry of r["destinations"]) {
2218
+ const d = normalizeWebhookDestination(entry);
2219
+ if (d) destinations.push(d);
2220
+ }
2221
+ }
2222
+ const enabled = r["enabled"] === true;
2223
+ if (!enabled && destinations.length === 0) return void 0;
2224
+ return { enabled, destinations };
2225
+ }
2226
+ function clampNumber(value, min, max, fallback) {
2227
+ const n = typeof value === "number" && Number.isFinite(value) ? value : fallback;
2228
+ return Math.min(max, Math.max(min, n));
2229
+ }
2230
+ function normalizeQueueSegments(raw) {
2231
+ const umq = raw?.userMessageQueue;
2232
+ const cq = raw?.concurrencyQueue;
2233
+ return {
2234
+ userMessageQueue: {
2235
+ enabled: umq?.enabled === true,
2236
+ delayMs: clampNumber(umq?.delayMs, 0, 1e4, DEFAULT_USER_MESSAGE_QUEUE.delayMs),
2237
+ waitTimeoutMs: clampNumber(
2238
+ umq?.waitTimeoutMs,
2239
+ 1e3,
2240
+ 3e5,
2241
+ DEFAULT_USER_MESSAGE_QUEUE.waitTimeoutMs
2242
+ )
2243
+ },
2244
+ concurrencyQueue: {
2245
+ maxQueueSizeFactor: clampNumber(
2246
+ cq?.maxQueueSizeFactor,
2247
+ 1,
2248
+ 10,
2249
+ DEFAULT_CONCURRENCY_QUEUE.maxQueueSizeFactor
2250
+ ),
2251
+ minQueueSize: clampNumber(cq?.minQueueSize, 1, 100, DEFAULT_CONCURRENCY_QUEUE.minQueueSize),
2252
+ waitTimeoutMs: clampNumber(
2253
+ cq?.waitTimeoutMs,
2254
+ 1e3,
2255
+ 3e5,
2256
+ DEFAULT_CONCURRENCY_QUEUE.waitTimeoutMs
2257
+ )
2258
+ }
2259
+ };
2260
+ }
2261
+ var ALL_ENDPOINTS = ["chat", "responses", "messages", "gemini"];
2262
+ function blankModelMap(endpoint) {
2263
+ const modelMap = {};
2264
+ for (const kind of modelKindsForEndpoint(endpoint)) modelMap[kind] = "";
2265
+ return modelMap;
2266
+ }
2267
+ function defaultEndpointConfig(endpoint) {
2268
+ if (isKindMappedEndpoint(endpoint)) {
2269
+ return { endpoint, modelMap: blankModelMap(endpoint), useSubscription: false };
2270
+ }
2271
+ if (endpoint === "chat") {
2272
+ return { endpoint, models: [], useSubscription: false };
2273
+ }
2274
+ return {
2275
+ endpoint,
2276
+ defaultModel: "",
2277
+ backgroundModel: "",
2278
+ useSubscription: false
2279
+ };
2280
+ }
2281
+ function normalizePrefixTargets(raw) {
2282
+ if (!raw || typeof raw !== "object") return void 0;
2283
+ const r = raw;
2284
+ const out = {};
2285
+ for (const key of ["claude", "gpt", "gemini"]) {
2286
+ const v = r[key];
2287
+ if (typeof v === "string" && v.trim() !== "") out[key] = v.trim();
2288
+ }
2289
+ return out.claude || out.gpt || out.gemini ? out : void 0;
2290
+ }
2291
+ function normalizeEndpointConfig(e) {
2292
+ const endpoint = e.endpoint;
2293
+ const useSubscription = e.useSubscription === true;
2294
+ if (isKindMappedEndpoint(endpoint)) {
2295
+ const rawMap = e.modelMap && typeof e.modelMap === "object" ? e.modelMap : {};
2296
+ const modelMap = {};
2297
+ for (const kind of modelKindsForEndpoint(endpoint)) {
2298
+ const v = rawMap[kind];
2299
+ modelMap[kind] = typeof v === "string" ? v : "";
2300
+ }
2301
+ return { endpoint, modelMap, useSubscription };
2302
+ }
2303
+ if (endpoint === "chat") {
2304
+ const models = Array.isArray(e.models) ? e.models.filter((m) => typeof m === "string" && m.trim() !== "") : [];
2305
+ const config2 = { endpoint, models, useSubscription };
2306
+ if (e.dispatchMode === "prefix") {
2307
+ config2.dispatchMode = "prefix";
2308
+ const targets = normalizePrefixTargets(e.prefixTargets);
2309
+ if (targets) config2.prefixTargets = targets;
2310
+ }
2311
+ return config2;
2312
+ }
2313
+ const config = {
2314
+ endpoint,
2315
+ defaultModel: typeof e.defaultModel === "string" ? e.defaultModel : "",
2316
+ backgroundModel: typeof e.backgroundModel === "string" ? e.backgroundModel : "",
2317
+ useSubscription
2318
+ };
2319
+ if (Array.isArray(e.backgroundModelIds)) {
2320
+ config.backgroundModelIds = e.backgroundModelIds;
2321
+ }
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
+
2409
+ // src/outbound-api/auditCapture.ts
2410
+ import { randomUUID } from "crypto";
2411
+
2412
+ // src/outbound-api/auditRedact.ts
2413
+ var AUDIT_REDACTED = "***REDACTED***";
2414
+ var RULES = [
2415
+ // `sk-…` family (Anthropic sk-ant-, omnicross sk-omnicross-, OpenAI sk-proj-/sk-).
2416
+ { re: /\bsk-[A-Za-z0-9_-]{6,}/g, replace: AUDIT_REDACTED },
2417
+ // Google API keys.
2418
+ { re: /\bAIza[A-Za-z0-9_-]{10,}/g, replace: AUDIT_REDACTED },
2419
+ // `Bearer <token>` (auth headers echoed into a body / SSE error).
2420
+ { re: /\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/gi, replace: `Bearer ${AUDIT_REDACTED}` },
2421
+ // Inline `x-api-key` / `api-key` / `authorization` label followed by a value.
2422
+ {
2423
+ re: /\b(x-api-key|api[_-]?key|authorization)(["']?\s*[:=]\s*["']?)[A-Za-z0-9._~+/=-]{6,}/gi,
2424
+ replace: `$1$2${AUDIT_REDACTED}`
2425
+ },
2426
+ // `x-goog-api-key` header echoed inline.
2427
+ {
2428
+ re: /\b(x-goog-api-key)(["']?\s*[:=]\s*["']?)[A-Za-z0-9._~+/=-]{6,}/gi,
2429
+ replace: `$1$2${AUDIT_REDACTED}`
2430
+ }
2431
+ ];
2432
+ function redactAuditText(text) {
2433
+ if (typeof text !== "string" || text.length === 0) return text ?? "";
2434
+ let out = text;
2435
+ for (const { re, replace } of RULES) {
2436
+ out = out.replace(re, replace);
2437
+ }
2438
+ return out;
2439
+ }
2440
+
2441
+ // src/outbound-api/auditCapture.ts
2442
+ function isStreamingContentType(res) {
2443
+ const ct = res.getHeader("content-type");
2444
+ const value = Array.isArray(ct) ? ct.join(";") : String(ct ?? "");
2445
+ return value.toLowerCase().includes("text/event-stream");
2446
+ }
2447
+ function resolveClientIp(req, trustForwardedFor) {
2448
+ if (trustForwardedFor) {
2449
+ const xff = req.headers["x-forwarded-for"];
2450
+ const raw = Array.isArray(xff) ? xff[0] : xff;
2451
+ if (typeof raw === "string" && raw.trim()) {
2452
+ const first = raw.split(",")[0]?.trim();
2453
+ if (first) return first;
2454
+ }
2455
+ }
2456
+ return req.socket?.remoteAddress ?? void 0;
2457
+ }
2458
+ function truncateToBytes(text, maxBytes) {
2459
+ const buf = Buffer.from(text, "utf8");
2460
+ if (buf.length <= maxBytes) return text;
2461
+ return buf.subarray(0, maxBytes).toString("utf8");
2462
+ }
2463
+ function beginAuditCapture(req, res, now) {
2464
+ const config = getAuditCaptureConfig();
2465
+ if (!config) return null;
2466
+ let rawRequestBody;
2467
+ const responseChunks = [];
2468
+ let responseBytes = 0;
2469
+ let streamDetected = false;
2470
+ let finished = false;
2471
+ const ctx = {
2472
+ setRequestBody(raw) {
2473
+ if (config.captureBodies) rawRequestBody = raw;
2474
+ }
2475
+ };
2476
+ if (config.captureBodies) {
2477
+ installResponseCapture(res, config.maxBodyBytes, {
2478
+ push(chunk) {
2479
+ responseBytes += chunk.length;
2480
+ responseChunks.push(chunk);
2481
+ },
2482
+ get bytes() {
2483
+ return responseBytes;
2484
+ },
2485
+ markStream() {
2486
+ streamDetected = true;
2487
+ },
2488
+ get isStream() {
2489
+ return streamDetected;
2490
+ }
2491
+ });
2492
+ }
2493
+ const finalize = () => {
2494
+ if (finished) return;
2495
+ finished = true;
2496
+ try {
2497
+ const record = {
2498
+ id: randomUUID(),
2499
+ ts: now,
2500
+ method: req.method ?? "",
2501
+ path: (req.url ?? "").split("?")[0] ?? "",
2502
+ status: res.statusCode,
2503
+ latencyMs: Math.max(0, Date.now() - now)
2504
+ };
2505
+ if (ctx.keyId != null) record.keyId = ctx.keyId;
2506
+ const ip = resolveClientIp(req, config.trustForwardedFor);
2507
+ if (ip) record.ip = ip;
2508
+ const ua = req.headers["user-agent"];
2509
+ if (typeof ua === "string" && ua) record.ua = ua;
2510
+ if (ctx.model) record.model = ctx.model;
2511
+ if (ctx.provider) record.provider = ctx.provider;
2512
+ const usage = readAuditUsage(res);
2513
+ if (usage) {
2514
+ if (typeof usage.inputTokens === "number") record.inputTokens = usage.inputTokens;
2515
+ if (typeof usage.outputTokens === "number") record.outputTokens = usage.outputTokens;
2516
+ if (typeof usage.costUsd === "number") record.costUsd = usage.costUsd;
2517
+ if (!record.model && usage.model) record.model = usage.model;
2518
+ if (!record.provider && usage.provider) record.provider = usage.provider;
2519
+ }
2520
+ if (ctx.error) record.error = redactAuditText(ctx.error);
2521
+ if (config.captureBodies) {
2522
+ if (rawRequestBody != null && rawRequestBody.length > 0) {
2523
+ record.requestBody = redactAuditText(truncateToBytes(rawRequestBody, config.maxBodyBytes));
2524
+ }
2525
+ if (!streamDetected && responseChunks.length > 0) {
2526
+ const body = Buffer.concat(responseChunks).toString("utf8");
2527
+ record.responseBody = redactAuditText(body);
2528
+ }
2529
+ }
2530
+ recordAudit(record);
2531
+ } catch {
2532
+ }
2533
+ };
2534
+ res.once("close", finalize);
2535
+ return ctx;
2536
+ }
2537
+ function installResponseCapture(res, maxBodyBytes, sink2) {
2538
+ let contentTypeChecked = false;
2539
+ const capture = (chunk) => {
2540
+ if (sink2.isStream) return;
2541
+ if (!contentTypeChecked) {
2542
+ contentTypeChecked = true;
2543
+ if (isStreamingContentType(res)) {
2544
+ sink2.markStream();
2545
+ return;
2546
+ }
2547
+ }
2548
+ if (sink2.bytes >= maxBodyBytes) return;
2549
+ let buf = null;
2550
+ if (Buffer.isBuffer(chunk)) buf = chunk;
2551
+ else if (typeof chunk === "string") buf = Buffer.from(chunk, "utf8");
2552
+ if (!buf || buf.length === 0) return;
2553
+ const remaining = maxBodyBytes - sink2.bytes;
2554
+ sink2.push(buf.length > remaining ? buf.subarray(0, remaining) : buf);
2555
+ };
2556
+ const originalWrite = res.write.bind(res);
2557
+ const originalEnd = res.end.bind(res);
2558
+ res.write = ((chunk, ...args) => {
2559
+ if (chunk != null) capture(chunk);
2560
+ return originalWrite(chunk, ...args);
2561
+ });
2562
+ res.end = ((chunk, ...args) => {
2563
+ if (chunk != null && typeof chunk !== "function") capture(chunk);
2564
+ return originalEnd(chunk, ...args);
2565
+ });
2566
+ }
2567
+
2568
+ // src/outbound-api/billingCapture.ts
2569
+ import { randomUUID as randomUUID2 } from "crypto";
2570
+ var defaultDefer = (fn) => {
2571
+ setTimeout(fn, 0);
2572
+ };
2573
+ function beginBillingCapture(_req, res, now, defer = defaultDefer) {
2574
+ if (!getBillingCaptureConfig()) return null;
2575
+ const ctx = {};
2576
+ let finished = false;
2577
+ const finalize = () => {
2578
+ if (finished) return;
2579
+ finished = true;
2580
+ const status = res.statusCode;
2581
+ defer(() => {
2582
+ try {
2583
+ const usage = readAuditUsage(res);
2584
+ if (!usage) return;
2585
+ const event = {
2586
+ id: randomUUID2(),
2587
+ ts: now,
2588
+ model: ctx.model ?? usage.model ?? "",
2589
+ authMode: ctx.authMode ?? "byo",
2590
+ inputTokens: usage.inputTokens ?? 0,
2591
+ outputTokens: usage.outputTokens ?? 0,
2592
+ costUsd: usage.costUsd ?? 0,
2593
+ status
2594
+ };
2595
+ if (ctx.keyId != null) event.keyId = ctx.keyId;
2596
+ const provider = ctx.provider ?? usage.provider;
2597
+ if (provider) event.provider = provider;
2598
+ publishBillingEvent(event);
2599
+ } catch {
2600
+ }
2601
+ });
2602
+ };
2603
+ res.once("close", finalize);
2604
+ return ctx;
2605
+ }
2606
+
1703
2607
  // src/outbound-api/outboundApiKeyAuth.ts
1704
2608
  import { createHash, randomBytes as randomBytes2 } from "crypto";
2609
+
2610
+ // src/outbound-api/keyPolicy.ts
2611
+ var DAY_MS = 864e5;
2612
+ function computeKeyExpiry(row, now) {
2613
+ const mode = row.activationMode ?? "fixed";
2614
+ if (mode === "activation") {
2615
+ if (row.activatedAt == null) return { expired: false, needsActivation: true };
2616
+ if (row.activationDays == null) return { expired: false };
2617
+ const effectiveExpiresAt = row.activatedAt + row.activationDays * DAY_MS;
2618
+ return { expired: now >= effectiveExpiresAt, effectiveExpiresAt };
2619
+ }
2620
+ if (row.expiresAt == null) return { expired: false };
2621
+ return { expired: now >= row.expiresAt, effectiveExpiresAt: row.expiresAt };
2622
+ }
2623
+ function checkKeyQuota(limits, spend) {
2624
+ if (!limits) return { allowed: true };
2625
+ if (limits.dailyUsd != null && spend.dailyUsd >= limits.dailyUsd) {
2626
+ return { allowed: false, scope: "daily", limitUsd: limits.dailyUsd, spentUsd: spend.dailyUsd };
2627
+ }
2628
+ if (limits.weeklyUsd != null && spend.weeklyUsd >= limits.weeklyUsd) {
2629
+ return { allowed: false, scope: "weekly", limitUsd: limits.weeklyUsd, spentUsd: spend.weeklyUsd };
2630
+ }
2631
+ if (limits.totalUsd != null && spend.totalUsd >= limits.totalUsd) {
2632
+ return { allowed: false, scope: "total", limitUsd: limits.totalUsd, spentUsd: spend.totalUsd };
2633
+ }
2634
+ return { allowed: true };
2635
+ }
2636
+ function canonicalModelId(value) {
2637
+ const idx = value.indexOf(",");
2638
+ const bare = idx >= 0 ? value.slice(idx + 1) : value;
2639
+ return bare.trim().toLowerCase();
2640
+ }
2641
+ function checkModelAllowed(restriction, resolvedModelId) {
2642
+ if (!restriction) return { allowed: true };
2643
+ const target = canonicalModelId(resolvedModelId);
2644
+ const listed = restriction.models.some((m) => canonicalModelId(m) === target);
2645
+ if (restriction.mode === "allowlist") {
2646
+ return listed ? { allowed: true } : { allowed: false, model: resolvedModelId };
2647
+ }
2648
+ return listed ? { allowed: false, model: resolvedModelId } : { allowed: true };
2649
+ }
2650
+
2651
+ // src/outbound-api/outboundApiKeyAuth.ts
1705
2652
  var SECRET_BYTES = 32;
1706
2653
  var PREFIX_LEN = 14;
1707
2654
  var KEY_PREFIX = "sk-omnicross-";
@@ -1749,143 +2696,730 @@ async function createNamedKey(db, name) {
1749
2696
  plaintextOnce: secret
1750
2697
  };
1751
2698
  }
1752
- async function verifyPresentedKey(db, presentedKey) {
1753
- if (!presentedKey) return null;
2699
+ function extractCostLimits(row) {
2700
+ const limits = {};
2701
+ if (row.dailyCostLimitUsd != null) limits.dailyUsd = row.dailyCostLimitUsd;
2702
+ if (row.totalCostLimitUsd != null) limits.totalUsd = row.totalCostLimitUsd;
2703
+ if (row.weeklyCostLimitUsd != null) limits.weeklyUsd = row.weeklyCostLimitUsd;
2704
+ return limits.dailyUsd != null || limits.totalUsd != null || limits.weeklyUsd != null ? limits : void 0;
2705
+ }
2706
+ function extractRateLimit(row) {
2707
+ if (row.rateLimitMaxRequests == null && row.rateLimitWindowMs == null) return void 0;
2708
+ const override = {};
2709
+ if (row.rateLimitMaxRequests != null) override.maxRequests = row.rateLimitMaxRequests;
2710
+ if (row.rateLimitWindowMs != null) override.windowMs = row.rateLimitWindowMs;
2711
+ return override;
2712
+ }
2713
+ function extractModelRestriction(row) {
2714
+ if (row.enableModelRestriction !== true) return void 0;
2715
+ return {
2716
+ mode: row.restrictionMode ?? "blacklist",
2717
+ models: row.restrictedModels ?? []
2718
+ };
2719
+ }
2720
+ function toVerifiedKey(row) {
2721
+ const key = { id: row.id };
2722
+ if (row.maxConcurrency !== void 0 && row.maxConcurrency !== null) {
2723
+ key.maxConcurrency = row.maxConcurrency;
2724
+ }
2725
+ const costLimits = extractCostLimits(row);
2726
+ if (costLimits) key.costLimits = costLimits;
2727
+ const rateLimit = extractRateLimit(row);
2728
+ if (rateLimit) key.rateLimit = rateLimit;
2729
+ const modelRestriction = extractModelRestriction(row);
2730
+ if (modelRestriction) key.modelRestriction = modelRestriction;
2731
+ return key;
2732
+ }
2733
+ async function verifyKey(db, presentedKey, now = Date.now()) {
2734
+ if (!presentedKey) return { status: "invalid" };
1754
2735
  const trimmed = presentedKey.trim();
1755
- if (!trimmed) return null;
2736
+ if (!trimmed) return { status: "invalid" };
1756
2737
  const row = await db.outboundApiKeysGetByHash(hashKey(trimmed));
1757
- if (!row) return null;
1758
- if (!row.enabled || row.revokedAt !== null) return null;
2738
+ if (!row) return { status: "invalid" };
2739
+ if (!row.enabled || row.revokedAt !== null) return { status: "invalid" };
2740
+ const expiry = computeKeyExpiry(row, now);
2741
+ if (expiry.expired) return { status: "expired" };
1759
2742
  void db.outboundApiKeysTouchLastUsed(row.id).catch(() => {
1760
2743
  });
1761
- return { id: row.id };
2744
+ if (expiry.needsActivation) {
2745
+ void db.outboundApiKeysMarkActivated(row.id, now).catch(() => {
2746
+ });
2747
+ }
2748
+ return { status: "ok", key: toVerifiedKey(row) };
2749
+ }
2750
+ async function verifyPresentedKey(db, presentedKey) {
2751
+ const result = await verifyKey(db, presentedKey);
2752
+ return result.status === "ok" ? result.key : null;
1762
2753
  }
1763
2754
 
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;
2755
+ // src/outbound-api/quotaWarn.ts
2756
+ var WEBHOOK_QUOTA_WARN_RATIO = 0.8;
2757
+ function computeQuotaWarnings(limits, spend, ratio = WEBHOOK_QUOTA_WARN_RATIO) {
2758
+ const out = [];
2759
+ const consider = (scope, limit, spent, windowStart) => {
2760
+ if (limit == null || limit <= 0) return;
2761
+ if (spent >= limit) return;
2762
+ if (spent >= ratio * limit) out.push({ scope, limitUsd: limit, spentUsd: spent, windowStart });
2763
+ };
2764
+ consider("daily", limits.dailyUsd, spend.dailyUsd, spend.dailyWindowStart);
2765
+ consider("weekly", limits.weeklyUsd, spend.weeklyUsd, spend.weeklyWindowStart);
2766
+ consider("total", limits.totalUsd, spend.totalUsd, 0);
2767
+ return out;
2768
+ }
2769
+ var WARN_GUARD_MAX = 1e4;
2770
+ var warned = /* @__PURE__ */ new Set();
2771
+ function markQuotaWarnedOnce(keyId, scope, windowStart) {
2772
+ const key = `${keyId}\0${scope}\0${windowStart}`;
2773
+ if (warned.has(key)) return false;
2774
+ if (warned.size >= WARN_GUARD_MAX) warned.clear();
2775
+ warned.add(key);
2776
+ return true;
2777
+ }
2778
+
2779
+ // src/outbound-api/outboundConcurrencyGate.ts
2780
+ var ConcurrencyQueueFullError = class extends Error {
2781
+ constructor(apiKeyId, maxQueueSize) {
2782
+ super(`concurrency queue full (max ${maxQueueSize}) for key '${apiKeyId}'`);
2783
+ this.apiKeyId = apiKeyId;
2784
+ this.maxQueueSize = maxQueueSize;
2785
+ this.name = "ConcurrencyQueueFullError";
2786
+ }
2787
+ apiKeyId;
2788
+ maxQueueSize;
2789
+ code = "concurrency_queue_full";
2790
+ };
2791
+ var ConcurrencyWaitTimeoutError = class extends Error {
2792
+ constructor(apiKeyId, waitTimeoutMs) {
2793
+ super(`concurrency wait timed out after ${waitTimeoutMs}ms for key '${apiKeyId}'`);
2794
+ this.apiKeyId = apiKeyId;
2795
+ this.waitTimeoutMs = waitTimeoutMs;
2796
+ this.name = "ConcurrencyWaitTimeoutError";
2797
+ }
2798
+ apiKeyId;
2799
+ waitTimeoutMs;
2800
+ code = "concurrency_wait_timeout";
2801
+ };
2802
+ var ConcurrencyWaitCancelledError = class extends Error {
2803
+ constructor(apiKeyId) {
2804
+ super(`concurrency wait cancelled for key '${apiKeyId}'`);
2805
+ this.apiKeyId = apiKeyId;
2806
+ this.name = "ConcurrencyWaitCancelledError";
2807
+ }
2808
+ apiKeyId;
2809
+ code = "concurrency_wait_cancelled";
2810
+ };
2811
+ function isConcurrencyRejection(err) {
2812
+ return err instanceof ConcurrencyQueueFullError || err instanceof ConcurrencyWaitTimeoutError || err instanceof ConcurrencyWaitCancelledError;
2813
+ }
2814
+ var OutboundConcurrencyGate = class {
2815
+ states = /* @__PURE__ */ new Map();
2816
+ ensure(apiKeyId) {
2817
+ let state = this.states.get(apiKeyId);
2818
+ if (!state) {
2819
+ state = { active: 0, waiters: [] };
2820
+ this.states.set(apiKeyId, state);
2821
+ }
2822
+ return state;
2823
+ }
2824
+ /** Delete a key's state once it is fully idle (no active, no waiters). */
2825
+ gc(apiKeyId) {
2826
+ const state = this.states.get(apiKeyId);
2827
+ if (state && state.active === 0 && state.waiters.length === 0) {
2828
+ this.states.delete(apiKeyId);
1795
2829
  }
1796
2830
  }
2831
+ /**
2832
+ * Acquire a concurrency slot for `apiKeyId` under `limit`. Grants immediately
2833
+ * when `active < limit`; else enqueues FIFO while under the per-key queue cap;
2834
+ * else rejects queue-full. `limit <= 0` = unlimited (always granted).
2835
+ */
2836
+ acquire(apiKeyId, limit, options) {
2837
+ const state = this.ensure(apiKeyId);
2838
+ const cap = limit > 0 ? limit : Infinity;
2839
+ if (state.active < cap) {
2840
+ state.active += 1;
2841
+ const slot = this.makeSlot(apiKeyId);
2842
+ return { granted: Promise.resolve(slot), cancel: slot.cancel };
2843
+ }
2844
+ const maxQueueSize = Math.max(
2845
+ Number.isFinite(cap) ? cap * options.maxQueueSizeFactor : Infinity,
2846
+ options.minQueueSize
2847
+ );
2848
+ if (state.waiters.length >= maxQueueSize) {
2849
+ const err = new ConcurrencyQueueFullError(apiKeyId, maxQueueSize);
2850
+ return { granted: Promise.reject(err), cancel: () => {
2851
+ } };
2852
+ }
2853
+ let settled = false;
2854
+ let granted = false;
2855
+ let resolveGrant;
2856
+ let rejectGrant;
2857
+ const grantedPromise = new Promise((resolve, reject) => {
2858
+ resolveGrant = resolve;
2859
+ rejectGrant = reject;
2860
+ });
2861
+ const waiter = {
2862
+ cap,
2863
+ settled: false,
2864
+ timeoutHandle: null,
2865
+ grant: () => {
2866
+ if (settled) return;
2867
+ settled = true;
2868
+ granted = true;
2869
+ waiter.settled = true;
2870
+ if (waiter.timeoutHandle) clearTimeout(waiter.timeoutHandle);
2871
+ state.active += 1;
2872
+ resolveGrant(this.makeSlot(apiKeyId));
2873
+ },
2874
+ fail: (err) => {
2875
+ if (settled) return;
2876
+ settled = true;
2877
+ waiter.settled = true;
2878
+ if (waiter.timeoutHandle) clearTimeout(waiter.timeoutHandle);
2879
+ const idx = state.waiters.indexOf(waiter);
2880
+ if (idx >= 0) state.waiters.splice(idx, 1);
2881
+ rejectGrant(err);
2882
+ this.gc(apiKeyId);
2883
+ }
2884
+ };
2885
+ waiter.timeoutHandle = setTimeout(() => {
2886
+ waiter.fail(new ConcurrencyWaitTimeoutError(apiKeyId, options.waitTimeoutMs));
2887
+ }, options.waitTimeoutMs);
2888
+ state.waiters.push(waiter);
2889
+ const cancel = () => {
2890
+ if (settled || granted) return;
2891
+ waiter.fail(new ConcurrencyWaitCancelledError(apiKeyId));
2892
+ };
2893
+ return { granted: grantedPromise, cancel };
2894
+ }
2895
+ /** Build an idempotent granted slot (`release` decrements; `cancel` is a no-op). */
2896
+ makeSlot(apiKeyId) {
2897
+ let released = false;
2898
+ const release2 = () => {
2899
+ if (released) return;
2900
+ released = true;
2901
+ const state = this.states.get(apiKeyId);
2902
+ if (!state) return;
2903
+ state.active = Math.max(0, state.active - 1);
2904
+ this.dispatchNext(apiKeyId);
2905
+ this.gc(apiKeyId);
2906
+ };
2907
+ return { release: release2, cancel: () => {
2908
+ } };
2909
+ }
2910
+ /** After a release freed a slot, grant the FIFO head if it fits under its cap. */
2911
+ dispatchNext(apiKeyId) {
2912
+ const state = this.states.get(apiKeyId);
2913
+ if (!state || state.waiters.length === 0) return;
2914
+ const head = state.waiters[0];
2915
+ if (state.active < head.cap) {
2916
+ state.waiters.shift();
2917
+ head.grant();
2918
+ }
2919
+ }
2920
+ /** Snapshot for observability — only keys with active slots or waiters. */
2921
+ getStatus() {
2922
+ const out = [];
2923
+ for (const [apiKeyId, state] of this.states) {
2924
+ if (state.active > 0 || state.waiters.length > 0) {
2925
+ out.push({ apiKeyId, active: state.active, waiting: state.waiters.length });
2926
+ }
2927
+ }
2928
+ return out;
2929
+ }
2930
+ /** Drop all state + pending timers (tests / teardown). */
2931
+ reset() {
2932
+ for (const state of this.states.values()) {
2933
+ for (const w of state.waiters) {
2934
+ if (w.timeoutHandle) clearTimeout(w.timeoutHandle);
2935
+ }
2936
+ }
2937
+ this.states.clear();
2938
+ }
2939
+ };
2940
+
2941
+ // src/outbound-api/outboundRateLimiter.ts
2942
+ var DEFAULT_WINDOW_MS = 6e4;
2943
+ var DEFAULT_MAX_REQUESTS = 60;
2944
+ var OutboundRateLimiter = class {
2945
+ windowMs;
2946
+ maxRequests;
2947
+ /** apiKeyId → ascending request timestamps within the current window. */
2948
+ hits = /* @__PURE__ */ new Map();
2949
+ constructor(options = {}) {
2950
+ this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
2951
+ this.maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
2952
+ }
2953
+ /**
2954
+ * Record a request for `apiKeyId` and decide whether it is allowed. Prunes
2955
+ * timestamps older than the window first; when allowed, the request's
2956
+ * timestamp is appended.
2957
+ *
2958
+ * `override` (outbound-key-policy) supplies a PER-KEY window/max for this
2959
+ * bucket, superseding the instance defaults for THAT key. Absent ⇒ the
2960
+ * instance default 60/60s (byte-identical to before this change). An effective
2961
+ * `maxRequests` of `0` means UNLIMITED — the request is allowed and NOT
2962
+ * recorded (no bucket growth).
2963
+ */
2964
+ check(apiKeyId, now = Date.now(), override) {
2965
+ const maxRequests = override?.maxRequests ?? this.maxRequests;
2966
+ const windowMs = override?.windowMs ?? this.windowMs;
2967
+ if (maxRequests === 0) return { allowed: true, retryAfterSeconds: 0 };
2968
+ const cutoff = now - windowMs;
2969
+ const timestamps = (this.hits.get(apiKeyId) ?? []).filter((ts) => ts > cutoff);
2970
+ if (timestamps.length >= maxRequests) {
2971
+ if (timestamps.length === 0) {
2972
+ this.hits.delete(apiKeyId);
2973
+ return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(windowMs / 1e3)) };
2974
+ }
2975
+ const oldest = timestamps[0];
2976
+ const retryAfterMs = Math.max(0, oldest + windowMs - now);
2977
+ this.hits.set(apiKeyId, timestamps);
2978
+ return {
2979
+ allowed: false,
2980
+ retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1e3))
2981
+ };
2982
+ }
2983
+ timestamps.push(now);
2984
+ this.hits.set(apiKeyId, timestamps);
2985
+ return { allowed: true, retryAfterSeconds: 0 };
2986
+ }
2987
+ /** Drop all recorded state (tests / teardown). */
2988
+ reset() {
2989
+ this.hits.clear();
2990
+ }
2991
+ };
2992
+
2993
+ // src/outbound-api/userMessageDetection.ts
2994
+ function asArray(v) {
2995
+ return Array.isArray(v) ? v : [];
2996
+ }
2997
+ function asObject(v) {
2998
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
2999
+ }
3000
+ function last(arr) {
3001
+ return arr.length > 0 ? arr[arr.length - 1] : void 0;
3002
+ }
3003
+ function isUserMessageAnthropic(body) {
3004
+ const messages = asArray(body["messages"]);
3005
+ const lastMsg = asObject(last(messages));
3006
+ if (!lastMsg || lastMsg["role"] !== "user") return false;
3007
+ const content = lastMsg["content"];
3008
+ if (typeof content === "string") return true;
3009
+ if (Array.isArray(content)) {
3010
+ const hasToolResult = content.some(
3011
+ (block) => asObject(block)?.["type"] === "tool_result"
3012
+ );
3013
+ return !hasToolResult;
3014
+ }
3015
+ return false;
3016
+ }
3017
+ function isUserMessageResponses(body) {
3018
+ const input = body["input"];
3019
+ if (typeof input === "string") return input.length > 0;
3020
+ if (Array.isArray(input)) {
3021
+ const lastItem = asObject(last(input));
3022
+ if (!lastItem) return false;
3023
+ if (lastItem["type"] === "function_call_output") return false;
3024
+ return lastItem["role"] === "user";
3025
+ }
1797
3026
  return false;
1798
3027
  }
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;
3028
+ function isUserMessageChat(body) {
3029
+ const messages = asArray(body["messages"]);
3030
+ const lastMsg = asObject(last(messages));
3031
+ if (!lastMsg) return false;
3032
+ return lastMsg["role"] === "user";
3033
+ }
3034
+ function isUserMessageGemini(body) {
3035
+ const contents = asArray(body["contents"]);
3036
+ const lastContent = asObject(last(contents));
3037
+ if (!lastContent || lastContent["role"] !== "user") return false;
3038
+ const parts = asArray(lastContent["parts"]);
3039
+ const hasFunctionResponse = parts.some(
3040
+ (part) => asObject(part)?.["functionResponse"] !== void 0
3041
+ );
3042
+ return !hasFunctionResponse;
3043
+ }
3044
+ function isUserMessageRequest(endpoint, parsedBody) {
3045
+ const body = asObject(parsedBody);
3046
+ if (!body) return false;
3047
+ switch (endpoint) {
3048
+ case "messages":
3049
+ return isUserMessageAnthropic(body);
3050
+ case "responses":
3051
+ return isUserMessageResponses(body);
3052
+ case "chat":
3053
+ return isUserMessageChat(body);
3054
+ case "gemini":
3055
+ return isUserMessageGemini(body);
3056
+ default:
3057
+ return false;
3058
+ }
3059
+ }
3060
+
3061
+ // src/outbound-api/userMessageSerialQueue.ts
3062
+ var SerialQueueTimeoutError = class extends Error {
3063
+ constructor(providerId, waitTimeoutMs) {
3064
+ super(`user-message serial queue wait timed out after ${waitTimeoutMs}ms for '${providerId}'`);
3065
+ this.providerId = providerId;
3066
+ this.waitTimeoutMs = waitTimeoutMs;
3067
+ this.name = "SerialQueueTimeoutError";
3068
+ }
3069
+ providerId;
3070
+ waitTimeoutMs;
3071
+ code = "serial_queue_timeout";
3072
+ };
3073
+ function isSerialQueueTimeout(err) {
3074
+ return err instanceof SerialQueueTimeoutError;
3075
+ }
3076
+ var UserMessageSerialQueue = class {
3077
+ states = /* @__PURE__ */ new Map();
3078
+ ensure(providerId) {
3079
+ let state = this.states.get(providerId);
3080
+ if (!state) {
3081
+ state = { holding: false, lastReleaseTs: null, waiters: [], dispatchHandle: null };
3082
+ this.states.set(providerId, state);
3083
+ }
3084
+ return state;
3085
+ }
3086
+ /**
3087
+ * Acquire the serial slot for `providerId`. Resolves immediately when the slot
3088
+ * is free, no one is queued, and the `delayMs` gap since the last release has
3089
+ * elapsed; otherwise enqueues FIFO and resolves in order (or rejects with a
3090
+ * {@link SerialQueueTimeoutError} after `waitTimeoutMs`).
3091
+ */
3092
+ acquire(providerId, options) {
3093
+ const { waitTimeoutMs, delayMs } = options;
3094
+ const now = options.now ?? Date.now();
3095
+ const state = this.ensure(providerId);
3096
+ const spacingElapsed = state.lastReleaseTs === null || now - state.lastReleaseTs >= delayMs;
3097
+ if (!state.holding && state.waiters.length === 0 && spacingElapsed) {
3098
+ state.holding = true;
3099
+ return Promise.resolve(this.makeSlot(providerId));
3100
+ }
3101
+ return new Promise((resolve, reject) => {
3102
+ const waiter = {
3103
+ delayMs,
3104
+ resolve,
3105
+ reject,
3106
+ timeoutHandle: null,
3107
+ done: false
3108
+ };
3109
+ waiter.timeoutHandle = setTimeout(() => {
3110
+ if (waiter.done) return;
3111
+ waiter.done = true;
3112
+ const idx = state.waiters.indexOf(waiter);
3113
+ if (idx >= 0) state.waiters.splice(idx, 1);
3114
+ reject(new SerialQueueTimeoutError(providerId, waitTimeoutMs));
3115
+ }, waitTimeoutMs);
3116
+ state.waiters.push(waiter);
3117
+ this.maybeDispatch(providerId);
3118
+ });
3119
+ }
3120
+ /** Build an idempotent release for a granted slot. */
3121
+ makeSlot(providerId) {
3122
+ let released = false;
3123
+ return {
3124
+ release: () => {
3125
+ if (released) return;
3126
+ released = true;
3127
+ const state = this.states.get(providerId);
3128
+ if (!state) return;
3129
+ state.holding = false;
3130
+ state.lastReleaseTs = Date.now();
3131
+ this.maybeDispatch(providerId);
3132
+ }
3133
+ };
3134
+ }
3135
+ /**
3136
+ * Grant the head waiter when the slot is free and the head's `delayMs` gap has
3137
+ * elapsed; otherwise schedule a dispatch for the residual gap. No-op while the
3138
+ * slot is held (release re-invokes this) or the queue is empty.
3139
+ */
3140
+ maybeDispatch(providerId) {
3141
+ const state = this.states.get(providerId);
3142
+ if (!state || state.holding || state.waiters.length === 0) return;
3143
+ const head = state.waiters[0];
3144
+ const now = Date.now();
3145
+ const elapsed = state.lastReleaseTs === null ? Infinity : now - state.lastReleaseTs;
3146
+ const residual = Math.max(0, head.delayMs - elapsed);
3147
+ if (residual <= 0) {
3148
+ this.grantHead(providerId);
3149
+ return;
3150
+ }
3151
+ if (state.dispatchHandle !== null) return;
3152
+ state.dispatchHandle = setTimeout(() => {
3153
+ state.dispatchHandle = null;
3154
+ this.grantHead(providerId);
3155
+ }, residual);
3156
+ }
3157
+ /** Hand the slot to the FIFO head (if free and someone is waiting). */
3158
+ grantHead(providerId) {
3159
+ const state = this.states.get(providerId);
3160
+ if (!state || state.holding || state.waiters.length === 0) return;
3161
+ if (state.dispatchHandle !== null) {
3162
+ clearTimeout(state.dispatchHandle);
3163
+ state.dispatchHandle = null;
3164
+ }
3165
+ const head = state.waiters.shift();
3166
+ if (!head) return;
3167
+ if (head.timeoutHandle) clearTimeout(head.timeoutHandle);
3168
+ head.done = true;
3169
+ state.holding = true;
3170
+ head.resolve(this.makeSlot(providerId));
3171
+ }
3172
+ /** Snapshot for observability — only keys holding a slot or with waiters. */
3173
+ getStatus() {
3174
+ const out = [];
3175
+ for (const [providerId, state] of this.states) {
3176
+ if (state.holding || state.waiters.length > 0) {
3177
+ out.push({ providerId, holding: state.holding, waiting: state.waiters.length });
3178
+ }
3179
+ }
3180
+ return out;
3181
+ }
3182
+ /** Drop all state + pending timers (tests / teardown). */
3183
+ reset() {
3184
+ for (const state of this.states.values()) {
3185
+ if (state.dispatchHandle) clearTimeout(state.dispatchHandle);
3186
+ for (const w of state.waiters) {
3187
+ if (w.timeoutHandle) clearTimeout(w.timeoutHandle);
3188
+ }
3189
+ }
3190
+ this.states.clear();
3191
+ }
3192
+ };
3193
+
3194
+ // src/outbound-api/voucher.ts
3195
+ import { createHash as createHash2 } from "crypto";
3196
+ var CODE_ENTROPY_CHARS = 32;
3197
+ var CODE_PREFIX = "CC_";
3198
+ var DISPLAY_PREFIX_LEN = 8;
3199
+ var DAY_MS2 = 864e5;
3200
+ function generateVoucherCode() {
3201
+ return CODE_PREFIX + randomBase62(CODE_ENTROPY_CHARS);
3202
+ }
3203
+ function hashVoucherCode(code) {
3204
+ return createHash2("sha256").update(code, "utf8").digest("hex");
3205
+ }
3206
+ function voucherCodePrefix(code) {
3207
+ return code.slice(0, DISPLAY_PREFIX_LEN);
3208
+ }
3209
+ function newVoucherId() {
3210
+ return `vch_${Date.now()}_${randomBase62(8)}`;
3211
+ }
3212
+ function computeVoucherGrant(voucher, currentKey, now) {
3213
+ if (voucher.type === "credit") {
3214
+ const current = currentKey?.totalCostLimitUsd ?? 0;
3215
+ let granted2 = current + (voucher.creditUsd ?? 0);
3216
+ if (voucher.maxTotalCostLimitUsd != null) {
3217
+ granted2 = Math.min(granted2, voucher.maxTotalCostLimitUsd);
3218
+ }
3219
+ return { totalCostLimitUsd: granted2 };
3220
+ }
3221
+ const currentExpiry = currentKey?.expiresAt ?? now;
3222
+ let granted = currentExpiry + (voucher.renewalDays ?? 0) * DAY_MS2;
3223
+ if (voucher.maxExpiryDays != null) {
3224
+ granted = Math.min(granted, now + voucher.maxExpiryDays * DAY_MS2);
3225
+ }
3226
+ return { expiresAt: granted };
3227
+ }
3228
+ function toVoucherInfo(v) {
3229
+ const info = {
3230
+ id: v.id,
3231
+ codePrefix: v.codePrefix,
3232
+ type: v.type,
3233
+ status: v.status,
3234
+ createdAt: v.createdAt
3235
+ };
3236
+ if (v.creditUsd != null) info.creditUsd = v.creditUsd;
3237
+ if (v.renewalDays != null) info.renewalDays = v.renewalDays;
3238
+ if (v.maxTotalCostLimitUsd != null) info.maxTotalCostLimitUsd = v.maxTotalCostLimitUsd;
3239
+ if (v.maxExpiryDays != null) info.maxExpiryDays = v.maxExpiryDays;
3240
+ if (v.redeemedAt != null) info.redeemedAt = v.redeemedAt;
3241
+ if (v.redeemedByKeyId != null) info.redeemedByKeyId = v.redeemedByKeyId;
3242
+ if (v.grantApplied != null) info.grantApplied = v.grantApplied;
3243
+ if (v.grantedTotalCostLimitUsd != null) info.grantedTotalCostLimitUsd = v.grantedTotalCostLimitUsd;
3244
+ if (v.grantedExpiresAt != null) info.grantedExpiresAt = v.grantedExpiresAt;
3245
+ if (v.revokedAt != null) info.revokedAt = v.revokedAt;
3246
+ return info;
3247
+ }
3248
+
3249
+ // src/outbound-api/voucherRedeem.ts
3250
+ function isRedeemRequest(method, url) {
3251
+ if (method !== "POST" || !url) return false;
3252
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
3253
+ return path.endsWith("/redeem");
3254
+ }
3255
+ function writeErr(res, status, message, headers = {}) {
3256
+ if (res.headersSent) return;
3257
+ res.writeHead(status, { "Content-Type": "application/json", ...headers });
3258
+ res.end(JSON.stringify({ error: { type: "voucher_error", message } }));
3259
+ }
3260
+ function writeResult(res, result) {
3261
+ if (res.headersSent) return;
3262
+ res.writeHead(200, { "Content-Type": "application/json" });
3263
+ res.end(JSON.stringify({ ok: true, ...result }));
1810
3264
  }
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;
3265
+ function readBody2(req) {
3266
+ return new Promise((resolve, reject) => {
3267
+ const chunks = [];
3268
+ req.on("data", (c) => chunks.push(c));
3269
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
3270
+ req.on("error", reject);
3271
+ });
1824
3272
  }
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
- }
3273
+ function grantToResult(type, granted) {
3274
+ const result = { type };
3275
+ if (granted.totalCostLimitUsd != null) result.totalCostLimitUsd = granted.totalCostLimitUsd;
3276
+ if (granted.expiresAt != null) result.expiresAt = granted.expiresAt;
3277
+ return result;
3278
+ }
3279
+ function currentResult(type, keyRow) {
3280
+ const result = { type };
3281
+ if (type === "credit") {
3282
+ if (keyRow?.totalCostLimitUsd != null) result.totalCostLimitUsd = keyRow.totalCostLimitUsd;
3283
+ } else if (keyRow?.expiresAt != null) {
3284
+ result.expiresAt = keyRow.expiresAt;
3285
+ }
3286
+ return result;
3287
+ }
3288
+ function recordedAbsolute(v) {
3289
+ const granted = {};
3290
+ if (v.grantedTotalCostLimitUsd != null) granted.totalCostLimitUsd = v.grantedTotalCostLimitUsd;
3291
+ if (v.grantedExpiresAt != null) granted.expiresAt = v.grantedExpiresAt;
3292
+ return granted;
3293
+ }
3294
+ function policyFrom(granted) {
3295
+ const policy = {};
3296
+ if (granted.totalCostLimitUsd != null) policy.totalCostLimitUsd = granted.totalCostLimitUsd;
3297
+ if (granted.expiresAt != null) policy.expiresAt = granted.expiresAt;
3298
+ return policy;
3299
+ }
3300
+ var NOT_REDEEMABLE = "Invalid or non-redeemable code";
3301
+ var fallbackMutex = null;
3302
+ function getFallbackMutex() {
3303
+ fallbackMutex ??= new KeyedMutex();
3304
+ return fallbackMutex;
3305
+ }
3306
+ async function handleVoucherRedeem(req, res, deps, voucherEnabled, redeemLimiter, verifiedKeyId, presentedKey, now, redeemMutex) {
3307
+ if (!voucherEnabled || !deps.voucherDb) {
3308
+ writeErr(res, 403, "Voucher redemption is not enabled");
3309
+ return;
1835
3310
  }
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;
3311
+ const voucherDb = deps.voucherDb;
3312
+ const rl = redeemLimiter.check(verifiedKeyId, now);
3313
+ if (!rl.allowed) {
3314
+ writeErr(res, 429, "Too many redeem attempts", { "Retry-After": String(rl.retryAfterSeconds) });
3315
+ return;
1850
3316
  }
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";
3317
+ const raw = await readBody2(req);
3318
+ let code;
3319
+ try {
3320
+ code = raw ? JSON.parse(raw)["code"] : void 0;
3321
+ } catch {
3322
+ writeErr(res, 400, "Invalid JSON in request body");
3323
+ return;
1867
3324
  }
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";
3325
+ if (typeof code !== "string" || !code.trim()) {
3326
+ writeErr(res, 400, "Missing redemption code");
3327
+ return;
1880
3328
  }
3329
+ const codeHash = hashVoucherCode(code.trim());
3330
+ const keyHash = hashKey(presentedKey.trim());
3331
+ const applyFirstPass = async (voucher, absolute) => {
3332
+ const ok = await deps.db.outboundApiKeysSetPolicy(verifiedKeyId, policyFrom(absolute));
3333
+ if (!ok) {
3334
+ await voucherDb.voucherRevertRedeem(voucher.id, verifiedKeyId);
3335
+ return { ok: false, status: 409, message: "Could not apply the grant \u2014 please retry" };
3336
+ }
3337
+ await voucherDb.voucherMarkGrantApplied(voucher.id);
3338
+ return { ok: true, result: grantToResult(voucher.type, absolute) };
3339
+ };
3340
+ const reconcileOne = async (voucher) => {
3341
+ const ok = await deps.db.outboundApiKeysSetPolicy(verifiedKeyId, policyFrom(recordedAbsolute(voucher)));
3342
+ if (ok) await voucherDb.voucherMarkGrantApplied(voucher.id);
3343
+ };
3344
+ const reconcileStranded = async () => {
3345
+ const all = await voucherDb.voucherList();
3346
+ const stranded = all.filter((v) => v.status === "redeemed" && v.grantApplied !== true && v.redeemedByKeyId === verifiedKeyId).sort((a, b) => (a.redeemedAt ?? 0) - (b.redeemedAt ?? 0));
3347
+ for (const v of stranded) await reconcileOne(v);
3348
+ };
3349
+ const runCritical = async () => {
3350
+ await reconcileStranded();
3351
+ const voucher = await voucherDb.voucherGetByHash(codeHash);
3352
+ if (!voucher || voucher.status === "revoked") {
3353
+ return { ok: false, status: 400, message: NOT_REDEEMABLE };
3354
+ }
3355
+ if (voucher.status === "redeemed") {
3356
+ if (voucher.redeemedByKeyId !== verifiedKeyId) {
3357
+ return { ok: false, status: 400, message: NOT_REDEEMABLE };
3358
+ }
3359
+ if (voucher.grantApplied === true) {
3360
+ const keyRow2 = await deps.db.outboundApiKeysGetByHash(keyHash);
3361
+ return { ok: true, result: currentResult(voucher.type, keyRow2) };
3362
+ }
3363
+ return await applyFirstPass(voucher, recordedAbsolute(voucher));
3364
+ }
3365
+ const keyRow = await deps.db.outboundApiKeysGetByHash(keyHash);
3366
+ const absolute = computeVoucherGrant(voucher, keyRow, now);
3367
+ const won = await voucherDb.voucherRedeemCas(voucher.id, verifiedKeyId, absolute, now);
3368
+ if (!won) {
3369
+ const after = await voucherDb.voucherGetByHash(codeHash);
3370
+ if (after?.status === "redeemed" && after.redeemedByKeyId === verifiedKeyId) {
3371
+ return after.grantApplied === true ? { ok: true, result: currentResult(after.type, await deps.db.outboundApiKeysGetByHash(keyHash)) } : await applyFirstPass(after, recordedAbsolute(after));
3372
+ }
3373
+ return { ok: false, status: 409, message: "Code was already redeemed" };
3374
+ }
3375
+ return await applyFirstPass(voucher, absolute);
3376
+ };
3377
+ const mutex = redeemMutex ?? getFallbackMutex();
3378
+ const outcome = await mutex.runExclusive(verifiedKeyId, runCritical);
3379
+ if (outcome.ok) writeResult(res, outcome.result);
3380
+ else writeErr(res, outcome.status, outcome.message);
1881
3381
  }
1882
3382
 
1883
3383
  // src/outbound-api/outboundApiRouter.ts
3384
+ var fallbackRedeemLimiter = null;
3385
+ function getFallbackRedeemLimiter() {
3386
+ fallbackRedeemLimiter ??= new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
3387
+ return fallbackRedeemLimiter;
3388
+ }
1884
3389
  function writeJsonError(res, status, message, headers = {}) {
1885
3390
  if (res.headersSent) return;
1886
3391
  res.writeHead(status, { "Content-Type": "application/json", ...headers });
1887
3392
  res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
1888
3393
  }
3394
+ function writeCostLimitError(res, scope, limitUsd, spentUsd) {
3395
+ if (res.headersSent) return;
3396
+ res.writeHead(402, { "Content-Type": "application/json" });
3397
+ res.end(
3398
+ JSON.stringify({
3399
+ error: {
3400
+ type: "outbound_api_error",
3401
+ message: `Cost limit reached for this key (${scope}): $${spentUsd.toFixed(4)} of $${limitUsd.toFixed(4)}`
3402
+ },
3403
+ scope,
3404
+ limitUsd,
3405
+ spentUsd
3406
+ })
3407
+ );
3408
+ }
3409
+ function writeModelNotAllowedError(res, model, mode) {
3410
+ if (res.headersSent) return;
3411
+ res.writeHead(403, { "Content-Type": "application/json" });
3412
+ res.end(
3413
+ JSON.stringify({
3414
+ error: {
3415
+ type: "outbound_api_error",
3416
+ message: `Model '${model}' is not permitted for this key (${mode})`
3417
+ },
3418
+ model,
3419
+ mode
3420
+ })
3421
+ );
3422
+ }
1889
3423
  function extractPresentedKey(req) {
1890
3424
  const auth = req.headers["authorization"];
1891
3425
  if (typeof auth === "string" && auth.trim()) {
@@ -1917,7 +3451,18 @@ function extractGeminiModelFromUrl(url) {
1917
3451
  const m = /\/models\/([^/:]+):(?:stream)?[gG]enerateContent$/.exec(path.replace(/\/+$/, ""));
1918
3452
  return m ? decodeURIComponent(m[1]) : void 0;
1919
3453
  }
1920
- function readBody2(req) {
3454
+ function isModelsListRequest(url) {
3455
+ if (!url) return false;
3456
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
3457
+ return path.endsWith("/models");
3458
+ }
3459
+ function writeChatModelsList(res, config) {
3460
+ const chat = config.endpoints.find((e) => e.endpoint === "chat");
3461
+ const data = (chat?.models ?? []).map((ref) => parseModelRef(ref)).filter((p) => p !== null).map((p) => ({ id: p.modelId, object: "model", owned_by: "omnicross" }));
3462
+ res.writeHead(200, { "Content-Type": "application/json" });
3463
+ res.end(JSON.stringify({ object: "list", data }));
3464
+ }
3465
+ function readBody3(req) {
1921
3466
  return new Promise((resolve, reject) => {
1922
3467
  const chunks = [];
1923
3468
  req.on("data", (chunk) => chunks.push(chunk));
@@ -1930,20 +3475,48 @@ function shimAuthHeader(req, routeToken) {
1930
3475
  delete req.headers["x-goog-api-key"];
1931
3476
  delete req.headers["x-api-key"];
1932
3477
  }
1933
- async function handleOutboundRequest(req, res, deps, config, rateLimiter) {
3478
+ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serialQueue, concurrencyGate, redeemLimiter, redeemMutex) {
3479
+ const now = Date.now();
3480
+ const audit = beginAuditCapture(req, res, now);
3481
+ const billing = beginBillingCapture(req, res, now);
1934
3482
  const presented = extractPresentedKey(req);
1935
- const verified = await verifyPresentedKey(deps.db, presented);
1936
- if (!verified) {
1937
- writeJsonError(res, 401, "Invalid or missing API key");
3483
+ const verification = await verifyKey(deps.db, presented, now);
3484
+ if (verification.status !== "ok") {
3485
+ writeJsonError(
3486
+ res,
3487
+ 401,
3488
+ verification.status === "expired" ? "API key has expired" : "Invalid or missing API key"
3489
+ );
3490
+ return;
3491
+ }
3492
+ const verified = verification.key;
3493
+ if (audit) audit.keyId = verified.id;
3494
+ if (billing) billing.keyId = verified.id;
3495
+ if (isRedeemRequest(req.method, req.url)) {
3496
+ await handleVoucherRedeem(
3497
+ req,
3498
+ res,
3499
+ deps,
3500
+ config.voucher?.enabled === true,
3501
+ redeemLimiter ?? getFallbackRedeemLimiter(),
3502
+ verified.id,
3503
+ presented ?? "",
3504
+ now,
3505
+ redeemMutex
3506
+ );
1938
3507
  return;
1939
3508
  }
1940
- const decision = rateLimiter.check(verified.id);
3509
+ const decision = rateLimiter.check(verified.id, now, verified.rateLimit);
1941
3510
  if (!decision.allowed) {
1942
3511
  writeJsonError(res, 429, "Rate limit exceeded", {
1943
3512
  "Retry-After": String(decision.retryAfterSeconds)
1944
3513
  });
1945
3514
  return;
1946
3515
  }
3516
+ if (req.method === "GET" && isModelsListRequest(req.url)) {
3517
+ writeChatModelsList(res, config);
3518
+ return;
3519
+ }
1947
3520
  const endpoint = selectEndpoint(req.method, req.url);
1948
3521
  if (!endpoint) {
1949
3522
  writeJsonError(res, 404, `Unsupported: ${req.method} ${req.url}`);
@@ -1954,53 +3527,165 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter) {
1954
3527
  writeJsonError(res, 503, `endpoint '${endpoint}' is not configured`);
1955
3528
  return;
1956
3529
  }
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;
3530
+ if (verified.costLimits && deps.keySpendTracker) {
3531
+ const spend = await deps.keySpendTracker.getSpend(verified.id, now);
3532
+ const quota = checkKeyQuota(verified.costLimits, spend);
3533
+ if (!quota.allowed) {
3534
+ emitWebhookEvent({
3535
+ kind: "key.quotaExceeded",
3536
+ at: now,
3537
+ keyId: verified.id,
3538
+ scope: quota.scope,
3539
+ limitUsd: quota.limitUsd,
3540
+ spentUsd: quota.spentUsd
3541
+ });
3542
+ writeCostLimitError(res, quota.scope, quota.limitUsd, quota.spentUsd);
3543
+ return;
3544
+ }
3545
+ for (const warn of computeQuotaWarnings(verified.costLimits, spend)) {
3546
+ if (markQuotaWarnedOnce(verified.id, warn.scope, warn.windowStart)) {
3547
+ emitWebhookEvent({
3548
+ kind: "key.quotaWarning",
3549
+ at: now,
3550
+ keyId: verified.id,
3551
+ scope: warn.scope,
3552
+ limitUsd: warn.limitUsd,
3553
+ spentUsd: warn.spentUsd
3554
+ });
3555
+ }
3556
+ }
1969
3557
  }
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;
3558
+ const concurrencyLimit = verified.maxConcurrency;
3559
+ let releaseConcurrency = null;
3560
+ if (typeof concurrencyLimit === "number" && concurrencyLimit > 0) {
3561
+ const cq = config.concurrencyQueue ?? DEFAULT_CONCURRENCY_QUEUE;
3562
+ const acquisition = concurrencyGate.acquire(verified.id, concurrencyLimit, {
3563
+ maxQueueSizeFactor: cq.maxQueueSizeFactor,
3564
+ minQueueSize: cq.minQueueSize,
3565
+ waitTimeoutMs: cq.waitTimeoutMs
3566
+ });
3567
+ const cancelOnClose = () => acquisition.cancel();
3568
+ res.once("close", cancelOnClose);
3569
+ let slot;
3570
+ try {
3571
+ slot = await acquisition.granted;
3572
+ } catch (err) {
3573
+ res.removeListener("close", cancelOnClose);
3574
+ if (isConcurrencyRejection(err)) {
3575
+ writeJsonError(res, 429, "Concurrency limit exceeded", { "Retry-After": "5" });
3576
+ return;
3577
+ }
3578
+ throw err;
3579
+ }
3580
+ res.removeListener("close", cancelOnClose);
3581
+ const release2 = () => slot.release();
3582
+ releaseConcurrency = release2;
3583
+ res.once("close", release2);
1991
3584
  }
1992
- const routeMap = deps.providerProxy.getRouteMap();
1993
- const token = routeMap.addRoute(resolved.route);
1994
3585
  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);
3586
+ const ingressFormat = endpointToIngressFormat(endpoint);
3587
+ const rawBody = await readBody3(req);
3588
+ let parsedBody = {};
3589
+ try {
3590
+ parsedBody = rawBody ? JSON.parse(rawBody) : {};
3591
+ } catch {
3592
+ writeJsonError(res, 400, "Invalid JSON in request body");
3593
+ return;
3594
+ }
3595
+ if (audit) audit.setRequestBody(rawBody);
3596
+ if (endpoint === "gemini" && typeof parsedBody["model"] !== "string") {
3597
+ const urlModel = extractGeminiModelFromUrl(req.url);
3598
+ if (urlModel) parsedBody["model"] = urlModel;
3599
+ }
3600
+ const sessionId = deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}` : null;
3601
+ const resolved = isKindMappedEndpoint(endpoint) || endpoint === "chat" ? await resolveRoute({
3602
+ config: endpointConfig,
3603
+ ingressFormat,
3604
+ llmConfig: deps.llmConfig,
3605
+ sessionId,
3606
+ // Capture the ORIGINAL requested id BEFORE any downstream swap; for
3607
+ // kind-mapped endpoints it selects the kind AND is stamped onto
3608
+ // `route.requestedModel`; for chat it is matched against the list.
3609
+ requestedModel: extractRequestedModel(ingressFormat, parsedBody),
3610
+ // Attribution: stamp the verified named-key id onto the route.
3611
+ apiKeyId: verified.id
3612
+ }) : await resolveRoute({
3613
+ config: endpointConfig,
3614
+ role: detectRequestRole(ingressFormat, parsedBody, {
3615
+ backgroundModelIds: endpointConfig.backgroundModelIds
3616
+ }),
3617
+ ingressFormat,
3618
+ llmConfig: deps.llmConfig,
3619
+ sessionId,
3620
+ // Attribution: stamp the verified named-key id onto the route.
3621
+ apiKeyId: verified.id
3622
+ });
3623
+ if (!resolved.ok) {
3624
+ writeJsonError(res, resolved.error.status, resolved.error.message);
3625
+ return;
3626
+ }
3627
+ if (audit) {
3628
+ audit.model = resolved.route.model;
3629
+ audit.provider = resolved.route.providerId;
3630
+ }
3631
+ if (billing) {
3632
+ billing.model = resolved.route.model;
3633
+ billing.provider = resolved.route.providerId;
3634
+ billing.authMode = resolved.route.authMode;
3635
+ }
3636
+ if (verified.modelRestriction) {
3637
+ const modelDecision = checkModelAllowed(verified.modelRestriction, resolved.route.model);
3638
+ if (!modelDecision.allowed) {
3639
+ writeModelNotAllowedError(res, modelDecision.model, verified.modelRestriction.mode);
3640
+ return;
3641
+ }
3642
+ }
3643
+ let releaseSerial = null;
3644
+ const providerKey = resolved.route.providerId;
3645
+ if (config.userMessageQueue?.enabled && providerKey && isUserMessageRequest(endpoint, parsedBody)) {
3646
+ const umq = config.userMessageQueue;
3647
+ try {
3648
+ const serialSlot = await serialQueue.acquire(providerKey, {
3649
+ waitTimeoutMs: umq.waitTimeoutMs,
3650
+ delayMs: umq.delayMs
3651
+ });
3652
+ releaseSerial = () => serialSlot.release();
3653
+ } catch (err) {
3654
+ if (isSerialQueueTimeout(err)) {
3655
+ writeJsonError(res, 503, "User-message serial queue wait timed out");
3656
+ return;
3657
+ }
3658
+ throw err;
3659
+ }
3660
+ }
3661
+ if (releaseSerial) {
3662
+ const original = res.writeHead.bind(res);
3663
+ const releaseOnce = releaseSerial;
3664
+ res.writeHead = ((...args) => {
3665
+ res.writeHead = original;
3666
+ releaseOnce();
3667
+ return original(...args);
3668
+ });
3669
+ }
3670
+ const routeMap = deps.providerProxy.getRouteMap();
3671
+ const token = routeMap.addRoute(resolved.route);
3672
+ try {
3673
+ shimAuthHeader(req, token);
3674
+ const replay = makeReplayRequest(req, rawBody);
3675
+ await routeRequest(replay, res, routeMap, deps.proxyDeps);
3676
+ } catch (err) {
3677
+ const message = serializeError(err);
3678
+ if (deps.logger) deps.logger.error("[OutboundApi] dispatch error:", message);
3679
+ else console.error("[OutboundApi] dispatch error:", message);
3680
+ emitWebhookEvent({ kind: "server.error", at: Date.now(), message });
3681
+ if (audit) audit.error = message;
3682
+ writeJsonError(res, 502, message);
3683
+ } finally {
3684
+ routeMap.removeRoute(token);
3685
+ if (releaseSerial) releaseSerial();
3686
+ }
2002
3687
  } finally {
2003
- routeMap.removeRoute(token);
3688
+ if (releaseConcurrency) releaseConcurrency();
2004
3689
  }
2005
3690
  }
2006
3691
  function makeReplayRequest(req, rawBody) {
@@ -2014,53 +3699,20 @@ function makeReplayRequest(req, rawBody) {
2014
3699
  return readable;
2015
3700
  }
2016
3701
 
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
3702
  // src/outbound-api/OutboundApiServer.ts
2061
3703
  var DEFAULT_OUTBOUND_PORT = 8765;
2062
3704
  var LOOPBACK_ADDR = "127.0.0.1";
2063
3705
  var LAN_ADDR = "0.0.0.0";
3706
+ var OutboundApiConfigError = class extends Error {
3707
+ missing;
3708
+ constructor(missing) {
3709
+ super(
3710
+ "Outbound API server cannot start: incomplete model-kind configuration \u2014 " + missing.map((m) => `${m.endpoint} missing [${m.missingKinds.join(", ")}]`).join("; ")
3711
+ );
3712
+ this.name = "OutboundApiConfigError";
3713
+ this.missing = missing;
3714
+ }
3715
+ };
2064
3716
  var OutboundApiServer = class {
2065
3717
  constructor(deps, onPortChange) {
2066
3718
  this.deps = deps;
@@ -2072,7 +3724,25 @@ var OutboundApiServer = class {
2072
3724
  boundPort = 0;
2073
3725
  boundAddr = LOOPBACK_ADDR;
2074
3726
  endpoints = [];
3727
+ userMessageQueue;
3728
+ concurrencyQueue;
3729
+ voucherConfig;
2075
3730
  rateLimiter = new OutboundRateLimiter();
3731
+ /**
3732
+ * Redeem-attempt limiter (voucher-redemption #9, design D6) — a SEPARATE bucket
3733
+ * from the traffic `rateLimiter`, keyed by the authenticating key id, so
3734
+ * brute-forcing `CC_` codes is throttled (a handful/min) without touching the
3735
+ * per-key request rate. Conservative fixed defaults (10 / 60s).
3736
+ */
3737
+ redeemLimiter = new OutboundRateLimiter({ maxRequests: 10, windowMs: 6e4 });
3738
+ /**
3739
+ * Per-key redeem mutex (voucher-redemption #9, MJ1 fix). One instance for the
3740
+ * server's lifetime so concurrent redeem REQUESTS for the same key serialize
3741
+ * (relative grant increments accumulate instead of clobbering a snapshot).
3742
+ */
3743
+ redeemMutex = new KeyedMutex();
3744
+ serialQueue = new UserMessageSerialQueue();
3745
+ concurrencyGate = new OutboundConcurrencyGate();
2076
3746
  /**
2077
3747
  * Apply a config. Restarts the listener ONLY when the bind address or port
2078
3748
  * changes (or when toggling enabled); per-endpoint routing config is updated
@@ -2080,12 +3750,25 @@ var OutboundApiServer = class {
2080
3750
  */
2081
3751
  async applyConfig(input) {
2082
3752
  this.endpoints = input.endpoints;
3753
+ this.userMessageQueue = input.userMessageQueue;
3754
+ this.concurrencyQueue = input.concurrencyQueue;
3755
+ this.voucherConfig = input.voucher;
2083
3756
  const wantAddr = input.networkBinding ? LAN_ADDR : LOOPBACK_ADDR;
2084
3757
  const wantPort = input.port ?? DEFAULT_OUTBOUND_PORT;
2085
3758
  if (!input.enabled) {
2086
3759
  await this.stop();
2087
3760
  return;
2088
3761
  }
3762
+ const missing = validateServerModelConfig({
3763
+ enabled: true,
3764
+ networkBinding: input.networkBinding,
3765
+ endpoints: input.endpoints,
3766
+ port: input.port
3767
+ });
3768
+ if (missing.length > 0) {
3769
+ await this.stop();
3770
+ throw new OutboundApiConfigError(missing);
3771
+ }
2089
3772
  const running = this.server !== null;
2090
3773
  const bindChanged = running && (this.boundAddr !== wantAddr || this.boundPort !== wantPort);
2091
3774
  if (running && !bindChanged) {
@@ -2101,7 +3784,7 @@ var OutboundApiServer = class {
2101
3784
  this.boundAddr = bindAddr;
2102
3785
  this.boundPort = actualPort;
2103
3786
  if (actualPort !== port) this.onPortChange?.(actualPort);
2104
- console.log(`[OutboundApiServer] Listening on ${bindAddr}:${actualPort}`);
3787
+ this.logInfo(`[OutboundApiServer] Listening on ${bindAddr}:${actualPort}`);
2105
3788
  return actualPort;
2106
3789
  }
2107
3790
  /** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
@@ -2123,7 +3806,7 @@ var OutboundApiServer = class {
2123
3806
  const addr = server.address();
2124
3807
  if (addr && typeof addr === "object") {
2125
3808
  server.removeListener("error", onError);
2126
- server.on("error", (e) => console.error("[OutboundApiServer] server error", serializeError(e)));
3809
+ server.on("error", (e) => this.logError("[OutboundApiServer] server error", serializeError(e)));
2127
3810
  this.server = server;
2128
3811
  resolve(addr.port);
2129
3812
  } else {
@@ -2134,21 +3817,58 @@ var OutboundApiServer = class {
2134
3817
  }
2135
3818
  /** Per-request handler. Auth is enforced on EVERY request (incl. loopback). */
2136
3819
  onRequest(req, res) {
3820
+ if (this.deps.healthReportProvider && this.tryServeHealth(req, res)) return;
2137
3821
  handleOutboundRequest(
2138
3822
  req,
2139
3823
  res,
2140
3824
  this.deps,
2141
- { endpoints: this.endpoints },
2142
- this.rateLimiter
3825
+ {
3826
+ endpoints: this.endpoints,
3827
+ userMessageQueue: this.userMessageQueue,
3828
+ concurrencyQueue: this.concurrencyQueue,
3829
+ voucher: this.voucherConfig
3830
+ },
3831
+ this.rateLimiter,
3832
+ this.serialQueue,
3833
+ this.concurrencyGate,
3834
+ this.redeemLimiter,
3835
+ this.redeemMutex
2143
3836
  ).catch((err) => {
2144
3837
  const message = serializeError(err);
2145
- console.error("[OutboundApiServer] unhandled error:", message);
3838
+ this.logError("[OutboundApiServer] unhandled error:", message);
2146
3839
  if (!res.headersSent) {
2147
3840
  res.writeHead(500, { "Content-Type": "application/json" });
2148
3841
  res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
2149
3842
  }
2150
3843
  });
2151
3844
  }
3845
+ /**
3846
+ * Serve `GET|HEAD /health` (+ `/healthz`) from the injected provider, returning
3847
+ * true when it handled the request. 200 when `ok`, else 503; secret-free body.
3848
+ */
3849
+ tryServeHealth(req, res) {
3850
+ const provider = this.deps.healthReportProvider;
3851
+ if (!provider) return false;
3852
+ if (req.method !== "GET" && req.method !== "HEAD") return false;
3853
+ const path = (req.url ?? "/").split("?")[0]?.replace(/\/+$/, "") || "/";
3854
+ if (path !== "/health" && path !== "/healthz") return false;
3855
+ const report = provider();
3856
+ res.writeHead(healthHttpStatus(report.status), { "Content-Type": "application/json" });
3857
+ res.end(req.method === "HEAD" ? void 0 : JSON.stringify(report));
3858
+ return true;
3859
+ }
3860
+ /** Route an info lifecycle line through the injected logger, else `console.log`
3861
+ * (byte-identical legacy fallback when no logger is wired). */
3862
+ logInfo(message) {
3863
+ if (this.deps.logger) this.deps.logger.info(message);
3864
+ else console.log(message);
3865
+ }
3866
+ /** Route an error lifecycle line through the injected logger, else `console.error`. */
3867
+ logError(message, detail) {
3868
+ if (this.deps.logger) this.deps.logger.error(message, detail);
3869
+ else if (detail === void 0) console.error(message);
3870
+ else console.error(message, detail);
3871
+ }
2152
3872
  /** Stop the listener and release the port. */
2153
3873
  async stop() {
2154
3874
  const server = this.server;
@@ -2157,7 +3877,7 @@ var OutboundApiServer = class {
2157
3877
  this.boundPort = 0;
2158
3878
  return new Promise((resolve) => {
2159
3879
  server.close(() => {
2160
- console.log("[OutboundApiServer] Stopped");
3880
+ this.logInfo("[OutboundApiServer] Stopped");
2161
3881
  resolve();
2162
3882
  });
2163
3883
  });
@@ -2189,6 +3909,17 @@ var OutboundApiServer = class {
2189
3909
  lanFormats: lanBase ? formatUrls(lanBase) : null
2190
3910
  };
2191
3911
  }
3912
+ /**
3913
+ * Live queue-occupancy snapshot (only active entries). This getter's name +
3914
+ * shape are FROZEN — `omnicross-uqc-daemon` spreads it into its `/status`
3915
+ * response; the existing {@link getStatus} shape is deliberately NOT changed.
3916
+ */
3917
+ getQueueStatus() {
3918
+ return {
3919
+ serial: this.serialQueue.getStatus(),
3920
+ concurrency: this.concurrencyGate.getStatus()
3921
+ };
3922
+ }
2192
3923
  };
2193
3924
  function formatUrls(base) {
2194
3925
  return {
@@ -2209,66 +3940,88 @@ function firstLanIPv4() {
2209
3940
  return null;
2210
3941
  }
2211
3942
 
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
- });
3943
+ // src/outbound-api/keySpendTracker.ts
3944
+ function startOfLocalDay(ts) {
3945
+ const d = new Date(ts);
3946
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
3947
+ }
3948
+ function startOfLocalWeek(ts) {
3949
+ const d = new Date(ts);
3950
+ const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());
3951
+ day.setDate(day.getDate() - day.getDay());
3952
+ return day.getTime();
3953
+ }
3954
+ var KeySpendTracker = class {
3955
+ constructor(seeder) {
3956
+ this.seeder = seeder;
3957
+ }
3958
+ seeder;
3959
+ /** apiKeyId → live accumulated spend (seeded lazily). */
3960
+ spend = /* @__PURE__ */ new Map();
3961
+ /**
3962
+ * Read the key's accumulated spend, seeding from the durable store on a cache
3963
+ * miss and rolling the daily/weekly windows on a hit.
3964
+ */
3965
+ async getSpend(apiKeyId, now) {
3966
+ const existing = this.spend.get(apiKeyId);
3967
+ if (existing) {
3968
+ rollWindows(existing, now);
3969
+ return existing;
3970
+ }
3971
+ const seeded = await this.seed(apiKeyId, now);
3972
+ const existingAfterSeed = this.spend.get(apiKeyId);
3973
+ if (existingAfterSeed) {
3974
+ rollWindows(existingAfterSeed, now);
3975
+ return existingAfterSeed;
2245
3976
  }
3977
+ this.spend.set(apiKeyId, seeded);
3978
+ return seeded;
3979
+ }
3980
+ /**
3981
+ * Increment the aggregate for one recorded request. No-op when the key has not
3982
+ * been seeded yet — a later `getSpend` will lazy-seed from the durable store,
3983
+ * which already includes this record, so it is neither lost nor double-counted.
3984
+ */
3985
+ add(apiKeyId, costUsd, now) {
3986
+ const s = this.spend.get(apiKeyId);
3987
+ if (!s) return;
3988
+ rollWindows(s, now);
3989
+ s.dailyUsd += costUsd;
3990
+ s.weeklyUsd += costUsd;
3991
+ s.totalUsd += costUsd;
3992
+ }
3993
+ /** Drop all cached spend (tests / teardown). */
3994
+ reset() {
3995
+ this.spend.clear();
3996
+ }
3997
+ /**
3998
+ * Derive a fresh spend snapshot for `apiKeyId` from the durable store in a
3999
+ * SINGLE scan (all three windows at once). `endTs` is EXCLUSIVE, so `now + 1`
4000
+ * includes a record stamped at exactly `now`.
4001
+ */
4002
+ async seed(apiKeyId, now) {
4003
+ const dailyWindowStart = startOfLocalDay(now);
4004
+ const weeklyWindowStart = startOfLocalWeek(now);
4005
+ const { totalUsd, dailyUsd, weeklyUsd } = await this.seeder.getSpendByKey({
4006
+ apiKeyId,
4007
+ dayStartTs: dailyWindowStart,
4008
+ weekStartTs: weeklyWindowStart,
4009
+ endTs: now + 1
4010
+ });
4011
+ return { dailyUsd, dailyWindowStart, weeklyUsd, weeklyWindowStart, totalUsd };
4012
+ }
4013
+ };
4014
+ function rollWindows(s, now) {
4015
+ const day = startOfLocalDay(now);
4016
+ if (s.dailyWindowStart !== day) {
4017
+ s.dailyUsd = 0;
4018
+ s.dailyWindowStart = day;
4019
+ }
4020
+ const week = startOfLocalWeek(now);
4021
+ if (s.weeklyWindowStart !== week) {
4022
+ s.weeklyUsd = 0;
4023
+ s.weeklyWindowStart = week;
2246
4024
  }
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
4025
  }
2273
4026
 
2274
4027
  // src/outbound-api/index.ts
@@ -3893,8 +5646,8 @@ var CompletionService = class {
3893
5646
  * emission is unconditional in production. Optional — when unset (unit-test
3894
5647
  * constructors), the emit calls no-op, identical to `usageRecorder`.
3895
5648
  */
3896
- setUsageEventSink(sink) {
3897
- this.usageEventSink = sink;
5649
+ setUsageEventSink(sink2) {
5650
+ this.usageEventSink = sink2;
3898
5651
  }
3899
5652
  /**
3900
5653
  * Set the vision-fallback provider used by `applyVisionFallback` to describe
@@ -4380,21 +6133,64 @@ export {
4380
6133
  ProviderProxy,
4381
6134
  getProviderProxy,
4382
6135
  __resetProviderProxyForTests,
6136
+ KeyedMutex,
6137
+ OUTBOUND_API_SERVER_CONFIG_KEY,
6138
+ DEFAULT_USER_MESSAGE_QUEUE,
6139
+ DEFAULT_CONCURRENCY_QUEUE,
6140
+ DEFAULT_ACCOUNT_PROBE,
6141
+ normalizeAccountProbe,
6142
+ normalizeAudit,
6143
+ normalizeBilling,
6144
+ DEFAULT_FINGERPRINT,
6145
+ normalizeFingerprint,
6146
+ normalizeVoucher,
6147
+ normalizeProxyConfig,
6148
+ normalizeProxySegment,
6149
+ normalizeWebhookDestination,
6150
+ normalizeWebhookSegment,
6151
+ normalizeQueueSegments,
6152
+ normalizePrefixTargets,
6153
+ defaultServerConfig,
6154
+ normalizeServerConfig,
6155
+ loadServerConfig,
6156
+ saveServerConfig,
6157
+ mergeServerConfig,
6158
+ AUDIT_REDACTED,
6159
+ redactAuditText,
6160
+ beginAuditCapture,
6161
+ beginBillingCapture,
6162
+ computeKeyExpiry,
6163
+ checkKeyQuota,
6164
+ randomBase62,
4383
6165
  hashKey,
4384
6166
  createNamedKey,
6167
+ verifyKey,
4385
6168
  verifyPresentedKey,
4386
- detectRequestRole,
4387
- endpointToIngressFormat,
6169
+ ConcurrencyQueueFullError,
6170
+ ConcurrencyWaitTimeoutError,
6171
+ ConcurrencyWaitCancelledError,
6172
+ isConcurrencyRejection,
6173
+ OutboundConcurrencyGate,
4388
6174
  OutboundRateLimiter,
6175
+ isUserMessageRequest,
6176
+ SerialQueueTimeoutError,
6177
+ isSerialQueueTimeout,
6178
+ UserMessageSerialQueue,
6179
+ generateVoucherCode,
6180
+ hashVoucherCode,
6181
+ voucherCodePrefix,
6182
+ newVoucherId,
6183
+ computeVoucherGrant,
6184
+ toVoucherInfo,
6185
+ isRedeemRequest,
6186
+ handleVoucherRedeem,
4389
6187
  DEFAULT_OUTBOUND_PORT,
6188
+ OutboundApiConfigError,
4390
6189
  OutboundApiServer,
4391
6190
  formatUrls,
4392
- OUTBOUND_API_SERVER_CONFIG_KEY,
4393
- defaultServerConfig,
4394
- normalizeServerConfig,
4395
- loadServerConfig,
4396
- saveServerConfig,
4397
- mergeServerConfig,
6191
+ startOfLocalDay,
6192
+ startOfLocalWeek,
6193
+ KeySpendTracker,
4398
6194
  getOutboundApiServer,
4399
6195
  __resetOutboundApiServerForTests
4400
6196
  };