@bitkyc08/opencodex 2.75.0 → 2.76.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 (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { ResponsesRequestContext, ResponsesAdmissionState, ResponsesDispatchers } from "./core-options";
2
+ import { capturePolicyRequestRoute, assertPolicyNormalizedRoute, recordPolicyPreparedDestination, policyCandidateRefusalResponse } from "./policy-request-scope";
2
3
  import {
3
4
  agentTaskRecoveryConfig,
4
5
  restoreCachedEncryptedAgentTasks,
@@ -96,6 +97,7 @@ import {
96
97
  resolveSubagentFallbackModelEligibility,
97
98
  canPassThroughEncryptedV2AgentTask,
98
99
  applyFinalRouteRequestNormalization,
100
+ previewXaiOauthWireModel,
99
101
  } from "./core-normalize";
100
102
  import {
101
103
  cachedDeniedCodexAccountIdsForModel,
@@ -146,6 +148,8 @@ import {
146
148
  conversationCarriesUploadedFiles,
147
149
  } from "./account-change-state";
148
150
 
151
+ import { redactHostedImageDisplayPaths } from "../responses-hosted-image-display";
152
+
149
153
  /** Parses, selects, and admits one request without changing the dispatch policy. */
150
154
  export async function prepareResponsesRequest(
151
155
  requestContext: Pick<ResponsesRequestContext, "options" | "config" | "req" | "logCtx">,
@@ -153,6 +157,8 @@ export async function prepareResponsesRequest(
153
157
  requestDispatchers: ResponsesDispatchers,
154
158
  ) {
155
159
  const { options, config, req, logCtx } = requestContext;
160
+ const policyScope = options.policyRequestScope ?? { triedDestinations: new Set<string>() };
161
+ const concreteSelection = options.comboAttempt || options.policyFallbackCandidate !== undefined;
156
162
 
157
163
  // The Chat and Anthropic surfaces replay through here with a Responses-shaped body,
158
164
  // so an omitted value means a genuine Responses inbound.
@@ -168,6 +174,8 @@ export async function prepareResponsesRequest(
168
174
  }
169
175
  return decodeRequestErrorResponse(err, "responses");
170
176
  }
177
+ // Remove display-only artifact paths before helpers, combo dispatch and upstream replay.
178
+ redactHostedImageDisplayPaths(body);
171
179
  observeCacheDiagnosticInbound(
172
180
  logCtx,
173
181
  body,
@@ -176,7 +184,7 @@ export async function prepareResponsesRequest(
176
184
  ? "system-derived"
177
185
  : options.promptCacheKeyIsSharedCohort === false ? "metadata-derived" : "caller",
178
186
  );
179
- if (!options.comboAttempt && !options.compactionRoutingOverride && inboundWire === "responses") {
187
+ if (!concreteSelection && !options.compactionRoutingOverride && inboundWire === "responses") {
180
188
  options.compactionRoutingOverride = applyCompactionRoutingOverride(body, req.headers, config, {
181
189
  endpoint: "responses",
182
190
  transport: options.inboundTransport,
@@ -186,20 +194,21 @@ export async function prepareResponsesRequest(
186
194
  // metadata, never from the model id: Phase 1 shares `gpt-5.6-luna` with the app's title/commit
187
195
  // helper calls. Read here, ahead of the shadow intercept below, because the phase decision is the
188
196
  // more specific of the two settings and must be the one that survives when both match one request.
189
- const memoryModelPhase = options.memoryModelPhase
197
+ // A JEV decision-model call is the router's own auxiliary turn, never a Codex memory phase.
198
+ const memoryModelPhase = options.internalDecisionCall ? undefined : options.memoryModelPhase
190
199
  ?? ((config.memoryModels?.extract || config.memoryModels?.consolidation)
191
- && !options.comboAttempt && !options.compactionRoutingOverride && inboundWire === "responses"
200
+ && !concreteSelection && !options.compactionRoutingOverride && inboundWire === "responses"
192
201
  ? detectMemoryModelPhase(body, req.headers, { transport: options.inboundTransport }) ?? undefined
193
202
  : undefined);
194
203
  const memoryModelTarget = memoryModelPhase ? configuredMemoryModel(config, memoryModelPhase) : undefined;
195
204
  // A combo child is a synthetic replay of the parent's decision: its model is already the target's
196
205
  // concrete provider/model, so neither site below may rewrite or re-resolve it. It keeps the phase
197
206
  // through `options.memoryModelPhase` instead, which is what applies the phase effort.
198
- const memoryModelApplies = memoryModelTarget !== undefined && options.comboAttempt !== true;
207
+ const memoryModelApplies = memoryModelTarget !== undefined && !concreteSelection;
199
208
  options.onRequestBodyParsed?.(body);
200
209
  // An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
201
210
  // as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
202
- const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
211
+ const comboRows = !concreteSelection && body && typeof body === "object" && !Array.isArray(body)
203
212
  && typeof (body as { model?: unknown }).model === "string"
204
213
  // One parse for both grammars, from the selector as the client sent it. Parsing them
205
214
  // separately made the outcome depend on which ran first.
@@ -228,7 +237,7 @@ export async function prepareResponsesRequest(
228
237
  }
229
238
  // Compaction may send the last client-visible bare model after a combo switch.
230
239
  // Configured selectors take precedence; otherwise recall before combo dispatch (#3891).
231
- if (!options.comboAttempt && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
240
+ if (!concreteSelection && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
232
241
  const rawModel = (body as { model?: unknown }).model;
233
242
  const rawInput = (body as { input?: unknown }).input;
234
243
  const isCompactionTrigger = Array.isArray(rawInput)
@@ -265,7 +274,7 @@ export async function prepareResponsesRequest(
265
274
  // A spawned sub-agent turn names its model on purpose; gpt-6-luna is both the helper
266
275
  // slug and a default sub-agent model, so neither intercept site may rewrite that turn.
267
276
  const threadSpawn = isThreadSpawnRequest(req.headers);
268
- if (!options.comboAttempt && !options.compactionRoutingOverride && !threadSpawn && !memoryModelApplies && body && typeof body === "object" && !Array.isArray(body)) {
277
+ if (!concreteSelection && !options.compactionRoutingOverride && !options.internalDecisionCall && !threadSpawn && !memoryModelApplies && body && typeof body === "object" && !Array.isArray(body)) {
269
278
  const shadowIntercept = config.shadowCallIntercept;
270
279
  const rawShadowModel = (body as { model?: unknown }).model;
271
280
  if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
@@ -283,7 +292,12 @@ export async function prepareResponsesRequest(
283
292
  }
284
293
  }
285
294
  }
286
- const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
295
+ const comboId = !concreteSelection ? comboIdFromRawBody(body, config) : null;
296
+ // A decision model may be a plain model or a non-JEV combo. A JEV combo here would recurse
297
+ // into another decision, so the call is refused and the parent decision fails open.
298
+ if (options.internalDecisionCall && comboId && config.combos?.[comboId]?.strategy === "jev") {
299
+ return formatErrorResponse(400, "invalid_request_error", "A JEV decision model cannot be a JEV combo");
300
+ }
287
301
  if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
288
302
  options.onRequestBodyRead?.();
289
303
  return requestDispatchers.handleComboResponses(req, body, comboId, config, logCtx, {
@@ -507,13 +521,17 @@ export async function prepareResponsesRequest(
507
521
  // request rather than a destination it never asked for.
508
522
  const inboundSelector = parsed.modelId;
509
523
  const admissionScope = resolveAdmissionModelScope(config, options.admission);
510
- const captureInboundRoutePolicy = (candidate: RouteResult): RouteResult => {
524
+ const captureInboundRoutePolicy = (candidate: RouteResult, captureRequestPolicy = true): RouteResult => {
511
525
  // Every route this request path produces passes through here: the direct
512
526
  // name, an alias, a policy or combo selection, a compaction override, a
513
527
  // shadow-intercept target and both subagent-fallback re-routes. Checking
514
528
  // the key's scope at this one point is what stops a rewrite from reaching
515
529
  // a destination the front door would have refused.
516
- assertRouteAllowedByScope(admissionScope, inboundSelector, candidate);
530
+ // A shadow-intercept target is probed before interception is decided, so it
531
+ // skips the request-owned policy capture here; the accepted target is
532
+ // captured where it becomes the request's route. A declined policy target
533
+ // must not lend its eligibility or "policy" route kind to the request.
534
+ if (captureRequestPolicy) capturePolicyRequestRoute(policyScope, candidate);
517
535
  candidate.staticPolicy = captureRouteStaticPolicy(
518
536
  candidate.providerName,
519
537
  candidate.modelId,
@@ -521,6 +539,11 @@ export async function prepareResponsesRequest(
521
539
  candidate.staticPolicy.effectiveAlias,
522
540
  inboundWire,
523
541
  );
542
+ // Fast-only keys authorize the same billed lane that final normalization serializes.
543
+ assertRouteAllowedByScope(admissionScope, inboundSelector, {
544
+ providerName: candidate.providerName,
545
+ modelId: previewXaiOauthWireModel(parsed, candidate, config, inboundWire),
546
+ });
524
547
  return candidate;
525
548
  };
526
549
  try {
@@ -528,11 +551,12 @@ export async function prepareResponsesRequest(
528
551
  // no canonical OpenAI route for (#2901). Only the initial compaction route
529
552
  // may fall back to the configured default provider; combo attempts and the
530
553
  // later fallback/recovery re-routes keep the ordinary reservation.
531
- const resolveRoute = (modelId: string) => captureInboundRoutePolicy(options.comboAttempt
554
+ const routeInbound = (modelId: string) => concreteSelection
532
555
  ? routeConcreteModel(config, modelId)
533
556
  : parsed._compactionRequest === true
534
557
  ? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
535
- : routeModel(config, modelId, evidenceFromBody(parsed._rawBody)));
558
+ : routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
559
+ const resolveRoute = (modelId: string) => captureInboundRoutePolicy(routeInbound(modelId));
536
560
  // The phase's destination. Resolved through the admission-scoped resolver every other route
537
561
  // uses, and it fails closed exactly like the shadow target: falling back to the native model
538
562
  // would spend the quota the operator routed away from, without their choosing it.
@@ -554,7 +578,7 @@ export async function prepareResponsesRequest(
554
578
  }
555
579
  const _sci = config.shadowCallIntercept;
556
580
  let shadowRoute: RouteResult | undefined;
557
- if (!memoryRoute && !options.memoryModelPhase && !options.compactionRoutingOverride && !threadSpawn && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
581
+ if (!concreteSelection && !memoryRoute && !options.memoryModelPhase && !options.compactionRoutingOverride && !options.internalDecisionCall && !threadSpawn && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
558
582
  const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
559
583
  let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
560
584
  try {
@@ -563,7 +587,7 @@ export async function prepareResponsesRequest(
563
587
  } catch { /* Native Codex helper calls remain OpenAI-owned without an enabled OpenAI route. */ }
564
588
  // A dead target fails this helper call once, before any send; it never falls through to
565
589
  // the native source model or to the default provider (#5618).
566
- const target = resolveShadowCallTarget(_sci.model, resolveRoute);
590
+ const target = resolveShadowCallTarget(_sci.model, modelId => captureInboundRoutePolicy(routeInbound(modelId), false));
567
591
  if ("unavailable" in target) {
568
592
  logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(sourcePrefix);
569
593
  logCtx.errorCode = INTERCEPT_TARGET_UNAVAILABLE_CODE;
@@ -587,11 +611,12 @@ export async function prepareResponsesRequest(
587
611
  );
588
612
  // Helpers must not resume/append into the parent thread's Cursor conversation.
589
613
  parsed._cursorIsolateConversation = true;
590
- shadowRoute = targetRoute;
614
+ shadowRoute = capturePolicyRequestRoute(policyScope, targetRoute);
591
615
  }
592
616
  }
593
617
  if (parsed._compactionRequest === true || options.compactionRoutingOverride) parsed._cursorIsolateConversation = true;
594
- route = memoryRoute ?? shadowRoute ?? resolveRoute(parsed.modelId);
618
+ route = memoryRoute ?? shadowRoute ?? resolveRoute(options.policyFallbackCandidate
619
+ ? `${options.policyFallbackCandidate.provider}/${options.policyFallbackCandidate.model}` : parsed.modelId);
595
620
  // Name the phase in the persisted route decision, so the request log says why this turn went to
596
621
  // the memory destination instead of leaving it looking like a plain user selection. Set here, on
597
622
  // the resolved route, so a combo child's own route carries it too.
@@ -615,7 +640,10 @@ export async function prepareResponsesRequest(
615
640
  if (parsed._compactionRequest === true) parsed._portableCompaction = true;
616
641
  }
617
642
  logCtx.routeDecision = route.routeDecision;
643
+ logCtx.policyEligibility = route.policyEligibility;
618
644
  } catch (err) {
645
+ const policyRefusal = policyCandidateRefusalResponse(err);
646
+ if (policyRefusal) return policyRefusal;
619
647
  if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
620
648
  if (err instanceof NoAvailableComboTargetsError) {
621
649
  return comboUnavailable(err.comboId);
@@ -827,7 +855,10 @@ export async function prepareResponsesRequest(
827
855
  );
828
856
  credentialDomainWasRewritten = true;
829
857
  logCtx.routeDecision = route.routeDecision;
858
+ logCtx.policyEligibility = route.policyEligibility;
830
859
  } catch (err) {
860
+ const policyRefusal = policyCandidateRefusalResponse(err);
861
+ if (policyRefusal) return policyRefusal;
831
862
  if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
832
863
  if (err instanceof NoAvailableComboTargetsError) {
833
864
  return comboUnavailable(err.comboId);
@@ -1038,7 +1069,10 @@ export async function prepareResponsesRequest(
1038
1069
  );
1039
1070
  credentialDomainWasRewritten = true;
1040
1071
  logCtx.routeDecision = route.routeDecision;
1072
+ logCtx.policyEligibility = route.policyEligibility;
1041
1073
  } catch (err) {
1074
+ const policyRefusal = policyCandidateRefusalResponse(err);
1075
+ if (policyRefusal) return policyRefusal;
1042
1076
  if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
1043
1077
  if (err instanceof NoAvailableComboTargetsError) {
1044
1078
  return comboUnavailable(err.comboId);
@@ -1178,6 +1212,7 @@ export async function prepareResponsesRequest(
1178
1212
  // upstream for reliability (#875); the answer must then be reframed to SSE
1179
1213
  // for streaming clients.
1180
1214
  const clientRequestedStream = parsed.stream;
1215
+ const admittedPolicyRoute = { providerName: route.providerName, modelId: route.modelId };
1181
1216
  await applyFinalRouteRequestNormalization({
1182
1217
  parsed,
1183
1218
  route,
@@ -1188,13 +1223,23 @@ export async function prepareResponsesRequest(
1188
1223
  inboundTransport: options.inboundTransport,
1189
1224
  claudeGoAffinity: options.claudeGoAffinity,
1190
1225
  });
1226
+ try {
1227
+ assertPolicyNormalizedRoute(policyScope, admittedPolicyRoute, route);
1228
+ } catch (error) {
1229
+ const policyRefusal = policyCandidateRefusalResponse(error);
1230
+ if (policyRefusal) return policyRefusal;
1231
+ throw error;
1232
+ }
1191
1233
  // Normalization is the last thing that can move the destination: resolving an
1192
1234
  // OpenAI virtual model rewrites route.modelId to the wire id that will
1193
1235
  // actually be billed. A scope checked only before this would authorize the
1194
1236
  // public selector and send the wire model, so the settled route is checked
1195
1237
  // once more here.
1196
- if (!routeAllowedByScope(admissionScope, route)) {
1197
- return admissionModelDeniedResponse(new AdmissionModelDeniedError(inboundSelector, route));
1238
+ const scopedDestination = parsed._wireModelOverride === undefined
1239
+ ? route
1240
+ : { providerName: route.providerName, modelId: parsed._wireModelOverride };
1241
+ if (!routeAllowedByScope(admissionScope, scopedDestination)) {
1242
+ return admissionModelDeniedResponse(new AdmissionModelDeniedError(inboundSelector, scopedDestination));
1198
1243
  }
1199
1244
  // Attribute local auth/cooldown failures to the public selector too; exact auth may fail before
1200
1245
  // the normal post-resolution provider label is assigned.
@@ -1304,6 +1349,13 @@ export async function prepareResponsesRequest(
1304
1349
 
1305
1350
  route.provider = applyCodexAuthContextToProvider(route.provider, admissionState.authCtx, route.codexAccountMode);
1306
1351
  applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
1352
+ try {
1353
+ recordPolicyPreparedDestination(policyScope, route.providerName, parsed._wireModelOverride ?? parsed.modelId);
1354
+ } catch (error) {
1355
+ const policyRefusal = policyCandidateRefusalResponse(error);
1356
+ if (policyRefusal) return policyRefusal;
1357
+ throw error;
1358
+ }
1307
1359
  logCtx.provider = route.codexAccountNamespace
1308
1360
  ? `${route.providerName}-${route.codexAccountNamespace}`
1309
1361
  : formatCodexProviderForLog(route.providerName, codexLogAccountId(admissionState.authCtx), config);
@@ -1,3 +1,5 @@
1
+ import { anthropicFamilyRejected, claimAnthropicFamilyRevalidation } from "../../oauth/anthropic-model-quota";
2
+ import { anthropicRatePauseUntil } from "../../oauth/anthropic-rate-limit-policy";
1
3
  import { bindAnthropicRefusalCredential } from "../../oauth/anthropic-account-refusal";
2
4
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
3
5
  import type { PreparedResponsesRequest } from "./request-prepare";
@@ -20,6 +22,7 @@ import {
20
22
  isAnthropicAccountPoolEnabled,
21
23
  getAnthropicPoolAccessSnapshot,
22
24
  getAnthropicAccountHealthSnapshot,
25
+ AnthropicAccountCooldownError,
23
26
  getEligibleAnthropicAccounts,
24
27
  commitAnthropicSelectionRouting,
25
28
  formatAnthropicProviderForLog,
@@ -174,7 +177,7 @@ export async function prepareResponsesTransport(
174
177
  ? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
175
178
  : await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
176
179
  }
177
- if (anthropicRouteDecision && !routeCandidates(getEligibleAnthropicAccounts(), anthropicRouteDecision).includes(candidate.accountId)) return null;
180
+ if (anthropicRouteDecision && !routeCandidates(getEligibleAnthropicAccounts(Date.now(), route.modelId), anthropicRouteDecision).includes(candidate.accountId)) return null;
178
181
  const committed = await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
179
182
  expectedSelection: oauthSelection,
180
183
  expectedCredentialGeneration: candidate.generation,
@@ -183,7 +186,7 @@ export async function prepareResponsesTransport(
183
186
  if (committed) {
184
187
  if (route.providerName === "anthropic" && !commitAnthropicSelectionRouting(
185
188
  candidate.accountId, oauthSelection, committed,
186
- { config, sessionKey: anthropicSessionKey, reason: anthropicReason, expectedCredentialGeneration: candidate.generation, routeDecision: anthropicRouteDecision },
189
+ { config, sessionKey: anthropicSessionKey, reason: anthropicReason, expectedCredentialGeneration: candidate.generation, routeDecision: anthropicRouteDecision, model: route.modelId },
187
190
  )) return null;
188
191
  oauthSelection = committed;
189
192
  servingOAuthSnapshot = candidate;
@@ -197,7 +200,7 @@ export async function prepareResponsesTransport(
197
200
  // A revision also changes on per-account policy edits. Re-evaluate the selector
198
201
  // after credential waits even without a model route, rather than reusing stale active.
199
202
  const revisedAnthropic = route.providerName === "anthropic"
200
- ? resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision) : null;
203
+ ? resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision, route.modelId) : null;
201
204
  const revisedAnthropicId = revisedAnthropic?.accountId;
202
205
  if (route.providerName === "anthropic" && !revisedAnthropicId) return null;
203
206
  if (revisedAnthropic) anthropicReason = revisedAnthropic.reason;
@@ -217,8 +220,11 @@ export async function prepareResponsesTransport(
217
220
  const admitted = await commitResolvedOAuthSelection(candidate);
218
221
  if (!admitted) throw new Error("OAuth selection changed during credential recovery");
219
222
  if (kiroLoadEnabled && options.accountLoad?.lease?.accountId !== admitted.accountId) {
220
- const replayLease = await acquireAccountLease("kiro", admitted.accountId, { maxConcurrentPerAccount: kiroCap });
221
- if (!replayLease || !options.accountLoad) {
223
+ const signal = options.abortSignal ?? req.signal;
224
+ const replayLease = await acquireAccountLease("kiro", admitted.accountId, {
225
+ maxConcurrentPerAccount: kiroCap, signal,
226
+ });
227
+ if (!replayLease || !options.accountLoad || options.accountLoad.cancelled || signal.aborted) {
222
228
  replayLease?.release();
223
229
  throw new Error("Kiro replay account capacity is full");
224
230
  }
@@ -263,16 +269,20 @@ export async function prepareResponsesTransport(
263
269
  retryParsed: OcxParsedRequest = parsed,
264
270
  ): Promise<OAuthAccessSnapshot | null> => {
265
271
  if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return null;
272
+ const signal = options.abortSignal ?? req.signal;
266
273
  let speculative = kiroLoadEnabled && options.accountLoad?.lease?.accountId !== snapshot.accountId
267
- ? await acquireAccountLease("kiro", snapshot.accountId, { maxConcurrentPerAccount: kiroCap }) : null;
274
+ ? await acquireAccountLease("kiro", snapshot.accountId, { maxConcurrentPerAccount: kiroCap, signal }) : null;
275
+ if (speculative && (options.accountLoad?.cancelled || signal.aborted)) { speculative.release(); return null; }
268
276
  if (kiroLoadEnabled && options.accountLoad?.lease?.accountId !== snapshot.accountId && !speculative) return null;
269
277
  let committed: OAuthAccessSnapshot | null;
270
278
  try { committed = await commitResolvedOAuthSelection(snapshot); }
271
279
  catch (error) { speculative?.release(); throw error; }
272
280
  if (!committed) { speculative?.release(); return null; }
281
+ if (speculative && (options.accountLoad?.cancelled || signal.aborted)) { speculative.release(); return null; }
273
282
  if (kiroLoadEnabled && committed.accountId !== (speculative?.accountId ?? options.accountLoad?.lease?.accountId)) {
274
283
  speculative?.release();
275
- speculative = await acquireAccountLease("kiro", committed.accountId, { maxConcurrentPerAccount: kiroCap });
284
+ speculative = await acquireAccountLease("kiro", committed.accountId, { maxConcurrentPerAccount: kiroCap, signal });
285
+ if (speculative && (options.accountLoad?.cancelled || signal.aborted)) { speculative.release(); return null; }
276
286
  if (!speculative) return null;
277
287
  }
278
288
  if (speculative && options.accountLoad) {
@@ -299,6 +309,12 @@ export async function prepareResponsesTransport(
299
309
  parsed._kiroAuthContext = kiroContext;
300
310
  if (retryParsed !== parsed) retryParsed._kiroAuthContext = { ...kiroContext };
301
311
  }
312
+ if (route.providerName === "zed") {
313
+ // Zed signs with its own user id; `snapshot.accountId` is the local slot hash.
314
+ const zedContext = { userId: snapshot.providerUserId ?? "" };
315
+ parsed._zedAuthContext = zedContext;
316
+ if (retryParsed !== parsed) retryParsed._zedAuthContext = { ...zedContext };
317
+ }
302
318
  // Re-stamp: a request that rotated accounts must be attributed to the account that actually
303
319
  // served it. All three rotation sites funnel through here, so this is the only re-stamp
304
320
  // needed -- and putting it anywhere else would let one of the three drift.
@@ -345,7 +361,7 @@ export async function prepareResponsesTransport(
345
361
  const row = getAccountCredentialWithStatus(route.providerName, binding.snapshot.accountId);
346
362
  return selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision
347
363
  && !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
348
- && (route.providerName !== "anthropic" || !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId))
364
+ && (route.providerName !== "anthropic" || !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId) && !anthropicRatePauseUntil(binding.snapshot.accountId) && !anthropicFamilyRejected(binding.snapshot.accountId, route.modelId))
349
365
  && credentialGeneration(row.credential) === binding.snapshot.generation;
350
366
  };
351
367
  const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
@@ -412,11 +428,11 @@ export async function prepareResponsesTransport(
412
428
  let candidate = servingOAuthSnapshot;
413
429
  if (route.providerName === "anthropic") {
414
430
  oauthSelection = captureOAuthAccountSelection(route.providerName);
415
- const accountId = await resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision);
431
+ const accountId = await resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
416
432
  candidate = await getAnthropicPoolAccessSnapshot(accountId);
417
433
  }
418
434
  if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed)) {
419
- if (route.providerName === "anthropic") await resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision);
435
+ if (route.providerName === "anthropic") await resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
420
436
  throw new Error("OAuth account selection changed before dispatch");
421
437
  }
422
438
  } else {
@@ -503,18 +519,22 @@ export async function prepareResponsesTransport(
503
519
  // Reselection can choose a provider override instead of the supplied executor.
504
520
  // Either way the send crosses the physical boundary, so the connection policy is
505
521
  // applied around whichever implementation was just selected (#4992).
506
- commitKeyAttemptSend();
507
522
  // The binding travels with the send, so a rebuilt request resolves its provider route
508
523
  // against the destination it is actually going to rather than the one this dispatch
509
524
  // started with. Account reselection can move the upstream host, which would otherwise
510
525
  // apply a host-scoped decision to a different host.
511
- const response = await sendWithConnectionPolicy(
512
- fetchImpl,
513
- destination,
514
- { ...dispatchInit, redirect: "manual" },
515
- { providerName: route.providerName, provider: route.provider },
516
- );
517
- if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
526
+ const releaseFamily = snapshot ? claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
527
+ if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
528
+ let response: Response;
529
+ try {
530
+ commitKeyAttemptSend();
531
+ response = await sendWithConnectionPolicy(
532
+ fetchImpl,
533
+ destination,
534
+ { ...dispatchInit, redirect: "manual" },
535
+ { providerName: route.providerName, provider: route.provider },
536
+ );
537
+ } finally { releaseFamily(); }
518
538
  // Observe each physical response before retries replace it. The binding belongs to
519
539
  // this dispatch, so a manual switch cannot file A's headers against B. Header
520
540
  // overrides and credential replacement make ownership unprovable: skip those writes.
@@ -523,10 +543,11 @@ export async function prepareResponsesTransport(
523
543
  const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
524
544
  if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation) {
525
545
  bindAnthropicRefusalCredential(response, snapshot);
526
- recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, response.headers, writerGeneration);
546
+ recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId);
527
547
  }
528
548
  } catch { /* best-effort observation cannot fail the response */ }
529
549
  }
550
+ if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
530
551
  return response;
531
552
  }
532
553
  const nextAdapter = await refreshDispatchAdapter(requestParsed);
@@ -577,13 +598,13 @@ export async function prepareResponsesTransport(
577
598
  const routeResult = resolveAnthropicModelRoute(config, route.modelId);
578
599
  if (routeResult.error) return formatErrorResponse(400, "invalid_request_error", `Invalid Anthropic model routes: ${routeResult.error}`);
579
600
  anthropicRouteDecision = routeResult.decision;
580
- const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision);
601
+ const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision, route.modelId);
581
602
  if (!selection.accountId) {
582
603
  if (selection.reason === "paused") return formatErrorResponse(403, "permission_error", "Anthropic OAuth accounts are paused. Resume an account in account settings and retry.");
583
604
  // Route names may resemble account IDs; log only the matched rule position.
584
605
  if (anthropicRouteDecision) console.warn(`[anthropic-pool] route:#${anthropicRouteDecision.position} ${selection.reason}; answering locally`);
585
606
  if (selection.reason === "all-cooled") {
586
- const retryAfterSec = getAnthropicPoolRetryAfterSeconds(Date.now(), anthropicRouteDecision);
607
+ const retryAfterSec = getAnthropicPoolRetryAfterSeconds(Date.now(), anthropicRouteDecision, route.modelId);
587
608
  return formatErrorResponse(
588
609
  429,
589
610
  "rate_limit_error",
@@ -667,11 +688,18 @@ export async function prepareResponsesTransport(
667
688
  if (safetyAlternateId && admitted.accountId !== safetyAlternateId)
668
689
  return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
669
690
  if (kiroLoadEnabled) {
691
+ const transportSignal = options.abortSignal ?? req.signal;
670
692
  const lease = await acquireAccountLease("kiro", admitted.accountId, {
671
- maxConcurrentPerAccount: kiroCap, waitMs: KIRO_ACCOUNT_WAIT_MS, signal: options.abortSignal ?? req.signal,
693
+ maxConcurrentPerAccount: kiroCap, waitMs: KIRO_ACCOUNT_WAIT_MS, signal: transportSignal,
672
694
  });
673
- if (!lease) return (options.abortSignal ?? req.signal).aborted
695
+ if (!lease) return transportSignal.aborted
674
696
  ? clientCancelledResponse() : capacityResponse();
697
+ // The lease may be granted between the holder's cleanup and this install;
698
+ // re-check cancellation before the holder can no longer reach it.
699
+ if (options.accountLoad?.cancelled || transportSignal.aborted) {
700
+ lease.release();
701
+ return clientCancelledResponse();
702
+ }
675
703
  if (options.accountLoad) options.accountLoad.lease = lease;
676
704
  else lease.release();
677
705
  }
@@ -717,6 +745,9 @@ export async function prepareResponsesTransport(
717
745
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
718
746
  parsed._kiroAuthContext = { ...(resolved.kiro ?? {}) };
719
747
  }
748
+ if (route.providerName === "zed") {
749
+ parsed._zedAuthContext = { userId: resolved.providerUserId ?? "" };
750
+ }
720
751
  // Project identity belongs to the admitted account on EVERY request, including
721
752
  // the request after a pool transition made that account the persisted active one.
722
753
  if (route.provider.googleMode === "cloud-code-assist") {
@@ -62,6 +62,7 @@ const RUNTURN_WS_ROUTE_STATE_KEYS = [
62
62
  "_cursorConversationId",
63
63
  "_cursorClientThreadId",
64
64
  "_kiroAuthContext",
65
+ "_zedAuthContext",
65
66
  "_providerContinuation",
66
67
  "_providerContinuationOwner",
67
68
  "_providerContinuationCandidate",
@@ -177,6 +177,11 @@ export async function executeResponsesSidecars(
177
177
  const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
178
178
  const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
179
179
  const canRunWebSearch = !!wsPlan && !transportState.adapter.runTurn;
180
+ let sidecarOutputStarted = false;
181
+ const sidecarHasCommittedOutput = () => sidecarOutputStarted
182
+ || (logCtx.activeAttempt?.deliverySummary?.semanticBytes ?? 0) > 0
183
+ || (logCtx.activeAttempt?.deliverySummary?.sideEffectEvents ?? 0) > 0;
184
+ const noteSidecarOutput = () => { sidecarOutputStarted = true; options.onFirstOutput?.(); };
180
185
  const rotateSidecarProviderOn429 = async (
181
186
  retryAfter: string | null,
182
187
  responseHeaders?: Headers,
@@ -270,7 +275,8 @@ export async function executeResponsesSidecars(
270
275
  const nextAccountId = await rotateAnthropicAccountOnResponse(
271
276
  originalResponse ?? new Response(null, { status: 429, headers: responseHeaders ?? (retryAfter ? { "retry-after": retryAfter } : undefined) }), {
272
277
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
273
- decision: transportState.anthropicRouteDecision, signal: options.abortSignal,
278
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: options.abortSignal,
279
+ allow429Recovery: !sidecarHasCommittedOutput(), allowAccountRefusal: !sidecarHasCommittedOutput(),
274
280
  canRetry: hop.allowed && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
275
281
  });
276
282
  if (!nextAccountId) {
@@ -422,7 +428,7 @@ export async function executeResponsesSidecars(
422
428
  on429: rotateSidecarProviderOn429,
423
429
  retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
424
430
  ? null : rateLimitRetryPolicyFor(route.provider),
425
- ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
431
+ onFirstOutput: noteSidecarOutput,
426
432
  ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
427
433
  onCompletedResponse: (response, providerState) => {
428
434
  const served = transportState.replayOAuthCredentialSnapshot;
@@ -493,7 +499,7 @@ export async function executeResponsesSidecars(
493
499
  maxSearches: wsPlan.maxSearches,
494
500
  forceEmptyResponseId: true,
495
501
  abortSignal: options.abortSignal,
496
- ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
502
+ onFirstOutput: noteSidecarOutput,
497
503
  onRequestBuilt: request => {
498
504
  recordAdapterReasoning(logCtx, request);
499
505
  recordAdapterTier(logCtx, request);