@bitkyc08/opencodex 2.72.0 → 2.73.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 (79) hide show
  1. package/gui/dist/assets/{App-Cf-WYstE.js → App-mHGa7dE1.js} +15 -15
  2. package/gui/dist/assets/{Tray-DkM0mULy.js → Tray-BxY_RYZg.js} +1 -1
  3. package/gui/dist/assets/{index-PWrwOfio.js → index-ur_BPgK1.js} +12 -12
  4. package/gui/dist/assets/{quota-summary-D-FKtrgr.js → quota-summary-D1Vq7N9H.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/native-exec.ts +28 -10
  8. package/src/adapters/devin/live-models.ts +5 -0
  9. package/src/adapters/kiro/reasoning.ts +2 -0
  10. package/src/adapters/openai-chat/messages.ts +4 -1
  11. package/src/adapters/openai-responses/passthrough.ts +12 -1
  12. package/src/adapters/openai-responses/reasoning.ts +15 -0
  13. package/src/bridge/internal.ts +12 -0
  14. package/src/cli/account-anthropic-threshold.ts +41 -0
  15. package/src/cli/account-api.ts +2 -0
  16. package/src/cli/account-extended.ts +32 -21
  17. package/src/cli/account.ts +1 -0
  18. package/src/cli/capabilities.ts +8 -4
  19. package/src/cli/index.ts +1 -1
  20. package/src/codex/catalog/build-entries.ts +6 -5
  21. package/src/codex/catalog/derive-entry.ts +3 -4
  22. package/src/codex/catalog/gather-capture.ts +2 -0
  23. package/src/codex/catalog/metadata.ts +6 -0
  24. package/src/codex/catalog/native-models.ts +17 -3
  25. package/src/codex/catalog/parsing.ts +4 -1
  26. package/src/codex/catalog/pinned-models.ts +3 -1
  27. package/src/codex/catalog/provider-models.ts +22 -2
  28. package/src/codex/data/roster-pinned-models.json +178 -1
  29. package/src/codex/model-entitlements.ts +1 -1
  30. package/src/codex/native-profile-store.ts +2 -1
  31. package/src/combos/resolve.ts +16 -1
  32. package/src/config/subagent-models.ts +30 -5
  33. package/src/generated/compatibility-version.json +95 -67
  34. package/src/generated/model-metadata.ts +4 -4
  35. package/src/lib/account-selection-events.ts +28 -0
  36. package/src/lib/errors.ts +7 -5
  37. package/src/lib/keyring-native.ts +132 -0
  38. package/src/lib/provider-outbound.ts +13 -0
  39. package/src/lib/retry-delay.ts +10 -0
  40. package/src/oauth/anthropic-account-threshold.ts +13 -0
  41. package/src/oauth/anthropic-routing.ts +223 -52
  42. package/src/oauth/index.ts +4 -2
  43. package/src/oauth/store.ts +45 -3
  44. package/src/oauth/types.ts +2 -0
  45. package/src/providers/api-key-resolve.ts +2 -4
  46. package/src/providers/codebuddy-models.ts +6 -0
  47. package/src/providers/kiro-models.ts +4 -0
  48. package/src/providers/label.ts +20 -1
  49. package/src/providers/quota/account-cache.ts +2 -1
  50. package/src/providers/quota/anthropic-cooldown-recovery.ts +118 -0
  51. package/src/providers/quota/vendor-probes-oauth.ts +84 -57
  52. package/src/providers/quota.ts +45 -46
  53. package/src/providers/registry/entries-core.ts +3 -1
  54. package/src/providers/registry/entries-extended.ts +14 -1
  55. package/src/providers/registry/model-seeds.ts +7 -1
  56. package/src/server/claude-messages.ts +84 -27
  57. package/src/server/management/anthropic-account-threshold.ts +27 -0
  58. package/src/server/management/oauth-account-routes.ts +17 -4
  59. package/src/server/management/route-registry.ts +1 -0
  60. package/src/server/messages-native-oauth.ts +20 -21
  61. package/src/server/messages-native.ts +39 -14
  62. package/src/server/relay-eager.ts +5 -2
  63. package/src/server/relay.ts +268 -82
  64. package/src/server/responses/adapter-continuation.ts +6 -0
  65. package/src/server/responses/adapter-delivery.ts +25 -7
  66. package/src/server/responses/adapter-dispatch.ts +31 -0
  67. package/src/server/responses/buffered-sse-json.ts +345 -0
  68. package/src/server/responses/core-codex-account.ts +4 -1
  69. package/src/server/responses/core-combo.ts +28 -0
  70. package/src/server/responses/core-lifetime.ts +4 -0
  71. package/src/server/responses/passthrough-delivery.ts +305 -10
  72. package/src/server/responses/passthrough-dispatch.ts +60 -25
  73. package/src/server/responses/passthrough-error.ts +3 -1
  74. package/src/server/responses/request-transport.ts +22 -5
  75. package/src/server/responses/terminal-error-redaction.ts +53 -0
  76. package/src/service/windows-taskxml.ts +20 -6
  77. package/src/service-manager-probe.ts +115 -6
  78. package/src/types/wire.ts +7 -0
  79. package/src/usage/expected-prices.ts +11 -0
@@ -1,4 +1,5 @@
1
1
  import { isNativeControlResponse } from "./native-response-control";
2
+ import { createOutboundCredentialMask, createTerminalErrorRedactionBlockRewrite } from "./terminal-error-redaction";
2
3
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
3
4
  import type { PreparedResponsesRequest } from "./request-prepare";
4
5
  import type { ResponsesTransport } from "./request-transport";
@@ -10,6 +11,7 @@ import {
10
11
  createSseInspector,
11
12
  markEagerRelaySseResponse,
12
13
  markNativePassthroughSseResponse,
14
+ markPreinspectedJsonResponse,
13
15
  consumeForInspection,
14
16
  consumeForResponseLogMetadata,
15
17
  relaySseWithFailedTail,
@@ -32,7 +34,7 @@ import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-m
32
34
  import { recordCodexUpstreamOutcome } from "../../codex/routing";
33
35
  import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant, releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
34
36
  import { consumeComboFailure } from "./core-combo-failure";
35
- import { readDisplaySafeErrorText } from "./core-errors";
37
+ import { clientCancelledResponse, readDisplaySafeErrorText } from "./core-errors";
36
38
  import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
37
39
  import { formatPassthroughUpstreamError } from "./passthrough-error";
38
40
  import { rewriteUpstreamPolicyRefusal } from "./policy-refusal";
@@ -131,13 +133,94 @@ import { readBoundedResponseBody } from "../../lib/bounded-body";
131
133
  import { idleDeadline } from "../../lib/abort";
132
134
  import { resolveStallTimeoutMs } from "../../stall-timeout";
133
135
  import { formatErrorResponse } from "../../bridge";
134
- import { inspectResponseLogJson } from "../request-log";
136
+ import {
137
+ httpStatusFromTerminalError,
138
+ inspectResponseLogJson,
139
+ inspectResponseLogSsePayloadParsed,
140
+ } from "../request-log";
135
141
  import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
136
142
  import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
137
143
  import { responsesJsonToSseStream } from "../responses-json-events";
144
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
145
+ import {
146
+ bufferedResponsesReadOptions,
147
+ collectBufferedResponsesSse,
148
+ type BufferedResponsesSseFailure,
149
+ type BufferedResponsesSseResult,
150
+ } from "./buffered-sse-json";
138
151
 
139
152
  const PLAINTEXT_V2_SSE_PREFIX_LIMIT = 4096;
140
153
 
154
+ /** Replay retained SSE in small views so the rewrite decoder never receives one 32 MiB chunk. */
155
+ function replayBufferedSse(bytes: Uint8Array): ReadableStream<Uint8Array> {
156
+ let offset = 0;
157
+ return new ReadableStream({
158
+ pull(controller) {
159
+ if (offset >= bytes.byteLength) {
160
+ controller.close();
161
+ return;
162
+ }
163
+ const end = Math.min(bytes.byteLength, offset + 64 * 1024);
164
+ controller.enqueue(bytes.subarray(offset, end));
165
+ offset = end;
166
+ },
167
+ });
168
+ }
169
+
170
+ const BUFFERED_CONTEXT_ERROR_CODES = new Set([
171
+ "context_length_exceeded", "context_window_exceeded", "input_too_long",
172
+ ]);
173
+ const BUFFERED_QUOTA_ERROR_CODES = new Set([
174
+ "usage_limit_exceeded", "usage_limit_reached", "rate_limit_exceeded", "insufficient_quota",
175
+ ]);
176
+ const BUFFERED_OVERLOAD_ERROR_CODES = new Set(["server_is_overloaded"]);
177
+
178
+ function bufferedErrorDefaults(status: number): { type: string; code: string } {
179
+ switch (status) {
180
+ case 400: return { type: "invalid_request_error", code: "invalid_request_error" };
181
+ case 401: return { type: "authentication_error", code: "invalid_api_key" };
182
+ case 403: return { type: "permission_error", code: "permission_denied" };
183
+ case 429: return { type: "rate_limit_error", code: "rate_limit_exceeded" };
184
+ case 499: return { type: "client_closed_request", code: "client_closed_request" };
185
+ case 503: return { type: "server_error", code: "server_is_overloaded" };
186
+ default: return { type: "upstream_error", code: "upstream_server_error" };
187
+ }
188
+ }
189
+
190
+ /** Preserve only explicit, recognized bare-error classes; diagnostic wording never sets status. */
191
+ function bufferedBareErrorPresentation(
192
+ failure: BufferedResponsesSseFailure,
193
+ message: string,
194
+ ): { status: number; error: { type: string; code: string; message: string } } {
195
+ if (failure.upstreamRefusalCode !== undefined) {
196
+ return {
197
+ status: 400,
198
+ error: { type: "invalid_request_error", code: failure.upstreamRefusalCode, message },
199
+ };
200
+ }
201
+ const explicitCode = failure.upstreamErrorCode;
202
+ const status = explicitCode !== undefined
203
+ ? BUFFERED_CONTEXT_ERROR_CODES.has(explicitCode) ? 400
204
+ : BUFFERED_QUOTA_ERROR_CODES.has(explicitCode) ? 429
205
+ : BUFFERED_OVERLOAD_ERROR_CODES.has(explicitCode) ? 503
206
+ : httpStatusFromTerminalError({ code: explicitCode })
207
+ : httpStatusFromTerminalError({ type: failure.upstreamErrorType });
208
+ const defaults = bufferedErrorDefaults(status);
209
+ if (status === 502) return { status, error: { ...defaults, message } };
210
+ const explicitType = failure.upstreamErrorType;
211
+ const typeMatchesStatus = explicitType !== undefined
212
+ && (httpStatusFromTerminalError({ type: explicitType }) === status
213
+ || (explicitType === "server_error" && status === 503));
214
+ return {
215
+ status,
216
+ error: {
217
+ type: typeMatchesStatus ? explicitType! : defaults.type,
218
+ code: explicitCode ?? defaults.code,
219
+ message,
220
+ },
221
+ };
222
+ }
223
+
141
224
  /** Prefix-probe budget: bounds one silent gap and the whole probe alike. */
142
225
  interface PlaintextV2SseProbeOptions {
143
226
  timeoutMs: number;
@@ -348,10 +431,12 @@ export async function deliverPassthroughResponse(
348
431
  const { requestBindings } = transportState;
349
432
 
350
433
  let upstreamResponse = nativeExchange.upstreamResponse;
434
+ const canonicalBufferedJson = clientRequestedStream !== true
435
+ && isCanonicalOpenAiForwardProvider(route.provider);
351
436
  const originalContentType = upstreamResponse.headers.get("content-type");
352
437
  if (isUsageDebugEnabled() && originalContentType) logCtx.usageDebugContentType = originalContentType;
353
438
  if (responseEffects.plaintextV2AgentMessageToolNames.size > 0
354
- && upstreamResponse.ok && upstreamResponse.body && parsed.stream
439
+ && upstreamResponse.ok && upstreamResponse.body && (parsed.stream || canonicalBufferedJson)
355
440
  && !originalContentType?.toLowerCase().includes("text/event-stream")
356
441
  && !originalContentType?.toLowerCase().includes("application/json")
357
442
  && !isCodexWsUpstreamResponse(upstreamResponse)
@@ -372,7 +457,11 @@ export async function deliverPassthroughResponse(
372
457
  // reach this fallback only after their first Responses event is confirmed.
373
458
  const passthroughCt = headers.get("content-type")?.toLowerCase();
374
459
  const isEventStream = passthroughCt?.includes("text/event-stream")
375
- || (responseEffects.plaintextV2AgentMessageToolNames.size === 0 && upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
460
+ || (responseEffects.plaintextV2AgentMessageToolNames.size === 0
461
+ && upstreamResponse.ok
462
+ && !!upstreamResponse.body
463
+ && !passthroughCt
464
+ && (parsed.stream || canonicalBufferedJson));
376
465
  const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
377
466
  config,
378
467
  admissionState.authCtx,
@@ -522,10 +611,13 @@ export async function deliverPassthroughResponse(
522
611
  const grokUpstreamEchoEnabled = isXaiResponsesDestination(route.provider)
523
612
  && responsesRequestMayReplayToolOutput(parsed._rawBody);
524
613
  if (isEventStream && upstreamResponse.body) {
525
- // For streamed passthrough, a successful terminal response means non-error upstream status
526
- // before relay starts. Waiting for SSE completion would retain request state across the whole
527
- // stream; a later body failure does not undo that this destination accepted and served the turn.
528
- commitReasoningReplayServingRoute(nativeExchange.request.headers);
614
+ // Streaming clients commit the serving route once the upstream accepted the turn; retaining
615
+ // request state until their body ends would change the existing relay contract. The canonical
616
+ // non-streaming fold below is different: nothing is published yet, so it defers this mutation
617
+ // until both the raw and rewritten terminal snapshots validate.
618
+ if (!canonicalBufferedJson) {
619
+ commitReasoningReplayServingRoute(nativeExchange.request.headers);
620
+ }
529
621
  const terminalRepairPolicy = route.staticPolicy.model.responsesTerminalRepair;
530
622
  // #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
531
623
  // and this branch relays that declaration on the assumption the destination executes it.
@@ -665,7 +757,9 @@ export async function deliverPassthroughResponse(
665
757
  // are not the Responses wire shapes the snapshot must mirror.
666
758
  // Only validated client blocks may publish plaintext continuation state.
667
759
  // Raw inspection precedes rewriting on eager relays, so it cannot own this write.
668
- const plaintextInspector = !grokUpstreamEchoEnabled && responseEffects.plaintextV2AgentMessageToolNames.size > 0
760
+ const plaintextInspector = !canonicalBufferedJson
761
+ && !grokUpstreamEchoEnabled
762
+ && responseEffects.plaintextV2AgentMessageToolNames.size > 0
669
763
  ? createSseInspector({ onCompletedResponse: rememberPassthroughResponseChecked })
670
764
  : undefined;
671
765
  const plaintextEncoder = plaintextInspector ? new TextEncoder() : undefined;
@@ -675,6 +769,7 @@ export async function deliverPassthroughResponse(
675
769
  return [block];
676
770
  }, { dispose: () => plaintextInspector.dispose() })
677
771
  : undefined;
772
+ const maskCredential = createOutboundCredentialMask(nativeExchange.request.headers);
678
773
  const blockRewrites = [
679
774
  payloadRewrites.length > 0
680
775
  ? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
@@ -742,12 +837,211 @@ export async function deliverPassthroughResponse(
742
837
  rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined,
743
838
  )
744
839
  : undefined,
840
+ createTerminalErrorRedactionBlockRewrite(nativeExchange.request.headers, maskCredential),
745
841
  rememberPlaintextBlock,
746
842
  ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
747
843
  const clientBlockRewrite = blockRewrites.length > 0
748
844
  ? composeSseBlockRewrites(...blockRewrites)
749
845
  : undefined;
750
846
  const needsClientRewrite = clientBlockRewrite !== undefined;
847
+
848
+ if (canonicalBufferedJson) {
849
+ const signal = options.abortSignal ?? req.signal;
850
+ const bufferedRead = bufferedResponsesReadOptions(resolveStallTimeoutMs(
851
+ config.stallTimeoutSec,
852
+ { localUpstream: nativeExchange.localUpstream },
853
+ ));
854
+ const failBufferedTurn = (
855
+ message: string,
856
+ failure?: BufferedResponsesSseFailure,
857
+ ): Response => {
858
+ upstream.abort(new Error(message));
859
+ const upstreamError = failure?.upstreamError;
860
+ const upstreamRefusalCode = failure?.upstreamRefusalCode;
861
+ const rawPresentation = upstreamError === undefined || failure === undefined
862
+ ? undefined
863
+ : bufferedBareErrorPresentation(failure, upstreamError);
864
+ const presentation = rawPresentation === undefined ? undefined : {
865
+ status: rawPresentation.status,
866
+ error: {
867
+ type: maskCredential(rawPresentation.error.type),
868
+ code: maskCredential(rawPresentation.error.code),
869
+ message: maskCredential(rawPresentation.error.message),
870
+ },
871
+ };
872
+ if (upstreamError !== undefined) {
873
+ // The streaming relay turns a bare `error` event into a terminal failure. Mirror that
874
+ // verdict for JSON callers before recording/formatting it, or a provider refusal is
875
+ // misreported as a retryable generic 502 and clients can replay a rejected turn.
876
+ const error = presentation!.error;
877
+ const terminalPayload = {
878
+ type: "response.failed",
879
+ response: { status: "failed", error, last_error: error },
880
+ };
881
+ // Classification comes only from the bounded structured type/code above. A bare
882
+ // transport reset can contain misleading copy such as "invalid api key"; letting the
883
+ // log's message fallback reinterpret it would retire a healthy credential.
884
+ logCtx.terminalHttpStatus = presentation!.status;
885
+ noteInspectedPayload(terminalPayload);
886
+ inspectResponseLogSsePayloadParsed(
887
+ logCtx,
888
+ JSON.stringify(terminalPayload),
889
+ terminalPayload,
890
+ );
891
+ }
892
+ const failureStatus = logCtx.terminalHttpStatus ?? 502;
893
+ if (recordTerminalOutcomes) {
894
+ logCtx.transportPhase = "mid_stream";
895
+ logCtx.terminalSource = "synthetic";
896
+ if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
897
+ terminalRecorder?.("failed", failureStatus);
898
+ options.onNativePassthroughTerminal?.("failed");
899
+ }
900
+ if (upstreamError !== undefined) {
901
+ const failureHeaders = new Headers(headers);
902
+ failureHeaders.set("content-type", "application/json");
903
+ failureHeaders.delete("content-length");
904
+ failureHeaders.delete("content-encoding");
905
+ if (upstreamRefusalCode !== undefined) failureHeaders.delete("retry-after");
906
+ const error = presentation!.error;
907
+ return formatPassthroughUpstreamError(
908
+ failureStatus,
909
+ JSON.stringify({ error, ...(upstreamRefusalCode === undefined ? {} : { retryable: false }) }),
910
+ {
911
+ headers: failureHeaders,
912
+ suppressRetryAfter: upstreamRefusalCode !== undefined,
913
+ },
914
+ );
915
+ }
916
+ return formatErrorResponse(502, "upstream_error", message);
917
+ };
918
+ let raw: BufferedResponsesSseResult | undefined = await collectBufferedResponsesSse(
919
+ passthroughSseBody,
920
+ upstream,
921
+ { signal, terminalBoundary: codexSafetyBufferingOptions, read: bufferedRead, retainTranscript: true },
922
+ );
923
+ if (!raw.ok) {
924
+ if (raw.kind === "aborted" || signal.aborted) {
925
+ responseEffects.responseCompletionCancelled = true;
926
+ options.onNativePassthroughCancel?.();
927
+ return clientCancelledResponse();
928
+ }
929
+ const message = raw.kind === "oversized"
930
+ ? "upstream SSE response exceeded the safe body limit"
931
+ : raw.kind === "timeout"
932
+ ? "upstream SSE response stalled before completing"
933
+ : raw.kind === "malformed" || raw.kind === "missing_terminal"
934
+ ? "upstream SSE response ended without one valid terminal response"
935
+ : "upstream SSE response failed before a valid terminal response";
936
+ return failBufferedTurn(message, raw);
937
+ }
938
+ if (!raw.bytes) return failBufferedTurn("upstream SSE transcript was not retained for rewriting");
939
+ let rawBytes: Uint8Array | undefined = raw.bytes;
940
+ raw = undefined; // Release the raw reconstruction graph before building the client copy.
941
+
942
+ // Reuse the ordinary client rewrite chain against the bounded transcript. The raw pass
943
+ // above validates before any continuation or outcome side effect; this second validation
944
+ // means a rewrite failure cannot publish a turn the non-streaming caller never received.
945
+ let bufferedClientBody: ReadableStream<Uint8Array> | undefined = clientBlockRewrite
946
+ ? relaySseWithBlockRewrite(
947
+ replayBufferedSse(rawBytes),
948
+ clientBlockRewrite,
949
+ translatorBudget,
950
+ )
951
+ : replayBufferedSse(rawBytes);
952
+ const client = await collectBufferedResponsesSse(bufferedClientBody, upstream, {
953
+ signal,
954
+ read: bufferedRead,
955
+ retainTranscript: false,
956
+ });
957
+ bufferedClientBody = undefined;
958
+ if (!client.ok) {
959
+ if (client.kind === "aborted" || signal.aborted) {
960
+ responseEffects.responseCompletionCancelled = true;
961
+ options.onNativePassthroughCancel?.();
962
+ return clientCancelledResponse();
963
+ }
964
+ const message = client.kind === "oversized"
965
+ ? "rewritten SSE response exceeded the safe body limit"
966
+ : client.kind === "timeout"
967
+ ? "rewritten SSE response stalled before completing"
968
+ : "rewritten SSE response did not contain one valid terminal response";
969
+ return failBufferedTurn(message, client);
970
+ }
971
+ if (Date.now() >= bufferedRead.deadlineAt) {
972
+ return failBufferedTurn("buffered Responses turn exceeded the safe total deadline");
973
+ }
974
+
975
+ const cancelAfterValidation = (): Response => {
976
+ responseEffects.responseCompletionCancelled = true;
977
+ options.onNativePassthroughCancel?.();
978
+ return clientCancelledResponse();
979
+ };
980
+ if (signal.aborted) return cancelAfterValidation();
981
+
982
+ // Effects intentionally run only after both bounded validations. They inspect the same
983
+ // upstream-facing transcript as the streaming relay and fire once; continuation storage
984
+ // receives the final client-visible response once after all restorations succeeded.
985
+ const reportNativeTerminal = recordTerminalOutcomes
986
+ ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
987
+ if (signal.aborted) return;
988
+ terminalRecorder?.(status, httpStatusOverride);
989
+ if (status === "failed" || status === "incomplete") {
990
+ const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
991
+ .find(value => value === 429 || value === 402);
992
+ if (!isFixedCodexAccount(admissionState.authCtx) && quotaFailureMessage !== undefined) {
993
+ recordSubagentQuotaFailureForThreadSpawn(
994
+ req.headers,
995
+ subagentQuotaFailureModel,
996
+ quotaFailureMessage,
997
+ config,
998
+ requestState.subagentFallbackAccountId,
999
+ );
1000
+ }
1001
+ }
1002
+ options.onNativePassthroughTerminal?.(status);
1003
+ }
1004
+ : undefined;
1005
+ const effectInspector = createSseInspector({
1006
+ onTerminal: reportNativeTerminal,
1007
+ logCtx,
1008
+ onParsedPayload: noteInspectedPayload,
1009
+ onFirstOutput: options.onFirstOutput,
1010
+ pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
1011
+ });
1012
+ try {
1013
+ // The deferred effects pass must not turn a bounded 32 MiB transcript into one long
1014
+ // event-loop monopoly. Keep decoder input small and yield between MiB groups; the
1015
+ // aggregate frame cap was already enforced by both validation passes.
1016
+ for (let offset = 0; offset < rawBytes.byteLength; offset += 64 * 1024) {
1017
+ if (signal.aborted) return cancelAfterValidation();
1018
+ effectInspector.feed(rawBytes.subarray(offset, Math.min(rawBytes.byteLength, offset + 64 * 1024)));
1019
+ if (offset > 0 && offset % (1024 * 1024) === 0) {
1020
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
1021
+ if (signal.aborted) return cancelAfterValidation();
1022
+ }
1023
+ }
1024
+ if (signal.aborted) return cancelAfterValidation();
1025
+ effectInspector.finish();
1026
+ } finally {
1027
+ effectInspector.dispose();
1028
+ }
1029
+ if (signal.aborted) return cancelAfterValidation();
1030
+ commitReasoningReplayServingRoute(nativeExchange.request.headers);
1031
+ rawBytes = undefined;
1032
+ if (client.terminal.status === "completed") {
1033
+ rememberPassthroughResponseChecked(client.terminal.response);
1034
+ }
1035
+
1036
+ const jsonHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions);
1037
+ jsonHeaders.set("content-type", "application/json");
1038
+ return markPreinspectedJsonResponse(new Response(JSON.stringify(client.terminal.response), {
1039
+ status: upstreamResponse.status,
1040
+ statusText: upstreamResponse.statusText,
1041
+ headers: jsonHeaders,
1042
+ }));
1043
+ }
1044
+
751
1045
  const relayPlatform = relayPlatformForTests ?? process.platform;
752
1046
  // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
753
1047
  // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
@@ -829,6 +1123,7 @@ export async function deliverPassthroughResponse(
829
1123
  }, {
830
1124
  clientGoneSignal: options.abortSignal,
831
1125
  terminalBoundary: codexSafetyBufferingOptions,
1126
+ maskCredential,
832
1127
  ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
833
1128
  ...(logCtx.upstreamError === undefined ? {} : { upstreamError: logCtx.upstreamError }),
834
1129
  });
@@ -922,7 +1217,7 @@ export async function deliverPassthroughResponse(
922
1217
  responseEffects.responseCompletionCancelled = true;
923
1218
  clientGone.abort(reason);
924
1219
  },
925
- { upstreamError: logCtx.upstreamError, terminalBoundary: codexSafetyBufferingOptions },
1220
+ { upstreamError: logCtx.upstreamError, terminalBoundary: codexSafetyBufferingOptions, maskCredential },
926
1221
  );
927
1222
  return markNativePassthroughSseResponse(new Response(clientBody, {
928
1223
  status: upstreamResponse.status,
@@ -137,7 +137,8 @@ import { resolveWireProtocolOverride } from "../adapter-resolve";
137
137
  import { refreshPoolForwardAuth, refreshNativeMainForwardAuth, withClaudeNativeSession } from "./core-auth";
138
138
  import { bindRouteReasoningReplayScope } from "./core-replay";
139
139
  import type { OAuthAccessSnapshot } from "../../oauth";
140
- import { publicOAuthAuthenticationErrorMessage } from "../../oauth";
140
+ import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
141
+ import { AnthropicAccountCooldownError } from "../../oauth/anthropic-routing";
141
142
  import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
142
143
  import {
143
144
  hasEligibleGenericOAuthFailoverTarget,
@@ -260,6 +261,18 @@ export async function preparePassthroughExchange(
260
261
  const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider)
261
262
  ? codexSafetyBufferingFilterOptions(config)
262
263
  : undefined;
264
+ // The canonical ChatGPT adapter coerces only its final outbound copy to `stream:true` so the
265
+ // separately captured client preference can still select JSON delivery. Recovery runs before
266
+ // delivery, though, and must classify the bytes the upstream was actually asked to send:
267
+ // ChatGPT may omit Content-Type, so consulting `parsed.stream` here would mistake its SSE body
268
+ // for non-streaming JSON and skip both opaque-state and safe-reset recovery.
269
+ const upstreamRequestsStream = parsed.stream === true
270
+ || isCanonicalOpenAiForwardProvider(route.provider);
271
+ // A JSON client still needs the canonical destination's HTTP/SSE response so delivery can
272
+ // validate and fold it. Letting the forced `stream:true` body select the WebSocket transport
273
+ // bypasses manual HTTP redirects and can turn a real 3xx into a connect timeout.
274
+ const canonicalBufferedJson = clientRequestedStream !== true
275
+ && isCanonicalOpenAiForwardProvider(route.provider);
263
276
  const imageGenCallAliases = route.provider.authMode === "forward"
264
277
  ? new Map<string, { namespace: string; name: string }>()
265
278
  : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
@@ -881,7 +894,23 @@ export async function preparePassthroughExchange(
881
894
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
882
895
  return formatErrorResponse(429, "request_send_budget_exhausted", err.message);
883
896
  }
884
- const localRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
897
+ const refusal = unwrapUpstreamRetryEvidenceError(err);
898
+ // Pacing may outlive the selected account's admission. No fetch occurred, so do
899
+ // not turn an operator pause into a 502 or charge it to host/account health.
900
+ if (refusal instanceof OAuthAccountPausedError || refusal instanceof OAuthLoginRequiredError || refusal instanceof AnthropicAccountCooldownError) {
901
+ releaseUpstreamHostAdmission(nativeHostState.lease);
902
+ nativeHostState.lease = null;
903
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
904
+ if (refusal instanceof OAuthLoginRequiredError) {
905
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal));
906
+ }
907
+ if (refusal instanceof AnthropicAccountCooldownError) {
908
+ return formatErrorResponse(429, "rate_limit_error", refusal.message,
909
+ refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
910
+ }
911
+ return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
912
+ }
913
+ const localRefusal = mapCodexAuthContextErrorToResponse(refusal, {
885
914
  now: Date.now(), accountSelector: route.codexAccountNamespace,
886
915
  });
887
916
  if (localRefusal) {
@@ -944,8 +973,9 @@ export async function preparePassthroughExchange(
944
973
  method: request.method,
945
974
  headers: request.headers,
946
975
  body: request.body,
947
- }, recovery), upstream.signal, connectMs, parsed.stream,
976
+ }, recovery), upstream.signal, connectMs, upstreamRequestsStream,
948
977
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
978
+ httpOnly: canonicalBufferedJson,
949
979
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
950
980
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
951
981
  ? options.nativeControl : undefined,
@@ -1048,12 +1078,13 @@ export async function preparePassthroughExchange(
1048
1078
  method: request.method,
1049
1079
  headers: request.headers,
1050
1080
  body: request.body,
1051
- }, innerRecovery), upstream.signal, connectMs, parsed.stream,
1081
+ }, innerRecovery), upstream.signal, connectMs, upstreamRequestsStream,
1052
1082
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1053
- nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1054
- && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1055
- ? options.nativeControl : undefined,
1056
- dispatchOverride: oauthDispatch(request),
1083
+ httpOnly: canonicalBufferedJson,
1084
+ nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1085
+ && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1086
+ ? options.nativeControl : undefined,
1087
+ dispatchOverride: oauthDispatch(request),
1057
1088
  providerName: route.providerName,
1058
1089
  modelId: route.modelId,
1059
1090
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1156,6 +1187,7 @@ export async function preparePassthroughExchange(
1156
1187
  // rather than a second one to announce.
1157
1188
  const oauthReplayExecutor = storedPoolReplayDispatchNotifier(
1158
1189
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1190
+ httpOnly: canonicalBufferedJson,
1159
1191
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1160
1192
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1161
1193
  ? options.nativeControl : undefined,
@@ -1185,7 +1217,7 @@ export async function preparePassthroughExchange(
1185
1217
  }, recovery),
1186
1218
  upstream.signal,
1187
1219
  connectMs,
1188
- parsed.stream,
1220
+ upstreamRequestsStream,
1189
1221
  oauthReplayExecutor,
1190
1222
  route.provider.authMode === "forward",
1191
1223
  ).then(adoptObservedResponse);
@@ -1303,12 +1335,13 @@ export async function preparePassthroughExchange(
1303
1335
  method: request.method,
1304
1336
  headers: request.headers,
1305
1337
  body: request.body,
1306
- }, recovery), upstream.signal, connectMs, parsed.stream,
1338
+ }, recovery), upstream.signal, connectMs, upstreamRequestsStream,
1307
1339
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1308
- nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1309
- && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1310
- ? options.nativeControl : undefined,
1311
- dispatchOverride: oauthDispatch(request),
1340
+ httpOnly: canonicalBufferedJson,
1341
+ nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1342
+ && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1343
+ ? options.nativeControl : undefined,
1344
+ dispatchOverride: oauthDispatch(request),
1312
1345
  providerName: route.providerName,
1313
1346
  modelId: route.modelId,
1314
1347
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1437,12 +1470,13 @@ export async function preparePassthroughExchange(
1437
1470
  method: request.method,
1438
1471
  headers: request.headers,
1439
1472
  body: request.body,
1440
- }, recovery), upstream.signal, connectMs, parsed.stream,
1473
+ }, recovery), upstream.signal, connectMs, upstreamRequestsStream,
1441
1474
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1442
- nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1443
- && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1444
- ? options.nativeControl : undefined,
1445
- dispatchOverride: oauthDispatch(request),
1475
+ httpOnly: canonicalBufferedJson,
1476
+ nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1477
+ && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1478
+ ? options.nativeControl : undefined,
1479
+ dispatchOverride: oauthDispatch(request),
1446
1480
  providerName: route.providerName,
