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