letmecode 0.1.20 → 0.1.22
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 +10 -27
- package/ink-app/dist/index.js +68 -35
- package/ink-app/dist/providers/antigravity/models.js +46 -0
- package/ink-app/dist/providers/antigravity/provider.js +288 -0
- package/ink-app/dist/providers/antigravity/quota-parser.js +49 -0
- package/ink-app/dist/providers/antigravity/rpc/client.js +54 -0
- package/ink-app/dist/providers/antigravity/rpc/discovery.js +84 -0
- package/ink-app/dist/providers/antigravity/rpc/quota.js +25 -0
- package/ink-app/dist/providers/antigravity/rpc/usage.js +80 -0
- package/ink-app/dist/providers/antigravity/types.js +1 -0
- package/ink-app/dist/providers/antigravity/usage-parse.js +23 -0
- package/ink-app/dist/providers/antigravity.js +2 -537
- package/ink-app/dist/providers/claude.js +176 -183
- package/ink-app/dist/providers/contract.js +5 -2
- package/ink-app/dist/providers/copilot/models.js +55 -0
- package/ink-app/dist/providers/copilot/otel/configure.js +134 -0
- package/ink-app/dist/providers/copilot/otel/discover.js +94 -0
- package/ink-app/dist/providers/copilot/otel/parse.js +228 -0
- package/ink-app/dist/providers/copilot/provider.js +259 -0
- package/ink-app/dist/providers/copilot/quota.js +257 -0
- package/ink-app/dist/providers/copilot/usage/aggregate.js +84 -0
- package/ink-app/dist/providers/copilot.js +4 -373
- package/ink-app/dist/providers/index.js +1 -1
- package/ink-app/dist/providers/pricing.js +5 -0
- package/ink-app/dist/reporting.js +11 -1
- package/package.json +11 -13
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const QUOTA_WINDOWS = {
|
|
2
|
+
"5h": {
|
|
3
|
+
scope: "primary",
|
|
4
|
+
windowMinutes: 300
|
|
5
|
+
},
|
|
6
|
+
weekly: {
|
|
7
|
+
scope: "secondary",
|
|
8
|
+
windowMinutes: 10080
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export function parseAntigravityQuotaEntries(groups) {
|
|
12
|
+
return groups.flatMap((group) => {
|
|
13
|
+
const modelScope = resolveQuotaGroupScope(`${group.displayName ?? ""} ${group.description ?? ""}`);
|
|
14
|
+
if (!modelScope) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
return (group.buckets ?? []).flatMap((bucket) => {
|
|
18
|
+
const window = bucket.window
|
|
19
|
+
? QUOTA_WINDOWS[bucket.window]
|
|
20
|
+
: undefined;
|
|
21
|
+
const resetAt = Date.parse(bucket.resetTime ?? "");
|
|
22
|
+
if (!bucket.bucketId ||
|
|
23
|
+
window === undefined ||
|
|
24
|
+
!Number.isFinite(resetAt) ||
|
|
25
|
+
typeof bucket.remainingFraction !== "number" ||
|
|
26
|
+
bucket.remainingFraction < 0 ||
|
|
27
|
+
bucket.remainingFraction > 1) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return [{
|
|
31
|
+
limitId: bucket.bucketId,
|
|
32
|
+
modelScope,
|
|
33
|
+
remainingFraction: bucket.remainingFraction,
|
|
34
|
+
resetAt,
|
|
35
|
+
...window
|
|
36
|
+
}];
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function resolveQuotaGroupScope(text) {
|
|
41
|
+
const normalized = text.toLowerCase();
|
|
42
|
+
if (/gemini/.test(normalized)) {
|
|
43
|
+
return "gemini";
|
|
44
|
+
}
|
|
45
|
+
if (/claude|gpt/.test(normalized)) {
|
|
46
|
+
return "third-party";
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import https from "node:https";
|
|
2
|
+
export const ANTIGRAVITY_RPC_PATHS = {
|
|
3
|
+
quotaSummary: "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary",
|
|
4
|
+
userStatus: "/exa.language_server_pb.LanguageServerService/GetUserStatus",
|
|
5
|
+
trajectories: "/exa.language_server_pb.LanguageServerService/GetAllCascadeTrajectories",
|
|
6
|
+
trajectorySteps: "/exa.language_server_pb.LanguageServerService/GetCascadeTrajectorySteps"
|
|
7
|
+
};
|
|
8
|
+
export async function rpc(server, endpoint, payload) {
|
|
9
|
+
const body = JSON.stringify(payload ?? {});
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const request = https.request({
|
|
12
|
+
hostname: "127.0.0.1",
|
|
13
|
+
port: server.port,
|
|
14
|
+
path: endpoint,
|
|
15
|
+
method: "POST",
|
|
16
|
+
rejectUnauthorized: false,
|
|
17
|
+
timeout: 5000,
|
|
18
|
+
headers: {
|
|
19
|
+
"X-Codeium-Csrf-Token": server.csrfToken,
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
"Connect-Protocol-Version": "1",
|
|
22
|
+
"Content-Length": Buffer.byteLength(body)
|
|
23
|
+
}
|
|
24
|
+
}, (response) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
response.on("data", (chunk) => {
|
|
27
|
+
chunks.push(chunk);
|
|
28
|
+
});
|
|
29
|
+
response.on("end", () => {
|
|
30
|
+
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
31
|
+
if (response.statusCode === undefined ||
|
|
32
|
+
response.statusCode >= 300) {
|
|
33
|
+
reject(new Error(`Antigravity RPC ${endpoint} failed with status ${response.statusCode ?? "unknown"}.`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!responseBody) {
|
|
37
|
+
resolve({});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
resolve(JSON.parse(responseBody));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
reject(new Error(`Antigravity RPC ${endpoint} returned invalid JSON.`, { cause: error }));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
request.on("timeout", () => {
|
|
49
|
+
request.destroy(new Error(`Antigravity RPC ${endpoint} timed out.`));
|
|
50
|
+
});
|
|
51
|
+
request.on("error", reject);
|
|
52
|
+
request.end(body);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { ANTIGRAVITY_RPC_PATHS, rpc } from "./client.js";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export async function findAntigravityLocalServer() {
|
|
7
|
+
const processes = await findAntigravityProcesses();
|
|
8
|
+
if (processes.length === 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const portsByPid = await readListeningPortsByPid();
|
|
12
|
+
for (const process of processes) {
|
|
13
|
+
for (const port of portsByPid.get(process.pid) ?? []) {
|
|
14
|
+
const server = {
|
|
15
|
+
port,
|
|
16
|
+
csrfToken: process.csrfToken
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
const quotaSummary = await rpc(server, ANTIGRAVITY_RPC_PATHS.quotaSummary);
|
|
20
|
+
return { server, quotaSummary };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// This port does not expose the expected Antigravity RPC API.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
async function findAntigravityProcesses() {
|
|
30
|
+
const entries = await fs.promises
|
|
31
|
+
.readdir("/proc")
|
|
32
|
+
.catch(() => []);
|
|
33
|
+
const processes = [];
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (!/^\d+$/.test(entry)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const args = await fs.promises
|
|
39
|
+
.readFile(`/proc/${entry}/cmdline`, "utf8")
|
|
40
|
+
.then((value) => value.split("\0").filter(Boolean))
|
|
41
|
+
.catch(() => []);
|
|
42
|
+
const command = args.join(" ").toLowerCase();
|
|
43
|
+
if (!command.includes("antigravity") ||
|
|
44
|
+
!/(language|extension)[_-]server/.test(command)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const inlineToken = args.find((arg) => arg.startsWith("--csrf_token="));
|
|
48
|
+
const tokenIndex = args.indexOf("--csrf_token");
|
|
49
|
+
const csrfToken = inlineToken?.slice("--csrf_token=".length) ??
|
|
50
|
+
(tokenIndex >= 0 ? args[tokenIndex + 1] : undefined);
|
|
51
|
+
if (!csrfToken) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
processes.push({
|
|
55
|
+
pid: Number(entry),
|
|
56
|
+
csrfToken
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return processes;
|
|
60
|
+
}
|
|
61
|
+
async function readListeningPortsByPid() {
|
|
62
|
+
const { stdout } = await execFileAsync("ss", ["-H", "-ltnp"], {
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
timeout: 5000
|
|
65
|
+
});
|
|
66
|
+
const portsByPid = new Map();
|
|
67
|
+
for (const line of stdout.split("\n")) {
|
|
68
|
+
const loopbackPorts = [
|
|
69
|
+
...line.matchAll(/(?:127\.0\.0\.1|\[::1\]):(\d+)/g)
|
|
70
|
+
].map((match) => Number(match[1]));
|
|
71
|
+
if (loopbackPorts.length === 0) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const pidMatch of line.matchAll(/pid=(\d+),/g)) {
|
|
75
|
+
const pid = Number(pidMatch[1]);
|
|
76
|
+
const ports = portsByPid.get(pid) ?? new Set();
|
|
77
|
+
for (const port of loopbackPorts) {
|
|
78
|
+
ports.add(port);
|
|
79
|
+
}
|
|
80
|
+
portsByPid.set(pid, ports);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return new Map([...portsByPid.entries()].map(([pid, ports]) => [pid, [...ports]]));
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ANTIGRAVITY_RPC_PATHS, rpc } from "./client.js";
|
|
2
|
+
import { asRecord } from "../../limits.js";
|
|
3
|
+
const METADATA = {
|
|
4
|
+
ideName: "antigravity",
|
|
5
|
+
extensionName: "antigravity",
|
|
6
|
+
ideVersion: "unknown",
|
|
7
|
+
locale: "en"
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Pulls the raw quota groups out of a RetrieveUserQuotaSummary payload.
|
|
11
|
+
* Validation and normalization are the parser's responsibility, so this only
|
|
12
|
+
* unwraps the envelope.
|
|
13
|
+
*/
|
|
14
|
+
export function extractQuotaGroups(payload) {
|
|
15
|
+
const response = asRecord(asRecord(payload)?.response);
|
|
16
|
+
const groups = response?.groups;
|
|
17
|
+
return Array.isArray(groups) ? groups : [];
|
|
18
|
+
}
|
|
19
|
+
export async function fetchAntigravityUserStatus(server) {
|
|
20
|
+
const status = await rpc(server, ANTIGRAVITY_RPC_PATHS.userStatus, { metadata: METADATA }).catch(() => null);
|
|
21
|
+
return {
|
|
22
|
+
email: status?.userStatus?.email ?? null,
|
|
23
|
+
planName: status?.userStatus?.planStatus?.planInfo?.planName ?? null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ANTIGRAVITY_RPC_PATHS, rpc } from "./client.js";
|
|
2
|
+
const TRAJECTORY_METADATA = {
|
|
3
|
+
ideName: "antigravity",
|
|
4
|
+
extensionName: "antigravity"
|
|
5
|
+
};
|
|
6
|
+
const STEPS_CONCURRENCY = 8;
|
|
7
|
+
/**
|
|
8
|
+
* Reconstructs per-response model usage from the Antigravity local language
|
|
9
|
+
* server.
|
|
10
|
+
*
|
|
11
|
+
* Every billable model call is recorded on a trajectory step as
|
|
12
|
+
* `metadata.modelUsage`, identified by `responseId`, and usage is spread across
|
|
13
|
+
* several step types (planner responses, checkpoints, ...). This reads every
|
|
14
|
+
* step rather than filtering by type. `GetCascadeTrajectorySteps` returns the
|
|
15
|
+
* full step list for a cascade in one response, so each cascade is fetched
|
|
16
|
+
* once, and cascades are fetched concurrently. De-duplication of responses is
|
|
17
|
+
* the caller's responsibility.
|
|
18
|
+
*/
|
|
19
|
+
export async function fetchAntigravityUsageRpcData(server) {
|
|
20
|
+
const trajectories = await rpc(server, ANTIGRAVITY_RPC_PATHS.trajectories, {
|
|
21
|
+
metadata: TRAJECTORY_METADATA
|
|
22
|
+
});
|
|
23
|
+
const cascades = Object.entries(trajectories.trajectorySummaries ?? {})
|
|
24
|
+
.map(([cascadeId, summary]) => ({
|
|
25
|
+
cascadeId,
|
|
26
|
+
stepCount: summary.stepCount ?? 0
|
|
27
|
+
}))
|
|
28
|
+
.filter((cascade) => cascade.stepCount > 0);
|
|
29
|
+
const perCascade = await mapWithConcurrency(cascades, STEPS_CONCURRENCY, (cascade) => fetchCascadeUsage(server, cascade.cascadeId, cascade.stepCount));
|
|
30
|
+
return perCascade.flat();
|
|
31
|
+
}
|
|
32
|
+
async function fetchCascadeUsage(server, cascadeId, stepCount) {
|
|
33
|
+
const response = await rpc(server, ANTIGRAVITY_RPC_PATHS.trajectorySteps, {
|
|
34
|
+
cascadeId,
|
|
35
|
+
startIndex: 0,
|
|
36
|
+
endIndex: stepCount
|
|
37
|
+
});
|
|
38
|
+
const usage = [];
|
|
39
|
+
for (const step of response.steps ?? []) {
|
|
40
|
+
const modelUsage = step.metadata?.modelUsage;
|
|
41
|
+
const created = step.metadata?.createdAt;
|
|
42
|
+
const responseId = modelUsage?.responseId;
|
|
43
|
+
const model = modelUsage?.model;
|
|
44
|
+
if (!modelUsage || !created || !responseId || !model) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
usage.push({
|
|
48
|
+
responseId,
|
|
49
|
+
cascadeId,
|
|
50
|
+
created,
|
|
51
|
+
model,
|
|
52
|
+
input: toTokenCount(modelUsage.inputTokens),
|
|
53
|
+
output: toTokenCount(modelUsage.outputTokens),
|
|
54
|
+
cacheRead: toTokenCount(modelUsage.cacheReadTokens),
|
|
55
|
+
reasoning: toTokenCount(modelUsage.thinkingOutputTokens)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return usage;
|
|
59
|
+
}
|
|
60
|
+
function toTokenCount(value) {
|
|
61
|
+
const parsed = typeof value === "string" ? Number(value) : value;
|
|
62
|
+
return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : 0;
|
|
63
|
+
}
|
|
64
|
+
async function mapWithConcurrency(items, limit, task) {
|
|
65
|
+
const results = new Array(items.length);
|
|
66
|
+
let cursor = 0;
|
|
67
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
68
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
69
|
+
while (true) {
|
|
70
|
+
const index = cursor;
|
|
71
|
+
cursor += 1;
|
|
72
|
+
if (index >= items.length) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
results[index] = await task(items[index]);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
await Promise.all(workers);
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fetchAntigravityUsageRpcData } from "./rpc/usage.js";
|
|
2
|
+
export async function collectUsageFromLocalRpc(server, options) {
|
|
3
|
+
const usage = await fetchAntigravityUsageRpcData(server);
|
|
4
|
+
options?.traceLogger?.log(`[Antigravity] usage: ${JSON.stringify(usage, null, 2)}`);
|
|
5
|
+
return usage.flatMap((entry) => {
|
|
6
|
+
const timestamp = Date.parse(entry.created);
|
|
7
|
+
if (!Number.isFinite(timestamp)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
return [{
|
|
11
|
+
type: "usage",
|
|
12
|
+
sessionId: entry.cascadeId,
|
|
13
|
+
responseId: entry.responseId,
|
|
14
|
+
timestamp,
|
|
15
|
+
modelId: entry.model,
|
|
16
|
+
input: entry.input,
|
|
17
|
+
cacheRead: entry.cacheRead,
|
|
18
|
+
cacheWrite: 0,
|
|
19
|
+
output: entry.output,
|
|
20
|
+
reasoning: entry.reasoning
|
|
21
|
+
}];
|
|
22
|
+
});
|
|
23
|
+
}
|