@hasna/economy 0.2.15 → 0.2.17
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/dist/cli/index.js +257 -25
- package/dist/db/database.d.ts +13 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +102 -24
- package/dist/ingest/billing.d.ts +18 -0
- package/dist/ingest/billing.d.ts.map +1 -0
- package/dist/mcp/index.js +79 -24
- package/dist/server/index.js +79 -24
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -96,9 +96,9 @@ var init_pricing = __esm(() => {
|
|
|
96
96
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
97
|
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
98
98
|
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
99
|
-
"gpt-5.4": { inputPer1M: 2.5, outputPer1M:
|
|
100
|
-
"gpt-5.4-pro": { inputPer1M:
|
|
101
|
-
"gpt-5.4-mini": { inputPer1M: 0.
|
|
99
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
100
|
+
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
101
|
+
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
102
102
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
103
103
|
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
104
104
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
@@ -134,6 +134,7 @@ __export(exports_database, {
|
|
|
134
134
|
upsertModelPricing: () => upsertModelPricing,
|
|
135
135
|
upsertGoal: () => upsertGoal,
|
|
136
136
|
upsertBudget: () => upsertBudget,
|
|
137
|
+
upsertBillingDaily: () => upsertBillingDaily,
|
|
137
138
|
setIngestState: () => setIngestState,
|
|
138
139
|
seedModelPricing: () => seedModelPricing,
|
|
139
140
|
rollupSession: () => rollupSession,
|
|
@@ -144,6 +145,7 @@ __export(exports_database, {
|
|
|
144
145
|
queryProjectBreakdown: () => queryProjectBreakdown,
|
|
145
146
|
queryModelBreakdown: () => queryModelBreakdown,
|
|
146
147
|
queryDailyBreakdown: () => queryDailyBreakdown,
|
|
148
|
+
queryBillingSummary: () => queryBillingSummary,
|
|
147
149
|
openDatabase: () => openDatabase,
|
|
148
150
|
listProjects: () => listProjects,
|
|
149
151
|
listModelPricing: () => listModelPricing,
|
|
@@ -161,7 +163,8 @@ __export(exports_database, {
|
|
|
161
163
|
deleteProject: () => deleteProject,
|
|
162
164
|
deleteModelPricing: () => deleteModelPricing,
|
|
163
165
|
deleteGoal: () => deleteGoal,
|
|
164
|
-
deleteBudget: () => deleteBudget
|
|
166
|
+
deleteBudget: () => deleteBudget,
|
|
167
|
+
clearBillingRange: () => clearBillingRange
|
|
165
168
|
});
|
|
166
169
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
167
170
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
@@ -309,6 +312,18 @@ function initSchema(db) {
|
|
|
309
312
|
machine_id TEXT,
|
|
310
313
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
311
314
|
);
|
|
315
|
+
|
|
316
|
+
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
317
|
+
date TEXT NOT NULL,
|
|
318
|
+
provider TEXT NOT NULL,
|
|
319
|
+
description TEXT DEFAULT '',
|
|
320
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
321
|
+
updated_at TEXT NOT NULL,
|
|
322
|
+
PRIMARY KEY (date, provider, description)
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
326
|
+
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
312
327
|
`);
|
|
313
328
|
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
314
329
|
if (!cols.some((c) => c.name === "machine_id")) {
|
|
@@ -327,11 +342,11 @@ function periodWhere(period) {
|
|
|
327
342
|
case "yesterday":
|
|
328
343
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
329
344
|
case "week":
|
|
330
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
345
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
331
346
|
case "month":
|
|
332
|
-
return `timestamp >= DATE('now', '
|
|
347
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
333
348
|
case "year":
|
|
334
|
-
return `timestamp >= DATE('now', '
|
|
349
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
335
350
|
case "all":
|
|
336
351
|
return "1=1";
|
|
337
352
|
}
|
|
@@ -343,11 +358,11 @@ function sessionPeriodWhere(period) {
|
|
|
343
358
|
case "yesterday":
|
|
344
359
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
345
360
|
case "week":
|
|
346
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
361
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
347
362
|
case "month":
|
|
348
|
-
return `started_at >= DATE('now', '
|
|
363
|
+
return `started_at >= DATE('now', 'start of month')`;
|
|
349
364
|
case "year":
|
|
350
|
-
return `started_at >= DATE('now', '
|
|
365
|
+
return `started_at >= DATE('now', 'start of year')`;
|
|
351
366
|
case "all":
|
|
352
367
|
return "1=1";
|
|
353
368
|
}
|
|
@@ -457,23 +472,66 @@ function queryModelBreakdown(db) {
|
|
|
457
472
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
458
473
|
`).all();
|
|
459
474
|
}
|
|
475
|
+
function labelForPath(projectPath, projectName) {
|
|
476
|
+
if (projectName && projectName.trim() !== "")
|
|
477
|
+
return projectName;
|
|
478
|
+
if (!projectPath)
|
|
479
|
+
return "";
|
|
480
|
+
const segments = projectPath.split("/").filter(Boolean);
|
|
481
|
+
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
482
|
+
for (const seg of segments) {
|
|
483
|
+
if (projectPrefix.test(seg))
|
|
484
|
+
return seg;
|
|
485
|
+
}
|
|
486
|
+
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
487
|
+
for (let i = segments.length - 1;i >= 0; i--) {
|
|
488
|
+
if (!generic.has(segments[i].toLowerCase()))
|
|
489
|
+
return segments[i];
|
|
490
|
+
}
|
|
491
|
+
return segments[segments.length - 1] ?? projectPath;
|
|
492
|
+
}
|
|
460
493
|
function queryProjectBreakdown(db) {
|
|
461
|
-
|
|
462
|
-
SELECT
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
466
|
-
COUNT(r.id) as requests,
|
|
467
|
-
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
468
|
-
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
469
|
-
MAX(s.started_at) as last_active
|
|
470
|
-
FROM sessions s
|
|
471
|
-
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
472
|
-
LEFT JOIN requests r ON r.session_id = s.id
|
|
473
|
-
WHERE s.project_path != '' OR s.project_name != ''
|
|
474
|
-
GROUP BY s.project_path
|
|
475
|
-
ORDER BY cost_usd DESC
|
|
494
|
+
const sessions = db.prepare(`
|
|
495
|
+
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
496
|
+
FROM sessions
|
|
497
|
+
WHERE project_path != '' OR project_name != ''
|
|
476
498
|
`).all();
|
|
499
|
+
const groups = new Map;
|
|
500
|
+
for (const s of sessions) {
|
|
501
|
+
const label = labelForPath(s.project_path, s.project_name);
|
|
502
|
+
if (!label)
|
|
503
|
+
continue;
|
|
504
|
+
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
505
|
+
g.sessionIds.push(s.id);
|
|
506
|
+
g.totalCost += s.total_cost_usd || 0;
|
|
507
|
+
if (!g.lastActive || s.started_at > g.lastActive)
|
|
508
|
+
g.lastActive = s.started_at;
|
|
509
|
+
if (!g.samplePath)
|
|
510
|
+
g.samplePath = s.project_path;
|
|
511
|
+
groups.set(label, g);
|
|
512
|
+
}
|
|
513
|
+
const result = [];
|
|
514
|
+
for (const [label, g] of groups.entries()) {
|
|
515
|
+
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
516
|
+
const reqStats = placeholders.length ? db.prepare(`
|
|
517
|
+
SELECT
|
|
518
|
+
COUNT(*) as requests,
|
|
519
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
520
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
521
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
522
|
+
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
523
|
+
result.push({
|
|
524
|
+
project_path: g.samplePath,
|
|
525
|
+
project_name: label,
|
|
526
|
+
sessions: g.sessionIds.length,
|
|
527
|
+
requests: reqStats.requests,
|
|
528
|
+
total_tokens: reqStats.total_tokens,
|
|
529
|
+
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
530
|
+
last_active: g.lastActive
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
534
|
+
return result;
|
|
477
535
|
}
|
|
478
536
|
function queryDailyBreakdown(db, days = 30) {
|
|
479
537
|
return db.prepare(`
|
|
@@ -591,6 +649,26 @@ function setIngestState(db, source, key, value) {
|
|
|
591
649
|
function queryRequestsSince(db, since) {
|
|
592
650
|
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
593
651
|
}
|
|
652
|
+
function upsertBillingDaily(db, row) {
|
|
653
|
+
db.prepare(`
|
|
654
|
+
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
655
|
+
VALUES (?, ?, ?, ?, ?)
|
|
656
|
+
`).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
|
|
657
|
+
}
|
|
658
|
+
function clearBillingRange(db, provider, fromDate, toDate) {
|
|
659
|
+
db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
|
|
660
|
+
}
|
|
661
|
+
function queryBillingSummary(db, period) {
|
|
662
|
+
const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
|
|
663
|
+
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
664
|
+
const by_provider = {};
|
|
665
|
+
let total = 0;
|
|
666
|
+
for (const r of rows) {
|
|
667
|
+
by_provider[r.provider] = r.cost;
|
|
668
|
+
total += r.cost;
|
|
669
|
+
}
|
|
670
|
+
return { total_usd: total, by_provider };
|
|
671
|
+
}
|
|
594
672
|
function listMachines(db) {
|
|
595
673
|
return db.prepare(`
|
|
596
674
|
SELECT
|
|
@@ -1971,6 +2049,119 @@ init_claude();
|
|
|
1971
2049
|
init_codex();
|
|
1972
2050
|
init_gemini();
|
|
1973
2051
|
|
|
2052
|
+
// src/ingest/billing.ts
|
|
2053
|
+
init_database();
|
|
2054
|
+
function getAnthropicAdminKey() {
|
|
2055
|
+
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
2056
|
+
}
|
|
2057
|
+
function getOpenAIAdminKey() {
|
|
2058
|
+
return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
|
|
2059
|
+
}
|
|
2060
|
+
function toISODate(d) {
|
|
2061
|
+
return d.toISOString().substring(0, 10);
|
|
2062
|
+
}
|
|
2063
|
+
async function syncAnthropicBilling(db, opts = {}) {
|
|
2064
|
+
const key = getAnthropicAdminKey();
|
|
2065
|
+
if (!key)
|
|
2066
|
+
throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
|
|
2067
|
+
const now = new Date;
|
|
2068
|
+
const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
|
|
2069
|
+
const days = opts.days ?? 31;
|
|
2070
|
+
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
2071
|
+
const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
|
|
2072
|
+
const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
|
|
2073
|
+
let totalUsd = 0;
|
|
2074
|
+
const buckets = [];
|
|
2075
|
+
let nextPage;
|
|
2076
|
+
do {
|
|
2077
|
+
const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
|
|
2078
|
+
url.searchParams.set("starting_at", startIso);
|
|
2079
|
+
url.searchParams.set("ending_at", endIso);
|
|
2080
|
+
url.searchParams.set("bucket_width", "1d");
|
|
2081
|
+
url.searchParams.set("limit", "31");
|
|
2082
|
+
url.searchParams.append("group_by[]", "description");
|
|
2083
|
+
if (nextPage)
|
|
2084
|
+
url.searchParams.set("page", nextPage);
|
|
2085
|
+
const res = await fetch(url.toString(), {
|
|
2086
|
+
headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
|
|
2087
|
+
});
|
|
2088
|
+
const data = await res.json();
|
|
2089
|
+
if (data.error)
|
|
2090
|
+
throw new Error(`Anthropic API: ${data.error.message}`);
|
|
2091
|
+
if (data.data)
|
|
2092
|
+
buckets.push(...data.data);
|
|
2093
|
+
nextPage = data.has_more ? data.next_page : undefined;
|
|
2094
|
+
} while (nextPage);
|
|
2095
|
+
const fromDateStr = toISODate(start);
|
|
2096
|
+
const toDateStr = toISODate(new Date(end.getTime() - 1000));
|
|
2097
|
+
clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
|
|
2098
|
+
const updatedAt = new Date().toISOString();
|
|
2099
|
+
for (const bucket of buckets) {
|
|
2100
|
+
const date = bucket.starting_at.substring(0, 10);
|
|
2101
|
+
for (const r of bucket.results) {
|
|
2102
|
+
const usd = Number(r.amount) / 100;
|
|
2103
|
+
if (usd === 0)
|
|
2104
|
+
continue;
|
|
2105
|
+
const desc = (r.description ?? "unknown").substring(0, 200);
|
|
2106
|
+
upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
|
|
2107
|
+
totalUsd += usd;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return { days: buckets.length, totalUsd };
|
|
2111
|
+
}
|
|
2112
|
+
async function syncOpenAIBilling(db, opts = {}) {
|
|
2113
|
+
const key = getOpenAIAdminKey();
|
|
2114
|
+
if (!key)
|
|
2115
|
+
throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
|
|
2116
|
+
const now = new Date;
|
|
2117
|
+
const end = opts.toDate ? new Date(opts.toDate) : now;
|
|
2118
|
+
const days = opts.days ?? 31;
|
|
2119
|
+
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
2120
|
+
const startSec = Math.floor(start.getTime() / 1000);
|
|
2121
|
+
const endSec = Math.floor(end.getTime() / 1000);
|
|
2122
|
+
let totalUsd = 0;
|
|
2123
|
+
const buckets = [];
|
|
2124
|
+
let nextPage;
|
|
2125
|
+
do {
|
|
2126
|
+
const url = new URL("https://api.openai.com/v1/organization/costs");
|
|
2127
|
+
url.searchParams.set("start_time", String(startSec));
|
|
2128
|
+
url.searchParams.set("end_time", String(endSec));
|
|
2129
|
+
url.searchParams.set("bucket_width", "1d");
|
|
2130
|
+
url.searchParams.set("limit", "31");
|
|
2131
|
+
url.searchParams.append("group_by[]", "line_item");
|
|
2132
|
+
if (nextPage)
|
|
2133
|
+
url.searchParams.set("page", nextPage);
|
|
2134
|
+
const res = await fetch(url.toString(), {
|
|
2135
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
2136
|
+
});
|
|
2137
|
+
const data = await res.json();
|
|
2138
|
+
if (data.error)
|
|
2139
|
+
throw new Error(`OpenAI API: ${data.error.message}`);
|
|
2140
|
+
if (data.data)
|
|
2141
|
+
buckets.push(...data.data);
|
|
2142
|
+
nextPage = data.has_more ? data.next_page : undefined;
|
|
2143
|
+
} while (nextPage);
|
|
2144
|
+
const fromDateStr = toISODate(start);
|
|
2145
|
+
const toDateStr = toISODate(new Date(end.getTime() - 1000));
|
|
2146
|
+
clearBillingRange(db, "openai", fromDateStr, toDateStr);
|
|
2147
|
+
const updatedAt = new Date().toISOString();
|
|
2148
|
+
for (const bucket of buckets) {
|
|
2149
|
+
const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
|
|
2150
|
+
for (const r of bucket.results) {
|
|
2151
|
+
const usd = Number(r.amount?.value ?? 0);
|
|
2152
|
+
if (usd === 0)
|
|
2153
|
+
continue;
|
|
2154
|
+
const desc = (r.line_item ?? "unknown").substring(0, 200);
|
|
2155
|
+
upsertBillingDaily(db, { date, provider: "openai", description: desc, cost_usd: usd, updated_at: updatedAt });
|
|
2156
|
+
totalUsd += usd;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return { days: buckets.length, totalUsd };
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/cli/index.ts
|
|
2163
|
+
init_database();
|
|
2164
|
+
|
|
1974
2165
|
// src/lib/package-metadata.ts
|
|
1975
2166
|
import { readFileSync as readFileSync5 } from "fs";
|
|
1976
2167
|
var cachedMetadata = null;
|
|
@@ -3035,5 +3226,46 @@ cloudCmd.command("status").description("Check cloud connection status").action(a
|
|
|
3035
3226
|
}
|
|
3036
3227
|
console.log();
|
|
3037
3228
|
});
|
|
3229
|
+
var billingCmd = program.command("billing").description("Pull actual billing from provider admin APIs (ground truth)");
|
|
3230
|
+
billingCmd.command("sync").description("Sync actual billing from Anthropic and OpenAI admin APIs").option("--days <n>", "Days of history to fetch", "31").option("--anthropic", "Only sync Anthropic").option("--openai", "Only sync OpenAI").action(async (opts) => {
|
|
3231
|
+
const db = openDatabase();
|
|
3232
|
+
const days = Number(opts.days ?? 31);
|
|
3233
|
+
const doBoth = !opts.anthropic && !opts.openai;
|
|
3234
|
+
if (opts.anthropic || doBoth) {
|
|
3235
|
+
process.stdout.write(chalk4.cyan("\u2192 Syncing Anthropic billing... "));
|
|
3236
|
+
try {
|
|
3237
|
+
const r = await syncAnthropicBilling(db, { days });
|
|
3238
|
+
console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
3239
|
+
} catch (e) {
|
|
3240
|
+
console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
if (opts.openai || doBoth) {
|
|
3244
|
+
process.stdout.write(chalk4.cyan("\u2192 Syncing OpenAI billing... "));
|
|
3245
|
+
try {
|
|
3246
|
+
const r = await syncOpenAIBilling(db, { days });
|
|
3247
|
+
console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
3248
|
+
} catch (e) {
|
|
3249
|
+
console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
3253
|
+
billingCmd.command("show").description("Show actual billing totals vs our estimated costs").option("--period <p>", "Period: today|yesterday|week|month|year|all", "month").action((opts) => {
|
|
3254
|
+
const db = openDatabase();
|
|
3255
|
+
const period = opts.period ?? "month";
|
|
3256
|
+
const actual = queryBillingSummary(db, period);
|
|
3257
|
+
const estimated = querySummary(db, period);
|
|
3258
|
+
console.log();
|
|
3259
|
+
console.log(chalk4.bold.cyan(` Billing ${period} (actual from admin APIs)
|
|
3260
|
+
`));
|
|
3261
|
+
printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [chalk4.white(p), fmt2(c)]));
|
|
3262
|
+
console.log();
|
|
3263
|
+
console.log(` ${chalk4.bold("Actual total:")} ${fmt2(actual.total_usd)}`);
|
|
3264
|
+
console.log(` ${chalk4.dim("Our estimate:")} ${fmt2(estimated.total_usd)}`);
|
|
3265
|
+
const diff = estimated.total_usd - actual.total_usd;
|
|
3266
|
+
const pct = actual.total_usd > 0 ? diff / actual.total_usd * 100 : 0;
|
|
3267
|
+
console.log(` ${chalk4.dim("Difference:")} ${fmt2(Math.abs(diff))} (${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%)`);
|
|
3268
|
+
console.log();
|
|
3269
|
+
});
|
|
3038
3270
|
registerBrainsCommand(program);
|
|
3039
3271
|
program.parse();
|
package/dist/db/database.d.ts
CHANGED
|
@@ -48,6 +48,19 @@ export declare function getGoalStatuses(db: Database): GoalStatus[];
|
|
|
48
48
|
export declare function getIngestState(db: Database, source: string, key: string): string | null;
|
|
49
49
|
export declare function setIngestState(db: Database, source: string, key: string, value: string): void;
|
|
50
50
|
export declare function queryRequestsSince(db: Database, since: string): EconomyRequest[];
|
|
51
|
+
export interface BillingDaily {
|
|
52
|
+
date: string;
|
|
53
|
+
provider: 'anthropic' | 'openai' | string;
|
|
54
|
+
description: string;
|
|
55
|
+
cost_usd: number;
|
|
56
|
+
updated_at: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function upsertBillingDaily(db: Database, row: BillingDaily): void;
|
|
59
|
+
export declare function clearBillingRange(db: Database, provider: string, fromDate: string, toDate: string): void;
|
|
60
|
+
export declare function queryBillingSummary(db: Database, period: Period): {
|
|
61
|
+
total_usd: number;
|
|
62
|
+
by_provider: Record<string, number>;
|
|
63
|
+
};
|
|
51
64
|
export interface MachineInfo {
|
|
52
65
|
machine_id: string;
|
|
53
66
|
sessions: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAKxD,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,MAAM,EACN,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,MAAM,EACN,aAAa,EACd,MAAM,mBAAmB,CAAA;AAE1B,wBAAgB,YAAY,IAAI,MAAM,CAKrC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAkBnC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAIlC;AAED,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,QAAQ,CAgBxE;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -94,9 +94,9 @@ var init_pricing = __esm(() => {
|
|
|
94
94
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
95
95
|
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
96
96
|
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
|
-
"gpt-5.4": { inputPer1M: 2.5, outputPer1M:
|
|
98
|
-
"gpt-5.4-pro": { inputPer1M:
|
|
99
|
-
"gpt-5.4-mini": { inputPer1M: 0.
|
|
97
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
98
|
+
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
99
|
+
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
100
100
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
101
101
|
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
102
102
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
@@ -270,6 +270,18 @@ function initSchema(db) {
|
|
|
270
270
|
machine_id TEXT,
|
|
271
271
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
272
272
|
);
|
|
273
|
+
|
|
274
|
+
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
275
|
+
date TEXT NOT NULL,
|
|
276
|
+
provider TEXT NOT NULL,
|
|
277
|
+
description TEXT DEFAULT '',
|
|
278
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
279
|
+
updated_at TEXT NOT NULL,
|
|
280
|
+
PRIMARY KEY (date, provider, description)
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
273
285
|
`);
|
|
274
286
|
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
275
287
|
if (!cols.some((c) => c.name === "machine_id")) {
|
|
@@ -288,11 +300,11 @@ function periodWhere(period) {
|
|
|
288
300
|
case "yesterday":
|
|
289
301
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
290
302
|
case "week":
|
|
291
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
303
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
292
304
|
case "month":
|
|
293
|
-
return `timestamp >= DATE('now', '
|
|
305
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
294
306
|
case "year":
|
|
295
|
-
return `timestamp >= DATE('now', '
|
|
307
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
296
308
|
case "all":
|
|
297
309
|
return "1=1";
|
|
298
310
|
}
|
|
@@ -304,11 +316,11 @@ function sessionPeriodWhere(period) {
|
|
|
304
316
|
case "yesterday":
|
|
305
317
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
306
318
|
case "week":
|
|
307
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
319
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
308
320
|
case "month":
|
|
309
|
-
return `started_at >= DATE('now', '
|
|
321
|
+
return `started_at >= DATE('now', 'start of month')`;
|
|
310
322
|
case "year":
|
|
311
|
-
return `started_at >= DATE('now', '
|
|
323
|
+
return `started_at >= DATE('now', 'start of year')`;
|
|
312
324
|
case "all":
|
|
313
325
|
return "1=1";
|
|
314
326
|
}
|
|
@@ -418,23 +430,66 @@ function queryModelBreakdown(db) {
|
|
|
418
430
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
419
431
|
`).all();
|
|
420
432
|
}
|
|
433
|
+
function labelForPath(projectPath, projectName) {
|
|
434
|
+
if (projectName && projectName.trim() !== "")
|
|
435
|
+
return projectName;
|
|
436
|
+
if (!projectPath)
|
|
437
|
+
return "";
|
|
438
|
+
const segments = projectPath.split("/").filter(Boolean);
|
|
439
|
+
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
440
|
+
for (const seg of segments) {
|
|
441
|
+
if (projectPrefix.test(seg))
|
|
442
|
+
return seg;
|
|
443
|
+
}
|
|
444
|
+
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
445
|
+
for (let i = segments.length - 1;i >= 0; i--) {
|
|
446
|
+
if (!generic.has(segments[i].toLowerCase()))
|
|
447
|
+
return segments[i];
|
|
448
|
+
}
|
|
449
|
+
return segments[segments.length - 1] ?? projectPath;
|
|
450
|
+
}
|
|
421
451
|
function queryProjectBreakdown(db) {
|
|
422
|
-
|
|
423
|
-
SELECT
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
427
|
-
COUNT(r.id) as requests,
|
|
428
|
-
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
429
|
-
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
430
|
-
MAX(s.started_at) as last_active
|
|
431
|
-
FROM sessions s
|
|
432
|
-
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
433
|
-
LEFT JOIN requests r ON r.session_id = s.id
|
|
434
|
-
WHERE s.project_path != '' OR s.project_name != ''
|
|
435
|
-
GROUP BY s.project_path
|
|
436
|
-
ORDER BY cost_usd DESC
|
|
452
|
+
const sessions = db.prepare(`
|
|
453
|
+
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
454
|
+
FROM sessions
|
|
455
|
+
WHERE project_path != '' OR project_name != ''
|
|
437
456
|
`).all();
|
|
457
|
+
const groups = new Map;
|
|
458
|
+
for (const s of sessions) {
|
|
459
|
+
const label = labelForPath(s.project_path, s.project_name);
|
|
460
|
+
if (!label)
|
|
461
|
+
continue;
|
|
462
|
+
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
463
|
+
g.sessionIds.push(s.id);
|
|
464
|
+
g.totalCost += s.total_cost_usd || 0;
|
|
465
|
+
if (!g.lastActive || s.started_at > g.lastActive)
|
|
466
|
+
g.lastActive = s.started_at;
|
|
467
|
+
if (!g.samplePath)
|
|
468
|
+
g.samplePath = s.project_path;
|
|
469
|
+
groups.set(label, g);
|
|
470
|
+
}
|
|
471
|
+
const result = [];
|
|
472
|
+
for (const [label, g] of groups.entries()) {
|
|
473
|
+
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
474
|
+
const reqStats = placeholders.length ? db.prepare(`
|
|
475
|
+
SELECT
|
|
476
|
+
COUNT(*) as requests,
|
|
477
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
478
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
479
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
480
|
+
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
481
|
+
result.push({
|
|
482
|
+
project_path: g.samplePath,
|
|
483
|
+
project_name: label,
|
|
484
|
+
sessions: g.sessionIds.length,
|
|
485
|
+
requests: reqStats.requests,
|
|
486
|
+
total_tokens: reqStats.total_tokens,
|
|
487
|
+
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
488
|
+
last_active: g.lastActive
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
492
|
+
return result;
|
|
438
493
|
}
|
|
439
494
|
function queryDailyBreakdown(db, days = 30) {
|
|
440
495
|
return db.prepare(`
|
|
@@ -552,6 +607,26 @@ function setIngestState(db, source, key, value) {
|
|
|
552
607
|
function queryRequestsSince(db, since) {
|
|
553
608
|
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
554
609
|
}
|
|
610
|
+
function upsertBillingDaily(db, row) {
|
|
611
|
+
db.prepare(`
|
|
612
|
+
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
613
|
+
VALUES (?, ?, ?, ?, ?)
|
|
614
|
+
`).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
|
|
615
|
+
}
|
|
616
|
+
function clearBillingRange(db, provider, fromDate, toDate) {
|
|
617
|
+
db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
|
|
618
|
+
}
|
|
619
|
+
function queryBillingSummary(db, period) {
|
|
620
|
+
const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
|
|
621
|
+
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
622
|
+
const by_provider = {};
|
|
623
|
+
let total = 0;
|
|
624
|
+
for (const r of rows) {
|
|
625
|
+
by_provider[r.provider] = r.cost;
|
|
626
|
+
total += r.cost;
|
|
627
|
+
}
|
|
628
|
+
return { total_usd: total, by_provider };
|
|
629
|
+
}
|
|
555
630
|
function listMachines(db) {
|
|
556
631
|
return db.prepare(`
|
|
557
632
|
SELECT
|
|
@@ -1037,6 +1112,7 @@ export {
|
|
|
1037
1112
|
upsertModelPricing,
|
|
1038
1113
|
upsertGoal,
|
|
1039
1114
|
upsertBudget,
|
|
1115
|
+
upsertBillingDaily,
|
|
1040
1116
|
setIngestState,
|
|
1041
1117
|
setActiveModel,
|
|
1042
1118
|
seedModelPricing,
|
|
@@ -1049,6 +1125,7 @@ export {
|
|
|
1049
1125
|
queryProjectBreakdown,
|
|
1050
1126
|
queryModelBreakdown,
|
|
1051
1127
|
queryDailyBreakdown,
|
|
1128
|
+
queryBillingSummary,
|
|
1052
1129
|
openDatabase,
|
|
1053
1130
|
normalizeModelName,
|
|
1054
1131
|
listProjects,
|
|
@@ -1078,6 +1155,7 @@ export {
|
|
|
1078
1155
|
deleteBudget,
|
|
1079
1156
|
computeCostFromDb,
|
|
1080
1157
|
computeCost,
|
|
1158
|
+
clearBillingRange,
|
|
1081
1159
|
clearActiveModel,
|
|
1082
1160
|
DEFAULT_PRICING,
|
|
1083
1161
|
DEFAULT_MODEL
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SqliteAdapter as Database } from '@hasna/cloud';
|
|
2
|
+
export declare function syncAnthropicBilling(db: Database, opts?: {
|
|
3
|
+
days?: number;
|
|
4
|
+
fromDate?: string;
|
|
5
|
+
toDate?: string;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
days: number;
|
|
8
|
+
totalUsd: number;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function syncOpenAIBilling(db: Database, opts?: {
|
|
11
|
+
days?: number;
|
|
12
|
+
fromDate?: string;
|
|
13
|
+
toDate?: string;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
days: number;
|
|
16
|
+
totalUsd: number;
|
|
17
|
+
}>;
|
|
18
|
+
//# sourceMappingURL=billing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../../src/ingest/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAgC7D,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,QAAQ,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/D,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAuD7C;AAeD,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,QAAQ,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/D,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAqD7C"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -95,9 +95,9 @@ var init_pricing = __esm(() => {
|
|
|
95
95
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
96
96
|
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
97
|
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
98
|
-
"gpt-5.4": { inputPer1M: 2.5, outputPer1M:
|
|
99
|
-
"gpt-5.4-pro": { inputPer1M:
|
|
100
|
-
"gpt-5.4-mini": { inputPer1M: 0.
|
|
98
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
99
|
+
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
100
|
+
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
101
101
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
102
102
|
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
103
103
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
@@ -271,6 +271,18 @@ function initSchema(db) {
|
|
|
271
271
|
machine_id TEXT,
|
|
272
272
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
273
273
|
);
|
|
274
|
+
|
|
275
|
+
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
276
|
+
date TEXT NOT NULL,
|
|
277
|
+
provider TEXT NOT NULL,
|
|
278
|
+
description TEXT DEFAULT '',
|
|
279
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
280
|
+
updated_at TEXT NOT NULL,
|
|
281
|
+
PRIMARY KEY (date, provider, description)
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
285
|
+
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
274
286
|
`);
|
|
275
287
|
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
276
288
|
if (!cols.some((c) => c.name === "machine_id")) {
|
|
@@ -289,11 +301,11 @@ function periodWhere(period) {
|
|
|
289
301
|
case "yesterday":
|
|
290
302
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
291
303
|
case "week":
|
|
292
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
304
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
293
305
|
case "month":
|
|
294
|
-
return `timestamp >= DATE('now', '
|
|
306
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
295
307
|
case "year":
|
|
296
|
-
return `timestamp >= DATE('now', '
|
|
308
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
297
309
|
case "all":
|
|
298
310
|
return "1=1";
|
|
299
311
|
}
|
|
@@ -305,11 +317,11 @@ function sessionPeriodWhere(period) {
|
|
|
305
317
|
case "yesterday":
|
|
306
318
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
307
319
|
case "week":
|
|
308
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
320
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
309
321
|
case "month":
|
|
310
|
-
return `started_at >= DATE('now', '
|
|
322
|
+
return `started_at >= DATE('now', 'start of month')`;
|
|
311
323
|
case "year":
|
|
312
|
-
return `started_at >= DATE('now', '
|
|
324
|
+
return `started_at >= DATE('now', 'start of year')`;
|
|
313
325
|
case "all":
|
|
314
326
|
return "1=1";
|
|
315
327
|
}
|
|
@@ -419,23 +431,66 @@ function queryModelBreakdown(db) {
|
|
|
419
431
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
420
432
|
`).all();
|
|
421
433
|
}
|
|
434
|
+
function labelForPath(projectPath, projectName) {
|
|
435
|
+
if (projectName && projectName.trim() !== "")
|
|
436
|
+
return projectName;
|
|
437
|
+
if (!projectPath)
|
|
438
|
+
return "";
|
|
439
|
+
const segments = projectPath.split("/").filter(Boolean);
|
|
440
|
+
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
441
|
+
for (const seg of segments) {
|
|
442
|
+
if (projectPrefix.test(seg))
|
|
443
|
+
return seg;
|
|
444
|
+
}
|
|
445
|
+
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
446
|
+
for (let i = segments.length - 1;i >= 0; i--) {
|
|
447
|
+
if (!generic.has(segments[i].toLowerCase()))
|
|
448
|
+
return segments[i];
|
|
449
|
+
}
|
|
450
|
+
return segments[segments.length - 1] ?? projectPath;
|
|
451
|
+
}
|
|
422
452
|
function queryProjectBreakdown(db) {
|
|
423
|
-
|
|
424
|
-
SELECT
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
428
|
-
COUNT(r.id) as requests,
|
|
429
|
-
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
430
|
-
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
431
|
-
MAX(s.started_at) as last_active
|
|
432
|
-
FROM sessions s
|
|
433
|
-
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
434
|
-
LEFT JOIN requests r ON r.session_id = s.id
|
|
435
|
-
WHERE s.project_path != '' OR s.project_name != ''
|
|
436
|
-
GROUP BY s.project_path
|
|
437
|
-
ORDER BY cost_usd DESC
|
|
453
|
+
const sessions = db.prepare(`
|
|
454
|
+
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
455
|
+
FROM sessions
|
|
456
|
+
WHERE project_path != '' OR project_name != ''
|
|
438
457
|
`).all();
|
|
458
|
+
const groups = new Map;
|
|
459
|
+
for (const s of sessions) {
|
|
460
|
+
const label = labelForPath(s.project_path, s.project_name);
|
|
461
|
+
if (!label)
|
|
462
|
+
continue;
|
|
463
|
+
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
464
|
+
g.sessionIds.push(s.id);
|
|
465
|
+
g.totalCost += s.total_cost_usd || 0;
|
|
466
|
+
if (!g.lastActive || s.started_at > g.lastActive)
|
|
467
|
+
g.lastActive = s.started_at;
|
|
468
|
+
if (!g.samplePath)
|
|
469
|
+
g.samplePath = s.project_path;
|
|
470
|
+
groups.set(label, g);
|
|
471
|
+
}
|
|
472
|
+
const result = [];
|
|
473
|
+
for (const [label, g] of groups.entries()) {
|
|
474
|
+
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
475
|
+
const reqStats = placeholders.length ? db.prepare(`
|
|
476
|
+
SELECT
|
|
477
|
+
COUNT(*) as requests,
|
|
478
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
479
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
480
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
481
|
+
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
482
|
+
result.push({
|
|
483
|
+
project_path: g.samplePath,
|
|
484
|
+
project_name: label,
|
|
485
|
+
sessions: g.sessionIds.length,
|
|
486
|
+
requests: reqStats.requests,
|
|
487
|
+
total_tokens: reqStats.total_tokens,
|
|
488
|
+
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
489
|
+
last_active: g.lastActive
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
493
|
+
return result;
|
|
439
494
|
}
|
|
440
495
|
function queryDailyBreakdown(db, days = 30) {
|
|
441
496
|
return db.prepare(`
|
package/dist/server/index.js
CHANGED
|
@@ -96,9 +96,9 @@ var init_pricing = __esm(() => {
|
|
|
96
96
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
97
|
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
98
98
|
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
99
|
-
"gpt-5.4": { inputPer1M: 2.5, outputPer1M:
|
|
100
|
-
"gpt-5.4-pro": { inputPer1M:
|
|
101
|
-
"gpt-5.4-mini": { inputPer1M: 0.
|
|
99
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
100
|
+
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
101
|
+
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
102
102
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
103
103
|
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
104
104
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
@@ -272,6 +272,18 @@ function initSchema(db) {
|
|
|
272
272
|
machine_id TEXT,
|
|
273
273
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
274
274
|
);
|
|
275
|
+
|
|
276
|
+
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
277
|
+
date TEXT NOT NULL,
|
|
278
|
+
provider TEXT NOT NULL,
|
|
279
|
+
description TEXT DEFAULT '',
|
|
280
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
281
|
+
updated_at TEXT NOT NULL,
|
|
282
|
+
PRIMARY KEY (date, provider, description)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
286
|
+
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
275
287
|
`);
|
|
276
288
|
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
277
289
|
if (!cols.some((c) => c.name === "machine_id")) {
|
|
@@ -290,11 +302,11 @@ function periodWhere(period) {
|
|
|
290
302
|
case "yesterday":
|
|
291
303
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
292
304
|
case "week":
|
|
293
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
305
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
294
306
|
case "month":
|
|
295
|
-
return `timestamp >= DATE('now', '
|
|
307
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
296
308
|
case "year":
|
|
297
|
-
return `timestamp >= DATE('now', '
|
|
309
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
298
310
|
case "all":
|
|
299
311
|
return "1=1";
|
|
300
312
|
}
|
|
@@ -306,11 +318,11 @@ function sessionPeriodWhere(period) {
|
|
|
306
318
|
case "yesterday":
|
|
307
319
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
308
320
|
case "week":
|
|
309
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
321
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
310
322
|
case "month":
|
|
311
|
-
return `started_at >= DATE('now', '
|
|
323
|
+
return `started_at >= DATE('now', 'start of month')`;
|
|
312
324
|
case "year":
|
|
313
|
-
return `started_at >= DATE('now', '
|
|
325
|
+
return `started_at >= DATE('now', 'start of year')`;
|
|
314
326
|
case "all":
|
|
315
327
|
return "1=1";
|
|
316
328
|
}
|
|
@@ -420,23 +432,66 @@ function queryModelBreakdown(db) {
|
|
|
420
432
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
421
433
|
`).all();
|
|
422
434
|
}
|
|
435
|
+
function labelForPath(projectPath, projectName) {
|
|
436
|
+
if (projectName && projectName.trim() !== "")
|
|
437
|
+
return projectName;
|
|
438
|
+
if (!projectPath)
|
|
439
|
+
return "";
|
|
440
|
+
const segments = projectPath.split("/").filter(Boolean);
|
|
441
|
+
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
442
|
+
for (const seg of segments) {
|
|
443
|
+
if (projectPrefix.test(seg))
|
|
444
|
+
return seg;
|
|
445
|
+
}
|
|
446
|
+
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
447
|
+
for (let i = segments.length - 1;i >= 0; i--) {
|
|
448
|
+
if (!generic.has(segments[i].toLowerCase()))
|
|
449
|
+
return segments[i];
|
|
450
|
+
}
|
|
451
|
+
return segments[segments.length - 1] ?? projectPath;
|
|
452
|
+
}
|
|
423
453
|
function queryProjectBreakdown(db) {
|
|
424
|
-
|
|
425
|
-
SELECT
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
429
|
-
COUNT(r.id) as requests,
|
|
430
|
-
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
431
|
-
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
432
|
-
MAX(s.started_at) as last_active
|
|
433
|
-
FROM sessions s
|
|
434
|
-
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
435
|
-
LEFT JOIN requests r ON r.session_id = s.id
|
|
436
|
-
WHERE s.project_path != '' OR s.project_name != ''
|
|
437
|
-
GROUP BY s.project_path
|
|
438
|
-
ORDER BY cost_usd DESC
|
|
454
|
+
const sessions = db.prepare(`
|
|
455
|
+
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
456
|
+
FROM sessions
|
|
457
|
+
WHERE project_path != '' OR project_name != ''
|
|
439
458
|
`).all();
|
|
459
|
+
const groups = new Map;
|
|
460
|
+
for (const s of sessions) {
|
|
461
|
+
const label = labelForPath(s.project_path, s.project_name);
|
|
462
|
+
if (!label)
|
|
463
|
+
continue;
|
|
464
|
+
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
465
|
+
g.sessionIds.push(s.id);
|
|
466
|
+
g.totalCost += s.total_cost_usd || 0;
|
|
467
|
+
if (!g.lastActive || s.started_at > g.lastActive)
|
|
468
|
+
g.lastActive = s.started_at;
|
|
469
|
+
if (!g.samplePath)
|
|
470
|
+
g.samplePath = s.project_path;
|
|
471
|
+
groups.set(label, g);
|
|
472
|
+
}
|
|
473
|
+
const result = [];
|
|
474
|
+
for (const [label, g] of groups.entries()) {
|
|
475
|
+
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
476
|
+
const reqStats = placeholders.length ? db.prepare(`
|
|
477
|
+
SELECT
|
|
478
|
+
COUNT(*) as requests,
|
|
479
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
480
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
481
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
482
|
+
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
483
|
+
result.push({
|
|
484
|
+
project_path: g.samplePath,
|
|
485
|
+
project_name: label,
|
|
486
|
+
sessions: g.sessionIds.length,
|
|
487
|
+
requests: reqStats.requests,
|
|
488
|
+
total_tokens: reqStats.total_tokens,
|
|
489
|
+
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
490
|
+
last_active: g.lastActive
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
494
|
+
return result;
|
|
440
495
|
}
|
|
441
496
|
function queryDailyBreakdown(db, days = 30) {
|
|
442
497
|
return db.prepare(`
|
package/package.json
CHANGED