@bitkyc08/opencodex 2.81.0 → 2.82.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 (212) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  11. package/src/adapters/openai-responses/passthrough.ts +9 -4
  12. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  13. package/src/claude/agents-inject.ts +14 -3
  14. package/src/claude/context-windows.ts +19 -17
  15. package/src/claude/desktop-3p.ts +88 -12
  16. package/src/claude/desktop-profile.ts +2 -1
  17. package/src/claude/gateway-cache.ts +18 -1
  18. package/src/claude/inbound.ts +77 -3
  19. package/src/claude/intercept/cli-catalog.ts +3 -0
  20. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  21. package/src/claude/intercept/picker-models.ts +14 -6
  22. package/src/claude/intercept/settings.ts +3 -0
  23. package/src/claude/long-context.ts +66 -0
  24. package/src/claude/model-info.ts +19 -16
  25. package/src/claude/outbound.ts +94 -9
  26. package/src/claude/subagent-model.ts +40 -11
  27. package/src/cli/agent.ts +9 -5
  28. package/src/cli/aside-profile-recovery.ts +33 -0
  29. package/src/cli/capabilities-base.ts +5 -5
  30. package/src/cli/capabilities-integrations.ts +2 -2
  31. package/src/cli/cli-path-diagnostics.ts +132 -0
  32. package/src/cli/desktop-runtime-guidance.ts +65 -0
  33. package/src/cli/dispatch.ts +5 -0
  34. package/src/cli/doctor.ts +16 -3
  35. package/src/cli/index.ts +14 -9
  36. package/src/cli/integration-aside-sync.ts +2 -1
  37. package/src/cli/integrations.ts +16 -3
  38. package/src/cli/resolve.ts +25 -2
  39. package/src/cli/root.ts +4 -2
  40. package/src/cli/status.ts +64 -12
  41. package/src/cli/update-restart-child.ts +4 -2
  42. package/src/cli/update-restart-home.ts +28 -10
  43. package/src/cli/update-restart-service-record.ts +116 -0
  44. package/src/cli/update-restart-supervision.ts +96 -0
  45. package/src/cli/update-restart.ts +59 -13
  46. package/src/cli/v2.ts +27 -20
  47. package/src/cli/version-skew-notice.ts +77 -0
  48. package/src/clients/lazycodex.ts +1 -1
  49. package/src/clients/omo-role-models.ts +32 -7
  50. package/src/codex/auth-api/account-selection.ts +107 -0
  51. package/src/codex/auth-api/routes.ts +10 -49
  52. package/src/codex/auth-context.ts +20 -1
  53. package/src/codex/autostart-health.ts +12 -3
  54. package/src/codex/catalog/build-entries.ts +18 -0
  55. package/src/codex/catalog/bundled.ts +155 -26
  56. package/src/codex/catalog/combo-member.ts +3 -2
  57. package/src/codex/catalog/effort.ts +26 -5
  58. package/src/codex/catalog/gather-capture.ts +5 -0
  59. package/src/codex/catalog/model-hints.ts +15 -8
  60. package/src/codex/catalog/model-visibility.ts +3 -1
  61. package/src/codex/catalog/operator-tiers.ts +38 -0
  62. package/src/codex/catalog/provider-models.ts +43 -35
  63. package/src/codex/catalog/retained-sync.ts +7 -5
  64. package/src/codex/catalog/routed-gather.ts +15 -1
  65. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  66. package/src/codex/catalog-auto-refresh.ts +19 -0
  67. package/src/codex/catalog.ts +1 -1
  68. package/src/codex/codex-home-owner.ts +3 -0
  69. package/src/codex/config-write-lock.ts +333 -0
  70. package/src/codex/convergence.ts +2 -1
  71. package/src/codex/features.ts +257 -200
  72. package/src/codex/inject/config-write-section.ts +34 -0
  73. package/src/codex/inject/multi-agent-v2.ts +14 -6
  74. package/src/codex/inject/remove.ts +36 -11
  75. package/src/codex/inject/restore.ts +85 -20
  76. package/src/codex/inject-coordination.ts +15 -6
  77. package/src/codex/inject.ts +62 -20
  78. package/src/codex/journal.ts +62 -17
  79. package/src/codex/main-account-cache.ts +43 -0
  80. package/src/codex/paths.ts +2 -0
  81. package/src/codex/prepared-home.ts +18 -0
  82. package/src/codex/prompt-journal.ts +25 -12
  83. package/src/codex/prompt-layers.ts +49 -26
  84. package/src/codex/prompt-lock-claim.ts +134 -0
  85. package/src/codex/prompt-lock-evidence.ts +26 -0
  86. package/src/codex/prompt-lock-io.ts +16 -0
  87. package/src/codex/prompt-lock-owner.ts +130 -0
  88. package/src/codex/prompt-lock.ts +84 -53
  89. package/src/codex/routing/failure-window.ts +146 -0
  90. package/src/codex/routing/selection.ts +4 -1
  91. package/src/codex/routing.ts +13 -17
  92. package/src/codex/runtime.ts +106 -18
  93. package/src/codex/shim.ts +45 -19
  94. package/src/combos/jev-service-exchange.ts +281 -0
  95. package/src/combos/jev.ts +34 -220
  96. package/src/config/atomic-write.ts +1 -1
  97. package/src/config/codex-ws-reuse-setting.ts +10 -0
  98. package/src/config/live-reconcile.ts +17 -0
  99. package/src/config/load-degrade.ts +8 -0
  100. package/src/config/rebase-provenance.ts +11 -0
  101. package/src/config/schema/config-schema.ts +4 -0
  102. package/src/generated/compatibility-version.json +303 -171
  103. package/src/generated/model-metadata.ts +2 -2
  104. package/src/grok/inject.ts +50 -4
  105. package/src/grok/sync.ts +2 -1
  106. package/src/images/loop.ts +29 -11
  107. package/src/integrations/aside-profiles.ts +10 -3
  108. package/src/integrations/catalog-refresh.ts +18 -2
  109. package/src/integrations/writer.ts +9 -3
  110. package/src/lib/bun-path-runtime.d.mts +32 -0
  111. package/src/lib/bun-path-runtime.mjs +95 -0
  112. package/src/lib/bun-runtime-preflight.ts +140 -0
  113. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  114. package/src/lib/desktop-cli-handoff.mjs +92 -0
  115. package/src/lib/desktop-cli-record.d.mts +29 -0
  116. package/src/lib/desktop-cli-record.mjs +102 -0
  117. package/src/lib/request-execution-budget.ts +8 -0
  118. package/src/lib/synced-state-location.ts +46 -6
  119. package/src/lib/test-home-guard.ts +87 -1
  120. package/src/lib/translator-budget.ts +17 -4
  121. package/src/lib/windows-atomic-replace.ts +38 -16
  122. package/src/lib/windows-elevation.ts +6 -0
  123. package/src/lib/windows-secret-acl.ts +92 -25
  124. package/src/lib/winsw.ts +2 -3
  125. package/src/oauth/anthropic-account-refusal.ts +39 -1
  126. package/src/oauth/anthropic-routing.ts +1 -1
  127. package/src/oauth/anthropic-send-ownership.ts +3 -3
  128. package/src/oauth/chatgpt.ts +113 -41
  129. package/src/oauth/index.ts +6 -2
  130. package/src/oauth/store.ts +1 -1
  131. package/src/providers/azure-model-metadata.ts +115 -0
  132. package/src/providers/derive.ts +21 -0
  133. package/src/providers/quota/vendor-probes-key.ts +127 -17
  134. package/src/providers/quota.ts +1 -1
  135. package/src/responses/reasoning-envelope.ts +29 -1
  136. package/src/responses/reasoning-replay-cache.ts +17 -1
  137. package/src/responses/spill-store.ts +15 -2
  138. package/src/responses/state/metrics.ts +8 -0
  139. package/src/responses/state/spill-failure.ts +12 -0
  140. package/src/responses/state/spill-queue.ts +12 -5
  141. package/src/responses/state.ts +18 -10
  142. package/src/responses/task-input.ts +17 -0
  143. package/src/server/background-lifecycle.ts +27 -0
  144. package/src/server/chat-completions.ts +1 -0
  145. package/src/server/chat-native.ts +28 -12
  146. package/src/server/claude-messages.ts +43 -6
  147. package/src/server/inbound-body-admission.ts +1 -0
  148. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  149. package/src/server/index/serve-options.ts +10 -2
  150. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  151. package/src/server/index/startup-warnings.ts +19 -0
  152. package/src/server/index.ts +1 -0
  153. package/src/server/management/agent-settings-routes.ts +93 -32
  154. package/src/server/management/codex-agent-role-routes.ts +4 -2
  155. package/src/server/management/codex-prompt-routes.ts +1 -0
  156. package/src/server/management/codex-role-auto-assign.ts +13 -2
  157. package/src/server/management/config-routes.ts +59 -8
  158. package/src/server/management/context.ts +2 -2
  159. package/src/server/management/shared.ts +3 -3
  160. package/src/server/messages-native-oauth.ts +9 -8
  161. package/src/server/messages-native.ts +49 -13
  162. package/src/server/messages-response-headers.ts +14 -0
  163. package/src/server/request-log-conversation.ts +48 -5
  164. package/src/server/request-log.ts +31 -2
  165. package/src/server/responses/adapter-continuation.ts +2 -2
  166. package/src/server/responses/adapter-dispatch.ts +34 -13
  167. package/src/server/responses/codex-ws-exchange.ts +17 -2
  168. package/src/server/responses/codex-ws-pool.ts +100 -13
  169. package/src/server/responses/codex-ws-wire.ts +9 -0
  170. package/src/server/responses/combo-requested-effort.ts +3 -1
  171. package/src/server/responses/core-codex-account.ts +22 -1
  172. package/src/server/responses/core-combo-native.ts +20 -3
  173. package/src/server/responses/core-combo.ts +22 -14
  174. package/src/server/responses/core-lifetime.ts +3 -36
  175. package/src/server/responses/core-options.ts +5 -0
  176. package/src/server/responses/core-replay.ts +73 -0
  177. package/src/server/responses/core.ts +3 -3
  178. package/src/server/responses/fetch-helpers.ts +26 -3
  179. package/src/server/responses/passthrough-delivery.ts +24 -2
  180. package/src/server/responses/passthrough-dispatch.ts +19 -2
  181. package/src/server/responses/request-prepare.ts +4 -0
  182. package/src/server/responses/request-send-budget.ts +112 -49
  183. package/src/server/responses/response-effects.ts +1 -0
  184. package/src/server/responses/run-turn-execution.ts +2 -0
  185. package/src/server/responses/sidecar-execution.ts +25 -16
  186. package/src/server/responses/sidecar-send-budget.ts +58 -0
  187. package/src/server/responses/ws-upstream.ts +19 -6
  188. package/src/server/startup-health-cache.ts +2 -1
  189. package/src/server/workflow-refusal.ts +3 -0
  190. package/src/service/cli.ts +20 -4
  191. package/src/service/desktop-command-guard.ts +32 -0
  192. package/src/service/desktop-startup.ts +66 -40
  193. package/src/service/desktop-supervision.d.mts +26 -0
  194. package/src/service/desktop-supervision.mjs +140 -0
  195. package/src/service/managing-cli.ts +1 -0
  196. package/src/service/orchestration.ts +46 -20
  197. package/src/service/repair.ts +25 -9
  198. package/src/service/state.ts +4 -3
  199. package/src/service/systemd.ts +17 -8
  200. package/src/service/windows-ops.ts +61 -22
  201. package/src/storage/policy-job.ts +5 -1
  202. package/src/storage/policy-worker.ts +3 -1
  203. package/src/storage/policy.ts +9 -0
  204. package/src/types/config.ts +21 -0
  205. package/src/types/request.ts +5 -0
  206. package/src/types/wire.ts +1 -0
  207. package/src/update/index.ts +57 -9
  208. package/src/update/restart-ownership.ts +10 -3
  209. package/src/update/runtime-ownership.d.mts +6 -0
  210. package/src/update/runtime-ownership.mjs +23 -7
  211. package/src/web-search/loop.ts +5 -2
  212. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -43,6 +43,10 @@ const pacingWebsocketDowngradeWarned = new Set<string>();
