@yansigit/opencodex 2.31.3 → 2.32.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 (107) hide show
  1. package/bin/ocx.mjs +99 -70
  2. package/gui/dist/assets/index-BG43zwVe.js +102 -0
  3. package/gui/dist/assets/index-CiSI-jrP.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +6 -8
  7. package/src/adapters/base.ts +2 -0
  8. package/src/adapters/command-code.ts +2 -3
  9. package/src/adapters/cursor/live-models.ts +8 -0
  10. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  11. package/src/adapters/cursor/tool-definitions.ts +1 -1
  12. package/src/adapters/google.ts +6 -7
  13. package/src/adapters/kiro.ts +0 -3
  14. package/src/adapters/openai-responses.ts +3 -0
  15. package/src/adapters/tool-catalog-nudge.ts +1 -1
  16. package/src/adapters/xai-web-search.ts +7 -2
  17. package/src/bridge.ts +21 -15
  18. package/src/cli/dispatch.ts +50 -2
  19. package/src/cli/doctor.ts +24 -11
  20. package/src/cli/help.ts +4 -3
  21. package/src/cli/index.ts +11 -4
  22. package/src/cli/models.ts +13 -3
  23. package/src/cli/observe.ts +20 -5
  24. package/src/cli/provider.ts +2 -1
  25. package/src/cli/registry.ts +7 -5
  26. package/src/cli/status.ts +2 -1
  27. package/src/cli/system-restart-client.ts +1 -1
  28. package/src/cli/usage-report.ts +134 -0
  29. package/src/codex/app-server-processes.ts +3 -1
  30. package/src/codex/catalog/aggregation.ts +13 -1
  31. package/src/codex/catalog/effort.ts +24 -9
  32. package/src/codex/catalog/model-metadata.ts +566 -0
  33. package/src/codex/catalog/parsing.ts +33 -0
  34. package/src/codex/catalog/provider-fetch.ts +123 -32
  35. package/src/codex/catalog/sync.ts +5 -4
  36. package/src/codex/desktop-app-restart.ts +342 -0
  37. package/src/codex/history-job.ts +32 -3
  38. package/src/codex/history-manifest.ts +112 -0
  39. package/src/codex/history-migration-guardian.ts +5 -5
  40. package/src/codex/history-provider.ts +825 -247
  41. package/src/codex/history-worker.ts +8 -5
  42. package/src/codex/inject.ts +49 -21
  43. package/src/codex/injected-marker.ts +1 -1
  44. package/src/codex/internal/history-writer.ts +4 -3
  45. package/src/codex/native-profile-startup.ts +157 -27
  46. package/src/codex/native-residue.ts +26 -33
  47. package/src/combos/failover.ts +27 -0
  48. package/src/compatibility/index.ts +26 -0
  49. package/src/compatibility/manifest.ts +253 -0
  50. package/src/compatibility/openai-responses.ts +81 -0
  51. package/src/config/atomic-write.ts +219 -0
  52. package/src/config/paths.ts +40 -0
  53. package/src/config/process-state.ts +308 -0
  54. package/src/config/provider-validation.ts +177 -0
  55. package/src/config.ts +75 -812
  56. package/src/generated/compatibility-version.json +145 -85
  57. package/src/images/plan.ts +5 -4
  58. package/src/integrations/ownership-policy.ts +141 -0
  59. package/src/integrations/ownership.ts +10 -0
  60. package/src/integrations/state.ts +44 -5
  61. package/src/integrations/writer.ts +6 -0
  62. package/src/lib/bounded-body.ts +14 -2
  63. package/src/lib/process-control.ts +2 -1
  64. package/src/lib/state-store-registrations.ts +2 -0
  65. package/src/lib/tool-argument-integers.ts +56 -5
  66. package/src/oauth/health.ts +1 -1
  67. package/src/providers/registry.ts +1 -1
  68. package/src/reasoning-effort.ts +19 -2
  69. package/src/responses/apply-patch-envelope.ts +63 -0
  70. package/src/responses/custom-tool-compat.ts +132 -38
  71. package/src/responses/parser.ts +3 -2
  72. package/src/responses/reasoning-replay-cache.ts +81 -3
  73. package/src/server/auth-cors.ts +9 -7
  74. package/src/server/index.ts +102 -21
  75. package/src/server/local-management-read-client.ts +1 -1
  76. package/src/server/local-provider-reload-client.ts +1 -1
  77. package/src/server/management/agent-settings-routes.ts +1 -1
  78. package/src/server/management/config-routes.ts +4 -1
  79. package/src/server/management/context.ts +1 -1
  80. package/src/server/management/logs-usage-routes.ts +27 -6
  81. package/src/server/management/model-routes.ts +8 -4
  82. package/src/server/management/native-integration-routes.ts +2 -1
  83. package/src/server/management/provider-capability-config.ts +1 -1
  84. package/src/server/management/system-restart.ts +1 -1
  85. package/src/server/port-reclaim.ts +1 -1
  86. package/src/server/proxy-liveness.ts +2 -1
  87. package/src/server/request-log-conversation.ts +30 -0
  88. package/src/server/responses/codex-auth-error.ts +55 -0
  89. package/src/server/responses/combo-stream-preflight.ts +171 -0
  90. package/src/server/responses/compact.ts +6 -21
  91. package/src/server/responses/core.ts +225 -94
  92. package/src/server/responses/fetch-helpers.ts +2 -97
  93. package/src/server/responses-custom-tool-repair.ts +41 -5
  94. package/src/server/responses-undeclared-tool-guard.ts +156 -15
  95. package/src/service.ts +8 -4
  96. package/src/types/request.ts +6 -1
  97. package/src/types/tools.ts +87 -11
  98. package/src/types.ts +1 -1
  99. package/src/update/index.ts +5 -4
  100. package/src/update/job.ts +3 -1
  101. package/src/update/transactional-install.mjs +8 -1
  102. package/src/usage/log.ts +16 -8
  103. package/src/usage/summary.ts +201 -8
  104. package/src/vision/describe.ts +18 -13
  105. package/src/web-search/executor.ts +10 -3
  106. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  107. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -55,7 +55,7 @@ import {
55
55
  type PersistedUsageEntry,
56
56
  } from "../../usage/log";
