@bitkyc08/opencodex 2.82.0 → 2.84.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 (266) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/cursor/catalog.ts +2 -2
  10. package/src/adapters/cursor/effort-map.ts +1 -1
  11. package/src/adapters/devin/live-models.ts +2 -2
  12. package/src/adapters/google-truncation.ts +22 -0
  13. package/src/adapters/google.ts +7 -3
  14. package/src/adapters/ollama-native.ts +47 -15
  15. package/src/adapters/openai-chat/messages.ts +8 -4
  16. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  17. package/src/adapters/openai-responses/passthrough.ts +5 -0
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/request-strips.ts +10 -2
  21. package/src/adapters/qoder/adapter.ts +16 -14
  22. package/src/bridge/errors.ts +26 -1
  23. package/src/bridge.ts +1 -1
  24. package/src/claude/context-windows.ts +57 -4
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/intercept/connect-proxy.ts +10 -3
  27. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  28. package/src/claude/intercept/picker-listener.ts +49 -1
  29. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  30. package/src/claude/intercept/settings.ts +101 -9
  31. package/src/cli/account-api.ts +65 -0
  32. package/src/cli/account-auth.ts +44 -12
  33. package/src/cli/account.ts +41 -5
  34. package/src/cli/capabilities-access-remote.ts +8 -0
  35. package/src/cli/capabilities-base.ts +13 -1
  36. package/src/cli/catalog-command-result.ts +2 -1
  37. package/src/cli/claude.ts +15 -6
  38. package/src/cli/config-command.ts +8 -2
  39. package/src/cli/debug.ts +8 -0
  40. package/src/cli/dispatch.ts +16 -3
  41. package/src/cli/export-command.ts +14 -1
  42. package/src/cli/hub-disable.ts +99 -0
  43. package/src/cli/hub.ts +4 -2
  44. package/src/cli/index.ts +13 -4
  45. package/src/cli/link-child.ts +104 -0
  46. package/src/cli/link-enrollment.ts +96 -0
  47. package/src/cli/link.ts +39 -24
  48. package/src/cli/observe-stream.ts +6 -1
  49. package/src/cli/observe.ts +10 -2
  50. package/src/cli/provider.ts +12 -0
  51. package/src/cli/registry.ts +5 -4
  52. package/src/cli/resolve.ts +1 -1
  53. package/src/cli/runtime-api.ts +27 -2
  54. package/src/cli/status.ts +104 -20
  55. package/src/client/connect.ts +39 -4
  56. package/src/client/hub-client.ts +14 -7
  57. package/src/client/hub-state.ts +25 -14
  58. package/src/client/link-home.ts +248 -0
  59. package/src/client/link-management-relay.ts +61 -0
  60. package/src/client/link-relay.ts +6 -1
  61. package/src/client/link-status.ts +14 -0
  62. package/src/client/machine-api.ts +14 -7
  63. package/src/client/machine-listener.ts +29 -4
  64. package/src/clients/config-export/model-metadata.ts +16 -0
  65. package/src/clients/config-export/zcode-store.ts +46 -11
  66. package/src/clients/config-export/zcode.ts +3 -5
  67. package/src/clients/config-export.ts +47 -17
  68. package/src/clients/omo-role-models.ts +6 -3
  69. package/src/codex/agent-role-models.ts +26 -8
  70. package/src/codex/auth-api/account-list.ts +17 -2
  71. package/src/codex/auth-api/account-selection.ts +17 -7
  72. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  73. package/src/codex/auth-api/routes.ts +5 -1
  74. package/src/codex/auth-context.ts +8 -3
  75. package/src/codex/autostart-health.ts +3 -1
  76. package/src/codex/catalog/bundled.ts +5 -1
  77. package/src/codex/catalog/effort.ts +2 -2
  78. package/src/codex/catalog/metadata.ts +4 -2
  79. package/src/codex/catalog/native-models.ts +17 -3
  80. package/src/codex/catalog/provider-models.ts +25 -0
  81. package/src/codex/desktop-app/darwin.ts +1 -1
  82. package/src/codex/desktop-app/handoff.ts +1 -1
  83. package/src/codex/desktop-app/linux.ts +1 -1
  84. package/src/codex/desktop-app/lock.ts +1 -1
  85. package/src/codex/desktop-app/types.ts +1 -1
  86. package/src/codex/desktop-app/windows.ts +1 -1
  87. package/src/codex/desktop-app-restart.ts +1 -1
  88. package/src/codex/main-account.ts +6 -7
  89. package/src/codex/native-profile-startup.ts +4 -1
  90. package/src/codex/prompt-journal.ts +20 -2
  91. package/src/codex/prompt-layers/encoding.ts +49 -0
  92. package/src/codex/prompt-layers/import-source.ts +30 -0
  93. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  94. package/src/codex/prompt-layers/toml-read.ts +52 -4
  95. package/src/codex/prompt-layers.ts +383 -20
  96. package/src/codex/prompt-lock-claim.ts +13 -3
  97. package/src/codex/prompt-lock-evidence.ts +3 -3
  98. package/src/codex/prompt-lock-owner.ts +14 -6
  99. package/src/codex/prompt-text-probe.ts +34 -5
  100. package/src/codex/quota.ts +13 -6
  101. package/src/codex/role-auto-assign.ts +2 -5
  102. package/src/codex/routing/active-account.ts +21 -0
  103. package/src/codex/routing.ts +1 -1
  104. package/src/codex/runtime.ts +196 -8
  105. package/src/codex/shim-overlay.ts +8 -4
  106. package/src/codex/shim.ts +1 -1
  107. package/src/combos/failover.ts +3 -0
  108. package/src/config/api-key-reconcile.ts +34 -0
  109. package/src/config/diagnostics.ts +16 -0
  110. package/src/config/feature-flags.ts +5 -0
  111. package/src/config/live-reconcile.ts +43 -29
  112. package/src/config/live-topology.ts +29 -0
  113. package/src/config/load-degrade.ts +2 -1
  114. package/src/config/paths.ts +3 -0
  115. package/src/config/salvage.ts +3 -1
  116. package/src/config/schema/config-schema.ts +2 -0
  117. package/src/generated/compatibility-version.json +373 -217
  118. package/src/grok/grpc-web.ts +23 -1
  119. package/src/grok/inject.ts +245 -221
  120. package/src/grok/reset-coupon-ledger.ts +293 -57
  121. package/src/grok/reset-coupons.ts +15 -6
  122. package/src/integrations/cursor-detect.ts +1 -1
  123. package/src/integrations/mutation-plan.ts +13 -7
  124. package/src/integrations/registry.ts +45 -11
  125. package/src/integrations/state.ts +19 -7
  126. package/src/integrations/target.ts +42 -5
  127. package/src/integrations/writer.ts +14 -6
  128. package/src/lib/app-owned-memory-stores.ts +4 -0
  129. package/src/lib/app-owned-memory.ts +11 -0
  130. package/src/lib/errors.ts +25 -0
  131. package/src/lib/json-byte-size.ts +31 -0
  132. package/src/lib/optional-shutdown-hooks.ts +35 -0
  133. package/src/lib/outbound-credential-registry.ts +210 -0
  134. package/src/lib/passive-read.ts +35 -0
  135. package/src/lib/redact.ts +2 -2
  136. package/src/lib/shadow-call.ts +44 -5
  137. package/src/link/codex-settings.ts +155 -0
  138. package/src/link/management-routes.ts +89 -0
  139. package/src/link/remote-probe.ts +38 -0
  140. package/src/link/ssh-argv.ts +26 -5
  141. package/src/link/ssh-runner.ts +58 -27
  142. package/src/link/status-projection.ts +22 -4
  143. package/src/link/supervisor.ts +144 -49
  144. package/src/link/tunnel-state.ts +11 -4
  145. package/src/oauth/anthropic-continuity.ts +4 -3
  146. package/src/oauth/anthropic-identity.ts +13 -2
  147. package/src/oauth/anthropic-model-quota.ts +13 -1
  148. package/src/oauth/anthropic-routing.ts +230 -23
  149. package/src/oauth/anthropic.ts +18 -4
  150. package/src/oauth/google-antigravity.ts +24 -7
  151. package/src/oauth/index.ts +25 -16
  152. package/src/oauth/kiro-account-identity.ts +106 -0
  153. package/src/oauth/kiro.ts +48 -5
  154. package/src/oauth/meta-muse-device.ts +1 -1
  155. package/src/oauth/plan.ts +29 -0
  156. package/src/oauth/store.ts +40 -3
  157. package/src/oauth/types.ts +3 -1
  158. package/src/plugins/loader.ts +11 -0
  159. package/src/providers/anthropic-billing-ownership.ts +74 -0
  160. package/src/providers/anthropic-billing-recovery.ts +31 -0
  161. package/src/providers/anthropic-billing-refusal.ts +55 -0
  162. package/src/providers/anthropic-fast.ts +1 -1
  163. package/src/providers/anthropic-reset-admission.ts +62 -0
  164. package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
  165. package/src/providers/api-key-resolve.ts +17 -2
  166. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  167. package/src/providers/devin-provider-merge-migration.ts +1 -1
  168. package/src/providers/github-copilot-auto.ts +169 -0
  169. package/src/providers/key-failover.ts +52 -5
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/model-rename-startup.ts +5 -3
  172. package/src/providers/muse-subscription-usage.ts +1 -1
  173. package/src/providers/quota/account-cache.ts +11 -0
  174. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  175. package/src/providers/quota/report-cache.ts +14 -0
  176. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  177. package/src/providers/quota.ts +4 -1
  178. package/src/providers/registry/entries-core.ts +20 -20
  179. package/src/providers/registry/entries-extended.ts +4 -1
  180. package/src/providers/registry/model-seeds.ts +20 -3
  181. package/src/providers/request-pacing.ts +9 -0
  182. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  183. package/src/providers/xai-fast-model.ts +1 -1
  184. package/src/responses/compaction.ts +14 -0
  185. package/src/responses/parser.ts +7 -1
  186. package/src/server/auth-cors.ts +42 -0
  187. package/src/server/configured-credentials.ts +194 -0
  188. package/src/server/inbound-body-admission.ts +24 -7
  189. package/src/server/index/link-listener.ts +30 -17
  190. package/src/server/index/link-management.ts +58 -0
  191. package/src/server/index/optional-listeners.ts +5 -1
  192. package/src/server/index/serve-options.ts +21 -2
  193. package/src/server/index.ts +1 -1
  194. package/src/server/lifecycle.ts +33 -5
  195. package/src/server/management/agent-settings-routes.ts +28 -10
  196. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  197. package/src/server/management/codex-agent-role-routes.ts +19 -6
  198. package/src/server/management/codex-prompt-routes.ts +136 -5
  199. package/src/server/management/codex-role-auto-assign.ts +1 -1
  200. package/src/server/management/grok-coupon-routes.ts +333 -57
  201. package/src/server/management/link-enrollment.ts +192 -0
  202. package/src/server/management/link-route-state.ts +185 -0
  203. package/src/server/management/link-routes.ts +92 -390
  204. package/src/server/management/model-rows.ts +3 -1
  205. package/src/server/management/provider-overwrite-carry.ts +10 -0
  206. package/src/server/management/provider-routes.ts +1 -0
  207. package/src/server/management/route-registry.ts +7 -5
  208. package/src/server/management/shared.ts +3 -0
  209. package/src/server/management-api.ts +3 -1
  210. package/src/server/messages-native.ts +68 -42
  211. package/src/server/relay-eager.ts +4 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-failure-attribution.ts +3 -0
  214. package/src/server/request-log-filter.ts +6 -1
  215. package/src/server/request-log-terminal-status.ts +243 -0
  216. package/src/server/request-log.ts +15 -118
  217. package/src/server/responses/adapter-continuation.ts +9 -1
  218. package/src/server/responses/adapter-dispatch.ts +293 -81
  219. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  220. package/src/server/responses/codex-ws-exchange.ts +147 -6
  221. package/src/server/responses/codex-ws-wire.ts +203 -0
  222. package/src/server/responses/collaboration.ts +36 -7
  223. package/src/server/responses/compact.ts +129 -50
  224. package/src/server/responses/context-overflow.ts +13 -1
  225. package/src/server/responses/core-codex-account.ts +105 -1
  226. package/src/server/responses/core-combo-failure.ts +14 -0
  227. package/src/server/responses/core-combo.ts +7 -2
  228. package/src/server/responses/core-errors.ts +29 -1
  229. package/src/server/responses/core-opaque-recovery.ts +5 -5
  230. package/src/server/responses/core-options.ts +2 -0
  231. package/src/server/responses/core.ts +6 -6
  232. package/src/server/responses/deferred-collab-surface.ts +158 -0
  233. package/src/server/responses/deferred-send-reporter.ts +25 -0
  234. package/src/server/responses/empty-completion-guard.ts +18 -2
  235. package/src/server/responses/fetch-helpers.ts +12 -1
  236. package/src/server/responses/jev-model-invoke.ts +1 -1
  237. package/src/server/responses/passthrough-delivery.ts +4 -1
  238. package/src/server/responses/passthrough-dispatch.ts +130 -64
  239. package/src/server/responses/passthrough-execution.ts +3 -3
  240. package/src/server/responses/request-prepare.ts +6 -4
  241. package/src/server/responses/request-transport.ts +131 -13
  242. package/src/server/responses/run-turn-execution.ts +13 -5
  243. package/src/server/responses/sidecar-execution.ts +16 -2
  244. package/src/server/responses/spend-storage-error.ts +33 -0
  245. package/src/server/responses/ws-upstream.ts +9 -1
  246. package/src/server/startup-action-control.ts +22 -2
  247. package/src/server/startup-health-cache.ts +4 -0
  248. package/src/server/system-env-shell.ts +7 -2
  249. package/src/server/system-env.ts +11 -1
  250. package/src/service/cli.ts +5 -1
  251. package/src/service/desktop-startup.ts +147 -34
  252. package/src/service/desktop-supervision.d.mts +27 -1
  253. package/src/service/desktop-supervision.mjs +111 -5
  254. package/src/service/health-sentinel-launch.ts +176 -0
  255. package/src/service/health-sentinel-main.ts +447 -0
  256. package/src/service/health-sentinel-policy.ts +241 -0
  257. package/src/service/health-sentinel-probe.ts +54 -0
  258. package/src/service/health-sentinel-windows.ts +87 -0
  259. package/src/service/linux-linger.ts +20 -0
  260. package/src/types/config.ts +5 -0
  261. package/src/types/request.ts +5 -0
  262. package/src/usage/expected-prices.ts +5 -1
  263. package/src/usage/log.ts +19 -1
  264. package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
  265. package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
  266. package/gui/dist/assets/index-D3FBCNiP.js +0 -94
