@yansigit/opencodex 2.33.0 → 2.33.1
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/gui/dist/assets/index-CIDo4y4k.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +3 -1
- package/src/adapters/command-code.ts +101 -20
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-transport.ts +3 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +13 -25
- package/src/adapters/cursor/native-exec.ts +18 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +20 -3
- package/src/adapters/cursor/request-builder.ts +7 -0
- package/src/adapters/cursor/tool-definitions.ts +22 -1
- package/src/adapters/cursor/tool-result-normalize.ts +21 -8
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/cursor.ts +114 -0
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google.ts +108 -16
- package/src/adapters/openai-responses.ts +1 -0
- package/src/chat/inbound.ts +15 -0
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog/provider-fetch.ts +34 -0
- package/src/generated/compatibility-version.json +91 -47
- package/src/generated/model-metadata.ts +3 -0
- package/src/oauth/aistudio-native-daemon.ts +62 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/key-providers.ts +8 -0
- package/src/oauth/login-cli.ts +66 -1
- package/src/providers/derive.ts +1 -1
- package/src/providers/quota.ts +90 -38
- package/src/providers/registry.ts +24 -3
- package/src/router.ts +3 -0
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/server/aistudio-ws-hub.ts +295 -0
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +2 -0
- package/src/server/index.ts +94 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/request-log-conversation.ts +12 -0
- package/src/server/request-log.ts +2 -1
- package/src/server/responses/core.ts +4 -3
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/ws-bridge.ts +2 -1
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/types/provider.ts +2 -1
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +31 -7
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +83 -0
- package/src/usage/log.ts +2 -2
- package/src/usage/summary.ts +34 -12
- package/src/web-search/index.ts +16 -8
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google AI Studio Web-UI (SAPISIDHASH) authorization & header builder.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface GoogleCookieJar {
|
|
6
|
+
sapisid?: string;
|
|
7
|
+
psid?: string;
|
|
8
|
+
ssid?: string;
|
|
9
|
+
hsid?: string;
|
|
10
|
+
sid?: string;
|
|
11
|
+
cookieHeader: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_ORIGIN = "https://aistudio.google.com";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate the Google internal SAPISIDHASH Authorization header value.
|
|
18
|
+
* Formula uses Unix seconds: "SAPISIDHASH <timestamp>_<sha1(timestamp + " " + SAPISID + " " + origin)>".
|
|
19
|
+
*/
|
|
20
|
+
export async function generateSapisidHash(
|
|
21
|
+
sapisid: string,
|
|
22
|
+
origin: string = DEFAULT_ORIGIN,
|
|
23
|
+
timestamp: number = Date.now()
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
// Callers historically passed Date.now() (milliseconds); Google expects Unix seconds.
|
|
26
|
+
const seconds = Math.floor(timestamp > 10_000_000_000 ? timestamp / 1000 : timestamp);
|
|
27
|
+
const raw = `${seconds} ${sapisid} ${origin}`;
|
|
28
|
+
const buf = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(raw));
|
|
29
|
+
const hexHash = Array.from(new Uint8Array(buf))
|
|
30
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
31
|
+
.join("");
|
|
32
|
+
return `SAPISIDHASH ${seconds}_${hexHash}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a raw cookie string (from browser export, header, or config) into tokens and a normalized string.
|
|
37
|
+
*/
|
|
38
|
+
export function parseGoogleCookieJar(cookieInput: string): GoogleCookieJar {
|
|
39
|
+
const cleanInput = (cookieInput || "").trim();
|
|
40
|
+
const jar: GoogleCookieJar = { cookieHeader: cleanInput };
|
|
41
|
+
if (!cleanInput) return jar;
|
|
42
|
+
if (/[\r\n\u0000]/.test(cleanInput)) return { cookieHeader: "" };
|
|
43
|
+
|
|
44
|
+
const parts = cleanInput.split(";").map((p) => p.trim());
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
const eqIdx = part.indexOf("=");
|
|
47
|
+
if (eqIdx === -1) continue;
|
|
48
|
+
const name = part.slice(0, eqIdx).trim();
|
|
49
|
+
const value = part.slice(eqIdx + 1).trim();
|
|
50
|
+
|
|
51
|
+
if (name === "SAPISID" || name === "__Secure-3PAPISID") {
|
|
52
|
+
jar.sapisid = value;
|
|
53
|
+
} else if (name === "__Secure-1PSID" || name === "__Secure-3PSID") {
|
|
54
|
+
jar.psid = value;
|
|
55
|
+
} else if (name === "SSID") {
|
|
56
|
+
jar.ssid = value;
|
|
57
|
+
} else if (name === "HSID") {
|
|
58
|
+
jar.hsid = value;
|
|
59
|
+
} else if (name === "SID") {
|
|
60
|
+
jar.sid = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return jar;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate whether a cookie jar contains sufficient authentication credentials.
|
|
69
|
+
*/
|
|
70
|
+
export function validateAiStudioCookies(jar: GoogleCookieJar): { valid: boolean; error?: string } {
|
|
71
|
+
if (!jar.sapisid || !jar.cookieHeader) {
|
|
72
|
+
return {
|
|
73
|
+
valid: false,
|
|
74
|
+
error: "Missing SAPISID cookie required for Google AI Studio authorization.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { valid: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build the HTTP headers required for alkalimakersuite-pa.clients6.google.com calls.
|
|
82
|
+
*/
|
|
83
|
+
export async function buildAiStudioHeaders(
|
|
84
|
+
jar: GoogleCookieJar,
|
|
85
|
+
origin: string = DEFAULT_ORIGIN
|
|
86
|
+
): Promise<Record<string, string>> {
|
|
87
|
+
const sapisid = jar.sapisid || "";
|
|
88
|
+
const authHeader = await generateSapisidHash(sapisid, origin);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"Authorization": authHeader,
|
|
92
|
+
"Cookie": jar.cookieHeader,
|
|
93
|
+
"X-Goog-AuthUser": "0",
|
|
94
|
+
"Origin": origin,
|
|
95
|
+
"Referer": origin.endsWith("/") ? origin : `${origin}/`,
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
2
|
import { deriveKeyLoginMap, enrichProviderFromRegistry, type DerivedKeyLoginProvider } from "../providers/derive";
|
|
3
3
|
import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
4
|
+
import { parseGoogleCookieJar, validateAiStudioCookies } from "./google-aistudio-auth";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* API-key "login" providers: not OAuth — the flow opens the provider's dashboard so the user can
|
|
@@ -86,6 +87,13 @@ export async function validateApiKey(
|
|
|
86
87
|
return "unknown";
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
if (provider.adapter === "google" && provider.googleMode === "ai-studio-web") {
|
|
91
|
+
const jar = parseGoogleCookieJar(key);
|
|
92
|
+
const val = validateAiStudioCookies(jar);
|
|
93
|
+
if (!val.valid) return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
if (provider.adapter === "google" && (provider.googleMode ?? "ai-studio") === "ai-studio") {
|
|
90
98
|
// Generative Language API rejects Bearer-wrapped API keys; probe models.list with the
|
|
91
99
|
// documented x-goog-api-key header instead (pageSize=1 — validation only needs a 200).
|
package/src/oauth/login-cli.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { codexAccountNamespaceProviderCollisionError } from "../codex/account-na
|
|
|
15
15
|
const LIVE_RELOAD_PROVIDERS = new Set<string>([
|
|
16
16
|
...listOAuthProviders(),
|
|
17
17
|
...Object.keys(KEY_LOGIN_PROVIDERS),
|
|
18
|
+
"google-aistudio",
|
|
18
19
|
]);
|
|
19
20
|
|
|
20
21
|
export function runningProxyUpdateHeaders(): Headers {
|
|
@@ -65,16 +66,80 @@ export function warnIfLiveReloadSkipped(result: LocalProviderReloadResult | null
|
|
|
65
66
|
|
|
66
67
|
export async function handleLogin(provider?: string): Promise<void> {
|
|
67
68
|
const name = (provider ?? "").trim().toLowerCase();
|
|
69
|
+
if (name === "google-aistudio" || name === "aistudio" || name === "gemini-aistudio") {
|
|
70
|
+
return handleAiStudioBridgeLogin();
|
|
71
|
+
}
|
|
68
72
|
if (isPublicOAuthProvider(name)) return handleOAuthLogin(name);
|
|
69
73
|
if (isKeyLoginProvider(name)) return handleKeyLogin(name);
|
|
70
74
|
console.error(
|
|
71
75
|
`Usage: ocx login <provider>\n` +
|
|
72
|
-
` OAuth
|
|
76
|
+
` OAuth / Web: ${[...listOAuthProviders(), "google-aistudio"].join(", ")}\n` +
|
|
73
77
|
` API-key login: ${Object.keys(KEY_LOGIN_PROVIDERS).join(", ")}`,
|
|
74
78
|
);
|
|
75
79
|
process.exit(1);
|
|
76
80
|
}
|
|
77
81
|
|
|
82
|
+
async function handleAiStudioBridgeLogin(): Promise<void> {
|
|
83
|
+
const live = await findLiveProxy();
|
|
84
|
+
const port = live?.port ?? 10100;
|
|
85
|
+
const bridgeUrl = "http://127.0.0.1:" + port + "/aistudio/bridge";
|
|
86
|
+
|
|
87
|
+
console.log("\n🌐 Google AI Studio Sign-In & Session Setup:");
|
|
88
|
+
console.log(" Option 1: Paste Session Token from the Brave/Chrome extension popup (Passkey-friendly)");
|
|
89
|
+
console.log(" Option 2: Open native macOS sign-in window");
|
|
90
|
+
console.log(" Option 3: Open browser bridge page (" + bridgeUrl + ")\n");
|
|
91
|
+
|
|
92
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
93
|
+
try {
|
|
94
|
+
const choice = await new Promise<string>((res) => {
|
|
95
|
+
rl.question("Paste Session Token (or press Enter for native window): ", (ans) => res(ans.trim()));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (choice.length > 20) {
|
|
99
|
+
const { saveAiStudioSessionFromToken } = await import("./aistudio-session-sync");
|
|
100
|
+
saveAiStudioSessionFromToken(choice);
|
|
101
|
+
console.log("\n✅ Session token imported successfully! Saved to ~/.opencodex/aistudio-session.json");
|
|
102
|
+
} else if (process.platform === "darwin") {
|
|
103
|
+
const { getAiStudioNativeDaemonSourcePath } = await import("./aistudio-native-daemon");
|
|
104
|
+
const swiftSrc = getAiStudioNativeDaemonSourcePath();
|
|
105
|
+
console.log("\n🚀 Opening native Google AI Studio login window...");
|
|
106
|
+
const proc = Bun.spawn(["swift", swiftSrc, "--login"], {
|
|
107
|
+
stdout: "inherit",
|
|
108
|
+
stderr: "inherit",
|
|
109
|
+
});
|
|
110
|
+
const code = await proc.exited;
|
|
111
|
+
if (code === 0) {
|
|
112
|
+
console.log("\n✅ Google AI Studio authenticated successfully! Session saved to ~/.opencodex/aistudio-session.json");
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.log("\n🌐 Opening bridge page in your browser: " + bridgeUrl);
|
|
116
|
+
openUrl(bridgeUrl);
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
rl.close();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
if (!config.providers["google-aistudio"]) {
|
|
124
|
+
config.providers["google-aistudio"] = {
|
|
125
|
+
adapter: "google",
|
|
126
|
+
googleMode: "ai-studio-web",
|
|
127
|
+
baseUrl: "https://alkalimakersuite-pa.clients6.google.com",
|
|
128
|
+
authMode: "local",
|
|
129
|
+
liveModels: false,
|
|
130
|
+
defaultModel: "gemini-3.7-flash",
|
|
131
|
+
models: ["gemini-3.7-flash", "gemini-3.1-pro-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-3.5-flash"],
|
|
132
|
+
};
|
|
133
|
+
saveConfig(config);
|
|
134
|
+
console.log("\n ✓ Configured 'google-aistudio' in ~/.opencodex/config.json");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
openUrl(bridgeUrl);
|
|
138
|
+
const reload = await notifyRunningProxy("google-aistudio");
|
|
139
|
+
console.log("\n✅ Ready! Use models with 'google-aistudio' provider in your coding agents.");
|
|
140
|
+
warnIfLiveReloadSkipped(reload);
|
|
141
|
+
}
|
|
142
|
+
|
|
78
143
|
async function handleOAuthLogin(name: string): Promise<void> {
|
|
79
144
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
80
145
|
try {
|
package/src/providers/derive.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface DerivedKeyLoginProvider {
|
|
|
49
49
|
thinkingBudgetModels?: string[];
|
|
50
50
|
escapeBuiltinToolNames?: boolean;
|
|
51
51
|
openaiChatEofTolerance?: boolean;
|
|
52
|
-
googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
|
|
52
|
+
googleMode?: "ai-studio" | "vertex" | "cloud-code-assist" | "ai-studio-web";
|
|
53
53
|
project?: string;
|
|
54
54
|
location?: string;
|
|
55
55
|
}
|
package/src/providers/quota.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport
|
|
|
16
16
|
import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
|
|
17
17
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
18
18
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
|
|
19
|
+
import { globalAiStudioRelayHub } from "../server/aistudio-ws-hub";
|
|
19
20
|
import {
|
|
20
21
|
captureConfigGeneration,
|
|
21
22
|
sweepExpiredOnWrite,
|
|
@@ -1401,7 +1402,12 @@ export interface ProviderAccountQuota {
|
|
|
1401
1402
|
|
|
1402
1403
|
/** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
|
|
1403
1404
|
export function supportsPerAccountQuota(provider: string): boolean {
|
|
1404
|
-
return
|
|
1405
|
+
return (
|
|
1406
|
+
provider === "anthropic" ||
|
|
1407
|
+
provider === "google-antigravity" ||
|
|
1408
|
+
provider === "command-code" ||
|
|
1409
|
+
provider === "cursor"
|
|
1410
|
+
);
|
|
1405
1411
|
}
|
|
1406
1412
|
|
|
1407
1413
|
function accountCacheKey(provider: string, accountId: string): string {
|
|
@@ -1526,6 +1532,14 @@ async function fetchAccountQuota(
|
|
|
1526
1532
|
quota = await fetchAnthropicUsageQuota(token);
|
|
1527
1533
|
} else if (provider === "google-antigravity") {
|
|
1528
1534
|
quota = await fetchAntigravityAccountQuota(accountId);
|
|
1535
|
+
} else if (provider === "command-code") {
|
|
1536
|
+
const token = await getTokenForAccountQuotaProbe(provider, accountId);
|
|
1537
|
+
const res = await fetchCommandCodeUsageQuota(token);
|
|
1538
|
+
quota = res === TERMINAL_QUOTA_FAILURE ? null : res;
|
|
1539
|
+
} else if (provider === "cursor") {
|
|
1540
|
+
const token = await getTokenForAccountQuotaProbe(provider, accountId);
|
|
1541
|
+
const res = await fetchCursorUsageQuota(token);
|
|
1542
|
+
quota = res?.quota ?? null;
|
|
1529
1543
|
}
|
|
1530
1544
|
if (!quota) {
|
|
1531
1545
|
// Preserve last-good bars and mark unavailable; advance TTL so failures
|
|
@@ -1741,8 +1755,9 @@ function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: nu
|
|
|
1741
1755
|
if (!row) return null;
|
|
1742
1756
|
const cap = toFiniteNumber(row.cap);
|
|
1743
1757
|
const used = toFiniteNumber(row.used);
|
|
1744
|
-
|
|
1745
|
-
|
|
1758
|
+
const percent = cap !== undefined && used !== undefined && cap > 0 && used >= 0
|
|
1759
|
+
? normalizePercent((used / cap) * 100)
|
|
1760
|
+
: normalizePercent(row.percent);
|
|
1746
1761
|
if (percent === undefined) return null;
|
|
1747
1762
|
const resetAt = quotaResetAt(row);
|
|
1748
1763
|
return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
@@ -1829,11 +1844,7 @@ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise
|
|
|
1829
1844
|
* usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
|
|
1830
1845
|
* whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
|
|
1831
1846
|
*/
|
|
1832
|
-
async function
|
|
1833
|
-
// Never release credentials to a user-edited or lookalike provider host.
|
|
1834
|
-
if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
|
|
1835
|
-
const bearer = await resolveCommandCodeQuotaBearer(config);
|
|
1836
|
-
if (!bearer) return null;
|
|
1847
|
+
async function fetchCommandCodeUsageQuota(bearer: string): Promise<ProviderQuota | null | typeof TERMINAL_QUOTA_FAILURE> {
|
|
1837
1848
|
const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
|
|
1838
1849
|
const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
|
|
1839
1850
|
const org = asRecord(whoami?.org);
|
|
@@ -1857,7 +1868,7 @@ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig
|
|
|
1857
1868
|
const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
|
|
1858
1869
|
const weekly = parseCommandCodeWindow(limits?.weekly);
|
|
1859
1870
|
const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
|
|
1860
|
-
return
|
|
1871
|
+
return {
|
|
1861
1872
|
...(fiveHour ? {
|
|
1862
1873
|
fiveHourPercent: fiveHour.percent,
|
|
1863
1874
|
...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
@@ -1868,18 +1879,21 @@ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig
|
|
|
1868
1879
|
} : {}),
|
|
1869
1880
|
...(creditsUsd ? { creditsUsd } : {}),
|
|
1870
1881
|
updatedAt: Date.now(),
|
|
1871
|
-
}
|
|
1882
|
+
};
|
|
1872
1883
|
}
|
|
1873
1884
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1885
|
+
async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
1886
|
+
// Never release credentials to a user-edited or lookalike provider host.
|
|
1887
|
+
if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
|
|
1888
|
+
const bearer = await resolveCommandCodeQuotaBearer(config);
|
|
1889
|
+
if (!bearer) return null;
|
|
1890
|
+
const result = await fetchCommandCodeUsageQuota(bearer);
|
|
1891
|
+
if (!result || result === TERMINAL_QUOTA_FAILURE) return result;
|
|
1892
|
+
return report(provider, "command-code:credits", result);
|
|
1893
|
+
}
|
|
1882
1894
|
|
|
1895
|
+
/** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
|
|
1896
|
+
async function fetchCursorUsageQuota(accessToken: string): Promise<{ quota: ProviderQuota; source: string } | null> {
|
|
1883
1897
|
const authHeaders = {
|
|
1884
1898
|
Accept: "application/json",
|
|
1885
1899
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -1939,15 +1953,17 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
1939
1953
|
}
|
|
1940
1954
|
|
|
1941
1955
|
if (totalPercent !== undefined || customWindows.length > 0) {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
...(
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1956
|
+
return {
|
|
1957
|
+
source: "cursor:period-usage",
|
|
1958
|
+
quota: {
|
|
1959
|
+
...(totalPercent !== undefined ? {
|
|
1960
|
+
monthlyPercent: totalPercent,
|
|
1961
|
+
...(resetAt !== undefined ? { monthlyResetAt: resetAt } : {}),
|
|
1962
|
+
} : {}),
|
|
1963
|
+
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
1964
|
+
updatedAt: Date.now(),
|
|
1965
|
+
},
|
|
1966
|
+
};
|
|
1951
1967
|
}
|
|
1952
1968
|
}
|
|
1953
1969
|
}
|
|
@@ -1973,12 +1989,14 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
1973
1989
|
? normalizePercent((used / limit) * 100)
|
|
1974
1990
|
: undefined);
|
|
1975
1991
|
if (percent !== undefined) {
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1992
|
+
return {
|
|
1993
|
+
source: "cursor:usage-summary",
|
|
1994
|
+
quota: {
|
|
1995
|
+
monthlyPercent: percent,
|
|
1996
|
+
monthlyResetAt: normalizeResetAt(body?.billingCycleEnd),
|
|
1997
|
+
updatedAt: Date.now(),
|
|
1998
|
+
},
|
|
1999
|
+
};
|
|
1982
2000
|
}
|
|
1983
2001
|
}
|
|
1984
2002
|
}
|
|
@@ -2027,11 +2045,26 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
2027
2045
|
return Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, start.getUTCDate());
|
|
2028
2046
|
})()
|
|
2029
2047
|
: undefined;
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2048
|
+
return {
|
|
2049
|
+
source: "cursor:auth-usage",
|
|
2050
|
+
quota: {
|
|
2051
|
+
monthlyPercent: percent,
|
|
2052
|
+
...(monthlyResetAt !== undefined ? { monthlyResetAt } : {}),
|
|
2053
|
+
updatedAt: Date.now(),
|
|
2054
|
+
},
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
2059
|
+
let accessToken: string;
|
|
2060
|
+
try {
|
|
2061
|
+
accessToken = await getValidAccessToken("cursor");
|
|
2062
|
+
} catch {
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
const result = await fetchCursorUsageQuota(accessToken);
|
|
2066
|
+
if (!result) return null;
|
|
2067
|
+
const built = report(provider, result.source, result.quota);
|
|
2035
2068
|
return built ? { ...built, reverseEngineered: true } : null;
|
|
2036
2069
|
}
|
|
2037
2070
|
|
|
@@ -2219,6 +2252,22 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
|
|
|
2219
2252
|
});
|
|
2220
2253
|
}
|
|
2221
2254
|
|
|
2255
|
+
async function fetchAiStudioQuota(name: string, provider: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
|
|
2256
|
+
const active = globalAiStudioRelayHub.hasActiveSessions();
|
|
2257
|
+
const sessionCount = globalAiStudioRelayHub.getActiveSessionCount();
|
|
2258
|
+
const now = Date.now();
|
|
2259
|
+
|
|
2260
|
+
return {
|
|
2261
|
+
provider: name,
|
|
2262
|
+
label: "Google AI Studio (Web)",
|
|
2263
|
+
source: active ? `Browser Relay (${sessionCount} active tab${sessionCount > 1 ? "s" : ""})` : "Browser Relay (Disconnected)",
|
|
2264
|
+
updatedAt: now,
|
|
2265
|
+
quota: {
|
|
2266
|
+
updatedAt: now,
|
|
2267
|
+
},
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2222
2271
|
async function maybeFetchProviderQuota(
|
|
2223
2272
|
name: string,
|
|
2224
2273
|
provider: OcxProviderConfig,
|
|
@@ -2294,6 +2343,9 @@ async function maybeFetchProviderQuota(
|
|
|
2294
2343
|
if ((provider.authMode ?? "key") === "key" && name === "neuralwatt") {
|
|
2295
2344
|
return fetchNeuralwattQuota(name, provider);
|
|
2296
2345
|
}
|
|
2346
|
+
if (provider.googleMode === "ai-studio-web" || name === "google-aistudio") {
|
|
2347
|
+
return fetchAiStudioQuota(name, provider);
|
|
2348
|
+
}
|
|
2297
2349
|
return null;
|
|
2298
2350
|
} catch {
|
|
2299
2351
|
return null;
|
|
@@ -299,7 +299,7 @@ export interface ProviderRegistryEntry {
|
|
|
299
299
|
jawcodeBundle?: string;
|
|
300
300
|
extraMetadataAliases?: string[];
|
|
301
301
|
metadataModelIdNormalize?: MetadataModelIdNormalize;
|
|
302
|
-
googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
|
|
302
|
+
googleMode?: "ai-studio" | "vertex" | "cloud-code-assist" | "ai-studio-web";
|
|
303
303
|
project?: string;
|
|
304
304
|
location?: string;
|
|
305
305
|
}
|
|
@@ -1599,6 +1599,27 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1599
1599
|
// evidence from ai.google.dev does not establish Vertex publisher availability.
|
|
1600
1600
|
{ id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
|
|
1601
1601
|
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.7-flash", requestPacing: { enabled: true, requestsPerMinute: 30, minIntervalMs: 2_000, jitterMs: 500 }, modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
|
|
1602
|
+
{
|
|
1603
|
+
id: "google-aistudio",
|
|
1604
|
+
label: "Google AI Studio (Web)",
|
|
1605
|
+
adapter: "google",
|
|
1606
|
+
baseUrl: "https://alkalimakersuite-pa.clients6.google.com",
|
|
1607
|
+
authKind: "local",
|
|
1608
|
+
keyOptional: true,
|
|
1609
|
+
featured: true,
|
|
1610
|
+
dashboardPreset: true,
|
|
1611
|
+
dashboardUrl: "https://aistudio.google.com",
|
|
1612
|
+
defaultModel: "gemini-3.7-flash",
|
|
1613
|
+
models: ["gemini-3.7-flash", "gemini-3.1-pro-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-3.5-flash"],
|
|
1614
|
+
liveModels: false,
|
|
1615
|
+
// Conservative pacing for a browser-backed subscription session: avoid bursts while
|
|
1616
|
+
// keeping interactive coding-agent requests usable. Jitter reduces synchronized retries.
|
|
1617
|
+
requestPacing: { enabled: true, requestsPerMinute: 8, minIntervalMs: 7_500, jitterMs: 1_500 },
|
|
1618
|
+
googleMode: "ai-studio-web",
|
|
1619
|
+
jawcodeBundle: "google",
|
|
1620
|
+
extraMetadataAliases: ["aistudio", "gemini-aistudio"],
|
|
1621
|
+
note: "Relays prompts through your active Google AI Studio / Google AI Pro browser session at /aistudio/bridge (default proxy port 10100).",
|
|
1622
|
+
},
|
|
1602
1623
|
{ id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
|
|
1603
1624
|
{ id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
1604
1625
|
{ id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
@@ -2913,8 +2934,8 @@ export function providerCodexAccountMode(id: string, provider?: OcxProviderConfi
|
|
|
2913
2934
|
*/
|
|
2914
2935
|
export function effectiveGoogleMode(
|
|
2915
2936
|
providerId: string,
|
|
2916
|
-
prov: { adapter?: string; googleMode?: "ai-studio" | "vertex" | "cloud-code-assist" },
|
|
2917
|
-
): "ai-studio" | "vertex" | "cloud-code-assist" | null {
|
|
2937
|
+
prov: { adapter?: string; googleMode?: "ai-studio" | "vertex" | "cloud-code-assist" | "ai-studio-web" },
|
|
2938
|
+
): "ai-studio" | "vertex" | "cloud-code-assist" | "ai-studio-web" | null {
|
|
2918
2939
|
if (prov.adapter !== "google") return null;
|
|
2919
2940
|
return prov.googleMode ?? getProviderRegistryEntry(providerId)?.googleMode ?? "ai-studio";
|
|
2920
2941
|
}
|
package/src/router.ts
CHANGED
|
@@ -398,6 +398,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
398
398
|
authMode: canonicalAuthMode,
|
|
399
399
|
apiKey: resolvedApiKey,
|
|
400
400
|
...(staticModelCatalog ? { liveModels: false } : {}),
|
|
401
|
+
...(provider.requestPacing === undefined && registryEntry.requestPacing
|
|
402
|
+
? { requestPacing: structuredClone(registryEntry.requestPacing) }
|
|
403
|
+
: {}),
|
|
401
404
|
...(headers ? { headers } : {}),
|
|
402
405
|
// Backfill the Google wire mode + Vertex project/location from the registry when the user
|
|
403
406
|
// config omits them, so a minimal `google-vertex`/`google-antigravity` entry still routes
|
|
@@ -63,6 +63,14 @@ export function clearCooldownState(poolKey?: string): void {
|
|
|
63
63
|
registryByPool.delete(poolKey);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export function clearPoolAccountCooldown(poolKey: string, accountId: string): boolean {
|
|
67
|
+
const registry = registryByPool.get(poolKey);
|
|
68
|
+
if (!registry) return false;
|
|
69
|
+
const existed = registry.get(accountId) !== null;
|
|
70
|
+
registry.clear(accountId);
|
|
71
|
+
return existed;
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
export function parseRetryAfterMs(
|
|
67
75
|
value: string | null | undefined,
|
|
68
76
|
now = Date.now(),
|