@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
@@ -23,6 +23,7 @@ import { resolveStallTimeoutMs } from "../../stall-timeout";
23
23
  import { clientEncoderForDelivery, deliverClientEncodedResponse } from "../inference/client-encoder-delivery";
24
24
  import { noteKiroServedSuccess } from "../../providers/kiro-usage";
25
25
  import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
26
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
26
27
 
27
28
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
28
29
  export async function deliverAdapterResponse(
@@ -30,6 +31,7 @@ export async function deliverAdapterResponse(
30
31
  requestState: Pick<
31
32
  PreparedResponsesRequest,
32
33
  | "parsed"
34
+ | "route"
33
35
  | "translatorBudget"
34
36
  | "toolBridgeMaps"
35
37
  | "rememberKiroDeliveredFinalAnswer"
@@ -51,6 +53,7 @@ export async function deliverAdapterResponse(
51
53
  const { logCtx, options, config } = requestContext;
52
54
  const {
53
55
  parsed,
56
+ route,
54
57
  translatorBudget,
55
58
  toolBridgeMaps,
56
59
  rememberKiroDeliveredFinalAnswer,
@@ -71,6 +74,7 @@ export async function deliverAdapterResponse(
71
74
  } = responseEffects;
72
75
  const { routedCompaction } = sidecarState;
73
76
  const bodyInactivityMs = resolveStallTimeoutMs(config.stallTimeoutSec, { localUpstream });
77
+ const upstreamRequestsStream = parsed.stream || isCanonicalOpenAiForwardProvider(route.provider);
74
78
 
75
79
 
76
80
  if (parsed.stream) {
@@ -199,15 +203,29 @@ export async function deliverAdapterResponse(
199
203
  });
200
204
  }
201
205
 
202
- if (transportState.activeAdapter.parseResponse) {
206
+ if (transportState.activeAdapter.parseResponse
207
+ || (upstreamRequestsStream && transportState.activeAdapter.parseStream)) {
203
208
  let events: AdapterEvent[];
204
209
  try {
205
- const initialEvents = await readResponseBodyWithInactivity(
206
- upstreamResponse,
207
- upstream.signal,
208
- bodyInactivityMs,
209
- response => transportState.activeAdapter.parseResponse!(response, translatorBudget, logCtx.activeTierMetadata),
210
- );
210
+ const initialEvents: AdapterEvent[] = [];
211
+ if (upstreamRequestsStream && transportState.activeAdapter.parseStream) {
212
+ // The canonical ChatGPT adapter coerces its actual upstream request to SSE even for a
213
+ // JSON client. Routed compaction still owns the non-streaming client response, so fold
214
+ // that SSE through the adapter instead of handing it to parseResponse as JSON.
215
+ for await (const event of readResponseStreamWithInactivity(
216
+ upstreamResponse,
217
+ upstream.signal,
218
+ bodyInactivityMs,
219
+ response => transportState.activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata),
220
+ )) initialEvents.push(event);
221
+ } else {
222
+ initialEvents.push(...await readResponseBodyWithInactivity(
223
+ upstreamResponse,
224
+ upstream.signal,
225
+ bodyInactivityMs,
226
+ response => transportState.activeAdapter.parseResponse!(response, translatorBudget, logCtx.activeTierMetadata),
227
+ ));
228
+ }
211
229
  for (const event of initialEvents) options.onCompactionRecoveryAdapterEvent?.(event);
212
230
  let guardedEvents: AdapterEvent[];
213
231
  if (terminalGuardEnabled) {
@@ -35,6 +35,7 @@ import {
35
35
  fetchWithResetRetry,
36
36
  applyUpstreamRecoveryInit,
37
37
  SendBudgetExhaustedError,
38
+ UpstreamRetryEvidenceError,
38
39
  prepareSameTarget429Wait,
39
40
  sleepWithAbort,
40
41
  } from "../../lib/upstream-retry";
@@ -54,8 +55,10 @@ import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
54
55
  import { resolveWireProtocolOverride } from "../adapter-resolve";
55
56
  import { bindRouteReasoningReplayScope } from "./core-replay";
56
57
  import {
58
+ AnthropicAccountCooldownError,
57
59
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
58
60
  rotateAnthropicAccountOn429,
61
+ recordAnthropicAccount429,
59
62
  getAnthropicPoolAccessSnapshot,
60
63
  formatAnthropicProviderForLog,
61
64
  } from "../../oauth/anthropic-routing";
@@ -382,6 +385,18 @@ export async function prepareAdapterExchange(
382
385
  cleanupUpstreamAbort();
383
386
  upstream.abort();
384
387
  if (options.abortSignal?.aborted) return clientCancelledResponse();
388
+ const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
389
+ // A pause committed during pacing is local admission policy, not a failed upstream.
390
+ if (refusal instanceof OAuthAccountPausedError) {
391
+ return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
392
+ }
393
+ if (refusal instanceof OAuthLoginRequiredError) {
394
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal));
395
+ }
396
+ if (refusal instanceof AnthropicAccountCooldownError) {
397
+ return formatErrorResponse(429, "rate_limit_error", refusal.message,
398
+ refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
399
+ }
385
400
  // A budget refusal is a decision this process made, not an upstream fault. Reporting it as
386
401
  // 502 does more than mislabel it: the Codex client retries 5xx and does not retry a 429, so
387
402
  // blaming the provider makes the caller send the whole turn again -- the amplification this
@@ -588,6 +603,17 @@ export async function prepareAdapterExchange(
588
603
  if (options.abortSignal?.aborted) {
589
604
  return { failed: clientCancelledResponse() };
590
605
  }
606
+ const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
607
+ if (refusal instanceof OAuthAccountPausedError) {
608
+ return { failed: formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal)) };
609
+ }
610
+ if (refusal instanceof OAuthLoginRequiredError) {
611
+ return { failed: formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal)) };
612
+ }
613
+ if (refusal instanceof AnthropicAccountCooldownError) {
614
+ return { failed: formatErrorResponse(429, "rate_limit_error", refusal.message,
615
+ refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) }) };
616
+ }
591
617
  // Same rule on the recovery leg: the ladder refused to send again, so the answer names
592
618
  // this proxy rather than the provider it never reached.
593
619
  if (err instanceof SendBudgetExhaustedError) {
@@ -1000,6 +1026,11 @@ export async function prepareAdapterExchange(
1000
1026
  break;
1001
1027
  }
1002
1028
  }
1029
+ if (upstreamResponse.status === 429 && transportState.anthropicPoolAccountId
1030
+ && transportState.anthropicPoolFailovers >= ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST) {
1031
+ recordAnthropicAccount429(config, transportState.anthropicPoolAccountId,
1032
+ upstreamResponse.headers.get("retry-after"), Date.now(), upstreamResponse.headers);
1033
+ }
1003
1034
  // Generic OAuth account failover (#2568) rotates reactively after a refusal when
1004
1035
  // two accounts are stored. Kiro additionally classifies bounded 400/403 refusals;
1005
1036
  // all other providers retain the original 429 loop below.
@@ -0,0 +1,345 @@
1
+ import type { ResponsesTerminalStatus } from "../../bridge";
2
+ import {
3
+ createSseInspector,
4
+ createSseTerminalOutputBoundary,
5
+ SseAggregateLimitError,
6
+ terminalStatusFromParsed,
7
+ type CodexSafetyBufferingFilterOptions,
8
+ type SseInspector,
9
+ } from "../relay";
10
+ import { SseFrameCountLimitError, SseFrameTooLargeError } from "../sse-frame-buffer";
11
+ import {
12
+ MAX_UPSTREAM_JSON_BODY_BYTES,
13
+ UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS,
14
+ UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
15
+ } from "./core-lifetime";
16
+
17
+ /** Larger than persistence inspection because this path must return the whole bounded response. */
18
+ export const MAX_BUFFERED_RESPONSES_OUTPUT_ITEMS = 10_000;
19
+ export const MAX_BUFFERED_RESPONSES_RECONSTRUCTION_BYTES = MAX_UPSTREAM_JSON_BODY_BYTES;
20
+ export const MAX_BUFFERED_RESPONSES_SSE_FRAMES = 100_000;
21
+ /**
22
+ * A buffered caller cannot consume partial output, so keep one independent lifetime ceiling even
23
+ * when the operator disables the ordinary silence clock. Fifteen minutes is intentionally much
24
+ * larger than the public default stall budget while still releasing an abandoned body eventually.
25
+ */
26
+ export const BUFFERED_RESPONSES_TOTAL_TIMEOUT_MS = 15 * 60_000;
27
+ const BUFFERED_SSE_PROCESSING_SLICE_BYTES = 64 * 1024;
28
+
29
+ export type BufferedResponsesTerminal = {
30
+ status: ResponsesTerminalStatus;
31
+ response: Record<string, unknown>;
32
+ };
33
+
34
+ export type BufferedResponsesSseFailure = {
35
+ ok: false;
36
+ kind: "aborted" | "malformed" | "missing_terminal" | "oversized" | "read_error" | "timeout";
37
+ error?: unknown;
38
+ /** Bounded diagnostic captured from a bare upstream `error` event. */
39
+ upstreamError?: string;
40
+ /** Fatal provider refusal verdict that must not be flattened into a retryable transport error. */
41
+ upstreamRefusalCode?: string;
42
+ /** Bounded structured class from the same bare error envelope as `upstreamError`. */
43
+ upstreamErrorType?: string;
44
+ /** Bounded structured code; unknown codes remain transport failures instead of message guesses. */
45
+ upstreamErrorCode?: string;
46
+ };
47
+
48
+ export type BufferedResponsesSseResult = {
49
+ ok: true;
50
+ /** Present only when the caller needs to replay the bounded transcript through rewrites. */
51
+ bytes?: Uint8Array;
52
+ terminal: BufferedResponsesTerminal;
53
+ } | BufferedResponsesSseFailure;
54
+
55
+ type BufferedReadOptions = {
56
+ maxBytes?: number;
57
+ maxFrames?: number;
58
+ totalTimeoutMs?: number;
59
+ /** Absolute whole-turn deadline shared by every validation pass. */
60
+ deadlineAt?: number;
61
+ inactivityTimeoutMs?: number;
62
+ firstByteTimeoutMs?: number;
63
+ };
64
+
65
+ export type BufferedResponsesReadOptions = BufferedReadOptions & { deadlineAt: number };
66
+
67
+ /**
68
+ * Resolve the read clocks for canonical SSE that must be folded into client JSON.
69
+ *
70
+ * A positive stall budget owns both time-to-first-byte and inter-chunk silence. A disabled budget
71
+ * must not become an immediate timeout, so both clocks fall back to the independent total ceiling.
72
+ * The ceiling remains separate because a JSON caller has no partial body to keep alive forever.
73
+ */
74
+ export function bufferedResponsesReadOptions(
75
+ stallTimeoutMs: number,
76
+ totalTimeoutMs = BUFFERED_RESPONSES_TOTAL_TIMEOUT_MS,
77
+ startedAt = Date.now(),
78
+ ): BufferedResponsesReadOptions {
79
+ if (!Number.isFinite(stallTimeoutMs)) throw new RangeError("stallTimeoutMs must be finite");
80
+ if (!Number.isSafeInteger(totalTimeoutMs) || totalTimeoutMs <= 0) {
81
+ throw new RangeError("totalTimeoutMs must be a positive safe integer");
82
+ }
83
+ if (!Number.isSafeInteger(startedAt) || startedAt < 0 || startedAt > Number.MAX_SAFE_INTEGER - totalTimeoutMs) {
84
+ throw new RangeError("startedAt must produce a safe deadline");
85
+ }
86
+ const silenceTimeoutMs = stallTimeoutMs > 0 ? Math.ceil(stallTimeoutMs) : totalTimeoutMs;
87
+ return {
88
+ totalTimeoutMs,
89
+ deadlineAt: startedAt + totalTimeoutMs,
90
+ firstByteTimeoutMs: silenceTimeoutMs,
91
+ inactivityTimeoutMs: silenceTimeoutMs,
92
+ };
93
+ }
94
+
95
+ type Uint8ReadResult = Awaited<ReturnType<ReadableStreamDefaultReader<Uint8Array>["read"]>>;
96
+
97
+ class BufferedSseTimeoutError extends Error {
98
+ constructor(deadline: "total" | "inactivity") {
99
+ super(`buffered Responses SSE ${deadline} timeout`);
100
+ this.name = "BufferedSseTimeoutError";
101
+ }
102
+ }
103
+
104
+ function createBufferedTerminalInspector(maxSourceBytes: number): {
105
+ inspector: SseInspector;
106
+ result: () => { ok: true; terminal: BufferedResponsesTerminal } | BufferedResponsesSseFailure;
107
+ } {
108
+ let malformed = false;
109
+ let terminalCount = 0;
110
+ let status: ResponsesTerminalStatus | undefined;
111
+ let terminalResponse: Record<string, unknown> | undefined;
112
+ const inspector = createSseInspector({
113
+ terminalReconstruction: {
114
+ maxItems: MAX_BUFFERED_RESPONSES_OUTPUT_ITEMS,
115
+ maxSourceBytes,
116
+ requireCompleteOutputIndices: true,
117
+ mergeSparseTerminalOutput: true,
118
+ },
119
+ onParsedPayload(payload) {
120
+ const candidateStatus = terminalStatusFromParsed(payload);
121
+ if (!candidateStatus) return;
122
+ terminalCount += 1;
123
+ status ??= candidateStatus;
124
+ },
125
+ onOpaquePayload(payload) {
126
+ if (payload !== "[DONE]") malformed = true;
127
+ },
128
+ onTerminalResponse(candidateStatus, response) {
129
+ if (candidateStatus === status && terminalResponse === undefined) {
130
+ if (response.status !== undefined && response.status !== candidateStatus) {
131
+ malformed = true;
132
+ return;
133
+ }
134
+ if (response.output !== undefined && !Array.isArray(response.output)) {
135
+ malformed = true;
136
+ return;
137
+ }
138
+ terminalResponse = {
139
+ ...response,
140
+ status: candidateStatus,
141
+ output: response.output ?? [],
142
+ } as Record<string, unknown>;
143
+ }
144
+ },
145
+ });
146
+ return {
147
+ inspector,
148
+ result: () => {
149
+ if (malformed) return { ok: false, kind: "malformed" };
150
+ if (terminalCount !== 1 || status === undefined) return { ok: false, kind: "missing_terminal" };
151
+ if (!terminalResponse || terminalResponse.status !== status || !Array.isArray(terminalResponse.output)) {
152
+ return { ok: false, kind: "malformed" };
153
+ }
154
+ return { ok: true, terminal: { status, response: terminalResponse } };
155
+ },
156
+ };
157
+ }
158
+
159
+ /** Strict synchronous seam for terminal/reconstruction tests; production feeds bytes incrementally. */
160
+ export function inspectBufferedResponsesTerminal(text: string):
161
+ | { ok: true; terminal: BufferedResponsesTerminal }
162
+ | BufferedResponsesSseFailure {
163
+ const state = createBufferedTerminalInspector(MAX_BUFFERED_RESPONSES_RECONSTRUCTION_BYTES);
164
+ try {
165
+ state.inspector.feed(new TextEncoder().encode(text));
166
+ state.inspector.finish();
167
+ return state.result();
168
+ } finally {
169
+ state.inspector.dispose();
170
+ }
171
+ }
172
+
173
+ function cancelReader(reader: ReadableStreamDefaultReader<Uint8Array>, reason?: unknown): void {
174
+ try { void reader.cancel(reason).catch(() => undefined); } catch { /* non-conforming stream */ }
175
+ }
176
+
177
+ async function deadlineRead(
178
+ reader: ReadableStreamDefaultReader<Uint8Array>,
179
+ signal: AbortSignal | undefined,
180
+ deadlineAt: number,
181
+ deadline: "total" | "inactivity",
182
+ ): Promise<Uint8ReadResult> {
183
+ if (signal?.aborted) throw signal.reason;
184
+ return await new Promise<Uint8ReadResult>((resolve, reject) => {
185
+ let settled = false;
186
+ let timer: ReturnType<typeof setTimeout> | undefined;
187
+ const finish = (callback: () => void): void => {
188
+ if (settled) return;
189
+ settled = true;
190
+ if (timer !== undefined) clearTimeout(timer);
191
+ signal?.removeEventListener("abort", onAbort);
192
+ callback();
193
+ };
194
+ const onAbort = () => finish(() => reject(signal?.reason));
195
+ timer = setTimeout(
196
+ () => finish(() => reject(new BufferedSseTimeoutError(deadline))),
197
+ Math.max(0, deadlineAt - Date.now()),
198
+ );
199
+ signal?.addEventListener("abort", onAbort, { once: true });
200
+ if (signal?.aborted) onAbort();
201
+ void reader.read().then(
202
+ value => finish(() => resolve(value)),
203
+ error => finish(() => reject(error)),
204
+ );
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Consume through the first terminal, which must validate, with bounded framing and deadlines.
210
+ * `retainTranscript:false` feeds the strict assembler directly and never materializes the second,
211
+ * client-facing SSE transcript. Side effects remain the delivery owner's responsibility.
212
+ */
213
+ export async function collectBufferedResponsesSse(
214
+ body: ReadableStream<Uint8Array>,
215
+ upstream: AbortController,
216
+ options: {
217
+ signal?: AbortSignal;
218
+ terminalBoundary?: CodexSafetyBufferingFilterOptions;
219
+ read?: BufferedReadOptions;
220
+ retainTranscript?: boolean;
221
+ } = {},
222
+ ): Promise<BufferedResponsesSseResult> {
223
+ const maxBytes = options.read?.maxBytes ?? MAX_UPSTREAM_JSON_BODY_BYTES;
224
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
225
+ throw new RangeError("maxBytes must be a positive safe integer");
226
+ }
227
+ const maxFrames = options.read?.maxFrames ?? MAX_BUFFERED_RESPONSES_SSE_FRAMES;
228
+ const totalTimeoutMs = options.read?.totalTimeoutMs ?? UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS;
229
+ const inactivityTimeoutMs = options.read?.inactivityTimeoutMs ?? UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS;
230
+ const firstByteTimeoutMs = options.read?.firstByteTimeoutMs ?? totalTimeoutMs;
231
+ const absoluteDeadlineAt = options.read?.deadlineAt;
232
+ if (absoluteDeadlineAt !== undefined && (!Number.isSafeInteger(absoluteDeadlineAt) || absoluteDeadlineAt < 0)) {
233
+ throw new RangeError("deadlineAt must be a non-negative safe integer");
234
+ }
235
+ const retainTranscript = options.retainTranscript === true;
236
+ const reader = body.getReader();
237
+ const boundary = createSseTerminalOutputBoundary({
238
+ ...options.terminalBoundary,
239
+ maxInputBytes: maxBytes,
240
+ maxOutputBytes: maxBytes,
241
+ maxFrames,
242
+ });
243
+ const state = createBufferedTerminalInspector(Math.min(
244
+ maxBytes,
245
+ MAX_BUFFERED_RESPONSES_RECONSTRUCTION_BYTES,
246
+ ));
247
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
248
+ let retained = retainTranscript ? new Uint8Array(Math.min(maxBytes, 64 * 1024)) : undefined;
249
+ let retainedBytes = 0;
250
+ const startedAt = Date.now();
251
+ const totalDeadlineAt = absoluteDeadlineAt ?? startedAt + totalTimeoutMs;
252
+ let inactivityDeadlineAt = startedAt + firstByteTimeoutMs;
253
+
254
+ const withBoundaryError = <T extends BufferedResponsesSseFailure>(failure: T): T => {
255
+ const upstreamError = boundary.upstreamError();
256
+ const upstreamRefusalCode = boundary.upstreamRefusalCode();
257
+ const upstreamErrorType = boundary.upstreamErrorType();
258
+ const upstreamErrorCode = boundary.upstreamErrorCode();
259
+ return {
260
+ ...failure,
261
+ ...(upstreamError === undefined ? {} : { upstreamError }),
262
+ ...(upstreamRefusalCode === undefined ? {} : { upstreamRefusalCode }),
263
+ ...(upstreamErrorType === undefined ? {} : { upstreamErrorType }),
264
+ ...(upstreamErrorCode === undefined ? {} : { upstreamErrorCode }),
265
+ };
266
+ };
267
+
268
+ const inspect = (chunk: Uint8Array): void => {
269
+ if (chunk.byteLength === 0) return;
270
+ // Validate incrementally without retaining a second decoded transcript. The shared inspector
271
+ // independently owns SSE framing and JSON reconstruction over the same bytes.
272
+ utf8.decode(chunk, { stream: true });
273
+ state.inspector.feed(chunk);
274
+ if (!retained) return;
275
+ if (chunk.byteLength > maxBytes - retainedBytes) throw new SseAggregateLimitError(maxBytes);
276
+ const required = retainedBytes + chunk.byteLength;
277
+ if (required > retained.byteLength) {
278
+ const grown = new Uint8Array(Math.min(maxBytes, Math.max(required, retained.byteLength * 2)));
279
+ grown.set(retained.subarray(0, retainedBytes));
280
+ retained = grown;
281
+ }
282
+ retained.set(chunk, retainedBytes);
283
+ retainedBytes = required;
284
+ };
285
+
286
+ try {
287
+ for (;;) {
288
+ const deadlineAt = Math.min(totalDeadlineAt, inactivityDeadlineAt);
289
+ const deadline = totalDeadlineAt <= inactivityDeadlineAt ? "total" : "inactivity";
290
+ const { done, value } = await deadlineRead(reader, options.signal, deadlineAt, deadline);
291
+ if (options.signal?.aborted) throw options.signal.reason;
292
+ if (done) {
293
+ inspect(boundary.finish());
294
+ break;
295
+ }
296
+ if (!value || value.byteLength === 0) continue;
297
+ inactivityDeadlineAt = Date.now() + inactivityTimeoutMs;
298
+ // Slice the reader's chunk before framing. This keeps one oversized network read from
299
+ // producing a transcript-sized array of frames/output buffers and gives the total deadline
300
+ // and aggregate frame cap a checkpoint between bounded pieces of synchronous work.
301
+ for (let offset = 0; offset < value.byteLength && !boundary.terminalSeen();) {
302
+ if (Date.now() >= totalDeadlineAt) throw new BufferedSseTimeoutError("total");
303
+ const end = Math.min(value.byteLength, offset + BUFFERED_SSE_PROCESSING_SLICE_BYTES);
304
+ inspect(boundary.feed(value.subarray(offset, end)));
305
+ offset = end;
306
+ }
307
+ if (boundary.terminalSeen()) {
308
+ cancelReader(reader, "Responses terminal event received");
309
+ break;
310
+ }
311
+ }
312
+ utf8.decode();
313
+ state.inspector.finish();
314
+ if (options.signal?.aborted) throw options.signal.reason;
315
+ const terminal = state.result();
316
+ if (!terminal.ok) {
317
+ upstream.abort(new Error(`buffered Responses SSE ${terminal.kind}`));
318
+ return withBoundaryError(terminal);
319
+ }
320
+ return {
321
+ ...terminal,
322
+ ...(retained ? { bytes: retained.subarray(0, retainedBytes) } : {}),
323
+ };
324
+ } catch (error) {
325
+ upstream.abort(error);
326
+ cancelReader(reader, error);
327
+ return withBoundaryError({
328
+ ok: false,
329
+ kind: options.signal?.aborted
330
+ ? "aborted"
331
+ : error instanceof BufferedSseTimeoutError
332
+ ? "timeout"
333
+ : error instanceof SseAggregateLimitError
334
+ || error instanceof SseFrameTooLargeError
335
+ || error instanceof SseFrameCountLimitError
336
+ ? "oversized"
337
+ : "read_error",
338
+ error,
339
+ });
340
+ } finally {
341
+ boundary.dispose();
342
+ state.inspector.dispose();
343
+ try { reader.releaseLock(); } catch { /* a cancelled pending read may still own it briefly */ }
344
+ }
345
+ }
@@ -395,6 +395,8 @@ export interface CodexPoolAccountRetryArgs {
395
395
  connectMs: number;
396
396
  passthroughEstimate?: number;
397
397
  stream: boolean;
398
+ /** Keep a buffered canonical client on HTTP/SSE after moving to another Pool account. */
399
+ httpOnly?: boolean;
398
400
  onResponse?: (
399
401
  response: Response,
400
402
  authCtx: CodexAuthContext,
@@ -535,7 +537,7 @@ export async function retryCodexPoolOnAlternateAccount(
535
537
  ): Promise<CodexPoolAccountRetryResult> {
536
538
  const {
537
539
  callerAuthHeaders, config, route, parsed, logCtx, options, firstAuthCtx, firstResponse,
538
- outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
540
+ outcomeStatus, upstream, connectMs, passthroughEstimate, stream, httpOnly,
539
541
  } = args;
540
542
  const inboundWire = options.inboundWire ?? "responses";
541
543
  const entitlementResolver = options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements;
@@ -877,6 +879,7 @@ export async function retryCodexPoolOnAlternateAccount(
877
879
  connectMs,
878
880
  stream,
879
881
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
882
+ httpOnly,
880
883
  providerName: route.providerName,
881
884
  modelId: route.modelId,
882
885
  onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
@@ -46,6 +46,11 @@ import {
46
46
  import { hasUnreadableEncryptedAgentTask } from "./encrypted-payload";
47
47
  import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
48
48
  import { memoryModelRouteReason } from "./memory-models";
49
+ import { poolAccountProviderLabel } from "../../providers/label";
50
+ import { getAccountSet } from "../../oauth/store";
51
+ import { formatAnthropicProviderForLog, getAnthropicAccountHealthSnapshot } from "../../oauth/anthropic-routing";
52
+ import { codexAccountLogLabel } from "../../codex/account-label";
53
+ import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
49
54
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
50
55
  import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
51
56
  import {
@@ -97,6 +102,22 @@ import { clientWireOf } from "../inference/client-wire";
97
102
  */
98
103
  export const COMBO_TARGET_BASE_SENDS = CODEX_TEXT_GUARDED_BUDGET_POLICY.baseSendAllowance;
99
104
 
105
+ function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId: string, label: string | undefined): string | undefined {
106
+ if (!label) return undefined;
107
+ if (providerName === "anthropic") {
108
+ const matches = getAccountSet("anthropic")?.accounts.filter(account =>
109
+ formatAnthropicProviderForLog("anthropic", account.id) === label) ?? [];
110
+ return matches.length === 1 && getAnthropicAccountHealthSnapshot(matches[0]!.id) ? label : undefined;
111
+ }
112
+ const provider = config.providers[providerName];
113
+ if (!provider || !isCanonicalOpenAiForwardProvider(provider)) return undefined;
114
+ const matches = (config.codexAccounts ?? []).filter(account =>
115
+ `${providerName}-${codexAccountLogLabel(account)}` === label);
116
+ return matches.length === 1
117
+ && getCodexQuotaHealthSnapshot(matches[0]!.id, codexQuotaScopeForModel(modelId))
118
+ ? label : undefined;
119
+ }
120
+
100
121
 
101
122
  /**
102
123
  * A combo's execution policy is DECLARED by the combo, not inherited from the single-target
@@ -977,6 +998,13 @@ export async function executeComboResponses(
977
998
  status: failure.response.status,
978
999
  code: failure.upstreamCode,
979
1000
  message: failure.classificationText,
1001
+ // The dispatch rewrote this to name the pool account that actually served the turn.
1002
+ failedAccount: cooledPoolAccountLabel(
1003
+ config,
1004
+ pick.target.provider,
1005
+ pick.target.model,
1006
+ poolAccountProviderLabel(childLog.provider, pick.target.provider),
1007
+ ),
980
1008
  onCooldownRecorded: target => {
981
1009
  failedTargetCooldownRecorded ||= targetKey(target) === failedTargetKey;
982
1010
  },
@@ -4,6 +4,8 @@ import {
4
4
  markNativePassthroughSseResponse,
5
5
  isEagerRelaySseResponse,
6
6
  markEagerRelaySseResponse,
7
+ isPreinspectedJsonResponse,
8
+ markPreinspectedJsonResponse,
7
9
  } from "../relay";
8
10
 
9
11
  // runTurn adapters own an event queue and perform their combo preflight before
@@ -77,6 +79,7 @@ export function finalizeOwnedTranslatorBudget(response: Response, budget: Transl
77
79
  if (isEagerRelaySseResponse(response)) {
78
80
  markEagerRelaySseResponse(finalizedResponse);
79
81
  }
82
+ if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(finalizedResponse);
80
83
  return finalizedResponse;
81
84
  }
82
85
 
@@ -99,6 +102,7 @@ export function finalizeAccountLease(response: Response, release: () => void): R
99
102
  const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
100
103
  if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
101
104
  if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
105
+ if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
102
106
  return wrapped;
103
107
  }
104
108