@yansigit/opencodex 2.33.1 → 2.35.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 (177) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -3
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +9 -9
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-shell.ts +103 -6
  17. package/src/adapters/cursor/native-exec.ts +20 -0
  18. package/src/adapters/cursor/protobuf-request.ts +74 -39
  19. package/src/adapters/cursor/request-builder.ts +32 -10
  20. package/src/adapters/cursor/tool-definitions.ts +13 -10
  21. package/src/adapters/cursor/tool-result-normalize.ts +35 -3
  22. package/src/adapters/cursor/types.ts +16 -4
  23. package/src/adapters/cursor.ts +97 -70
  24. package/src/adapters/google-antigravity-replay.ts +105 -25
  25. package/src/adapters/google-antigravity-wire.ts +5 -0
  26. package/src/adapters/google-errors.ts +41 -12
  27. package/src/adapters/google-http.ts +12 -11
  28. package/src/adapters/google.ts +156 -65
  29. package/src/adapters/image.ts +1 -1
  30. package/src/adapters/kiro-constants.ts +15 -0
  31. package/src/adapters/kiro-tools.ts +43 -15
  32. package/src/adapters/kiro.ts +54 -9
  33. package/src/adapters/openai-chat.ts +286 -242
  34. package/src/adapters/openai-responses.ts +334 -24
  35. package/src/adapters/run-turn-queue.ts +36 -1
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -26
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +27 -7
  44. package/src/cli/account-extended.ts +10 -3
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/init.ts +1 -0
  52. package/src/cli/models-runtime.ts +95 -0
  53. package/src/cli/models.ts +13 -7
  54. package/src/cli/provider-runtime.ts +16 -2
  55. package/src/cli/registry.ts +6 -1
  56. package/src/cli/telemetry-commands.ts +25 -0
  57. package/src/cli/v2.ts +34 -10
  58. package/src/codex/account-pause.ts +2 -1
  59. package/src/codex/account-priority.ts +3 -2
  60. package/src/codex/app-server-processes.ts +80 -6
  61. package/src/codex/auth-api.ts +48 -8
  62. package/src/codex/auth-context.ts +21 -18
  63. package/src/codex/catalog/aggregation.ts +6 -0
  64. package/src/codex/catalog/model-metadata.ts +13 -1
  65. package/src/codex/catalog/native-models.ts +5 -2
  66. package/src/codex/catalog/parsing.ts +16 -0
  67. package/src/codex/catalog/provider-fetch.ts +20 -37
  68. package/src/codex/catalog/sync.ts +127 -2
  69. package/src/codex/catalog.ts +1 -1
  70. package/src/codex/codex-write-lock.ts +3 -1
  71. package/src/codex/convergence-types.ts +1 -1
  72. package/src/codex/convergence.ts +22 -2
  73. package/src/codex/desired-state.ts +2 -2
  74. package/src/codex/desktop-app-restart.ts +18 -5
  75. package/src/codex/inject-coordination.ts +83 -0
  76. package/src/codex/inject.ts +14 -1
  77. package/src/codex/log-guard/inspect.ts +22 -4
  78. package/src/codex/model-entitlements.ts +9 -2
  79. package/src/codex/prompt-layers.ts +371 -25
  80. package/src/codex/prompt-text-probe.ts +238 -0
  81. package/src/codex/quota.ts +123 -18
  82. package/src/codex/routing.ts +9 -0
  83. package/src/codex/subagent-model-fallback.ts +198 -27
  84. package/src/codex/transition-state.ts +107 -8
  85. package/src/combos/types.ts +10 -0
  86. package/src/compatibility/openai-responses.ts +33 -1
  87. package/src/config/autonomous-remediation.ts +21 -0
  88. package/src/config/provider-validation.ts +14 -0
  89. package/src/config/rebase-provenance.ts +68 -0
  90. package/src/config.ts +191 -17
  91. package/src/generated/compatibility-version.json +229 -153
  92. package/src/images/loop.ts +5 -4
  93. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  94. package/src/lab/fabric/producer-child.ts +1 -1
  95. package/src/lib/config-ownership.ts +20 -0
  96. package/src/lib/errors.ts +11 -2
  97. package/src/lib/package-tree-integrity.ts +101 -0
  98. package/src/oauth/aistudio-credentials.ts +65 -0
  99. package/src/oauth/aistudio-native-daemon.ts +54 -0
  100. package/src/oauth/generic-account-failover.ts +231 -0
  101. package/src/oauth/index.ts +57 -5
  102. package/src/oauth/key-providers.ts +10 -1
  103. package/src/oauth/kiro.ts +45 -0
  104. package/src/oauth/login-cli.ts +19 -20
  105. package/src/oauth/types.ts +15 -0
  106. package/src/providers/codex-capacity.ts +5 -2
  107. package/src/providers/command-code-efforts.ts +38 -6
  108. package/src/providers/context-cap.ts +4 -3
  109. package/src/providers/default-aliases.ts +65 -0
  110. package/src/providers/derive.ts +28 -0
  111. package/src/providers/fastwire.ts +7 -1
  112. package/src/providers/model-presets.ts +119 -0
  113. package/src/providers/new-model-policy.ts +146 -0
  114. package/src/providers/provider-id-rewrite.ts +2 -1
  115. package/src/providers/quota.ts +73 -14
  116. package/src/providers/registry.ts +160 -68
  117. package/src/providers/slug-codec.ts +52 -0
  118. package/src/responses/code-mode-helper-compat.ts +50 -0
  119. package/src/responses/custom-tool-compat.ts +34 -10
  120. package/src/responses/parser.ts +4 -0
  121. package/src/responses/schema.ts +5 -1
  122. package/src/responses/thought-signature-replay.ts +17 -0
  123. package/src/router.ts +40 -2
  124. package/src/routing/analytics.ts +1 -0
  125. package/src/routing/quota.ts +10 -0
  126. package/src/server/auth-cors.ts +23 -0
  127. package/src/server/chat-completions.ts +24 -16
  128. package/src/server/chat-native-sse.ts +3 -3
  129. package/src/server/chat-native.ts +30 -11
  130. package/src/server/claude-messages.ts +1 -1
  131. package/src/server/effort-policy.ts +16 -0
  132. package/src/server/index.ts +152 -80
  133. package/src/server/lifecycle.ts +52 -1
  134. package/src/server/management/agent-settings-routes.ts +31 -15
  135. package/src/server/management/codex-prompt-routes.ts +570 -0
  136. package/src/server/management/combo-routes.ts +2 -1
  137. package/src/server/management/config-routes.ts +27 -9
  138. package/src/server/management/context.ts +9 -0
  139. package/src/server/management/logs-usage-routes.ts +1 -1
  140. package/src/server/management/model-routes.ts +266 -0
  141. package/src/server/management/provider-routes.ts +137 -3
  142. package/src/server/management/routing-profile-routes.ts +2 -2
  143. package/src/server/management-api.ts +2 -0
  144. package/src/server/relay-eager.ts +147 -20
  145. package/src/server/relay.ts +251 -19
  146. package/src/server/request-log-conversation.ts +21 -0
  147. package/src/server/request-log.ts +46 -20
  148. package/src/server/responses/collaboration.ts +42 -5
  149. package/src/server/responses/combo-stream-preflight.ts +10 -3
  150. package/src/server/responses/core.ts +571 -137
  151. package/src/server/responses/empty-completion-guard.ts +35 -0
  152. package/src/server/responses/fetch-helpers.ts +14 -6
  153. package/src/server/responses/input-admission.ts +3 -1
  154. package/src/server/responses/passthrough-error.ts +33 -9
  155. package/src/server/responses/responses-field-backfill.ts +105 -13
  156. package/src/server/responses/ws-upstream.ts +35 -5
  157. package/src/server/responses-custom-tool-repair.ts +52 -7
  158. package/src/server/responses-terminal-repair.ts +25 -4
  159. package/src/server/sse-frame-buffer.ts +31 -4
  160. package/src/server/ws-bridge.ts +15 -4
  161. package/src/telemetry/dispatcher.ts +44 -0
  162. package/src/telemetry/fingerprint.ts +24 -0
  163. package/src/telemetry/hook.ts +43 -0
  164. package/src/telemetry/ledger.ts +54 -0
  165. package/src/telemetry/types.ts +23 -0
  166. package/src/types/config.ts +66 -14
  167. package/src/types/provider.ts +77 -0
  168. package/src/types/request.ts +16 -10
  169. package/src/types/tools.ts +20 -25
  170. package/src/types.ts +1 -0
  171. package/src/usage/expected-prices.ts +43 -24
  172. package/src/usage/log.ts +17 -7
  173. package/src/web-search/exa-executor.ts +40 -9
  174. package/src/web-search/loop.ts +5 -4
  175. package/gui/dist/assets/index-CIDo4y4k.js +0 -102
  176. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
  177. package/src/server/aistudio-ws-hub.ts +0 -295
