@yansigit/opencodex 2.32.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 (142) hide show
  1. package/README.md +2 -2
  2. package/gui/dist/assets/index-CIDo4y4k.js +102 -0
  3. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +8 -5
  6. package/src/adapters/anthropic.ts +20 -6
  7. package/src/adapters/azure.ts +20 -4
  8. package/src/adapters/base.ts +3 -1
  9. package/src/adapters/command-code.ts +131 -17
  10. package/src/adapters/cursor/envelope-echo.ts +162 -0
  11. package/src/adapters/cursor/live-transport.ts +4 -2
  12. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  13. package/src/adapters/cursor/native-exec-network.ts +3 -5
  14. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  15. package/src/adapters/cursor/native-exec-shell.ts +13 -25
  16. package/src/adapters/cursor/native-exec.ts +18 -10
  17. package/src/adapters/cursor/protobuf-events.ts +186 -9
  18. package/src/adapters/cursor/protobuf-request.ts +53 -18
  19. package/src/adapters/cursor/request-builder.ts +11 -3
  20. package/src/adapters/cursor/tool-definitions.ts +49 -2
  21. package/src/adapters/cursor/tool-result-normalize.ts +21 -8
  22. package/src/adapters/cursor/types.ts +11 -3
  23. package/src/adapters/cursor.ts +123 -0
  24. package/src/adapters/google-aistudio-parser.ts +49 -0
  25. package/src/adapters/google-antigravity-replay.ts +2 -2
  26. package/src/adapters/google-antigravity-wire.ts +7 -0
  27. package/src/adapters/google-errors.ts +6 -2
  28. package/src/adapters/google-http.ts +30 -7
  29. package/src/adapters/google-truncation.ts +5 -0
  30. package/src/adapters/google-wire-compiler.ts +38 -6
  31. package/src/adapters/google.ts +242 -28
  32. package/src/adapters/kiro-tools.ts +20 -9
  33. package/src/adapters/openai-chat.ts +9 -0
  34. package/src/adapters/openai-responses.ts +2 -1
  35. package/src/bridge.ts +112 -9
  36. package/src/chat/inbound.ts +15 -0
  37. package/src/claude/context-windows.ts +16 -9
  38. package/src/cli/doctor.ts +2 -2
  39. package/src/cli/index.ts +10 -3
  40. package/src/cli/provider.ts +6 -0
  41. package/src/cli/status.ts +23 -0
  42. package/src/codex/auth-api.ts +4 -2
  43. package/src/codex/autostart-health.ts +16 -0
  44. package/src/codex/catalog/aggregation.ts +12 -12
  45. package/src/codex/catalog/effort.ts +18 -3
  46. package/src/codex/catalog/metadata.ts +27 -1
  47. package/src/codex/catalog/model-metadata.ts +39 -12
  48. package/src/codex/catalog/parsing.ts +38 -27
  49. package/src/codex/catalog/provider-fetch.ts +232 -133
  50. package/src/codex/catalog/sync.ts +1 -1
  51. package/src/codex/convergence.ts +5 -0
  52. package/src/codex/shim.ts +56 -3
  53. package/src/config/provider-validation.ts +37 -0
  54. package/src/config.ts +78 -2
  55. package/src/generated/compatibility-version.json +187 -119
  56. package/src/generated/model-metadata.ts +3 -0
  57. package/src/images/loop.ts +37 -6
  58. package/src/lib/azure-identity.ts +154 -0
  59. package/src/lib/debug.ts +42 -0
  60. package/src/lib/errors.ts +14 -0
  61. package/src/lib/provider-outbound.ts +45 -33
  62. package/src/lib/provider-tls-profile.ts +309 -0
  63. package/src/lib/proxy-env.ts +49 -0
  64. package/src/lib/redact.ts +10 -1
  65. package/src/oauth/aistudio-native-daemon.ts +62 -0
  66. package/src/oauth/aistudio-session-sync.ts +95 -0
  67. package/src/oauth/antigravity-routing.ts +282 -236
  68. package/src/oauth/callback-server.ts +22 -2
  69. package/src/oauth/command-code.ts +5 -16
  70. package/src/oauth/google-aistudio-auth.ts +98 -0
  71. package/src/oauth/google-antigravity.ts +42 -5
  72. package/src/oauth/index.ts +15 -3
  73. package/src/oauth/key-providers.ts +8 -0
  74. package/src/oauth/kimi.ts +9 -1
  75. package/src/oauth/login-cli.ts +66 -1
  76. package/src/oauth/open-browser-choice.ts +26 -0
  77. package/src/oauth/store.ts +6 -0
  78. package/src/providers/antigravity-quota.ts +3 -1
  79. package/src/providers/api-keys.ts +2 -1
  80. package/src/providers/auto-compact-budget.ts +65 -0
  81. package/src/providers/derive.ts +5 -1
  82. package/src/providers/key-failover.ts +5 -1
  83. package/src/providers/openai-tiers.ts +5 -0
  84. package/src/providers/provider-id-rewrite.ts +1 -0
  85. package/src/providers/quota.ts +148 -50
  86. package/src/providers/registry.ts +27 -4
  87. package/src/providers/request-pacing.ts +33 -6
  88. package/src/providers/xai-transport.ts +21 -0
  89. package/src/responses/google-provider-options.ts +36 -0
  90. package/src/responses/namespace-tool-compat.ts +84 -4
  91. package/src/responses/parser.ts +11 -0
  92. package/src/responses/provider-opaque-metadata.ts +3 -3
  93. package/src/responses/schema.ts +37 -0
  94. package/src/responses/state.ts +94 -4
  95. package/src/router.ts +11 -2
  96. package/src/routing/account-pool/cooldown.ts +8 -0
  97. package/src/routing/account-pool/index.ts +1 -0
  98. package/src/server/aistudio-ws-hub.ts +295 -0
  99. package/src/server/auth-cors.ts +29 -0
  100. package/src/server/chat-completions.ts +2 -0
  101. package/src/server/images.ts +19 -35
  102. package/src/server/index.ts +94 -0
  103. package/src/server/management/agent-settings-routes.ts +205 -15
  104. package/src/server/management/combo-routes.ts +6 -0
  105. package/src/server/management/config-routes.ts +31 -5
  106. package/src/server/management/logs-usage-routes.ts +11 -5
  107. package/src/server/management/model-rows.ts +4 -0
  108. package/src/server/management/oauth-account-routes.ts +38 -7
  109. package/src/server/management/provider-routes.ts +113 -15
  110. package/src/server/management/routing-profile-routes.ts +3 -0
  111. package/src/server/port-reclaim.ts +19 -1
  112. package/src/server/request-log-conversation.ts +12 -0
  113. package/src/server/request-log.ts +23 -1
  114. package/src/server/responses/agent-task-recovery.ts +1 -1
  115. package/src/server/responses/compact.ts +30 -1
  116. package/src/server/responses/core.ts +363 -156
  117. package/src/server/responses/empty-completion-guard.ts +35 -6
  118. package/src/server/responses/fetch-helpers.ts +18 -5
  119. package/src/server/responses/policy-fallback.ts +1 -1
  120. package/src/server/responses/v2-native-parent-override.ts +59 -0
  121. package/src/server/responses/ws-upstream.ts +75 -2
  122. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  123. package/src/server/ws-bridge.ts +2 -1
  124. package/src/service.ts +1 -1
  125. package/src/smoke/fingerprint-cache.ts +133 -0
  126. package/src/smoke/live-scenarios.ts +33 -0
  127. package/src/smoke/runner.ts +119 -0
  128. package/src/types/config.ts +16 -1
  129. package/src/types/provider.ts +18 -1
  130. package/src/types/request.ts +30 -0
  131. package/src/types/tools.ts +51 -0
  132. package/src/types.ts +6 -0
  133. package/src/usage/command-code-manifest.ts +116 -0
  134. package/src/usage/cost.ts +2 -2
  135. package/src/usage/expected-prices.ts +83 -0
  136. package/src/usage/log.ts +2 -2
  137. package/src/usage/summary.ts +34 -12
  138. package/src/web-search/gemini-executor.ts +6 -4
  139. package/src/web-search/index.ts +16 -8
  140. package/src/web-search/loop.ts +42 -6
  141. package/gui/dist/assets/index-BG43zwVe.js +0 -102
  142. package/gui/dist/assets/index-CiSI-jrP.css +0 -1
