dsh-plugin-subscriptions 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
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 { LlmAdapter } from '@deepseek-ai/dsh-llm';
7
+ import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
8
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
9
+ import type { FlowSpec } from '../auth/oauth-flow.js';
10
+ import type { AntigravitySession } from '../auth/store.js';
11
+ import type { AntigravityRequest } from '../translate/antigravity.js';
12
+ import { TokenManager } from './common.js';
13
+ import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
14
+ export declare const ANTIGRAVITY_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
15
+ export declare const ANTIGRAVITY_TOKEN_URL = "https://oauth2.googleapis.com/token";
16
+ export declare const ANTIGRAVITY_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
17
+ export declare const ANTIGRAVITY_DEFAULT_BASE_URL = "https://daily-cloudcode-pa.googleapis.com";
18
+ export declare const ANTIGRAVITY_PROD_BASE_URL = "https://cloudcode-pa.googleapis.com";
19
+ export declare const ANTIGRAVITY_DEFAULT_USER_AGENT = "antigravity/1.104.0 dsh-plugin-subscriptions";
20
+ export declare const ANTIGRAVITY_PREEMPT_MS: number;
21
+ /** Antigravity, not Gemini CLI, OAuth scopes from the local reference clients. */
22
+ export declare const ANTIGRAVITY_SCOPES: readonly ["openid", "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/cclog", "https://www.googleapis.com/auth/experimentsandconfigs"];
23
+ /** OAuth client configuration. Values must come from config/environment. */
24
+ export interface AntigravityOAuthConfig {
25
+ clientId: string;
26
+ clientSecret?: string;
27
+ }
28
+ /** Runtime endpoint configuration. */
29
+ export interface AntigravityRuntimeConfig {
30
+ baseURL?: string;
31
+ userAgent?: string;
32
+ /** Activate an eligible account when loadCodeAssist has no project yet. */
33
+ onboard?: boolean;
34
+ }
35
+ /** Resolve and validate a user-supplied OAuth config without embedded credentials. */
36
+ export declare function resolveAntigravityOAuthConfig(config?: Partial<AntigravityOAuthConfig>): AntigravityOAuthConfig;
37
+ /** Normalize the configured API origin and reject paths/credentials. */
38
+ export declare function antigravityBaseURL(value?: string): string;
39
+ /** Google authorization-code + PKCE flow for Antigravity. */
40
+ export declare function antigravityFlow(oauth: AntigravityOAuthConfig): FlowSpec;
41
+ interface AntigravityAccountInfo {
42
+ projectId: string;
43
+ account?: string;
44
+ plan?: string;
45
+ }
46
+ /** Shared Antigravity API headers. */
47
+ export declare function antigravityHeaders(accessToken: string, userAgent?: string): Record<string, string>;
48
+ /** Read (and, when enabled, initialize) the Antigravity project/account. */
49
+ export declare function discoverAntigravityAccount(accessToken: string, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn): Promise<AntigravityAccountInfo>;
50
+ /** Exchange a Google OAuth authorization code and discover the Antigravity project. */
51
+ export declare function exchangeAntigravityCode(code: string, verifier: string, redirectUri: string, oauth: AntigravityOAuthConfig, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn): Promise<AntigravitySession>;
52
+ /** Refresh a stored Antigravity Google token, preserving project/account metadata. */
53
+ export declare function refreshAntigravity(session: AntigravitySession, oauth: AntigravityOAuthConfig, fetchFn?: FetchFn): Promise<AntigravitySession>;
54
+ /** Refresh failures that require a fresh Google consent grant. */
55
+ export declare function isAntigravityPermanentRefreshError(error: unknown): boolean;
56
+ /** Fetch the authenticated account's live Antigravity model catalog. */
57
+ export declare function fetchAntigravityModels(session: AntigravitySession, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
58
+ /** Fetch plan and per-model quota windows when the upstream exposes them. */
59
+ export declare function fetchAntigravityUsage(session: AntigravitySession, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
60
+ /** URL for either v1internal generation transport. */
61
+ export declare function antigravityGenerateURL(baseURL: string | undefined, stream: boolean): string;
62
+ /** Forward one already-built payload to generateContent or streamGenerateContent. */
63
+ export declare function requestAntigravityContent(session: AntigravitySession, payload: AntigravityRequest, stream: boolean, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn, signal?: AbortSignal): Promise<Response>;
64
+ export interface AntigravityAdapterOptions {
65
+ models: readonly ModelEntry[];
66
+ streamIdleTimeoutMs: number;
67
+ tokens: TokenManager<AntigravitySession>;
68
+ discovery: boolean;
69
+ runtime?: AntigravityRuntimeConfig;
70
+ onWarn?: (message: string) => void;
71
+ fetchFn?: FetchFn;
72
+ resolveAttachments?: () => AttachmentStore | undefined;
73
+ catalogStore?: CatalogPersistence;
74
+ }
75
+ /** DSH provider adapter for the `antigravity` route. */
76
+ export declare class AntigravityAdapter extends LlmAdapter {
77
+ private readonly options;
78
+ private readonly catalog;
79
+ constructor(options: AntigravityAdapterOptions);
80
+ providerInfo(provider: string): LlmProviderInfo;
81
+ private staticModels;
82
+ private fetchCatalog;
83
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
84
+ private discovered;
85
+ resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
86
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
87
+ /** Non-stream forwarding seam used by tests and future DSH complete calls. */
88
+ generate(options: GenerateOptions): Promise<StreamChunk[]>;
89
+ }
90
+ export {};
@@ -0,0 +1,392 @@
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 { parseAntigravityResponse, streamAntigravity, toAntigravityRequest, } from '../translate/antigravity.js';
9
+ import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
10
+ import { proxiedFetch } from '../http.js';
11
+ export const ANTIGRAVITY_AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
12
+ export const ANTIGRAVITY_TOKEN_URL = 'https://oauth2.googleapis.com/token';
13
+ export const ANTIGRAVITY_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
14
+ export const ANTIGRAVITY_DEFAULT_BASE_URL = 'https://daily-cloudcode-pa.googleapis.com';
15
+ export const ANTIGRAVITY_PROD_BASE_URL = 'https://cloudcode-pa.googleapis.com';
16
+ export const ANTIGRAVITY_DEFAULT_USER_AGENT = 'antigravity/1.104.0 dsh-plugin-subscriptions';
17
+ export const ANTIGRAVITY_PREEMPT_MS = 5 * 60_000;
18
+ const ANTIGRAVITY_CALLBACK_PATH = '/oauth-callback';
19
+ const ANTIGRAVITY_CONTEXT_WINDOW = 1_024_000;
20
+ const ANTIGRAVITY_DEFAULT_MAX_TOKENS = 65_536;
21
+ /** Antigravity, not Gemini CLI, OAuth scopes from the local reference clients. */
22
+ export const ANTIGRAVITY_SCOPES = [
23
+ 'openid',
24
+ 'https://www.googleapis.com/auth/cloud-platform',
25
+ 'https://www.googleapis.com/auth/userinfo.email',
26
+ 'https://www.googleapis.com/auth/userinfo.profile',
27
+ 'https://www.googleapis.com/auth/cclog',
28
+ 'https://www.googleapis.com/auth/experimentsandconfigs',
29
+ ];
30
+ /** Resolve and validate a user-supplied OAuth config without embedded credentials. */
31
+ export function resolveAntigravityOAuthConfig(config) {
32
+ const clientId = config?.clientId?.trim() || process.env.ANTIGRAVITY_CLIENT_ID?.trim() || '';
33
+ const clientSecret = config?.clientSecret?.trim() || process.env.ANTIGRAVITY_CLIENT_SECRET?.trim();
34
+ if (clientId.length === 0) {
35
+ throw new Error('Antigravity OAuth is not configured; set config.antigravity.clientId or ANTIGRAVITY_CLIENT_ID '
36
+ + '(and clientSecret/ANTIGRAVITY_CLIENT_SECRET when required by the Google OAuth client)');
37
+ }
38
+ return { clientId, ...clientSecret === undefined || clientSecret.length === 0 ? {} : { clientSecret } };
39
+ }
40
+ /** Normalize the configured API origin and reject paths/credentials. */
41
+ export function antigravityBaseURL(value) {
42
+ const parsed = new URL(value?.trim() || ANTIGRAVITY_DEFAULT_BASE_URL);
43
+ if (parsed.protocol !== 'https:' || parsed.username.length > 0 || parsed.password.length > 0) {
44
+ throw new Error('config.antigravity.baseURL must be an HTTPS origin without credentials');
45
+ }
46
+ if (parsed.pathname !== '/' || parsed.search.length > 0 || parsed.hash.length > 0) {
47
+ throw new Error('config.antigravity.baseURL must not contain a path, query, or fragment');
48
+ }
49
+ return parsed.origin;
50
+ }
51
+ /** Google authorization-code + PKCE flow for Antigravity. */
52
+ export function antigravityFlow(oauth) {
53
+ return {
54
+ callbackPath: ANTIGRAVITY_CALLBACK_PATH,
55
+ listen: { host: 'localhost', ports: [51121, 0] },
56
+ timeoutMs: 5 * 60_000,
57
+ buildAuthorizeUrl({ redirectUri, state, pkce }) {
58
+ const params = new URLSearchParams({
59
+ access_type: 'offline',
60
+ client_id: oauth.clientId,
61
+ code_challenge: pkce.challenge,
62
+ code_challenge_method: 'S256',
63
+ include_granted_scopes: 'true',
64
+ prompt: 'consent',
65
+ redirect_uri: redirectUri,
66
+ response_type: 'code',
67
+ scope: ANTIGRAVITY_SCOPES.join(' '),
68
+ state,
69
+ });
70
+ return `${ANTIGRAVITY_AUTHORIZE_URL}?${params.toString()}`;
71
+ },
72
+ };
73
+ }
74
+ /** Shared Antigravity API headers. */
75
+ export function antigravityHeaders(accessToken, userAgent = ANTIGRAVITY_DEFAULT_USER_AGENT) {
76
+ return {
77
+ 'authorization': `Bearer ${accessToken}`,
78
+ 'content-type': 'application/json',
79
+ 'user-agent': userAgent,
80
+ };
81
+ }
82
+ /** POST a v1internal JSON method and classify non-2xx responses. */
83
+ async function callInternal(method, body, accessToken, runtime, fetchFn, signal) {
84
+ const response = await fetchFn(`${antigravityBaseURL(runtime.baseURL)}/v1internal:${method}`, {
85
+ method: 'POST',
86
+ headers: antigravityHeaders(accessToken, runtime.userAgent),
87
+ body: JSON.stringify(body),
88
+ ...signal === undefined ? {} : { signal },
89
+ });
90
+ if (!response.ok)
91
+ throw await httpLlmError(response, `Antigravity ${method}`);
92
+ return response.json();
93
+ }
94
+ function projectIdOf(value) {
95
+ if (typeof value === 'string' && value.length > 0)
96
+ return value;
97
+ if (typeof value === 'object' && value !== null) {
98
+ const id = value.id;
99
+ if (typeof id === 'string' && id.length > 0)
100
+ return id;
101
+ }
102
+ return undefined;
103
+ }
104
+ /** Read (and, when enabled, initialize) the Antigravity project/account. */
105
+ export async function discoverAntigravityAccount(accessToken, runtime = {}, fetchFn = proxiedFetch) {
106
+ const metadata = { ideType: 'ANTIGRAVITY', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' };
107
+ const load = await callInternal('loadCodeAssist', { metadata }, accessToken, runtime, fetchFn);
108
+ let projectId = projectIdOf(load.cloudaicompanionProject);
109
+ if (projectId === undefined && runtime.onboard !== false) {
110
+ const tierId = load.allowedTiers?.find(tier => tier.isDefault)?.id ?? 'LEGACY';
111
+ const onboardBody = { tierId, metadata };
112
+ for (let attempt = 0; attempt < 10; attempt++) {
113
+ const result = await callInternal('onboardUser', onboardBody, accessToken, runtime, fetchFn);
114
+ if (result.done === true) {
115
+ projectId = projectIdOf(result.response?.cloudaicompanionProject);
116
+ break;
117
+ }
118
+ await new Promise(resolve => setTimeout(resolve, 1_000));
119
+ }
120
+ }
121
+ if (projectId === undefined) {
122
+ throw new Error('Antigravity account has no Cloud AI Companion project; open Antigravity and complete onboarding, then log in again');
123
+ }
124
+ let account;
125
+ try {
126
+ const profileResponse = await fetchFn(ANTIGRAVITY_USERINFO_URL, {
127
+ headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json' },
128
+ });
129
+ if (profileResponse.ok) {
130
+ const profile = await profileResponse.json();
131
+ if (typeof profile.email === 'string' && profile.email.length > 0)
132
+ account = profile.email;
133
+ }
134
+ }
135
+ catch {
136
+ // Identity is display-only; project discovery is the login boundary.
137
+ }
138
+ const plan = load.paidTier?.name ?? load.paidTier?.id ?? load.currentTier?.name ?? load.currentTier?.id;
139
+ return {
140
+ projectId,
141
+ ...account === undefined ? {} : { account },
142
+ ...typeof plan !== 'string' || plan.length === 0 ? {} : { plan },
143
+ };
144
+ }
145
+ function sessionFromTokens(tokens, account, fallback) {
146
+ if (typeof tokens.access_token !== 'string' || tokens.access_token.length === 0) {
147
+ throw new Error('Antigravity token endpoint returned no access token');
148
+ }
149
+ const refreshToken = tokens.refresh_token ?? fallback?.refreshToken;
150
+ if (refreshToken === undefined || refreshToken.length === 0) {
151
+ throw new Error('Antigravity token endpoint returned no refresh token; revoke the app grant and log in again');
152
+ }
153
+ if (typeof tokens.expires_in !== 'number' || tokens.expires_in <= 0) {
154
+ throw new Error('Antigravity token endpoint returned no usable expiry');
155
+ }
156
+ return {
157
+ accessToken: tokens.access_token,
158
+ refreshToken,
159
+ expiresAt: Date.now() + tokens.expires_in * 1000,
160
+ projectId: account.projectId,
161
+ ...tokens.scope === undefined ? {} : { scopes: tokens.scope },
162
+ ...account.account === undefined ? {} : { account: account.account },
163
+ ...account.plan === undefined ? {} : { plan: account.plan },
164
+ };
165
+ }
166
+ /** Exchange a Google OAuth authorization code and discover the Antigravity project. */
167
+ export async function exchangeAntigravityCode(code, verifier, redirectUri, oauth, runtime = {}, fetchFn = proxiedFetch) {
168
+ const body = new URLSearchParams({
169
+ grant_type: 'authorization_code',
170
+ code,
171
+ redirect_uri: redirectUri,
172
+ client_id: oauth.clientId,
173
+ code_verifier: verifier,
174
+ ...oauth.clientSecret === undefined ? {} : { client_secret: oauth.clientSecret },
175
+ });
176
+ const response = await fetchFn(ANTIGRAVITY_TOKEN_URL, {
177
+ method: 'POST',
178
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
179
+ body: body.toString(),
180
+ });
181
+ if (!response.ok)
182
+ throw await oauthEndpointError(response, 'Antigravity');
183
+ const tokens = await response.json();
184
+ if (typeof tokens.access_token !== 'string')
185
+ throw new Error('Antigravity token endpoint returned no access token');
186
+ const account = await discoverAntigravityAccount(tokens.access_token, runtime, fetchFn);
187
+ return sessionFromTokens(tokens, account);
188
+ }
189
+ /** Refresh a stored Antigravity Google token, preserving project/account metadata. */
190
+ export async function refreshAntigravity(session, oauth, fetchFn = proxiedFetch) {
191
+ const body = new URLSearchParams({
192
+ grant_type: 'refresh_token',
193
+ refresh_token: session.refreshToken,
194
+ client_id: oauth.clientId,
195
+ ...oauth.clientSecret === undefined ? {} : { client_secret: oauth.clientSecret },
196
+ });
197
+ const response = await fetchFn(ANTIGRAVITY_TOKEN_URL, {
198
+ method: 'POST',
199
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
200
+ body: body.toString(),
201
+ });
202
+ if (!response.ok)
203
+ throw await oauthEndpointError(response, 'Antigravity');
204
+ return sessionFromTokens(await response.json(), {
205
+ projectId: session.projectId,
206
+ ...session.account === undefined ? {} : { account: session.account },
207
+ ...session.plan === undefined ? {} : { plan: session.plan },
208
+ }, session);
209
+ }
210
+ /** Refresh failures that require a fresh Google consent grant. */
211
+ export function isAntigravityPermanentRefreshError(error) {
212
+ return error instanceof OAuthEndpointError
213
+ && (error.status === 400 || error.status === 401 || error.status === 403)
214
+ && (error.oauthCode === 'invalid_grant' || error.status !== 400);
215
+ }
216
+ /** Fetch the authenticated account's live Antigravity model catalog. */
217
+ export async function fetchAntigravityModels(session, runtime = {}, fetchFn = proxiedFetch) {
218
+ const payload = await callInternal('fetchAvailableModels', { project: session.projectId }, session.accessToken, runtime, fetchFn);
219
+ if (typeof payload.models !== 'object' || payload.models === null) {
220
+ throw new Error('Antigravity models endpoint returned no models object');
221
+ }
222
+ const models = Object.entries(payload.models).map(([id, model]) => ({
223
+ id,
224
+ name: model.displayName ?? id.split('-').map(word => word.length === 0 ? word : word[0].toUpperCase() + word.slice(1)).join(' '),
225
+ ...model.description === undefined ? {} : { description: model.description },
226
+ contextWindow: model.inputTokenLimit ?? model.maxInputTokens ?? ANTIGRAVITY_CONTEXT_WINDOW,
227
+ inputModalities: ['text', 'image'],
228
+ }));
229
+ if (models.length === 0)
230
+ throw new Error('Antigravity models endpoint returned an empty catalog');
231
+ return models;
232
+ }
233
+ function resetTime(value) {
234
+ if (typeof value !== 'string')
235
+ return undefined;
236
+ const parsed = Date.parse(value);
237
+ return Number.isFinite(parsed) ? parsed : undefined;
238
+ }
239
+ function usageWindow(kind, scope, quota) {
240
+ const remaining = quota?.remainingFraction;
241
+ if (typeof remaining !== 'number' || !Number.isFinite(remaining))
242
+ return undefined;
243
+ const resetsAt = resetTime(quota?.resetTime);
244
+ return {
245
+ kind,
246
+ scope,
247
+ usedPercent: Math.max(0, Math.min(100, (1 - remaining) * 100)),
248
+ ...resetsAt === undefined ? {} : { resetsAt },
249
+ };
250
+ }
251
+ /** Fetch plan and per-model quota windows when the upstream exposes them. */
252
+ export async function fetchAntigravityUsage(session, runtime = {}, fetchFn = proxiedFetch, signal) {
253
+ const metadata = { ideType: 'ANTIGRAVITY', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' };
254
+ const [models, account] = await Promise.all([
255
+ callInternal('fetchAvailableModels', { project: session.projectId }, session.accessToken, runtime, fetchFn, signal),
256
+ callInternal('loadCodeAssist', { metadata }, session.accessToken, runtime, fetchFn, signal),
257
+ ]);
258
+ const windows = [];
259
+ for (const [modelId, model] of Object.entries(models.models ?? {})) {
260
+ const ordinary = usageWindow('other', modelId, model.quotaInfo);
261
+ const weekly = usageWindow('weekly', modelId, model.weeklyQuotaInfo ?? model.weeklyQuota);
262
+ if (ordinary !== undefined)
263
+ windows.push(ordinary);
264
+ if (weekly !== undefined)
265
+ windows.push(weekly);
266
+ }
267
+ const plan = account.paidTier?.name ?? account.paidTier?.id
268
+ ?? account.currentTier?.name ?? account.currentTier?.id ?? session.plan;
269
+ const credits = account.paidTier?.availableCredits?.[0]?.creditAmount;
270
+ const displayPlan = credits === undefined ? plan : `${plan ?? 'Antigravity'} · ${String(credits)} credits`;
271
+ return {
272
+ supported: true,
273
+ windows,
274
+ ...displayPlan === undefined ? {} : { plan: displayPlan },
275
+ };
276
+ }
277
+ /** URL for either v1internal generation transport. */
278
+ export function antigravityGenerateURL(baseURL, stream) {
279
+ return `${antigravityBaseURL(baseURL)}/v1internal:${stream ? 'streamGenerateContent?alt=sse' : 'generateContent'}`;
280
+ }
281
+ /** Forward one already-built payload to generateContent or streamGenerateContent. */
282
+ export async function requestAntigravityContent(session, payload, stream, runtime = {}, fetchFn = proxiedFetch, signal) {
283
+ return fetchFn(antigravityGenerateURL(runtime.baseURL, stream), {
284
+ method: 'POST',
285
+ headers: {
286
+ ...antigravityHeaders(session.accessToken, runtime.userAgent),
287
+ accept: stream ? 'text/event-stream' : 'application/json',
288
+ },
289
+ body: JSON.stringify(payload),
290
+ ...signal === undefined ? {} : { signal },
291
+ });
292
+ }
293
+ /** DSH provider adapter for the `antigravity` route. */
294
+ export class AntigravityAdapter extends LlmAdapter {
295
+ options;
296
+ catalog;
297
+ constructor(options) {
298
+ super();
299
+ this.options = options;
300
+ this.catalog = new ModelCatalogCache(options.catalogStore);
301
+ }
302
+ providerInfo(provider) {
303
+ return { id: provider, name: 'Google Antigravity' };
304
+ }
305
+ staticModels(provider) {
306
+ return this.options.models.map(model => ({
307
+ provider,
308
+ id: model.id,
309
+ name: model.name ?? model.id,
310
+ inputModalities: model.inputModalities ?? ['text', 'image'],
311
+ }));
312
+ }
313
+ fetchCatalog() {
314
+ return this.options.tokens.session().then(session => fetchAntigravityModels(session, this.options.runtime, this.options.fetchFn));
315
+ }
316
+ async listModels(provider) {
317
+ if (await this.options.tokens.peek() === undefined)
318
+ return [];
319
+ if (!this.options.discovery)
320
+ return this.staticModels(provider);
321
+ try {
322
+ const models = await discoverOrRetryAuth(force => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog()));
323
+ return models.map(model => ({
324
+ provider,
325
+ id: model.id,
326
+ name: model.name,
327
+ ...model.description === undefined ? {} : { description: model.description },
328
+ inputModalities: model.inputModalities ?? ['text', 'image'],
329
+ }));
330
+ }
331
+ catch (error) {
332
+ if (isMissingOrInvalidCredential(error))
333
+ return [];
334
+ this.options.onWarn?.(`Antigravity model discovery failed; using the built-in catalog (${errorChain(error)})`);
335
+ return this.staticModels(provider);
336
+ }
337
+ }
338
+ async discovered(model) {
339
+ if (!this.options.discovery)
340
+ return undefined;
341
+ const models = await this.catalog.resolve(() => this.fetchCatalog());
342
+ return models?.find(entry => entry.id === model);
343
+ }
344
+ async resolveModel(provider, model) {
345
+ const discovered = await this.discovered(model);
346
+ const configured = this.options.models.find(entry => entry.id === model);
347
+ return {
348
+ provider,
349
+ id: model,
350
+ name: discovered?.name ?? configured?.name ?? model,
351
+ ...discovered?.description === undefined ? {} : { description: discovered.description },
352
+ inputModalities: discovered?.inputModalities ?? configured?.inputModalities ?? ['text', 'image'],
353
+ context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? ANTIGRAVITY_CONTEXT_WINDOW },
354
+ defaultMaxTokens: configured?.maxTokens ?? ANTIGRAVITY_DEFAULT_MAX_TOKENS,
355
+ };
356
+ }
357
+ async *stream(options) {
358
+ const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
359
+ try {
360
+ let session = await this.options.tokens.session();
361
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), watchdog.signal);
362
+ const payload = toAntigravityRequest(options, messages, session.projectId);
363
+ let response = await requestAntigravityContent(session, payload, true, this.options.runtime, this.options.fetchFn, watchdog.signal);
364
+ if (response.status === 401) {
365
+ this.catalog.invalidate();
366
+ session = await this.options.tokens.session(true);
367
+ response = await requestAntigravityContent(session, payload, true, this.options.runtime, this.options.fetchFn, watchdog.signal);
368
+ }
369
+ if (!response.ok)
370
+ throw await httpLlmError(response, 'Antigravity API');
371
+ if (response.body === null) {
372
+ throw new LlmError('Antigravity API returned no response body', EMPTY_RESPONSE_CODE);
373
+ }
374
+ yield* streamAntigravity(response.body, () => { watchdog.pulse(); });
375
+ }
376
+ catch (error) {
377
+ throw mapFetchFailure('Antigravity API', error, watchdog, options.signal);
378
+ }
379
+ finally {
380
+ watchdog.stop();
381
+ }
382
+ }
383
+ /** Non-stream forwarding seam used by tests and future DSH complete calls. */
384
+ async generate(options) {
385
+ const session = await this.options.tokens.session();
386
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), options.signal);
387
+ const response = await requestAntigravityContent(session, toAntigravityRequest(options, messages, session.projectId), false, this.options.runtime, this.options.fetchFn, options.signal);
388
+ if (!response.ok)
389
+ throw await httpLlmError(response, 'Antigravity API');
390
+ return parseAntigravityResponse(await response.json());
391
+ }
392
+ }
@@ -8,6 +8,7 @@ import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortI
8
8
  import { resolveImages } from '../translate/resolved.js';
