@yansigit/opencodex 2.31.1 → 2.31.2

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.
@@ -106,11 +106,25 @@ import {
106
106
  rotateAnthropicAccountOn429,
107
107
  } from "../../oauth/anthropic-routing";
108
108
  import {
109
+ antigravity429StickWaitMs,
110
+ antigravitySessionKeyFromParts,
109
111
  bindAntigravityProject,
110
- isAntigravityAccountInCooldown,
111
- nextAntigravityAccount,
112
+ bindAntigravitySessionAffinity,
113
+ resolveAntigravityAccountForSession,
114
+ rotateAntigravityAccountOn429,
112
115
  } from "../../oauth/antigravity-routing";
113
- import { getAccountCredential, getAccountSet, setActiveAccount } from "../../oauth/store";
116
+ import {
117
+ CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST,
118
+ bindCursorSessionAffinity,
119
+ cursorSessionKeyFromParts,
120
+ formatCursorProviderForLog,
121
+ isCursorAccountPoolActive,
122
+ recordCursorAccountBillingCooldown,
123
+ resolveCursorAccountForSession,
124
+ rotateCursorAccountOn429,
125
+ rotateCursorAccountOnAuth,
126
+ } from "../../oauth/cursor-routing";
127
+ import { getAccountCredential } from "../../oauth/store";
114
128
  import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
115
129
  import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
116
130
  import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