@@ -8,7 +8,7 @@ import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache
8
8
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
9
9
  import { codexPlanKey } from "../codex/plan";
10
10
  import { resolveEnvValue } from "../config";
11
- import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
11
+ import { getValidAccessToken, getValidAccessTokenForAccount, getValidAccessTokenSnapshot, getValidAccessTokenSnapshotForAccount, type OAuthAccessSnapshot } from "../oauth";
12
12
  import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
13
13
  import { antigravityUserAgent } from "../adapters/client-fingerprint";
14
14
  import { apiKeyPoolEntryId } from "./api-keys";
@@ -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,
@@ -34,6 +35,7 @@ import {
34
35
  isTerminalAntigravityQuotaStatus,
35
36
  } from "./antigravity-quota";
36
37
  import { antigravityHostCandidates, isAntigravityHttpsHost } from "../adapters/google-antigravity-hosts";
38
+ import { providerTlsFetch } from "../lib/provider-tls-profile";
37
39
 
38
40
  /** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
39
41
  const ACCOUNT_TOKEN_SKEW_MS = 60_000;
@@ -1400,7 +1402,12 @@ export interface ProviderAccountQuota {
1400
1402
 
1401
1403
  /** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
1402
1404
  export function supportsPerAccountQuota(provider: string): boolean {
1403
- return provider === "anthropic";
1405
+ return (
1406
+ provider === "anthropic" ||
1407
+ provider === "google-antigravity" ||
1408
+ provider === "command-code" ||
1409
+ provider === "cursor"
1410
+ );
1404
1411
  }
1405
1412
 
1406
1413
  function accountCacheKey(provider: string, accountId: string): string {
@@ -1519,8 +1526,21 @@ async function fetchAccountQuota(
1519
1526
 
1520
1527
  const probe = (async (): Promise<AccountQuotaCacheEntry> => {
1521
1528
  try {
1522
- const token = await getTokenForAccountQuotaProbe(provider, accountId);
1523
- const quota = await fetchAnthropicUsageQuota(token);
1529
+ let quota: ProviderQuota | null = null;
1530
+ if (provider === "anthropic") {
1531
+ const token = await getTokenForAccountQuotaProbe(provider, accountId);
1532
+ quota = await fetchAnthropicUsageQuota(token);
1533
+ } else if (provider === "google-antigravity") {
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;
1543
+ }
1524
1544
  if (!quota) {
1525
1545
  // Preserve last-good bars and mark unavailable; advance TTL so failures
1526
1546
  // negative-cache instead of re-probing on every GUI poll.
@@ -1735,8 +1755,9 @@ function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: nu
1735
1755
  if (!row) return null;
1736
1756
  const cap = toFiniteNumber(row.cap);
1737
1757
  const used = toFiniteNumber(row.used);
1738
- if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null;
1739
- 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);
1740
1761
  if (percent === undefined) return null;
1741
1762
  const resetAt = quotaResetAt(row);
1742
1763
  return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
@@ -1823,11 +1844,7 @@ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise
1823
1844
  * usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
1824
1845
  * whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
1825
1846
  */