1447
1481
  modelId: route.modelId,
1448
1482
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1554,7 +1588,8 @@ export async function preparePassthroughExchange(
1554
1588
  upstream,
1555
1589
  connectMs,
1556
1590
  passthroughEstimate,
1557
- stream: parsed.stream,
1591
+ stream: upstreamRequestsStream,
1592
+ httpOnly: canonicalBufferedJson,
1558
1593
  onResponse: (response, retryAuthCtx, retryRequest) => {
1559
1594
  adoptCodexWsStage(response);
1560
1595
  captureAffinityResponse(
@@ -1604,7 +1639,7 @@ export async function preparePassthroughExchange(
1604
1639
  const recoveryContentType = upstreamResponse.headers.get("content-type")?.toLowerCase() ?? "";
1605
1640
  const streamedFunctionOutputCandidate = upstreamResponse.ok
1606
1641
  && !!upstreamResponse.body
1607
- && (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && parsed.stream))
1642
+ && (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && upstreamRequestsStream))
1608
1643
  && !opaqueBlobRecoveryGuard.attempted
1609
1644
  && !configuredTransientSendBudgetExhausted()
1610
1645
  && outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body);
@@ -1622,7 +1657,7 @@ export async function preparePassthroughExchange(
1622
1657
  if (decryptRejection) preflightLog.upstreamError = ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
1623
1658
  return decryptRejection;
1624
1659
  }, {
1625
- allowMissingContentType: !recoveryContentType && parsed.stream,
1660
+ allowMissingContentType: !recoveryContentType && upstreamRequestsStream,
1626
1661
  replayReadErrors: true,
1627
1662
  });
