@bitkyc08/opencodex 2.88.0 → 2.90.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 (291) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-DgxkE6Md.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
  6. package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
  7. package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +2 -2
  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/base.ts +4 -0
  14. package/src/adapters/claude-cli/adapter.ts +13 -7
  15. package/src/adapters/claude-cli/usage-admission.ts +14 -2
  16. package/src/adapters/coding-agent/protocol.ts +6 -0
  17. package/src/adapters/coding-agent/turn.ts +14 -4
  18. package/src/adapters/cursor/protobuf-request.ts +3 -0
  19. package/src/adapters/google-http.ts +3 -2
  20. package/src/adapters/google-progress-history.ts +19 -1
  21. package/src/adapters/google-vertex-endpoint.ts +67 -0
  22. package/src/adapters/google.ts +62 -37
  23. package/src/adapters/image.ts +5 -3
  24. package/src/adapters/input-media-guard.ts +5 -2
  25. package/src/adapters/kiro-refusal.ts +41 -1
  26. package/src/adapters/ollama-native.ts +1 -0
  27. package/src/adapters/openai-chat/messages.ts +34 -4
  28. package/src/adapters/openai-chat/passthrough.ts +37 -4
  29. package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
  30. package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
  31. package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
  32. package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
  33. package/src/adapters/openai-chat.ts +7 -6
  34. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  35. package/src/adapters/openai-responses/opengateway-input.ts +139 -0
  36. package/src/adapters/openai-responses/passthrough.ts +22 -13
  37. package/src/adapters/openai-responses/reasoning.ts +68 -1
  38. package/src/adapters/openai-responses/transport.ts +13 -1
  39. package/src/adapters/registry.ts +1 -1
  40. package/src/adapters/upstream-http-error.ts +3 -2
  41. package/src/adapters/zed.ts +7 -4
  42. package/src/bridge/internal.ts +11 -1
  43. package/src/bridge/response-json.ts +2 -2
  44. package/src/bridge/sse.ts +10 -4
  45. package/src/claude/desktop-3p-library.ts +43 -3
  46. package/src/claude/desktop-3p.ts +20 -10
  47. package/src/claude/desktop-picker-profile.ts +18 -9
  48. package/src/claude/desktop-remote-store-io.ts +2 -2
  49. package/src/claude/inbound.ts +12 -26
  50. package/src/claude/outbound.ts +5 -0
  51. package/src/cli/account-api.ts +13 -1
  52. package/src/cli/account-extended.ts +17 -5
  53. package/src/cli/account-key-quota.ts +4 -1
  54. package/src/cli/account.ts +1 -1
  55. package/src/cli/capabilities-agents-routing.ts +45 -8
  56. package/src/cli/capabilities-base.ts +5 -5
  57. package/src/cli/capabilities-provider-models.ts +5 -4
  58. package/src/cli/combo-result.ts +8 -2
  59. package/src/cli/combo.ts +17 -0
  60. package/src/cli/ensure-desktop-cold-start.ts +2 -2
  61. package/src/cli/help.ts +1 -1
  62. package/src/cli/message-command.ts +4 -1
  63. package/src/cli/message-remote-args.ts +69 -0
  64. package/src/cli/message-remote-command.ts +77 -0
  65. package/src/cli/provider-runtime.ts +69 -1
  66. package/src/cli/registry.ts +3 -3
  67. package/src/codex/account-lifecycle.ts +19 -6
  68. package/src/codex/account-pause.ts +6 -1
  69. package/src/codex/account-runtime-state.ts +5 -0
  70. package/src/codex/account-store.ts +55 -9
  71. package/src/codex/account-usability.ts +4 -1
  72. package/src/codex/auth-api/account-list.ts +13 -5
  73. package/src/codex/auth-api/account-pause-group.ts +2 -22
  74. package/src/codex/auth-api/login-collision.ts +59 -0
  75. package/src/codex/auth-api/login-flow.ts +12 -7
  76. package/src/codex/auth-api/main-account-probe.ts +3 -2
  77. package/src/codex/auth-api/pool-mode-gate.ts +81 -13
  78. package/src/codex/auth-collision.ts +31 -4
  79. package/src/codex/auth-context.ts +57 -10
  80. package/src/codex/catalog/aggregation.ts +15 -0
  81. package/src/codex/catalog/auto-review.ts +2 -1
  82. package/src/codex/catalog/build-entries.ts +17 -7
  83. package/src/codex/catalog/derive-entry.ts +3 -1
  84. package/src/codex/catalog/metadata.ts +2 -0
  85. package/src/codex/catalog/model-visibility.ts +0 -2
  86. package/src/codex/catalog/ownership.ts +71 -0
  87. package/src/codex/catalog/parsing.ts +12 -3
  88. package/src/codex/catalog/retained-sync.ts +2 -1
  89. package/src/codex/catalog/routed-removal.ts +4 -4
  90. package/src/codex/cli-update-plan.ts +18 -3
  91. package/src/codex/collision-identity.ts +78 -0
  92. package/src/codex/convergence.ts +2 -1
  93. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  94. package/src/codex/low-quota-protection.ts +57 -31
  95. package/src/codex/main-account-cache.ts +77 -3
  96. package/src/codex/main-account-hard-lock.ts +14 -0
  97. package/src/codex/main-account.ts +6 -2
  98. package/src/codex/native-residue.ts +2 -3
  99. package/src/codex/quota.ts +26 -2
  100. package/src/codex/routing/cooldown-math.ts +5 -0
  101. package/src/codex/routing/failure-window.ts +61 -9
  102. package/src/codex/routing.ts +4 -3
  103. package/src/codex/shim-diagnostics.ts +2 -1
  104. package/src/codex/shim-fingerprint.ts +11 -2
  105. package/src/codex/shim-templates.ts +16 -5
  106. package/src/codex/shim-windows-refresh.ts +224 -0
  107. package/src/codex/shim.ts +45 -21
  108. package/src/combos/failover.ts +10 -2
  109. package/src/combos/jev-model-backend.ts +30 -7
  110. package/src/combos/jev-model-request.ts +20 -0
  111. package/src/combos/jev-quota-activation.ts +153 -0
  112. package/src/combos/jev-quota-config.ts +62 -0
  113. package/src/combos/jev-quota-route.ts +26 -0
  114. package/src/combos/jev-quota.ts +88 -0
  115. package/src/combos/jev.ts +25 -12
  116. package/src/combos/types.ts +27 -0
  117. package/src/compatibility/index.ts +3 -0
  118. package/src/compatibility/openai-responses.ts +8 -1
  119. package/src/compatibility/opengateway-responses.ts +61 -0
  120. package/src/config/account-auto-pause-revisions.ts +136 -0
  121. package/src/config/diagnostics.ts +13 -0
  122. package/src/config/live-reconcile.ts +9 -0
  123. package/src/config/persist-unlocked.ts +10 -1
  124. package/src/config/provider-validation.ts +7 -2
  125. package/src/config/removed-opencode-free.ts +135 -0
  126. package/src/config/schema/account-auto-pause.ts +14 -0
  127. package/src/config/schema/config-schema.ts +15 -0
  128. package/src/config/schema/leaf-validators.ts +7 -0
  129. package/src/config.ts +10 -3
  130. package/src/generated/compatibility-version.json +476 -216
  131. package/src/generated/model-metadata.ts +1 -1
  132. package/src/images/loop.ts +34 -11
  133. package/src/lib/account-auto-pause-response.ts +16 -0
  134. package/src/lib/bun-runtime.ts +18 -1
  135. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  136. package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
  137. package/src/lib/decision-quota-sink.ts +76 -0
  138. package/src/lib/provider-admission-wait.ts +14 -0
  139. package/src/lib/service-secrets.ts +37 -2
  140. package/src/lib/state-store-registrations.ts +25 -0
  141. package/src/lib/translator-budget.ts +1 -1
  142. package/src/lib/upstream-retry.ts +15 -0
  143. package/src/link/ssh-argv.ts +3 -0
  144. package/src/messaging/envelope.ts +6 -3
  145. package/src/messaging/remote-auth.ts +83 -0
  146. package/src/messaging/remote-bridge.ts +270 -0
  147. package/src/messaging/remote-contract.ts +108 -0
  148. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  149. package/src/messaging/remote-enrollment.ts +244 -0
  150. package/src/messaging/remote-envelope.ts +40 -0
  151. package/src/messaging/remote-files.ts +117 -0
  152. package/src/messaging/remote-owner.ts +169 -0
  153. package/src/messaging/remote-ports.ts +56 -0
  154. package/src/messaging/remote-process.ts +131 -0
  155. package/src/messaging/remote-replies.ts +98 -0
  156. package/src/messaging/remote-rpc-admission.ts +32 -0
  157. package/src/messaging/remote-send.ts +72 -0
  158. package/src/messaging/remote-store.ts +111 -0
  159. package/src/messaging/remote-tunnels.ts +65 -0
  160. package/src/messaging/rpc.ts +13 -1
  161. package/src/oauth/account-auto-pause-policy.ts +66 -0
  162. package/src/oauth/account-auto-pause.ts +171 -0
  163. package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
  164. package/src/oauth/account-import/parser.ts +15 -2
  165. package/src/oauth/account-import/types.ts +7 -0
  166. package/src/oauth/account-quota-rank.ts +2 -1
  167. package/src/oauth/anthropic-account-refusal.ts +6 -0
  168. package/src/oauth/anthropic-model-family.ts +8 -0
  169. package/src/oauth/anthropic-model-quota.ts +2 -6
  170. package/src/oauth/api-key-pool-preserve.ts +37 -0
  171. package/src/oauth/chatgpt-shared.ts +10 -1
  172. package/src/oauth/dsh.ts +45 -2
  173. package/src/oauth/index.ts +5 -35
  174. package/src/oauth/kiro.ts +44 -1
  175. package/src/oauth/login-flow-state.ts +11 -2
  176. package/src/oauth/store.ts +35 -8
  177. package/src/oauth/types.ts +2 -0
  178. package/src/plugins/upstream-hooks.ts +14 -1
  179. package/src/providers/api-key-pause.ts +32 -0
  180. package/src/providers/api-key-resolve.ts +10 -4
  181. package/src/providers/api-key-selection.ts +6 -1
  182. package/src/providers/api-keys.ts +8 -4
  183. package/src/providers/claude-cli-usage.ts +25 -5
  184. package/src/providers/initial-model-selection.ts +2 -2
  185. package/src/providers/key-failover.ts +21 -13
  186. package/src/providers/key-store.ts +8 -3
  187. package/src/providers/mistral-transport.ts +207 -0
  188. package/src/providers/model-rename-fields.ts +1 -0
  189. package/src/providers/openai-sidecar.ts +5 -0
  190. package/src/providers/opencode-zen-rate-limit.ts +6 -72
  191. package/src/providers/quota/account-cache.ts +15 -1
  192. package/src/providers/quota/anthropic-account-quota.ts +20 -7
  193. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  194. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  195. package/src/providers/quota/antigravity.ts +45 -8
  196. package/src/providers/quota/failure-codes.ts +18 -0
  197. package/src/providers/quota/report-cache.ts +14 -0
  198. package/src/providers/quota/vendor-probes-oauth.ts +15 -6
  199. package/src/providers/quota-decision-publication.ts +77 -0
  200. package/src/providers/quota-decision-snapshot.ts +145 -0
  201. package/src/providers/quota-routing-cache.ts +4 -1
  202. package/src/providers/quota-types.ts +6 -1
  203. package/src/providers/quota.ts +8 -3
  204. package/src/providers/registry/entries-extended.ts +14 -53
  205. package/src/providers/registry/model-seeds.ts +1 -1
  206. package/src/providers/registry.ts +1 -2
  207. package/src/providers/routed-config.ts +1 -1
  208. package/src/providers/stale-vision-classification-migration.ts +1 -1
  209. package/src/providers/xai-transport.ts +2 -1
  210. package/src/responses/audio.ts +46 -0
  211. package/src/responses/input-media.ts +37 -15
  212. package/src/responses/parser-content.ts +22 -27
  213. package/src/responses/parser.ts +18 -10
  214. package/src/responses/reasoning-retention.ts +218 -11
  215. package/src/responses/schema.ts +3 -4
  216. package/src/responses/web-search-replay.ts +147 -0
  217. package/src/routing/compatibility/behavior.ts +4 -1
  218. package/src/server/auth-cors.ts +7 -0
  219. package/src/server/background-lifecycle.ts +2 -1
  220. package/src/server/chat-native.ts +36 -20
  221. package/src/server/claude-messages.ts +2 -1
  222. package/src/server/index/optional-listeners.ts +6 -0
  223. package/src/server/index/serve-options.ts +8 -2
  224. package/src/server/index/websocket-handler.ts +10 -0
  225. package/src/server/lifecycle.ts +17 -1
  226. package/src/server/management/combo-routes.ts +17 -2
  227. package/src/server/management/oauth-account-routes.ts +12 -2
  228. package/src/server/management/provider-completion-test.ts +289 -0
  229. package/src/server/management/provider-routes.ts +9 -1
  230. package/src/server/management/route-registry.ts +2 -0
  231. package/src/server/messages-native.ts +29 -4
  232. package/src/server/messages-shared.ts +4 -1
  233. package/src/server/request-log.ts +23 -9
  234. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  235. package/src/server/responses/adapter-continuation.ts +2 -2
  236. package/src/server/responses/adapter-dispatch.ts +79 -30
  237. package/src/server/responses/codex-ws-exchange.ts +3 -0
  238. package/src/server/responses/codex-ws-metadata.ts +44 -0
  239. package/src/server/responses/codex-ws-session.ts +21 -1
  240. package/src/server/responses/combo-session-recall.ts +25 -0
  241. package/src/server/responses/combo-stream-preflight.ts +10 -3
  242. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  243. package/src/server/responses/compact.ts +10 -5
  244. package/src/server/responses/context-overflow.ts +141 -2
  245. package/src/server/responses/core-auth.ts +20 -0
  246. package/src/server/responses/core-codex-account.ts +28 -15
  247. package/src/server/responses/core-combo.ts +34 -5
  248. package/src/server/responses/core-opaque-recovery.ts +20 -11
  249. package/src/server/responses/core.ts +12 -9
  250. package/src/server/responses/deepseek-image-files.ts +121 -0
  251. package/src/server/responses/empty-completion-guard.ts +10 -0
  252. package/src/server/responses/fetch-helpers.ts +65 -14
  253. package/src/server/responses/input-admission.ts +6 -2
  254. package/src/server/responses/jev-model-invoke.ts +13 -14
  255. package/src/server/responses/main-hard-lock-routing.ts +33 -8
  256. package/src/server/responses/native-steering-log.ts +2 -2
  257. package/src/server/responses/opengateway-completion-guard.ts +232 -0
  258. package/src/server/responses/passthrough-completion.ts +41 -0
  259. package/src/server/responses/passthrough-delivery.ts +5 -2
  260. package/src/server/responses/passthrough-dispatch.ts +42 -45
  261. package/src/server/responses/passthrough-key-rotation.ts +40 -0
  262. package/src/server/responses/passthrough-observations.ts +38 -0
  263. package/src/server/responses/reasoning-archive-error.ts +21 -0
  264. package/src/server/responses/request-compression.ts +98 -0
  265. package/src/server/responses/request-prepare.ts +11 -1
  266. package/src/server/responses/request-transport.ts +92 -5
  267. package/src/server/responses/run-turn-execution.ts +2 -0
  268. package/src/server/responses/sidecar-execution.ts +16 -3
  269. package/src/server/responses/terminal-guard.ts +2 -0
  270. package/src/server/startup-action-control.ts +2 -2
  271. package/src/server/startup-health-cache.ts +2 -2
  272. package/src/server/windows-tray-control.ts +2 -2
  273. package/src/service/definition.ts +9 -1
  274. package/src/service/orchestration.ts +17 -0
  275. package/src/types/account-auto-pause.ts +15 -0
  276. package/src/types/config.ts +23 -1
  277. package/src/types/provider.ts +11 -5
  278. package/src/types/request.ts +36 -3
  279. package/src/types.ts +2 -0
  280. package/src/usage/cost.ts +17 -1
  281. package/src/usage/expected-prices.ts +3 -1
  282. package/src/usage/jev-stats.ts +4 -0
  283. package/src/usage/log.ts +2 -0
  284. package/src/usage/summary.ts +7 -0
  285. package/src/web-search/loop.ts +39 -13
  286. package/src/web-search/progress-stream.ts +4 -1
  287. package/src/web-search/run-turn-loop.ts +9 -3
  288. package/src/web-search/sidecar-search-replay.ts +96 -0
  289. package/gui/dist/assets/App-CHC89EXQ.js +0 -58
  290. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
  291. package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