1826
- async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1827
- // Never release credentials to a user-edited or lookalike provider host.
1828
- if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
1829
- const bearer = await resolveCommandCodeQuotaBearer(config);
1830
- if (!bearer) return null;
1847
+ async function fetchCommandCodeUsageQuota(bearer: string): Promise<ProviderQuota | null | typeof TERMINAL_QUOTA_FAILURE> {
1831
1848
  const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
1832
1849
  const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
1833
1850
  const org = asRecord(whoami?.org);
@@ -1851,7 +1868,7 @@ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig
1851
1868
  const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
1852
1869
  const weekly = parseCommandCodeWindow(limits?.weekly);
1853
1870
  const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
1854
- return report(provider, "command-code:credits", {
1871
+ return {
1855
1872
  ...(fiveHour ? {
1856
1873
  fiveHourPercent: fiveHour.percent,
1857
1874
  ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
@@ -1862,18 +1879,21 @@ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig
1862
1879
  } : {}),
1863
1880
  ...(creditsUsd ? { creditsUsd } : {}),
1864
1881
  updatedAt: Date.now(),
1865
- });
1882
+ };
1866
1883
  }
1867
1884
 
1868
- /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) unofficial, may change. */
1869
- async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
1870
- let accessToken: string;
1871
- try {
1872
- accessToken = await getValidAccessToken("cursor");
1873
- } catch {
1874
- return null;
1875
- }
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
+ }
1876
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> {
1877
1897
  const authHeaders = {
1878
1898
  Accept: "application/json",
1879
1899
  Authorization: `Bearer ${accessToken}`,
@@ -1933,15 +1953,17 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
1933
1953
  }
1934
1954
 
1935
1955
  if (totalPercent !== undefined || customWindows.length > 0) {
1936
- const built = report(provider, "cursor:period-usage", {
1937
- ...(totalPercent !== undefined ? {
1938
- monthlyPercent: totalPercent,
1939
- ...(resetAt !== undefined ? { monthlyResetAt: resetAt } : {}),
1940
- } : {}),
1941
- ...(customWindows.length > 0 ? { customWindows } : {}),
1942
- updatedAt: Date.now(),
1943
- });
1944
- 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
+ };
1945
1967
  }