43
43
  */
44
44
  const EGRESS_DECIDED = Symbol.for("opencodex.provider-egress.decided");
45
45
  const UPSTREAM_REWRITTEN = Symbol.for("opencodex.plugins.upstream-rewritten");
46
+ // Survives queued credential rebuilds and executor replacement without importing budget owners.
47
+ const PHYSICAL_DISPATCH = Symbol.for("opencodex.physical-dispatch");
48
+ // One WS attempt and its HTTP fallback are one logical physical-send receipt.
49
+ const WS_FALLBACK_RECEIPT = Symbol.for("opencodex.ws-fallback-receipt");
46
50
 
47
51
  /**
48
52
  * Announce once, per provider, that an explicit egress route moved this provider off the
@@ -200,6 +204,7 @@ export function sendWithConnectionPolicy(
200
204
  const largeCodexBody = typeof body === "string"
201
205
  && /^https:\/\/chatgpt\.com\/backend-api\/codex\/responses(?:\/compact)?$/.test(target)
202
206
  && Buffer.byteLength(body, "utf8") >= 1024 * 1024;
207
+ (init as Record<symbol, (() => void) | undefined> | undefined)?.[PHYSICAL_DISPATCH]?.();
203
208
  return physicalFetch(input, {
204
209
  ...init,
205
210
  ...(largeCodexBody ? { body: Buffer.from(body, "utf8") } : {}),
@@ -233,10 +238,13 @@ export interface ProviderFetchOptions {
233
238
  onCodexWsQuota?: CodexWsQuotaObserver;
234
239
  /** Synchronous admission at actual credential dispatch, after pacing/backoff. */
