@hasna/economy 0.2.42 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- 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 +8253 -6105
- package/dist/db/cloud.d.ts +28 -0
- package/dist/db/cloud.d.ts.map +1 -0
- 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/db/pg-sync-worker.d.ts +2 -0
- package/dist/db/pg-sync-worker.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +23 -0
- package/dist/db/sync-pg.d.ts.map +1 -0
- 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 +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/otel/index.js +21 -267
- package/dist/server/index.js +3145 -321
- package/dist/server/pg-sync-worker.js +31 -0
- package/dist/server/serve.d.ts +31 -1
- 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 +9 -4
- 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,10 +2228,103 @@ 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
|
|
|
2263
|
+
// src/db/cloud.ts
|
|
2264
|
+
var exports_cloud = {};
|
|
2265
|
+
__export(exports_cloud, {
|
|
2266
|
+
resolveSigningSecret: () => resolveSigningSecret,
|
|
2267
|
+
openCloudDatabase: () => openCloudDatabase,
|
|
2268
|
+
isCloudMode: () => isCloudMode,
|
|
2269
|
+
getCloudDatabaseUrl: () => getCloudDatabaseUrl,
|
|
2270
|
+
createCloudPool: () => createCloudPool,
|
|
2271
|
+
cloudMigrations: () => cloudMigrations,
|
|
2272
|
+
authClientFromPool: () => authClientFromPool
|
|
2273
|
+
});
|
|
2274
|
+
import pg from "pg";
|
|
2275
|
+
function getCloudDatabaseUrl() {
|
|
2276
|
+
return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
|
|
2277
|
+
}
|
|
2278
|
+
function isCloudMode() {
|
|
2279
|
+
const mode = process.env["HASNA_ECONOMY_STORAGE_MODE"]?.trim().toLowerCase();
|
|
2280
|
+
if (mode === "cloud")
|
|
2281
|
+
return true;
|
|
2282
|
+
if (mode === "local")
|
|
2283
|
+
return false;
|
|
2284
|
+
return Boolean(getCloudDatabaseUrl());
|
|
2285
|
+
}
|
|
2286
|
+
function resolveSigningSecret() {
|
|
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;
|
|
2288
|
+
}
|
|
2289
|
+
function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
|
|
2290
|
+
if (!dsn) {
|
|
2291
|
+
throw new Error("economy cloud mode requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
|
|
2292
|
+
}
|
|
2293
|
+
return new SyncPgAdapter(dsn);
|
|
2294
|
+
}
|
|
2295
|
+
function createCloudPool(dsn = getCloudDatabaseUrl()) {
|
|
2296
|
+
if (!dsn)
|
|
2297
|
+
throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
|
|
2298
|
+
const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
2299
|
+
const pool = new pg.Pool({ connectionString: dsn, ssl, max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
|
|
2300
|
+
pool.on("error", (err) => {
|
|
2301
|
+
console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
|
|
2302
|
+
});
|
|
2303
|
+
return pool;
|
|
2304
|
+
}
|
|
2305
|
+
function authClientFromPool(pool) {
|
|
2306
|
+
return {
|
|
2307
|
+
async many(sql, params = []) {
|
|
2308
|
+
const res = await pool.query(sql, params);
|
|
2309
|
+
return res.rows;
|
|
2310
|
+
},
|
|
2311
|
+
async get(sql, params = []) {
|
|
2312
|
+
const res = await pool.query(sql, params);
|
|
2313
|
+
return res.rows[0] ?? null;
|
|
2314
|
+
},
|
|
2315
|
+
async execute(sql, params = []) {
|
|
2316
|
+
await pool.query(sql, params);
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
function cloudMigrations() {
|
|
2321
|
+
return PG_MIGRATIONS;
|
|
2322
|
+
}
|
|
2323
|
+
var init_cloud = __esm(() => {
|
|
2324
|
+
init_sync_pg();
|
|
2325
|
+
init_pg_migrations();
|
|
2326
|
+
});
|
|
2327
|
+
|
|
1850
2328
|
// src/ingest/billing.ts
|
|
1851
2329
|
var exports_billing = {};
|
|
1852
2330
|
__export(exports_billing, {
|
|
@@ -2143,15 +2621,15 @@ var init_open_projects = __esm(() => {
|
|
|
2143
2621
|
});
|
|
2144
2622
|
|
|
2145
2623
|
// src/lib/config.ts
|
|
2146
|
-
import { existsSync as
|
|
2147
|
-
import { dirname, join as
|
|
2624
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
2625
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
2148
2626
|
function getConfigPath() {
|
|
2149
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
2627
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join10(getDataDir(), "config.json");
|
|
2150
2628
|
}
|
|
2151
2629
|
function loadConfig() {
|
|
2152
2630
|
try {
|
|
2153
2631
|
const configPath = getConfigPath();
|
|
2154
|
-
if (
|
|
2632
|
+
if (existsSync11(configPath)) {
|
|
2155
2633
|
const raw = readFileSync10(configPath, "utf-8");
|
|
2156
2634
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
2157
2635
|
}
|
|
@@ -3904,157 +4382,10 @@ async function ingestCodexQuota(db, verbose = false) {
|
|
|
3904
4382
|
|
|
3905
4383
|
// src/lib/sync-all.ts
|
|
3906
4384
|
init_database();
|
|
3907
|
-
|
|
3908
|
-
// src/lib/cloud-sync.ts
|
|
3909
|
-
init_database();
|
|
3910
|
-
|
|
3911
|
-
// src/lib/package-metadata.ts
|
|
3912
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
3913
|
-
var cachedMetadata = null;
|
|
3914
|
-
function getPackageMetadata() {
|
|
3915
|
-
if (cachedMetadata)
|
|
3916
|
-
return cachedMetadata;
|
|
3917
|
-
const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
|
|
3918
|
-
const parsed = JSON.parse(raw);
|
|
3919
|
-
cachedMetadata = {
|
|
3920
|
-
name: parsed.name ?? "@hasna/economy",
|
|
3921
|
-
version: parsed.version ?? "0.0.0"
|
|
3922
|
-
};
|
|
3923
|
-
return cachedMetadata;
|
|
3924
|
-
}
|
|
3925
|
-
var packageMetadata = getPackageMetadata();
|
|
3926
|
-
|
|
3927
|
-
// src/lib/cloud-sync.ts
|
|
3928
|
-
var CLOUD_TABLES = [
|
|
3929
|
-
"requests",
|
|
3930
|
-
"sessions",
|
|
3931
|
-
"projects",
|
|
3932
|
-
"budgets",
|
|
3933
|
-
"goals",
|
|
3934
|
-
"model_pricing",
|
|
3935
|
-
"billing_daily",
|
|
3936
|
-
"subscriptions",
|
|
3937
|
-
"usage_snapshots",
|
|
3938
|
-
"savings_daily",
|
|
3939
|
-
"machines",
|
|
3940
|
-
"ingest_state"
|
|
3941
|
-
];
|
|
3942
|
-
function getCloudDatabaseUrl() {
|
|
3943
|
-
return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
|
|
3944
|
-
}
|
|
3945
|
-
function isCloudAutoEnabled() {
|
|
3946
|
-
return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
|
|
3947
|
-
}
|
|
3948
|
-
function getCloudPullIntervalMinutes() {
|
|
3949
|
-
const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
|
|
3950
|
-
if (!raw)
|
|
3951
|
-
return 15;
|
|
3952
|
-
const n = Number(raw);
|
|
3953
|
-
return Number.isFinite(n) && n > 0 ? n : 15;
|
|
3954
|
-
}
|
|
3955
|
-
async function getCloudPg() {
|
|
3956
|
-
const url = getCloudDatabaseUrl();
|
|
3957
|
-
if (!url) {
|
|
3958
|
-
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
3959
|
-
}
|
|
3960
|
-
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
3961
|
-
return new PgAdapterAsync(url);
|
|
3962
|
-
}
|
|
3963
|
-
async function runCloudMigrations(cloud) {
|
|
3964
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3965
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
3966
|
-
await cloud.run(sql);
|
|
3967
|
-
}
|
|
3968
|
-
}
|
|
3969
|
-
async function cloudPush(opts) {
|
|
3970
|
-
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
3971
|
-
const cloud = await getCloudPg();
|
|
3972
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3973
|
-
await runCloudMigrations(cloud);
|
|
3974
|
-
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3975
|
-
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3976
|
-
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3977
|
-
touchMachineRegistry(local, "push");
|
|
3978
|
-
local.close();
|
|
3979
|
-
await cloud.close();
|
|
3980
|
-
return { rows, machine: getMachineId() };
|
|
3981
|
-
}
|
|
3982
|
-
async function cloudPull(opts) {
|
|
3983
|
-
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3984
|
-
const cloud = await getCloudPg();
|
|
3985
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3986
|
-
await runCloudMigrations(cloud);
|
|
3987
|
-
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3988
|
-
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3989
|
-
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3990
|
-
touchMachineRegistry(local, "pull");
|
|
3991
|
-
local.close();
|
|
3992
|
-
await cloud.close();
|
|
3993
|
-
setLastCloudPull();
|
|
3994
|
-
return { rows, machine: getMachineId() };
|
|
3995
|
-
}
|
|
3996
|
-
function setLastCloudPull(at = new Date().toISOString()) {
|
|
3997
|
-
const db = openDatabase();
|
|
3998
|
-
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
3999
|
-
}
|
|
4000
|
-
function getLastCloudPull() {
|
|
4001
|
-
const db = openDatabase();
|
|
4002
|
-
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
4003
|
-
return row?.value ?? null;
|
|
4004
|
-
}
|
|
4005
|
-
function shouldPullFromCloud() {
|
|
4006
|
-
if (!getCloudDatabaseUrl())
|
|
4007
|
-
return false;
|
|
4008
|
-
const last = getLastCloudPull();
|
|
4009
|
-
if (!last)
|
|
4010
|
-
return true;
|
|
4011
|
-
const ageMs = Date.now() - new Date(last).getTime();
|
|
4012
|
-
return ageMs > getCloudPullIntervalMinutes() * 60000;
|
|
4013
|
-
}
|
|
4014
|
-
async function maybePullFromCloud() {
|
|
4015
|
-
if (!shouldPullFromCloud())
|
|
4016
|
-
return false;
|
|
4017
|
-
try {
|
|
4018
|
-
await cloudPull();
|
|
4019
|
-
return true;
|
|
4020
|
-
} catch {
|
|
4021
|
-
return false;
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
async function maybePushAfterIngest() {
|
|
4025
|
-
if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
|
|
4026
|
-
return false;
|
|
4027
|
-
try {
|
|
4028
|
-
await cloudPush();
|
|
4029
|
-
return true;
|
|
4030
|
-
} catch {
|
|
4031
|
-
return false;
|
|
4032
|
-
}
|
|
4033
|
-
}
|
|
4034
|
-
function touchMachineRegistry(db, direction) {
|
|
4035
|
-
const now = new Date().toISOString();
|
|
4036
|
-
const machine = getMachineId();
|
|
4037
|
-
db.prepare(`
|
|
4038
|
-
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
4039
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4040
|
-
ON CONFLICT(machine_id) DO UPDATE SET
|
|
4041
|
-
hostname = excluded.hostname,
|
|
4042
|
-
last_seen_at = excluded.last_seen_at,
|
|
4043
|
-
last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
|
|
4044
|
-
last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
|
|
4045
|
-
economy_version = excluded.economy_version,
|
|
4046
|
-
updated_at = excluded.updated_at
|
|
4047
|
-
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
4048
|
-
}
|
|
4049
|
-
|
|
4050
|
-
// src/lib/sync-all.ts
|
|
4051
4385
|
async function syncAll(db, opts = {}) {
|
|
4052
4386
|
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
4053
4387
|
const all = !anySpecific;
|
|
4054
|
-
const
|
|
4055
|
-
if (useCloud)
|
|
4056
|
-
await maybePullFromCloud();
|
|
4057
|
-
const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
|
|
4388
|
+
const result = { deduped: 0 };
|
|
4058
4389
|
if (all || opts.claude) {
|
|
4059
4390
|
result.claude = await ingestClaude(db, opts.verbose);
|
|
4060
4391
|
result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
|
|
@@ -4077,158 +4408,2505 @@ async function syncAll(db, opts = {}) {
|
|
|
4077
4408
|
result.hermes = await ingestHermes(db, opts.verbose);
|
|
4078
4409
|
if (opts.dedupe !== false)
|
|
4079
4410
|
result.deduped = dedupeRequests(db);
|
|
4080
|
-
if (useCloud)
|
|
4081
|
-
result.cloudPushed = await maybePushAfterIngest();
|
|
4082
4411
|
return result;
|
|
4083
4412
|
}
|
|
4084
4413
|
|
|
4085
|
-
// src/lib/
|
|
4086
|
-
function ymd(date) {
|
|
4087
|
-
return date.toISOString().substring(0, 10);
|
|
4088
|
-
}
|
|
4089
|
-
function usageSnapshotFilterForPeriod(period) {
|
|
4090
|
-
const now = new Date;
|
|
4091
|
-
switch (period) {
|
|
4092
|
-
case "today":
|
|
4093
|
-
return { date: ymd(now) };
|
|
4094
|
-
case "yesterday": {
|
|
4095
|
-
const yesterday = new Date(now);
|
|
4096
|
-
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
4097
|
-
return { date: ymd(yesterday) };
|
|
4098
|
-
}
|
|
4099
|
-
case "week": {
|
|
4100
|
-
const weekAgo = new Date(now);
|
|
4101
|
-
weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
|
|
4102
|
-
return { since: ymd(weekAgo) };
|
|
4103
|
-
}
|
|
4104
|
-
case "month":
|
|
4105
|
-
return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
|
|
4106
|
-
case "year":
|
|
4107
|
-
return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
|
|
4108
|
-
case "all":
|
|
4109
|
-
return {};
|
|
4110
|
-
}
|
|
4111
|
-
}
|
|
4112
|
-
|
|
4113
|
-
// src/lib/billing-diff.ts
|
|
4414
|
+
// src/lib/brief.ts
|
|
4114
4415
|
init_database();
|
|
4115
|
-
var
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
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
|
|
4120
4429
|
};
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
return
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
delta_usd: delta,
|
|
4150
|
-
delta_pct: deltaPct,
|
|
4151
|
-
threshold_pct: thresholdPct,
|
|
4152
|
-
is_alert: Math.abs(deltaPct) > thresholdPct,
|
|
4153
|
-
by_agent,
|
|
4154
|
-
by_provider: actual.by_provider
|
|
4155
|
-
};
|
|
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;
|
|
4156
4458
|
}
|
|
4157
|
-
function
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
case "yesterday":
|
|
4162
|
-
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
4163
|
-
case "week":
|
|
4164
|
-
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
4165
|
-
case "month":
|
|
4166
|
-
return `${column} >= DATE('now', 'start of month')`;
|
|
4167
|
-
case "year":
|
|
4168
|
-
return `${column} >= DATE('now', 'start of year')`;
|
|
4169
|
-
case "all":
|
|
4170
|
-
return "1=1";
|
|
4171
|
-
}
|
|
4459
|
+
function requestPeriodWhere2(period) {
|
|
4460
|
+
if (period === "today")
|
|
4461
|
+
return `DATE(timestamp) = DATE('now')`;
|
|
4462
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
4172
4463
|
}
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
// src/lib/serve-auth.ts
|
|
4178
|
-
function getServeApiToken() {
|
|
4179
|
-
return process.env["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
|
|
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')`;
|
|
4180
4468
|
}
|
|
4181
|
-
function
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
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
|
+
};
|
|
4186
4486
|
}
|
|
4187
|
-
function
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
return true;
|
|
4196
|
-
if (req.headers.get("X-Economy-Token") === token)
|
|
4197
|
-
return true;
|
|
4198
|
-
return false;
|
|
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
|
+
};
|
|
4199
4495
|
}
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
import { randomUUID } from "crypto";
|
|
4203
|
-
import { existsSync as existsSync11 } from "fs";
|
|
4204
|
-
import { resolve, sep } from "path";
|
|
4205
|
-
var CORS = {
|
|
4206
|
-
"Access-Control-Allow-Origin": "*",
|
|
4207
|
-
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
4208
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
|
|
4209
|
-
};
|
|
4210
|
-
var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
4211
|
-
var SYNC_SOURCES = ["all", ...AGENTS];
|
|
4212
|
-
var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
4213
|
-
function json(data, status = 200) {
|
|
4214
|
-
return new Response(JSON.stringify(data), {
|
|
4215
|
-
status,
|
|
4216
|
-
headers: { "Content-Type": "application/json", ...CORS }
|
|
4217
|
-
});
|
|
4496
|
+
function latestTimestamp(...values) {
|
|
4497
|
+
return values.filter(Boolean).sort().at(-1) ?? null;
|
|
4218
4498
|
}
|
|
4219
|
-
function
|
|
4220
|
-
return
|
|
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
|
+
};
|
|
4221
4513
|
}
|
|
4222
|
-
function
|
|
4223
|
-
return
|
|
4514
|
+
function emptyTotals() {
|
|
4515
|
+
return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
|
|
4224
4516
|
}
|
|
4225
|
-
function
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
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
|
+
|
|
5134
|
+
// src/lib/periods.ts
|
|
5135
|
+
function ymd(date) {
|
|
5136
|
+
return date.toISOString().substring(0, 10);
|
|
5137
|
+
}
|
|
5138
|
+
function usageSnapshotFilterForPeriod(period) {
|
|
5139
|
+
const now = new Date;
|
|
5140
|
+
switch (period) {
|
|
5141
|
+
case "today":
|
|
5142
|
+
return { date: ymd(now) };
|
|
5143
|
+
case "yesterday": {
|
|
5144
|
+
const yesterday = new Date(now);
|
|
5145
|
+
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
5146
|
+
return { date: ymd(yesterday) };
|
|
5147
|
+
}
|
|
5148
|
+
case "week": {
|
|
5149
|
+
const weekAgo = new Date(now);
|
|
5150
|
+
weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
|
|
5151
|
+
return { since: ymd(weekAgo) };
|
|
5152
|
+
}
|
|
5153
|
+
case "month":
|
|
5154
|
+
return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
|
|
5155
|
+
case "year":
|
|
5156
|
+
return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
|
|
5157
|
+
case "all":
|
|
5158
|
+
return {};
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
// src/lib/billing-diff.ts
|
|
5163
|
+
init_database();
|
|
5164
|
+
var PROVIDER_TO_AGENT = {
|
|
5165
|
+
anthropic: "claude",
|
|
5166
|
+
openai: "codex",
|
|
5167
|
+
gemini: "gemini",
|
|
5168
|
+
google: "gemini"
|
|
5169
|
+
};
|
|
5170
|
+
function queryBillingDiff(db, period, thresholdPct = 15) {
|
|
5171
|
+
const estimated = querySummary(db, period, undefined, true);
|
|
5172
|
+
const actual = queryBillingSummary(db, period);
|
|
5173
|
+
const delta = estimated.total_usd - actual.total_usd;
|
|
5174
|
+
const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
|
|
5175
|
+
const agentRows = db.prepare(`
|
|
5176
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
|
|
5177
|
+
FROM requests
|
|
5178
|
+
WHERE ${periodWhere3(period, "timestamp")}
|
|
5179
|
+
GROUP BY agent
|
|
5180
|
+
`).all();
|
|
5181
|
+
const by_agent = agentRows.map((row) => {
|
|
5182
|
+
const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
|
|
5183
|
+
const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
|
|
5184
|
+
const rowDelta = row.estimated_usd - actualUsd;
|
|
5185
|
+
const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
|
|
5186
|
+
return {
|
|
5187
|
+
agent: row.agent,
|
|
5188
|
+
estimated_usd: row.estimated_usd,
|
|
5189
|
+
actual_usd: actualUsd,
|
|
5190
|
+
delta_usd: rowDelta,
|
|
5191
|
+
delta_pct: rowPct
|
|
5192
|
+
};
|
|
5193
|
+
}).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
|
|
5194
|
+
return {
|
|
5195
|
+
period,
|
|
5196
|
+
estimated_usd: estimated.total_usd,
|
|
5197
|
+
actual_usd: actual.total_usd,
|
|
5198
|
+
delta_usd: delta,
|
|
5199
|
+
delta_pct: deltaPct,
|
|
5200
|
+
threshold_pct: thresholdPct,
|
|
5201
|
+
is_alert: Math.abs(deltaPct) > thresholdPct,
|
|
5202
|
+
by_agent,
|
|
5203
|
+
by_provider: actual.by_provider
|
|
5204
|
+
};
|
|
5205
|
+
}
|
|
5206
|
+
function periodWhere3(period, column) {
|
|
5207
|
+
switch (period) {
|
|
5208
|
+
case "today":
|
|
5209
|
+
return `DATE(${column}) = DATE('now')`;
|
|
5210
|
+
case "yesterday":
|
|
5211
|
+
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
5212
|
+
case "week":
|
|
5213
|
+
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
5214
|
+
case "month":
|
|
5215
|
+
return `${column} >= DATE('now', 'start of month')`;
|
|
5216
|
+
case "year":
|
|
5217
|
+
return `${column} >= DATE('now', 'start of year')`;
|
|
5218
|
+
case "all":
|
|
5219
|
+
return "1=1";
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
|
|
5223
|
+
// src/server/serve.ts
|
|
5224
|
+
init_database();
|
|
5225
|
+
|
|
5226
|
+
// src/lib/serve-auth.ts
|
|
5227
|
+
function getServeApiToken() {
|
|
5228
|
+
return process.env["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
|
|
5229
|
+
}
|
|
5230
|
+
function getServeBindHost() {
|
|
5231
|
+
const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
|
|
5232
|
+
if (explicit)
|
|
5233
|
+
return explicit;
|
|
5234
|
+
return getServeApiToken() ? "127.0.0.1" : "0.0.0.0";
|
|
5235
|
+
}
|
|
5236
|
+
function isAuthorizedRequest(req, path) {
|
|
5237
|
+
const token = getServeApiToken();
|
|
5238
|
+
if (!token)
|
|
5239
|
+
return true;
|
|
5240
|
+
if (path === "/health")
|
|
5241
|
+
return true;
|
|
5242
|
+
const auth = req.headers.get("Authorization");
|
|
5243
|
+
if (auth === `Bearer ${token}`)
|
|
5244
|
+
return true;
|
|
5245
|
+
if (req.headers.get("X-Economy-Token") === token)
|
|
5246
|
+
return true;
|
|
5247
|
+
return false;
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
// src/server/serve.ts
|
|
5251
|
+
import { randomUUID } from "crypto";
|
|
5252
|
+
import { existsSync as existsSync12 } from "fs";
|
|
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
|
|
5272
|
+
init_cloud();
|
|
5273
|
+
import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
|
|
5274
|
+
|
|
5275
|
+
// src/openapi.ts
|
|
5276
|
+
var openApiSpec = {
|
|
5277
|
+
openapi: "3.0.3",
|
|
5278
|
+
info: {
|
|
5279
|
+
title: "@hasna/economy self-hosted API",
|
|
5280
|
+
version: "0.0.0",
|
|
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."
|
|
5282
|
+
},
|
|
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
|
+
],
|
|
5301
|
+
components: {
|
|
5302
|
+
securitySchemes: {
|
|
5303
|
+
apiKey: {
|
|
5304
|
+
type: "apiKey",
|
|
5305
|
+
in: "header",
|
|
5306
|
+
name: "x-api-key"
|
|
5307
|
+
},
|
|
5308
|
+
bearer: {
|
|
5309
|
+
type: "http",
|
|
5310
|
+
scheme: "bearer"
|
|
5311
|
+
}
|
|
5312
|
+
},
|
|
5313
|
+
schemas: {
|
|
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
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
},
|
|
5618
|
+
paths: {
|
|
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
|
+
},
|
|
6330
|
+
"/v1/budgets": {
|
|
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
|
+
}
|
|
6414
|
+
},
|
|
6415
|
+
"/v1/goals": {
|
|
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
|
+
}
|
|
6499
|
+
},
|
|
6500
|
+
"/v1/pricing": {
|
|
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
|
+
}
|
|
6584
|
+
},
|
|
6585
|
+
"/v1/subscriptions": {
|
|
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
|
+
}
|
|
6669
|
+
},
|
|
6670
|
+
"/v1/project-registry": {
|
|
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
|
+
}
|
|
6749
|
+
},
|
|
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
|
+
}
|
|
6867
|
+
}
|
|
6868
|
+
};
|
|
6869
|
+
|
|
6870
|
+
// src/server/serve.ts
|
|
6871
|
+
function openApiDocument() {
|
|
6872
|
+
const spec = openApiSpec;
|
|
6873
|
+
return { ...spec, info: { ...spec["info"], version: packageMetadata.version } };
|
|
6874
|
+
}
|
|
6875
|
+
function serveMode() {
|
|
6876
|
+
return isCloudMode() ? "self_hosted" : "local";
|
|
6877
|
+
}
|
|
6878
|
+
function foundationEnvelope(status, extra = {}) {
|
|
6879
|
+
return { status, version: packageMetadata.version, mode: serveMode(), service: "economy", ...extra };
|
|
6880
|
+
}
|
|
6881
|
+
var CORS = {
|
|
6882
|
+
"Access-Control-Allow-Origin": "*",
|
|
6883
|
+
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
6884
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
|
|
6885
|
+
};
|
|
6886
|
+
var APP = "economy";
|
|
6887
|
+
var ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
6888
|
+
var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
6889
|
+
var SYNC_SOURCES = ["all", ...AGENTS];
|
|
6890
|
+
var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
6891
|
+
function json(data, status = 200) {
|
|
6892
|
+
return new Response(JSON.stringify(data), {
|
|
6893
|
+
status,
|
|
6894
|
+
headers: { "Content-Type": "application/json", ...CORS }
|
|
6895
|
+
});
|
|
6896
|
+
}
|
|
6897
|
+
function ok(data, meta) {
|
|
6898
|
+
return json({ data, meta: meta ?? {} });
|
|
6899
|
+
}
|
|
6900
|
+
function err(message, status = 400) {
|
|
6901
|
+
return json({ error: message }, status);
|
|
6902
|
+
}
|
|
6903
|
+
function normalizeBudgetPeriod(value) {
|
|
6904
|
+
switch (value) {
|
|
6905
|
+
case "day":
|
|
6906
|
+
case "daily":
|
|
6907
|
+
return "daily";
|
|
6908
|
+
case "week":
|
|
6909
|
+
case "weekly":
|
|
4232
6910
|
return "weekly";
|
|
4233
6911
|
case "month":
|
|
4234
6912
|
case "monthly":
|
|
@@ -4252,6 +6930,11 @@ function optionalAgent(value) {
|
|
|
4252
6930
|
return null;
|
|
4253
6931
|
return typeof value === "string" && AGENTS.includes(value) ? value : undefined;
|
|
4254
6932
|
}
|
|
6933
|
+
function requiredScopesForRequest(method, path) {
|
|
6934
|
+
if (method === "POST" && path === "/api/ingest")
|
|
6935
|
+
return [ECONOMY_WRITE_SCOPE];
|
|
6936
|
+
return;
|
|
6937
|
+
}
|
|
4255
6938
|
function stringArray(value) {
|
|
4256
6939
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
4257
6940
|
}
|
|
@@ -4267,19 +6950,20 @@ function dashboardPath(root, pathname) {
|
|
|
4267
6950
|
const filePath = resolve(rootPath, relativePath);
|
|
4268
6951
|
return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
|
|
4269
6952
|
}
|
|
6953
|
+
var FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
|
|
4270
6954
|
function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
4271
6955
|
return async function fetch2(req) {
|
|
4272
6956
|
const url = new URL(req.url);
|
|
4273
|
-
if (url.pathname.startsWith("/api") || url.pathname
|
|
6957
|
+
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
4274
6958
|
return apiHandler(req);
|
|
4275
6959
|
}
|
|
4276
|
-
if (
|
|
6960
|
+
if (existsSync12(dashboardDir)) {
|
|
4277
6961
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
4278
|
-
if (filePath &&
|
|
6962
|
+
if (filePath && existsSync12(filePath)) {
|
|
4279
6963
|
return new Response(Bun.file(filePath));
|
|
4280
6964
|
}
|
|
4281
6965
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
4282
|
-
if (indexPath &&
|
|
6966
|
+
if (indexPath && existsSync12(indexPath)) {
|
|
4283
6967
|
return new Response(Bun.file(indexPath));
|
|
4284
6968
|
}
|
|
4285
6969
|
}
|
|
@@ -4291,15 +6975,38 @@ function applyFields(obj, fields) {
|
|
|
4291
6975
|
return obj;
|
|
4292
6976
|
return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
|
|
4293
6977
|
}
|
|
4294
|
-
function createHandler(db) {
|
|
6978
|
+
function createHandler(db, options = {}) {
|
|
4295
6979
|
return async function handler(req) {
|
|
4296
6980
|
const url = new URL(req.url);
|
|
4297
|
-
const
|
|
6981
|
+
const rawPath = url.pathname;
|
|
6982
|
+
const path = rawPath.startsWith("/v1/") ? "/api" + rawPath.slice(3) : rawPath === "/v1" ? "/api" : rawPath;
|
|
4298
6983
|
const method = req.method;
|
|
4299
6984
|
if (method === "OPTIONS")
|
|
4300
6985
|
return new Response(null, { status: 204, headers: CORS });
|
|
4301
|
-
if (
|
|
6986
|
+
if (method === "GET" && (rawPath === "/health" || rawPath === "/healthz")) {
|
|
6987
|
+
return json(foundationEnvelope("ok"));
|
|
6988
|
+
}
|
|
6989
|
+
if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version")) {
|
|
6990
|
+
return json(foundationEnvelope("ok"));
|
|
6991
|
+
}
|
|
6992
|
+
if (method === "GET" && (rawPath === "/ready" || rawPath === "/readyz")) {
|
|
6993
|
+
const result = options.readyCheck ? await options.readyCheck() : { ready: true };
|
|
6994
|
+
return json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), result.ready ? 200 : 503);
|
|
6995
|
+
}
|
|
6996
|
+
if (method === "GET" && rawPath === "/openapi.json") {
|
|
6997
|
+
return json(openApiDocument());
|
|
6998
|
+
}
|
|
6999
|
+
if (options.authenticator) {
|
|
7000
|
+
const decision = await options.authenticator.authenticate(req.headers, {
|
|
7001
|
+
method,
|
|
7002
|
+
path: rawPath,
|
|
7003
|
+
requiredScopes: requiredScopesForRequest(method, path)
|
|
7004
|
+
});
|
|
7005
|
+
if (!decision.ok)
|
|
7006
|
+
return json({ error: decision.reason ?? "unauthorized", message: decision.message }, decision.status || 401);
|
|
7007
|
+
} else if (!isAuthorizedRequest(req, path)) {
|
|
4302
7008
|
return err("Unauthorized", 401);
|
|
7009
|
+
}
|
|
4303
7010
|
if (path === "/health")
|
|
4304
7011
|
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
4305
7012
|
if (path === "/api/summary" && method === "GET") {
|
|
@@ -4546,15 +7253,22 @@ function createHandler(db) {
|
|
|
4546
7253
|
} catch {}
|
|
4547
7254
|
return ok(results);
|
|
4548
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
|
+
}
|
|
4549
7262
|
if (path === "/api/usage" && method === "GET") {
|
|
4550
7263
|
const period = url.searchParams.get("period") ?? "month";
|
|
4551
|
-
const
|
|
7264
|
+
const agentParam = url.searchParams.get("agent") ?? undefined;
|
|
7265
|
+
const agent = agentParam && isAgent(agentParam) ? agentParam : undefined;
|
|
4552
7266
|
return ok({
|
|
4553
7267
|
snapshots: queryUsageSnapshots(db, {
|
|
4554
|
-
agent
|
|
7268
|
+
agent,
|
|
4555
7269
|
...usageSnapshotFilterForPeriod(period)
|
|
4556
7270
|
}),
|
|
4557
|
-
summary: querySummary(db, period, undefined, true)
|
|
7271
|
+
summary: querySummary(db, period, undefined, true, agent)
|
|
4558
7272
|
});
|
|
4559
7273
|
}
|
|
4560
7274
|
if (path === "/api/savings" && method === "GET") {
|
|
@@ -4562,6 +7276,47 @@ function createHandler(db) {
|
|
|
4562
7276
|
const agent = url.searchParams.get("agent") ?? undefined;
|
|
4563
7277
|
return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
|
|
4564
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
|
+
}
|
|
4565
7320
|
if (path === "/api/subscriptions" && method === "GET") {
|
|
4566
7321
|
return ok(listSubscriptions(db));
|
|
4567
7322
|
}
|
|
@@ -4652,27 +7407,68 @@ function createHandler(db) {
|
|
|
4652
7407
|
return err("Not found", 404);
|
|
4653
7408
|
};
|
|
4654
7409
|
}
|
|
7410
|
+
function isLocalHost(host) {
|
|
7411
|
+
return ["127.0.0.1", "localhost", "::1"].includes(host);
|
|
7412
|
+
}
|
|
4655
7413
|
function startServer(port = 3456, options = {}) {
|
|
4656
|
-
const
|
|
4657
|
-
|
|
4658
|
-
const
|
|
4659
|
-
const
|
|
7414
|
+
const cloud = options.db ? false : isCloudMode();
|
|
7415
|
+
const hostname2 = options.hostname ?? (cloud ? process.env["ECONOMY_HOST"] ?? "0.0.0.0" : getServeBindHost());
|
|
7416
|
+
const log = options.log ?? console.log;
|
|
7417
|
+
const handlerOptions = {};
|
|
7418
|
+
let db;
|
|
7419
|
+
if (cloud) {
|
|
7420
|
+
db = openCloudDatabase();
|
|
7421
|
+
const pool = createCloudPool();
|
|
7422
|
+
const signingSecret = resolveSigningSecret();
|
|
7423
|
+
if (signingSecret) {
|
|
7424
|
+
const keys = new ApiKeyStore(authClientFromPool(pool));
|
|
7425
|
+
keys.ensureSchema().catch((e) => log(`api_keys ensureSchema: ${e instanceof Error ? e.message : String(e)}`));
|
|
7426
|
+
handlerOptions.authenticator = verifyApiKey({
|
|
7427
|
+
app: APP,
|
|
7428
|
+
signingSecret,
|
|
7429
|
+
isRevoked: keys.statusChecker(),
|
|
7430
|
+
audit: (event) => log(JSON.stringify({ evt: "api_auth", outcome: event.outcome, kid: event.kid, reason: event.reason, path: event.path, status: event.status }))
|
|
7431
|
+
});
|
|
7432
|
+
} else if (!isLocalHost(hostname2)) {
|
|
7433
|
+
throw new Error("economy-serve on a non-local host requires an API signing secret (HASNA_ECONOMY_API_SIGNING_KEY / API_KEY_SIGNING_SECRET)");
|
|
7434
|
+
}
|
|
7435
|
+
handlerOptions.readyCheck = async () => {
|
|
7436
|
+
try {
|
|
7437
|
+
const res = await pool.query("SELECT to_regclass($1) AS reg", ["public.requests"]);
|
|
7438
|
+
if (!res.rows[0]?.reg)
|
|
7439
|
+
return { ready: false, detail: "pending_migrations:requests" };
|
|
7440
|
+
return { ready: true };
|
|
7441
|
+
} catch (error) {
|
|
7442
|
+
return { ready: false, detail: error instanceof Error ? error.message : "storage_unreachable" };
|
|
7443
|
+
}
|
|
7444
|
+
};
|
|
7445
|
+
} else {
|
|
7446
|
+
db = options.db ?? openDatabase();
|
|
7447
|
+
ensurePricingSeeded(db);
|
|
7448
|
+
}
|
|
7449
|
+
const apiHandler = createHandler(db, handlerOptions);
|
|
4660
7450
|
const server = Bun.serve({
|
|
4661
7451
|
port,
|
|
4662
7452
|
hostname: hostname2,
|
|
4663
7453
|
fetch: createServerFetch(apiHandler, options.dashboardDir)
|
|
4664
7454
|
});
|
|
4665
7455
|
const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
|
|
4666
|
-
const log = options.log ?? console.log;
|
|
4667
7456
|
log(`economy-serve listening on ${address}`);
|
|
4668
7457
|
return server;
|
|
4669
7458
|
}
|
|
4670
7459
|
|
|
4671
7460
|
// src/server/index.ts
|
|
4672
7461
|
function printHelp() {
|
|
4673
|
-
console.log(`Usage: economy-serve [options]
|
|
7462
|
+
console.log(`Usage: economy-serve [command] [options]
|
|
4674
7463
|
|
|
4675
7464
|
REST API server for ${packageMetadata.name}
|
|
7465
|
+
Foundation probes: GET /health, /ready, /version -> { status, version, mode }
|
|
7466
|
+
Versioned API: /v1/* (API-key auth via @hasna/contracts in cloud mode)
|
|
7467
|
+
|
|
7468
|
+
Commands:
|
|
7469
|
+
(default) start the HTTP server
|
|
7470
|
+
migrate apply the cloud Postgres schema + api_keys, then exit
|
|
7471
|
+
version print { status, version, mode }
|
|
4676
7472
|
|
|
4677
7473
|
Options:
|
|
4678
7474
|
-p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
|
|
@@ -4698,18 +7494,46 @@ function parsePort(raw, label) {
|
|
|
4698
7494
|
}
|
|
4699
7495
|
return value;
|
|
4700
7496
|
}
|
|
7497
|
+
async function runMigrate() {
|
|
7498
|
+
const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
7499
|
+
const { applyPgMigrations } = await import("@hasna/cloud");
|
|
7500
|
+
const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
|
|
7501
|
+
const dsn = getCloudDatabaseUrl2();
|
|
7502
|
+
if (!dsn)
|
|
7503
|
+
throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
|
|
7504
|
+
const result = await applyPgMigrations(dsn, cloudMigrations2(), "economy");
|
|
7505
|
+
const pool = createCloudPool2(dsn);
|
|
7506
|
+
try {
|
|
7507
|
+
await new ApiKeyStore2(authClientFromPool2(pool)).ensureSchema();
|
|
7508
|
+
} finally {
|
|
7509
|
+
await pool.end().catch(() => {});
|
|
7510
|
+
}
|
|
7511
|
+
console.log(JSON.stringify({ evt: "migrate", service: result.service, applied: result.applied, alreadyApplied: result.alreadyApplied, errors: result.errors, api_keys: "ensured" }));
|
|
7512
|
+
if (result.errors.length)
|
|
7513
|
+
process.exit(1);
|
|
7514
|
+
}
|
|
4701
7515
|
var args = process.argv.slice(2);
|
|
4702
|
-
|
|
4703
|
-
|
|
7516
|
+
var sub = args[0];
|
|
7517
|
+
if (sub === "version") {
|
|
7518
|
+
console.log(JSON.stringify({ status: "ok", version: packageMetadata.version, mode: process.env["HASNA_ECONOMY_STORAGE_MODE"] === "cloud" ? "self_hosted" : "local" }));
|
|
4704
7519
|
process.exit(0);
|
|
4705
7520
|
}
|
|
4706
|
-
if (
|
|
7521
|
+
if (sub === "migrate") {
|
|
7522
|
+
runMigrate().catch((error) => {
|
|
7523
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
7524
|
+
process.exit(1);
|
|
7525
|
+
});
|
|
7526
|
+
} else if (args.includes("--help") || args.includes("-h")) {
|
|
7527
|
+
printHelp();
|
|
7528
|
+
process.exit(0);
|
|
7529
|
+
} else if (args.includes("--version") || args.includes("-V")) {
|
|
4707
7530
|
console.log(packageMetadata.version);
|
|
4708
7531
|
process.exit(0);
|
|
4709
|
-
}
|
|
4710
|
-
try {
|
|
4711
|
-
|
|
4712
|
-
} catch (error) {
|
|
4713
|
-
|
|
4714
|
-
|
|
7532
|
+
} else {
|
|
7533
|
+
try {
|
|
7534
|
+
startServer(resolvePort(args));
|
|
7535
|
+
} catch (error) {
|
|
7536
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
7537
|
+
process.exit(1);
|
|
7538
|
+
}
|
|
4715
7539
|
}
|