@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
@@ -53,6 +53,9 @@ import { classifyModelFamilyForQuota } from "../../oauth/account-quota-rank";
53
53
  import { expandInferenceOAuthSendBudget } from "../inference/context";
54
54
  import { stampOAuthAccountLabel, usesApiKeyAccount } from "../../providers/label";
55
55
  import { resolveProviderTransport } from "../../providers/xai-transport";
56
+ import { captureRouteStaticPolicy } from "../../router";
57
+ import { CopilotAutoHttpError, resolveCopilotAuto } from "../../providers/github-copilot-auto";
58
+ import { renameRoutedIdentityInContext } from "../../adapters/identity";
56
59
  import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
57
60
  import {
58
61
  providerApiKeySelectionIsCurrent,
@@ -87,6 +90,13 @@ import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
87
90
  import { clientCancelledResponse } from "./core-errors";
88
91
  import { acquireAccountLease, KIRO_ACCOUNT_WAIT_MS } from "../../oauth/kiro-account-load";
89
92
 
93
+ /** Return a renewed Chat wire to the native owner before sending inference. */
94
+ export class CopilotAutoAdapterHandoff extends Error {
95
+ constructor(readonly adapter: ProviderAdapter) {
96
+ super("Copilot Auto dispatch requires the Chat adapter");
97
+ }
98
+ }
99
+
90
100
  /** Owns live credential selection and adapter bindings for one request. */
91
101
  export async function prepareResponsesTransport(
92
102
  requestContext: Pick<ResponsesRequestContext, "config" | "logCtx" | "options" | "req">,
@@ -160,6 +170,8 @@ export async function prepareResponsesTransport(
160
170
  || route.providerName === "devin"
161
171
  ) && route.provider.authMode === "oauth";
162
172
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
173
+ // One inference-401 replay belongs to this request, even when Auto changes exchange wire.
174
+ let oauth401ReplayAttempted = false;
163
175
  let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
164
176
  let anthropicPoolAccountId: string | null = null;
165
177
  let anthropicPoolFailovers = 0;
@@ -281,6 +293,84 @@ export async function prepareResponsesTransport(
281
293
  * the case the fence exists for. Stays 0 for every provider without a passive quota.
282
294
  */
283
295
  let passiveQuotaWriterGeneration = 0;
296
+ const copilotRequestedModel = route.modelId;
297
+ /** Project only bounded Copilot refusal metadata; upstream bodies and session tokens never reach the client. */
298
+ const copilotRefusalResponse = (error: unknown): Response | undefined => {
299
+ if (!(error instanceof CopilotAutoHttpError) || ![401, 403, 429].includes(error.status)) return;
300
+ const response = formatErrorResponse(error.status, error.status === 429 ? "rate_limit_error" : "authentication_error",
301
+ error.message);
302
+ if (error.retryAfter) response.headers.set("retry-after", error.retryAfter);
303
+ return response;
304
+ };
305
+ let copilotSessionExpiresAt = Infinity;
306
+ let copilotNegotiationRefreshed = false;
307
+ let copilotAutoAdapter: OcxProviderConfig["adapter"] | undefined;
308
+ /** The operator-configured wire, captured before the first Auto negotiation can rewrite route.provider. */
309
+ let copilotConfiguredWire: { adapter: OcxProviderConfig["adapter"]; responsesPath?: string;
310
+ chatCompletionsPath?: string; apiVersion?: string } | undefined;
311
+ /** Start every negotiation from the configured wire, so a previous credential's Auto adapter,
312
+ * endpoint and session headers never carry over to a credential that keeps the named model. */
313
+ const copilotNegotiationBase = (provider: OcxProviderConfig): OcxProviderConfig => {
314
+ if (!copilotConfiguredWire) return provider;
315
+ const headers = new Headers(provider.headers);
316
+ headers.delete("Copilot-Session-Token");
317
+ if (copilotConfiguredWire.apiVersion === undefined) headers.delete("X-GitHub-Api-Version");
318
+ else headers.set("X-GitHub-Api-Version", copilotConfiguredWire.apiVersion);
319
+ const base: OcxProviderConfig = { ...provider, adapter: copilotConfiguredWire.adapter,
320
+ headers: Object.fromEntries(headers) };
321
+ if (copilotConfiguredWire.responsesPath === undefined) delete base.responsesPath;
322
+ else base.responsesPath = copilotConfiguredWire.responsesPath;
323
+ if (copilotConfiguredWire.chatCompletionsPath === undefined) delete base.chatCompletionsPath;
324
+ else base.chatCompletionsPath = copilotConfiguredWire.chatCompletionsPath;
325
+ return base;
326
+ };
327
+ /** Resolve the request's ephemeral wire selection and recover at most one OAuth negotiation 401. */
328
+ const resolveCopilotSelection = async (requestParsed: OcxParsedRequest, insideDispatch = false): Promise<void> => {
329
+ if (route.providerName !== "github-copilot" || route.provider.authMode === "forward") return;
330
+ copilotConfiguredWire ??= {
331
+ adapter: route.provider.adapter,
332
+ responsesPath: route.provider.responsesPath,
333
+ chatCompletionsPath: route.provider.chatCompletionsPath,
334
+ apiVersion: new Headers(route.provider.headers).get("X-GitHub-Api-Version") ?? undefined,
335
+ };
336
+ /** Fence control-plane sends to the captured credential; reuse an already-held dispatch concurrency lease. */
337
+ const negotiate = () => {
338
+ const binding: DispatchBinding | undefined = oauthSelection && servingOAuthSnapshot
339
+ ? { kind: "oauth", selection: { ...oauthSelection }, snapshot: servingOAuthSnapshot }
340
+ : { kind: "api-key", provider: { ...route.provider } };
341
+ return resolveCopilotAuto(copilotNegotiationBase(route.provider), copilotRequestedModel, requestParsed,
342
+ options.abortSignal ?? req.signal, /** Fence each negotiation send against the captured credential binding before touching the network. */
343
+ () => selectionIsCurrent(binding), !insideDispatch);
344
+ };
345
+ let selected: Awaited<ReturnType<typeof resolveCopilotAuto>>;
346
+ try { selected = await negotiate(); }
347
+ catch (error) {
348
+ if (!(error instanceof CopilotAutoHttpError) || error.status !== 401 || copilotNegotiationRefreshed
349
+ || oauth401ReplayAttempted || route.provider.authMode !== "oauth" || !servingOAuthSnapshot) throw error;
350
+ copilotNegotiationRefreshed = true;
351
+ const refreshed = await refreshResolvedOAuthSelection(servingOAuthSnapshot);
352
+ route.provider = resolveProviderTransport(route.providerName,
353
+ { ...route.provider, apiKey: refreshed.accessToken }, parsed.options.promptCacheKey,
354
+ resolveCopilotApiBaseUrl(refreshed.apiBaseUrl));
355
+ sentOAuthSnapshot = refreshed;
356
+ replayOAuthCredentialSnapshot = { accountId: refreshed.accountId, generation: refreshed.generation };
357
+ selected = await negotiate();
358
+ }
359
+ copilotSessionExpiresAt = selected.expiresAt ?? Infinity;
360
+ route.provider = selected.provider;
361
+ route.modelId = selected.modelId;
362
+ route.staticPolicy = captureRouteStaticPolicy(route.providerName, route.modelId, route.provider,
363
+ route.staticPolicy.effectiveAlias, inboundWire);
364
+ copilotAutoAdapter = selected.auto ? selected.provider.adapter : undefined;
365
+ for (const target of new Set([parsed, requestParsed])) {
366
+ target.modelId = selected.modelId;
367
+ if (target._rawBody && typeof target._rawBody === "object")
368
+ (target._rawBody as { model?: string }).model = selected.modelId;
369
+ target.context = renameRoutedIdentityInContext(target.context, selected.modelId);
370
+ }
371
+ logCtx.model = selected.modelId;
372
+ if (logCtx.activeAttempt) logCtx.activeAttempt.model = selected.modelId;
373
+ };
284
374
  /**
285
375
  * Apply a rotated account's FULL credential snapshot to the live route (#2568d).
286
376
  *
@@ -306,6 +396,7 @@ export async function prepareResponsesTransport(
306
396
  const applyFailoverSnapshot = async (
307
397
  snapshot: OAuthAccessSnapshot,
308
398
  retryParsed: OcxParsedRequest = parsed,
399
+ insideDispatch = false,
309
400
  ): Promise<OAuthAccessSnapshot | null> => {
310
401
  requireAnthropicAdmission();
311
402
  if (snapshot.provider !== route.providerName) return null;
@@ -342,6 +433,9 @@ export async function prepareResponsesTransport(
342
433
  }
343
434
  if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
344
435
  route.provider = rotatedProvider;
436
+ await resolveCopilotSelection(retryParsed, insideDispatch);
437
+ // Negotiation itself may have recovered a 401 with a newer committed credential.
438
+ if (route.providerName === "github-copilot" && servingOAuthSnapshot) snapshot = servingOAuthSnapshot;
345
439
  if (route.providerName === "kiro") {
346
440
  const kiroContext = { ...(snapshot.kiro ?? {}) };
347
441
  // Terminal-guard continuations are rebuilt from a shallow clone. Updating only the
@@ -411,7 +505,9 @@ export async function prepareResponsesTransport(
411
505
  && (anthropicInstance === undefined || !anthropicRouting!.getAnthropicAccountHealthSnapshot(binding.snapshot.accountId) && !anthropicRatePolicy!.anthropicRatePauseUntil(binding.snapshot.accountId) && !anthropicQuota!.anthropicFamilyRejected(binding.snapshot.accountId, route.modelId))
412
506
  && credentialGeneration(row.credential) === binding.snapshot.generation;
413
507
  };
508
+ /** Bind the negotiated wire adapter and its built requests to the serving credential's authority. */
414
509
  const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
510
+ if (copilotAutoAdapter) provider = { ...provider, adapter: copilotAutoAdapter };
415
511
  const resolved = resolveAdapter(provider, retention, route.providerName);
416
512
  if (route.provider.authMode === "forward") return resolved;
417
513
  const binding: DispatchBinding | undefined = route.provider.authMode === "oauth"
@@ -468,6 +564,7 @@ export async function prepareResponsesTransport(
468
564
  }
469
565
  return resolved;
470
566
  };
567
+ /** Rebuild a stale queued selection while retaining the caller's dispatch lease and renewing Auto metadata. */
471
568
  const refreshDispatchAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
472
569
  requireAnthropicAdmission();
473
570
  if (route.provider.authMode === "oauth") {
@@ -479,7 +576,7 @@ export async function prepareResponsesTransport(
479
576
  const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
480
577
  candidate = await anthropicRouting!.getAnthropicPoolAccessSnapshot(accountId);
481
578
  }
482
- if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed)) {
579
+ if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed, true)) {
483
580
  if (anthropicInstance !== undefined) await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
484
581
  throw new Error("OAuth account selection changed before dispatch");
485
582
  }
