@bitkyc08/opencodex 2.82.0 → 2.83.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 (85) hide show
  1. package/gui/dist/assets/{App-Ds_qL7o8.js → App-Ba8uG696.js} +4 -4
  2. package/gui/dist/assets/{Tray-BqHF5Trd.js → Tray-IImbNPRy.js} +1 -1
  3. package/gui/dist/assets/{index-D3FBCNiP.js → index-BHRZdJuv.js} +12 -12
  4. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-DiJO6sYY.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/catalog.ts +2 -2
  8. package/src/adapters/cursor/effort-map.ts +1 -1
  9. package/src/adapters/devin/live-models.ts +2 -2
  10. package/src/adapters/google-truncation.ts +22 -0
  11. package/src/adapters/google.ts +7 -3
  12. package/src/adapters/ollama-native.ts +18 -14
  13. package/src/adapters/openai-responses/passthrough.ts +2 -0
  14. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  15. package/src/adapters/openai-responses/reasoning.ts +11 -1
  16. package/src/bridge/errors.ts +7 -1
  17. package/src/claude/context-windows.ts +3 -3
  18. package/src/claude/desktop-picker.ts +1 -1
  19. package/src/cli/account-auth.ts +20 -12
  20. package/src/cli/resolve.ts +1 -1
  21. package/src/clients/config-export/model-metadata.ts +16 -0
  22. package/src/clients/config-export/zcode-store.ts +46 -11
  23. package/src/clients/config-export/zcode.ts +3 -5
  24. package/src/clients/config-export.ts +1 -1
  25. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  26. package/src/codex/auth-context.ts +8 -3
  27. package/src/codex/catalog/native-models.ts +1 -1
  28. package/src/codex/catalog/provider-models.ts +25 -0
  29. package/src/codex/desktop-app/darwin.ts +1 -1
  30. package/src/codex/desktop-app/handoff.ts +1 -1
  31. package/src/codex/desktop-app/linux.ts +1 -1
  32. package/src/codex/desktop-app/lock.ts +1 -1
  33. package/src/codex/desktop-app/types.ts +1 -1
  34. package/src/codex/desktop-app/windows.ts +1 -1
  35. package/src/codex/desktop-app-restart.ts +1 -1
  36. package/src/codex/main-account.ts +6 -7
  37. package/src/generated/compatibility-version.json +91 -75
  38. package/src/grok/inject.ts +245 -221
  39. package/src/grok/reset-coupon-ledger.ts +223 -57
  40. package/src/grok/reset-coupons.ts +13 -6
  41. package/src/integrations/cursor-detect.ts +1 -1
  42. package/src/lib/errors.ts +25 -0
  43. package/src/lib/redact.ts +2 -2
  44. package/src/oauth/index.ts +1 -1
  45. package/src/oauth/meta-muse-device.ts +1 -1
  46. package/src/oauth/types.ts +1 -1
  47. package/src/providers/anthropic-fast.ts +1 -1
  48. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  49. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  50. package/src/providers/devin-provider-merge-migration.ts +1 -1
  51. package/src/providers/github-copilot-auto.ts +169 -0
  52. package/src/providers/model-presets.ts +1 -1
  53. package/src/providers/muse-subscription-usage.ts +1 -1
  54. package/src/providers/registry/entries-core.ts +6 -6
  55. package/src/providers/registry/entries-extended.ts +4 -0
  56. package/src/providers/registry/model-seeds.ts +3 -3
  57. package/src/providers/xai-fast-model.ts +1 -1
  58. package/src/server/inbound-body-admission.ts +24 -7
  59. package/src/server/index/link-listener.ts +30 -17
  60. package/src/server/index/serve-options.ts +8 -2
  61. package/src/server/index.ts +1 -1
  62. package/src/server/lifecycle.ts +31 -4
  63. package/src/server/management/grok-coupon-routes.ts +277 -52
  64. package/src/server/management/link-routes.ts +42 -20
  65. package/src/server/management/route-registry.ts +2 -2
  66. package/src/server/relay-eager.ts +2 -0
  67. package/src/server/relay.ts +18 -7
  68. package/src/server/request-log-failure-attribution.ts +3 -0
  69. package/src/server/request-log-terminal-status.ts +186 -0
  70. package/src/server/request-log.ts +15 -118
  71. package/src/server/responses/adapter-continuation.ts +9 -1
  72. package/src/server/responses/adapter-dispatch.ts +65 -6
  73. package/src/server/responses/compact.ts +3 -2
  74. package/src/server/responses/core-opaque-recovery.ts +5 -5
  75. package/src/server/responses/core.ts +6 -6
  76. package/src/server/responses/empty-completion-guard.ts +18 -2
  77. package/src/server/responses/jev-model-invoke.ts +1 -1
  78. package/src/server/responses/passthrough-dispatch.ts +115 -60
  79. package/src/server/responses/passthrough-execution.ts +3 -3
  80. package/src/server/responses/request-transport.ts +125 -5
  81. package/src/server/responses/run-turn-execution.ts +13 -5
  82. package/src/server/responses/sidecar-execution.ts +5 -0
  83. package/src/server/responses/spend-storage-error.ts +33 -0
  84. package/src/types/request.ts +5 -0
  85. package/src/usage/expected-prices.ts +1 -1