1628
1663
  if (options.abortSignal?.aborted) return transportFailureResponse(options.abortSignal.reason);
@@ -1772,7 +1807,7 @@ export async function preparePassthroughExchange(
1772
1807
  && !(options.nativeControl && options.inboundTransport === "websocket")
1773
1808
  && ambiguousResend() !== undefined
1774
1809
  && remainingTransientSendBudget(transientSendAttempts()) > 0
1775
- && (streamRecoveryContentType.includes("text/event-stream") || (!streamRecoveryContentType && parsed.stream));
1810
+ && (streamRecoveryContentType.includes("text/event-stream") || (!streamRecoveryContentType && upstreamRequestsStream));
1776
1811
  if (protocolRecoveryCandidate) {
1777
1812
  upstreamResponse = deferProtocolSafeResetRecovery(
1778
1813
  upstreamResponse,
@@ -1791,11 +1826,11 @@ export async function preparePassthroughExchange(
1791
1826
  authorize: () => authorizeResendForRecovery(stage, "connection-reset", ambiguousResend()).allowed,
1792
1827
  acceptResponse: candidate => {
1793
1828
  const type = candidate.headers.get("content-type")?.toLowerCase() ?? "";
1794
- return type.includes("text/event-stream") || (!type && parsed.stream);
1829
+ return type.includes("text/event-stream") || (!type && upstreamRequestsStream);
1795
1830
  },
1796
1831
  },
1797
1832
  ),
1798
- { allowMissingContentType: !streamRecoveryContentType && parsed.stream },
1833
+ { allowMissingContentType: !streamRecoveryContentType && upstreamRequestsStream },
1799
1834
  );
1800
1835
  }
1801
1836
  break;
@@ -79,6 +79,8 @@ export function formatPassthroughUpstreamError(
79
79
  statusText?: string;
80
80
  headers?: Headers;
81
81
  now?: number;
82
+ /** Caller holds an authoritative fatal verdict; neither inherited nor inferred waits apply. */
83
+ suppressRetryAfter?: boolean;
82
84
  /**
83
85
  * Provenance from the caller that still holds the response: this body is a refusal this
84
86
  * proxy synthesized. The body check below is the fallback for a re-wrapped body, and it
@@ -96,7 +98,7 @@ export function formatPassthroughUpstreamError(
96
98
  const replayRefusal = options?.replayRefusal === true || isReplayRefusalBody(trimmed);
97
99
  // Two different reasons to answer with no wait at all, handled the same way: a hard policy
98
100
  // block will not become servable, and a refusal we made was never a rate limit.
99
- const suppressRetryAfter = cyberPolicyFailure || replayRefusal;
101
+ const suppressRetryAfter = options?.suppressRetryAfter === true || cyberPolicyFailure || replayRefusal;
100
102
  const resolved = suppressRetryAfter
101
103
  ? undefined
102
104
  : resolveClientRetryAfter({