shariq-pi-extensions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/docs/ARCHITECTURE.md +54 -0
  5. package/docs/DEVELOPMENT.md +88 -0
  6. package/docs/EXTENSIONS.md +89 -0
  7. package/extensions/antigravity-provider/LICENSE +21 -0
  8. package/extensions/antigravity-provider/README.md +23 -0
  9. package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
  10. package/extensions/antigravity-provider/antigravity/index.ts +67 -0
  11. package/extensions/antigravity-provider/antigravity/models.ts +184 -0
  12. package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
  13. package/extensions/antigravity-provider/index.ts +3 -0
  14. package/extensions/ask-user/README.md +21 -0
  15. package/extensions/ask-user/index.ts +417 -0
  16. package/extensions/background-terminals/README.md +43 -0
  17. package/extensions/background-terminals/index.ts +400 -0
  18. package/extensions/background-terminals/src/manager.ts +480 -0
  19. package/extensions/background-terminals/src/output-buffer.ts +110 -0
  20. package/extensions/background-terminals/src/presentation.ts +74 -0
  21. package/extensions/background-terminals/src/types.ts +47 -0
  22. package/extensions/background-terminals/src/ui.ts +478 -0
  23. package/extensions/context-usage/LICENSE +21 -0
  24. package/extensions/context-usage/README.md +20 -0
  25. package/extensions/context-usage/index.ts +2006 -0
  26. package/extensions/context-usage/lib/ansi.ts +60 -0
  27. package/extensions/context-usage/lib/boundary.ts +27 -0
  28. package/extensions/context-usage/lib/chat.ts +190 -0
  29. package/extensions/context-usage/lib/config.ts +30 -0
  30. package/extensions/context-usage/lib/fmt.ts +59 -0
  31. package/extensions/context-usage/lib/style.ts +260 -0
  32. package/extensions/copy-all/README.md +15 -0
  33. package/extensions/copy-all/index.ts +72 -0
  34. package/extensions/factory-provider/API_KEYS.md +17 -0
  35. package/extensions/factory-provider/AUTH.md +15 -0
  36. package/extensions/factory-provider/README.md +90 -0
  37. package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
  38. package/extensions/factory-provider/UPDATING.md +97 -0
  39. package/extensions/factory-provider/factory/api-keys.ts +407 -0
  40. package/extensions/factory-provider/factory/auth.ts +132 -0
  41. package/extensions/factory-provider/factory/constants.ts +15 -0
  42. package/extensions/factory-provider/factory/dashboard.ts +363 -0
  43. package/extensions/factory-provider/factory/droid.ts +84 -0
  44. package/extensions/factory-provider/factory/limits.ts +342 -0
  45. package/extensions/factory-provider/factory/models.ts +283 -0
  46. package/extensions/factory-provider/factory/responses.ts +331 -0
  47. package/extensions/factory-provider/factory/warm-theme.ts +53 -0
  48. package/extensions/factory-provider/factory/websocket.ts +130 -0
  49. package/extensions/factory-provider/index.ts +204 -0
  50. package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
  51. package/extensions/firecrawl-web/README.md +31 -0
  52. package/extensions/firecrawl-web/auth.ts +92 -0
  53. package/extensions/firecrawl-web/client.ts +238 -0
  54. package/extensions/firecrawl-web/index.ts +96 -0
  55. package/extensions/firecrawl-web/output.ts +125 -0
  56. package/extensions/git-info/README.md +25 -0
  57. package/extensions/git-info/index.ts +162 -0
  58. package/extensions/git-info/src/git.ts +206 -0
  59. package/extensions/git-info/src/ui.ts +291 -0
  60. package/extensions/goal/README.md +44 -0
  61. package/extensions/goal/constants.ts +4 -0
  62. package/extensions/goal/extension.ts +1064 -0
  63. package/extensions/goal/index.ts +2 -0
  64. package/extensions/goal/types.ts +56 -0
  65. package/extensions/goal/ui.ts +334 -0
  66. package/extensions/orchestration/README.md +58 -0
  67. package/extensions/orchestration/dashboard.ts +192 -0
  68. package/extensions/orchestration/engine.ts +706 -0
  69. package/extensions/orchestration/index.ts +243 -0
  70. package/extensions/orchestration/prompts.ts +124 -0
  71. package/extensions/orchestration/protocol.ts +116 -0
  72. package/extensions/orchestration/settings-ui.ts +41 -0
  73. package/extensions/orchestration/settings.ts +59 -0
  74. package/extensions/orchestration/storage.ts +49 -0
  75. package/extensions/orchestration/types.ts +105 -0
  76. package/extensions/performance-status/README.md +5 -0
  77. package/extensions/performance-status/index.ts +171 -0
  78. package/extensions/pi-memory/README.md +53 -0
  79. package/extensions/pi-memory/index.ts +326 -0
  80. package/extensions/pi-memory/src/config.ts +75 -0
  81. package/extensions/pi-memory/src/database.ts +617 -0
  82. package/extensions/pi-memory/src/extraction.ts +178 -0
  83. package/extensions/pi-memory/src/project.ts +80 -0
  84. package/extensions/pi-memory/src/projection.ts +59 -0
  85. package/extensions/pi-memory/src/redaction.ts +60 -0
  86. package/extensions/pi-memory/src/retrieval.ts +30 -0
  87. package/extensions/pi-memory/src/service.ts +194 -0
  88. package/extensions/pi-memory/src/session.ts +146 -0
  89. package/extensions/pi-memory/src/types.ts +82 -0
  90. package/extensions/shared/README.md +18 -0
  91. package/extensions/shared/activity-status.ts +31 -0
  92. package/extensions/shared/child-session.ts +150 -0
  93. package/extensions/shared/context-utilization.ts +47 -0
  94. package/extensions/shared/dashboard-state.ts +99 -0
  95. package/extensions/shared/tool-call-timeout.ts +104 -0
  96. package/extensions/shared/tui-dashboard.ts +139 -0
  97. package/extensions/shell-shortcuts/README.md +17 -0
  98. package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
  99. package/extensions/shell-shortcuts/index.ts +6 -0
  100. package/extensions/subagents/README.md +89 -0
  101. package/extensions/subagents/index.ts +1386 -0
  102. package/extensions/subagents/src/backend.ts +69 -0
  103. package/extensions/subagents/src/backends/pi.ts +781 -0
  104. package/extensions/subagents/src/catalog.ts +85 -0
  105. package/extensions/subagents/src/config.ts +241 -0
  106. package/extensions/subagents/src/context.ts +57 -0
  107. package/extensions/subagents/src/coordinator.ts +64 -0
  108. package/extensions/subagents/src/domain.ts +294 -0
  109. package/extensions/subagents/src/format.ts +74 -0
  110. package/extensions/subagents/src/manager.ts +813 -0
  111. package/extensions/subagents/src/prompt.ts +94 -0
  112. package/extensions/subagents/src/result-delivery.ts +20 -0
  113. package/extensions/subagents/src/runtime.ts +48 -0
  114. package/extensions/subagents/src/ui/takeover.ts +695 -0
  115. package/extensions/subagents/src/ui/transcript.ts +182 -0
  116. package/extensions/subagents/src/worktree.ts +175 -0
  117. package/extensions/web-fetch/README.md +13 -0
  118. package/extensions/web-fetch/index.ts +441 -0
  119. package/package.json +117 -0
  120. package/skills/background-terminals/SKILL.md +42 -0
  121. package/skills/orchestration/SKILL.md +33 -0
  122. package/skills/subagents/SKILL.md +89 -0
  123. package/themes/ember-warm-dark.json +85 -0