1946
1968
  }
1947
1969
  }
@@ -1967,12 +1989,14 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
1967
1989
  ? normalizePercent((used / limit) * 100)
1968
1990
  : undefined);
1969
1991
  if (percent !== undefined) {
1970
- const built = report(provider, "cursor:usage-summary", {
1971
- monthlyPercent: percent,
1972
- monthlyResetAt: normalizeResetAt(body?.billingCycleEnd),
1973
- updatedAt: Date.now(),
1974
- });
1975
- 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
+ };
1976
2000
  }
1977
2001
  }
1978
2002
  }
@@ -2021,11 +2045,26 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
2021
2045
  return Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, start.getUTCDate());
2022
2046
  })()
2023
2047
  : undefined;
2024
- const built = report(provider, "cursor:auth-usage", {
2025
- monthlyPercent: percent,
2026
- ...(monthlyResetAt !== undefined ? { monthlyResetAt } : {}),
2027
- updatedAt: Date.now(),
2028
- });
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);
2029
2068
  return built ? { ...built, reverseEngineered: true } : null;
2030
2069
  }
2031
2070
 
@@ -2077,23 +2116,63 @@ function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | un
2077
2116
  return normalizePercent(100 - remaining);
2078
2117
  }
2079
2118
 
2119
+ async function fetchAntigravityAccountQuota(accountId: string): Promise<ProviderQuota | null> {
2120
+ let snapshot: OAuthAccessSnapshot;
2121
+ try {
2122
+ snapshot = await getValidAccessTokenSnapshotForAccount("google-antigravity", accountId);
2123
+ } catch {
2124
+ return null;
2125
+ }
2126
+ if (!snapshot.projectId) return null;
2127
+ const entry = getProviderRegistryEntry("google-antigravity");
2128
+ const baseUrl = (entry?.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, "");
2129
+ const providerConfig: Pick<OcxProviderConfig, "adapter" | "authMode" | "baseUrl" | "googleMode" | "tlsProfile"> = {
2130
+ adapter: "google",
2131
+ authMode: "oauth",
2132
+ googleMode: "cloud-code-assist",
2133
+ baseUrl,
2134
+ };
2135
+ const fetchImpl = providerTlsFetch("google-antigravity", providerConfig, globalThis.fetch);
2136
+ let liveQuota: ProviderQuota | null;
2137
+ try {
2138
+ liveQuota = await fetchAntigravityLiveQuota({
2139
+ accessToken: snapshot.accessToken,
2140
+ projectId: snapshot.projectId,
2141
+ baseUrl,
2142
+ timeoutMs: REQUEST_TIMEOUT_MS,
2143
+ fetchImpl,
2144
+ });
2145
+ } catch (error) {
2146
+ if (error instanceof AntigravityQuotaRpcError && isTerminalAntigravityQuotaStatus(error.status)) {
2147
+ return null;
2148
+ }
2149
+ liveQuota = null;
2150
+ }
2151
+ if (!liveQuota) return null;
2152
+ return {
2153
+ ...liveQuota,
2154
+ updatedAt: Date.now(),
2155
+ };
2156
+ }
2157
+
2080
2158
  async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
2081
- const credential = getCredential("google-antigravity");
2082
- if (!credential?.projectId) return null;
2083
- let accessToken: string;
2159
+ let snapshot: OAuthAccessSnapshot;
2084
2160
  try {
2085
- accessToken = await getValidAccessToken("google-antigravity");
2161
+ snapshot = await getValidAccessTokenSnapshot("google-antigravity");
2086
2162
  } catch {
2087
2163
  return null;
2088
2164
  }
2165
+ if (!snapshot.projectId) return null;
2089
2166
  const baseUrl = (config.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, "");
