@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
@@ -32,6 +32,7 @@ import {
32
32
  resolveCodexModelEntitlements,
33
33
  invalidateCodexModelEntitlementsForAccount,
34
34
  entitledCodexAccountIdsForModel,
35
+ recordCodexModelDenialEvidence,
35
36
  } from "../../codex/model-entitlements";
36
37
  import type { TransientSendBudget } from "../../lib/upstream-retry";
37
38
  import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
@@ -161,43 +162,93 @@ export function normalizeCodexUnsupportedModelDetail(value: string): string {
161
162
  }
162
163
 
163
164
 
164
- export function isAllowListedCodexAccountModel400(
165
+ /**
166
+ * The model id an authenticated Codex refusal names, or `undefined` when the body is not that
167
+ * refusal.
168
+ *
169
+ * Extracted rather than string-compared so the caller can learn WHICH model was refused. The
170
+ * route and the wire can legitimately disagree: `applyCodexAccountGatedWireNormalization`
171
+ * rewrites `gpt-daybreak-blue-latest` to `gpt-5.6-sol` before dispatch, so upstream names the
172
+ * model it was actually sent. Building the expected sentence from `route.modelId` alone made
173
+ * that comparison fail for the one model that is still account-gated, which silently disabled
174
+ * both the alternate-account retry and the same-account ladder built for exactly that case.
175
+ *
176
+ * The envelope is unchanged and stays exact: a top-level `detail` string, whitespace-collapsed
177
+ * and case-folded, matching the whole sentence with nothing before or after it. No prose is
178
+ * inferred and no other 400 shape is admitted, because a 400 is also what a malformed request
179
+ * earns and that must never read as an entitlement fact.
180
+ */
181
+ export function codexUnsupportedModelFromDetail(
165
182
  status: number,
166
183
  bodyText: string,
167
- modelId: string,
168
- ): boolean {
169
- if (status !== 400) return false;
184
+ ): string | undefined {
185
+ if (status !== 400) return undefined;
170
186
  try {
171
187
  const payload = JSON.parse(bodyText) as unknown;
172
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
188
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
173
189
  const detail = (payload as { detail?: unknown }).detail;
174
- if (typeof detail !== "string") return false;
175
- const expected = `The '${modelId}' model is not supported when using Codex with a ChatGPT account.`;
176
- return normalizeCodexUnsupportedModelDetail(detail)
177
- === normalizeCodexUnsupportedModelDetail(expected);
190
+ if (typeof detail !== "string") return undefined;
191
+ const matched = /^the '([^']{1,256})' model is not supported when using codex with a chatgpt account\.$/u
192
+ .exec(normalizeCodexUnsupportedModelDetail(detail));
193
+ return matched?.[1];
178
194
  } catch {
179
- return false;
195
+ return undefined;
180
196
  }
181
197
  }
182
198
 
183
199
 