235
240
  beforeDispatch?: (headers: Headers) => void;
241
+ /** One-shot receipt after final local HTTP admission, immediately before executor entry. */
242
+ onPhysicalDispatch?: () => void;
236
243
  /** Revalidate/rebuild a queued request at its physical send boundary, after pacing. */
237
244
  dispatchOverride?: (input: Parameters<typeof globalThis.fetch>[0], init: RequestInit, execute: typeof globalThis.fetch) => Promise<Response>;
238
245
  }
239
246
 
247
+ /** Compose provider pacing and transport admission; receipts run only at executor entry. */
240
248
  export function providerFetch(
241
249
  provider: OcxProviderConfig,
242
250
  runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
@@ -301,7 +309,14 @@ export function providerFetch(
301
309
  options.beforeDispatch?.(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)));
302
310
  // No proxy option is attached here: a `dispatchOverride` may rebuild this request against
303
311
  // a different destination, so the route is decided at the physical send instead.
304
- const dispatchInit = { ...withUpstreamHttpVersion(input, init, provider), timeout: 0 };
312
+ const sharedReceipt = (init as Record<symbol, (() => void) | undefined> | undefined)?.[WS_FALLBACK_RECEIPT];
313
+ let notified = false;
314
+ const dispatchInit = { ...withUpstreamHttpVersion(input, init, provider), timeout: 0,
315
+ ...(options.onPhysicalDispatch ? { [PHYSICAL_DISPATCH]: () => {
316
+ if (sharedReceipt) sharedReceipt();
317
+ else if (!notified) { options.onPhysicalDispatch!(); notified = true; }
318
+ } } : {}),
319
+ };
305
320
  return options.dispatchOverride
306
321
  ? options.dispatchOverride(input, dispatchInit, dispatch)
307
322
  : dispatch(input, dispatchInit);
@@ -332,8 +347,16 @@ export function providerFetch(
332
347
  // used, protocol pin included: a WS turn that falls back is serving the
333
348
  // request over HTTP, and dropping the provider's `upstreamHttpVersion`
334
349
  // there would silently negotiate a transport the operator ruled out.
335
- return codexWsUpstreamFetch(input, init, httpFetch, runtime, options.onCodexWsQuota, options.beforeDispatch, options.nativeControl,
336
- async () => { (await waitForPacing(init.signal ?? undefined))?.release(); });
350
+ let notified = false;
351
+ const notifyOnce = (): void => {
352
+ if (notified) return;
353
+ notified = true;
354
+ options.onPhysicalDispatch?.();
355
+ };
356
+ const fallback = ((url: Parameters<typeof globalThis.fetch>[0], fallbackInit?: RequestInit) =>
357
+ httpFetch(url, { ...fallbackInit, [WS_FALLBACK_RECEIPT]: notifyOnce } as RequestInit)) as typeof globalThis.fetch;
358
+ return codexWsUpstreamFetch(input, init, fallback, runtime, options.onCodexWsQuota, options.beforeDispatch, options.nativeControl,
359
+ async () => { (await waitForPacing(init.signal ?? undefined))?.release(); }, notifyOnce);
337
360
  }
338
361
  return httpFetch(input, init);
339
362
  };
@@ -24,13 +24,16 @@ import { teeWithBoundedInspection } from "../inspection-tee";
24
24
  import {
25
25
  codexForwardTerminalOutcomeRecorder,
26
26
  usesCodexForwardPoolAuth,
27
+ liveMainQuotaDispatch,
27
28
  codexQuotaOutcomeMeta,
28
29
  codexDenialOutcomeMeta,
29
30
  isFixedCodexAccount,
30
31
  shouldDeferCodexResetDerivedCooldown,
31
32
  } from "./core-codex-account";
33
+ import { isMainQuotaDispatchLive, isMainQuotaDispatchWsClaimed } from "../../codex/main-account-cache";
34
+ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/account-id";
32
35
  import type { ResponsesTerminalStatus } from "../../bridge";
