@yansigit/opencodex 2.31.3 → 2.33.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 (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -1,6 +1,6 @@
1
1
  import type { Server } from "bun";
2
2
  import { randomUUID } from "node:crypto";
3
- import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
3
+ import { adapterEventDiagnosticDetails, bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type BridgeDiagnosticContext, type BridgeDiagnosticSequence, type ResponsesTerminalStatus } from "../../bridge";
4
4
  import { formatPassthroughUpstreamError } from "./passthrough-error";
5
5
  import {
6
6
  createResponsesFieldBackfillBlockRewrite,
@@ -14,6 +14,8 @@ import {
14
14
  resolveEnvValue,
15
15
  } from "../../config";
16
16
  import { parseRequest } from "../../responses/parser";
17
+ import { googleProviderOptionsRouteError } from "../../responses/google-provider-options";
18
+ import type { ProviderAdapter } from "../../adapters/base";
17
19
  import {
18
20
  bindReasoningReplayScope,
19
21
  commitReasoningReplayServingIdentity,
@@ -22,8 +24,10 @@ import {
22
24
  durableReplayDestinationIdentity,
23
25
  durableReplayCredentialIdentity,
24
26
  reasoningReplayKeyCredentialIdentity,
27
+ reasoningReplayOpaqueBlobRejectionMemoized,
25
28
  reasoningReplayOAuthCredentialIdentity,
26
29
  reasoningReplayServingIdentityChanged,
30
+ rememberReasoningReplayOpaqueBlobRejection,
27
31
  } from "../../responses/reasoning-replay-cache";
28
32
  import { awaitThoughtSignatureDurability, thoughtSignatureReplaySalt } from "../../responses/thought-signature-replay";
29
33
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
@@ -68,9 +72,12 @@ import {
68
72
  pickComboTarget,
69
73
  targetKey,
70
74
  } from "../../combos";
71
- import { isInjectionDebugEnabled } from "../../lib/debug-settings";
75
+ import { isDebugEnabled, isInjectionDebugEnabled } from "../../lib/debug-settings";
76
+ import { debugStreamDiagnostic } from "../../lib/debug";
72
77
  import { injectionDebugLog } from "../../lib/injection-debug-log";
78
+ import { OcxRequestValidationError } from "../../lib/errors";
73
79
  import { resolveClientRetryAfter } from "../../lib/retry-after";
80
+ import { antigravityOAuthDestinationConfigError, isAntigravityOAuthProvider } from "../../lib/provider-tls-profile";
74
81
  import { enrichOpenCodeZenRateLimitMessage } from "../../providers/opencode-zen-rate-limit";
75
82
  import { modelInList, namespacedToolName } from "../../types";
76
83
  import type {
@@ -88,6 +95,7 @@ import {
88
95
  forceRefreshOAuthAccessSnapshot,
89
96
  getOAuthCredentialApiBaseUrl,
90
97
  getValidAccessTokenForAccount,
98
+ getValidAccessTokenSnapshotForAccount,
91
99
  getValidAccessTokenSnapshot,
92
100
  publicOAuthAuthenticationErrorMessage,
93
101
  type OAuthAccessSnapshot,
@@ -106,12 +114,12 @@ import {
106
114
  rotateAnthropicAccountOn429,
107
115
  } from "../../oauth/anthropic-routing";
108
116
  import {
109
- antigravity429StickWaitMs,
110
117
  antigravitySessionKeyFromParts,
111
- bindAntigravityProject,
112
118
  bindAntigravitySessionAffinity,
119
+ recordAntigravityCooldown,
120
+ recordAntigravitySyntheticFailure,
113
121
  resolveAntigravityAccountForSession,
114
- rotateAntigravityAccountOn429,
122
+ retryableAntigravity429DelayMs,
115
123
  } from "../../oauth/antigravity-routing";
116
124
  import {
117
125
  CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST,
@@ -133,16 +141,12 @@ import {
133
141
  applyCodexAuthContextToProvider,
134
142
  codexPoolAffinityKey,
135
143
  CodexAccountCooldownError,
136
- codexMainProfileDrainingResponse,
137
- cooldownErrorResponse,
138
144
  CodexAuthContextError,
139
- CodexDirectAuthenticationError,
140
145
  CodexMainProfileDrainingError,
141
146
  CodexPoolAuthenticationError,
142
147
  CodexThreadAffinityExpiredError,
143
148
  headersForCodexAuthContext,
144
149
  materializeCodexUpstreamAuth,
145
- CodexMainSubstitutionUnavailableError,
146
150
  isCodexAuthContextUsable,
147
151
  resolveCodexAuthContext,
148
152
  codexProbeLeaseId,
@@ -172,6 +176,7 @@ import {
172
176
  fetchWithResetRetry,
173
177
  fetchWithTransientRetry,
174
178
  prepareSameTarget429Wait,
179
+ sleepWithAbort,
175
180
  } from "../../lib/upstream-retry";
176
181
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
177
182
  import type { DataPlaneAdmission } from "../auth-cors";
@@ -213,7 +218,7 @@ import {
213
218
  rotateProviderTransportOn429,
214
219
  } from "../../providers/key-failover";
215
220
  import { shouldAttemptImageTierRetry } from "../image-retry";
216
- import { resolveProviderTransport } from "../../providers/xai-transport";
221
+ import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
217
222
  import type { WsData } from "../ws-bridge";
218
223
  import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
219
224
  import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
@@ -245,6 +250,7 @@ import {
245
250
  import {
246
251
  conversationIdFromResponsesRequest,
247
252
  normalizeLogConversationId,
253
+ reasoningReplayConversationIdFromResponsesRequest,
248
254
  sessionIdHeaderFromRequest,
249
255
  } from "../request-log-conversation";
250
256
  import type { AttemptRecoveryKind } from "../../usage/log";
@@ -291,6 +297,8 @@ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from ".
291
297
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
292
298
 
293
299
  import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
300
+ import { decideV2NativeParentOverride } from "./v2-native-parent-override";
301
+ import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
294
302
  import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
295
303
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
296
304
  import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
@@ -326,11 +334,16 @@ import {
326
334
  type RoutedNamespaceToolAliases,
327
335
  } from "../../responses/namespace-tool-compat";
328
336
  import {
337
+ collectDeclaredNamelessClientCallTypes,
329
338
  collectDeclaredWireToolNames,
339
+ collectProviderExecutedCallTypes,
330
340
  createUndeclaredToolCallGuardBlockRewrite,
341
+ currentTurnWireToolCatalogBody,
342
+ hasExplicitWireToolCatalog,
331
343
  undeclaredToolCallMessage,
332
344
  undeclaredToolCallName,
333
345
  undeclaredToolCallNameInResponse,
346
+ type ProviderExecutedCallType,
334
347
  } from "../responses-undeclared-tool-guard";
335
348
  import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
336
349
  import { responsesJsonToSseStream } from "../responses-json-events";
@@ -339,6 +352,40 @@ import {
339
352
  emptyCompletionRetryEnabled,
340
353
  guardEmptyCompletionEventStream,
341
354
  } from "./empty-completion-guard";
355
+ import { preflightComboStreamResponse } from "./combo-stream-preflight";
356
+
357
+ // runTurn adapters own an event queue and perform their combo preflight before
358
+ // bridging. A second byte-stream reader would reinterpret that transport's
359
+ // already-committed event boundary and can replay custom adapter work.
360
+ const runTurnAdapterSseResponses = new WeakSet<Response>();
361
+
362
+ function diagnoseAdapterEvents(
363
+ events: AsyncIterable<AdapterEvent>,
364
+ adapterName: string,
365
+ requestId: string | undefined,
366
+ logCtx: RequestLogContext,
367
+ state: BridgeDiagnosticSequence,
368
+ ): AsyncIterable<AdapterEvent> {
369
+ if (!requestId) return events;
370
+ return (async function* () {
371
+ for await (const event of events) {
372
+ const attempt = logCtx.activeAttempt;
373
+ debugStreamDiagnostic(
374
+ {
375
+ requestId,
376
+ adapterName,
377
+ ...(attempt?.ordinal !== undefined ? { attempt: attempt.ordinal } : {}),
378
+ ...(attempt?.recoveryKinds.at(-1) !== undefined ? { recovery: attempt.recoveryKinds.at(-1) } : {}),
379
+ },
380
+ "adapter",
381
+ ++state.value,
382
+ event.type,
383
+ adapterEventDiagnosticDetails(event),
384
+ );
385
+ yield event;
386
+ }
387
+ })();
388
+ }
342
389
 
343
390
  /**
344
391
  * Adapters whose continuation state must survive Codex's store:false requests.
@@ -535,6 +582,9 @@ function bindRouteReasoningReplayScope(args: {
535
582
  if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
536
583
  parsed._stripReasoningEncryptedContent = true;
537
584
  }
585
+ if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
586
+ parsed._stripReasoningEncryptedContent = true;
587
+ }
538
588
  bindProviderContinuationForRoute(parsed, continuationOwner);
539
589
  }
540
590
 
@@ -648,6 +698,26 @@ async function opaqueBlobRejectionBodyForRecovery(
648
698
  }
649
699
  }
650
700
 
701
+ /**
702
+ * Materialize an upstream error body only when the bounded reader observed a complete,
703
+ * display-safe payload. Partial timeout and over-limit prefixes are attacker-controlled,
704
+ * so callers keep their existing status-only fallback instead.
705
+ */
706
+ export async function readDisplaySafeErrorText(
707
+ response: Response,
708
+ signal: AbortSignal,
709
+ fallback: string,
710
+ ): Promise<string> {
711
+ try {
712
+ const body = await readBoundedResponseBody(response, { signal });
713
+ return body.displaySafe ? body.text : fallback;
714
+ } catch {
715
+ // Preserve the former Response.text().catch(fallback) contract. Request-abort
716
+ // classification remains owned by the surrounding response pipeline.
717
+ return fallback;
718
+ }
719
+ }
720
+
651
721
  function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
652
722
  parsed._stripReasoningEncryptedContent = true;
653
723
  }
@@ -688,9 +758,20 @@ async function attemptOpaqueBlobRecovery(
688
758
  }
689
759
 
690
760
  args.guard.attempted = true;
761
+ const rejectedScope = args.parsed._reasoningReplayScope
762
+ ? {
763
+ clientThreadId: args.parsed._reasoningReplayScope.clientThreadId,
764
+ ...(args.parsed._reasoningReplayScope.current
765
+ ? { current: { ...args.parsed._reasoningReplayScope.current } }
766
+ : {}),
767
+ }
768
+ : undefined;
691
769
  prepareOpaqueBlobRecovery(args.parsed);
692
770
  try { void args.response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
693
771
  const result = await rebuild("opaque-blob-rejection");
772
+ if (!("failed" in result) && result.ok) {
773
+ rememberReasoningReplayOpaqueBlobRejection(rejectedScope);
774
+ }
694
775
  return "failed" in result
695
776
  ? { kind: "failed", response: result.failed }
696
777
  : { kind: "recovered", response: result };
@@ -1517,44 +1598,20 @@ async function resolveResponsesCodexAuth(
1517
1598
  substituteMainCredential,
1518
1599
  };
1519
1600
  } catch (err) {
1520
- if (err instanceof CodexAccountCooldownError) {
1521
- return { ok: false, response: cooldownErrorResponse(err, Date.now(), route.codexAccountNamespace) };
1522
- }
1523
- if (err instanceof CodexMainProfileDrainingError) {
1524
- return { ok: false, response: codexMainProfileDrainingResponse() };
1525
- }
1526
- if (err instanceof CodexThreadAffinityExpiredError) {
1527
- return {
1528
- ok: false,
1529
- response: formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"),
1530
- };
1531
- }
1532
1601
  if (err instanceof CodexAuthContextError) {
1533
1602
  const safeAccountLabel = route.codexAccountNamespace
1534
1603
  ? `${route.providerName}-${route.codexAccountNamespace}`
1535
1604
  : formatCodexProviderForLog(route.providerName, err.accountId, config);
1536
1605
  console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
1537
- return {
1538
- ok: false,
1539
- response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
1540
- };
1541
- }
1542
- if (err instanceof CodexPoolAuthenticationError) {
1543
- return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
1544
- }
1545
- if (err instanceof CodexDirectAuthenticationError) {
1546
- return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
1547
1606
  }
1548
1607
  if (err instanceof ForwardAdmissionCredentialError) {
1549
1608
  return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
1550
1609
  }
1551
- if (err instanceof CodexMainSubstitutionUnavailableError) {
1552
- // Fail BEFORE any upstream I/O. The alternative is forwarding the admission secret.
1553
- return {
1554
- ok: false,
1555
- response: formatErrorResponse(401, "authentication_error", "No usable Codex main credential to serve this request"),
1556
- };
1557
- }
1610
+ const response = mapCodexAuthContextErrorToResponse(err, {
1611
+ accountSelector: route.codexAccountNamespace,
1612
+ now: Date.now(),
1613
+ });
1614
+ if (response) return { ok: false, response };
1558
1615
  throw err;
1559
1616
  }
1560
1617
  }
@@ -1932,6 +1989,31 @@ export async function handleComboResponses(
1932
1989
  return clientCancelledResponse();
1933
1990
  }
1934
1991
 
1992
+ if (response.ok && !runTurnAdapterSseResponses.has(response)) {
1993
+ const nativePassthrough = isNativePassthroughSseResponse(response);
1994
+ const eagerRelay = isEagerRelaySseResponse(response);
1995
+ let preflight;
1996
+ try {
1997
+ preflight = await preflightComboStreamResponse(response, childLog);
1998
+ } catch (error) {
1999
+ callbackGate.discard();
2000
+ if (options.abortSignal?.aborted) {
2001
+ retainCancelledAttempt();
2002
+ return clientCancelledResponse();
2003
+ }
2004
+ throw error;
2005
+ }
2006
+ if (preflight.kind === "failed") {
2007
+ callbackGate.discard();
2008
+ terminalRecorder?.("failed", preflight.response.status);
2009
+ response = preflight.response;
2010
+ } else {
2011
+ response = preflight.response;
2012
+ if (nativePassthrough) markNativePassthroughSseResponse(response);
2013
+ if (eagerRelay) markEagerRelaySseResponse(response);
2014
+ }
2015
+ }
2016
+
1935
2017
  if (response.ok) {
1936
2018
  if (!attempt) return response;
1937
2019
  sealRequestAttemptIdentity(
@@ -1989,7 +2071,7 @@ export async function handleComboResponses(
1989
2071
  );
1990
2072
  finishRequestAttempt(
1991
2073
  attempt,
1992
- response.status,
2074
+ failure.response.status,
1993
2075
  Date.now() - (started ?? Date.now()),
1994
2076
  failure.usage,
1995
2077
  );
@@ -2004,7 +2086,7 @@ export async function handleComboResponses(
2004
2086
  return lastFailure;
2005
2087
  }
2006
2088
  console.warn(
2007
- `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${started === undefined ? 0 : Date.now() - started}ms`,
2089
+ `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${started === undefined ? 0 : Date.now() - started}ms`,
2008
2090
  );
2009
2091
  const nextPick = advanceComboAfterFailure(config, pick, {
2010
2092
  retryAfter: failure.retryAfter,
@@ -2212,6 +2294,7 @@ async function handleResponsesInner(
2212
2294
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
2213
2295
  try {
2214
2296
  parsed = parseRequest(body);
2297
+ parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort === true;
2215
2298
  toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
2216
2299
  if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
2217
2300
  const providerContinuationCandidate = options.comboReplaySnapshot
@@ -2220,7 +2303,6 @@ async function handleResponsesInner(
2220
2303
  if (providerContinuationCandidate) parsed._providerContinuationCandidate = providerContinuationCandidate;
2221
2304
  if (inboundClientThreadId) {
2222
2305
  parsed._clientThreadId = inboundClientThreadId;
2223
- parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId };
2224
2306
  } else if (
2225
2307
  options.inboundWire === "anthropic"
2226
2308
  && options.promptCacheKeyIsSharedCohort !== true
@@ -2256,15 +2338,31 @@ async function handleResponsesInner(
2256
2338
  ...(force ? { force: true } : {}),
2257
2339
  ...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}),
2258
2340
  });
2259
- // Prefer a pre-populated id (routed Claude) over Responses headers that may be
2260
- // absent or synthetically injected (session_id from prompt_cache_key).
2261
- if (!logCtx.conversationId) {
2262
- logCtx.conversationId = conversationIdFromResponsesRequest({
2341
+ const resolvedConversationId = conversationIdFromResponsesRequest({
2342
+ clientThreadId: parsed._clientThreadId,
2343
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2344
+ threadIdHeader: req.headers.get("thread-id"),
2345
+ cursorConversationId: parsed._cursorConversationId,
2346
+ });
2347
+ // _clientThreadId remains the routing/continuation identity supplied by Codex. Replay state uses
2348
+ // a dedicated raw conversation namespace so mixed headers that carry the same identity still
2349
+ // match, and a shared/synthetic session_id cannot coalesce distinct thread/Cursor conversations.
2350
+ // Keep an Anthropic prompt_cache_key scope already bound above (#1735/#1926).
2351
+ if (!parsed._reasoningReplayScope) {
2352
+ const reasoningReplayConversationId = reasoningReplayConversationIdFromResponsesRequest({
2263
2353
  clientThreadId: parsed._clientThreadId,
2264
- sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2265
2354
  threadIdHeader: req.headers.get("thread-id"),
2266
2355
  cursorConversationId: parsed._cursorConversationId,
2356
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2267
2357
  });
2358
+ if (reasoningReplayConversationId) {
2359
+ parsed._reasoningReplayScope = { clientThreadId: reasoningReplayConversationId };
2360
+ }
2361
+ }
2362
+ // Prefer a pre-populated id (routed Claude) over Responses headers that may be
2363
+ // absent or synthetically injected (session_id from prompt_cache_key).
2364
+ if (!logCtx.conversationId) {
2365
+ logCtx.conversationId = resolvedConversationId;
2268
2366
  }
2269
2367
  logCtx.requestedModel = parsed.modelId;
2270
2368
  logCtx.requestedEffort = parsed.options.reasoning;
@@ -2322,6 +2420,27 @@ async function handleResponsesInner(
2322
2420
  return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
2323
2421
  }
2324
2422
 
2423
+ const parentOverride = decideV2NativeParentOverride({
2424
+ kind: "responses",
2425
+ config,
2426
+ headers: req.headers,
2427
+ parsed,
2428
+ sourceRoute: route,
2429
+ comboAttempt: options.comboAttempt,
2430
+ targetEvidence: evidenceFromBody(parsed._rawBody),
2431
+ });
2432
+ if (parentOverride.kind === "reject") {
2433
+ if (parentOverride.trace) logCtx.routeDecision = parentOverride.trace as typeof logCtx.routeDecision;
2434
+ return formatErrorResponse(404, "invalid_request_error", parentOverride.message);
2435
+ }
2436
+ if (parentOverride.kind === "override") {
2437
+ route = parentOverride.route;
2438
+ parsed.modelId = route.modelId;
2439
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
2440
+ (parsed._rawBody as { model?: string }).model = route.modelId;
2441
+ }
2442
+ }
2443
+
2325
2444
  const hasUnexpandedPreviousResponse = !!parsed.previousResponseId
2326
2445
  && parsed._previousResponseInputExpanded !== true;
2327
2446
  // Exact account selectors are isolated from Pool-wide quota work. A canonical replay miss must
@@ -2445,6 +2564,7 @@ async function handleResponsesInner(
2445
2564
  "_providerContinuationOwner",
2446
2565
  "_cursorConversationId",
2447
2566
  "_clientThreadId",
2567
+ "_promptCacheKeyIsSharedCohort",
2448
2568
  "_reasoningReplayScope",
2449
2569
  "_cursorIsolateConversation",
2450
2570
  ];
@@ -2609,14 +2729,28 @@ async function handleResponsesInner(
2609
2729
 
2610
2730
  // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
2611
2731
  // existing openai-chat / anthropic adapters authenticate with no change.
2612
- const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
2613
- && route.provider.authMode === "oauth";
2732
+ const isAntigravityOAuth = isAntigravityOAuthProvider(route.providerName, route.provider);
2733
+ if (route.providerName === "google-antigravity") {
2734
+ const antigravityConfigError = antigravityOAuthDestinationConfigError(route.providerName, route.provider);
2735
+ if (antigravityConfigError) {
2736
+ return formatErrorResponse(400, "invalid_request_error", antigravityConfigError);
2737
+ }
2738
+ }
2739
+ const isOAuth401ReplayProvider = isAntigravityOAuth
2740
+ || ((route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
2741
+ && route.provider.authMode === "oauth");
2614
2742
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
2615
2743
  let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
2616
2744
  let anthropicPoolAccountId: string | null = null;
2617
2745
  let anthropicPoolFailovers = 0;
2618
- let antigravityAccountId: string | undefined;
2619
- let antigravityFailovers = 0;
2746
+ let antigravityAccountId: string | null = null;
2747
+ let antigravitySessionKey: string | null = null;
2748
+ let antigravity429RetryAttempted = false;
2749
+ const observeAntigravityProviderError = isAntigravityOAuth
2750
+ ? (error: { code?: string; status?: number; message?: string }) => {
2751
+ if (antigravityAccountId) recordAntigravitySyntheticFailure(antigravityAccountId, error);
2752
+ }
2753
+ : undefined;
2620
2754
  let cursorPoolAccountId: string | null = null;
2621
2755
  let cursorPoolFailovers = 0;
2622
2756
  const oauthSessionKeyParts = {
@@ -2629,18 +2763,16 @@ async function handleResponsesInner(
2629
2763
  const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
2630
2764
  ? anthropicSessionKeyFromParts(oauthSessionKeyParts)
2631
2765
  : null;
2632
- const antigravitySessionKey = route.providerName === "google-antigravity"
2633
- && route.provider.authMode === "oauth"
2634
- && route.provider.googleMode === "cloud-code-assist"
2635
- ? antigravitySessionKeyFromParts(oauthSessionKeyParts)
2636
- : null;
2766
+ if (isAntigravityOAuth) {
2767
+ antigravitySessionKey = antigravitySessionKeyFromParts(oauthSessionKeyParts);
2768
+ }
2637
2769
  const cursorSessionKey = route.providerName === "cursor"
2638
2770
  && route.provider.authMode === "oauth"
2639
2771
  ? cursorSessionKeyFromParts({
2640
2772
  clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
2641
2773
  })
2642
2774
  : null;
2643
- if (route.provider.authMode === "oauth") {
2775
+ if (route.provider.authMode === "oauth" || isAntigravityOAuth) {
2644
2776
  try {
2645
2777
  if (route.providerName === "anthropic" && isAnthropicAccountPoolEnabled(config)) {
2646
2778
  const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config);
@@ -2662,50 +2794,6 @@ async function handleResponsesInner(
2662
2794
  promoteAnthropicActiveAccount(selection.accountId);
2663
2795
  route.provider = { ...route.provider, apiKey: accessToken };
2664
2796
  logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
2665
- } else if (
2666
- route.providerName === "google-antigravity"
2667
- && route.provider.googleMode === "cloud-code-assist"
2668
- ) {
2669
- const selection = resolveAntigravityAccountForSession(antigravitySessionKey);
2670
- if (!selection.accountId) {
2671
- if (selection.reason === "all-cooled") {
2672
- return formatErrorResponse(
2673
- 429,
2674
- "rate_limit_error",
2675
- "All Google Antigravity OAuth accounts are temporarily unavailable",
2676
- );
2677
- }
2678
- return formatErrorResponse(
2679
- 401,
2680
- "authentication_error",
2681
- "No eligible Google Antigravity OAuth account available",
2682
- );
2683
- }
2684
- let resolved = await getValidAccessTokenSnapshot(route.providerName);
2685
- if (selection.accountId !== resolved.accountId) {
2686
- const accessToken = await getValidAccessTokenForAccount("google-antigravity", selection.accountId);
2687
- const nextCredential = getAccountCredential("google-antigravity", selection.accountId);
2688
- resolved = {
2689
- ...resolved,
2690
- accountId: selection.accountId,
2691
- accessToken,
2692
- // Always replace; omitting a missing id would keep the previous account's projectId.
2693
- projectId: nextCredential?.projectId,
2694
- };
2695
- }
2696
- replayOAuthCredentialSnapshot = {
2697
- accountId: resolved.accountId,
2698
- generation: resolved.generation,
2699
- };
2700
- if (selection.reason === "failover") replayOAuthCredentialSnapshot = undefined;
2701
- route.provider = { ...route.provider, apiKey: resolved.accessToken };
2702
- antigravityAccountId = selection.accountId;
2703
- bindAntigravitySessionAffinity(antigravitySessionKey, selection.accountId);
2704
- const bound = bindAntigravityProject(route.provider, resolved.projectId);
2705
- if (!bound.ok) {
2706
- return formatErrorResponse(bound.status, bound.type, bound.message);
2707
- }
2708
- route.provider = bound.provider;
2709
2797
  } else if (
2710
2798
  route.providerName === "cursor"
2711
2799
  && route.provider.authMode === "oauth"
@@ -2737,7 +2825,28 @@ async function handleResponsesInner(
2737
2825
  route.provider = { ...route.provider, apiKey: resolved.accessToken };
2738
2826
  logCtx.provider = formatCursorProviderForLog("cursor", selection.accountId);
2739
2827
  } else {
2740
- const resolved = await getValidAccessTokenSnapshot(route.providerName);
2828
+ let resolved: OAuthAccessSnapshot;
2829
+ if (isAntigravityOAuth) {
2830
+ const selection = resolveAntigravityAccountForSession(antigravitySessionKey);
2831
+ if (!selection.accountId) {
2832
+ return formatErrorResponse(401, "authentication_error", "Selected Antigravity OAuth account is unavailable");
2833
+ }
2834
+ if (selection.cooldownUntil !== undefined) {
2835
+ const retryAfter = Math.max(1, Math.ceil((selection.cooldownUntil - Date.now()) / 1000));
2836
+ if (selection.cooldownKind === "geoblock") {
2837
+ return formatErrorResponse(403, "permission_error", "Selected Antigravity OAuth account is in local cooldown after upstream location block");
2838
+ }
2839
+ return formatErrorResponse(429, "rate_limit_error", "Selected Antigravity OAuth account is in local cooldown after upstream rate limit", { retryAfter: String(retryAfter) });
2840
+ }
2841
+ if (selection.reason === "active-needs-reauth") {
2842
+ return formatErrorResponse(401, "authentication_error", "Selected Antigravity OAuth account needs reauthentication");
2843
+ }
2844
+ antigravityAccountId = selection.accountId;
2845
+ resolved = await getValidAccessTokenSnapshotForAccount(route.providerName, selection.accountId);
2846
+ bindAntigravitySessionAffinity(antigravitySessionKey, selection.accountId);
2847
+ } else {
2848
+ resolved = await getValidAccessTokenSnapshot(route.providerName);
2849
+ }
2741
2850
  replayOAuthCredentialSnapshot = {
2742
2851
  accountId: resolved.accountId,
2743
2852
  generation: resolved.generation,
@@ -2749,6 +2858,14 @@ async function handleResponsesInner(
2749
2858
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
2750
2859
  parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
2751
2860
  }
2861
+ // Antigravity (cloud-code-assist) must use the project paired with this exact token
2862
+ // snapshot. A configured project may belong to another account and is never a fallback.
2863
+ if (isAntigravityOAuth) {
2864
+ if (!resolved.projectId) {
2865
+ return formatErrorResponse(400, "invalid_request_error", "Antigravity project unavailable — re-run `ocx login google-antigravity`");
2866
+ }
2867
+ route.provider = { ...route.provider, project: resolved.projectId };
2868
+ }
2752
2869
  }
2753
2870
  } catch (err) {
2754
2871
  if (err instanceof UnsupportedOAuthProviderError) {
@@ -2788,6 +2905,27 @@ async function handleResponsesInner(
2788
2905
  delete logCtx.accountLogLabel;
2789
2906
  }
2790
2907
  const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
2908
+ const googleOptionsError = googleProviderOptionsRouteError(parsed, {
2909
+ providerName: route.providerName,
2910
+ provider: adapterProvider,
2911
+ adapterName: adapter.name,
2912
+ });
2913
+ if (googleOptionsError) {
2914
+ return formatErrorResponse(400, "invalid_request_error", googleOptionsError);
2915
+ }
2916
+ const assertGoogleOptionsRoute = (
2917
+ candidate: Pick<ProviderAdapter, "name" | "validateRequest">,
2918
+ provider: OcxProviderConfig,
2919
+ requestParsed: OcxParsedRequest = parsed,
2920
+ ): void => {
2921
+ const message = googleProviderOptionsRouteError(requestParsed, {
2922
+ providerName: route.providerName,
2923
+ provider,
2924
+ adapterName: candidate.name,
2925
+ });
2926
+ if (message) throw new OcxRequestValidationError(message);
2927
+ if (requestParsed.options.providerOptions?.google) candidate.validateRequest?.(requestParsed);
2928
+ };
2791
2929
  bindRouteReasoningReplayScope({
2792
2930
  parsed,
2793
2931
  providerName: route.providerName,
@@ -2802,6 +2940,7 @@ async function handleResponsesInner(
2802
2940
  }
2803
2941
  logCtx.providerAdapter = adapter.name;
2804
2942
  try {
2943
+ assertGoogleOptionsRoute(adapter, adapterProvider);
2805
2944
  adapter.validateRequest?.(parsed);
2806
2945
  } catch (err) {
2807
2946
  return formatErrorResponse(
@@ -2842,6 +2981,23 @@ async function handleResponsesInner(
2842
2981
  );
2843
2982
  if (passiveSubjectId) logCtx.activeAttempt.labRouteSubjectId = passiveSubjectId;
2844
2983
  }
2984
+ const diagnosticRequestId = isDebugEnabled() ? randomUUID() : undefined;
2985
+ const adapterDiagnosticState: BridgeDiagnosticSequence = { value: 0 };
2986
+ const diagnosticContext: BridgeDiagnosticContext | undefined = diagnosticRequestId
2987
+ ? { requestId: diagnosticRequestId, adapterName: adapter.name, sequence: adapterDiagnosticState }
2988
+ : undefined;
2989
+ const noteDiagnosticAttempt = (
2990
+ attempt: RequestLogContext["activeAttempt"],
2991
+ inputEstimate: number | undefined,
2992
+ recovery?: AttemptRecoveryKind,
2993
+ adapterName?: string,
2994
+ ): void => {
2995
+ noteAttemptSend(attempt, inputEstimate, recovery);
2996
+ if (!diagnosticContext) return;
2997
+ diagnosticContext.attempt = attempt?.ordinal;
2998
+ diagnosticContext.recovery = recovery;
2999
+ if (adapterName) diagnosticContext.adapterName = adapterName;
3000
+ };
2845
3001
  const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
2846
3002
 
2847
3003
  if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
@@ -2976,6 +3132,7 @@ async function handleResponsesInner(
2976
3132
  ? new Map<string, { namespace: string; name: string }>()
2977
3133
  : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
2978
3134
  const routedCustomToolNames = new Set<string>();
3135
+ const routedCustomToolRepairNames = new Set<string>();
2979
3136
  const routedToolSearchNames = new Set<string>();
2980
3137
  // Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
2981
3138
  // previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
@@ -2998,8 +3155,26 @@ async function handleResponsesInner(
2998
3155
  + `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
2999
3156
  );
3000
3157
  }
3158
+ // Preserve the caller's readable catalog boundary before provider-specific normalization can
3159
+ // remove an unsupported final entry (for example xAI cached-only web search).
3160
+ const replayedInputPrefixLength = parsed._replayPrefixLen ?? 0;
3161
+ const clientToolAuthorizationBody = currentTurnWireToolCatalogBody(
3162
+ parsed._rawBody,
3163
+ replayedInputPrefixLength,
3164
+ );
3165
+ const clientExplicitWireToolCatalog = hasExplicitWireToolCatalog(clientToolAuthorizationBody);
3166
+ const clientDeclaredWireToolNames = collectDeclaredWireToolNames(clientToolAuthorizationBody);
3167
+ const clientDeclaredNamelessCallTypes = collectDeclaredNamelessClientCallTypes(
3168
+ clientToolAuthorizationBody,
3169
+ );
3170
+ // Hosted calls the PROVIDER runs itself. Gated on the destination actually being xAI, so a
3171
+ // declaration alone cannot buy the exemption on some other upstream that never serves it.
3172
+ const providerExecutedCallTypes = isXaiResponsesDestination(route.provider)
3173
+ ? collectProviderExecutedCallTypes(clientToolAuthorizationBody)
3174
+ : new Set<ProviderExecutedCallType>();
3001
3175
  let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
3002
3176
  try {
3177
+ assertGoogleOptionsRoute(adapter, adapterProvider);
3003
3178
  request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
3004
3179
  } catch (error) {
3005
3180
  releaseCodexAuthContextProbeLease(authCtx);
@@ -3019,6 +3194,12 @@ async function handleResponsesInner(
3019
3194
  || toolBridgeMaps.toolNsMap.get(name)?.freeform === true
3020
3195
  ) routedCustomToolNames.add(name);
3021
3196
  }
3197
+ for (const name of request.routedCustomToolRepairNames ?? []) {
3198
+ if (
3199
+ toolBridgeMaps.freeformToolNames.has(name)
3200
+ || toolBridgeMaps.toolNsMap.get(name)?.freeform === true
3201
+ ) routedCustomToolRepairNames.add(name);
3202
+ }
3022
3203
  }
3023
3204
  for (const name of request.convertedRoutedToolSearchNames ?? []) {
3024
3205
  // The adapter already keeps this set empty when tool_choice forbids the private search.
@@ -3034,13 +3215,13 @@ async function handleResponsesInner(
3034
3215
  // call it cannot execute, and the turn showed a bare `aborted` with the file untouched.
3035
3216
  // Forward auth is the canonical ChatGPT backend speaking Codex's own protocol rather than a
3036
3217
  // routed provider, so it keeps passing through unguarded, as it does for the rewrites above.
3037
- // The guard needs a catalog to compare against, so it stands down when the request carries
3038
- // none. That is not the same claim as "no tools means no tool may be called": a passthrough
3039
- // request can legitimately omit `tools` entirely and still receive a tool call the client
3040
- // understands — `tests/github-copilot-stream-contract.test.ts` sends `{model, input, stream}`
3041
- // with no tools and Copilot answers with a `custom_tool_call` for `apply_patch`. Policing an
3042
- // empty catalog truncates that turn. An unreadable body lands here too, since it yields no
3043
- // names either.
3218
+ // The guard needs a catalog to compare against, so it stands down when the request omits one.
3219
+ // An explicit empty catalog is still authoritative: it declares that no client tools may be
3220
+ // called. A passthrough request can legitimately omit `tools` entirely and still receive a call
3221
+ // the client understands — `tests/github-copilot-stream-contract.test.ts` sends
3222
+ // `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
3223
+ // `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
3224
+ // too because the proxy cannot establish the caller's declared authorization boundary.
3044
3225
  const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
3045
3226
  try {
3046
3227
  const body = JSON.parse(bodyText) as unknown;
@@ -3051,16 +3232,46 @@ async function handleResponsesInner(
3051
3232
  return undefined;
3052
3233
  }
3053
3234
  };
3054
- let outboundRequestBody = parseOutboundRequestBody(request.body);
3055
- const declaredWireToolNames = collectDeclaredWireToolNames(outboundRequestBody);
3056
- // Union with the caller's own catalog, because the outbound body is not always a complete
3057
- // record of it: hosted-tool preference REPLACES a client tool with its hosted form, so a
3058
- // request declaring `image_gen.generate` ships `{type:"image_generation"}` upstream and gets
3059
- // the client's name back. Widening only ever makes the guard fire less; a name declared in
3060
- // neither place — #1700's `apply_patch` — is still refused.
3061
- for (const name of toolBridgeMaps.declaredToolNames) declaredWireToolNames.add(name);
3062
- const undeclaredToolGuardActive = declaredWireToolNames.size > 0
3063
- && route.provider.authMode !== "forward";
3235
+ let outboundRequestBody: Record<string, unknown> | undefined;
3236
+ const declaredWireToolNames = new Set<string>();
3237
+ const declaredNamelessClientCallTypes = new Set<string>();
3238
+ let undeclaredToolGuardActive = false;
3239
+ const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
3240
+ outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
3241
+ declaredWireToolNames.clear();
3242
+ // With no replay prefix the full outbound body belongs to this turn and its normalized
3243
+ // aliases are authoritative. A continuation's outbound body still contains historical
3244
+ // catalogs (and may promote historical tool-search definitions), so it can never widen the
3245
+ // current caller snapshot captured above.
3246
+ if (replayedInputPrefixLength === 0) {
3247
+ for (const name of collectDeclaredWireToolNames(outboundRequestBody)) {
3248
+ declaredWireToolNames.add(name);
3249
+ }
3250
+ }
3251
+ for (const name of clientDeclaredWireToolNames) declaredWireToolNames.add(name);
3252
+ declaredNamelessClientCallTypes.clear();
3253
+ if (replayedInputPrefixLength === 0) {
3254
+ for (const callType of collectDeclaredNamelessClientCallTypes(outboundRequestBody)) {
3255
+ declaredNamelessClientCallTypes.add(callType);
3256
+ }
3257
+ }
3258
+ for (const callType of clientDeclaredNamelessCallTypes) {
3259
+ declaredNamelessClientCallTypes.add(callType);
3260
+ }
3261
+ // On an ordinary request these maps capture caller-catalog identities that normalization may
3262
+ // replace on the outbound wire (for example a client image tool becoming hosted). On replay,
3263
+ // however, the parsed maps also contain historical catalog entries, so only the bounded
3264
+ // current-turn wire snapshot above may authorize a call.
3265
+ if (replayedInputPrefixLength === 0) {
3266
+ for (const name of toolBridgeMaps.declaredToolNames) declaredWireToolNames.add(name);
3267
+ }
3268
+ undeclaredToolGuardActive = (
3269
+ declaredWireToolNames.size > 0
3270
+ || clientDeclaredNamelessCallTypes.size > 0
3271
+ || clientExplicitWireToolCatalog
3272
+ ) && route.provider.authMode !== "forward";
3273
+ };
3274
+ refreshUndeclaredToolGuard(request);
3064
3275
  // A refused turn must not seed `previous_response_id` replay. The inspection branch reads the
3065
3276
  // untouched upstream stream, so it can still observe a `response.completed` the client never
3066
3277
  // received; checking the payload itself rather than a flag shared with the client relay keeps
@@ -3073,11 +3284,19 @@ async function handleResponsesInner(
3073
3284
  // rejection is sticky for the whole turn, set from every parsed payload on the inspection side.
3074
3285
  let inspectionSawUndeclaredTool = false;
3075
3286
  const noteInspectedPayload = (payload: unknown) => {
3287
+ if (isAntigravityOAuth && antigravityAccountId) {
3288
+ recordAntigravitySyntheticFailure(antigravityAccountId, payload);
3289
+ }
3076
3290
  // Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth
3077
3291
  // provider) every name looks undeclared, and flipping this would stop recording continuation
3078
3292
  // state for exactly the passthrough traffic the guard deliberately stands down for.
3079
3293
  if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
3080
- if (undeclaredToolCallName(payload, declaredWireToolNames) !== undefined) {
3294
+ if (undeclaredToolCallName(
3295
+ payload,
3296
+ declaredWireToolNames,
3297
+ declaredNamelessClientCallTypes,
3298
+ providerExecutedCallTypes,
3299
+ ) !== undefined) {
3081
3300
  inspectionSawUndeclaredTool = true;
3082
3301
  }
3083
3302
  };
@@ -3086,7 +3305,12 @@ async function handleResponsesInner(
3086
3305
  if (inspectionSawUndeclaredTool) return;
3087
3306
  if (
3088
3307
  undeclaredToolGuardActive
3089
- && undeclaredToolCallNameInResponse(response, declaredWireToolNames) !== undefined
3308
+ && undeclaredToolCallNameInResponse(
3309
+ response,
3310
+ declaredWireToolNames,
3311
+ declaredNamelessClientCallTypes,
3312
+ providerExecutedCallTypes,
3313
+ ) !== undefined
3090
3314
  ) {
3091
3315
  return;
3092
3316
  }
@@ -3194,7 +3418,7 @@ async function handleResponsesInner(
3194
3418
  // Body is a replayable string; nothing has streamed to the client yet.
3195
3419
  upstreamResponse = await fetchWithTransientRetry(
3196
3420
  recovery => {
3197
- noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery);
3421
+ noteDiagnosticAttempt(logCtx.activeAttempt, passthroughEstimate, recovery, route.provider.adapter);
3198
3422
  return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
3199
3423
  method: request.method,
3200
3424
  headers: request.headers,
@@ -3235,10 +3459,17 @@ async function handleResponsesInner(
3235
3459
  upstream.abort();
3236
3460
  return { failed: formatErrorResponse(502, "upstream_error", "Recovery changed the provider wire unexpectedly") };
3237
3461
  }
3462
+ try {
3463
+ assertGoogleOptionsRoute(retryAdapter, route.provider);
3464
+ } catch (err) {
3465
+ upstream.abort();
3466
+ return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(err instanceof Error ? err.message : String(err))) };
3467
+ }
3238
3468
  try {
3239
3469
  request = await retryAdapter.buildRequest(parsed, {
3240
3470
  headers: selectedForwardHeaders,
3241
3471
  translatorBudget,
3472
+ ...(observeAntigravityProviderError ? { onProviderError: observeAntigravityProviderError } : {}),
3242
3473
  });
3243
3474
  refreshRoutedNamespaceToolAliases(request);
3244
3475
  recordAdapterReasoning(logCtx, request);
@@ -3253,7 +3484,7 @@ async function handleResponsesInner(
3253
3484
  ? request.usageLog.inputTokens
3254
3485
  : undefined;
3255
3486
  if (passthroughEstimate !== undefined) logCtx.usageLogInputTokens = passthroughEstimate;
3256
- outboundRequestBody = parseOutboundRequestBody(request.body);
3487
+ refreshUndeclaredToolGuard(request);
3257
3488
  logCtx.providerAdapter = retryAdapter.name;
3258
3489
  sealRequestAttemptIdentity(
3259
3490
  logCtx.activeAttempt,
@@ -3264,7 +3495,7 @@ async function handleResponsesInner(
3264
3495
  try {
3265
3496
  return await fetchWithTransientRetry(
3266
3497
  innerRecovery => {
3267
- noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, innerRecovery ?? recovery);
3498
+ noteDiagnosticAttempt(logCtx.activeAttempt, passthroughEstimate, innerRecovery ?? recovery, retryAdapter.name);
3268
3499
  return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
3269
3500
  method: request.method,
3270
3501
  headers: request.headers,
@@ -3312,6 +3543,14 @@ async function handleResponsesInner(
3312
3543
  releaseCodexAuthContextProbeLease(authCtx);
3313
3544
  return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
3314
3545
  }
3546
+ if (isAntigravityOAuth && refreshed.accountId !== antigravityAccountId) {
3547
+ upstream.abort();
3548
+ return formatErrorResponse(401, "authentication_error", "Antigravity OAuth account changed during refresh");
3549
+ }
3550
+ if (isAntigravityOAuth && !refreshed.projectId) {
3551
+ upstream.abort();
3552
+ return formatErrorResponse(400, "invalid_request_error", "Antigravity project unavailable — re-run `ocx login google-antigravity`");
3553
+ }
3315
3554
  sentOAuthSnapshot = refreshed;
3316
3555
  replayOAuthCredentialSnapshot = {
3317
3556
  accountId: refreshed.accountId,
@@ -3320,6 +3559,9 @@ async function handleResponsesInner(
3320
3559
  if (route.providerName === "kiro") {
3321
3560
  parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
3322
3561
  }
3562
+ if (isAntigravityOAuth) {
3563
+ route.provider = { ...route.provider, project: refreshed.projectId };
3564
+ }
3323
3565
  const refreshedProvider = resolveProviderTransport(
3324
3566
  route.providerName,
3325
3567
  { ...route.provider, apiKey: refreshed.accessToken },
@@ -3350,9 +3592,11 @@ async function handleResponsesInner(
3350
3592
  logCtx.accountLogLabel,
3351
3593
  );
3352
3594
  try {
3595
+ assertGoogleOptionsRoute(refreshedAdapter, refreshedProvider);
3353
3596
  request = await refreshedAdapter.buildRequest(parsed, {
3354
3597
  headers: selectedForwardHeaders,
3355
3598
  translatorBudget,
3599
+ ...(observeAntigravityProviderError ? { onProviderError: observeAntigravityProviderError } : {}),
3356
3600
  });
3357
3601
  refreshRoutedNamespaceToolAliases(request);
3358
3602
  recordAdapterReasoning(logCtx, request);
@@ -3363,10 +3607,11 @@ async function handleResponsesInner(
3363
3607
  const msg = err instanceof Error ? err.message : String(err);
3364
3608
  return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
3365
3609
  }
3610
+ refreshUndeclaredToolGuard(request);
3366
3611
  try {
3367
3612
  upstreamResponse = await fetchWithTransientRetry(
3368
3613
  recovery => {
3369
- noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "oauth-401");
3614
+ noteDiagnosticAttempt(logCtx.activeAttempt, passthroughEstimate, recovery ?? "oauth-401", refreshedAdapter.name);
3370
3615
  return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
3371
3616
  method: request.method,
3372
3617
  headers: request.headers,
@@ -3428,7 +3673,7 @@ async function handleResponsesInner(
3428
3673
  recovery => {
3429
3674
  // The first send of every replay is itself a rate-limit retry; inner transient-5xx
3430
3675
  // recoveries keep their own label (recovery is provided for those).
3431
- noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "rate-limit-429");
3676
+ noteDiagnosticAttempt(logCtx.activeAttempt, passthroughEstimate, recovery ?? "rate-limit-429", route.provider.adapter);
3432
3677
  return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
3433
3678
  method: request.method,
3434
3679
  headers: request.headers,
@@ -3514,6 +3759,7 @@ async function handleResponsesInner(
3514
3759
  authCtx = retry.authCtx;
3515
3760
  request = retry.request;
3516
3761
  refreshRoutedNamespaceToolAliases(request);
3762
+ refreshUndeclaredToolGuard(request);
3517
3763
  upstreamResponse = retry.upstreamResponse;
3518
3764
  selectedForwardHeaders = retry.selectedForwardHeaders;
3519
3765
  // Keep subagent quota-failure health keyed to the account that actually served.
@@ -3552,13 +3798,21 @@ async function handleResponsesInner(
3552
3798
  const passthroughCt = headers.get("content-type")?.toLowerCase();
3553
3799
  const isEventStream = passthroughCt?.includes("text/event-stream")
3554
3800
  || (upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
3555
- const terminalRecorder = codexForwardTerminalOutcomeRecorder(
3801
+ const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
3556
3802
  config,
3557
3803
  authCtx,
3558
3804
  route.provider,
3559
3805
  route.modelId,
3560
3806
  logCtx,
3561
3807
  );
3808
+ let terminalOutcomeRecorded = false;
3809
+ const terminalRecorder = recordTerminalOutcome
3810
+ ? (status: ResponsesTerminalStatus, httpStatusOverride?: number): void => {
3811
+ if (terminalOutcomeRecorded) return;
3812
+ terminalOutcomeRecorded = true;
3813
+ recordTerminalOutcome(status, httpStatusOverride);
3814
+ }
3815
+ : undefined;
3562
3816
  const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
3563
3817
  // Capture quota from upstream response for multi-account tracking
3564
3818
  if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
@@ -3635,19 +3889,13 @@ async function handleResponsesInner(
3635
3889
  options.onConsumedComboFailure?.(failure);
3636
3890
  return failure.response;
3637
3891
  }
3638
- // The plain passthrough error path has no bounded reader of its own: `.text()` attaches
3639
- // the reader only when it runs, so an abort landing between fetch resolution and that
3640
- // call orphans Bun's internal read rejection (src/lib/abort.ts).
3641
- const detachPassthroughErrorGuard = cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
3642
- try {
3643
- const errorText = await upstreamResponse.text().catch(() => "");
3644
- return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
3645
- statusText: upstreamResponse.statusText,
3646
- headers,
3647
- });
3648
- } finally {
3649
- detachPassthroughErrorGuard();
3650
- }
3892
+ // The bounded reader owns the original body, deadline, abort settlement, and lock.
3893
+ // Unsafe partial data falls back to #452's non-empty status-only JSON.
3894
+ const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
3895
+ return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
3896
+ statusText: upstreamResponse.statusText,
3897
+ headers,
3898
+ });
3651
3899
  }
3652
3900
 
3653
3901
  // Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
@@ -3710,8 +3958,12 @@ async function handleResponsesInner(
3710
3958
  payloadRewrites.length > 0
3711
3959
  ? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
3712
3960
  : undefined,
3713
- routedCustomToolNames.size > 0
3714
- ? createRoutedCustomToolRestoreBlockRewrite(routedCustomToolNames, translatorBudget)
3961
+ routedCustomToolNames.size > 0 || routedCustomToolRepairNames.size > 0
3962
+ ? createRoutedCustomToolRestoreBlockRewrite(
3963
+ routedCustomToolNames,
3964
+ translatorBudget,
3965
+ routedCustomToolRepairNames,
3966
+ )
3715
3967
  : undefined,
3716
3968
  routedToolSearchNames.size > 0
3717
3969
  ? createRoutedToolSearchRestoreBlockRewrite(routedToolSearchNames, translatorBudget)
@@ -3726,7 +3978,11 @@ async function handleResponsesInner(
3726
3978
  // Last: every rewrite above can still rename or reshape a call item, so the guard must
3727
3979
  // compare the names the client will actually receive against the declared catalog.
3728
3980
  undeclaredToolGuardActive
3729
- ? createUndeclaredToolCallGuardBlockRewrite(declaredWireToolNames)
3981
+ ? createUndeclaredToolCallGuardBlockRewrite(
3982
+ declaredWireToolNames,
3983
+ declaredNamelessClientCallTypes,
3984
+ providerExecutedCallTypes,
3985
+ )
3730
3986
  : undefined,
3731
3987
  ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
3732
3988
  const clientBlockRewrite = blockRewrites.length > 0
@@ -3916,6 +4172,7 @@ async function handleResponsesInner(
3916
4172
  const restored = restoreRoutedCustomCallsInJson(
3917
4173
  restoredNamespace,
3918
4174
  routedCustomToolNames,
4175
+ routedCustomToolRepairNames,
3919
4176
  );
3920
4177
  const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
3921
4178
  restored,
@@ -3942,7 +4199,12 @@ async function handleResponsesInner(
3942
4199
  if (undeclaredToolGuardActive) {
3943
4200
  const undeclared = (() => {
3944
4201
  try {
3945
- return undeclaredToolCallNameInResponse(JSON.parse(clientJson), declaredWireToolNames);
4202
+ return undeclaredToolCallNameInResponse(
4203
+ JSON.parse(clientJson),
4204
+ declaredWireToolNames,
4205
+ declaredNamelessClientCallTypes,
4206
+ providerExecutedCallTypes,
4207
+ );
3946
4208
  } catch {
3947
4209
  return undefined;
3948
4210
  }
@@ -4139,7 +4401,8 @@ async function handleResponsesInner(
4139
4401
  ...(vidPlan ? { videoPlan: vidPlan } : {}),
4140
4402
  forwardHeaders: selectedForwardHeaders,
4141
4403
  onAttemptSend: (recovery?: AttemptRecoveryKind) =>
4142
- noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery),
4404
+ noteDiagnosticAttempt(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery, adapter.name),
4405
+ ...(diagnosticContext ? { diagnostic: diagnosticContext } : {}),
4143
4406
  abortSignal: options.abortSignal,
4144
4407
  maxRounds: imgPlan && vidPlan
4145
4408
  ? clampImageMaxRounds(Math.min(config.images?.maxRounds ?? 3, config.images?.videoMaxRounds ?? 2))
@@ -4154,6 +4417,7 @@ async function handleResponsesInner(
4154
4417
  recordAdapterReasoning(logCtx, request);
4155
4418
  recordAdapterTier(logCtx, request);
4156
4419
  },
4420
+ validateAdapter: (requestParsed, candidate) => assertGoogleOptionsRoute(candidate, route.provider, requestParsed),
4157
4421
  ...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
4158
4422
  onUsage: usage => {
4159
4423
  // Cursor may assign _cursorConversationId inside the image loop's first runTurn;
@@ -4243,8 +4507,10 @@ async function handleResponsesInner(
4243
4507
  recordAdapterReasoning(logCtx, request);
4244
4508
  recordAdapterTier(logCtx, request);
4245
4509
  },
4510
+ validateAdapter: (requestParsed, candidate) => assertGoogleOptionsRoute(candidate, route.provider, requestParsed),
4246
4511
  onAttemptSend: (recovery?: AttemptRecoveryKind) =>
4247
- noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery),
4512
+ noteDiagnosticAttempt(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery, adapter.name),
4513
+ ...(diagnosticContext ? { diagnostic: diagnosticContext } : {}),
4248
4514
  onUsage: usage => {
4249
4515
  logCtx.usageFromBridge = true;
4250
4516
  if (usage) {
@@ -4333,10 +4599,14 @@ async function handleResponsesInner(
4333
4599
  pacingSlotAcquired = false,
4334
4600
  ): Promise<void> => {
4335
4601
  try {
4602
+ if (diagnosticContext) {
4603
+ diagnosticContext.recovery = recovery;
4604
+ diagnosticContext.attempt = logCtx.activeAttempt?.ordinal;
4605
+ }
4336
4606
  if (!pacingSlotAcquired) {
4337
4607
  await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
4338
4608
  }
4339
- noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery);
4609
+ noteDiagnosticAttempt(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery, adapter.name);
4340
4610
  const runTurnProviderFetch = providerFetch(
4341
4611
  route.provider,
4342
4612
  options.codexWsRuntimeIdentity,
@@ -4390,13 +4660,15 @@ async function handleResponsesInner(
4390
4660
  onBacklogExceeded: () => runTurnAbort.abort(),
4391
4661
  });
4392
4662
  void runTurnAttempt(retryQueue, "empty-completion");
4393
- return retryQueue.stream();
4663
+ return diagnoseAdapterEvents(retryQueue.stream(), adapter.name, diagnosticRequestId, logCtx, adapterDiagnosticState);
4394
4664
  };
4395
4665
 
4396
4666
  const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
4397
4667
  if (parsed.stream) {
4398
4668
  void runTurn();
4399
- let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
4669
+ let eventSource: AsyncIterable<AdapterEvent> = diagnoseAdapterEvents(
4670
+ queue.stream(), adapter.name, diagnosticRequestId, logCtx, adapterDiagnosticState,
4671
+ );
4400
4672
  if (options.comboAttempt) {
4401
4673
  const preflight = await preflightAdapterEvents(eventSource);
4402
4674
  if (preflight.error || preflight.empty) {
@@ -4434,6 +4706,7 @@ async function handleResponsesInner(
4434
4706
  // grok-build's strict decoder dies on the typed response.heartbeat frame; its
4435
4707
  // eventsource layer tolerates comment keep-alives. Codex needs the opposite.
4436
4708
  ...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
4709
+ ...(diagnosticContext ? { diagnostic: diagnosticContext } : {}),
4437
4710
  onUsage: usage => {
4438
4711
  // Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
4439
4712
  // zero-default detail objects, so provenance must come from here (cache_detail_missing).
@@ -4458,9 +4731,11 @@ async function handleResponsesInner(
4458
4731
  );
4459
4732
  const bridgeTurnAc = new AbortController();
4460
4733
  const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
4461
- return new Response(trackedSse, {
4734
+ const response = new Response(trackedSse, {
4462
4735
  headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
4463
4736
  });
4737
+ runTurnAdapterSseResponses.add(response);
4738
+ return response;
4464
4739
  }
4465
4740
 
4466
4741
  await runTurn();
@@ -4540,7 +4815,12 @@ async function handleResponsesInner(
4540
4815
  let initialRequest: AdapterRequest | undefined;
4541
4816
  let inputTokenEstimate: number | undefined;
4542
4817
  try {
4543
- initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
4818
+ assertGoogleOptionsRoute(activeAdapter, route.provider);
4819
+ initialRequest = await activeAdapter.buildRequest(parsed, {
4820
+ headers: selectedForwardHeaders,
4821
+ translatorBudget,
4822
+ ...(observeAntigravityProviderError ? { onProviderError: observeAntigravityProviderError } : {}),
4823
+ });
4544
4824
  refreshRoutedNamespaceToolAliases(initialRequest);
4545
4825
  recordAdapterReasoning(logCtx, initialRequest);
4546
4826
  recordAdapterTier(logCtx, initialRequest);
@@ -4577,7 +4857,7 @@ async function handleResponsesInner(
4577
4857
  let upstreamResponse: Response;
4578
4858
  try {
4579
4859
  if (activeAdapter.fetchResponse) {
4580
- noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
4860
+ noteDiagnosticAttempt(logCtx.activeAttempt, inputTokenEstimate, undefined, activeAdapter.name);
4581
4861
  await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal);
4582
4862
  upstreamResponse = await activeAdapter.fetchResponse(builtInitialRequest, {
4583
4863
  abortSignal: upstream.signal,
@@ -4586,6 +4866,7 @@ async function handleResponsesInner(
4586
4866
  executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4587
4867
  providerName: route.providerName,
4588
4868
  modelId: route.modelId,
4869
+ pacingSlotAcquired: true,
4589
4870
  }),
4590
4871
  ...(antigravityAccountId ? { accountId: antigravityAccountId } : {}),
4591
4872
  });
@@ -4597,7 +4878,7 @@ async function handleResponsesInner(
4597
4878
  const fetchWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry;
4598
4879
  upstreamResponse = await fetchWithRetryPolicy(
4599
4880
  recovery => {
4600
- noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery);
4881
+ noteDiagnosticAttempt(logCtx.activeAttempt, inputTokenEstimate, recovery, activeAdapter.name);
4601
4882
  return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
4602
4883
  method: builtInitialRequest.method,
4603
4884
  headers: builtInitialRequest.headers,
@@ -4648,6 +4929,13 @@ async function handleResponsesInner(
4648
4929
  const rebuildAndRefetch = async (
4649
4930
  recovery: AttemptRecoveryKind,
4650
4931
  ): Promise<Response | { failed: Response }> => {
4932
+ try {
4933
+ assertGoogleOptionsRoute(activeAdapter, route.provider);
4934
+ } catch (err) {
4935
+ cleanupUpstreamAbort();
4936
+ upstream.abort();
4937
+ return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(err instanceof Error ? err.message : String(err))) };
4938
+ }
4651
4939
  let retryRequest: AdapterRequest;
4652
4940
  if (sameTargetRequest !== undefined && sameTargetParsed === parsed && sameTargetToken === transportToken) {
4653
4941
  // Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
@@ -4658,6 +4946,7 @@ async function handleResponsesInner(
4658
4946
  headers: selectedForwardHeaders,
4659
4947
  translatorBudget,
4660
4948
  ...(imageTierBias > 0 ? { imageTierBias } : {}),
4949
+ ...(observeAntigravityProviderError ? { onProviderError: observeAntigravityProviderError } : {}),
4661
4950
  });
4662
4951
  recordAdapterReasoning(logCtx, retryRequest);
4663
4952
  recordAdapterTier(logCtx, retryRequest);
@@ -4682,7 +4971,7 @@ async function handleResponsesInner(
4682
4971
  if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
4683
4972
  logCtx.providerAdapter = activeAdapter.name;
4684
4973
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
4685
- noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
4974
+ noteDiagnosticAttempt(logCtx.activeAttempt, retryEstimate, recovery, activeAdapter.name);
4686
4975
  try {
4687
4976
  try {
4688
4977
  if (activeAdapter.fetchResponse) {
@@ -4694,6 +4983,7 @@ async function handleResponsesInner(
4694
4983
  executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4695
4984
  providerName: route.providerName,
4696
4985
  modelId: route.modelId,
4986
+ pacingSlotAcquired: true,
4697
4987
  }),
4698
4988
  ...(antigravityAccountId ? { accountId: antigravityAccountId } : {}),
4699
4989
  });
@@ -4735,6 +5025,14 @@ async function handleResponsesInner(
4735
5025
  cleanupUpstreamAbort();
4736
5026
  return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
4737
5027
  }
5028
+ if (isAntigravityOAuth && refreshed.accountId !== antigravityAccountId) {
5029
+ cleanupUpstreamAbort();
5030
+ return formatErrorResponse(401, "authentication_error", "Antigravity OAuth account changed during refresh");
5031
+ }
5032
+ if (isAntigravityOAuth && !refreshed.projectId) {
5033
+ cleanupUpstreamAbort();
5034
+ return formatErrorResponse(400, "invalid_request_error", "Antigravity project unavailable — re-run `ocx login google-antigravity`");
5035
+ }
4738
5036
  sentOAuthSnapshot = refreshed;
4739
5037
  replayOAuthCredentialSnapshot = {
4740
5038
  accountId: refreshed.accountId,
@@ -4743,6 +5041,9 @@ async function handleResponsesInner(
4743
5041
  if (route.providerName === "kiro") {
4744
5042
  parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
4745
5043
  }
5044
+ if (isAntigravityOAuth) {
5045
+ route.provider = { ...route.provider, project: refreshed.projectId };
5046
+ }
4746
5047
  const refreshedProvider = resolveProviderTransport(
4747
5048
  route.providerName,
4748
5049
  { ...route.provider, apiKey: refreshed.accessToken },
@@ -4838,6 +5139,37 @@ async function handleResponsesInner(
4838
5139
  upstreamResponse = result;
4839
5140
  }
4840
5141
 
5142
+ // Antigravity never rotates accounts. Record the account cooldown and allow only one
5143
+ // short, abort-aware replay on the same credential; longer Retry-After values surface to
5144
+ // the client so a conversation cannot churn through accounts or requests.
5145
+ if (upstreamResponse.status === 403 && isAntigravityOAuth && antigravityAccountId) {
5146
+ recordAntigravityCooldown(antigravityAccountId, upstreamResponse.headers.get("retry-after"), Date.now(), "geoblock");
5147
+ }
5148
+ if (upstreamResponse.status === 429 && isAntigravityOAuth && antigravityAccountId) {
5149
+ recordAntigravityCooldown(antigravityAccountId, upstreamResponse.headers.get("retry-after"));
5150
+ const retryAfter = retryableAntigravity429DelayMs(upstreamResponse.headers.get("retry-after"));
5151
+ if (!antigravity429RetryAttempted && retryAfter !== null) {
5152
+ antigravity429RetryAttempted = true;
5153
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
5154
+ try {
5155
+ await sleepWithAbort(retryAfter, upstream.signal);
5156
+ } catch {
5157
+ cleanupUpstreamAbort();
5158
+ upstream.abort();
5159
+ return clientCancelledResponse();
5160
+ }
5161
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
5162
+ cleanupUpstreamAbort();
5163
+ upstream.abort();
5164
+ return clientCancelledResponse();
5165
+ }
5166
+ const result = await rebuildAndRefetch("rate-limit-429");
5167
+ if ("failed" in result) return result.failed;
5168
+ upstreamResponse = result;
5169
+ continue recovery;
5170
+ }
5171
+ }
5172
+
4841
5173
  // Opt-in Anthropic OAuth account pool (#294): cool the failed account and retry
4842
5174
  // with another eligible OAuth account (bounded per request). Disabled by default.
4843
5175
  while (
@@ -4874,75 +5206,6 @@ async function handleResponsesInner(
4874
5206
  break;
4875
5207
  }
4876
5208
  }
4877
- // Antigravity OAuth accounts use the same bounded pre-stream carousel as Anthropic, but
4878
- // their process-local routing module also excludes accounts cooled by quota/rate-limit
4879
- // responses. Geoblocked 403s intentionally never enter this loop.
4880
- while (
4881
- upstreamResponse.status === 429
4882
- && route.providerName === "google-antigravity"
4883
- && route.provider.googleMode === "cloud-code-assist"
4884
- && antigravityAccountId
4885
- && antigravityFailovers < 3
4886
- ) {
4887
- const stickWaitMs = antigravity429StickWaitMs(antigravityAccountId);
4888
- if (stickWaitMs !== null) {
4889
- await Bun.sleep(stickWaitMs);
4890
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4891
- invalidateSameTargetRequest();
4892
- activeAdapter = resolveAdapter(
4893
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4894
- config.cacheRetention,
4895
- );
4896
- bindRouteReasoningReplayScope({
4897
- parsed,
4898
- providerName: route.providerName,
4899
- provider: route.provider,
4900
- adapterName: activeAdapter.name,
4901
- codexAuthContext: authCtx,
4902
- forwardHeaders: selectedForwardHeaders,
4903
- });
4904
- const result = await rebuildAndRefetch("rate-limit-429");
4905
- if ("failed" in result) return result.failed;
4906
- upstreamResponse = result;
4907
- continue;
4908
- }
4909
- const nextAccountId = rotateAntigravityAccountOn429(antigravityAccountId, antigravitySessionKey);
4910
- if (!nextAccountId) break;
4911
- try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4912
- try {
4913
- const accessToken = await getValidAccessTokenForAccount("google-antigravity", nextAccountId);
4914
- const nextCredential = getAccountCredential("google-antigravity", nextAccountId);
4915
- const bound = bindAntigravityProject(
4916
- { ...route.provider, apiKey: accessToken },
4917
- nextCredential?.projectId,
4918
- );
4919
- if (!bound.ok) {
4920
- return formatErrorResponse(bound.status, bound.type, bound.message);
4921
- }
4922
- antigravityAccountId = nextAccountId;
4923
- antigravityFailovers += 1;
4924
- route.provider = bound.provider;
4925
- replayOAuthCredentialSnapshot = undefined;
4926
- invalidateSameTargetRequest();
4927
- activeAdapter = resolveAdapter(
4928
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4929
- config.cacheRetention,
4930
- );
4931
- bindRouteReasoningReplayScope({
4932
- parsed,
4933
- providerName: route.providerName,
4934
- provider: route.provider,
4935
- adapterName: activeAdapter.name,
4936
- codexAuthContext: authCtx,
4937
- forwardHeaders: selectedForwardHeaders,
4938
- });
4939
- const result = await rebuildAndRefetch("rate-limit-429");
4940
- if ("failed" in result) return result.failed;
4941
- upstreamResponse = result;
4942
- } catch {
4943
- break;
4944
- }
4945
- }
4946
5209
  if (
4947
5210
  upstreamResponse.status === 402
4948
5211
  && cursorPoolAccountId
@@ -5093,14 +5356,16 @@ async function handleResponsesInner(
5093
5356
  options.onConsumedComboFailure?.(failure);
5094
5357
  return failure.response;
5095
5358
  }
5096
- // The plain error path has no bounded reader of its own: `.text()` attaches the reader
5097
- // only when it runs, so an abort landing between fetch resolution and that call orphans
5098
- // Bun's internal read rejection — the uncatchable teardown `cancelBodyOnAbort` absorbs
5099
- // (src/lib/abort.ts). Found while investigating #1419; not a fix for the native trap.
5100
- const detachErrorBodyGuard = cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
5101
- const errorText = await upstreamResponse.text().catch(() => "unknown error");
5102
- detachErrorBodyGuard();
5103
- cleanupUpstreamAbort();
5359
+ let errorText: string;
5360
+ try {
5361
+ errorText = await readDisplaySafeErrorText(
5362
+ upstreamResponse,
5363
+ upstream.signal,
5364
+ "unknown error",
5365
+ );
5366
+ } finally {
5367
+ cleanupUpstreamAbort();
5368
+ }
5104
5369
  if (!isFixedCodexAccount(authCtx)) {
5105
5370
  recordSubagentQuotaFailureForThreadSpawn(
5106
5371
  req.headers,
@@ -5160,6 +5425,16 @@ async function handleResponsesInner(
5160
5425
  nextParsed: OcxParsedRequest,
5161
5426
  initialRecoveryKind?: AttemptRecoveryKind,
5162
5427
  ): AsyncGenerator<AdapterEvent> {
5428
+ const requestValidationEvent = (error: unknown): AdapterEvent | undefined => {
5429
+ if (!(error instanceof OcxRequestValidationError)) return undefined;
5430
+ return {
5431
+ type: "error",
5432
+ status: error.status,
5433
+ errorType: "invalid_request_error",
5434
+ code: "invalid_request_error",
5435
+ message: error.message,
5436
+ };
5437
+ };
5163
5438
  let response: Response | undefined;
5164
5439
  // One-shot recovery label for the next top-of-loop continuation send after a failover rotation.
5165
5440
  let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined = initialRecoveryKind;
@@ -5172,6 +5447,7 @@ async function handleResponsesInner(
5172
5447
  */
5173
5448
  const fetchContinuation = async (recoveryKind?: AttemptRecoveryKind): Promise<Response> => {
5174
5449
  let continuationRequest: AdapterRequest | undefined;
5450
+ assertGoogleOptionsRoute(activeAdapter, route.provider, nextParsed);
5175
5451
  if (sameTargetRequest !== undefined && sameTargetParsed === nextParsed && sameTargetToken === transportToken) {
5176
5452
  // Same target (key/adapter/parsed/tier unchanged): replay the exact cached request.
5177
5453
  continuationRequest = sameTargetRequest;
@@ -5181,6 +5457,7 @@ async function handleResponsesInner(
5181
5457
  headers: selectedForwardHeaders,
5182
5458
  translatorBudget,
5183
5459
  ...(imageTierBias > 0 ? { imageTierBias } : {}),
5460
+ ...(observeAntigravityProviderError ? { onProviderError: observeAntigravityProviderError } : {}),
5184
5461
  });
5185
5462
  recordAdapterReasoning(logCtx, continuationRequest);
5186
5463
  recordAdapterTier(logCtx, continuationRequest);
@@ -5206,8 +5483,13 @@ async function handleResponsesInner(
5206
5483
  // Optional recovery label for same-target / failover continuation sends.
5207
5484
  const replayKind: AttemptRecoveryKind | undefined = recoveryKind;
5208
5485
  try {
5486
+ if (diagnosticContext) {
5487
+ diagnosticContext.recovery = replayKind;
5488
+ diagnosticContext.attempt = logCtx.activeAttempt?.ordinal;
5489
+ diagnosticContext.adapterName = activeAdapter.name;
5490
+ }
5209
5491
  if (activeAdapter.fetchResponse) {
5210
- noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replayKind);
5492
+ noteDiagnosticAttempt(logCtx.activeAttempt, continuationEstimate, replayKind, activeAdapter.name);
5211
5493
  await waitForProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal);
5212
5494
  return await activeAdapter.fetchResponse(builtContinuationRequest, {
5213
5495
  abortSignal: upstream.signal,
@@ -5216,6 +5498,7 @@ async function handleResponsesInner(
5216
5498
  executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
5217
5499
  providerName: route.providerName,
5218
5500
  modelId: nextParsed.modelId,
5501
+ pacingSlotAcquired: true,
5219
5502
  }),
5220
5503
  ...(antigravityAccountId ? { accountId: antigravityAccountId } : {}),
5221
5504
  });
@@ -5225,7 +5508,7 @@ async function handleResponsesInner(
5225
5508
  const fetchContinuationWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry;
5226
5509
  return await fetchContinuationWithRetryPolicy(
5227
5510
  recovery => {
5228
- noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind);
5511
+ noteDiagnosticAttempt(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind, activeAdapter.name);
5229
5512
  return fetchWithHeaderTimeout(
5230
5513
  builtContinuationRequest.url,
5231
5514
  applyUpstreamRecoveryInit({
@@ -5254,7 +5537,10 @@ async function handleResponsesInner(
5254
5537
  nextContinuationRecoveryKind = undefined;
5255
5538
  response = await fetchContinuation(recoveryKind);
5256
5539
  } catch (error) {
5257
- if (options.abortSignal?.aborted || upstream.signal.aborted) {
5540
+ const validationEvent = requestValidationEvent(error);
5541
+ if (validationEvent) {
5542
+ yield validationEvent;
5543
+ } else if (options.abortSignal?.aborted || upstream.signal.aborted) {
5258
5544
  yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
5259
5545
  } else {
5260
5546
  yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
@@ -5300,7 +5586,10 @@ async function handleResponsesInner(
5300
5586
  try {
5301
5587
  response = await fetchContinuation("rate-limit-429");
5302
5588
  } catch (error) {
5303
- if (options.abortSignal?.aborted || upstream.signal.aborted) {
5589
+ const validationEvent = requestValidationEvent(error);
5590
+ if (validationEvent) {
5591
+ yield validationEvent;
5592
+ } else if (options.abortSignal?.aborted || upstream.signal.aborted) {
5304
5593
  yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
5305
5594
  } else {
5306
5595
  yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
@@ -5309,6 +5598,42 @@ async function handleResponsesInner(
5309
5598
  }
5310
5599
  }
5311
5600
 
5601
+ if (response.status === 403 && isAntigravityOAuth && antigravityAccountId) {
5602
+ recordAntigravityCooldown(antigravityAccountId, response.headers.get("retry-after"), Date.now(), "geoblock");
5603
+ }
5604
+ if (response.status === 429 && isAntigravityOAuth && antigravityAccountId) {
5605
+ recordAntigravityCooldown(antigravityAccountId, response.headers.get("retry-after"));
5606
+ const retryAfter = retryableAntigravity429DelayMs(response.headers.get("retry-after"));
5607
+ if (!antigravity429RetryAttempted && retryAfter !== null) {
5608
+ antigravity429RetryAttempted = true;
5609
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
5610
+ try {
5611
+ await sleepWithAbort(retryAfter, upstream.signal);
5612
+ } catch {
5613
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
5614
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
5615
+ } else {
5616
+ yield { type: "error", message: "Provider continuation failed: retry wait interrupted" };
5617
+ }
5618
+ return;
5619
+ }
5620
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
5621
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
5622
+ return;
5623
+ }
5624
+ try {
5625
+ response = await fetchContinuation("rate-limit-429");
5626
+ } catch (error) {
5627
+ if (options.abortSignal?.aborted || upstream.signal.aborted) {
5628
+ yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
5629
+ } else {
5630
+ yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
5631
+ }
5632
+ return;
5633
+ }
5634
+ }
5635
+ }
5636
+
5312
5637
  if (response.status === 429 && hasKeyPoolFailover(route.provider)) {
5313
5638
  const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
5314
5639
  retryAfter: response.headers.get("retry-after"),
@@ -5499,12 +5824,7 @@ async function handleResponsesInner(
5499
5824
  }
5500
5825
 
5501
5826
  if (!response.ok) {
5502
- // Same pre-read guard as the initial response's error branch: a non-2xx continuation body
5503
- // is still a Bun fetch body, and an abort landing before `.text()` attaches its reader
5504
- // orphans the internal rejection.
5505
- const detachContinuationErrorGuard = cancelBodyOnAbort(response.body, upstream.signal);
5506
- const errorText = await response.text().catch(() => "unknown error");
5507
- detachContinuationErrorGuard();
5827
+ const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
5508
5828
  yield {
5509
5829
  type: "error",
5510
5830
  status: response.status,
@@ -5520,7 +5840,13 @@ async function handleResponsesInner(
5520
5840
  const detachContinuationBodyGuard = cancelBodyOnAbort(response.body, upstream.signal);
5521
5841
  try {
5522
5842
  if (nextParsed.stream) {
5523
- yield* activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata);
5843
+ yield* diagnoseAdapterEvents(
5844
+ activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata),
5845
+ activeAdapter.name,
5846
+ diagnosticRequestId,
5847
+ logCtx,
5848
+ adapterDiagnosticState,
5849
+ );
5524
5850
  } else if (activeAdapter.parseResponse) {
5525
5851
  yield* await activeAdapter.parseResponse(response, translatorBudget, logCtx.activeTierMetadata);
5526
5852
  } else {
@@ -5530,7 +5856,10 @@ async function handleResponsesInner(
5530
5856
  detachContinuationBodyGuard();
5531
5857
  }
5532
5858
  } catch (error) {
5533
- if (options.abortSignal?.aborted) {
5859
+ const validationEvent = requestValidationEvent(error);
5860
+ if (validationEvent) {
5861
+ yield validationEvent;
5862
+ } else if (options.abortSignal?.aborted) {
5534
5863
  yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
5535
5864
  } else {
5536
5865
  yield { type: "error", message: `Provider continuation parse failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
@@ -5552,11 +5881,18 @@ async function handleResponsesInner(
5552
5881
  };
5553
5882
 
5554
5883
  if (parsed.stream) {
5555
- const initialEventStream = activeAdapter.parseStream(
5556
- upstreamResponse,
5557
- translatorBudget,
5558
- logCtx.activeTierMetadata,
5884
+ const initialEventStream = diagnoseAdapterEvents(
5885
+ activeAdapter.parseStream(upstreamResponse, translatorBudget, logCtx.activeTierMetadata),
5886
+ activeAdapter.name,
5887
+ diagnosticRequestId,
5888
+ logCtx,
5889
+ adapterDiagnosticState,
5559
5890
  );
5891
+ if (diagnosticContext) {
5892
+ diagnosticContext.adapterName = activeAdapter.name;
5893
+ diagnosticContext.attempt = logCtx.activeAttempt?.ordinal;
5894
+ diagnosticContext.recovery = logCtx.activeAttempt?.recoveryKinds.at(-1);
5895
+ }
5560
5896
  const eventStream = terminalGuardEnabled
5561
5897
  ? guardTerminalEventStream({
5562
5898
  parsed,
@@ -5592,6 +5928,7 @@ async function handleResponsesInner(
5592
5928
  ...(routedCompaction ? { compaction: true } : {}),
5593
5929
  // Same grok-surface split as the runTurn branch above.
5594
5930
  ...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
5931
+ ...(diagnosticContext ? { diagnostic: diagnosticContext } : {}),
5595
5932
  onUsage: usage => {
5596
5933
  // Raw adapter usage, pre wire-normalization (see the runTurn branch above).
5597
5934
  logCtx.usageFromBridge = true;