@@ -506,7 +520,7 @@ function bindRouteReasoningReplayScope(args: {
506
520
  // seed assigned before route binding. A Cursor conversation must be scoped to the exact
507
521
  // provider/destination/adapter/model/credential that serves it.
508
522
  if (continuationOwner) parsed._cursorIdentityScope = providerContinuationRouteScope(continuationOwner);
509
- else if (!parsed._cursorIdentityScope?.startsWith("cursor-unowned:")) {
523
+ else if (!parsed._cursorIdentityScope?.trim()) {
510
524
  // Prevent the adapter's token-only fallback from recreating a provider-private id after the
511
525
  // route owner failed closed. The sentinel is per parsed request and contains no credential.
512
526
  parsed._cursorIdentityScope = `cursor-unowned:${randomUUID()}`;
@@ -1185,6 +1199,8 @@ export interface HandleResponsesOptions {
1185
1199
  admission?: DataPlaneAdmission;
1186
1200
  /** Called at most once after the complete client body is read and accepted for dispatch. */
1187
1201
  onRequestBodyRead?: () => void;
1202
+ /** Internal combo handoff invoked after the final adapter accepts the parsed request. */
1203
+ onRequestValidated?: () => void;
1188
1204
  forceEmptyResponseId?: boolean;
1189
1205
  abortSignal?: AbortSignal;
1190
1206
  /** One-shot TTFT callback: first non-empty model output observed (WP4). */
@@ -1820,6 +1836,7 @@ export async function handleComboResponses(
1820
1836
  let lastFailure: Response | null = null;
1821
1837
  while (pick) {
1822
1838
  if (options.abortSignal?.aborted) return clientCancelledResponse();
1839
+ const selectedPick = pick;
1823
1840
  const childLog: RequestLogContext = {
1824
1841
  model: pick.target.model,
1825
1842
  provider: pick.target.provider,
@@ -1841,24 +1858,31 @@ export async function handleComboResponses(
1841
1858
  });
1842
1859
  let resolvedAuth: CodexAuthContext | undefined;
1843
1860
  let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
1844
- const started = Date.now();
1845
- const attempt = beginRequestAttempt(
1846
- (logCtx.attempts?.length ?? 0) + 1,
1847
- pick.target.provider,
1848
- pick.target.model,
1849
- config.providers[pick.target.provider]!.adapter,
1850
- );
1851
- childLog.activeAttempt = attempt;
1861
+ let started: number | undefined;
1862
+ let attempt: ReturnType<typeof beginRequestAttempt> | undefined;
1863
+ const beginAttempt = (): void => {
1864
+ if (attempt) return;
1865
+ started = Date.now();
1866
+ attempt = beginRequestAttempt(
1867
+ (logCtx.attempts?.length ?? 0) + 1,
1868
+ selectedPick.target.provider,
1869
+ selectedPick.target.model,
1870
+ config.providers[selectedPick.target.provider]!.adapter,
1871
+ );
1872
+ childLog.activeAttempt = attempt;
1873
+ childLog.activeAttemptStartedAt = started;
1874
+ recordAttemptRequestedEffort(childLog);
1875
+ };
1852
1876
  let attemptRetained = false;
1853
1877
  const retainCancelledAttempt = (): void => {
1854
- if (attemptRetained) return;
1878
+ if (attemptRetained || !attempt) return;
1855
1879
  sealRequestAttemptIdentity(
1856
1880
  attempt,
1857
1881
  childLog.provider,
1858
1882
  childLog.providerAdapter ?? attempt.adapter,
1859
1883
  childLog.accountLogLabel,
1860
1884
  );
1861
- finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
1885
+ finishRequestAttempt(attempt, 499, Date.now() - (started ?? Date.now()), childLog.usage);
1862
1886
  (logCtx.attempts ??= []).push(attempt);
1863
1887
  attemptRetained = true;
1864
1888
  };
@@ -1878,10 +1902,11 @@ export async function handleComboResponses(
1878
1902
  comboAttempt: true,
1879
1903
  comboReplaySnapshot,
1880
1904
  deferCodexResetDerivedCooldown,
1905
+ onRequestValidated: beginAttempt,
1881
1906
  // Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
1882
1907
  // Object.assign(logCtx, childLog) would overwrite the request-relative value).
1883
1908
  onFirstOutput: () => {
1884
- if (attempt.firstOutputMs === undefined) {
1909
+ if (attempt && started !== undefined && attempt.firstOutputMs === undefined) {
1885
1910
  attempt.firstOutputMs = Math.max(0, Date.now() - started);
1886
1911
  }
1887
1912
  options.onFirstOutput?.();
@@ -1908,6 +1933,7 @@ export async function handleComboResponses(
1908
1933
  }
1909
1934
 
1910
1935
  if (response.ok) {
1936
+ if (!attempt) return response;
1911
1937
  sealRequestAttemptIdentity(
1912
1938
  attempt,
1913
1939
  childLog.provider,
@@ -1954,20 +1980,22 @@ export async function handleComboResponses(
1954
1980
  retainCancelledAttempt();
1955
1981
  return clientCancelledResponse();
1956
1982
  }
1957
- sealRequestAttemptIdentity(
1958
- attempt,
1959
- childLog.provider,
1960
- childLog.providerAdapter ?? attempt.adapter,
1961
- childLog.accountLogLabel,
1962
- );
1963
- finishRequestAttempt(
1964
- attempt,
1965
- response.status,
1966
- Date.now() - started,
1967
- failure.usage,
1968
- );
1969
- (logCtx.attempts ??= []).push(attempt);
1970
- attemptRetained = true;
1983
+ if (attempt) {
1984
+ sealRequestAttemptIdentity(
1985
+ attempt,
1986
+ childLog.provider,
1987
+ childLog.providerAdapter ?? attempt.adapter,
1988
+ childLog.accountLogLabel,
1989
+ );
1990
+ finishRequestAttempt(
1991
+ attempt,
1992
+ response.status,
1993
+ Date.now() - (started ?? Date.now()),
1994
+ failure.usage,
1995
+ );
1996
+ (logCtx.attempts ??= []).push(attempt);
1997
+ attemptRetained = true;
1998
+ }
1971
1999
  lastFailure = failure.response;
1972
2000
  if (comboFailureDecision(failure.response.status, failure.classificationText, {
1973
2001
  code: failure.upstreamCode,
@@ -1976,7 +2004,7 @@ export async function handleComboResponses(
1976
2004
  return lastFailure;
1977
2005
  }
1978
2006
  console.warn(
1979
- `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
2007
+ `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${started === undefined ? 0 : Date.now() - started}ms`,
1980
2008
  );
1981
2009
  const nextPick = advanceComboAfterFailure(config, pick, {
1982
2010
  retryAfter: failure.retryAfter,
@@ -2589,13 +2617,27 @@ async function handleResponsesInner(
2589
2617
  let anthropicPoolFailovers = 0;
2590
2618
  let antigravityAccountId: string | undefined;
2591
2619
  let antigravityFailovers = 0;
2620
+ let cursorPoolAccountId: string | null = null;
2621
+ let cursorPoolFailovers = 0;
2622
+ const oauthSessionKeyParts = {
2623
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2624
+ threadIdHeader: req.headers.get("thread-id"),
2625
+ promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
2626
+ clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
2627
+ promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
2628
+ };
2592
2629
  const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
2593
- ? anthropicSessionKeyFromParts({
2594
- sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2595
- threadIdHeader: req.headers.get("thread-id"),
2596
- promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
2630
+ ? anthropicSessionKeyFromParts(oauthSessionKeyParts)
2631
+ : null;
2632
+ const antigravitySessionKey = route.providerName === "google-antigravity"
2633
+ && route.provider.authMode === "oauth"
2634
+ && route.provider.googleMode === "cloud-code-assist"
2635
+ ? antigravitySessionKeyFromParts(oauthSessionKeyParts)
2636
+ : null;
2637
+ const cursorSessionKey = route.providerName === "cursor"
2638
+ && route.provider.authMode === "oauth"
2639
+ ? cursorSessionKeyFromParts({
2597
2640
  clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
2598
- promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
2599
2641
  })
2600
2642
  : null;
2601
2643
  if (route.provider.authMode === "oauth") {
@@ -2620,48 +2662,88 @@ async function handleResponsesInner(
2620
2662
  promoteAnthropicActiveAccount(selection.accountId);
2621
2663
  route.provider = { ...route.provider, apiKey: accessToken };
2622
2664
  logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
2623
- } else {
2624
- let resolved = await getValidAccessTokenSnapshot(route.providerName);
2625
- let skippedAntigravityCooldown = false;
2626
- if (route.providerName === "google-antigravity"
2627
- && route.provider.googleMode === "cloud-code-assist"
2628
- && isAntigravityAccountInCooldown(resolved.accountId)) {
2629
- const accountIds = getAccountSet("google-antigravity")?.accounts.map(account => account.id) ?? [];
2630
- const nextAccountId = nextAntigravityAccount(accountIds, resolved.accountId);
2631
- if (!nextAccountId) {
2632
- return formatErrorResponse(429, "rate_limit_error", "All Google Antigravity OAuth accounts are temporarily unavailable");
2665
+ } else if (
2666
+ route.providerName === "google-antigravity"
2667
+ && route.provider.googleMode === "cloud-code-assist"
2668
+ ) {
2669
+ const selection = resolveAntigravityAccountForSession(antigravitySessionKey);
2670
+ if (!selection.accountId) {
2671
+ if (selection.reason === "all-cooled") {
2672
+ return formatErrorResponse(
2673
+ 429,
2674
+ "rate_limit_error",
2675
+ "All Google Antigravity OAuth accounts are temporarily unavailable",
2676
+ );
2633
2677
  }
2634
- const accessToken = await getValidAccessTokenForAccount("google-antigravity", nextAccountId);
2635
- const nextCredential = getAccountCredential("google-antigravity", nextAccountId);
2678
+ return formatErrorResponse(
2679
+ 401,
2680
+ "authentication_error",
2681
+ "No eligible Google Antigravity OAuth account available",
2682
+ );
2683
+ }
2684
+ let resolved = await getValidAccessTokenSnapshot(route.providerName);
2685
+ if (selection.accountId !== resolved.accountId) {
2686
+ const accessToken = await getValidAccessTokenForAccount("google-antigravity", selection.accountId);
2687
+ const nextCredential = getAccountCredential("google-antigravity", selection.accountId);
2636
2688
  resolved = {
2637
2689
  ...resolved,
2638
- accountId: nextAccountId,
2690
+ accountId: selection.accountId,
2639
2691
  accessToken,
2640
2692
  // Always replace; omitting a missing id would keep the previous account's projectId.
2641
2693
  projectId: nextCredential?.projectId,
2642
2694
  };
2643
- skippedAntigravityCooldown = true;
2644
2695
  }
2645
2696
  replayOAuthCredentialSnapshot = {
2646
2697
  accountId: resolved.accountId,
2647
2698
  generation: resolved.generation,
2648
2699
  };
2649
- if (skippedAntigravityCooldown) replayOAuthCredentialSnapshot = undefined;
2650
- if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
2700
+ if (selection.reason === "failover") replayOAuthCredentialSnapshot = undefined;
2651
2701
  route.provider = { ...route.provider, apiKey: resolved.accessToken };
2652
- if (route.providerName === "google-antigravity" && route.provider.googleMode === "cloud-code-assist") {
2653
- antigravityAccountId = resolved.accountId;
2654
- // Always overwrite `project` from the credential in use. A missing id fails closed
2655
- // so a rotated account cannot inherit the previous account's Cloud Code Assist project.
2656
- const bound = bindAntigravityProject(route.provider, resolved.projectId);
2657
- if (!bound.ok) {
2658
- return formatErrorResponse(bound.status, bound.type, bound.message);
2659
- }
2660
- route.provider = bound.provider;
2661
- if (skippedAntigravityCooldown) {
2662
- void setActiveAccount("google-antigravity", resolved.accountId).catch(() => { /* best-effort promotion */ });
2702
+ antigravityAccountId = selection.accountId;
2703
+ bindAntigravitySessionAffinity(antigravitySessionKey, selection.accountId);
2704
+ const bound = bindAntigravityProject(route.provider, resolved.projectId);
2705
+ if (!bound.ok) {
2706
+ return formatErrorResponse(bound.status, bound.type, bound.message);
2707
+ }
2708
+ route.provider = bound.provider;
2709
+ } else if (
2710
+ route.providerName === "cursor"
2711
+ && route.provider.authMode === "oauth"
2712
+ && isCursorAccountPoolActive(config)
2713
+ ) {
2714
+ const selection = resolveCursorAccountForSession(cursorSessionKey, config);
2715
+ if (!selection.accountId) {
2716
+ if (selection.reason === "all-cooled") {
2717
+ return formatErrorResponse(
2718
+ 429,
2719
+ "rate_limit_error",
2720
+ "All Cursor OAuth accounts are temporarily rate-limited",
2721
+ );
2663
2722
  }
2723
+ return formatErrorResponse(401, "authentication_error", "No eligible Cursor OAuth account available");
2664
2724
  }
2725
+ let resolved = await getValidAccessTokenSnapshot("cursor");
2726
+ if (selection.accountId !== resolved.accountId) {
2727
+ const accessToken = await getValidAccessTokenForAccount("cursor", selection.accountId);
2728
+ resolved = { ...resolved, accountId: selection.accountId, accessToken };
2729
+ }
2730
+ cursorPoolAccountId = selection.accountId;
2731
+ parsed._cursorIdentityScope = selection.accountId;
2732
+ bindCursorSessionAffinity(cursorSessionKey, selection.accountId);
2733
+ replayOAuthCredentialSnapshot = {
2734
+ accountId: resolved.accountId,
2735
+ generation: resolved.generation,
2736
+ };
2737
+ route.provider = { ...route.provider, apiKey: resolved.accessToken };
2738
+ logCtx.provider = formatCursorProviderForLog("cursor", selection.accountId);
2739
+ } else {
2740
+ const resolved = await getValidAccessTokenSnapshot(route.providerName);
2741
+ replayOAuthCredentialSnapshot = {
2742
+ accountId: resolved.accountId,
2743
+ generation: resolved.generation,
2744
+ };
2745
+ if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
2746
+ route.provider = { ...route.provider, apiKey: resolved.accessToken };
2665
2747
  if (route.providerName === "kiro") {
2666
2748
  // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
2667
2749
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
@@ -2719,6 +2801,16 @@ async function handleResponsesInner(
2719
2801
  logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
2720
2802
  }
2721
2803
  logCtx.providerAdapter = adapter.name;
2804
+ try {
2805
+ adapter.validateRequest?.(parsed);
2806
+ } catch (err) {
2807
+ return formatErrorResponse(
2808
+ 400,
2809
+ "invalid_request_error",
2810
+ redactSecretString(err instanceof Error ? err.message : String(err)),
2811
+ );
2812
+ }
2813
+ options.onRequestValidated?.();
2722
2814
  // Ordinary requests receive one durable attempt only after their final initial
2723
2815
  // adapter is resolved. Combo children own their attempt and retries keep it.
2724
2816
  if (!options.comboAttempt && !logCtx.activeAttempt) {
@@ -4782,8 +4874,29 @@ async function handleResponsesInner(
4782
4874
  && antigravityAccountId
4783
4875
  && antigravityFailovers < 3
4784
4876
  ) {
4785
- const accountIds = getAccountSet("google-antigravity")?.accounts.map(account => account.id) ?? [];
4786
- const nextAccountId = nextAntigravityAccount(accountIds, antigravityAccountId);
4877
+ const stickWaitMs = antigravity429StickWaitMs(antigravityAccountId);
4878
+ if (stickWaitMs !== null) {
4879
+ await Bun.sleep(stickWaitMs);
4880
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4881
+ invalidateSameTargetRequest();
4882
+ activeAdapter = resolveAdapter(
4883
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4884
+ config.cacheRetention,
4885
+ );
4886
+ bindRouteReasoningReplayScope({
4887
+ parsed,
4888
+ providerName: route.providerName,
4889
+ provider: route.provider,
4890
+ adapterName: activeAdapter.name,
4891
+ codexAuthContext: authCtx,
4892
+ forwardHeaders: selectedForwardHeaders,
4893
+ });
4894
+ const result = await rebuildAndRefetch("rate-limit-429");
4895
+ if ("failed" in result) return result.failed;
4896
+ upstreamResponse = result;
4897
+ continue;
4898
+ }
4899
+ const nextAccountId = rotateAntigravityAccountOn429(antigravityAccountId, antigravitySessionKey);
4787
4900
  if (!nextAccountId) break;
4788
4901
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4789
4902
  try {
@@ -4813,7 +4926,6 @@ async function handleResponsesInner(
4813
4926
  codexAuthContext: authCtx,
4814
4927
  forwardHeaders: selectedForwardHeaders,
4815
4928
  });
4816
- void setActiveAccount("google-antigravity", nextAccountId).catch(() => { /* best-effort promotion */ });
4817
4929
  const result = await rebuildAndRefetch("rate-limit-429");
4818
4930
  if ("failed" in result) return result.failed;
4819
4931
  upstreamResponse = result;
@@ -4821,6 +4933,105 @@ async function handleResponsesInner(
4821
4933
  break;
4822
4934
  }
4823
4935
  }
4936
+ if (
4937
+ upstreamResponse.status === 402
4938
+ && cursorPoolAccountId
4939
+ && isCursorAccountPoolActive(config)
4940
+ ) {
4941
+ recordCursorAccountBillingCooldown(
4942
+ cursorPoolAccountId,
4943
+ upstreamResponse.headers.get("retry-after"),
4944
+ );
4945
+ }
4946
+ while (
4947
+ (upstreamResponse.status === 401 || upstreamResponse.status === 403)
4948
+ && route.providerName === "cursor"
4949
+ && route.provider.authMode === "oauth"
4950
+ && cursorPoolAccountId
4951
+ && isCursorAccountPoolActive(config)
4952
+ && cursorPoolFailovers < CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST
4953
+ ) {
4954
+ const nextAccountId = rotateCursorAccountOnAuth(
4955
+ config,
4956
+ cursorPoolAccountId,
4957
+ cursorSessionKey,
4958
+ );
4959
+ if (!nextAccountId) break;
4960
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4961
+ try {
4962
+ const accessToken = await getValidAccessTokenForAccount("cursor", nextAccountId);
4963
+ cursorPoolAccountId = nextAccountId;
4964
+ cursorPoolFailovers += 1;
4965
+ parsed._cursorIdentityScope = nextAccountId;
4966
+ route.provider = { ...route.provider, apiKey: accessToken };
4967
+ replayOAuthCredentialSnapshot = undefined;
4968
+ invalidateSameTargetRequest();
4969
+ activeAdapter = resolveAdapter(
4970
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4971
+ config.cacheRetention,
4972
+ );
4973
+ bindRouteReasoningReplayScope({
4974
+ parsed,
4975
+ providerName: route.providerName,
4976
+ provider: route.provider,
4977
+ adapterName: activeAdapter.name,
4978
+ codexAuthContext: authCtx,
4979
+ forwardHeaders: selectedForwardHeaders,
4980
+ });
4981
+ logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
4982
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
4983
+ const result = await rebuildAndRefetch("cursor-oauth-auth");
4984
+ if ("failed" in result) return result.failed;
4985
+ upstreamResponse = result;
4986
+ } catch {
4987
+ break;
4988
+ }
4989
+ }
4990
+ while (
4991
+ upstreamResponse.status === 429
4992
+ && route.providerName === "cursor"
4993
+ && route.provider.authMode === "oauth"
4994
+ && cursorPoolAccountId
4995
+ && isCursorAccountPoolActive(config)
4996
+ && cursorPoolFailovers < CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST
4997
+ ) {
4998
+ const nextAccountId = rotateCursorAccountOn429(
4999
+ config,
5000
+ cursorPoolAccountId,
5001
+ upstreamResponse.headers.get("retry-after"),
5002
+ cursorSessionKey,
5003
+ );
5004
+ if (!nextAccountId) break;
5005
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
5006
+ try {
5007
+ const accessToken = await getValidAccessTokenForAccount("cursor", nextAccountId);
5008
+ cursorPoolAccountId = nextAccountId;
5009
+ cursorPoolFailovers += 1;
5010
+ parsed._cursorIdentityScope = nextAccountId;
5011
+ route.provider = { ...route.provider, apiKey: accessToken };
5012
+ replayOAuthCredentialSnapshot = undefined;
5013
+ invalidateSameTargetRequest();
5014
+ activeAdapter = resolveAdapter(
5015
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5016
+ config.cacheRetention,
5017
+ );
5018
+ bindRouteReasoningReplayScope({
5019
+ parsed,
5020
+ providerName: route.providerName,
5021
+ provider: route.provider,
5022
+ adapterName: activeAdapter.name,
5023
+ codexAuthContext: authCtx,
5024
+ forwardHeaders: selectedForwardHeaders,
5025
+ });
5026
+ logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5027
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5028
+ const result = await rebuildAndRefetch("cursor-oauth-429");
5029
+ if ("failed" in result) return result.failed;
5030
+ upstreamResponse = result;
5031
+ } catch {
5032
+ break;
5033
+ }
5034
+ }
4824
5035
  // Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
4825
5036
  // or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
4826
5037
  // the missing authoritative signal. Rebuild once through the same sanitation path used by a
@@ -5155,6 +5366,113 @@ async function handleResponsesInner(
5155
5366
  }
5156
5367
  }
5157
5368
  }
5369
+ if (
5370
+ response.status === 402
5371
+ && cursorPoolAccountId
5372
+ && isCursorAccountPoolActive(config)
5373
+ ) {
5374
+ recordCursorAccountBillingCooldown(
5375
+ cursorPoolAccountId,
5376
+ response.headers.get("retry-after"),
5377
+ );
5378
+ }
5379
+ if (
5380
+ (response.status === 401 || response.status === 403)
5381
+ && route.providerName === "cursor"
5382
+ && route.provider.authMode === "oauth"
5383
+ && cursorPoolAccountId
5384
+ && isCursorAccountPoolActive(config)
5385
+ && cursorPoolFailovers < CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST
5386
+ ) {
5387
+ const nextAccountId = rotateCursorAccountOnAuth(
5388
+ config,
5389
+ cursorPoolAccountId,
5390
+ cursorSessionKey,
5391
+ );
5392
+ if (nextAccountId) {
5393
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
5394
+ try {
5395
+ const accessToken = await getValidAccessTokenForAccount("cursor", nextAccountId);
5396
+ cursorPoolAccountId = nextAccountId;
5397
+ cursorPoolFailovers += 1;
5398
+ parsed._cursorIdentityScope = nextAccountId;
5399
+ route.provider = { ...route.provider, apiKey: accessToken };
5400
+ replayOAuthCredentialSnapshot = undefined;
5401
+ invalidateSameTargetRequest();
5402
+ activeAdapter = resolveAdapter(
5403
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5404
+ config.cacheRetention,
5405
+ );
5406
+ bindRouteReasoningReplayScope({
5407
+ parsed: nextParsed,
5408
+ providerName: route.providerName,
5409
+ provider: route.provider,
5410
+ adapterName: activeAdapter.name,
5411
+ });
5412
+ bindRouteReasoningReplayScope({
5413
+ parsed,
5414
+ providerName: route.providerName,
5415
+ provider: route.provider,
5416
+ adapterName: activeAdapter.name,
5417
+ });
5418
+ logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5419
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5420
+ nextContinuationRecoveryKind = "cursor-oauth-auth";
5421
+ continue;
5422
+ } catch {
5423
+ // fall through to emit continuation error below
5424
+ }
5425
+ }
5426
+ }
5427
+ if (
5428
+ response.status === 429
5429
+ && route.providerName === "cursor"
5430
+ && route.provider.authMode === "oauth"
5431
+ && cursorPoolAccountId
5432
+ && isCursorAccountPoolActive(config)
5433
+ && cursorPoolFailovers < CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST
5434
+ ) {
5435
+ const nextAccountId = rotateCursorAccountOn429(
5436
+ config,
5437
+ cursorPoolAccountId,
5438
+ response.headers.get("retry-after"),
5439
+ cursorSessionKey,
5440
+ );
5441
+ if (nextAccountId) {
5442
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
5443
+ try {
5444
+ const accessToken = await getValidAccessTokenForAccount("cursor", nextAccountId);
5445
+ cursorPoolAccountId = nextAccountId;
5446
+ cursorPoolFailovers += 1;
5447
+ parsed._cursorIdentityScope = nextAccountId;
5448
+ route.provider = { ...route.provider, apiKey: accessToken };
5449
+ replayOAuthCredentialSnapshot = undefined;
5450
+ invalidateSameTargetRequest();
5451
+ activeAdapter = resolveAdapter(
5452
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5453
+ config.cacheRetention,
5454
+ );
5455
+ bindRouteReasoningReplayScope({
5456
+ parsed: nextParsed,
5457
+ providerName: route.providerName,
5458
+ provider: route.provider,
5459
+ adapterName: activeAdapter.name,
5460
+ });
5461
+ bindRouteReasoningReplayScope({
5462
+ parsed,
5463
+ providerName: route.providerName,
5464
+ provider: route.provider,
5465
+ adapterName: activeAdapter.name,
5466
+ });
5467
+ logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5468
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5469
+ nextContinuationRecoveryKind = "cursor-oauth-429";
5470
+ continue;
5471
+ } catch {
5472
+ // fall through to emit continuation error below
5473
+ }
5474
+ }
5475
+ }
5158
5476
  if (shouldAttemptImageTierRetry({
5159
5477
  status: response.status,
5160
5478
  adapterName: activeAdapter.name,
@@ -601,6 +601,14 @@ export interface OcxConfig {
601
601
  /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
602
602
  stickyLimit?: number;
603
603
  };
604
+ /**
605
+ * Opt-in Cursor OAuth account pool. Default OFF.
606
+ * Sticky `_clientThreadId` affinity + bounded 429/auth failover when ≥2 OAuth accounts exist.
607
+ * Does not wire weighted-round-robin `CursorCredentialRouter`.
608
+ */
609
+ cursorAccountPool?: {
610
+ enabled?: boolean;
611
+ };
604
612
  /** Virtual `combo/<id>` models spanning concrete provider/model targets (issue #133). */
605
613
  combos?: Record<string, OcxComboConfig>;
606
614
  /**
@@ -245,9 +245,13 @@ export interface OcxRequestOptions {
245
245
  /**
246
246
  * Responses `text.format` (json_schema / json_object), preserved for adapters whose
247
247
  * upstream wire has an equivalent. The openai-chat adapter re-nests it as chat
248
- * `response_format`, the exact inverse of responseFormatToText in src/chat/inbound.ts.
249
- * The native passthrough ignores it (it forwards `_rawBody.text` verbatim) and Kiro
250
- * keeps rejecting structured output via `_structuredOutput`.
248
+ * `response_format`, the exact inverse of responseFormatToText in src/chat/inbound.ts; the
249
+ * Google adapter lowers supported requests to Gemini JSON mode (`responseMimeType` /
250
+ * `responseSchema`) but skips requests with tools, Claude models, or image-capable models.
251
+ * The `openai-chat` adapter can omit it for models in `noStructuredOutputModels`; Kiro
252
+ * rejects structured output via `_structuredOutput`; and Cursor has no structured-output
253
+ * wire field and rejects the request before transport.
254
+ * Native passthrough does not consume this option and forwards `_rawBody.text` verbatim.
251
255
  */
252
256
  textFormat?: {
253
257
  type: "json_schema" | "json_object";
package/src/usage/log.ts CHANGED
@@ -28,6 +28,8 @@ export type AttemptRecoveryKind =
28
28
  | "key-429"
29
29
  | "rate-limit-429"
30
30
  | "anthropic-oauth-429"
31
+ | "cursor-oauth-auth"
32
+ | "cursor-oauth-429"
31
33
  | "image-413"
32
34
  | "opaque-blob-rejection"
33
35
  | "empty-completion";
@@ -218,6 +220,8 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
218
220
  "key-429",
219
221
  "rate-limit-429",
220
222
  "anthropic-oauth-429",
223
+ "cursor-oauth-auth",
224
+ "cursor-oauth-429",
221
225
  "image-413",
222
226
  "opaque-blob-rejection",
223
227
  "empty-completion",