2167
+ const fetchImpl = providerTlsFetch(provider, config, globalThis.fetch);
2090
2168
  let liveQuota: ProviderQuota | null;
2091
2169
  try {
2092
2170
  liveQuota = await fetchAntigravityLiveQuota({
2093
- accessToken,
2094
- projectId: credential.projectId,
2171
+ accessToken: snapshot.accessToken,
2172
+ projectId: snapshot.projectId,
2095
2173
  baseUrl,
2096
2174
  timeoutMs: REQUEST_TIMEOUT_MS,
2175
+ fetchImpl,
2097
2176
  });
2098
2177
  } catch (error) {
2099
2178
  if (error instanceof AntigravityQuotaRpcError && isTerminalAntigravityQuotaStatus(error.status)) {
@@ -2106,15 +2185,15 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
2106
2185
  for (const [index, host] of antigravityHostCandidates(baseUrl).entries()) {
2107
2186
  if (!isAntigravityHttpsHost(host)) continue;
2108
2187
  try {
2109
- const response = await fetch(`${host}/v1internal:fetchAvailableModels`, {
2188
+ const response = await fetchImpl(`${host}/v1internal:fetchAvailableModels`, {
2110
2189
  method: "POST",
2111
2190
  headers: {
2112
2191
  Accept: "application/json",
2113
2192
  "Content-Type": "application/json",
2114
2193
  "User-Agent": antigravityUserAgent(),
2115
- Authorization: `Bearer ${accessToken}`,
2194
+ Authorization: `Bearer ${snapshot.accessToken}`,
2116
2195
  },
2117
- body: JSON.stringify({ project: credential.projectId }),
2196
+ body: JSON.stringify({ project: snapshot.projectId }),
2118
2197
  redirect: "error",
2119
2198
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2120
2199
  });
@@ -2173,6 +2252,22 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
2173
2252
  });
2174
2253
  }
2175
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
+
2176
2271
  async function maybeFetchProviderQuota(
2177
2272
  name: string,
2178
2273
  provider: OcxProviderConfig,
@@ -2248,6 +2343,9 @@ async function maybeFetchProviderQuota(
2248
2343
  if ((provider.authMode ?? "key") === "key" && name === "neuralwatt") {
2249
2344
  return fetchNeuralwattQuota(name, provider);
2250
2345
  }
2346
+ if (provider.googleMode === "ai-studio-web" || name === "google-aistudio") {
2347
+ return fetchAiStudioQuota(name, provider);
2348
+ }
2251
2349
  return null;
2252
2350
  } catch {
2253
2351
  return null;
@@ -161,6 +161,7 @@ export interface ProviderRegistryEntry {
161
161
  note?: string;
162
162
  dashboardUrl?: string;
163
163
  defaultModel?: string;
164
+ requestPacing?: OcxProviderConfig["requestPacing"];
164
165
  models?: string[];
165
166
  liveModels?: boolean;
166
167
  /**
@@ -298,7 +299,7 @@ export interface ProviderRegistryEntry {
298
299
  jawcodeBundle?: string;
299
300
  extraMetadataAliases?: string[];
300
301
  metadataModelIdNormalize?: MetadataModelIdNormalize;
301
- googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
302
+ googleMode?: "ai-studio" | "vertex" | "cloud-code-assist" | "ai-studio-web";
302
303
  project?: string;
303
304
  location?: string;
304
305
  }
@@ -1597,7 +1598,28 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1597
1598
  // 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
1598
1599
  // evidence from ai.google.dev does not establish Vertex publisher availability.
1599
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"] },
1600
- { 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", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
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
+ },
1601
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" },
1602
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" },
1603
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" },
@@ -1867,6 +1889,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1867
1889
  liveModels: true,
1868
1890
  preserveCustomDestination: true,
1869
1891
  defaultModel: "deepseek/deepseek-v4-flash",
1892
+ promptCacheKey: true,
1870
1893
  // The default is also the cold-start seed: live discovery failure must not empty the catalog
1871
1894
  // for a freshly configured provider with no stale cache (issue #308 pattern).
1872
1895
  models: ["deepseek/deepseek-v4-flash"],
@@ -2911,8 +2934,8 @@ export function providerCodexAccountMode(id: string, provider?: OcxProviderConfi
2911
2934
  */
2912
2935
  export function effectiveGoogleMode(
2913
2936
  providerId: string,
2914
- prov: { adapter?: string; googleMode?: "ai-studio" | "vertex" | "cloud-code-assist" },
2915
- ): "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 {
2916
2939
  if (prov.adapter !== "google") return null;
2917
2940
  return prov.googleMode ?? getProviderRegistryEntry(providerId)?.googleMode ?? "ai-studio";
2918
2941
  }
@@ -33,6 +33,8 @@ interface Waiter {
33
33
  modelId?: string;
34
34
  providerIntervalMs: number;
35
35
  modelIntervalMs: number;
36
+ providerJitterMs: number;
37
+ modelJitterMs: number;
36
38
  queuedAt: number;
37
39
  signal?: AbortSignal;
38
40
  resolve: () => void;
@@ -54,6 +56,7 @@ export interface RequestPacingRuntime {
54
56
  setTimer: (callback: () => void, delayMs: number) => unknown;
55
57
  clearTimer: (handle: unknown) => void;
56
58
  enqueueMicrotask: (callback: () => void) => void;
59
+ random: () => number;
57
60
  }
58
61
 
59
62
  export interface ProviderRequestPacingStatus {
@@ -71,6 +74,7 @@ const defaultRuntime: RequestPacingRuntime = {
71
74
  setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
72
75
  clearTimer: handle => clearTimeout(handle as ReturnType<typeof setTimeout>),
73
76
  enqueueMicrotask: queueMicrotask,
77
+ random: Math.random,
74
78
  };
75
79
  let runtime = defaultRuntime;
76
80
  let lastReconciledGeneration = 0;
@@ -90,6 +94,19 @@ function normalizedInterval(rule: RequestPacingRule | undefined): number {
90
94
  return Math.max(rpmInterval, fixedInterval);
91
95
  }
92
96
 
97
+ function normalizedJitter(rule: RequestPacingRule | undefined): number {
98
+ return rule?.jitterMs !== undefined && Number.isInteger(rule.jitterMs)
99
+ ? Math.min(60_000, Math.max(0, rule.jitterMs))
100
+ : 0;
101
+ }
102
+
103
+ function jitterDelay(jitterMs: number): number {
104
+ if (jitterMs <= 0) return 0;
105
+ const sample = runtime.random();
106
+ if (!Number.isFinite(sample)) return 0;
107
+ return Math.min(jitterMs, Math.floor(Math.max(0, sample) * jitterMs));
108
+ }
109
+
93
110
  export function requestPacingIntervalMs(provider: OcxProviderConfig, modelId?: string): number {
94
111
  const policy = provider.requestPacing;
95
112
  if (!policy?.enabled) return 0;
@@ -100,12 +117,17 @@ export function requestPacingIntervalMs(provider: OcxProviderConfig, modelId?: s
100
117
  function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string): {
101
118
  providerIntervalMs: number;
102
119
  modelIntervalMs: number;
120
+ providerJitterMs: number;
121
+ modelJitterMs: number;
103
122
  } {
104
123
  const policy = provider.requestPacing;
105
- if (!policy?.enabled) return { providerIntervalMs: 0, modelIntervalMs: 0 };
124
+ if (!policy?.enabled) return { providerIntervalMs: 0, modelIntervalMs: 0, providerJitterMs: 0, modelJitterMs: 0 };
125
+ const model = modelId ? policy.models?.[modelId] : undefined;
106
126
  return {
107
127
  providerIntervalMs: normalizedInterval(policy),
108
- modelIntervalMs: modelId ? normalizedInterval(policy.models?.[modelId]) : 0,
128
+ modelIntervalMs: normalizedInterval(model),
129
+ providerJitterMs: normalizedJitter(policy),
130
+ modelJitterMs: normalizedJitter(model),
109
131
  };
110
132
  }
111
133
 
@@ -186,9 +208,9 @@ function runQueue(providerName: string, state: ProviderPacer): void {
186
208
  const startedAt = runtime.now();
187
209
  state.lastStartedAt = startedAt;
188
210
  state.lastModelId = waiter.modelId;
189
- state.providerNextStartAt = startedAt + waiter.providerIntervalMs;
190
- if (waiter.modelId && waiter.modelIntervalMs > 0) {
191
- state.modelNextStartAt.set(waiter.modelId, startedAt + waiter.modelIntervalMs);
211
+ state.providerNextStartAt = startedAt + waiter.providerIntervalMs + jitterDelay(waiter.providerJitterMs);
212
+ if (waiter.modelId && (waiter.modelIntervalMs > 0 || waiter.modelJitterMs > 0)) {
213
+ state.modelNextStartAt.set(waiter.modelId, startedAt + waiter.modelIntervalMs + jitterDelay(waiter.modelJitterMs));
192
214
  }
193
215
  waiter.resolve();
194
216
  runtime.enqueueMicrotask(() => runQueue(providerName, state));
@@ -201,7 +223,12 @@ export async function waitForProviderRequestSlot(
201
223
  signal?: AbortSignal,
202
224
  ): Promise<void> {
203
225
  const intervals = requestPacingIntervals(provider, modelId);
204
- if (Math.max(intervals.providerIntervalMs, intervals.modelIntervalMs) <= 0) return;
226
+ if (Math.max(
227
+ intervals.providerIntervalMs,
228
+ intervals.modelIntervalMs,
229
+ intervals.providerJitterMs,
230
+ intervals.modelJitterMs,
231
+ ) <= 0) return;
205
232
  if (signal?.aborted) throw abortReason(signal);
206
233
 
207
234
  const state = pacers.get(providerName) ?? {
@@ -4,6 +4,27 @@ import { resolveGithubCopilotTransport } from "./github-copilot-transport";
4
4
 
5
5
  export const XAI_GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
6
6
 
7
+ /** The two hosts that serve xAI's Responses API: the public API and the Grok CLI proxy. */
8
+ const XAI_RESPONSES_HOSTS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
9
+
10
+ /**
11
+ * True when this provider's Responses traffic terminates at xAI itself.
12
+ *
13
+ * Probed 2026-08-22 one field per request: the two hosts accept and refuse exactly the same
14
+ * web_search fields, so they are one dialect rather than two. Matching is exact-host over
15
+ * https, which keeps lookalikes (`api.x.ai.evil.test`) and nonstandard ports out.
16
+ */
17
+ export function isXaiResponsesDestination(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
18
+ try {
19
+ const url = new URL(provider.baseUrl);
20
+ return url.protocol === "https:"
21
+ && XAI_RESPONSES_HOSTS.has(url.hostname.toLowerCase())
22
+ && (url.port === "" || url.port === "443");
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
7
28
  export const XAI_GROK_COMPATIBILITY = {
8
29
  version: "0.2.93",
9
30
  userAgent: "opencodex-grok/0.2.93",
@@ -0,0 +1,36 @@
1
+ import { effectiveGoogleMode } from "../providers/registry";
2
+ import type { OcxParsedRequest, OcxProviderConfig } from "../types";
3
+
4
+ const AI_STUDIO_CACHED_CONTENT = /^cachedContents\/[^/?#\s]+$/;
5
+ const VERTEX_CACHED_CONTENT = /^projects\/[^/?#\s]+\/locations\/[^/?#\s]+\/cachedContents\/[^/?#\s]+$/;
6
+
7
+ export function googleProviderOptionsRouteError(
8
+ parsed: Pick<OcxParsedRequest, "options">,
9
+ route: {
10
+ providerName: string;
11
+ provider: OcxProviderConfig;
12
+ adapterName: string;
13
+ },
14
+ ): string | undefined {
15
+ if (!parsed.options.providerOptions?.google) return undefined;
16
+ const mode = effectiveGoogleMode(route.providerName, route.provider);
17
+ if (mode === "cloud-code-assist") {
18
+ return "provider_options.google is not supported on Google Cloud Code Assist routes";
19
+ }
20
+ if (route.adapterName !== "google" || (mode !== "ai-studio" && mode !== "vertex")) {
21
+ return "provider_options.google is supported only on Google AI Studio or Vertex routes";
22
+ }
23
+ if (mode === "ai-studio") {
24
+ const cachedContent = parsed.options.providerOptions.google.cachedContent;
25
+ if (cachedContent && VERTEX_CACHED_CONTENT.test(cachedContent)) {
26
+ return "provider_options.google.cached_content must use cachedContents/{id} on Google AI Studio routes";
27
+ }
28
+ }
29
+ if (mode === "vertex") {
30
+ const cachedContent = parsed.options.providerOptions.google.cachedContent;
31
+ if (cachedContent && AI_STUDIO_CACHED_CONTENT.test(cachedContent)) {
32
+ return "provider_options.google.cached_content must use projects/{project}/locations/{location}/cachedContents/{id} on Vertex routes";
33
+ }
34
+ }
35
+ return undefined;
36
+ }