57
57
  import { getUsageDebugLogEntries } from "../../usage/debug";
58
- import { parseRange, parseUsageSurface, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
58
+ import { USAGE_RANGES, USAGE_SURFACES, parseRange, parseUsageSurface, projectUsageSummary, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
59
59
  import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
60
60
  import { getProviderRegistryEntry } from "../../providers/registry";
61
61
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
@@ -197,6 +197,17 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
197
197
  if (url.pathname === "/api/usage" && req.method === "GET") {
198
198
  const range = parseRange(url.searchParams.get("range"));
199
199
  const surface = parseUsageSurface(url.searchParams.get("surface"));
200
+ // Applied to the OUTGOING payload only. A filtered summary must never reach
201
+ // the cache or the warm loop below: the key is `range:surface`, so a
202
+ // filtered entry stored under it would be served to the next unfiltered
203
+ // caller, dashboard included.
204
+ const filter = {
205
+ provider: url.searchParams.get("provider"),
206
+ model: url.searchParams.get("model"),
207
+ };
208
+ const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
209
+ projectUsageSummary(summary, filter, entries);
210
+ const filterRequested = Boolean(filter.provider ?? filter.model);
200
211
  const now = Date.now();
201
212
  try {
202
213
  const cacheKey = `${range}:${surface}`;
@@ -206,6 +217,12 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
206
217
  const observedSize = observed?.size ?? 0;
207
218
  const cached = getUsageSummaryCacheEntry(cacheKey);
208
219
  if (cached
220
+ // A filtered response is re-summarised from entries, which a cached
221
+ // summary does not carry. Serving it from cache would mean projecting
222
+ // over collapsed breakdown rows again — the exact defect the
223
+ // re-summarisation replaced. The unfiltered cache stays warm either
224
+ // way; only the filtered caller pays for the read.
225
+ && !filterRequested
209
226
  && cached.identityKey === identityKey
210
227
  && cached.maxReadBytes === effectiveReadLimit
211
228
  && cached.overlayVersion === userCostOverlayVersion()
@@ -214,7 +231,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
214
231
  && observedSize >= cached.lastSeenSize) {
215
232
  return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
216
233
  }
217
- if (cached) discardUsageSummaryCacheEntry(cacheKey);
234
+ if (cached && !filterRequested) discardUsageSummaryCacheEntry(cacheKey);
218
235
  // Capture the overlay version BEFORE reading/computing: the cache entry
219
236
  // must be stamped with the version the summary was priced under. Reading
220
237
  // it again at stamp time could cache an old-price summary as current,
@@ -238,14 +255,18 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
238
255
  // summary may mix old and new prices. Serve it uncached: the next
239
256
  // request recomputes against the settled overlay instead of caching a
240
257
  // mixed-price entry under either version.
241
- return jsonResponse(summary);
258
+ return jsonResponse(project(summary, snapshot.entries));
242
259
  }
243
260
  const freshUntil = now + 60_000;
244
261
  const snapshotIdentity = `${usageLogIdentityKey(snapshot.revision)}\0${effectiveReadLimit}`;
245
262
  const revisionKey = `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`;
246
263
  const lastSeenSize = snapshot.revision?.size ?? 0;
247
- const ranges: UsageRange[] = ["7d", "30d", "all"];
248
- const surfaces: UsageSurface[] = ["all", "codex", "claude", "grok"];
264
+ // Derived from the canonical constants rather than re-listed: a subset
265
+ // literal type-checks perfectly happily, so a range added to the union
266
+ // and forgotten here would never be warmed and never invalidated
267
+ // alongside its siblings.
268
+ const ranges: readonly UsageRange[] = USAGE_RANGES;
269
+ const surfaces: readonly UsageSurface[] = USAGE_SURFACES;
249
270
  for (const nextRange of ranges) {
250
271
  for (const nextSurface of surfaces) {
251
272
  const nextSummary = nextRange === range && nextSurface === surface ? summary : {
@@ -279,7 +300,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
279
300
  effectiveReadLimit,
280
301
  now,
281
302
  );
282
- return jsonResponse(summary);
303
+ return jsonResponse(project(summary, snapshot.entries));
283
304
  } catch {
284
305
  return jsonResponse({
285
306
  range,
@@ -394,8 +394,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
394
394
  }
395
395
 
396
396
  if (url.pathname === "/api/custom-models" && req.method === "POST") {
397
- let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; reasoningEfforts?: unknown; defaultReasoningEffort?: unknown };
398
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
397
+ let parsedBody: unknown;
398
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
399
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
400
+ const body = parsedBody;
399
401
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
400
402
  const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
401
403
  if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
@@ -441,8 +443,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
441
443
  if (customPutMatch && req.method === "PUT") {
442
444
  let id: string;
443
445
  try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
444
- let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown; reasoningEfforts?: unknown; defaultReasoningEffort?: unknown };
445
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
446
+ let parsedBody: unknown;
447
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
448
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
449
+ const body = parsedBody;
446
450
  const list = config.customModels ?? [];
447
451
  const idx = list.findIndex(cm => cm.id === id);
448
452
  if (idx === -1) return jsonResponse({ error: "not found" }, 404);
@@ -17,7 +17,8 @@
17
17
  * Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
18
18
  * 011 (Claude Code), 012 (Grok).
19
19
  */
20
- import { loadConfig, readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
20
+ import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
21
+ import { readRuntimePort } from "../../config/process-state";
21
22
  import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
22
23
  import { providerContextCap } from "../../providers/context-cap";
23
24
  import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
@@ -1,4 +1,4 @@
1
- import { booleanRecordConfigError } from "../../config";
1
+ import { booleanRecordConfigError } from "../../config/provider-validation";
2
2
  import type { OcxConfig } from "../../types";
3
3
 
4
4
  /**
@@ -33,7 +33,7 @@ import {
33
33
  stopServerListener,
34
34
  } from "../lifecycle";
35
35
  import { isServiceViable } from "../../service";
36
- import { readRuntimePort } from "../../config";
36
+ import { readRuntimePort } from "../../config/process-state";
37
37
  import { withProcessRuntimeProvenance } from "../../lib/bun-runtime";
38
38
  import { selfLaunchArgv } from "../../lib/self-launch-argv";
39
39
  import {
@@ -9,7 +9,7 @@
9
9
  * (non-ocx, non-allowlisted) processes are never killed.
10
10
  */
11
11
  import { execFileSync } from "node:child_process";
12
- import { verifyPidIdentity } from "../config";
12
+ import { verifyPidIdentity } from "../config/process-state";
13
13
  import { isProcessAlive, killProxy } from "../lib/process-control";
14
14
  import { isPortAvailable, type WaitForPortOptions } from "./ports";
15
15
  import { dropWindowsTcpRowsForLocalPort } from "./windows-tcp-drop";
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * Lives outside cli.ts (which dispatches argv at module top level) so tests can import it.
11
11
  */
12
- import { loadConfig, readAlivePid, readRuntimePort, verifyPidIdentity } from "../config";
12
+ import { loadConfig } from "../config";
13
+ import { readAlivePid, readRuntimePort, verifyPidIdentity } from "../config/process-state";
13
14
  import { directLocalHttpFetch } from "./direct-local-http";
14
15
 
15
16
  export interface HealthzIdentity {
@@ -61,6 +61,36 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
61
61
  return headers.get("session_id") ?? headers.get("session-id");
62
62
  }
63
63
 
64
+ function firstSanitizedConversationId(
65
+ ...values: Array<string | null | undefined>
66
+ ): string | undefined {
67
+ for (const value of values) {
68
+ const sanitized = sanitizeConversationIdInput(value);
69
+ if (sanitized) return sanitized;
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ /**
75
+ * Conversation namespace for reasoning replay. Unlike the persisted log id, this stays the raw
76
+ * sanitized identity so mixed headers that carry the same conversation still hit one serving
77
+ * record. Do not hash, and do not prefer session_id over a true per-conversation thread/Cursor
78
+ * identity: session_id can be synthesized from a shared prompt_cache_key.
79
+ */
80
+ export function reasoningReplayConversationIdFromResponsesRequest(input: {
81
+ clientThreadId?: string;
82
+ threadIdHeader?: string | null;
83
+ cursorConversationId?: string;
84
+ sessionIdHeader?: string | null;
85
+ }): string | undefined {
86
+ return firstSanitizedConversationId(
87
+ input.clientThreadId,
88
+ input.threadIdHeader,
89
+ input.cursorConversationId,
90
+ input.sessionIdHeader,
91
+ );
92
+ }
93
+
64
94
  export function conversationIdFromResponsesRequest(input: {
65
95
  clientThreadId?: string;
66
96
  sessionIdHeader?: string | null;
@@ -0,0 +1,55 @@
1
+ import { formatErrorResponse } from "../../bridge";
2
+ import {
3
+ CodexAccountCooldownError,
4
+ codexMainProfileDrainingResponse,
5
+ cooldownErrorResponse,
6
+ CodexAuthContextError,
7
+ CodexDirectAuthenticationError,
8
+ CodexMainProfileDrainingError,
9
+ CodexMainSubstitutionUnavailableError,
10
+ CodexPoolAuthenticationError,
11
+ CodexThreadAffinityExpiredError,
12
+ } from "../../codex/auth-context";
13
+
14
+ export interface CodexAuthContextErrorResponseOptions {
15
+ accountSelector?: string;
16
+ now: number;
17
+ }
18
+
19
+ /** Shared HTTP contract for Codex auth-context failures on Responses surfaces. */
20
+ export function mapCodexAuthContextErrorToResponse(
21
+ error: unknown,
22
+ options: CodexAuthContextErrorResponseOptions,
23
+ ): Response | undefined {
24
+ if (error instanceof CodexAccountCooldownError) {
25
+ return cooldownErrorResponse(error, options.now, options.accountSelector);
26
+ }
27
+ if (error instanceof CodexMainProfileDrainingError) {
28
+ return codexMainProfileDrainingResponse();
29
+ }
30
+ if (error instanceof CodexThreadAffinityExpiredError) {
31
+ return formatErrorResponse(
32
+ 409,
33
+ "invalid_request_error",
34
+ "Codex thread account affinity expired; start a new session",
35
+ );
36
+ }
37
+ if (error instanceof CodexAuthContextError) {
38
+ return formatErrorResponse(
39
+ 401,
40
+ "authentication_error",
41
+ "Selected Codex account needs reauthentication",
42
+ );
43
+ }
44
+ if (error instanceof CodexPoolAuthenticationError || error instanceof CodexDirectAuthenticationError) {
45
+ return formatErrorResponse(401, "authentication_error", error.message);
46
+ }
47
+ if (error instanceof CodexMainSubstitutionUnavailableError) {
48
+ return formatErrorResponse(
49
+ 401,
50
+ "authentication_error",
51
+ "No usable Codex main credential to serve this request",
52
+ );
53
+ }
54
+ return undefined;
55
+ }
@@ -0,0 +1,171 @@
1
+ import type { ResponsesTerminalStatus } from "../../bridge";
2
+ import type { RequestLogContext } from "../request-log";
3
+ import { createSseInspector } from "../relay";
4
+ import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
5
+
6
+ const COMBO_STREAM_PREFLIGHT_MAX_BYTES = MAX_CLIENT_SSE_FRAME_BYTES;
7
+
8
+ const PRE_OUTPUT_CONTROL_EVENTS = new Set([
9
+ "response.created",
10
+ "response.in_progress",
11
+ "response.queued",
12
+ "response.heartbeat",
13
+ ]);
14
+
15
+ const TERMINAL_EVENTS = new Set([
16
+ "response.completed",
17
+ "response.failed",
18
+ "response.incomplete",
19
+ ]);
20
+
21
+ /**
22
+ * Decide when replaying the request on another combo target would risk duplicating
23
+ * client-visible output or a tool-side effect. Unknown event types commit the child
24
+ * conservatively; only the small Responses lifecycle preamble remains replayable.
25
+ */
26
+ export function comboStreamPayloadCommitsOutput(payload: unknown): boolean {
27
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return true;
28
+ const type = (payload as { type?: unknown }).type;
29
+ if (typeof type !== "string") return true;
30
+ return !PRE_OUTPUT_CONTROL_EVENTS.has(type) && !TERMINAL_EVENTS.has(type);
31
+ }
32
+
33
+ function replayBufferedResponse(
34
+ response: Response,
35
+ reader: ReadableStreamDefaultReader<Uint8Array>,
36
+ buffered: Uint8Array[],
37
+ ): Response {
38
+ let index = 0;
39
+ const body = new ReadableStream<Uint8Array>({
40
+ async pull(controller) {
41
+ if (index < buffered.length) {
42
+ controller.enqueue(buffered[index++]!);
43
+ return;
44
+ }
45
+ try {
46
+ const next = await reader.read();
47
+ if (next.done) controller.close();
48
+ else controller.enqueue(next.value);
49
+ } catch (error) {
50
+ try { controller.error(error); } catch { /* consumer already closed */ }
51
+ }
52
+ },
53
+ cancel(reason) {
54
+ reader.cancel(reason).catch(() => undefined);
55
+ },
56
+ });
57
+ return new Response(body, {
58
+ status: response.status,
59
+ statusText: response.statusText,
60
+ headers: response.headers,
61
+ });
62
+ }
63
+
64
+ function failedTerminalResponse(
65
+ response: Response,
66
+ terminalPayload: Record<string, unknown>,
67
+ logCtx: RequestLogContext,
68
+ ): Response {
69
+ const nested = terminalPayload.response;
70
+ const terminalResponse = nested && typeof nested === "object" && !Array.isArray(nested)
71
+ ? nested as Record<string, unknown>
72
+ : {};
73
+ const nestedError = terminalResponse.error;
74
+ const error = nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)
75
+ ? nestedError as Record<string, unknown>
76
+ : {
77
+ type: "upstream_error",
78
+ code: "upstream_server_error",
79
+ message: logCtx.upstreamError ?? "Provider stream failed before producing output",
80
+ };
81
+ const headers = new Headers(response.headers);
82
+ headers.set("content-type", "application/json");
83
+ headers.delete("content-length");
84
+ headers.delete("content-encoding");
85
+ const usage = terminalResponse.usage;
86
+ return new Response(JSON.stringify({
87
+ error,
88
+ // The combo classifier needs only the error and optional usage. Do not carry
89
+ // response ids, provider metadata, or future terminal fields into the client
90
+ // error envelope merely because they shared the terminal snapshot.
91
+ response: {
92
+ error,
93
+ ...(usage && typeof usage === "object" && !Array.isArray(usage) ? { usage } : {}),
94
+ },
95
+ }), {
96
+ status: logCtx.terminalHttpStatus ?? 502,
97
+ headers,
98
+ });
99
+ }
100
+
101
+ export type ComboStreamPreflightResult =
102
+ | { kind: "accepted"; response: Response }
103
+ | { kind: "failed"; response: Response };
104
+
105
+ /**
106
+ * Buffer a combo child's downstream SSE only until the request becomes unsafe to
107
+ * replay or reaches a terminal. This owns exactly one body reader. The aggregate
108
+ * buffer is capped; hitting the cap commits the current target instead of growing
109
+ * memory or guessing that replay is safe.
110
+ */
111
+ export async function preflightComboStreamResponse(
112
+ response: Response,
113
+ logCtx: RequestLogContext,
114
+ ): Promise<ComboStreamPreflightResult> {
115
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
116
+ if (!response.ok || !response.body || !contentType.includes("text/event-stream")) {
117
+ return { kind: "accepted", response };
118
+ }
119
+
120
+ const reader = response.body.getReader();
121
+ const buffered: Uint8Array[] = [];
122
+ let bufferedBytes = 0;
123
+ let outputCommitted = false;
124
+ let terminalStatus: ResponsesTerminalStatus | undefined;
125
+ let failedPayload: Record<string, unknown> | undefined;
126
+ const inspector = createSseInspector({
127
+ logCtx,
128
+ onParsedPayload: payload => {
129
+ if (comboStreamPayloadCommitsOutput(payload)) outputCommitted = true;
130
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
131
+ if ((payload as { type?: unknown }).type === "response.failed") {
132
+ failedPayload = payload as Record<string, unknown>;
133
+ }
134
+ },
135
+ onTerminal: status => { terminalStatus = status; },
136
+ });
137
+
138
+ try {
139
+ for (;;) {
140
+ const next = await reader.read();
141
+ if (next.done) {
142
+ inspector.finish();
143
+ } else {
144
+ if (bufferedBytes + next.value.byteLength > COMBO_STREAM_PREFLIGHT_MAX_BYTES) {
145
+ // Keep the cap about memory the preflight allocates. The upstream chunk already exists;
146
+ // copying it before committing would transiently exceed the boundary for no
147
+ // replay benefit. Preserve it unsliced behind the already-bounded prefix.
148
+ return {
149
+ kind: "accepted",
150
+ response: replayBufferedResponse(response, reader, [...buffered, next.value]),
151
+ };
152
+ }
153
+ const retained = next.value.slice();
154
+ buffered.push(retained);
155
+ bufferedBytes += retained.byteLength;
156
+ inspector.feed(retained);
157
+ }
158
+
159
+ if (terminalStatus === "failed" && !outputCommitted && failedPayload) {
160
+ await reader.cancel("retrying zero-output combo stream failure").catch(() => undefined);
161
+ return { kind: "failed", response: failedTerminalResponse(response, failedPayload, logCtx) };
162
+ }
163
+ if (next.done || terminalStatus !== undefined || outputCommitted
164
+ || bufferedBytes >= COMBO_STREAM_PREFLIGHT_MAX_BYTES) {
165
+ return { kind: "accepted", response: replayBufferedResponse(response, reader, buffered) };
166
+ }
167
+ }
168
+ } finally {
169
+ inspector.dispose();
170
+ }
171
+ }
@@ -42,17 +42,9 @@ import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSide
42
42
  import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
43
43
  import {
44
44
  applyCodexAuthContextToProvider,
45
- CodexAccountCooldownError,
46
- codexMainProfileDrainingResponse,
47
- cooldownErrorResponse,
48
- CodexAuthContextError,
49
- CodexDirectAuthenticationError,
50
45
  CodexMainProfileDrainingError,
51
- CodexPoolAuthenticationError,
52
- CodexThreadAffinityExpiredError,
53
46
  headersForCodexAuthContext,
54
47
  materializeCodexUpstreamAuth,
55
- CodexMainSubstitutionUnavailableError,
56
48
  isCodexAuthContextUsable,
57
49
  resolveCodexAuthContext,
58
50
  codexProbeLeaseId,
@@ -134,6 +126,7 @@ import {
134
126
  usesCodexForwardPoolAuth,
135
127
  } from "./core";
136
128
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
129
+ import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
137
130
 
138
131
  export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
139
132
 
@@ -395,19 +388,11 @@ export async function handleResponsesCompact(
395
388
  }
396
389
  }
397
390
  } catch (err) {
398
- if (err instanceof CodexAccountCooldownError) {
399
- return cooldownErrorResponse(err, Date.now(), route.codexAccountNamespace);
400
- }
401
- if (err instanceof CodexMainProfileDrainingError) return codexMainProfileDrainingResponse();
402
- if (err instanceof CodexThreadAffinityExpiredError) {
403
- return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
404
- }
405
- if (err instanceof CodexAuthContextError) {
406
- return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
407
- }
408
- if (err instanceof CodexPoolAuthenticationError || err instanceof CodexDirectAuthenticationError) {
409
- return formatErrorResponse(401, "authentication_error", err.message);
410
- }
391
+ const response = mapCodexAuthContextErrorToResponse(err, {
392
+ accountSelector: route.codexAccountNamespace,
393
+ now: Date.now(),
394
+ });
395
+ if (response) return response;
411
396
  throw err;
412
397
  }
413
398
  const base = isCanonicalOpenAiForwardProvider(compactProvider)