@bitkyc08/opencodex 2.55.0 → 2.57.0

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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -25,8 +25,12 @@ import {
25
25
  applyUpstreamRecoveryInit,
26
26
  fetchWithResetRetry,
27
27
  fetchWithTransientRetry,
28
+ isNonReplayableResponse,
29
+ isReplayRefusalCode,
30
+ isReplayRefusalResponse,
28
31
  prepareSameTarget429Wait,
29
32
  type UpstreamSendRecovery,
33
+ UPSTREAM_RESET_REPLAY_REFUSED_CODE,
30
34
  } from "../lib/upstream-retry";
31
35
  import {
32
36
  isTranslatorBudgetExceededError,
@@ -51,7 +55,9 @@ import { linkAbortSignal } from "./responses";
51
55
  import {
52
56
  addFinalRequestLog,
53
57
  beginRequestAttempt,
54
- noteAttemptSend,
58
+ noteProviderAttemptSend,
59
+ recordKeyAttemptFailure,
60
+ recordKeyWireAttemptUsage,
55
61
  recordFirstOutput,
56
62
  recordAttemptCredentialSource,
57
63
  sealRequestAttemptIdentity,
@@ -344,10 +350,12 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
344
350
  const encoding = new Headers(init.headers).get("accept-encoding");
345
351
  if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
346
352
  if (init.signal?.aborted) throw init.signal.reason;
347
- noteAttemptSend(attempt, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
348
- return ((activeProvider as OcxProviderTransport).fetch ?? execute)(request.url, applyUpstreamRecoveryInit({
353
+ noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
354
+ const dispatched = await ((activeProvider as OcxProviderTransport).fetch ?? execute)(request.url, applyUpstreamRecoveryInit({
349
355
  ...init, method: request.method, headers, body: request.body,
350
356
  }, transportRecovery));
357
+ if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
358
+ return dispatched;
351
359
  },
352
360
  }),
353
361
  );
@@ -375,6 +383,11 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
375
383
  let retries = 0;
376
384
  while (
377
385
  response.status === 429
386
+ // A 429 this proxy synthesized for a refused reset replay is not a provider rate
387
+ // limit: waiting and re-sending here is exactly the duplicate inference the refusal
388
+ // exists to stop. It kept the same shape under the old 502 only because 502 never
389
+ // matched this branch.
390
+ && !isNonReplayableResponse(response)
378
391
  && retryPolicy
379
392
  && retries < retryPolicy.attempts
380
393
  && transientSendAvailable()
@@ -388,7 +401,9 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
388
401
  if (upstream.signal.aborted) throw upstream.signal.reason;
389
402
  response = await send(activeRequest, "rate-limit-429");
390
403
  }
391
- while (response.status === 429 && hasKeyPoolFailover(activeProvider)) {
404
+ // Same reason as above, plus a second one: rotating here would write a cooldown against
405
+ // a key that rate-limited nothing, and that false signal outlives the request.
406
+ while (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
392
407
  const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
393
408
  retryAfter: response.headers.get("retry-after"),
394
409
  now: Date.now(),
@@ -474,6 +489,12 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
474
489
  if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
475
490
  classified.code = CYBER_POLICY_ERROR_CODE;
476
491
  classified.type = cyberPolicyErrorType(upstreamType);
492
+ } else if (isReplayRefusalResponse(response) || isReplayRefusalCode(upstreamCode)) {
493
+ // 429 classifies as a rate limit and a rate limit already carries a code, so the branch
494
+ // below -- which only fills an EMPTY code -- could never restore this one. Without it the
495
+ // client is told the provider throttled the turn, when what happened is that this proxy
496
+ // declined to send it a second time.
497
+ classified.code = UPSTREAM_RESET_REPLAY_REFUSED_CODE;
477
498
  } else if (upstreamCode === "model_not_found") {
478
499
  classified.code = "model_not_found";
479
500
  classified.type = "invalid_request_error";
@@ -481,7 +502,10 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
481
502
  classified.code = upstreamCode;
482
503
  }
483
504
  const status = isCyberPolicyCode(classified.code) ? 400 : response.status;
484
- const retryAfter = isCyberPolicyCode(classified.code)
505
+ // A refusal this proxy made has no wait to report. Synthesizing one here would hand the
506
+ // client the default two-second retry for a rate limit that never happened, which is the
507
+ // duplicate send the refusal exists to prevent.
508
+ const retryAfter = isCyberPolicyCode(classified.code) || isReplayRefusalCode(classified.code)
485
509
  ? undefined
486
510
  : resolveClientRetryAfter({
487
511
  status: response.status,
@@ -509,8 +533,10 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
509
533
  stallTimeoutSec: config.stallTimeoutSec,
510
534
  onFirstOutput: logIds ? () => recordFirstOutput(logCtx, logIds.start) : undefined,
511
535
  onUsage: usage => {
512
- logCtx.usage = usage;
513
- attempt.usage = usage;
536
+ if (!recordKeyWireAttemptUsage(logCtx, usage)) {
537
+ logCtx.usage = usage;
538
+ attempt.usage = usage;
539
+ }
514
540
  },
515
541
  onTerminal: (status: number, message?: string) => {
516
542
  terminalStatus = status;
@@ -600,8 +626,10 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
600
626
  if (!completion) return fail(502, "upstream response contained no choices", "upstream_error");
601
627
  const usage = usageFromChat(completion.usage);
602
628
  if (usage) {
603
- logCtx.usage = usage;
604
- attempt.usage = usage;
629
+ if (!recordKeyWireAttemptUsage(logCtx, usage)) {
630
+ logCtx.usage = usage;
631
+ attempt.usage = usage;
632
+ }
605
633
  }
606
634
  if (logIds) recordFirstOutput(logCtx, logIds.start);
607
635
  try {
@@ -0,0 +1,88 @@
1
+ import {
2
+ assertServerAuthConfig,
3
+ corsHeaders,
4
+ managementCorsHeaders,
5
+ isAllowedRequestOrigin,
6
+ isAllowedManagementOrigin,
7
+ isApiAuthRequired,
8
+ isLoopbackHostname,
9
+ jsonResponse,
10
+ admissionFields,
11
+ resolveApiAuth,
12
+ resolveResponsesApiAuth,
13
+ requestPolicyView,
14
+ type DataPlaneAdmission,
15
+ type RequestPolicyView,
16
+ safeConfigDTO,
17
+ setCorsOrigin,
18
+ withCors,
19
+ withManagementCors,
20
+ } from "../auth-cors";
21
+
22
+ // Header-safe by construction: a key id reaches a response header, so anything outside this
23
+ // class could inject a header break or a control character into a response we control.
24
+ const REMOTE_CATALOG_KEY_ID_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
25
+ export const GUI_PAIRING_EXCHANGE_BODY_LIMIT = 4 * 1024;
26
+ export const REMOTE_WORKSPACE_PAIRING_BODY_LIMIT = 32 * 1024;
27
+
28
+ /**
29
+ * Read at most `limit` bytes of a request body, or refuse.
30
+ *
31
+ * Returns null the moment the body is known to exceed `limit`, without retaining the excess.
32
+ * `req.text()` cannot express that: it buffers to completion first, so a caller who omits
33
+ * Content-Length or uses chunked framing decides how much memory the process spends. That
34
+ * matters here because the one caller is an unauthenticated endpoint.
35
+ *
36
+ * limit+1 is the stopping point rather than limit, so a body exactly at the limit is still
37
+ * accepted and only a genuinely over-limit body is rejected.
38
+ */
39
+ export async function readBoundedRequestText(req: Request, limit: number): Promise<string | null> {
40
+ const body = req.body;
41
+ if (!body) return "";
42
+ const reader = body.getReader();
43
+ const chunks: Uint8Array[] = [];
44
+ let total = 0;
45
+ try {
46
+ for (;;) {
47
+ const { done, value } = await reader.read();
48
+ if (done) break;
49
+ if (!value || value.byteLength === 0) continue;
50
+ total += value.byteLength;
51
+ if (total > limit) return null;
52
+ chunks.push(value);
53
+ }
54
+ } finally {
55
+ // Cancel rather than only releasing the lock: on the reject path the peer may still be
56
+ // sending, and an uncancelled body keeps that transfer alive.
57
+ await reader.cancel().catch(() => {});
58
+ }
59
+ const joined = new Uint8Array(total);
60
+ let offset = 0;
61
+ for (const chunk of chunks) {
62
+ joined.set(chunk, offset);
63
+ offset += chunk.byteLength;
64
+ }
65
+ return new TextDecoder().decode(joined);
66
+ }
67
+
68
+ /**
69
+ * Name WHICH configured credential was admitted, so a multi-key operator can attribute a
70
+ * catalog read.
71
+ *
72
+ * Scoped to configured keys on purpose: an environment token or a loopback bind has no key
73
+ * to name, and emitting one anyway would invent an attribution that does not exist. 200 only
74
+ * — this route emits no validator and therefore never answers 304.
75
+ *
76
+ * An id that fails the header-safe pattern is omitted rather than sanitized, with one warning
77
+ * that does NOT repeat the id: logging the offending value is how a malformed id becomes a
78
+ * log-injection vector instead of a dropped header.
79
+ */
80
+ export function withRemoteCatalogKeyId(response: Response, admission: DataPlaneAdmission): Response {
81
+ if (response.status !== 200 || admission.kind !== "configured") return response;
82
+ if (!REMOTE_CATALOG_KEY_ID_PATTERN.test(admission.keyId)) {
83
+ console.warn("[remote-catalog] configured API key id is not header-safe; omitting x-opencodex-key-id");
84
+ return response;
85
+ }
86
+ response.headers.set("x-opencodex-key-id", admission.keyId);
87
+ return response;
88
+ }