9
9
  import { markMessageCache, streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
10
10
  import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
11
+ import { proxiedFetch } from '../http.js';
11
12
  export const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
12
13
  export const CLAUDE_AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
13
14
  export const CLAUDE_TOKEN_URL = 'https://claude.ai/v1/oauth/token';
@@ -79,7 +80,7 @@ export const claudeFlow = {
79
80
  /** Best-effort account profile; login must not fail when this does. */
80
81
  async function fetchClaudeProfile(accessToken) {
81
82
  try {
82
- const response = await fetch(CLAUDE_PROFILE_URL, {
83
+ const response = await proxiedFetch(CLAUDE_PROFILE_URL, {
83
84
  headers: { authorization: `Bearer ${accessToken}` },
84
85
  });
85
86
  if (!response.ok)
@@ -129,7 +130,7 @@ async function claudeSession(tokens, fallbackRefreshToken, withProfile) {
129
130
  * @returns the session to store.
130
131
  */
131
132
  export async function exchangeClaudeCode(code, verifier, redirectUri, state) {
132
- const response = await fetch(CLAUDE_TOKEN_URL, {
133
+ const response = await proxiedFetch(CLAUDE_TOKEN_URL, {
133
134
  method: 'POST',
134
135
  headers: { 'content-type': 'application/json' },
135
136
  body: JSON.stringify({
@@ -151,7 +152,7 @@ export async function exchangeClaudeCode(code, verifier, redirectUri, state) {
151
152
  * @returns the fresh session to store.
152
153
  */
153
154
  export async function refreshClaude(session) {
154
- const response = await fetch(CLAUDE_TOKEN_URL, {
155
+ const response = await proxiedFetch(CLAUDE_TOKEN_URL, {
155
156
  method: 'POST',
156
157
  headers: { 'content-type': 'application/json' },
157
158
  body: JSON.stringify({
@@ -237,7 +238,7 @@ function claudeLimitsWindows(value) {
237
238
  * @param signal - caller cancellation from the RPC transport.
238
239
  * @returns the mapped usage snapshot.
239
240
  */
240
- export async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
241
+ export async function fetchClaudeUsage(session, fetchFn = proxiedFetch, signal) {
241
242
  const response = await fetchFn(CLAUDE_USAGE_URL, {
242
243
  headers: {
243
244
  'authorization': `Bearer ${session.accessToken}`,
@@ -288,7 +289,7 @@ function claudeReasoning(capabilities) {
288
289
  return efforts.length > 0 ? { efforts } : undefined;
289
290
  }
290
291
  /** Fetch the live model catalog from the subscription endpoint. */
291
- export async function fetchClaudeModels(session, fetchFn = fetch) {
292
+ export async function fetchClaudeModels(session, fetchFn = proxiedFetch) {
292
293
  const response = await fetchFn(CLAUDE_MODELS_URL, {
293
294
  headers: {
294
295
  'authorization': `Bearer ${session.accessToken}`,
@@ -494,7 +495,7 @@ export class ClaudeAdapter extends LlmAdapter {
494
495
  ? String(options.reasoningEffort)
495
496
  : undefined;
496
497
  const body = claudeRequestBody(options, messages, maxTokens, thinking, effort);
497
- return fetch(CLAUDE_API_URL, {
498
+ return proxiedFetch(CLAUDE_API_URL, {
498
499
  method: 'POST',
499
500
  headers: {
500
501
  'authorization': `Bearer ${session.accessToken}`,
@@ -9,6 +9,7 @@ import { decodeJwtPayload } from '../auth/jwt.js';
9
9
  import { resolveImages } from '../translate/resolved.js';
10
10
  import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
11
11
  import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
12
+ import { proxiedFetch } from '../http.js';
12
13
  export const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
13
14
  export const CODEX_AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
14
15
  export const CODEX_TOKEN_URL = 'https://auth.openai.com/oauth/token';
@@ -152,7 +153,7 @@ function codexSession(tokens, fallback) {
152
153
  * @returns the session to store.
153
154
  */
154
155
  export async function exchangeCodexCode(code, verifier, redirectUri) {
155
- const response = await fetch(CODEX_TOKEN_URL, {
156
+ const response = await proxiedFetch(CODEX_TOKEN_URL, {
156
157
  method: 'POST',
157
158
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
158
159
  body: new URLSearchParams({
@@ -173,7 +174,7 @@ export async function exchangeCodexCode(code, verifier, redirectUri) {
173
174
  * @returns the fresh session to store.
174
175
  */
175
176
  export async function refreshCodex(session) {
176
- const response = await fetch(CODEX_TOKEN_URL, {
177
+ const response = await proxiedFetch(CODEX_TOKEN_URL, {
177
178
  method: 'POST',
178
179
  headers: { 'content-type': 'application/json' },
179
180
  body: JSON.stringify({
@@ -253,7 +254,7 @@ function codexUsageWindow(value, fallbackKind) {
253
254
  * @param signal - caller cancellation from the RPC transport.
254
255
  * @returns the mapped usage snapshot.
255
256
  */
256
- export async function fetchCodexUsage(session, fetchFn = fetch, signal) {
257
+ export async function fetchCodexUsage(session, fetchFn = proxiedFetch, signal) {
257
258
  const response = await fetchFn(CODEX_USAGE_URL, {
258
259
  headers: {
259
260
  'authorization': `Bearer ${session.accessToken}`,
@@ -309,7 +310,7 @@ function supportsFastTier(entry) {
309
310
  * @param fetchFn - fetch implementation (injectable for tests).
310
311
  * @returns discovered models: hidden entries dropped, sorted by priority.
311
312
  */
312
- export async function fetchCodexModels(session, fetchFn = fetch) {
313
+ export async function fetchCodexModels(session, fetchFn = proxiedFetch) {
313
314
  const url = `${CODEX_MODELS_URL}?client_version=${CODEX_CLIENT_VERSION}`;
314
315
  const response = await fetchFn(url, {
315
316
  headers: {
@@ -573,7 +574,7 @@ export class CodexAdapter extends LlmAdapter {
573
574
  const fast = this.options.speedFor !== undefined
574
575
  && await this.options.speedFor(options.sessionId, options.model);
575
576
  const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
576
- return fetch(CODEX_API_URL, {
577
+ return proxiedFetch(CODEX_API_URL, {
577
578
  method: 'POST',
578
579
  headers: {
579
580
  'authorization': `Bearer ${session.accessToken}`,
@@ -220,7 +220,7 @@ export interface CopilotAdapterOptions {
220
220
  discovery: boolean;
221
221
  /** Warning sink for discovery failures that fall back to the static catalog. */
222
222
  onWarn?: (message: string) => void;
223
- /** Fetch implementation for discovery (defaults to global fetch). */
223
+ /** Fetch implementation for discovery (defaults to the proxy-aware fetch). */
224
224
  fetchFn?: FetchFn;
225
225
  /** Resolve the attachment service per request; absent means image requests fail loudly. */
226
226
  resolveAttachments?: () => AttachmentStore | undefined;