@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,561 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
getCurrentAvailableModels,
|
|
4
|
+
getCurrentEndpoint,
|
|
5
|
+
getCurrentMatchedModelDebug,
|
|
6
|
+
setLastAvailableModels,
|
|
7
|
+
setLastEndpoint,
|
|
8
|
+
setLastError,
|
|
9
|
+
setLastMatchedModelDebug,
|
|
10
|
+
setLastStatus,
|
|
11
|
+
} from "../diagnostics/diagnostics.js";
|
|
12
|
+
import { assertSafeApiBaseUrl, safeError } from "../utils/security.js";
|
|
13
|
+
import type { AntigravityApiKey, AvailableModelsRaw, DynamicModelInfo } from "../types/types.js";
|
|
14
|
+
import { antigravityEnv, asString, escapeRegExp, isRecord } from "../utils/util.js";
|
|
15
|
+
import { antigravityFetch } from "../utils/http.js";
|
|
16
|
+
import { registerDiscoveredModelEnums, registerModelEnum } from "../models/models.js";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
19
|
+
export const ENDPOINT_FALLBACKS = [
|
|
20
|
+
DEFAULT_ENDPOINT,
|
|
21
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
22
|
+
"https://cloudcode-pa.googleapis.com",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const PROJECT_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
26
|
+
const projectCache = new Map<string, { projectId: string | undefined; expiresAt: number }>();
|
|
27
|
+
|
|
28
|
+
const MODEL_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
29
|
+
const modelCache = new Map<string, { result: DynamicModelInfo | undefined; expiresAt: number }>();
|
|
30
|
+
|
|
31
|
+
/** Metadata lookups (project/model discovery) must be fast; a stalled endpoint should
|
|
32
|
+
* fall through to the next candidate instead of hanging the whole request. */
|
|
33
|
+
const DISCOVERY_TIMEOUT_MS = 8000;
|
|
34
|
+
|
|
35
|
+
/** In-flight de-dupe: concurrent requests for the same (token, project, model) share one probe. */
|
|
36
|
+
const inFlightModelLookups = new Map<string, Promise<DynamicModelInfo | undefined>>();
|
|
37
|
+
|
|
38
|
+
/** UUID-shaped stable id from a seed (account email preferred over cwd). */
|
|
39
|
+
export function stableProjectId(seed: string): string {
|
|
40
|
+
const bytes = createHash("sha1").update(`antigravity:${seed}`).digest().subarray(0, 16);
|
|
41
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
42
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
43
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
44
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fallback project id when discovery fails.
|
|
49
|
+
* Prefer ANTIGRAVITY_PROJECT_ID, then a stable seed (email), never process.cwd().
|
|
50
|
+
*/
|
|
51
|
+
export function defaultProjectId(seed = "antigravity-default"): string {
|
|
52
|
+
return antigravityEnv("PROJECT_ID")?.trim() || stableProjectId(seed);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @deprecated Use defaultProjectId(seed); kept for scripts that imported the old constant. */
|
|
56
|
+
export const DEFAULT_PROJECT_ID = defaultProjectId();
|
|
57
|
+
|
|
58
|
+
export function endpointCandidates(): string[] {
|
|
59
|
+
const explicit = antigravityEnv("BASE_URL")?.trim();
|
|
60
|
+
return explicit ? [assertSafeApiBaseUrl(explicit)] : ENDPOINT_FALLBACKS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_USER_AGENT =
|
|
64
|
+
"antigravity/cli/1.1.23 (aidev_client; os_type=linux; arch=amd64; cl=974125021; auth_method=consumer)";
|
|
65
|
+
|
|
66
|
+
/** Default User-Agent matching pure Antigravity CLI wire fingerprint. */
|
|
67
|
+
export function defaultUserAgent(): string {
|
|
68
|
+
return DEFAULT_USER_AGENT;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** HTTP headers for Antigravity API requests matching CLI wire traffic. */
|
|
72
|
+
export function antigravityHeaders(token: string): Record<string, string> {
|
|
73
|
+
return {
|
|
74
|
+
Authorization: `Bearer ${token}`,
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"User-Agent": antigravityEnv("USER_AGENT") || defaultUserAgent(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function jsonOrTextError(text: string): string {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(text) as {
|
|
83
|
+
error?: { message?: string; status?: string; code?: number };
|
|
84
|
+
};
|
|
85
|
+
if (parsed.error?.message) return parsed.error.message;
|
|
86
|
+
} catch {
|
|
87
|
+
// not JSON
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseApiKey(apiKeyRaw: string | undefined): AntigravityApiKey {
|
|
93
|
+
if (!apiKeyRaw) {
|
|
94
|
+
throw new Error("No Antigravity OAuth credentials. Run /login antigravity.");
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(apiKeyRaw) as Partial<AntigravityApiKey>;
|
|
98
|
+
if (!parsed.token || !parsed.projectId) throw new Error("missing token or projectId");
|
|
99
|
+
return { token: parsed.token, projectId: parsed.projectId };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Invalid Antigravity credentials. Run /login antigravity. (${safeError(error)})`,
|
|
103
|
+
{ cause: error },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function extractProjectId(data: unknown): string | undefined {
|
|
109
|
+
if (!isRecord(data)) return undefined;
|
|
110
|
+
const direct =
|
|
111
|
+
data.antigravityProjectId ??
|
|
112
|
+
data.projectId ??
|
|
113
|
+
data.backendProjectId ??
|
|
114
|
+
data.userDefinedCloudaicompanionProject ??
|
|
115
|
+
data.cloudaicompanionProject ??
|
|
116
|
+
data.project;
|
|
117
|
+
const directId = asString(direct);
|
|
118
|
+
if (directId) return directId;
|
|
119
|
+
if (isRecord(direct)) {
|
|
120
|
+
const nestedId = asString(direct.id);
|
|
121
|
+
if (nestedId) return nestedId;
|
|
122
|
+
}
|
|
123
|
+
for (const key of ["projects", "projectIds", "cloudaicompanionProjects"]) {
|
|
124
|
+
const value = data[key];
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
for (const item of value) {
|
|
127
|
+
const nested = extractProjectId(item);
|
|
128
|
+
if (nested) return nested;
|
|
129
|
+
const itemId = asString(item);
|
|
130
|
+
if (itemId) return itemId;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function listCloudAICompanionProjects(token: string): Promise<string | undefined> {
|
|
138
|
+
for (const endpoint of endpointCandidates()) {
|
|
139
|
+
try {
|
|
140
|
+
const res = await antigravityFetch(`${endpoint}/v1internal:listCloudAICompanionProjects`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: antigravityHeaders(token),
|
|
143
|
+
body: JSON.stringify({}),
|
|
144
|
+
signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
|
|
145
|
+
});
|
|
146
|
+
setLastStatus(res.status);
|
|
147
|
+
setLastEndpoint(endpoint);
|
|
148
|
+
if (!res.ok) continue;
|
|
149
|
+
return extractProjectId(await res.json());
|
|
150
|
+
} catch (error) {
|
|
151
|
+
setLastError(safeError(error));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function collectModelLabels(value: unknown, out: string[] = []): string[] {
|
|
158
|
+
if (!value || out.length > 50) return out;
|
|
159
|
+
if (typeof value === "string") {
|
|
160
|
+
if (/gemini|claude|gpt-oss/i.test(value)) out.push(value);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
for (const item of value) collectModelLabels(item, out);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
if (isRecord(value)) {
|
|
168
|
+
for (const key of ["id", "name", "label", "displayName", "model", "modelId"]) {
|
|
169
|
+
collectModelLabels(value[key], out);
|
|
170
|
+
}
|
|
171
|
+
for (const nested of Object.values(value)) {
|
|
172
|
+
if (nested && typeof nested === "object") collectModelLabels(nested, out);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function summarizeModelCandidate(value: unknown): string {
|
|
179
|
+
if (!isRecord(value)) return String(value ?? "none");
|
|
180
|
+
const out: Record<string, unknown> = {};
|
|
181
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
182
|
+
if (/token|auth|credential|secret|email/i.test(key)) continue;
|
|
183
|
+
if (raw === null || ["string", "number", "boolean"].includes(typeof raw)) out[key] = raw;
|
|
184
|
+
else if (Array.isArray(raw)) out[key] = `[array:${String(raw.length)}]`;
|
|
185
|
+
else if (isRecord(raw)) {
|
|
186
|
+
out[key] = `{${Object.keys(raw).slice(0, 12).join(",")}}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return JSON.stringify(out).slice(0, 1200);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Runtime ids look like gemini-*, claude-*, gpt-oss-*, never MODEL_PLACEHOLDER_* enums. */
|
|
193
|
+
export function isUsableRuntimeModelId(id: string): boolean {
|
|
194
|
+
return /^(gemini-|claude-|gpt-oss-)/i.test(id) && !/\s/.test(id) && !/^MODEL_/i.test(id);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildModelMatchRegex(requestedId: string): RegExp {
|
|
198
|
+
const req = requestedId.toLowerCase();
|
|
199
|
+
// Display names from fetchAvailableModels (keys are the real runtime ids):
|
|
200
|
+
// gemini-3.8-flash-low → "Gemini 3.8 Flash (Low)"
|
|
201
|
+
// gemini-3.8-flash-medium → "Gemini 3.8 Flash (Medium)"
|
|
202
|
+
// gemini-3.8-flash-high → "Gemini 3.8 Flash (High)"
|
|
203
|
+
// gemini-3.7-flash-low → "Gemini 3.7 Flash (Low)"
|
|
204
|
+
// gemini-3.7-flash-medium → "Gemini 3.7 Flash (Medium)"
|
|
205
|
+
// gemini-3.7-flash-high → "Gemini 3.7 Flash (High)"
|
|
206
|
+
// gemini-3.6-flash-low → "Gemini 3.6 Flash (Low)"
|
|
207
|
+
// gemini-3.6-flash-medium → "Gemini 3.6 Flash (Medium)"
|
|
208
|
+
// gemini-3.6-flash-high → "Gemini 3.6 Flash (High)"
|
|
209
|
+
// gemini-3.5-flash-extra-low → "Gemini 3.5 Flash (Low)"
|
|
210
|
+
// gemini-3.5-flash-low → "Gemini 3.5 Flash (Medium)"
|
|
211
|
+
// gemini-3-flash-agent → "Gemini 3.5 Flash (High)"
|
|
212
|
+
if (req === "gemini-3.8-flash-low") return /gemini[- ]3\.8[- ]flash \(low\)/i;
|
|
213
|
+
if (req === "gemini-3.8-flash-medium") return /gemini[- ]3\.8[- ]flash \(medium\)/i;
|
|
214
|
+
if (req === "gemini-3.8-flash-high") return /gemini[- ]3\.8[- ]flash \(high\)/i;
|
|
215
|
+
if (req === "gemini-3.7-flash-low") return /gemini[- ]3\.7[- ]flash \(low\)/i;
|
|
216
|
+
if (req === "gemini-3.7-flash-medium") return /gemini[- ]3\.7[- ]flash \(medium\)/i;
|
|
217
|
+
if (req === "gemini-3.7-flash-high") return /gemini[- ]3\.7[- ]flash \(high\)/i;
|
|
218
|
+
if (req === "gemini-3.6-flash-low") return /gemini[- ]3\.6[- ]flash \(low\)/i;
|
|
219
|
+
if (req === "gemini-3.6-flash-medium") return /gemini[- ]3\.6[- ]flash \(medium\)/i;
|
|
220
|
+
if (req === "gemini-3.6-flash-high") return /gemini[- ]3\.6[- ]flash \(high\)/i;
|
|
221
|
+
if (req === "gemini-3.5-flash-extra-low") return /gemini[- ]3\.5[- ]flash \(low\)/i;
|
|
222
|
+
if (req === "gemini-3.5-flash-low" || req === "gemini-3.5-flash-medium")
|
|
223
|
+
return /gemini[- ]3\.5[- ]flash \(medium\)/i;
|
|
224
|
+
if (req === "gemini-3.5-flash-high" || req === "gemini-3-flash-agent")
|
|
225
|
+
return /gemini[- ]3\.5[- ]flash \(high\)/i;
|
|
226
|
+
if (req.includes("claude-opus-4-6")) return /claude.*opus.*4\.6/i;
|
|
227
|
+
if (req.includes("claude-sonnet-4-6")) return /claude.*sonnet.*4\.6/i;
|
|
228
|
+
if (req.includes("gpt-oss-120b")) return /gpt.*oss.*120b/i;
|
|
229
|
+
if (req === "gemini-3.1-pro-low") return /gemini[- ]3\.1[- ]pro \(low\)/i;
|
|
230
|
+
if (req === "gemini-3.1-pro-high" || req === "gemini-pro-agent")
|
|
231
|
+
return /gemini[- ]3\.1[- ]pro \(high\)/i;
|
|
232
|
+
const escaped = escapeRegExp(req).replace(/\\-/g, "[- ]");
|
|
233
|
+
return new RegExp(escaped, "i");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function dynamicModelFromInfo(modelId: string, info: unknown): DynamicModelInfo {
|
|
237
|
+
if (!isRecord(info)) return { id: modelId };
|
|
238
|
+
setLastMatchedModelDebug(summarizeModelCandidate({ modelId, ...info }));
|
|
239
|
+
const experiments = Array.isArray(info.modelExperiments)
|
|
240
|
+
? info.modelExperiments.filter((item): item is string => typeof item === "string")
|
|
241
|
+
: undefined;
|
|
242
|
+
const modelEnum = asString(info.model);
|
|
243
|
+
if (modelEnum) {
|
|
244
|
+
registerModelEnum(modelId, modelEnum);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
id: modelId,
|
|
248
|
+
experiments,
|
|
249
|
+
apiProvider: asString(info.apiProvider),
|
|
250
|
+
modelProvider: asString(info.modelProvider),
|
|
251
|
+
model: modelEnum,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolve a requested runtime model against fetchAvailableModels payload.
|
|
257
|
+
* The real runtime ids are the keys of `data.models`; the nested `model` field is often
|
|
258
|
+
* a MODEL_PLACEHOLDER_* enum that 404s on streamGenerateContent.
|
|
259
|
+
*/
|
|
260
|
+
function findDynamicModel(value: unknown, requestedId: string): DynamicModelInfo | undefined {
|
|
261
|
+
if (!value) return undefined;
|
|
262
|
+
|
|
263
|
+
if (isRecord(value) && isRecord(value.models)) {
|
|
264
|
+
const modelsMap = value.models;
|
|
265
|
+
if (isUsableRuntimeModelId(requestedId) && requestedId in modelsMap) {
|
|
266
|
+
return dynamicModelFromInfo(requestedId, modelsMap[requestedId]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const targetRegex = buildModelMatchRegex(requestedId);
|
|
270
|
+
for (const [modelId, info] of Object.entries(modelsMap)) {
|
|
271
|
+
if (!isUsableRuntimeModelId(modelId)) continue;
|
|
272
|
+
if (targetRegex.test(modelId)) return dynamicModelFromInfo(modelId, info);
|
|
273
|
+
if (isRecord(info)) {
|
|
274
|
+
const label = info.label ?? info.displayName ?? info.name;
|
|
275
|
+
if (typeof label === "string" && targetRegex.test(label)) {
|
|
276
|
+
return dynamicModelFromInfo(modelId, info);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const targetRegex = buildModelMatchRegex(requestedId);
|
|
284
|
+
|
|
285
|
+
if (typeof value === "string") {
|
|
286
|
+
return targetRegex.test(value) && isUsableRuntimeModelId(value) ? { id: value } : undefined;
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
for (const item of value) {
|
|
290
|
+
const found = findDynamicModel(item, requestedId);
|
|
291
|
+
if (found) return found;
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
if (isRecord(value)) {
|
|
296
|
+
for (const nested of Object.values(value)) {
|
|
297
|
+
if (nested && typeof nested === "object") {
|
|
298
|
+
const found = findDynamicModel(nested, requestedId);
|
|
299
|
+
if (found) return found;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function fetchAvailableRuntimeModelUncached(
|
|
307
|
+
token: string,
|
|
308
|
+
projectId: string,
|
|
309
|
+
requestedRuntimeModel: string,
|
|
310
|
+
): Promise<DynamicModelInfo | undefined> {
|
|
311
|
+
const body = JSON.stringify({ project: projectId });
|
|
312
|
+
const endpoints = endpointCandidates();
|
|
313
|
+
let lastLabels = "";
|
|
314
|
+
|
|
315
|
+
// Try endpoints in priority order (production first). If the primary endpoint
|
|
316
|
+
// resolves the model, return immediately without waiting on slower sandbox endpoints.
|
|
317
|
+
for (const endpoint of endpoints) {
|
|
318
|
+
try {
|
|
319
|
+
const res = await antigravityFetch(`${endpoint}/v1internal:fetchAvailableModels`, {
|
|
320
|
+
method: "POST",
|
|
321
|
+
headers: antigravityHeaders(token),
|
|
322
|
+
body,
|
|
323
|
+
signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
|
|
324
|
+
});
|
|
325
|
+
setLastStatus(res.status);
|
|
326
|
+
if (!res.ok) continue;
|
|
327
|
+
setLastEndpoint(endpoint);
|
|
328
|
+
const data: unknown = await res.json();
|
|
329
|
+
if (isRecord(data) && isRecord(data.models)) {
|
|
330
|
+
registerDiscoveredModelEnums(data.models as Record<string, { model?: unknown }>);
|
|
331
|
+
}
|
|
332
|
+
const labels = [...new Set(collectModelLabels(data))].slice(0, 16);
|
|
333
|
+
if (labels.length) lastLabels = labels.join(",");
|
|
334
|
+
const found = findDynamicModel(data, requestedRuntimeModel);
|
|
335
|
+
if (found) {
|
|
336
|
+
if (lastLabels) setLastAvailableModels(lastLabels);
|
|
337
|
+
return found;
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
setLastError(safeError(error));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (lastLabels) setLastAvailableModels(lastLabels);
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function fetchAvailableRuntimeModel(
|
|
349
|
+
token: string,
|
|
350
|
+
projectId: string,
|
|
351
|
+
requestedRuntimeModel: string,
|
|
352
|
+
): Promise<DynamicModelInfo | undefined> {
|
|
353
|
+
const cacheKey = `${token}::${projectId}::${requestedRuntimeModel}`;
|
|
354
|
+
const cached = modelCache.get(cacheKey);
|
|
355
|
+
if (cached && cached.expiresAt > Date.now()) return cached.result;
|
|
356
|
+
|
|
357
|
+
// De-dupe concurrent lookups for the same key (e.g. parallel requests right after
|
|
358
|
+
// startup) so they share one probe instead of each firing their own round-trips.
|
|
359
|
+
const inFlight = inFlightModelLookups.get(cacheKey);
|
|
360
|
+
if (inFlight) return inFlight;
|
|
361
|
+
|
|
362
|
+
const promise = fetchAvailableRuntimeModelUncached(token, projectId, requestedRuntimeModel).then(
|
|
363
|
+
(result) => {
|
|
364
|
+
modelCache.set(cacheKey, { result, expiresAt: Date.now() + MODEL_CACHE_TTL_MS });
|
|
365
|
+
return result;
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
inFlightModelLookups.set(cacheKey, promise);
|
|
369
|
+
try {
|
|
370
|
+
return await promise;
|
|
371
|
+
} finally {
|
|
372
|
+
inFlightModelLookups.delete(cacheKey);
|
|
373
|
+
// Evict expired entries; bound map size to avoid unbounded growth.
|
|
374
|
+
if (modelCache.size > 64) {
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
for (const [key, entry] of modelCache) {
|
|
377
|
+
if (entry.expiresAt <= now) modelCache.delete(key);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function clearModelCache(): void {
|
|
384
|
+
modelCache.clear();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function fetchAvailableModelsFromEndpoint(
|
|
388
|
+
endpoint: string,
|
|
389
|
+
token: string,
|
|
390
|
+
projectId: string,
|
|
391
|
+
signal?: AbortSignal,
|
|
392
|
+
): Promise<{ endpoint: string; status: number; data: unknown } | undefined> {
|
|
393
|
+
try {
|
|
394
|
+
const res = await antigravityFetch(`${endpoint}/v1internal:fetchAvailableModels`, {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers: antigravityHeaders(token),
|
|
397
|
+
body: JSON.stringify({ project: projectId }),
|
|
398
|
+
signal: catalogSignal(signal),
|
|
399
|
+
});
|
|
400
|
+
const text = await res.text();
|
|
401
|
+
let data: unknown;
|
|
402
|
+
try {
|
|
403
|
+
data = JSON.parse(text) as unknown;
|
|
404
|
+
} catch {
|
|
405
|
+
data = { raw: text };
|
|
406
|
+
}
|
|
407
|
+
if (!res.ok) {
|
|
408
|
+
const message =
|
|
409
|
+
isRecord(data) && isRecord(data.error) && typeof data.error.message === "string"
|
|
410
|
+
? data.error.message
|
|
411
|
+
: text;
|
|
412
|
+
setLastError(message);
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
return { endpoint, status: res.status, data };
|
|
416
|
+
} catch (error) {
|
|
417
|
+
setLastError(safeError(error));
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function catalogSignal(signal?: AbortSignal): AbortSignal {
|
|
423
|
+
const timeout = AbortSignal.timeout(DISCOVERY_TIMEOUT_MS);
|
|
424
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Merge catalog payloads from one or more fetchAvailableModels responses. */
|
|
428
|
+
export function mergeAvailableModelsResults(
|
|
429
|
+
results: Array<{ endpoint: string; status: number; data: unknown } | undefined>,
|
|
430
|
+
): { endpoint: string; status: number; data: AvailableModelsRaw } {
|
|
431
|
+
const mergedModels: Record<string, unknown> = {};
|
|
432
|
+
let defaultAgentModelId: string | undefined;
|
|
433
|
+
let defaultAgentModel: string | undefined;
|
|
434
|
+
let lastEndpoint = "";
|
|
435
|
+
let lastStatus = 0;
|
|
436
|
+
|
|
437
|
+
for (const result of results) {
|
|
438
|
+
if (!result) continue;
|
|
439
|
+
setLastEndpoint(result.endpoint);
|
|
440
|
+
setLastStatus(result.status);
|
|
441
|
+
lastEndpoint = result.endpoint;
|
|
442
|
+
lastStatus = result.status;
|
|
443
|
+
const data = result.data;
|
|
444
|
+
if (isRecord(data) && isRecord(data.models)) {
|
|
445
|
+
Object.assign(mergedModels, data.models);
|
|
446
|
+
registerDiscoveredModelEnums(data.models as Record<string, { model?: unknown }>);
|
|
447
|
+
}
|
|
448
|
+
if (isRecord(data) && typeof data.defaultAgentModelId === "string") {
|
|
449
|
+
defaultAgentModelId = data.defaultAgentModelId;
|
|
450
|
+
}
|
|
451
|
+
if (isRecord(data) && typeof data.defaultAgentModel === "string") {
|
|
452
|
+
defaultAgentModel = data.defaultAgentModel;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!lastEndpoint) {
|
|
457
|
+
throw new Error(`/v1internal:fetchAvailableModels failed: no endpoint available`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
endpoint: lastEndpoint,
|
|
462
|
+
status: lastStatus,
|
|
463
|
+
data: {
|
|
464
|
+
models: mergedModels as AvailableModelsRaw["models"],
|
|
465
|
+
defaultAgentModelId,
|
|
466
|
+
defaultAgentModel,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Merge fetchAvailableModels across endpoint candidates so daily/sandbox-only
|
|
473
|
+
* models appear alongside production catalog entries.
|
|
474
|
+
*/
|
|
475
|
+
export async function fetchAvailableModelsCatalog(
|
|
476
|
+
token: string,
|
|
477
|
+
projectId: string,
|
|
478
|
+
signal?: AbortSignal,
|
|
479
|
+
): Promise<{ endpoint: string; status: number; data: AvailableModelsRaw }> {
|
|
480
|
+
const results = await Promise.all(
|
|
481
|
+
endpointCandidates().map((endpoint) =>
|
|
482
|
+
fetchAvailableModelsFromEndpoint(endpoint, token, projectId, signal),
|
|
483
|
+
),
|
|
484
|
+
);
|
|
485
|
+
return mergeAvailableModelsResults(results);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function loadCodeAssistUncached(token: string): Promise<string | undefined> {
|
|
489
|
+
const body = JSON.stringify({
|
|
490
|
+
metadata: {
|
|
491
|
+
ideType: "ANTIGRAVITY",
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
for (const endpoint of endpointCandidates()) {
|
|
496
|
+
try {
|
|
497
|
+
const res = await antigravityFetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: antigravityHeaders(token),
|
|
500
|
+
body,
|
|
501
|
+
signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
|
|
502
|
+
});
|
|
503
|
+
setLastStatus(res.status);
|
|
504
|
+
setLastEndpoint(endpoint);
|
|
505
|
+
if (!res.ok) continue;
|
|
506
|
+
const project = extractProjectId(await res.json());
|
|
507
|
+
if (project) return project;
|
|
508
|
+
return await listCloudAICompanionProjects(token);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
setLastError(safeError(error));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Discover project id with a short in-memory LRU cache keyed by access token. */
|
|
517
|
+
export async function loadCodeAssist(token: string): Promise<string | undefined> {
|
|
518
|
+
const cached = projectCache.get(token);
|
|
519
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
520
|
+
// Refresh LRU order: delete and re-insert so newest is at the end.
|
|
521
|
+
projectCache.delete(token);
|
|
522
|
+
projectCache.set(token, cached);
|
|
523
|
+
return cached.projectId;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const projectId = await loadCodeAssistUncached(token);
|
|
527
|
+
projectCache.set(token, { projectId, expiresAt: Date.now() + PROJECT_CACHE_TTL_MS });
|
|
528
|
+
|
|
529
|
+
// Evict oldest entry when the cache exceeds 32 entries (O(1) — Map preserves insertion order).
|
|
530
|
+
if (projectCache.size > 32) {
|
|
531
|
+
const oldestKey = projectCache.keys().next().value;
|
|
532
|
+
if (oldestKey !== undefined) projectCache.delete(oldestKey);
|
|
533
|
+
}
|
|
534
|
+
return projectId;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function clearProjectCache(): void {
|
|
538
|
+
projectCache.clear();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function resolveProjectId(opts: {
|
|
542
|
+
token: string;
|
|
543
|
+
credentialProjectId?: string;
|
|
544
|
+
email?: string;
|
|
545
|
+
warmedProject?: string | null;
|
|
546
|
+
}): string {
|
|
547
|
+
return (
|
|
548
|
+
antigravityEnv("PROJECT_ID")?.trim() ||
|
|
549
|
+
opts.warmedProject ||
|
|
550
|
+
opts.credentialProjectId ||
|
|
551
|
+
defaultProjectId(opts.email || "antigravity-default")
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Build a diagnostic suffix using the active request bag. */
|
|
556
|
+
export function formatRequestDiagnostics(extra: {
|
|
557
|
+
projectId: string;
|
|
558
|
+
runtimeModel: string;
|
|
559
|
+
}): string {
|
|
560
|
+
return `endpoint=${getCurrentEndpoint() || "unknown"}, project=${extra.projectId}, runtimeModel=${extra.runtimeModel}, matched=${getCurrentMatchedModelDebug() || "none"}, available=${getCurrentAvailableModels() || "unknown"}`;
|
|
561
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { redactSecrets } from "../utils/security.js";
|
|
3
|
+
|
|
4
|
+
export type DiagnosticsSnapshot = {
|
|
5
|
+
status?: number;
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
resolvedRuntimeModel?: string;
|
|
10
|
+
availableModels?: string;
|
|
11
|
+
matchedModelDebug?: string;
|
|
12
|
+
latencyMs?: number;
|
|
13
|
+
maskedEmail?: string;
|
|
14
|
+
tokenExpiry?: string;
|
|
15
|
+
toolSchemaWarnings?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const storage = new AsyncLocalStorage<DiagnosticsSnapshot>();
|
|
19
|
+
|
|
20
|
+
/** Last completed request snapshot for `/antigravity.doctor`. */
|
|
21
|
+
let lastSnapshot: DiagnosticsSnapshot = {};
|
|
22
|
+
|
|
23
|
+
function currentBag(): DiagnosticsSnapshot {
|
|
24
|
+
return storage.getStore() ?? lastSnapshot;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Run work with an isolated diagnostics bag; commits it to `lastSnapshot` when done. */
|
|
28
|
+
export async function runWithDiagnostics<T>(fn: () => Promise<T>): Promise<T> {
|
|
29
|
+
const bag: DiagnosticsSnapshot = {};
|
|
30
|
+
return storage.run(bag, async () => {
|
|
31
|
+
try {
|
|
32
|
+
return await fn();
|
|
33
|
+
} finally {
|
|
34
|
+
lastSnapshot = { ...bag };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getLastDiagnostics(): Readonly<DiagnosticsSnapshot> {
|
|
40
|
+
return lastSnapshot;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read endpoint from the active request bag (or last snapshot outside a request). */
|
|
44
|
+
export function getCurrentEndpoint(): string | undefined {
|
|
45
|
+
return currentBag().endpoint;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getCurrentMatchedModelDebug(): string | undefined {
|
|
49
|
+
return currentBag().matchedModelDebug;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getCurrentAvailableModels(): string | undefined {
|
|
53
|
+
return currentBag().availableModels;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function setLastStatus(status: number | undefined): void {
|
|
57
|
+
currentBag().status = status;
|
|
58
|
+
}
|
|
59
|
+
export function setLastEndpoint(endpoint: string | undefined): void {
|
|
60
|
+
currentBag().endpoint = endpoint;
|
|
61
|
+
}
|
|
62
|
+
export function setLastError(error: string | undefined): void {
|
|
63
|
+
currentBag().error = error === undefined ? undefined : redactSecrets(error).slice(0, 800);
|
|
64
|
+
}
|
|
65
|
+
export function setLastProjectId(projectId: string | undefined): void {
|
|
66
|
+
currentBag().projectId = projectId;
|
|
67
|
+
}
|
|
68
|
+
export function setLastResolvedRuntimeModel(model: string | undefined): void {
|
|
69
|
+
currentBag().resolvedRuntimeModel = model;
|
|
70
|
+
}
|
|
71
|
+
export function setLastAvailableModels(models: string | undefined): void {
|
|
72
|
+
currentBag().availableModels = models;
|
|
73
|
+
}
|
|
74
|
+
export function setLastMatchedModelDebug(debug: string | undefined): void {
|
|
75
|
+
currentBag().matchedModelDebug =
|
|
76
|
+
debug === undefined ? undefined : redactSecrets(debug).slice(0, 1200);
|
|
77
|
+
}
|
|
78
|
+
export function setLastLatencyMs(ms: number | undefined): void {
|
|
79
|
+
currentBag().latencyMs = ms;
|
|
80
|
+
}
|
|
81
|
+
export function setLastMaskedEmail(email: string | undefined): void {
|
|
82
|
+
currentBag().maskedEmail = email;
|
|
83
|
+
}
|
|
84
|
+
export function setLastTokenExpiry(expiry: string | undefined): void {
|
|
85
|
+
currentBag().tokenExpiry = expiry;
|
|
86
|
+
}
|
|
87
|
+
/** Store sanitized tool-schema omissions for the next doctor report. */
|
|
88
|
+
export function setLastToolSchemaWarnings(warnings: string[] | undefined): void {
|
|
89
|
+
currentBag().toolSchemaWarnings =
|
|
90
|
+
warnings === undefined ? undefined : redactSecrets(warnings.join(" | ")).slice(0, 1200);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Test helper: reset last snapshot between cases. */
|
|
94
|
+
export function resetDiagnosticsForTests(): void {
|
|
95
|
+
lastSnapshot = {};
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./diagnostics.js";
|