@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.
Files changed (62) hide show
  1. package/gui/dist/assets/index-CIDo4y4k.js +102 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +3 -1
  4. package/src/adapters/command-code.ts +101 -20
  5. package/src/adapters/cursor/envelope-echo.ts +162 -0
  6. package/src/adapters/cursor/live-transport.ts +3 -1
  7. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  8. package/src/adapters/cursor/native-exec-network.ts +3 -5
  9. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  10. package/src/adapters/cursor/native-exec-shell.ts +13 -25
  11. package/src/adapters/cursor/native-exec.ts +18 -10
  12. package/src/adapters/cursor/protobuf-events.ts +28 -2
  13. package/src/adapters/cursor/protobuf-request.ts +20 -3
  14. package/src/adapters/cursor/request-builder.ts +7 -0
  15. package/src/adapters/cursor/tool-definitions.ts +22 -1
  16. package/src/adapters/cursor/tool-result-normalize.ts +21 -8
  17. package/src/adapters/cursor/types.ts +7 -0
  18. package/src/adapters/cursor.ts +114 -0
  19. package/src/adapters/google-aistudio-parser.ts +49 -0
  20. package/src/adapters/google.ts +108 -16
  21. package/src/adapters/openai-responses.ts +1 -0
  22. package/src/chat/inbound.ts +15 -0
  23. package/src/cli/index.ts +1 -1
  24. package/src/codex/catalog/provider-fetch.ts +34 -0
  25. package/src/generated/compatibility-version.json +91 -47
  26. package/src/generated/model-metadata.ts +3 -0
  27. package/src/oauth/aistudio-native-daemon.ts +62 -0
  28. package/src/oauth/aistudio-session-sync.ts +95 -0
  29. package/src/oauth/google-aistudio-auth.ts +98 -0
  30. package/src/oauth/key-providers.ts +8 -0
  31. package/src/oauth/login-cli.ts +66 -1
  32. package/src/providers/derive.ts +1 -1
  33. package/src/providers/quota.ts +90 -38
  34. package/src/providers/registry.ts +24 -3
  35. package/src/router.ts +3 -0
  36. package/src/routing/account-pool/cooldown.ts +8 -0
  37. package/src/routing/account-pool/index.ts +1 -0
  38. package/src/server/aistudio-ws-hub.ts +295 -0
  39. package/src/server/auth-cors.ts +1 -0
  40. package/src/server/chat-completions.ts +2 -0
  41. package/src/server/index.ts +94 -0
  42. package/src/server/management/logs-usage-routes.ts +11 -5
  43. package/src/server/management/oauth-account-routes.ts +13 -3
  44. package/src/server/port-reclaim.ts +19 -1
  45. package/src/server/request-log-conversation.ts +12 -0
  46. package/src/server/request-log.ts +2 -1
  47. package/src/server/responses/core.ts +4 -3
  48. package/src/server/responses/policy-fallback.ts +1 -1
  49. package/src/server/ws-bridge.ts +2 -1
  50. package/src/smoke/fingerprint-cache.ts +133 -0
  51. package/src/smoke/live-scenarios.ts +33 -0
  52. package/src/smoke/runner.ts +119 -0
  53. package/src/types/provider.ts +2 -1
  54. package/src/types/request.ts +2 -0
  55. package/src/types/tools.ts +31 -7
  56. package/src/usage/command-code-manifest.ts +116 -0
  57. package/src/usage/cost.ts +2 -2
  58. package/src/usage/expected-prices.ts +83 -0
  59. package/src/usage/log.ts +2 -2
  60. package/src/usage/summary.ts +34 -12
  61. package/src/web-search/index.ts +16 -8
  62. 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).
@@ -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 login: ${listOAuthProviders().join(", ")}\n` +
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 {
@@ -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
  }
@@ -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 provider === "anthropic" || provider === "google-antigravity";
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
- if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null;
1745
- const percent = normalizePercent((used / cap) * 100);
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 fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
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 report(provider, "command-code:credits", {
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
- /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) unofficial, may change. */
1875
- async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
1876
- let accessToken: string;
1877
- try {
1878
- accessToken = await getValidAccessToken("cursor");
1879
- } catch {
1880
- return null;
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
- const built = report(provider, "cursor:period-usage", {
1943
- ...(totalPercent !== undefined ? {
1944
- monthlyPercent: totalPercent,
1945
- ...(resetAt !== undefined ? { monthlyResetAt: resetAt } : {}),
1946
- } : {}),
1947
- ...(customWindows.length > 0 ? { customWindows } : {}),
1948
- updatedAt: Date.now(),
1949
- });
1950
- if (built) return { ...built, reverseEngineered: true };
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
- const built = report(provider, "cursor:usage-summary", {
1977
- monthlyPercent: percent,
1978
- monthlyResetAt: normalizeResetAt(body?.billingCycleEnd),
1979
- updatedAt: Date.now(),
1980
- });
1981
- if (built) return { ...built, reverseEngineered: true };
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
- const built = report(provider, "cursor:auth-usage", {
2031
- monthlyPercent: percent,
2032
- ...(monthlyResetAt !== undefined ? { monthlyResetAt } : {}),
2033
- updatedAt: Date.now(),
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(),
@@ -26,6 +26,7 @@ export {
26
26
  STICK_WAIT_MAX_MS,
27
27
  classifyPoolHttpStatus,
28
28
  clearCooldownState,
29
+ clearPoolAccountCooldown,
29
30
  getPoolCooldownRegistry,
30
31
  isAccountInCooldown,
31
32
  isAccountPoolEligible,