@@ -0,0 +1,519 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createAssistantMessageEventStream, type AssistantMessageEventStream } from "@earendil-works/pi-ai";
3
+ import {
4
+ lastEndpoint,
5
+ lastMatchedModelDebug,
6
+ lastAvailableModels,
7
+ setLastStatus,
8
+ setLastEndpoint,
9
+ setLastError,
10
+ setLastProjectId,
11
+ setLastResolvedRuntimeModel,
12
+ setLastAvailableModels,
13
+ endpointCandidates,
14
+ safeError,
15
+ sanitizeText,
16
+ parseApiKey,
17
+ antigravityEnv,
18
+ antigravityHeaders,
19
+ jsonOrTextError,
20
+ loadCodeAssist,
21
+ fetchAvailableRuntimeModel,
22
+ DEFAULT_PROJECT_ID,
23
+ } from "./oauth.ts";
24
+ import { getAntigravityRequestModelId, PROVIDER_ID } from "./models.ts";
25
+
26
+ const ANTIGRAVITY_SYSTEM_INSTRUCTION =
27
+ "You are Antigravity, a powerful agentic AI coding assistant designed by Google DeepMind. " +
28
+ "You are pair programming with a user to solve coding tasks. Be concise, practical, and tool-aware.";
29
+
30
+ const ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION =
31
+ 'CRITICAL: NEVER output rule checks, formatting guidelines, constraint checklists (e.g. "No emdashes"), or your thinking/personality preambles in the final response. Output only the final response.';
32
+
33
+ let _toolCallCounter = 0;
34
+
35
+ function sanitizeToolCallId(id: string, fallbackName?: string): string {
36
+ const cleaned = id.replace(/[^a-zA-Z0-9_-]/g, "_");
37
+ // Cap ID length to 64 characters (matching yofriadi / API limitations)
38
+ const capped = cleaned.slice(0, 64);
39
+ return capped || `${fallbackName || "tool"}_${Date.now()}_${++_toolCallCounter}`;
40
+ }
41
+
42
+ function toolCallIdNeeded(modelId: string, runtimeModel: string): boolean {
43
+ return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-") ||
44
+ runtimeModel.startsWith("claude-") || runtimeModel.startsWith("gpt-oss-");
45
+ }
46
+
47
+ function asTextParts(content: unknown): any[] {
48
+ if (typeof content === "string") return [{ text: sanitizeText(content) }];
49
+ if (!Array.isArray(content)) return [];
50
+ return content.flatMap<any>((item) => {
51
+ if (!item || typeof item !== "object") return [];
52
+ const block = item as any;
53
+ if (block.type === "text") return [{ text: sanitizeText(block.text) }];
54
+ if (block.type === "image") {
55
+ const data = block.data || block.source?.data;
56
+ const mimeType = block.mimeType || block.mediaType || block.source?.mediaType || "image/png";
57
+ return data ? [{ inlineData: { mimeType, data } }] : [];
58
+ }
59
+ return [];
60
+ });
61
+ }
62
+
63
+ function convertMessages(model: any, context: any, runtimeModel: string): any[] {
64
+ const contents: any[] = [];
65
+ const messages = Array.isArray(context.messages) ? context.messages : [];
66
+ for (const msg of messages) {
67
+ if (msg.role === "user") {
68
+ const parts = asTextParts(msg.content);
69
+ if (parts.length) contents.push({ role: "user", parts });
70
+ } else if (msg.role === "assistant") {
71
+ const parts: any[] = [];
72
+ for (const block of msg.content || []) {
73
+ if (block.type === "text" && String(block.text || "").trim()) parts.push({ text: sanitizeText(block.text) });
74
+ else if (block.type === "thinking" && String(block.thinking || "").trim()) {
75
+ if (msg.provider === PROVIDER_ID && msg.model === model.id) parts.push({ thought: true, text: sanitizeText(block.thinking), ...(block.thinkingSignature ? { thoughtSignature: block.thinkingSignature } : {}) });
76
+ else parts.push({ text: sanitizeText(block.thinking) });
77
+ } else if (block.type === "toolCall") {
78
+ parts.push({
79
+ functionCall: {
80
+ name: block.name,
81
+ args: block.arguments ?? {},
82
+ ...(toolCallIdNeeded(model.id, runtimeModel) ? { id: sanitizeToolCallId(block.id || "", block.name) } : {}),
83
+ },
84
+ ...(block.thoughtSignature ? { thoughtSignature: block.thoughtSignature } : {}),
85
+ });
86
+ }
87
+ }
88
+ if (parts.length) contents.push({ role: "model", parts });
89
+ } else if (msg.role === "toolResult") {
90
+ const content = Array.isArray(msg.content) ? msg.content : [];
91
+ const text = content.filter((c: any) => c.type === "text").map((c: any) => sanitizeText(c.text)).join("\n");
92
+ const responseText = text || (msg.isError ? "Tool failed" : "");
93
+ const part = {
94
+ functionResponse: {
95
+ name: msg.toolName,
96
+ response: msg.isError ? { error: responseText } : { output: responseText },
97
+ ...(toolCallIdNeeded(model.id, runtimeModel) ? { id: sanitizeToolCallId(msg.toolCallId || "", msg.toolName) } : {}),
98
+ },
99
+ };
100
+ const last = contents[contents.length - 1];
101
+ if (last?.role === "user" && last.parts?.some((p: any) => p.functionResponse)) last.parts.push(part);
102
+ else contents.push({ role: "user", parts: [part] });
103
+ }
104
+ }
105
+ return contents;
106
+ }
107
+
108
+ function stripMetaSchema(schema: unknown): unknown {
109
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return schema;
110
+ const omit = new Set(["$schema", "$id", "$anchor", "$dynamicAnchor", "$vocabulary", "$comment", "$defs", "definitions"]);
111
+ const out: Record<string, unknown> = {};
112
+ for (const [key, value] of Object.entries(schema)) {
113
+ if (!omit.has(key)) out[key] = stripMetaSchema(value);
114
+ }
115
+ return out;
116
+ }
117
+
118
+ function normalizeGoogleSchema(schema: unknown): unknown {
119
+ if (!schema || typeof schema !== "object") return schema;
120
+ if (Array.isArray(schema)) return schema.map(normalizeGoogleSchema);
121
+ const out: Record<string, unknown> = {};
122
+ for (const [key, value] of Object.entries(schema)) {
123
+ if (key === "type" && typeof value === "string") out[key] = value.toUpperCase();
124
+ else out[key] = normalizeGoogleSchema(value);
125
+ }
126
+ return out;
127
+ }
128
+
129
+ function convertTools(tools: any[] | undefined, useLegacyParameters = false): any[] | undefined {
130
+ if (!tools?.length) return undefined;
131
+ return [
132
+ {
133
+ functionDeclarations: tools.map((tool) => {
134
+ const parameters = stripMetaSchema(tool.parameters);
135
+ return {
136
+ name: tool.name,
137
+ description: tool.description,
138
+ ...(useLegacyParameters
139
+ ? { parameters: normalizeGoogleSchema(parameters) }
140
+ : { parametersJsonSchema: parameters }),
141
+ };
142
+ }),
143
+ },
144
+ ];
145
+ }
146
+
147
+ type AgyWireMetadata = { modelEnum: string; thinkingBudget?: number };
148
+
149
+ const AGY_WIRE_METADATA: Record<string, AgyWireMetadata> = {
150
+ "gemini-3.5-flash-extra-low": { modelEnum: "MODEL_PLACEHOLDER_M187", thinkingBudget: 1_000 },
151
+ "gemini-3.5-flash-low": { modelEnum: "MODEL_PLACEHOLDER_M20", thinkingBudget: 4_000 },
152
+ "gemini-3-flash-agent": { modelEnum: "MODEL_PLACEHOLDER_M84", thinkingBudget: 10_000 },
153
+ "gemini-3.6-flash-low": { modelEnum: "MODEL_PLACEHOLDER_M73", thinkingBudget: 1_000 },
154
+ "gemini-3.6-flash-medium": { modelEnum: "MODEL_PLACEHOLDER_M72", thinkingBudget: 4_000 },
155
+ "gemini-3.6-flash-high": { modelEnum: "MODEL_PLACEHOLDER_M71", thinkingBudget: 10_000 },
156
+ "gemini-3.7-flash-low": { modelEnum: "MODEL_PLACEHOLDER_M300", thinkingBudget: 1_000 },
157
+ "gemini-3.7-flash-medium": { modelEnum: "MODEL_PLACEHOLDER_M299", thinkingBudget: 4_000 },
158
+ "gemini-3.7-flash-high": { modelEnum: "MODEL_PLACEHOLDER_M298", thinkingBudget: -1 },
159
+ "gemini-3.1-pro-low": { modelEnum: "MODEL_PLACEHOLDER_M36", thinkingBudget: 1_001 },
160
+ "gemini-pro-agent": { modelEnum: "MODEL_PLACEHOLDER_M16", thinkingBudget: 10_001 },
161
+ "claude-sonnet-4-6": { modelEnum: "MODEL_PLACEHOLDER_M35", thinkingBudget: 1_024 },
162
+ "claude-opus-4-6-thinking": { modelEnum: "MODEL_PLACEHOLDER_M26", thinkingBudget: 1_024 },
163
+ "gpt-oss-120b-medium": { modelEnum: "MODEL_OPENAI_GPT_OSS_120B_MEDIUM", thinkingBudget: 8_192 },
164
+ };
165
+
166
+ const agyConversationId = randomUUID();
167
+ const agyTrajectoryId = randomUUID();
168
+ let agyRequestSequence = 0;
169
+
170
+ function fnv1a64Signed(input: string): string {
171
+ let hash = 0xcbf29ce484222325n;
172
+ for (const byte of Buffer.from(input, "utf8")) {
173
+ hash ^= BigInt(byte);
174
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
175
+ }
176
+ return BigInt.asIntN(64, hash).toString();
177
+ }
178
+
179
+ function countAgyRequestSteps(contents: unknown): number {
180
+ if (!Array.isArray(contents)) return 1;
181
+ let functionResponses = 0;
182
+ for (const content of contents) {
183
+ if (!content || typeof content !== "object") continue;
184
+ const parts = (content as { parts?: unknown }).parts;
185
+ if (!Array.isArray(parts)) continue;
186
+ functionResponses += parts.filter((part) => part && typeof part === "object" && "functionResponse" in part).length;
187
+ }
188
+ return Math.max(1, contents.length + functionResponses);
189
+ }
190
+
191
+ function applyAgyRequestMetadata(request: any, runtimeModel: string) {
192
+ const timestamp = Date.now();
193
+ const lastStepIndex = countAgyRequestSteps(request.contents);
194
+ const isClaude = runtimeModel.startsWith("claude-");
195
+ const isNonGemini = isClaude || runtimeModel.startsWith("gpt-");
196
+ const wire = AGY_WIRE_METADATA[runtimeModel];
197
+ request.labels = {
198
+ last_step_index: String(lastStepIndex),
199
+ ...(wire ? { model_enum: wire.modelEnum } : {}),
200
+ trajectory_id: agyTrajectoryId,
201
+ used_claude: String(isClaude),
202
+ used_claude_conservative: String(isClaude),
203
+ used_non_gemini_model: String(isNonGemini),
204
+ };
205
+ request.sessionId = fnv1a64Signed(process.cwd());
206
+ return {
207
+ requestId: `agent/${agyConversationId}/${timestamp}/${agyTrajectoryId}/${lastStepIndex + 1 + agyRequestSequence++}`,
208
+ thinkingBudget: wire?.thinkingBudget,
209
+ };
210
+ }
211
+
212
+ function buildRequest(model: any, context: any, projectId: string, options: any, runtimeModel: string): any {
213
+ const request: any = {
214
+ contents: convertMessages(model, context, runtimeModel),
215
+ systemInstruction: {
216
+ role: "user",
217
+ parts: [
218
+ { text: ANTIGRAVITY_SYSTEM_INSTRUCTION },
219
+ { text: `Please ignore following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` },
220
+ { text: ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION },
221
+ ...(context.systemPrompt ? [{ text: sanitizeText(context.systemPrompt) }] : [])
222
+ ],
223
+ },
224
+ };
225
+ const generationConfig: any = {};
226
+ if (options?.temperature !== undefined) generationConfig.temperature = options.temperature;
227
+ if (options?.maxTokens !== undefined) generationConfig.maxOutputTokens = options.maxTokens;
228
+ else generationConfig.maxOutputTokens = Math.min(8192, model.maxTokens || 8192);
229
+
230
+ const metadata = applyAgyRequestMetadata(request, runtimeModel);
231
+ if (metadata.thinkingBudget !== undefined) {
232
+ generationConfig.thinkingConfig = {
233
+ includeThoughts: true,
234
+ thinkingBudget: metadata.thinkingBudget,
235
+ };
236
+ }
237
+
238
+ if (Object.keys(generationConfig).length) request.generationConfig = generationConfig;
239
+ const tools = convertTools(context.tools, model.id.startsWith("claude-"));
240
+ if (tools) {
241
+ request.tools = tools;
242
+ if (model.id.startsWith("claude-")) {
243
+ request.toolConfig = options?.toolChoice
244
+ ? {
245
+ functionCallingConfig: {
246
+ mode: options.toolChoice === "none" ? "NONE" : options.toolChoice === "any" ? "ANY" : "AUTO",
247
+ },
248
+ }
249
+ : {
250
+ functionCallingConfig: {
251
+ mode: "VALIDATED",
252
+ },
253
+ };
254
+ } else if (options?.toolChoice) {
255
+ request.toolConfig = {
256
+ functionCallingConfig: {
257
+ mode: options.toolChoice === "none" ? "NONE" : options.toolChoice === "any" ? "ANY" : "AUTO",
258
+ },
259
+ };
260
+ }
261
+ }
262
+ const payload: any = { project: projectId, model: runtimeModel, request, requestType: "agent", userAgent: "antigravity", requestId: metadata.requestId };
263
+
264
+ return payload;
265
+ }
266
+
267
+ function mapStopReason(reason: string | undefined): "stop" | "length" | "toolUse" | "error" {
268
+ if (reason === "STOP") return "stop";
269
+ if (reason === "MAX_TOKENS") return "length";
270
+ return reason ? "error" : "stop";
271
+ }
272
+
273
+ function friendlyAntigravityError(status: number | undefined, text: string): string {
274
+ const msg = jsonOrTextError(text);
275
+ if (status === 400) {
276
+ if (/API key not valid|API_KEY_INVALID/i.test(msg)) return "Antigravity login expired or credentials are invalid. Next: run /login antigravity, then retry.";
277
+ if (/Invalid JSON payload|Unknown name/i.test(msg)) return "Antigravity request format was rejected by the backend. Next: switch to a simpler model or retry after updating the extension.";
278
+ if (/Request contains an invalid argument/i.test(msg)) return "Antigravity rejected this request. Next: retry once; if it keeps failing, switch models or re-login.";
279
+ return `Bad request from Antigravity. Next: retry once, then run /login antigravity if it keeps failing. Backend said: ${msg}`;
280
+ }
281
+ if (status === 401) {
282
+ return "Antigravity authentication failed. Next: run /login antigravity, then retry.";
283
+ }
284
+ if (status === 403) {
285
+ if (/permission|forbidden|access/i.test(msg)) return "Antigravity access was denied for this account or project. Next: try another model, re-login, or use an account with access.";
286
+ return `Antigravity denied this request. Next: re-login or try another model. Backend said: ${msg}`;
287
+ }
288
+ if (status === 404) {
289
+ if (/Requested entity was not found/i.test(msg)) return "This model is not available for the current Antigravity account or project. Next: switch to gemini-3.6-flash, gemini-3.5-flash, or another model listed by /model.";
290
+ return `Antigravity could not find the requested resource. Next: retry or switch models. Backend said: ${msg}`;
291
+ }
292
+ if (status === 408) {
293
+ return "Antigravity timed out. Next: retry the same request.";
294
+ }
295
+ if (status === 409) {
296
+ return "Antigravity reported a conflict for this request. Next: retry once or start a new chat session.";
297
+ }
298
+ if (status === 429) {
299
+ const wait = msg.match(/Resets? in ([^.\n]+)/i)?.[1]?.trim();
300
+ if (/Individual quota reached/i.test(msg)) return `Quota reached. Please wait ${wait || "for reset"}. Next: switch models or try again after reset.`;
301
+ if (/quota/i.test(msg)) return `Quota reached.${wait ? ` Please wait ${wait}.` : ""} Next: switch models or retry later.`;
302
+ return `Rate limited by Antigravity. Next: wait a bit and retry.${wait ? ` Reset: ${wait}.` : ""}`;
303
+ }
304
+ if (status === 500) {
305
+ return "Antigravity had an internal server error. Next: retry in a moment or switch models.";
306
+ }
307
+ if (status === 502) {
308
+ return "Antigravity returned a bad gateway error. Next: retry in a moment.";
309
+ }
310
+ if (status === 503) {
311
+ if (/No capacity available/i.test(msg)) return "This model has no capacity right now. Next: retry later or switch to another model.";
312
+ return "Antigravity is temporarily unavailable. Next: retry in a moment or switch models.";
313
+ }
314
+ if (status === 504) {
315
+ return "Antigravity timed out upstream. Next: retry in a moment.";
316
+ }
317
+ return msg;
318
+ }
319
+
320
+ function createOutput(model: any): any {
321
+ return {
322
+ role: "assistant",
323
+ content: [],
324
+ api: "antigravity-api",
325
+ provider: PROVIDER_ID,
326
+ model: model.id,
327
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
328
+ stopReason: "stop",
329
+ timestamp: Date.now(),
330
+ };
331
+ }
332
+
333
+ async function streamResponse(response: Response, stream: AssistantMessageEventStream, output: any): Promise<boolean> {
334
+ if (!response.body) throw new Error("No response body");
335
+ const reader = response.body.getReader();
336
+ const decoder = new TextDecoder();
337
+ let buffer = "";
338
+ let started = false;
339
+ let currentBlock: any = null;
340
+ let hasContent = false;
341
+ const blocks = output.content;
342
+ const blockIndex = () => blocks.length - 1;
343
+ const ensureStarted = () => {
344
+ if (!started) {
345
+ stream.push({ type: "start", partial: output });
346
+ started = true;
347
+ }
348
+ };
349
+ const finishCurrent = () => {
350
+ if (!currentBlock) return;
351
+ if (currentBlock.type === "text") stream.push({ type: "text_end", contentIndex: blockIndex(), content: currentBlock.text, partial: output });
352
+ else if (currentBlock.type === "thinking") stream.push({ type: "thinking_end", contentIndex: blockIndex(), content: currentBlock.thinking, partial: output });
353
+ currentBlock = null;
354
+ };
355
+
356
+ while (true) {
357
+ const { done, value } = await reader.read();
358
+ if (done) break;
359
+ buffer += decoder.decode(value, { stream: true });
360
+ const lines = buffer.split("\n");
361
+ buffer = lines.pop() || "";
362
+ for (const line of lines) {
363
+ if (!line.startsWith("data:")) continue;
364
+ const json = line.slice(5).trim();
365
+ if (!json || json === "[DONE]") continue;
366
+ let chunk: any;
367
+ try {
368
+ chunk = JSON.parse(json);
369
+ } catch {
370
+ continue;
371
+ }
372
+ if (chunk.error) throw new Error(chunk.error.message || JSON.stringify(chunk.error));
373
+ const responseData = chunk.response || chunk;
374
+ const candidate = responseData.candidates?.[0];
375
+ for (const part of candidate?.content?.parts || []) {
376
+ if (part.text !== undefined) {
377
+ hasContent = true;
378
+ const isThinking = part.thought === true;
379
+ const type = isThinking ? "thinking" : "text";
380
+ if (!currentBlock || currentBlock.type !== type) {
381
+ finishCurrent();
382
+ currentBlock = isThinking ? { type: "thinking", thinking: "", thinkingSignature: undefined } : { type: "text", text: "" };
383
+ blocks.push(currentBlock);
384
+ ensureStarted();
385
+ stream.push({ type: isThinking ? "thinking_start" : "text_start", contentIndex: blockIndex(), partial: output });
386
+ }
387
+ if (isThinking) {
388
+ currentBlock.thinking += part.text;
389
+ if (part.thoughtSignature) currentBlock.thinkingSignature = part.thoughtSignature;
390
+ stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta: part.text, partial: output });
391
+ } else {
392
+ currentBlock.text += part.text;
393
+ if (part.thoughtSignature) currentBlock.textSignature = part.thoughtSignature;
394
+ stream.push({ type: "text_delta", contentIndex: blockIndex(), delta: part.text, partial: output });
395
+ }
396
+ }
397
+ if (part.functionCall) {
398
+ hasContent = true;
399
+ finishCurrent();
400
+ const rawId = part.functionCall.id || "";
401
+ const toolCall = { type: "toolCall" as const, id: sanitizeToolCallId(rawId, part.functionCall.name), name: part.functionCall.name || "", arguments: part.functionCall.args || {}, ...(part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {}) };
402
+ blocks.push(toolCall);
403
+ ensureStarted();
404
+ stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
405
+ stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta: JSON.stringify(toolCall.arguments), partial: output });
406
+ stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
407
+ }
408
+ }
409
+ if (candidate?.finishReason) output.stopReason = blocks.some((b: any) => b.type === "toolCall") ? "toolUse" : mapStopReason(candidate.finishReason);
410
+ if (responseData.usageMetadata) {
411
+ const prompt = responseData.usageMetadata.promptTokenCount || 0;
412
+ const cacheRead = responseData.usageMetadata.cachedContentTokenCount || 0;
413
+ output.usage.input = prompt - cacheRead;
414
+ output.usage.output = (responseData.usageMetadata.candidatesTokenCount || 0) + (responseData.usageMetadata.thoughtsTokenCount || 0);
415
+ output.usage.cacheRead = cacheRead;
416
+ output.usage.totalTokens = responseData.usageMetadata.totalTokenCount || 0;
417
+
418
+ // Cost tracking math (critical for notrace dashboard UI)
419
+ let inCost = 0, outCost = 0, cacheCost = 0;
420
+ const m = (output.model || "").toLowerCase();
421
+ if (m.includes("pro")) {
422
+ inCost = 1.25; outCost = 5.0; cacheCost = 0.31;
423
+ } else {
424
+ inCost = 0.075; outCost = 0.3; cacheCost = 0.018;
425
+ }
426
+ output.usage.cost.input = output.usage.input * inCost / 1000000;
427
+ output.usage.cost.output = output.usage.output * outCost / 1000000;
428
+ output.usage.cost.cacheRead = output.usage.cacheRead * cacheCost / 1000000;
429
+ output.usage.cost.cacheWrite = 0;
430
+ output.usage.cost.total = output.usage.cost.input + output.usage.cost.output + output.usage.cost.cacheRead;
431
+ }
432
+ }
433
+ }
434
+ finishCurrent();
435
+ return hasContent;
436
+ }
437
+
438
+ export function streamAntigravity(model: any, context: any, options?: any): any {
439
+ const stream = createAssistantMessageEventStream();
440
+ void (async () => {
441
+ const output = createOutput(model);
442
+ try {
443
+ const creds = parseApiKey(options?.apiKey);
444
+ const warmedProject = await loadCodeAssist(creds.token, options?.signal);
445
+ const projectId = antigravityEnv("PROJECT_ID")?.trim() || warmedProject || creds.projectId || DEFAULT_PROJECT_ID;
446
+ setLastProjectId(projectId);
447
+ // Dynamic effort-based model routing
448
+ const effort = options?.reasoning ?? "off";
449
+ const baseRuntimeModel = antigravityEnv("RUNTIME_MODEL")?.trim() || getAntigravityRequestModelId(model.id, effort);
450
+
451
+ await fetchAvailableRuntimeModel(creds.token, projectId, baseRuntimeModel, options?.signal);
452
+ const runtimeModel = baseRuntimeModel;
453
+
454
+ setLastResolvedRuntimeModel(runtimeModel);
455
+
456
+ const body = JSON.stringify(buildRequest(model, context, projectId, options || {}, runtimeModel));
457
+
458
+ // Claude interleaving beta header if using a Claude reasoning model
459
+ const isClaudeReasoning = model.id.startsWith("claude-") && model.reasoning;
460
+ const requestHeaders: Record<string, string> = {
461
+ ...antigravityHeaders(creds.token),
462
+ ...(isClaudeReasoning ? { "anthropic-beta": "interleaved-thinking-2025-05-14" } : {}),
463
+ };
464
+
465
+ let response: Response | undefined;
466
+ let lastText = "";
467
+ let received = false;
468
+
469
+ // Empty-stream retry loop (up to 2 retries with backoff)
470
+ for (let emptyAttempt = 0; emptyAttempt <= 2; emptyAttempt++) {
471
+ if (options?.signal?.aborted) throw new Error("Request was aborted");
472
+ if (emptyAttempt > 0) {
473
+ const delay = 500 * Math.pow(2, emptyAttempt - 1);
474
+ await new Promise((res) => setTimeout(res, delay));
475
+ }
476
+
477
+ for (const endpoint of endpointCandidates()) {
478
+ setLastEndpoint(endpoint);
479
+ response = await fetch(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
480
+ method: "POST",
481
+ headers: requestHeaders,
482
+ body,
483
+ signal: options?.signal,
484
+ });
485
+ setLastStatus(response.status);
486
+ if (response.ok) break;
487
+ lastText = await response.text();
488
+ if (response.status === 429 && /Individual quota reached/i.test(lastText)) break;
489
+ if (![403, 404, 429, 500, 502, 503, 504].includes(response.status)) break;
490
+ }
491
+
492
+ if (!response || !response.ok) {
493
+ const friendly = friendlyAntigravityError(response?.status, lastText);
494
+ if (response?.status === 429 && /Quota reached\./i.test(friendly)) throw new Error(friendly);
495
+ throw new Error(`Antigravity API error (${response?.status ?? "no response"}, endpoint=${lastEndpoint || "unknown"}, project=${projectId}, runtimeModel=${runtimeModel}, matched=${lastMatchedModelDebug || "none"}, available=${lastAvailableModels || "unknown"}): ${friendly}`);
496
+ }
497
+
498
+ // Reset output contents before retry
499
+ output.content = [];
500
+ output.usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
501
+ output.stopReason = "stop";
502
+
503
+ received = await streamResponse(response, stream, output);
504
+ if (received) break;
505
+ }
506
+
507
+ if (!received) throw new Error("Antigravity API returned an empty response");
508
+ stream.push({ type: "done", reason: output.stopReason, message: output });
509
+ stream.end();
510
+ } catch (error) {
511
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
512
+ output.errorMessage = safeError(error);
513
+ setLastError(output.errorMessage);
514
+ stream.push({ type: "error", reason: output.stopReason, error: output });
515
+ stream.end();
516
+ }
517
+ })();
518
+ return stream;
519
+ }
@@ -0,0 +1,67 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ PROVIDER_ID,
4
+ PROVIDER_NAME,
5
+ ANTIGRAVITY_MODELS,
6
+ } from "./models.ts";
7
+ import {
8
+ loginAntigravity,
9
+ refreshAntigravityToken,
10
+ getApiKey,
11
+ lastStatus,
12
+ lastEndpoint,
13
+ lastError,
14
+ lastProjectId,
15
+ lastResolvedRuntimeModel,
16
+ lastAvailableModels,
17
+ lastMatchedModelDebug,
18
+ DEFAULT_ENDPOINT,
19
+ } from "./oauth.ts";
20
+ import { streamAntigravity } from "./cloud-code-assist.ts";
21
+
22
+ export default function antigravityProviderExtension(pi: ExtensionAPI) {
23
+ pi.registerProvider(PROVIDER_ID, {
24
+ name: PROVIDER_NAME,
25
+ baseUrl: DEFAULT_ENDPOINT,
26
+ api: "antigravity-api" as any,
27
+ models: ANTIGRAVITY_MODELS,
28
+ oauth: {
29
+ name: PROVIDER_NAME,
30
+ login: loginAntigravity as any,
31
+ refreshToken: refreshAntigravityToken as any,
32
+ getApiKey: getApiKey as any,
33
+ },
34
+ streamSimple: streamAntigravity,
35
+ } as any);
36
+
37
+ pi.registerCommand("antigravity.doctor", {
38
+ description: "Show sanitized Antigravity provider diagnostics",
39
+ handler: async (_args, ctx) => {
40
+ const lines = [
41
+ `provider=${PROVIDER_ID}`,
42
+ `lastResolvedRuntimeModel=${lastResolvedRuntimeModel || "none"}`,
43
+ `availableModels=${lastAvailableModels || "none"}`,
44
+ `matchedModel=${lastMatchedModelDebug || "none"}`,
45
+ `lastEndpoint=${lastEndpoint || "none"}`,
46
+ `lastStatus=${lastStatus ?? "none"}`,
47
+ `lastProjectId=${lastProjectId || "none"}`,
48
+ `lastError=${lastError || "none"}`,
49
+ "transport=native-streamSimple",
50
+ "runtimeCli=not-used",
51
+ ];
52
+ const text = lines.join("\n");
53
+ if (ctx.hasUI) ctx.ui.notify(`Antigravity doctor\n${text}`, "info");
54
+ console.log(text);
55
+ },
56
+ });
57
+
58
+ return {
59
+ deactivate: async () => {
60
+ try {
61
+ pi.unregisterProvider(PROVIDER_ID);
62
+ } catch {
63
+ // ignore
64
+ }
65
+ },
66
+ };
67
+ }