dsh-subscriptions 0.1.0

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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/claude-code-creds.d.ts +23 -0
  5. package/lib/auth/claude-code-creds.js +172 -0
  6. package/lib/auth/device-flow.d.ts +55 -0
  7. package/lib/auth/device-flow.js +177 -0
  8. package/lib/auth/jwt.d.ts +10 -0
  9. package/lib/auth/jwt.js +25 -0
  10. package/lib/auth/oauth-flow.d.ts +91 -0
  11. package/lib/auth/oauth-flow.js +227 -0
  12. package/lib/auth/pkce.d.ts +31 -0
  13. package/lib/auth/pkce.js +35 -0
  14. package/lib/auth/rpc.d.ts +209 -0
  15. package/lib/auth/rpc.js +386 -0
  16. package/lib/auth/store.d.ts +196 -0
  17. package/lib/auth/store.js +468 -0
  18. package/lib/client/CursorCard.d.ts +9 -0
  19. package/lib/client/CursorCard.js +113 -0
  20. package/lib/client/ExternalUsageCards.d.ts +9 -0
  21. package/lib/client/ExternalUsageCards.js +72 -0
  22. package/lib/client/ImageGallery.d.ts +54 -0
  23. package/lib/client/ImageGallery.js +112 -0
  24. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  25. package/lib/client/ImageGenerateToolview.js +133 -0
  26. package/lib/client/ProviderAccountManager.d.ts +13 -0
  27. package/lib/client/ProviderAccountManager.js +147 -0
  28. package/lib/client/ProviderModelEditor.d.ts +39 -0
  29. package/lib/client/ProviderModelEditor.js +107 -0
  30. package/lib/client/SpeedSelect.d.ts +71 -0
  31. package/lib/client/SpeedSelect.js +177 -0
  32. package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
  33. package/lib/client/SubscriptionUsageBadge.js +543 -0
  34. package/lib/client/SubscriptionsSection.d.ts +140 -0
  35. package/lib/client/SubscriptionsSection.js +544 -0
  36. package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
  37. package/lib/client/UsageBadgeDisplaySetting.js +36 -0
  38. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  39. package/lib/client/VideoGenerateToolview.js +175 -0
  40. package/lib/client/account-preferences.d.ts +24 -0
  41. package/lib/client/account-preferences.js +21 -0
  42. package/lib/client/fast-command.d.ts +2 -0
  43. package/lib/client/fast-command.js +4 -0
  44. package/lib/client/host-contract.d.ts +90 -0
  45. package/lib/client/host-contract.js +101 -0
  46. package/lib/client/host-icons.d.ts +24 -0
  47. package/lib/client/host-icons.js +21 -0
  48. package/lib/client/index.d.ts +27 -0
  49. package/lib/client/index.js +136 -0
  50. package/lib/client/locales.d.ts +297 -0
  51. package/lib/client/locales.js +295 -0
  52. package/lib/client/subscription-card-styles.d.ts +3 -0
  53. package/lib/client/subscription-card-styles.js +53 -0
  54. package/lib/client/subscriptions-rpc.d.ts +14 -0
  55. package/lib/client/subscriptions-rpc.js +34 -0
  56. package/lib/client/usage-badge-preferences.d.ts +11 -0
  57. package/lib/client/usage-badge-preferences.js +41 -0
  58. package/lib/client.js +4472 -0
  59. package/lib/client.js.map +1 -0
  60. package/lib/compat.d.ts +36 -0
  61. package/lib/compat.js +20 -0
  62. package/lib/http.d.ts +6 -0
  63. package/lib/http.js +15 -0
  64. package/lib/index.d.ts +80 -0
  65. package/lib/index.js +14018 -0
  66. package/lib/model-defaults.d.ts +23 -0
  67. package/lib/model-defaults.js +237 -0
  68. package/lib/provider-settings.d.ts +39 -0
  69. package/lib/provider-settings.js +151 -0
  70. package/lib/providers/account-preferences.d.ts +41 -0
  71. package/lib/providers/account-preferences.js +153 -0
  72. package/lib/providers/accounts.d.ts +112 -0
  73. package/lib/providers/accounts.js +142 -0
  74. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  75. package/lib/providers/antigravity-oauth-client.js +7 -0
  76. package/lib/providers/antigravity.d.ts +110 -0
  77. package/lib/providers/antigravity.js +516 -0
  78. package/lib/providers/catalog-store.d.ts +48 -0
  79. package/lib/providers/catalog-store.js +243 -0
  80. package/lib/providers/claude-cli-version.d.ts +15 -0
  81. package/lib/providers/claude-cli-version.js +24 -0
  82. package/lib/providers/claude.d.ts +181 -0
  83. package/lib/providers/claude.js +680 -0
  84. package/lib/providers/codex-client-version.d.ts +8 -0
  85. package/lib/providers/codex-client-version.js +17 -0
  86. package/lib/providers/codex-search.d.ts +29 -0
  87. package/lib/providers/codex-search.js +198 -0
  88. package/lib/providers/codex.d.ts +229 -0
  89. package/lib/providers/codex.js +821 -0
  90. package/lib/providers/common.d.ts +389 -0
  91. package/lib/providers/common.js +656 -0
  92. package/lib/providers/copilot.d.ts +344 -0
  93. package/lib/providers/copilot.js +888 -0
  94. package/lib/providers/cursor-adapter.d.ts +15 -0
  95. package/lib/providers/cursor-adapter.js +39 -0
  96. package/lib/providers/cursor-auth.d.ts +43 -0
  97. package/lib/providers/cursor-auth.js +169 -0
  98. package/lib/providers/cursor-model-settings.d.ts +15 -0
  99. package/lib/providers/cursor-model-settings.js +51 -0
  100. package/lib/providers/cursor-usage.d.ts +9 -0
  101. package/lib/providers/cursor-usage.js +112 -0
  102. package/lib/providers/external-usage-controller.d.ts +19 -0
  103. package/lib/providers/external-usage-controller.js +32 -0
  104. package/lib/providers/external-usage.d.ts +8 -0
  105. package/lib/providers/external-usage.js +82 -0
  106. package/lib/providers/grok.d.ts +205 -0
  107. package/lib/providers/grok.js +713 -0
  108. package/lib/providers/image-pool.d.ts +30 -0
  109. package/lib/providers/image-pool.js +89 -0
  110. package/lib/providers/npm-cli-version.d.ts +56 -0
  111. package/lib/providers/npm-cli-version.js +98 -0
  112. package/lib/providers/pool-family.d.ts +56 -0
  113. package/lib/providers/pool-family.js +45 -0
  114. package/lib/providers/pool-health.d.ts +74 -0
  115. package/lib/providers/pool-health.js +150 -0
  116. package/lib/providers/pool-usage.d.ts +86 -0
  117. package/lib/providers/pool-usage.js +223 -0
  118. package/lib/providers/pool.d.ts +108 -0
  119. package/lib/providers/pool.js +368 -0
  120. package/lib/providers/rate-limit.d.ts +192 -0
  121. package/lib/providers/rate-limit.js +338 -0
  122. package/lib/tools/image-generate.d.ts +128 -0
  123. package/lib/tools/image-generate.js +438 -0
  124. package/lib/tools/registration.d.ts +15 -0
  125. package/lib/tools/registration.js +24 -0
  126. package/lib/tools/video-generate.d.ts +89 -0
  127. package/lib/tools/video-generate.js +257 -0
  128. package/lib/tools/x-search.d.ts +58 -0
  129. package/lib/tools/x-search.js +197 -0
  130. package/lib/translate/anthropic.d.ts +161 -0
  131. package/lib/translate/anthropic.js +515 -0
  132. package/lib/translate/antigravity-schema.d.ts +4 -0
  133. package/lib/translate/antigravity-schema.js +145 -0
  134. package/lib/translate/antigravity-thinking.d.ts +5 -0
  135. package/lib/translate/antigravity-thinking.js +44 -0
  136. package/lib/translate/antigravity.d.ts +114 -0
  137. package/lib/translate/antigravity.js +366 -0
  138. package/lib/translate/chat-completions.d.ts +120 -0
  139. package/lib/translate/chat-completions.js +382 -0
  140. package/lib/translate/resolved.d.ts +80 -0
  141. package/lib/translate/resolved.js +118 -0
  142. package/lib/translate/responses.d.ts +179 -0
  143. package/lib/translate/responses.js +408 -0
  144. package/lib/translate/sse.d.ts +21 -0
  145. package/lib/translate/sse.js +56 -0
  146. package/package.json +114 -0
  147. package/vendor/cursor/LICENSE +21 -0
  148. package/vendor/cursor/index.d.ts +18 -0
  149. package/vendor/cursor/index.js +3574 -0
  150. package/vendor/cursor/proto.js +254 -0
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Google Antigravity subscription provider. This is intentionally separate
3
+ * from Gemini CLI: it uses Antigravity OAuth scopes, project discovery, and
4
+ * the daily-cloudcode-pa v1internal request envelope.
5
+ */
6
+ import { errorChain, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
7
+ import { resolveImages } from '../translate/resolved.js';
8
+ import { antigravityReasoning } from '../translate/antigravity-thinking.js';
9
+ import { parseAntigravityResponse, streamAntigravity, toAntigravityRequest, } from '../translate/antigravity.js';
10
+ import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, discoverAcrossAccounts, isDiscoveryAborted, } from './common.js';
11
+ import { hostFetch } from '../http.js';
12
+ import { ANTIGRAVITY_DEFAULT_CLIENT_ID, ANTIGRAVITY_DEFAULT_CLIENT_SECRET } from './antigravity-oauth-client.js';
13
+ import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
14
+ import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, subscriptionRetryPolicy } from './rate-limit.js';
15
+ export const ANTIGRAVITY_AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
16
+ export const ANTIGRAVITY_TOKEN_URL = 'https://oauth2.googleapis.com/token';
17
+ export const ANTIGRAVITY_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
18
+ export const ANTIGRAVITY_DEFAULT_BASE_URL = 'https://daily-cloudcode-pa.googleapis.com';
19
+ export const ANTIGRAVITY_PROD_BASE_URL = 'https://cloudcode-pa.googleapis.com';
20
+ export const ANTIGRAVITY_DEFAULT_USER_AGENT = 'antigravity/1.104.0 dsh-subscriptions';
21
+ export const ANTIGRAVITY_PREEMPT_MS = 5 * 60_000;
22
+ const ANTIGRAVITY_CALLBACK_PATH = '/oauth-callback';
23
+ const ANTIGRAVITY_CONTEXT_WINDOW = 1_024_000;
24
+ // Fallback only when discovery is unavailable or the model omits its output cap.
25
+ const ANTIGRAVITY_DEFAULT_MAX_TOKENS = 32_768;
26
+ /** Antigravity, not Gemini CLI, OAuth scopes from the local reference clients. */
27
+ export const ANTIGRAVITY_SCOPES = [
28
+ 'openid',
29
+ 'https://www.googleapis.com/auth/cloud-platform',
30
+ 'https://www.googleapis.com/auth/userinfo.email',
31
+ 'https://www.googleapis.com/auth/userinfo.profile',
32
+ 'https://www.googleapis.com/auth/cclog',
33
+ 'https://www.googleapis.com/auth/experimentsandconfigs',
34
+ ];
35
+ /** Resolve a client pair: plugin config, environment, then the public desktop default. */
36
+ export function resolveAntigravityOAuthConfig(config) {
37
+ const configuredId = config?.clientId?.trim();
38
+ const configuredSecret = config?.clientSecret?.trim();
39
+ const environmentId = process.env.ANTIGRAVITY_CLIENT_ID?.trim();
40
+ const environmentSecret = process.env.ANTIGRAVITY_CLIENT_SECRET?.trim();
41
+ if (configuredId) {
42
+ // A custom PKCE-only client must never inherit a different client's secret.
43
+ return { clientId: configuredId, ...configuredSecret ? { clientSecret: configuredSecret } : {} };
44
+ }
45
+ if (configuredSecret)
46
+ throw new Error('config.antigravity.clientSecret requires config.antigravity.clientId');
47
+ if (environmentId) {
48
+ return { clientId: environmentId, ...environmentSecret ? { clientSecret: environmentSecret } : {} };
49
+ }
50
+ if (environmentSecret)
51
+ throw new Error('ANTIGRAVITY_CLIENT_SECRET requires ANTIGRAVITY_CLIENT_ID');
52
+ return { clientId: ANTIGRAVITY_DEFAULT_CLIENT_ID, clientSecret: ANTIGRAVITY_DEFAULT_CLIENT_SECRET };
53
+ }
54
+ /** Normalize the configured API origin and reject paths/credentials. */
55
+ export function antigravityBaseURL(value) {
56
+ const parsed = new URL(value?.trim() || ANTIGRAVITY_DEFAULT_BASE_URL);
57
+ if (parsed.protocol !== 'https:' || parsed.username.length > 0 || parsed.password.length > 0) {
58
+ throw new Error('config.antigravity.baseURL must be an HTTPS origin without credentials');
59
+ }
60
+ if (parsed.pathname !== '/' || parsed.search.length > 0 || parsed.hash.length > 0) {
61
+ throw new Error('config.antigravity.baseURL must not contain a path, query, or fragment');
62
+ }
63
+ return parsed.origin;
64
+ }
65
+ /** Try the production endpoint only when the default endpoint is unavailable. */
66
+ async function fetchAntigravity(method, init, runtime, fetchFn) {
67
+ // Explicit origins stay pinned, and onboarding is not repeated across endpoints.
68
+ const endpoints = runtime.baseURL?.trim() || method === 'onboardUser'
69
+ ? [antigravityBaseURL(runtime.baseURL)]
70
+ : [ANTIGRAVITY_DEFAULT_BASE_URL, ANTIGRAVITY_PROD_BASE_URL];
71
+ for (const [index, endpoint] of endpoints.entries()) {
72
+ init.signal?.throwIfAborted();
73
+ let response;
74
+ try {
75
+ response = await fetchFn(`${endpoint}/v1internal:${method}`, init);
76
+ }
77
+ catch (error) {
78
+ if (init.signal?.aborted || index === endpoints.length - 1)
79
+ throw error;
80
+ continue;
81
+ }
82
+ if (index === endpoints.length - 1 || ![404, 502, 503, 504].includes(response.status))
83
+ return response;
84
+ await response.body?.cancel();
85
+ }
86
+ throw new Error('Antigravity endpoint list is empty');
87
+ }
88
+ /** Google authorization-code + PKCE flow for Antigravity. */
89
+ export function antigravityFlow(oauth) {
90
+ return {
91
+ callbackPath: ANTIGRAVITY_CALLBACK_PATH,
92
+ listen: { host: 'localhost', ports: [51121, 0] },
93
+ timeoutMs: 5 * 60_000,
94
+ buildAuthorizeUrl({ redirectUri, state, pkce }) {
95
+ const params = new URLSearchParams({
96
+ access_type: 'offline',
97
+ client_id: oauth.clientId,
98
+ code_challenge: pkce.challenge,
99
+ code_challenge_method: 'S256',
100
+ include_granted_scopes: 'true',
101
+ prompt: 'consent',
102
+ redirect_uri: redirectUri,
103
+ response_type: 'code',
104
+ scope: ANTIGRAVITY_SCOPES.join(' '),
105
+ state,
106
+ });
107
+ return `${ANTIGRAVITY_AUTHORIZE_URL}?${params.toString()}`;
108
+ },
109
+ };
110
+ }
111
+ /** Shared Antigravity API headers. */
112
+ export function antigravityHeaders(accessToken, userAgent = ANTIGRAVITY_DEFAULT_USER_AGENT) {
113
+ return {
114
+ 'authorization': `Bearer ${accessToken}`,
115
+ 'content-type': 'application/json',
116
+ 'user-agent': userAgent,
117
+ };
118
+ }
119
+ /** POST a v1internal JSON method and classify non-2xx responses. */
120
+ async function callInternal(method, body, accessToken, runtime, fetchFn, signal) {
121
+ const response = await fetchAntigravity(method, {
122
+ method: 'POST',
123
+ headers: antigravityHeaders(accessToken, runtime.userAgent),
124
+ body: JSON.stringify(body),
125
+ ...signal === undefined ? {} : { signal },
126
+ }, runtime, fetchFn);
127
+ if (!response.ok)
128
+ throw await httpLlmError(response, `Antigravity ${method}`);
129
+ return response.json();
130
+ }
131
+ function projectIdOf(value) {
132
+ if (typeof value === 'string' && value.length > 0)
133
+ return value;
134
+ if (typeof value === 'object' && value !== null) {
135
+ const id = value.id;
136
+ if (typeof id === 'string' && id.length > 0)
137
+ return id;
138
+ }
139
+ return undefined;
140
+ }
141
+ /** Read (and, when enabled, initialize) the Antigravity project/account. */
142
+ export async function discoverAntigravityAccount(accessToken, runtime = {}, fetchFn = hostFetch) {
143
+ const metadata = { ideType: 'ANTIGRAVITY', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' };
144
+ const load = await callInternal('loadCodeAssist', { metadata }, accessToken, runtime, fetchFn);
145
+ let projectId = projectIdOf(load.cloudaicompanionProject);
146
+ if (projectId === undefined && runtime.onboard !== false) {
147
+ const tierId = load.allowedTiers?.find(tier => tier.isDefault)?.id ?? 'LEGACY';
148
+ const onboardBody = { tierId, metadata };
149
+ for (let attempt = 0; attempt < 10; attempt++) {
150
+ const result = await callInternal('onboardUser', onboardBody, accessToken, runtime, fetchFn);
151
+ if (result.done === true) {
152
+ projectId = projectIdOf(result.response?.cloudaicompanionProject);
153
+ break;
154
+ }
155
+ await new Promise(resolve => setTimeout(resolve, 1_000));
156
+ }
157
+ }
158
+ if (projectId === undefined) {
159
+ throw new Error('Antigravity account has no Cloud AI Companion project; open Antigravity and complete onboarding, then log in again');
160
+ }
161
+ let account;
162
+ try {
163
+ const profileResponse = await fetchFn(ANTIGRAVITY_USERINFO_URL, {
164
+ headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json' },
165
+ });
166
+ if (profileResponse.ok) {
167
+ const profile = await profileResponse.json();
168
+ if (typeof profile.email === 'string' && profile.email.length > 0)
169
+ account = profile.email;
170
+ }
171
+ }
172
+ catch {
173
+ // Identity is display-only; project discovery is the login boundary.
174
+ }
175
+ const plan = load.paidTier?.name ?? load.paidTier?.id ?? load.currentTier?.name ?? load.currentTier?.id;
176
+ return {
177
+ projectId,
178
+ ...account === undefined ? {} : { account },
179
+ ...typeof plan !== 'string' || plan.length === 0 ? {} : { plan },
180
+ };
181
+ }
182
+ function sessionFromTokens(tokens, account, fallback) {
183
+ if (typeof tokens.access_token !== 'string' || tokens.access_token.length === 0) {
184
+ throw new Error('Antigravity token endpoint returned no access token');
185
+ }
186
+ const refreshToken = tokens.refresh_token ?? fallback?.refreshToken;
187
+ if (refreshToken === undefined || refreshToken.length === 0) {
188
+ throw new Error('Antigravity token endpoint returned no refresh token; revoke the app grant and log in again');
189
+ }
190
+ if (typeof tokens.expires_in !== 'number' || tokens.expires_in <= 0) {
191
+ throw new Error('Antigravity token endpoint returned no usable expiry');
192
+ }
193
+ return {
194
+ accessToken: tokens.access_token,
195
+ refreshToken,
196
+ expiresAt: Date.now() + tokens.expires_in * 1000,
197
+ projectId: account.projectId,
198
+ ...tokens.scope === undefined ? {} : { scopes: tokens.scope },
199
+ ...account.account === undefined ? {} : { account: account.account },
200
+ ...account.plan === undefined ? {} : { plan: account.plan },
201
+ };
202
+ }
203
+ /** Exchange a Google OAuth authorization code and discover the Antigravity project. */
204
+ export async function exchangeAntigravityCode(code, verifier, redirectUri, oauth, runtime = {}, fetchFn = hostFetch) {
205
+ const body = new URLSearchParams({
206
+ grant_type: 'authorization_code',
207
+ code,
208
+ redirect_uri: redirectUri,
209
+ client_id: oauth.clientId,
210
+ code_verifier: verifier,
211
+ ...oauth.clientSecret === undefined ? {} : { client_secret: oauth.clientSecret },
212
+ });
213
+ const response = await fetchFn(ANTIGRAVITY_TOKEN_URL, {
214
+ method: 'POST',
215
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
216
+ body: body.toString(),
217
+ });
218
+ if (!response.ok)
219
+ throw await oauthEndpointError(response, 'Antigravity');
220
+ const tokens = await response.json();
221
+ if (typeof tokens.access_token !== 'string')
222
+ throw new Error('Antigravity token endpoint returned no access token');
223
+ const account = await discoverAntigravityAccount(tokens.access_token, runtime, fetchFn);
224
+ return sessionFromTokens(tokens, account);
225
+ }
226
+ /** Refresh a stored Antigravity Google token, preserving project/account metadata. */
227
+ export async function refreshAntigravity(session, oauth, fetchFn = hostFetch) {
228
+ const body = new URLSearchParams({
229
+ grant_type: 'refresh_token',
230
+ refresh_token: session.refreshToken,
231
+ client_id: oauth.clientId,
232
+ ...oauth.clientSecret === undefined ? {} : { client_secret: oauth.clientSecret },
233
+ });
234
+ const response = await fetchFn(ANTIGRAVITY_TOKEN_URL, {
235
+ method: 'POST',
236
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
237
+ body: body.toString(),
238
+ });
239
+ if (!response.ok)
240
+ throw await oauthEndpointError(response, 'Antigravity');
241
+ return sessionFromTokens(await response.json(), {
242
+ projectId: session.projectId,
243
+ ...session.account === undefined ? {} : { account: session.account },
244
+ ...session.plan === undefined ? {} : { plan: session.plan },
245
+ }, session);
246
+ }
247
+ /** Refresh failures that require a fresh Google consent grant. */
248
+ export function isAntigravityPermanentRefreshError(error) {
249
+ return error instanceof OAuthEndpointError
250
+ && (error.status === 400 || error.status === 401 || error.status === 403)
251
+ && (error.oauthCode === 'invalid_grant' || error.status !== 400);
252
+ }
253
+ /** Fetch the authenticated account's live Antigravity model catalog. */
254
+ export async function fetchAntigravityModels(session, runtime = {}, fetchFn = hostFetch, signal) {
255
+ const payload = await callInternal('fetchAvailableModels', { project: session.projectId }, session.accessToken, runtime, fetchFn, signal);
256
+ if (typeof payload.models !== 'object' || payload.models === null) {
257
+ throw new Error('Antigravity models endpoint returned no models object');
258
+ }
259
+ const models = Object.entries(payload.models).map(([id, model]) => ({
260
+ id,
261
+ name: model.displayName ?? id.split('-').map(word => word.length === 0 ? word : word[0].toUpperCase() + word.slice(1)).join(' '),
262
+ ...model.description === undefined ? {} : { description: model.description },
263
+ contextWindow: model.inputTokenLimit ?? model.maxInputTokens ?? ANTIGRAVITY_CONTEXT_WINDOW,
264
+ ...typeof model.maxOutputTokens === 'number' && Number.isSafeInteger(model.maxOutputTokens) && model.maxOutputTokens > 0
265
+ ? { maxOutputTokens: model.maxOutputTokens } : {},
266
+ inputModalities: ['text', 'image'],
267
+ }));
268
+ if (models.length === 0)
269
+ throw new Error('Antigravity models endpoint returned an empty catalog');
270
+ return models;
271
+ }
272
+ function resetTime(value) {
273
+ if (typeof value !== 'string')
274
+ return undefined;
275
+ const parsed = Date.parse(value);
276
+ return Number.isFinite(parsed) ? parsed : undefined;
277
+ }
278
+ function usageWindow(kind, scope, quota) {
279
+ const remaining = quota?.remainingFraction;
280
+ if (typeof remaining !== 'number' || !Number.isFinite(remaining))
281
+ return undefined;
282
+ const resetsAt = resetTime(quota?.resetTime);
283
+ return {
284
+ kind,
285
+ scope,
286
+ usedPercent: Math.max(0, Math.min(100, (1 - remaining) * 100)),
287
+ ...resetsAt === undefined ? {} : { resetsAt },
288
+ };
289
+ }
290
+ /** Fetch plan and per-model quota windows when the upstream exposes them. */
291
+ export async function fetchAntigravityUsage(session, runtime = {}, fetchFn = hostFetch, signal) {
292
+ const metadata = { ideType: 'ANTIGRAVITY', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' };
293
+ const [models, account] = await Promise.all([
294
+ callInternal('fetchAvailableModels', { project: session.projectId }, session.accessToken, runtime, fetchFn, signal),
295
+ callInternal('loadCodeAssist', { metadata }, session.accessToken, runtime, fetchFn, signal),
296
+ ]);
297
+ const windows = [];
298
+ for (const [modelId, model] of Object.entries(models.models ?? {})) {
299
+ const ordinary = usageWindow('other', modelId, model.quotaInfo);
300
+ const weekly = usageWindow('weekly', modelId, model.weeklyQuotaInfo ?? model.weeklyQuota);
301
+ if (ordinary !== undefined)
302
+ windows.push(ordinary);
303
+ if (weekly !== undefined)
304
+ windows.push(weekly);
305
+ }
306
+ const plan = account.paidTier?.name ?? account.paidTier?.id
307
+ ?? account.currentTier?.name ?? account.currentTier?.id ?? session.plan;
308
+ const credits = account.paidTier?.availableCredits?.[0]?.creditAmount;
309
+ const displayPlan = credits === undefined ? plan : `${plan ?? 'Antigravity'} · ${String(credits)} credits`;
310
+ return {
311
+ supported: true,
312
+ windows,
313
+ ...displayPlan === undefined ? {} : { plan: displayPlan },
314
+ };
315
+ }
316
+ /** URL for either v1internal generation transport. */
317
+ export function antigravityGenerateURL(baseURL, stream) {
318
+ return `${antigravityBaseURL(baseURL)}/v1internal:${stream ? 'streamGenerateContent?alt=sse' : 'generateContent'}`;
319
+ }
320
+ /** Forward one already-built payload to generateContent or streamGenerateContent. */
321
+ export async function requestAntigravityContent(session, payload, stream, runtime = {}, fetchFn = hostFetch, signal) {
322
+ return fetchAntigravity(stream ? 'streamGenerateContent?alt=sse' : 'generateContent', {
323
+ method: 'POST',
324
+ headers: {
325
+ ...antigravityHeaders(session.accessToken, runtime.userAgent),
326
+ accept: stream ? 'text/event-stream' : 'application/json',
327
+ },
328
+ body: JSON.stringify(payload),
329
+ ...signal === undefined ? {} : { signal },
330
+ }, runtime, fetchFn);
331
+ }
332
+ /** DSH provider adapter for the `antigravity` route. */
333
+ export class AntigravityAdapter extends LlmAdapter {
334
+ options;
335
+ catalog;
336
+ accountCatalogs = new Map();
337
+ catalogOwner;
338
+ constructor(options) {
339
+ super();
340
+ this.options = options;
341
+ this.catalog = new ModelCatalogCache(options.catalogStore);
342
+ }
343
+ providerInfo(provider) {
344
+ return { id: provider, name: 'Google Antigravity' };
345
+ }
346
+ /** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
347
+ clearAccountCatalog(account) {
348
+ if (account === undefined)
349
+ this.accountCatalogs.clear();
350
+ else
351
+ this.accountCatalogs.delete(account);
352
+ if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
353
+ this.catalogOwner = undefined;
354
+ this.catalog.invalidate();
355
+ }
356
+ }
357
+ /** Persisted cache for the default account; a throwaway cache for any other. */
358
+ async catalogFor(account) {
359
+ const defaultKey = await this.options.tokens.defaultAccount();
360
+ const key = account ?? defaultKey;
361
+ if (key === undefined || key === defaultKey) {
362
+ if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
363
+ this.catalog.invalidate();
364
+ }
365
+ this.catalogOwner = defaultKey;
366
+ return this.catalog;
367
+ }
368
+ let cache = this.accountCatalogs.get(key);
369
+ if (cache === undefined) {
370
+ cache = new ModelCatalogCache();
371
+ this.accountCatalogs.set(key, cache);
372
+ }
373
+ return cache;
374
+ }
375
+ providerRetryPolicy(provider) {
376
+ return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `antigravity: provider "${provider}" retryPolicy`);
377
+ }
378
+ staticModels(provider) {
379
+ return this.options.models.map(model => ({
380
+ provider,
381
+ id: model.id,
382
+ name: model.name ?? model.id,
383
+ inputModalities: model.inputModalities ?? ['text', 'image'],
384
+ }));
385
+ }
386
+ fetchCatalog(account, signal) {
387
+ return this.options.tokens.session(account).then(session => fetchAntigravityModels(session, this.options.runtime, this.options.fetchFn, signal));
388
+ }
389
+ async listModels(provider) {
390
+ const own = await this.listOwnModels(provider);
391
+ const pool = this.options.pool?.();
392
+ if (pool === undefined)
393
+ return own;
394
+ const extra = await pool.modelsForProvider(provider);
395
+ const seen = new Set(own.map(model => model.id));
396
+ // Account pools reuse the catalog row; only configured tiers are extra.
397
+ return [...own, ...extra.filter(model => !seen.has(model.id))];
398
+ }
399
+ /** The provider's own catalog: union of every account, or one account when named. */
400
+ async listOwnModels(provider, account, signal) {
401
+ if (account === undefined) {
402
+ const accounts = (await this.options.tokens.list()).map(entry => entry.key);
403
+ if (accounts.length === 0)
404
+ return [];
405
+ return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
406
+ }
407
+ if (await this.options.tokens.peek(account) === undefined)
408
+ return [];
409
+ if (!this.options.discovery)
410
+ return this.staticModels(provider);
411
+ const catalog = await this.catalogFor(account);
412
+ try {
413
+ const models = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
414
+ return models.map(model => ({
415
+ provider,
416
+ id: model.id,
417
+ name: model.name,
418
+ ...model.description === undefined ? {} : { description: model.description },
419
+ inputModalities: model.inputModalities ?? ['text', 'image'],
420
+ }));
421
+ }
422
+ catch (error) {
423
+ if (isDiscoveryAborted(error, signal))
424
+ throw error;
425
+ if (isMissingOrInvalidCredential(error))
426
+ return [];
427
+ this.options.onWarn?.(`Antigravity model discovery failed; using the built-in catalog (${errorChain(error)})`);
428
+ return this.staticModels(provider);
429
+ }
430
+ }
431
+ async discovered(model, account) {
432
+ if (!this.options.discovery)
433
+ return undefined;
434
+ const accounts = account === undefined ? (await this.options.tokens.list()).map(entry => entry.key) : [account];
435
+ return discoverAcrossAccounts(accounts, async (key) => {
436
+ const catalog = await this.catalogFor(key);
437
+ const models = await catalog.resolve(() => this.fetchCatalog(key));
438
+ return models?.find(entry => entry.id === model);
439
+ });
440
+ }
441
+ async resolveModel(provider, model) {
442
+ const pool = this.options.pool?.();
443
+ if (pool !== undefined && await pool.owns(provider, model)) {
444
+ return pool.resolveModel(provider, model);
445
+ }
446
+ return this.resolveOwnModel(provider, model);
447
+ }
448
+ /** Capability resolution of the provider's own models (the pool resolves members here). */
449
+ async resolveOwnModel(provider, model, account) {
450
+ const discovered = await this.discovered(model, account);
451
+ const configured = this.options.models.find(entry => entry.id === model);
452
+ const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), antigravityReasoning(model));
453
+ const outputLimit = discovered?.maxOutputTokens;
454
+ const preferredMaxTokens = configured?.maxTokens ?? outputLimit ?? ANTIGRAVITY_DEFAULT_MAX_TOKENS;
455
+ // Configuration may lower the default, but must not exceed a known server cap.
456
+ const defaultMaxTokens = outputLimit === undefined ? preferredMaxTokens : Math.min(preferredMaxTokens, outputLimit);
457
+ return {
458
+ provider,
459
+ id: model,
460
+ name: discovered?.name ?? configured?.name ?? model,
461
+ ...discovered?.description === undefined ? {} : { description: discovered.description },
462
+ inputModalities: discovered?.inputModalities ?? configured?.inputModalities ?? ['text', 'image'],
463
+ context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? ANTIGRAVITY_CONTEXT_WINDOW },
464
+ defaultMaxTokens,
465
+ ...reasoning === undefined ? {} : { reasoning },
466
+ };
467
+ }
468
+ async *stream(options) {
469
+ const pool = this.options.pool?.();
470
+ if (pool !== undefined && await pool.owns(options.provider, options.model)) {
471
+ yield* pool.stream(options);
472
+ return;
473
+ }
474
+ yield* this.streamCore(options);
475
+ }
476
+ /** Pool seam: stream through one specific account instead of the default. */
477
+ streamAccount(options, account) {
478
+ return this.streamCore(options, account);
479
+ }
480
+ async *streamCore(options, account) {
481
+ const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
482
+ try {
483
+ let session = await this.options.tokens.session(account);
484
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), watchdog.signal);
485
+ let payload = toAntigravityRequest(options, messages, session.projectId, true);
486
+ let response = await requestAntigravityContent(session, payload, true, this.options.runtime, this.options.fetchFn, watchdog.signal);
487
+ if (response.status === 401) {
488
+ this.clearAccountCatalog(account);
489
+ session = await this.options.tokens.session(account, true);
490
+ payload = toAntigravityRequest(options, messages, session.projectId, true);
491
+ response = await requestAntigravityContent(session, payload, true, this.options.runtime, this.options.fetchFn, watchdog.signal);
492
+ }
493
+ if (!response.ok)
494
+ throw await httpLlmError(response, 'Antigravity API');
495
+ if (response.body === null) {
496
+ throw new LlmError('Antigravity API returned no response body', EMPTY_RESPONSE_CODE);
497
+ }
498
+ yield* streamAntigravity(response.body, () => { watchdog.pulse(); });
499
+ }
500
+ catch (error) {
501
+ throw mapFetchFailure('Antigravity API', error, watchdog, options.signal);
502
+ }
503
+ finally {
504
+ watchdog.stop();
505
+ }
506
+ }
507
+ /** Non-stream forwarding seam used by tests and future DSH complete calls. */
508
+ async generate(options) {
509
+ const session = await this.options.tokens.session();
510
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), options.signal);
511
+ const response = await requestAntigravityContent(session, toAntigravityRequest(options, messages, session.projectId), false, this.options.runtime, this.options.fetchFn, options.signal);
512
+ if (!response.ok)
513
+ throw await httpLlmError(response, 'Antigravity API');
514
+ return parseAntigravityResponse(await response.json());
515
+ }
516
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * On-disk discovered-model-catalog cache at
3
+ * `~/.dsh/plugins/subscriptions/models.json` — the durable half of each
4
+ * provider's {@link ModelCatalogCache}. One entry per provider: the last
5
+ * successfully discovered catalog with its fetch time, so capability metadata
6
+ * (reasoning efforts) survives restarts and network failures.
7
+ *
8
+ * Unlike the auth store, this file is a cache: a missing, corrupt, or
9
+ * malformed file silently reads as absent, because the next successful
10
+ * discovery rewrites it. Loads are strictly validated — a malformed entry
11
+ * passed through `resolveModel` would make the harness's metadata validation
12
+ * throw on every call, which is worse than having no fallback at all.
13
+ */
14
+ import type { ProviderId } from '../auth/store.js';
15
+ import type { CatalogPersistence, CatalogSnapshot } from './common.js';
16
+ /**
17
+ * Absolute path of the catalog store file.
18
+ * @returns `dshHomePath('plugins', 'subscriptions', 'models.json')`.
19
+ */
20
+ export declare function modelsFilePath(): string;
21
+ /**
22
+ * Validate one persisted snapshot. Strict: any malformed field drops the
23
+ * whole snapshot rather than repairing it — the next successful discovery
24
+ * rewrites the entry anyway.
25
+ * @param value - the raw per-provider file entry.
26
+ * @returns the validated snapshot, or undefined when unusable.
27
+ */
28
+ export declare function sanitizeSnapshot(value: unknown): CatalogSnapshot | undefined;
29
+ /**
30
+ * Build the durable half of one provider's catalog cache over the shared
31
+ * models.json file (concurrent writers are last-writer-wins, acceptable for
32
+ * a cache).
33
+ * @param provider - the provider route keying the file entry.
34
+ * @param path - store file path; defaults to {@link modelsFilePath}.
35
+ * @returns the persistence hooks for {@link ModelCatalogCache}.
36
+ */
37
+ export declare function catalogStore(provider: ProviderId, path?: string): CatalogPersistence;
38
+ /**
39
+ * The durable half of one NON-default account's catalog cache. The provider
40
+ * entry belongs to the default account; every other account keeps its own
41
+ * snapshot under `accounts[provider][account]`, so a working secondary login
42
+ * survives restarts and network failures even when the default login is dead.
43
+ * @param provider - the provider route.
44
+ * @param account - the canonical account key.
45
+ * @param path - store file path; defaults to {@link modelsFilePath}.
46
+ * @returns the persistence hooks for {@link ModelCatalogCache}.
47
+ */
48
+ export declare function accountCatalogStore(provider: ProviderId, account: string, path?: string): CatalogPersistence;