@bitkyc08/opencodex 2.65.0 → 2.66.0-preview.20260925

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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-BVjA0T6O.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
  5. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  6. package/gui/dist/assets/index-vO4dgfiU.js +86 -0
  7. package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-DGLte6IR.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
@@ -39,6 +39,18 @@ export interface ConsumedComboFailure {
39
39
 
40
40
 
41
41
 
42
+ /**
43
+ * Direct client encoding for a translated Chat or Messages turn (PF-09). `model` is the model
44
+ * string the client asked for, which every client frame carries; `inputTokenFloor` is the
45
+ * Messages prompt estimate `message_start` reports when no usage arrived first (#4857).
46
+ */
47
+ export interface ClientEncoderOption {
48
+ protocol: "chat" | "messages";
49
+ stream: boolean;
50
+ model: string;
51
+ inputTokenFloor?: number;
52
+ }
53
+
42
54
  export interface HandleResponsesOptions {
43
55
  /** Internal Claude replay identity; consumed only by the final canonical Go transport. */
44
56
  claudeGoAffinity?: { sessionLane?: string };
@@ -94,6 +106,8 @@ export interface HandleResponsesOptions {
94
106
  * it. Omitted means a genuine Responses inbound.
95
107
  */
96
108
  inboundWire?: InboundWire;
109
+ /** PF-07: the Chat source a combo child may send natively; set only by the Chat ingress. */
110
+ protocolSource?: import("./core-combo-native").ComboProtocolSource;
97
111
  /** Internal transport identity for route-scoped upstream compatibility policy. */
98
112
  inboundTransport?: "websocket";
99
113
  /**
@@ -147,6 +161,14 @@ export interface HandleResponsesOptions {
147
161
  * rebuilds headers and carries the fact through this flag.
148
162
  */
149
163
  visionDescribeTerminal?: boolean;
164
+ /**
165
+ * Set only by the Chat and Messages ingresses when `protocols.rollout.directEncoders` is on and
166
+ * the settled route is one non-Responses target. Adapter delivery then encodes the adapter
167
+ * events straight into the client's wire and returns a response marked with `markClientWire`.
168
+ * Passthrough, run-turn adapters, combo children and compaction turns ignore it and keep
169
+ * returning a Responses body, which the ingress converts as before.
170
+ */
171
+ clientEncoder?: ClientEncoderOption;
150
172
  }
151
173
 
152
174
  /** Values shared by the call, not a bag of mutable pipeline state. */
@@ -9,8 +9,7 @@ import type {
9
9
  import { createTranslatorBudget } from "../../lib/translator-budget";
10
10
  import { captureExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
11
11
  import { captureCallerDirectAuth } from "../../providers/caller-authorization";
12
- import { createRequestExecutionBudget } from "../../lib/request-execution-budget";
13
- import { attachRequestSpendTracker } from "./request-spend";
12
+ import { createInferenceSendBudget } from "../inference/context";
14
13
  import { finalizeOwnedTranslatorBudget } from "./core-lifetime";
15
14
  import type { TranslatorBudget } from "../../lib/translator-budget";
16
15
  import { executeComboResponses } from "./core-combo";
@@ -31,7 +30,6 @@ import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
31
30
 
32
31
  /** Public Responses entry and compatibility exports. Implementations live with their owners. */
33
32
 
34
-
35
33
  /**
36
34
  * Route one `/v1/responses` request through the adapter pipeline: recovery loop, passthrough
37
35
  * wire, image/web-search bridges, and the terminal-guard continuation.
@@ -58,7 +56,7 @@ export async function handleResponses(
58
56
  || req.headers.get("x-opencodex-vision-describe") === "1",
59
57
  translatorBudget,
60
58
  // Once at ingress, spend observer included: a combo child inherits the parent's holder.
61
- sendBudget: options.sendBudget ?? createRequestExecutionBudget(undefined, undefined, attachRequestSpendTracker(req, logCtx)),
59
+ sendBudget: options.sendBudget ?? createInferenceSendBudget(req, logCtx),
62
60
  });
63
61
  return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
64
62
  } catch (error) {
@@ -182,29 +180,19 @@ async function handleResponsesInner(
182
180
  const requestDispatchers: ResponsesDispatchers = { handleResponses, handleComboResponses };
183
181
 
184
182
  export { adapterNeedsForcedContinuation } from "./core-replay";
185
- export { sidecarOutcomeRecorder } from "./core-codex-account";
186
- export { codexLogAccountId } from "./core-codex-account";
183
+ export {
184
+ sidecarOutcomeRecorder, codexLogAccountId, usesCodexForwardPoolAuth, preAuthUpstreamHostCircuitKey,
185
+ upstreamHostCircuitOpenResponse, shouldRetryCodexPoolAccountQuota, shouldRetryCodexScopedQuotaOnAlternate,
186
+ shouldRetryCodexPoolAccountTransient, codexAccountGatedCanonicalWireModel, codexForwardTerminalOutcomeRecorder,
187
+ } from "./core-codex-account";
187
188
  export { shouldAttemptOpaqueBlobRecovery } from "./core-opaque-recovery";
188
- export { readDisplaySafeErrorText } from "./core-errors";
189
- export { usesCodexForwardPoolAuth } from "./core-codex-account";
190
- export { preAuthUpstreamHostCircuitKey } from "./core-codex-account";
191
- export { upstreamHostCircuitOpenResponse } from "./core-codex-account";
192
- export { shouldRetryCodexPoolAccountQuota, shouldRetryCodexScopedQuotaOnAlternate } from "./core-codex-account";
193
- export { shouldRetryCodexPoolAccountTransient } from "./core-codex-account";
194
- export { codexAccountGatedCanonicalWireModel } from "./core-codex-account";
195
- export { codexForwardTerminalOutcomeRecorder } from "./core-codex-account";
196
- export { decodeRequestErrorResponse } from "./core-errors";
197
- export { comboUnavailableResponse } from "./core-errors";
198
- export type { ConsumedComboFailure } from "./core-options";
199
- export type { HandleResponsesOptions } from "./core-options";
200
- export { clientCancelledResponse } from "./core-errors";
201
- export { sanitizedRetryAfter } from "./core-combo-failure";
202
- export { consumeComboFailure } from "./core-combo-failure";
203
- export { usageFromComboFailureText } from "./core-combo-failure";
204
- export { createChildPassthroughCallbackGate } from "./core-combo-failure";
205
- export { buildComboChildHeaders } from "./core-combo-failure";
206
- export { UPSTREAM_JSON_BODY_READ_OPTIONS } from "./core-lifetime";
189
+ export { readDisplaySafeErrorText, decodeRequestErrorResponse, comboUnavailableResponse, clientCancelledResponse } from "./core-errors";
190
+ export type { ConsumedComboFailure, HandleResponsesOptions } from "./core-options";
191
+ export {
192
+ sanitizedRetryAfter, consumeComboFailure, usageFromComboFailureText, createChildPassthroughCallbackGate,
193
+ buildComboChildHeaders,
194
+ } from "./core-combo-failure";
195
+ export { UPSTREAM_JSON_BODY_READ_OPTIONS, linkAbortSignal } from "./core-lifetime";
207
196
  export { poolCredentialRefreshIncompleteResponse } from "./core-auth";
208
197
  export { applyServiceTierGate } from "./core-normalize";
209
- export { linkAbortSignal } from "./core-lifetime";
210
198
  export { DEFAULT_SHADOW_SOURCE_MODELS, isShadowSourceModel, shadowSourceModels } from "../../lib/shadow-call";
@@ -53,9 +53,14 @@ export async function prepareResponsesSidecarAuth(
53
53
  && (!isCanonicalOpenAiForwardProvider(route.provider) || parsed._portableCompaction === true);
54
54
  const needsOpenAiVision = !visionDescribeTerminal
55
55
  && shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed, route.providerName);
56
- const needsOpenAiSearch = !routedCompaction && !transportState.adapter.runTurn
57
- && (shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough)
58
- || shouldResolveOpenAiPassthroughWebSearchBridge(route.provider, parsed, isPassthrough));
56
+ // LOCAL PATCH (runturn-websearch): runTurn adapters can run the web-search
57
+ // sidecar through src/web-search/run-turn-loop.ts, so resolve the helper
58
+ // credential for them too. The passthrough bridge stays fetch-only.
59
+ const needsOpenAiSearch = !routedCompaction
60
+ && (transportState.adapter.runTurn
61
+ ? shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough)
62
+ : (shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough)
63
+ || shouldResolveOpenAiPassthroughWebSearchBridge(route.provider, parsed, isPassthrough)));
59
64
  if (needsOpenAiVision || needsOpenAiSearch) {
60
65
  try {
61
66
  const candidates = listOpenAiForwardSidecarCandidates(config);
@@ -31,6 +31,8 @@ import {
31
31
  import { resolveWireProtocolOverride } from "../adapter-resolve";
32
32
  import { formatErrorResponse, bridgeToResponsesSSE, buildResponseJSON } from "../../bridge";
33
33
  import { redactSecretString } from "../../lib/redact";
34
+ import { jsonUtf8Bytes } from "../../lib/json-byte-size";
35
+ import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
34
36
  import {
35
37
  guardEmptyCompletionEventStream,
36
38
  observeEmptyCompletion,
@@ -40,6 +42,28 @@ import { rememberResponseState } from "../../responses/state";
40
42
  import { trackStreamLifetime } from "../lifecycle";
41
43
  import { awaitThoughtSignatureDurability } from "../../responses/thought-signature-replay";
42
44
  import { undeclaredToolCallMessage } from "../responses-undeclared-tool-guard";
45
+ // LOCAL PATCH (runturn-websearch)
46
+ import { planWebSearch } from "../../web-search";
47
+ import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
48
+ import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
49
+
50
+ // LOCAL PATCH (runturn-websearch): top-level fields route binding or the
51
+ // adapter itself may write during a turn. Iteration-local `turnParsed` objects
52
+ // are shallow copies of `parsed`, so these are mirrored both directions around
53
+ // each runTurn dispatch — clones would otherwise keep stale route state and
54
+ // adapter-written values (e.g. Cursor's conversation id) would be lost.
55
+ const RUNTURN_WS_ROUTE_STATE_KEYS = [
56
+ "_cursorIdentityScope",
57
+ "_cursorConversationId",
58
+ "_cursorClientThreadId",
59
+ "_kiroAuthContext",
60
+ "_providerContinuation",
61
+ "_providerContinuationOwner",
62
+ "_providerContinuationCandidate",
63
+ "_stripReasoningEncryptedContent",
64
+ "_dropForeignReasoningItemIds",
65
+ "_reasoningReplayScope",
66
+ ] as const;
43
67
 
44
68
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
45
69
  export async function executeResponsesRunTurn(
@@ -71,7 +95,7 @@ export async function executeResponsesRunTurn(
71
95
  | "noteRoutedAttemptSend"
72
96
  | "bindKeyUsageFromBridge"
73
97
  >,
74
- sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
98
+ sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction" | "openAiSidecar">,
75
99
  responseEffects: Pick<
76
100
  ResponsesEffects,
77
101
  | "cancelResponseCompletion"
@@ -121,6 +145,31 @@ export async function executeResponsesRunTurn(
121
145
  } = responseEffects;
122
146
  const { routedCompaction } = sidecarState;
123
147
 
148
+ // LOCAL PATCH (runturn-websearch): resolving the OpenAI search credential can
149
+ // hold the account's sole cooldown-recovery probe lease. The fetch path hands
150
+ // it back through the bridge's onFinalize; runTurn owns no such hook, so this
151
+ // turn releases it on every exit — normal end, error, cancel, and every
152
+ // pre-response failure below. After an executed search the outcome recorder
153
+ // already settled the lease, making each release a generation-bound no-op.
154
+ const releaseSearchProbeLease = (): void => {
155
+ sidecarState.openAiSidecar?.releaseProbeLease?.();
156
+ };
157
+ // When Codex declared hosted web_search and a sidecar plan resolves, drive
158
+ // the routed model through the same search interception the fetch-path loop
159
+ // runs — injected as a function tool, calls intercepted, results appended to
160
+ // the message history between runTurn dispatches.
161
+ let wsPlan: ReturnType<typeof planWebSearch>;
162
+ try {
163
+ wsPlan = !routedCompaction
164
+ ? planWebSearch(config, parsed, false, route.provider, route.modelId, sidecarState.openAiSidecar, {
165
+ admission: options.admission, codexAuthPolicy: options.codexAuthPolicy, providerName: route.providerName,
166
+ })
167
+ : undefined;
168
+ } catch (error) {
169
+ releaseSearchProbeLease();
170
+ throw error;
171
+ }
172
+
124
173
  const runTurnAbort = new AbortController();
125
174
  const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
126
175
  const queue = createAdapterEventQueue({
@@ -140,6 +189,7 @@ export async function executeResponsesRunTurn(
140
189
  } catch (error) {
141
190
  cleanupRunTurnAbort();
142
191
  queue.close();
192
+ releaseSearchProbeLease();
143
193
  throw error;
144
194
  }
145
195
  // One attempt of the runTurn transport, against an explicit queue. The
@@ -147,16 +197,35 @@ export async function executeResponsesRunTurn(
147
197
  // same forwarded headers, same abort signal) through a fresh queue, so the
148
198
  // attempt body must not capture the first queue. Each attempt consumes its
149
199
  // own provider pacing slot (#1584): retries are paced like first attempts.
200
+ // LOCAL PATCH (runturn-websearch): dispatch sequence. A 429 preflight
201
+ // rotation replays the turn while the abandoned attempt may still be
202
+ // in-flight; only the latest dispatch may merge adapter-written route
203
+ // state back onto `parsed`, or the superseded attempt would restore the
204
+ // failed account's cursor/continuation over the rotation's rebind.
205
+ let runTurnAttemptSeq = 0;
150
206
  const runTurnAttempt = async (
151
207
  targetQueue: AdapterEventQueue,
152
208
  recovery?: AttemptRecoveryKind,
153
209
  pacingSlotAcquired = false,
210
+ // LOCAL PATCH (runturn-websearch): iteration-local parsed — later search
211
+ // rounds carry the grown message history and adjusted tool list while
212
+ // selection/replay binding stays on the request's own parsed object.
213
+ turnParsed: PreparedResponsesRequest["parsed"] = parsed,
154
214
  ): Promise<void> => {
215
+ const attemptSeq = ++runTurnAttemptSeq;
155
216
  try {
156
217
  if (!pacingSlotAcquired) {
157
218
  await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
158
219
  }
159
220
  await refreshRunTurnSelection();
221
+ // LOCAL PATCH (runturn-websearch): refreshRunTurnSelection binds route
222
+ // state onto `parsed`; mirror it onto the iteration-local copy the
223
+ // adapter actually receives.
224
+ {
225
+ const routeState: Record<string, unknown> = {};
226
+ for (const k of RUNTURN_WS_ROUTE_STATE_KEYS) routeState[k] = parsed[k];
227
+ Object.assign(turnParsed, routeState);
228
+ }
160
229
  // An adapter that reports its own sends accounts for the first one at the boundary that
161
230
  // dispatches it. Logging here would claim a send that the adapter's own budget can still
162
231
  // refuse, which is exactly what happens once earlier recovery has spent the allowance.
@@ -175,7 +244,7 @@ export async function executeResponsesRunTurn(
175
244
  },
176
245
  );
177
246
  await transportState.runTurnAdapter.runTurn?.(
178
- parsed,
247
+ turnParsed,
179
248
  {
180
249
  headers: requestState.selectedForwardHeaders,
181
250
  abortSignal: runTurnAbort.signal,
@@ -195,6 +264,17 @@ export async function executeResponsesRunTurn(
195
264
  },
196
265
  targetQueue.push,
197
266
  );
267
+ // LOCAL PATCH (runturn-websearch): adapters may write conversation/
268
+ // continuation state onto the object they received; merge it back so
269
+ // the next iteration's copy and request-level consumers observe it.
270
+ // Skipped once a newer attempt has dispatched: this attempt was
271
+ // abandoned by a 429 rotation, so its account's route state is stale
272
+ // and writing it back would undo the rotation's rebind.
273
+ if (attemptSeq === runTurnAttemptSeq) {
274
+ const routeState: Record<string, unknown> = {};
275
+ for (const k of RUNTURN_WS_ROUTE_STATE_KEYS) routeState[k] = turnParsed[k];
276
+ Object.assign(parsed, routeState);
277
+ }
198
278
  } catch (err) {
199
279
  targetQueue.push(err instanceof RequestPacingQueueOverloadError
200
280
  ? {
@@ -229,7 +309,30 @@ export async function executeResponsesRunTurn(
229
309
  targetQueue.close();
230
310
  }
231
311
  };
232
- const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
312
+ // LOCAL PATCH (runturn-websearch): the first iteration carries the
313
+ // synthetic web_search tool; later iterations get their own queue so the
314
+ // search loop can buffer each turn's events before deciding to intercept.
315
+ const wsFirstParsed = wsPlan ? runTurnWebSearchInitialParsed(parsed) : parsed;
316
+ const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true, wsFirstParsed);
317
+ const runTurnFailoverArmed = () =>
318
+ route.provider.authMode === "oauth"
319
+ || !!(transportState.genericFailoverAccountId
320
+ && isGenericOAuthFailoverEnabled(config, route.providerName));
321
+ const dispatchSearchIteration = (iterParsed: PreparedResponsesRequest["parsed"]): AsyncIterable<AdapterEvent> => {
322
+ const iterQueue = createAdapterEventQueue({
323
+ onBacklogExceeded: () => runTurnAbort.abort(),
324
+ });
325
+ void runTurnAttempt(iterQueue, undefined, false, iterParsed);
326
+ const stream = iterQueue.stream();
327
+ if (!runTurnFailoverArmed()) return stream;
328
+ // LOCAL PATCH (runturn-websearch): a post-search iteration can open on a
329
+ // 429 too — the search cells already reached the client, so only this
330
+ // answer call rotates. Preflight replays it on the next account with the
331
+ // grown history intact; a mid-stream error still ends the turn as before.
332
+ return (async function* () {
333
+ yield* await preflightRunTurnFailover(stream, iterParsed);
334
+ })();
335
+ };
233
336
  const rotateRunTurnAdapterOnPreflight429 = async (
234
337
  error: Extract<AdapterEvent, { type: "error" }>,
235
338
  ): Promise<boolean> => {
@@ -331,6 +434,10 @@ export async function executeResponsesRunTurn(
331
434
  };
332
435
  const preflightRunTurnFailover = async (
333
436
  firstSource: AsyncIterable<AdapterEvent>,
437
+ // LOCAL PATCH (runturn-websearch): replayed attempts re-dispatch this
438
+ // request — the grown-history iteration for post-search legs, the
439
+ // tool-injected first request elsewhere.
440
+ replayParsed: PreparedResponsesRequest["parsed"] = wsFirstParsed,
334
441
  ): Promise<AsyncIterable<AdapterEvent>> => {
335
442
  let source = firstSource;
336
443
  try {
@@ -344,7 +451,7 @@ export async function executeResponsesRunTurn(
344
451
  const retryQueue = createAdapterEventQueue({
345
452
  onBacklogExceeded: () => runTurnAbort.abort(),
346
453
  });
347
- void runTurnAttempt(retryQueue, "oauth-account-429");
454
+ void runTurnAttempt(retryQueue, "oauth-account-429", false, replayParsed);
348
455
  source = retryQueue.stream();
349
456
  }
350
457
  } finally {
@@ -372,6 +479,8 @@ export async function executeResponsesRunTurn(
372
479
  event: AdapterEvent,
373
480
  ): Extract<AdapterEvent, { type: "error" }> | undefined => {
374
481
  if (!enforceDeclaredToolNames || event.type !== "tool_call_start") return undefined;
482
+ // This tool is declared to the adapter by the private search loop.
483
+ if (wsPlan && event.name === WEB_SEARCH_TOOL_NAME) return undefined;
375
484
  const effectiveName = normalizeDeclaredToolName(event.name, declaredToolNames);
376
485
  if (declaredToolNames.has(effectiveName)) return undefined;
377
486
  return {
@@ -382,9 +491,10 @@ export async function executeResponsesRunTurn(
382
491
  };
383
492
  };
384
493
  if (parsed.stream) {
494
+ try {
385
495
  void runTurn();
386
496
  let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
387
- if (route.provider.authMode === "oauth" || (transportState.genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
497
+ if (runTurnFailoverArmed()) {
388
498
  // Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
389
499
  // replayed transparently; after any output reaches the bridge, a later error stays terminal.
390
500
  eventSource = await preflightRunTurnFailover(eventSource);
@@ -399,11 +509,32 @@ export async function executeResponsesRunTurn(
399
509
  // A replay-unsafe heartbeat means the adapter already ran a local side effect, so the
400
510
  // combo must not send this turn to another target: the failure stays with this child.
401
511
  if (preflight.replayUnsafe) markResponseNonReplayable(failure);
512
+ releaseSearchProbeLease();
402
513
  return failure;
403
514
  }
404
515
  eventSource = preflight.stream;
405
516
  }
406
- const guardedSource = emptyCompletionGuardEnabled
517
+ // LOCAL PATCH (runturn-websearch): intercept web_search calls across
518
+ // iterations; terminal output keeps flowing through the same queue/bridge.
519
+ if (wsPlan) {
520
+ eventSource = runTurnWebSearchLoop(eventSource, {
521
+ parsed,
522
+ plan: wsPlan,
523
+ translatorBudget,
524
+ emptyCompletionRetry: emptyCompletionGuardEnabled,
525
+ forwardProvider: wsPlan.forwardSidecar?.provider,
526
+ forwardHeaders: wsPlan.forwardSidecar?.headers ?? requestState.selectedForwardHeaders,
527
+ ...(wsPlan.exaConfigured ? { exaApiKey: config.webSearchSidecar?.exaApiKey } : {}),
528
+ recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
529
+ abortSignal: runTurnAbort.signal,
530
+ dispatch: dispatchSearchIteration,
531
+ });
532
+ }
533
+ // LOCAL PATCH (runturn-websearch): the empty-completion retry replays
534
+ // the ORIGINAL parsed request — no synthetic tool, no gathered results —
535
+ // so it must not fire while the search loop owns the turn. An empty
536
+ // forced answer is recovered inside runTurnWebSearchLoop instead.
537
+ const guardedSource = emptyCompletionGuardEnabled && !wsPlan
407
538
  ? guardEmptyCompletionEventStream({
408
539
  firstEvents: eventSource,
409
540
  // Identical-turn retry: same parsed request, same headers, same
@@ -428,7 +559,7 @@ export async function executeResponsesRunTurn(
428
559
  translatorBudget,
429
560
  replayCacheScope: parsed._reasoningReplayScope,
430
561
  ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
431
- stallTimeoutSec: config.stallTimeoutSec,
562
+ stallTimeoutSec: wsPlan?.stallTimeoutSec ?? config.stallTimeoutSec,
432
563
  hideThinkingSummary: parsed.options.hideThinkingSummary,
433
564
  declaredToolNames,
434
565
  enforceDeclaredToolNames,
@@ -459,25 +590,38 @@ export async function executeResponsesRunTurn(
459
590
  },
460
591
  );
461
592
  const bridgeTurnAc = new AbortController();
462
- const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
593
+ // LOCAL PATCH (runturn-websearch): sidecar resolution may hold the
594
+ // account's cooldown-recovery probe. Hand it back when the stream ends —
595
+ // completion, failure, or client cancel; a search that ran already
596
+ // settled it through recordOutcome, so this release is a safe no-op then.
597
+ const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, releaseSearchProbeLease, options.turnAdmissionLease);
463
598
  const response = new Response(trackedSse, {
464
599
  headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
465
600
  });
466
601
  runTurnAdapterSseResponses.add(response);
467
602
  return response;
603
+ } catch (error) {
604
+ // Every pre-response exit hands the probe lease back too: a turn that
605
+ // never started streaming has no later owner to release it.
606
+ releaseSearchProbeLease();
607
+ throw error;
608
+ }
468
609
  }
469
610
 
611
+ try {
470
612
  await runTurn();
471
613
  const firstAttemptEvents = await queue.collect();
472
614
  let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
473
- if (route.provider.authMode === "oauth" || (transportState.genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName))) {
615
+ if (runTurnFailoverArmed()) {
474
616
  runTurnEvents = [];
475
617
  for await (const event of await preflightRunTurnFailover(
476
618
  (async function* () { yield* firstAttemptEvents; })(),
477
619
  )) runTurnEvents.push(event);
478
620
  }
479
621
  let events: AdapterEvent[];
480
- if (emptyCompletionGuardEnabled) {
622
+ // LOCAL PATCH (runturn-websearch): same exclusion as the streaming branch —
623
+ // a search-aware retry runs inside runTurnWebSearchLoop, not the raw guard.
624
+ if (emptyCompletionGuardEnabled && !wsPlan) {
481
625
  events = [];
482
626
  for await (const event of guardEmptyCompletionEventStream({
483
627
  firstEvents: (async function* () { yield* runTurnEvents; })(),
@@ -486,60 +630,110 @@ export async function executeResponsesRunTurn(
486
630
  } else {
487
631
  events = runTurnEvents;
488
632
  }
489
- if (options.comboAttempt) {
490
- const firstMeaningfulIndex = events.findIndex(event => event.type !== "heartbeat");
491
- const firstMeaningful = firstMeaningfulIndex === -1 ? undefined : events[firstMeaningfulIndex];
492
- // Same boundary as the streaming preflight: a replay-unsafe heartbeat means the adapter
493
- // already ran a local side effect, so an undeclared tool call after it keeps the bridge's
494
- // fail-closed refusal instead of becoming a hop that sends the turn to another target.
495
- const replayUnsafe = events
496
- .slice(0, firstMeaningfulIndex === -1 ? events.length : firstMeaningfulIndex)
497
- .some(event => event.type === "heartbeat" && event.replayUnsafe === true);
498
- const classifiedError = firstMeaningful && !replayUnsafe
499
- ? classifyUndeclaredFirstTool(firstMeaningful)
500
- : undefined;
501
- if (!firstMeaningful || firstMeaningful.type === "error" || classifiedError) {
502
- const message = classifiedError?.message ?? (firstMeaningful?.type === "error"
503
- ? firstMeaningful.message
504
- : "Adapter ended before producing a response");
505
- const failure = formatErrorResponse(502, "upstream_error", redactSecretString(message));
506
- if (replayUnsafe) markResponseNonReplayable(failure);
507
- return failure;
633
+ // This collector outlives each search iteration, including live-output
634
+ // events that the loop no longer owns. Keep its lease until JSON is built.
635
+ let searchOutputBytes = 0;
636
+ try {
637
+ // LOCAL PATCH (runturn-websearch): buffered path runs the same interception
638
+ // loop; iterations dispatch through the same attempt body on fresh queues.
639
+ if (wsPlan) {
640
+ const searched: AdapterEvent[] = [];
641
+ let retainedReplayUnsafe = false;
642
+ for await (const event of runTurnWebSearchLoop(
643
+ (async function* () { yield* events; })(),
644
+ {
645
+ parsed,
646
+ plan: wsPlan,
647
+ translatorBudget,
648
+ emptyCompletionRetry: emptyCompletionGuardEnabled,
649
+ forwardProvider: wsPlan.forwardSidecar?.provider,
650
+ forwardHeaders: wsPlan.forwardSidecar?.headers ?? requestState.selectedForwardHeaders,
651
+ ...(wsPlan.exaConfigured ? { exaApiKey: config.webSearchSidecar?.exaApiKey } : {}),
652
+ recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
653
+ abortSignal: runTurnAbort.signal,
654
+ dispatch: dispatchSearchIteration,
655
+ },
656
+ )) {
657
+ if (event.type === "heartbeat") {
658
+ if (!event.replayUnsafe || retainedReplayUnsafe) continue;
659
+ retainedReplayUnsafe = true;
660
+ }
661
+ if (event.type === "error" && event.code === "translation_buffer_limit") runTurnAbort.abort();
662
+ const bytes = jsonUtf8Bytes(event) + 1;
663
+ translatorBudget.chargeRetained(bytes, { kind: "retained_collectors" });
664
+ searchOutputBytes += bytes;
665
+ searched.push(event);
666
+ }
667
+ events = searched;
508
668
  }
669
+ if (options.comboAttempt) {
670
+ const firstMeaningfulIndex = events.findIndex(event => event.type !== "heartbeat");
671
+ const firstMeaningful = firstMeaningfulIndex === -1 ? undefined : events[firstMeaningfulIndex];
672
+ // Same boundary as the streaming preflight: a replay-unsafe heartbeat means the adapter
673
+ // already ran a local side effect, so an undeclared tool call after it keeps the bridge's
674
+ // fail-closed refusal instead of becoming a hop that sends the turn to another target.
675
+ const replayUnsafe = events
676
+ .slice(0, firstMeaningfulIndex === -1 ? events.length : firstMeaningfulIndex)
677
+ .some(event => event.type === "heartbeat" && event.replayUnsafe === true);
678
+ const classifiedError = firstMeaningful && !replayUnsafe
679
+ ? classifyUndeclaredFirstTool(firstMeaningful)
680
+ : undefined;
681
+ if (!firstMeaningful || firstMeaningful.type === "error" || classifiedError) {
682
+ const message = classifiedError?.message ?? (firstMeaningful?.type === "error"
683
+ ? firstMeaningful.message
684
+ : "Adapter ended before producing a response");
685
+ const failure = formatErrorResponse(502, "upstream_error", redactSecretString(message));
686
+ if (replayUnsafe) markResponseNonReplayable(failure);
687
+ return failure;
688
+ }
689
+ }
690
+ let providerState: OcxProviderContinuationState | undefined;
691
+ const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
692
+ translatorBudget,
693
+ replayCacheScope: parsed._reasoningReplayScope,
694
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
695
+ toolNsMap,
696
+ declaredToolNames,
697
+ enforceDeclaredToolNames,
698
+ toolParameterSchemas,
699
+ freeformToolNames,
700
+ toolSearchToolNames,
701
+ ...(routedCompaction ? { compaction: true } : {}),
702
+ onProviderState: state => { providerState = state; },
703
+ onUsage: usage => {
704
+ transportState.bindKeyUsageFromBridge(usage);
705
+ },
706
+ });
707
+ if (!routedCompaction) {
708
+ rememberKiroDeliveredFinalAnswer(transportState.adapter.name, json);
709
+ rememberResponseState(
710
+ parsed._rawBody,
711
+ json,
712
+ continuationStateForResponse(providerState),
713
+ responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
714
+ );
715
+ }
716
+ // #1926 gap 2: the buffered path queued its signature persists inside
717
+ // buildResponseJSON; bound the durability window before the JSON becomes
718
+ // externally visible.
719
+ await awaitThoughtSignatureDurability();
720
+ if (adapterResponseReachedServingTerminal(events, json)) {
721
+ commitReasoningReplayServingRoute();
722
+ }
723
+ notifyResponseComplete(json);
724
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
725
+ } catch (error) {
726
+ if (!isTranslatorBudgetExceededError(error)) throw error;
727
+ runTurnAbort.abort();
728
+ return formatErrorResponse(502, "upstream_error", "upstream translation buffer exceeded the safe limit", {
729
+ code: "translation_buffer_limit",
730
+ });
731
+ } finally {
732
+ translatorBudget.releaseRetained(searchOutputBytes, { kind: "retained_collectors" });
509
733
  }
510
- let providerState: OcxProviderContinuationState | undefined;
511
- const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
512
- translatorBudget,
513
- replayCacheScope: parsed._reasoningReplayScope,
514
- hideThinkingSummary: parsed.options.hideThinkingSummary,
515
- toolNsMap,
516
- declaredToolNames,
517
- enforceDeclaredToolNames,
518
- toolParameterSchemas,
519
- freeformToolNames,
520
- toolSearchToolNames,
521
- ...(routedCompaction ? { compaction: true } : {}),
522
- onProviderState: state => { providerState = state; },
523
- onUsage: usage => {
524
- transportState.bindKeyUsageFromBridge(usage);
525
- },
526
- });
527
- if (!routedCompaction) {
528
- rememberKiroDeliveredFinalAnswer(transportState.adapter.name, json);
529
- rememberResponseState(
530
- parsed._rawBody,
531
- json,
532
- continuationStateForResponse(providerState),
533
- responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
534
- );
535
- }
536
- // #1926 gap 2: the buffered path queued its signature persists inside
537
- // buildResponseJSON; bound the durability window before the JSON becomes
538
- // externally visible.
539
- await awaitThoughtSignatureDurability();
540
- if (adapterResponseReachedServingTerminal(events, json)) {
541
- commitReasoningReplayServingRoute();
734
+ } finally {
735
+ // The buffered turn ends inside this function, so its every exit —
736
+ // JSON response, combo failure, or thrown error — hands the lease back.
737
+ releaseSearchProbeLease();
542
738
  }
543
- notifyResponseComplete(json);
544
- return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
545
739
  }
@@ -143,8 +143,11 @@ export async function executeResponsesSidecars(
143
143
  // Web-search's loop only supports buildRequest/fetch/parseStream — NOT adapter.runTurn. Sending
144
144
  // Cursor/runTurn requests into runWithWebSearch produces empty HTTP failures. So:
145
145
  // - non-runTurn: web-search wins over image when both eligible (documented priority)
146
- // - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
147
- // can proceed for web-search-only turns
146
+ // - runTurn: a search plan takes priority over media bridges and is executed
147
+ // by executeResponsesRunTurn; the fetch loop below remains fetch-only.
148
+ // LOCAL PATCH (runturn-websearch): runTurn adapters run their own web-search
149
+ // loop inside executeResponsesRunTurn (src/web-search/run-turn-loop.ts); the
150
+ // fetch-path loop below stays non-runTurn-only.
148
151
  const wsPlan = !routedCompaction
149
152
  ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, {
150
153
  admission: options.admission, codexAuthPolicy: options.codexAuthPolicy, providerName: route.providerName,
@@ -280,7 +283,7 @@ export async function executeResponsesSidecars(
280
283
  });
281
284
  return rotatedAdapter;
282
285
  };
283
- if ((imgPlan || vidPlan) && canRunWebSearch) {
286
+ if ((imgPlan || vidPlan) && wsPlan) {
284
287
  // Web search takes priority when both are active — the media bridge cannot run
285
288
  // alongside runWithWebSearch. Surface a runtime signal so the user knows their
286
289
  // configured video/image bridge was skipped for this turn, rather than silently
@@ -288,7 +291,7 @@ export async function executeResponsesSidecars(
288
291
  if (vidPlan) console.warn("[videos] video bridge skipped: web search is active for this turn");
289
292
  if (imgPlan) console.warn("[images] image bridge skipped: web search is active for this turn");
290
293
  }
291
- if ((imgPlan || vidPlan) && (!wsPlan || transportState.adapter.runTurn)) {
294
+ if ((imgPlan || vidPlan) && !wsPlan) {
292
295
  // The image bridge detects a hosted image_generation tool and requires streaming.
293
296
  // The video bridge activates from config and injects a tool — it also needs streaming
294
297
  // (the loop returns SSE). For video-only (no imgPlan) on a non-streaming request, skip
@@ -256,6 +256,13 @@ export async function injectSystemEnv(
256
256
  };
257
257
 
258
258
  try {
259
+ // Versions before 2.11 injected this key, which prevents Claude's gateway model
260
+ // discovery. Records written after it left the tracking list no longer name it,
261
+ // so membership cannot find it — clear it whenever the live value is the only
262
+ // one we ever injected. A user-set "1" is indistinguishable and is cleared too.
263
+ if (launchctlGetenv("_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL") === "1") {
264
+ unsetLaunchctlEnv("_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL");
265
+ }
259
266
  inject("ANTHROPIC_BASE_URL", destination.origin);
260
267
  inject("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", "1");
261
268
  if (markerMode === "proxy") {