33
- import { isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./ws-upstream";
36
+ import { isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse, isCodexWsPreludeProjection } from "./ws-upstream";
34
37
  import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-model-fallback";
35
38
  import { recordCodexUpstreamOutcome } from "../../codex/routing";
36
39
  import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant, releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
@@ -139,6 +142,7 @@ import {
139
142
  httpStatusFromTerminalError,
140
143
  inspectResponseLogJson,
141
144
  inspectResponseLogSsePayloadParsed,
145
+ noteUpstreamRequestId,
142
146
  } from "../request-log";
143
147
  import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
144
148
  import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
@@ -406,6 +410,9 @@ export async function deliverPassthroughResponse(
406
410
  | "localUpstream"
407
411
  >,
408
412
  ): Promise<Response> {
413
+ const { route } = requestState;
414
+ // The proof must belong to the response whose headers are published.
415
+ const arrivalMainDispatch = liveMainQuotaDispatch(admissionState.authCtx, route.provider);
409
416
  const { logCtx, config, options, req } = requestContext;
410
417
  const {
411
418
  codexSafetyBufferingOptions,
@@ -429,7 +436,7 @@ export async function deliverPassthroughResponse(
429
436
  normalizeFunctionCompletionJson,
430
437
  } = nativeExchange;
431
438
  const { commitReasoningReplayServingRoute, recordTerminalOutcomes } = responseEffects;
432
- const { parsed, route, subagentQuotaFailureModel, clientRequestedStream, translatorBudget, inboundWire } = requestState;
439
+ const { parsed, subagentQuotaFailureModel, clientRequestedStream, translatorBudget, inboundWire } = requestState;
433
440
  const enforceDeclaredToolNames = inboundWire !== "chat" && inboundWire !== "anthropic";
434
441
  const { openAiSidecar } = sidecarState;
435
442
  const { requestBindings } = transportState;
@@ -452,6 +459,7 @@ export async function deliverPassthroughResponse(
452
459
  }
453
460
 
454
461
  const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions);
462
+ if (!upstreamResponse.ok) noteUpstreamRequestId(logCtx, headers);
455
463
  const resolvedModel = headers.get("openai-model")?.trim();
456
464
  if (resolvedModel) {
457
465
  logCtx.servedModel = resolvedModel;
@@ -535,6 +543,20 @@ export async function deliverPassthroughResponse(
535
543
  ...(admissionState.authCtx.kind === "pool" ? { credentialGeneration: admissionState.authCtx.generation } : {}),
536
544
  });
537
545
  }
546
+ } else {
547
+ // The WS observer is the only plain-main publisher for a WebSocket exchange;
548
+ // a prelude projection carries the prelude snapshot, not fresh evidence.
549
+ // The dispatch claim is authoritative because downstream wrappers can replace the Response.
550
+ if (arrivalMainDispatch && !isMainQuotaDispatchWsClaimed(arrivalMainDispatch)
551
+ && !(isCodexWsUpstreamResponse(upstreamResponse) || isCodexWsPreludeProjection(upstreamResponse))) {
552
+ const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
553
+ // Import yields; same-account token replacement leaves the identity writer live.
554
+ // Re-check the credential fence with no await before publication.
555
+ if (isMainQuotaDispatchLive(arrivalMainDispatch)) {
556
+ applyAccountQuotaFromUpstreamHeaders(MAIN_CODEX_ACCOUNT_ID, upstreamResponse.headers,
557
+ arrivalMainDispatch.configGeneration, arrivalMainDispatch.writer, { modelId: route.modelId });
558
+ }
559
+ }
538
560
  }
539
561
 
540
562
  // Non-2xx passthrough failures must never reach Codex as an empty body —
@@ -12,6 +12,7 @@ import type { ResponsesTransport } from "./request-transport";
12
12
  import type { ResponsesEffects } from "./response-effects";
13
13
  import type { ResponsesSendBudget } from "./request-send-budget";
14
14
  import { transientSendCapFor } from "./request-send-budget";
15
+ import { renewMainQuotaDispatchForAttempt } from "../../codex/main-account-cache";
15
16
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
16
17
  import { isLocalUpstream } from "../../lib/local-upstream";
17
18
  import { codexSafetyBufferingFilterOptions, terminalStatusFromParsed } from "../relay";