@@ -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,21 @@
1
+ import { formatErrorResponse } from "../../bridge";
2
+ import { ReasoningArchiveBusyError, ReasoningArchiveError,
3
+ ReasoningArchiveQuotaError } from "../../responses/reasoning-retention";
4
+
5
+ /** Local storage refusal before any send. Never expose filesystem messages or causes. */
6
+ export function reasoningArchiveErrorResponse(error: unknown): Response | undefined {
7
+ let code: string;
8
+ let message: string;
9
+ if (error instanceof ReasoningArchiveQuotaError) {
10
+ code = "reasoning_archive_quota_exceeded";
11
+ message = "Local reasoning archive is full. Review and manually remove older reasoning archives, then retry the unchanged request.";
12
+ } else if (error instanceof ReasoningArchiveBusyError) {
13
+ code = "reasoning_archive_busy";
14
+ message = "Another local reasoning archive writer is active. Wait for it to finish, then retry the unchanged request.";
15
+ } else if (error instanceof ReasoningArchiveError) {
16
+ code = "reasoning_archive_unavailable";
17
+ message = "Local reasoning archive is unavailable. Check archive directory permissions and local storage health before retrying the unchanged request.";
18
+ } else return undefined;
19
+ // Match the local spend-ledger storage refusal's 503 convention without guessing a delay.
20
+ return formatErrorResponse(503, "server_error", message, { code, classified: true });
21
+ }
@@ -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
+ }
@@ -100,6 +100,7 @@ import type {
100
100
  import {
101
101
  codexRouteCredentialDomainHeaders,
102
102
  codexRouteCredentialOwnership,
103
+ mainHardLockFallbackCallerCheck,
103
104
  resolveResponsesCodexAuth,
104
105
  withClaudeNativeSession,
105
106
  } from "./core-auth";
@@ -364,7 +365,16 @@ export async function prepareResponsesRequest(
364
365
  // must run before comboIdFromRawBody reads `model`.
365
366
  if (!concreteSelection && !options.compactionRoutingOverride && !isCompactionTrigger && !options.internalDecisionCall
366
367
  && !shadowCallIntercepted && !shadowCallMatches && !memoryModelApplies && !threadSpawn && inboundWire === "responses") {
367
- options.mainHardLockFallbackOverride = applyMainHardLockFallbackOverride(body, config);
368
+ try {
369
+ options.mainHardLockFallbackOverride = await applyMainHardLockFallbackOverride(
370
+ body, config, undefined, mainHardLockFallbackCallerCheck(req, config, options),
371
+ );
372
+ } catch (error) {
373
+ if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
374
+ const mapped = mapCodexAuthContextErrorToResponse(error, { now: Date.now() });
375
+ if (mapped) return mapped;
376
+ throw error;
377
+ }
368
378
  }
369
379
  const comboId = !concreteSelection ? comboIdFromRawBody(body, config) : null;
370
380
  // A decision model may be a plain model or a non-JEV combo. A JEV combo here would recurse
@@ -71,7 +71,8 @@ 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 { codexWsExecutorBinding, physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
75
+ import { deepSeekImageFileBody, needsDeepSeekImageFiles } from "./deepseek-image-files";
75
76
  import type { ProviderFetchOptions } from "./fetch-helpers";
76
77
  import { captureConfigGeneration } from "../../lib/state-store-sweeper";
77
78
  import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuota } from "../../providers/quota";
@@ -80,6 +81,7 @@ import { formatErrorResponse } from "../../bridge";
80
81
  import { bindRouteReasoningReplayScope } from "./core-replay";
81
82
  import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
82
83
  import { redactSecretString } from "../../lib/redact";
84
+ import { captureAccountAutoPauseAttempt } from "../../oauth/account-auto-pause";
83
85
  import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
84
86
  import { distributedKeyPhysicalFetch } from "../../providers/api-key-response-ownership";
85
87
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
@@ -589,10 +591,62 @@ export async function prepareResponsesTransport(
589
591
  };
590
592
  }
591
593
  const build = resolved.buildRequest.bind(resolved);
594
+ const fileProvider = route.provider;
595
+ const fileProviderName = route.providerName;
596
+ // Persistence consent is configuration: capture its generation with the provider snapshot.
597
+ const fileConsentGeneration = captureConfigGeneration();
592
598
  resolved.buildRequest = async (requestParsed, incoming) => {
593
599
  const request = await build(requestParsed, incoming);
594
600
  // Capture at adapter creation, never from mutable serving state after an await.
595
601
  if (binding) requestBindings.set(request, binding);
602
+ const allowedImageFileModels = fileProvider.modelCapabilities
603
+ ? Object.entries(fileProvider.modelCapabilities)
604
+ .filter(([, cap]) => cap?.deepseekImageFiles === true)
605
+ .map(([model]) => model)
606
+ : [];
607
+ if (allowedImageFileModels.includes(request.outboundModelId ?? requestParsed.modelId) && needsDeepSeekImageFiles(request)) {
608
+ const consentModel = request.outboundModelId ?? requestParsed.modelId;
609
+ try {
610
+ // Ordinary uploads use provider pacing. A queued rebuild retains its enclosing
611
+ // fetch lease; reacquiring that same concurrency slot would deadlock at a limit of one.
612
+ const uploadFetch = providerFetch(fileProvider, options.codexWsRuntimeIdentity, {
613
+ httpOnly: true, providerName: fileProviderName, modelId: requestParsed.modelId,
614
+ beforeDispatch: () => {
615
+ if (!selectionIsCurrent(binding)) throw new Error("Credential selection changed before image upload");
616
+ // A queued upload must not outlive a revoked opt-in or a configuration reload.
617
+ if (captureConfigGeneration() !== fileConsentGeneration
618
+ || fileProvider.modelCapabilities?.[consentModel]?.deepseekImageFiles !== true) {
619
+ throw new Error("DeepSeek image file consent changed before image upload");
620
+ }
621
+ },
622
+ });
623
+ const uploadSignal = incoming.abortSignal ?? options.abortSignal ?? req.signal;
624
+ const pacedUpload: typeof fetch = incoming.dispatchPacingHeld
625
+ ? (async (input, init) => {
626
+ // The inference lease owns concurrency, but each physical upload still
627
+ // consumes an interval slot, including after a credential rebuild.
628
+ const slot = await waitForProviderRequestSlot(fileProviderName, fileProvider, requestParsed.modelId,
629
+ init?.signal ?? uploadSignal, { concurrency: false });
630
+ try { return await uploadFetch.unpacedFetch!(input, init); }
631
+ finally { releaseProviderRequestSlot(slot); }
632
+ }) as typeof fetch
633
+ : uploadFetch;
634
+ const body = await deepSeekImageFileBody(request, pacedUpload,
635
+ uploadSignal, {
636
+ allowedModels: allowedImageFileModels, budget: incoming.translatorBudget,
637
+ });
638
+ if (body !== undefined) {
639
+ request.releaseBodyObservation?.();
640
+ request.body = body;
641
+ const headers = new Headers(request.headers);
642
+ headers.delete("content-length");
643
+ request.headers = Object.fromEntries(headers);
644
+ request.releaseBodyObservation = incoming.translatorBudget.observeExternallyCapped(
645
+ "passthrough_serialization", Buffer.byteLength(body, "utf8"),
646
+ );
647
+ }
648
+ } catch (error) { request.releaseBodyObservation?.(); throw error; }
649
+ }
596
650
  return request;
597
651
  };
598
652
  if (resolved.runTurn) {
@@ -612,6 +666,10 @@ export async function prepareResponsesTransport(
612
666
  // Pacing may outlive admission. Preserve local pause/cooldown reasons even when
613
667
  // a strict route rejects the old proposal before another bearer can be resolved.
614
668
  let candidate = servingOAuthSnapshot;
669
+ if (anthropicInstance === undefined && candidate && getAccountSet(route.providerName)?.accounts.find(row => row.id === candidate!.accountId)?.paused) {
670
+ oauthSelection = captureOAuthAccountSelection(route.providerName);
671
+ candidate = oauthSelection ? await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true }) : undefined;
672
+ }
615
673
  if (anthropicInstance !== undefined) {
616
674
  oauthSelection = captureOAuthAccountSelection(route.providerName);
617
675
  const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
@@ -644,6 +702,24 @@ export async function prepareResponsesTransport(
644
702
  invalidateSameTargetRequest();
645
703
  return adapter;
646
704
  };
705
+ const recoverAutoPausedSelection = async (requestParsed: OcxParsedRequest): Promise<boolean> => {
706
+ if (route.provider.authMode === "forward") return false;
707
+ const available = route.provider.authMode === "oauth"
708
+ ? getAccountSet(route.providerName)?.accounts.some(row => !row.paused && !row.needsReauth)
709
+ : !!config.providers[route.providerName]?.apiKey;
710
+ if (!available) return false;
711
+ await refreshDispatchAdapter(requestParsed);
712
+ // Bind before the recovery caller rebuilds. A terminal retry can be a clone;
713
+ // the outer request also owns continuation persistence and replay publication.
714
+ for (const replayParsed of new Set([parsed, requestParsed])) {
715
+ bindRouteReasoningReplayScope({
716
+ parsed: replayParsed, providerName: route.providerName, provider: route.provider,
717
+ adapterName: activeAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
718
+ forwardHeaders: requestState.selectedForwardHeaders,
719
+ });
720
+ }
721
+ return true;
722
+ };
647
723
  const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
648
724
  requestParsed._cursorIdentityScope = undefined;
649
725
  requestParsed._cursorConversationId = undefined;
@@ -704,6 +780,7 @@ export async function prepareResponsesTransport(
704
780
  providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
705
781
  pacingSlot: attemptSlot,
706
782
  turnScopedPacing: true,
783
+ accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, headers),
707
784
  beforeDispatch: beforePhysicalSend,
708
785
  });