184
- export async function shouldRetryCodexPoolAccountModel400(
200
+ /**
201
+ * The refused model id when this response is the exact unsupported-model refusal for this
202
+ * request, read from a bounded clone.
203
+ *
204
+ * Same admission rules {@link shouldRetryCodexPoolAccountModel400} always applied, which is now
205
+ * a predicate over this: a truncated or non-display-safe body proves nothing and is refused.
206
+ * Returning the id lets the caller record the denial against the model upstream actually named.
207
+ */
208
+ export async function codexPoolAccountModel400Denial(
185
209
  response: Response,
186
210
  modelId: string,
187
211
  signal?: AbortSignal,
188
- ): Promise<boolean> {
189
- if (response.status !== 400) return false;
212
+ wireModelId?: string,
213
+ ): Promise<string | undefined> {
214
+ if (response.status !== 400) return undefined;
190
215
  try {
191
216
  const body = await readBoundedResponseBody(response.clone(), { signal });
192
- return body.displaySafe
193
- && !body.truncated
194
- && isAllowListedCodexAccountModel400(response.status, body.text, modelId);
217
+ if (!body.displaySafe || body.truncated) return undefined;
218
+ return isAllowListedCodexAccountModel400(response.status, body.text, modelId, wireModelId)
219
+ ? codexUnsupportedModelFromDetail(response.status, body.text)
220
+ : undefined;
195
221
  } catch {
196
- return false;
222
+ return undefined;
197
223
  }
198
224
  }
199
225
 
200
226
 
227
+ export function isAllowListedCodexAccountModel400(
228
+ status: number,
229
+ bodyText: string,
230
+ modelId: string,
231
+ wireModelId?: string,
232
+ ): boolean {
233
+ const refused = codexUnsupportedModelFromDetail(status, bodyText);
234
+ if (refused === undefined) return false;
235
+ return [modelId, wireModelId].some(candidate => (
236
+ candidate !== undefined
237
+ && refused === normalizeCodexUnsupportedModelDetail(candidate)
238
+ ));
239
+ }
240
+
241
+
242
+ export async function shouldRetryCodexPoolAccountModel400(
243
+ response: Response,
244
+ modelId: string,
245
+ signal?: AbortSignal,
246
+ wireModelId?: string,
247
+ ): Promise<boolean> {
248
+ return await codexPoolAccountModel400Denial(response, modelId, signal, wireModelId) !== undefined;
249
+ }
250
+
251
+
201
252
  /** Pre-stream quota/billing rejections that warrant one alternate-account attempt (#584). */
202
253
  export function codexQuotaFailureMessage(body: string): string | undefined {
203
254
  try {
@@ -277,7 +328,8 @@ export interface CodexPoolAccountRetryArgs {
277
328
  /** Sanitized caller input, before any selected Pool credential was materialized. */
278
329
  callerAuthHeaders: Headers;
279
330
  config: OcxConfig;
280
- route: { providerName: string; modelId: string; provider: OcxProviderConfig };
331
+ /** Actual routed result narrowed to the fields this retry consumes. */
332
+ route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
281
333
  parsed: OcxParsedRequest;
282
334
  logCtx: RequestLogContext;
283
335
  options: {
@@ -630,7 +682,7 @@ export async function retryCodexPoolOnAlternateAccount(
630
682
  "pool",
631
683
  );
632
684
  const retryAdapter = resolveAdapter(
633
- resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
685
+ resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
634
686
  config.cacheRetention,
635
687
  route.providerName,
636
688
  );
@@ -778,15 +830,29 @@ export async function retryCodexPoolOnAlternateAccount(
778
830
  }
779
831
  retrySendCount += 1;
780
832
  args.onResponse?.(upstreamResponse, retryAuthCtx, request);
833
+ // The alternate account can refuse the same model, and that refusal is evidence about the
834
+ // account that produced it. Read BEFORE the ladder's own break, so the ordinary
835
+ // single-retry path -- every flagship model, which is the #4906 case -- records it too
836
+ // rather than only the gated ladder below. Without this the pool learns nothing from a
837
+ // refusal and the next request repeats the same selection.
838
+ const retryModelDenial = await codexPoolAccountModel400Denial(
839
+ upstreamResponse,
840
+ route.modelId,
841
+ options.abortSignal,
842
+ parsed.modelId,
843
+ );
844
+ if (retryModelDenial !== undefined) {
845
+ recordCodexModelDenialEvidence(
846
+ retryAuthCtx.accountId,
847
+ retryModelDenial,
848
+ retryAuthCtx.kind === "pool" ? retryAuthCtx.generation : undefined,
849
+ );
850
+ }
781
851
  if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
782
852
  // Caller-owned main is an alternate-account replay and can never enter the bounded
783
853
  // same-stored-account 400 loop above. Keep that invariant explicit for the account-id reads.
784
854
  if (retryAuthCtx.kind === "main") break;
785
- if (!await shouldRetryCodexPoolAccountModel400(
786
- upstreamResponse,
787
- route.modelId,
788
- options.abortSignal,
789
- )) break;
855
+ if (retryModelDenial === undefined) break;
790
856
  invalidateCodexModelEntitlementsForAccount(retryAuthCtx.accountId);
791
857
  let refreshed: Awaited<ReturnType<typeof resolveCodexModelEntitlements>>;
792
858
  try {
@@ -29,7 +29,6 @@ import {
29
29
  import { formatErrorResponse } from "../../bridge";
30
30
  import {
31
31
  expandPreviousResponseInput,
32
- previousResponseScopeMismatch,
33
32
  previousResponseReplayFailure,
34
33
  previousResponseProviderState,
35
34
  } from "../../responses/state";
@@ -201,11 +200,12 @@ export async function executeComboResponses(
201
200
  // imageInput is disabled (and so targets see the full replayed input).
202
201
  const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
203
202
  const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
204
- const scopeMismatch = previousResponseScopeMismatch(body);
205
- if (scopeMismatch) {
206
- console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
203
+ const replayFailure = previousResponseReplayFailure(body);
204
+ if (replayFailure?.reason === "scope_mismatch") {
205
+ console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
207
206
  }
208
- if (previousResponseReplayFailure(body)) {
207
+ // Local replay failures require full client replay.
208
+ if (replayFailure) {
209
209
  return formatErrorResponse(
210
210
  400,
211
211
  "previous_response_not_found",
@@ -235,7 +235,7 @@ export async function executeComboResponses(
235
235
  sourceBody: body,
236
236
  previousResponseInputExpanded: body !== rawBody
237
237
  && typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
238
- providerContinuation: !scopeMismatch && body !== rawBody && requestedPreviousId
238
+ providerContinuation: body !== rawBody && requestedPreviousId
239
239
  ? previousResponseProviderState(requestedPreviousId)
240
240
  : undefined,
241
241
  recoveredPlaintext: false,
@@ -521,7 +521,7 @@ export async function executeComboResponses(
521
521
  // The live config can change while the child is streaming. Never retain credentials.
522
522
  const currentCombo = getCombo(config, comboId);
523
523
  const provider = config.providers[completedTarget.provider];
524
- if (Object.hasOwn(config.providers, completedTarget.provider)
524
+ if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
525
525
  && provider && provider.disabled !== true
526
526
  && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
527
527
  rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
@@ -13,7 +13,6 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
13
13
  import type { RequestLogContext } from "../request-log";
14
14
  import type { HandleResponsesOptions } from "./core-options";
15
15
  import { prepareEffortNormalization } from "../effort-policy";
16
- import { providerModelResponsesUpstreamStreaming } from "../../providers/registry";
17
16
  import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
18
17
  import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
19
18
  import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
@@ -80,6 +79,7 @@ export function canPassThroughEncryptedV2AgentTask(
80
79
  route.modelId,
81
80
  provider,
82
81
  inboundWire,
82
+ route.staticPolicy,
83
83
  ).adapter === "openai-responses";
84
84
  }
85
85
 
@@ -126,6 +126,8 @@ export async function applyFinalRouteRequestNormalization(args: {
126
126
  const responseModelId = parsed.modelId;
127
127
  const preserveAnthropicResponseModel = route.providerName === "anthropic"
128
128
  || route.provider.adapter === "anthropic";
129
+ const finalSelectedModelId = route.modelId;
130
+ const virtualModel = applyOpenAiVirtualModel(parsed, route, logCtx, inboundWire);
129
131
 
130
132
  // Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
131
133
  if (route.modelId !== parsed.modelId) {
@@ -136,17 +138,21 @@ export async function applyFinalRouteRequestNormalization(args: {
136
138
  }
137
139
  // Transport-neutral reliability policy (#875): applies to any Responses
138
140
  // upstream whose final adapter is openai-responses, not only WS turns.
139
- const responsesUpstreamStreaming = providerModelResponsesUpstreamStreaming(
141
+ const responsesUpstreamStreaming = route.staticPolicy.model.responsesUpstreamStreaming;
142
+
143
+ // Preserve the routed destination for Go recognition, then settle the wire before
144
+ // deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
145
+ const routedProvider = route.provider;
146
+ const wireProvider = resolveWireProtocolOverride(
140
147
  route.providerName,
141
- route.provider,
142
148
  route.modelId,
149
+ routedProvider,
150
+ inboundWire,
151
+ route.staticPolicy,
143
152
  );
144
-
145
- // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
146
- // this request will actually use (#404).
147
- route.provider = resolveOpenCodeGoTransport(route.provider,
148
- args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req));
149
- route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
153
+ route.provider = resolveOpenCodeGoTransport(wireProvider,
154
+ args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req),
155
+ routedProvider);
150
156
  parsed._plaintextV2AgentMessages = shouldPreparePlaintextV2AgentMessages({
151
157
  enabled: config.plaintextV2AgentMessages === true,
152
158
  inboundWire,
@@ -161,7 +167,7 @@ export async function applyFinalRouteRequestNormalization(args: {
161
167
  || (!summary && route.provider.showThinkingSummary !== true);
162
168
  }
163
169
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
164
- logCtx.model = route.modelId;
170
+ logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
165
171
  logCtx.provider = route.providerName;
166
172
  logCtx.providerAdapter = route.provider.adapter;
167
173
  logCtx.routeDecision = route.routeDecision;
@@ -187,11 +193,6 @@ export async function applyFinalRouteRequestNormalization(args: {
187
193
  (parsed._rawBody as Record<string, unknown>).store = false;
188
194
  }
189
195
 
190
- // Final selected model before virtual wire-model rewriting (Pro aliases).
191
- const finalSelectedModelId = route.modelId;
192
-
193
- // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
194
- applyOpenAiVirtualModel(parsed, route, logCtx);
195
196
  if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
196
197
  logCtx.resolvedModel = route.modelId;
197
198
  logCtx.preserveResolvedModelFromRoute = true;
@@ -88,6 +88,7 @@ export function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
88
88
  try {
89
89
  const payload = JSON.parse(bodyText) as unknown;
90
90
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
91
+ if (upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
91
92
  const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
92
93
  if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
93
94
  if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
@@ -11,6 +11,7 @@ import type { NativeMainRefreshDependencies } from "../../codex/main-account";
11
11
  import type { InboundWire } from "../../providers/registry";
12
12
  import type { ExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
13
13
  import type { CallerDirectAuth } from "../../providers/caller-authorization";
14
+ import type { CompactionRoutingOverride } from "./compaction-routing";
14
15
  import type { TranslatorBudget } from "../../lib/translator-budget";
15
16
  import type { TransientSendBudget } from "../../lib/upstream-retry";
16
17
  import type { RequestLogContext } from "../request-log";
@@ -51,6 +52,8 @@ export interface HandleResponsesOptions {
51
52
  admission?: DataPlaneAdmission;
52
53
  /** Called at most once after the complete client body is read and accepted for dispatch. */
53
54
  onRequestBodyRead?: () => void;
55
+ /** Internal handoff for retry wrappers that must reuse the already-accounted request body. */
56
+ onRequestBodyParsed?: (body: unknown) => void;
54
57
  forceEmptyResponseId?: boolean;
55
58
  /** Internal, connection-owned control channel; never reconstructed from headers. */
56
59
  nativeControl?: NativeResponseControl;
@@ -105,6 +108,7 @@ export interface HandleResponsesOptions {
105
108
  callerDirectAuth?: CallerDirectAuth | null;
106
109
  /** Internal recursion guard; callers outside this module must not set it. */
107
110
  comboAttempt?: boolean;
111
+ compactionRoutingOverride?: CompactionRoutingOverride | null;
108
112
  /** Internal combo handoff for one parent-validated continuation snapshot. */
109
113
  comboReplaySnapshot?: {
110
114
  sourceBody: unknown;
@@ -98,6 +98,19 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
98
98
 
99
99
 
100
100
  export function looksLikeBackendCiphertext(payload: string): boolean {
101
+ // Unknown replay history has no authenticity proof. Require the key-independent Fernet wire
102
+ // structure instead of granting ciphertext authority to any long base64-like model output.
103
+ // Proven backend bytes still remain opaque and byte-identical; malformed/plaintext slots are
104
+ // lowered by the compatibility path below rather than poisoning every later native replay.
105
+ return isStructurallyValidFernetToken(payload);
106
+ }
107
+
108
+ /**
109
+ * Pre-route compatibility keeps an encoded-looking unknown slot opaque until the destination is
110
+ * known. A routed destination strips that slot instead of exposing possible truncated ciphertext;
111
+ * the canonical backend later applies the stricter structural classifier.
112
+ */
113
+ function looksLikeUnknownOpaqueSlot(payload: string): boolean {
101
114
  return payload.length >= 64 && /^[A-Za-z0-9+/=_-]+$/.test(payload);
102
115
  }
103
116
 
@@ -480,7 +493,10 @@ function contentWithoutCiphertext(content: unknown[]): unknown[] {
480
493
 
481
494
 
482
495
 
483
- export function sanitizeEncryptedContentInPlace(input: unknown): number {
496
+ export function sanitizeEncryptedContentInPlace(
497
+ input: unknown,
498
+ options: { preserveUnknownOpaqueSlots?: boolean } = {},
499
+ ): number {
484
500
  if (!Array.isArray(input)) return 0;
485
501
  let rewritten = 0;
486
502
  const protectedFragments = new WeakSet<object>();
@@ -512,7 +528,9 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
512
528
  && typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
513
529
  ) {
514
530
  const payload = (child as { encrypted_content: string }).encrypted_content;
515
- if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload)) {
531
+ const preserveUnknown = options.preserveUnknownOpaqueSlots === true
532
+ && looksLikeUnknownOpaqueSlot(payload);
533
+ if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload) && !preserveUnknown) {
516
534
  const parts = encryptedSlotParts(payload);
517
535
  frame.node.splice(frame.index, 1, ...parts);
518
536
  rewritten += 1;
@@ -11,6 +11,7 @@ import type { WsData } from "../ws-bridge";
11
11
  import { waitForProviderRequestSlot } from "../../providers/request-pacing";
12
12
  import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
13
13
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
14
+ import { configuredOutboundFetch } from "../../lib/proxy-env";
14
15
 
15
16
  export { withUpstreamHttpVersion };
16
17
 
@@ -44,6 +45,31 @@ export function safeOriginLabel(url: string): string {
44
45
  }
45
46
  }
46
47
 
48
+ /**
49
+ * Check whether a target host should bypass Bun's keep-alive pool reuse.
50
+ * Configured via the `OCX_FRESH_CONNECTION_HOSTS` environment variable (comma-separated).
51
+ */
52
+ export function wantsFreshConnection(
53
+ input: Parameters<typeof globalThis.fetch>[0],
54
+ hostsEnv = process.env.OCX_FRESH_CONNECTION_HOSTS,
55
+ ): boolean {
56
+ if (!hostsEnv) return false;
57
+ try {
58
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
59
+ const host = new URL(rawUrl).hostname.toLowerCase();
60
+ const targets = hostsEnv
61
+ .split(",")
62
+ .map(h => h.trim().toLowerCase().replace(/^\.+/, ""))
63
+ .filter(Boolean);
64
+ for (const target of targets) {
65
+ if (host === target || host.endsWith(`.${target}`)) return true;
66
+ }
67
+ } catch {
68
+ /* unparseable target URL keeps default connection behavior */
69
+ }
70
+ return false;
71
+ }
72
+
47
73
 
48
74
 
49
75
  export interface PaceAwareFetch {
@@ -53,6 +79,38 @@ export interface PaceAwareFetch {
53
79
 
54
80
  export type ProviderFetch = typeof globalThis.fetch & PaceAwareFetch;
55
81
 
82
+ /**
83
+ * Apply the physical-send connection policy to whichever fetch actually performs the send.
84
+ *
85
+ * The executor `providerFetch` builds is not the only physical boundary. A `dispatchOverride`
86
+ * that revalidates credentials re-reads `route.provider.fetch` at send time -- reselection can
87
+ * install a different provider transport after this wrapper was constructed -- and then calls
88
+ * that fetch directly instead of the supplied executor. Keeping the policy inside the executor
89
+ * alone therefore left every provider-scoped transport reusing a pooled socket for a host the
90
+ * operator had named in `OCX_FRESH_CONNECTION_HOSTS` (#4992). The policy belongs around the
91
+ * selected fetch so it follows the selection rather than the construction.
92
+ *
93
+ * Idempotent on purpose: an override that hands the send back to the supplied executor passes
94
+ * through here twice, and both passes derive the same headers from the same wire URL.
95
+ */
96
+ export function sendWithConnectionPolicy(
97
+ physicalFetch: typeof globalThis.fetch,
98
+ input: Parameters<typeof globalThis.fetch>[0],
99
+ init?: RequestInit,
100
+ ): Promise<Response> {
101
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
102
+ const fresh = wantsFreshConnection(input);
103
+ if (fresh) {
104
+ headers.set("Connection", "close");
105
+ }
106
+ return physicalFetch(input, {
107
+ ...init,
108
+ headers,
109
+ redirect: "manual",
110
+ ...(fresh ? { keepalive: false } : {}),
111
+ });
112
+ }
113
+
56
114
  export interface ProviderFetchOptions {
57
115
  nativeControl?: NativeResponseControl;
58
116
  providerName?: string;
@@ -72,7 +130,11 @@ export function providerFetch(
72
130
  runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
73
131
  options: ProviderFetchOptions = {},
74
132
  ): ProviderFetch {
75
- const base = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
133
+ const configuredFetch = Object.assign(
134
+ (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => configuredOutboundFetch(input, init),
135
+ { preconnect: globalThis.fetch.preconnect?.bind(globalThis.fetch) },
136
+ ) as typeof globalThis.fetch;
137
+ const base = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? configuredFetch;
76
138
  const preconnect = (...args: Parameters<typeof globalThis.fetch.preconnect>): void => {
77
139
  base.preconnect?.(...args);
78
140
  };
@@ -80,11 +142,15 @@ export function providerFetch(
80
142
  // Return the original 3xx so the response owner retains its retry/health/relay contract.
81
143
  const dispatch = Object.assign(
82
144
  (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
83
- base(input, { ...init, redirect: "manual" }),
145
+ sendWithConnectionPolicy(base, input, init),
84
146
  { preconnect },
85
147
  ) as typeof globalThis.fetch;
86
148
  const httpFetch = Object.assign(
87
149
  async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
150
+ // The hook inspects the outgoing headers and refuses the send by throwing; it is not a
151
+ // mutator, and the copy it receives is deliberately not threaded onward. `Connection`
152
+ // is decided inside `dispatch`, which runs after this, so the fresh-connection policy
153
+ // wins regardless of what any caller or hook put in the header.
88
154
  options.beforeDispatch?.(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)));
89
155
  const dispatchInit = { ...withUpstreamHttpVersion(input, init, provider), timeout: 0 };
90
156
  return options.dispatchOverride
@@ -30,21 +30,18 @@ import type { ResponsesTerminalStatus } from "../../bridge";
30
30
  import { isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./ws-upstream";
31
31
  import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-model-fallback";
32
32
  import { recordCodexUpstreamOutcome } from "../../codex/routing";
33
- import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant } from "../../codex/auth-context";
33
+ import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant, releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
34
34
  import { consumeComboFailure } from "./core-combo-failure";
35
35
  import { readDisplaySafeErrorText } from "./core-errors";
36
36
  import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
37
37
  import { formatPassthroughUpstreamError } from "./passthrough-error";
38
- import {
39
- providerModelResponsesTerminalRepair,
40
- providerModelResponsesUpstreamStreaming,
41
- } from "../../providers/registry";
42
38
  import {
43
39
  resolvePassthroughWebSearchBridgeAuth,
44
40
  planPassthroughWebSearchBridge,
45
41
  createPassthroughWebSearchBridgeStream,
46
42
  createPassthroughWebSearchBridgeExecutor,
47
43
  } from "../../web-search/passthrough-bridge";
44
+ import { bridgeSearchReplayScope } from "../../responses/bridge-search-replay-cache";
48
45
  import { fetchWithHeaderTimeout, providerFetch } from "./fetch-helpers";
49
46
  import { providerApiKeySelectionIsCurrent } from "../../providers/api-key-selection";
50
47
  import { requiresVisionPreprocessing } from "../../vision";
@@ -82,7 +79,10 @@ import {
82
79
  import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
83
80
  import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
84
81
  import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
85
- import { createGrokResponsesControlFrameBlockRewrite } from "../grok-responses-control-frame";
82
+ import {
83
+ createGrokResponsesControlFrameBlockRewrite,
84
+ createGrokResponsesTimestampBlockRewrite,
85
+ } from "../grok-responses-control-frame";
86
86
  import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
87
87
  import {
88
88
  createPlaintextV2AgentMessageCallRestoreRewrite,
@@ -98,6 +98,24 @@ import {
98
98
  normalizeDefaultNamespaceInJson,
99
99
  } from "../responses-undeclared-tool-guard";
100
100
  import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
101
+
102
+ /**
103
+ * Platform override for the two relay-path policy calls below. Tests only.
104
+ *
105
+ * The eager relay is reachable only on win32 and darwin, so a Linux shard cannot exercise it
106
+ * without claiming to be one of them. Overwriting `process.platform` globally does that, and a
107
+ * great deal more: every filesystem, ACL and state-directory decision in the process follows it,
108
+ * and the spend-ledger owner lowercases its home on win32, which on a case-sensitive filesystem
109
+ * names a DIFFERENT directory. A row that did that stopped being able to reserve its send and
110
+ * delivered no terminal at all, reporting as a relay defect. This narrows the claim to the two
111
+ * calls that actually choose the relay path.
112
+ */
113
+ let relayPlatformForTests: NodeJS.Platform | undefined;
114
+
115
+ /** Internal test contract, not operator configuration: no config key reaches this. */
116
+ export function setRelayPlatformForTests(platform: NodeJS.Platform | undefined): void {
117
+ relayPlatformForTests = platform;
118
+ }
101
119
  import { linkAbortSignal, UPSTREAM_JSON_BODY_READ_OPTIONS } from "./core-lifetime";
102
120
  import { registerTurn, unregisterTurn, trackStreamLifetime } from "../lifecycle";
103
121
  import { relaySseEagerBounded } from "../relay-eager";
@@ -339,11 +357,7 @@ export async function deliverPassthroughResponse(
339
357
  // before relay starts. Waiting for SSE completion would retain request state across the whole
340
358
  // stream; a later body failure does not undo that this destination accepted and served the turn.
341
359
  commitReasoningReplayServingRoute(nativeExchange.request.headers);
342
- const terminalRepairPolicy = providerModelResponsesTerminalRepair(
343
- route.providerName,
344
- route.provider,
345
- route.modelId,
346
- );
360
+ const terminalRepairPolicy = route.staticPolicy.model.responsesTerminalRepair;
347
361
  // #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
348
362
  // and this branch relays that declaration on the assumption the destination executes it.
349
363
  // A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
@@ -402,12 +416,20 @@ export async function deliverPassthroughResponse(
402
416
  describeImages: requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName),
403
417
  sidecar: config.webSearchSidecar,
404
418
  }),
419
+ // Scope the executed-search memo to this exact upstream (#4587). The Responses adapter
420
+ // derives the same scope from the same base URL before the NEXT turn is dispatched, so
421
+ // a replayed hosted cell can be turned back into the destination's own call and result.
422
+ destinationScope: bridgeSearchReplayScope(route.provider.baseUrl),
405
423
  // Appending a search result can push the continuation past the ceiling the first leg
406
424
  // was admitted under, so the same limit is re-applied before every later send.
407
425
  checkOutboundBody: (continuationBody: string) => {
408
426
  const result = checkOutboundBodySize(continuationBody, config.maxUpstreamBodyBytes);
409
427
  return result.admitted ? undefined : describeOutboundBodyRefusal(result);
410
428
  },
429
+ // Resolution can acquire the account's sole cooldown-recovery probe before the routed
430
+ // provider reveals whether it will request search. Hand an unused lease back on every
431
+ // terminal path; after an executed search, the outcome recorder has already settled it.
432
+ onFinalize: () => releaseCodexAuthContextProbeLease(openAiSidecar?.authContext),
411
433
  signal: upstream.signal,
412
434
  })
413
435
  : upstreamResponse.body;
@@ -489,6 +511,9 @@ export async function deliverPassthroughResponse(
489
511
  grokClientCompatibilityEnabled
490
512
  ? createGrokResponsesControlFrameBlockRewrite()
491
513
  : undefined,
514
+ grokClientCompatibilityEnabled
515
+ ? createGrokResponsesTimestampBlockRewrite()
516
+ : undefined,
492
517
  grokClientCompatibilityEnabled
493
518
  ? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
494
519
  : undefined,
@@ -520,12 +545,13 @@ export async function deliverPassthroughResponse(
520
545
  ? composeSseBlockRewrites(...blockRewrites)
521
546
  : undefined;
522
547
  const needsClientRewrite = clientBlockRewrite !== undefined;
548
+ const relayPlatform = relayPlatformForTests ?? process.platform;
523
549
  // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
524
550
  // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
525
551
  // lost). The eager single reader applies the same rewrites inline.
526
- const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
552
+ const win32EagerRewrite = isWin32EagerRewrite(relayPlatform, needsClientRewrite);
527
553
  const eagerPath = selectEagerPath(
528
- process.platform,
554
+ relayPlatform,
529
555
  needsClientRewrite,
530
556
  config.streamMode ?? "auto",
531
557
  );
@@ -796,7 +822,7 @@ export async function deliverPassthroughResponse(
796
822
  // stream that never closes. Non-streaming clients keep the plain JSON.
797
823
  if (clientRequestedStream === true
798
824
  && options.inboundTransport !== "websocket"
799
- && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
825
+ && route.staticPolicy.model.responsesUpstreamStreaming === false
800
826
  && route.provider.adapter === "openai-responses") {
801
827
  let completed: Record<string, unknown> | undefined;
802
828
  try {
@@ -843,7 +869,7 @@ export async function deliverPassthroughResponse(
843
869
  // WS turns reframe this JSON into events in the bridge, which is the
844
870
  // other relay-free path — normalize ids so both bounded-JSON paths agree.
845
871
  const outboundJson = options.inboundTransport === "websocket"
846
- && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
872
+ && route.staticPolicy.model.responsesUpstreamStreaming === false
847
873
  && hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
848
874
  ? (() => {
849
875
  try {