@@ -225,6 +226,10 @@ export async function preparePassthroughExchange(
225
226
  | "pendingHopPermit"
226
227
  | "workflowRootId"
227
228
  | "sendsUsed"
229
+ | "targetSendsUsed"
230
+ | "initialSendAllowance"
231
+ | "noteInitialDispatch"
232
+ | "adapterSendBudget"
228
233
  >,
229
234
  ) {
230
235
  const { config, logCtx, options, req } = requestContext;
@@ -753,7 +758,7 @@ export async function preparePassthroughExchange(
753
758
  const transientSendPolicy = () => transientRetryPolicyFor(route.provider);
754
759
  const transientSendAttempts = (): number => transientSendCapFor(
755
760
  transientSendPolicy()?.attempts,
756
- sendBudgetState.sendsUsed,
761
+ sendBudgetState.targetSendsUsed,
757
762
  );
758
763
  const configuredTransientSendBudgetExhausted = (): boolean =>
759
764
  transientSendPolicy() !== null && transientSendAttempts() === 0;
@@ -798,6 +803,9 @@ export async function preparePassthroughExchange(
798
803
  const sendAmbiguousReplacement = (
799
804
  signal: AbortSignal = upstream.signal,
800
805
  ): Promise<Response> => {
806
+ if (admissionState.authCtx.kind === "main" && admissionState.authCtx.mainQuotaDispatch) {
807
+ admissionState.authCtx.mainQuotaDispatch = renewMainQuotaDispatchForAttempt(admissionState.authCtx.mainQuotaDispatch);
808
+ }
801
809
  const report = transientSendReporter();
802
810
  let started = false;
803
811
  const run = () => fetchWithHeaderTimeout(
@@ -968,6 +976,11 @@ export async function preparePassthroughExchange(
968
976
  };
969
977
  const initialBodyRefusal = refuseOversizedOutboundBody(request);
970
978
  if (initialBodyRefusal) return initialBodyRefusal;
979
+ // A combo-owned first send is settled by the physical-dispatch receipt unless spend
980
+ // enforcement is on, where the shared reporter already owns the permit lifecycle.
981
+ // Stable per request: the Combo booking (`reserveDispatch`) already started and froze the spend
982
+ // policy, so `spendEnforced` cannot change before dispatch even though no reporter starts it here.
983
+ const receiptMode = !sendBudgetState.adapterSendBudget?.spendEnforced && Boolean(options.comboInitialSend);
971
984
  try {
972
985
  // Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
973
986
  // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
@@ -994,6 +1007,7 @@ export async function preparePassthroughExchange(
994
1007
  dispatchOverride: oauthDispatch(request),
995
1008
  providerName: route.providerName,
996
1009
  modelId: route.modelId,
1010
+ onPhysicalDispatch: receiptMode ? sendBudgetState.noteInitialDispatch : undefined,
997
1011
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
998
1012
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
999
1013
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
@@ -1004,7 +1018,10 @@ export async function preparePassthroughExchange(
1004
1018
  .then(adoptObservedResponse);
1005
1019
  },
1006
1020
  { abortSignal: upstream.signal, label: safeHostLabel(request.url),
1007
- attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: transientSendReporter(),
1021
+ attempts: sendBudgetState.initialSendAllowance(transientSendAttempts()),
1022
+ // Reporter path: direct requests and enforced spend. Receipt-mode sends are settled
1023
+ // by `onPhysicalDispatch`; a reporter as well would count them twice.
1024
+ ...(receiptMode ? {} : { onSendsConsumed: transientSendReporter() }),
1008
1025
  claimAmbiguousResend: claimPreHeaderResend,
1009
1026
  },
1010
1027
  );
@@ -381,6 +381,10 @@ export async function prepareResponsesRequest(
381
381
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
382
382
  try {
383
383
  parsed = parseRequest(body);
384
+ if (options.inboundWire === "anthropic") {
385
+ parsed._nativeReasoningReplay = options.nativeReasoningReplay;
386
+ parsed._nativeReasoningMint = options.nativeReasoningMint;
387
+ }
384
388
  parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
385
389
  // The body may have been rebuilt since the inbound observation (previous-response
386
390
  // expansion); alias the parsed raw body to the same draft so the outbound
@@ -9,7 +9,7 @@ import {
9
9
  import { poolContinuityRefusalReason, workflowRefusalResponse } from "../workflow-refusal";
10
10
  import type { AttemptRecoveryKind, AttemptRecoveryWithheld } from "../../usage/log";
11
11
  import { noteAttemptRecoveryWithheld, noteAttemptSend } from "../request-log";
12
- import { TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
12
+ import { TRANSIENT_RETRY_MAX_ATTEMPTS, SendBudgetExhaustedError } from "../../lib/upstream-retry";
13
13
  import type {
14
14
  DispatchDecision,
15
15
  DispatchIntent,
@@ -58,10 +58,24 @@ export function createResponsesSendBudget(
58
58
  // sends once per child seven hundred times, so every send charged to the request is charged
59
59
  // to the root as well (#4546).
60
60
  const workflowRootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
61
- const inheritedPermit = options.compactionRecoveryPermit ?? options.comboDispatchPermit;
61
+ // A child that owns its Combo booking settles that exact permit through the reporter, even
62
+ // when its caller handed ownership over only as `comboInitialSend`.
63
+ const inheritedPermit = options.compactionRecoveryPermit ?? options.comboDispatchPermit
64
+ ?? options.comboInitialSend?.permit;
62
65
  let pendingHopPermit: SingleUseDispatchPermit | undefined = options.compactionRecoveryPermit;
66
+ let initialPermit = options.comboInitialSend?.permit;
67
+ // Count this owner's physical sends, not later bookings made by other shared-ledger owners.
68
+ // Direct callers retain sends reported before this owner was constructed.
69
+ let targetSendsUsed = initialPermit ? 0 : Math.max(0,
70
+ sendBudget.used - (options.compactionRecoveryPermit ? 1 : 0));
71
+ /** Attribute reported physical sends to this owner; the prepaid booking is spent once reported. */
72
+ const noteTargetSends = (charged: number): void => {
73
+ targetSendsUsed += charged;
74
+ if (charged > 0) initialPermit = undefined;
75
+ };
63
76
  const recordTransientSends = (used: number, permit?: SingleUseDispatchPermit): void => {
64
77
  const charged = Math.max(0, used);
78
+ noteTargetSends(charged);
65
79
  reportDispatchSends(sendBudget, charged, permit);
66
80
  options.onCompactionRecoverySendsReported?.(charged);
67
81
  chargeWorkflowSends(workflowRootId, charged);
@@ -71,6 +85,7 @@ export function createResponsesSendBudget(
71
85
  const transientSendReporter = (permit = pendingHopPermit ?? inheritedPermit) =>
72
86
  createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? logCtx.provider,
73
87
  identityId: logCtx.accountLogLabel }), permit, charged => {
88
+ noteTargetSends(Math.max(0, charged));
74
89
  options.onCompactionRecoverySendsReported?.(charged);
75
90
  chargeWorkflowSends(workflowRootId, charged);
76
91
  });
@@ -136,6 +151,7 @@ export function createResponsesSendBudget(
136
151
  // Ordinal 1 is skipped because the caller normally records it before dispatch. An adapter
137
152
  // that reports every send asks for it to be counted here instead, so that the first send is
138
153
  // logged where it actually happens rather than before admission could still refuse it.
154
+ targetSendsUsed += 1;
139
155
  if (send.ordinal <= 1 && options.includeFirst !== true) return;
140
156
  noteAttemptSend(logCtx.activeAttempt, inputTokens, send.recovery);
141
157
  };
@@ -191,21 +207,29 @@ export function createResponsesSendBudget(
191
207
  * The hop hands its reservation down through `pendingHopPermit`, the same seam the
192
208
  * passthrough ladder already uses, and this view spends it on the adapter's FIRST
193
209
  * reservation. Every later send in that ladder is a new physical send and is charged
194
- * normally. The refundable view below keeps an unused hop open until executor invocation;
195
- * its adapter release hands back an abandoned replay. The ordinary view retains early
196
- * settlement for existing callers.
210
+ * normally. Claiming preserves the refundable single-use booking until physical dispatch;
211
+ * an abandoned replay releases only that exact booking.
197
212
  */
198
- const claimHopPermit = (): SingleUseDispatchPermit | undefined => {
199
- const permit = pendingHopPermit;
200
- pendingHopPermit = undefined;
201
- return permit;
213
+ const permits = {
214
+ /** Transfer the initial booking once to the adapter dispatch owner. */
215
+ claimInitialPermit: () => {
216
+ const permit = initialPermit;
217
+ initialPermit = undefined;
218
+ return permit;
219
+ },
220
+ /** Transfer a pending recovery booking once, without creating a new allowance. */
221
+ claimHopPermit: () => {
222
+ const permit = pendingHopPermit;
223
+ pendingHopPermit = undefined;
224
+ return permit;
225
+ },
202
226
  };
203
- const adapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
204
- ? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit });
205
- // Opt in only where the adapter's executor is the confirmation boundary. Ordinary callers
206
- // keep the existing early settlement contract, including already-settled permit fallback.
207
- const refundableAdapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
208
- ? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit }, true);
227
+ const adapterDispatchBudget = adapterSendBudget === undefined
228
+ ? undefined : adapterDispatchBudgetView(adapterSendBudget, permits);
229
+ // Preserve the existing opted-in view for direct callers, including its admitted replacement
230
+ // of an already-settled hop. Exact-owned Combo and sidecar handoffs use the ordinary view.
231
+ const refundableAdapterDispatchBudget = adapterSendBudget === undefined
232
+ ? undefined : adapterDispatchBudgetView(adapterSendBudget, permits, true);
209
233
  /**
210
234
  * How many sends a recovery leg may make, and the permit that authorises the last one.
211
235
  *
@@ -292,12 +316,34 @@ export function createResponsesSendBudget(
292
316
  noteTransientSends,
293
317
  transientSendReporter,
294
318
  remainingTransientSendBudget,
319
+ /** Only this child's prepaid send, not every pending external booking, is spendable here. */
320
+ initialSendAllowance: (cap: number): number => Math.min(cap,
321
+ remainingTransientSendBudget(cap) + (initialPermit ? 1 : 0)),
322
+ /** At final executor admission, settle only the owning booking or reserve this send. */
323
+ noteInitialDispatch: (prepaid = initialPermit): void => {
324
+ if (prepaid) {
325
+ if (!prepaid.assumeCharge()) throw new SendBudgetExhaustedError();
326
+ if (prepaid === initialPermit) initialPermit = undefined;
327
+ } else if (adapterSendBudget && (options.comboInitialSend || options.compactionRecoveryPermit)) {
328
+ const decision = adapterSendBudget.reserveDispatch({ sendClass: "transient",
329
+ targetKey: adapterSendBudget.lastTargetKey ?? `${logCtx.provider}/${logCtx.model}` });
330
+ if (!decision.allowed || !decision.permit.use()) throw new SendBudgetExhaustedError();
331
+ } else {
332
+ // Legacy direct callers can prebook without handing over a permit; keep their reporter.
333
+ noteTransientSends(1);
334
+ return;
335
+ }
336
+ targetSendsUsed += 1;
337
+ options.onCompactionRecoverySendsReported?.(1);
338
+ chargeWorkflowSends(workflowRootId, 1);
339
+ },
340
+ /** Physical sends reported by this owner; unrelated shared reservations cannot consume it. */
341
+ get targetSendsUsed(): number { return targetSendsUsed; },
295
342
  /**
296
- * Physical sends this logical request has already made.
343
+ * Charged sends, including open prepaid reservations, across this logical request.
297
344
  *
298
- * A live getter, not a snapshot: it is read once per dispatch leg to resolve a configured
299
- * ladder, and a value frozen at construction would answer for a request that had sent
300
- * nothing.
345
+ * A live getter, not a completed-send snapshot. Initial ladders use the child's own
346
+ * prepaid allowance and target-local count; shared admission keeps every booking spent.
301
347
  */
302
348
  get sendsUsed(): number { return sendBudget.used; },
303
349
  adapterSendBudget,
@@ -335,8 +381,11 @@ export type ResponsesSendBudget = Exclude<ReturnType<typeof createResponsesSendB
335
381
  */
336
382
  function adapterDispatchBudgetView(
337
383
  budget: RequestExecutionBudget,
338
- hop: { claimHopPermit: () => SingleUseDispatchPermit | undefined },
339
- refundableHop = false,
384
+ hop: {
385
+ claimInitialPermit: () => SingleUseDispatchPermit | undefined;
386
+ claimHopPermit: () => SingleUseDispatchPermit | undefined;
387
+ },
388
+ legacyHopReplacement = false,
340
389
  ): RequestExecutionBudget {
341
390
  return {
342
391
  get physicalStarted() { return budget.physicalStarted; },
@@ -360,10 +409,22 @@ function adapterDispatchBudgetView(
360
409
  claimAmbiguousResend: (limit: number): boolean => budget.claimAmbiguousResend?.(limit) === true,
361
410
  get ambiguousResendSpent(): boolean { return budget.ambiguousResendSpent === true; },
362
411
  reserveDispatch(intent: DispatchIntent): DispatchDecision {
363
- const refundable = refundableHop || budget.spendEnforced === true;
364
412
  // A dispatch whose upstream state is unknown is refused on its own merits. A hop that
365
413
  // already paid does not make an unsafe replay safe, so that check stays with the budget.
366
414
  if (intent.replaySafe !== false) {
415
+ // The Combo reserves a logical provider/model identity. The child's first actual
416
+ // endpoint is not a recovery transition; retain the original refundable permit.
417
+ const initialPermit = hop.claimInitialPermit();
418
+ if (initialPermit) {
419
+ budget.bindPrepaidTarget?.(intent.targetKey);
420
+ return { allowed: true, permit: {
421
+ sendClass: initialPermit.sendClass,
422
+ execute: run => initialPermit.execute ? initialPermit.execute(run) : run(),
423
+ use: () => initialPermit.assumeCharge(),
424
+ assumeCharge: () => initialPermit.assumeCharge(),
425
+ release: () => initialPermit.release(),
426
+ } };
427
+ }
367
428
  const hopPermit = hop.claimHopPermit();
368
429
  if (hopPermit && intent.targetKey !== budget.lastTargetKey) {
369
430
  // The rotated credential can select a different regional endpoint. Replace the
@@ -372,35 +433,37 @@ function adapterDispatchBudgetView(
372
433
  hopPermit.release();
373
434
  return budget.reserveDispatch({ ...intent, sendClass: hopPermit.sendClass });
374
435
  }
375
- if (hopPermit !== undefined && (refundable || hopPermit.assumeCharge())) {
376
- let settled = false;
377
- let executionPermit = hopPermit;
378
- const confirm = (): boolean => {
379
- if (settled) return false;
380
- settled = true;
381
- // Admission can still reject this replay. Close its external booking only when
382
- // the adapter invokes its executor, leaving release() able to refund it until then.
383
- if (!refundable || hopPermit.assumeCharge()) return true;
384
- // Preserve the old fallback for a permit another leg already settled.
385
- const fresh = budget.reserveDispatch(intent);
386
- if (!fresh.allowed) return false;
387
- executionPermit = fresh.permit;
388
- return fresh.permit.assumeCharge();
389
- };
390
- return {
391
- allowed: true,
392
- permit: {
436
+ // Reservation precedes pacing and beforeDispatch. Transfer the exact open booking;
437
+ // only physical consumption closes external reporting and makes it non-refundable.
438
+ // The underlying permit enforces single use, including settlement by another owner.
439
+ if (hopPermit !== undefined) {
440
+ if (legacyHopReplacement) {
441
+ let settled = false;
442
+ let executionPermit = hopPermit;
443
+ const confirm = (): boolean => {
444
+ if (settled) return false;
445
+ settled = true;
446
+ if (hopPermit.assumeCharge()) return true;
447
+ // Preserve the old fallback for a permit another leg already settled.
448
+ const fresh = budget.reserveDispatch(intent);
449
+ if (!fresh.allowed) return false;
450
+ executionPermit = fresh.permit;
451
+ return fresh.permit.assumeCharge();
452
+ };
453
+ return { allowed: true, permit: {
393
454
  sendClass: hopPermit.sendClass,
394
455
  execute: run => executionPermit.execute ? executionPermit.execute(run) : run(),
395
- use: confirm,
396
- assumeCharge: confirm,
397
- release: (): void => {
398
- if (!refundable || settled) return;
399
- settled = true;
400
- hopPermit.release();
401
- },
402
- },
403
- };
456
+ use: confirm, assumeCharge: confirm,
457
+ release: () => { if (!settled) { settled = true; hopPermit.release(); } },
458
+ } };
459
+ }
460
+ return { allowed: true, permit: {
461
+ sendClass: hopPermit.sendClass,
462
+ execute: run => hopPermit.execute ? hopPermit.execute(run) : run(),
463
+ use: () => hopPermit.assumeCharge(),
464
+ assumeCharge: () => hopPermit.assumeCharge(),
465
+ release: () => hopPermit.release(),
466
+ } };
404
467
  }
405
468
  }
406
469
  return budget.reserveDispatch(intent);
@@ -75,6 +75,7 @@ export function createResponsesEffects(
75
75
  // message, and leave Codex fataling on a missing compaction item (#422).
76
76
  const commitReasoningReplayServingRoute = (outboundHeaders?: HeadersInit): void => {
77
77
  commitReasoningReplayServingIdentity(parsed._reasoningReplayScope);
78
+ if (parsed._nativeReasoningMint) parsed._nativeReasoningMint.owner = parsed._nativeReasoningBoundOwner;
78
79
  rememberServingConversationStateIssuer(admissionState.authCtx, poolAffinityKey);
79
80
  // History has no model namespace. Record the account that actually accepted this
80
81
  // final attempt, after refresh/failover, rather than guessing from mutable affinity.
@@ -235,6 +235,7 @@ export async function executeResponsesRunTurn(
235
235
  preacquiredSlot?: ProviderRequestSlot,
236
236
  ): Promise<void> => {
237
237
  const attemptSeq = ++runTurnAttemptSeq;
238
+ if (attemptSeq === 1 && options.comboInitialSend) options.comboInitialSend.producerOwned = true;
238
239
  let pacingSlot = preacquiredSlot;
239
240
  const emit = (event: AdapterEvent) => {
240
241
  options.onCompactionRecoveryAdapterEvent?.(event);
@@ -339,6 +340,7 @@ export async function executeResponsesRunTurn(
339
340
  message: err instanceof Error ? err.message : String(err),
340
341
  });
341
342
  } finally {
343
+ if (attemptSeq === 1) options.comboInitialSend?.permit.release();
342
344
  devinProducers.delete(producerAbort);
343
345
  cleanupProducerAbort?.();
344
346
  releaseProviderRequestSlot(pacingSlot);
@@ -48,6 +48,7 @@ import { normalizeLogConversationId } from "../request-log-conversation";
48
48
  import { rememberResponseState } from "../../responses/state";
49
49
  import { trackStreamLifetime } from "../lifecycle";
50
50
  import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
51
+ import { createSidecarSendBudget } from "./sidecar-send-budget";
51
52
 
52
53
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
53
54
  export async function executeResponsesSidecars(
@@ -90,7 +91,7 @@ export async function executeResponsesSidecars(
90
91
  | "notifyResponseComplete"
91
92
  | "cancelResponseCompletion"
92
93
  >,
93
- sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "pendingHopPermit" | "noteAdapterPhysicalSend">,
94
+ sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
94
95
  ) {
95
96
  const { config, options, logCtx } = requestContext;
96
97
  const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
@@ -114,6 +115,7 @@ export async function executeResponsesSidecars(
114
115
  } = requestState;
115
116
  const { routedCompaction, openAiSidecar } = sidecarState;
116
117
  const { reserveCredentialHop } = sendBudgetState;
118
+ const sidecarBudget = createSidecarSendBudget(options, sendBudgetState, () => logCtx.usageLogInputTokens);
117
119
  const {
118
120
  commitReasoningReplayServingRoute,
119
121
  continuationStateForResponse,
@@ -202,7 +204,7 @@ export async function executeResponsesSidecars(
202
204
  ): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
203
205
  const antigravityValidationResponse = canRunWebSearch && route.providerName === "google-antigravity"
204
206
  && route.provider.authMode === "oauth" && originalResponse?.status === 403;
205
- if (route.providerName !== "kiro" && !(anthropicInstance && originalResponse?.status === 403)
207
+ if (route.providerName !== "kiro" && !(anthropicInstance && (originalResponse?.status === 403 || originalResponse?.status === 401))
206
208
  && !antigravityValidationResponse && originalResponse && originalResponse.status !== 429) return null;
207
209
  let antigravityVerification = false;
208
210
  if (antigravityValidationResponse) {
@@ -264,7 +266,8 @@ export async function executeResponsesSidecars(
264
266
  return null;
265
267
  }
266
268
  recoveryKind = "oauth-account-403";
267
- sendBudgetState.pendingHopPermit = hop.permit;
269
+ if (!options.comboInitialSend) sendBudgetState.pendingHopPermit = hop.permit;
270
+ else sidecarBudget.ownCredentialHop(hop.permit);
268
271
  }
269
272
  const rotated = !originalResponse || originalResponse.status === 429
270
273
  ? rotateProviderTransportOn429(config, route.providerName, route.provider, {
@@ -323,8 +326,9 @@ export async function executeResponsesSidecars(
323
326
  return null;
324
327
  }
325
328
  recoveryKind = "oauth-account-429";
326
- if (canRunWebSearch && route.providerName === "google-antigravity") sendBudgetState.pendingHopPermit = hop.permit;
327
- else hop.permit?.use();
329
+ if (!options.comboInitialSend && canRunWebSearch && route.providerName === "google-antigravity")
330
+ sendBudgetState.pendingHopPermit = hop.permit;
331
+ else sidecarBudget.ownCredentialHop(hop.permit);
328
332
  } else if (
329
333
  // Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
330
334
  // web-search or image-bridge turn was terminal even with the pool fully enabled -- while
@@ -364,8 +368,8 @@ export async function executeResponsesSidecars(
364
368
  hop.permit?.release();
365
369
  return null;
366
370
  }
367
- recoveryKind = "anthropic-oauth-429";
368
- hop.permit?.use();
371
+ recoveryKind = originalResponse?.status === 401 ? "oauth-401" : "anthropic-oauth-429";
372
+ sidecarBudget.ownCredentialHop(hop.permit);
369
373
  } else {
370
374
  // No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
371
375
  // credential. The 429 is terminal for this sidecar turn.
@@ -451,11 +455,14 @@ export async function executeResponsesSidecars(
451
455
  const imageProviderFetch = providerFetch(
452
456
  route.provider,
453
457
  options.codexWsRuntimeIdentity,
454
- { providerName: route.providerName, modelId: route.modelId },
458
+ { providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(transportState.adapter) },
455
459
  );
456
460
  const imgResponse = await runWithImageBridge({
457
461
  parsed, adapter: transportState.adapter,
458
- incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget },
462
+ incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
463
+ onProducerStart: sidecarBudget.takeProducerOwnership,
464
+ onProducerEnd: sidecarBudget.release,
465
+ onIterationEnd: sidecarBudget.releaseUnsentHop,
459
466
  ...(imgPlan ? { plan: imgPlan } : {}),
460
467
  ...(vidPlan ? { videoPlan: vidPlan } : {}),
461
468
  forwardHeaders: requestState.selectedForwardHeaders,
@@ -471,10 +478,10 @@ export async function executeResponsesSidecars(
471
478
  stallTimeoutSec: config.stallTimeoutSec,
472
479
  waitForRequestSlot: imageProviderFetch.waitForPacing,
473
480
  fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
474
- fetchForRequest: (request, iterParsed) => {
481
+ fetchForRequest: (request, iterParsed, adapter) => {
475
482
  const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
476
483
  dispatchOverride: oauthDispatch(request, iterParsed),
477
- providerName: route.providerName, modelId: route.modelId,
484
+ providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
478
485
  });
479
486
  return fetch.unpacedFetch ?? fetch;
480
487
  },
@@ -541,12 +548,14 @@ export async function executeResponsesSidecars(
541
548
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
542
549
  providerName: route.providerName,
543
550
  modelId: route.modelId,
551
+ ...sidecarBudget.fetchOptions(transportState.adapter),
544
552
  })(input, init)) as typeof globalThis.fetch;
545
553
  const wsResponse = await runWithWebSearch({
546
554
  parsed, adapter: transportState.adapter,
547
- fetchForRequest: (request, iterParsed) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
555
+ onIterationEnd: sidecarBudget.releaseUnsentHop,
556
+ fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
548
557
  dispatchOverride: oauthDispatch(request, iterParsed),
549
- providerName: route.providerName, modelId: route.modelId,
558
+ providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
550
559
  }),
551
560
  incomingMeta: {
552
561
  headers: requestState.selectedForwardHeaders,
@@ -554,11 +563,11 @@ export async function executeResponsesSidecars(
554
563
  abortSignal: options.abortSignal,
555
564
  translatorBudget,
556
565
  providerFetch: routedProviderFetch,
557
- ...(route.providerName === "google-antigravity" ? {
566
+ ...(!options.comboInitialSend && route.providerName === "google-antigravity" ? {
558
567
  sendBudget: sendBudgetState.refundableAdapterDispatchBudget,
559
568
  onPhysicalSend: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) =>
560
569
  sendBudgetState.noteAdapterPhysicalSend(logCtx.usageLogInputTokens, send),
561
- } : {}),
570
+ } : sidecarBudget.incomingMeta),
562
571
  },
563
572
  backend: wsPlan.backend,
564
573
  forwardProvider: wsPlan.forwardSidecar?.provider,
@@ -591,7 +600,7 @@ export async function executeResponsesSidecars(
591
600
  stallTimeoutSec: wsPlan.stallTimeoutSec,
592
601
  streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
593
602
  on429: rotateSidecarProviderOn429,
594
- ...(route.providerName === "google-antigravity" ? { onIterationEnd: () => {
603
+ ...(!options.comboInitialSend && route.providerName === "google-antigravity" ? { onIterationEnd: () => {
595
604
  // The next Antigravity adapter dispatch claims this permit. If request construction,
596
605
  // admission or cancellation stopped it first, return the unused send allowance.
597
606
  const permit = sendBudgetState.pendingHopPermit;