709
786
  await run(requestParsed, { ...incoming, pacingSlot: attemptSlot, providerFetch: fetch, beforePhysicalSend }, event => { if (!refused) emit(event); });
@@ -734,7 +811,8 @@ export async function prepareResponsesTransport(
734
811
  pacingChanged = false;
735
812
  }
736
813
  if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
737
- const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
814
+ const ws = codexWsExecutorBinding(execute);
815
+ const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? ws?.http ?? execute;
738
816
  const binding = requestBindings.get(wireRequest);
739
817
  const snapshot = anthropicInstance !== undefined && binding?.kind === "oauth"
740
818
  ? binding.snapshot : undefined;
@@ -756,6 +834,8 @@ export async function prepareResponsesTransport(
756
834
  // apply a host-scoped decision to a different host.
757
835
  const releaseFamily = snapshot ? anthropicQuota!.claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
758
836
  if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
837
+ const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, binding?.kind === "api-key" ? binding.provider : route.provider,
838
+ { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, new Headers(dispatchInit.headers));
759
839
  let response: Response;
760
840
  let physicalOwner: AnthropicPhysicalSendOwnership | null = null;
761
841
  try {
@@ -772,10 +852,14 @@ export async function prepareResponsesTransport(
772
852
  passiveQuotaWriterGeneration = writerGeneration;
773
853
  }
774
854
  commitKeyAttemptSend();
775
- response = await sendWithConnectionPolicy(
855
+ // The WS executor dials its own socket: HTTP connection policy and body coding do not apply.
856
+ // WS eligibility (egress, pacing, URL) was checked for the provider it was built for; a
857
+ // provider reselected above was not, so it takes the HTTP fallback under its own route.
858
+ response = ws?.provider === route.provider ? await execute(destination, dispatchInit) : await sendWithConnectionPolicy(
776
859
  distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
777
860
  destination,
778
- { ...dispatchInit, redirect: "manual" },
861
+ { ...dispatchInit, redirect: "manual",
862
+ ...physicalSendObserverInit(observeAutoPause, destination instanceof Request ? destination.url : String(destination)) },
779
863
  { providerName: route.providerName, provider: route.provider },
780
864
  );
781
865
  } finally { releaseFamily(); }
@@ -790,11 +874,12 @@ export async function prepareResponsesTransport(
790
874
  && current.credential.access === snapshot.accessToken
791
875
  && credentialGeneration(current.credential) === snapshot.generation) {
792
876
  bindAnthropicRefusalCredentialForSend(response, physicalOwner);
793
- recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId);
877
+ recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId, snapshot.generation);
794
878
  }
