@herbertgao/pi-extensions 2026.9.3 → 2026.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +20 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/pi-antigravity/LICENSE +21 -0
- package/node_modules/pi-antigravity/README.md +194 -0
- package/node_modules/pi-antigravity/package.json +67 -0
- package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
- package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
- package/node_modules/pi-antigravity/src/client/client.ts +561 -0
- package/node_modules/pi-antigravity/src/client/index.ts +1 -0
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/node_modules/pi-antigravity/src/image/image.ts +336 -0
- package/node_modules/pi-antigravity/src/image/index.ts +1 -0
- package/node_modules/pi-antigravity/src/index.ts +280 -0
- package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
- package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
- package/node_modules/pi-antigravity/src/models/index.ts +3 -0
- package/node_modules/pi-antigravity/src/models/models.ts +500 -0
- package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
- package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
- package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
- package/node_modules/pi-antigravity/src/types/index.ts +2 -0
- package/node_modules/pi-antigravity/src/types/types.ts +292 -0
- package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
- package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
- package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
- package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
- package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
- package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
- package/node_modules/pi-antigravity/tsconfig.json +21 -0
- package/package.json +5 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export enum ThinkingEffort {
|
|
2
|
+
Off = "off",
|
|
3
|
+
Minimal = "minimal",
|
|
4
|
+
Low = "low",
|
|
5
|
+
Medium = "medium",
|
|
6
|
+
High = "high",
|
|
7
|
+
Xhigh = "xhigh",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export enum ToolChoice {
|
|
11
|
+
Auto = "auto",
|
|
12
|
+
None = "none",
|
|
13
|
+
Any = "any",
|
|
14
|
+
Required = "required",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export enum GeminiToolCallingMode {
|
|
18
|
+
None = "NONE",
|
|
19
|
+
Any = "ANY",
|
|
20
|
+
Auto = "AUTO",
|
|
21
|
+
Validated = "VALIDATED",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export enum GeminiRole {
|
|
25
|
+
User = "user",
|
|
26
|
+
Model = "model",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export enum AntigravityRequestType {
|
|
30
|
+
Agent = "agent",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export enum AntigravityUserAgent {
|
|
34
|
+
Antigravity = "antigravity",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export enum StopReason {
|
|
38
|
+
Stop = "stop",
|
|
39
|
+
Length = "length",
|
|
40
|
+
ToolUse = "toolUse",
|
|
41
|
+
Error = "error",
|
|
42
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { Server } from "node:http";
|
|
2
|
+
import type {
|
|
3
|
+
OAuthCredentials,
|
|
4
|
+
SimpleStreamOptions,
|
|
5
|
+
TextContent,
|
|
6
|
+
ThinkingContent,
|
|
7
|
+
ToolCall,
|
|
8
|
+
} from "@earendil-works/pi-ai";
|
|
9
|
+
import type {
|
|
10
|
+
AntigravityRequestType,
|
|
11
|
+
AntigravityUserAgent,
|
|
12
|
+
GeminiRole,
|
|
13
|
+
GeminiToolCallingMode,
|
|
14
|
+
ThinkingEffort,
|
|
15
|
+
ToolChoice,
|
|
16
|
+
} from "./enums.js";
|
|
17
|
+
|
|
18
|
+
// OAuth & Auth Types
|
|
19
|
+
export type AntigravityOAuthCredentials = OAuthCredentials & {
|
|
20
|
+
projectId?: string;
|
|
21
|
+
email?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AntigravityApiKey = {
|
|
25
|
+
token: string;
|
|
26
|
+
projectId: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type DynamicModelInfo = {
|
|
30
|
+
id: string;
|
|
31
|
+
experiments?: string[];
|
|
32
|
+
apiProvider?: string;
|
|
33
|
+
modelProvider?: string;
|
|
34
|
+
model?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type CallbackServer = {
|
|
38
|
+
server: Server;
|
|
39
|
+
waitForCode: () => Promise<{ code: string; state: string }>;
|
|
40
|
+
cleanup: () => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Model Types
|
|
44
|
+
export type AntigravityRouting = {
|
|
45
|
+
off?: string;
|
|
46
|
+
routing?: Partial<Record<ThinkingEffort, string>>;
|
|
47
|
+
defaultRequestId?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Stream & API Types
|
|
51
|
+
export const ANTIGRAVITY_API = "antigravity-api" as const;
|
|
52
|
+
export type AntigravityApi = typeof ANTIGRAVITY_API;
|
|
53
|
+
|
|
54
|
+
export type AntigravityStreamOptions = Omit<SimpleStreamOptions, "toolChoice"> & {
|
|
55
|
+
toolChoice?: ToolChoice | `${ToolChoice}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type GeminiTextPart = { text: string; thoughtSignature?: string };
|
|
59
|
+
export type GeminiInlineDataPart = { inlineData: { mimeType: string; data: string } };
|
|
60
|
+
export type GeminiThoughtPart = {
|
|
61
|
+
thought: true;
|
|
62
|
+
text: string;
|
|
63
|
+
thoughtSignature?: string;
|
|
64
|
+
};
|
|
65
|
+
export type GeminiFunctionCallPart = {
|
|
66
|
+
functionCall: {
|
|
67
|
+
name: string;
|
|
68
|
+
args: Record<string, unknown>;
|
|
69
|
+
id?: string;
|
|
70
|
+
};
|
|
71
|
+
thoughtSignature?: string;
|
|
72
|
+
};
|
|
73
|
+
export type GeminiFunctionResponsePart = {
|
|
74
|
+
functionResponse: {
|
|
75
|
+
name: string;
|
|
76
|
+
response: { error: string } | { output: string };
|
|
77
|
+
id?: string;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
export type GeminiPart =
|
|
81
|
+
| GeminiTextPart
|
|
82
|
+
| GeminiInlineDataPart
|
|
83
|
+
| GeminiThoughtPart
|
|
84
|
+
| GeminiFunctionCallPart
|
|
85
|
+
| GeminiFunctionResponsePart;
|
|
86
|
+
|
|
87
|
+
export type GeminiContent = {
|
|
88
|
+
role: GeminiRole;
|
|
89
|
+
parts: GeminiPart[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type GeminiFunctionDeclaration = {
|
|
93
|
+
name: string;
|
|
94
|
+
description: string;
|
|
95
|
+
parameters?: unknown;
|
|
96
|
+
parametersJsonSchema?: unknown;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type GeminiToolConfig = {
|
|
100
|
+
functionCallingConfig: {
|
|
101
|
+
mode: GeminiToolCallingMode;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type ThinkingWire = {
|
|
106
|
+
includeThoughts: boolean;
|
|
107
|
+
thinkingBudget: number;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type GeminiGenerationConfig = {
|
|
111
|
+
temperature?: number;
|
|
112
|
+
maxOutputTokens?: number;
|
|
113
|
+
thinkingConfig?: ThinkingWire;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type GeminiRequestBody = {
|
|
117
|
+
contents: GeminiContent[];
|
|
118
|
+
systemInstruction: {
|
|
119
|
+
role: GeminiRole.User;
|
|
120
|
+
parts: GeminiTextPart[];
|
|
121
|
+
};
|
|
122
|
+
generationConfig?: GeminiGenerationConfig;
|
|
123
|
+
tools?: { functionDeclarations: GeminiFunctionDeclaration[] }[];
|
|
124
|
+
toolConfig?: GeminiToolConfig;
|
|
125
|
+
sessionId?: string;
|
|
126
|
+
labels?: Record<string, string>;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type AntigravityGenerateRequest = {
|
|
130
|
+
project: string;
|
|
131
|
+
model: string;
|
|
132
|
+
request: GeminiRequestBody;
|
|
133
|
+
requestType: AntigravityRequestType.Agent;
|
|
134
|
+
userAgent: AntigravityUserAgent.Antigravity;
|
|
135
|
+
requestId: string;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type StreamPart = {
|
|
139
|
+
text?: string;
|
|
140
|
+
thought?: boolean;
|
|
141
|
+
thoughtSignature?: string;
|
|
142
|
+
functionCall?: {
|
|
143
|
+
id?: string;
|
|
144
|
+
name?: string;
|
|
145
|
+
args?: Record<string, unknown>;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type StreamUsageMetadata = {
|
|
150
|
+
promptTokenCount?: number;
|
|
151
|
+
cachedContentTokenCount?: number;
|
|
152
|
+
candidatesTokenCount?: number;
|
|
153
|
+
thoughtsTokenCount?: number;
|
|
154
|
+
totalTokenCount?: number;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type StreamCandidate = {
|
|
158
|
+
content?: { parts?: StreamPart[] };
|
|
159
|
+
finishReason?: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type StreamResponseData = {
|
|
163
|
+
candidates?: StreamCandidate[];
|
|
164
|
+
usageMetadata?: StreamUsageMetadata;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type StreamChunk = StreamResponseData & {
|
|
168
|
+
error?: { message?: string };
|
|
169
|
+
response?: StreamResponseData;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export type LooseImageBlock = {
|
|
173
|
+
type: "image";
|
|
174
|
+
data?: string;
|
|
175
|
+
mimeType?: string;
|
|
176
|
+
mediaType?: string;
|
|
177
|
+
source?: { data?: string; mediaType?: string };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export type ContentBlock = TextContent | ThinkingContent | ToolCall | LooseImageBlock;
|
|
181
|
+
|
|
182
|
+
export type ActiveTextBlock = TextContent;
|
|
183
|
+
export type ActiveThinkingBlock = ThinkingContent;
|
|
184
|
+
export type ActiveBlock = ActiveTextBlock | ActiveThinkingBlock;
|
|
185
|
+
|
|
186
|
+
// Usage & Quota Types
|
|
187
|
+
export type QuotaBucket = {
|
|
188
|
+
bucketId: string;
|
|
189
|
+
displayName: string;
|
|
190
|
+
window?: string;
|
|
191
|
+
resetTime?: string;
|
|
192
|
+
description?: string;
|
|
193
|
+
remainingFraction: number;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type QuotaGroup = {
|
|
197
|
+
displayName: string;
|
|
198
|
+
description?: string;
|
|
199
|
+
buckets: QuotaBucket[];
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type ModelQuotaRow = {
|
|
203
|
+
modelId: string;
|
|
204
|
+
displayName?: string;
|
|
205
|
+
remainingFraction?: number;
|
|
206
|
+
resetTime?: string;
|
|
207
|
+
modelProvider?: string;
|
|
208
|
+
supportsThinking?: boolean;
|
|
209
|
+
supportsImages?: boolean;
|
|
210
|
+
recommended?: boolean;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export type TierInfo = {
|
|
214
|
+
id?: string;
|
|
215
|
+
name?: string;
|
|
216
|
+
description?: string;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export type AccountUsage = {
|
|
220
|
+
projectId: string;
|
|
221
|
+
endpoint: string;
|
|
222
|
+
email?: string;
|
|
223
|
+
productTier?: TierInfo;
|
|
224
|
+
paidTier?: TierInfo;
|
|
225
|
+
planLabel?: string;
|
|
226
|
+
groups: QuotaGroup[];
|
|
227
|
+
groupDescription?: string;
|
|
228
|
+
/** Set when the (subscription-gated) quota-summary RPC was unavailable, e.g. free-tier SUBSCRIPTION_REQUIRED. */
|
|
229
|
+
quotaSummaryError?: string;
|
|
230
|
+
models: ModelQuotaRow[];
|
|
231
|
+
defaultAgentModelId?: string;
|
|
232
|
+
fetchedAt: number;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export type ApiErrorBody = {
|
|
236
|
+
error?: { message?: string };
|
|
237
|
+
raw?: string;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export type QuotaBucketRaw = {
|
|
241
|
+
bucketId?: unknown;
|
|
242
|
+
displayName?: unknown;
|
|
243
|
+
window?: unknown;
|
|
244
|
+
resetTime?: unknown;
|
|
245
|
+
description?: unknown;
|
|
246
|
+
remainingFraction?: unknown;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export type QuotaGroupRaw = {
|
|
250
|
+
displayName?: unknown;
|
|
251
|
+
description?: unknown;
|
|
252
|
+
buckets?: QuotaBucketRaw[];
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export type QuotaSummaryRaw = {
|
|
256
|
+
description?: unknown;
|
|
257
|
+
groups?: QuotaGroupRaw[];
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export type ModelInfoRaw = {
|
|
261
|
+
isInternal?: unknown;
|
|
262
|
+
displayName?: unknown;
|
|
263
|
+
label?: unknown;
|
|
264
|
+
modelName?: unknown;
|
|
265
|
+
model?: unknown;
|
|
266
|
+
modelProvider?: unknown;
|
|
267
|
+
apiProvider?: unknown;
|
|
268
|
+
supportsThinking?: unknown;
|
|
269
|
+
supportsImages?: unknown;
|
|
270
|
+
recommended?: unknown;
|
|
271
|
+
quotaInfo?: {
|
|
272
|
+
remainingFraction?: unknown;
|
|
273
|
+
resetTime?: unknown;
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export type AvailableModelsRaw = {
|
|
278
|
+
models?: Record<string, ModelInfoRaw>;
|
|
279
|
+
defaultAgentModelId?: unknown;
|
|
280
|
+
defaultAgentModel?: unknown;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export type TierRaw = {
|
|
284
|
+
id?: unknown;
|
|
285
|
+
name?: unknown;
|
|
286
|
+
description?: unknown;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export type LoadCodeAssistRaw = {
|
|
290
|
+
currentTier?: TierRaw;
|
|
291
|
+
paidTier?: TierRaw;
|
|
292
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./usage.js";
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
antigravityHeaders,
|
|
4
|
+
endpointCandidates,
|
|
5
|
+
extractProjectId,
|
|
6
|
+
fetchAvailableModelsCatalog,
|
|
7
|
+
parseApiKey,
|
|
8
|
+
resolveProjectId,
|
|
9
|
+
} from "../client/client.js";
|
|
10
|
+
import {
|
|
11
|
+
setLastEndpoint,
|
|
12
|
+
setLastError,
|
|
13
|
+
setLastProjectId,
|
|
14
|
+
setLastStatus,
|
|
15
|
+
} from "../diagnostics/diagnostics.js";
|
|
16
|
+
import { isRecord } from "../utils/util.js";
|
|
17
|
+
import { safeError } from "../utils/security.js";
|
|
18
|
+
import { antigravityFetch } from "../utils/http.js";
|
|
19
|
+
import type {
|
|
20
|
+
AccountUsage,
|
|
21
|
+
ApiErrorBody,
|
|
22
|
+
AvailableModelsRaw,
|
|
23
|
+
LoadCodeAssistRaw,
|
|
24
|
+
ModelQuotaRow,
|
|
25
|
+
QuotaBucket,
|
|
26
|
+
QuotaGroup,
|
|
27
|
+
QuotaSummaryRaw,
|
|
28
|
+
TierInfo,
|
|
29
|
+
TierRaw,
|
|
30
|
+
} from "../types/types.js";
|
|
31
|
+
|
|
32
|
+
function clampFraction(value: unknown): number | undefined {
|
|
33
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
34
|
+
if (value < 0) return 0;
|
|
35
|
+
if (value > 1) return 1;
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function remainingPercent(remaining?: number): number | undefined {
|
|
40
|
+
if (remaining === undefined) return undefined;
|
|
41
|
+
return Math.round(remaining * 1000) / 10;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function progressBar(remaining?: number, width = 20): string {
|
|
45
|
+
if (remaining === undefined) return `[${"?".repeat(width)}]`;
|
|
46
|
+
const filled = Math.max(0, Math.min(width, Math.round(remaining * width)));
|
|
47
|
+
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatReset(resetTime?: string): string {
|
|
51
|
+
if (!resetTime) return "n/a";
|
|
52
|
+
const ts = Date.parse(resetTime);
|
|
53
|
+
if (!Number.isFinite(ts)) return resetTime;
|
|
54
|
+
const delta = ts - Date.now();
|
|
55
|
+
if (delta <= 0) return "now";
|
|
56
|
+
const totalMin = Math.round(delta / 60000);
|
|
57
|
+
const days = Math.floor(totalMin / (60 * 24));
|
|
58
|
+
const hours = Math.floor((totalMin % (60 * 24)) / 60);
|
|
59
|
+
const mins = totalMin % 60;
|
|
60
|
+
if (days > 0) return `${days}d ${hours}h`;
|
|
61
|
+
if (hours > 0) return `${hours}h ${mins}m`;
|
|
62
|
+
return `${mins}m`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function jsonHeaders(token: string): Record<string, string> {
|
|
66
|
+
return {
|
|
67
|
+
...antigravityHeaders(token),
|
|
68
|
+
Accept: "application/json",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function postJson(
|
|
73
|
+
path: string,
|
|
74
|
+
token: string,
|
|
75
|
+
body: Record<string, unknown>,
|
|
76
|
+
): Promise<{ endpoint: string; status: number; data: unknown }> {
|
|
77
|
+
let lastErrorText = "";
|
|
78
|
+
for (const endpoint of endpointCandidates()) {
|
|
79
|
+
try {
|
|
80
|
+
const res = await antigravityFetch(`${endpoint}${path}`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: jsonHeaders(token),
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
});
|
|
85
|
+
setLastEndpoint(endpoint);
|
|
86
|
+
setLastStatus(res.status);
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
let data: unknown;
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(text) as unknown;
|
|
91
|
+
} catch {
|
|
92
|
+
data = { raw: text } satisfies ApiErrorBody;
|
|
93
|
+
}
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const errorBody = isRecord(data) ? (data as ApiErrorBody) : undefined;
|
|
96
|
+
lastErrorText =
|
|
97
|
+
typeof errorBody?.error?.message === "string" ? errorBody.error.message : text;
|
|
98
|
+
if (![403, 404, 429, 500, 502, 503, 504].includes(res.status)) {
|
|
99
|
+
throw new Error(`${path} failed (${String(res.status)}): ${lastErrorText.slice(0, 300)}`);
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
return { endpoint, status: res.status, data };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
lastErrorText = safeError(error);
|
|
106
|
+
setLastError(lastErrorText);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`${path} failed: ${lastErrorText || "no endpoint available"}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseQuotaSummary(data: unknown): { groups: QuotaGroup[]; description?: string } {
|
|
113
|
+
const summary = (isRecord(data) ? data : {}) as QuotaSummaryRaw;
|
|
114
|
+
const groups: QuotaGroup[] = [];
|
|
115
|
+
for (const group of summary.groups || []) {
|
|
116
|
+
const buckets: QuotaBucket[] = [];
|
|
117
|
+
for (const bucket of group.buckets || []) {
|
|
118
|
+
const remaining = clampFraction(bucket.remainingFraction);
|
|
119
|
+
if (remaining === undefined && !bucket.bucketId) continue;
|
|
120
|
+
buckets.push({
|
|
121
|
+
bucketId: String(bucket.bucketId || bucket.displayName || "unknown"),
|
|
122
|
+
displayName: String(bucket.displayName || bucket.bucketId || "Limit"),
|
|
123
|
+
window: bucket.window ? String(bucket.window) : undefined,
|
|
124
|
+
resetTime: bucket.resetTime ? String(bucket.resetTime) : undefined,
|
|
125
|
+
description: bucket.description ? String(bucket.description) : undefined,
|
|
126
|
+
remainingFraction: remaining ?? 0,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (!buckets.length && !group.displayName) continue;
|
|
130
|
+
groups.push({
|
|
131
|
+
displayName: String(group.displayName || "Quota group"),
|
|
132
|
+
description: group.description ? String(group.description) : undefined,
|
|
133
|
+
buckets,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
groups,
|
|
138
|
+
description: summary.description ? String(summary.description) : undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseModels(data: unknown): {
|
|
143
|
+
models: ModelQuotaRow[];
|
|
144
|
+
defaultAgentModelId?: string;
|
|
145
|
+
} {
|
|
146
|
+
const raw = (isRecord(data) ? data : {}) as AvailableModelsRaw;
|
|
147
|
+
const modelsObj = raw.models && isRecord(raw.models) ? raw.models : {};
|
|
148
|
+
const models: ModelQuotaRow[] = [];
|
|
149
|
+
for (const [modelId, info] of Object.entries(modelsObj)) {
|
|
150
|
+
if (!info || !isRecord(info)) continue;
|
|
151
|
+
if (info.isInternal || String(modelId).startsWith("chat_")) continue;
|
|
152
|
+
const qi = isRecord(info.quotaInfo) ? info.quotaInfo : {};
|
|
153
|
+
models.push({
|
|
154
|
+
modelId,
|
|
155
|
+
displayName:
|
|
156
|
+
typeof info.displayName === "string"
|
|
157
|
+
? info.displayName
|
|
158
|
+
: typeof info.label === "string"
|
|
159
|
+
? info.label
|
|
160
|
+
: typeof info.modelName === "string"
|
|
161
|
+
? info.modelName
|
|
162
|
+
: undefined,
|
|
163
|
+
remainingFraction: clampFraction(qi.remainingFraction),
|
|
164
|
+
resetTime: qi.resetTime ? String(qi.resetTime) : undefined,
|
|
165
|
+
modelProvider:
|
|
166
|
+
typeof info.modelProvider === "string"
|
|
167
|
+
? info.modelProvider
|
|
168
|
+
: typeof info.apiProvider === "string"
|
|
169
|
+
? info.apiProvider
|
|
170
|
+
: undefined,
|
|
171
|
+
supportsThinking: !!info.supportsThinking,
|
|
172
|
+
supportsImages: !!info.supportsImages,
|
|
173
|
+
recommended: !!info.recommended,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
models.sort((a, b) => a.modelId.localeCompare(b.modelId));
|
|
177
|
+
return {
|
|
178
|
+
models,
|
|
179
|
+
defaultAgentModelId:
|
|
180
|
+
raw.defaultAgentModelId || raw.defaultAgentModel
|
|
181
|
+
? String(raw.defaultAgentModelId || raw.defaultAgentModel)
|
|
182
|
+
: undefined,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseTier(value: unknown): TierInfo | undefined {
|
|
187
|
+
if (!isRecord(value)) return undefined;
|
|
188
|
+
const tier = value as TierRaw;
|
|
189
|
+
if (!tier.id && !tier.name) return undefined;
|
|
190
|
+
return {
|
|
191
|
+
id: tier.id ? String(tier.id) : undefined,
|
|
192
|
+
name: tier.name ? String(tier.name) : undefined,
|
|
193
|
+
description: tier.description ? String(tier.description) : undefined,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function loadCodeAssistSafe(token: string) {
|
|
198
|
+
try {
|
|
199
|
+
return await postJson("/v1internal:loadCodeAssist", token, {
|
|
200
|
+
metadata: {
|
|
201
|
+
ideType: "ANTIGRAVITY",
|
|
202
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
203
|
+
pluginType: "GEMINI",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The user-quota-summary RPC is gated behind a paid subscription: free-tier
|
|
213
|
+
* accounts get 403 SUBSCRIPTION_REQUIRED (#3501). It is best-effort diagnostics
|
|
214
|
+
* only — never let it block the rest of the account data (models, tier, project).
|
|
215
|
+
*/
|
|
216
|
+
async function fetchQuotaSummarySafe(token: string): Promise<
|
|
217
|
+
| { ok: true; result: { endpoint: string; status: number; data: unknown } }
|
|
218
|
+
| {
|
|
219
|
+
ok: false;
|
|
220
|
+
error: string;
|
|
221
|
+
}
|
|
222
|
+
> {
|
|
223
|
+
try {
|
|
224
|
+
return { ok: true, result: await postJson("/v1internal:retrieveUserQuotaSummary", token, {}) };
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const msg = safeError(error);
|
|
227
|
+
setLastError(msg);
|
|
228
|
+
return { ok: false, error: msg };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function fetchAccountUsage(apiKeyRaw?: string): Promise<AccountUsage> {
|
|
233
|
+
const creds = parseApiKey(apiKeyRaw);
|
|
234
|
+
const initialProjectId =
|
|
235
|
+
creds.projectId ||
|
|
236
|
+
resolveProjectId({
|
|
237
|
+
token: creds.token,
|
|
238
|
+
credentialProjectId: creds.projectId,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Fetch loadCodeAssist, quota summary, and available models all in parallel
|
|
242
|
+
// to minimize command execution latency.
|
|
243
|
+
const [assistResult, summaryRes, available] = await Promise.all([
|
|
244
|
+
loadCodeAssistSafe(creds.token),
|
|
245
|
+
fetchQuotaSummarySafe(creds.token),
|
|
246
|
+
fetchAvailableModelsCatalog(creds.token, initialProjectId),
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
// Derive project ID from the loadCodeAssist response or stored project ID.
|
|
250
|
+
const discoveredProject = assistResult ? extractProjectId(assistResult.data) : undefined;
|
|
251
|
+
const projectId = resolveProjectId({
|
|
252
|
+
token: creds.token,
|
|
253
|
+
warmedProject: discoveredProject ?? null,
|
|
254
|
+
credentialProjectId: creds.projectId,
|
|
255
|
+
});
|
|
256
|
+
setLastProjectId(projectId);
|
|
257
|
+
|
|
258
|
+
const summary = summaryRes.ok ? summaryRes.result : null;
|
|
259
|
+
const quotaSummaryError = summaryRes.ok ? undefined : summaryRes.error;
|
|
260
|
+
const { groups, description } = summary
|
|
261
|
+
? parseQuotaSummary(summary.data)
|
|
262
|
+
: { groups: [], description: undefined };
|
|
263
|
+
const { models, defaultAgentModelId } = parseModels(available.data);
|
|
264
|
+
|
|
265
|
+
const assistData = (isRecord(assistResult?.data) ? assistResult.data : {}) as LoadCodeAssistRaw;
|
|
266
|
+
const productTier = parseTier(assistData.currentTier);
|
|
267
|
+
const paidTier = parseTier(assistData.paidTier);
|
|
268
|
+
|
|
269
|
+
// Google returns currentTier=free-tier even for Google AI Pro accounts.
|
|
270
|
+
// The real subscription lives in paidTier (e.g. g1-pro-tier / Google AI Pro).
|
|
271
|
+
const planLabel = paidTier?.name
|
|
272
|
+
? `${paidTier.name}${paidTier.id ? ` (${paidTier.id})` : ""}`
|
|
273
|
+
: productTier?.name
|
|
274
|
+
? `${productTier.name}${productTier.id ? ` (${productTier.id})` : ""}`
|
|
275
|
+
: undefined;
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
projectId,
|
|
279
|
+
endpoint: summary?.endpoint ?? available.endpoint ?? assistResult?.endpoint,
|
|
280
|
+
productTier,
|
|
281
|
+
paidTier,
|
|
282
|
+
planLabel,
|
|
283
|
+
groups,
|
|
284
|
+
groupDescription: description,
|
|
285
|
+
quotaSummaryError,
|
|
286
|
+
models,
|
|
287
|
+
defaultAgentModelId,
|
|
288
|
+
fetchedAt: Date.now(),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function quotaErrorNote(msg: string): string {
|
|
293
|
+
if (/SUBSCRIPTION_REQUIRED|#3501|(?:lack|missing).*license/i.test(msg)) {
|
|
294
|
+
return "Aggregate quota summary needs a paid subscription (free-tier can't use that endpoint). Per-model usage is still available via /antigravity.models.";
|
|
295
|
+
}
|
|
296
|
+
return `Aggregate quota summary unavailable: ${msg.slice(0, 160)}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function formatUsageSummary(usage: AccountUsage): string {
|
|
300
|
+
const lines: string[] = [];
|
|
301
|
+
|
|
302
|
+
if (usage.planLabel) lines.push(usage.planLabel);
|
|
303
|
+
|
|
304
|
+
if (!usage.groups.length) {
|
|
305
|
+
if (usage.quotaSummaryError) {
|
|
306
|
+
lines.push(quotaErrorNote(usage.quotaSummaryError));
|
|
307
|
+
} else {
|
|
308
|
+
lines.push("No quota groups returned.");
|
|
309
|
+
}
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const group of usage.groups) {
|
|
314
|
+
if (lines.length) lines.push("");
|
|
315
|
+
lines.push(group.displayName);
|
|
316
|
+
for (const bucket of group.buckets) {
|
|
317
|
+
const rem = remainingPercent(bucket.remainingFraction);
|
|
318
|
+
lines.push(
|
|
319
|
+
` ${progressBar(bucket.remainingFraction)} ${bucket.displayName}: ${rem ?? "?"}% left · resets ${formatReset(bucket.resetTime)}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return lines.join("\n").trimEnd();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function formatModelsList(usage: AccountUsage, opts?: { all?: boolean }): string {
|
|
328
|
+
const lines: string[] = [];
|
|
329
|
+
lines.push("Antigravity available models");
|
|
330
|
+
lines.push(`project=${usage.projectId}`);
|
|
331
|
+
if (usage.defaultAgentModelId) lines.push(`defaultAgentModel=${usage.defaultAgentModelId}`);
|
|
332
|
+
lines.push("");
|
|
333
|
+
|
|
334
|
+
const rows = opts?.all
|
|
335
|
+
? usage.models
|
|
336
|
+
: usage.models.filter((m) => !/tab_|chat_/i.test(m.modelId));
|
|
337
|
+
|
|
338
|
+
if (!rows.length) {
|
|
339
|
+
lines.push("No models returned.");
|
|
340
|
+
return lines.join("\n");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const maxId = Math.max(...rows.map((m) => m.modelId.length), 8);
|
|
344
|
+
for (const m of rows) {
|
|
345
|
+
const rem = remainingPercent(m.remainingFraction);
|
|
346
|
+
const flags = [
|
|
347
|
+
m.recommended ? "recommended" : "",
|
|
348
|
+
m.supportsThinking ? "thinking" : "",
|
|
349
|
+
m.supportsImages ? "images" : "",
|
|
350
|
+
]
|
|
351
|
+
.filter(Boolean)
|
|
352
|
+
.join(",");
|
|
353
|
+
const name = m.displayName && m.displayName !== m.modelId ? ` ${m.displayName}` : "";
|
|
354
|
+
lines.push(
|
|
355
|
+
`${m.modelId.padEnd(maxId)} rem ${rem === undefined ? " ?" : String(rem).padStart(5)}% reset ${formatReset(m.resetTime).padEnd(8)}${flags ? ` [${flags}]` : ""}${name}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
lines.push("");
|
|
359
|
+
lines.push("Note: remaining % is pool-shared (not a private per-model budget).");
|
|
360
|
+
return lines.join("\n");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function resolveApiKeyFromContext(
|
|
364
|
+
ctx: ExtensionCommandContext,
|
|
365
|
+
): Promise<string | undefined> {
|
|
366
|
+
try {
|
|
367
|
+
return await ctx.modelRegistry.getApiKeyForProvider("antigravity");
|
|
368
|
+
} catch {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
}
|