@@ -1,3 +1,4 @@
1
+ import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
1
2
  import { createPhysicalSendReporter } from "../../lib/request-execution-budget";
2
3
  import { createInferenceSendBudget } from "../inference/context";
3
4
  import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
@@ -1099,11 +1100,11 @@ export async function handleResponsesCompact(
1099
1100
  let outcomeCtx = authCtx;
1100
1101
  const localDispatchRefusal = (error: unknown): Response | undefined => {
1101
1102
  const cause = unwrapUpstreamRetryEvidenceError(error);
1102
- const response = cause instanceof SendBudgetExhaustedError
1103
+ const response = spendLedgerStorageErrorResponse(error, logCtx) ?? (cause instanceof SendBudgetExhaustedError
1103
1104
  ? unboundPoolSpendRefusalResponse(logCtx) ?? formatErrorResponse(429, "request_send_budget_exhausted", cause.message)
1104
1105
  : mapCodexAuthContextErrorToResponse(cause, {
1105
1106
  now: Date.now(), accountSelector: route.codexAccountNamespace,
1106
- });
1107
+ }));
1107
1108
  if (response) {
1108
1109
  releaseUpstreamHostAdmission(compactHostAdmissionLease);
1109
1110
  compactHostAdmissionLease = null;
@@ -465,13 +465,13 @@ export function resetStreamedOpaqueBlobLogContext(logCtx: RequestLogContext): vo
465
465
  export type OpaqueBlobRecoveryGuard = { attempted: boolean };
466
466
 
467
467
 
468
- export type OpaqueBlobRecoveryResult =
468
+ export type OpaqueBlobRecoveryResult<Failure = Response> =
469
469
  | { kind: "skipped" }
470
470
  | { kind: "recovered"; response: Response }
471
- | { kind: "failed"; response: Response };
471
+ | { kind: "failed"; response: Failure };
472
472
 
473
473
 
474
- export async function attemptOpaqueBlobRecovery(
474
+ export async function attemptOpaqueBlobRecovery<Failure = Response>(
475
475
  args: {
476
476
  response: Response;
477
477
  outboundBody?: string;
@@ -480,8 +480,8 @@ export async function attemptOpaqueBlobRecovery(
480
480
  guard: OpaqueBlobRecoveryGuard;
481
481
  signal: AbortSignal;
482
482
  },
483
- rebuild: (kind: AttemptRecoveryKind) => Promise<Response | { failed: Response }>,
484
- ): Promise<OpaqueBlobRecoveryResult> {
483
+ rebuild: (kind: AttemptRecoveryKind) => Promise<Response | { failed: Failure }>,
484
+ ): Promise<OpaqueBlobRecoveryResult<Failure>> {
485
485
  const errorBody = await opaqueBlobRejectionBodyForRecovery(
486
486
  args.response,
487
487
  args.outboundBody,
@@ -28,7 +28,6 @@ import { deliverAdapterResponse } from "./adapter-delivery";
28
28
  import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health";
29
29
  import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
30
30
  import { runWithCompactionRecovery } from "./compaction-recovery";
31
-
32
31
  /**
33
32
  * Route one `/v1/responses` request through the adapter pipeline: recovery loop, passthrough
34
33
  * wire, image/web-search bridges, and the terminal-guard continuation.
@@ -75,7 +74,6 @@ export async function handleResponses(
75
74
  if (!options.comboInitialSend?.producerOwned) options.comboInitialSend?.permit.release();
76
75
  }
77
76
  }
78
-
79
77
  export async function handleComboResponses(
80
78
  req: Request,
81
79
  rawBody: unknown,
@@ -94,7 +92,6 @@ export async function handleComboResponses(
94
92
  requestDispatchers,
95
93
  );
96
94
  }
97
-
98
95
  /** Compose request phases while retaining the original admission-finally ownership. */
99
96
  async function handleResponsesInner(
100
97
  req: Request,
@@ -107,6 +104,7 @@ async function handleResponsesInner(
107
104
  pendingHostAdmissionLease: null,
108
105
  authCtx: { kind: "main", accountId: null },
109
106
  };
107
+ let releasePendingSend = () => {};
110
108
  try {
111
109
  const requestState = await prepareResponsesRequest(requestContext, admissionState, requestDispatchers);
112
110
  if (requestState instanceof Response) return requestState;
@@ -124,7 +122,7 @@ async function handleResponsesInner(
124
122
  const sendBudgetState = createResponsesSendBudget(requestContext);
125
123
  if (sendBudgetState instanceof Response) return sendBudgetState;
126
124
  if ("passthrough" in transportState.adapter && transportState.adapter.passthrough && !sidecarState.routedCompaction) {
127
- return await executePassthroughResponse(
125
+ const passthroughResult = await executePassthroughResponse(
128
126
  requestContext,
129
127
  admissionState,
130
128
  requestState,
@@ -133,6 +131,9 @@ async function handleResponsesInner(
133
131
  responseEffects,
134
132
  sendBudgetState,
135
133
  );
134
+ if (passthroughResult instanceof Response) return passthroughResult;
135
+ const unclaimedHop = sendBudgetState.pendingHopPermit;
136
+ releasePendingSend = () => { if (sendBudgetState.pendingHopPermit === unclaimedHop) { unclaimedHop?.release(); sendBudgetState.pendingHopPermit = undefined; } };
136
137
  }
137
138
  const sidecarPlans = await executeResponsesSidecars(
138
139
  requestContext,
@@ -182,15 +183,14 @@ async function handleResponsesInner(
182
183
  continuationState,
183
184
  );
184
185
  } finally {
186
+ releasePendingSend();
185
187
  if (admissionState.pendingHostAdmissionLease) {
186
188
  releaseUpstreamHostAdmission(admissionState.pendingHostAdmissionLease);
187
189
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
188
190
  }
189
191
  }
190
192
  }
191
-
192
193
  const requestDispatchers: ResponsesDispatchers = { handleResponses, handleComboResponses };
193
-
194
194
  export { adapterNeedsForcedContinuation } from "./core-replay";
195
195
  export {
196
196
  sidecarOutcomeRecorder, codexLogAccountId, usesCodexForwardPoolAuth, preAuthUpstreamHostCircuitKey,
@@ -293,11 +293,27 @@ export async function* guardEmptyCompletionEventStream(
293
293
  return;
294
294
  }
295
295
  if (event.type === "error") {
296
- if (retries > 0 && event.status !== 499) {
296
+ if (event.replaySafeBeforeOutput === true && retries < maxRetries) {
297
+ // The upstream failed the turn before any text or tool call (#6876: Vertex dropped a
298
+ // malformed function call). Nothing actionable reached the client, so this is the same
299
+ // replay-safe shape as an empty completion: retry the identical turn once.
300
+ usage = mergeUsage(usage, event.usage);
301
+ retries += 1;
302
+ try {
303
+ source = await options.continuation();
304
+ } catch {
305
+ yield emptyCompletionRetryFailedEvent(usage, true);
306
+ return;
307
+ }
308
+ terminalSeen = true;
309
+ break;
310
+ }
311
+ if (retries > 0 && event.status !== 499 && event.replaySafeBeforeOutput !== true) {
297
312
  // The retry failed upstream. Its body cannot reach the client (the
298
313
  // 200 head went out with the first attempt), so state the failure in
299
314
  // the stream's own error framing — same move as the router's
300
- // empty_completion_retry_failed. Client cancels (499) pass through.
315
+ // empty_completion_retry_failed. Client cancels (499) pass through, and a
316
+ // replay-safe failure that repeats keeps its own, more specific message.
301
317
  yield emptyCompletionRetryFailedEvent(mergeUsage(usage, event.usage), true);
302
318
  return;
303
319
  }
@@ -136,7 +136,7 @@ export function createJevModelInvoker(context: JevModelInvokerContext): JevModel
136
136
  if (new TextEncoder().encode(body).byteLength > JEV_MAX_REQUEST_BYTES) {
137
137
  throw new JevModelInvokeError("malformed", "decision request too large");
138
138
  }
139
- const lease = tryAdmitTurn();
139
+ const lease = tryAdmitTurn(undefined, signal);
140
140
  if (!lease) throw new JevModelInvokeError("network", "decision turn not admitted");
141
141
  const childLog: RequestLogContext = { model, provider: "unknown", inboundProtocol: "responses" };
142
142
  let usage: Record<string, number> | undefined;
@@ -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";
@@ -166,6 +167,9 @@ import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-ze
166
167
  import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
167
168
  import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
168
169
 
170
+ /** Transfer an uncommitted Copilot exchange to the existing translated pipeline without new admission. */
171
+ export interface PassthroughAdapterHandoff { kind: "adapter-handoff" }
172
+
169
173
  /** Prepares and recovers one native Responses exchange before client commitment. */
170
174
  export async function preparePassthroughExchange(
171
175
  requestContext: Pick<ResponsesRequestContext, "config" | "logCtx" | "options" | "req">,
@@ -192,6 +196,12 @@ export async function preparePassthroughExchange(
192
196
  | "passiveQuotaWriterGeneration"
193
197
  | "oauthDispatch"
194
198
  | "resolveSelectionAdapter"
199
+ | "resolveCopilotSelection"
200
+ | "copilotRefusalResponse"
201
+ | "oauth401ReplayAttempted"
202
+ | "activeAdapter"
203
+ | "runTurnAdapter"
204
+ | "invalidateSameTargetRequest"
195
205
  | "isOAuth401ReplayProvider"
196
206
  | "sentOAuthSnapshot"
197
207
  | "refreshResolvedOAuthSelection"
@@ -248,6 +258,8 @@ export async function preparePassthroughExchange(
248
258
  passiveQuotaWriterGeneration,
249
259
  oauthDispatch,
250
260
  resolveSelectionAdapter,
261
+ resolveCopilotSelection,
262
+ copilotRefusalResponse,
251
263
  isOAuth401ReplayProvider,
252
264
  refreshResolvedOAuthSelection,
253
265
  applyFailoverSnapshot,
@@ -732,7 +744,7 @@ export async function preparePassthroughExchange(
732
744
  // consumer's cancel to a signalled fetch, so we pass the signal and relay through relayWithAbort,
733
745
  // whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
734
746
  const upstream = new AbortController();
735
- linkAbortSignal(upstream, options.abortSignal);
747
+ const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal ?? req.signal);
736
748
  const connectMs = config.connectTimeoutMs ?? 200_000;
737
749
  let upstreamResponse: Response;
738
750
  /**
@@ -822,7 +834,7 @@ export async function preparePassthroughExchange(
822
834
  httpOnly: true,
823
835
  providerName: route.providerName,
824
836
  modelId: route.modelId,
825
- dispatchOverride: oauthDispatch(request),
837
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
826
838
  beforeDispatch: headers => {
827
839
  if (signal.aborted) throw signal.reason;
828
840
  if (!transportState.selectionIsCurrent(transportState.requestBindings.get(request))) {
@@ -896,9 +908,10 @@ export async function preparePassthroughExchange(
896
908
  describeOutboundBodyRefusal(result),
897
909
  );
898
910
  };
899
- const transportFailureResponse = (err: unknown): Response => {
911
+ const transportFailureResponse = (err: unknown): Response | PassthroughAdapterHandoff => {
912
+ cleanupUpstreamAbort();
900
913
  upstream.abort();
901
- if (options.abortSignal?.aborted) {
914
+ if (options.abortSignal?.aborted || req.signal.aborted) {
902
915
  releaseUpstreamHostAdmission(nativeHostState.lease);
903
916
  nativeHostState.lease = null;
904
917
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
@@ -915,6 +928,17 @@ export async function preparePassthroughExchange(
915
928
  ?? formatErrorResponse(429, "request_send_budget_exhausted", err.message);
916
929
  }
917
930
  const refusal = unwrapUpstreamRetryEvidenceError(err);
931
+ if (refusal instanceof CopilotAutoAdapterHandoff) {
932
+ bindRefreshedAdapter(refusal.adapter);
933
+ return handoffToChat(refusal.adapter);
934
+ }
935
+ const copilotRefusal = copilotRefusalResponse(refusal);
936
+ if (copilotRefusal) {
937
+ releaseUpstreamHostAdmission(nativeHostState.lease);
938
+ nativeHostState.lease = null;
939
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
940
+ return copilotRefusal;
941
+ }
918
942
  // Pacing may outlive the selected account's admission. No fetch occurred, so do
919
943
  // not turn an operator pause into a 502 or charge it to host/account health.
920
944
  if (refusal instanceof OAuthAccountPausedError || refusal instanceof OAuthLoginRequiredError || refusal instanceof AnthropicAccountCooldownError) {
@@ -930,7 +954,7 @@ export async function preparePassthroughExchange(
930
954
  }
931
955
  return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
932
956
  }
933
- const localRefusal = mapCodexAuthContextErrorToResponse(refusal, {
957
+ const localRefusal = spendLedgerStorageErrorResponse(err, logCtx) ?? mapCodexAuthContextErrorToResponse(refusal, {
934
958
  now: Date.now(), accountSelector: route.codexAccountNamespace,
935
959
  });
936
960
  if (localRefusal) {
@@ -1004,7 +1028,7 @@ export async function preparePassthroughExchange(
1004
1028
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1005
1029
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1006
1030
  ? options.nativeControl : undefined,
1007
- dispatchOverride: oauthDispatch(request),
1031
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1008
1032
  providerName: route.providerName,
1009
1033
  modelId: route.modelId,
1010
1034
  onPhysicalDispatch: receiptMode ? sendBudgetState.noteInitialDispatch : undefined,
@@ -1034,7 +1058,33 @@ export async function preparePassthroughExchange(
1034
1058
  const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
1035
1059
  // At most one reasoning-effort downgrade per request.
1036
1060
  const reasoningEffortDowngradeGuard: { attempted: boolean } = { attempted: false };
1037
- let oauth401ReplayAttempted = false;
1061
+ /** Rebind the refreshed wire before transferring this request to the Chat pipeline. */
1062
+ function bindRefreshedAdapter(refreshedAdapter: typeof transportState.adapter) {
1063
+ bindRouteReasoningReplayScope({
1064
+ parsed,
1065
+ providerName: route.providerName,
1066
+ provider: route.provider,
1067
+ adapterName: refreshedAdapter.name,
1068
+ oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1069
+ });
1070
+ logCtx.providerAdapter = refreshedAdapter.name;
1071
+ sealRequestAttemptIdentity(
1072
+ logCtx.activeAttempt,
1073
+ logCtx.provider,
1074
+ refreshedAdapter.name,
1075
+ logCtx.accountLogLabel,
1076
+ );
1077
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, refreshedAdapter.name);
1078
+ }
1079
+ /** Transfer adapter ownership without retaining the native abort link or request cache. */
1080
+ function handoffToChat(refreshedAdapter: typeof transportState.adapter) {
1081
+ cleanupUpstreamAbort();
1082
+ upstream.abort();
1083
+ transportState.adapter = transportState.activeAdapter = transportState.runTurnAdapter = refreshedAdapter;
1084
+ transportState.invalidateSameTargetRequest();
1085
+ return { kind: "adapter-handoff" as const };
1086
+ }
1087
+ let oauth401ReplayAttempted = transportState.oauth401ReplayAttempted;
1038
1088
  let codex401ReplayKind: "main" | "stored" | null = null;
1039
1089
  // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
1040
1090
  // moments later; at most one byte-identical replay is allowed per request.
@@ -1043,7 +1093,7 @@ export async function preparePassthroughExchange(
1043
1093
  let rateLimitRetries = 0;
1044
1094
  const rebuildAndRefetch = async (
1045
1095
  recovery: AttemptRecoveryKind,
1046
- ): Promise<Response | { failed: Response }> => {
1096
+ ): Promise<Response | { failed: Response | PassthroughAdapterHandoff }> => {
1047
1097
  const retryAdapter = resolveSelectionAdapter(
1048
1098
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1049
1099
  config.cacheRetention,
@@ -1113,7 +1163,7 @@ export async function preparePassthroughExchange(
1113
1163
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1114
1164
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1115
1165
  ? options.nativeControl : undefined,
1116
- dispatchOverride: oauthDispatch(request),
1166
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1117
1167
  providerName: route.providerName,
1118
1168
  modelId: route.modelId,
1119
1169
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1220,7 +1270,7 @@ export async function preparePassthroughExchange(
1220
1270
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1221
1271
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1222
1272
  ? options.nativeControl : undefined,
1223
- dispatchOverride: oauthDispatch(request),
1273
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1224
1274
  providerName: route.providerName,
1225
1275
  modelId: route.modelId,
1226
1276
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1282,7 +1332,7 @@ export async function preparePassthroughExchange(
1282
1332
  // fault and throw away the credential evidence the client needs.
1283
1333
  && !sendBudgetExhausted(transientSendAttempts())
1284
1334
  ) {
1285
- oauth401ReplayAttempted = true;
1335
+ oauth401ReplayAttempted = transportState.oauth401ReplayAttempted = true;
1286
1336
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1287
1337
  let refreshed: OAuthAccessSnapshot;
1288
1338
  try {
@@ -1305,7 +1355,7 @@ export async function preparePassthroughExchange(
1305
1355
  if (route.providerName === "kiro") {
1306
1356
  parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
1307
1357
  }
1308
- const refreshedProvider = resolveProviderTransport(
1358
+ let refreshedProvider = resolveProviderTransport(
1309
1359
  route.providerName,
1310
1360
  {
1311
1361
  ...route.provider,
@@ -1318,29 +1368,21 @@ export async function preparePassthroughExchange(
1318
1368
  : undefined,
1319
1369
  );
1320
1370
  route.provider = refreshedProvider;
1371
+ try { await resolveCopilotSelection(parsed); }
1372
+ catch (err) { return transportFailureResponse(err); }
1373
+ refreshedProvider = route.provider;
1321
1374
  const refreshedAdapter = resolveSelectionAdapter(
1322
1375
  resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
1323
1376
  config.cacheRetention,
1324
1377
  );
1378
+ bindRefreshedAdapter(refreshedAdapter);
1325
1379
  if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
1380
+ if (route.providerName === "github-copilot" && refreshedAdapter.name === "openai-chat") {
1381
+ return handoffToChat(refreshedAdapter);
1382
+ }
1326
1383
  upstream.abort();
1327
1384
  return formatErrorResponse(502, "upstream_error", "OAuth refresh changed the provider wire unexpectedly");
1328
1385
  }
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
1386
  try {
1345
1387
  request = await refreshedAdapter.buildRequest(parsed, {
1346
1388
  headers: requestState.selectedForwardHeaders,
@@ -1373,7 +1415,7 @@ export async function preparePassthroughExchange(
1373
1415
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1374
1416
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1375
1417
  ? options.nativeControl : undefined,
1376
- dispatchOverride: oauthDispatch(request),
1418
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1377
1419
  providerName: route.providerName,
1378
1420
  modelId: route.modelId,
1379
1421
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1414,38 +1456,50 @@ export async function preparePassthroughExchange(
1414
1456
  true,
1415
1457
  );
1416
1458
  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,
1459
+ let transferred = false;
1460
+ try {
1461
+ const nextAccountId = rotateGenericOAuthAccountOn429(
1462
+ config, route.providerName, transportState.genericFailoverAccountId,
1463
+ upstreamResponse.headers.get("retry-after"),
1464
+ Date.now(),
1465
+ route.modelId,
1432
1466
  );
1433
- bindRouteReasoningReplayScope({
1434
- parsed, providerName: route.providerName, provider: route.provider,
1435
- adapterName: "openai-responses", oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1436
- });
1467
+ let snapshot: OAuthAccessSnapshot | undefined;
1468
+ if (nextAccountId) {
1469
+ try { snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); }
1470
+ catch { /* Keep the original 429 body readable when the next credential is unavailable. */ }
1471
+ }
1472
+ if (snapshot && await applyFailoverSnapshot(snapshot)) {
1473
+ transportState.genericFailovers += 1;
1474
+ route.provider = resolveProviderTransport(
1475
+ route.providerName, route.provider, parsed.options.promptCacheKey, transportState.sentOAuthSnapshot?.apiBaseUrl,
1476
+ );
1477
+ const rotatedAdapter = resolveSelectionAdapter(
1478
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1479
+ config.cacheRetention,
1480
+ );
1481
+ bindRefreshedAdapter(rotatedAdapter);
1482
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
1483
+ sendBudgetState.pendingHopPermit = hop.permit;
1484
+ if (route.providerName === "github-copilot" && rotatedAdapter.name === "openai-chat") {
1485
+ transferred = true;
1486
+ return handoffToChat(rotatedAdapter);
1487
+ }
1488
+ const result = await rebuildAndRefetch("oauth-account-429");
1489
+ sendBudgetState.pendingHopPermit = undefined;
1490
+ if ("failed" in result) return result.failed;
1491
+ upstreamResponse = result;
1492
+ continue passthroughRecovery;
1493
+ }
1494
+ } catch (err) {
1437
1495
  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;
1496
+ return transportFailureResponse(err);
1497
+ } finally {
1498
+ if (!transferred) {
1499
+ if (sendBudgetState.pendingHopPermit === hop.permit) sendBudgetState.pendingHopPermit = undefined;
1500
+ hop.permit?.release();
1501
+ }
1446
1502
  }
1447
- // No credential moved, so the reservation costs nothing.
1448
- hop.permit?.release();
1449
1503
  } else {
1450
1504
  // The activation quorum ignores cooldowns; prove that the selector has a live alternate
1451
1505
  // before describing this as a recovery that only the shared request budget withheld.
@@ -1508,7 +1562,7 @@ export async function preparePassthroughExchange(
1508
1562
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1509
1563
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
1510
1564
  ? options.nativeControl : undefined,
1511
- dispatchOverride: oauthDispatch(request),
1565
+ dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
1512
1566
  providerName: route.providerName,
1513
1567
  modelId: route.modelId,
1514
1568
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
@@ -1875,6 +1929,7 @@ export async function preparePassthroughExchange(
1875
1929
  const localUpstream = isLocalUpstream(route.provider.baseUrl);
1876
1930
 
1877
1931
  return {
1932
+ kind: undefined,
1878
1933
  codexSafetyBufferingOptions,
1879
1934
  imageGenCallAliases,
1880
1935
  routedCustomToolNames,
@@ -1917,4 +1972,4 @@ export async function preparePassthroughExchange(
1917
1972
  };
1918
1973
  }
1919
1974
 
1920
- export type PassthroughExchange = Exclude<Awaited<ReturnType<typeof preparePassthroughExchange>>, Response>;
1975
+ 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,