@hasna/economy 0.2.15 → 0.2.16

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 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: 10, cacheReadPer1M: 0.625, cacheWritePer1M: 0 },
100
- "gpt-5.4-pro": { inputPer1M: 5, outputPer1M: 20, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
101
- "gpt-5.4-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 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', '-30 days')`;
347
+ return `timestamp >= DATE('now', 'start of month')`;
333
348
  case "year":
334
- return `timestamp >= DATE('now', '-365 days')`;
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', '-30 days')`;
363
+ return `started_at >= DATE('now', 'start of month')`;
349
364
  case "year":
350
- return `started_at >= DATE('now', '-365 days')`;
365
+ return `started_at >= DATE('now', 'start of year')`;
351
366
  case "all":
352
367
  return "1=1";
353
368
  }
@@ -459,19 +474,39 @@ function queryModelBreakdown(db) {
459
474
  }
460
475
  function queryProjectBreakdown(db) {
461
476
  return db.prepare(`
477
+ WITH labeled AS (
478
+ SELECT
479
+ s.id,
480
+ s.project_path,
481
+ s.total_cost_usd,
482
+ s.started_at,
483
+ COALESCE(
484
+ NULLIF(s.project_name, ''),
485
+ CASE
486
+ WHEN s.project_path LIKE '%/%'
487
+ THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
488
+ ELSE s.project_path
489
+ END
490
+ ) as label
491
+ FROM sessions s
492
+ WHERE s.project_path != '' OR s.project_name != ''
493
+ )
462
494
  SELECT
463
- s.project_path,
464
- COALESCE(p.name, s.project_name) as project_name,
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
495
+ MIN(l.project_path) as project_path,
496
+ l.label as project_name,
497
+ COUNT(DISTINCT l.id) as sessions,
498
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
499
+ COALESCE(
500
+ (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
501
+ SUM(l.total_cost_usd)
502
+ ) as cost_usd,
503
+ COALESCE(
504
+ (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
505
+ 0
506
+ ) as total_tokens,
507
+ MAX(l.started_at) as last_active
508
+ FROM labeled l
509
+ GROUP BY l.label
475
510
  ORDER BY cost_usd DESC
476
511
  `).all();
477
512
  }