795
879
  } catch { /* best-effort observation cannot fail the response */ }
796
880
  }
797
881
  if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
882
+ if (observeAutoPause) response = await observeAutoPause(response);
798
883
  return response;
799
884
  }
800
885
  let nextAdapter: ProviderAdapter;
@@ -815,6 +900,7 @@ export async function prepareResponsesTransport(
815
900
  adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
816
901
  const rebuilt = await nextAdapter.buildRequest(requestParsed, {
817
902
  headers: requestState.selectedForwardHeaders, providerName: route.providerName, translatorBudget,
903
+ dispatchPacingHeld: true,
818
904
  ...(imageTierBias > 0 ? { imageTierBias } : {}),
819
905
  });
820
906
  const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
@@ -1280,6 +1366,7 @@ export async function prepareResponsesTransport(
1280
1366
  oauthDispatch,
1281
1367
  noteRoutedAttemptSend,
1282
1368
  commitKeyAttemptSend,
1369
+ recoverAutoPausedSelection,
1283
1370
  bindKeyUsageFromBridge,
1284
1371
  anthropicSessionKey,
1285
1372
  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;
@@ -238,6 +238,11 @@ export interface ServiceInstallPreparationDeps extends RuntimePreflightDeps {
238
238
  supervisionLatch?: SupervisionLatch;
239
239
  diagnose?: () => ServiceDiagnostic;
240
240
  managerOps?: (backend: ServiceBackend) => ServiceInstallCleanupOps | null;
241
+ /**
242
+ * Task Scheduler only: ends surviving launcher/wrapper processes of THIS
243
+ * installation. Scoped to canonical script/launcher tokens, never a bare PID.
244
+ */
245
+ killServiceWrappers?: () => void;
241
246
  stopTrackedProxy?: () => Promise<unknown>;
242
247
  platform?: NodeJS.Platform;
243
248
  }
@@ -279,6 +284,18 @@ export async function prepareServiceInstall(
279
284
  if (!manager) throw new Error(`Background service manager is unavailable for ${backend}.`);
280
285
  if (manager.status() !== null) manager.stop();
281
286
  }
287
+ if (platform === "win32") {
288
+ // `schtasks /end` ends the task instance but leaves the cmd `:loop` wrapper
289
+ // alive to respawn the proxy mid-install — the same survivor
290
+ // `ocx stop` kills via killWindowsServiceWrapperProcesses. Run it
291
+ // after every manager has been asked to stop and before the tracked-proxy
292
+ // kill so a child respawned in that gap is still caught below. It also runs
293
+ // when no manager stop happened: a registration deleted mid-uninstall can
294
+ // leave the detached wrapper looping. A manager stop that refused propagates
295
+ // above before this runs, so the respawner of a still-running service is
296
+ // never killed.
297
+ (deps.killServiceWrappers ?? killWindowsServiceWrapperProcesses)();
298
+ }
282
299
  await (deps.stopTrackedProxy ?? stopTrackedProxyIfRunning)();
283
300
  }
284
301
 
@@ -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
+ }