@@ -1,3 +1,4 @@
1
+ import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
1
2
  import { createSteeringSettingsNormalizer } from "./native-steering-policy";
2
3
  import { nativeResponseControlEligible } from "./native-response-control";
3
4
  import { NativeInjectionReplay } from "./native-injection-replay";
@@ -8,7 +9,7 @@ import type {
8
9
  PassthroughAdmissionState,
9
10
  } from "./core-options";
10
11
  import type { PreparedResponsesRequest } from "./request-prepare";
11
- import type { ResponsesTransport } from "./request-transport";
12
+ import { CopilotAutoAdapterHandoff, type ResponsesTransport } from "./request-transport";
12
13
  import type { ResponsesEffects } from "./response-effects";
13
14
  import type { ResponsesSendBudget } from "./request-send-budget";
14
15
  import { transientSendCapFor } from "./request-send-budget";
@@ -90,7 +91,7 @@ import {
90
91
  safeHostLabel,
91
92
  } from "./fetch-helpers";
92
93
  import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
93
- import { clientCancelledResponse } from "./core-errors";
94
+ import { anthropicPoolCooldownResponse, clientCancelledResponse } from "./core-errors";
94
95
  import {
95
96
  upstreamHostCircuitOpenResponse,
96
97
  usesCodexForwardPoolAuth,
@@ -100,6 +101,8 @@ import {
100
101
  shouldRetryCodexPoolAccountQuota,
101
102
  shouldRetryCodexPoolAccountTransient,
102
103
  retryCodexPoolOnAlternateAccount,
104
+ PoolPreOutputRefusals,
105
+ poolPreOutputRefusalHold,
103
106
  } from "./core-codex-account";
104
107
  import {
105
108
  clearCodexModelDenialEvidence,
@@ -166,6 +169,9 @@ import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-ze
166
169
  import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
167
170
  import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
168
171
 
172
+ /** Transfer an uncommitted Copilot exchange to the existing translated pipeline without new admission. */
173
+ export interface PassthroughAdapterHandoff { kind: "adapter-handoff" }
174
+
169
175
  /** Prepares and recovers one native Responses exchange before client commitment. */
170
176
  export async function preparePassthroughExchange(
171
177
  requestContext: Pick<ResponsesRequestContext, "config" | "logCtx" | "options" | "req">,
@@ -192,6 +198,12 @@ export async function preparePassthroughExchange(
192
198
  | "passiveQuotaWriterGeneration"
193
199
  | "oauthDispatch"
194
200
  | "resolveSelectionAdapter"
201
+ | "resolveCopilotSelection"
202
+ | "copilotRefusalResponse"
203
+ | "oauth401ReplayAttempted"
204
+ | "activeAdapter"
205
+ | "runTurnAdapter"
206
+ | "invalidateSameTargetRequest"
195
207
  | "isOAuth401ReplayProvider"
196
208
  | "sentOAuthSnapshot"
197
209
  | "refreshResolvedOAuthSelection"
@@ -248,6 +260,8 @@ export async function preparePassthroughExchange(
248
260
  passiveQuotaWriterGeneration,
249
261
  oauthDispatch,
250
262
  resolveSelectionAdapter,
263
+ resolveCopilotSelection,
264
+ copilotRefusalResponse,
251
265
  isOAuth401ReplayProvider,
252
266
  refreshResolvedOAuthSelection,
253
267
  applyFailoverSnapshot,
@@ -732,7 +746,7 @@ export async function preparePassthroughExchange(
732
746
  // consumer's cancel to a signalled fetch, so we pass the signal and relay through relayWithAbort,
733
747
  // whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
734
748
  const upstream = new AbortController();
735
- linkAbortSignal(upstream, options.abortSignal);
749
+ const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal ?? req.signal);
736
750
  const connectMs = config.connectTimeoutMs ?? 200_000;
737
751
  let upstreamResponse: Response;
738
752
  /**
@@ -822,7 +836,7 @@ export async function preparePassthroughExchange(
822
836
  httpOnly: true,
823
837
  providerName: route.providerName,
824
838
  modelId: route.modelId,
825
- dispatchOverride: oauthDispatch(request),
839
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
826
840
  beforeDispatch: headers => {
827
841
  if (signal.aborted) throw signal.reason;
828
842
  if (!transportState.selectionIsCurrent(transportState.requestBindings.get(request))) {
@@ -896,9 +910,10 @@ export async function preparePassthroughExchange(
896
910
  describeOutboundBodyRefusal(result),
897
911
  );
898
912
  };
899
- const transportFailureResponse = (err: unknown): Response => {
913
+ const transportFailureResponse = (err: unknown): Response | PassthroughAdapterHandoff => {
914
+ cleanupUpstreamAbort();
900
915
  upstream.abort();
901
- if (options.abortSignal?.aborted) {
916
+ if (options.abortSignal?.aborted || req.signal.aborted) {
902
917
  releaseUpstreamHostAdmission(nativeHostState.lease);
903
918
  nativeHostState.lease = null;
904
919
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
@@ -915,6 +930,17 @@ export async function preparePassthroughExchange(
915
930
  ?? formatErrorResponse(429, "request_send_budget_exhausted", err.message);
916
931
  }
917
932
  const refusal = unwrapUpstreamRetryEvidenceError(err);
933
+ if (refusal instanceof CopilotAutoAdapterHandoff) {
934
+ bindRefreshedAdapter(refusal.adapter);
935
+ return handoffToChat(refusal.adapter);
936
+ }
937
+ const copilotRefusal = copilotRefusalResponse(refusal);
938
+ if (copilotRefusal) {
939
+ releaseUpstreamHostAdmission(nativeHostState.lease);
940
+ nativeHostState.lease = null;
941
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
942
+ return copilotRefusal;
943
+ }
918
944
  // Pacing may outlive the selected account's admission. No fetch occurred, so do
919
945
  // not turn an operator pause into a 502 or charge it to host/account health.
920
946
  if (refusal instanceof OAuthAccountPausedError || refusal instanceof OAuthLoginRequiredError || refusal instanceof AnthropicAccountCooldownError) {
@@ -925,12 +951,11 @@ export async function preparePassthroughExchange(
925
951
  return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal));
926
952
  }
927
953
  if (refusal instanceof AnthropicAccountCooldownError) {
928
- return formatErrorResponse(429, "rate_limit_error", refusal.message,
929
- refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
954
+ return anthropicPoolCooldownResponse(refusal, { comboAttempt: options.comboAttempt, logCtx });
930
955
  }
931
956
  return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
932
957
  }
933
- const localRefusal = mapCodexAuthContextErrorToResponse(refusal, {
958
+ const localRefusal = spendLedgerStorageErrorResponse(err, logCtx) ?? mapCodexAuthContextErrorToResponse(refusal, {
934
959
  now: Date.now(), accountSelector: route.codexAccountNamespace,
935
960
  });
936
961
  if (localRefusal) {
@@ -981,6 +1006,9 @@ export async function preparePassthroughExchange(
981
1006
  // Stable per request: the Combo booking (`reserveDispatch`) already started and froze the spend
982
1007
  // policy, so `spendEnforced` cannot change before dispatch even though no reporter starts it here.
983
1008
  const receiptMode = !sendBudgetState.adapterSendBudget?.spendEnforced && Boolean(options.comboInitialSend);
1009
+ // #6969: Pool sends hold empty lifecycle frames so a pre-output refusal reaches the ladder below.
1010
+ const preOutputRefusals = new PoolPreOutputRefusals();
1011
+ const preOutputRefusalHold = () => poolPreOutputRefusalHold(admissionState.authCtx, route.provider, connectMs, options.comboAttempt);
984
1012
  try {
985
1013
  // Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
986
1014
  // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
@@ -1004,18 +1032,19 @@ export async function preparePassthroughExchange(
1004
1032
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1005
1033
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1006
1034
  ? options.nativeControl : undefined,
1007
- dispatchOverride: oauthDispatch(request),
1035
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1008
1036
  providerName: route.providerName,
1009
1037
  modelId: route.modelId,
1010
1038
  onPhysicalDispatch: receiptMode ? sendBudgetState.noteInitialDispatch : undefined,
1011
1039
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1012
1040
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1013
1041
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1042
+ preOutputRefusalHold: preOutputRefusalHold(),
1014
1043
  }),
1015
1044
  route.provider.authMode === "forward")
1016
1045
  // Every real attempt response — including an intermediate 5xx the
1017
1046
  // retry wrapper replaces — proves the host was reached (#914 review).
1018
- .then(adoptObservedResponse);
1047
+ .then(adoptObservedResponse).then(preOutputRefusals.observe);
1019
1048
  },
1020
1049
  { abortSignal: upstream.signal, label: safeHostLabel(request.url),
1021
1050
  attempts: sendBudgetState.initialSendAllowance(transientSendAttempts()),
@@ -1026,6 +1055,8 @@ export async function preparePassthroughExchange(
1026
1055
  },
1027
1056
  );
1028
1057
  } catch (err) {
1058
+ preOutputRefusals.recordOnLocalExit(err, options.abortSignal?.aborted === true || req.signal.aborted,
1059
+ config, admissionState.authCtx, route.provider, route.modelId);
1029
1060
  return transportFailureResponse(err);
1030
1061
  } finally {
1031
1062
  request.releaseBodyObservation?.();
@@ -1034,7 +1065,33 @@ export async function preparePassthroughExchange(
1034
1065
  const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
1035
1066
  // At most one reasoning-effort downgrade per request.
1036
1067
  const reasoningEffortDowngradeGuard: { attempted: boolean } = { attempted: false };
1037
- let oauth401ReplayAttempted = false;
1068
+ /** Rebind the refreshed wire before transferring this request to the Chat pipeline. */
1069
+ function bindRefreshedAdapter(refreshedAdapter: typeof transportState.adapter) {
1070
+ bindRouteReasoningReplayScope({
1071
+ parsed,
1072
+ providerName: route.providerName,
1073
+ provider: route.provider,
1074
+ adapterName: refreshedAdapter.name,
1075
+ oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1076
+ });
1077
+ logCtx.providerAdapter = refreshedAdapter.name;
1078
+ sealRequestAttemptIdentity(
1079
+ logCtx.activeAttempt,
1080
+ logCtx.provider,
1081
+ refreshedAdapter.name,
1082
+ logCtx.accountLogLabel,
1083
+ );
1084
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, refreshedAdapter.name);
1085
+ }
1086
+ /** Transfer adapter ownership without retaining the native abort link or request cache. */
1087
+ function handoffToChat(refreshedAdapter: typeof transportState.adapter) {
1088
+ cleanupUpstreamAbort();
1089
+ upstream.abort();
1090
+ transportState.adapter = transportState.activeAdapter = transportState.runTurnAdapter = refreshedAdapter;
1091
+ transportState.invalidateSameTargetRequest();
1092
+ return { kind: "adapter-handoff" as const };
1093
+ }
1094
+ let oauth401ReplayAttempted = transportState.oauth401ReplayAttempted;
1038
1095
  let codex401ReplayKind: "main" | "stored" | null = null;
1039
1096
  // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
1040
1097
  // moments later; at most one byte-identical replay is allowed per request.
@@ -1043,7 +1100,7 @@ export async function preparePassthroughExchange(
1043
1100
  let rateLimitRetries = 0;
1044
1101
  const rebuildAndRefetch = async (
1045
1102
  recovery: AttemptRecoveryKind,
1046
- ): Promise<Response | { failed: Response }> => {
1103
+ ): Promise<Response | { failed: Response | PassthroughAdapterHandoff }> => {
1047
1104
  const retryAdapter = resolveSelectionAdapter(
1048
1105
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1049
1106
  config.cacheRetention,
@@ -1113,7 +1170,7 @@ export async function preparePassthroughExchange(
1113
1170
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1114
1171
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1115
1172
  ? options.nativeControl : undefined,
1116
- dispatchOverride: oauthDispatch(request),
1173
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1117
1174
  providerName: route.providerName,
1118
1175
  modelId: route.modelId,
1119
1176
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1220,7 +1277,7 @@ export async function preparePassthroughExchange(
1220
1277
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1221
1278
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1222
1279
  ? options.nativeControl : undefined,
1223
- dispatchOverride: oauthDispatch(request),
1280
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1224
1281
  providerName: route.providerName,
1225
1282
  modelId: route.modelId,
1226
1283
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1282,7 +1339,7 @@ export async function preparePassthroughExchange(
1282
1339
  // fault and throw away the credential evidence the client needs.
1283
1340
  && !sendBudgetExhausted(transientSendAttempts())
1284
1341
  ) {
1285
- oauth401ReplayAttempted = true;
1342
+ oauth401ReplayAttempted = transportState.oauth401ReplayAttempted = true;
1286
1343
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1287
1344
  let refreshed: OAuthAccessSnapshot;
1288
1345
  try {
@@ -1305,7 +1362,7 @@ export async function preparePassthroughExchange(
1305
1362
  if (route.providerName === "kiro") {
1306
1363
  parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
1307
1364
  }
1308
- const refreshedProvider = resolveProviderTransport(
1365
+ let refreshedProvider = resolveProviderTransport(
1309
1366
  route.providerName,
1310
1367
  {
1311
1368
  ...route.provider,
@@ -1318,29 +1375,21 @@ export async function preparePassthroughExchange(
1318
1375
  : undefined,
1319
1376
  );
1320
1377
  route.provider = refreshedProvider;
1378
+ try { await resolveCopilotSelection(parsed); }
1379
+ catch (err) { return transportFailureResponse(err); }
1380
+ refreshedProvider = route.provider;
1321
1381
  const refreshedAdapter = resolveSelectionAdapter(
1322
1382
  resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
1323
1383
  config.cacheRetention,
1324
1384
  );
1385
+ bindRefreshedAdapter(refreshedAdapter);
1325
1386
  if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
1387
+ if (route.providerName === "github-copilot" && refreshedAdapter.name === "openai-chat") {
1388
+ return handoffToChat(refreshedAdapter);
1389
+ }
1326
1390
  upstream.abort();
1327
1391
  return formatErrorResponse(502, "upstream_error", "OAuth refresh changed the provider wire unexpectedly");
1328
1392
  }
1329
- bindRouteReasoningReplayScope({
1330
- parsed,
1331
- providerName: route.providerName,
1332
- provider: refreshedProvider,
1333
- adapterName: refreshedAdapter.name,
1334
- oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1335
- });
1336
- logCtx.providerAdapter = refreshedAdapter.name;
1337
- sealRequestAttemptIdentity(
1338
- logCtx.activeAttempt,
1339
- logCtx.provider,
1340
- refreshedAdapter.name,
1341
- logCtx.accountLogLabel,
1342
- );
1343
- recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, refreshedAdapter.name);
1344
1393
  try {
1345
1394
  request = await refreshedAdapter.buildRequest(parsed, {
1346
1395
  headers: requestState.selectedForwardHeaders,
@@ -1373,7 +1422,7 @@ export async function preparePassthroughExchange(
1373
1422
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1374
1423
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1375
1424
  ? options.nativeControl : undefined,
1376
- dispatchOverride: oauthDispatch(request),
1425
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1377
1426
  providerName: route.providerName,
1378
1427
  modelId: route.modelId,
1379
1428
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1414,38 +1463,50 @@ export async function preparePassthroughExchange(
1414
1463
  true,
1415
1464
  );
1416
1465
  if (hop.allowed) {
1417
- const nextAccountId = rotateGenericOAuthAccountOn429(
1418
- config, route.providerName, transportState.genericFailoverAccountId,
1419
- upstreamResponse.headers.get("retry-after"),
1420
- Date.now(),
1421
- route.modelId,
1422
- );
1423
- let snapshot: OAuthAccessSnapshot | undefined;
1424
- if (nextAccountId) {
1425
- try { snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); }
1426
- catch { /* Keep the original 429 body readable when the next credential is unavailable. */ }
1427
- }
1428
- if (snapshot && await applyFailoverSnapshot(snapshot)) {
1429
- transportState.genericFailovers += 1;
1430
- route.provider = resolveProviderTransport(
1431
- route.providerName, route.provider, parsed.options.promptCacheKey, transportState.sentOAuthSnapshot?.apiBaseUrl,
1466
+ let transferred = false;
1467
+ try {
1468
+ const nextAccountId = rotateGenericOAuthAccountOn429(
1469
+ config, route.providerName, transportState.genericFailoverAccountId,
1470
+ upstreamResponse.headers.get("retry-after"),
1471
+ Date.now(),
1472
+ route.modelId,
1432
1473
  );
1433
- bindRouteReasoningReplayScope({
1434
- parsed, providerName: route.providerName, provider: route.provider,
1435
- adapterName: "openai-responses", oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1436
- });
1474
+ let snapshot: OAuthAccessSnapshot | undefined;
1475
+ if (nextAccountId) {
1476
+ try { snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); }
1477
+ catch { /* Keep the original 429 body readable when the next credential is unavailable. */ }
1478
+ }
1479
+ if (snapshot && await applyFailoverSnapshot(snapshot)) {
1480
+ transportState.genericFailovers += 1;
1481
+ route.provider = resolveProviderTransport(
1482
+ route.providerName, route.provider, parsed.options.promptCacheKey, transportState.sentOAuthSnapshot?.apiBaseUrl,
1483
+ );
1484
+ const rotatedAdapter = resolveSelectionAdapter(
1485
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1486
+ config.cacheRetention,
1487
+ );
1488
+ bindRefreshedAdapter(rotatedAdapter);
1489
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
1490
+ sendBudgetState.pendingHopPermit = hop.permit;
1491
+ if (route.providerName === "github-copilot" && rotatedAdapter.name === "openai-chat") {
1492
+ transferred = true;
1493
+ return handoffToChat(rotatedAdapter);
1494
+ }
1495
+ const result = await rebuildAndRefetch("oauth-account-429");
1496
+ sendBudgetState.pendingHopPermit = undefined;
1497
+ if ("failed" in result) return result.failed;
1498
+ upstreamResponse = result;
1499
+ continue passthroughRecovery;
1500
+ }
1501
+ } catch (err) {
1437
1502
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
1438
- // The replay IS this hop's send, so the rebuild spends the reservation instead of
1439
- // asking for one of its own.
1440
- sendBudgetState.pendingHopPermit = hop.permit;
1441
- const result = await rebuildAndRefetch("oauth-account-429");
1442
- sendBudgetState.pendingHopPermit = undefined;
1443
- if ("failed" in result) return result.failed;
1444
- upstreamResponse = result;
1445
- continue passthroughRecovery;
1503
+ return transportFailureResponse(err);
1504
+ } finally {
1505
+ if (!transferred) {
1506
+ if (sendBudgetState.pendingHopPermit === hop.permit) sendBudgetState.pendingHopPermit = undefined;
1507
+ hop.permit?.release();
1508
+ }
1446
1509
  }
1447
- // No credential moved, so the reservation costs nothing.
1448
- hop.permit?.release();
1449
1510
  } else {
1450
1511
  // The activation quorum ignores cooldowns; prove that the selector has a live alternate
1451
1512
  // before describing this as a recovery that only the shared request budget withheld.
@@ -1508,7 +1569,7 @@ export async function preparePassthroughExchange(
1508
1569
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1509
1570
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1510
1571
  ? options.nativeControl : undefined,
1511
- dispatchOverride: oauthDispatch(request),
1572
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1512
1573
  providerName: route.providerName,
1513
1574
  modelId: route.modelId,
1514
1575
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1622,6 +1683,7 @@ export async function preparePassthroughExchange(
1622
1683
  passthroughEstimate,
1623
1684
  stream: upstreamRequestsStream,
1624
1685
  httpOnly: canonicalBufferedJson,
1686
+ preOutputRefusalHold: preOutputRefusalHold(),
1625
1687
  onResponse: (response, retryAuthCtx, retryRequest) => {
1626
1688
  adoptCodexWsStage(response);
1627
1689
  captureAffinityResponse(
@@ -1648,6 +1710,8 @@ export async function preparePassthroughExchange(
1648
1710
  }
1649
1711
  }
1650
1712
  }
1713
+ // #6969: a projection nothing above recovered goes on as the original stream, before any reader.
1714
+ upstreamResponse = preOutputRefusals.restore(upstreamResponse);
1651
1715
  // The deterministic route record cannot classify history it never observed (restart, expiry,
1652
1716
  // eviction, or an older transcript). Inspect only a bounded clone of a 4xx whose exact outbound
1653
1717
  // Responses body still carries opaque state, then rebuild once through the ordinary adapter
@@ -1875,6 +1939,7 @@ export async function preparePassthroughExchange(
1875
1939
  const localUpstream = isLocalUpstream(route.provider.baseUrl);
1876
1940
 
1877
1941
  return {
1942
+ kind: undefined,
1878
1943
  codexSafetyBufferingOptions,
1879
1944
  imageGenCallAliases,
1880
1945
  routedCustomToolNames,
@@ -1914,7 +1979,8 @@ export async function preparePassthroughExchange(
1914
1979
  connectMs,
1915
1980
  localUpstream,
1916
1981
  upstreamResponse,
1982
+ terminalOutcomeRecorded: preOutputRefusals.outcomeRecorded,
1917
1983
  };
1918
1984
  }
1919
1985
 
1920
- export type PassthroughExchange = Exclude<Awaited<ReturnType<typeof preparePassthroughExchange>>, Response>;
1986
+ export type PassthroughExchange = Exclude<Awaited<ReturnType<typeof preparePassthroughExchange>>, Response | PassthroughAdapterHandoff>;
@@ -8,7 +8,7 @@ import type { ResponsesTransport } from "./request-transport";
8
8
  import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
9
9
  import type { ResponsesEffects } from "./response-effects";
10
10
  import type { ResponsesSendBudget } from "./request-send-budget";
11
- import { preparePassthroughExchange } from "./passthrough-dispatch";
11
+ import { preparePassthroughExchange, type PassthroughAdapterHandoff } from "./passthrough-dispatch";
12
12
  import { deliverPassthroughResponse } from "./passthrough-delivery";
13
13
  import { guardDirectPassthroughBodyInactivity } from "../../lib/response-body-inactivity";
14
14
  import { resolveStallTimeoutMs } from "../../stall-timeout";
@@ -24,7 +24,7 @@ export async function executePassthroughResponse(
24
24
  sidecarState: ResponsesSidecarAuth,
25
25
  responseEffects: ResponsesEffects,
26
26
  sendBudgetState: ResponsesSendBudget,
27
- ): Promise<Response> {
27
+ ): Promise<Response | PassthroughAdapterHandoff> {
28
28
  const nativeHostState: PassthroughAdmissionState = { lease: admissionState.pendingHostAdmissionLease };
29
29
  admissionState.pendingHostAdmissionLease = null;
30
30
  try {
@@ -37,7 +37,7 @@ export async function executePassthroughResponse(
37
37
  responseEffects,
38
38
  sendBudgetState,
39
39
  );
40
- if (nativeExchange instanceof Response) return nativeExchange;
40
+ if (nativeExchange instanceof Response || nativeExchange.kind === "adapter-handoff") return nativeExchange;
41
41
  const response = await deliverPassthroughResponse(
42
42
  requestContext,
43
43
  admissionState,
@@ -33,6 +33,7 @@ import { resolveContextPrincipal } from "../auth-cors";
33
33
  import { resolveSkillsSnapshotScopeKey, snapshotSkillsCatalogInBody } from "./skills-snapshot";
34
34
  import {
35
35
  isShadowSourceModel,
36
+ shadowCallPreservesSelectedModel,
36
37
  shadowSourceModelPrefix,
37
38
  shouldInterceptShadowCall,
38
39
  } from "../../lib/shadow-call";
@@ -275,10 +276,11 @@ export async function prepareResponsesRequest(
275
276
  }
276
277
  }
277
278
  let shadowCallIntercepted = false;
278
- // A spawned sub-agent turn names its model on purpose; gpt-6-luna is both the helper
279
- // slug and a default sub-agent model, so neither intercept site may rewrite that turn.
279
+ // Luna is also selectable by users/children. Both intercept sites use the same
280
+ // per-turn provenance; WS frames must not inherit the handshake's classification.
280
281
  const threadSpawn = isThreadSpawnRequest(req.headers);
281
- if (!concreteSelection && !options.compactionRoutingOverride && !options.internalDecisionCall && !threadSpawn && !memoryModelApplies && body && typeof body === "object" && !Array.isArray(body)) {
282
+ const preserveShadowModel = shadowCallPreservesSelectedModel(body, req.headers, options.inboundTransport);
283
+ if (!concreteSelection && !options.compactionRoutingOverride && !options.internalDecisionCall && !preserveShadowModel && !memoryModelApplies && body && typeof body === "object" && !Array.isArray(body)) {
282
284
  const shadowIntercept = config.shadowCallIntercept;
283
285
  const rawShadowModel = (body as { model?: unknown }).model;
284
286
  if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
@@ -613,7 +615,7 @@ export async function prepareResponsesRequest(
613
615
  }
614
616
  const _sci = config.shadowCallIntercept;
615
617
  let shadowRoute: RouteResult | undefined;
616
- if (!concreteSelection && !memoryRoute && !options.memoryModelPhase && !options.compactionRoutingOverride && !options.internalDecisionCall && !threadSpawn && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
618
+ if (!concreteSelection && !memoryRoute && !options.memoryModelPhase && !options.compactionRoutingOverride && !options.internalDecisionCall && !preserveShadowModel && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
617
619
  const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
618
620
  let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
619
621
  try {