@bitkyc08/opencodex 2.58.0 → 2.59.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 (188) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  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 +1 -1
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/cursor/catalog.ts +11 -0
  10. package/src/adapters/cursor/effort-map.ts +16 -2
  11. package/src/adapters/cursor/envelope-echo.ts +55 -2
  12. package/src/adapters/cursor/message-mapper.ts +3 -2
  13. package/src/adapters/cursor/protobuf-request.ts +8 -5
  14. package/src/adapters/cursor/request-builder.ts +14 -3
  15. package/src/adapters/cursor/thread-continuity.ts +105 -31
  16. package/src/adapters/cursor/tool-guidance.ts +5 -4
  17. package/src/adapters/cursor.ts +42 -1
  18. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  19. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  20. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  21. package/src/adapters/devin.ts +75 -13
  22. package/src/adapters/google-antigravity-wire.ts +29 -2
  23. package/src/adapters/google-http.ts +8 -1
  24. package/src/adapters/google.ts +23 -4
  25. package/src/adapters/openai-chat/response-events.ts +61 -0
  26. package/src/adapters/openai-chat.ts +5 -10
  27. package/src/adapters/openai-responses/passthrough.ts +10 -1
  28. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  29. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  30. package/src/adapters/responses-tool-schema.ts +76 -46
  31. package/src/adapters/run-turn-queue.ts +17 -4
  32. package/src/bridge/response-json.ts +1 -1
  33. package/src/bridge/sse.ts +165 -24
  34. package/src/claude/context-windows.ts +22 -0
  35. package/src/claude/outbound.ts +35 -4
  36. package/src/cli/account-api.ts +4 -3
  37. package/src/cli/account-extended.ts +22 -2
  38. package/src/cli/account-orca-import.ts +63 -0
  39. package/src/cli/account.ts +32 -4
  40. package/src/cli/capabilities.ts +40 -0
  41. package/src/cli/claude.ts +29 -1
  42. package/src/cli/codex-cli-update.ts +97 -2
  43. package/src/cli/dispatch.ts +54 -0
  44. package/src/cli/doctor.ts +197 -2
  45. package/src/cli/help.ts +4 -1
  46. package/src/cli/index.ts +88 -20
  47. package/src/cli/models-runtime.ts +33 -4
  48. package/src/cli/registry.ts +11 -1
  49. package/src/cli/runtime-api.ts +44 -0
  50. package/src/cli/start-args.ts +94 -0
  51. package/src/cli/system-command.ts +2 -0
  52. package/src/client/machine-api.ts +4 -3
  53. package/src/client/machine-listener.ts +14 -1
  54. package/src/clients/config-export/constants.ts +2 -3
  55. package/src/clients/config-export.ts +5 -5
  56. package/src/codex/account-store.ts +81 -5
  57. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  58. package/src/codex/auth-api/routes.ts +17 -2
  59. package/src/codex/auth-context.ts +16 -12
  60. package/src/codex/catalog/build-entries.ts +25 -4
  61. package/src/codex/catalog/derive-entry.ts +8 -1
  62. package/src/codex/catalog/effort.ts +10 -6
  63. package/src/codex/catalog/gather-capture.ts +1 -0
  64. package/src/codex/catalog/model-hints.ts +37 -5
  65. package/src/codex/catalog/parsing.ts +83 -5
  66. package/src/codex/catalog/reserve-warn.ts +96 -0
  67. package/src/codex/catalog/retained-sync.ts +19 -0
  68. package/src/codex/catalog/routed-gather.ts +42 -3
  69. package/src/codex/cli-installation-identity.ts +210 -0
  70. package/src/codex/cli-installation-targets.ts +158 -0
  71. package/src/codex/convergence.ts +5 -0
  72. package/src/codex/history-provider.ts +4 -1
  73. package/src/codex/history-state-open.ts +105 -0
  74. package/src/codex/inject/config-toml.ts +44 -2
  75. package/src/codex/inject.ts +3 -2
  76. package/src/codex/lineage.ts +83 -32
  77. package/src/codex/loopback-target.ts +31 -0
  78. package/src/codex/main-account-hard-lock.ts +2 -1
  79. package/src/codex/main-account.ts +10 -3
  80. package/src/codex/main-device-reauth.ts +17 -9
  81. package/src/codex/model-entitlements.ts +60 -1
  82. package/src/codex/observed-model-denials.ts +137 -0
  83. package/src/codex/orca-auth-source.ts +94 -0
  84. package/src/codex/orca-import.ts +219 -0
  85. package/src/codex/prompt-text-probe.ts +282 -12
  86. package/src/codex/quota-401-recovery.ts +12 -0
  87. package/src/codex/quota-types.ts +65 -0
  88. package/src/codex/quota.ts +24 -19
  89. package/src/codex/routing/cooldown-math.ts +8 -47
  90. package/src/codex/routing/pin-drain.ts +57 -0
  91. package/src/codex/routing.ts +13 -15
  92. package/src/codex/subagent-model-fallback.ts +94 -0
  93. package/src/codex/windows-installation-files.ts +224 -0
  94. package/src/combos/failover.ts +122 -5
  95. package/src/config/diagnostics.ts +21 -0
  96. package/src/config/load-degrade.ts +15 -0
  97. package/src/config/pending-teardown.ts +8 -0
  98. package/src/config/process-state.ts +36 -3
  99. package/src/config/provider-relative-send-path.ts +16 -0
  100. package/src/config/proxy-env.ts +23 -5
  101. package/src/config/schema/config-schema.ts +21 -0
  102. package/src/config/schema/leaf-validators.ts +64 -17
  103. package/src/generated/compatibility-version.json +235 -163
  104. package/src/generated/model-metadata.ts +1 -1
  105. package/src/lib/bounded-body.ts +4 -2
  106. package/src/lib/destination-policy.ts +48 -6
  107. package/src/lib/errors.ts +3 -15
  108. package/src/lib/local-destinations.ts +32 -5
  109. package/src/lib/provider-outbound.ts +3 -3
  110. package/src/lib/proxy-env.ts +70 -3
  111. package/src/lib/request-execution-budget.ts +11 -3
  112. package/src/lib/response-body-inactivity.ts +193 -0
  113. package/src/lib/retry-delay.ts +69 -0
  114. package/src/lib/socks5-fetch.ts +631 -0
  115. package/src/lib/spend-reservation-ledger.ts +115 -9
  116. package/src/lib/workflow-budget.ts +145 -8
  117. package/src/oauth/account-quota-rank.ts +72 -15
  118. package/src/oauth/generic-account-failover.ts +40 -27
  119. package/src/oauth/orcarouter.ts +15 -2
  120. package/src/oauth/store.ts +8 -0
  121. package/src/providers/codex-capacity.ts +9 -0
  122. package/src/providers/devin-provider-merge-migration.ts +33 -12
  123. package/src/providers/free-directory.ts +20 -2
  124. package/src/providers/key-failover.ts +261 -7
  125. package/src/providers/model-rename-migration.ts +1 -0
  126. package/src/providers/openai-sidecar.ts +4 -0
  127. package/src/providers/opencode-go-transport.ts +14 -5
  128. package/src/providers/quota/report-cache.ts +3 -0
  129. package/src/providers/registry/entries-extended.ts +96 -0
  130. package/src/providers/registry/model-seeds.ts +78 -21
  131. package/src/responses/apply-patch-envelope.ts +44 -11
  132. package/src/responses/bridge-search-replay-cache.ts +152 -0
  133. package/src/responses/code-mode-helper-compat.ts +26 -16
  134. package/src/responses/custom-tool-compat.ts +1 -1
  135. package/src/responses/hosted-tool-policy.ts +85 -2
  136. package/src/responses/schema.ts +9 -2
  137. package/src/server/auth-cors.ts +26 -0
  138. package/src/server/chat-completions.ts +9 -4
  139. package/src/server/chat-native-sse.ts +26 -9
  140. package/src/server/chat-native.ts +10 -4
  141. package/src/server/claude-messages.ts +24 -2
  142. package/src/server/gui-static.ts +36 -2
  143. package/src/server/inbound-body-admission.ts +187 -0
  144. package/src/server/index.ts +15 -19
  145. package/src/server/management/api-access.ts +3 -4
  146. package/src/server/management/config-routes.ts +31 -6
  147. package/src/server/management/provider-capability-config.ts +35 -7
  148. package/src/server/management/provider-routes.ts +70 -18
  149. package/src/server/proxy-liveness.ts +97 -2
  150. package/src/server/relay.ts +17 -24
  151. package/src/server/request-log.ts +25 -1
  152. package/src/server/responses/adapter-continuation.ts +71 -27
  153. package/src/server/responses/adapter-delivery.ts +39 -8
  154. package/src/server/responses/adapter-dispatch.ts +52 -24
  155. package/src/server/responses/compact.ts +60 -11
  156. package/src/server/responses/core-codex-account.ts +83 -22
  157. package/src/server/responses/core-normalize.ts +12 -5
  158. package/src/server/responses/fetch-helpers.ts +68 -2
  159. package/src/server/responses/passthrough-delivery.ts +10 -1
  160. package/src/server/responses/passthrough-dispatch.ts +113 -48
  161. package/src/server/responses/passthrough-execution.ts +11 -1
  162. package/src/server/responses/request-prepare.ts +29 -0
  163. package/src/server/responses/request-send-budget.ts +84 -7
  164. package/src/server/responses/request-sidecar-auth.ts +16 -8
  165. package/src/server/responses/request-spend.ts +38 -9
  166. package/src/server/responses/request-transport.ts +13 -10
  167. package/src/server/responses/run-turn-execution.ts +20 -5
  168. package/src/server/responses/sidecar-execution.ts +2 -0
  169. package/src/server/responses/ws-upstream.ts +2 -1
  170. package/src/server/responses-custom-tool-repair.ts +2 -2
  171. package/src/server/sse-frame-buffer.ts +12 -10
  172. package/src/server/sse-payload-rewrite.ts +36 -9
  173. package/src/server/system-env-shell.ts +5 -1
  174. package/src/server/system-env.ts +7 -1
  175. package/src/server/workflow-refusal.ts +56 -2
  176. package/src/service/cli.ts +16 -6
  177. package/src/service/guards.ts +10 -0
  178. package/src/service/health.ts +43 -0
  179. package/src/service/state.ts +7 -2
  180. package/src/types/accounts.ts +4 -0
  181. package/src/types/config.ts +100 -3
  182. package/src/types/provider.ts +19 -0
  183. package/src/types/request.ts +7 -1
  184. package/src/types/wire.ts +9 -1
  185. package/src/usage/expected-prices.ts +28 -0
  186. package/src/usage/log.ts +87 -4
  187. package/src/web-search/passthrough-bridge.ts +39 -5
  188. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
