pi-provider-antigravity 0.13.0 → 0.15.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.
package/README.md CHANGED
@@ -22,7 +22,7 @@ Use Google Antigravity models directly in [Pi](https://pi.dev).
22
22
 
23
23
  ## Requirements
24
24
 
25
- - Pi >= 0.80.0
25
+ - Pi >= 0.86.0 (For Pi 0.80.0 ~ 0.85.x, see [Legacy Pi Support](#legacy-pi-support))
26
26
  - A Google account with Antigravity access
27
27
 
28
28
  ## Installation
@@ -37,6 +37,22 @@ Or try it without installing:
37
37
  pi -e npm:pi-provider-antigravity
38
38
  ```
39
39
 
40
+ ### Legacy Pi Support
41
+
42
+ We strongly recommend upgrading to Pi >= 0.86.0 to take advantage of the latest SDK capabilities, streaming reliability improvements, and full feature parity.
43
+
44
+ However, if you are unable to upgrade your Pi installation just yet (for environment or workflow reasons), you can pin to the compatible `0.13.0` release for Pi 0.80.0 ~ 0.85.x:
45
+
46
+ ```bash
47
+ pi install npm:pi-provider-antigravity@0.13.0
48
+ ```
49
+
50
+ Or try it without installing:
51
+
52
+ ```bash
53
+ pi -e npm:pi-provider-antigravity@0.13.0
54
+ ```
55
+
40
56
  ## Quick Start
41
57
 
42
58
  1. Authenticate:
package/dist/index.js CHANGED
@@ -28,13 +28,15 @@ function resolveToken(auth) {
28
28
  async function postAntigravityRaw({
29
29
  auth,
30
30
  path: path2,
31
+ endpoint = DEFAULT_ENDPOINT,
32
+ headers,
31
33
  body,
32
34
  signal
33
35
  }) {
34
36
  const token = resolveToken(auth);
35
- return fetch(`${DEFAULT_ENDPOINT}/${path2}`, {
37
+ return fetch(`${endpoint}/${path2}`, {
36
38
  method: "POST",
37
- headers: buildAntigravityHeaders(token),
39
+ headers: { ...buildAntigravityHeaders(token), ...headers },
38
40
  body: JSON.stringify(body),
39
41
  signal
40
42
  });
@@ -56,7 +58,7 @@ async function postAntigravityStream(params) {
56
58
  if (!res.body) {
57
59
  throw new Error(`No response stream received from Antigravity for ${params.path}.`);
58
60
  }
59
- return res.body;
61
+ return { response: res, stream: res.body };
60
62
  }
61
63
  function buildAntigravityHeaders(token, userAgent = DEFAULT_USER_AGENT) {
62
64
  return {
@@ -122,7 +124,8 @@ async function fetchProjectId(token, signal) {
122
124
  if (data.cloudaicompanionProject) {
123
125
  return data.cloudaicompanionProject;
124
126
  }
125
- } catch {
127
+ } catch (err) {
128
+ if (signal?.aborted) throw err;
126
129
  }
127
130
  return FALLBACK_PROJECT_ID;
128
131
  }
@@ -142,16 +145,37 @@ async function loginAntigravity(callbacks) {
142
145
  });
143
146
  const authUrl = `${AUTH_URL}?${authParams.toString()}`;
144
147
  callbacks.onAuth({ url: authUrl });
148
+ let timer;
149
+ const abortPromise = new Promise((_, rej) => {
150
+ if (callbacks.signal?.aborted) {
151
+ rej(callbacks.signal.reason ?? new Error("Login cancelled"));
152
+ return;
153
+ }
154
+ callbacks.signal?.addEventListener(
155
+ "abort",
156
+ () => rej(callbacks.signal?.reason ?? new Error("Login cancelled")),
157
+ { once: true }
158
+ );
159
+ });
145
160
  const manualPromptPromise = (async () => {
146
161
  const input = await callbacks.onPrompt({
147
162
  message: "Paste authorization code from browser:"
148
163
  });
149
164
  return extractCodeFromInput(input);
150
165
  })();
151
- const timeout = new Promise(
152
- (_, rej) => setTimeout(() => rej(new Error("Timed out waiting for authorization code (5 minutes)")), OAUTH_CALLBACK_TIMEOUT_MS)
153
- );
154
- const code = await Promise.race([manualPromptPromise, timeout]);
166
+ const timeoutPromise = new Promise((_, rej) => {
167
+ timer = setTimeout(
168
+ () => rej(new Error("Timed out waiting for authorization code (5 minutes)")),
169
+ OAUTH_CALLBACK_TIMEOUT_MS
170
+ );
171
+ });
172
+ let code;
173
+ try {
174
+ code = await Promise.race([manualPromptPromise, timeoutPromise, abortPromise]);
175
+ } finally {
176
+ if (timer) clearTimeout(timer);
177
+ }
178
+ callbacks.onProgress?.("Exchanging authorization code for tokens...");
155
179
  const tokenParams = new URLSearchParams({
156
180
  client_id: CLIENT_ID,
157
181
  client_secret: CLIENT_SECRET,
@@ -163,14 +187,16 @@ async function loginAntigravity(callbacks) {
163
187
  const tokenRes = await fetch(TOKEN_URL, {
164
188
  method: "POST",
165
189
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
166
- body: tokenParams.toString()
190
+ body: tokenParams.toString(),
191
+ signal: callbacks.signal
167
192
  });
168
193
  if (!tokenRes.ok) {
169
194
  const errText = await tokenRes.text();
170
195
  throw new Error(`Failed to exchange token (${tokenRes.status}): ${errText}`);
171
196
  }
172
197
  const tokens = await tokenRes.json();
173
- const projectId = await fetchProjectId(tokens.access_token);
198
+ callbacks.onProgress?.("Resolving Code Assist project ID...");
199
+ const projectId = await fetchProjectId(tokens.access_token, callbacks.signal);
174
200
  return {
175
201
  refresh: tokens.refresh_token || "",
176
202
  access: JSON.stringify({ token: tokens.access_token, projectId }),
@@ -527,6 +553,7 @@ function synthesizeDynamicModel(baseId, items) {
527
553
  const isGpt = family === "gpt";
528
554
  const defaultContext = isFlash ? 1048576 : isClaude ? 25e4 : isGpt ? 128e3 : 1048576;
529
555
  const defaultMaxOutput = isClaude ? 64e3 : isGpt ? 32768 : 65536;
556
+ const promptCache = isClaude ? { short: 300, long: 3600 } : { short: 300 };
530
557
  return {
531
558
  id: baseId,
532
559
  name: formatModelDisplayName(baseId, repItem?.displayName),
@@ -537,6 +564,7 @@ function synthesizeDynamicModel(baseId, items) {
537
564
  thinkingLevelMap,
538
565
  input: repItem?.supportsImages ? ["text", "image"] : ["text"],
539
566
  cost: estimateModelCost(baseId),
567
+ promptCache,
540
568
  contextWindow: repItem?.maxTokens || defaultContext,
541
569
  maxTokens: repItem?.maxOutputTokens || defaultMaxOutput
542
570
  };
@@ -1384,11 +1412,19 @@ var QuotaStatusCoordinator = class {
1384
1412
 
1385
1413
  // src/stream.ts
1386
1414
  import {
1415
+ calculateCost,
1387
1416
  createAssistantMessageEventStream
1388
1417
  } from "@earendil-works/pi-ai";
1389
1418
 
1390
1419
  // src/builder.ts
1391
1420
  import { createHash as createHash2, randomUUID } from "node:crypto";
1421
+ import {
1422
+ collapseSystemMessages,
1423
+ getCurrentSystemPrompt,
1424
+ getCurrentTools,
1425
+ normalizeContext,
1426
+ withoutInitialSystemMessage
1427
+ } from "@earendil-works/pi-ai";
1392
1428
  var BASE64_SIGNATURE_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/;
1393
1429
  var SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
1394
1430
  function isValidThoughtSignature(sig) {
@@ -1400,11 +1436,11 @@ function resolveThoughtSignature(isSameProviderAndModel, sig) {
1400
1436
  if (!isSameProviderAndModel) return void 0;
1401
1437
  return isValidThoughtSignature(sig) ? sig : void 0;
1402
1438
  }
1403
- function deriveSessionId(context, explicitSessionId) {
1439
+ function deriveSessionId(conversation, explicitSessionId) {
1404
1440
  if (explicitSessionId && typeof explicitSessionId === "string" && explicitSessionId.trim().length > 0) {
1405
1441
  return explicitSessionId.trim();
1406
1442
  }
1407
- const firstMsg = context.messages?.[0];
1443
+ const firstMsg = conversation?.[0];
1408
1444
  if (!firstMsg) {
1409
1445
  return randomUUID();
1410
1446
  }
@@ -1497,9 +1533,9 @@ function convertTools(tools) {
1497
1533
  }
1498
1534
  ];
1499
1535
  }
1500
- function translateTurnTrace(context, runtimeModelId) {
1536
+ function translateTurnTrace(conversation, runtimeModelId) {
1501
1537
  const contents = [];
1502
- const messages = context.messages || [];
1538
+ const messages = conversation || [];
1503
1539
  const isGeminiRequest = classifyModelFamily(runtimeModelId) === "gemini";
1504
1540
  const isGptRequest = classifyModelFamily(runtimeModelId) === "gpt";
1505
1541
  for (const msg of messages) {
@@ -1690,9 +1726,13 @@ function buildAntigravityRequestBody(params) {
1690
1726
  maxOutputTokens = 65536
1691
1727
  } = params;
1692
1728
  const runtimeModelId = plan.runtimeModelId;
1693
- const sessionId = deriveSessionId(context, params.sessionId);
1729
+ const transcript = collapseSystemMessages(context);
1730
+ const systemPrompt = getCurrentSystemPrompt(transcript.messages);
1731
+ const tools = convertTools(getCurrentTools(transcript.messages));
1732
+ const conversation = withoutInitialSystemMessage(transcript.messages);
1733
+ const sessionId = deriveSessionId(conversation, params.sessionId);
1694
1734
  const trajectoryId = params.trajectoryId && typeof params.trajectoryId === "string" && params.trajectoryId.trim().length > 0 ? params.trajectoryId.trim() : resolveSessionTrajectory(sessionId);
1695
- const contents = translateTurnTrace(context, runtimeModelId);
1735
+ const contents = translateTurnTrace(conversation, runtimeModelId);
1696
1736
  const requestIndex = contents.filter(
1697
1737
  (c) => c.role === "model" && !c.parts?.some((p) => p.functionResponse)
1698
1738
  ).length;
@@ -1714,13 +1754,12 @@ function buildAntigravityRequestBody(params) {
1714
1754
  const request = {
1715
1755
  contents
1716
1756
  };
1717
- if (context.systemPrompt) {
1757
+ if (systemPrompt) {
1718
1758
  request.systemInstruction = {
1719
1759
  role: "user",
1720
- parts: [{ text: context.systemPrompt }]
1760
+ parts: [{ text: systemPrompt }]
1721
1761
  };
1722
1762
  }
1723
- const tools = convertTools(context.tools);
1724
1763
  if (tools) {
1725
1764
  request.tools = tools;
1726
1765
  }
@@ -1791,7 +1830,14 @@ function processLine(rawLine, state, onEvent) {
1791
1830
  } catch {
1792
1831
  return;
1793
1832
  }
1833
+ const streamError = payload?.error ?? payload?.response?.error;
1834
+ if (streamError) {
1835
+ throw new Error(streamError.message ?? JSON.stringify(streamError));
1836
+ }
1794
1837
  const response = payload.response || payload;
1838
+ if (response.responseId && !state.responseId) {
1839
+ state.responseId = response.responseId;
1840
+ }
1795
1841
  if (response.usageMetadata) {
1796
1842
  const um = response.usageMetadata;
1797
1843
  const promptTokens = typeof um.promptTokenCount === "number" ? um.promptTokenCount : 0;
@@ -1808,8 +1854,14 @@ function processLine(rawLine, state, onEvent) {
1808
1854
  if (!candidate) return;
1809
1855
  if (candidate.finishReason === "MAX_TOKENS") {
1810
1856
  state.stopReason = "length";
1811
- } else if (typeof candidate.finishReason === "string" && candidate.finishReason !== "STOP") {
1857
+ state.rawStopReason = "MAX_TOKENS";
1858
+ } else if (candidate.finishReason === "STOP") {
1859
+ if (state.stopReason === "pending") {
1860
+ state.stopReason = "stop";
1861
+ }
1862
+ } else if (typeof candidate.finishReason === "string") {
1812
1863
  state.stopReason = "error";
1864
+ state.rawStopReason = candidate.finishReason;
1813
1865
  }
1814
1866
  const parts = candidate.content?.parts || [];
1815
1867
  for (const part of parts) {
@@ -1838,7 +1890,9 @@ function processLine(rawLine, state, onEvent) {
1838
1890
  });
1839
1891
  } else if (part.functionCall) {
1840
1892
  closeOpenBlock(state, onEvent);
1841
- state.stopReason = "toolUse";
1893
+ if (state.stopReason === "pending" || state.stopReason === "stop") {
1894
+ state.stopReason = "toolUse";
1895
+ }
1842
1896
  const block = {
1843
1897
  type: "toolCall",
1844
1898
  id: part.functionCall.id || `call_${state.content.length}`,
@@ -1886,33 +1940,43 @@ function finishStream(state, onEvent) {
1886
1940
  closeOpenBlock(state, onEvent);
1887
1941
  attachLoneSignature(state.content, state.lastThoughtSignature);
1888
1942
  }
1889
- async function consumeAntigravityStream(streamBody, output, stream, modelCost) {
1943
+ var NON_OVERFLOW_PATTERN = /rate limit|too many requests|quota exceeded|resource_exhausted/i;
1944
+ var CONTEXT_OVERFLOW_PATTERN = /(?:(?:input|prompt) token count.*exceeds|prompt (?:is )?too long)/i;
1945
+ function normalizeOverflowError(rawMessage) {
1946
+ if (rawMessage.includes("context_length_exceeded")) return rawMessage;
1947
+ if (!NON_OVERFLOW_PATTERN.test(rawMessage) && CONTEXT_OVERFLOW_PATTERN.test(rawMessage)) {
1948
+ return `context_length_exceeded: ${rawMessage}`;
1949
+ }
1950
+ return rawMessage;
1951
+ }
1952
+ async function consumeAntigravityStream(streamBody, output, stream, model, signal) {
1890
1953
  const state = {
1891
1954
  content: output.content,
1892
1955
  usage: { input: 0, output: 0, cacheRead: 0, reasoning: 0, total: 0 },
1893
- stopReason: "stop",
1956
+ stopReason: "pending",
1894
1957
  openType: null,
1895
1958
  buffer: ""
1896
1959
  };
1960
+ let lastTotalTokens = -1;
1897
1961
  const updateUsageAndCost = () => {
1898
1962
  output.usage.input = state.usage.input;
1899
1963
  output.usage.cacheRead = state.usage.cacheRead;
1900
1964
  output.usage.output = state.usage.output;
1901
1965
  output.usage.reasoning = state.usage.reasoning;
1902
1966
  output.usage.totalTokens = state.usage.total;
1903
- if (modelCost) {
1904
- const inputCost = output.usage.input * (modelCost.input || 0) / 1e6;
1905
- const outputCost = output.usage.output * (modelCost.output || 0) / 1e6;
1906
- const cacheCost = output.usage.cacheRead * (modelCost.cacheRead || 0) / 1e6;
1907
- output.usage.cost = {
1908
- input: inputCost,
1909
- output: outputCost,
1910
- cacheRead: cacheCost,
1911
- cacheWrite: 0,
1912
- total: inputCost + outputCost + cacheCost
1913
- };
1967
+ if (state.usage.total !== lastTotalTokens) {
1968
+ lastTotalTokens = state.usage.total;
1969
+ if (model.cost) {
1970
+ calculateCost(model, output.usage);
1971
+ }
1972
+ }
1973
+ if (state.responseId) {
1974
+ output.responseId ||= state.responseId;
1914
1975
  }
1915
1976
  output.stopReason = state.stopReason;
1977
+ if (state.rawStopReason) {
1978
+ output.rawStopReason = state.rawStopReason;
1979
+ }
1916
1980
  };
1917
1981
  const dispatchEvent = (ev) => {
1918
1982
  updateUsageAndCost();
@@ -1920,13 +1984,20 @@ async function consumeAntigravityStream(streamBody, output, stream, modelCost) {
1920
1984
  };
1921
1985
  const reader = streamBody.getReader();
1922
1986
  const decoder = new TextDecoder();
1923
- while (true) {
1924
- const { done, value } = await reader.read();
1925
- if (done) break;
1926
- feedChunk(decoder.decode(value, { stream: true }), state, dispatchEvent);
1987
+ try {
1988
+ while (true) {
1989
+ if (signal?.aborted) {
1990
+ throw new Error("Request was aborted");
1991
+ }
1992
+ const { done, value } = await reader.read();
1993
+ if (done) break;
1994
+ feedChunk(decoder.decode(value, { stream: true }), state, dispatchEvent);
1995
+ }
1996
+ finishStream(state, dispatchEvent);
1997
+ updateUsageAndCost();
1998
+ } finally {
1999
+ reader.releaseLock();
1927
2000
  }
1928
- finishStream(state, dispatchEvent);
1929
- updateUsageAndCost();
1930
2001
  }
1931
2002
  function streamAntigravity(model, context, options, catalog) {
1932
2003
  const stream = createAssistantMessageEventStream();
@@ -1954,22 +2025,60 @@ function streamAntigravity(model, context, options, catalog) {
1954
2025
  const effort = options?.reasoning;
1955
2026
  const trajectoryId = options && "trajectoryId" in options && typeof options.trajectoryId === "string" ? options.trajectoryId : void 0;
1956
2027
  const plan = catalog.resolvePlan(model.id, effort);
1957
- const requestBody = buildAntigravityRequestBody({
2028
+ let requestBody = buildAntigravityRequestBody({
1958
2029
  projectId,
1959
2030
  plan,
1960
2031
  context,
1961
2032
  sessionId: options?.sessionId,
1962
2033
  trajectoryId,
1963
- maxOutputTokens: model.maxTokens,
2034
+ maxOutputTokens: options?.maxTokens ?? model.maxTokens,
1964
2035
  toolChoice: options?.toolChoice
1965
2036
  });
1966
- const streamBody = await postAntigravityStream({
2037
+ if (options?.onPayload) {
2038
+ const replacement = await options.onPayload(requestBody, model);
2039
+ if (replacement !== void 0) {
2040
+ requestBody = replacement;
2041
+ }
2042
+ }
2043
+ const endpoint = model.baseUrl || DEFAULT_ENDPOINT;
2044
+ const mergedHeaders = {};
2045
+ if (model.headers) {
2046
+ for (const [k, v] of Object.entries(model.headers)) {
2047
+ if (typeof v === "string") mergedHeaders[k] = v;
2048
+ }
2049
+ }
2050
+ if (options?.headers) {
2051
+ for (const [k, v] of Object.entries(options.headers)) {
2052
+ if (typeof v === "string") mergedHeaders[k] = v;
2053
+ }
2054
+ }
2055
+ const { response, stream: streamBody } = await postAntigravityStream({
1967
2056
  auth: token,
2057
+ endpoint,
1968
2058
  path: "v1internal:streamGenerateContent?alt=sse",
2059
+ headers: mergedHeaders,
1969
2060
  body: requestBody,
1970
2061
  signal: options?.signal
1971
2062
  });
1972
- await consumeAntigravityStream(streamBody, output, stream, model.cost);
2063
+ if (options?.onResponse) {
2064
+ const resHeaders = {};
2065
+ response.headers.forEach((val, key) => {
2066
+ resHeaders[key] = val;
2067
+ });
2068
+ await options.onResponse({ status: response.status, headers: resHeaders }, model);
2069
+ }
2070
+ await consumeAntigravityStream(streamBody, output, stream, model, options?.signal);
2071
+ if (options?.signal?.aborted) {
2072
+ throw new Error("Request was aborted");
2073
+ }
2074
+ if (output.stopReason === "pending") {
2075
+ throw new Error("Provider stream ended without a stop reason");
2076
+ }
2077
+ if (output.stopReason === "error" || output.stopReason === "aborted") {
2078
+ throw new Error(
2079
+ output.rawStopReason ? `Provider stopped with: ${output.rawStopReason}` : output.errorMessage || "An unknown error occurred"
2080
+ );
2081
+ }
1973
2082
  const doneReason = output.stopReason === "toolUse" || output.stopReason === "length" ? output.stopReason : "stop";
1974
2083
  stream.push({
1975
2084
  type: "done",
@@ -1979,7 +2088,8 @@ function streamAntigravity(model, context, options, catalog) {
1979
2088
  stream.end();
1980
2089
  } catch (error) {
1981
2090
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
1982
- output.errorMessage = error instanceof Error ? error.message : String(error);
2091
+ const rawMessage = error instanceof Error ? error.message : String(error);
2092
+ output.errorMessage = normalizeOverflowError(rawMessage);
1983
2093
  stream.push({ type: "error", reason: output.stopReason, error: output });
1984
2094
  stream.end();
1985
2095
  }
@@ -2277,6 +2387,7 @@ function index_default(pi) {
2277
2387
  // Purely dynamic provider per Pi SDK architecture
2278
2388
  oauth: {
2279
2389
  name: PROVIDER_NAME,
2390
+ isSubscription: true,
2280
2391
  login: loginAntigravity,
2281
2392
  refreshToken: refreshAntigravityToken,
2282
2393
  getApiKey