@benjamolina/pi-antigravity-guard 0.1.0 → 0.2.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
@@ -1,6 +1,6 @@
1
1
  # Pi Antigravity Guard
2
2
 
3
- `@benjamolina/pi-antigravity-guard` is a Pi extension that registers the text-only `antigravity-guard` provider. It exposes exactly one selectable model, `antigravity-gemini-3.8-flash`, which is sent to Antigravity as `gemini-3.8-flash`.
3
+ `@benjamolina/pi-antigravity-guard` is a Pi extension that registers the text-only `antigravity-guard` provider. It exposes exactly one selectable model, `antigravity-gemini-3.8-flash`, which is sent to Antigravity as `gemini-3.8-flash-tiered`.
4
4
 
5
5
  ## Use
6
6
 
@@ -12,13 +12,15 @@ Install the package in Pi, then run:
12
12
 
13
13
  Choose browser login, or choose manual login and paste the complete callback URL. The loopback callback uses fixed port `51121`; browser callback failures, remote shells, and port conflicts can use the manual callback-URL flow.
14
14
 
15
- Start a fresh text-only session with `--no-builtin-tools` and disable any active extension-provided tools. That flag does not disable extension tools by itself. Tools, tool history, images, and thinking content are unsupported and rejected rather than silently changed.
15
+ Start a fresh text-only session with `--no-builtin-tools` and disable any active extension-provided tools. That flag does not disable extension tools by itself. Tools, tool history, and images are unsupported and rejected rather than silently changed.
16
+
17
+ `@benjamolina/pi-antigravity-guard@0.2.0` supports Pi reasoning levels `low`, `medium`, and `high`. An authorized live matrix against `gemini-3.8-flash-tiered` confirmed all three levels; `minimal` was rejected twice with zero usage and is not exposed. Each supported level is sent as the matching native `thinkingLevel` with visible thoughts enabled. When reasoning is off or omitted, the request uses native `low` thinking without returning visible thoughts. Custom `thinkingBudgets` are unsupported and rejected.
16
18
 
17
19
  ## Limits and safety
18
20
 
19
21
  - This provider stores credentials through Pi's OAuth lifecycle and never reads or changes OpenCode account files; each is independent.
20
22
  - Costs are reported as zero because subscription usage is unpriced, not because access is free.
21
- - The documented public-to-wire model mapping is not proof that the model is live, available, or entitled. No authorized live OAuth or generation check has been run.
23
+ - The public-to-wire model mapping remains separate from availability and entitlement. The documented reasoning levels are limited to the authorized live matrix results above.
22
24
  - It has no account rotation, quota fallback, model substitution, or tool support.
23
25
 
24
26
  To remove it, disable or uninstall `@benjamolina/pi-antigravity-guard` and reload Pi. Removal does not delete Pi-managed credentials or revoke tokens.