@@ -107,6 +107,7 @@ import {
107
107
  } from "../../codex/upstream-host-health";
108
108
  import {
109
109
  ForwardAdmissionCredentialError,
110
+ contextPrincipalIdOf,
110
111
  hasForwardableCodexBearer,
111
112
  validateForwardAdmissionCredential,
112
113
  } from "../auth-cors";
@@ -114,7 +115,11 @@ import type { DataPlaneAdmission } from "../auth-cors";
114
115
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
115
116
  import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
116
117
  import { NATIVE_RESERVE_MODEL } from "../../codex/catalog/native-models";
117
- import { isCodexReserveRequestEligible } from "../../codex/loopback-target";
118
+ import {
119
+ isCodexReserveOptInMissing,
120
+ isCodexReserveRequestEligible,
121
+ CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE,
122
+ } from "../../codex/loopback-target";
118
123
  import { slugsEquivalent } from "../../providers/slug-codec";
119
124
  import { decideTier, tierValueAfterDecision } from "../../providers/fastwire";
120
125
  import { fastPolicyForModel } from "../../providers/service-tier";
@@ -210,8 +215,33 @@ function pruneCompactHandoffRoutes(now: number): void {
210
215
  }
211
216
  }
212
217
 
213
- function rememberCompactHandoffRoute(req: Request, model: string, now = Date.now()): void {
214
- const key = sessionLaneIdFromRequest(req.headers);
218
+ /**
219
+ * The handoff map is process-global, so a caller-controlled lane header alone
220
+ * cannot be the key: two authenticated clients sending the same lane header
221
+ * would share one fallback route. Namespace the lane by the admitted principal.
222
+ * A loopback or missing admission has no authenticated identity to bind this
223
+ * cross-request state to, so it is ineligible rather than trusted.
224
+ */
225
+ function compactHandoffRouteKey(req: Request, admission: DataPlaneAdmission | undefined): string | null {
226
+ const lane = sessionLaneIdFromRequest(req.headers);
227
+ if (!lane || !admission || admission.kind === "loopback") return null;
228
+ // Fail closed rather than substituting a weaker identity. `keyId` survives a
229
+ // rotation and every identity-less environment admission would collapse into
230
+ // one bucket, which is the collision this key exists to prevent. Production
231
+ // admission always mints `contextPrincipalId` for configured and environment
232
+ // holders, so no real authenticated caller loses the route.
233
+ const principal = contextPrincipalIdOf(admission);
234
+ if (!principal) return null;
235
+ return `${principal}\u0000${lane}`;
236
+ }
237
+
238
+ function rememberCompactHandoffRoute(
239
+ req: Request,
240
+ admission: DataPlaneAdmission | undefined,
241
+ model: string,
242
+ now = Date.now(),
243
+ ): void {
244
+ const key = compactHandoffRouteKey(req, admission);
215
245
  if (!key || model.length > COMPACT_HANDOFF_MODEL_MAX_LENGTH) return;
216
246
  pruneCompactHandoffRoutes(now);
217
247
  compactHandoffRoutes.delete(key);
@@ -219,13 +249,18 @@ function rememberCompactHandoffRoute(req: Request, model: string, now = Date.now
219
249
  pruneCompactHandoffRoutes(now);
220
250
  }
221
251
 
222
- function forgetCompactHandoffRoute(req: Request): void {
223
- const key = sessionLaneIdFromRequest(req.headers);
252
+ function forgetCompactHandoffRoute(req: Request, admission?: DataPlaneAdmission): void {
253
+ const key = compactHandoffRouteKey(req, admission);
224
254
  if (key) compactHandoffRoutes.delete(key);
225
255
  }
226
256
 
227
- function compactHandoffRoute(req: Request, previousModel: string, now = Date.now()): string | null {
228
- const key = sessionLaneIdFromRequest(req.headers);
257
+ function compactHandoffRoute(
258
+ req: Request,
259
+ admission: DataPlaneAdmission | undefined,
260
+ previousModel: string,
261
+ now = Date.now(),
262
+ ): string | null {
263
+ const key = compactHandoffRouteKey(req, admission);
229
264
  if (!key) return null;
230
265
  pruneCompactHandoffRoutes(now);
231
266
  const entry = compactHandoffRoutes.get(key);
@@ -632,6 +667,20 @@ export async function handleResponsesCompact(
632
667
  ? `${route.providerName}-${route.codexAccountNamespace}`
633
668
  : route.providerName;
634
669
  logCtx.providerAdapter = route.provider.adapter;
670
+ // #4940, and the same refusal the ordinary Responses path makes in request-prepare.ts. Compact has
671
+ // to repeat it rather than inherit it: the native branch below dispatches straight to
672
+ // `/responses/compact` and only the routed fallback replays through handleResponses, so relying on
673
+ // that one seam would leave the native compact turn forwarding a Reserve model the opt-in has made
674
+ // unservable. Composed from the same three facts, in the same order, as `customReserveForward`
675
+ // further down, and placed before the virtual-model rewrite so it reads the model the caller
676
+ // actually selected -- and before auth, host-circuit admission, or any upstream byte.
677
+ //
678
+ // No terminal-helper or inbound-wire qualifier is needed here the way it is on the ordinary path:
679
+ // this endpoint is the native Codex compaction wire, and a vision/search helper never reaches it.
680
+ if (isCodexReserveOptInMissing(config, selectedModelId, admission)
681
+ && isCanonicalOpenAiForwardProvider(route.provider)) {
682
+ return formatErrorResponse(400, "invalid_request_error", CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE);
683
+ }
635
684
  const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
636
685
  if (virtual) {
637
686
  route.modelId = virtual.wireModelId;
@@ -1254,10 +1303,10 @@ export async function handleResponsesCompact(
1254
1303
  // synthetic buffer errors are not upstream bodies and stay uninspected.
1255
1304
  if (buffered.ok) {
1256
1305
  inspectResponseLogJson(logCtx, await buffered.clone().text());
1257
- forgetCompactHandoffRoute(req);
1306
+ forgetCompactHandoffRoute(req, admission);
1258
1307
  rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
1259
1308
  } else if (quotaFailure && !storedPool401ReplayAttempted) {
1260
- const fallbackModel = compactHandoffRoute(req, raw.model);
1309
+ const fallbackModel = compactHandoffRoute(req, admission, raw.model);
1261
1310
  if (fallbackModel && !req.signal.aborted) {
1262
1311
  const fallbackReq = new Request(req.url, {
1263
1312
  method: "POST",
@@ -1389,7 +1438,7 @@ export async function handleResponsesCompact(
1389
1438
  const result = new Response(JSON.stringify({ output: compactionItems }), {
1390
1439
  headers: { "Content-Type": "application/json" },
1391
1440
  });
1392
- rememberCompactHandoffRoute(req, raw.model);
1441
+ rememberCompactHandoffRoute(req, admission, raw.model);
1393
1442
  return result;
1394
1443
  }
1395
1444
  const encrypted = compactionItems[0]!.encrypted_content;
@@ -1400,6 +1449,6 @@ export async function handleResponsesCompact(
1400
1449
  }
1401
1450
  const summary = decoded;
1402
1451
  const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
1403
- rememberCompactHandoffRoute(req, raw.model);
1452
+ rememberCompactHandoffRoute(req, admission, raw.model);
1404
1453
  return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
1405
1454
  }
@@ -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 {
@@ -778,15 +829,25 @@ export async function retryCodexPoolOnAlternateAccount(
778
829
  }
779
830
  retrySendCount += 1;
780
831
  args.onResponse?.(upstreamResponse, retryAuthCtx, request);
832
+ // The alternate account can refuse the same model, and that refusal is evidence about the
833
+ // account that produced it. Read BEFORE the ladder's own break, so the ordinary
834
+ // single-retry path -- every flagship model, which is the #4906 case -- records it too
835
+ // rather than only the gated ladder below. Without this the pool learns nothing from a
836
+ // refusal and the next request repeats the same selection.
837
+ const retryModelDenial = await codexPoolAccountModel400Denial(
838
+ upstreamResponse,
839
+ route.modelId,
840
+ options.abortSignal,
841
+ parsed.modelId,
842
+ );
843
+ if (retryModelDenial !== undefined) {
844
+ recordCodexModelDenialEvidence(retryAuthCtx.accountId, retryModelDenial);
845
+ }
781
846
  if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
782
847
  // Caller-owned main is an alternate-account replay and can never enter the bounded
783
848
  // same-stored-account 400 loop above. Keep that invariant explicit for the account-id reads.
784
849
  if (retryAuthCtx.kind === "main") break;
785
- if (!await shouldRetryCodexPoolAccountModel400(
786
- upstreamResponse,
787
- route.modelId,
788
- options.abortSignal,
789
- )) break;
850
+ if (retryModelDenial === undefined) break;
790
851
  invalidateCodexModelEntitlementsForAccount(retryAuthCtx.accountId);
791
852
  let refreshed: Awaited<ReturnType<typeof resolveCodexModelEntitlements>>;
792
853
  try {
@@ -142,11 +142,18 @@ export async function applyFinalRouteRequestNormalization(args: {
142
142
  route.modelId,
143
143
  );
144
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);
145
+ // Preserve the routed destination for Go recognition, then settle the wire before
146
+ // deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
147
+ const routedProvider = route.provider;
148
+ const wireProvider = resolveWireProtocolOverride(
149
+ route.providerName,
150
+ route.modelId,
151
+ routedProvider,
152
+ inboundWire,
153
+ );
154
+ route.provider = resolveOpenCodeGoTransport(wireProvider,
155
+ args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req),
156
+ routedProvider);
150
157
  parsed._plaintextV2AgentMessages = shouldPreparePlaintextV2AgentMessages({
151
158
  enabled: config.plaintextV2AgentMessages === true,
152
159
  inboundWire,
@@ -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,7 +30,7 @@ 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";
@@ -45,6 +45,7 @@ import {
45
45
  createPassthroughWebSearchBridgeStream,
46
46
  createPassthroughWebSearchBridgeExecutor,
47
47
  } from "../../web-search/passthrough-bridge";
48
+ import { bridgeSearchReplayScope } from "../../responses/bridge-search-replay-cache";
48
49
  import { fetchWithHeaderTimeout, providerFetch } from "./fetch-helpers";
49
50
  import { providerApiKeySelectionIsCurrent } from "../../providers/api-key-selection";
50
51
  import { requiresVisionPreprocessing } from "../../vision";
@@ -402,12 +403,20 @@ export async function deliverPassthroughResponse(
402
403
  describeImages: requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName),
403
404
  sidecar: config.webSearchSidecar,
404
405
  }),
406
+ // Scope the executed-search memo to this exact upstream (#4587). The Responses adapter
407
+ // derives the same scope from the same base URL before the NEXT turn is dispatched, so
408
+ // a replayed hosted cell can be turned back into the destination's own call and result.
409
+ destinationScope: bridgeSearchReplayScope(route.provider.baseUrl),
405
410
  // Appending a search result can push the continuation past the ceiling the first leg
406
411
  // was admitted under, so the same limit is re-applied before every later send.
407
412
  checkOutboundBody: (continuationBody: string) => {
408
413
  const result = checkOutboundBodySize(continuationBody, config.maxUpstreamBodyBytes);
409
414
  return result.admitted ? undefined : describeOutboundBodyRefusal(result);
410
415
  },
416
+ // Resolution can acquire the account's sole cooldown-recovery probe before the routed
417
+ // provider reveals whether it will request search. Hand an unused lease back on every
418
+ // terminal path; after an executed search, the outcome recorder has already settled it.
419
+ onFinalize: () => releaseCodexAuthContextProbeLease(openAiSidecar?.authContext),
411
420
  signal: upstream.signal,
412
421
  })
413
422
  : upstreamResponse.body;