@@ -591,6 +626,26 @@ function setIngestState(db, source, key, value) {
591
626
  function queryRequestsSince(db, since) {
592
627
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
593
628
  }
629
+ function upsertBillingDaily(db, row) {
630
+ db.prepare(`
631
+ INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
632
+ VALUES (?, ?, ?, ?, ?)
633
+ `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
634
+ }
635
+ function clearBillingRange(db, provider, fromDate, toDate) {
636
+ db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
637
+ }
638
+ function queryBillingSummary(db, period) {
639
+ 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";
640
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
641
+ const by_provider = {};
642
+ let total = 0;
643
+ for (const r of rows) {
644
+ by_provider[r.provider] = r.cost;
645
+ total += r.cost;
646
+ }
647
+ return { total_usd: total, by_provider };
648
+ }
594
649
  function listMachines(db) {
595
650
  return db.prepare(`
596
651
  SELECT
@@ -1971,6 +2026,119 @@ init_claude();
1971
2026
  init_codex();
1972
2027
  init_gemini();
1973
2028
 
2029
+ // src/ingest/billing.ts
2030
+ init_database();
2031
+ function getAnthropicAdminKey() {
2032
+ return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
2033
+ }
2034
+ function getOpenAIAdminKey() {
2035
+ return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
2036
+ }
2037
+ function toISODate(d) {
2038
+ return d.toISOString().substring(0, 10);
2039
+ }
2040
+ async function syncAnthropicBilling(db, opts = {}) {
2041
+ const key = getAnthropicAdminKey();
2042
+ if (!key)
2043
+ throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
2044
+ const now = new Date;
2045
+ const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
2046
+ const days = opts.days ?? 31;
2047
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
2048
+ const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
2049
+ const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
2050
+ let totalUsd = 0;
2051
+ const buckets = [];
2052
+ let nextPage;
2053
+ do {
2054
+ const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
2055
+ url.searchParams.set("starting_at", startIso);
2056
+ url.searchParams.set("ending_at", endIso);
2057
+ url.searchParams.set("bucket_width", "1d");
2058
+ url.searchParams.set("limit", "31");
2059
+ url.searchParams.append("group_by[]", "description");
2060
+ if (nextPage)
2061
+ url.searchParams.set("page", nextPage);
2062
+ const res = await fetch(url.toString(), {
2063
+ headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
2064
+ });
2065
+ const data = await res.json();
2066
+ if (data.error)
2067
+ throw new Error(`Anthropic API: ${data.error.message}`);
2068
+ if (data.data)
2069
+ buckets.push(...data.data);
2070
+ nextPage = data.has_more ? data.next_page : undefined;
2071
+ } while (nextPage);
2072
+ const fromDateStr = toISODate(start);
2073
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
2074
+ clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
2075
+ const updatedAt = new Date().toISOString();
2076
+ for (const bucket of buckets) {
2077
+ const date = bucket.starting_at.substring(0, 10);
2078
+ for (const r of bucket.results) {
2079
+ const usd = Number(r.amount) / 100;
2080
+ if (usd === 0)
2081
+ continue;
2082
+ const desc = (r.description ?? "unknown").substring(0, 200);
2083
+ upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
2084
+ totalUsd += usd;
2085
+ }
2086
+ }
2087
+ return { days: buckets.length, totalUsd };
2088
+ }
2089
+ async function syncOpenAIBilling(db, opts = {}) {
2090
+ const key = getOpenAIAdminKey();
2091
+ if (!key)
2092
+ throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
2093
+ const now = new Date;
2094
+ const end = opts.toDate ? new Date(opts.toDate) : now;
2095
+ const days = opts.days ?? 31;
2096
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
2097
+ const startSec = Math.floor(start.getTime() / 1000);
2098
+ const endSec = Math.floor(end.getTime() / 1000);
2099
+ let totalUsd = 0;
2100
+ const buckets = [];
2101
+ let nextPage;
2102
+ do {
2103
+ const url = new URL("https://api.openai.com/v1/organization/costs");
2104
+ url.searchParams.set("start_time", String(startSec));
2105
+ url.searchParams.set("end_time", String(endSec));
2106
+ url.searchParams.set("bucket_width", "1d");
2107
+ url.searchParams.set("limit", "31");
2108
+ url.searchParams.append("group_by[]", "line_item");
2109
+ if (nextPage)
2110
+ url.searchParams.set("page", nextPage);
2111
+ const res = await fetch(url.toString(), {
2112
+ headers: { Authorization: `Bearer ${key}` }
2113
+ });
2114
+ const data = await res.json();
2115
+ if (data.error)
2116
+ throw new Error(`OpenAI API: ${data.error.message}`);
2117
+ if (data.data)
2118
+ buckets.push(...data.data);
2119
+ nextPage = data.has_more ? data.next_page : undefined;
2120
+ } while (nextPage);
2121
+ const fromDateStr = toISODate(start);
2122
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
2123
+ clearBillingRange(db, "openai", fromDateStr, toDateStr);
2124
+ const updatedAt = new Date().toISOString();
2125
+ for (const bucket of buckets) {
2126
+ const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
2127
+ for (const r of bucket.results) {
2128
+ const usd = Number(r.amount?.value ?? 0);
2129
+ if (usd === 0)
2130
+ continue;
2131
+ const desc = (r.line_item ?? "unknown").substring(0, 200);
2132
+ upsertBillingDaily(db, { date, provider: "openai", description: desc, cost_usd: usd, updated_at: updatedAt });
2133
+ totalUsd += usd;
2134
+ }
2135
+ }
2136
+ return { days: buckets.length, totalUsd };
2137
+ }
2138
+
2139
+ // src/cli/index.ts
2140
+ init_database();
2141
+
1974
2142
  // src/lib/package-metadata.ts
1975
2143
  import { readFileSync as readFileSync5 } from "fs";
1976
2144
  var cachedMetadata = null;
@@ -3035,5 +3203,46 @@ cloudCmd.command("status").description("Check cloud connection status").action(a
3035
3203
  }
3036
3204
  console.log();
3037
3205
  });
3206
+ var billingCmd = program.command("billing").description("Pull actual billing from provider admin APIs (ground truth)");
3207
+ 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) => {
3208
+ const db = openDatabase();
3209
+ const days = Number(opts.days ?? 31);
3210
+ const doBoth = !opts.anthropic && !opts.openai;
3211
+ if (opts.anthropic || doBoth) {
3212
+ process.stdout.write(chalk4.cyan("\u2192 Syncing Anthropic billing... "));
3213
+ try {
3214
+ const r = await syncAnthropicBilling(db, { days });
3215
+ console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
3216
+ } catch (e) {
3217
+ console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
3218
+ }
3219
+ }
3220
+ if (opts.openai || doBoth) {
3221
+ process.stdout.write(chalk4.cyan("\u2192 Syncing OpenAI billing... "));
3222
+ try {
3223
+ const r = await syncOpenAIBilling(db, { days });
3224
+ console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
3225
+ } catch (e) {
3226
+ console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
3227
+ }
3228
+ }
3229
+ });
3230
+ 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) => {
3231
+ const db = openDatabase();
3232
+ const period = opts.period ?? "month";
3233
+ const actual = queryBillingSummary(db, period);
3234
+ const estimated = querySummary(db, period);
3235
+ console.log();
3236
+ console.log(chalk4.bold.cyan(` Billing ${period} (actual from admin APIs)
3237
+ `));
3238
+ printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [chalk4.white(p), fmt2(c)]));
3239
+ console.log();
3240
+ console.log(` ${chalk4.bold("Actual total:")} ${fmt2(actual.total_usd)}`);
3241
+ console.log(` ${chalk4.dim("Our estimate:")} ${fmt2(estimated.total_usd)}`);
3242
+ const diff = estimated.total_usd - actual.total_usd;
3243
+ const pct = actual.total_usd > 0 ? diff / actual.total_usd * 100 : 0;
3244
+ console.log(` ${chalk4.dim("Difference:")} ${fmt2(Math.abs(diff))} (${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%)`);
3245
+ console.log();
3246
+ });
3038
3247
  registerBrainsCommand(program);
3039
3248
  program.parse();
@@ -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":"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"}
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: 10, cacheReadPer1M: 0.625, cacheWritePer1M: 0 },
98
- "gpt-5.4-pro": { inputPer1M: 5, outputPer1M: 20, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
99
- "gpt-5.4-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 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', '-30 days')`;
305
+ return `timestamp >= DATE('now', 'start of month')`;
294
306
  case "year":
295
- return `timestamp >= DATE('now', '-365 days')`;
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', '-30 days')`;
321
+ return `started_at >= DATE('now', 'start of month')`;
310
322
  case "year":
311
- return `started_at >= DATE('now', '-365 days')`;
323
+ return `started_at >= DATE('now', 'start of year')`;
312
324
  case "all":
313
325
  return "1=1";
314
326
  }
@@ -420,19 +432,39 @@ function queryModelBreakdown(db) {
420
432
  }
421
433
  function queryProjectBreakdown(db) {
422
434
  return db.prepare(`
435
+ WITH labeled AS (
436
+ SELECT
437
+ s.id,
438
+ s.project_path,
439
+ s.total_cost_usd,
440
+ s.started_at,
441
+ COALESCE(
442
+ NULLIF(s.project_name, ''),
443
+ CASE
444
+ WHEN s.project_path LIKE '%/%'
445
+ THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
446
+ ELSE s.project_path
447
+ END
448
+ ) as label
449
+ FROM sessions s
450
+ WHERE s.project_path != '' OR s.project_name != ''
451
+ )
423
452
  SELECT
424
- s.project_path,
425
- COALESCE(p.name, s.project_name) as project_name,
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
453
+ MIN(l.project_path) as project_path,
454
+ l.label as project_name,
455
+ COUNT(DISTINCT l.id) as sessions,
456
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
457
+ COALESCE(
458
+ (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
459
+ SUM(l.total_cost_usd)
460
+ ) as cost_usd,
461
+ COALESCE(
462
+ (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
463
+ 0
464
+ ) as total_tokens,
465
+ MAX(l.started_at) as last_active
466
+ FROM labeled l
467
+ GROUP BY l.label
436
468
  ORDER BY cost_usd DESC
437
469
  `).all();
438
470
  }
@@ -552,6 +584,26 @@ function setIngestState(db, source, key, value) {
552
584
  function queryRequestsSince(db, since) {
553
585
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
554
586
  }
587
+ function upsertBillingDaily(db, row) {
588
+ db.prepare(`
589
+ INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
590
+ VALUES (?, ?, ?, ?, ?)
591
+ `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
592
+ }
593
+ function clearBillingRange(db, provider, fromDate, toDate) {
594
+ db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
595
+ }
596
+ function queryBillingSummary(db, period) {
597
+ 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";
598
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
599
+ const by_provider = {};
600
+ let total = 0;
601
+ for (const r of rows) {
602
+ by_provider[r.provider] = r.cost;
603
+ total += r.cost;
604
+ }
605
+ return { total_usd: total, by_provider };
606
+ }
555
607
  function listMachines(db) {
556
608
  return db.prepare(`
557
609
  SELECT
@@ -1037,6 +1089,7 @@ export {
1037
1089
  upsertModelPricing,
1038
1090
  upsertGoal,
1039
1091
  upsertBudget,
1092
+ upsertBillingDaily,
1040
1093
  setIngestState,
1041
1094
  setActiveModel,
1042
1095
  seedModelPricing,
@@ -1049,6 +1102,7 @@ export {
1049
1102
  queryProjectBreakdown,
1050
1103
  queryModelBreakdown,
1051
1104
  queryDailyBreakdown,
1105
+ queryBillingSummary,
1052
1106
  openDatabase,
1053
1107
  normalizeModelName,
1054
1108
  listProjects,
@@ -1078,6 +1132,7 @@ export {
1078
1132
  deleteBudget,
1079
1133
  computeCostFromDb,
1080
1134
  computeCost,
1135
+ clearBillingRange,
1081
1136
  clearActiveModel,
1082
1137
  DEFAULT_PRICING,
1083
1138
  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: 10, cacheReadPer1M: 0.625, cacheWritePer1M: 0 },
99
- "gpt-5.4-pro": { inputPer1M: 5, outputPer1M: 20, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
100
- "gpt-5.4-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 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', '-30 days')`;
306
+ return `timestamp >= DATE('now', 'start of month')`;
295
307
  case "year":
296
- return `timestamp >= DATE('now', '-365 days')`;
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', '-30 days')`;
322
+ return `started_at >= DATE('now', 'start of month')`;
311
323
  case "year":
312
- return `started_at >= DATE('now', '-365 days')`;
324
+ return `started_at >= DATE('now', 'start of year')`;
313
325
  case "all":
314
326
  return "1=1";
315
327
  }
@@ -421,19 +433,39 @@ function queryModelBreakdown(db) {
421
433
  }
422
434
  function queryProjectBreakdown(db) {
423
435
  return db.prepare(`
436
+ WITH labeled AS (
437
+ SELECT
438
+ s.id,
439
+ s.project_path,
440
+ s.total_cost_usd,
441
+ s.started_at,
442
+ COALESCE(
443
+ NULLIF(s.project_name, ''),
444
+ CASE
445
+ WHEN s.project_path LIKE '%/%'
446
+ THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
447
+ ELSE s.project_path
448
+ END
449
+ ) as label
450
+ FROM sessions s
451
+ WHERE s.project_path != '' OR s.project_name != ''
452
+ )
424
453
  SELECT
425
- s.project_path,
426
- COALESCE(p.name, s.project_name) as project_name,
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
454
+ MIN(l.project_path) as project_path,
455
+ l.label as project_name,
456
+ COUNT(DISTINCT l.id) as sessions,
457
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
458
+ COALESCE(
459
+ (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
460
+ SUM(l.total_cost_usd)
461
+ ) as cost_usd,
462
+ COALESCE(
463
+ (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
464
+ 0
465
+ ) as total_tokens,
466
+ MAX(l.started_at) as last_active
467
+ FROM labeled l
468
+ GROUP BY l.label
437
469
  ORDER BY cost_usd DESC
438
470
  `).all();
439
471
  }
@@ -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: 10, cacheReadPer1M: 0.625, cacheWritePer1M: 0 },
100
- "gpt-5.4-pro": { inputPer1M: 5, outputPer1M: 20, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
101
- "gpt-5.4-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 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', '-30 days')`;
307
+ return `timestamp >= DATE('now', 'start of month')`;
296
308
  case "year":
297
- return `timestamp >= DATE('now', '-365 days')`;
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', '-30 days')`;
323
+ return `started_at >= DATE('now', 'start of month')`;
312
324
  case "year":
313
- return `started_at >= DATE('now', '-365 days')`;
325
+ return `started_at >= DATE('now', 'start of year')`;
314
326
  case "all":
315
327
  return "1=1";
316
328
  }
@@ -422,19 +434,39 @@ function queryModelBreakdown(db) {
422
434
  }
423
435
  function queryProjectBreakdown(db) {
424
436
  return db.prepare(`
437
+ WITH labeled AS (
438
+ SELECT
439
+ s.id,
440
+ s.project_path,
441
+ s.total_cost_usd,
442
+ s.started_at,
443
+ COALESCE(
444
+ NULLIF(s.project_name, ''),
445
+ CASE
446
+ WHEN s.project_path LIKE '%/%'
447
+ THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
448
+ ELSE s.project_path
449
+ END
450
+ ) as label
451
+ FROM sessions s
452
+ WHERE s.project_path != '' OR s.project_name != ''
453
+ )
425
454
  SELECT
426
- s.project_path,
427
- COALESCE(p.name, s.project_name) as project_name,
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
455
+ MIN(l.project_path) as project_path,
456
+ l.label as project_name,
457
+ COUNT(DISTINCT l.id) as sessions,
458
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
459
+ COALESCE(
460
+ (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
461
+ SUM(l.total_cost_usd)
462
+ ) as cost_usd,
463
+ COALESCE(
464
+ (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
465
+ 0
466
+ ) as total_tokens,
467
+ MAX(l.started_at) as last_active
468
+ FROM labeled l
469
+ GROUP BY l.label
438
470
  ORDER BY cost_usd DESC
439
471
  `).all();
440
472
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",