@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
@@ -0,0 +1,869 @@
1
+ import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
2
+ import type { PreparedResponsesRequest } from "./request-prepare";
3
+ import type { ResponsesTransport } from "./request-transport";
4
+ import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
5
+ import type { ResponsesEffects } from "./response-effects";
6
+ import type { PassthroughExchange } from "./passthrough-dispatch";
7
+ import {
8
+ sanitizePassthroughHeaders,
9
+ createSseInspector,
10
+ markEagerRelaySseResponse,
11
+ markNativePassthroughSseResponse,
12
+ consumeForInspection,
13
+ consumeForResponseLogMetadata,
14
+ relaySseWithFailedTail,
15
+ relayWithAbort,
16
+ } from "../relay";
17
+ import { isUsageDebugEnabled } from "../../usage/debug";
18
+ import { isReplayRefusalResponse } from "../../lib/upstream-retry";
19
+ import { teeWithBoundedInspection } from "../inspection-tee";
20
+ import {
21
+ codexForwardTerminalOutcomeRecorder,
22
+ usesCodexForwardPoolAuth,
23
+ codexQuotaOutcomeMeta,
24
+ codexDenialOutcomeMeta,
25
+ isFixedCodexAccount,
26
+ shouldDeferCodexResetDerivedCooldown,
27
+ } from "./core-codex-account";
28
+ import type { ResponsesTerminalStatus } from "../../bridge";
29
+ import { isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./ws-upstream";
30
+ import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-model-fallback";
31
+ import { recordCodexUpstreamOutcome } from "../../codex/routing";
32
+ import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant } from "../../codex/auth-context";
33
+ import { consumeComboFailure } from "./core-combo-failure";
34
+ import { readDisplaySafeErrorText } from "./core-errors";
35
+ import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
36
+ import { formatPassthroughUpstreamError } from "./passthrough-error";
37
+ import {
38
+ providerModelResponsesTerminalRepair,
39
+ providerModelResponsesUpstreamStreaming,
40
+ } from "../../providers/registry";
41
+ import {
42
+ resolvePassthroughWebSearchBridgeAuth,
43
+ planPassthroughWebSearchBridge,
44
+ createPassthroughWebSearchBridgeStream,
45
+ createPassthroughWebSearchBridgeExecutor,
46
+ } from "../../web-search/passthrough-bridge";
47
+ import { fetchWithHeaderTimeout, providerFetch } from "./fetch-helpers";
48
+ import { providerApiKeySelectionIsCurrent } from "../../providers/api-key-selection";
49
+ import { requiresVisionPreprocessing } from "../../vision";
50
+ import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
51
+ import { relayResponsesSseWithTerminalRepair } from "../responses-terminal-repair";
52
+ import {
53
+ hasResponsesSnapshotRepair,
54
+ createResponsesSnapshotBlockRewrite,
55
+ } from "../responses-snapshot-repair";
56
+ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
57
+ import { createImageGenCallRestoreRewrite, restoreImageGenCallsInJson } from "../responses-image-gen-repair";
58
+ import {
59
+ createSelfNamedToolCallNamespaceScrubRewrite,
60
+ scrubSelfNamedToolCallNamespaceInJson,
61
+ } from "../responses-self-named-namespace-scrub";
62
+ import {
63
+ createMuseToolNameRestoreRewrite,
64
+ restoreMuseToolNamesInJson,
65
+ } from "../../responses/muse-tool-name-alias";
66
+ import {
67
+ createRoutedNamespaceCallRestoreRewrite,
68
+ restoreRoutedNamespaceCallsInJson,
69
+ } from "../../responses/namespace-tool-compat";
70
+ import {
71
+ hasResponsesItemIdRepair,
72
+ createResponsesItemIdPayloadRewrite,
73
+ repairResponsesJsonItemIds,
74
+ } from "../responses-item-id-repair";
75
+ import {
76
+ payloadRewriteAsBlockRewrite,
77
+ composeSsePayloadRewrites,
78
+ composeSseBlockRewrites,
79
+ relaySseWithBlockRewrite,
80
+ } from "../sse-payload-rewrite";
81
+ import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
82
+ import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
83
+ import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
84
+ import { createGrokResponsesControlFrameBlockRewrite } from "../grok-responses-control-frame";
85
+ import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
86
+ import {
87
+ createPlaintextV2AgentMessageCallRestoreRewrite,
88
+ restorePlaintextV2AgentMessageCallsInJsonResult,
89
+ PLAINTEXT_V2_AGENT_MESSAGE_RESTORE_OVERFLOW_MESSAGE,
90
+ } from "../../responses/plaintext-v2-agent-messages";
91
+ import { createResponsesFieldBackfillBlockRewrite } from "./responses-field-backfill";
92
+ import { createResponsesFunctionToolRepairBlockRewrite } from "../responses-function-tool-repair";
93
+ import {
94
+ createUndeclaredToolCallGuardBlockRewrite,
95
+ undeclaredToolCallNameInResponse,
96
+ undeclaredToolCallMessage,
97
+ normalizeDefaultNamespaceInJson,
98
+ } from "../responses-undeclared-tool-guard";
99
+ import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
100
+ import { linkAbortSignal, UPSTREAM_JSON_BODY_READ_OPTIONS } from "./core-lifetime";
101
+ import { registerTurn, unregisterTurn, trackStreamLifetime } from "../lifecycle";
102
+ import { relaySseEagerBounded } from "../relay-eager";
103
+ import { readBoundedResponseBody } from "../../lib/bounded-body";
104
+ import { formatErrorResponse } from "../../bridge";
105
+ import { inspectResponseLogJson } from "../request-log";
106
+ import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
107
+ import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
108
+ import { responsesJsonToSseStream } from "../responses-json-events";
109
+
110
+ /** One responsibility of the Responses request pipeline; state owners are explicit. */
111
+ export async function deliverPassthroughResponse(
112
+ requestContext: Pick<ResponsesRequestContext, "logCtx" | "config" | "options" | "req">,
113
+ admissionState: ResponsesAdmissionState,
114
+ requestState: Pick<
115
+ PreparedResponsesRequest,
116
+ | "parsed"
117
+ | "route"
118
+ | "subagentQuotaFailureModel"
119
+ | "subagentFallbackAccountId"
120
+ | "clientRequestedStream"
121
+ | "translatorBudget"
122
+ >,
123
+ transportState: Pick<ResponsesTransport, "requestBindings">,
124
+ sidecarState: Pick<ResponsesSidecarAuth, "openAiSidecar">,
125
+ responseEffects: Pick<
126
+ ResponsesEffects,
127
+ | "plaintextV2AgentMessageToolNames"
128
+ | "commitReasoningReplayServingRoute"
129
+ | "routedMuseToolNameAliases"
130
+ | "routedNamespaceToolAliases"
131
+ | "plaintextV2AgentMessageAliasedToolNames"
132
+ | "recordTerminalOutcomes"
133
+ | "responseCompletionCancelled"
134
+ >,
135
+ nativeExchange: Pick<
136
+ PassthroughExchange,
137
+ | "upstreamResponse"
138
+ | "codexSafetyBufferingOptions"
139
+ | "upstream"
140
+ | "request"
141
+ | "connectMs"
142
+ | "imageGenCallAliases"
143
+ | "selfNamedNamespaceScrubAuthorization"
144
+ | "authorizedBareNamespaceToolAliases"
145
+ | "rememberPassthroughResponseChecked"
146
+ | "routedCustomToolNames"
147
+ | "routedCustomToolRepairNames"
148
+ | "declaredWireToolNames"
149
+ | "routedToolSearchNames"
150
+ | "outboundRequestBody"
151
+ | "functionRepairSchemas"
152
+ | "undeclaredToolGuardActive"
153
+ | "declaredNamelessClientCallTypes"
154
+ | "providerExecutedCallTypes"
155
+ | "declaredBareWireToolNames"
156
+ | "rememberPassthroughResponse"
157
+ | "noteInspectedPayload"
158
+ | "normalizeFunctionCompletionJson"
159
+ >,
160
+ ): Promise<Response> {
161
+ const { logCtx, config, options, req } = requestContext;
162
+ const {
163
+ upstreamResponse,
164
+ codexSafetyBufferingOptions,
165
+ upstream,
166
+ connectMs,
167
+ imageGenCallAliases,
168
+ selfNamedNamespaceScrubAuthorization,
169
+ authorizedBareNamespaceToolAliases,
170
+ rememberPassthroughResponseChecked,
171
+ routedCustomToolNames,
172
+ routedCustomToolRepairNames,
173
+ declaredWireToolNames,
174
+ routedToolSearchNames,
175
+ functionRepairSchemas,
176
+ declaredNamelessClientCallTypes,
177
+ providerExecutedCallTypes,
178
+ declaredBareWireToolNames,
179
+ rememberPassthroughResponse,
180
+ noteInspectedPayload,
181
+ normalizeFunctionCompletionJson,
182
+ } = nativeExchange;
183
+ const { commitReasoningReplayServingRoute, recordTerminalOutcomes } = responseEffects;
184
+ const { parsed, route, subagentQuotaFailureModel, clientRequestedStream, translatorBudget } = requestState;
185
+ const { openAiSidecar } = sidecarState;
186
+ const { requestBindings } = transportState;
187
+
188
+ const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions);
189
+ const resolvedModel = headers.get("openai-model")?.trim();
190
+ if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
191
+ if (isUsageDebugEnabled()) {
192
+ const upstreamContentType = upstreamResponse.headers.get("content-type");
193
+ if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
194
+ }
195
+ // The chatgpt backend may omit Content-Type on SSE responses. Fall back to
196
+ // treating a successful body as SSE when the caller requested streaming.
197
+ const passthroughCt = headers.get("content-type")?.toLowerCase();
198
+ const isEventStream = passthroughCt?.includes("text/event-stream")
199
+ || (responseEffects.plaintextV2AgentMessageToolNames.size === 0 && upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
200
+ const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
201
+ config,
202
+ admissionState.authCtx,
203
+ route.provider,
204
+ route.modelId,
205
+ logCtx,
206
+ );
207
+ let terminalOutcomeRecorded = false;
208
+ const terminalRecorder = recordTerminalOutcome
209
+ ? (status: ResponsesTerminalStatus, httpStatusOverride?: number): void => {
210
+ if (terminalOutcomeRecorded) return;
211
+ terminalOutcomeRecorded = true;
212
+ recordTerminalOutcome(status, httpStatusOverride);
213
+ }
214
+ : undefined;
215
+ const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
216
+ // Capture quota from upstream response for multi-account tracking
217
+ if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
218
+ // primary was the 5h window; it now carries weekly data for GPT plans.
219
+ // Prefer primary when present, fall back to secondary for compatibility.
220
+ const quotaMeta = { ...codexQuotaOutcomeMeta(upstreamResponse), ...(await codexDenialOutcomeMeta(upstreamResponse)) };
221
+ const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
222
+ if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
223
+ applyAccountQuotaFromUpstreamHeaders(admissionState.authCtx.accountId, upstreamResponse.headers,
224
+ admissionState.authCtx.writerGeneration, admissionState.authCtx.kind === "main-pool" ? admissionState.authCtx.mainQuotaWriter : undefined,
225
+ { modelId: route.modelId, poolWriter: admissionState.authCtx.kind === "pool" ? admissionState.authCtx.poolQuotaWriter : undefined });
226
+ }
227
+ if (terminalBodyWillRecord) {
228
+ options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
229
+ terminalRecorder(status, httpStatusOverride);
230
+ if (status === "failed" || status === "incomplete") {
231
+ const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
232
+ .find(value => value === 429 || value === 402);
233
+ if (!isFixedCodexAccount(admissionState.authCtx) && quotaFailureMessage !== undefined) {
234
+ recordSubagentQuotaFailureForThreadSpawn(
235
+ req.headers,
236
+ subagentQuotaFailureModel,
237
+ quotaFailureMessage,
238
+ config,
239
+ requestState.subagentFallbackAccountId,
240
+ );
241
+ }
242
+ }
243
+ options.onNativePassthroughTerminal?.(status);
244
+ });
245
+ } else if (!shouldDeferCodexResetDerivedCooldown(
246
+ upstreamResponse,
247
+ options.deferCodexResetDerivedCooldown,
248
+ ) && !isReplayRefusalResponse(upstreamResponse)) {
249
+ // A refusal this proxy made is not evidence about the account. Recording it would
250
+ // classify the synthetic 429 as quota exhaustion and write a default cooldown against
251
+ // a credential the request may never have reached, and that false signal outlives the
252
+ // request. The sibling recorders on this path already decline: the terminal recorder
253
+ // needs an ok streaming body, and the quota-header snapshot finds no quota headers.
254
+ recordCodexUpstreamOutcome(config, admissionState.authCtx.accountId, upstreamResponse.status, {
255
+ ...quotaMeta,
256
+ threadId: admissionState.authCtx.affinityKey,
257
+ fixedAccount: admissionState.authCtx.fixedAccount,
258
+ modelId: route.modelId,
259
+ probeLeaseId: codexProbeLeaseId(admissionState.authCtx),
260
+ probeQuotaScope: codexProbeQuotaScope(admissionState.authCtx),
261
+ transientProbe: codexTransientProbeGrant(admissionState.authCtx),
262
+ writerGeneration: admissionState.authCtx.writerGeneration,
263
+ // Includes a replay's second 401, which is the case that actually retires the
264
+ // account — fence it on the credential the request was holding.
265
+ ...(admissionState.authCtx.kind === "pool" ? { credentialGeneration: admissionState.authCtx.generation } : {}),
266
+ });
267
+ }
268
+ }
269
+
270
+ // Non-2xx passthrough failures must never reach Codex as an empty body —
271
+ // Codex renders that as the opaque "Unknown error" (#452). Combo attempts
272
+ // keep their typed failure envelope. Except for the classified 413 below,
273
+ // non-empty bodies are relayed verbatim
274
+ // (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
275
+ // Manual-redirect policy (#914): a 3xx is relayed as-is (Location preserved
276
+ // through sanitizePassthroughHeaders) so a redirect to a dead host can never
277
+ // masquerade as a pre-connection failure after the credential was seen.
278
+ // The numeric outcome above already classified it neutral — no streak.
279
+ if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
280
+ return new Response(upstreamResponse.body, {
281
+ status: upstreamResponse.status,
282
+ statusText: upstreamResponse.statusText,
283
+ headers: sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions),
284
+ });
285
+ }
286
+ if (!upstreamResponse.ok) {
287
+ if (options.comboAttempt) {
288
+ // No pre-read guard here: `consumeComboFailure` -> `readBoundedResponseBody` reads
289
+ // `response.body` itself and already threads the abort signal through its own read,
290
+ // and the combo contract is that this body's getter is touched exactly once (pinned by
291
+ // "captures passthrough failed usage from its original bounded body exactly once").
292
+ // Attaching a guard would be a second `.body` access and break that contract for no
293
+ // gain, since the bounded reader owns settlement on this path.
294
+ const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
295
+ options.onConsumedComboFailure?.(failure);
296
+ return failure.response;
297
+ }
298
+ // The bounded reader owns the original body, deadline, abort settlement, and lock.
299
+ // Unsafe partial data falls back to #452's non-empty status-only JSON.
300
+ const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
301
+ if (upstreamResponse.status === 413) {
302
+ return clientRequestedStream
303
+ ? streamingContextOverflowResponse(parsed._responseModelId ?? parsed.modelId, translatorBudget)
304
+ : jsonContextOverflowResponse();
305
+ }
306
+ return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
307
+ statusText: upstreamResponse.statusText,
308
+ headers,
309
+ // Provenance, not inference: `errorText` is empty when the bounded read finds nothing
310
+ // display-safe, and an empty body is exactly what the retryable-429 default fires on.
311
+ replayRefusal: isReplayRefusalResponse(upstreamResponse),
312
+ });
313
+ }
314
+
315
+ // Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
316
+ // async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
317
+ // native relay, never enters JS Sink.write); branch[1] is consumed in the
318
+ // background for terminal-outcome/quota inspection only.
319
+ // #314 alternative shape: win32 no-rewrite traffic follows the runtime/config
320
+ // gate; darwin no-rewrite traffic joins it only for explicit
321
+ // `streamMode: "eager-relay"` opt-in. Darwin `auto` always stays tee. The
322
+ // eager shape skips tee and uses one bounded reader with inline inspection
323
+ // (src/server/relay-eager.ts; policy:
324
+ // devlog/_fin/260731_macos_rss_retention/100_darwin_eager_optin.md).
325
+ // The bundled known-bad runtime remains on tee by default on both platforms.
326
+ if (isEventStream && upstreamResponse.body) {
327
+ // For streamed passthrough, a successful terminal response means non-error upstream status
328
+ // before relay starts. Waiting for SSE completion would retain request state across the whole
329
+ // stream; a later body failure does not undo that this destination accepted and served the turn.
330
+ commitReasoningReplayServingRoute(nativeExchange.request.headers);
331
+ const terminalRepairPolicy = providerModelResponsesTerminalRepair(
332
+ route.providerName,
333
+ route.provider,
334
+ route.modelId,
335
+ );
336
+ // #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
337
+ // and this branch relays that declaration on the assumption the destination executes it.
338
+ // A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
339
+ // web_search that nothing runs, and the undeclared-tool guard below ends the turn. When the
340
+ // provider opts in, the bridge intercepts that one call, runs the search, continues the
341
+ // conversation upstream, and hands back ordinary Responses SSE — so every rewrite below,
342
+ // including the guard itself, still inspects the client-facing stream. Default OFF: without
343
+ // the opt-in this is one planner call and the relay is byte-identical to before.
344
+ const webSearchBridgeAuth = resolvePassthroughWebSearchBridgeAuth(
345
+ route.provider.webSearchBridge?.backend,
346
+ config,
347
+ openAiSidecar,
348
+ );
349
+ const webSearchBridgePlan = planPassthroughWebSearchBridge(parsed, route.provider, {
350
+ providerName: route.providerName,
351
+ isPassthrough: true,
352
+ stream: parsed.stream === true,
353
+ auth: webSearchBridgeAuth,
354
+ });
355
+ // Capture the binding that actually served the first leg, after its permitted reselection.
356
+ const webSearchBridgeBinding = requestBindings.get(nativeExchange.request);
357
+ // The bridge wraps the RAW upstream body, so terminal repair below still owns the single
358
+ // client-facing terminal — the bridge drops the terminal of every intercepted leg.
359
+ const upstreamSseBody = webSearchBridgePlan
360
+ ? createPassthroughWebSearchBridgeStream({
361
+ plan: webSearchBridgePlan,
362
+ firstLeg: upstreamResponse.body,
363
+ requestBody: nativeExchange.request.body,
364
+ // Continuation legs replay the same built request with the executed search appended.
365
+ // The first leg already passed the recovery ladder, the outbound size ceiling, and the
366
+ // host circuit; a KEY-auth destination has no OAuth refresh to replay on a later leg.
367
+ send: (continuationBody: string) => fetchWithHeaderTimeout(
368
+ nativeExchange.request.url,
369
+ { method: nativeExchange.request.method, headers: nativeExchange.request.headers, body: continuationBody },
370
+ upstream.signal,
371
+ connectMs,
372
+ true,
373
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
374
+ // Pacing can outlive a manual selection change. A continuation must retain the
375
+ // first leg's key and appended search result, never rebuild from the original turn.
376
+ beforeDispatch: () => {
377
+ if (webSearchBridgeBinding?.kind !== "api-key"
378
+ || !providerApiKeySelectionIsCurrent(config, route.providerName, webSearchBridgeBinding.provider)) {
379
+ throw new Error("API key selection changed during a web-search continuation");
380
+ }
381
+ },
382
+ providerName: route.providerName,
383
+ modelId: route.modelId,
384
+ }),
385
+ false,
386
+ ),
387
+ execute: createPassthroughWebSearchBridgeExecutor(webSearchBridgePlan, {
388
+ providerApiKey: route.provider.apiKey ?? "",
389
+ auth: webSearchBridgeAuth,
390
+ hostedTool: parsed._webSearch,
391
+ describeImages: requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName),
392
+ sidecar: config.webSearchSidecar,
393
+ }),
394
+ // Appending a search result can push the continuation past the ceiling the first leg
395
+ // was admitted under, so the same limit is re-applied before every later send.
396
+ checkOutboundBody: (continuationBody: string) => {
397
+ const result = checkOutboundBodySize(continuationBody, config.maxUpstreamBodyBytes);
398
+ return result.admitted ? undefined : describeOutboundBodyRefusal(result);
399
+ },
400
+ signal: upstream.signal,
401
+ })
402
+ : upstreamResponse.body;
403
+ const passthroughSseBody = terminalRepairPolicy
404
+ ? relayResponsesSseWithTerminalRepair(
405
+ upstreamSseBody,
406
+ upstream,
407
+ terminalRepairPolicy,
408
+ translatorBudget,
409
+ options.responsesTerminalRepairScheduler,
410
+ )
411
+ : upstreamSseBody;
412
+ const repairConfig = route.provider.responsesItemIdRepair;
413
+ // Grok Build renders deltas live but reconstructs its durable assistant
414
+ // turn from the completed response snapshot. Native Responses streams
415
+ // may instead carry the complete items in output_item.done, so the
416
+ // explicit Grok compatibility marker enables strict client compatibility rewrites.
417
+ // The provider's broader snapshot/lifecycle repair remains opt-in.
418
+ const grokClientCompatibilityEnabled = logCtx.surface === "grok";
419
+ const snapshotRepairEnabled = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair);
420
+ const githubCopilotRepairEnabled = route.providerName === "github-copilot";
421
+ const responseModelRewrite = parsed._responseModelId !== undefined
422
+ && parsed._responseModelId !== parsed.modelId
423
+ ? createResponsesModelPayloadRewrite(parsed._responseModelId)
424
+ : undefined;
425
+ // Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
426
+ const payloadRewrites = [
427
+ createImageGenCallRestoreRewrite(imageGenCallAliases),
428
+ // #3217: a call whose namespace repeats its own name is unroutable in codex-rs.
429
+ createSelfNamedToolCallNamespaceScrubRewrite(selfNamedNamespaceScrubAuthorization),
430
+ responseEffects.routedMuseToolNameAliases.size > 0
431
+ ? createMuseToolNameRestoreRewrite(responseEffects.routedMuseToolNameAliases)
432
+ : undefined,
433
+ responseEffects.routedNamespaceToolAliases.size > 0
434
+ ? createRoutedNamespaceCallRestoreRewrite(responseEffects.routedNamespaceToolAliases)
435
+ : undefined,
436
+ authorizedBareNamespaceToolAliases.size > 0
437
+ ? createRoutedNamespaceCallRestoreRewrite(authorizedBareNamespaceToolAliases)
438
+ : undefined,
439
+ hasResponsesItemIdRepair(repairConfig)
440
+ ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
441
+ : undefined,
442
+ responseModelRewrite,
443
+ ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
444
+ // #893: sparse-snapshot gateways get field backfills AND lifecycle event
445
+ // injection at the block level, after payload rewrites. Defaults come
446
+ // from the finalized OUTBOUND body — the normalized internal tool shapes
447
+ // are not the Responses wire shapes the snapshot must mirror.
448
+ // Only validated client blocks may publish plaintext continuation state.
449
+ // Raw inspection precedes rewriting on eager relays, so it cannot own this write.
450
+ const plaintextInspector = responseEffects.plaintextV2AgentMessageToolNames.size > 0
451
+ ? createSseInspector({ onCompletedResponse: rememberPassthroughResponseChecked })
452
+ : undefined;
453
+ const plaintextEncoder = plaintextInspector ? new TextEncoder() : undefined;
454
+ const rememberPlaintextBlock = plaintextInspector
455
+ ? Object.assign((block: string): readonly string[] => {
456
+ plaintextInspector.feed(plaintextEncoder!.encode(`${block}\n\n`));
457
+ return [block];
458
+ }, { dispose: () => plaintextInspector.dispose() })
459
+ : undefined;
460
+ const blockRewrites = [
461
+ payloadRewrites.length > 0
462
+ ? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
463
+ : undefined,
464
+ routedCustomToolNames.size > 0 || routedCustomToolRepairNames.size > 0
465
+ ? createRoutedCustomToolRestoreBlockRewrite(
466
+ routedCustomToolNames,
467
+ translatorBudget,
468
+ routedCustomToolRepairNames,
469
+ declaredWireToolNames,
470
+ )
471
+ : undefined,
472
+ routedToolSearchNames.size > 0
473
+ ? createRoutedToolSearchRestoreBlockRewrite(routedToolSearchNames, translatorBudget)
474
+ : undefined,
475
+ githubCopilotRepairEnabled
476
+ ? createGithubCopilotResponsesBlockRewrite(translatorBudget)
477
+ : undefined,
478
+ grokClientCompatibilityEnabled
479
+ ? createGrokResponsesControlFrameBlockRewrite()
480
+ : undefined,
481
+ grokClientCompatibilityEnabled
482
+ ? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
483
+ : undefined,
484
+ snapshotRepairEnabled
485
+ ? createResponsesSnapshotBlockRewrite(nativeExchange.outboundRequestBody, translatorBudget)
486
+ : undefined,
487
+ responseEffects.plaintextV2AgentMessageToolNames.size > 0
488
+ ? payloadRewriteAsBlockRewrite(createPlaintextV2AgentMessageCallRestoreRewrite(
489
+ responseEffects.plaintextV2AgentMessageToolNames, responseEffects.plaintextV2AgentMessageAliasedToolNames,
490
+ ))
491
+ : undefined,
492
+ createResponsesFieldBackfillBlockRewrite(),
493
+ functionRepairSchemas.size > 0
494
+ ? createResponsesFunctionToolRepairBlockRewrite(functionRepairSchemas, translatorBudget)
495
+ : undefined,
496
+ // Last: every rewrite above can still rename or reshape a call item, so the guard must
497
+ // compare the names the client will actually receive against the declared catalog.
498
+ nativeExchange.undeclaredToolGuardActive
499
+ ? createUndeclaredToolCallGuardBlockRewrite(
500
+ declaredWireToolNames,
501
+ declaredNamelessClientCallTypes,
502
+ providerExecutedCallTypes,
503
+ declaredBareWireToolNames,
504
+ )
505
+ : undefined,
506
+ rememberPlaintextBlock,
507
+ ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
508
+ const clientBlockRewrite = blockRewrites.length > 0
509
+ ? composeSseBlockRewrites(...blockRewrites)
510
+ : undefined;
511
+ const needsClientRewrite = clientBlockRewrite !== undefined;
512
+ // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
513
+ // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
514
+ // lost). The eager single reader applies the same rewrites inline.
515
+ const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
516
+ const eagerPath = selectEagerPath(
517
+ process.platform,
518
+ needsClientRewrite,
519
+ config.streamMode ?? "auto",
520
+ );
521
+ // A successful Codex WS upgrade is a push source. If it entered tee(),
522
+ // the inspection branch could drain continuously while the slow client
523
+ // branch retained bytes without a bound. Force the existing bounded,
524
+ // single-reader relay before tee; HTTP fallback responses stay unmarked.
525
+ const forceCodexWsEagerRelay = isCodexWsUpstreamResponse(upstreamResponse);
526
+ const inlineEagerRewrite = needsClientRewrite
527
+ && (forceCodexWsEagerRelay || win32EagerRewrite || eagerPath?.useEagerRelay === true);
528
+ if (forceCodexWsEagerRelay || eagerPath?.useEagerRelay || win32EagerRewrite) {
529
+ const turnAc = new AbortController();
530
+ linkAbortSignal(upstream, turnAc.signal);
531
+ registerTurn(turnAc, options.turnAdmissionLease);
532
+ const reportNativeTerminal = recordTerminalOutcomes
533
+ ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
534
+ terminalRecorder?.(status, httpStatusOverride);
535
+ if (status === "failed" || status === "incomplete") {
536
+ const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
537
+ .find(value => value === 429 || value === 402);
538
+ if (!isFixedCodexAccount(admissionState.authCtx) && quotaFailureMessage !== undefined) {
539
+ recordSubagentQuotaFailureForThreadSpawn(
540
+ req.headers,
541
+ subagentQuotaFailureModel,
542
+ quotaFailureMessage,
543
+ config,
544
+ requestState.subagentFallbackAccountId,
545
+ );
546
+ }
547
+ }
548
+ options.onNativePassthroughTerminal?.(status);
549
+ }
550
+ : undefined;
551
+ const inspector = createSseInspector({
552
+ onTerminal: reportNativeTerminal,
553
+ logCtx,
554
+ onCompletedResponse: rememberPassthroughResponse && responseEffects.plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
555
+ onParsedPayload: noteInspectedPayload,
556
+ onFirstOutput: options.onFirstOutput,
557
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
558
+ });
559
+ const eagerBody = relaySseEagerBounded(passthroughSseBody, turnAc, {
560
+ inspectChunk: chunk => inspector.feed(chunk),
561
+ finishInspection: () => inspector.finish(),
562
+ disposeInspection: () => inspector.dispose(),
563
+ // Stream lifetime follows the protocol terminal even when this request
564
+ // has no outcome callback configured (reported() would stay false).
565
+ sawTerminal: () => inspector.terminalSeen(),
566
+ ...(clientBlockRewrite
567
+ ? { rewriteBlocks: clientBlockRewrite }
568
+ : {}),
569
+ onSynthetic: (kind, reason) => {
570
+ if (!reportNativeTerminal) return;
571
+ if (kind === "incomplete") {
572
+ logCtx.terminalSource = "synthetic";
573
+ reportNativeTerminal("incomplete");
574
+ } else if (reason === "upstream_error") {
575
+ logCtx.terminalSource = "synthetic";
576
+ reportNativeTerminal("failed", logCtx.terminalHttpStatus ?? 502);
577
+ } else {
578
+ logCtx.transportPhase = "mid_stream";
579
+ logCtx.terminalSource = "synthetic";
580
+ if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
581
+ reportNativeTerminal("failed", 502);
582
+ }
583
+ },
584
+ onClientCancel: () => {
585
+ responseEffects.responseCompletionCancelled = true;
586
+ options.onNativePassthroughCancel?.();
587
+ },
588
+ onDone: () => unregisterTurn(turnAc),
589
+ }, {
590
+ clientGoneSignal: options.abortSignal,
591
+ terminalBoundary: codexSafetyBufferingOptions,
592
+ ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
593
+ ...(logCtx.upstreamError === undefined ? {} : { upstreamError: logCtx.upstreamError }),
594
+ });
595
+ // When selected, this relay closes response.completed even if upstream
596
+ // keeps the connection alive. Marked Codex WS traffic, Windows
597
+ // forced-rewrite traffic, and Darwin explicit eager traffic apply
598
+ // client rewrites inline rather than via the tee()+JS-pull chain.
599
+ if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
600
+ return markEagerRelaySseResponse(
601
+ markNativePassthroughSseResponse(new Response(eagerBody, {
602
+ status: upstreamResponse.status,
603
+ headers,
604
+ })),
605
+ );
606
+ }
607
+ const turnAc = new AbortController();
608
+ const clientGone = new AbortController();
609
+ const clientGoneSignal = options.abortSignal
610
+ ? AbortSignal.any([clientGone.signal, options.abortSignal])
611
+ : clientGone.signal;
612
+ // Pace against raw bytes before rewrites, without detaching terminal ownership.
613
+ const [nativeBody, inspectBody] = teeWithBoundedInspection(passthroughSseBody, { clientGoneSignal });
614
+ linkAbortSignal(upstream, turnAc.signal);
615
+ registerTurn(turnAc, options.turnAdmissionLease);
616
+ const inspectionConsumerOptions = {
617
+ // Request abort can reject the fetch body before the response cancel hook runs.
618
+ clientGoneSignal,
619
+ drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
620
+ upstream,
621
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
622
+ onParsedPayload: noteInspectedPayload,
623
+ };
624
+ if (recordTerminalOutcomes) {
625
+ // A real terminal was parsed from the (teed) inspection stream — record it as the outcome
626
+ // even if the client has already disconnected: the turn genuinely reached that terminal, so
627
+ // it must log as completed/failed, not be dropped or downgraded to a cancel (#44). A pure
628
+ // client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
629
+ const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
630
+ terminalRecorder?.(status, httpStatusOverride);
631
+ if (status === "failed" || status === "incomplete") {
632
+ const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
633
+ .find(value => value === 429 || value === 402);
634
+ if (!isFixedCodexAccount(admissionState.authCtx) && quotaFailureMessage !== undefined) {
635
+ recordSubagentQuotaFailureForThreadSpawn(
636
+ req.headers,
637
+ subagentQuotaFailureModel,
638
+ quotaFailureMessage,
639
+ config,
640
+ requestState.subagentFallbackAccountId,
641
+ );
642
+ }
643
+ }
644
+ options.onNativePassthroughTerminal?.(status);
645
+ };
646
+ consumeForInspection(
647
+ inspectBody,
648
+ reportNativeTerminal,
649
+ turnAc.signal,
650
+ () => unregisterTurn(turnAc),
651
+ logCtx,
652
+ () => {
653
+ responseEffects.responseCompletionCancelled = true;
654
+ options.onNativePassthroughCancel?.();
655
+ },
656
+ rememberPassthroughResponse && responseEffects.plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
657
+ options.onFirstOutput,
658
+ inspectionConsumerOptions,
659
+ );
660
+ } else {
661
+ consumeForResponseLogMetadata(
662
+ inspectBody,
663
+ logCtx,
664
+ turnAc.signal,
665
+ () => unregisterTurn(turnAc),
666
+ rememberPassthroughResponse && responseEffects.plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
667
+ options.onFirstOutput,
668
+ inspectionConsumerOptions,
669
+ );
670
+ }
671
+ if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
672
+ // Windows was handled by the eager terminal-aware branch above. Remaining
673
+ // tee traffic can use the JS relay to close on a protocol terminal and to
674
+ // convert a mid-stream reset into a clean response.failed event.
675
+ const rewrittenBody = clientBlockRewrite !== undefined
676
+ ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget)
677
+ : nativeBody;
678
+ const clientBody = relaySseWithFailedTail(
679
+ rewrittenBody,
680
+ upstream,
681
+ reason => {
682
+ responseEffects.responseCompletionCancelled = true;
683
+ clientGone.abort(reason);
684
+ },
685
+ { upstreamError: logCtx.upstreamError, terminalBoundary: codexSafetyBufferingOptions },
686
+ );
687
+ return markNativePassthroughSseResponse(new Response(clientBody, {
688
+ status: upstreamResponse.status,
689
+ headers,
690
+ }));
691
+ }
692
+ if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
693
+ // Bounded whole-body read: a non-streaming upstream JSON body is fully materialized
694
+ // here (and again by the request-log finalizer and the WebSocket bridge's reframing),
695
+ // so an unbounded .text() would let a hostile or stuck upstream grow proxy memory
696
+ // without limit. This path is no longer rare — WebSocket turns for models whose
697
+ // streaming terminal event is unreliable are deliberately answered with bounded JSON.
698
+ // Oversize and stall deadlines both fail closed; a partial body is never parsed.
699
+ const bounded = await readBoundedResponseBody(upstreamResponse, UPSTREAM_JSON_BODY_READ_OPTIONS);
700
+ if (bounded.oversized) {
701
+ return formatErrorResponse(502, "upstream_error", "upstream JSON response exceeded the safe body limit");
702
+ }
703
+ if (bounded.truncated) {
704
+ return formatErrorResponse(502, "upstream_error", "upstream JSON response stalled before completing");
705
+ }
706
+ const text = bounded.text;
707
+ inspectResponseLogJson(logCtx, text);
708
+ let plaintextV2RestoreFailed = false;
709
+ let clientJson = (() => {
710
+ const restoredNamespace = restoreRoutedNamespaceCallsInJson(
711
+ scrubSelfNamedToolCallNamespaceInJson(
712
+ restoreMuseToolNamesInJson(
713
+ restoreImageGenCallsInJson(text, imageGenCallAliases),
714
+ responseEffects.routedMuseToolNameAliases,
715
+ ),
716
+ selfNamedNamespaceScrubAuthorization,
717
+ ),
718
+ responseEffects.routedNamespaceToolAliases,
719
+ );
720
+ const restoredAuthorizedBareNamespace = restoreRoutedNamespaceCallsInJson(
721
+ restoredNamespace,
722
+ authorizedBareNamespaceToolAliases,
723
+ );
724
+ const restored = restoreRoutedCustomCallsInJson(
725
+ restoredAuthorizedBareNamespace,
726
+ routedCustomToolNames,
727
+ routedCustomToolRepairNames,
728
+ declaredWireToolNames,
729
+ );
730
+ const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
731
+ restored,
732
+ routedToolSearchNames,
733
+ );
734
+ const normalizedJson = normalizeFunctionCompletionJson(restoredToolSearch);
735
+ const plaintextRestore = restorePlaintextV2AgentMessageCallsInJsonResult(
736
+ normalizedJson, responseEffects.plaintextV2AgentMessageToolNames, responseEffects.plaintextV2AgentMessageAliasedToolNames,
737
+ );
738
+ plaintextV2RestoreFailed = plaintextRestore.overflowed;
739
+ const repaired = plaintextRestore.value;
740
+ const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
741
+ ? rewriteResponsesModelJson(repaired, parsed._responseModelId)
742
+ : repaired;
743
+ return modelRewritten;
744
+ })();
745
+ if (plaintextV2RestoreFailed) {
746
+ return formatErrorResponse(502, "upstream_error", PLAINTEXT_V2_AGENT_MESSAGE_RESTORE_OVERFLOW_MESSAGE);
747
+ }
748
+ // #1700: same fail-closed policy as the SSE relay above. Both the plain JSON answer and
749
+ // the reframed-SSE branch below are built from this body, so one check covers them. This
750
+ // runs BEFORE the continuation cache write below: a refused turn must not become state a
751
+ // later `previous_response_id` replay can expand from.
752
+ if (nativeExchange.undeclaredToolGuardActive) {
753
+ const undeclared = (() => {
754
+ try {
755
+ return undeclaredToolCallNameInResponse(
756
+ JSON.parse(clientJson),
757
+ declaredWireToolNames,
758
+ declaredNamelessClientCallTypes,
759
+ providerExecutedCallTypes,
760
+ declaredBareWireToolNames,
761
+ );
762
+ } catch {
763
+ return undefined;
764
+ }
765
+ })();
766
+ if (undeclared !== undefined) {
767
+ return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared));
768
+ }
769
+ clientJson = normalizeDefaultNamespaceInJson(
770
+ clientJson,
771
+ declaredWireToolNames,
772
+ declaredBareWireToolNames,
773
+ );
774
+ }
775
+ commitReasoningReplayServingRoute(nativeExchange.request.headers);
776
+ try {
777
+ rememberPassthroughResponseChecked(
778
+ JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown; model?: unknown },
779
+ );
780
+ } catch { /* non-JSON despite content-type; recording is best-effort */ }
781
+ // #875: the transport-neutral reliability policy forced a bounded JSON
782
+ // upstream for a client that asked for SSE. Reframe the completed JSON
783
+ // as the canonical terminal SSE sequence (created → output_item.done →
784
+ // terminal → [DONE]) so Codex commits the turn instead of hanging on a
785
+ // stream that never closes. Non-streaming clients keep the plain JSON.
786
+ if (clientRequestedStream === true
787
+ && options.inboundTransport !== "websocket"
788
+ && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
789
+ && route.provider.adapter === "openai-responses") {
790
+ let completed: Record<string, unknown> | undefined;
791
+ try {
792
+ const parsedCompleted = JSON.parse(clientJson) as unknown;
793
+ if (!parsedCompleted || typeof parsedCompleted !== "object" || Array.isArray(parsedCompleted)) {
794
+ throw new TypeError("bounded Responses JSON is not an object");
795
+ }
796
+ let candidate = parsedCompleted as Record<string, unknown>;
797
+ // The bounded-JSON answer bypasses the SSE relay, so it also bypasses
798
+ // the SSE item-id rewrite. Apply the same client-facing normalization
799
+ // here or this policy would silently disable id repair for the very
800
+ // providers that need it (raw record already happened above).
801
+ if (hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)) {
802
+ candidate = repairResponsesJsonItemIds(candidate, route.provider.responsesItemIdRepair!, translatorBudget);
803
+ }
804
+ completed = candidate;
805
+ } catch {
806
+ // Non-JSON despite content-type: fall through to the plain relay.
807
+ }
808
+ if (completed) {
809
+ let stream: ReadableStream<Uint8Array>;
810
+ try {
811
+ stream = responsesJsonToSseStream(completed);
812
+ } catch (error) {
813
+ if (error instanceof RangeError) {
814
+ return formatErrorResponse(
815
+ 502,
816
+ "upstream_error",
817
+ "upstream JSON response exceeded the synthesized SSE item limit",
818
+ );
819
+ }
820
+ throw error;
821
+ }
822
+ const sseHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions);
823
+ sseHeaders.set("content-type", "text/event-stream");
824
+ sseHeaders.set("cache-control", "no-store");
825
+ return new Response(stream, {
826
+ status: upstreamResponse.status,
827
+ statusText: upstreamResponse.statusText,
828
+ headers: sseHeaders,
829
+ });
830
+ }
831
+ }
832
+ // WS turns reframe this JSON into events in the bridge, which is the
833
+ // other relay-free path — normalize ids so both bounded-JSON paths agree.
834
+ const outboundJson = options.inboundTransport === "websocket"
835
+ && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
836
+ && hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
837
+ ? (() => {
838
+ try {
839
+ return JSON.stringify(repairResponsesJsonItemIds(
840
+ JSON.parse(clientJson) as Record<string, unknown>,
841
+ route.provider.responsesItemIdRepair!,
842
+ translatorBudget,
843
+ ));
844
+ } catch {
845
+ return clientJson;
846
+ }
847
+ })()
848
+ : clientJson;
849
+ return new Response(outboundJson, {
850
+ status: upstreamResponse.status,
851
+ statusText: upstreamResponse.statusText,
852
+ headers,
853
+ });
854
+ }
855
+ if (responseEffects.plaintextV2AgentMessageToolNames.size > 0) {
856
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
857
+ return formatErrorResponse(502, "upstream_error", "plaintext V2 agent-message response used an unsupported content type");
858
+ }
859
+ // An unclassified passthrough body is relayed directly and has no bounded completion observer;
860
+ // use the same non-error-status success boundary as SSE instead of retaining per-stream state.
861
+ commitReasoningReplayServingRoute(nativeExchange.request.headers);
862
+ const body = relayWithAbort(upstreamResponse.body, upstream);
863
+ const turnAc = new AbortController();
864
+ const tracked = body ? trackStreamLifetime(body, turnAc, undefined, options.turnAdmissionLease) : null;
865
+ return new Response(tracked, {
866
+ status: upstreamResponse.status,
867
+ headers,
868
+ });
869
+ }