@yansigit/opencodex 2.31.0 → 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.
- package/README.md +7 -0
- package/gui/dist/assets/{index-BNJ7r4Gd.js → index-BAMgarF9.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +71 -0
- package/src/adapters/cursor.ts +90 -28
- package/src/adapters/google-wire-compiler.ts +8 -0
- package/src/adapters/google.ts +26 -0
- package/src/generated/compatibility-version.json +41 -17
- package/src/oauth/anthropic-routing.ts +129 -219
- package/src/oauth/antigravity-routing.ts +165 -0
- package/src/oauth/cursor-routing.ts +252 -0
- package/src/providers/cursor-pool.ts +3 -3
- package/src/routing/account-pool/affinity.ts +125 -0
- package/src/routing/account-pool/cooldown.ts +145 -0
- package/src/routing/account-pool/index.ts +45 -0
- package/src/routing/account-pool/resolve.ts +356 -0
- package/src/routing/account-pool/types.ts +32 -0
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/oauth-account-routes.ts +47 -12
- package/src/server/responses/core.ts +383 -65
- package/src/types/config.ts +8 -0
- package/src/types/request.ts +7 -3
- package/src/usage/log.ts +4 -0
|
@@ -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
|
-
|
|
111
|
-
|
|
112
|
+
bindAntigravitySessionAffinity,
|
|
113
|
+
resolveAntigravityAccountForSession,
|
|
114
|
+
rotateAntigravityAccountOn429,
|
|
112
115
|
} from "../../oauth/antigravity-routing";
|
|
113
|
-
import {
|
|
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?.
|
|
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
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
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
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
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
|
-
|
|
2635
|
-
|
|
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:
|
|
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 (
|
|
2650
|
-
if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
|
|
2700
|
+
if (selection.reason === "failover") replayOAuthCredentialSnapshot = undefined;
|
|
2651
2701
|
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
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
|
|
4786
|
-
|
|
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,
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
/**
|
package/src/types/request.ts
CHANGED
|
@@ -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
|
-
*
|
|
250
|
-
*
|
|
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",
|