@benjamolina/pi-antigravity-guard 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.
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/auth-http.d.ts +17 -0
- package/dist/auth-http.js +143 -0
- package/dist/context.d.ts +41 -0
- package/dist/context.js +128 -0
- package/dist/extension.d.ts +2 -0
- package/dist/extension.js +4 -0
- package/dist/loopback.d.ts +45 -0
- package/dist/loopback.js +171 -0
- package/dist/oauth.d.ts +21 -0
- package/dist/oauth.js +175 -0
- package/dist/project.d.ts +14 -0
- package/dist/project.js +154 -0
- package/dist/provider.d.ts +6 -0
- package/dist/provider.js +83 -0
- package/dist/response.d.ts +30 -0
- package/dist/response.js +113 -0
- package/dist/sse.d.ts +16 -0
- package/dist/sse.js +110 -0
- package/dist/stream.d.ts +36 -0
- package/dist/stream.js +269 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +0 -0
- package/package.json +44 -0
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { ANTIGRAVITY_OAUTH_CLIENT, buildAuthorizationUrl } from "@benjamolina/antigravity-guard-core";
|
|
3
|
+
import { exchangeAuthorizationCode, refreshCredentials } from "./auth-http.js";
|
|
4
|
+
import { openLoopbackReceiver, validateLoopbackCallback } from "./loopback.js";
|
|
5
|
+
import { defaultProjectId, resolveLoginProject } from "./project.js";
|
|
6
|
+
const ATTEMPT_TIMEOUT_MS = 5 * 60_000;
|
|
7
|
+
const PI_OAUTH_CLIENT = {
|
|
8
|
+
...ANTIGRAVITY_OAUTH_CLIENT,
|
|
9
|
+
scopes: [...ANTIGRAVITY_OAUTH_CLIENT.scopes, "https://www.googleapis.com/auth/aicode"],
|
|
10
|
+
};
|
|
11
|
+
const trustedErrors = new WeakSet();
|
|
12
|
+
export function createPiOAuthLifecycle(dependencies) {
|
|
13
|
+
const bytes = dependencies.randomBytes ?? randomBytes;
|
|
14
|
+
const exchange = dependencies.exchange ?? exchangeAuthorizationCode;
|
|
15
|
+
const refresh = dependencies.refresh ?? refreshCredentials;
|
|
16
|
+
const project = dependencies.project ?? resolveLoginProject;
|
|
17
|
+
const openLoopback = dependencies.openLoopback ?? openLoopbackReceiver;
|
|
18
|
+
let active = false;
|
|
19
|
+
return {
|
|
20
|
+
async login(callbacks) {
|
|
21
|
+
if (active)
|
|
22
|
+
throw new Error("An Antigravity login is already in progress.");
|
|
23
|
+
active = true;
|
|
24
|
+
let receiver;
|
|
25
|
+
let signal;
|
|
26
|
+
try {
|
|
27
|
+
const deadline = dependencies.now() + ATTEMPT_TIMEOUT_MS;
|
|
28
|
+
signal = composedSignal(callbacks.signal, ATTEMPT_TIMEOUT_MS);
|
|
29
|
+
const state = base64Url(bytes(32));
|
|
30
|
+
const verifier = base64Url(bytes(32));
|
|
31
|
+
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
32
|
+
const authorizationUrl = buildAuthorizationUrl(PI_OAUTH_CLIENT, { challenge, state });
|
|
33
|
+
const method = await abortable(() => callbacks.onSelect({
|
|
34
|
+
message: "Choose how to complete Antigravity login.",
|
|
35
|
+
options: [
|
|
36
|
+
{ id: "browser", label: "Open browser and wait for callback" },
|
|
37
|
+
{ id: "manual", label: "Paste the full callback URL" },
|
|
38
|
+
],
|
|
39
|
+
}), signal);
|
|
40
|
+
if (!method)
|
|
41
|
+
throw cancelled();
|
|
42
|
+
if (method === "browser") {
|
|
43
|
+
const opened = await untrusted(() => openLoopback({ state, signal, totalDeadlineMs: deadline - dependencies.now() }), "Antigravity login failed.");
|
|
44
|
+
receiver = opened;
|
|
45
|
+
await abortable(() => untrusted(() => opened.ready, "Antigravity login failed."), signal);
|
|
46
|
+
}
|
|
47
|
+
else if (method !== "manual") {
|
|
48
|
+
throw cancelled();
|
|
49
|
+
}
|
|
50
|
+
callbacks.onAuth({ url: authorizationUrl });
|
|
51
|
+
const code = receiver ? await codeFromLoopback(receiver, callbacks, signal, state) : await codeFromPrompt(callbacks, signal, state);
|
|
52
|
+
const credentials = await abortable(() => untrusted(() => exchange({
|
|
53
|
+
code, verifier, fetch: dependencies.fetch, now: dependencies.now, signal, deadlineMs: deadline,
|
|
54
|
+
}), "Antigravity token exchange failed after authorization."), signal);
|
|
55
|
+
const discovery = await abortable(() => Promise.resolve(project({ accessToken: credentials.access, fetch: dependencies.fetch, now: dependencies.now, platform: process.platform, signal, deadlineMs: deadline })).catch(() => ({})), signal);
|
|
56
|
+
return piCredentials(credentials, discovery);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (trusted(error))
|
|
60
|
+
throw error;
|
|
61
|
+
if (signal?.aborted)
|
|
62
|
+
throw cancelled();
|
|
63
|
+
throw new OAuthLifecycleError("Antigravity login failed.");
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
try {
|
|
67
|
+
void Promise.resolve(receiver?.cancel()).catch(() => undefined);
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
active = false;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
async refreshToken(credentials, signal) {
|
|
74
|
+
try {
|
|
75
|
+
const refreshed = await abortable(() => untrusted(() => refresh({ credentials: credentialsFrom(credentials), fetch: dependencies.fetch, now: dependencies.now, signal }), "Antigravity credential refresh failed."), signal);
|
|
76
|
+
return piCredentials(refreshed, credentialProject(credentials));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (trusted(error))
|
|
80
|
+
throw error;
|
|
81
|
+
if (signal.aborted)
|
|
82
|
+
throw cancelled();
|
|
83
|
+
throw new OAuthLifecycleError("Antigravity credential refresh failed.");
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
getApiKey(credentials) {
|
|
87
|
+
const project = credentialProject(credentials);
|
|
88
|
+
return JSON.stringify({ token: credentials.access, projectId: project.projectId ?? defaultProjectId(project.email ?? "antigravity-default") });
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function codeFromLoopback(receiver, callbacks, signal, state) {
|
|
93
|
+
const result = await abortable(() => untrusted(() => receiver.result, "Antigravity login failed."), signal);
|
|
94
|
+
if (result.kind === "callback")
|
|
95
|
+
return result.code;
|
|
96
|
+
if (result.kind === "manual")
|
|
97
|
+
return codeFromPrompt(callbacks, signal, state);
|
|
98
|
+
if (result.kind === "denied")
|
|
99
|
+
throw new OAuthLifecycleError("Antigravity authorization was denied.");
|
|
100
|
+
throw cancelled();
|
|
101
|
+
}
|
|
102
|
+
async function codeFromPrompt(callbacks, signal, state) {
|
|
103
|
+
let input;
|
|
104
|
+
try {
|
|
105
|
+
input = await abortable(() => callbacks.onPrompt({ message: "Paste the full callback URL." }), signal);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw cancelled();
|
|
109
|
+
}
|
|
110
|
+
if (!input.trim())
|
|
111
|
+
throw cancelled();
|
|
112
|
+
return codeFromManualCallback(input, state);
|
|
113
|
+
}
|
|
114
|
+
function codeFromManualCallback(input, state) {
|
|
115
|
+
let result;
|
|
116
|
+
try {
|
|
117
|
+
const url = new URL(input);
|
|
118
|
+
if (url.origin !== "http://localhost:51121" || url.pathname !== "/oauth-callback" || url.username || url.password || url.hash)
|
|
119
|
+
throw new Error();
|
|
120
|
+
result = validateLoopbackCallback({ method: "GET", host: "localhost:51121", remoteAddress: "127.0.0.1", url: `${url.pathname}${url.search}` }, state);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new OAuthLifecycleError("Paste the full callback URL from Antigravity login.");
|
|
124
|
+
}
|
|
125
|
+
if (result.kind === "callback")
|
|
126
|
+
return result.code;
|
|
127
|
+
if (result.kind === "denied")
|
|
128
|
+
throw new OAuthLifecycleError("Antigravity authorization was denied.");
|
|
129
|
+
throw new OAuthLifecycleError("Paste the full callback URL from Antigravity login.");
|
|
130
|
+
}
|
|
131
|
+
function credentialsFrom(credentials) {
|
|
132
|
+
return credentials;
|
|
133
|
+
}
|
|
134
|
+
function credentialProject(credentials) {
|
|
135
|
+
const values = credentials;
|
|
136
|
+
return {
|
|
137
|
+
...(nonempty(values.projectId) ? { projectId: values.projectId } : {}),
|
|
138
|
+
...(nonempty(values.email) ? { email: values.email } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function piCredentials(credentials, discovered = {}) {
|
|
142
|
+
const email = nonempty(discovered.email) ? discovered.email : undefined;
|
|
143
|
+
const projectId = nonempty(discovered.projectId) ? discovered.projectId : defaultProjectId(email ?? "antigravity-default");
|
|
144
|
+
return { refresh: credentials.refresh, access: credentials.access, expires: credentials.expires, projectId, ...(email ? { email } : {}) };
|
|
145
|
+
}
|
|
146
|
+
function untrusted(operation, message) {
|
|
147
|
+
return Promise.resolve().then(operation).catch(() => { throw new OAuthLifecycleError(message); });
|
|
148
|
+
}
|
|
149
|
+
function composedSignal(signal, timeoutMs) {
|
|
150
|
+
return signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
|
|
151
|
+
}
|
|
152
|
+
function abortable(operation, signal) {
|
|
153
|
+
if (signal.aborted)
|
|
154
|
+
return Promise.reject(cancelled());
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const abort = () => reject(cancelled());
|
|
157
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
158
|
+
operation().then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function base64Url(value) {
|
|
162
|
+
return Buffer.from(value).toString("base64url");
|
|
163
|
+
}
|
|
164
|
+
function nonempty(value) {
|
|
165
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
166
|
+
}
|
|
167
|
+
function cancelled() {
|
|
168
|
+
return new OAuthLifecycleError("Antigravity login was cancelled.");
|
|
169
|
+
}
|
|
170
|
+
function trusted(error) {
|
|
171
|
+
return error instanceof OAuthLifecycleError && trustedErrors.delete(error);
|
|
172
|
+
}
|
|
173
|
+
class OAuthLifecycleError extends Error {
|
|
174
|
+
constructor(message) { super(message); trustedErrors.add(this); }
|
|
175
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LoginProjectOptions {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
fetch: typeof globalThis.fetch;
|
|
4
|
+
now: () => number;
|
|
5
|
+
platform: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
deadlineMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface LoginProject {
|
|
10
|
+
email?: string;
|
|
11
|
+
projectId?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveLoginProject(options: LoginProjectOptions): Promise<LoginProject>;
|
|
14
|
+
export declare function defaultProjectId(seed: string): string;
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const USER_INFO_ENDPOINT = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json";
|
|
3
|
+
const PROJECT_ENDPOINTS = [
|
|
4
|
+
"https://daily-cloudcode-pa.googleapis.com",
|
|
5
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
6
|
+
"https://cloudcode-pa.googleapis.com",
|
|
7
|
+
];
|
|
8
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
10
|
+
const ANTIGRAVITY_USER_AGENT = "antigravity/cli/1.1.23 (aidev_client; os_type=linux; arch=amd64; cl=974125021; auth_method=consumer)";
|
|
11
|
+
export async function resolveLoginProject(options) {
|
|
12
|
+
const email = await userEmail(options);
|
|
13
|
+
const projectId = await discoveredProject(options);
|
|
14
|
+
return { ...(email ? { email } : {}), ...(projectId ? { projectId } : {}) };
|
|
15
|
+
}
|
|
16
|
+
export function defaultProjectId(seed) {
|
|
17
|
+
const bytes = createHash("sha1").update(`antigravity:${seed}`).digest().subarray(0, 16);
|
|
18
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
19
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
20
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
21
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
22
|
+
}
|
|
23
|
+
async function userEmail(options) {
|
|
24
|
+
const { payload } = await bestEffort(options, USER_INFO_ENDPOINT, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
redirect: "error",
|
|
27
|
+
headers: { Authorization: `Bearer ${options.accessToken}` },
|
|
28
|
+
});
|
|
29
|
+
return isRecord(payload) && nonempty(payload.email) ? payload.email : undefined;
|
|
30
|
+
}
|
|
31
|
+
async function discoveredProject(options) {
|
|
32
|
+
const request = {
|
|
33
|
+
method: "POST",
|
|
34
|
+
redirect: "error",
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${options.accessToken}`,
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"User-Agent": ANTIGRAVITY_USER_AGENT,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } }),
|
|
41
|
+
};
|
|
42
|
+
for (const origin of PROJECT_ENDPOINTS) {
|
|
43
|
+
const response = await bestEffort(options, `${origin}/v1internal:loadCodeAssist`, request);
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
continue;
|
|
46
|
+
const projectId = projectFrom(response.payload);
|
|
47
|
+
return projectId ?? listProject(options, request);
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
async function listProject(options, request) {
|
|
52
|
+
const listRequest = { ...request, body: "{}" };
|
|
53
|
+
for (const origin of PROJECT_ENDPOINTS) {
|
|
54
|
+
const { payload } = await bestEffort(options, `${origin}/v1internal:listCloudAICompanionProjects`, listRequest);
|
|
55
|
+
const projectId = projectFrom(payload);
|
|
56
|
+
if (projectId)
|
|
57
|
+
return projectId;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
async function bestEffort(options, endpoint, request) {
|
|
62
|
+
const timeout = options.deadlineMs === undefined
|
|
63
|
+
? REQUEST_TIMEOUT_MS
|
|
64
|
+
: Math.min(REQUEST_TIMEOUT_MS, options.deadlineMs - options.now());
|
|
65
|
+
if (timeout <= 0 || options.signal?.aborted)
|
|
66
|
+
return { ok: false };
|
|
67
|
+
const signal = options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await options.fetch(endpoint, { ...request, signal });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { ok: false };
|
|
74
|
+
}
|
|
75
|
+
if (!response?.ok)
|
|
76
|
+
return { ok: false };
|
|
77
|
+
try {
|
|
78
|
+
const body = await boundedBody(response.body, signal);
|
|
79
|
+
return body === undefined ? { ok: true } : { ok: true, payload: JSON.parse(body) };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { ok: true };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function boundedBody(body, signal) {
|
|
86
|
+
if (!body)
|
|
87
|
+
return undefined;
|
|
88
|
+
const reader = body.getReader();
|
|
89
|
+
const chunks = [];
|
|
90
|
+
let size = 0;
|
|
91
|
+
let complete = false;
|
|
92
|
+
try {
|
|
93
|
+
while (true) {
|
|
94
|
+
const next = await abortable(reader.read(), signal);
|
|
95
|
+
if (next.done) {
|
|
96
|
+
complete = true;
|
|
97
|
+
const output = new Uint8Array(size);
|
|
98
|
+
let offset = 0;
|
|
99
|
+
for (const chunk of chunks) {
|
|
100
|
+
output.set(chunk, offset);
|
|
101
|
+
offset += chunk.byteLength;
|
|
102
|
+
}
|
|
103
|
+
return new TextDecoder().decode(output);
|
|
104
|
+
}
|
|
105
|
+
size += next.value.byteLength;
|
|
106
|
+
if (size > MAX_BODY_BYTES)
|
|
107
|
+
return undefined;
|
|
108
|
+
chunks.push(next.value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (!complete)
|
|
113
|
+
await reader.cancel().catch(() => undefined);
|
|
114
|
+
reader.releaseLock();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function abortable(promise, signal) {
|
|
118
|
+
if (signal.aborted)
|
|
119
|
+
return Promise.reject(new Error("aborted"));
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const abort = () => reject(new Error("aborted"));
|
|
122
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
123
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function projectFrom(value) {
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
for (const entry of value) {
|
|
129
|
+
const projectId = projectFrom(entry);
|
|
130
|
+
if (projectId)
|
|
131
|
+
return projectId;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (!isRecord(value))
|
|
136
|
+
return undefined;
|
|
137
|
+
const project = value.cloudaicompanionProject;
|
|
138
|
+
if (nonempty(project))
|
|
139
|
+
return project;
|
|
140
|
+
if (isRecord(project) && nonempty(project.id))
|
|
141
|
+
return project.id;
|
|
142
|
+
for (const entry of Object.values(value)) {
|
|
143
|
+
const projectId = projectFrom(entry);
|
|
144
|
+
if (projectId)
|
|
145
|
+
return projectId;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function isRecord(value) {
|
|
150
|
+
return typeof value === "object" && value !== null;
|
|
151
|
+
}
|
|
152
|
+
function nonempty(value) {
|
|
153
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
154
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export declare function registerAntigravityProvider(pi: Pick<ExtensionAPI, "registerProvider">): void;
|
|
3
|
+
export declare function parseProviderApiKey(value: string): {
|
|
4
|
+
token: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
};
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ANTIGRAVITY_ENDPOINTS } from "@benjamolina/antigravity-guard-core";
|
|
2
|
+
import { createPiOAuthLifecycle } from "./oauth.js";
|
|
3
|
+
import { createPiLifecycleStream, executeStreamTransport } from "./stream.js";
|
|
4
|
+
const PROVIDER = "antigravity-guard";
|
|
5
|
+
const API = "antigravity-guard-sse";
|
|
6
|
+
const MODEL = "antigravity-gemini-3.8-flash";
|
|
7
|
+
const INVALID_CREDENTIALS = "Antigravity credentials are invalid. Run /login antigravity-guard.";
|
|
8
|
+
export function registerAntigravityProvider(pi) {
|
|
9
|
+
const oauth = createPiOAuthLifecycle({ fetch: globalThis.fetch, now: Date.now });
|
|
10
|
+
pi.registerProvider(PROVIDER, {
|
|
11
|
+
name: "Antigravity Guard",
|
|
12
|
+
baseUrl: ANTIGRAVITY_ENDPOINTS.daily,
|
|
13
|
+
api: API,
|
|
14
|
+
models: [{
|
|
15
|
+
id: MODEL,
|
|
16
|
+
name: "Gemini 3.8 Flash (Antigravity, text only)",
|
|
17
|
+
reasoning: false,
|
|
18
|
+
input: ["text"],
|
|
19
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
20
|
+
contextWindow: 1_048_576,
|
|
21
|
+
maxTokens: 65_536,
|
|
22
|
+
}],
|
|
23
|
+
oauth: {
|
|
24
|
+
name: "Antigravity Guard",
|
|
25
|
+
isSubscription: true,
|
|
26
|
+
login: oauth.login,
|
|
27
|
+
refreshToken: oauth.refreshToken,
|
|
28
|
+
getApiKey: oauth.getApiKey,
|
|
29
|
+
},
|
|
30
|
+
streamSimple(model, context, options) {
|
|
31
|
+
return streamText(model, context, options);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function parseProviderApiKey(value) {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(INVALID_CREDENTIALS);
|
|
42
|
+
}
|
|
43
|
+
if (!isRecord(parsed) || Object.keys(parsed).length !== 2 || !nonempty(parsed.token) || !nonempty(parsed.projectId)) {
|
|
44
|
+
throw new Error(INVALID_CREDENTIALS);
|
|
45
|
+
}
|
|
46
|
+
return { token: parsed.token, projectId: parsed.projectId };
|
|
47
|
+
}
|
|
48
|
+
function streamText(model, context, options) {
|
|
49
|
+
return createPiLifecycleStream({
|
|
50
|
+
model,
|
|
51
|
+
now: Date.now,
|
|
52
|
+
signal: options?.signal,
|
|
53
|
+
runTransport: ({ onSemantic, signal }) => {
|
|
54
|
+
const credentials = parseProviderApiKey(options?.apiKey ?? "");
|
|
55
|
+
return executeStreamTransport({
|
|
56
|
+
accessToken: credentials.token,
|
|
57
|
+
projectId: credentials.projectId,
|
|
58
|
+
context,
|
|
59
|
+
fetch: options?.fetch ?? globalThis.fetch,
|
|
60
|
+
generationOptions: options,
|
|
61
|
+
headers: safeHeaders(options?.headers),
|
|
62
|
+
model,
|
|
63
|
+
now: Date.now,
|
|
64
|
+
onSemantic,
|
|
65
|
+
platform: process.platform,
|
|
66
|
+
requestId: `agent-${crypto.randomUUID()}`,
|
|
67
|
+
signal,
|
|
68
|
+
timeoutMs: options?.timeoutMs,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function safeHeaders(headers) {
|
|
74
|
+
if (!headers)
|
|
75
|
+
return undefined;
|
|
76
|
+
return Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null));
|
|
77
|
+
}
|
|
78
|
+
function isRecord(value) {
|
|
79
|
+
return typeof value === "object" && value !== null;
|
|
80
|
+
}
|
|
81
|
+
function nonempty(value) {
|
|
82
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
83
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type ResponseSemantic = TextSemantic | FinishSemantic | UsageSemantic;
|
|
2
|
+
export interface TextSemantic {
|
|
3
|
+
type: "text";
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface FinishSemantic {
|
|
7
|
+
type: "finish";
|
|
8
|
+
reason: "stop" | "length";
|
|
9
|
+
}
|
|
10
|
+
export interface UsageSemantic {
|
|
11
|
+
type: "usage";
|
|
12
|
+
input: number;
|
|
13
|
+
output: number;
|
|
14
|
+
cacheRead: number;
|
|
15
|
+
cacheWrite: 0;
|
|
16
|
+
total: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class ResponseSemanticError extends Error {
|
|
19
|
+
}
|
|
20
|
+
export declare class ResponseSemantics {
|
|
21
|
+
private finished;
|
|
22
|
+
private text;
|
|
23
|
+
private done;
|
|
24
|
+
private usage;
|
|
25
|
+
push(record: string): ResponseSemantic[];
|
|
26
|
+
finish(): void;
|
|
27
|
+
addText(text: string): void;
|
|
28
|
+
addFinish(): void;
|
|
29
|
+
private updateUsage;
|
|
30
|
+
}
|
package/dist/response.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export class ResponseSemanticError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export class ResponseSemantics {
|
|
4
|
+
finished = false;
|
|
5
|
+
text = false;
|
|
6
|
+
done = false;
|
|
7
|
+
usage = { prompt: 0, cacheRead: 0, output: 0 };
|
|
8
|
+
push(record) {
|
|
9
|
+
if (this.done)
|
|
10
|
+
throw new ResponseSemanticError("Response data arrived after [DONE].");
|
|
11
|
+
if (record === "[DONE]") {
|
|
12
|
+
if (!this.finished)
|
|
13
|
+
throw new ResponseSemanticError("[DONE] arrived before a finish reason.");
|
|
14
|
+
this.done = true;
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
if (this.finished)
|
|
18
|
+
throw new ResponseSemanticError("Response data arrived after a finish reason.");
|
|
19
|
+
const value = parse(record);
|
|
20
|
+
if (hasError(value))
|
|
21
|
+
throw new ResponseSemanticError("Antigravity returned an error response.");
|
|
22
|
+
const response = field(value, "response");
|
|
23
|
+
if (hasError(response) || field(response, "promptFeedback") !== undefined)
|
|
24
|
+
throw new ResponseSemanticError("Antigravity returned an invalid response.");
|
|
25
|
+
validateMetadata(response);
|
|
26
|
+
const events = [];
|
|
27
|
+
const candidates = field(response, "candidates");
|
|
28
|
+
if (candidates !== undefined)
|
|
29
|
+
candidate(candidates, events, this);
|
|
30
|
+
const usage = field(response, "usageMetadata");
|
|
31
|
+
if (usage !== undefined)
|
|
32
|
+
events.push(this.updateUsage(usage));
|
|
33
|
+
return events;
|
|
34
|
+
}
|
|
35
|
+
finish() {
|
|
36
|
+
if (!this.finished)
|
|
37
|
+
throw new ResponseSemanticError("Response ended without a finish reason.");
|
|
38
|
+
if (!this.text)
|
|
39
|
+
throw new ResponseSemanticError("Response ended without text.");
|
|
40
|
+
}
|
|
41
|
+
addText(text) { this.text = this.text || text.length > 0; }
|
|
42
|
+
addFinish() { this.finished = true; }
|
|
43
|
+
updateUsage(value) {
|
|
44
|
+
const prompt = count(value, "promptTokenCount", this.usage.prompt);
|
|
45
|
+
const cacheRead = count(value, "cachedContentTokenCount", this.usage.cacheRead);
|
|
46
|
+
const output = count(value, "candidatesTokenCount", this.usage.output) + count(value, "thoughtsTokenCount", 0);
|
|
47
|
+
const reported = field(value, "totalTokenCount");
|
|
48
|
+
if (cacheRead > prompt || (reported !== undefined && reported !== prompt + output))
|
|
49
|
+
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
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function candidate(value, events, semantics) {
|
|
55
|
+
if (!Array.isArray(value) || value.length !== 1)
|
|
56
|
+
throw new ResponseSemanticError("Antigravity returned invalid candidates.");
|
|
57
|
+
const candidate = value[0];
|
|
58
|
+
if (candidate === undefined)
|
|
59
|
+
return;
|
|
60
|
+
const index = field(candidate, "index");
|
|
61
|
+
if (index !== undefined && index !== 0)
|
|
62
|
+
throw new ResponseSemanticError("Antigravity returned invalid candidates.");
|
|
63
|
+
const content = field(candidate, "content");
|
|
64
|
+
if (content !== undefined) {
|
|
65
|
+
const parts = field(content, "parts");
|
|
66
|
+
if (!Array.isArray(parts))
|
|
67
|
+
throw new ResponseSemanticError("Antigravity returned invalid content.");
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
const text = field(part, "text");
|
|
70
|
+
if (typeof text !== "string" || Object.keys(part).some((name) => name !== "text" && name !== "thoughtSignature"))
|
|
71
|
+
throw new ResponseSemanticError("Antigravity returned unsupported content.");
|
|
72
|
+
semantics.addText(text);
|
|
73
|
+
events.push({ type: "text", text });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const finishReason = field(candidate, "finishReason");
|
|
77
|
+
if (finishReason !== undefined) {
|
|
78
|
+
if (finishReason !== "STOP" && finishReason !== "MAX_TOKENS")
|
|
79
|
+
throw new ResponseSemanticError("Antigravity returned an unsupported finish reason.");
|
|
80
|
+
semantics.addFinish();
|
|
81
|
+
events.push({ type: "finish", reason: finishReason === "STOP" ? "stop" : "length" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function validateMetadata(value) {
|
|
85
|
+
for (const name of ["responseId", "modelVersion"]) {
|
|
86
|
+
const metadata = field(value, name);
|
|
87
|
+
if (metadata !== undefined && (typeof metadata !== "string" || !metadata))
|
|
88
|
+
throw new ResponseSemanticError("Antigravity returned invalid metadata.");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function count(value, name, fallback) {
|
|
92
|
+
const valueAtName = field(value, name);
|
|
93
|
+
if (valueAtName === undefined)
|
|
94
|
+
return fallback;
|
|
95
|
+
if (typeof valueAtName !== "number" || !Number.isSafeInteger(valueAtName) || valueAtName < 0)
|
|
96
|
+
throw new ResponseSemanticError("Antigravity returned invalid usage.");
|
|
97
|
+
return valueAtName;
|
|
98
|
+
}
|
|
99
|
+
function parse(record) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(record);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new ResponseSemanticError("Antigravity returned invalid JSON.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function hasError(value) { return field(value, "error") !== undefined; }
|
|
108
|
+
function field(value, name) {
|
|
109
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
110
|
+
throw new ResponseSemanticError("Antigravity returned an invalid response.");
|
|
111
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
112
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
113
|
+
}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class SseFrameError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare class SseFramer {
|
|
4
|
+
private decoder;
|
|
5
|
+
private data;
|
|
6
|
+
private line;
|
|
7
|
+
private recordBytes;
|
|
8
|
+
private carriageReturn;
|
|
9
|
+
private bom;
|
|
10
|
+
push(bytes: Uint8Array): string[];
|
|
11
|
+
finish(): string[];
|
|
12
|
+
private append;
|
|
13
|
+
private flush;
|
|
14
|
+
private count;
|
|
15
|
+
private endLine;
|
|
16
|
+
}
|