@bitkyc08/opencodex 2.88.0 → 2.89.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 (205) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/{index-B5bsvxor.js → index-uchD-ngf.js} +13 -13
  7. package/gui/dist/assets/{quota-summary-D8y2wTsB.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic.ts +10 -3
  13. package/src/adapters/coding-agent/protocol.ts +6 -0
  14. package/src/adapters/google-http.ts +3 -2
  15. package/src/adapters/google-vertex-endpoint.ts +67 -0
  16. package/src/adapters/google.ts +35 -29
  17. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  18. package/src/adapters/openai-responses/passthrough.ts +17 -12
  19. package/src/adapters/openai-responses/reasoning.ts +68 -1
  20. package/src/adapters/registry.ts +1 -1
  21. package/src/adapters/upstream-http-error.ts +3 -2
  22. package/src/adapters/zed.ts +7 -4
  23. package/src/bridge/internal.ts +11 -1
  24. package/src/bridge/response-json.ts +2 -2
  25. package/src/bridge/sse.ts +10 -4
  26. package/src/claude/inbound.ts +12 -26
  27. package/src/claude/outbound.ts +5 -0
  28. package/src/cli/account-api.ts +13 -1
  29. package/src/cli/account-extended.ts +17 -5
  30. package/src/cli/account-key-quota.ts +4 -1
  31. package/src/cli/account.ts +1 -1
  32. package/src/cli/capabilities-agents-routing.ts +45 -8
  33. package/src/cli/capabilities-base.ts +5 -5
  34. package/src/cli/capabilities-provider-models.ts +5 -4
  35. package/src/cli/combo-result.ts +8 -2
  36. package/src/cli/combo.ts +17 -0
  37. package/src/cli/help.ts +1 -1
  38. package/src/cli/message-command.ts +4 -1
  39. package/src/cli/message-remote-args.ts +69 -0
  40. package/src/cli/message-remote-command.ts +77 -0
  41. package/src/cli/provider-runtime.ts +69 -1
  42. package/src/cli/registry.ts +3 -3
  43. package/src/codex/account-pause.ts +6 -1
  44. package/src/codex/account-runtime-state.ts +5 -0
  45. package/src/codex/account-store.ts +30 -3
  46. package/src/codex/auth-api/account-list.ts +3 -0
  47. package/src/codex/auth-api/main-account-probe.ts +3 -2
  48. package/src/codex/auth-context.ts +9 -4
  49. package/src/codex/catalog/aggregation.ts +15 -0
  50. package/src/codex/catalog/auto-review.ts +2 -1
  51. package/src/codex/catalog/build-entries.ts +17 -7
  52. package/src/codex/catalog/derive-entry.ts +3 -1
  53. package/src/codex/catalog/metadata.ts +2 -0
  54. package/src/codex/catalog/model-visibility.ts +0 -2
  55. package/src/codex/catalog/ownership.ts +71 -0
  56. package/src/codex/catalog/parsing.ts +12 -3
  57. package/src/codex/catalog/retained-sync.ts +2 -1
  58. package/src/codex/catalog/routed-removal.ts +4 -4
  59. package/src/codex/convergence.ts +2 -1
  60. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  61. package/src/codex/low-quota-protection.ts +57 -31
  62. package/src/codex/main-account-cache.ts +77 -3
  63. package/src/codex/main-account.ts +6 -2
  64. package/src/codex/native-residue.ts +2 -3
  65. package/src/codex/quota.ts +26 -2
  66. package/src/combos/failover.ts +2 -1
  67. package/src/combos/jev-model-backend.ts +30 -7
  68. package/src/combos/jev-model-request.ts +20 -0
  69. package/src/combos/jev-quota-activation.ts +153 -0
  70. package/src/combos/jev-quota-config.ts +62 -0
  71. package/src/combos/jev-quota-route.ts +26 -0
  72. package/src/combos/jev-quota.ts +88 -0
  73. package/src/combos/jev.ts +25 -12
  74. package/src/combos/types.ts +27 -0
  75. package/src/compatibility/index.ts +3 -0
  76. package/src/compatibility/openai-responses.ts +8 -1
  77. package/src/compatibility/opengateway-responses.ts +61 -0
  78. package/src/config/account-auto-pause-revisions.ts +136 -0
  79. package/src/config/diagnostics.ts +13 -0
  80. package/src/config/live-reconcile.ts +9 -0
  81. package/src/config/persist-unlocked.ts +10 -1
  82. package/src/config/removed-opencode-free.ts +135 -0
  83. package/src/config/schema/account-auto-pause.ts +14 -0
  84. package/src/config/schema/config-schema.ts +15 -0
  85. package/src/config/schema/leaf-validators.ts +7 -0
  86. package/src/config.ts +10 -3
  87. package/src/generated/compatibility-version.json +344 -144
  88. package/src/images/loop.ts +32 -9
  89. package/src/lib/account-auto-pause-response.ts +16 -0
  90. package/src/lib/bun-runtime.ts +18 -1
  91. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  92. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  93. package/src/lib/decision-quota-sink.ts +76 -0
  94. package/src/lib/state-store-registrations.ts +25 -0
  95. package/src/link/ssh-argv.ts +3 -0
  96. package/src/messaging/envelope.ts +6 -3
  97. package/src/messaging/remote-auth.ts +83 -0
  98. package/src/messaging/remote-bridge.ts +270 -0
  99. package/src/messaging/remote-contract.ts +108 -0
  100. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  101. package/src/messaging/remote-enrollment.ts +244 -0
  102. package/src/messaging/remote-envelope.ts +40 -0
  103. package/src/messaging/remote-files.ts +117 -0
  104. package/src/messaging/remote-owner.ts +169 -0
  105. package/src/messaging/remote-ports.ts +56 -0
  106. package/src/messaging/remote-process.ts +131 -0
  107. package/src/messaging/remote-replies.ts +98 -0
  108. package/src/messaging/remote-rpc-admission.ts +32 -0
  109. package/src/messaging/remote-send.ts +72 -0
  110. package/src/messaging/remote-store.ts +111 -0
  111. package/src/messaging/remote-tunnels.ts +65 -0
  112. package/src/messaging/rpc.ts +13 -1
  113. package/src/oauth/account-auto-pause-policy.ts +66 -0
  114. package/src/oauth/account-auto-pause.ts +171 -0
  115. package/src/oauth/account-quota-rank.ts +2 -1
  116. package/src/oauth/anthropic-account-refusal.ts +6 -0
  117. package/src/oauth/anthropic-model-family.ts +8 -0
  118. package/src/oauth/anthropic-model-quota.ts +2 -6
  119. package/src/oauth/api-key-pool-preserve.ts +37 -0
  120. package/src/oauth/index.ts +5 -35
  121. package/src/oauth/store.ts +35 -8
  122. package/src/oauth/types.ts +2 -0
  123. package/src/plugins/upstream-hooks.ts +14 -1
  124. package/src/providers/api-key-pause.ts +32 -0
  125. package/src/providers/api-key-resolve.ts +10 -4
  126. package/src/providers/api-key-selection.ts +6 -1
  127. package/src/providers/api-keys.ts +8 -4
  128. package/src/providers/claude-cli-usage.ts +5 -0
  129. package/src/providers/key-failover.ts +11 -11
  130. package/src/providers/model-rename-fields.ts +1 -0
  131. package/src/providers/opencode-zen-rate-limit.ts +6 -72
  132. package/src/providers/quota/account-cache.ts +15 -1
  133. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  134. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  135. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  136. package/src/providers/quota/antigravity.ts +39 -6
  137. package/src/providers/quota/report-cache.ts +14 -0
  138. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  139. package/src/providers/quota-decision-publication.ts +77 -0
  140. package/src/providers/quota-decision-snapshot.ts +145 -0
  141. package/src/providers/quota-routing-cache.ts +3 -0
  142. package/src/providers/quota-types.ts +6 -1
  143. package/src/providers/quota.ts +3 -2
  144. package/src/providers/registry/entries-extended.ts +8 -52
  145. package/src/providers/registry/model-seeds.ts +1 -1
  146. package/src/providers/registry.ts +1 -2
  147. package/src/providers/stale-vision-classification-migration.ts +1 -1
  148. package/src/responses/parser.ts +18 -10
  149. package/src/responses/web-search-replay.ts +138 -0
  150. package/src/routing/compatibility/behavior.ts +4 -1
  151. package/src/server/auth-cors.ts +7 -0
  152. package/src/server/background-lifecycle.ts +2 -1
  153. package/src/server/chat-native.ts +28 -13
  154. package/src/server/claude-messages.ts +2 -1
  155. package/src/server/index/optional-listeners.ts +6 -0
  156. package/src/server/management/combo-routes.ts +17 -2
  157. package/src/server/management/oauth-account-routes.ts +10 -1
  158. package/src/server/management/provider-completion-test.ts +289 -0
  159. package/src/server/management/provider-routes.ts +9 -1
  160. package/src/server/management/route-registry.ts +2 -0
  161. package/src/server/messages-native.ts +18 -4
  162. package/src/server/messages-shared.ts +4 -1
  163. package/src/server/request-log.ts +7 -2
  164. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  165. package/src/server/responses/adapter-dispatch.ts +77 -28
  166. package/src/server/responses/combo-session-recall.ts +25 -0
  167. package/src/server/responses/combo-stream-preflight.ts +3 -1
  168. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  169. package/src/server/responses/compact.ts +8 -5
  170. package/src/server/responses/context-overflow.ts +141 -2
  171. package/src/server/responses/core-codex-account.ts +7 -1
  172. package/src/server/responses/core-combo.ts +31 -5
  173. package/src/server/responses/core.ts +12 -9
  174. package/src/server/responses/empty-completion-guard.ts +2 -0
  175. package/src/server/responses/fetch-helpers.ts +35 -4
  176. package/src/server/responses/jev-model-invoke.ts +13 -14
  177. package/src/server/responses/native-steering-log.ts +2 -2
  178. package/src/server/responses/passthrough-delivery.ts +3 -2
  179. package/src/server/responses/passthrough-dispatch.ts +30 -33
  180. package/src/server/responses/passthrough-observations.ts +38 -0
  181. package/src/server/responses/request-compression.ts +98 -0
  182. package/src/server/responses/request-transport.ts +32 -3
  183. package/src/server/responses/run-turn-execution.ts +2 -0
  184. package/src/server/responses/sidecar-execution.ts +16 -3
  185. package/src/server/responses/terminal-guard.ts +2 -0
  186. package/src/server/startup-action-control.ts +2 -2
  187. package/src/server/startup-health-cache.ts +2 -2
  188. package/src/server/windows-tray-control.ts +2 -2
  189. package/src/service/definition.ts +9 -1
  190. package/src/types/account-auto-pause.ts +15 -0
  191. package/src/types/config.ts +23 -1
  192. package/src/types/provider.ts +9 -5
  193. package/src/types/request.ts +20 -2
  194. package/src/types.ts +1 -0
  195. package/src/usage/cost.ts +17 -1
  196. package/src/usage/expected-prices.ts +3 -1
  197. package/src/usage/jev-stats.ts +4 -0
  198. package/src/usage/log.ts +2 -0
  199. package/src/usage/summary.ts +7 -0
  200. package/src/web-search/loop.ts +37 -11
  201. package/src/web-search/progress-stream.ts +4 -1
  202. package/src/web-search/run-turn-loop.ts +9 -3
  203. package/src/web-search/sidecar-search-replay.ts +96 -0
  204. package/gui/dist/assets/App-CHC89EXQ.js +0 -58
  205. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
@@ -1,3 +1,5 @@
1
+ import { recoverAutoPausedAccount } from "./account-auto-pause-recovery";
2
+ import { adoptCodexWsStage, capturePassthroughAffinity } from "./passthrough-observations";
1
3
  import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
2
4
  import { requestPacingOverloadResponse } from "./pacing-overload";
3
5
  import { recordDistributedResponseFailure } from "../../providers/api-key-response-ownership";
@@ -99,7 +101,6 @@ import {
99
101
  upstreamHostCircuitOpenResponse,
100
102
  usesCodexForwardPoolAuth,
101
103
  codexWsQuotaObserver,
102
- isFixedCodexAccount,
103
104
  codexPoolAccountModel400Denial,
104
105
  shouldRetryCodexPoolAccountQuota,
105
106
  shouldRetryCodexPoolAccountTransient,
@@ -111,7 +112,7 @@ import {
111
112
  clearCodexModelDenialEvidence,
112
113
  recordCodexModelDenialEvidence,
113
114
  } from "../../codex/model-entitlements";
114
- import { codexWsSocketDeathStage, isCodexWsUpstreamResponse, readCodexWsStage } from "./codex-ws-wire";
115
+ import { codexWsSocketDeathStage, isCodexWsUpstreamResponse } from "./codex-ws-wire";
115
116
  import { linkAbortSignal } from "./core-lifetime";
116
117
  import type { CodexAuthContext } from "../../codex/auth-context";
117
118
  import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
@@ -130,6 +131,7 @@ import {
130
131
  } from "../../lib/upstream-retry";
131
132
  import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
132
133
  import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
134
+ import { captureAccountAutoPauseAttempt, wasAccountAutoPaused, wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
133
135
  import { recordCodexUpstreamOutcome } from "../../codex/routing";
134
136
  import { describeUpstreamConnectFailure } from "./upstream-error";
135
137
  import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
@@ -152,7 +154,6 @@ import {
152
154
  rotateGenericOAuthAccountOn429,
153
155
  failoverAccountSnapshot,
154
156
  } from "../../oauth/generic-account-failover";
155
- import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
156
157
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
157
158
  import {
158
159
  attemptOpaqueBlobRecovery,
@@ -201,6 +202,7 @@ export async function preparePassthroughExchange(
201
202
  | "genericFailoverAccountId"
202
203
  | "passiveQuotaWriterGeneration"
203
204
  | "oauthDispatch"
205
+ | "recoverAutoPausedSelection"
204
206
  | "resolveSelectionAdapter"
205
207
  | "resolveCopilotSelection"
206
208
  | "copilotRefusalResponse"
@@ -733,22 +735,11 @@ export async function preparePassthroughExchange(
733
735
  }
734
736
  nativeHostState.lease = null;
735
737
  };
736
- /**
737
- * #4191: a Codex WS exchange pins its content-free stage record on the
738
- * Response it resolves (markCodexWsStage). Adopting the record here, at
739
- * the single funnel every physical upstream response passes through,
740
- * binds it to the attempt that actually served it — including the 502/504
741
- * pre-response JSON settles that never reach the SSE relay.
742
- */
743
- const adoptCodexWsStage = (response: Response): void => {
744
- const stage = readCodexWsStage(response);
745
- if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
746
- };
747
738
  let upstreamResponse: Response, lastDistributedResponse: Response | undefined;
748
739
  const adoptObservedResponse = <T extends Response>(response: T): T => {
749
740
  if (!isNonReplayableResponse(response)) lastDistributedResponse = response.status === 401 || response.status === 429 ? response : undefined;
750
741
  settleObservedHostResponse();
751
- adoptCodexWsStage(response);
742
+ adoptCodexWsStage(response, logCtx);
752
743
  return response;
753
744
  };
754
745
  let passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
@@ -1050,6 +1041,7 @@ export async function preparePassthroughExchange(
1050
1041
  providerName: route.providerName,
1051
1042
  modelId: route.modelId,
1052
1043
  onPhysicalDispatch: receiptMode ? sendBudgetState.noteInitialDispatch : undefined,
1044
+ accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
1053
1045
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1054
1046
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1055
1047
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
@@ -1187,6 +1179,7 @@ export async function preparePassthroughExchange(
1187
1179
  dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1188
1180
  providerName: route.providerName,
1189
1181
  modelId: route.modelId,
1182
+ accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
1190
1183
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1191
1184
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1192
1185
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
@@ -1209,8 +1202,18 @@ export async function preparePassthroughExchange(
1209
1202
 
1210
1203
  // Keep recovery kinds in sync with the generic `recovery:` loop below.
1211
1204
  passthroughRecovery: for (;;) {
1205
+ if (wasAccountAutoPaused(upstreamResponse) && route.provider.authMode !== "forward") {
1206
+ const recovered = await recoverAutoPausedAccount({ requestKey: transportState, reserve: reserveCredentialHop,
1207
+ remaining: transientSendCapFor(transientRetryPolicyFor(route.provider)?.attempts, sendBudgetState.targetSendsUsed),
1208
+ refresh: () => transportState.recoverAutoPausedSelection(parsed), refetch: () => rebuildAndRefetch("oauth-account-429"), setPermit: permit => { sendBudgetState.pendingHopPermit = permit; } });
1209
+ if (!recovered) break;
1210
+ if ("failed" in recovered) return recovered.failed;
1211
+ upstreamResponse = recovered; continue passthroughRecovery;
1212
+ }
1213
+
1214
+
1212
1215
  if (
1213
- upstreamResponse.status === 401
1216
+ upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)
1214
1217
  && (admissionState.authCtx.kind === "main-pool" || admissionState.authCtx.kind === "pool")
1215
1218
  && usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)
1216
1219
  && codex401ReplayKind === null
@@ -1293,6 +1296,7 @@ export async function preparePassthroughExchange(
1293
1296
  dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1294
1297
  providerName: route.providerName,
1295
1298
  modelId: route.modelId,
1299
+ accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
1296
1300
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1297
1301
  beforeDispatch: headers => {
1298
1302
  replayGuard?.(headers);
@@ -1336,14 +1340,14 @@ export async function preparePassthroughExchange(
1336
1340
  continue passthroughRecovery;
1337
1341
  }
1338
1342
 
1339
- if (codex401ReplayKind !== null && upstreamResponse.status === 401) break;
1343
+ if (codex401ReplayKind !== null && upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)) break;
1340
1344
 
1341
1345
  // Native Responses providers return before the generic adapter recovery loop below. Keep
1342
1346
  // their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
1343
1347
  // rebuilt replay. xAI's current subscription models use this branch now that their official
1344
1348
  // Grok CLI catalog declares the Responses backend.
1345
1349
  if (
1346
- upstreamResponse.status === 401
1350
+ upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)
1347
1351
  && isOAuth401ReplayProvider
1348
1352
  && transportState.sentOAuthSnapshot
1349
1353
  && !oauth401ReplayAttempted
@@ -1440,6 +1444,7 @@ export async function preparePassthroughExchange(
1440
1444
  dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1441
1445
  providerName: route.providerName,
1442
1446
  modelId: route.modelId,
1447
+ accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
1443
1448
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1444
1449
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1445
1450
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
@@ -1586,6 +1591,7 @@ export async function preparePassthroughExchange(
1586
1591
  dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1587
1592
  providerName: route.providerName,
1588
1593
  modelId: route.modelId,
1594
+ accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
1589
1595
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1590
1596
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1591
1597
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
@@ -1608,18 +1614,7 @@ export async function preparePassthroughExchange(
1608
1614
  || captureAuthCtx.kind === "pool"
1609
1615
  || captureAuthCtx.kind === "main-pool",
1610
1616
  ): void => {
1611
- if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
1612
- captureCodexAffinityDiagnostic({
1613
- inboundHeaders: req.headers,
1614
- outboundHeaders: captureRequest.headers,
1615
- authKind: captureAuthCtx.kind,
1616
- accountMode: route.codexAccountMode,
1617
- fixedAccount: isFixedCodexAccount(captureAuthCtx),
1618
- credentialSubstituted,
1619
- accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
1620
- wireModelNormalized: parsed.modelId !== route.modelId,
1621
- status: response.status,
1622
- });
1617
+ capturePassthroughAffinity(response, req.headers, captureRequest.headers, captureAuthCtx, route, parsed.modelId, credentialSubstituted);
1623
1618
  };
1624
1619
  recordDistributedResponseFailure(lastDistributedResponse ?? upstreamResponse, config, route.providerName, options.abortSignal);
1625
1620
  captureAffinityResponse(upstreamResponse);
@@ -1646,7 +1641,9 @@ export async function preparePassthroughExchange(
1646
1641
  options.abortSignal,
1647
1642
  parsed.modelId,
1648
1643
  );
1649
- if (model400Denial !== undefined) {
1644
+ if (wasAccountAutoPaused(upstreamResponse)) {
1645
+ poolRetryOutcome = upstreamResponse.status;
1646
+ } else if (model400Denial !== undefined) {
1650
1647
  // Spend this refusal on more than one retry. It is the account's own authenticated
1651
1648
  // answer about this model, and the roster cache that selection otherwise reads expires
1652
1649
  // five minutes after a catalog sync fills it -- so without remembering this, the next
@@ -1679,7 +1676,7 @@ export async function preparePassthroughExchange(
1679
1676
  // ladder does have one — retrying the account the refreshed roster still grants — and
1680
1677
  // keeps it. An earlier revision also broke here on a non-400 outcome, which no test could
1681
1678
  // justify because this flag already produced the identical result.
1682
- const storedReplaySpent = codex401ReplayKind === "stored";
1679
+ const storedReplaySpent = codex401ReplayKind === "stored" && !wasAccountAutoPaused(upstreamResponse);
1683
1680
  const retry = await retryCodexPoolOnAlternateAccount({
1684
1681
  callerAuthHeaders,
1685
1682
  config,
@@ -1698,7 +1695,7 @@ export async function preparePassthroughExchange(
1698
1695
  httpOnly: canonicalBufferedJson,
1699
1696
  preOutputRefusalHold: preOutputRefusalHold(),
1700
1697
  onResponse: (response, retryAuthCtx, retryRequest) => {
1701
- adoptCodexWsStage(response);
1698
+ adoptCodexWsStage(response, logCtx);
1702
1699
  captureAffinityResponse(
1703
1700
  response,
1704
1701
  retryAuthCtx,
@@ -0,0 +1,38 @@
1
+ import { readCodexWsStage } from "./codex-ws-wire";
2
+ import type { RequestLogContext } from "../request-log";
3
+
4
+ /**
5
+ * #4191: a Codex WS exchange pins its content-free stage record on the
6
+ * Response it resolves (markCodexWsStage). Adopting the record here, at
7
+ * the single funnel every physical upstream response passes through,
8
+ * binds it to the attempt that actually served it — including the 502/504
9
+ * pre-response JSON settles that never reach the SSE relay.
10
+ */
11
+ export function adoptCodexWsStage(response: Response, logCtx: RequestLogContext): void {
12
+ const stage = readCodexWsStage(response);
13
+ if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
14
+ }
15
+
16
+ import type { CodexAuthContext } from "../../codex/auth-context";
17
+ import type { RouteResult } from "../../router";
18
+ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
19
+ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
20
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
21
+ import { isFixedCodexAccount } from "./core-codex-account";
22
+
23
+ export function capturePassthroughAffinity(response: Response, inboundHeaders: Headers, outboundHeaders: Record<string, string>,
24
+ captureAuthCtx: CodexAuthContext, route: Pick<RouteResult, "provider" | "modelId" | "codexAccountMode">,
25
+ parsedModelId: string, credentialSubstituted: boolean): void {
26
+ if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
27
+ captureCodexAffinityDiagnostic({
28
+ inboundHeaders: inboundHeaders,
29
+ outboundHeaders: outboundHeaders,
30
+ authKind: captureAuthCtx.kind,
31
+ accountMode: route.codexAccountMode,
32
+ fixedAccount: isFixedCodexAccount(captureAuthCtx),
33
+ credentialSubstituted,
34
+ accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
35
+ wireModelNormalized: parsedModelId !== route.modelId,
36
+ status: response.status,
37
+ });
38
+ }
@@ -0,0 +1,98 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { constants, gzipSync, zstdCompressSync } from "node:zlib";
3
+ import type { OcxConfig } from "../../types";
4
+
5
+ /**
6
+ * Opt-in compression of outbound native ChatGPT Responses HTTP request bodies (#7247).
7
+ *
8
+ * Codex re-sends the whole conversation on every turn. When that turn travels over HTTP the body
9
+ * reaches `chatgpt.com` identity-encoded, which on a metered egress is the dominant cost of
10
+ * running Codex through the proxy. The backend already accepts coded request bodies: Codex CLI
11
+ * itself sends `content-encoding: zstd` there (see `src/server/request-decompress.ts`).
12
+ *
13
+ * Applied only at the final HTTP send (`sendWithConnectionPolicy`), after the WebSocket decision
14
+ * and plugin rewrites, so WebSocket frames and every other destination are untouched.
15
+ *
16
+ * Compression is synchronous on purpose. An await at the physical-send boundary would open a gap
17
+ * between dispatch admission (credential selection, attempt receipts, live reserve policy) and the
18
+ * send itself. The cost is bounded instead: bodies above {@link MAX_COMPRESSIBLE_BODY_BYTES} are sent
19
+ * as they are, which keeps the event-loop block to tens of milliseconds on ordinary hardware.
20
+ */
21
+ export type UpstreamRequestCompression = "off" | "gzip" | "zstd";
22
+
23
+ /** Below this the header overhead outweighs any saving. */
24
+ export const MIN_COMPRESSIBLE_BODY_BYTES = 1024;
25
+ /** Above this the synchronous compression block is no longer negligible; send identity. */
26
+ export const MAX_COMPRESSIBLE_BODY_BYTES = 16 * 1024 * 1024;
27
+
28
+ const CANONICAL_CODEX_RESPONSES = /^https:\/\/chatgpt\.com\/backend-api\/codex\/responses(?:\/compact)?$/;
29
+
30
+ /** True for the native ChatGPT Responses and compact endpoints, the only compressed destinations. */
31
+ export function isCanonicalCodexResponsesUrl(url: string): boolean {
32
+ return CANONICAL_CODEX_RESPONSES.test(url);
33
+ }
34
+
35
+ /** Anything other than an explicit codec, including a malformed hand edit, means off. */
36
+ export function resolveUpstreamRequestCompression(value: unknown): UpstreamRequestCompression {
37
+ return value === "gzip" || value === "zstd" ? value : "off";
38
+ }
39
+
40
+ // The live server config object, installed once at startup by `setLiveStateStoreConfig`. Live
41
+ // reconciliation mutates that object in place, so the field is read per send rather than copied.
42
+ let boundConfig: Pick<OcxConfig, "upstreamRequestCompression"> | null = null;
43
+
44
+ export function bindUpstreamRequestCompressionConfig(config: Pick<OcxConfig, "upstreamRequestCompression"> | null): void {
45
+ boundConfig = config;
46
+ }
47
+
48
+ export function currentUpstreamRequestCompression(): UpstreamRequestCompression {
49
+ return resolveUpstreamRequestCompression(boundConfig?.upstreamRequestCompression);
50
+ }
51
+
52
+ // ArrayBuffer-backed, which is what node:zlib returns and what a fetch BodyInit accepts.
53
+ type CodedBytes = Buffer<ArrayBuffer>;
54
+ type Compressor = (mode: Exclude<UpstreamRequestCompression, "off">, bytes: Buffer) => CodedBytes;
55
+
56
+ const defaultCompressor: Compressor = (mode, bytes) => mode === "gzip"
57
+ ? gzipSync(bytes, { level: 1 }) as CodedBytes
58
+ : zstdCompressSync(bytes, { params: { [constants.ZSTD_c_compressionLevel]: 3 } }) as CodedBytes;
59
+
60
+ let compressor: Compressor = defaultCompressor;
61
+ let maxBodyBytes = MAX_COMPRESSIBLE_BODY_BYTES;
62
+
63
+ /** Test seam: replace the codec (failure injection) and the size ceiling; null restores defaults. */
64
+ export function setUpstreamRequestCompressionForTests(next: { compressor?: Compressor | null; maxBodyBytes?: number | null }): void {
65
+ if (next.compressor !== undefined) compressor = next.compressor ?? defaultCompressor;
66
+ if (next.maxBodyBytes !== undefined) maxBodyBytes = next.maxBodyBytes ?? MAX_COMPRESSIBLE_BODY_BYTES;
67
+ }
68
+
69
+ export interface CompressedUpstreamBody {
70
+ body: CodedBytes;
71
+ encoding: Exclude<UpstreamRequestCompression, "off">;
72
+ }
73
+
74
+ /**
75
+ * Compress one final HTTP send body when the operator opted in and the send is eligible.
76
+ *
77
+ * Returns null — send unchanged — for any other destination, a non-string body (an already coded
78
+ * buffer or a stream, which also makes nested passes idempotent), a request that already declares
79
+ * a `content-encoding`, a body outside the size window, or a codec failure. A failure never fails
80
+ * the request: identity is what the proxy sent before this option existed.
81
+ */
82
+ export function compressUpstreamRequestBody(
83
+ target: string,
84
+ body: unknown,
85
+ headers: Headers,
86
+ mode: UpstreamRequestCompression = currentUpstreamRequestCompression(),
87
+ ): CompressedUpstreamBody | null {
88
+ if (mode === "off" || typeof body !== "string" || !isCanonicalCodexResponsesUrl(target)) return null;
89
+ if (headers.has("content-encoding")) return null;
90
+ const size = Buffer.byteLength(body, "utf8");
91
+ if (size < MIN_COMPRESSIBLE_BODY_BYTES || size > maxBodyBytes) return null;
92
+ try {
93
+ return { body: compressor(mode, Buffer.from(body, "utf8")), encoding: mode };
94
+ } catch {
95
+ // Deliberately silent: the error could only describe the body, and identity is still correct.
96
+ return null;
97
+ }
98
+ }
@@ -71,7 +71,7 @@ import {
71
71
  resolveCurrentProviderApiKeyTransport,
72
72
  } from "../../providers/api-key-selection";
73
73
  import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
74
- import { providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
74
+ import { physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
75
75
  import type { ProviderFetchOptions } from "./fetch-helpers";
76
76
  import { captureConfigGeneration } from "../../lib/state-store-sweeper";
77
77
  import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuota } from "../../providers/quota";
@@ -80,6 +80,7 @@ import { formatErrorResponse } from "../../bridge";
80
80
  import { bindRouteReasoningReplayScope } from "./core-replay";
81
81
  import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
82
82
  import { redactSecretString } from "../../lib/redact";
83
+ import { captureAccountAutoPauseAttempt } from "../../oauth/account-auto-pause";
83
84
  import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
84
85
  import { distributedKeyPhysicalFetch } from "../../providers/api-key-response-ownership";
85
86
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
@@ -612,6 +613,10 @@ export async function prepareResponsesTransport(
612
613
  // Pacing may outlive admission. Preserve local pause/cooldown reasons even when
613
614
  // a strict route rejects the old proposal before another bearer can be resolved.
614
615
  let candidate = servingOAuthSnapshot;
616
+ if (anthropicInstance === undefined && candidate && getAccountSet(route.providerName)?.accounts.find(row => row.id === candidate!.accountId)?.paused) {
617
+ oauthSelection = captureOAuthAccountSelection(route.providerName);
618
+ candidate = oauthSelection ? await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true }) : undefined;
619
+ }
615
620
  if (anthropicInstance !== undefined) {
616
621
  oauthSelection = captureOAuthAccountSelection(route.providerName);
617
622
  const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
@@ -644,6 +649,24 @@ export async function prepareResponsesTransport(
644
649
  invalidateSameTargetRequest();
645
650
  return adapter;
646
651
  };
652
+ const recoverAutoPausedSelection = async (requestParsed: OcxParsedRequest): Promise<boolean> => {
653
+ if (route.provider.authMode === "forward") return false;
654
+ const available = route.provider.authMode === "oauth"
655
+ ? getAccountSet(route.providerName)?.accounts.some(row => !row.paused && !row.needsReauth)
656
+ : !!config.providers[route.providerName]?.apiKey;
657
+ if (!available) return false;
658
+ await refreshDispatchAdapter(requestParsed);
659
+ // Bind before the recovery caller rebuilds. A terminal retry can be a clone;
660
+ // the outer request also owns continuation persistence and replay publication.
661
+ for (const replayParsed of new Set([parsed, requestParsed])) {
662
+ bindRouteReasoningReplayScope({
663
+ parsed: replayParsed, providerName: route.providerName, provider: route.provider,
664
+ adapterName: activeAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
665
+ forwardHeaders: requestState.selectedForwardHeaders,
666
+ });
667
+ }
668
+ return true;
669
+ };
647
670
  const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
648
671
  requestParsed._cursorIdentityScope = undefined;
649
672
  requestParsed._cursorConversationId = undefined;
@@ -704,6 +727,7 @@ export async function prepareResponsesTransport(
704
727
  providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
705
728
  pacingSlot: attemptSlot,
706
729
  turnScopedPacing: true,
730
+ accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, headers),
707
731
  beforeDispatch: beforePhysicalSend,
708
732
  });
709
733
  await run(requestParsed, { ...incoming, pacingSlot: attemptSlot, providerFetch: fetch, beforePhysicalSend }, event => { if (!refused) emit(event); });
@@ -756,6 +780,8 @@ export async function prepareResponsesTransport(
756
780
  // apply a host-scoped decision to a different host.
757
781
  const releaseFamily = snapshot ? anthropicQuota!.claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
758
782
  if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
783
+ const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, binding?.kind === "api-key" ? binding.provider : route.provider,
784
+ { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, new Headers(dispatchInit.headers));
759
785
  let response: Response;
760
786
  let physicalOwner: AnthropicPhysicalSendOwnership | null = null;
761
787
  try {
@@ -775,7 +801,8 @@ export async function prepareResponsesTransport(
775
801
  response = await sendWithConnectionPolicy(
776
802
  distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
777
803
  destination,
778
- { ...dispatchInit, redirect: "manual" },
804
+ { ...dispatchInit, redirect: "manual",
805
+ ...physicalSendObserverInit(observeAutoPause, destination instanceof Request ? destination.url : String(destination)) },
779
806
  { providerName: route.providerName, provider: route.provider },
780
807
  );
781
808
  } finally { releaseFamily(); }
@@ -790,11 +817,12 @@ export async function prepareResponsesTransport(
790
817
  && current.credential.access === snapshot.accessToken
791
818
  && credentialGeneration(current.credential) === snapshot.generation) {
792
819
  bindAnthropicRefusalCredentialForSend(response, physicalOwner);
793
- recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId);
820
+ recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId, snapshot.generation);
794
821
  }
795
822
  } catch { /* best-effort observation cannot fail the response */ }
796
823
  }
797
824
  if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
825
+ if (observeAutoPause) response = await observeAutoPause(response);
798
826
  return response;
799
827
  }
800
828
  let nextAdapter: ProviderAdapter;
@@ -1280,6 +1308,7 @@ export async function prepareResponsesTransport(
1280
1308
  oauthDispatch,
1281
1309
  noteRoutedAttemptSend,
1282
1310
  commitKeyAttemptSend,
1311
+ recoverAutoPausedSelection,
1283
1312
  bindKeyUsageFromBridge,
1284
1313
  anthropicSessionKey,
1285
1314
  isPassthrough,
@@ -53,6 +53,7 @@ import { emptyToolCatalogRefusal, guardEmptyToolCatalogEvents, guardEmptyToolCat
53
53
  // LOCAL PATCH (runturn-websearch)
54
54
  import { planWebSearch } from "../../web-search";
55
55
  import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
56
+ import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
56
57
  import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
57
58
  import { orderDevinMessagesOutput } from "../../claude/devin-output-order";
58
59
  import { unboundPoolSpendRefusalMessage } from "../workflow-refusal";
@@ -217,6 +218,7 @@ export async function executeResponsesRunTurn(
217
218
  releaseSearchProbeLease();
218
219
  throw error;
219
220
  }
221
+ if (!wsPlan) stripReplayedSearchPairs(parsed);
220
222
 
221
223
  const runTurnAbort = new AbortController();
222
224
  const cleanupRequestAbort = linkAbortSignal(runTurnAbort, req.signal);
@@ -39,6 +39,7 @@ import { AdmissionModelDeniedError, admissionModelDeniedResponse } from "../admi
39
39
  import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
40
40
  import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
41
41
  import { anthropicPoolCooldownResponse, readDisplaySafeErrorText } from "./core-errors";
42
+ import { sidecarContextOverflowResponse } from "./context-overflow";
42
43
  import {
43
44
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
44
45
  anthropicRoutingFor,
@@ -56,6 +57,7 @@ import { rememberResponseState } from "../../responses/state";
56
57
  import { trackStreamLifetime } from "../lifecycle";
57
58
  import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
58
59
  import { createSidecarSendBudget } from "./sidecar-send-budget";
60
+ import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
59
61
 
60
62
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
61
63
  export async function executeResponsesSidecars(
@@ -216,6 +218,8 @@ export async function executeResponsesSidecars(
216
218
  })
217
219
  : undefined;
218
220
  const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
221
+ // Replayed search pairs call the synthetic web_search tool only the sidecar declares (#7269).
222
+ if (!wsPlan) stripReplayedSearchPairs(parsed);
219
223
  const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
220
224
  const canRunWebSearch = !!wsPlan && !transportState.adapter.runTurn;
221
225
  let sidecarOutputStarted = false;
@@ -548,7 +552,7 @@ export async function executeResponsesSidecars(
548
552
  options.codexWsRuntimeIdentity,
549
553
  { providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(transportState.adapter) },
550
554
  );
551
- const imgResponse = await runWithImageBridge({
555
+ const imgLoopResponse = await runWithImageBridge({
552
556
  parsed, adapter: transportState.adapter,
553
557
  ...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
554
558
  clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
@@ -619,8 +623,12 @@ export async function executeResponsesSidecars(
619
623
  notifyResponseComplete(response);
620
624
  },
621
625
  });
622
- const imgScopeRefusal = sidecarScopeRefusal(imgResponse);
626
+ const imgScopeRefusal = sidecarScopeRefusal(imgLoopResponse);
623
627
  if (imgScopeRefusal) return imgScopeRefusal;
628
+ // A provider overflow on the first iteration answers as the main path's context failure (#7316).
629
+ const imgResponse = options.comboAttempt ? imgLoopResponse : sidecarContextOverflowResponse(imgLoopResponse, {
630
+ stream: parsed.stream, modelId: parsed._responseModelId ?? parsed.modelId, translatorBudget,
631
+ });
624
632
  if (imgResponse.body) {
625
633
  const imgTurnAc = new AbortController();
626
634
  imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
@@ -732,7 +740,12 @@ export async function executeResponsesSidecars(
732
740
  ? anthropicRoutingFor(anthropicInstance).anthropicPoolUsageLimitAfterRefusal(Date.now(), transportState.anthropicRouteDecision, route.modelId)
733
741
  : null;
734
742
  if (poolUsageLimit) void loopResponse.body?.cancel().catch(() => {});
735
- const wsResponse = poolUsageLimit ? anthropicPoolCooldownResponse(poolUsageLimit, { logCtx }) : loopResponse;
743
+ // A provider overflow on the first iteration answers as the main path's context failure (#7316).
744
+ const wsResponse = poolUsageLimit ? anthropicPoolCooldownResponse(poolUsageLimit, { logCtx })
745
+ : options.comboAttempt ? loopResponse
746
+ : sidecarContextOverflowResponse(loopResponse, {
747
+ stream: parsed.stream, modelId: parsed._responseModelId ?? parsed.modelId, translatorBudget,
748
+ });
736
749
  // Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
737
750
  // in-flight web-search turns instead of skipping them during graceful shutdown.
738
751
  if (wsResponse.body) {
@@ -170,6 +170,7 @@ export function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undef
170
170
  const cachedInputTokens = sumOptional("cachedInputTokens");
171
171
  const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
172
172
  const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
173
+ const cacheCreation1hInputTokens = sumOptional("cacheCreation1hInputTokens");
173
174
  const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
174
175
  const providerCredits = sumOptional("providerCredits");
175
176
  const inputTokens = first.inputTokens + second.inputTokens;
@@ -181,6 +182,7 @@ export function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undef
181
182
  ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
182
183
  ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
183
184
  ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
185
+ ...(cacheCreation1hInputTokens !== undefined ? { cacheCreation1hInputTokens } : {}),
184
186
  ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
185
187
  ...(providerCredits !== undefined ? { providerCredits } : {}),
186
188
  ...(first.estimated || second.estimated ? { estimated: true } : {}),
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { execFile } from "node:child_process";
3
3
  import { join } from "node:path";
4
- import { durableBunPath } from "../lib/bun-runtime";
4
+ import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
5
5
  import { selfLaunchArgv } from "../lib/self-launch-argv";
6
6
  import { isDesktopSupervised } from "../lib/system-restart-contract";
7
7
  import { withLoginShellPath } from "./login-shell-path";
@@ -196,7 +196,7 @@ async function runCliInstall(
196
196
  isStandaloneExecutable: installLaunch.isStandaloneExecutable,
197
197
  });
198
198
  const run = installLaunch.execFile ?? execFile;
199
- const env = await installChildEnv(action);
199
+ const env = durableBunChildEnv(await installChildEnv(action));
200
200
  return new Promise((resolve, reject) => {
201
201
  const timeout = process.platform === "win32" && action === "install-service"
202
202
  ? 0
@@ -4,7 +4,7 @@ import { codexAutoStartEnabled } from "../config";
4
4
  import { deriveStartupHealth, startupHealthProbeBudgetMs, type StartupHealth } from "../codex/autostart-health";
5
5
  import { getCodexRoutingKind } from "../codex/inject";
6
6
  import { diagnoseCodexShim } from "../codex/shim";
7
- import { durableBunPath } from "../lib/bun-runtime";
7
+ import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
8
8
  import { selfLaunchArgv } from "../lib/self-launch-argv";
9
9
  import { desktopStartupOwnership } from "../service/desktop-startup";
10
10
  import type { OcxConfig } from "../types";
@@ -129,7 +129,7 @@ function runProbe(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHea
129
129
  return new Promise(resolve => {
130
130
  execFile(bun, selfLaunchArgv(["__startup-health"], { sourceEntrypoint: cli }), {
131
131
  encoding: "utf8",
132
- env: process.env,
132
+ env: durableBunChildEnv(process.env),
133
133
  timeout: PROBE_TIMEOUT_MS,
134
134
  windowsHide: true,
135
135
  maxBuffer: 256 * 1024,
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { join } from "node:path";
3
- import { durableBunPath } from "../lib/bun-runtime";
3
+ import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
4
4
  import type { WindowsTrayStatus } from "../tray/windows";
5
5
 
6
6
  export type WindowsTrayAction = "install" | "start" | "stop" | "status" | "uninstall";
@@ -15,7 +15,7 @@ export function runWindowsTrayAction(action: WindowsTrayAction): Promise<Windows
15
15
  return new Promise((resolve, reject) => {
16
16
  execFile(bun, [cli, "tray", action, "--json"], {
17
17
  encoding: "utf8",
18
- env: process.env,
18
+ env: durableBunChildEnv(process.env),
19
19
  timeout: 30_000,
20
20
  windowsHide: true,
21
21
  maxBuffer: 256 * 1024,
@@ -90,8 +90,16 @@ export function parseBakedListenPort(read: () => string): number | null {
90
90
  export function resolvedProxyEnv(env: NodeJS.ProcessEnv = process.env): { name: string; value: string }[] {
91
91
  const resolved: { name: string; value: string }[] = [];
92
92
  for (const key of PROXY_ENV_KEYS) {
93
- const value = env[key]?.trim() || env[key.toLowerCase()]?.trim();
93
+ const value = [env[key]?.trim(), env[key.toLowerCase()]?.trim()].find(v => v && !OWN_CLAUDE_INTERCEPT_PROXY.test(v));
94
94
  if (value) resolved.push({ name: key, value });
95
95
  }
96
96
  return resolved;
97
97
  }
98
+
99
+ /**
100
+ * The URL Claude first-party writes into `~/.claude/settings.json` (`claudeInterceptProxyUrl`),
101
+ * which every process Claude Code starts inherits. Baked into the service, it would send the
102
+ * proxy's own outbound traffic back through its intercept listener. Only the token-bearing form
103
+ * is matched: a plain `http://127.0.0.1:<port>` is a user's local proxy client and is kept.
104
+ */
105
+ const OWN_CLAUDE_INTERCEPT_PROXY = /^http:\/\/opencodex:[^@/]+@127\.0\.0\.1:\d{1,5}\/?$/i;
@@ -0,0 +1,15 @@
1
+ export const ACCOUNT_PAUSE_REASONS = [
2
+ "oauth_organization_disabled", "account_permission_denied", "subscription_inactive",
3
+ "subscription_expired", "plan_downgraded",
4
+ ] as const;
5
+ export type AccountPauseReason = typeof ACCOUNT_PAUSE_REASONS[number];
6
+ export interface AccountPauseDetails {
7
+ reason: AccountPauseReason;
8
+ pausedAt: number;
9
+ }
10
+ export interface AccountAutoPausePolicy {
11
+ /** Explicit opt-in. Omitted and false both leave account eligibility unchanged. */
12
+ enabled?: boolean;
13
+ /** Consecutive permanent refusals; defaults to 2. */
14
+ consecutiveFailures?: number;
15
+ }