@@ -70,6 +70,7 @@ import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
70
70
  import { handleComboRoutes } from "./management/combo-routes";
71
71
  import { handleSystemRoutes } from "./management/system-routes";
72
72
  import { handleSidebarRoutes } from "./management/sidebar-routes";
73
+ import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
73
74
  import { handleIntegrationRoutes } from "./management/integration-routes";
74
75
  import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
75
76
  import type { ManagementContext } from "./management/context";
@@ -229,6 +230,7 @@ export async function handleManagementAPI(
229
230
  ?? (await handleIntegrationRoutes(ctx))
230
231
  ?? (await handleNativeIntegrationRoutes(ctx))
231
232
  ?? (await handleAgentSettingsRoutes(ctx))
233
+ ?? (await handleCodexPromptRoutes(ctx))
232
234
  ?? (await handleOauthAccountRoutes(ctx))
233
235
  ?? (await handleComboRoutes(ctx))
234
236
  ?? (await handleSystemRoutes(ctx))
@@ -24,7 +24,12 @@
24
24
  * up to the drain window.
25
25
  */
26
26
 
27
- import { buildFailedTailPayload, createSseTerminalOutputBoundary } from "./relay";
27
+ import {
28
+ adapterEofIncompleteFrame,
29
+ createSseTerminalOutputBoundary,
30
+ doneFrame,
31
+ failedTailFrame,
32
+ } from "./relay";
28
33
  import {
29
34
  nextSseBlock,
30
35
  payloadRewriteAsBlockRewrite,
@@ -100,9 +105,19 @@ export function relaySseEagerBounded(
100
105
  const now = opts?.now ?? Date.now;
101
106
 
102
107
  const reader = body.getReader();
108
+ const terminalEncoder = new TextEncoder();
109
+ const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder);
110
+ const terminalSentinel = doneFrame(terminalEncoder);
103
111
  const terminalBoundary = createSseTerminalOutputBoundary();
104
112
  const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks
105
113
  ?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
114
+ const encodeFailedTail = (error: unknown): Uint8Array | null => {
115
+ try {
116
+ return failedTailFrame(terminalEncoder, error);
117
+ } catch {
118
+ return null;
119
+ }
120
+ };
106
121
  const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
107
122
  const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
108
123
  const rewriteBudget = opts?.rewriteBudget;
@@ -164,13 +179,21 @@ export function relaySseEagerBounded(
164
179
  let queuedBytes = 0;
165
180
  let cancelled = false;
166
181
  let done = false;
167
- const terminalSentinel = new TextEncoder().encode("data: [DONE]\n\n");
168
182
  // Pause gate: resolved by client pull, client cancel, or upstream abort so a
169
183
  // paused producer ALWAYS resumes (audit blocker 2 — no deadlock; onDone and
170
184
  // turn unregistration stay reachable, drainAndShutdown never hangs).
171
185
  let wake: (() => void) | null = null;
172
186
  const wakeUp = () => { const w = wake; wake = null; w?.(); };
173
- const paused = () => new Promise<void>(resolve => { wake = resolve; });
187
+ const paused = () => new Promise<void>(resolve => {
188
+ wake = resolve;
189
+ // A pull, cancel, or abort can win the tiny window between the loop's
190
+ // predicate check and installing this resolver. Re-check every wake
191
+ // predicate after installation so that an already-fired wake cannot leave
192
+ // the producer parked forever.
193
+ if (queuedBytes <= maxQueueBytes || cancelled || upstream.signal.aborted) {
194
+ wakeUp();
195
+ }
196
+ });
174
197
  upstream.signal.addEventListener("abort", wakeUp, { once: true });
175
198
 
176
199
  let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
@@ -196,6 +219,9 @@ export function relaySseEagerBounded(
196
219
 
197
220
  const producer = async () => {
198
221
  let syntheticKind: "incomplete" | "failed" | null = null;
222
+ let deliveryFallbackSent = false;
223
+ let priorRewriteFailure = false;
224
+ let priorRewriteError: unknown;
199
225
  // reader.read() is not intrinsically tied to the upstream AbortController
200
226
  // (a fetch body usually rejects on abort, but that coupling is the fetch
201
227
  // implementation's, not the stream's), so abort must break a parked read on
@@ -221,18 +247,46 @@ export function relaySseEagerBounded(
221
247
  if (upstreamDone) {
222
248
  hooks.finishInspection();
223
249
  const boundedTail = terminalBoundary.finish();
250
+ let clientTail = boundedTail;
251
+ let rewriteFailed = false;
252
+ let rewriteError: unknown;
224
253
  if (activeRewrite) {
225
- const rewritten = rewriteOutbound(boundedTail);
226
- const tail = joinUint8Arrays(rewritten, flushRewriteTail());
227
- if (tail.byteLength > 0 && !cancelled) {
228
- queuedBytes += tail.byteLength;
229
- try { controllerRef?.enqueue(tail); } catch { /* client already gone */ }
254
+ try {
255
+ const rewritten = rewriteOutbound(boundedTail);
256
+ clientTail = joinUint8Arrays(rewritten, flushRewriteTail());
257
+ } catch (error) {
258
+ rewriteFailed = true;
259
+ rewriteError = error;
260
+ clientTail = new Uint8Array(0);
261
+ }
262
+ }
263
+ if (rewriteFailed) {
264
+ const safeTail = encodeFailedTail(rewriteError);
265
+ if (safeTail && !cancelled && !upstream.signal.aborted) {
266
+ if (!hooks.sawTerminal()) syntheticKind = "failed";
267
+ queuedBytes += safeTail.byteLength;
268
+ try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
269
+ try { controllerRef?.close(); } catch { /* client already gone */ }
230
270
  }
231
- } else if (boundedTail.byteLength > 0 && !cancelled) {
232
- queuedBytes += boundedTail.byteLength;
233
- try { controllerRef?.enqueue(boundedTail); } catch { /* client already gone */ }
271
+ break;
272
+ }
273
+ if (clientTail.byteLength > 0 && !cancelled) {
274
+ queuedBytes += clientTail.byteLength;
275
+ try { controllerRef?.enqueue(clientTail); } catch { /* client already gone */ }
234
276
  }
235
- if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
277
+ if (terminalBoundary.terminalSeen()) {
278
+ if (!terminalBoundary.doneSeen() && !cancelled) {
279
+ queuedBytes += terminalSentinel.byteLength;
280
+ try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
281
+ }
282
+ } else if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
283
+ // A clean 200 EOF without a Responses terminal must be visible to
284
+ // Codex as one incomplete turn, followed by the normal sentinel.
285
+ queuedBytes += adapterEofFrame.byteLength + terminalSentinel.byteLength;
286
+ try {
287
+ controllerRef?.enqueue(adapterEofFrame);
288
+ controllerRef?.enqueue(terminalSentinel);
289
+ } catch { /* client already gone */ }
236
290
  syntheticKind = "incomplete";
237
291
  }
238
292
  break;
@@ -247,7 +301,21 @@ export function relaySseEagerBounded(
247
301
  continue;
248
302
  }
249
303
  const terminalBounded = terminalBoundary.feed(value);
250
- const outbound = activeRewrite ? rewriteOutbound(terminalBounded) : terminalBounded;
304
+ let outbound: Uint8Array;
305
+ if (activeRewrite) {
306
+ try {
307
+ outbound = rewriteOutbound(terminalBounded);
308
+ } catch (error) {
309
+ // Preserve the first rewrite failure across the teardown flush. A
310
+ // second empty flush may succeed, but the terminal bytes still
311
+ // must not bypass the failed rewrite or become DONE-only output.
312
+ priorRewriteFailure = true;
313
+ priorRewriteError = error;
314
+ throw error;
315
+ }
316
+ } else {
317
+ outbound = terminalBounded;
318
+ }
251
319
  if (outbound.byteLength > 0) {
252
320
  queuedBytes += outbound.byteLength;
253
321
  try {
@@ -278,16 +346,75 @@ export function relaySseEagerBounded(
278
346
  } catch (err) {
279
347
  // Upstream read failure. Distinguish genuine mid-stream reset from
280
348
  // abort-driven teardown (shutdown/cancel-expiry) — audit M3.
281
- if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
349
+ // A read can fail after delivering an unterminated terminal block. Flush
350
+ // both observers before deciding whether this is a synthetic reset so
351
+ // eager mode matches the tee/pull boundary semantics at EOF.
352
+ let boundedTail: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
353
+ let tailTerminal = false;
354
+ let tailDone = false;
355
+ try { hooks.finishInspection(); } catch { /* preserve the original read failure */ }
356
+ try {
357
+ boundedTail = terminalBoundary.finish();
358
+ tailTerminal = terminalBoundary.terminalSeen();
359
+ tailDone = terminalBoundary.doneSeen();
360
+ } catch {
361
+ // A near-cap ambiguous delimiter tail may itself overflow at EOF.
362
+ // Preserve the original read/framing failure and continue emitting
363
+ // the bounded failed tail instead of letting cleanup throw again.
364
+ }
365
+ let clientTail: Uint8Array<ArrayBufferLike> = boundedTail;
366
+ let rewriteFailed = false;
367
+ let rewriteError: unknown;
368
+ if (priorRewriteFailure) {
369
+ rewriteFailed = true;
370
+ rewriteError = priorRewriteError;
371
+ clientTail = new Uint8Array(0);
372
+ } else if (activeRewrite) {
373
+ try {
374
+ const rewritten = rewriteOutbound(boundedTail);
375
+ clientTail = joinUint8Arrays(rewritten, flushRewriteTail());
376
+ } catch (error) {
377
+ rewriteFailed = true;
378
+ rewriteError = error;
379
+ clientTail = new Uint8Array(0);
380
+ }
381
+ }
382
+ if (clientTail.byteLength > 0 && !cancelled && !upstream.signal.aborted) {
383
+ queuedBytes += clientTail.byteLength;
384
+ try { controllerRef?.enqueue(clientTail); } catch { /* client already torn down */ }
385
+ }
386
+ if (rewriteFailed && !cancelled && !upstream.signal.aborted) {
387
+ // Never bypass a client rewrite after it fails: boundedTail can contain
388
+ // provider metadata or content that the active rewrite was required to
389
+ // remove. Emit one safe failed envelope instead. When inspection has
390
+ // already reported the real upstream terminal, this is a delivery
391
+ // fallback only and must not create a second accounting outcome.
392
+ const safeTail = encodeFailedTail(rewriteError ?? err);
393
+ if (safeTail && !cancelled && !upstream.signal.aborted) {
394
+ if (!hooks.sawTerminal()) syntheticKind = "failed";
395
+ deliveryFallbackSent = true;
396
+ queuedBytes += safeTail.byteLength;
397
+ try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
398
+ try { controllerRef?.close(); } catch { /* client already gone */ }
399
+ }
400
+ } else if (tailTerminal && !cancelled && !upstream.signal.aborted) {
401
+ if (!tailDone) {
402
+ queuedBytes += terminalSentinel.byteLength;
403
+ try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
404
+ }
405
+ } else if (!tailTerminal && !cancelled && !upstream.signal.aborted) {
282
406
  // Serializing `err` can run user-defined accessors (Error.message
283
407
  // getters, toString) that re-entrantly cancel the client or abort the
284
408
  // upstream. Build the tail FIRST, then re-check eligibility before
285
409
  // committing to the synthetic terminal (adversarial review blocker).
286
- const tail = new TextEncoder().encode(
287
- `\n\nevent: response.failed\ndata: ${buildFailedTailPayload(err)}\n\ndata: [DONE]\n\n`,
288
- );
289
- if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
290
- syntheticKind = "failed";
410
+ const tail = encodeFailedTail(err);
411
+ if (tail && !cancelled && !upstream.signal.aborted) {
412
+ // Inspection and client framing have separate bounded parsers. If
413
+ // inspection resynchronized after an oversized frame and observed a
414
+ // later real terminal, it still must not suppress a terminal delivery
415
+ // to the client. Only accounting remains tied to the inspected result.
416
+ if (!hooks.sawTerminal()) syntheticKind = "failed";
417
+ deliveryFallbackSent = true;
291
418
  queuedBytes += tail.byteLength;
292
419
  try { controllerRef?.enqueue(tail); } catch { /* client already torn down */ }
293
420
  try { controllerRef?.close(); } catch { /* client already torn down */ }
@@ -306,7 +433,7 @@ export function relaySseEagerBounded(
306
433
  if (cancelled && !hooks.sawTerminal()) {
307
434
  hooks.onClientCancel();
308
435
  }
309
- if (cancelled || upstream.signal.aborted || syntheticKind === "failed") {
436
+ if (cancelled || upstream.signal.aborted || syntheticKind === "failed" || deliveryFallbackSent) {
310
437
  upstream.abort();
311
438
  reader.cancel().catch(() => {});
312
439
  }
@@ -1,4 +1,12 @@
1
1
  import type { ResponsesTerminalStatus } from "../bridge";
2
+ import {
3
+ cyberPolicyErrorType,
4
+ CYBER_POLICY_ERROR_CODE,
5
+ CYBER_POLICY_FALLBACK_MESSAGE,
6
+ isCyberPolicyCode,
7
+ isCyberPolicyMessage,
8
+ } from "../lib/errors";
9
+ import { redactSecretString } from "../lib/redact";
2
10
  import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
3
11
  import { isUsageDebugEnabled } from "../usage/debug";
4
12
  import {
@@ -16,6 +24,7 @@ import {
16
24
  joinSseFrameBytes,
17
25
  MAX_CLIENT_SSE_FRAME_BYTES,
18
26
  } from "./sse-frame-buffer";
27
+ import { replaceSseDataPayload } from "./sse-payload-rewrite";
19
28
 
20
29
  const nativePassthroughSseResponses = new WeakSet<Response>();
21
30
  const eagerRelaySseResponses = new WeakSet<Response>();
@@ -24,6 +33,30 @@ export const MAX_INSPECTION_SSE_FRAME_BYTES = MAX_CLIENT_SSE_FRAME_BYTES;
24
33
  export const MAX_COMPLETED_OUTPUT_ITEMS = 256;
25
34
  export const MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES = 8 * 1024 * 1024;
26
35
  export const MAX_TAIL_ERROR_MESSAGE_CHARS = 512;
36
+ const ADAPTER_EOF_INCOMPLETE_PAYLOAD = JSON.stringify({
37
+ type: "response.incomplete",
38
+ response: {
39
+ status: "incomplete",
40
+ incomplete_details: { reason: "adapter_eof" },
41
+ },
42
+ });
43
+ const DONE_SSE_FRAME_TEXT = "data: [DONE]\n\n";
44
+ const FAILED_TAIL_FALLBACK_PAYLOAD = JSON.stringify({
45
+ type: "response.failed",
46
+ response: {
47
+ status: "failed",
48
+ error: {
49
+ type: "upstream_error",
50
+ code: "upstream_reset",
51
+ message: "Upstream stream terminated unexpectedly",
52
+ },
53
+ last_error: {
54
+ type: "upstream_error",
55
+ code: "upstream_reset",
56
+ message: "Upstream stream terminated unexpectedly",
57
+ },
58
+ },
59
+ });
27
60
 
28
61
  export type InspectionCounters = {
29
62
  frameBufferHighWaterBytes: number;
@@ -99,6 +132,21 @@ export function buildFailedTailPayload(err: unknown): string {
99
132
  });
100
133
  }
101
134
 
135
+ function buildFailedTailPayloadOrFallback(err: unknown): string {
136
+ try {
137
+ return buildFailedTailPayload(err);
138
+ } catch {
139
+ // Error.message and String(error) may execute hostile accessors. Preserve a
140
+ // bounded protocol terminal even when diagnostic serialization is unsafe.
141
+ return FAILED_TAIL_FALLBACK_PAYLOAD;
142
+ }
143
+ }
144
+
145
+ export function failedTailFrame(encoder: TextEncoder, err: unknown): Uint8Array {
146
+ const payload = buildFailedTailPayloadOrFallback(err);
147
+ return encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`);
148
+ }
149
+
102
150
  export type SseTerminalOutputBoundary = {
103
151
  feed(chunk: Uint8Array): Uint8Array;
104
152
  finish(): Uint8Array;
@@ -111,13 +159,16 @@ export type SseTerminalOutputBoundary = {
111
159
  * Frame-aware client output boundary shared by both native Responses relays.
112
160
  * It buffers only the current incomplete SSE block under the same hard byte
113
161
  * cap as inspection, forwards complete blocks through the first Responses
114
- * terminal, and drops every later block/byte.
162
+ * terminal, and drops every later block/byte. A premature [DONE] is held until
163
+ * a terminal arrives so clean EOF can synthesize one terminal and one sentinel.
115
164
  */
116
165
  export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
117
166
  const decoder = new TextDecoder();
167
+ const encoder = new TextEncoder();
118
168
  const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES);
119
169
  let terminal = false;
120
170
  let done = false;
171
+ let pendingDone: { block: Uint8Array; delimiter: Uint8Array } | null = null;
121
172
  let disposed = false;
122
173
 
123
174
  const processFrames = (
@@ -129,16 +180,37 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
129
180
  for (const frame of frames) {
130
181
  const payload = sseDataPayload(decoder.decode(frame.block));
131
182
  const isDone = payload === "[DONE]";
132
- // Preserve every frame through the first Responses terminal. A [DONE]
133
- // frame is also preserved when it immediately follows that terminal in
134
- // the same upstream chunk; every later non-DONE frame is dropped.
135
- if (!responsesTerminal || isDone) output.push(frame.block, frame.delimiter);
183
+ const parsed = payload === null ? undefined : parseSsePayload(payload);
184
+ const policyError = parsed !== undefined && isPolicyRewriteType(parsed)
185
+ ? cyberPolicyTerminalError(parsed)
186
+ : undefined;
187
+ const outboundBlock = policyError
188
+ ? encoder.encode(rewritePolicyTerminalBlock(
189
+ decoder.decode(frame.block),
190
+ policyFailurePayload(policyError, parsed),
191
+ ))
192
+ : frame.block;
136
193
  if (isDone) {
137
194
  done = true;
195
+ if (responsesTerminal) {
196
+ output.push(outboundBlock, frame.delimiter);
197
+ } else if (!pendingDone) {
198
+ // Do not expose a sentinel before a Responses terminal. If EOF
199
+ // follows, the synthetic incomplete path owns the one sentinel;
200
+ // if a terminal arrives later, this pending frame is emitted then.
201
+ pendingDone = { block: outboundBlock, delimiter: frame.delimiter };
202
+ }
138
203
  continue;
139
204
  }
140
- if (!responsesTerminal && payload && terminalStatusFromSsePayload(payload)) {
205
+ // Preserve every frame through the first Responses terminal. Every
206
+ // later non-DONE frame is dropped.
207
+ if (!responsesTerminal) output.push(outboundBlock, frame.delimiter);
208
+ if (!responsesTerminal && payload && terminalStatusFromParsed(parsed)) {
141
209
  responsesTerminal = true;
210
+ if (pendingDone) {
211
+ output.push(pendingDone.block, pendingDone.delimiter);
212
+ pendingDone = null;
213
+ }
142
214
  }
143
215
  }
144
216
  if (responsesTerminal) {
@@ -155,13 +227,22 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
155
227
  },
156
228
  finish() {
157
229
  if (disposed || terminal) return new Uint8Array(0);
158
- return framer.finish();
230
+ const tail = framer.finish();
231
+ if (tail.byteLength === 0) return new Uint8Array(0);
232
+ // EOF may cut off the final SSE block before its blank-line delimiter.
233
+ // Feed it through the exact same parser/rewrite/terminal path as a
234
+ // complete frame, using a synthetic delimiter so the client receives a
235
+ // dispatchable event rather than an unterminated tail.
236
+ const tailText = decoder.decode(tail);
237
+ const delimiter = encoder.encode(tailText.includes("\r\n") ? "\r\n\r\n" : "\n\n");
238
+ return processFrames([{ block: tail, delimiter }]);
159
239
  },
160
240
  terminalSeen: () => terminal,
161
241
  doneSeen: () => done,
162
242
  dispose() {
163
243
  if (disposed) return;
164
244
  disposed = true;
245
+ pendingDone = null;
165
246
  framer.dispose();
166
247
  },
167
248
  };
@@ -198,7 +279,7 @@ export function relaySseWithFailedTail(
198
279
  // Preserve through the terminal block only, add the conventional sentinel
199
280
  // when there was no real [DONE] data event, then stop reading upstream.
200
281
  if (!terminalBoundary.doneSeen()) {
201
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
282
+ controller.enqueue(doneFrame(encoder));
202
283
  }
203
284
  closed = true;
204
285
  controller.close();
@@ -219,6 +300,16 @@ export function relaySseWithFailedTail(
219
300
  if (done) {
220
301
  const tail = terminalBoundary.finish();
221
302
  if (tail.byteLength > 0) controller.enqueue(tail);
303
+ if (terminalBoundary.terminalSeen()) {
304
+ if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
305
+ } else {
306
+ // A clean upstream EOF is still a failed Responses turn when no
307
+ // protocol terminal arrived. Make that state explicit so Codex
308
+ // does not treat HTTP 200 + bare EOF as a retryable disconnect.
309
+ const incomplete = adapterEofIncompleteFrame(encoder);
310
+ controller.enqueue(incomplete);
311
+ controller.enqueue(doneFrame(encoder));
312
+ }
222
313
  terminalBoundary.dispose();
223
314
  controller.close();
224
315
  return;
@@ -228,8 +319,10 @@ export function relaySseWithFailedTail(
228
319
  }
229
320
  } catch (err) {
230
321
  let partial: Uint8Array = new Uint8Array(0);
322
+ let tailTerminal = false;
231
323
  try {
232
324
  partial = terminalBoundary.finish();
325
+ tailTerminal = terminalBoundary.terminalSeen();
233
326
  } catch {
234
327
  // A near-cap ambiguous delimiter tail may itself overflow at EOF.
235
328
  // Preserve the original read/framing failure and continue emitting
@@ -237,11 +330,14 @@ export function relaySseWithFailedTail(
237
330
  }
238
331
  terminalBoundary.dispose();
239
332
  if (closed) return;
240
- const payload = buildFailedTailPayload(err);
241
333
  try {
242
334
  if (partial.byteLength > 0) controller.enqueue(partial);
243
- // Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
244
- controller.enqueue(encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\ndata: [DONE]\n\n`));
335
+ if (tailTerminal) {
336
+ if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
337
+ } else {
338
+ // Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
339
+ controller.enqueue(failedTailFrame(encoder, err));
340
+ }
245
341
  controller.close();
246
342
  } catch { /* client already torn down */ }
247
343
  upstream.abort();
@@ -276,15 +372,139 @@ export function sseDataPayload(block: string): string | null {
276
372
  return data.length > 0 ? data.join("\n") : null;
277
373
  }
278
374
 
279
- export function terminalStatusFromSsePayload(payload: string): ResponsesTerminalStatus | null {
280
- if (payload === "[DONE]") return null;
375
+ type JsonRecord = Record<string, unknown>;
376
+
377
+ function asJsonRecord(value: unknown): JsonRecord | null {
378
+ return value !== null && typeof value === "object" && !Array.isArray(value)
379
+ ? value as JsonRecord
380
+ : null;
381
+ }
382
+
383
+ function stringField(record: JsonRecord | null, key: string): string | undefined {
384
+ const value = record?.[key];
385
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
386
+ }
387
+
388
+ /**
389
+ * Return a high-confidence policy error carried by an upstream terminal shape.
390
+ * Deliberately inspect structured error fields and known refusal copy only — a
391
+ * bare `cyber_policy` token in an unrelated payload is not sufficient.
392
+ */
393
+ function cyberPolicyTerminalError(parsed: unknown): { message: string; type?: string } | undefined {
394
+ const root = asJsonRecord(parsed);
395
+ if (!root) return undefined;
396
+ const response = asJsonRecord(root.response);
397
+ const candidates = [
398
+ asJsonRecord(root.error),
399
+ asJsonRecord(root.last_error),
400
+ asJsonRecord(response?.error),
401
+ asJsonRecord(response?.incomplete_details),
402
+ root,
403
+ response,
404
+ ];
405
+ for (const candidate of candidates) {
406
+ if (!candidate) continue;
407
+ if (isCyberPolicyCode(stringField(candidate, "code"))) {
408
+ return {
409
+ message: stringField(candidate, "message")
410
+ ?? CYBER_POLICY_FALLBACK_MESSAGE,
411
+ ...(stringField(candidate, "type") ? { type: stringField(candidate, "type") } : {}),
412
+ };
413
+ }
414
+ }
415
+ for (const candidate of candidates) {
416
+ const message = stringField(candidate, "message");
417
+ if (message && isCyberPolicyMessage(message)) {
418
+ return {
419
+ message,
420
+ ...(stringField(candidate, "type") ? { type: stringField(candidate, "type") } : {}),
421
+ };
422
+ }
423
+ }
424
+ return undefined;
425
+ }
426
+
427
+ function parseSsePayload(payload: string): unknown | undefined {
428
+ if (payload === "[DONE]") return undefined;
281
429
  try {
282
- return terminalStatusFromParsed(JSON.parse(payload));
430
+ return JSON.parse(payload);
283
431
  } catch {
284
- return null;
432
+ return undefined;
285
433
  }
286
434
  }
287
435
 
436
+ function isPolicyRewriteType(parsed: unknown): boolean {
437
+ const type = asJsonRecord(parsed)?.type;
438
+ return type === "response.failed" || type === "response.incomplete" || type === "error";
439
+ }
440
+
441
+ function rewritePolicyTerminalBlock(block: string, payload: string): string {
442
+ const newline = block.includes("\r\n") ? "\r\n" : "\n";
443
+ const rewritten = replaceSseDataPayload(block, payload);
444
+ const lines = rewritten.split(/\r?\n/);
445
+ let eventRewritten = false;
446
+ const withEvent = lines.map(line => {
447
+ if (!eventRewritten && line.startsWith("event:")) {
448
+ eventRewritten = true;
449
+ return "event: response.failed";
450
+ }
451
+ return line;
452
+ });
453
+ if (!eventRewritten) withEvent.unshift("event: response.failed");
454
+ return withEvent.join(newline);
455
+ }
456
+
457
+ function policyFailurePayload(policyError: { message: string; type?: string }, parsed: unknown): string {
458
+ const error = {
459
+ type: cyberPolicyErrorType(policyError.type),
460
+ code: CYBER_POLICY_ERROR_CODE,
461
+ message: redactSecretString(policyError.message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS),
462
+ };
463
+ const root = asJsonRecord(parsed);
464
+ const originalResponse = asJsonRecord(root?.response);
465
+ const preservedResponse = Object.fromEntries(
466
+ Object.entries(originalResponse ?? {}).filter(([key]) => key !== "incomplete_details"),
467
+ );
468
+ const response = {
469
+ ...preservedResponse,
470
+ status: "failed",
471
+ error,
472
+ last_error: error,
473
+ retryable: false,
474
+ };
475
+ // Responses event metadata such as sequence_number is normally top-level.
476
+ // Keep it (and any other non-error, non-response fields) while replacing only
477
+ // the protocol type and error envelope.
478
+ const preservedRoot = root
479
+ ? Object.fromEntries(Object.entries(root).filter(([key]) => (
480
+ key !== "type"
481
+ && key !== "response"
482
+ && key !== "error"
483
+ && key !== "last_error"
484
+ && key !== "retryable"
485
+ )))
486
+ : {};
487
+ return JSON.stringify({
488
+ ...preservedRoot,
489
+ type: "response.failed",
490
+ retryable: false,
491
+ response,
492
+ });
493
+ }
494
+
495
+ export function adapterEofIncompleteFrame(encoder: TextEncoder): Uint8Array {
496
+ return encoder.encode(`event: response.incomplete\ndata: ${ADAPTER_EOF_INCOMPLETE_PAYLOAD}\n\n`);
497
+ }
498
+
499
+ export function doneFrame(encoder: TextEncoder): Uint8Array {
500
+ return encoder.encode(DONE_SSE_FRAME_TEXT);
501
+ }
502
+
503
+ export function terminalStatusFromSsePayload(payload: string): ResponsesTerminalStatus | null {
504
+ if (payload === "[DONE]") return null;
505
+ return terminalStatusFromParsed(parseSsePayload(payload));
506
+ }
507
+
288
508
  /** True when a native Responses SSE payload carries the FIRST kind of non-empty model output. */
289
509
  export function isFirstOutputSsePayload(payload: string | null): boolean {
290
510
  if (!payload || payload === "[DONE]") return false;
@@ -322,14 +542,16 @@ function createFirstOutputReporter(onFirstOutput?: () => void): {
322
542
  }
323
543
 
324
544
  export function terminalStatusFromParsed(parsed: unknown): ResponsesTerminalStatus | null {
325
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
326
- switch ((parsed as { type?: unknown }).type) {
545
+ const type = asJsonRecord(parsed)?.type;
546
+ switch (type) {
327
547
  case "response.completed":
328
548
  return "completed";
329
549
  case "response.failed":
330
550
  return "failed";
331
551
  case "response.incomplete":
332
- return "incomplete";
552
+ return cyberPolicyTerminalError(parsed) ? "failed" : "incomplete";
553
+ case "error":
554
+ return cyberPolicyTerminalError(parsed) ? "failed" : null;
333
555
  default:
334
556
  return null;
335
557
  }
@@ -800,6 +1022,9 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
800
1022
  }
801
1023
  reportFirstOutput.parsed(parsed);
802
1024
  const status = terminalStatusFromParsed(parsed);
1025
+ const policyTerminal = status === "failed"
1026
+ && isPolicyRewriteType(parsed)
1027
+ && cyberPolicyTerminalError(parsed) !== undefined;
803
1028
  if (status) sawTerminal = true;
804
1029
  if (!reported && handlers.onTerminal && status) {
805
1030
  try {
@@ -808,7 +1033,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
808
1033
  handlers.logCtx.transportPhase = "terminal_sse";
809
1034
  handlers.logCtx.terminalSource = "upstream";
810
1035
  }
811
- handlers.onTerminal(status);
1036
+ handlers.onTerminal(status, policyTerminal ? 400 : undefined);
812
1037
  } finally {
813
1038
  if (status === "failed" || status === "incomplete") clearCompletedItems();
814
1039
  }
@@ -1087,6 +1312,13 @@ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
1087
1312
  }
1088
1313
  }
1089
1314
  } catch {
1315
+ // A read error can follow a final SSE block without its blank-line
1316
+ // delimiter. Flush that candidate before classifying the transport as a
1317
+ // synthetic reset; otherwise a real completed/failed/policy terminal is
1318
+ // downgraded to 502 on the inspection branch.
1319
+ if (!cancelled) {
1320
+ try { inspector.finish(); } catch { /* preserve the original read error */ }
1321
+ }
1090
1322
  if (clientGone) clientGoneWithoutTerminal = !inspector.terminalSeen();
1091
1323
  else if (!cancelled) options.onReadError?.();
1092
1324
  } finally {