@@ -487,6 +584,7 @@ export async function prepareResponsesTransport(
487
584
  const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
488
585
  if (!current) throw new Error("API key selection is unavailable before dispatch");
489
586
  route.provider = current;
587
+ await resolveCopilotSelection(requestParsed, true);
490
588
  }
491
589
  adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
492
590
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
@@ -552,13 +650,14 @@ export async function prepareResponsesTransport(
552
650
  throw new Error("Account selection changed repeatedly before turn dispatch");
553
651
  } finally { producer?.close(); }
554
652
  };
555
- const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed): ProviderFetchOptions["dispatchOverride"] => {
653
+ /** Fence the physical send, with bounded credential/session rebuilds and safe negotiated refusals. */
654
+ const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed, owner: "adapter" | "passthrough" = "adapter"): ProviderFetchOptions["dispatchOverride"] => {
556
655
  if (route.provider.authMode === "forward") return undefined;
557
- return async (input, init, execute) => {
656
+ return /** Dispatch with the current binding and session, rebuilding at most three times before returning a safe refusal. */ async (input, init, execute) => {
558
657
  let destination = input;
559
658
  let dispatchInit = init;
560
659
  for (let attempt = 0; attempt < 3; attempt++) {
561
- if (selectionIsCurrent(requestBindings.get(wireRequest))) {
660
+ if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
562
661
  const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
563
662
  const binding = requestBindings.get(wireRequest);
564
663
  const snapshot = anthropicInstance !== undefined && binding?.kind === "oauth"
@@ -622,7 +721,16 @@ export async function prepareResponsesTransport(
622
721
  if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
623
722
  return response;
624
723
  }
625
- const nextAdapter = await refreshDispatchAdapter(requestParsed);
724
+ let nextAdapter: ProviderAdapter;
725
+ try { nextAdapter = await refreshDispatchAdapter(requestParsed); }
726
+ catch (error) {
727
+ const refusal = copilotRefusalResponse(error);
728
+ if (refusal) return refusal;
729
+ throw error;
730
+ }
731
+ if (owner === "passthrough" && route.providerName === "github-copilot" && nextAdapter.name === "openai-chat") {
732
+ throw new CopilotAutoAdapterHandoff(nextAdapter);
733
+ }
626
734
  // Rebind before rebuilding: the rebuild's bridged-search restore and continuation
627
735
  // restore key on the serving identity, which must be the refreshed route's, not the
628
736
  // credential whose selection just lapsed.
@@ -876,7 +984,15 @@ export async function prepareResponsesTransport(
876
984
  ? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
877
985
  : undefined,
878
986
  );
987
+ try { await resolveCopilotSelection(parsed); }
988
+ catch (error) {
989
+ if ((options.abortSignal ?? req.signal).aborted) return clientCancelledResponse();
990
+ const refusal = copilotRefusalResponse(error);
991
+ if (refusal) return refusal;
992
+ return formatErrorResponse(502, "provider_error", "GitHub Copilot Auto negotiation failed; retry or check your Copilot account permissions.");
993
+ }
879
994
  let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy);
995
+ if (copilotAutoAdapter) adapterProvider = { ...adapterProvider, adapter: copilotAutoAdapter };
880
996
  const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
881
997
  && !isCanonicalOpenAiForwardProvider(adapterProvider)
882
998
  && ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
@@ -1008,6 +1124,8 @@ export async function prepareResponsesTransport(
1008
1124
  set genericFailovers(value: typeof genericFailovers) {
1009
1125
  genericFailovers = value;
1010
1126
  },
1127
+ get oauth401ReplayAttempted(): boolean { return oauth401ReplayAttempted; },
1128
+ set oauth401ReplayAttempted(value: boolean) { oauth401ReplayAttempted = value; },
1011
1129
  get adapter(): ProviderAdapter {
1012
1130
  return adapter;
1013
1131
  },
@@ -1065,6 +1183,8 @@ export async function prepareResponsesTransport(
1065
1183
  applyFailoverSnapshot,
1066
1184
  selectionIsCurrent,
1067
1185
  resolveSelectionAdapter,
1186
+ resolveCopilotSelection,
1187
+ copilotRefusalResponse,
1068
1188
  refreshRunTurnAdapter,
1069
1189
  oauthDispatch,
1070
1190
  noteRoutedAttemptSend,
@@ -1,3 +1,4 @@
1
+ import { mapSpendLedgerStorageError } from "./spend-storage-error";
1
2
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
2
3
  import type { PreparedResponsesRequest } from "./request-prepare";
3
4
  import type { ResponsesTransport } from "./request-transport";
@@ -20,7 +21,7 @@ import type { AttemptRecoveryKind } from "../../usage/log";
20
21
  import { providerFetch } from "./fetch-helpers";
21
22
  import { normalizeLogConversationId } from "../request-log-conversation";
22
23
  import { normalizeDeclaredToolName, type AdapterEvent, type OcxProviderContinuationState } from "../../types";
23
- import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
24
+ import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE, SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE, type OcxErrorPayload } from "../../lib/errors";
24
25
  import { SendBudgetExhaustedError, markResponseNonReplayable } from "../../lib/upstream-retry";
25
26
  import {
26
27
  hasEligibleGenericOAuthFailoverTarget,
@@ -315,7 +316,8 @@ export async function executeResponsesRunTurn(
315
316
  Object.assign(parsed, routeState);
316
317
  }
317
318
  } catch (err) {
318
- emit(err instanceof RequestPacingQueueOverloadError
319
+ const storageRefusal = mapSpendLedgerStorageError(err, logCtx);
320
+ emit(storageRefusal ? { type: "error", ...storageRefusal } : err instanceof RequestPacingQueueOverloadError
319
321
  ? {
320
322
  type: "error",
321
323
  status: 429,
@@ -713,7 +715,8 @@ export async function executeResponsesRunTurn(
713
715
  runTurnAbort.abort();
714
716
  queue.close();
715
717
  const message = preflight.error?.message ?? "Adapter ended before producing a response";
716
- const failure = formatErrorResponse(502, "upstream_error", redactSecretString(message));
718
+ const failure = formatErrorResponse(preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503 : 502,
719
+ preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : "upstream_error", redactSecretString(message));
717
720
  // A replay-unsafe heartbeat means the adapter already ran a local side effect, so the
718
721
  // combo must not send this turn to another target: the failure stays with this child.
719
722
  if (preflight.replayUnsafe) markResponseNonReplayable(failure);
@@ -912,7 +915,9 @@ export async function executeResponsesRunTurn(
912
915
  const message = classifiedError?.message ?? (firstMeaningful?.type === "error"
913
916
  ? firstMeaningful.message
914
917
  : "Adapter ended before producing a response");
915
- const failure = formatErrorResponse(502, "upstream_error", redactSecretString(message));
918
+ const storageRefusal = firstMeaningful?.type === "error" && firstMeaningful.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
919
+ const failure = formatErrorResponse(storageRefusal ? 503 : 502,
920
+ storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : "upstream_error", redactSecretString(message));
916
921
  if (replayUnsafe) markResponseNonReplayable(failure);
917
922
  return failure;
918
923
  }
@@ -956,7 +961,10 @@ export async function executeResponsesRunTurn(
956
961
  commitReasoningReplayServingRoute();
957
962
  }
958
963
  notifyResponseComplete(json);
959
- return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
964
+ return new Response(JSON.stringify(json), {
965
+ status: (json.error as OcxErrorPayload | undefined)?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503 : 200,
966
+ headers: { "Content-Type": "application/json" },
967
+ });
960
968
  } catch (error) {
961
969
  if (!isTranslatorBudgetExceededError(error)) throw error;
962
970
  runTurnAbort.abort();
@@ -79,6 +79,7 @@ export async function executeResponsesSidecars(
79
79
  | "anthropicSessionKey"
80
80
  | "commitResolvedOAuthSelection"
81
81
  | "resolveSelectionAdapter"
82
+ | "resolveCopilotSelection"
82
83
  | "oauthDispatch"
83
84
  | "noteRoutedAttemptSend"
84
85
  | "bindKeyUsageFromBridge"
@@ -103,6 +104,7 @@ export async function executeResponsesSidecars(
103
104
  anthropicSessionKey,
104
105
  commitResolvedOAuthSelection,
105
106
  resolveSelectionAdapter,
107
+ resolveCopilotSelection,
106
108
  oauthDispatch,
107
109
  } = transportState;
108
110
  const {
@@ -196,6 +198,7 @@ export async function executeResponsesSidecars(
196
198
  // A single request may rotate once for a verification refusal. Keep this separate from the
197
199
  // broader OAuth failover count: a sidecar turn can also spend that count on rate-limit recovery.
198
200
  let antigravityValidationRotationAttempted = false;
201
+ /** Admit a replacement credential, or leave the loop's original refusal authoritative. */
199
202
  const rotateSidecarProviderOn429 = async (
200
203
  retryAfter: string | null,
201
204
  responseHeaders?: Headers,
@@ -280,6 +283,8 @@ export async function executeResponsesSidecars(
280
283
  // `applyFailoverSnapshot` has already rebound the route to the sibling identity/project.
281
284
  } else if (rotated) {
282
285
  route.provider = rotated;
286
+ try { await resolveCopilotSelection(retryParsed ?? parsed); }
287
+ catch { return null; } // Keep the sidecar's original refusal when replacement negotiation fails.
283
288
  } else if (
284
289
  // A POSITIVE gate, not an early return. An early `return null` here made every later arm
285
290
  // unreachable: Anthropic never has a genericFailoverAccountId (isGenericFailoverProvider
@@ -0,0 +1,33 @@
1
+ import { formatErrorResponse } from "../../bridge";
2
+ import { SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE } from "../../lib/errors";
3
+ import { markLocalRequestLogRefusal, type RequestLogContext } from "../request-log";
4
+ import { SpendLedgerFileRefusedError } from "../../lib/spend-reservation-ledger";
5
+
6
+ type StorageRefusalLog = Pick<RequestLogContext, "localTerminalReason" | "terminalSource" | "errorCode">;
7
+
8
+ /** Local storage refusal, including retry wrappers: never provider-health evidence. */
9
+ export function mapSpendLedgerStorageError(error: unknown, logCtx?: StorageRefusalLog) {
10
+ const seen = new Set<unknown>();
11
+ while (error instanceof Error && !seen.has(error)) {
12
+ if (error instanceof SpendLedgerFileRefusedError) {
13
+ if (logCtx) {
14
+ markLocalRequestLogRefusal(logCtx, SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE);
15
+ logCtx.errorCode = SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
16
+ }
17
+ return {
18
+ status: 503,
19
+ errorType: "server_error",
20
+ code: SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE,
21
+ message: error.message,
22
+ } as const;
23
+ }
24
+ seen.add(error);
25
+ error = error.cause;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ export function spendLedgerStorageErrorResponse(error: unknown, logCtx?: StorageRefusalLog): Response | undefined {
31
+ const refusal = mapSpendLedgerStorageError(error, logCtx);
32
+ return refusal && formatErrorResponse(refusal.status, refusal.code, refusal.message);
33
+ }
@@ -442,6 +442,11 @@ export type AdapterEvent =
442
442
  errorType?: string;
443
443
  code?: string;
444
444
  retryable?: boolean;
445
+ /**
446
+ * Proxy-internal, never serialized: the upstream failed the turn before producing text or a
447
+ * tool call, so the opt-in empty-completion guard may replay the identical request once.
448
+ */
449
+ replaySafeBeforeOutput?: boolean;
445
450
  };
446
451
 
447
452
  /**
@@ -835,7 +835,7 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
835
835
  {
836
836
  // xAI documents the same whole-request >=200k band for grok-4.7;
837
837
  // priority stacking remains a lower bound. See
838
- // devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
838
+ // devlog/_fin/260923_grok47_parity/010_probe-evidence.md.
839
839
  provider: "xai",
840
840
  modelId: "grok-4.7",
841
841
  thresholdInputTokens: 200_000,