package/dist/context.d.ts CHANGED
@@ -1,12 +1,20 @@
1
1
  import type { Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
2
2
  declare const WIRE_MODEL = "gemini-3.8-flash-tiered";
3
+ declare const THINKING_LEVELS: readonly ["low", "medium", "high"];
4
+ type ThinkingLevel = typeof THINKING_LEVELS[number];
3
5
  interface Part {
4
6
  text: string;
7
+ thought?: true;
8
+ thoughtSignature?: string;
5
9
  }
6
10
  interface Content {
7
11
  role: "user" | "model";
8
12
  parts: Part[];
9
13
  }
14
+ interface ThinkingConfig {
15
+ thinkingLevel: ThinkingLevel;
16
+ includeThoughts: boolean;
17
+ }
10
18
  export interface GenerationRequest {
11
19
  project: string;
12
20
  model: typeof WIRE_MODEL;
@@ -18,10 +26,7 @@ export interface GenerationRequest {
18
26
  generationConfig: {
19
27
  temperature: number;
20
28
  maxOutputTokens: number;
21
- thinkingConfig: {
22
- thinkingLevel: "low";
23
- includeThoughts: false;
24
- };
29
+ thinkingConfig: ThinkingConfig;
25
30
  };
26
31
  };
27
32
  requestType: "agent";
package/dist/context.js CHANGED
@@ -1,8 +1,10 @@
1
1
  const MAX_TEXT_BYTES = 8 * 1024 * 1024;
2
2
  const MAX_OUTPUT_TOKENS = 65_536;
3
+ const PROVIDER = "antigravity-guard";
3
4
  const PUBLIC_MODEL = "antigravity-gemini-3.8-flash";
4
5
  const WIRE_MODEL = "gemini-3.8-flash-tiered";
5
6
  const RESERVED_HEADERS = new Set(["authorization", "host", "content-type", "content-length"]);
7
+ const THINKING_LEVELS = ["low", "medium", "high"];
6
8
  export class ContextSerializationError extends Error {
7
9
  }
8
10
  export function serializeTextContext(input) {
@@ -20,10 +22,8 @@ export function serializeTextContext(input) {
20
22
  const tools = field(context, "tools");
21
23
  if (systemPrompt !== undefined && typeof systemPrompt !== "string")
22
24
  fail("Text-only context is required.");
23
- if (tools !== undefined) {
24
- if (isDenseArray(tools).length)
25
- fail("Tools are not supported by this text-only provider.");
26
- }
25
+ if (tools !== undefined && isDenseArray(tools).length)
26
+ fail("Tools are not supported by this text-only provider.");
27
27
  validateOptions(options);
28
28
  let textBytes = byteLength(systemPrompt ?? "");
29
29
  const contents = [];
@@ -35,11 +35,12 @@ export function serializeTextContext(input) {
35
35
  fail("Tool history is not supported by this text-only provider.");
36
36
  if (role !== "user" && role !== "assistant")
37
37
  fail("Unsupported context role for this text-only provider.");
38
- const text = textParts(field(message, "content", true));
39
- textBytes += text.reduce((total, part) => total + byteLength(part.text), 0);
38
+ const assistant = role === "assistant";
39
+ const parts = messageParts(field(message, "content", true), assistant, assistant && isSameProviderAndModel(message));
40
+ textBytes += parts.reduce((total, part) => total + byteLength(part.text), 0);
40
41
  if (textBytes > MAX_TEXT_BYTES)
41
42
  fail("Text context is too large.");
42
- contents.push({ role: role === "assistant" ? "model" : "user", parts: text });
43
+ contents.push({ role: role === "assistant" ? "model" : "user", parts });
43
44
  }
44
45
  if (!contents.length)
45
46
  fail("A text conversation is required.");
@@ -49,7 +50,7 @@ export function serializeTextContext(input) {
49
50
  return { project, model: WIRE_MODEL, request: { contents, ...(systemInstruction ? { systemInstruction } : {}), generationConfig: {
50
51
  temperature: typeof temperature === "number" ? temperature : 1,
51
52
  maxOutputTokens: typeof maxTokens === "number" ? maxTokens : 4096,
52
- thinkingConfig: { thinkingLevel: "low", includeThoughts: false },
53
+ thinkingConfig: resolveThinkingConfig(options),
53
54
  } }, requestType: "agent", userAgent: "antigravity", requestId };
54
55
  }
55
56
  catch (error) {
@@ -58,29 +59,69 @@ export function serializeTextContext(input) {
58
59
  fail("Invalid text context.");
59
60
  }
60
61
  }
61
- function textParts(content) {
62
+ function messageParts(content, assistant, sameProviderAndModel) {
62
63
  if (typeof content === "string")
63
64
  return content ? [{ text: content }] : fail("A text conversation is required.");
64
65
  const parts = isDenseArray(content);
65
66
  if (!parts.length)
66
67
  fail("A text conversation is required.");
67
68
  return parts.map((part) => {
68
- if (!isRecord(part) || field(part, "type", true) !== "text")
69
- fail("Only text context is supported by this provider.");
70
- const text = field(part, "text", true);
71
- if (typeof text !== "string" || !text)
69
+ if (!isRecord(part))
72
70
  fail("Only text context is supported by this provider.");
73
- return { text };
71
+ const type = field(part, "type", true);
72
+ if (type === "text") {
73
+ const text = field(part, "text", true);
74
+ if (typeof text !== "string")
75
+ fail("Only text context is supported by this provider.");
76
+ const thoughtSignature = sameProviderAndModel ? validThoughtSignature(field(part, "textSignature")) : undefined;
77
+ if (!text && !thoughtSignature)
78
+ fail("Only text context is supported by this provider.");
79
+ return { text, ...(thoughtSignature ? { thoughtSignature } : {}) };
80
+ }
81
+ if (type === "thinking" && assistant && sameProviderAndModel) {
82
+ const text = field(part, "thinking", true);
83
+ if (typeof text !== "string")
84
+ fail("Only text context is supported by this provider.");
85
+ const thoughtSignature = validThoughtSignature(field(part, "thinkingSignature"));
86
+ if (!text && !thoughtSignature)
87
+ fail("Only text context is supported by this provider.");
88
+ return { thought: true, text, ...(thoughtSignature ? { thoughtSignature } : {}) };
89
+ }
90
+ if (type === "thinking" && assistant) {
91
+ const text = field(part, "thinking", true);
92
+ if (typeof text !== "string" || !text)
93
+ fail("Only text context is supported by this provider.");
94
+ return { text };
95
+ }
96
+ fail("Only text context is supported by this provider.");
74
97
  });
75
98
  }
99
+ function isSameProviderAndModel(message) {
100
+ return field(message, "provider") === PROVIDER && field(message, "model") === PUBLIC_MODEL;
101
+ }
102
+ function validThoughtSignature(value) {
103
+ if (typeof value !== "string" || !value || value.length % 4 !== 0)
104
+ return undefined;
105
+ return /^[A-Za-z0-9+/]+={0,2}$/.test(value) ? value : undefined;
106
+ }
107
+ function resolveThinkingConfig(options) {
108
+ const reasoning = option(options, "reasoning");
109
+ if (isThinkingLevel(reasoning))
110
+ return { thinkingLevel: reasoning, includeThoughts: true };
111
+ return { thinkingLevel: "low", includeThoughts: false };
112
+ }
76
113
  function validateOptions(options) {
77
114
  if (options === undefined)
78
115
  return;
79
116
  if (!isRecord(options))
80
117
  fail("Invalid generation options.");
81
118
  const reasoning = option(options, "reasoning"), budgets = option(options, "thinkingBudgets"), deferred = option(options, "deferred"), toolChoice = option(options, "toolChoice"), sampling = option(options, "samplingParams"), temperature = option(options, "temperature"), maxTokens = option(options, "maxTokens"), headers = option(options, "headers");
82
- if (reasoning !== undefined || budgets !== undefined || deferred)
83
- fail("Reasoning and deferred requests are not supported.");
119
+ if (reasoning !== undefined && reasoning !== "off" && !isThinkingLevel(reasoning))
120
+ fail("Unsupported reasoning level.");
121
+ if (budgets !== undefined)
122
+ fail("Thinking budgets are not supported.");
123
+ if (deferred)
124
+ fail("Deferred requests are not supported.");
84
125
  if (toolChoice !== undefined && toolChoice !== "none")
85
126
  fail("Tools are not supported by this text-only provider.");
86
127
  if (sampling !== undefined)
@@ -95,6 +136,9 @@ function validateOptions(options) {
95
136
  if (RESERVED_HEADERS.has(name.toLowerCase()))
96
137
  fail("Custom headers cannot replace protected request headers.");
97
138
  }
139
+ function isThinkingLevel(value) {
140
+ return typeof value === "string" && THINKING_LEVELS.includes(value);
141
+ }
98
142
  function byteLength(value) {
99
143
  return new TextEncoder().encode(value).byteLength;
100
144
  }
package/dist/provider.js CHANGED
@@ -14,7 +14,8 @@ export function registerAntigravityProvider(pi) {
14
14
  models: [{
15
15
  id: MODEL,
16
16
  name: "Gemini 3.8 Flash (Antigravity, text only)",
17
- reasoning: false,
17
+ reasoning: true,
18
+ thinkingLevelMap: { minimal: null, low: "low", medium: "medium", high: "high" },
18
19
  input: ["text"],
19
20
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
20
21
  contextWindow: 1_048_576,
@@ -1,7 +1,13 @@
1
- export type ResponseSemantic = TextSemantic | FinishSemantic | UsageSemantic;
1
+ export type ResponseSemantic = TextSemantic | ThinkingSemantic | FinishSemantic | UsageSemantic;
2
2
  export interface TextSemantic {
3
3
  type: "text";
4
4
  text: string;
5
+ signature?: string;
6
+ }
7
+ export interface ThinkingSemantic {
8
+ type: "thinking";
9
+ thinking: string;
10
+ signature?: string;
5
11
  }
6
12
  export interface FinishSemantic {
7
13
  type: "finish";
@@ -13,18 +19,19 @@ export interface UsageSemantic {
13
19
  output: number;
14
20
  cacheRead: number;
15
21
  cacheWrite: 0;
22
+ reasoning: number;
16
23
  total: number;
17
24
  }
18
25
  export declare class ResponseSemanticError extends Error {
19
26
  }
20
27
  export declare class ResponseSemantics {
21
28
  private finished;
22
- private text;
29
+ private content;
23
30
  private done;
24
31
  private usage;
25
32
  push(record: string): ResponseSemantic[];
26
33
  finish(): void;
27
- addText(text: string): void;
34
+ addContent(text: string): void;
28
35
  addFinish(): void;
29
36
  private updateUsage;
30
37
  }
package/dist/response.js CHANGED
@@ -2,9 +2,9 @@ export class ResponseSemanticError extends Error {
2
2
  }
3
3
  export class ResponseSemantics {
4
4
  finished = false;
5
- text = false;
5
+ content = false;
6
6
  done = false;
7
- usage = { prompt: 0, cacheRead: 0, output: 0 };
7
+ usage = { prompt: 0, cacheRead: 0, output: 0, reasoning: 0 };
8
8
  push(record) {
9
9
  if (this.done)
10
10
  throw new ResponseSemanticError("Response data arrived after [DONE].");
@@ -35,20 +35,22 @@ export class ResponseSemantics {
35
35
  finish() {
36
36
  if (!this.finished)
37
37
  throw new ResponseSemanticError("Response ended without a finish reason.");
38
- if (!this.text)
39
- throw new ResponseSemanticError("Response ended without text.");
38
+ if (!this.content)
39
+ throw new ResponseSemanticError("Response ended without content.");
40
40
  }
41
- addText(text) { this.text = this.text || text.length > 0; }
41
+ addContent(text) { this.content = this.content || text.length > 0; }
42
42
  addFinish() { this.finished = true; }
43
43
  updateUsage(value) {
44
44
  const prompt = count(value, "promptTokenCount", this.usage.prompt);
45
45
  const cacheRead = count(value, "cachedContentTokenCount", this.usage.cacheRead);
46
- const output = count(value, "candidatesTokenCount", this.usage.output) + count(value, "thoughtsTokenCount", 0);
46
+ const candidates = count(value, "candidatesTokenCount", this.usage.output - this.usage.reasoning);
47
+ const reasoning = count(value, "thoughtsTokenCount", this.usage.reasoning);
48
+ const output = candidates + reasoning;
47
49
  const reported = field(value, "totalTokenCount");
48
50
  if (cacheRead > prompt || (reported !== undefined && reported !== prompt + output))
49
51
  throw new ResponseSemanticError("Antigravity returned invalid usage.");
50
- this.usage = { prompt, cacheRead, output };
51
- return { type: "usage", input: prompt - cacheRead, output, cacheRead, cacheWrite: 0, total: prompt + output };
52
+ this.usage = { prompt, cacheRead, output, reasoning };
53
+ return { type: "usage", input: prompt - cacheRead, output, cacheRead, cacheWrite: 0, reasoning, total: prompt + output };
52
54
  }
53
55
  }
54
56
  function candidate(value, events, semantics) {
@@ -67,10 +69,15 @@ function candidate(value, events, semantics) {
67
69
  throw new ResponseSemanticError("Antigravity returned invalid content.");
68
70
  for (const part of parts) {
69
71
  const text = field(part, "text");
70
- if (typeof text !== "string" || Object.keys(part).some((name) => name !== "text" && name !== "thoughtSignature"))
72
+ const thought = field(part, "thought");
73
+ if (typeof text !== "string" || (thought !== undefined && typeof thought !== "boolean") || Object.keys(part).some((name) => name !== "text" && name !== "thought" && name !== "thoughtSignature"))
71
74
  throw new ResponseSemanticError("Antigravity returned unsupported content.");
72
- semantics.addText(text);
73
- events.push({ type: "text", text });
75
+ const signature = validThoughtSignature(field(part, "thoughtSignature"));
76
+ semantics.addContent(text);
77
+ if (thought === true)
78
+ events.push({ type: "thinking", thinking: text, ...(signature ? { signature } : {}) });
79
+ else
80
+ events.push({ type: "text", text, ...(signature ? { signature } : {}) });
74
81
  }
75
82
  }
76
83
  const finishReason = field(candidate, "finishReason");
@@ -81,6 +88,11 @@ function candidate(value, events, semantics) {
81
88
  events.push({ type: "finish", reason: finishReason === "STOP" ? "stop" : "length" });
82
89
  }
83
90
  }
91
+ function validThoughtSignature(value) {
92
+ if (typeof value !== "string" || !value || value.length % 4 !== 0)
93
+ return undefined;
94
+ return /^[A-Za-z0-9+/]+={0,2}$/.test(value) ? value : undefined;
95
+ }
84
96
  function validateMetadata(value) {
85
97
  for (const name of ["responseId", "modelVersion"]) {
86
98
  const metadata = field(value, name);
package/dist/stream.js CHANGED
@@ -37,6 +37,17 @@ export function createPiLifecycleStream(input) {
37
37
  };
38
38
  let complete = false;
39
39
  let textStarted = false;
40
+ let currentBlock;
41
+ const closeBlock = () => {
42
+ if (!currentBlock)
43
+ return;
44
+ const contentIndex = output.content.length - 1;
45
+ if (currentBlock.type === "text")
46
+ stream.push({ type: "text_end", contentIndex, content: currentBlock.text, partial: output });
47
+ else
48
+ stream.push({ type: "thinking_end", contentIndex, content: currentBlock.thinking, partial: output });
49
+ currentBlock = undefined;
50
+ };
40
51
  let removeAbort = () => { };
41
52
  const finalize = (reason, errorMessage) => {
42
53
  if (complete)
@@ -47,8 +58,7 @@ export function createPiLifecycleStream(input) {
47
58
  controller.abort();
48
59
  output.stopReason = reason;
49
60
  if (reason === "stop" || reason === "length") {
50
- if (textStarted)
51
- stream.push({ type: "text_end", contentIndex: 0, content: output.content[0]?.type === "text" ? output.content[0].text : "", partial: output });
61
+ closeBlock();
52
62
  stream.push({ type: "done", reason, message: output });
53
63
  }
54
64
  else {
@@ -60,6 +70,35 @@ export function createPiLifecycleStream(input) {
60
70
  const onSemantic = (semantic) => {
61
71
  if (complete)
62
72
  return;
73
+ if (isContentSemantic(semantic)) {
74
+ if (!currentBlock || currentBlock.type !== semantic.type) {
75
+ closeBlock();
76
+ if (semantic.type === "text") {
77
+ currentBlock = { type: "text", text: "" };
78
+ output.content.push(currentBlock);
79
+ stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
80
+ }
81
+ else {
82
+ currentBlock = { type: "thinking", thinking: "" };
83
+ output.content.push(currentBlock);
84
+ stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
85
+ }
86
+ }
87
+ const contentIndex = output.content.length - 1;
88
+ if (semantic.type === "text" && currentBlock.type === "text") {
89
+ currentBlock.text += semantic.text;
90
+ if (semantic.signature)
91
+ currentBlock.textSignature = semantic.signature;
92
+ stream.push({ type: "text_delta", contentIndex, delta: semantic.text, partial: output });
93
+ }
94
+ else if (semantic.type === "thinking" && currentBlock.type === "thinking") {
95
+ currentBlock.thinking += semantic.thinking;
96
+ if (semantic.signature)
97
+ currentBlock.thinkingSignature = semantic.signature;
98
+ stream.push({ type: "thinking_delta", contentIndex, delta: semantic.thinking, partial: output });
99
+ }
100
+ return;
101
+ }
63
102
  if (semantic.type === "text" && semantic.text) {
64
103
  if (!textStarted) {
65
104
  textStarted = true;
@@ -76,6 +115,7 @@ export function createPiLifecycleStream(input) {
76
115
  output.usage.output = semantic.output;
77
116
  output.usage.cacheRead = semantic.cacheRead;
78
117
  output.usage.cacheWrite = semantic.cacheWrite;
118
+ output.usage.reasoning = semantic.reasoning;
79
119
  output.usage.totalTokens = semantic.input + semantic.output + semantic.cacheRead + semantic.cacheWrite;
80
120
  output.usage.cost = calculateCost(input.model, output.usage);
81
121
  }
@@ -95,6 +135,9 @@ export function createPiLifecycleStream(input) {
95
135
  finalize(signal.aborted ? "aborted" : "error"); }, (error) => finalize(signal.aborted || input.signal?.aborted ? "aborted" : "error", isLocalStreamError(error) ? error.message : undefined));
96
136
  return stream;
97
137
  }
138
+ function isContentSemantic(semantic) {
139
+ return semantic.type === "text" || semantic.type === "thinking";
140
+ }
98
141
  export async function executeStreamTransport(input) {
99
142
  validateInput(input);
100
143
  const signal = totalSignal(input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benjamolina/pi-antigravity-guard",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Pi adapter for Antigravity Guard",
5
5
  "type": "module",
6
6
  "license": "MIT",