@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -11,6 +11,7 @@ import type { PreparedResponsesRequest } from "./request-prepare";
11
11
  import type { ResponsesTransport } from "./request-transport";
12
12
  import type { ResponsesEffects } from "./response-effects";
13
13
  import type { ResponsesSendBudget } from "./request-send-budget";
14
+ import { transientSendCapFor } from "./request-send-budget";
14
15
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
15
16
  import { codexSafetyBufferingFilterOptions, terminalStatusFromParsed } from "../relay";
16
17
  import { imageGenToolCallAliases } from "../responses-image-gen-repair";
@@ -68,6 +69,7 @@ import {
68
69
  sealRequestAttemptIdentity,
69
70
  recordAttemptCredentialSource,
70
71
  } from "../request-log";
72
+ import { noteAttemptRecoveryWithheld } from "../request-log";
71
73
  import {
72
74
  upstreamHostHealthKey,
73
75
  normalizeUpstreamHostCircuitThreshold,
@@ -91,11 +93,15 @@ import {
91
93
  usesCodexForwardPoolAuth,
92
94
  codexWsQuotaObserver,
93
95
  isFixedCodexAccount,
94
- shouldRetryCodexPoolAccountModel400,
96
+ codexPoolAccountModel400Denial,
95
97
  shouldRetryCodexPoolAccountQuota,
96
98
  shouldRetryCodexPoolAccountTransient,
97
99
  retryCodexPoolOnAlternateAccount,
98
100
  } from "./core-codex-account";
101
+ import {
102
+ clearCodexModelDenialEvidence,
103
+ recordCodexModelDenialEvidence,
104
+ } from "../../codex/model-entitlements";
99
105
  import { readCodexWsStage } from "./codex-ws-wire";
100
106
  import { linkAbortSignal } from "./core-lifetime";
101
107
  import type { CodexAuthContext } from "../../codex/auth-context";
@@ -105,9 +111,8 @@ import {
105
111
  SendBudgetExhaustedError,
106
112
  fetchWithTransientRetry,
107
113
  applyUpstreamRecoveryInit,
108
- TRANSIENT_RETRY_MAX_ATTEMPTS,
109
114
  isNonReplayableResponse,
110
- prepareSameTarget429Wait,
115
+ prepareSameTarget429Wait,
111
116
  sleepWithAbort,
112
117
  } from "../../lib/upstream-retry";
113
118
  import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
@@ -115,7 +120,12 @@ import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upst
115
120
  import { recordCodexUpstreamOutcome } from "../../codex/routing";
116
121
  import { describeUpstreamConnectFailure } from "./upstream-error";
117
122
  import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
118
- import { rateLimitRetryPolicyFor, rateLimitRetryDelayMs } from "../../providers/key-failover";
123
+ import {
124
+ isOpenCodeGoDestination,
125
+ rateLimitRetryPolicyFor,
126
+ rateLimitRetryDelayMs,
127
+ transientRetryPolicyFor,
128
+ } from "../../providers/key-failover";
119
129
  import type { AttemptRecoveryKind } from "../../usage/log";
120
130
  import { resolveWireProtocolOverride } from "../adapter-resolve";
121
131
  import { refreshPoolForwardAuth, refreshNativeMainForwardAuth, withClaudeNativeSession } from "./core-auth";
@@ -133,6 +143,7 @@ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
133
143
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
134
144
  import {
135
145
  attemptOpaqueBlobRecovery,
146
+ isEncryptedFunctionOutputRejection,
136
147
  outboundResponsesBodyCarriesEncryptedFunctionOutput,
137
148
  resetStreamedOpaqueBlobLogContext,
138
149
  consoleGoUploadRejectionBody,
@@ -198,6 +209,7 @@ export async function preparePassthroughExchange(
198
209
  | "reserveCredentialHop"
199
210
  | "pendingHopPermit"
200
211
  | "workflowRootId"
212
+ | "sendsUsed"
201
213
  >,
202
214
  ) {
203
215
  const { config, logCtx, options, req } = requestContext;
@@ -665,6 +677,33 @@ export async function preparePassthroughExchange(
665
677
  linkAbortSignal(upstream, options.abortSignal);
666
678
  const connectMs = config.connectTimeoutMs ?? 200_000;
667
679
  let upstreamResponse: Response;
680
+ /**
681
+ * This leg's transient-5xx ladder cap, from the provider's own `transientRetryOn5xx`.
682
+ *
683
+ * The lane used to pass `TRANSIENT_RETRY_MAX_ATTEMPTS` at every call site, so an operator who
684
+ * configured the option on a key-auth `openai-responses` provider changed nothing in either
685
+ * direction, while the same provider on `openai-chat` was tuned normally. That asymmetry is
686
+ * #4893. The gate in `transientRetryPolicyFor` returns null for OAuth and forward providers,
687
+ * so the ChatGPT pool keeps exactly the ladder it has always had.
688
+ *
689
+ * Read per call rather than captured once, for two reasons. `route.provider` is reassigned
690
+ * inside the recovery loop by credential rotation and transport resolution, so a hoisted
691
+ * policy could outlive the provider row it came from. And the configured value is a total for
692
+ * the whole request, so it has to be measured against what the request has already sent at
693
+ * the moment each leg asks.
694
+ *
695
+ * Still bounded by the request: every site feeds this to `remainingTransientSendBudget` or
696
+ * `recoverySendAllowance`, which intersect it with the request-wide base allowance. So a
697
+ * provider can narrow this request's sends exactly and cannot widen the bound that exists to
698
+ * stop per-request amplification (#4546).
699
+ */
700
+ const transientSendPolicy = () => transientRetryPolicyFor(route.provider);
701
+ const transientSendAttempts = (): number => transientSendCapFor(
702
+ transientSendPolicy()?.attempts,
703
+ sendBudgetState.sendsUsed,
704
+ );
705
+ const configuredTransientSendBudgetExhausted = (): boolean =>
706
+ transientSendPolicy() !== null && transientSendAttempts() === 0;
668
707
  /**
669
708
  * Refuse a built body that exceeds the operator's configured ceiling, before it is sent.
670
709
  *
@@ -804,7 +843,15 @@ export async function preparePassthroughExchange(
804
843
  // retry wrapper replaces — proves the host was reached (#914 review).
805
844
  .then(adoptObservedResponse);
806
845
  },
807
- { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(TRANSIENT_RETRY_MAX_ATTEMPTS), onSendsConsumed: noteTransientSends },
846
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url),
847
+ attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends,
848
+ // The OpenCode Go destination stalls-then-drops inference sends (ambiguous
849
+ // pre-header resets surfacing as refused 429s); its subscription traffic is
850
+ // inference-only, so a bounded reset replay here absorbs the blip instead of
851
+ // failing the turn. Recovery legs keep the fail-closed refusal; only this
852
+ // initial send is replay-eligible. Attempts stay budget-bounded via attempts.
853
+ replaySafe: isOpenCodeGoDestination(route.provider),
854
+ },
808
855
  );
809
856
  } catch (err) {
810
857
  return transportFailureResponse(err);
@@ -826,7 +873,7 @@ export async function preparePassthroughExchange(
826
873
  recovery: AttemptRecoveryKind,
827
874
  ): Promise<Response | { failed: Response }> => {
828
875
  const retryAdapter = resolveSelectionAdapter(
829
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
876
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
830
877
  config.cacheRetention,
831
878
  );
832
879
  if (!("passthrough" in retryAdapter) || !retryAdapter.passthrough) {
@@ -864,14 +911,15 @@ export async function preparePassthroughExchange(
864
911
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name);
865
912
  const rebuiltBodyRefusal = refuseOversizedOutboundBody(request);
866
913
  if (rebuiltBodyRefusal) return { failed: rebuiltBodyRefusal };
867
- // The base allowance is spent first; once it is gone this leg may still draw the one
868
- // shared final-recovery reserve, which is what keeps a validated sanitized rebuild
869
- // after a 5xx streak alive at four total sends instead of dying at three. Reserved
870
- // outside the try so the finally can hand it back if the leg never reached its send.
914
+ // The base allowance is spent first. An unconfigured provider may then draw the one shared
915
+ // final-recovery reserve, which keeps a validated sanitized rebuild after a 5xx streak alive
916
+ // at four total sends instead of dying at three. An explicit provider total cannot widen.
917
+ // Reserve outside the try so the finally can hand it back if the leg never reaches its send.
871
918
  const allowance = recoverySendAllowance(
872
- TRANSIENT_RETRY_MAX_ATTEMPTS,
919
+ transientSendAttempts(),
873
920
  recoveryClassFor(recovery),
874
921
  `${route.providerName}|${route.modelId}|${recovery}`,
922
+ { allowFinalRecoveryReserve: transientSendPolicy() === null },
875
923
  );
876
924
  try {
877
925
  return await fetchWithTransientRetry(
@@ -951,7 +999,7 @@ export async function preparePassthroughExchange(
951
999
  route.provider = replay.provider;
952
1000
  requestState.selectedForwardHeaders = withClaudeNativeSession(replay.headers, replay.provider, options.claudeNativeSessionId);
953
1001
  const replayAdapter = resolveSelectionAdapter(
954
- resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
1002
+ resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire, route.staticPolicy),
955
1003
  config.cacheRetention,
956
1004
  );
957
1005
  if (!("passthrough" in replayAdapter) || !replayAdapter.passthrough) {
@@ -1033,7 +1081,7 @@ export async function preparePassthroughExchange(
1033
1081
  // Refused here, before the 401 body is cancelled: once it is gone the request can only
1034
1082
  // answer with a synthetic 502, which would report a proxy budget decision as an upstream
1035
1083
  // fault and throw away the credential evidence the client needs.
1036
- && !sendBudgetExhausted()
1084
+ && !sendBudgetExhausted(transientSendAttempts())
1037
1085
  ) {
1038
1086
  oauth401ReplayAttempted = true;
1039
1087
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
@@ -1072,7 +1120,7 @@ export async function preparePassthroughExchange(
1072
1120
  );
1073
1121
  route.provider = refreshedProvider;
1074
1122
  const refreshedAdapter = resolveSelectionAdapter(
1075
- resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
1123
+ resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
1076
1124
  config.cacheRetention,
1077
1125
  );
1078
1126
  if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
@@ -1134,7 +1182,7 @@ export async function preparePassthroughExchange(
1134
1182
  route.provider.authMode === "forward")
1135
1183
  .then(adoptObservedResponse);
1136
1184
  },
1137
- { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(TRANSIENT_RETRY_MAX_ATTEMPTS), onSendsConsumed: noteTransientSends },
1185
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends },
1138
1186
  );
1139
1187
  } catch (err) {
1140
1188
  return transportFailureResponse(err);
@@ -1145,13 +1193,13 @@ export async function preparePassthroughExchange(
1145
1193
 
1146
1194
  // Native Responses returns before the generic adapter's OAuth rotation loop. Keep
1147
1195
  // the same quorum, cooldown and request budget here, before any client bytes flow.
1148
- if (
1149
- upstreamResponse.status === 429
1196
+ if (
1197
+ upstreamResponse.status === 429
1150
1198
  // Not a provider rate limit when this proxy synthesized it for a refused reset
1151
1199
  // replay; rotating accounts on it would re-send an inference that may already
1152
1200
  // have run and would cool down an account that refused nothing.
1153
1201
  && !isNonReplayableResponse(upstreamResponse)
1154
- && transportState.genericFailoverAccountId
1202
+ && transportState.genericFailoverAccountId
1155
1203
  && transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
1156
1204
  && isGenericOAuthFailoverEnabled(config, route.providerName)
1157
1205
  ) {
@@ -1167,6 +1215,8 @@ export async function preparePassthroughExchange(
1167
1215
  const nextAccountId = rotateGenericOAuthAccountOn429(
1168
1216
  config, route.providerName, transportState.genericFailoverAccountId,
1169
1217
  upstreamResponse.headers.get("retry-after"),
1218
+ Date.now(),
1219
+ route.modelId,
1170
1220
  );
1171
1221
  let snapshot: OAuthAccessSnapshot | undefined;
1172
1222
  if (nextAccountId) {
@@ -1194,6 +1244,11 @@ export async function preparePassthroughExchange(
1194
1244
  }
1195
1245
  // No credential moved, so the reservation costs nothing.
1196
1246
  hop.permit?.release();
1247
+ } else {
1248
+ // Rotation was available -- the roster cap above admitted it -- and the shared request
1249
+ // budget refused. Recorded so a one-send log is not read as "nothing was eligible",
1250
+ // which is the ambiguity this attribution exists to remove (#5044).
1251
+ noteAttemptRecoveryWithheld(logCtx.activeAttempt, "rotation-send-budget");
1197
1252
  }
1198
1253
  }
1199
1254
 
@@ -1203,15 +1258,15 @@ export async function preparePassthroughExchange(
1203
1258
  // immediately with no same-key replay. Pre-stream only — nothing has been relayed yet, so
1204
1259
  // the replay is lossless (same invariant as the recovery loop). Forward/OAuth providers
1205
1260
  // keep their pool logic below (rateLimitRetryPolicyFor returns null for them).
1206
- while (
1207
- upstreamResponse.status === 429
1261
+ while (
1262
+ upstreamResponse.status === 429
1208
1263
  && !isNonReplayableResponse(upstreamResponse)
1209
- && rateLimitPolicy !== null
1264
+ && rateLimitPolicy !== null
1210
1265
  && rateLimitRetries < rateLimitPolicy.attempts
1211
1266
  // Checked here rather than inside the helper: prepareSameTarget429Wait releases the 429
1212
1267
  // body, so a refusal discovered after the wait can no longer return the real rate-limit
1213
1268
  // answer and would surface a synthetic 502 instead.
1214
- && !sendBudgetExhausted()
1269
+ && !sendBudgetExhausted(transientSendAttempts())
1215
1270
  ) {
1216
1271
  rateLimitRetries += 1;
1217
1272
  // Release unread body + deliberate wait via the shared same-target helper.
@@ -1259,7 +1314,7 @@ export async function preparePassthroughExchange(
1259
1314
  route.provider.authMode === "forward")
1260
1315
  .then(adoptObservedResponse);
1261
1316
  },
1262
- { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(TRANSIENT_RETRY_MAX_ATTEMPTS), onSendsConsumed: noteTransientSends },
1317
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends },
1263
1318
  );
1264
1319
  } catch (err) {
1265
1320
  return transportFailureResponse(err);
@@ -1291,11 +1346,37 @@ export async function preparePassthroughExchange(
1291
1346
 
1292
1347
  if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
1293
1348
  let poolRetryOutcome: number | undefined;
1294
- if (await shouldRetryCodexPoolAccountModel400(
1349
+ // A success is the freshest evidence there is about this pair, and it outranks any earlier
1350
+ // refusal: whatever the entitlement was when upstream declined, it is not that now. Both
1351
+ // ids are cleared because the wire model can differ from the routed one.
1352
+ if (upstreamResponse.ok) {
1353
+ clearCodexModelDenialEvidence(
1354
+ admissionState.authCtx.accountId,
1355
+ route.modelId,
1356
+ admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
1357
+ );
1358
+ clearCodexModelDenialEvidence(
1359
+ admissionState.authCtx.accountId,
1360
+ parsed.modelId,
1361
+ admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
1362
+ );
1363
+ }
1364
+ const model400Denial = await codexPoolAccountModel400Denial(
1295
1365
  upstreamResponse,
1296
1366
  route.modelId,
1297
1367
  options.abortSignal,
1298
- )) {
1368
+ parsed.modelId,
1369
+ );
1370
+ if (model400Denial !== undefined) {
1371
+ // Spend this refusal on more than one retry. It is the account's own authenticated
1372
+ // answer about this model, and the roster cache that selection otherwise reads expires
1373
+ // five minutes after a catalog sync fills it -- so without remembering this, the next
1374
+ // request selects the same account on quota alone and takes the same 400 (#4906).
1375
+ recordCodexModelDenialEvidence(
1376
+ admissionState.authCtx.accountId,
1377
+ model400Denial,
1378
+ admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
1379
+ );
1299
1380
  poolRetryOutcome = 400;
1300
1381
  } else if (!admissionState.authCtx.fixedAccount && await shouldRetryCodexPoolAccountQuota(
1301
1382
  upstreamResponse,
@@ -1365,18 +1446,20 @@ export async function preparePassthroughExchange(
1365
1446
  // eviction, or an older transcript). Inspect only a bounded clone of a 4xx whose exact outbound
1366
1447
  // Responses body still carries opaque state, then rebuild once through the ordinary adapter
1367
1448
  // sanitation path. A second rejection falls through unchanged because the guard stays armed.
1368
- const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
1369
- response: upstreamResponse,
1370
- outboundBody: request.body,
1371
- adapterName: transportState.adapter.name,
1372
- parsed,
1373
- guard: opaqueBlobRecoveryGuard,
1374
- signal: upstream.signal,
1375
- }, rebuildAndRefetch);
1376
- if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
1377
- if (opaqueBlobRecovery.kind === "recovered") {
1378
- upstreamResponse = opaqueBlobRecovery.response;
1379
- continue passthroughRecovery;
1449
+ if (!configuredTransientSendBudgetExhausted()) {
1450
+ const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
1451
+ response: upstreamResponse,
1452
+ outboundBody: request.body,
1453
+ adapterName: transportState.adapter.name,
1454
+ parsed,
1455
+ guard: opaqueBlobRecoveryGuard,
1456
+ signal: upstream.signal,
1457
+ }, rebuildAndRefetch);
1458
+ if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
1459
+ if (opaqueBlobRecovery.kind === "recovered") {
1460
+ upstreamResponse = opaqueBlobRecovery.response;
1461
+ continue passthroughRecovery;
1462
+ }
1380
1463
  }
1381
1464
 
1382
1465
  const recoveryContentType = upstreamResponse.headers.get("content-type")?.toLowerCase() ?? "";
@@ -1384,6 +1467,7 @@ export async function preparePassthroughExchange(
1384
1467
  && !!upstreamResponse.body
1385
1468
  && (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && parsed.stream))
1386
1469
  && !opaqueBlobRecoveryGuard.attempted
1470
+ && !configuredTransientSendBudgetExhausted()
1387
1471
  && outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body);
1388
1472
  if (streamedFunctionOutputCandidate) {
1389
1473
  const preflightLog: RequestLogContext = { model: logCtx.model, provider: logCtx.provider };
@@ -1391,8 +1475,13 @@ export async function preparePassthroughExchange(
1391
1475
  payload => {
1392
1476
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
1393
1477
  const type = (payload as { type?: unknown }).type;
1394
- return (type === "error" || type === "response.failed" || type === "response.incomplete")
1395
- && upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
1478
+ const decryptRejection = (type === "error" || type === "response.failed" || type === "response.incomplete")
1479
+ && isEncryptedFunctionOutputRejection(JSON.stringify(payload));
1480
+ // `detail` is a real WebSocket error shape but the generic preflight failure projector
1481
+ // intentionally understands only Responses error/message fields. Preserve only this
1482
+ // exact identity so the projected 502 can enter the existing single-shot recovery.
1483
+ if (decryptRejection) preflightLog.upstreamError = ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
1484
+ return decryptRejection;
1396
1485
  }, {
1397
1486
  allowMissingContentType: !recoveryContentType && parsed.stream,
1398
1487
  replayReadErrors: true,
@@ -1400,19 +1489,21 @@ export async function preparePassthroughExchange(
1400
1489
  if (options.abortSignal?.aborted) return transportFailureResponse(options.abortSignal.reason);
1401
1490
  upstreamResponse = preflight.response;
1402
1491
  if (preflight.kind === "failed") {
1403
- const streamedOpaqueRecovery = await attemptOpaqueBlobRecovery({
1404
- response: upstreamResponse,
1405
- outboundBody: request.body,
1406
- adapterName: transportState.adapter.name,
1407
- parsed,
1408
- guard: opaqueBlobRecoveryGuard,
1409
- signal: upstream.signal,
1410
- }, rebuildAndRefetch);
1411
- if (streamedOpaqueRecovery.kind === "failed") return streamedOpaqueRecovery.response;
1412
- if (streamedOpaqueRecovery.kind === "recovered") {
1413
- resetStreamedOpaqueBlobLogContext(logCtx);
1414
- upstreamResponse = streamedOpaqueRecovery.response;
1415
- continue passthroughRecovery;
1492
+ if (!configuredTransientSendBudgetExhausted()) {
1493
+ const streamedOpaqueRecovery = await attemptOpaqueBlobRecovery({
1494
+ response: upstreamResponse,
1495
+ outboundBody: request.body,
1496
+ adapterName: transportState.adapter.name,
1497
+ parsed,
1498
+ guard: opaqueBlobRecoveryGuard,
1499
+ signal: upstream.signal,
1500
+ }, rebuildAndRefetch);
1501
+ if (streamedOpaqueRecovery.kind === "failed") return streamedOpaqueRecovery.response;
1502
+ if (streamedOpaqueRecovery.kind === "recovered") {
1503
+ resetStreamedOpaqueBlobLogContext(logCtx);
1504
+ upstreamResponse = streamedOpaqueRecovery.response;
1505
+ continue passthroughRecovery;
1506
+ }
1416
1507
  }
1417
1508
  logCtx.upstreamError = preflightLog.upstreamError;
1418
1509
  logCtx.terminalHttpStatus = preflightLog.terminalHttpStatus;
@@ -1431,6 +1522,7 @@ export async function preparePassthroughExchange(
1431
1522
  upstream.signal,
1432
1523
  );
1433
1524
  if (uploadRejectionBody !== undefined
1525
+ && !configuredTransientSendBudgetExhausted()
1434
1526
  && isTransientConsoleGoUploadRejection({
1435
1527
  status: upstreamResponse.status,
1436
1528
  errorBody: uploadRejectionBody,
@@ -1469,7 +1561,7 @@ export async function preparePassthroughExchange(
1469
1561
  requested: parsed.options.reasoning,
1470
1562
  rejectionText,
1471
1563
  });
1472
- if (downgrade) {
1564
+ if (downgrade && !configuredTransientSendBudgetExhausted()) {
1473
1565
  reasoningEffortDowngradeGuard.attempted = true;
1474
1566
  parsed.options.reasoning = downgrade.effort;
1475
1567
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
@@ -10,6 +10,8 @@ import type { ResponsesEffects } from "./response-effects";
10
10
  import type { ResponsesSendBudget } from "./request-send-budget";
11
11
  import { preparePassthroughExchange } from "./passthrough-dispatch";
12
12
  import { deliverPassthroughResponse } from "./passthrough-delivery";
13
+ import { guardDirectPassthroughBodyInactivity } from "../../lib/response-body-inactivity";
14
+ import { resolveStallTimeoutSec } from "../../stall-timeout";
13
15
  import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health";
14
16
  import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
15
17
 
@@ -36,7 +38,7 @@ export async function executePassthroughResponse(
36
38
  sendBudgetState,
37
39
  );
38
40
  if (nativeExchange instanceof Response) return nativeExchange;
39
- return await deliverPassthroughResponse(
41
+ const response = await deliverPassthroughResponse(
40
42
  requestContext,
41
43
  admissionState,
42
44
  requestState,
@@ -45,6 +47,14 @@ export async function executePassthroughResponse(
45
47
  responseEffects,
46
48
  nativeExchange,
47
49
  );
50
+ // Delivery has already classified SSE (including a missing upstream content type)
51
+ // and consumed bounded JSON/errors. Guard only its remaining direct body, outside
52
+ // the relay/lifetime wrappers so their prefetch cannot arm our inactivity clock.
53
+ return guardDirectPassthroughBodyInactivity(
54
+ response,
55
+ nativeExchange.upstream.signal,
56
+ resolveStallTimeoutSec(requestContext.config.stallTimeoutSec) * 1000,
57
+ );
48
58
  } finally {
49
59
  if (nativeHostState.lease) {
50
60
  releaseUpstreamHostAdmission(nativeHostState.lease);
@@ -1,6 +1,5 @@
1
1
  import { comboFailureDecision } from "../../combos/failover";
2
2
  import { readBoundedResponseBody } from "../../lib/bounded-body";
3
- import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "../request-decompress";
4
3
  import { finishRequestAttempt, type RequestLogContext } from "../request-log";
5
4
  import { linkRequestSessionLane } from "../request-log-conversation";
6
5
  import type { OcxConfig } from "../../types";
@@ -129,6 +128,7 @@ export async function handleResponsesWithPolicyFallback(
129
128
  const runCore = deps.runCore ?? handleResponsesCore;
130
129
  let requestBodyReadNotified = false;
131
130
  let storedPool401ReplayDispatched = false;
131
+ let rawBody: Record<string, unknown> | null = null;
132
132
  const coreOptions: CoreOptions = {
133
133
  ...options,
134
134
  openAiSidecarAuth: options.openAiSidecarAuth === undefined
@@ -144,23 +144,15 @@ export async function handleResponsesWithPolicyFallback(
144
144
  options.onRequestBodyRead?.();
145
145
  },
146
146
  } : {}),
147
+ onRequestBodyParsed: body => {
148
+ options.onRequestBodyParsed?.(body);
149
+ if (body && typeof body === "object" && !Array.isArray(body)) rawBody = body as Record<string, unknown>;
150
+ },
147
151
  onStoredPool401ReplayDispatched: () => {
148
152
  storedPool401ReplayDispatched = true;
149
153
  options.onStoredPool401ReplayDispatched?.();
150
154
  },
151
155
  };
152
- let rawBody: Record<string, unknown> | null = null;
153
- try {
154
- const parsed = await readJsonRequestBody(
155
- req.clone(),
156
- undefined,
157
- resolveInboundBodyLimitBytes(config.maxInboundBodyBytes),
158
- );
159
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) rawBody = parsed as Record<string, unknown>;
160
- } catch {
161
- // Core owns the client-facing parse/decompression error.
162
- }
163
-
164
156
  let response: Response;
165
157
  try {
166
158
  response = await runCore(req, config, logCtx, coreOptions);