@hasna/economy 0.2.43 → 0.3.1
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/dashboard/README.md +73 -0
- package/dist/cli/commands/brief.d.ts +4 -69
- package/dist/cli/commands/brief.d.ts.map +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/tui.d.ts +21 -5
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +8227 -6395
- package/dist/db/database.d.ts +18 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.d.ts +1 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1855 -1613
- package/dist/lib/analytics.d.ts +68 -0
- package/dist/lib/analytics.d.ts.map +1 -0
- package/dist/lib/brief.d.ts +72 -0
- package/dist/lib/brief.d.ts.map +1 -0
- package/dist/lib/cloud-storage.d.ts +48 -0
- package/dist/lib/cloud-storage.d.ts.map +1 -0
- package/dist/lib/contracts-client/index.d.ts +3 -0
- package/dist/lib/contracts-client/index.d.ts.map +1 -0
- package/dist/lib/contracts-client/mode.d.ts +20 -0
- package/dist/lib/contracts-client/mode.d.ts.map +1 -0
- package/dist/lib/contracts-client/storage.d.ts +83 -0
- package/dist/lib/contracts-client/storage.d.ts.map +1 -0
- package/dist/lib/contracts-client/transport.d.ts +148 -0
- package/dist/lib/contracts-client/transport.d.ts.map +1 -0
- package/dist/lib/pricing.d.ts +3 -0
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +283 -0
- package/dist/lib/store/index.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +0 -2
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/index.js +1934 -536
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +21 -267
- package/dist/server/index.js +2794 -331
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -2
- package/dashboard/dist/assets/charts-BtCbslx4.js +0 -36
- package/dashboard/dist/assets/index-BDmtt3RI.css +0 -1
- package/dashboard/dist/assets/index-BFwRLqg7.js +0 -6
- package/dashboard/dist/assets/vendor-Cz8jb743.js +0 -53
- package/dashboard/dist/index.html +0 -16
- package/dashboard/dist/logo.jpg +0 -0
- package/dashboard/dist/vite.svg +0 -1
package/dist/server/index.js
CHANGED
|
@@ -21,8 +21,10 @@ var __require = import.meta.require;
|
|
|
21
21
|
var exports_pricing = {};
|
|
22
22
|
__export(exports_pricing, {
|
|
23
23
|
normalizeModelName: () => normalizeModelName,
|
|
24
|
+
getPricingFromRows: () => getPricingFromRows,
|
|
24
25
|
getPricingFromDb: () => getPricingFromDb,
|
|
25
26
|
getPricing: () => getPricing,
|
|
27
|
+
estimateCostFromRows: () => estimateCostFromRows,
|
|
26
28
|
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
27
29
|
computeCostFromDb: () => computeCostFromDb,
|
|
28
30
|
computeCost: () => computeCost,
|
|
@@ -213,6 +215,25 @@ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens
|
|
|
213
215
|
return 0;
|
|
214
216
|
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
215
217
|
}
|
|
218
|
+
function getPricingFromRows(rows, model) {
|
|
219
|
+
if (isFreeModel(model))
|
|
220
|
+
return FREE_PRICING;
|
|
221
|
+
for (const key of modelLookupKeys(model)) {
|
|
222
|
+
const row = rows.find((r) => r.model === key);
|
|
223
|
+
if (row)
|
|
224
|
+
return modelPricingFromDbRow(row);
|
|
225
|
+
}
|
|
226
|
+
const match = bestModelMatch(model, rows.map((r) => [r.model, r]));
|
|
227
|
+
if (!match)
|
|
228
|
+
return null;
|
|
229
|
+
return modelPricingFromDbRow(match);
|
|
230
|
+
}
|
|
231
|
+
function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
232
|
+
const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
|
|
233
|
+
if (!pricing)
|
|
234
|
+
return 0;
|
|
235
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
236
|
+
}
|
|
216
237
|
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
217
238
|
if (isFreeModel(model))
|
|
218
239
|
return 0;
|
|
@@ -1495,6 +1516,9 @@ function getIngestState(db, source, key) {
|
|
|
1495
1516
|
function setIngestState(db, source, key, value) {
|
|
1496
1517
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1497
1518
|
}
|
|
1519
|
+
function queryRequestsSince(db, since) {
|
|
1520
|
+
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
1521
|
+
}
|
|
1498
1522
|
function upsertBillingDaily(db, row) {
|
|
1499
1523
|
db.prepare(`
|
|
1500
1524
|
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
@@ -1657,13 +1681,374 @@ function dedupeRequests(db) {
|
|
|
1657
1681
|
}
|
|
1658
1682
|
return removed;
|
|
1659
1683
|
}
|
|
1684
|
+
function ingestSpecs() {
|
|
1685
|
+
return [
|
|
1686
|
+
{
|
|
1687
|
+
table: "requests",
|
|
1688
|
+
pk: ["id"],
|
|
1689
|
+
columns: [
|
|
1690
|
+
"id",
|
|
1691
|
+
"agent",
|
|
1692
|
+
"session_id",
|
|
1693
|
+
"model",
|
|
1694
|
+
"input_tokens",
|
|
1695
|
+
"output_tokens",
|
|
1696
|
+
"cache_read_tokens",
|
|
1697
|
+
"cache_create_tokens",
|
|
1698
|
+
"cache_create_5m_tokens",
|
|
1699
|
+
"cache_create_1h_tokens",
|
|
1700
|
+
"cost_usd",
|
|
1701
|
+
"cost_basis",
|
|
1702
|
+
"duration_ms",
|
|
1703
|
+
"timestamp",
|
|
1704
|
+
"source_request_id",
|
|
1705
|
+
"machine_id",
|
|
1706
|
+
"attribution_tag",
|
|
1707
|
+
"account_key",
|
|
1708
|
+
"account_tool",
|
|
1709
|
+
"account_name",
|
|
1710
|
+
"account_email",
|
|
1711
|
+
"account_source",
|
|
1712
|
+
"updated_at",
|
|
1713
|
+
"synced_at"
|
|
1714
|
+
],
|
|
1715
|
+
map: (r, now) => [
|
|
1716
|
+
strOr(r["id"]),
|
|
1717
|
+
strOr(r["agent"]),
|
|
1718
|
+
strOr(r["session_id"]),
|
|
1719
|
+
strOr(r["model"]),
|
|
1720
|
+
numOr(r["input_tokens"]),
|
|
1721
|
+
numOr(r["output_tokens"]),
|
|
1722
|
+
numOr(r["cache_read_tokens"]),
|
|
1723
|
+
numOr(r["cache_create_tokens"]),
|
|
1724
|
+
numOr(r["cache_create_5m_tokens"] ?? r["cache_create_tokens"]),
|
|
1725
|
+
numOr(r["cache_create_1h_tokens"]),
|
|
1726
|
+
numOr(r["cost_usd"]),
|
|
1727
|
+
strOr(r["cost_basis"], "estimated"),
|
|
1728
|
+
numOr(r["duration_ms"]),
|
|
1729
|
+
strOr(r["timestamp"]),
|
|
1730
|
+
r["source_request_id"] == null ? null : strOr(r["source_request_id"]),
|
|
1731
|
+
strOr(r["machine_id"]),
|
|
1732
|
+
strOr(r["attribution_tag"]),
|
|
1733
|
+
strOr(r["account_key"]),
|
|
1734
|
+
strOr(r["account_tool"]),
|
|
1735
|
+
strOr(r["account_name"]),
|
|
1736
|
+
strOr(r["account_email"]),
|
|
1737
|
+
strOr(r["account_source"]),
|
|
1738
|
+
strOr(r["updated_at"], now),
|
|
1739
|
+
strOr(r["synced_at"])
|
|
1740
|
+
]
|
|
1741
|
+
},
|
|
1742
|
+
{
|
|
1743
|
+
table: "sessions",
|
|
1744
|
+
pk: ["id"],
|
|
1745
|
+
columns: [
|
|
1746
|
+
"id",
|
|
1747
|
+
"agent",
|
|
1748
|
+
"project_path",
|
|
1749
|
+
"project_name",
|
|
1750
|
+
"started_at",
|
|
1751
|
+
"ended_at",
|
|
1752
|
+
"total_cost_usd",
|
|
1753
|
+
"total_tokens",
|
|
1754
|
+
"request_count",
|
|
1755
|
+
"machine_id",
|
|
1756
|
+
"attribution_tag",
|
|
1757
|
+
"account_key",
|
|
1758
|
+
"account_tool",
|
|
1759
|
+
"account_name",
|
|
1760
|
+
"account_email",
|
|
1761
|
+
"account_source",
|
|
1762
|
+
"updated_at",
|
|
1763
|
+
"synced_at"
|
|
1764
|
+
],
|
|
1765
|
+
map: (r, now) => [
|
|
1766
|
+
strOr(r["id"]),
|
|
1767
|
+
strOr(r["agent"]),
|
|
1768
|
+
strOr(r["project_path"]),
|
|
1769
|
+
strOr(r["project_name"]),
|
|
1770
|
+
strOr(r["started_at"]),
|
|
1771
|
+
r["ended_at"] == null ? null : strOr(r["ended_at"]),
|
|
1772
|
+
numOr(r["total_cost_usd"]),
|
|
1773
|
+
numOr(r["total_tokens"]),
|
|
1774
|
+
numOr(r["request_count"]),
|
|
1775
|
+
strOr(r["machine_id"]),
|
|
1776
|
+
strOr(r["attribution_tag"]),
|
|
1777
|
+
strOr(r["account_key"]),
|
|
1778
|
+
strOr(r["account_tool"]),
|
|
1779
|
+
strOr(r["account_name"]),
|
|
1780
|
+
strOr(r["account_email"]),
|
|
1781
|
+
strOr(r["account_source"]),
|
|
1782
|
+
strOr(r["updated_at"], now),
|
|
1783
|
+
strOr(r["synced_at"])
|
|
1784
|
+
]
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
table: "projects",
|
|
1788
|
+
pk: ["id"],
|
|
1789
|
+
columns: ["id", "path", "name", "description", "tags", "created_at"],
|
|
1790
|
+
map: (r) => [
|
|
1791
|
+
strOr(r["id"]),
|
|
1792
|
+
strOr(r["path"]),
|
|
1793
|
+
strOr(r["name"]),
|
|
1794
|
+
r["description"] == null ? null : strOr(r["description"]),
|
|
1795
|
+
typeof r["tags"] === "string" ? r["tags"] : JSON.stringify(r["tags"] ?? []),
|
|
1796
|
+
strOr(r["created_at"])
|
|
1797
|
+
]
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
table: "budgets",
|
|
1801
|
+
pk: ["id"],
|
|
1802
|
+
columns: ["id", "project_path", "agent", "period", "limit_usd", "alert_at_percent", "created_at", "updated_at"],
|
|
1803
|
+
map: (r) => [
|
|
1804
|
+
strOr(r["id"]),
|
|
1805
|
+
r["project_path"] == null ? null : strOr(r["project_path"]),
|
|
1806
|
+
r["agent"] == null ? null : strOr(r["agent"]),
|
|
1807
|
+
strOr(r["period"]),
|
|
1808
|
+
numOr(r["limit_usd"]),
|
|
1809
|
+
numOr(r["alert_at_percent"]),
|
|
1810
|
+
strOr(r["created_at"]),
|
|
1811
|
+
strOr(r["updated_at"])
|
|
1812
|
+
]
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
table: "goals",
|
|
1816
|
+
pk: ["id"],
|
|
1817
|
+
columns: ["id", "period", "project_path", "agent", "limit_usd", "created_at", "updated_at"],
|
|
1818
|
+
map: (r) => [
|
|
1819
|
+
strOr(r["id"]),
|
|
1820
|
+
strOr(r["period"]),
|
|
1821
|
+
r["project_path"] == null ? null : strOr(r["project_path"]),
|
|
1822
|
+
r["agent"] == null ? null : strOr(r["agent"]),
|
|
1823
|
+
numOr(r["limit_usd"]),
|
|
1824
|
+
strOr(r["created_at"]),
|
|
1825
|
+
strOr(r["updated_at"])
|
|
1826
|
+
]
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
table: "billing_daily",
|
|
1830
|
+
pk: ["date", "provider", "description"],
|
|
1831
|
+
columns: ["date", "provider", "description", "cost_usd", "updated_at"],
|
|
1832
|
+
map: (r) => [strOr(r["date"]), strOr(r["provider"]), strOr(r["description"]), numOr(r["cost_usd"]), strOr(r["updated_at"])]
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
table: "model_pricing",
|
|
1836
|
+
pk: ["model"],
|
|
1837
|
+
columns: [
|
|
1838
|
+
"model",
|
|
1839
|
+
"input_per_1m",
|
|
1840
|
+
"output_per_1m",
|
|
1841
|
+
"cache_read_per_1m",
|
|
1842
|
+
"cache_write_per_1m",
|
|
1843
|
+
"cache_write_1h_per_1m",
|
|
1844
|
+
"cache_storage_per_1m_hour",
|
|
1845
|
+
"updated_at"
|
|
1846
|
+
],
|
|
1847
|
+
map: (r) => [
|
|
1848
|
+
strOr(r["model"]),
|
|
1849
|
+
numOr(r["input_per_1m"]),
|
|
1850
|
+
numOr(r["output_per_1m"]),
|
|
1851
|
+
numOr(r["cache_read_per_1m"]),
|
|
1852
|
+
numOr(r["cache_write_per_1m"]),
|
|
1853
|
+
numOr(r["cache_write_1h_per_1m"]),
|
|
1854
|
+
numOr(r["cache_storage_per_1m_hour"]),
|
|
1855
|
+
strOr(r["updated_at"])
|
|
1856
|
+
]
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
table: "subscriptions",
|
|
1860
|
+
pk: ["id"],
|
|
1861
|
+
columns: [
|
|
1862
|
+
"id",
|
|
1863
|
+
"agent",
|
|
1864
|
+
"provider",
|
|
1865
|
+
"plan",
|
|
1866
|
+
"monthly_fee_usd",
|
|
1867
|
+
"included_usage_usd",
|
|
1868
|
+
"billing_cycle_start",
|
|
1869
|
+
"reset_policy",
|
|
1870
|
+
"active",
|
|
1871
|
+
"created_at",
|
|
1872
|
+
"updated_at"
|
|
1873
|
+
],
|
|
1874
|
+
map: (r) => [
|
|
1875
|
+
strOr(r["id"]),
|
|
1876
|
+
r["agent"] == null ? null : strOr(r["agent"]),
|
|
1877
|
+
strOr(r["provider"]),
|
|
1878
|
+
strOr(r["plan"]),
|
|
1879
|
+
numOr(r["monthly_fee_usd"]),
|
|
1880
|
+
numOr(r["included_usage_usd"]),
|
|
1881
|
+
r["billing_cycle_start"] == null ? null : strOr(r["billing_cycle_start"]),
|
|
1882
|
+
strOr(r["reset_policy"], "monthly"),
|
|
1883
|
+
numOr(r["active"], 1),
|
|
1884
|
+
strOr(r["created_at"]),
|
|
1885
|
+
strOr(r["updated_at"])
|
|
1886
|
+
]
|
|
1887
|
+
},
|
|
1888
|
+
{
|
|
1889
|
+
table: "usage_snapshots",
|
|
1890
|
+
pk: ["id"],
|
|
1891
|
+
columns: ["id", "agent", "date", "metric", "value", "unit", "machine_id", "updated_at"],
|
|
1892
|
+
map: (r, now) => [
|
|
1893
|
+
strOr(r["id"], `${strOr(r["agent"])}-${strOr(r["date"])}-${strOr(r["metric"])}-${strOr(r["machine_id"])}`),
|
|
1894
|
+
strOr(r["agent"]),
|
|
1895
|
+
strOr(r["date"]),
|
|
1896
|
+
strOr(r["metric"]),
|
|
1897
|
+
numOr(r["value"]),
|
|
1898
|
+
strOr(r["unit"]),
|
|
1899
|
+
strOr(r["machine_id"]),
|
|
1900
|
+
strOr(r["updated_at"], now)
|
|
1901
|
+
]
|
|
1902
|
+
}
|
|
1903
|
+
];
|
|
1904
|
+
}
|
|
1905
|
+
function bulkIngest(db, body) {
|
|
1906
|
+
const now = new Date().toISOString();
|
|
1907
|
+
const ingested = {};
|
|
1908
|
+
for (const spec of ingestSpecs()) {
|
|
1909
|
+
const raw = body[spec.table];
|
|
1910
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
1911
|
+
continue;
|
|
1912
|
+
const cols = spec.columns;
|
|
1913
|
+
const pkIdx = spec.pk.map((k) => cols.indexOf(k));
|
|
1914
|
+
const deduped = new Map;
|
|
1915
|
+
for (const row of raw) {
|
|
1916
|
+
const values = spec.map(row, now);
|
|
1917
|
+
const key = pkIdx.map((i) => String(values[i])).join("\x00");
|
|
1918
|
+
deduped.set(key, values);
|
|
1919
|
+
}
|
|
1920
|
+
const rows = [...deduped.values()];
|
|
1921
|
+
const updateCols = cols.filter((c) => !spec.pk.includes(c));
|
|
1922
|
+
const conflictAction = updateCols.length ? `DO UPDATE SET ${updateCols.map((c) => `"${c}" = excluded."${c}"`).join(", ")}` : "DO NOTHING";
|
|
1923
|
+
const colList = cols.map((c) => `"${c}"`).join(", ");
|
|
1924
|
+
const conflict = spec.pk.map((c) => `"${c}"`).join(", ");
|
|
1925
|
+
const chunkRows = Math.max(1, Math.floor(20000 / cols.length));
|
|
1926
|
+
const rowPlaceholder = `(${cols.map(() => "?").join(", ")})`;
|
|
1927
|
+
for (let i = 0;i < rows.length; i += chunkRows) {
|
|
1928
|
+
const chunk = rows.slice(i, i + chunkRows);
|
|
1929
|
+
const valuesSql = chunk.map(() => rowPlaceholder).join(", ");
|
|
1930
|
+
const sql = `INSERT INTO ${spec.table} (${colList}) VALUES ${valuesSql} ON CONFLICT (${conflict}) ${conflictAction}`;
|
|
1931
|
+
const params = [];
|
|
1932
|
+
for (const values of chunk)
|
|
1933
|
+
params.push(...values);
|
|
1934
|
+
db.prepare(sql).run(...params);
|
|
1935
|
+
}
|
|
1936
|
+
ingested[spec.table] = rows.length;
|
|
1937
|
+
}
|
|
1938
|
+
const total = Object.values(ingested).reduce((a, b) => a + b, 0);
|
|
1939
|
+
return { ingested, total };
|
|
1940
|
+
}
|
|
1941
|
+
var numOr = (v, d = 0) => {
|
|
1942
|
+
if (v == null)
|
|
1943
|
+
return d;
|
|
1944
|
+
const num = Number(v);
|
|
1945
|
+
return Number.isFinite(num) ? num : d;
|
|
1946
|
+
}, strOr = (v, d = "") => v == null ? d : String(v);
|
|
1660
1947
|
var init_database = () => {};
|
|
1661
1948
|
|
|
1949
|
+
// src/db/sync-pg.ts
|
|
1950
|
+
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
1951
|
+
import { fileURLToPath } from "url";
|
|
1952
|
+
import { dirname, join as join9 } from "path";
|
|
1953
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1954
|
+
import { translateSql, translateParams } from "@hasna/cloud";
|
|
1955
|
+
function translateSqliteDates(sql) {
|
|
1956
|
+
let s = sql;
|
|
1957
|
+
const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
|
|
1958
|
+
s = s.replace(/DATE\('now',\s*'weekday 0',\s*'-7 days'\)/gi, isoDate("(date_trunc('week', now() + interval '1 day') - interval '1 day')"));
|
|
1959
|
+
s = s.replace(/DATE\('now',\s*'start of month'\)/gi, isoDate("date_trunc('month', now())"));
|
|
1960
|
+
s = s.replace(/DATE\('now',\s*'start of year'\)/gi, isoDate("date_trunc('year', now())"));
|
|
1961
|
+
s = s.replace(/DATE\('now',\s*'(-?\d+)\s*days?'\)/gi, (_m, n) => isoDate(`now() + interval '${n} days'`));
|
|
1962
|
+
s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
|
|
1963
|
+
s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
|
|
1964
|
+
s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
|
|
1965
|
+
const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
|
|
1966
|
+
s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
|
|
1967
|
+
const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
|
|
1968
|
+
return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
|
|
1969
|
+
});
|
|
1970
|
+
s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
|
|
1971
|
+
s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
|
|
1972
|
+
s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
|
|
1973
|
+
s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
|
|
1974
|
+
return s;
|
|
1975
|
+
}
|
|
1976
|
+
function resolveWorkerPath() {
|
|
1977
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1978
|
+
const candidates = [
|
|
1979
|
+
join9(here, "pg-sync-worker.ts"),
|
|
1980
|
+
join9(here, "pg-sync-worker.js"),
|
|
1981
|
+
join9(here, "..", "db", "pg-sync-worker.js")
|
|
1982
|
+
];
|
|
1983
|
+
for (const candidate of candidates)
|
|
1984
|
+
if (existsSync10(candidate))
|
|
1985
|
+
return candidate;
|
|
1986
|
+
return candidates[0];
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
class SyncPgAdapter {
|
|
1990
|
+
worker;
|
|
1991
|
+
port;
|
|
1992
|
+
counter = 0;
|
|
1993
|
+
constructor(dsn) {
|
|
1994
|
+
const workerPath = resolveWorkerPath();
|
|
1995
|
+
this.worker = new Worker(workerPath, { workerData: { dsn } });
|
|
1996
|
+
this.worker.on("error", (err) => console.error(JSON.stringify({ evt: "pg_worker_error", message: err.message })));
|
|
1997
|
+
const channel = new MessageChannel;
|
|
1998
|
+
this.port = channel.port1;
|
|
1999
|
+
this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
|
|
2000
|
+
}
|
|
2001
|
+
query(sql, params) {
|
|
2002
|
+
const pgSql = translateSqliteDates(translateSql(sql, "pg"));
|
|
2003
|
+
const pgParams = translateParams(params);
|
|
2004
|
+
const id = ++this.counter;
|
|
2005
|
+
const signal = new SharedArrayBuffer(4);
|
|
2006
|
+
const sig = new Int32Array(signal);
|
|
2007
|
+
Atomics.store(sig, 0, 0);
|
|
2008
|
+
this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
|
|
2009
|
+
Atomics.wait(sig, 0, 0);
|
|
2010
|
+
let received = receiveMessageOnPort(this.port);
|
|
2011
|
+
for (let attempt = 0;!received && attempt < 1e6; attempt++)
|
|
2012
|
+
received = receiveMessageOnPort(this.port);
|
|
2013
|
+
if (!received)
|
|
2014
|
+
throw new Error("SyncPgAdapter: no reply from worker");
|
|
2015
|
+
const reply = received.message;
|
|
2016
|
+
if (!reply.ok)
|
|
2017
|
+
throw new Error(reply.error);
|
|
2018
|
+
return { rows: reply.rows, rowCount: reply.rowCount };
|
|
2019
|
+
}
|
|
2020
|
+
run(sql, ...params) {
|
|
2021
|
+
const result = this.query(sql, params);
|
|
2022
|
+
return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
|
|
2023
|
+
}
|
|
2024
|
+
get(sql, ...params) {
|
|
2025
|
+
return this.query(sql, params).rows[0] ?? null;
|
|
2026
|
+
}
|
|
2027
|
+
all(sql, ...params) {
|
|
2028
|
+
return this.query(sql, params).rows;
|
|
2029
|
+
}
|
|
2030
|
+
exec(sql) {
|
|
2031
|
+
this.query(sql, []);
|
|
2032
|
+
}
|
|
2033
|
+
prepare(sql) {
|
|
2034
|
+
const self = this;
|
|
2035
|
+
return {
|
|
2036
|
+
run: (...params) => self.run(sql, ...params),
|
|
2037
|
+
get: (...params) => self.get(sql, ...params),
|
|
2038
|
+
all: (...params) => self.all(sql, ...params),
|
|
2039
|
+
finalize: () => {}
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
transaction(fn) {
|
|
2043
|
+
return fn();
|
|
2044
|
+
}
|
|
2045
|
+
close() {
|
|
2046
|
+
this.worker.terminate();
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
var init_sync_pg = () => {};
|
|
2050
|
+
|
|
1662
2051
|
// src/db/pg-migrations.ts
|
|
1663
|
-
var exports_pg_migrations = {};
|
|
1664
|
-
__export(exports_pg_migrations, {
|
|
1665
|
-
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1666
|
-
});
|
|
1667
2052
|
var PG_MIGRATIONS;
|
|
1668
2053
|
var init_pg_migrations = __esm(() => {
|
|
1669
2054
|
PG_MIGRATIONS = [
|
|
@@ -1843,125 +2228,51 @@ var init_pg_migrations = __esm(() => {
|
|
|
1843
2228
|
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
1844
2229
|
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
|
|
1845
2230
|
`CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
|
|
1846
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)
|
|
2231
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`,
|
|
2232
|
+
`ALTER TABLE requests ALTER COLUMN input_tokens TYPE BIGINT`,
|
|
2233
|
+
`ALTER TABLE requests ALTER COLUMN output_tokens TYPE BIGINT`,
|
|
2234
|
+
`ALTER TABLE requests ALTER COLUMN cache_read_tokens TYPE BIGINT`,
|
|
2235
|
+
`ALTER TABLE requests ALTER COLUMN cache_create_tokens TYPE BIGINT`,
|
|
2236
|
+
`ALTER TABLE requests ALTER COLUMN cache_create_5m_tokens TYPE BIGINT`,
|
|
2237
|
+
`ALTER TABLE requests ALTER COLUMN cache_create_1h_tokens TYPE BIGINT`,
|
|
2238
|
+
`ALTER TABLE requests ALTER COLUMN duration_ms TYPE BIGINT`,
|
|
2239
|
+
`ALTER TABLE sessions ALTER COLUMN total_tokens TYPE BIGINT`,
|
|
2240
|
+
`ALTER TABLE sessions ALTER COLUMN request_count TYPE BIGINT`,
|
|
2241
|
+
`ALTER TABLE requests ALTER COLUMN cost_usd TYPE DOUBLE PRECISION`,
|
|
2242
|
+
`ALTER TABLE sessions ALTER COLUMN total_cost_usd TYPE DOUBLE PRECISION`,
|
|
2243
|
+
`ALTER TABLE budgets ALTER COLUMN limit_usd TYPE DOUBLE PRECISION`,
|
|
2244
|
+
`ALTER TABLE goals ALTER COLUMN limit_usd TYPE DOUBLE PRECISION`,
|
|
2245
|
+
`ALTER TABLE model_pricing ALTER COLUMN input_per_1m TYPE DOUBLE PRECISION`,
|
|
2246
|
+
`ALTER TABLE model_pricing ALTER COLUMN output_per_1m TYPE DOUBLE PRECISION`,
|
|
2247
|
+
`ALTER TABLE model_pricing ALTER COLUMN cache_read_per_1m TYPE DOUBLE PRECISION`,
|
|
2248
|
+
`ALTER TABLE model_pricing ALTER COLUMN cache_write_per_1m TYPE DOUBLE PRECISION`,
|
|
2249
|
+
`ALTER TABLE model_pricing ALTER COLUMN cache_write_1h_per_1m TYPE DOUBLE PRECISION`,
|
|
2250
|
+
`ALTER TABLE model_pricing ALTER COLUMN cache_storage_per_1m_hour TYPE DOUBLE PRECISION`,
|
|
2251
|
+
`ALTER TABLE billing_daily ALTER COLUMN cost_usd TYPE DOUBLE PRECISION`,
|
|
2252
|
+
`ALTER TABLE subscriptions ALTER COLUMN monthly_fee_usd TYPE DOUBLE PRECISION`,
|
|
2253
|
+
`ALTER TABLE subscriptions ALTER COLUMN included_usage_usd TYPE DOUBLE PRECISION`,
|
|
2254
|
+
`ALTER TABLE usage_snapshots ALTER COLUMN value TYPE DOUBLE PRECISION`,
|
|
2255
|
+
`ALTER TABLE savings_daily ALTER COLUMN api_equivalent_usd TYPE DOUBLE PRECISION`,
|
|
2256
|
+
`ALTER TABLE savings_daily ALTER COLUMN subscription_fee_usd TYPE DOUBLE PRECISION`,
|
|
2257
|
+
`ALTER TABLE savings_daily ALTER COLUMN included_consumed_usd TYPE DOUBLE PRECISION`,
|
|
2258
|
+
`ALTER TABLE savings_daily ALTER COLUMN on_demand_usd TYPE DOUBLE PRECISION`,
|
|
2259
|
+
`ALTER TABLE savings_daily ALTER COLUMN saved_usd TYPE DOUBLE PRECISION`
|
|
1847
2260
|
];
|
|
1848
2261
|
});
|
|
1849
2262
|
|
|
1850
|
-
// src/db/sync-pg.ts
|
|
1851
|
-
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
1852
|
-
import { fileURLToPath } from "url";
|
|
1853
|
-
import { dirname, join as join9 } from "path";
|
|
1854
|
-
import { existsSync as existsSync10 } from "fs";
|
|
1855
|
-
import { translateSql, translateParams } from "@hasna/cloud";
|
|
1856
|
-
function translateSqliteDates(sql) {
|
|
1857
|
-
let s = sql;
|
|
1858
|
-
const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
|
|
1859
|
-
s = s.replace(/DATE\('now',\s*'weekday 0',\s*'-7 days'\)/gi, isoDate("(date_trunc('week', now() + interval '1 day') - interval '1 day')"));
|
|
1860
|
-
s = s.replace(/DATE\('now',\s*'start of month'\)/gi, isoDate("date_trunc('month', now())"));
|
|
1861
|
-
s = s.replace(/DATE\('now',\s*'start of year'\)/gi, isoDate("date_trunc('year', now())"));
|
|
1862
|
-
s = s.replace(/DATE\('now',\s*'(-?\d+)\s*days?'\)/gi, (_m, n) => isoDate(`now() + interval '${n} days'`));
|
|
1863
|
-
s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
|
|
1864
|
-
s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
|
|
1865
|
-
s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
|
|
1866
|
-
const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
|
|
1867
|
-
s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
|
|
1868
|
-
const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
|
|
1869
|
-
return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
|
|
1870
|
-
});
|
|
1871
|
-
s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
|
|
1872
|
-
s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
|
|
1873
|
-
s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
|
|
1874
|
-
s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
|
|
1875
|
-
return s;
|
|
1876
|
-
}
|
|
1877
|
-
function resolveWorkerPath() {
|
|
1878
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
1879
|
-
const candidates = [
|
|
1880
|
-
join9(here, "pg-sync-worker.ts"),
|
|
1881
|
-
join9(here, "pg-sync-worker.js"),
|
|
1882
|
-
join9(here, "..", "db", "pg-sync-worker.js")
|
|
1883
|
-
];
|
|
1884
|
-
for (const candidate of candidates)
|
|
1885
|
-
if (existsSync10(candidate))
|
|
1886
|
-
return candidate;
|
|
1887
|
-
return candidates[0];
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
class SyncPgAdapter {
|
|
1891
|
-
worker;
|
|
1892
|
-
port;
|
|
1893
|
-
counter = 0;
|
|
1894
|
-
constructor(dsn) {
|
|
1895
|
-
const workerPath = resolveWorkerPath();
|
|
1896
|
-
this.worker = new Worker(workerPath, { workerData: { dsn } });
|
|
1897
|
-
this.worker.on("error", (err) => console.error(JSON.stringify({ evt: "pg_worker_error", message: err.message })));
|
|
1898
|
-
const channel = new MessageChannel;
|
|
1899
|
-
this.port = channel.port1;
|
|
1900
|
-
this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
|
|
1901
|
-
}
|
|
1902
|
-
query(sql, params) {
|
|
1903
|
-
const pgSql = translateSqliteDates(translateSql(sql, "pg"));
|
|
1904
|
-
const pgParams = translateParams(params);
|
|
1905
|
-
const id = ++this.counter;
|
|
1906
|
-
const signal = new SharedArrayBuffer(4);
|
|
1907
|
-
const sig = new Int32Array(signal);
|
|
1908
|
-
Atomics.store(sig, 0, 0);
|
|
1909
|
-
this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
|
|
1910
|
-
Atomics.wait(sig, 0, 0);
|
|
1911
|
-
let received = receiveMessageOnPort(this.port);
|
|
1912
|
-
for (let attempt = 0;!received && attempt < 1e6; attempt++)
|
|
1913
|
-
received = receiveMessageOnPort(this.port);
|
|
1914
|
-
if (!received)
|
|
1915
|
-
throw new Error("SyncPgAdapter: no reply from worker");
|
|
1916
|
-
const reply = received.message;
|
|
1917
|
-
if (!reply.ok)
|
|
1918
|
-
throw new Error(reply.error);
|
|
1919
|
-
return { rows: reply.rows, rowCount: reply.rowCount };
|
|
1920
|
-
}
|
|
1921
|
-
run(sql, ...params) {
|
|
1922
|
-
const result = this.query(sql, params);
|
|
1923
|
-
return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
|
|
1924
|
-
}
|
|
1925
|
-
get(sql, ...params) {
|
|
1926
|
-
return this.query(sql, params).rows[0] ?? null;
|
|
1927
|
-
}
|
|
1928
|
-
all(sql, ...params) {
|
|
1929
|
-
return this.query(sql, params).rows;
|
|
1930
|
-
}
|
|
1931
|
-
exec(sql) {
|
|
1932
|
-
this.query(sql, []);
|
|
1933
|
-
}
|
|
1934
|
-
prepare(sql) {
|
|
1935
|
-
const self = this;
|
|
1936
|
-
return {
|
|
1937
|
-
run: (...params) => self.run(sql, ...params),
|
|
1938
|
-
get: (...params) => self.get(sql, ...params),
|
|
1939
|
-
all: (...params) => self.all(sql, ...params),
|
|
1940
|
-
finalize: () => {}
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
transaction(fn) {
|
|
1944
|
-
return fn();
|
|
1945
|
-
}
|
|
1946
|
-
close() {
|
|
1947
|
-
this.worker.terminate();
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
var init_sync_pg = () => {};
|
|
1951
|
-
|
|
1952
2263
|
// src/db/cloud.ts
|
|
1953
2264
|
var exports_cloud = {};
|
|
1954
2265
|
__export(exports_cloud, {
|
|
1955
2266
|
resolveSigningSecret: () => resolveSigningSecret,
|
|
1956
2267
|
openCloudDatabase: () => openCloudDatabase,
|
|
1957
2268
|
isCloudMode: () => isCloudMode,
|
|
1958
|
-
getCloudDatabaseUrl: () =>
|
|
2269
|
+
getCloudDatabaseUrl: () => getCloudDatabaseUrl,
|
|
1959
2270
|
createCloudPool: () => createCloudPool,
|
|
1960
2271
|
cloudMigrations: () => cloudMigrations,
|
|
1961
2272
|
authClientFromPool: () => authClientFromPool
|
|
1962
2273
|
});
|
|
1963
2274
|
import pg from "pg";
|
|
1964
|
-
function
|
|
2275
|
+
function getCloudDatabaseUrl() {
|
|
1965
2276
|
return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
|
|
1966
2277
|
}
|
|
1967
2278
|
function isCloudMode() {
|
|
@@ -1970,18 +2281,18 @@ function isCloudMode() {
|
|
|
1970
2281
|
return true;
|
|
1971
2282
|
if (mode === "local")
|
|
1972
2283
|
return false;
|
|
1973
|
-
return Boolean(
|
|
2284
|
+
return Boolean(getCloudDatabaseUrl());
|
|
1974
2285
|
}
|
|
1975
2286
|
function resolveSigningSecret() {
|
|
1976
2287
|
return process.env["HASNA_ECONOMY_API_SIGNING_KEY"]?.trim() || process.env["HASNA_API_SIGNING_KEY"]?.trim() || process.env["API_KEY_SIGNING_SECRET"]?.trim() || undefined;
|
|
1977
2288
|
}
|
|
1978
|
-
function openCloudDatabase(dsn =
|
|
2289
|
+
function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
|
|
1979
2290
|
if (!dsn) {
|
|
1980
2291
|
throw new Error("economy cloud mode requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
|
|
1981
2292
|
}
|
|
1982
2293
|
return new SyncPgAdapter(dsn);
|
|
1983
2294
|
}
|
|
1984
|
-
function createCloudPool(dsn =
|
|
2295
|
+
function createCloudPool(dsn = getCloudDatabaseUrl()) {
|
|
1985
2296
|
if (!dsn)
|
|
1986
2297
|
throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
|
|
1987
2298
|
const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
@@ -4071,157 +4382,10 @@ async function ingestCodexQuota(db, verbose = false) {
|
|
|
4071
4382
|
|
|
4072
4383
|
// src/lib/sync-all.ts
|
|
4073
4384
|
init_database();
|
|
4074
|
-
|
|
4075
|
-
// src/lib/cloud-sync.ts
|
|
4076
|
-
init_database();
|
|
4077
|
-
|
|
4078
|
-
// src/lib/package-metadata.ts
|
|
4079
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
4080
|
-
var cachedMetadata = null;
|
|
4081
|
-
function getPackageMetadata() {
|
|
4082
|
-
if (cachedMetadata)
|
|
4083
|
-
return cachedMetadata;
|
|
4084
|
-
const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
|
|
4085
|
-
const parsed = JSON.parse(raw);
|
|
4086
|
-
cachedMetadata = {
|
|
4087
|
-
name: parsed.name ?? "@hasna/economy",
|
|
4088
|
-
version: parsed.version ?? "0.0.0"
|
|
4089
|
-
};
|
|
4090
|
-
return cachedMetadata;
|
|
4091
|
-
}
|
|
4092
|
-
var packageMetadata = getPackageMetadata();
|
|
4093
|
-
|
|
4094
|
-
// src/lib/cloud-sync.ts
|
|
4095
|
-
var CLOUD_TABLES = [
|
|
4096
|
-
"requests",
|
|
4097
|
-
"sessions",
|
|
4098
|
-
"projects",
|
|
4099
|
-
"budgets",
|
|
4100
|
-
"goals",
|
|
4101
|
-
"model_pricing",
|
|
4102
|
-
"billing_daily",
|
|
4103
|
-
"subscriptions",
|
|
4104
|
-
"usage_snapshots",
|
|
4105
|
-
"savings_daily",
|
|
4106
|
-
"machines",
|
|
4107
|
-
"ingest_state"
|
|
4108
|
-
];
|
|
4109
|
-
function getCloudDatabaseUrl() {
|
|
4110
|
-
return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
|
|
4111
|
-
}
|
|
4112
|
-
function isCloudAutoEnabled() {
|
|
4113
|
-
return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
|
|
4114
|
-
}
|
|
4115
|
-
function getCloudPullIntervalMinutes() {
|
|
4116
|
-
const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
|
|
4117
|
-
if (!raw)
|
|
4118
|
-
return 15;
|
|
4119
|
-
const n = Number(raw);
|
|
4120
|
-
return Number.isFinite(n) && n > 0 ? n : 15;
|
|
4121
|
-
}
|
|
4122
|
-
async function getCloudPg() {
|
|
4123
|
-
const url = getCloudDatabaseUrl();
|
|
4124
|
-
if (!url) {
|
|
4125
|
-
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
4126
|
-
}
|
|
4127
|
-
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
4128
|
-
return new PgAdapterAsync(url);
|
|
4129
|
-
}
|
|
4130
|
-
async function runCloudMigrations(cloud) {
|
|
4131
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
4132
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
4133
|
-
await cloud.run(sql);
|
|
4134
|
-
}
|
|
4135
|
-
}
|
|
4136
|
-
async function cloudPush(opts) {
|
|
4137
|
-
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
4138
|
-
const cloud = await getCloudPg();
|
|
4139
|
-
const local = new SqliteAdapter(getDbPath());
|
|
4140
|
-
await runCloudMigrations(cloud);
|
|
4141
|
-
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
4142
|
-
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
4143
|
-
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
4144
|
-
touchMachineRegistry(local, "push");
|
|
4145
|
-
local.close();
|
|
4146
|
-
await cloud.close();
|
|
4147
|
-
return { rows, machine: getMachineId() };
|
|
4148
|
-
}
|
|
4149
|
-
async function cloudPull(opts) {
|
|
4150
|
-
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
4151
|
-
const cloud = await getCloudPg();
|
|
4152
|
-
const local = new SqliteAdapter(getDbPath());
|
|
4153
|
-
await runCloudMigrations(cloud);
|
|
4154
|
-
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
4155
|
-
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
4156
|
-
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
4157
|
-
touchMachineRegistry(local, "pull");
|
|
4158
|
-
local.close();
|
|
4159
|
-
await cloud.close();
|
|
4160
|
-
setLastCloudPull();
|
|
4161
|
-
return { rows, machine: getMachineId() };
|
|
4162
|
-
}
|
|
4163
|
-
function setLastCloudPull(at = new Date().toISOString()) {
|
|
4164
|
-
const db = openDatabase();
|
|
4165
|
-
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
4166
|
-
}
|
|
4167
|
-
function getLastCloudPull() {
|
|
4168
|
-
const db = openDatabase();
|
|
4169
|
-
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
4170
|
-
return row?.value ?? null;
|
|
4171
|
-
}
|
|
4172
|
-
function shouldPullFromCloud() {
|
|
4173
|
-
if (!getCloudDatabaseUrl())
|
|
4174
|
-
return false;
|
|
4175
|
-
const last = getLastCloudPull();
|
|
4176
|
-
if (!last)
|
|
4177
|
-
return true;
|
|
4178
|
-
const ageMs = Date.now() - new Date(last).getTime();
|
|
4179
|
-
return ageMs > getCloudPullIntervalMinutes() * 60000;
|
|
4180
|
-
}
|
|
4181
|
-
async function maybePullFromCloud() {
|
|
4182
|
-
if (!shouldPullFromCloud())
|
|
4183
|
-
return false;
|
|
4184
|
-
try {
|
|
4185
|
-
await cloudPull();
|
|
4186
|
-
return true;
|
|
4187
|
-
} catch {
|
|
4188
|
-
return false;
|
|
4189
|
-
}
|
|
4190
|
-
}
|
|
4191
|
-
async function maybePushAfterIngest() {
|
|
4192
|
-
if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
|
|
4193
|
-
return false;
|
|
4194
|
-
try {
|
|
4195
|
-
await cloudPush();
|
|
4196
|
-
return true;
|
|
4197
|
-
} catch {
|
|
4198
|
-
return false;
|
|
4199
|
-
}
|
|
4200
|
-
}
|
|
4201
|
-
function touchMachineRegistry(db, direction) {
|
|
4202
|
-
const now = new Date().toISOString();
|
|
4203
|
-
const machine = getMachineId();
|
|
4204
|
-
db.prepare(`
|
|
4205
|
-
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
4206
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4207
|
-
ON CONFLICT(machine_id) DO UPDATE SET
|
|
4208
|
-
hostname = excluded.hostname,
|
|
4209
|
-
last_seen_at = excluded.last_seen_at,
|
|
4210
|
-
last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
|
|
4211
|
-
last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
|
|
4212
|
-
economy_version = excluded.economy_version,
|
|
4213
|
-
updated_at = excluded.updated_at
|
|
4214
|
-
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
4215
|
-
}
|
|
4216
|
-
|
|
4217
|
-
// src/lib/sync-all.ts
|
|
4218
4385
|
async function syncAll(db, opts = {}) {
|
|
4219
4386
|
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
4220
4387
|
const all = !anySpecific;
|
|
4221
|
-
const
|
|
4222
|
-
if (useCloud)
|
|
4223
|
-
await maybePullFromCloud();
|
|
4224
|
-
const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
|
|
4388
|
+
const result = { deduped: 0 };
|
|
4225
4389
|
if (all || opts.claude) {
|
|
4226
4390
|
result.claude = await ingestClaude(db, opts.verbose);
|
|
4227
4391
|
result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
|
|
@@ -4244,11 +4408,729 @@ async function syncAll(db, opts = {}) {
|
|
|
4244
4408
|
result.hermes = await ingestHermes(db, opts.verbose);
|
|
4245
4409
|
if (opts.dedupe !== false)
|
|
4246
4410
|
result.deduped = dedupeRequests(db);
|
|
4247
|
-
if (useCloud)
|
|
4248
|
-
result.cloudPushed = await maybePushAfterIngest();
|
|
4249
4411
|
return result;
|
|
4250
4412
|
}
|
|
4251
4413
|
|
|
4414
|
+
// src/lib/brief.ts
|
|
4415
|
+
init_database();
|
|
4416
|
+
var ZERO_REQUEST_TOTALS = {
|
|
4417
|
+
sessions: 0,
|
|
4418
|
+
request_sessions: 0,
|
|
4419
|
+
requests: 0,
|
|
4420
|
+
input_tokens: 0,
|
|
4421
|
+
output_tokens: 0,
|
|
4422
|
+
cache_read_tokens: 0,
|
|
4423
|
+
cache_create_tokens: 0,
|
|
4424
|
+
cache_create_5m_tokens: 0,
|
|
4425
|
+
cache_create_1h_tokens: 0,
|
|
4426
|
+
total_tokens: 0,
|
|
4427
|
+
cost_usd: 0,
|
|
4428
|
+
last_active: null
|
|
4429
|
+
};
|
|
4430
|
+
var ZERO_SESSION_TOTALS = {
|
|
4431
|
+
sessions: 0,
|
|
4432
|
+
requests: 0,
|
|
4433
|
+
total_tokens: 0,
|
|
4434
|
+
cost_usd: 0,
|
|
4435
|
+
last_active: null
|
|
4436
|
+
};
|
|
4437
|
+
function formatAge(timestamp, now) {
|
|
4438
|
+
if (!timestamp)
|
|
4439
|
+
return "never";
|
|
4440
|
+
const t = new Date(timestamp).getTime();
|
|
4441
|
+
if (!Number.isFinite(t))
|
|
4442
|
+
return "unknown";
|
|
4443
|
+
const ageMs = Math.max(0, now.getTime() - t);
|
|
4444
|
+
const minutes = Math.floor(ageMs / 60000);
|
|
4445
|
+
if (minutes < 1)
|
|
4446
|
+
return "<1m";
|
|
4447
|
+
if (minutes < 60)
|
|
4448
|
+
return `${minutes}m`;
|
|
4449
|
+
const hours = Math.floor(minutes / 60);
|
|
4450
|
+
if (hours < 48)
|
|
4451
|
+
return `${hours}h`;
|
|
4452
|
+
return `${Math.floor(hours / 24)}d`;
|
|
4453
|
+
}
|
|
4454
|
+
function machineFilter(machine) {
|
|
4455
|
+
if (!machine || machine === "all")
|
|
4456
|
+
return;
|
|
4457
|
+
return machine;
|
|
4458
|
+
}
|
|
4459
|
+
function requestPeriodWhere2(period) {
|
|
4460
|
+
if (period === "today")
|
|
4461
|
+
return `DATE(timestamp) = DATE('now')`;
|
|
4462
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
4463
|
+
}
|
|
4464
|
+
function sessionPeriodWhere2(period) {
|
|
4465
|
+
if (period === "today")
|
|
4466
|
+
return `DATE(started_at) = DATE('now')`;
|
|
4467
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
4468
|
+
}
|
|
4469
|
+
function baseRequestTotals(row) {
|
|
4470
|
+
return {
|
|
4471
|
+
...ZERO_REQUEST_TOTALS,
|
|
4472
|
+
...row,
|
|
4473
|
+
sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
|
|
4474
|
+
request_sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
|
|
4475
|
+
requests: Number(row?.requests ?? 0),
|
|
4476
|
+
input_tokens: Number(row?.input_tokens ?? 0),
|
|
4477
|
+
output_tokens: Number(row?.output_tokens ?? 0),
|
|
4478
|
+
cache_read_tokens: Number(row?.cache_read_tokens ?? 0),
|
|
4479
|
+
cache_create_tokens: Number(row?.cache_create_tokens ?? 0),
|
|
4480
|
+
cache_create_5m_tokens: Number(row?.cache_create_5m_tokens ?? 0),
|
|
4481
|
+
cache_create_1h_tokens: Number(row?.cache_create_1h_tokens ?? 0),
|
|
4482
|
+
total_tokens: Number(row?.total_tokens ?? 0),
|
|
4483
|
+
cost_usd: Number(row?.cost_usd ?? 0),
|
|
4484
|
+
last_active: row?.last_active ?? null
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
function baseSessionTotals(row) {
|
|
4488
|
+
return {
|
|
4489
|
+
sessions: Number(row?.sessions ?? 0),
|
|
4490
|
+
requests: Number(row?.requests ?? 0),
|
|
4491
|
+
total_tokens: Number(row?.total_tokens ?? 0),
|
|
4492
|
+
cost_usd: Number(row?.cost_usd ?? 0),
|
|
4493
|
+
last_active: row?.last_active ?? null
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
function latestTimestamp(...values) {
|
|
4497
|
+
return values.filter(Boolean).sort().at(-1) ?? null;
|
|
4498
|
+
}
|
|
4499
|
+
function combineTotals(requests, sessions) {
|
|
4500
|
+
return {
|
|
4501
|
+
sessions: requests.request_sessions + sessions.sessions,
|
|
4502
|
+
requests: requests.requests + sessions.requests,
|
|
4503
|
+
input_tokens: requests.input_tokens,
|
|
4504
|
+
output_tokens: requests.output_tokens,
|
|
4505
|
+
cache_read_tokens: requests.cache_read_tokens,
|
|
4506
|
+
cache_create_tokens: requests.cache_create_tokens,
|
|
4507
|
+
cache_create_5m_tokens: requests.cache_create_5m_tokens,
|
|
4508
|
+
cache_create_1h_tokens: requests.cache_create_1h_tokens,
|
|
4509
|
+
total_tokens: requests.total_tokens + sessions.total_tokens,
|
|
4510
|
+
cost_usd: requests.cost_usd + sessions.cost_usd,
|
|
4511
|
+
last_active: latestTimestamp(requests.last_active, sessions.last_active)
|
|
4512
|
+
};
|
|
4513
|
+
}
|
|
4514
|
+
function emptyTotals() {
|
|
4515
|
+
return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
|
|
4516
|
+
}
|
|
4517
|
+
function requestTotalsSql(where, machine) {
|
|
4518
|
+
const params = [];
|
|
4519
|
+
let machineClause = "";
|
|
4520
|
+
if (machine) {
|
|
4521
|
+
machineClause = " AND machine_id = ?";
|
|
4522
|
+
params.push(machine);
|
|
4523
|
+
}
|
|
4524
|
+
return {
|
|
4525
|
+
sql: `
|
|
4526
|
+
SELECT
|
|
4527
|
+
COUNT(DISTINCT session_id) as request_sessions,
|
|
4528
|
+
COUNT(*) as requests,
|
|
4529
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
4530
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
4531
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
4532
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
4533
|
+
COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
|
|
4534
|
+
COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
4535
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
4536
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
4537
|
+
MAX(timestamp) as last_active
|
|
4538
|
+
FROM requests
|
|
4539
|
+
WHERE ${where}${machineClause}
|
|
4540
|
+
`,
|
|
4541
|
+
params
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
4544
|
+
function sessionOnlyTotalsSql(where, machine) {
|
|
4545
|
+
const params = [];
|
|
4546
|
+
let machineClause = "";
|
|
4547
|
+
if (machine) {
|
|
4548
|
+
machineClause = " AND machine_id = ?";
|
|
4549
|
+
params.push(machine);
|
|
4550
|
+
}
|
|
4551
|
+
return {
|
|
4552
|
+
sql: `
|
|
4553
|
+
SELECT
|
|
4554
|
+
COUNT(*) as sessions,
|
|
4555
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
4556
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
4557
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
4558
|
+
MAX(started_at) as last_active
|
|
4559
|
+
FROM sessions
|
|
4560
|
+
WHERE ${where}${machineClause}
|
|
4561
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
4562
|
+
`,
|
|
4563
|
+
params
|
|
4564
|
+
};
|
|
4565
|
+
}
|
|
4566
|
+
function queryRequestTotals(db, where, params, machine) {
|
|
4567
|
+
const q = requestTotalsSql(where, machine);
|
|
4568
|
+
return baseRequestTotals(db.prepare(q.sql).get(...params, ...q.params));
|
|
4569
|
+
}
|
|
4570
|
+
function querySessionOnlyTotals(db, where, params, machine) {
|
|
4571
|
+
const q = sessionOnlyTotalsSql(where, machine);
|
|
4572
|
+
return baseSessionTotals(db.prepare(q.sql).get(...params, ...q.params));
|
|
4573
|
+
}
|
|
4574
|
+
function queryPeriodTotals(db, period, machine) {
|
|
4575
|
+
const summary = querySummary(db, period, machine);
|
|
4576
|
+
const requests = queryRequestTotals(db, requestPeriodWhere2(period), [], machine);
|
|
4577
|
+
const sessions = querySessionOnlyTotals(db, sessionPeriodWhere2(period), [], machine);
|
|
4578
|
+
const totals = combineTotals(requests, sessions);
|
|
4579
|
+
return {
|
|
4580
|
+
...totals,
|
|
4581
|
+
sessions: summary.sessions,
|
|
4582
|
+
requests: summary.requests,
|
|
4583
|
+
total_tokens: summary.tokens,
|
|
4584
|
+
cost_usd: summary.total_usd
|
|
4585
|
+
};
|
|
4586
|
+
}
|
|
4587
|
+
function querySinceTotals(db, since, machine) {
|
|
4588
|
+
const requests = queryRequestTotals(db, "timestamp >= ?", [since], machine);
|
|
4589
|
+
const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
|
|
4590
|
+
return combineTotals(requests, sessions);
|
|
4591
|
+
}
|
|
4592
|
+
function parseSinceDate(input, now) {
|
|
4593
|
+
const value = input.trim();
|
|
4594
|
+
const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
|
|
4595
|
+
if (match) {
|
|
4596
|
+
const amount = Number(match[1]);
|
|
4597
|
+
const unit = match[2].toLowerCase();
|
|
4598
|
+
const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
|
|
4599
|
+
return {
|
|
4600
|
+
timestamp: new Date(now.getTime() - amount * multiplier).toISOString(),
|
|
4601
|
+
label: value
|
|
4602
|
+
};
|
|
4603
|
+
}
|
|
4604
|
+
const parsed = new Date(value);
|
|
4605
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
4606
|
+
throw new Error("--since must be a duration like 24h, 7d, 30m, 2w, or an ISO date");
|
|
4607
|
+
}
|
|
4608
|
+
return { timestamp: parsed.toISOString(), label: value };
|
|
4609
|
+
}
|
|
4610
|
+
function queryMachineSinceRows(db, since, machine) {
|
|
4611
|
+
const knownMachines = new Set;
|
|
4612
|
+
for (const m of listMachines(db, "all")) {
|
|
4613
|
+
if (m.machine_id)
|
|
4614
|
+
knownMachines.add(m.machine_id);
|
|
4615
|
+
}
|
|
4616
|
+
for (const m of listMachineRegistry(db)) {
|
|
4617
|
+
if (m.machine_id)
|
|
4618
|
+
knownMachines.add(m.machine_id);
|
|
4619
|
+
}
|
|
4620
|
+
if (machine)
|
|
4621
|
+
knownMachines.add(machine);
|
|
4622
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
4623
|
+
const requestRows = db.prepare(`
|
|
4624
|
+
SELECT
|
|
4625
|
+
machine_id,
|
|
4626
|
+
COUNT(DISTINCT session_id) as request_sessions,
|
|
4627
|
+
COUNT(*) as requests,
|
|
4628
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
4629
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
4630
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
4631
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
4632
|
+
COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
|
|
4633
|
+
COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
4634
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
4635
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
4636
|
+
MAX(timestamp) as last_active
|
|
4637
|
+
FROM requests
|
|
4638
|
+
WHERE timestamp >= ?
|
|
4639
|
+
AND machine_id != ''
|
|
4640
|
+
${requestMachineClause}
|
|
4641
|
+
GROUP BY machine_id
|
|
4642
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
4643
|
+
const sessionMachineClause = machine ? " AND machine_id = ?" : "";
|
|
4644
|
+
const sessionRows = db.prepare(`
|
|
4645
|
+
SELECT
|
|
4646
|
+
machine_id,
|
|
4647
|
+
COUNT(*) as sessions,
|
|
4648
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
4649
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
4650
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
4651
|
+
MAX(started_at) as last_active
|
|
4652
|
+
FROM sessions
|
|
4653
|
+
WHERE started_at >= ?
|
|
4654
|
+
AND machine_id != ''
|
|
4655
|
+
${sessionMachineClause}
|
|
4656
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
4657
|
+
GROUP BY machine_id
|
|
4658
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
4659
|
+
const groups = new Map;
|
|
4660
|
+
for (const row of requestRows) {
|
|
4661
|
+
knownMachines.add(row.machine_id);
|
|
4662
|
+
groups.set(row.machine_id, combineTotals(baseRequestTotals(row), ZERO_SESSION_TOTALS));
|
|
4663
|
+
}
|
|
4664
|
+
for (const row of sessionRows) {
|
|
4665
|
+
knownMachines.add(row.machine_id);
|
|
4666
|
+
const current = groups.get(row.machine_id) ?? emptyTotals();
|
|
4667
|
+
const sessions = baseSessionTotals(row);
|
|
4668
|
+
groups.set(row.machine_id, {
|
|
4669
|
+
...current,
|
|
4670
|
+
sessions: current.sessions + sessions.sessions,
|
|
4671
|
+
requests: current.requests + sessions.requests,
|
|
4672
|
+
total_tokens: current.total_tokens + sessions.total_tokens,
|
|
4673
|
+
cost_usd: current.cost_usd + sessions.cost_usd,
|
|
4674
|
+
last_active: latestTimestamp(current.last_active, sessions.last_active)
|
|
4675
|
+
});
|
|
4676
|
+
}
|
|
4677
|
+
const lastData = queryMachineLastData(db, machine);
|
|
4678
|
+
const rows = [...knownMachines].filter((m) => !machine || m === machine).map((machineId) => {
|
|
4679
|
+
const totals = groups.get(machineId) ?? emptyTotals();
|
|
4680
|
+
const lastDataAt = lastData.get(machineId) ?? totals.last_active;
|
|
4681
|
+
return {
|
|
4682
|
+
machine_id: machineId,
|
|
4683
|
+
...totals,
|
|
4684
|
+
last_data_at: lastDataAt,
|
|
4685
|
+
last_data_age: ""
|
|
4686
|
+
};
|
|
4687
|
+
});
|
|
4688
|
+
return rows.sort((a, b) => b.cost_usd - a.cost_usd || a.machine_id.localeCompare(b.machine_id));
|
|
4689
|
+
}
|
|
4690
|
+
function queryMachineLastData(db, machine) {
|
|
4691
|
+
const filter = machine ? " AND machine_id = ?" : "";
|
|
4692
|
+
const requestRows = db.prepare(`
|
|
4693
|
+
SELECT machine_id, MAX(timestamp) as last_at
|
|
4694
|
+
FROM requests
|
|
4695
|
+
WHERE machine_id != ''${filter}
|
|
4696
|
+
GROUP BY machine_id
|
|
4697
|
+
`).all(...machine ? [machine] : []);
|
|
4698
|
+
const sessionRows = db.prepare(`
|
|
4699
|
+
SELECT machine_id, MAX(started_at) as last_at
|
|
4700
|
+
FROM sessions
|
|
4701
|
+
WHERE machine_id != ''${filter}
|
|
4702
|
+
GROUP BY machine_id
|
|
4703
|
+
`).all(...machine ? [machine] : []);
|
|
4704
|
+
const result = new Map;
|
|
4705
|
+
for (const row of [...requestRows, ...sessionRows]) {
|
|
4706
|
+
result.set(row.machine_id, latestTimestamp(result.get(row.machine_id), row.last_at));
|
|
4707
|
+
}
|
|
4708
|
+
return result;
|
|
4709
|
+
}
|
|
4710
|
+
function queryAgentSinceRows(db, since, machine) {
|
|
4711
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
4712
|
+
const sessionMachineClause = machine ? " AND machine_id = ?" : "";
|
|
4713
|
+
const rows = db.prepare(`
|
|
4714
|
+
WITH request_rows AS (
|
|
4715
|
+
SELECT
|
|
4716
|
+
agent,
|
|
4717
|
+
session_id,
|
|
4718
|
+
1 as requests,
|
|
4719
|
+
input_tokens,
|
|
4720
|
+
output_tokens,
|
|
4721
|
+
cache_read_tokens,
|
|
4722
|
+
cache_create_tokens,
|
|
4723
|
+
COALESCE(cache_create_5m_tokens, cache_create_tokens) as cache_create_5m_tokens,
|
|
4724
|
+
COALESCE(cache_create_1h_tokens, 0) as cache_create_1h_tokens,
|
|
4725
|
+
input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
|
|
4726
|
+
cost_usd,
|
|
4727
|
+
timestamp as last_active
|
|
4728
|
+
FROM requests
|
|
4729
|
+
WHERE timestamp >= ?${requestMachineClause}
|
|
4730
|
+
),
|
|
4731
|
+
session_only_rows AS (
|
|
4732
|
+
SELECT
|
|
4733
|
+
agent,
|
|
4734
|
+
id as session_id,
|
|
4735
|
+
COALESCE(request_count, 0) as requests,
|
|
4736
|
+
0 as input_tokens,
|
|
4737
|
+
0 as output_tokens,
|
|
4738
|
+
0 as cache_read_tokens,
|
|
4739
|
+
0 as cache_create_tokens,
|
|
4740
|
+
0 as cache_create_5m_tokens,
|
|
4741
|
+
0 as cache_create_1h_tokens,
|
|
4742
|
+
COALESCE(total_tokens, 0) as total_tokens,
|
|
4743
|
+
COALESCE(total_cost_usd, 0) as cost_usd,
|
|
4744
|
+
started_at as last_active
|
|
4745
|
+
FROM sessions
|
|
4746
|
+
WHERE started_at >= ?${sessionMachineClause}
|
|
4747
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
4748
|
+
),
|
|
4749
|
+
combined AS (
|
|
4750
|
+
SELECT * FROM request_rows
|
|
4751
|
+
UNION ALL
|
|
4752
|
+
SELECT * FROM session_only_rows
|
|
4753
|
+
)
|
|
4754
|
+
SELECT
|
|
4755
|
+
agent,
|
|
4756
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
4757
|
+
COALESCE(SUM(requests), 0) as requests,
|
|
4758
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
4759
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
4760
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
4761
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
4762
|
+
COALESCE(SUM(cache_create_5m_tokens), 0) as cache_create_5m_tokens,
|
|
4763
|
+
COALESCE(SUM(cache_create_1h_tokens), 0) as cache_create_1h_tokens,
|
|
4764
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
4765
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
4766
|
+
MAX(last_active) as last_active
|
|
4767
|
+
FROM combined
|
|
4768
|
+
GROUP BY agent
|
|
4769
|
+
ORDER BY cost_usd DESC
|
|
4770
|
+
`).all(...machine ? [since, machine, since, machine] : [since, since]);
|
|
4771
|
+
if (rows.length > 0)
|
|
4772
|
+
return rows;
|
|
4773
|
+
return queryAgentBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(agentBreakdownToBriefRow);
|
|
4774
|
+
}
|
|
4775
|
+
function agentBreakdownToBriefRow(row) {
|
|
4776
|
+
return {
|
|
4777
|
+
agent: row.agent,
|
|
4778
|
+
sessions: row.sessions,
|
|
4779
|
+
requests: row.requests,
|
|
4780
|
+
input_tokens: 0,
|
|
4781
|
+
output_tokens: 0,
|
|
4782
|
+
cache_read_tokens: 0,
|
|
4783
|
+
cache_create_tokens: 0,
|
|
4784
|
+
cache_create_5m_tokens: 0,
|
|
4785
|
+
cache_create_1h_tokens: 0,
|
|
4786
|
+
total_tokens: row.total_tokens,
|
|
4787
|
+
cost_usd: row.cost_usd,
|
|
4788
|
+
last_active: row.last_active
|
|
4789
|
+
};
|
|
4790
|
+
}
|
|
4791
|
+
function normalizeAccountEmail2(email) {
|
|
4792
|
+
return email.trim().toLowerCase();
|
|
4793
|
+
}
|
|
4794
|
+
function accountKey2(row) {
|
|
4795
|
+
const agent = row.agent || row.account_tool || "unknown";
|
|
4796
|
+
const email = normalizeAccountEmail2(row.account_email);
|
|
4797
|
+
if (email)
|
|
4798
|
+
return `${agent}:${email}`;
|
|
4799
|
+
if (row.account_name)
|
|
4800
|
+
return `${agent}:${row.account_name}`;
|
|
4801
|
+
return row.account_key || `${agent}:unknown`;
|
|
4802
|
+
}
|
|
4803
|
+
function addAccountRow(groups, row) {
|
|
4804
|
+
if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
|
|
4805
|
+
return;
|
|
4806
|
+
const key = accountKey2(row);
|
|
4807
|
+
const email = normalizeAccountEmail2(row.account_email);
|
|
4808
|
+
const current = groups.get(key) ?? {
|
|
4809
|
+
account_key: key,
|
|
4810
|
+
account_tool: row.agent || row.account_tool || "unknown",
|
|
4811
|
+
account_name: row.account_name || email || row.account_key || "unknown",
|
|
4812
|
+
account_email: email || null,
|
|
4813
|
+
account_source: row.account_source || "unknown",
|
|
4814
|
+
sessions: 0,
|
|
4815
|
+
requests: 0,
|
|
4816
|
+
input_tokens: 0,
|
|
4817
|
+
output_tokens: 0,
|
|
4818
|
+
cache_read_tokens: 0,
|
|
4819
|
+
cache_create_tokens: 0,
|
|
4820
|
+
cache_create_5m_tokens: 0,
|
|
4821
|
+
cache_create_1h_tokens: 0,
|
|
4822
|
+
total_tokens: 0,
|
|
4823
|
+
cost_usd: 0,
|
|
4824
|
+
last_active: null
|
|
4825
|
+
};
|
|
4826
|
+
current.sessions += Number(row.sessions ?? 0);
|
|
4827
|
+
if (!current.account_email && email)
|
|
4828
|
+
current.account_email = email;
|
|
4829
|
+
if ((!current.account_name || current.account_name === "unknown") && (row.account_name || email)) {
|
|
4830
|
+
current.account_name = row.account_name || email;
|
|
4831
|
+
}
|
|
4832
|
+
if ((!current.account_source || current.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
|
|
4833
|
+
current.account_source = row.account_source;
|
|
4834
|
+
}
|
|
4835
|
+
current.requests += Number(row.requests ?? 0);
|
|
4836
|
+
current.input_tokens += Number(row.input_tokens ?? 0);
|
|
4837
|
+
current.output_tokens += Number(row.output_tokens ?? 0);
|
|
4838
|
+
current.cache_read_tokens += Number(row.cache_read_tokens ?? 0);
|
|
4839
|
+
current.cache_create_tokens += Number(row.cache_create_tokens ?? 0);
|
|
4840
|
+
current.cache_create_5m_tokens += Number(row.cache_create_5m_tokens ?? 0);
|
|
4841
|
+
current.cache_create_1h_tokens += Number(row.cache_create_1h_tokens ?? 0);
|
|
4842
|
+
current.total_tokens += Number(row.total_tokens ?? 0);
|
|
4843
|
+
current.cost_usd += Number(row.cost_usd ?? 0);
|
|
4844
|
+
current.last_active = latestTimestamp(current.last_active, row.last_active);
|
|
4845
|
+
groups.set(key, current);
|
|
4846
|
+
}
|
|
4847
|
+
function queryAccountSinceRows(db, since, machine) {
|
|
4848
|
+
const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
|
|
4849
|
+
const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
|
|
4850
|
+
const requestRows = db.prepare(`
|
|
4851
|
+
SELECT
|
|
4852
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
|
|
4853
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
|
|
4854
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
|
|
4855
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
|
|
4856
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
|
|
4857
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
4858
|
+
COUNT(DISTINCT r.session_id) as sessions,
|
|
4859
|
+
COUNT(*) as requests,
|
|
4860
|
+
COALESCE(SUM(r.input_tokens), 0) as input_tokens,
|
|
4861
|
+
COALESCE(SUM(r.output_tokens), 0) as output_tokens,
|
|
4862
|
+
COALESCE(SUM(r.cache_read_tokens), 0) as cache_read_tokens,
|
|
4863
|
+
COALESCE(SUM(r.cache_create_tokens), 0) as cache_create_tokens,
|
|
4864
|
+
COALESCE(SUM(COALESCE(r.cache_create_5m_tokens, r.cache_create_tokens, 0)), 0) as cache_create_5m_tokens,
|
|
4865
|
+
COALESCE(SUM(COALESCE(r.cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
4866
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
4867
|
+
COALESCE(SUM(r.cost_usd), 0) as cost_usd,
|
|
4868
|
+
MAX(r.timestamp) as last_active
|
|
4869
|
+
FROM requests r
|
|
4870
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
4871
|
+
WHERE r.timestamp >= ?${requestMachineClause}
|
|
4872
|
+
GROUP BY
|
|
4873
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
|
|
4874
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), ''),
|
|
4875
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), ''),
|
|
4876
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), ''),
|
|
4877
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''),
|
|
4878
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown')
|
|
4879
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
4880
|
+
const sessionRows = db.prepare(`
|
|
4881
|
+
SELECT
|
|
4882
|
+
s.agent as agent,
|
|
4883
|
+
s.account_key as account_key,
|
|
4884
|
+
s.account_tool as account_tool,
|
|
4885
|
+
s.account_name as account_name,
|
|
4886
|
+
COALESCE(s.account_email, '') as account_email,
|
|
4887
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
4888
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
4889
|
+
COALESCE(SUM(s.request_count), 0) as requests,
|
|
4890
|
+
0 as input_tokens,
|
|
4891
|
+
0 as output_tokens,
|
|
4892
|
+
0 as cache_read_tokens,
|
|
4893
|
+
0 as cache_create_tokens,
|
|
4894
|
+
0 as cache_create_5m_tokens,
|
|
4895
|
+
0 as cache_create_1h_tokens,
|
|
4896
|
+
COALESCE(SUM(s.total_tokens), 0) as total_tokens,
|
|
4897
|
+
COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
|
|
4898
|
+
MAX(s.started_at) as last_active
|
|
4899
|
+
FROM sessions s
|
|
4900
|
+
WHERE s.started_at >= ?${sessionMachineClause}
|
|
4901
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = s.id)
|
|
4902
|
+
GROUP BY
|
|
4903
|
+
s.agent,
|
|
4904
|
+
s.account_key,
|
|
4905
|
+
s.account_tool,
|
|
4906
|
+
s.account_name,
|
|
4907
|
+
COALESCE(s.account_email, ''),
|
|
4908
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown')
|
|
4909
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
4910
|
+
const groups = new Map;
|
|
4911
|
+
for (const row of requestRows)
|
|
4912
|
+
addAccountRow(groups, row);
|
|
4913
|
+
for (const row of sessionRows)
|
|
4914
|
+
addAccountRow(groups, row);
|
|
4915
|
+
const rows = [...groups.values()];
|
|
4916
|
+
rows.sort((a, b) => b.cost_usd - a.cost_usd || a.account_key.localeCompare(b.account_key));
|
|
4917
|
+
if (rows.length > 0)
|
|
4918
|
+
return rows;
|
|
4919
|
+
return queryAccountBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(accountBreakdownToBriefRow);
|
|
4920
|
+
}
|
|
4921
|
+
function accountBreakdownToBriefRow(row) {
|
|
4922
|
+
return {
|
|
4923
|
+
account_key: row.account_key,
|
|
4924
|
+
account_tool: row.account_tool,
|
|
4925
|
+
account_name: row.account_name,
|
|
4926
|
+
account_email: row.account_email,
|
|
4927
|
+
account_source: row.account_source,
|
|
4928
|
+
sessions: row.sessions,
|
|
4929
|
+
requests: row.requests,
|
|
4930
|
+
input_tokens: 0,
|
|
4931
|
+
output_tokens: 0,
|
|
4932
|
+
cache_read_tokens: 0,
|
|
4933
|
+
cache_create_tokens: 0,
|
|
4934
|
+
cache_create_5m_tokens: 0,
|
|
4935
|
+
cache_create_1h_tokens: 0,
|
|
4936
|
+
total_tokens: row.total_tokens,
|
|
4937
|
+
cost_usd: row.cost_usd,
|
|
4938
|
+
last_active: row.last_active
|
|
4939
|
+
};
|
|
4940
|
+
}
|
|
4941
|
+
function queryFreshness(db, now, machine, localSync) {
|
|
4942
|
+
const requestFilter = machine ? " AND machine_id = ?" : "";
|
|
4943
|
+
const requestRows = db.prepare(`
|
|
4944
|
+
SELECT machine_id, MAX(timestamp) as max_request_at
|
|
4945
|
+
FROM requests
|
|
4946
|
+
WHERE machine_id != ''${requestFilter}
|
|
4947
|
+
GROUP BY machine_id
|
|
4948
|
+
`).all(...machine ? [machine] : []);
|
|
4949
|
+
const registryRows = listMachineRegistry(db).filter((row) => !machine || row.machine_id === machine);
|
|
4950
|
+
const machines = new Map;
|
|
4951
|
+
for (const row of requestRows) {
|
|
4952
|
+
machines.set(row.machine_id, {
|
|
4953
|
+
machine_id: row.machine_id,
|
|
4954
|
+
max_request_at: row.max_request_at,
|
|
4955
|
+
request_age: formatAge(row.max_request_at, now),
|
|
4956
|
+
last_merge_sync_at: null,
|
|
4957
|
+
merge_sync_age: "never"
|
|
4958
|
+
});
|
|
4959
|
+
}
|
|
4960
|
+
for (const row of registryRows) {
|
|
4961
|
+
const current = machines.get(row.machine_id) ?? {
|
|
4962
|
+
machine_id: row.machine_id,
|
|
4963
|
+
max_request_at: null,
|
|
4964
|
+
request_age: "never",
|
|
4965
|
+
last_merge_sync_at: null,
|
|
4966
|
+
merge_sync_age: "never"
|
|
4967
|
+
};
|
|
4968
|
+
const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
|
|
4969
|
+
current.last_merge_sync_at = syncAt;
|
|
4970
|
+
current.merge_sync_age = formatAge(syncAt, now);
|
|
4971
|
+
machines.set(row.machine_id, current);
|
|
4972
|
+
}
|
|
4973
|
+
if (localSync && (!machine || machine === localSync.machineId)) {
|
|
4974
|
+
const current = machines.get(localSync.machineId) ?? {
|
|
4975
|
+
machine_id: localSync.machineId,
|
|
4976
|
+
max_request_at: null,
|
|
4977
|
+
request_age: "never",
|
|
4978
|
+
last_merge_sync_at: null,
|
|
4979
|
+
merge_sync_age: "never"
|
|
4980
|
+
};
|
|
4981
|
+
current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
|
|
4982
|
+
current.merge_sync_age = formatAge(current.last_merge_sync_at, now);
|
|
4983
|
+
machines.set(localSync.machineId, current);
|
|
4984
|
+
}
|
|
4985
|
+
const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
|
|
4986
|
+
return {
|
|
4987
|
+
machines: rows,
|
|
4988
|
+
max_request_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.max_request_at ?? "never"} (${row.request_age})`).join("; ") : "none",
|
|
4989
|
+
merge_sync_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.last_merge_sync_at ?? "never"} (${row.merge_sync_age})`).join("; ") : "none"
|
|
4990
|
+
};
|
|
4991
|
+
}
|
|
4992
|
+
function buildBrief(db, opts = {}) {
|
|
4993
|
+
const now = opts.now ?? new Date;
|
|
4994
|
+
const machine = machineFilter(opts.machine);
|
|
4995
|
+
const sinceInput = opts.since ?? "24h";
|
|
4996
|
+
const since = parseSinceDate(sinceInput, now);
|
|
4997
|
+
const summaries = [
|
|
4998
|
+
{
|
|
4999
|
+
label: "Today",
|
|
5000
|
+
period: "today",
|
|
5001
|
+
...queryPeriodTotals(db, "today", machine)
|
|
5002
|
+
},
|
|
5003
|
+
{
|
|
5004
|
+
label: "Week",
|
|
5005
|
+
period: "week",
|
|
5006
|
+
...queryPeriodTotals(db, "week", machine)
|
|
5007
|
+
},
|
|
5008
|
+
{
|
|
5009
|
+
label: `Since ${since.label}`,
|
|
5010
|
+
period: "since",
|
|
5011
|
+
since: since.timestamp,
|
|
5012
|
+
...querySinceTotals(db, since.timestamp, machine)
|
|
5013
|
+
}
|
|
5014
|
+
];
|
|
5015
|
+
const machines = queryMachineSinceRows(db, since.timestamp, machine);
|
|
5016
|
+
for (const row of machines) {
|
|
5017
|
+
row.last_data_age = formatAge(row.last_data_at, now);
|
|
5018
|
+
}
|
|
5019
|
+
return {
|
|
5020
|
+
generated_at: now.toISOString(),
|
|
5021
|
+
machine: machine ?? "all",
|
|
5022
|
+
since: {
|
|
5023
|
+
input: sinceInput,
|
|
5024
|
+
timestamp: since.timestamp,
|
|
5025
|
+
label: since.label
|
|
5026
|
+
},
|
|
5027
|
+
summaries,
|
|
5028
|
+
machines,
|
|
5029
|
+
agents: queryAgentSinceRows(db, since.timestamp, machine),
|
|
5030
|
+
accounts: queryAccountSinceRows(db, since.timestamp, machine),
|
|
5031
|
+
freshness: queryFreshness(db, now, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
|
|
5032
|
+
};
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
// src/lib/analytics.ts
|
|
5036
|
+
function queryProjectDetail(db, nameOrPath) {
|
|
5037
|
+
const like = `%${nameOrPath}%`;
|
|
5038
|
+
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
5039
|
+
FROM sessions
|
|
5040
|
+
WHERE project_name LIKE ? OR project_path LIKE ?
|
|
5041
|
+
ORDER BY started_at DESC`).all(like, like);
|
|
5042
|
+
if (sessions.length === 0)
|
|
5043
|
+
return null;
|
|
5044
|
+
const first = sessions[0];
|
|
5045
|
+
const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
|
|
5046
|
+
const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
|
|
5047
|
+
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
5048
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
5049
|
+
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
5050
|
+
AND r.timestamp >= DATE('now', '-14 days')
|
|
5051
|
+
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
5052
|
+
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
5053
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
5054
|
+
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
5055
|
+
GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
|
|
5056
|
+
const top_sessions = [...sessions].sort((a, b) => (b.total_cost_usd ?? 0) - (a.total_cost_usd ?? 0)).slice(0, 5).map((s) => ({ id: s.id, total_cost_usd: s.total_cost_usd ?? 0, started_at: s.started_at }));
|
|
5057
|
+
return {
|
|
5058
|
+
project_name: first.project_name || nameOrPath,
|
|
5059
|
+
project_path: first.project_path || "",
|
|
5060
|
+
total_cost_usd: totalCost,
|
|
5061
|
+
total_tokens: totalTokens,
|
|
5062
|
+
sessions: sessions.length,
|
|
5063
|
+
daily,
|
|
5064
|
+
models,
|
|
5065
|
+
top_sessions
|
|
5066
|
+
};
|
|
5067
|
+
}
|
|
5068
|
+
function exportDateWhere(column, period) {
|
|
5069
|
+
switch (period) {
|
|
5070
|
+
case "today":
|
|
5071
|
+
return `DATE(${column}) = DATE('now')`;
|
|
5072
|
+
case "week":
|
|
5073
|
+
return `${column} >= DATE('now', '-7 days')`;
|
|
5074
|
+
case "all":
|
|
5075
|
+
return "1=1";
|
|
5076
|
+
default:
|
|
5077
|
+
return `${column} >= DATE('now', '-30 days')`;
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
function queryExportRows(db, type, period) {
|
|
5081
|
+
if (type === "requests") {
|
|
5082
|
+
const where2 = exportDateWhere("timestamp", period);
|
|
5083
|
+
return db.prepare(`SELECT * FROM requests WHERE ${where2} ORDER BY timestamp ASC`).all();
|
|
5084
|
+
}
|
|
5085
|
+
const where = exportDateWhere("started_at", period);
|
|
5086
|
+
return db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
5087
|
+
}
|
|
5088
|
+
function queryRangeStats(db, from, to) {
|
|
5089
|
+
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
|
|
5090
|
+
COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
|
|
5091
|
+
FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
5092
|
+
const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
5093
|
+
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
5094
|
+
}
|
|
5095
|
+
function queryForecast(db, now = new Date) {
|
|
5096
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
5097
|
+
const dayOfMonth = now.getDate();
|
|
5098
|
+
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
5099
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
5100
|
+
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
5101
|
+
const projected = dailyAvg * daysInMonth;
|
|
5102
|
+
const sevenDaysAgo = new Date(now);
|
|
5103
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
5104
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
5105
|
+
const last7DailyAvg = last7.cost / 7;
|
|
5106
|
+
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
5107
|
+
FROM requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
|
|
5108
|
+
return {
|
|
5109
|
+
day_of_month: dayOfMonth,
|
|
5110
|
+
days_in_month: daysInMonth,
|
|
5111
|
+
spent_so_far_usd: monthSoFar.cost,
|
|
5112
|
+
daily_avg_usd: dailyAvg,
|
|
5113
|
+
projected_usd: projected,
|
|
5114
|
+
last7_daily_avg_usd: last7DailyAvg,
|
|
5115
|
+
last7_projected_usd: last7DailyAvg * daysInMonth,
|
|
5116
|
+
cheapest_day: dailyCosts[0] ?? null,
|
|
5117
|
+
most_expensive_day: dailyCosts[dailyCosts.length - 1] ?? null
|
|
5118
|
+
};
|
|
5119
|
+
}
|
|
5120
|
+
function queryModelEfficiency(db) {
|
|
5121
|
+
return db.prepare(`SELECT model,
|
|
5122
|
+
COALESCE(SUM(input_tokens),0) as input,
|
|
5123
|
+
COALESCE(SUM(output_tokens),0) as output,
|
|
5124
|
+
COALESCE(SUM(cache_read_tokens),0) as cache_read,
|
|
5125
|
+
COALESCE(SUM(cache_create_tokens),0) as cache_write,
|
|
5126
|
+
COUNT(*) as requests,
|
|
5127
|
+
COALESCE(SUM(cost_usd),0) as cost
|
|
5128
|
+
FROM requests GROUP BY model ORDER BY cost DESC`).all();
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
// src/server/serve.ts
|
|
5132
|
+
init_database();
|
|
5133
|
+
|
|
4252
5134
|
// src/lib/periods.ts
|
|
4253
5135
|
function ymd(date) {
|
|
4254
5136
|
return date.toISOString().substring(0, 10);
|
|
@@ -4369,6 +5251,24 @@ function isAuthorizedRequest(req, path) {
|
|
|
4369
5251
|
import { randomUUID } from "crypto";
|
|
4370
5252
|
import { existsSync as existsSync12 } from "fs";
|
|
4371
5253
|
import { resolve, sep } from "path";
|
|
5254
|
+
|
|
5255
|
+
// src/lib/package-metadata.ts
|
|
5256
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
5257
|
+
var cachedMetadata = null;
|
|
5258
|
+
function getPackageMetadata() {
|
|
5259
|
+
if (cachedMetadata)
|
|
5260
|
+
return cachedMetadata;
|
|
5261
|
+
const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
|
|
5262
|
+
const parsed = JSON.parse(raw);
|
|
5263
|
+
cachedMetadata = {
|
|
5264
|
+
name: parsed.name ?? "@hasna/economy",
|
|
5265
|
+
version: parsed.version ?? "0.0.0"
|
|
5266
|
+
};
|
|
5267
|
+
return cachedMetadata;
|
|
5268
|
+
}
|
|
5269
|
+
var packageMetadata = getPackageMetadata();
|
|
5270
|
+
|
|
5271
|
+
// src/server/serve.ts
|
|
4372
5272
|
init_cloud();
|
|
4373
5273
|
import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
|
|
4374
5274
|
|
|
@@ -4380,85 +5280,1590 @@ var openApiSpec = {
|
|
|
4380
5280
|
version: "0.0.0",
|
|
4381
5281
|
description: "AI coding cost tracker \u2014 self-hosted control plane. Cloud mode is PURE REMOTE per Amendment A1: the serve process reads/writes the shared RDS Postgres directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open."
|
|
4382
5282
|
},
|
|
4383
|
-
servers: [
|
|
4384
|
-
|
|
5283
|
+
servers: [
|
|
5284
|
+
{
|
|
5285
|
+
url: "https://economy.hasna.xyz",
|
|
5286
|
+
description: "prod"
|
|
5287
|
+
},
|
|
5288
|
+
{
|
|
5289
|
+
url: "http://localhost:3456",
|
|
5290
|
+
description: "local"
|
|
5291
|
+
}
|
|
5292
|
+
],
|
|
5293
|
+
security: [
|
|
5294
|
+
{
|
|
5295
|
+
apiKey: []
|
|
5296
|
+
},
|
|
5297
|
+
{
|
|
5298
|
+
bearer: []
|
|
5299
|
+
}
|
|
5300
|
+
],
|
|
4385
5301
|
components: {
|
|
4386
5302
|
securitySchemes: {
|
|
4387
|
-
apiKey: {
|
|
4388
|
-
|
|
5303
|
+
apiKey: {
|
|
5304
|
+
type: "apiKey",
|
|
5305
|
+
in: "header",
|
|
5306
|
+
name: "x-api-key"
|
|
5307
|
+
},
|
|
5308
|
+
bearer: {
|
|
5309
|
+
type: "http",
|
|
5310
|
+
scheme: "bearer"
|
|
5311
|
+
}
|
|
4389
5312
|
},
|
|
4390
5313
|
schemas: {
|
|
4391
|
-
Envelope: {
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
5314
|
+
Envelope: {
|
|
5315
|
+
type: "object",
|
|
5316
|
+
properties: {
|
|
5317
|
+
data: {},
|
|
5318
|
+
meta: {
|
|
5319
|
+
type: "object",
|
|
5320
|
+
additionalProperties: true
|
|
5321
|
+
}
|
|
5322
|
+
},
|
|
5323
|
+
required: [
|
|
5324
|
+
"data"
|
|
5325
|
+
]
|
|
5326
|
+
},
|
|
5327
|
+
Foundation: {
|
|
5328
|
+
type: "object",
|
|
5329
|
+
properties: {
|
|
5330
|
+
status: {
|
|
5331
|
+
type: "string"
|
|
5332
|
+
},
|
|
5333
|
+
version: {
|
|
5334
|
+
type: "string"
|
|
5335
|
+
},
|
|
5336
|
+
mode: {
|
|
5337
|
+
type: "string"
|
|
5338
|
+
},
|
|
5339
|
+
service: {
|
|
5340
|
+
type: "string"
|
|
5341
|
+
}
|
|
5342
|
+
},
|
|
5343
|
+
required: [
|
|
5344
|
+
"status",
|
|
5345
|
+
"version",
|
|
5346
|
+
"mode"
|
|
5347
|
+
]
|
|
5348
|
+
},
|
|
5349
|
+
Error: {
|
|
5350
|
+
type: "object",
|
|
5351
|
+
properties: {
|
|
5352
|
+
error: {
|
|
5353
|
+
type: "string"
|
|
5354
|
+
},
|
|
5355
|
+
message: {
|
|
5356
|
+
type: "string"
|
|
5357
|
+
}
|
|
5358
|
+
},
|
|
5359
|
+
required: [
|
|
5360
|
+
"error"
|
|
5361
|
+
]
|
|
5362
|
+
},
|
|
5363
|
+
MutationOk: {
|
|
5364
|
+
type: "object",
|
|
5365
|
+
properties: {
|
|
5366
|
+
ok: {
|
|
5367
|
+
type: "boolean"
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
},
|
|
5371
|
+
CostSummary: {
|
|
5372
|
+
type: "object",
|
|
5373
|
+
properties: {
|
|
5374
|
+
total_cost_usd: {
|
|
5375
|
+
type: "number"
|
|
5376
|
+
},
|
|
5377
|
+
total_tokens: {
|
|
5378
|
+
type: "number"
|
|
5379
|
+
},
|
|
5380
|
+
request_count: {
|
|
5381
|
+
type: "number"
|
|
5382
|
+
},
|
|
5383
|
+
session_count: {
|
|
5384
|
+
type: "number"
|
|
5385
|
+
},
|
|
5386
|
+
period: {
|
|
5387
|
+
type: "string"
|
|
5388
|
+
}
|
|
5389
|
+
},
|
|
5390
|
+
additionalProperties: true
|
|
5391
|
+
},
|
|
5392
|
+
Session: {
|
|
5393
|
+
type: "object",
|
|
5394
|
+
properties: {
|
|
5395
|
+
id: {
|
|
5396
|
+
type: "string"
|
|
5397
|
+
},
|
|
5398
|
+
agent: {
|
|
5399
|
+
type: "string"
|
|
5400
|
+
},
|
|
5401
|
+
project_path: {
|
|
5402
|
+
type: "string"
|
|
5403
|
+
},
|
|
5404
|
+
project_name: {
|
|
5405
|
+
type: "string"
|
|
5406
|
+
},
|
|
5407
|
+
started_at: {
|
|
5408
|
+
type: "string"
|
|
5409
|
+
},
|
|
5410
|
+
ended_at: {
|
|
5411
|
+
type: "string",
|
|
5412
|
+
nullable: true
|
|
5413
|
+
},
|
|
5414
|
+
total_cost_usd: {
|
|
5415
|
+
type: "number"
|
|
5416
|
+
},
|
|
5417
|
+
total_tokens: {
|
|
5418
|
+
type: "number"
|
|
5419
|
+
},
|
|
5420
|
+
request_count: {
|
|
5421
|
+
type: "number"
|
|
5422
|
+
}
|
|
5423
|
+
},
|
|
5424
|
+
additionalProperties: true
|
|
5425
|
+
},
|
|
5426
|
+
ModelBreakdown: {
|
|
5427
|
+
type: "object",
|
|
5428
|
+
additionalProperties: true
|
|
5429
|
+
},
|
|
5430
|
+
ProjectBreakdown: {
|
|
5431
|
+
type: "object",
|
|
5432
|
+
additionalProperties: true
|
|
5433
|
+
},
|
|
5434
|
+
AgentBreakdown: {
|
|
5435
|
+
type: "object",
|
|
5436
|
+
additionalProperties: true
|
|
5437
|
+
},
|
|
5438
|
+
AccountBreakdown: {
|
|
5439
|
+
type: "object",
|
|
5440
|
+
additionalProperties: true
|
|
5441
|
+
},
|
|
5442
|
+
DailyPoint: {
|
|
5443
|
+
type: "object",
|
|
5444
|
+
additionalProperties: true
|
|
5445
|
+
},
|
|
5446
|
+
BudgetStatus: {
|
|
5447
|
+
type: "object",
|
|
5448
|
+
properties: {
|
|
5449
|
+
id: {
|
|
5450
|
+
type: "string"
|
|
5451
|
+
},
|
|
5452
|
+
limit_usd: {
|
|
5453
|
+
type: "number"
|
|
5454
|
+
},
|
|
5455
|
+
period: {
|
|
5456
|
+
type: "string"
|
|
5457
|
+
},
|
|
5458
|
+
spent_usd: {
|
|
5459
|
+
type: "number"
|
|
5460
|
+
}
|
|
5461
|
+
},
|
|
5462
|
+
additionalProperties: true
|
|
5463
|
+
},
|
|
5464
|
+
CreateBudgetInput: {
|
|
5465
|
+
type: "object",
|
|
5466
|
+
properties: {
|
|
5467
|
+
limit_usd: {
|
|
5468
|
+
type: "number"
|
|
5469
|
+
},
|
|
5470
|
+
alert_at_percent: {
|
|
5471
|
+
type: "number"
|
|
5472
|
+
},
|
|
5473
|
+
period: {
|
|
5474
|
+
type: "string",
|
|
5475
|
+
enum: [
|
|
5476
|
+
"daily",
|
|
5477
|
+
"weekly",
|
|
5478
|
+
"monthly",
|
|
5479
|
+
"day",
|
|
5480
|
+
"week",
|
|
5481
|
+
"month"
|
|
5482
|
+
]
|
|
5483
|
+
},
|
|
5484
|
+
project_path: {
|
|
5485
|
+
type: "string",
|
|
5486
|
+
nullable: true
|
|
5487
|
+
},
|
|
5488
|
+
agent: {
|
|
5489
|
+
type: "string",
|
|
5490
|
+
nullable: true
|
|
5491
|
+
}
|
|
5492
|
+
},
|
|
5493
|
+
required: [
|
|
5494
|
+
"limit_usd"
|
|
5495
|
+
]
|
|
5496
|
+
},
|
|
5497
|
+
GoalStatus: {
|
|
5498
|
+
type: "object",
|
|
5499
|
+
additionalProperties: true
|
|
5500
|
+
},
|
|
5501
|
+
CreateGoalInput: {
|
|
5502
|
+
type: "object",
|
|
5503
|
+
properties: {
|
|
5504
|
+
period: {
|
|
5505
|
+
type: "string",
|
|
5506
|
+
enum: [
|
|
5507
|
+
"day",
|
|
5508
|
+
"week",
|
|
5509
|
+
"month",
|
|
5510
|
+
"year"
|
|
5511
|
+
]
|
|
5512
|
+
},
|
|
5513
|
+
limit_usd: {
|
|
5514
|
+
type: "number"
|
|
5515
|
+
},
|
|
5516
|
+
project_path: {
|
|
5517
|
+
type: "string",
|
|
5518
|
+
nullable: true
|
|
5519
|
+
},
|
|
5520
|
+
agent: {
|
|
5521
|
+
type: "string",
|
|
5522
|
+
nullable: true
|
|
5523
|
+
}
|
|
5524
|
+
},
|
|
5525
|
+
required: [
|
|
5526
|
+
"limit_usd"
|
|
5527
|
+
]
|
|
5528
|
+
},
|
|
5529
|
+
ModelPricing: {
|
|
5530
|
+
type: "object",
|
|
5531
|
+
additionalProperties: true
|
|
5532
|
+
},
|
|
5533
|
+
CreatePricingInput: {
|
|
5534
|
+
type: "object",
|
|
5535
|
+
properties: {
|
|
5536
|
+
model: {
|
|
5537
|
+
type: "string"
|
|
5538
|
+
},
|
|
5539
|
+
input_per_1m: {
|
|
5540
|
+
type: "number"
|
|
5541
|
+
},
|
|
5542
|
+
output_per_1m: {
|
|
5543
|
+
type: "number"
|
|
5544
|
+
}
|
|
5545
|
+
},
|
|
5546
|
+
required: [
|
|
5547
|
+
"model",
|
|
5548
|
+
"input_per_1m",
|
|
5549
|
+
"output_per_1m"
|
|
5550
|
+
]
|
|
5551
|
+
},
|
|
5552
|
+
Subscription: {
|
|
5553
|
+
type: "object",
|
|
5554
|
+
additionalProperties: true
|
|
5555
|
+
},
|
|
5556
|
+
CreateSubscriptionInput: {
|
|
5557
|
+
type: "object",
|
|
5558
|
+
properties: {
|
|
5559
|
+
provider: {
|
|
5560
|
+
type: "string"
|
|
5561
|
+
},
|
|
5562
|
+
plan: {
|
|
5563
|
+
type: "string"
|
|
5564
|
+
},
|
|
5565
|
+
monthly_fee_usd: {
|
|
5566
|
+
type: "number"
|
|
5567
|
+
},
|
|
5568
|
+
included_usage_usd: {
|
|
5569
|
+
type: "number"
|
|
5570
|
+
},
|
|
5571
|
+
agent: {
|
|
5572
|
+
type: "string",
|
|
5573
|
+
nullable: true
|
|
5574
|
+
}
|
|
5575
|
+
},
|
|
5576
|
+
required: [
|
|
5577
|
+
"provider",
|
|
5578
|
+
"plan"
|
|
5579
|
+
]
|
|
5580
|
+
},
|
|
5581
|
+
Project: {
|
|
5582
|
+
type: "object",
|
|
5583
|
+
additionalProperties: true
|
|
5584
|
+
},
|
|
5585
|
+
CreateProjectInput: {
|
|
5586
|
+
type: "object",
|
|
5587
|
+
properties: {
|
|
5588
|
+
path: {
|
|
5589
|
+
type: "string"
|
|
5590
|
+
},
|
|
5591
|
+
name: {
|
|
5592
|
+
type: "string"
|
|
5593
|
+
},
|
|
5594
|
+
description: {
|
|
5595
|
+
type: "string"
|
|
5596
|
+
},
|
|
5597
|
+
tags: {
|
|
5598
|
+
type: "array",
|
|
5599
|
+
items: {
|
|
5600
|
+
type: "string"
|
|
5601
|
+
}
|
|
5602
|
+
}
|
|
5603
|
+
},
|
|
5604
|
+
required: [
|
|
5605
|
+
"path"
|
|
5606
|
+
]
|
|
5607
|
+
},
|
|
5608
|
+
MachineInfo: {
|
|
5609
|
+
type: "object",
|
|
5610
|
+
additionalProperties: true
|
|
5611
|
+
},
|
|
5612
|
+
BillingSummary: {
|
|
5613
|
+
type: "object",
|
|
5614
|
+
additionalProperties: true
|
|
5615
|
+
}
|
|
4414
5616
|
}
|
|
4415
5617
|
},
|
|
4416
5618
|
paths: {
|
|
4417
|
-
"/health": {
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
5619
|
+
"/health": {
|
|
5620
|
+
get: {
|
|
5621
|
+
operationId: "health",
|
|
5622
|
+
summary: "Liveness probe",
|
|
5623
|
+
security: [],
|
|
5624
|
+
responses: {
|
|
5625
|
+
"200": {
|
|
5626
|
+
description: "ok",
|
|
5627
|
+
content: {
|
|
5628
|
+
"application/json": {
|
|
5629
|
+
schema: {
|
|
5630
|
+
$ref: "#/components/schemas/Foundation"
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
},
|
|
5638
|
+
"/ready": {
|
|
5639
|
+
get: {
|
|
5640
|
+
operationId: "ready",
|
|
5641
|
+
summary: "Readiness probe (storage reachable + migrated)",
|
|
5642
|
+
security: [],
|
|
5643
|
+
responses: {
|
|
5644
|
+
"200": {
|
|
5645
|
+
description: "ready",
|
|
5646
|
+
content: {
|
|
5647
|
+
"application/json": {
|
|
5648
|
+
schema: {
|
|
5649
|
+
$ref: "#/components/schemas/Foundation"
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
},
|
|
5654
|
+
"503": {
|
|
5655
|
+
description: "not ready",
|
|
5656
|
+
content: {
|
|
5657
|
+
"application/json": {
|
|
5658
|
+
schema: {
|
|
5659
|
+
$ref: "#/components/schemas/Foundation"
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
},
|
|
5667
|
+
"/version": {
|
|
5668
|
+
get: {
|
|
5669
|
+
operationId: "version",
|
|
5670
|
+
summary: "Version probe",
|
|
5671
|
+
security: [],
|
|
5672
|
+
responses: {
|
|
5673
|
+
"200": {
|
|
5674
|
+
description: "version",
|
|
5675
|
+
content: {
|
|
5676
|
+
"application/json": {
|
|
5677
|
+
schema: {
|
|
5678
|
+
$ref: "#/components/schemas/Foundation"
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
},
|
|
5686
|
+
"/v1/summary": {
|
|
5687
|
+
get: {
|
|
5688
|
+
operationId: "getSummary",
|
|
5689
|
+
summary: "Cost summary for a period",
|
|
5690
|
+
parameters: [
|
|
5691
|
+
{
|
|
5692
|
+
name: "period",
|
|
5693
|
+
in: "query",
|
|
5694
|
+
schema: {
|
|
5695
|
+
type: "string"
|
|
5696
|
+
}
|
|
5697
|
+
},
|
|
5698
|
+
{
|
|
5699
|
+
name: "machine",
|
|
5700
|
+
in: "query",
|
|
5701
|
+
schema: {
|
|
5702
|
+
type: "string"
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
],
|
|
5706
|
+
responses: {
|
|
5707
|
+
"200": {
|
|
5708
|
+
description: "ok",
|
|
5709
|
+
content: {
|
|
5710
|
+
"application/json": {
|
|
5711
|
+
schema: {
|
|
5712
|
+
type: "object",
|
|
5713
|
+
properties: {
|
|
5714
|
+
data: {
|
|
5715
|
+
$ref: "#/components/schemas/CostSummary"
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
},
|
|
5725
|
+
"/v1/sessions": {
|
|
5726
|
+
get: {
|
|
5727
|
+
operationId: "listSessions",
|
|
5728
|
+
summary: "List sessions",
|
|
5729
|
+
parameters: [
|
|
5730
|
+
{
|
|
5731
|
+
name: "agent",
|
|
5732
|
+
in: "query",
|
|
5733
|
+
schema: {
|
|
5734
|
+
type: "string"
|
|
5735
|
+
}
|
|
5736
|
+
},
|
|
5737
|
+
{
|
|
5738
|
+
name: "project",
|
|
5739
|
+
in: "query",
|
|
5740
|
+
schema: {
|
|
5741
|
+
type: "string"
|
|
5742
|
+
}
|
|
5743
|
+
},
|
|
5744
|
+
{
|
|
5745
|
+
name: "search",
|
|
5746
|
+
in: "query",
|
|
5747
|
+
schema: {
|
|
5748
|
+
type: "string"
|
|
5749
|
+
}
|
|
5750
|
+
},
|
|
5751
|
+
{
|
|
5752
|
+
name: "machine",
|
|
5753
|
+
in: "query",
|
|
5754
|
+
schema: {
|
|
5755
|
+
type: "string"
|
|
5756
|
+
}
|
|
5757
|
+
},
|
|
5758
|
+
{
|
|
5759
|
+
name: "account",
|
|
5760
|
+
in: "query",
|
|
5761
|
+
schema: {
|
|
5762
|
+
type: "string"
|
|
5763
|
+
}
|
|
5764
|
+
},
|
|
5765
|
+
{
|
|
5766
|
+
name: "limit",
|
|
5767
|
+
in: "query",
|
|
5768
|
+
schema: {
|
|
5769
|
+
type: "integer"
|
|
5770
|
+
}
|
|
5771
|
+
},
|
|
5772
|
+
{
|
|
5773
|
+
name: "offset",
|
|
5774
|
+
in: "query",
|
|
5775
|
+
schema: {
|
|
5776
|
+
type: "integer"
|
|
5777
|
+
}
|
|
5778
|
+
},
|
|
5779
|
+
{
|
|
5780
|
+
name: "since",
|
|
5781
|
+
in: "query",
|
|
5782
|
+
schema: {
|
|
5783
|
+
type: "string"
|
|
5784
|
+
}
|
|
5785
|
+
},
|
|
5786
|
+
{
|
|
5787
|
+
name: "fields",
|
|
5788
|
+
in: "query",
|
|
5789
|
+
schema: {
|
|
5790
|
+
type: "string"
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
],
|
|
5794
|
+
responses: {
|
|
5795
|
+
"200": {
|
|
5796
|
+
description: "ok",
|
|
5797
|
+
content: {
|
|
5798
|
+
"application/json": {
|
|
5799
|
+
schema: {
|
|
5800
|
+
type: "object",
|
|
5801
|
+
properties: {
|
|
5802
|
+
data: {
|
|
5803
|
+
type: "array",
|
|
5804
|
+
items: {
|
|
5805
|
+
$ref: "#/components/schemas/Session"
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
},
|
|
5816
|
+
"/v1/sessions/{id}/requests": {
|
|
5817
|
+
get: {
|
|
5818
|
+
operationId: "getSessionRequests",
|
|
5819
|
+
summary: "Requests for a session",
|
|
5820
|
+
parameters: [
|
|
5821
|
+
{
|
|
5822
|
+
name: "id",
|
|
5823
|
+
in: "path",
|
|
5824
|
+
required: true,
|
|
5825
|
+
schema: {
|
|
5826
|
+
type: "string"
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5829
|
+
],
|
|
5830
|
+
responses: {
|
|
5831
|
+
"200": {
|
|
5832
|
+
description: "ok",
|
|
5833
|
+
content: {
|
|
5834
|
+
"application/json": {
|
|
5835
|
+
schema: {
|
|
5836
|
+
$ref: "#/components/schemas/Envelope"
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
},
|
|
5841
|
+
"404": {
|
|
5842
|
+
description: "not found",
|
|
5843
|
+
content: {
|
|
5844
|
+
"application/json": {
|
|
5845
|
+
schema: {
|
|
5846
|
+
$ref: "#/components/schemas/Error"
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
}
|
|
5853
|
+
},
|
|
5854
|
+
"/v1/top": {
|
|
5855
|
+
get: {
|
|
5856
|
+
operationId: "topSessions",
|
|
5857
|
+
summary: "Top sessions by cost",
|
|
5858
|
+
parameters: [
|
|
5859
|
+
{
|
|
5860
|
+
name: "n",
|
|
5861
|
+
in: "query",
|
|
5862
|
+
schema: {
|
|
5863
|
+
type: "integer"
|
|
5864
|
+
}
|
|
5865
|
+
},
|
|
5866
|
+
{
|
|
5867
|
+
name: "agent",
|
|
5868
|
+
in: "query",
|
|
5869
|
+
schema: {
|
|
5870
|
+
type: "string"
|
|
5871
|
+
}
|
|
5872
|
+
},
|
|
5873
|
+
{
|
|
5874
|
+
name: "since",
|
|
5875
|
+
in: "query",
|
|
5876
|
+
schema: {
|
|
5877
|
+
type: "string"
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
],
|
|
5881
|
+
responses: {
|
|
5882
|
+
"200": {
|
|
5883
|
+
description: "ok",
|
|
5884
|
+
content: {
|
|
5885
|
+
"application/json": {
|
|
5886
|
+
schema: {
|
|
5887
|
+
type: "object",
|
|
5888
|
+
properties: {
|
|
5889
|
+
data: {
|
|
5890
|
+
type: "array",
|
|
5891
|
+
items: {
|
|
5892
|
+
$ref: "#/components/schemas/Session"
|
|
5893
|
+
}
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
},
|
|
5903
|
+
"/v1/models": {
|
|
5904
|
+
get: {
|
|
5905
|
+
operationId: "modelBreakdown",
|
|
5906
|
+
summary: "Cost by model",
|
|
5907
|
+
responses: {
|
|
5908
|
+
"200": {
|
|
5909
|
+
description: "ok",
|
|
5910
|
+
content: {
|
|
5911
|
+
"application/json": {
|
|
5912
|
+
schema: {
|
|
5913
|
+
type: "object",
|
|
5914
|
+
properties: {
|
|
5915
|
+
data: {
|
|
5916
|
+
type: "array",
|
|
5917
|
+
items: {
|
|
5918
|
+
$ref: "#/components/schemas/ModelBreakdown"
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
},
|
|
5929
|
+
"/v1/projects": {
|
|
5930
|
+
get: {
|
|
5931
|
+
operationId: "projectBreakdown",
|
|
5932
|
+
summary: "Cost by project",
|
|
5933
|
+
parameters: [
|
|
5934
|
+
{
|
|
5935
|
+
name: "period",
|
|
5936
|
+
in: "query",
|
|
5937
|
+
schema: {
|
|
5938
|
+
type: "string"
|
|
5939
|
+
}
|
|
5940
|
+
},
|
|
5941
|
+
{
|
|
5942
|
+
name: "machine",
|
|
5943
|
+
in: "query",
|
|
5944
|
+
schema: {
|
|
5945
|
+
type: "string"
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
],
|
|
5949
|
+
responses: {
|
|
5950
|
+
"200": {
|
|
5951
|
+
description: "ok",
|
|
5952
|
+
content: {
|
|
5953
|
+
"application/json": {
|
|
5954
|
+
schema: {
|
|
5955
|
+
type: "object",
|
|
5956
|
+
properties: {
|
|
5957
|
+
data: {
|
|
5958
|
+
type: "array",
|
|
5959
|
+
items: {
|
|
5960
|
+
$ref: "#/components/schemas/ProjectBreakdown"
|
|
5961
|
+
}
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
},
|
|
5971
|
+
"/v1/accounts": {
|
|
5972
|
+
get: {
|
|
5973
|
+
operationId: "accountBreakdown",
|
|
5974
|
+
summary: "Cost by account",
|
|
5975
|
+
parameters: [
|
|
5976
|
+
{
|
|
5977
|
+
name: "period",
|
|
5978
|
+
in: "query",
|
|
5979
|
+
schema: {
|
|
5980
|
+
type: "string"
|
|
5981
|
+
}
|
|
5982
|
+
},
|
|
5983
|
+
{
|
|
5984
|
+
name: "machine",
|
|
5985
|
+
in: "query",
|
|
5986
|
+
schema: {
|
|
5987
|
+
type: "string"
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
],
|
|
5991
|
+
responses: {
|
|
5992
|
+
"200": {
|
|
5993
|
+
description: "ok",
|
|
5994
|
+
content: {
|
|
5995
|
+
"application/json": {
|
|
5996
|
+
schema: {
|
|
5997
|
+
type: "object",
|
|
5998
|
+
properties: {
|
|
5999
|
+
data: {
|
|
6000
|
+
type: "array",
|
|
6001
|
+
items: {
|
|
6002
|
+
$ref: "#/components/schemas/AccountBreakdown"
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
},
|
|
6013
|
+
"/v1/breakdown": {
|
|
6014
|
+
get: {
|
|
6015
|
+
operationId: "breakdown",
|
|
6016
|
+
summary: "Cost breakdown by dimension",
|
|
6017
|
+
parameters: [
|
|
6018
|
+
{
|
|
6019
|
+
name: "by",
|
|
6020
|
+
in: "query",
|
|
6021
|
+
schema: {
|
|
6022
|
+
type: "string",
|
|
6023
|
+
enum: [
|
|
6024
|
+
"model",
|
|
6025
|
+
"project",
|
|
6026
|
+
"agent",
|
|
6027
|
+
"account"
|
|
6028
|
+
]
|
|
6029
|
+
}
|
|
6030
|
+
},
|
|
6031
|
+
{
|
|
6032
|
+
name: "period",
|
|
6033
|
+
in: "query",
|
|
6034
|
+
schema: {
|
|
6035
|
+
type: "string"
|
|
6036
|
+
}
|
|
6037
|
+
},
|
|
6038
|
+
{
|
|
6039
|
+
name: "machine",
|
|
6040
|
+
in: "query",
|
|
6041
|
+
schema: {
|
|
6042
|
+
type: "string"
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
],
|
|
6046
|
+
responses: {
|
|
6047
|
+
"200": {
|
|
6048
|
+
description: "ok",
|
|
6049
|
+
content: {
|
|
6050
|
+
"application/json": {
|
|
6051
|
+
schema: {
|
|
6052
|
+
$ref: "#/components/schemas/Envelope"
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
},
|
|
6060
|
+
"/v1/daily": {
|
|
6061
|
+
get: {
|
|
6062
|
+
operationId: "daily",
|
|
6063
|
+
summary: "Daily cost points",
|
|
6064
|
+
parameters: [
|
|
6065
|
+
{
|
|
6066
|
+
name: "days",
|
|
6067
|
+
in: "query",
|
|
6068
|
+
schema: {
|
|
6069
|
+
type: "integer"
|
|
6070
|
+
}
|
|
6071
|
+
},
|
|
6072
|
+
{
|
|
6073
|
+
name: "machine",
|
|
6074
|
+
in: "query",
|
|
6075
|
+
schema: {
|
|
6076
|
+
type: "string"
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
],
|
|
6080
|
+
responses: {
|
|
6081
|
+
"200": {
|
|
6082
|
+
description: "ok",
|
|
6083
|
+
content: {
|
|
6084
|
+
"application/json": {
|
|
6085
|
+
schema: {
|
|
6086
|
+
type: "object",
|
|
6087
|
+
properties: {
|
|
6088
|
+
data: {
|
|
6089
|
+
type: "array",
|
|
6090
|
+
items: {
|
|
6091
|
+
$ref: "#/components/schemas/DailyPoint"
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
}
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
},
|
|
6102
|
+
"/v1/hourly": {
|
|
6103
|
+
get: {
|
|
6104
|
+
operationId: "hourly",
|
|
6105
|
+
summary: "Hourly cost points",
|
|
6106
|
+
parameters: [
|
|
6107
|
+
{
|
|
6108
|
+
name: "hours",
|
|
6109
|
+
in: "query",
|
|
6110
|
+
schema: {
|
|
6111
|
+
type: "integer"
|
|
6112
|
+
}
|
|
6113
|
+
},
|
|
6114
|
+
{
|
|
6115
|
+
name: "machine",
|
|
6116
|
+
in: "query",
|
|
6117
|
+
schema: {
|
|
6118
|
+
type: "string"
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
],
|
|
6122
|
+
responses: {
|
|
6123
|
+
"200": {
|
|
6124
|
+
description: "ok",
|
|
6125
|
+
content: {
|
|
6126
|
+
"application/json": {
|
|
6127
|
+
schema: {
|
|
6128
|
+
$ref: "#/components/schemas/Envelope"
|
|
6129
|
+
}
|
|
6130
|
+
}
|
|
6131
|
+
}
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
}
|
|
6135
|
+
},
|
|
6136
|
+
"/v1/machines": {
|
|
6137
|
+
get: {
|
|
6138
|
+
operationId: "listMachines",
|
|
6139
|
+
summary: "Known machines",
|
|
6140
|
+
responses: {
|
|
6141
|
+
"200": {
|
|
6142
|
+
description: "ok",
|
|
6143
|
+
content: {
|
|
6144
|
+
"application/json": {
|
|
6145
|
+
schema: {
|
|
6146
|
+
type: "object",
|
|
6147
|
+
properties: {
|
|
6148
|
+
data: {
|
|
6149
|
+
type: "array",
|
|
6150
|
+
items: {
|
|
6151
|
+
$ref: "#/components/schemas/MachineInfo"
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
},
|
|
6162
|
+
"/v1/fleet": {
|
|
6163
|
+
get: {
|
|
6164
|
+
operationId: "fleet",
|
|
6165
|
+
summary: "Fleet summary",
|
|
6166
|
+
parameters: [
|
|
6167
|
+
{
|
|
6168
|
+
name: "period",
|
|
6169
|
+
in: "query",
|
|
6170
|
+
schema: {
|
|
6171
|
+
type: "string"
|
|
6172
|
+
}
|
|
6173
|
+
},
|
|
6174
|
+
{
|
|
6175
|
+
name: "machine",
|
|
6176
|
+
in: "query",
|
|
6177
|
+
schema: {
|
|
6178
|
+
type: "string"
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
],
|
|
6182
|
+
responses: {
|
|
6183
|
+
"200": {
|
|
6184
|
+
description: "ok",
|
|
6185
|
+
content: {
|
|
6186
|
+
"application/json": {
|
|
6187
|
+
schema: {
|
|
6188
|
+
$ref: "#/components/schemas/Envelope"
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
},
|
|
6196
|
+
"/v1/billing": {
|
|
6197
|
+
get: {
|
|
6198
|
+
operationId: "billing",
|
|
6199
|
+
summary: "Provider billing summary",
|
|
6200
|
+
parameters: [
|
|
6201
|
+
{
|
|
6202
|
+
name: "period",
|
|
6203
|
+
in: "query",
|
|
6204
|
+
schema: {
|
|
6205
|
+
type: "string"
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
],
|
|
6209
|
+
responses: {
|
|
6210
|
+
"200": {
|
|
6211
|
+
description: "ok",
|
|
6212
|
+
content: {
|
|
6213
|
+
"application/json": {
|
|
6214
|
+
schema: {
|
|
6215
|
+
type: "object",
|
|
6216
|
+
properties: {
|
|
6217
|
+
data: {
|
|
6218
|
+
$ref: "#/components/schemas/BillingSummary"
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
},
|
|
6228
|
+
"/v1/billing/diff": {
|
|
6229
|
+
get: {
|
|
6230
|
+
operationId: "billingDiff",
|
|
6231
|
+
summary: "Billing vs tracked diff",
|
|
6232
|
+
parameters: [
|
|
6233
|
+
{
|
|
6234
|
+
name: "period",
|
|
6235
|
+
in: "query",
|
|
6236
|
+
schema: {
|
|
6237
|
+
type: "string"
|
|
6238
|
+
}
|
|
6239
|
+
},
|
|
6240
|
+
{
|
|
6241
|
+
name: "threshold",
|
|
6242
|
+
in: "query",
|
|
6243
|
+
schema: {
|
|
6244
|
+
type: "number"
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
],
|
|
6248
|
+
responses: {
|
|
6249
|
+
"200": {
|
|
6250
|
+
description: "ok",
|
|
6251
|
+
content: {
|
|
6252
|
+
"application/json": {
|
|
6253
|
+
schema: {
|
|
6254
|
+
$ref: "#/components/schemas/Envelope"
|
|
6255
|
+
}
|
|
6256
|
+
}
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
},
|
|
6262
|
+
"/v1/usage": {
|
|
6263
|
+
get: {
|
|
6264
|
+
operationId: "usage",
|
|
6265
|
+
summary: "Usage snapshots + summary",
|
|
6266
|
+
parameters: [
|
|
6267
|
+
{
|
|
6268
|
+
name: "period",
|
|
6269
|
+
in: "query",
|
|
6270
|
+
schema: {
|
|
6271
|
+
type: "string"
|
|
6272
|
+
}
|
|
6273
|
+
},
|
|
6274
|
+
{
|
|
6275
|
+
name: "agent",
|
|
6276
|
+
in: "query",
|
|
6277
|
+
schema: {
|
|
6278
|
+
type: "string"
|
|
6279
|
+
}
|
|
6280
|
+
}
|
|
6281
|
+
],
|
|
6282
|
+
responses: {
|
|
6283
|
+
"200": {
|
|
6284
|
+
description: "ok",
|
|
6285
|
+
content: {
|
|
6286
|
+
"application/json": {
|
|
6287
|
+
schema: {
|
|
6288
|
+
$ref: "#/components/schemas/Envelope"
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
6295
|
+
},
|
|
6296
|
+
"/v1/savings": {
|
|
6297
|
+
get: {
|
|
6298
|
+
operationId: "savings",
|
|
6299
|
+
summary: "Savings summary",
|
|
6300
|
+
parameters: [
|
|
6301
|
+
{
|
|
6302
|
+
name: "period",
|
|
6303
|
+
in: "query",
|
|
6304
|
+
schema: {
|
|
6305
|
+
type: "string"
|
|
6306
|
+
}
|
|
6307
|
+
},
|
|
6308
|
+
{
|
|
6309
|
+
name: "agent",
|
|
6310
|
+
in: "query",
|
|
6311
|
+
schema: {
|
|
6312
|
+
type: "string"
|
|
6313
|
+
}
|
|
6314
|
+
}
|
|
6315
|
+
],
|
|
6316
|
+
responses: {
|
|
6317
|
+
"200": {
|
|
6318
|
+
description: "ok",
|
|
6319
|
+
content: {
|
|
6320
|
+
"application/json": {
|
|
6321
|
+
schema: {
|
|
6322
|
+
$ref: "#/components/schemas/Envelope"
|
|
6323
|
+
}
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
},
|
|
4436
6330
|
"/v1/budgets": {
|
|
4437
|
-
get: {
|
|
4438
|
-
|
|
6331
|
+
get: {
|
|
6332
|
+
operationId: "listBudgets",
|
|
6333
|
+
summary: "List budgets",
|
|
6334
|
+
responses: {
|
|
6335
|
+
"200": {
|
|
6336
|
+
description: "ok",
|
|
6337
|
+
content: {
|
|
6338
|
+
"application/json": {
|
|
6339
|
+
schema: {
|
|
6340
|
+
type: "object",
|
|
6341
|
+
properties: {
|
|
6342
|
+
data: {
|
|
6343
|
+
type: "array",
|
|
6344
|
+
items: {
|
|
6345
|
+
$ref: "#/components/schemas/BudgetStatus"
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
},
|
|
6355
|
+
post: {
|
|
6356
|
+
operationId: "createBudget",
|
|
6357
|
+
summary: "Create a budget",
|
|
6358
|
+
requestBody: {
|
|
6359
|
+
required: true,
|
|
6360
|
+
content: {
|
|
6361
|
+
"application/json": {
|
|
6362
|
+
schema: {
|
|
6363
|
+
$ref: "#/components/schemas/CreateBudgetInput"
|
|
6364
|
+
}
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
},
|
|
6368
|
+
responses: {
|
|
6369
|
+
"200": {
|
|
6370
|
+
description: "created",
|
|
6371
|
+
content: {
|
|
6372
|
+
"application/json": {
|
|
6373
|
+
schema: {
|
|
6374
|
+
type: "object",
|
|
6375
|
+
properties: {
|
|
6376
|
+
data: {
|
|
6377
|
+
$ref: "#/components/schemas/BudgetStatus"
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
},
|
|
6387
|
+
"/v1/budgets/{id}": {
|
|
6388
|
+
delete: {
|
|
6389
|
+
operationId: "deleteBudget",
|
|
6390
|
+
summary: "Delete a budget",
|
|
6391
|
+
parameters: [
|
|
6392
|
+
{
|
|
6393
|
+
name: "id",
|
|
6394
|
+
in: "path",
|
|
6395
|
+
required: true,
|
|
6396
|
+
schema: {
|
|
6397
|
+
type: "string"
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
],
|
|
6401
|
+
responses: {
|
|
6402
|
+
"200": {
|
|
6403
|
+
description: "ok",
|
|
6404
|
+
content: {
|
|
6405
|
+
"application/json": {
|
|
6406
|
+
schema: {
|
|
6407
|
+
$ref: "#/components/schemas/MutationOk"
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
4439
6414
|
},
|
|
4440
|
-
"/v1/budgets/{id}": { delete: { operationId: "deleteBudget", summary: "Delete a budget", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4441
6415
|
"/v1/goals": {
|
|
4442
|
-
get: {
|
|
4443
|
-
|
|
6416
|
+
get: {
|
|
6417
|
+
operationId: "listGoals",
|
|
6418
|
+
summary: "List goals",
|
|
6419
|
+
responses: {
|
|
6420
|
+
"200": {
|
|
6421
|
+
description: "ok",
|
|
6422
|
+
content: {
|
|
6423
|
+
"application/json": {
|
|
6424
|
+
schema: {
|
|
6425
|
+
type: "object",
|
|
6426
|
+
properties: {
|
|
6427
|
+
data: {
|
|
6428
|
+
type: "array",
|
|
6429
|
+
items: {
|
|
6430
|
+
$ref: "#/components/schemas/GoalStatus"
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
},
|
|
6440
|
+
post: {
|
|
6441
|
+
operationId: "createGoal",
|
|
6442
|
+
summary: "Create a goal",
|
|
6443
|
+
requestBody: {
|
|
6444
|
+
required: true,
|
|
6445
|
+
content: {
|
|
6446
|
+
"application/json": {
|
|
6447
|
+
schema: {
|
|
6448
|
+
$ref: "#/components/schemas/CreateGoalInput"
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
},
|
|
6453
|
+
responses: {
|
|
6454
|
+
"200": {
|
|
6455
|
+
description: "created",
|
|
6456
|
+
content: {
|
|
6457
|
+
"application/json": {
|
|
6458
|
+
schema: {
|
|
6459
|
+
type: "object",
|
|
6460
|
+
properties: {
|
|
6461
|
+
data: {
|
|
6462
|
+
$ref: "#/components/schemas/GoalStatus"
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
},
|
|
6472
|
+
"/v1/goals/{id}": {
|
|
6473
|
+
delete: {
|
|
6474
|
+
operationId: "deleteGoal",
|
|
6475
|
+
summary: "Delete a goal",
|
|
6476
|
+
parameters: [
|
|
6477
|
+
{
|
|
6478
|
+
name: "id",
|
|
6479
|
+
in: "path",
|
|
6480
|
+
required: true,
|
|
6481
|
+
schema: {
|
|
6482
|
+
type: "string"
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
],
|
|
6486
|
+
responses: {
|
|
6487
|
+
"200": {
|
|
6488
|
+
description: "ok",
|
|
6489
|
+
content: {
|
|
6490
|
+
"application/json": {
|
|
6491
|
+
schema: {
|
|
6492
|
+
$ref: "#/components/schemas/MutationOk"
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
4444
6499
|
},
|
|
4445
|
-
"/v1/goals/{id}": { delete: { operationId: "deleteGoal", summary: "Delete a goal", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4446
6500
|
"/v1/pricing": {
|
|
4447
|
-
get: {
|
|
4448
|
-
|
|
6501
|
+
get: {
|
|
6502
|
+
operationId: "listPricing",
|
|
6503
|
+
summary: "List model pricing",
|
|
6504
|
+
responses: {
|
|
6505
|
+
"200": {
|
|
6506
|
+
description: "ok",
|
|
6507
|
+
content: {
|
|
6508
|
+
"application/json": {
|
|
6509
|
+
schema: {
|
|
6510
|
+
type: "object",
|
|
6511
|
+
properties: {
|
|
6512
|
+
data: {
|
|
6513
|
+
type: "array",
|
|
6514
|
+
items: {
|
|
6515
|
+
$ref: "#/components/schemas/ModelPricing"
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
}
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
},
|
|
6525
|
+
post: {
|
|
6526
|
+
operationId: "upsertPricing",
|
|
6527
|
+
summary: "Upsert model pricing",
|
|
6528
|
+
requestBody: {
|
|
6529
|
+
required: true,
|
|
6530
|
+
content: {
|
|
6531
|
+
"application/json": {
|
|
6532
|
+
schema: {
|
|
6533
|
+
$ref: "#/components/schemas/CreatePricingInput"
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
},
|
|
6538
|
+
responses: {
|
|
6539
|
+
"200": {
|
|
6540
|
+
description: "ok",
|
|
6541
|
+
content: {
|
|
6542
|
+
"application/json": {
|
|
6543
|
+
schema: {
|
|
6544
|
+
type: "object",
|
|
6545
|
+
properties: {
|
|
6546
|
+
data: {
|
|
6547
|
+
$ref: "#/components/schemas/ModelPricing"
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6554
|
+
}
|
|
6555
|
+
}
|
|
6556
|
+
},
|
|
6557
|
+
"/v1/pricing/{model}": {
|
|
6558
|
+
delete: {
|
|
6559
|
+
operationId: "deletePricing",
|
|
6560
|
+
summary: "Delete model pricing",
|
|
6561
|
+
parameters: [
|
|
6562
|
+
{
|
|
6563
|
+
name: "model",
|
|
6564
|
+
in: "path",
|
|
6565
|
+
required: true,
|
|
6566
|
+
schema: {
|
|
6567
|
+
type: "string"
|
|
6568
|
+
}
|
|
6569
|
+
}
|
|
6570
|
+
],
|
|
6571
|
+
responses: {
|
|
6572
|
+
"200": {
|
|
6573
|
+
description: "ok",
|
|
6574
|
+
content: {
|
|
6575
|
+
"application/json": {
|
|
6576
|
+
schema: {
|
|
6577
|
+
$ref: "#/components/schemas/MutationOk"
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
4449
6584
|
},
|
|
4450
|
-
"/v1/pricing/{model}": { delete: { operationId: "deletePricing", summary: "Delete model pricing", parameters: [{ name: "model", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4451
6585
|
"/v1/subscriptions": {
|
|
4452
|
-
get: {
|
|
4453
|
-
|
|
6586
|
+
get: {
|
|
6587
|
+
operationId: "listSubscriptions",
|
|
6588
|
+
summary: "List subscriptions",
|
|
6589
|
+
responses: {
|
|
6590
|
+
"200": {
|
|
6591
|
+
description: "ok",
|
|
6592
|
+
content: {
|
|
6593
|
+
"application/json": {
|
|
6594
|
+
schema: {
|
|
6595
|
+
type: "object",
|
|
6596
|
+
properties: {
|
|
6597
|
+
data: {
|
|
6598
|
+
type: "array",
|
|
6599
|
+
items: {
|
|
6600
|
+
$ref: "#/components/schemas/Subscription"
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
}
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
},
|
|
6610
|
+
post: {
|
|
6611
|
+
operationId: "upsertSubscription",
|
|
6612
|
+
summary: "Upsert a subscription",
|
|
6613
|
+
requestBody: {
|
|
6614
|
+
required: true,
|
|
6615
|
+
content: {
|
|
6616
|
+
"application/json": {
|
|
6617
|
+
schema: {
|
|
6618
|
+
$ref: "#/components/schemas/CreateSubscriptionInput"
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
},
|
|
6623
|
+
responses: {
|
|
6624
|
+
"200": {
|
|
6625
|
+
description: "ok",
|
|
6626
|
+
content: {
|
|
6627
|
+
"application/json": {
|
|
6628
|
+
schema: {
|
|
6629
|
+
type: "object",
|
|
6630
|
+
properties: {
|
|
6631
|
+
data: {
|
|
6632
|
+
$ref: "#/components/schemas/Subscription"
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
},
|
|
6642
|
+
"/v1/subscriptions/{id}": {
|
|
6643
|
+
delete: {
|
|
6644
|
+
operationId: "deleteSubscription",
|
|
6645
|
+
summary: "Delete a subscription",
|
|
6646
|
+
parameters: [
|
|
6647
|
+
{
|
|
6648
|
+
name: "id",
|
|
6649
|
+
in: "path",
|
|
6650
|
+
required: true,
|
|
6651
|
+
schema: {
|
|
6652
|
+
type: "string"
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
],
|
|
6656
|
+
responses: {
|
|
6657
|
+
"200": {
|
|
6658
|
+
description: "ok",
|
|
6659
|
+
content: {
|
|
6660
|
+
"application/json": {
|
|
6661
|
+
schema: {
|
|
6662
|
+
$ref: "#/components/schemas/MutationOk"
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
4454
6669
|
},
|
|
4455
|
-
"/v1/subscriptions/{id}": { delete: { operationId: "deleteSubscription", summary: "Delete a subscription", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4456
6670
|
"/v1/project-registry": {
|
|
4457
|
-
get: {
|
|
4458
|
-
|
|
6671
|
+
get: {
|
|
6672
|
+
operationId: "listProjectRegistry",
|
|
6673
|
+
summary: "List registered projects",
|
|
6674
|
+
responses: {
|
|
6675
|
+
"200": {
|
|
6676
|
+
description: "ok",
|
|
6677
|
+
content: {
|
|
6678
|
+
"application/json": {
|
|
6679
|
+
schema: {
|
|
6680
|
+
type: "object",
|
|
6681
|
+
properties: {
|
|
6682
|
+
data: {
|
|
6683
|
+
type: "array",
|
|
6684
|
+
items: {
|
|
6685
|
+
$ref: "#/components/schemas/Project"
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
},
|
|
6695
|
+
post: {
|
|
6696
|
+
operationId: "upsertProject",
|
|
6697
|
+
summary: "Register a project",
|
|
6698
|
+
requestBody: {
|
|
6699
|
+
required: true,
|
|
6700
|
+
content: {
|
|
6701
|
+
"application/json": {
|
|
6702
|
+
schema: {
|
|
6703
|
+
$ref: "#/components/schemas/CreateProjectInput"
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
},
|
|
6708
|
+
responses: {
|
|
6709
|
+
"200": {
|
|
6710
|
+
description: "ok",
|
|
6711
|
+
content: {
|
|
6712
|
+
"application/json": {
|
|
6713
|
+
schema: {
|
|
6714
|
+
$ref: "#/components/schemas/MutationOk"
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
},
|
|
6722
|
+
"/v1/project-registry/{id}": {
|
|
6723
|
+
delete: {
|
|
6724
|
+
operationId: "deleteProject",
|
|
6725
|
+
summary: "Delete a registered project",
|
|
6726
|
+
parameters: [
|
|
6727
|
+
{
|
|
6728
|
+
name: "id",
|
|
6729
|
+
in: "path",
|
|
6730
|
+
required: true,
|
|
6731
|
+
schema: {
|
|
6732
|
+
type: "string"
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
],
|
|
6736
|
+
responses: {
|
|
6737
|
+
"200": {
|
|
6738
|
+
description: "ok",
|
|
6739
|
+
content: {
|
|
6740
|
+
"application/json": {
|
|
6741
|
+
schema: {
|
|
6742
|
+
$ref: "#/components/schemas/MutationOk"
|
|
6743
|
+
}
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
4459
6749
|
},
|
|
4460
|
-
"/v1/
|
|
4461
|
-
|
|
6750
|
+
"/v1/sync": {
|
|
6751
|
+
post: {
|
|
6752
|
+
operationId: "sync",
|
|
6753
|
+
summary: "Trigger an ingest sync",
|
|
6754
|
+
requestBody: {
|
|
6755
|
+
required: false,
|
|
6756
|
+
content: {
|
|
6757
|
+
"application/json": {
|
|
6758
|
+
schema: {
|
|
6759
|
+
type: "object",
|
|
6760
|
+
properties: {
|
|
6761
|
+
sources: {
|
|
6762
|
+
type: "string"
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
},
|
|
6769
|
+
responses: {
|
|
6770
|
+
"200": {
|
|
6771
|
+
description: "ok",
|
|
6772
|
+
content: {
|
|
6773
|
+
"application/json": {
|
|
6774
|
+
schema: {
|
|
6775
|
+
$ref: "#/components/schemas/Envelope"
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
}
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
},
|
|
6783
|
+
"/v1/ingest": {
|
|
6784
|
+
post: {
|
|
6785
|
+
operationId: "ingest",
|
|
6786
|
+
summary: "Bulk-import local rows into the cloud DB (merge by primary key, idempotent)",
|
|
6787
|
+
requestBody: {
|
|
6788
|
+
required: true,
|
|
6789
|
+
content: {
|
|
6790
|
+
"application/json": {
|
|
6791
|
+
schema: {
|
|
6792
|
+
type: "object",
|
|
6793
|
+
properties: {
|
|
6794
|
+
requests: {
|
|
6795
|
+
type: "array",
|
|
6796
|
+
items: {
|
|
6797
|
+
type: "object"
|
|
6798
|
+
}
|
|
6799
|
+
},
|
|
6800
|
+
sessions: {
|
|
6801
|
+
type: "array",
|
|
6802
|
+
items: {
|
|
6803
|
+
type: "object"
|
|
6804
|
+
}
|
|
6805
|
+
},
|
|
6806
|
+
projects: {
|
|
6807
|
+
type: "array",
|
|
6808
|
+
items: {
|
|
6809
|
+
type: "object"
|
|
6810
|
+
}
|
|
6811
|
+
},
|
|
6812
|
+
budgets: {
|
|
6813
|
+
type: "array",
|
|
6814
|
+
items: {
|
|
6815
|
+
type: "object"
|
|
6816
|
+
}
|
|
6817
|
+
},
|
|
6818
|
+
goals: {
|
|
6819
|
+
type: "array",
|
|
6820
|
+
items: {
|
|
6821
|
+
type: "object"
|
|
6822
|
+
}
|
|
6823
|
+
},
|
|
6824
|
+
billing_daily: {
|
|
6825
|
+
type: "array",
|
|
6826
|
+
items: {
|
|
6827
|
+
type: "object"
|
|
6828
|
+
}
|
|
6829
|
+
},
|
|
6830
|
+
model_pricing: {
|
|
6831
|
+
type: "array",
|
|
6832
|
+
items: {
|
|
6833
|
+
type: "object"
|
|
6834
|
+
}
|
|
6835
|
+
},
|
|
6836
|
+
subscriptions: {
|
|
6837
|
+
type: "array",
|
|
6838
|
+
items: {
|
|
6839
|
+
type: "object"
|
|
6840
|
+
}
|
|
6841
|
+
},
|
|
6842
|
+
usage_snapshots: {
|
|
6843
|
+
type: "array",
|
|
6844
|
+
items: {
|
|
6845
|
+
type: "object"
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6852
|
+
},
|
|
6853
|
+
responses: {
|
|
6854
|
+
"200": {
|
|
6855
|
+
description: "ok",
|
|
6856
|
+
content: {
|
|
6857
|
+
"application/json": {
|
|
6858
|
+
schema: {
|
|
6859
|
+
$ref: "#/components/schemas/Envelope"
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
4462
6867
|
}
|
|
4463
6868
|
};
|
|
4464
6869
|
|
|
@@ -4478,6 +6883,8 @@ var CORS = {
|
|
|
4478
6883
|
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
4479
6884
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
|
|
4480
6885
|
};
|
|
6886
|
+
var APP = "economy";
|
|
6887
|
+
var ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
4481
6888
|
var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
4482
6889
|
var SYNC_SOURCES = ["all", ...AGENTS];
|
|
4483
6890
|
var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
@@ -4523,6 +6930,11 @@ function optionalAgent(value) {
|
|
|
4523
6930
|
return null;
|
|
4524
6931
|
return typeof value === "string" && AGENTS.includes(value) ? value : undefined;
|
|
4525
6932
|
}
|
|
6933
|
+
function requiredScopesForRequest(method, path) {
|
|
6934
|
+
if (method === "POST" && path === "/api/ingest")
|
|
6935
|
+
return [ECONOMY_WRITE_SCOPE];
|
|
6936
|
+
return;
|
|
6937
|
+
}
|
|
4526
6938
|
function stringArray(value) {
|
|
4527
6939
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
4528
6940
|
}
|
|
@@ -4585,7 +6997,11 @@ function createHandler(db, options = {}) {
|
|
|
4585
6997
|
return json(openApiDocument());
|
|
4586
6998
|
}
|
|
4587
6999
|
if (options.authenticator) {
|
|
4588
|
-
const decision = await options.authenticator.authenticate(req.headers, {
|
|
7000
|
+
const decision = await options.authenticator.authenticate(req.headers, {
|
|
7001
|
+
method,
|
|
7002
|
+
path: rawPath,
|
|
7003
|
+
requiredScopes: requiredScopesForRequest(method, path)
|
|
7004
|
+
});
|
|
4589
7005
|
if (!decision.ok)
|
|
4590
7006
|
return json({ error: decision.reason ?? "unauthorized", message: decision.message }, decision.status || 401);
|
|
4591
7007
|
} else if (!isAuthorizedRequest(req, path)) {
|
|
@@ -4837,15 +7253,22 @@ function createHandler(db, options = {}) {
|
|
|
4837
7253
|
} catch {}
|
|
4838
7254
|
return ok(results);
|
|
4839
7255
|
}
|
|
7256
|
+
if (path === "/api/ingest" && method === "POST") {
|
|
7257
|
+
const body = await jsonBody(req);
|
|
7258
|
+
if (!body)
|
|
7259
|
+
return err("invalid JSON body");
|
|
7260
|
+
return ok(bulkIngest(db, body));
|
|
7261
|
+
}
|
|
4840
7262
|
if (path === "/api/usage" && method === "GET") {
|
|
4841
7263
|
const period = url.searchParams.get("period") ?? "month";
|
|
4842
|
-
const
|
|
7264
|
+
const agentParam = url.searchParams.get("agent") ?? undefined;
|
|
7265
|
+
const agent = agentParam && isAgent(agentParam) ? agentParam : undefined;
|
|
4843
7266
|
return ok({
|
|
4844
7267
|
snapshots: queryUsageSnapshots(db, {
|
|
4845
|
-
agent
|
|
7268
|
+
agent,
|
|
4846
7269
|
...usageSnapshotFilterForPeriod(period)
|
|
4847
7270
|
}),
|
|
4848
|
-
summary: querySummary(db, period, undefined, true)
|
|
7271
|
+
summary: querySummary(db, period, undefined, true, agent)
|
|
4849
7272
|
});
|
|
4850
7273
|
}
|
|
4851
7274
|
if (path === "/api/savings" && method === "GET") {
|
|
@@ -4853,6 +7276,47 @@ function createHandler(db, options = {}) {
|
|
|
4853
7276
|
const agent = url.searchParams.get("agent") ?? undefined;
|
|
4854
7277
|
return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
|
|
4855
7278
|
}
|
|
7279
|
+
if (path === "/api/brief" && method === "GET") {
|
|
7280
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
7281
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
7282
|
+
try {
|
|
7283
|
+
return ok(buildBrief(db, { since, machine }));
|
|
7284
|
+
} catch (e) {
|
|
7285
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
if (path === "/api/projects/detail" && method === "GET") {
|
|
7289
|
+
const q = url.searchParams.get("q")?.trim();
|
|
7290
|
+
if (!q)
|
|
7291
|
+
return err("q is required");
|
|
7292
|
+
return ok(queryProjectDetail(db, q));
|
|
7293
|
+
}
|
|
7294
|
+
if (path === "/api/export" && method === "GET") {
|
|
7295
|
+
const type = url.searchParams.get("type") ?? "sessions";
|
|
7296
|
+
if (type !== "sessions" && type !== "requests")
|
|
7297
|
+
return err("type must be sessions or requests");
|
|
7298
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
7299
|
+
return ok(queryExportRows(db, type, period));
|
|
7300
|
+
}
|
|
7301
|
+
if (path === "/api/compare" && method === "GET") {
|
|
7302
|
+
const from = url.searchParams.get("from")?.trim();
|
|
7303
|
+
const to = url.searchParams.get("to")?.trim();
|
|
7304
|
+
if (!from || !to)
|
|
7305
|
+
return err("from and to are required (YYYY-MM-DD)");
|
|
7306
|
+
return ok(queryRangeStats(db, from, to));
|
|
7307
|
+
}
|
|
7308
|
+
if (path === "/api/forecast" && method === "GET") {
|
|
7309
|
+
return ok(queryForecast(db));
|
|
7310
|
+
}
|
|
7311
|
+
if (path === "/api/efficiency" && method === "GET") {
|
|
7312
|
+
return ok(queryModelEfficiency(db));
|
|
7313
|
+
}
|
|
7314
|
+
if (path === "/api/requests" && method === "GET") {
|
|
7315
|
+
const since = url.searchParams.get("since")?.trim();
|
|
7316
|
+
if (!since)
|
|
7317
|
+
return err("since is required (ISO timestamp)");
|
|
7318
|
+
return ok(queryRequestsSince(db, since));
|
|
7319
|
+
}
|
|
4856
7320
|
if (path === "/api/subscriptions" && method === "GET") {
|
|
4857
7321
|
return ok(listSubscriptions(db));
|
|
4858
7322
|
}
|
|
@@ -4943,7 +7407,6 @@ function createHandler(db, options = {}) {
|
|
|
4943
7407
|
return err("Not found", 404);
|
|
4944
7408
|
};
|
|
4945
7409
|
}
|
|
4946
|
-
var APP = "economy";
|
|
4947
7410
|
function isLocalHost(host) {
|
|
4948
7411
|
return ["127.0.0.1", "localhost", "::1"].includes(host);
|
|
4949
7412
|
}
|
|
@@ -5032,10 +7495,10 @@ function parsePort(raw, label) {
|
|
|
5032
7495
|
return value;
|
|
5033
7496
|
}
|
|
5034
7497
|
async function runMigrate() {
|
|
5035
|
-
const { getCloudDatabaseUrl:
|
|
7498
|
+
const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
5036
7499
|
const { applyPgMigrations } = await import("@hasna/cloud");
|
|
5037
7500
|
const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
|
|
5038
|
-
const dsn =
|
|
7501
|
+
const dsn = getCloudDatabaseUrl2();
|
|
5039
7502
|
if (!dsn)
|
|
5040
7503
|
throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
|
|
5041
7504
|
const result = await applyPgMigrations(dsn, cloudMigrations2(), "economy");
|