@hasna/economy 0.3.28 → 0.5.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 +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +18 -22
- package/SECURITY.md +1 -1
- package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
- package/dist/chunks/billing-bkpxy4kv.js +13 -0
- package/dist/chunks/config-j9mv3ehk.js +15 -0
- package/dist/chunks/index-2hwzmfgv.js +3407 -0
- package/dist/chunks/index-axh32d30.js +1313 -0
- package/dist/chunks/index-emvs5ph2.js +2085 -0
- package/dist/chunks/index-ez9thsr7.js +3440 -0
- package/dist/chunks/index-hesxq90g.js +62 -0
- package/dist/chunks/index-k540hfww.js +258 -0
- package/dist/chunks/index-kgy9b6bf.js +451 -0
- package/dist/chunks/index-kn5572kd.js +124 -0
- package/dist/chunks/index-p35cyhc4.js +4 -0
- package/dist/chunks/index-w3bvv0ns.js +26 -0
- package/dist/chunks/index-zpvea08j.js +3372 -0
- package/dist/chunks/menubar-6xtkt17h.js +296 -0
- package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
- package/dist/chunks/pricing-f80p81gr.js +29 -0
- package/dist/chunks/pricing-vf2pycmh.js +30 -0
- package/dist/chunks/pricing-y21mcjsa.js +30 -0
- package/dist/chunks/serve-0k69e4h9.js +4655 -0
- package/dist/chunks/sqlite-store-501f32ts.js +258 -0
- package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
- package/dist/chunks/sqlite-store-st99garx.js +258 -0
- package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
- package/dist/chunks/tui-7f8f951w.js +22 -0
- package/dist/chunks/watch-mm1j39vy.js +196 -0
- package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
- package/dist/cli/commands/brief.d.ts.map +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/journal.d.ts +3 -0
- package/dist/cli/commands/journal.d.ts.map +1 -0
- package/dist/cli/commands/menubar.d.ts +97 -4
- package/dist/cli/commands/menubar.d.ts.map +1 -1
- package/dist/cli/commands/tui.d.ts +1 -1
- 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 +1524 -9998
- package/dist/db/cloud.d.ts +18 -12
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +33 -12
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/ingest-concurrency-worker.d.ts +2 -0
- package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
- package/dist/db/ingest-errors.d.ts +9 -0
- package/dist/db/ingest-errors.d.ts.map +1 -0
- package/dist/db/ingest-schema.d.ts +3 -0
- package/dist/db/ingest-schema.d.ts.map +1 -0
- package/dist/db/ingest-specs.d.ts +8 -0
- package/dist/db/ingest-specs.d.ts.map +1 -0
- package/dist/db/ingest-test-contract.d.ts +3 -0
- package/dist/db/ingest-test-contract.d.ts.map +1 -0
- package/dist/db/ingest-validation.d.ts +12 -0
- package/dist/db/ingest-validation.d.ts.map +1 -0
- package/dist/db/ingest.d.ts +27 -0
- package/dist/db/ingest.d.ts.map +1 -0
- package/dist/db/legacy-schema.d.ts +7 -0
- package/dist/db/legacy-schema.d.ts.map +1 -0
- package/dist/db/observation-contract.d.ts +3 -0
- package/dist/db/observation-contract.d.ts.map +1 -0
- package/dist/db/observation-provenance.d.ts +23 -0
- package/dist/db/observation-provenance.d.ts.map +1 -0
- package/dist/db/observation-schema.d.ts +24 -0
- package/dist/db/observation-schema.d.ts.map +1 -0
- package/dist/db/pg-connection.d.ts +10 -0
- package/dist/db/pg-connection.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +4 -9
- package/dist/db/pg-migrate.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/pg-protocol.d.ts +28 -0
- package/dist/db/pg-protocol.d.ts.map +1 -0
- package/dist/db/sqlite-store.d.ts +4 -0
- package/dist/db/sqlite-store.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +17 -21
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/db/third-party-sqlite.d.ts +10 -0
- package/dist/db/third-party-sqlite.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1281 -2405
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +25 -0
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/collection-outcome.d.ts +17 -0
- package/dist/ingest/collection-outcome.d.ts.map +1 -0
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +9 -0
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/quota-observation.d.ts +16 -0
- package/dist/ingest/quota-observation.d.ts.map +1 -0
- package/dist/ingest/source-time.d.ts +4 -0
- package/dist/ingest/source-time.d.ts.map +1 -0
- package/dist/lib/accounts-store.d.ts +109 -0
- package/dist/lib/accounts-store.d.ts.map +1 -0
- package/dist/lib/accounts.d.ts +2 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +19 -0
- package/dist/lib/api-display-url.d.ts.map +1 -0
- package/dist/lib/autosync-gate.d.ts +22 -0
- package/dist/lib/autosync-gate.d.ts.map +1 -0
- package/dist/lib/billing-diff.d.ts +18 -40
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/brief.d.ts +2 -0
- package/dist/lib/brief.d.ts.map +1 -1
- package/dist/lib/budget-decimal.d.ts +14 -0
- package/dist/lib/budget-decimal.d.ts.map +1 -0
- package/dist/lib/cloud-ingest.d.ts +30 -1
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.d.ts +166 -10
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/ingest-batches.d.ts +21 -0
- package/dist/lib/ingest-batches.d.ts.map +1 -0
- package/dist/lib/ingest-journal-client.d.ts +37 -0
- package/dist/lib/ingest-journal-client.d.ts.map +1 -0
- package/dist/lib/ingest-journal-lock.d.ts +5 -0
- package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
- package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
- package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
- package/dist/lib/ingest-journal-operation.d.ts +55 -0
- package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
- package/dist/lib/ingest-journal.d.ts +73 -0
- package/dist/lib/ingest-journal.d.ts.map +1 -0
- package/dist/lib/money-display.d.ts +13 -0
- package/dist/lib/money-display.d.ts.map +1 -0
- package/dist/lib/periods.d.ts +12 -0
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +14 -0
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +64 -7
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/serve-auth.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +29 -22
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/summary-contract.d.ts +32 -0
- package/dist/lib/summary-contract.d.ts.map +1 -0
- package/dist/lib/summary-test-contract.d.ts +3 -0
- package/dist/lib/summary-test-contract.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +4 -0
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/sync-maintenance.d.ts.map +1 -1
- package/dist/lib/sync-report.d.ts +4 -0
- package/dist/lib/sync-report.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +9 -0
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
- package/dist/mcp/agent-registry-store.d.ts +30 -0
- package/dist/mcp/agent-registry-store.d.ts.map +1 -0
- package/dist/mcp/agent-registry.d.ts +78 -0
- package/dist/mcp/agent-registry.d.ts.map +1 -0
- package/dist/mcp/harness.d.ts +26 -0
- package/dist/mcp/harness.d.ts.map +1 -0
- package/dist/mcp/index.js +3219 -3357
- package/dist/mcp/ingest-journal-tool.d.ts +2 -0
- package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +2197 -121
- package/dist/server/human-auth.d.ts +20 -0
- package/dist/server/human-auth.d.ts.map +1 -0
- package/dist/server/index.js +10477 -6378
- package/dist/server/legacy-inventory.d.ts +52 -0
- package/dist/server/legacy-inventory.d.ts.map +1 -0
- package/dist/server/legacy-operations.d.ts +64 -0
- package/dist/server/legacy-operations.d.ts.map +1 -0
- package/dist/server/legacy-plan.d.ts +55 -0
- package/dist/server/legacy-plan.d.ts.map +1 -0
- package/dist/server/legacy-preservation.d.ts +58 -0
- package/dist/server/legacy-preservation.d.ts.map +1 -0
- package/dist/server/pg-sync-worker.js +195 -21
- package/dist/server/serve.d.ts +20 -18
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/service-admission.d.ts +10 -0
- package/dist/server/service-admission.d.ts.map +1 -0
- package/dist/server/service-dispatcher.d.ts +45 -0
- package/dist/server/service-dispatcher.d.ts.map +1 -0
- package/dist/server/service-operations.d.ts +5 -0
- package/dist/server/service-operations.d.ts.map +1 -0
- package/dist/server/service-protocol.d.ts +50 -0
- package/dist/server/service-protocol.d.ts.map +1 -0
- package/dist/server/service-worker.d.ts +2 -0
- package/dist/server/service-worker.d.ts.map +1 -0
- package/dist/server/service-worker.js +8029 -0
- package/dist/types/index.d.ts +66 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/cli.md +2 -3
- package/docs/configuration.md +25 -12
- package/docs/cost-reporting.md +49 -0
- package/docs/historical-data-reconciliation.md +203 -0
- package/docs/ingest-journal.md +108 -0
- package/docs/ingestion.md +61 -5
- package/docs/mcp.md +5 -3
- package/docs/native-budget-save.md +84 -0
- package/docs/native-human-auth.md +102 -0
- package/docs/otel.md +8 -2
- package/docs/rest-api.md +4 -4
- package/package.json +17 -20
- package/postinstall.js +52 -1
- package/dashboard/README.md +0 -26
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
- package/dist/lib/test-hermetic-accounts.d.ts +0 -6
- package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,2036 +1,77 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
close() {
|
|
63
|
-
this.db.close();
|
|
64
|
-
}
|
|
65
|
-
transaction(fn) {
|
|
66
|
-
return this.db.transaction(fn)();
|
|
67
|
-
}
|
|
68
|
-
get raw() {
|
|
69
|
-
return this.db;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
var init_sqlite_adapter = () => {};
|
|
73
|
-
|
|
74
|
-
// src/lib/pricing.ts
|
|
75
|
-
var exports_pricing = {};
|
|
76
|
-
__export(exports_pricing, {
|
|
77
|
-
normalizeModelName: () => normalizeModelName,
|
|
78
|
-
getPricingFromRows: () => getPricingFromRows,
|
|
79
|
-
getPricingFromDb: () => getPricingFromDb,
|
|
80
|
-
getPricing: () => getPricing,
|
|
81
|
-
estimateCostFromRows: () => estimateCostFromRows,
|
|
82
|
-
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
83
|
-
computeCostFromDb: () => computeCostFromDb,
|
|
84
|
-
computeCost: () => computeCost,
|
|
85
|
-
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
86
|
-
});
|
|
87
|
-
function normalizeModelName(raw) {
|
|
88
|
-
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
89
|
-
}
|
|
90
|
-
function normalizeModelNamePreservingProvider(raw) {
|
|
91
|
-
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
92
|
-
}
|
|
93
|
-
function modelLookupKeys(raw) {
|
|
94
|
-
const withProvider = normalizeModelNamePreservingProvider(raw);
|
|
95
|
-
const withoutProvider = normalizeModelName(raw);
|
|
96
|
-
return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
|
|
97
|
-
}
|
|
98
|
-
function bestPrefixMatch(normalized, entries) {
|
|
99
|
-
let best = null;
|
|
100
|
-
for (const entry of entries) {
|
|
101
|
-
const [key] = entry;
|
|
102
|
-
if (normalized !== key && !normalized.startsWith(`${key}-`))
|
|
103
|
-
continue;
|
|
104
|
-
if (!best || key.length > best[0].length)
|
|
105
|
-
best = entry;
|
|
106
|
-
}
|
|
107
|
-
return best?.[1] ?? null;
|
|
108
|
-
}
|
|
109
|
-
function bestModelMatch(model, entries) {
|
|
110
|
-
for (const key of modelLookupKeys(model)) {
|
|
111
|
-
const match = bestPrefixMatch(key, entries);
|
|
112
|
-
if (match)
|
|
113
|
-
return match;
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
function exactModelMatch(model, entries) {
|
|
118
|
-
for (const key of modelLookupKeys(model)) {
|
|
119
|
-
const match = entries.find(([entryKey]) => entryKey === key);
|
|
120
|
-
if (match)
|
|
121
|
-
return match[1];
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
function ensurePricingSeeded(db) {
|
|
126
|
-
seedModelPricing(db, DEFAULT_PRICING);
|
|
127
|
-
repairLegacySeededPricing(db);
|
|
128
|
-
repairMissingDefaultCacheWrite1h(db);
|
|
129
|
-
repairMissingDefaultCacheStorage(db);
|
|
130
|
-
removeDeprecatedDefaultPricing(db);
|
|
131
|
-
}
|
|
132
|
-
function repairLegacySeededPricing(db) {
|
|
133
|
-
const now = new Date().toISOString();
|
|
134
|
-
const legacyModels = new Set([
|
|
135
|
-
...Object.keys(LEGACY_DEFAULT_PRICING),
|
|
136
|
-
...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
|
|
137
|
-
]);
|
|
138
|
-
for (const model of legacyModels) {
|
|
139
|
-
const current = getModelPricing(db, model);
|
|
140
|
-
const next = DEFAULT_PRICING[model];
|
|
141
|
-
if (!current || !next)
|
|
142
|
-
continue;
|
|
143
|
-
const legacy = LEGACY_DEFAULT_PRICING[model];
|
|
144
|
-
const legacyRows = [
|
|
145
|
-
...legacy ? [legacy] : [],
|
|
146
|
-
...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
|
|
147
|
-
];
|
|
148
|
-
if (!legacyRows.some((row) => samePricing(current, row)))
|
|
149
|
-
continue;
|
|
150
|
-
upsertModelPricing(db, {
|
|
151
|
-
model,
|
|
152
|
-
input_per_1m: next.inputPer1M,
|
|
153
|
-
output_per_1m: next.outputPer1M,
|
|
154
|
-
cache_read_per_1m: next.cacheReadPer1M,
|
|
155
|
-
cache_write_per_1m: next.cacheWritePer1M,
|
|
156
|
-
cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
|
|
157
|
-
cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
|
|
158
|
-
updated_at: now
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
function repairMissingDefaultCacheWrite1h(db) {
|
|
163
|
-
const now = new Date().toISOString();
|
|
164
|
-
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
165
|
-
if (!next.cacheWrite1hPer1M)
|
|
166
|
-
continue;
|
|
167
|
-
const current = getModelPricing(db, model);
|
|
168
|
-
if (!current)
|
|
169
|
-
continue;
|
|
170
|
-
if ((current.cache_write_1h_per_1m ?? 0) !== 0)
|
|
171
|
-
continue;
|
|
172
|
-
if (!sameBasePricing(current, next))
|
|
173
|
-
continue;
|
|
174
|
-
upsertModelPricing(db, {
|
|
175
|
-
model,
|
|
176
|
-
input_per_1m: current.input_per_1m,
|
|
177
|
-
output_per_1m: current.output_per_1m,
|
|
178
|
-
cache_read_per_1m: current.cache_read_per_1m,
|
|
179
|
-
cache_write_per_1m: current.cache_write_per_1m,
|
|
180
|
-
cache_write_1h_per_1m: next.cacheWrite1hPer1M,
|
|
181
|
-
cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
|
|
182
|
-
updated_at: now
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function repairMissingDefaultCacheStorage(db) {
|
|
187
|
-
const now = new Date().toISOString();
|
|
188
|
-
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
189
|
-
if (!next.cacheStoragePer1MHour)
|
|
190
|
-
continue;
|
|
191
|
-
const current = getModelPricing(db, model);
|
|
192
|
-
if (!current)
|
|
193
|
-
continue;
|
|
194
|
-
if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
|
|
195
|
-
continue;
|
|
196
|
-
if (!sameBasePricing(current, next))
|
|
197
|
-
continue;
|
|
198
|
-
upsertModelPricing(db, {
|
|
199
|
-
model,
|
|
200
|
-
input_per_1m: current.input_per_1m,
|
|
201
|
-
output_per_1m: current.output_per_1m,
|
|
202
|
-
cache_read_per_1m: current.cache_read_per_1m,
|
|
203
|
-
cache_write_per_1m: current.cache_write_per_1m,
|
|
204
|
-
cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
|
|
205
|
-
cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
|
|
206
|
-
updated_at: now
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
function removeDeprecatedDefaultPricing(db) {
|
|
211
|
-
for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
|
|
212
|
-
const current = getModelPricing(db, model);
|
|
213
|
-
if (!current)
|
|
214
|
-
continue;
|
|
215
|
-
if (!removedRows.some((row) => samePricing(current, row)))
|
|
216
|
-
continue;
|
|
217
|
-
deleteModelPricing(db, model);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
function sameBasePricing(row, pricing) {
|
|
221
|
-
return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M;
|
|
222
|
-
}
|
|
223
|
-
function samePricing(row, pricing) {
|
|
224
|
-
return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M && (row.cache_write_1h_per_1m ?? 0) === (pricing.cacheWrite1hPer1M ?? 0) && (row.cache_storage_per_1m_hour ?? 0) === (pricing.cacheStoragePer1MHour ?? 0);
|
|
225
|
-
}
|
|
226
|
-
function getPricingFromDb(db, model) {
|
|
227
|
-
if (isFreeModel(model))
|
|
228
|
-
return FREE_PRICING;
|
|
229
|
-
for (const key of modelLookupKeys(model)) {
|
|
230
|
-
const row = getModelPricing(db, key);
|
|
231
|
-
if (row)
|
|
232
|
-
return modelPricingFromDbRow(row);
|
|
233
|
-
}
|
|
234
|
-
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
235
|
-
const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
|
|
236
|
-
if (!match)
|
|
237
|
-
return null;
|
|
238
|
-
return modelPricingFromDbRow(match);
|
|
239
|
-
}
|
|
240
|
-
function modelPricingFromDbRow(row) {
|
|
241
|
-
const seeded = DEFAULT_PRICING[row.model];
|
|
242
|
-
const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
|
|
243
|
-
return {
|
|
244
|
-
inputPer1M: row.input_per_1m,
|
|
245
|
-
outputPer1M: row.output_per_1m,
|
|
246
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
247
|
-
cacheWritePer1M: row.cache_write_per_1m,
|
|
248
|
-
cacheWrite1hPer1M,
|
|
249
|
-
cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
function getPricing(model) {
|
|
253
|
-
if (isFreeModel(model))
|
|
254
|
-
return FREE_PRICING;
|
|
255
|
-
return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
|
|
256
|
-
}
|
|
257
|
-
function isFreeModel(model) {
|
|
258
|
-
return model.trim().toLowerCase().endsWith(":free");
|
|
259
|
-
}
|
|
260
|
-
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
261
|
-
const pricing = getPricing(model);
|
|
262
|
-
if (!pricing)
|
|
263
|
-
return 0;
|
|
264
|
-
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
265
|
-
}
|
|
266
|
-
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
267
|
-
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
268
|
-
if (!pricing)
|
|
269
|
-
return 0;
|
|
270
|
-
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
271
|
-
}
|
|
272
|
-
function getPricingFromRows(rows, model) {
|
|
273
|
-
if (isFreeModel(model))
|
|
274
|
-
return FREE_PRICING;
|
|
275
|
-
for (const key of modelLookupKeys(model)) {
|
|
276
|
-
const row = rows.find((r) => r.model === key);
|
|
277
|
-
if (row)
|
|
278
|
-
return modelPricingFromDbRow(row);
|
|
279
|
-
}
|
|
280
|
-
const match = bestModelMatch(model, rows.map((r) => [r.model, r]));
|
|
281
|
-
if (!match)
|
|
282
|
-
return null;
|
|
283
|
-
return modelPricingFromDbRow(match);
|
|
284
|
-
}
|
|
285
|
-
function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
286
|
-
const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
|
|
287
|
-
if (!pricing)
|
|
288
|
-
return 0;
|
|
289
|
-
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
290
|
-
}
|
|
291
|
-
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
292
|
-
if (isFreeModel(model))
|
|
293
|
-
return 0;
|
|
294
|
-
let effective = pricing;
|
|
295
|
-
const promptTier = bestModelMatch(model, Object.entries(GEMINI_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(QWEN_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(MINIMAX_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(XAI_PROMPT_TIERS)) ?? exactModelMatch(model, Object.entries(OPENAI_PROMPT_TIERS));
|
|
296
|
-
if (promptTier) {
|
|
297
|
-
const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
|
|
298
|
-
if (billablePromptTokens > promptTier.threshold) {
|
|
299
|
-
effective = {
|
|
300
|
-
...pricing,
|
|
301
|
-
inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
|
|
302
|
-
outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
|
|
303
|
-
cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
|
|
304
|
-
cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
309
|
-
}
|
|
310
|
-
var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS;
|
|
311
|
-
var init_pricing = __esm(() => {
|
|
312
|
-
init_database();
|
|
313
|
-
DEFAULT_PRICING = {
|
|
314
|
-
"claude-opus-4-8": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
315
|
-
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
316
|
-
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
317
|
-
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
318
|
-
"claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
319
|
-
"claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
320
|
-
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
321
|
-
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
322
|
-
"claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
323
|
-
"claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
324
|
-
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
|
|
325
|
-
"claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
|
|
326
|
-
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
327
|
-
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
|
|
328
|
-
"gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
329
|
-
"gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
330
|
-
"gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
331
|
-
"gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
332
|
-
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
333
|
-
"gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
334
|
-
"gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
335
|
-
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
336
|
-
"gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
337
|
-
"google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
|
|
338
|
-
"google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
339
|
-
"google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
340
|
-
"google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
|
|
341
|
-
"google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
|
|
342
|
-
"google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
|
|
343
|
-
"google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
|
|
344
|
-
"gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
345
|
-
"gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
346
|
-
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
347
|
-
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
348
|
-
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
349
|
-
"gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
350
|
-
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
351
|
-
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
352
|
-
"gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
353
|
-
"gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
354
|
-
"gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
355
|
-
"gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
|
|
356
|
-
"gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
357
|
-
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
358
|
-
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
359
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
360
|
-
"o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
361
|
-
o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
362
|
-
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
363
|
-
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
|
|
364
|
-
"qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
365
|
-
"qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
366
|
-
"qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
367
|
-
"qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
368
|
-
"qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
369
|
-
"qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
370
|
-
"qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
371
|
-
"qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
372
|
-
"qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
373
|
-
"qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
374
|
-
"minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
375
|
-
"minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
376
|
-
"minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
377
|
-
"minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
378
|
-
"minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
379
|
-
"grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
380
|
-
"grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
381
|
-
"grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
382
|
-
"grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
383
|
-
"grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
384
|
-
"grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
385
|
-
"grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
386
|
-
"grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
387
|
-
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
388
|
-
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
|
|
389
|
-
"glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
|
|
390
|
-
"glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
391
|
-
"z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
|
|
392
|
-
"z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
|
|
393
|
-
"kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
|
|
394
|
-
"kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
|
|
395
|
-
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
396
|
-
"moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
397
|
-
"moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
|
|
398
|
-
"moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
399
|
-
};
|
|
400
|
-
LEGACY_DEFAULT_PRICING = {
|
|
401
|
-
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
402
|
-
"claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
403
|
-
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
|
|
404
|
-
"gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
405
|
-
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
406
|
-
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
407
|
-
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
408
|
-
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
409
|
-
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
410
|
-
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
411
|
-
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
412
|
-
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
413
|
-
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
414
|
-
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
415
|
-
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
416
|
-
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
417
|
-
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
418
|
-
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
419
|
-
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
420
|
-
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
421
|
-
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
|
|
422
|
-
};
|
|
423
|
-
ADDITIONAL_LEGACY_DEFAULT_PRICING = {
|
|
424
|
-
"gemini-2.5-pro": [
|
|
425
|
-
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
426
|
-
],
|
|
427
|
-
"qwen3.6-plus": [
|
|
428
|
-
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
429
|
-
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
430
|
-
],
|
|
431
|
-
"qwen3.6-flash": [
|
|
432
|
-
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
433
|
-
],
|
|
434
|
-
"qwen3.6-max-preview": [
|
|
435
|
-
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
436
|
-
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
437
|
-
],
|
|
438
|
-
"qwen/qwen3.6-plus": [
|
|
439
|
-
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
440
|
-
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
441
|
-
],
|
|
442
|
-
"qwen/qwen3.6-flash": [
|
|
443
|
-
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
444
|
-
],
|
|
445
|
-
"qwen/qwen3.6-max-preview": [
|
|
446
|
-
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
447
|
-
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
448
|
-
]
|
|
449
|
-
};
|
|
450
|
-
REMOVED_DEFAULT_PRICING = {
|
|
451
|
-
"claude-3-5-sonnet": [
|
|
452
|
-
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
453
|
-
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
454
|
-
],
|
|
455
|
-
"claude-3-sonnet": [
|
|
456
|
-
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
457
|
-
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
458
|
-
],
|
|
459
|
-
"gemini-3.1-pro": [
|
|
460
|
-
{ inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
461
|
-
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
|
|
462
|
-
],
|
|
463
|
-
"gemini-1.5-pro": [
|
|
464
|
-
{ inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
465
|
-
],
|
|
466
|
-
"gemini-1.5-flash": [
|
|
467
|
-
{ inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
468
|
-
],
|
|
469
|
-
"gpt-5.3-chat": [
|
|
470
|
-
{ inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
471
|
-
{ inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
|
|
472
|
-
],
|
|
473
|
-
"qwen3.6": [
|
|
474
|
-
{ inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
475
|
-
]
|
|
476
|
-
};
|
|
477
|
-
FREE_PRICING = {
|
|
478
|
-
inputPer1M: 0,
|
|
479
|
-
outputPer1M: 0,
|
|
480
|
-
cacheReadPer1M: 0,
|
|
481
|
-
cacheWritePer1M: 0,
|
|
482
|
-
cacheWrite1hPer1M: 0,
|
|
483
|
-
cacheStoragePer1MHour: 0
|
|
484
|
-
};
|
|
485
|
-
GEMINI_PROMPT_TIERS = {
|
|
486
|
-
"gemini-3.1-pro-preview": {
|
|
487
|
-
threshold: 200000,
|
|
488
|
-
inputPer1M: 4,
|
|
489
|
-
outputPer1M: 18,
|
|
490
|
-
cacheReadPer1M: 0.4
|
|
491
|
-
},
|
|
492
|
-
"gemini-2.5-pro": {
|
|
493
|
-
threshold: 200000,
|
|
494
|
-
inputPer1M: 2.5,
|
|
495
|
-
outputPer1M: 15,
|
|
496
|
-
cacheReadPer1M: 0.25
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
OPENAI_PROMPT_TIERS = {
|
|
500
|
-
"gpt-5.5": {
|
|
501
|
-
threshold: 272000,
|
|
502
|
-
inputMultiplier: 2,
|
|
503
|
-
outputMultiplier: 1.5,
|
|
504
|
-
cacheReadMultiplier: 2
|
|
505
|
-
},
|
|
506
|
-
"gpt-5.4-pro": {
|
|
507
|
-
threshold: 272000,
|
|
508
|
-
inputMultiplier: 2,
|
|
509
|
-
outputMultiplier: 1.5,
|
|
510
|
-
cacheReadMultiplier: 2
|
|
511
|
-
},
|
|
512
|
-
"gpt-5.4": {
|
|
513
|
-
threshold: 272000,
|
|
514
|
-
inputMultiplier: 2,
|
|
515
|
-
outputMultiplier: 1.5,
|
|
516
|
-
cacheReadMultiplier: 2
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
QWEN_PROMPT_TIERS = {
|
|
520
|
-
"qwen3.6-plus": {
|
|
521
|
-
threshold: 256000,
|
|
522
|
-
inputPer1M: 1.3,
|
|
523
|
-
outputPer1M: 3.9,
|
|
524
|
-
cacheReadPer1M: 0.13,
|
|
525
|
-
cacheWritePer1M: 1.625
|
|
526
|
-
},
|
|
527
|
-
"qwen3.6-flash": {
|
|
528
|
-
threshold: 256000,
|
|
529
|
-
inputPer1M: 1,
|
|
530
|
-
outputPer1M: 4,
|
|
531
|
-
cacheReadPer1M: 0.1,
|
|
532
|
-
cacheWritePer1M: 1.25
|
|
533
|
-
},
|
|
534
|
-
"qwen3.6-max-preview": {
|
|
535
|
-
threshold: 128000,
|
|
536
|
-
inputPer1M: 1.6,
|
|
537
|
-
outputPer1M: 9.6,
|
|
538
|
-
cacheReadPer1M: 0.16,
|
|
539
|
-
cacheWritePer1M: 2
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
MINIMAX_PROMPT_TIERS = {
|
|
543
|
-
"minimax/minimax-m1": {
|
|
544
|
-
threshold: Number.POSITIVE_INFINITY
|
|
545
|
-
},
|
|
546
|
-
"minimax-m1": {
|
|
547
|
-
threshold: 200000,
|
|
548
|
-
inputPer1M: 1.3
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
XAI_PROMPT_TIERS = {
|
|
552
|
-
"grok-4.3": {
|
|
553
|
-
threshold: 200000,
|
|
554
|
-
inputPer1M: 2.5,
|
|
555
|
-
outputPer1M: 5,
|
|
556
|
-
cacheReadPer1M: 0.4
|
|
557
|
-
},
|
|
558
|
-
"grok-latest": {
|
|
559
|
-
threshold: 200000,
|
|
560
|
-
inputPer1M: 2.5,
|
|
561
|
-
outputPer1M: 5,
|
|
562
|
-
cacheReadPer1M: 0.4
|
|
563
|
-
},
|
|
564
|
-
"grok-4.20": {
|
|
565
|
-
threshold: 200000,
|
|
566
|
-
inputPer1M: 2.5,
|
|
567
|
-
outputPer1M: 5,
|
|
568
|
-
cacheReadPer1M: 0.4
|
|
569
|
-
},
|
|
570
|
-
"grok-4-1-fast": {
|
|
571
|
-
threshold: 128000,
|
|
572
|
-
inputPer1M: 0.4,
|
|
573
|
-
outputPer1M: 1,
|
|
574
|
-
cacheReadPer1M: 0
|
|
575
|
-
},
|
|
576
|
-
"grok-4-fast": {
|
|
577
|
-
threshold: 128000,
|
|
578
|
-
inputPer1M: 0.4,
|
|
579
|
-
outputPer1M: 1,
|
|
580
|
-
cacheReadPer1M: 0
|
|
581
|
-
},
|
|
582
|
-
"grok-4": {
|
|
583
|
-
threshold: 128000,
|
|
584
|
-
inputPer1M: 6,
|
|
585
|
-
outputPer1M: 30,
|
|
586
|
-
cacheReadPer1M: 0
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
// src/db/database.ts
|
|
592
|
-
import { execFileSync } from "child_process";
|
|
593
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
594
|
-
import { hostname, platform } from "os";
|
|
595
|
-
import { homedir } from "os";
|
|
596
|
-
import { join, resolve } from "path";
|
|
597
|
-
import { dataDir } from "@hasna/paths";
|
|
598
|
-
function normalizeMachineId(value) {
|
|
599
|
-
const id = value?.trim().toLowerCase().split(".")[0];
|
|
600
|
-
return id && id.length > 0 ? id : null;
|
|
601
|
-
}
|
|
602
|
-
function macHostMachineId() {
|
|
603
|
-
if (platform() !== "darwin")
|
|
604
|
-
return null;
|
|
605
|
-
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
606
|
-
try {
|
|
607
|
-
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
608
|
-
encoding: "utf8",
|
|
609
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
610
|
-
timeout: 1000
|
|
611
|
-
});
|
|
612
|
-
const id = normalizeMachineId(value);
|
|
613
|
-
if (id && id !== "mac" && id !== "localhost")
|
|
614
|
-
return id;
|
|
615
|
-
} catch {}
|
|
616
|
-
}
|
|
617
|
-
return null;
|
|
618
|
-
}
|
|
619
|
-
function getMachineId() {
|
|
620
|
-
const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
|
|
621
|
-
if (envMachine)
|
|
622
|
-
return envMachine;
|
|
623
|
-
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
624
|
-
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
625
|
-
return macHostMachineId() ?? hostMachine;
|
|
626
|
-
return hostMachine;
|
|
627
|
-
}
|
|
628
|
-
function getHomeDir() {
|
|
629
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
630
|
-
if (!home)
|
|
631
|
-
throw new Error("Could not resolve the user home directory");
|
|
632
|
-
return home;
|
|
633
|
-
}
|
|
634
|
-
function getResolverDataRoot() {
|
|
635
|
-
return dataDir({
|
|
636
|
-
app: "economy",
|
|
637
|
-
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
function getLegacyDataRoot() {
|
|
641
|
-
return join(getHomeDir(), ".hasna", "economy");
|
|
642
|
-
}
|
|
643
|
-
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
644
|
-
const dataOverride = env.HASNA_DATA_HOME;
|
|
645
|
-
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
646
|
-
return true;
|
|
647
|
-
return existsSync(join(resolved, "economy.db"));
|
|
648
|
-
}
|
|
649
|
-
function getExactDataRoot() {
|
|
650
|
-
const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
|
|
651
|
-
if (dir && dir.trim())
|
|
652
|
-
return dir.trim();
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
function getDataDir() {
|
|
656
|
-
const exact = getExactDataRoot();
|
|
657
|
-
const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
|
|
658
|
-
const oldDir = join(getHomeDir(), ".economy");
|
|
659
|
-
if (existsSync(oldDir) && !existsSync(effective)) {
|
|
660
|
-
mkdirSync(effective, { recursive: true });
|
|
661
|
-
for (const file of readdirSync(oldDir)) {
|
|
662
|
-
const oldPath = join(oldDir, file);
|
|
663
|
-
if (statSync(oldPath).isFile()) {
|
|
664
|
-
copyFileSync(oldPath, join(effective, file));
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
mkdirSync(effective, { recursive: true });
|
|
669
|
-
return effective;
|
|
670
|
-
}
|
|
671
|
-
function getDbPath() {
|
|
672
|
-
if (process.env["HASNA_ECONOMY_DB_PATH"])
|
|
673
|
-
return process.env["HASNA_ECONOMY_DB_PATH"];
|
|
674
|
-
if (process.env["ECONOMY_DB"])
|
|
675
|
-
return process.env["ECONOMY_DB"];
|
|
676
|
-
return join(getDataDir(), "economy.db");
|
|
677
|
-
}
|
|
678
|
-
function isSqliteBusyError(error) {
|
|
679
|
-
const candidate = error;
|
|
680
|
-
const code = typeof candidate.code === "string" ? candidate.code : "";
|
|
681
|
-
const message = typeof candidate.message === "string" ? candidate.message : String(error);
|
|
682
|
-
return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
|
|
683
|
-
}
|
|
684
|
-
function retryDelayMs(attempt) {
|
|
685
|
-
return Math.min(1000, 50 * 2 ** attempt);
|
|
686
|
-
}
|
|
687
|
-
function withSqliteBusyRetry(operation, context) {
|
|
688
|
-
const maxAttempts = 8;
|
|
689
|
-
let lastError;
|
|
690
|
-
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
691
|
-
try {
|
|
692
|
-
return operation();
|
|
693
|
-
} catch (error) {
|
|
694
|
-
if (!isSqliteBusyError(error))
|
|
695
|
-
throw error;
|
|
696
|
-
lastError = error;
|
|
697
|
-
if (attempt === maxAttempts - 1)
|
|
698
|
-
break;
|
|
699
|
-
Bun.sleepSync(retryDelayMs(attempt));
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
|
|
703
|
-
}
|
|
704
|
-
function openDatabase(dbPath, skipSeed = false) {
|
|
705
|
-
const path = dbPath ?? getDbPath();
|
|
706
|
-
if (path !== ":memory:") {
|
|
707
|
-
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
708
|
-
if (dir && !existsSync(dir))
|
|
709
|
-
mkdirSync(dir, { recursive: true });
|
|
710
|
-
}
|
|
711
|
-
const db = withSqliteBusyRetry(() => {
|
|
712
|
-
const opened = new SqliteAdapter(path);
|
|
713
|
-
try {
|
|
714
|
-
opened.exec("PRAGMA busy_timeout = 10000");
|
|
715
|
-
opened.exec("PRAGMA journal_mode = WAL");
|
|
716
|
-
opened.exec("PRAGMA foreign_keys = ON");
|
|
717
|
-
initSchema(opened);
|
|
718
|
-
return opened;
|
|
719
|
-
} catch (error) {
|
|
720
|
-
try {
|
|
721
|
-
opened.close();
|
|
722
|
-
} catch {}
|
|
723
|
-
throw error;
|
|
724
|
-
}
|
|
725
|
-
}, `opening ${path}`);
|
|
726
|
-
if (!skipSeed) {
|
|
727
|
-
Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
|
|
728
|
-
}
|
|
729
|
-
return db;
|
|
730
|
-
}
|
|
731
|
-
function quoteSqlIdent(identifier) {
|
|
732
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
733
|
-
}
|
|
734
|
-
function hasColumn(db, table, column) {
|
|
735
|
-
const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
|
|
736
|
-
return columns.some((c) => c.name === column);
|
|
737
|
-
}
|
|
738
|
-
function addColumnIfMissing(db, table, column, definition) {
|
|
739
|
-
if (hasColumn(db, table, column))
|
|
740
|
-
return false;
|
|
741
|
-
try {
|
|
742
|
-
db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
|
|
743
|
-
return true;
|
|
744
|
-
} catch (error) {
|
|
745
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
746
|
-
if (/duplicate column name/i.test(message))
|
|
747
|
-
return true;
|
|
748
|
-
throw error;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
function initSchema(db) {
|
|
752
|
-
db.exec(`
|
|
753
|
-
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
754
|
-
id TEXT PRIMARY KEY,
|
|
755
|
-
kind TEXT NOT NULL,
|
|
756
|
-
name TEXT NOT NULL,
|
|
757
|
-
repo_path TEXT,
|
|
758
|
-
labels_json TEXT DEFAULT '{}',
|
|
759
|
-
created_at TEXT NOT NULL,
|
|
760
|
-
updated_at TEXT NOT NULL
|
|
761
|
-
);
|
|
762
|
-
|
|
763
|
-
CREATE TABLE IF NOT EXISTS requests (
|
|
764
|
-
id TEXT PRIMARY KEY,
|
|
765
|
-
agent TEXT NOT NULL,
|
|
766
|
-
session_id TEXT NOT NULL,
|
|
767
|
-
model TEXT NOT NULL,
|
|
768
|
-
input_tokens INTEGER DEFAULT 0,
|
|
769
|
-
output_tokens INTEGER DEFAULT 0,
|
|
770
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
771
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
772
|
-
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
773
|
-
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
774
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
775
|
-
duration_ms INTEGER DEFAULT 0,
|
|
776
|
-
timestamp TEXT NOT NULL,
|
|
777
|
-
source_request_id TEXT,
|
|
778
|
-
machine_id TEXT DEFAULT '',
|
|
779
|
-
cost_center_id TEXT,
|
|
780
|
-
account_key TEXT DEFAULT '',
|
|
781
|
-
account_tool TEXT DEFAULT '',
|
|
782
|
-
account_name TEXT DEFAULT '',
|
|
783
|
-
account_email TEXT DEFAULT '',
|
|
784
|
-
account_source TEXT DEFAULT ''
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
788
|
-
id TEXT PRIMARY KEY,
|
|
789
|
-
agent TEXT NOT NULL,
|
|
790
|
-
project_path TEXT DEFAULT '',
|
|
791
|
-
project_name TEXT DEFAULT '',
|
|
792
|
-
started_at TEXT NOT NULL,
|
|
793
|
-
ended_at TEXT,
|
|
794
|
-
total_cost_usd REAL DEFAULT 0,
|
|
795
|
-
total_tokens INTEGER DEFAULT 0,
|
|
796
|
-
request_count INTEGER DEFAULT 0,
|
|
797
|
-
machine_id TEXT DEFAULT '',
|
|
798
|
-
cost_center_id TEXT,
|
|
799
|
-
account_key TEXT DEFAULT '',
|
|
800
|
-
account_tool TEXT DEFAULT '',
|
|
801
|
-
account_name TEXT DEFAULT '',
|
|
802
|
-
account_email TEXT DEFAULT '',
|
|
803
|
-
account_source TEXT DEFAULT ''
|
|
804
|
-
);
|
|
805
|
-
|
|
806
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
807
|
-
id TEXT PRIMARY KEY,
|
|
808
|
-
path TEXT UNIQUE NOT NULL,
|
|
809
|
-
name TEXT NOT NULL,
|
|
810
|
-
description TEXT,
|
|
811
|
-
tags TEXT DEFAULT '[]',
|
|
812
|
-
created_at TEXT NOT NULL
|
|
813
|
-
);
|
|
814
|
-
|
|
815
|
-
CREATE TABLE IF NOT EXISTS budgets (
|
|
816
|
-
id TEXT PRIMARY KEY,
|
|
817
|
-
project_path TEXT,
|
|
818
|
-
agent TEXT,
|
|
819
|
-
cost_center_id TEXT,
|
|
820
|
-
period TEXT NOT NULL,
|
|
821
|
-
limit_usd REAL NOT NULL,
|
|
822
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
823
|
-
created_at TEXT NOT NULL,
|
|
824
|
-
updated_at TEXT NOT NULL
|
|
825
|
-
);
|
|
826
|
-
|
|
827
|
-
CREATE TABLE IF NOT EXISTS goals (
|
|
828
|
-
id TEXT PRIMARY KEY,
|
|
829
|
-
period TEXT NOT NULL,
|
|
830
|
-
project_path TEXT,
|
|
831
|
-
agent TEXT,
|
|
832
|
-
limit_usd REAL NOT NULL,
|
|
833
|
-
created_at TEXT NOT NULL,
|
|
834
|
-
updated_at TEXT NOT NULL
|
|
835
|
-
);
|
|
836
|
-
|
|
837
|
-
CREATE TABLE IF NOT EXISTS ingest_state (
|
|
838
|
-
source TEXT NOT NULL,
|
|
839
|
-
key TEXT NOT NULL,
|
|
840
|
-
value TEXT NOT NULL,
|
|
841
|
-
PRIMARY KEY (source, key)
|
|
842
|
-
);
|
|
843
|
-
|
|
844
|
-
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
|
|
845
|
-
CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
|
|
846
|
-
CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
|
|
847
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
848
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
849
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
850
|
-
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
851
|
-
|
|
852
|
-
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
853
|
-
model TEXT PRIMARY KEY,
|
|
854
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
855
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
856
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
857
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
858
|
-
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
859
|
-
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
860
|
-
updated_at TEXT NOT NULL
|
|
861
|
-
);
|
|
862
|
-
|
|
863
|
-
CREATE TABLE IF NOT EXISTS feedback (
|
|
864
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
865
|
-
message TEXT NOT NULL,
|
|
866
|
-
email TEXT,
|
|
867
|
-
category TEXT DEFAULT 'general',
|
|
868
|
-
version TEXT,
|
|
869
|
-
machine_id TEXT,
|
|
870
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
871
|
-
);
|
|
872
|
-
|
|
873
|
-
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
874
|
-
date TEXT NOT NULL,
|
|
875
|
-
provider TEXT NOT NULL,
|
|
876
|
-
description TEXT DEFAULT '',
|
|
877
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
878
|
-
updated_at TEXT NOT NULL,
|
|
879
|
-
PRIMARY KEY (date, provider, description)
|
|
880
|
-
);
|
|
881
|
-
|
|
882
|
-
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
883
|
-
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
884
|
-
|
|
885
|
-
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
886
|
-
id TEXT PRIMARY KEY,
|
|
887
|
-
agent TEXT,
|
|
888
|
-
provider TEXT NOT NULL,
|
|
889
|
-
plan TEXT NOT NULL,
|
|
890
|
-
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
891
|
-
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
892
|
-
billing_cycle_start TEXT,
|
|
893
|
-
reset_policy TEXT DEFAULT 'monthly',
|
|
894
|
-
active INTEGER NOT NULL DEFAULT 1,
|
|
895
|
-
created_at TEXT NOT NULL,
|
|
896
|
-
updated_at TEXT NOT NULL
|
|
897
|
-
);
|
|
898
|
-
|
|
899
|
-
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
900
|
-
id TEXT PRIMARY KEY,
|
|
901
|
-
agent TEXT NOT NULL,
|
|
902
|
-
date TEXT NOT NULL,
|
|
903
|
-
metric TEXT NOT NULL,
|
|
904
|
-
value REAL NOT NULL DEFAULT 0,
|
|
905
|
-
unit TEXT DEFAULT '',
|
|
906
|
-
machine_id TEXT DEFAULT '',
|
|
907
|
-
updated_at TEXT NOT NULL
|
|
908
|
-
);
|
|
909
|
-
|
|
910
|
-
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
911
|
-
date TEXT NOT NULL,
|
|
912
|
-
agent TEXT DEFAULT '',
|
|
913
|
-
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
914
|
-
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
915
|
-
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
916
|
-
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
917
|
-
saved_usd REAL NOT NULL DEFAULT 0,
|
|
918
|
-
updated_at TEXT NOT NULL,
|
|
919
|
-
PRIMARY KEY (date, agent)
|
|
920
|
-
);
|
|
921
|
-
|
|
922
|
-
CREATE TABLE IF NOT EXISTS machines (
|
|
923
|
-
machine_id TEXT PRIMARY KEY,
|
|
924
|
-
hostname TEXT NOT NULL,
|
|
925
|
-
last_seen_at TEXT,
|
|
926
|
-
last_push_at TEXT,
|
|
927
|
-
last_pull_at TEXT,
|
|
928
|
-
economy_version TEXT,
|
|
929
|
-
updated_at TEXT NOT NULL
|
|
930
|
-
);
|
|
931
|
-
|
|
932
|
-
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
933
|
-
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
934
|
-
`);
|
|
935
|
-
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
936
|
-
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
937
|
-
if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
|
|
938
|
-
db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
939
|
-
}
|
|
940
|
-
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
941
|
-
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
942
|
-
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
943
|
-
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
944
|
-
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
945
|
-
}
|
|
946
|
-
addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
|
|
947
|
-
addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
|
|
948
|
-
addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
|
|
949
|
-
if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
|
|
950
|
-
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
951
|
-
}
|
|
952
|
-
addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
|
|
953
|
-
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
954
|
-
addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
|
|
955
|
-
}
|
|
956
|
-
addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
|
|
957
|
-
if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
|
|
958
|
-
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
959
|
-
}
|
|
960
|
-
addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
|
|
961
|
-
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
962
|
-
addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
|
|
963
|
-
}
|
|
964
|
-
addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
|
|
965
|
-
addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
|
|
966
|
-
db.exec(`
|
|
967
|
-
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
968
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
969
|
-
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
970
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
971
|
-
CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
|
|
972
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
|
|
973
|
-
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
974
|
-
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
975
|
-
`);
|
|
976
|
-
}
|
|
977
|
-
function periodWhere(period) {
|
|
978
|
-
switch (period) {
|
|
979
|
-
case "today":
|
|
980
|
-
return `DATE(timestamp) = DATE('now')`;
|
|
981
|
-
case "yesterday":
|
|
982
|
-
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
983
|
-
case "week":
|
|
984
|
-
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
985
|
-
case "month":
|
|
986
|
-
return `timestamp >= DATE('now', 'start of month')`;
|
|
987
|
-
case "year":
|
|
988
|
-
return `timestamp >= DATE('now', 'start of year')`;
|
|
989
|
-
case "all":
|
|
990
|
-
return "1=1";
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
function sessionPeriodWhere(period) {
|
|
994
|
-
switch (period) {
|
|
995
|
-
case "today":
|
|
996
|
-
return `DATE(started_at) = DATE('now')`;
|
|
997
|
-
case "yesterday":
|
|
998
|
-
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
999
|
-
case "week":
|
|
1000
|
-
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1001
|
-
case "month":
|
|
1002
|
-
return `started_at >= DATE('now', 'start of month')`;
|
|
1003
|
-
case "year":
|
|
1004
|
-
return `started_at >= DATE('now', 'start of year')`;
|
|
1005
|
-
case "all":
|
|
1006
|
-
return "1=1";
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
function requestPeriodWhere(period) {
|
|
1010
|
-
switch (period) {
|
|
1011
|
-
case "today":
|
|
1012
|
-
return `DATE(timestamp) = DATE('now')`;
|
|
1013
|
-
case "yesterday":
|
|
1014
|
-
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
1015
|
-
case "week":
|
|
1016
|
-
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1017
|
-
case "month":
|
|
1018
|
-
return `timestamp >= DATE('now', 'start of month')`;
|
|
1019
|
-
case "year":
|
|
1020
|
-
return `timestamp >= DATE('now', 'start of year')`;
|
|
1021
|
-
case "all":
|
|
1022
|
-
return "1=1";
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function querySessions(db, filter = {}) {
|
|
1026
|
-
const conditions = [];
|
|
1027
|
-
const params = [];
|
|
1028
|
-
if (filter.agent) {
|
|
1029
|
-
conditions.push("agent = ?");
|
|
1030
|
-
params.push(filter.agent);
|
|
1031
|
-
}
|
|
1032
|
-
if (filter.project) {
|
|
1033
|
-
conditions.push("project_path LIKE ?");
|
|
1034
|
-
params.push(`%${filter.project}%`);
|
|
1035
|
-
}
|
|
1036
|
-
if (filter.account) {
|
|
1037
|
-
const q = `%${filter.account}%`;
|
|
1038
|
-
conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
|
|
1039
|
-
params.push(q, q, q);
|
|
1040
|
-
}
|
|
1041
|
-
if (filter.since) {
|
|
1042
|
-
conditions.push("started_at >= ?");
|
|
1043
|
-
params.push(filter.since);
|
|
1044
|
-
}
|
|
1045
|
-
if (filter.machine) {
|
|
1046
|
-
conditions.push("machine_id = ?");
|
|
1047
|
-
params.push(filter.machine);
|
|
1048
|
-
}
|
|
1049
|
-
if (filter.search) {
|
|
1050
|
-
const q = `%${filter.search}%`;
|
|
1051
|
-
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
1052
|
-
params.push(q, q, `${filter.search}%`);
|
|
1053
|
-
}
|
|
1054
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1055
|
-
const limit = filter.limit ?? 50;
|
|
1056
|
-
const offset = filter.offset ?? 0;
|
|
1057
|
-
return db.prepare(`
|
|
1058
|
-
SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
1059
|
-
`).all(...params, limit, offset);
|
|
1060
|
-
}
|
|
1061
|
-
function queryTopSessions(db, n = 10, agent, since) {
|
|
1062
|
-
const conditions = [];
|
|
1063
|
-
const params = [];
|
|
1064
|
-
if (agent) {
|
|
1065
|
-
conditions.push("agent = ?");
|
|
1066
|
-
params.push(agent);
|
|
1067
|
-
}
|
|
1068
|
-
if (since) {
|
|
1069
|
-
conditions.push("started_at >= ?");
|
|
1070
|
-
params.push(since);
|
|
1071
|
-
}
|
|
1072
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1073
|
-
return db.prepare(`SELECT * FROM sessions ${where} ORDER BY total_cost_usd DESC LIMIT ?`).all(...params, n);
|
|
1074
|
-
}
|
|
1075
|
-
function querySummary(db, period, machine, allMachines = false, agent) {
|
|
1076
|
-
const rWhere = periodWhere(period);
|
|
1077
|
-
const sWhere = sessionPeriodWhere(period);
|
|
1078
|
-
const machineClause = !allMachines && machine ? " AND machine_id = ?" : "";
|
|
1079
|
-
const agentClause = agent ? " AND agent = ?" : "";
|
|
1080
|
-
const params = [];
|
|
1081
|
-
if (!allMachines && machine)
|
|
1082
|
-
params.push(machine);
|
|
1083
|
-
if (agent)
|
|
1084
|
-
params.push(agent);
|
|
1085
|
-
const r = db.prepare(`
|
|
1086
|
-
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
1087
|
-
COUNT(*) as requests,
|
|
1088
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
1089
|
-
FROM requests WHERE ${rWhere}${machineClause}${agentClause}
|
|
1090
|
-
`).get(...params);
|
|
1091
|
-
const codexTotals = db.prepare(`
|
|
1092
|
-
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1093
|
-
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
1094
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1095
|
-
COUNT(*) as sessions
|
|
1096
|
-
FROM sessions
|
|
1097
|
-
WHERE ${sWhere}${machineClause}${agentClause}
|
|
1098
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1099
|
-
`).get(...params);
|
|
1100
|
-
const requestSessionCount = db.prepare(`
|
|
1101
|
-
SELECT COUNT(DISTINCT session_id) as sessions
|
|
1102
|
-
FROM requests
|
|
1103
|
-
WHERE ${rWhere}${machineClause}${agentClause}
|
|
1104
|
-
`).get(...params);
|
|
1105
|
-
return {
|
|
1106
|
-
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
1107
|
-
requests: r.requests + codexTotals.requests,
|
|
1108
|
-
tokens: r.tokens + codexTotals.tokens,
|
|
1109
|
-
sessions: requestSessionCount.sessions + codexTotals.sessions,
|
|
1110
|
-
period
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
function queryModelBreakdown(db) {
|
|
1114
|
-
return db.prepare(`
|
|
1115
|
-
SELECT model, agent,
|
|
1116
|
-
COUNT(*) as requests,
|
|
1117
|
-
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
1118
|
-
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
1119
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1120
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1121
|
-
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
1122
|
-
`).all();
|
|
1123
|
-
}
|
|
1124
|
-
function queryAgentBreakdown(db, period = "all", machine) {
|
|
1125
|
-
const requestWhere = requestPeriodWhere(period);
|
|
1126
|
-
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1127
|
-
const machineParams = machine ? [machine] : [];
|
|
1128
|
-
const groups = new Map;
|
|
1129
|
-
const requestRows = db.prepare(`
|
|
1130
|
-
SELECT agent,
|
|
1131
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1132
|
-
COUNT(*) as requests,
|
|
1133
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1134
|
-
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1135
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1136
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1137
|
-
COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1138
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
1139
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1140
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1141
|
-
MAX(timestamp) as last_active
|
|
1142
|
-
FROM requests
|
|
1143
|
-
WHERE ${requestWhere}${machineClause}
|
|
1144
|
-
GROUP BY agent
|
|
1145
|
-
ORDER BY api_equivalent_usd DESC
|
|
1146
|
-
`).all(...machineParams);
|
|
1147
|
-
for (const row of requestRows) {
|
|
1148
|
-
groups.set(row.agent, row);
|
|
1149
|
-
}
|
|
1150
|
-
const sessionWhere = sessionPeriodWhere(period);
|
|
1151
|
-
const sessionOnlyRows = db.prepare(`
|
|
1152
|
-
SELECT agent,
|
|
1153
|
-
COUNT(*) as sessions,
|
|
1154
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1155
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1156
|
-
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1157
|
-
MAX(started_at) as last_active
|
|
1158
|
-
FROM sessions
|
|
1159
|
-
WHERE ${sessionWhere}${machineClause}
|
|
1160
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1161
|
-
GROUP BY agent
|
|
1162
|
-
`).all(...machineParams);
|
|
1163
|
-
for (const row of sessionOnlyRows) {
|
|
1164
|
-
const existing = groups.get(row.agent) ?? {
|
|
1165
|
-
agent: row.agent,
|
|
1166
|
-
sessions: 0,
|
|
1167
|
-
requests: 0,
|
|
1168
|
-
total_tokens: 0,
|
|
1169
|
-
api_equivalent_usd: 0,
|
|
1170
|
-
billable_usd: 0,
|
|
1171
|
-
metered_api_usd: 0,
|
|
1172
|
-
subscription_included_usd: 0,
|
|
1173
|
-
estimated_usd: 0,
|
|
1174
|
-
unknown_usd: 0,
|
|
1175
|
-
cost_usd: 0,
|
|
1176
|
-
last_active: ""
|
|
1177
|
-
};
|
|
1178
|
-
existing.sessions += row.sessions;
|
|
1179
|
-
existing.requests += row.requests;
|
|
1180
|
-
existing.total_tokens += row.total_tokens;
|
|
1181
|
-
existing.api_equivalent_usd += row.cost_usd;
|
|
1182
|
-
existing.estimated_usd += row.cost_usd;
|
|
1183
|
-
existing.cost_usd += row.cost_usd;
|
|
1184
|
-
if (!existing.last_active || row.last_active > existing.last_active)
|
|
1185
|
-
existing.last_active = row.last_active;
|
|
1186
|
-
groups.set(row.agent, existing);
|
|
1187
|
-
}
|
|
1188
|
-
return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
|
|
1189
|
-
}
|
|
1190
|
-
function pathProjectLabel(projectPath) {
|
|
1191
|
-
if (!projectPath)
|
|
1192
|
-
return "";
|
|
1193
|
-
const segments = projectPath.split("/").filter(Boolean);
|
|
1194
|
-
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
1195
|
-
for (const seg of segments) {
|
|
1196
|
-
if (projectPrefix.test(seg))
|
|
1197
|
-
return seg;
|
|
1198
|
-
}
|
|
1199
|
-
const generic = new Set([
|
|
1200
|
-
"web",
|
|
1201
|
-
"app",
|
|
1202
|
-
"apps",
|
|
1203
|
-
"packages",
|
|
1204
|
-
"src",
|
|
1205
|
-
"lib",
|
|
1206
|
-
"server",
|
|
1207
|
-
"client",
|
|
1208
|
-
"api",
|
|
1209
|
-
"frontend",
|
|
1210
|
-
"backend",
|
|
1211
|
-
"home",
|
|
1212
|
-
"users",
|
|
1213
|
-
"workspace",
|
|
1214
|
-
"workspaces",
|
|
1215
|
-
"hasna"
|
|
1216
|
-
]);
|
|
1217
|
-
for (let i = segments.length - 1;i >= 0; i--) {
|
|
1218
|
-
if (!generic.has(segments[i].toLowerCase()))
|
|
1219
|
-
return segments[i];
|
|
1220
|
-
}
|
|
1221
|
-
return null;
|
|
1222
|
-
}
|
|
1223
|
-
function isRepoLikeLabel(label) {
|
|
1224
|
-
return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
|
|
1225
|
-
}
|
|
1226
|
-
function labelForPath(projectPath, projectName) {
|
|
1227
|
-
const pathLabel = pathProjectLabel(projectPath);
|
|
1228
|
-
if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
|
|
1229
|
-
return pathLabel;
|
|
1230
|
-
if (projectName && projectName.trim() !== "")
|
|
1231
|
-
return projectName;
|
|
1232
|
-
if (pathLabel)
|
|
1233
|
-
return pathLabel;
|
|
1234
|
-
return projectPath;
|
|
1235
|
-
}
|
|
1236
|
-
function groupKeyForPath(projectPath, projectName) {
|
|
1237
|
-
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1238
|
-
}
|
|
1239
|
-
function laterTimestamp(current, candidate) {
|
|
1240
|
-
if (!candidate)
|
|
1241
|
-
return current;
|
|
1242
|
-
return candidate > current ? candidate : current;
|
|
1243
|
-
}
|
|
1244
|
-
function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
|
|
1245
|
-
const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
|
|
1246
|
-
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1247
|
-
const sessionMachineParams = machine ? [machine, machine] : [];
|
|
1248
|
-
const requestMachineParams = machine ? [machine] : [];
|
|
1249
|
-
const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1250
|
-
const sessionOnlyMachineParams = machine ? [machine] : [];
|
|
1251
|
-
const sessions = db.prepare(`
|
|
1252
|
-
SELECT id, project_path, project_name
|
|
1253
|
-
FROM sessions
|
|
1254
|
-
WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
|
|
1255
|
-
`).all(...sessionMachineParams);
|
|
1256
|
-
const groups = new Map;
|
|
1257
|
-
const groupKeyBySession = new Map;
|
|
1258
|
-
for (const s of sessions) {
|
|
1259
|
-
const label = labelForPath(s.project_path, s.project_name);
|
|
1260
|
-
if (!label)
|
|
1261
|
-
continue;
|
|
1262
|
-
const key = groupKeyForPath(s.project_path, s.project_name);
|
|
1263
|
-
const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
|
|
1264
|
-
if (!g.samplePath)
|
|
1265
|
-
g.samplePath = s.project_path;
|
|
1266
|
-
groups.set(key, g);
|
|
1267
|
-
groupKeyBySession.set(s.id, key);
|
|
1268
|
-
}
|
|
1269
|
-
const groupFor = (sessionId) => {
|
|
1270
|
-
const key = groupKeyBySession.get(sessionId);
|
|
1271
|
-
return key === undefined ? undefined : groups.get(key);
|
|
1272
|
-
};
|
|
1273
|
-
const requestRows = db.prepare(`
|
|
1274
|
-
SELECT session_id,
|
|
1275
|
-
COUNT(*) as requests,
|
|
1276
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1277
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1278
|
-
MAX(timestamp) as last_active
|
|
1279
|
-
FROM requests
|
|
1280
|
-
WHERE ${requestWhere}${requestMachineClause}
|
|
1281
|
-
GROUP BY session_id
|
|
1282
|
-
`).all(...requestScopeParams, ...requestMachineParams);
|
|
1283
|
-
for (const row of requestRows) {
|
|
1284
|
-
const g = groupFor(row.session_id);
|
|
1285
|
-
if (!g)
|
|
1286
|
-
continue;
|
|
1287
|
-
g.sessions += 1;
|
|
1288
|
-
g.requests += row.requests;
|
|
1289
|
-
g.cost_usd += row.cost_usd;
|
|
1290
|
-
g.total_tokens += row.total_tokens;
|
|
1291
|
-
g.last_active = laterTimestamp(g.last_active, row.last_active);
|
|
1292
|
-
}
|
|
1293
|
-
const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
|
|
1294
|
-
const sessionOnlyRows = db.prepare(`
|
|
1295
|
-
SELECT id,
|
|
1296
|
-
COALESCE(request_count, 0) as request_count,
|
|
1297
|
-
COALESCE(total_tokens, 0) as total_tokens,
|
|
1298
|
-
COALESCE(total_cost_usd, 0) as total_cost_usd,
|
|
1299
|
-
started_at
|
|
1300
|
-
FROM sessions
|
|
1301
|
-
WHERE ${sessionWhere}${sessionOnlyMachineClause}
|
|
1302
|
-
`).all(...sessionScopeParams, ...sessionOnlyMachineParams);
|
|
1303
|
-
for (const row of sessionOnlyRows) {
|
|
1304
|
-
if (sessionsWithRequests.has(row.id))
|
|
1305
|
-
continue;
|
|
1306
|
-
const g = groupFor(row.id);
|
|
1307
|
-
if (!g)
|
|
1308
|
-
continue;
|
|
1309
|
-
g.sessions += 1;
|
|
1310
|
-
g.requests += row.request_count;
|
|
1311
|
-
g.total_tokens += row.total_tokens;
|
|
1312
|
-
g.cost_usd += row.total_cost_usd;
|
|
1313
|
-
g.last_active = laterTimestamp(g.last_active, row.started_at);
|
|
1314
|
-
}
|
|
1315
|
-
const result = [];
|
|
1316
|
-
for (const g of groups.values()) {
|
|
1317
|
-
if (g.sessions === 0)
|
|
1318
|
-
continue;
|
|
1319
|
-
result.push({
|
|
1320
|
-
project_path: g.samplePath,
|
|
1321
|
-
project_name: g.label,
|
|
1322
|
-
sessions: g.sessions,
|
|
1323
|
-
requests: g.requests,
|
|
1324
|
-
total_tokens: g.total_tokens,
|
|
1325
|
-
cost_usd: g.cost_usd,
|
|
1326
|
-
last_active: g.last_active
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1330
|
-
return result;
|
|
1331
|
-
}
|
|
1332
|
-
function queryProjectBreakdown(db, period = "all", machine) {
|
|
1333
|
-
return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
|
|
1334
|
-
}
|
|
1335
|
-
function normalizeAccountEmail(email) {
|
|
1336
|
-
return (email ?? "").trim().toLowerCase();
|
|
1337
|
-
}
|
|
1338
|
-
function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
|
|
1339
|
-
const identityAgent = (agent || "").trim();
|
|
1340
|
-
const normalizedEmail = normalizeAccountEmail(accountEmail);
|
|
1341
|
-
if (identityAgent && normalizedEmail)
|
|
1342
|
-
return `${identityAgent}:${normalizedEmail}`;
|
|
1343
|
-
if (identityAgent && accountName)
|
|
1344
|
-
return `${identityAgent}:${accountName}`;
|
|
1345
|
-
if (accountKey)
|
|
1346
|
-
return accountKey;
|
|
1347
|
-
return identityAgent ? `${identityAgent}:unknown` : "";
|
|
1348
|
-
}
|
|
1349
|
-
function addAccountBreakdownRow(groups, row, sessionOnly) {
|
|
1350
|
-
const agent = row.agent || row.account_tool;
|
|
1351
|
-
const email = normalizeAccountEmail(row.account_email);
|
|
1352
|
-
const accountName = row.account_name || email || row.account_key;
|
|
1353
|
-
const key = accountIdentityKey(agent, row.account_key, accountName, email);
|
|
1354
|
-
if (!key)
|
|
1355
|
-
return;
|
|
1356
|
-
const group = groups.get(key) ?? {
|
|
1357
|
-
account_key: key,
|
|
1358
|
-
account_tool: agent,
|
|
1359
|
-
account_name: accountName,
|
|
1360
|
-
account_email: email || null,
|
|
1361
|
-
account_source: row.account_source || "unknown",
|
|
1362
|
-
sessionIds: new Set,
|
|
1363
|
-
requests: 0,
|
|
1364
|
-
total_tokens: 0,
|
|
1365
|
-
api_equivalent_usd: 0,
|
|
1366
|
-
metered_api_usd: 0,
|
|
1367
|
-
subscription_included_usd: 0,
|
|
1368
|
-
estimated_usd: 0,
|
|
1369
|
-
unknown_usd: 0,
|
|
1370
|
-
last_active: ""
|
|
1371
|
-
};
|
|
1372
|
-
if (!group.account_email && email)
|
|
1373
|
-
group.account_email = email;
|
|
1374
|
-
if (!group.account_name && accountName)
|
|
1375
|
-
group.account_name = accountName;
|
|
1376
|
-
if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
|
|
1377
|
-
group.account_source = row.account_source;
|
|
1378
|
-
}
|
|
1379
|
-
if (row.session_id)
|
|
1380
|
-
group.sessionIds.add(row.session_id);
|
|
1381
|
-
group.requests += row.requests;
|
|
1382
|
-
group.total_tokens += row.total_tokens;
|
|
1383
|
-
group.api_equivalent_usd += row.cost_usd;
|
|
1384
|
-
if (sessionOnly) {
|
|
1385
|
-
group.estimated_usd += row.cost_usd;
|
|
1386
|
-
} else if (row.cost_basis === "metered_api") {
|
|
1387
|
-
group.metered_api_usd += row.cost_usd;
|
|
1388
|
-
} else if (row.cost_basis === "subscription_included") {
|
|
1389
|
-
group.subscription_included_usd += row.cost_usd;
|
|
1390
|
-
} else if (row.cost_basis === "unknown") {
|
|
1391
|
-
group.unknown_usd += row.cost_usd;
|
|
1392
|
-
} else {
|
|
1393
|
-
group.estimated_usd += row.cost_usd;
|
|
1394
|
-
}
|
|
1395
|
-
if (!group.last_active || row.last_active > group.last_active)
|
|
1396
|
-
group.last_active = row.last_active;
|
|
1397
|
-
groups.set(key, group);
|
|
1398
|
-
}
|
|
1399
|
-
function queryAccountBreakdown(db, period = "all", machine) {
|
|
1400
|
-
const requestWhere = requestPeriodWhere(period);
|
|
1401
|
-
const sessionWhere = sessionPeriodWhere(period);
|
|
1402
|
-
const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
|
|
1403
|
-
const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
|
|
1404
|
-
const requestMachineParams = machine ? [machine] : [];
|
|
1405
|
-
const sessionMachineParams = machine ? [machine] : [];
|
|
1406
|
-
const groups = new Map;
|
|
1407
|
-
const requestRows = db.prepare(`
|
|
1408
|
-
SELECT
|
|
1409
|
-
r.session_id as session_id,
|
|
1410
|
-
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
|
|
1411
|
-
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
|
|
1412
|
-
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
|
|
1413
|
-
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
|
|
1414
|
-
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
|
|
1415
|
-
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1416
|
-
1 as requests,
|
|
1417
|
-
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1418
|
-
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1419
|
-
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1420
|
-
r.timestamp as last_active
|
|
1421
|
-
FROM requests r
|
|
1422
|
-
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1423
|
-
WHERE ${requestWhere}${requestMachineClause}
|
|
1424
|
-
AND (
|
|
1425
|
-
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
|
|
1426
|
-
OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
|
|
1427
|
-
OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
|
|
1428
|
-
OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
|
|
1429
|
-
)
|
|
1430
|
-
`).all(...requestMachineParams);
|
|
1431
|
-
for (const row of requestRows)
|
|
1432
|
-
addAccountBreakdownRow(groups, row, false);
|
|
1433
|
-
const sessionOnlyRows = db.prepare(`
|
|
1434
|
-
SELECT
|
|
1435
|
-
s.id as session_id,
|
|
1436
|
-
s.agent as agent,
|
|
1437
|
-
s.account_key as account_key,
|
|
1438
|
-
s.account_tool as account_tool,
|
|
1439
|
-
s.account_name as account_name,
|
|
1440
|
-
s.account_email as account_email,
|
|
1441
|
-
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1442
|
-
COALESCE(s.request_count, 0) as requests,
|
|
1443
|
-
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1444
|
-
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1445
|
-
'estimated' as cost_basis,
|
|
1446
|
-
s.started_at as last_active
|
|
1447
|
-
FROM sessions s
|
|
1448
|
-
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1449
|
-
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1450
|
-
AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
|
|
1451
|
-
`).all(...sessionMachineParams);
|
|
1452
|
-
for (const row of sessionOnlyRows)
|
|
1453
|
-
addAccountBreakdownRow(groups, row, true);
|
|
1454
|
-
const result = [...groups.values()].map((group) => ({
|
|
1455
|
-
account_key: group.account_key,
|
|
1456
|
-
account_tool: group.account_tool,
|
|
1457
|
-
account_name: group.account_name,
|
|
1458
|
-
account_email: group.account_email,
|
|
1459
|
-
account_source: group.account_source,
|
|
1460
|
-
sessions: group.sessionIds.size,
|
|
1461
|
-
requests: group.requests,
|
|
1462
|
-
total_tokens: group.total_tokens,
|
|
1463
|
-
api_equivalent_usd: group.api_equivalent_usd,
|
|
1464
|
-
billable_usd: group.metered_api_usd,
|
|
1465
|
-
metered_api_usd: group.metered_api_usd,
|
|
1466
|
-
subscription_included_usd: group.subscription_included_usd,
|
|
1467
|
-
estimated_usd: group.estimated_usd,
|
|
1468
|
-
unknown_usd: group.unknown_usd,
|
|
1469
|
-
cost_usd: group.api_equivalent_usd,
|
|
1470
|
-
last_active: group.last_active
|
|
1471
|
-
}));
|
|
1472
|
-
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1473
|
-
return result;
|
|
1474
|
-
}
|
|
1475
|
-
function kindFromCostCenterId(id) {
|
|
1476
|
-
const prefix = id.split(":", 1)[0] ?? "";
|
|
1477
|
-
return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
|
|
1478
|
-
}
|
|
1479
|
-
function addCostCenterBreakdownRow(groups, row, sessionOnly) {
|
|
1480
|
-
if (!row.cost_center_id)
|
|
1481
|
-
return;
|
|
1482
|
-
const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
|
|
1483
|
-
const group = groups.get(row.cost_center_id) ?? {
|
|
1484
|
-
cost_center_id: row.cost_center_id,
|
|
1485
|
-
kind: derivedKind,
|
|
1486
|
-
name: row.name || row.cost_center_id,
|
|
1487
|
-
repo_path: row.repo_path ?? null,
|
|
1488
|
-
labels_json: row.labels_json || "{}",
|
|
1489
|
-
sessionIds: new Set,
|
|
1490
|
-
requests: 0,
|
|
1491
|
-
total_tokens: 0,
|
|
1492
|
-
api_equivalent_usd: 0,
|
|
1493
|
-
metered_api_usd: 0,
|
|
1494
|
-
subscription_included_usd: 0,
|
|
1495
|
-
estimated_usd: 0,
|
|
1496
|
-
unknown_usd: 0,
|
|
1497
|
-
last_active: ""
|
|
1498
|
-
};
|
|
1499
|
-
group.kind = derivedKind;
|
|
1500
|
-
if (row.name)
|
|
1501
|
-
group.name = row.name;
|
|
1502
|
-
if (row.repo_path && !group.repo_path)
|
|
1503
|
-
group.repo_path = row.repo_path;
|
|
1504
|
-
if (row.labels_json && row.labels_json !== "{}")
|
|
1505
|
-
group.labels_json = row.labels_json;
|
|
1506
|
-
if (row.session_id)
|
|
1507
|
-
group.sessionIds.add(row.session_id);
|
|
1508
|
-
group.requests += row.requests;
|
|
1509
|
-
group.total_tokens += row.total_tokens;
|
|
1510
|
-
group.api_equivalent_usd += row.cost_usd;
|
|
1511
|
-
if (sessionOnly) {
|
|
1512
|
-
group.estimated_usd += row.cost_usd;
|
|
1513
|
-
} else if (row.cost_basis === "metered_api") {
|
|
1514
|
-
group.metered_api_usd += row.cost_usd;
|
|
1515
|
-
} else if (row.cost_basis === "subscription_included") {
|
|
1516
|
-
group.subscription_included_usd += row.cost_usd;
|
|
1517
|
-
} else if (row.cost_basis === "unknown") {
|
|
1518
|
-
group.unknown_usd += row.cost_usd;
|
|
1519
|
-
} else {
|
|
1520
|
-
group.estimated_usd += row.cost_usd;
|
|
1521
|
-
}
|
|
1522
|
-
if (!group.last_active || row.last_active > group.last_active)
|
|
1523
|
-
group.last_active = row.last_active;
|
|
1524
|
-
groups.set(row.cost_center_id, group);
|
|
1525
|
-
}
|
|
1526
|
-
function queryCostCenterBreakdown(db, period = "all", filter = {}) {
|
|
1527
|
-
const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
|
|
1528
|
-
const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
|
|
1529
|
-
const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
|
|
1530
|
-
const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
|
|
1531
|
-
const requestParams = [];
|
|
1532
|
-
const sessionParams = [];
|
|
1533
|
-
if (filter.since) {
|
|
1534
|
-
requestParams.push(filter.since);
|
|
1535
|
-
sessionParams.push(filter.since);
|
|
1536
|
-
}
|
|
1537
|
-
if (filter.machine) {
|
|
1538
|
-
requestParams.push(filter.machine);
|
|
1539
|
-
sessionParams.push(filter.machine);
|
|
1540
|
-
}
|
|
1541
|
-
const groups = new Map;
|
|
1542
|
-
const requestRows = db.prepare(`
|
|
1543
|
-
SELECT
|
|
1544
|
-
COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
|
|
1545
|
-
COALESCE(cc.kind, '') as kind,
|
|
1546
|
-
COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
|
|
1547
|
-
cc.repo_path as repo_path,
|
|
1548
|
-
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1549
|
-
r.session_id as session_id,
|
|
1550
|
-
1 as requests,
|
|
1551
|
-
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1552
|
-
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1553
|
-
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1554
|
-
r.timestamp as last_active
|
|
1555
|
-
FROM requests r
|
|
1556
|
-
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1557
|
-
LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
|
|
1558
|
-
WHERE ${requestWhere}${requestMachineClause}
|
|
1559
|
-
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
|
|
1560
|
-
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
|
|
1561
|
-
`).all(...requestParams);
|
|
1562
|
-
for (const row of requestRows)
|
|
1563
|
-
addCostCenterBreakdownRow(groups, row, false);
|
|
1564
|
-
const sessionOnlyRows = db.prepare(`
|
|
1565
|
-
SELECT
|
|
1566
|
-
s.cost_center_id as cost_center_id,
|
|
1567
|
-
COALESCE(cc.kind, '') as kind,
|
|
1568
|
-
COALESCE(cc.name, s.cost_center_id) as name,
|
|
1569
|
-
cc.repo_path as repo_path,
|
|
1570
|
-
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1571
|
-
s.id as session_id,
|
|
1572
|
-
COALESCE(s.request_count, 0) as requests,
|
|
1573
|
-
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1574
|
-
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1575
|
-
'estimated' as cost_basis,
|
|
1576
|
-
s.started_at as last_active
|
|
1577
|
-
FROM sessions s
|
|
1578
|
-
LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
|
|
1579
|
-
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1580
|
-
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1581
|
-
AND s.cost_center_id IS NOT NULL
|
|
1582
|
-
AND s.cost_center_id != ''
|
|
1583
|
-
`).all(...sessionParams);
|
|
1584
|
-
for (const row of sessionOnlyRows)
|
|
1585
|
-
addCostCenterBreakdownRow(groups, row, true);
|
|
1586
|
-
const result = [...groups.values()].map((group) => ({
|
|
1587
|
-
cost_center_id: group.cost_center_id,
|
|
1588
|
-
kind: group.kind,
|
|
1589
|
-
name: group.name,
|
|
1590
|
-
repo_path: group.repo_path,
|
|
1591
|
-
labels_json: group.labels_json,
|
|
1592
|
-
sessions: group.sessionIds.size,
|
|
1593
|
-
requests: group.requests,
|
|
1594
|
-
total_tokens: group.total_tokens,
|
|
1595
|
-
api_equivalent_usd: group.api_equivalent_usd,
|
|
1596
|
-
billable_usd: group.metered_api_usd,
|
|
1597
|
-
metered_api_usd: group.metered_api_usd,
|
|
1598
|
-
subscription_included_usd: group.subscription_included_usd,
|
|
1599
|
-
estimated_usd: group.estimated_usd,
|
|
1600
|
-
unknown_usd: group.unknown_usd,
|
|
1601
|
-
cost_usd: group.api_equivalent_usd,
|
|
1602
|
-
last_active: group.last_active
|
|
1603
|
-
}));
|
|
1604
|
-
const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
|
|
1605
|
-
filtered.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1606
|
-
return filtered;
|
|
1607
|
-
}
|
|
1608
|
-
function queryProjectBreakdownSince(db, since, machine) {
|
|
1609
|
-
return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
|
|
1610
|
-
}
|
|
1611
|
-
function queryAgentBreakdownSince(db, since) {
|
|
1612
|
-
return db.prepare(`
|
|
1613
|
-
SELECT agent,
|
|
1614
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1615
|
-
COUNT(*) as requests,
|
|
1616
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1617
|
-
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1618
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1619
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1620
|
-
MAX(timestamp) as last_active
|
|
1621
|
-
FROM requests
|
|
1622
|
-
WHERE timestamp >= ?
|
|
1623
|
-
GROUP BY agent
|
|
1624
|
-
ORDER BY api_equivalent_usd DESC
|
|
1625
|
-
`).all(since);
|
|
1626
|
-
}
|
|
1627
|
-
function queryModelBreakdownSince(db, since) {
|
|
1628
|
-
return db.prepare(`
|
|
1629
|
-
SELECT model, agent,
|
|
1630
|
-
COUNT(*) as requests,
|
|
1631
|
-
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
1632
|
-
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
1633
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1634
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1635
|
-
FROM requests WHERE timestamp >= ?
|
|
1636
|
-
GROUP BY model, agent ORDER BY cost_usd DESC
|
|
1637
|
-
`).all(since);
|
|
1638
|
-
}
|
|
1639
|
-
function queryAccountBreakdownSince(db, since) {
|
|
1640
|
-
return db.prepare(`
|
|
1641
|
-
WITH request_rows AS (
|
|
1642
|
-
SELECT
|
|
1643
|
-
r.session_id as session_id,
|
|
1644
|
-
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
|
|
1645
|
-
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
|
|
1646
|
-
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
|
|
1647
|
-
LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
|
|
1648
|
-
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1649
|
-
1 as requests,
|
|
1650
|
-
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1651
|
-
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1652
|
-
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1653
|
-
r.timestamp as last_active
|
|
1654
|
-
FROM requests r
|
|
1655
|
-
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1656
|
-
WHERE r.timestamp >= ?
|
|
1657
|
-
AND (
|
|
1658
|
-
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
|
|
1659
|
-
OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
|
|
1660
|
-
OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
|
|
1661
|
-
OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
|
|
1662
|
-
)
|
|
1663
|
-
),
|
|
1664
|
-
session_only_rows AS (
|
|
1665
|
-
SELECT
|
|
1666
|
-
s.id as session_id,
|
|
1667
|
-
COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
|
|
1668
|
-
s.account_key as raw_account_key,
|
|
1669
|
-
s.account_name as raw_account_name,
|
|
1670
|
-
LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
|
|
1671
|
-
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1672
|
-
COALESCE(s.request_count, 0) as requests,
|
|
1673
|
-
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1674
|
-
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1675
|
-
'estimated' as cost_basis,
|
|
1676
|
-
s.started_at as last_active
|
|
1677
|
-
FROM sessions s
|
|
1678
|
-
WHERE s.started_at >= ?
|
|
1679
|
-
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1680
|
-
AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
|
|
1681
|
-
),
|
|
1682
|
-
normalized AS (
|
|
1683
|
-
SELECT
|
|
1684
|
-
CASE
|
|
1685
|
-
WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
|
|
1686
|
-
WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
|
|
1687
|
-
ELSE raw_account_key
|
|
1688
|
-
END as account_key,
|
|
1689
|
-
account_agent as account_tool,
|
|
1690
|
-
raw_account_name as account_name,
|
|
1691
|
-
raw_account_email as account_email,
|
|
1692
|
-
account_source,
|
|
1693
|
-
session_id,
|
|
1694
|
-
requests,
|
|
1695
|
-
total_tokens,
|
|
1696
|
-
cost_usd,
|
|
1697
|
-
cost_basis,
|
|
1698
|
-
last_active
|
|
1699
|
-
FROM request_rows
|
|
1700
|
-
UNION ALL
|
|
1701
|
-
SELECT
|
|
1702
|
-
CASE
|
|
1703
|
-
WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
|
|
1704
|
-
WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
|
|
1705
|
-
ELSE raw_account_key
|
|
1706
|
-
END as account_key,
|
|
1707
|
-
account_agent as account_tool,
|
|
1708
|
-
raw_account_name as account_name,
|
|
1709
|
-
raw_account_email as account_email,
|
|
1710
|
-
account_source,
|
|
1711
|
-
session_id,
|
|
1712
|
-
requests,
|
|
1713
|
-
total_tokens,
|
|
1714
|
-
cost_usd,
|
|
1715
|
-
cost_basis,
|
|
1716
|
-
last_active
|
|
1717
|
-
FROM session_only_rows
|
|
1718
|
-
)
|
|
1719
|
-
SELECT account_key,
|
|
1720
|
-
account_tool,
|
|
1721
|
-
COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
|
|
1722
|
-
NULLIF(account_email, '') as account_email,
|
|
1723
|
-
COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
|
|
1724
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1725
|
-
COALESCE(SUM(requests), 0) as requests,
|
|
1726
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1727
|
-
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1728
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1729
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1730
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1731
|
-
COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1732
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
1733
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1734
|
-
MAX(last_active) as last_active
|
|
1735
|
-
FROM normalized
|
|
1736
|
-
WHERE account_key != ''
|
|
1737
|
-
GROUP BY account_key, account_tool, account_email
|
|
1738
|
-
ORDER BY api_equivalent_usd DESC
|
|
1739
|
-
`).all(since, since);
|
|
1740
|
-
}
|
|
1741
|
-
function queryDailyBreakdown(db, days = 30, machine) {
|
|
1742
|
-
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1743
|
-
const params = machine ? [`-${days}`, machine] : [`-${days}`];
|
|
1744
|
-
return db.prepare(`
|
|
1745
|
-
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1746
|
-
FROM requests
|
|
1747
|
-
WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
|
|
1748
|
-
GROUP BY DATE(timestamp), agent
|
|
1749
|
-
ORDER BY date ASC
|
|
1750
|
-
`).all(...params);
|
|
1751
|
-
}
|
|
1752
|
-
function upsertProject(db, project) {
|
|
1753
|
-
db.prepare(`
|
|
1754
|
-
INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
|
|
1755
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
1756
|
-
`).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
|
|
1757
|
-
}
|
|
1758
|
-
function getProject(db, path) {
|
|
1759
|
-
const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
|
|
1760
|
-
if (!row)
|
|
1761
|
-
return null;
|
|
1762
|
-
return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
|
|
1763
|
-
}
|
|
1764
|
-
function deleteProject(db, path) {
|
|
1765
|
-
db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
|
|
1766
|
-
}
|
|
1767
|
-
function upsertBudget(db, budget) {
|
|
1768
|
-
db.prepare(`
|
|
1769
|
-
INSERT OR REPLACE INTO budgets
|
|
1770
|
-
(id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1771
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1772
|
-
`).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.cost_center_id ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
|
|
1773
|
-
}
|
|
1774
|
-
function listBudgets(db) {
|
|
1775
|
-
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
1776
|
-
}
|
|
1777
|
-
function deleteBudget(db, id) {
|
|
1778
|
-
db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
|
|
1779
|
-
}
|
|
1780
|
-
function getBudgetStatuses(db) {
|
|
1781
|
-
const budgets = listBudgets(db);
|
|
1782
|
-
return budgets.map((b) => {
|
|
1783
|
-
const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
|
|
1784
|
-
let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
|
|
1785
|
-
const params = [];
|
|
1786
|
-
if (b.project_path) {
|
|
1787
|
-
spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
|
|
1788
|
-
params.push(b.project_path);
|
|
1789
|
-
}
|
|
1790
|
-
if (b.agent) {
|
|
1791
|
-
spendQuery += ` AND agent = ?`;
|
|
1792
|
-
params.push(b.agent);
|
|
1793
|
-
}
|
|
1794
|
-
if (b.cost_center_id) {
|
|
1795
|
-
spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
|
|
1796
|
-
params.push(b.cost_center_id);
|
|
1797
|
-
}
|
|
1798
|
-
const row = db.prepare(spendQuery).get(...params);
|
|
1799
|
-
const spend = row.spend;
|
|
1800
|
-
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
1801
|
-
return {
|
|
1802
|
-
...b,
|
|
1803
|
-
current_spend_usd: spend,
|
|
1804
|
-
percent_used: percent,
|
|
1805
|
-
is_over_limit: percent >= 100,
|
|
1806
|
-
is_over_alert: percent >= b.alert_at_percent
|
|
1807
|
-
};
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
function upsertGoal(db, goal) {
|
|
1811
|
-
db.prepare(`
|
|
1812
|
-
INSERT OR REPLACE INTO goals
|
|
1813
|
-
(id, period, project_path, agent, limit_usd, created_at, updated_at)
|
|
1814
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1815
|
-
`).run(goal.id, goal.period, goal.project_path ?? null, goal.agent ?? null, goal.limit_usd, goal.created_at, goal.updated_at);
|
|
1816
|
-
}
|
|
1817
|
-
function deleteGoal(db, id) {
|
|
1818
|
-
db.prepare(`DELETE FROM goals WHERE id = ?`).run(id);
|
|
1819
|
-
}
|
|
1820
|
-
function listGoals(db) {
|
|
1821
|
-
return db.prepare(`SELECT * FROM goals ORDER BY created_at DESC`).all();
|
|
1822
|
-
}
|
|
1823
|
-
function getGoalStatuses(db) {
|
|
1824
|
-
const goals = listGoals(db);
|
|
1825
|
-
return goals.map((g) => {
|
|
1826
|
-
const periodStart = g.period === "day" ? "DATE('now')" : g.period === "week" ? "DATE('now', '-7 days')" : g.period === "month" ? "DATE('now', '-30 days')" : "DATE('now', '-365 days')";
|
|
1827
|
-
let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
|
|
1828
|
-
const params = [];
|
|
1829
|
-
if (g.project_path) {
|
|
1830
|
-
spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
|
|
1831
|
-
params.push(g.project_path);
|
|
1832
|
-
}
|
|
1833
|
-
if (g.agent) {
|
|
1834
|
-
spendQuery += ` AND agent = ?`;
|
|
1835
|
-
params.push(g.agent);
|
|
1836
|
-
}
|
|
1837
|
-
const row = db.prepare(spendQuery).get(...params);
|
|
1838
|
-
const spend = row.spend;
|
|
1839
|
-
const percent = g.limit_usd > 0 ? spend / g.limit_usd * 100 : 0;
|
|
1840
|
-
return {
|
|
1841
|
-
...g,
|
|
1842
|
-
current_spend_usd: spend,
|
|
1843
|
-
percent_used: percent,
|
|
1844
|
-
is_on_track: percent < 70,
|
|
1845
|
-
is_at_risk: percent >= 70 && percent <= 100,
|
|
1846
|
-
is_over: percent > 100
|
|
1847
|
-
};
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
function queryRequestsSince(db, since) {
|
|
1851
|
-
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
1852
|
-
}
|
|
1853
|
-
function insertFeedback(db, record) {
|
|
1854
|
-
db.prepare(`INSERT INTO feedback (message, email, category, version, machine_id) VALUES (?, ?, ?, ?, ?)`).run(record.message, record.email ?? null, record.category ?? "general", record.version ?? null, record.machine_id ?? null);
|
|
1855
|
-
}
|
|
1856
|
-
function billingPeriodWhere(period) {
|
|
1857
|
-
return period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
|
|
1858
|
-
}
|
|
1859
|
-
function queryBillingSummary(db, period) {
|
|
1860
|
-
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${billingPeriodWhere(period)} GROUP BY provider`).all();
|
|
1861
|
-
const by_provider = {};
|
|
1862
|
-
let total = 0;
|
|
1863
|
-
for (const r of rows) {
|
|
1864
|
-
by_provider[r.provider] = r.cost;
|
|
1865
|
-
total += r.cost;
|
|
1866
|
-
}
|
|
1867
|
-
return { total_usd: total, by_provider };
|
|
1868
|
-
}
|
|
1869
|
-
function countBillingRecords(db, period) {
|
|
1870
|
-
const row = db.prepare(`SELECT COUNT(*) as c FROM billing_daily WHERE ${billingPeriodWhere(period)}`).get();
|
|
1871
|
-
return row?.c ?? 0;
|
|
1872
|
-
}
|
|
1873
|
-
function getSessionDetail(db, id) {
|
|
1874
|
-
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
|
|
1875
|
-
if (!session)
|
|
1876
|
-
return null;
|
|
1877
|
-
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
1878
|
-
return { session, requests };
|
|
1879
|
-
}
|
|
1880
|
-
function listMachines(db, period = "all") {
|
|
1881
|
-
const rWhere = requestPeriodWhere(period);
|
|
1882
|
-
const sWhere = sessionPeriodWhere(period);
|
|
1883
|
-
return db.prepare(`
|
|
1884
|
-
WITH request_stats AS (
|
|
1885
|
-
SELECT
|
|
1886
|
-
machine_id,
|
|
1887
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1888
|
-
COUNT(*) as requests,
|
|
1889
|
-
COALESCE(SUM(cost_usd), 0) as total_cost_usd,
|
|
1890
|
-
MAX(timestamp) as last_active
|
|
1891
|
-
FROM requests
|
|
1892
|
-
WHERE machine_id != ''
|
|
1893
|
-
AND ${rWhere}
|
|
1894
|
-
GROUP BY machine_id
|
|
1895
|
-
),
|
|
1896
|
-
session_only_stats AS (
|
|
1897
|
-
SELECT
|
|
1898
|
-
machine_id,
|
|
1899
|
-
COUNT(*) as sessions,
|
|
1900
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1901
|
-
COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
|
|
1902
|
-
MAX(started_at) as last_active
|
|
1903
|
-
FROM sessions
|
|
1904
|
-
WHERE machine_id != ''
|
|
1905
|
-
AND ${sWhere}
|
|
1906
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1907
|
-
GROUP BY machine_id
|
|
1908
|
-
),
|
|
1909
|
-
combined AS (
|
|
1910
|
-
SELECT * FROM request_stats
|
|
1911
|
-
UNION ALL
|
|
1912
|
-
SELECT * FROM session_only_stats
|
|
1913
|
-
)
|
|
1914
|
-
SELECT
|
|
1915
|
-
machine_id,
|
|
1916
|
-
COALESCE(SUM(sessions), 0) as sessions,
|
|
1917
|
-
COALESCE(SUM(requests), 0) as requests,
|
|
1918
|
-
COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
|
|
1919
|
-
MAX(last_active) as last_active
|
|
1920
|
-
FROM combined
|
|
1921
|
-
GROUP BY machine_id
|
|
1922
|
-
ORDER BY total_cost_usd DESC
|
|
1923
|
-
`).all();
|
|
1924
|
-
}
|
|
1925
|
-
function upsertModelPricing(db, p) {
|
|
1926
|
-
db.prepare(`
|
|
1927
|
-
INSERT OR REPLACE INTO model_pricing
|
|
1928
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, cache_write_1h_per_1m, cache_storage_per_1m_hour, updated_at)
|
|
1929
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1930
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.cache_write_1h_per_1m ?? 0, p.cache_storage_per_1m_hour ?? 0, p.updated_at);
|
|
1931
|
-
}
|
|
1932
|
-
function getModelPricing(db, model) {
|
|
1933
|
-
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
1934
|
-
}
|
|
1935
|
-
function listModelPricing(db) {
|
|
1936
|
-
return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
|
|
1937
|
-
}
|
|
1938
|
-
function deleteModelPricing(db, model) {
|
|
1939
|
-
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
1940
|
-
}
|
|
1941
|
-
function seedModelPricing(db, defaults) {
|
|
1942
|
-
const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
|
|
1943
|
-
const now = new Date().toISOString();
|
|
1944
|
-
for (const [model, p] of Object.entries(defaults)) {
|
|
1945
|
-
if (existing.has(model))
|
|
1946
|
-
continue;
|
|
1947
|
-
upsertModelPricing(db, {
|
|
1948
|
-
model,
|
|
1949
|
-
input_per_1m: p.inputPer1M,
|
|
1950
|
-
output_per_1m: p.outputPer1M,
|
|
1951
|
-
cache_read_per_1m: p.cacheReadPer1M,
|
|
1952
|
-
cache_write_per_1m: p.cacheWritePer1M,
|
|
1953
|
-
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1954
|
-
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
1955
|
-
updated_at: now
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
function upsertSubscription(db, sub) {
|
|
1960
|
-
db.prepare(`
|
|
1961
|
-
INSERT OR REPLACE INTO subscriptions
|
|
1962
|
-
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
1963
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1964
|
-
`).run(sub.id, sub.agent, sub.provider, sub.plan, sub.monthly_fee_usd, sub.included_usage_usd, sub.billing_cycle_start, sub.reset_policy, sub.active, sub.created_at, sub.updated_at);
|
|
1965
|
-
}
|
|
1966
|
-
function listSubscriptions(db) {
|
|
1967
|
-
return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
|
|
1968
|
-
}
|
|
1969
|
-
function deleteSubscription(db, id) {
|
|
1970
|
-
db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
|
|
1971
|
-
}
|
|
1972
|
-
function queryUsageSnapshots(db, opts = {}) {
|
|
1973
|
-
const conditions = [];
|
|
1974
|
-
const params = [];
|
|
1975
|
-
if (opts.agent) {
|
|
1976
|
-
conditions.push("agent = ?");
|
|
1977
|
-
params.push(opts.agent);
|
|
1978
|
-
}
|
|
1979
|
-
if (opts.date) {
|
|
1980
|
-
conditions.push("date = ?");
|
|
1981
|
-
params.push(opts.date);
|
|
1982
|
-
}
|
|
1983
|
-
if (opts.since) {
|
|
1984
|
-
conditions.push("date >= ?");
|
|
1985
|
-
params.push(opts.since);
|
|
1986
|
-
}
|
|
1987
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1988
|
-
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1989
|
-
}
|
|
1990
|
-
function listMachineRegistry(db) {
|
|
1991
|
-
return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
|
|
1992
|
-
}
|
|
1993
|
-
var init_database = __esm(() => {
|
|
1994
|
-
init_sqlite_adapter();
|
|
1995
|
-
});
|
|
1996
|
-
|
|
1997
|
-
// src/lib/agents.ts
|
|
1998
|
-
var AGENTS = [
|
|
1999
|
-
"claude",
|
|
2000
|
-
"takumi",
|
|
2001
|
-
"codex",
|
|
2002
|
-
"gemini",
|
|
2003
|
-
"opencode",
|
|
2004
|
-
"cursor",
|
|
2005
|
-
"pi",
|
|
2006
|
-
"hermes"
|
|
2007
|
-
];
|
|
2008
|
-
var COST_BASIS = [
|
|
2009
|
-
"metered_api",
|
|
2010
|
-
"subscription_included",
|
|
2011
|
-
"estimated",
|
|
2012
|
-
"unknown"
|
|
2013
|
-
];
|
|
2014
|
-
function isAgent(value) {
|
|
2015
|
-
return AGENTS.includes(value);
|
|
2016
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
AGENTS,
|
|
4
|
+
COST_BASIS,
|
|
5
|
+
MAX_INGEST_BYTES,
|
|
6
|
+
__require,
|
|
7
|
+
cacheDir,
|
|
8
|
+
calendarPeriodBounds,
|
|
9
|
+
calendarPeriodWhere,
|
|
10
|
+
canonicalIngestBodyHash,
|
|
11
|
+
deleteBudget,
|
|
12
|
+
deleteGoal,
|
|
13
|
+
deleteModelPricing,
|
|
14
|
+
deleteProject,
|
|
15
|
+
deleteSubscription,
|
|
16
|
+
ensurePricingSeeded,
|
|
17
|
+
estimateCostFromRows,
|
|
18
|
+
getBudgetStatuses,
|
|
19
|
+
getGoalStatuses,
|
|
20
|
+
getMachineId,
|
|
21
|
+
getProject,
|
|
22
|
+
getSessionDetail,
|
|
23
|
+
hasKnownUsagePrice,
|
|
24
|
+
insertFeedback,
|
|
25
|
+
isAgent,
|
|
26
|
+
listMachineRegistry,
|
|
27
|
+
listMachines,
|
|
28
|
+
listModelPricing,
|
|
29
|
+
listSubscriptions,
|
|
30
|
+
queryAccountBreakdown,
|
|
31
|
+
queryAccountBreakdownSince,
|
|
32
|
+
queryAgentBreakdown,
|
|
33
|
+
queryAgentBreakdownSince,
|
|
34
|
+
queryBillingSummary,
|
|
35
|
+
queryCostCenterBreakdown,
|
|
36
|
+
queryDailyBreakdown,
|
|
37
|
+
queryModelBreakdown,
|
|
38
|
+
queryModelBreakdownSince,
|
|
39
|
+
queryProjectBreakdown,
|
|
40
|
+
queryProjectBreakdownSince,
|
|
41
|
+
queryRequestsSince,
|
|
42
|
+
querySessions,
|
|
43
|
+
querySummary,
|
|
44
|
+
queryTopSessions,
|
|
45
|
+
queryUsageSnapshots,
|
|
46
|
+
requireCurrentBillingSummary,
|
|
47
|
+
requireCurrentCostSummary,
|
|
48
|
+
sessionOverlapWhere,
|
|
49
|
+
stableJson,
|
|
50
|
+
subscriptionId,
|
|
51
|
+
upsertBudget,
|
|
52
|
+
upsertGoal,
|
|
53
|
+
upsertModelPricing,
|
|
54
|
+
upsertProject,
|
|
55
|
+
upsertSubscription,
|
|
56
|
+
usageSnapshotFilterForPeriod,
|
|
57
|
+
validateIngest
|
|
58
|
+
} from "./chunks/index-zpvea08j.js";
|
|
2017
59
|
// src/lib/store/index.ts
|
|
2018
|
-
init_database();
|
|
2019
|
-
init_pricing();
|
|
2020
60
|
import { randomUUID } from "crypto";
|
|
2021
61
|
// package.json
|
|
2022
62
|
var package_default = {
|
|
2023
63
|
name: "@hasna/economy",
|
|
2024
|
-
version: "0.
|
|
2025
|
-
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API
|
|
64
|
+
version: "0.5.0",
|
|
65
|
+
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
2026
66
|
repository: {
|
|
2027
67
|
type: "git",
|
|
2028
|
-
url: "
|
|
68
|
+
url: "https://github.com/hasna/apps.git",
|
|
69
|
+
directory: "apps/economy"
|
|
2029
70
|
},
|
|
2030
71
|
bugs: {
|
|
2031
|
-
url: "https://github.com/hasna/
|
|
72
|
+
url: "https://github.com/hasna/apps/issues"
|
|
2032
73
|
},
|
|
2033
|
-
homepage: "https://github.com/hasna/economy#readme",
|
|
74
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
|
|
2034
75
|
type: "module",
|
|
2035
76
|
main: "dist/index.js",
|
|
2036
77
|
types: "dist/index.d.ts",
|
|
@@ -2053,22 +94,21 @@ var package_default = {
|
|
|
2053
94
|
files: [
|
|
2054
95
|
"dist",
|
|
2055
96
|
"docs",
|
|
2056
|
-
"dashboard/dist",
|
|
2057
97
|
"postinstall.js",
|
|
2058
98
|
"LICENSE",
|
|
2059
99
|
"README.md",
|
|
2060
100
|
"CHANGELOG.md",
|
|
2061
101
|
"SECURITY.md",
|
|
2062
102
|
"CONTRIBUTING.md",
|
|
2063
|
-
"CODE_OF_CONDUCT.md"
|
|
103
|
+
"CODE_OF_CONDUCT.md",
|
|
104
|
+
"!dist/lib/test-hermetic-*"
|
|
2064
105
|
],
|
|
2065
106
|
scripts: {
|
|
2066
|
-
build:
|
|
2067
|
-
"build:cli":
|
|
2068
|
-
"build:mcp":
|
|
2069
|
-
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
2070
|
-
"build:lib":
|
|
2071
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
107
|
+
build: 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && tsc --emitDeclarationOnly --outDir dist',
|
|
108
|
+
"build:cli": 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
109
|
+
"build:mcp": 'bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
110
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external",
|
|
111
|
+
"build:lib": 'bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
2072
112
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
2073
113
|
prepack: "bun run build && bun run artifact-scan",
|
|
2074
114
|
typecheck: "tsc --noEmit",
|
|
@@ -2098,21 +138,18 @@ var package_default = {
|
|
|
2098
138
|
access: "public"
|
|
2099
139
|
},
|
|
2100
140
|
dependencies: {
|
|
2101
|
-
"@hasna/
|
|
2102
|
-
"@hasna/agent-registry": "^0.1.0",
|
|
2103
|
-
"@hasna/contracts": "0.13.4",
|
|
141
|
+
"@hasna/contracts": "1.0.2",
|
|
2104
142
|
"@hasna/events": "^0.1.16",
|
|
2105
|
-
"@hasna/mcp-harness": "^0.1.0",
|
|
2106
|
-
"@hasna/paths": "0.1.0",
|
|
2107
143
|
"@hasna/projects": "1.0.0",
|
|
2108
144
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
2109
145
|
chalk: "^5.4.1",
|
|
2110
146
|
commander: "^13.1.0",
|
|
2111
147
|
pg: "8.13.1",
|
|
2112
|
-
zod: "^3.24.2"
|
|
148
|
+
zod: "^3.24.2",
|
|
149
|
+
"@hasna/trash": "0.2.3"
|
|
2113
150
|
},
|
|
2114
151
|
devDependencies: {
|
|
2115
|
-
"@types/bun": "
|
|
152
|
+
"@types/bun": "1.3.14",
|
|
2116
153
|
"@types/pg": "^8.20.0",
|
|
2117
154
|
"bun-types": "latest",
|
|
2118
155
|
typescript: "^5.7.2"
|
|
@@ -2136,254 +173,277 @@ function getPackageMetadata() {
|
|
|
2136
173
|
var packageMetadata = getPackageMetadata();
|
|
2137
174
|
|
|
2138
175
|
// src/lib/savings.ts
|
|
2139
|
-
function
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
const now = new Date;
|
|
2157
|
-
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
2158
|
-
switch (period) {
|
|
2159
|
-
case "today":
|
|
2160
|
-
case "yesterday":
|
|
2161
|
-
return monthlyFee / daysInMonth;
|
|
2162
|
-
case "week":
|
|
2163
|
-
return monthlyFee / daysInMonth * 7;
|
|
2164
|
-
case "month":
|
|
2165
|
-
return monthlyFee;
|
|
2166
|
-
case "year":
|
|
2167
|
-
return monthlyFee * 12;
|
|
2168
|
-
case "all":
|
|
2169
|
-
return monthlyFee;
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
function proratedIncludedConsumed(includedUsage, includedCap, period) {
|
|
2173
|
-
const cap = prorateMonthlyFee(includedCap, period);
|
|
2174
|
-
return cap > 0 ? Math.min(includedUsage, cap) : includedUsage;
|
|
2175
|
-
}
|
|
2176
|
-
function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
|
|
2177
|
-
return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
|
|
176
|
+
function requireCurrentSavingsReport(value) {
|
|
177
|
+
if (!value || typeof value !== "object")
|
|
178
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid savings response");
|
|
179
|
+
const report = value;
|
|
180
|
+
if (report.comparison_basis !== "included_usage_vs_current_plan_estimate" || !["estimated", "not_applicable", "unavailable"].includes(String(report.comparison_status))) {
|
|
181
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: The service must support the current subscription comparison contract");
|
|
182
|
+
}
|
|
183
|
+
for (const field of ["api_equivalent_usd", "subscription_fee_usd", "on_demand_usd", "saved_usd", "net_comparison_usd"]) {
|
|
184
|
+
const number = report[field];
|
|
185
|
+
if (number !== null && (typeof number !== "number" || !Number.isFinite(number))) {
|
|
186
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid monetary field " + field);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (report.comparison_status === "unavailable" && (report.saved_usd !== null || report.net_comparison_usd !== null)) {
|
|
190
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Unavailable comparison carries a monetary claim");
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
2178
193
|
}
|
|
2179
|
-
function
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
const
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
COALESCE(SUM(included_usage_usd), 0) as included
|
|
2206
|
-
FROM subscriptions
|
|
2207
|
-
WHERE active = 1${agent ? " AND (agent = ? OR agent IS NULL)" : ""}
|
|
2208
|
-
`).get(...agent ? [agent] : []);
|
|
2209
|
-
const subscriptionFee = prorateMonthlyFee(subs.fee, period);
|
|
2210
|
-
const apiEquivalent = apiRow.total + includedRow.total;
|
|
2211
|
-
const includedConsumed = proratedIncludedConsumed(includedRow.total, subs.included, period);
|
|
2212
|
-
const onDemand = onDemandRow.total;
|
|
2213
|
-
const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
|
|
2214
|
-
const byAgent = {};
|
|
2215
|
-
if (!agent) {
|
|
2216
|
-
const onDemandByAgent = new Map;
|
|
2217
|
-
for (const row of db.prepare(`
|
|
2218
|
-
SELECT agent, COALESCE(SUM(value), 0) as total
|
|
2219
|
-
FROM usage_snapshots
|
|
2220
|
-
WHERE ${subWhere}
|
|
2221
|
-
AND metric = 'on_demand_usd'
|
|
2222
|
-
GROUP BY agent
|
|
2223
|
-
`).all()) {
|
|
2224
|
-
onDemandByAgent.set(row.agent, row.total);
|
|
194
|
+
function prorateMonthlyFee(monthlyFee, period, now = new Date) {
|
|
195
|
+
if (!Number.isFinite(monthlyFee) || monthlyFee < 0)
|
|
196
|
+
throw new RangeError("Monthly fee must be finite and nonnegative");
|
|
197
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
198
|
+
if (bounds.start_date === null)
|
|
199
|
+
return null;
|
|
200
|
+
let cursor = new Date(bounds.start_date + "T00:00:00.000Z");
|
|
201
|
+
const end = new Date(bounds.end_date_exclusive + "T00:00:00.000Z");
|
|
202
|
+
let fee = 0;
|
|
203
|
+
while (cursor < end) {
|
|
204
|
+
const monthEnd = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 1));
|
|
205
|
+
const next = new Date(Math.min(end.getTime(), monthEnd.getTime()));
|
|
206
|
+
const days = (next.getTime() - cursor.getTime()) / 86400000;
|
|
207
|
+
const monthDays = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 0)).getUTCDate();
|
|
208
|
+
fee += monthlyFee / monthDays * days;
|
|
209
|
+
cursor = next;
|
|
210
|
+
}
|
|
211
|
+
return fee;
|
|
212
|
+
}
|
|
213
|
+
function feeFor(plans, period, now) {
|
|
214
|
+
let known = 0;
|
|
215
|
+
let complete = true;
|
|
216
|
+
for (const plan of plans) {
|
|
217
|
+
if (plan.fee_basis !== "observed" && plan.fee_basis !== "configured" || plan.monthly_fee_usd === null || !Number.isFinite(plan.monthly_fee_usd) || plan.monthly_fee_usd < 0) {
|
|
218
|
+
complete = false;
|
|
219
|
+
continue;
|
|
2225
220
|
}
|
|
2226
|
-
const
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
221
|
+
const amount = prorateMonthlyFee(plan.monthly_fee_usd, period, now);
|
|
222
|
+
if (amount === null || !Number.isFinite(amount) || !Number.isFinite(known + amount))
|
|
223
|
+
complete = false;
|
|
224
|
+
else
|
|
225
|
+
known += amount;
|
|
226
|
+
}
|
|
227
|
+
return { known, total: complete ? known : null };
|
|
228
|
+
}
|
|
229
|
+
function summarizeOnDemand(observations, period, now = new Date) {
|
|
230
|
+
const rows = observations.filter((row) => row.metric === "on_demand_usd");
|
|
231
|
+
if (!rows.length)
|
|
232
|
+
return { usd: null, status: "not_observed" };
|
|
233
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
234
|
+
const start = bounds.start_date ? Date.parse(bounds.start_date + "T00:00:00Z") : -Infinity;
|
|
235
|
+
const end = bounds.end_date_exclusive ? Date.parse(bounds.end_date_exclusive + "T00:00:00Z") : Infinity;
|
|
236
|
+
const windows = new Map;
|
|
237
|
+
for (const row of rows) {
|
|
238
|
+
const from = Date.parse(row.window_start ?? "");
|
|
239
|
+
const until = Date.parse(row.window_end ?? "");
|
|
240
|
+
const at = Date.parse(row.source_timestamp ?? row.observed_at ?? "");
|
|
241
|
+
if (row.unit.toLowerCase() !== "usd" || !Number.isFinite(row.value) || row.value < 0 || !row.provider || !row.account_key || !Number.isFinite(from) || !Number.isFinite(until) || from >= until || !Number.isFinite(at) || row.value_kind !== "cumulative" && row.value_kind !== "delta") {
|
|
242
|
+
return { usd: null, status: "unavailable" };
|
|
2236
243
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
244
|
+
if (until <= start || from >= end)
|
|
245
|
+
continue;
|
|
246
|
+
if (from < start || until > end)
|
|
247
|
+
return { usd: null, status: "unavailable" };
|
|
248
|
+
const key = JSON.stringify([row.provider, row.account_key, from, until]);
|
|
249
|
+
const previous = windows.get(key);
|
|
250
|
+
if (previous && previous.row.value_kind !== row.value_kind)
|
|
251
|
+
return { usd: null, status: "unavailable" };
|
|
252
|
+
if (previous && ((row.value_kind === "delta" || at === previous.at) && previous.row.value !== row.value)) {
|
|
253
|
+
return { usd: null, status: "unavailable" };
|
|
254
|
+
}
|
|
255
|
+
if (!previous || at > previous.at)
|
|
256
|
+
windows.set(key, { row, at, start: from, end: until });
|
|
257
|
+
}
|
|
258
|
+
const byAccount = new Map;
|
|
259
|
+
let total = 0;
|
|
260
|
+
for (const window of windows.values()) {
|
|
261
|
+
const key = JSON.stringify([window.row.provider, window.row.account_key]);
|
|
262
|
+
const intervals = byAccount.get(key) ?? [];
|
|
263
|
+
if (intervals.some((other) => window.start < other.end && window.end > other.start)) {
|
|
264
|
+
return { usd: null, status: "unavailable" };
|
|
265
|
+
}
|
|
266
|
+
intervals.push(window);
|
|
267
|
+
byAccount.set(key, intervals);
|
|
268
|
+
total += window.row.value;
|
|
269
|
+
}
|
|
270
|
+
return windows.size ? { usd: total, status: "observed" } : { usd: null, status: "not_observed" };
|
|
271
|
+
}
|
|
272
|
+
function buildSavingsSummary(period, usage, plans, observations, agent, now = new Date, prices = []) {
|
|
273
|
+
const selected = usage.filter((row) => !agent || row.agent === agent);
|
|
274
|
+
const activePlans = plans.filter((row) => row.active === 1);
|
|
275
|
+
const selectedPlans = activePlans.filter((row) => !agent || row.agent === agent);
|
|
276
|
+
const sharedPlans = activePlans.filter((row) => !row.agent || !row.account_key);
|
|
277
|
+
const fee = feeFor(selectedPlans, period, now);
|
|
278
|
+
const unallocated = feeFor(sharedPlans, period, now);
|
|
279
|
+
const reasons = new Set;
|
|
280
|
+
let priced = 0, metered = 0, estimated = 0, included = 0, unknownPrices = 0, unknownBilling = 0, estimatedSessions = 0;
|
|
281
|
+
for (const row of selected) {
|
|
282
|
+
if (row.session_count) {
|
|
283
|
+
estimatedSessions += row.session_count;
|
|
284
|
+
reasons.add("billing_route_unknown");
|
|
285
|
+
}
|
|
286
|
+
if (!hasKnownUsagePrice(row, prices)) {
|
|
287
|
+
unknownPrices += row.requests;
|
|
288
|
+
reasons.add("model_price_unknown");
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
priced += row.cost_usd;
|
|
292
|
+
if (row.cost_basis === "metered_api")
|
|
293
|
+
metered += row.cost_usd;
|
|
294
|
+
else if (row.cost_basis === "subscription_included") {
|
|
295
|
+
included += row.cost_usd;
|
|
296
|
+
const matches = selectedPlans.filter((plan) => !!row.account_key && plan.account_key === row.account_key);
|
|
297
|
+
if (matches.length === 0)
|
|
298
|
+
reasons.add("subscription_account_unmapped");
|
|
299
|
+
if (matches.length > 1)
|
|
300
|
+
reasons.add("subscription_account_ambiguous");
|
|
301
|
+
} else {
|
|
302
|
+
estimated += row.cost_usd;
|
|
303
|
+
unknownBilling += row.requests;
|
|
304
|
+
reasons.add("billing_route_unknown");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (unknownPrices)
|
|
308
|
+
reasons.add("model_price_unknown");
|
|
309
|
+
if (unknownBilling)
|
|
310
|
+
reasons.add("billing_route_unknown");
|
|
311
|
+
if (fee.total === null)
|
|
312
|
+
reasons.add(period === "all" ? "historical_plan_fees_unavailable" : "plan_fee_unknown");
|
|
313
|
+
if (included > 0 && !agent && sharedPlans.some((plan) => !plan.account_key))
|
|
314
|
+
reasons.add("subscription_account_unmapped");
|
|
315
|
+
const demand = summarizeOnDemand(observations.filter((row) => !agent || row.agent === agent), period, now);
|
|
316
|
+
const applicable = selectedPlans.length > 0 || selected.some((row) => row.cost_basis === "subscription_included");
|
|
317
|
+
const net = reasons.size ? null : applicable ? included - fee.known : 0;
|
|
318
|
+
const byAgent = {};
|
|
319
|
+
if (!agent) {
|
|
320
|
+
const agents = new Set([...usage.map((row) => row.agent), ...activePlans.flatMap((row) => row.agent ? [row.agent] : [])]);
|
|
321
|
+
for (const name of [...agents].sort()) {
|
|
322
|
+
const result = buildSavingsSummary(period, usage, plans, observations, name, now, prices);
|
|
323
|
+
const { by_agent: _, ...scalars } = result;
|
|
324
|
+
byAgent[name] = scalars;
|
|
2272
325
|
}
|
|
2273
326
|
}
|
|
2274
327
|
return {
|
|
2275
328
|
period,
|
|
2276
|
-
api_equivalent_usd:
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
329
|
+
api_equivalent_usd: reasons.has("model_price_unknown") ? null : priced,
|
|
330
|
+
priced_usage_value_usd: priced,
|
|
331
|
+
recorded_metered_api_usd: metered,
|
|
332
|
+
estimated_usage_value_usd: estimated,
|
|
333
|
+
subscription_fee_usd: fee.total,
|
|
334
|
+
known_subscription_fee_usd: fee.known,
|
|
335
|
+
unallocated_subscription_fee_usd: unallocated.total,
|
|
336
|
+
included_consumed_usd: included,
|
|
337
|
+
on_demand_usd: demand.usd,
|
|
338
|
+
on_demand_status: demand.status,
|
|
339
|
+
saved_usd: net === null ? null : Math.max(0, net),
|
|
340
|
+
net_comparison_usd: net,
|
|
341
|
+
comparison_status: reasons.size ? "unavailable" : applicable ? "estimated" : "not_applicable",
|
|
342
|
+
comparison_basis: "included_usage_vs_current_plan_estimate",
|
|
343
|
+
unavailable_reasons: [...reasons].sort(),
|
|
344
|
+
unknown_price_requests: unknownPrices,
|
|
345
|
+
unknown_billing_requests: unknownBilling,
|
|
346
|
+
estimated_only_sessions: estimatedSessions,
|
|
2281
347
|
by_agent: byAgent
|
|
2282
348
|
};
|
|
2283
349
|
}
|
|
350
|
+
function querySavingsSummary(db, period, agent) {
|
|
351
|
+
const now = new Date;
|
|
352
|
+
const usage = db.prepare(`
|
|
353
|
+
SELECT agent, account_key, cost_basis, model,
|
|
354
|
+
CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END AS zero_cost,
|
|
355
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd, COUNT(*) AS requests,
|
|
356
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
357
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_create_tokens),0) AS cache_create_tokens,
|
|
358
|
+
COALESCE(SUM(cache_create_1h_tokens),0) AS cache_create_1h_tokens
|
|
359
|
+
FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp", now)}
|
|
360
|
+
GROUP BY agent, account_key, cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END
|
|
361
|
+
`).all();
|
|
362
|
+
const plans = db.prepare("SELECT * FROM subscriptions WHERE active = 1").all();
|
|
363
|
+
usage.push(...db.prepare(`
|
|
364
|
+
SELECT s.agent, s.account_key, 'estimated' AS cost_basis,
|
|
365
|
+
COALESCE(SUM(s.total_cost_usd), 0) AS cost_usd,
|
|
366
|
+
COALESCE(SUM(s.request_count), 0) AS requests, COUNT(*) AS session_count
|
|
367
|
+
FROM economy_effective_sessions s WHERE ${sessionOverlapWhere(period, "s", now)}
|
|
368
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
|
|
369
|
+
GROUP BY s.agent, s.account_key
|
|
370
|
+
`).all());
|
|
371
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
372
|
+
const windowWhere = bounds.start_date === null ? "1=1" : `(
|
|
373
|
+
(window_start IS NOT NULL AND window_end IS NOT NULL
|
|
374
|
+
AND window_start < '${bounds.end_date_exclusive}T00:00:00.000Z'
|
|
375
|
+
AND window_end > '${bounds.start_date}T00:00:00.000Z')
|
|
376
|
+
OR ((window_start IS NULL OR window_end IS NULL) AND ${calendarPeriodWhere(period, "date", now)})
|
|
377
|
+
)`;
|
|
378
|
+
const observations = db.prepare(`
|
|
379
|
+
SELECT * FROM usage_snapshots WHERE metric = 'on_demand_usd'
|
|
380
|
+
AND ${windowWhere}
|
|
381
|
+
`).all();
|
|
382
|
+
return buildSavingsSummary(period, usage, plans, observations, agent, now, listModelPricing(db));
|
|
383
|
+
}
|
|
2284
384
|
|
|
2285
385
|
// src/lib/billing-diff.ts
|
|
2286
|
-
init_database();
|
|
2287
|
-
var PROVIDER_TO_AGENT = {
|
|
2288
|
-
anthropic: "claude",
|
|
2289
|
-
openai: "codex",
|
|
2290
|
-
gemini: "gemini",
|
|
2291
|
-
google: "gemini"
|
|
2292
|
-
};
|
|
2293
|
-
function billingDeltaPct(estimatedUsd, actualUsd) {
|
|
2294
|
-
if (!(actualUsd > 0))
|
|
2295
|
-
return null;
|
|
2296
|
-
return (estimatedUsd - actualUsd) / actualUsd * 100;
|
|
2297
|
-
}
|
|
2298
386
|
function queryBillingDiff(db, period, thresholdPct = 15) {
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
const
|
|
2302
|
-
const
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
const agentRows = db.prepare(`
|
|
2308
|
-
SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
|
|
2309
|
-
FROM requests
|
|
2310
|
-
WHERE ${periodWhere3(period, "timestamp")}
|
|
2311
|
-
GROUP BY agent
|
|
387
|
+
if (!Number.isFinite(thresholdPct) || thresholdPct < 0)
|
|
388
|
+
throw new RangeError("thresholdPct must be a finite nonnegative number");
|
|
389
|
+
const usage = querySummary(db, period, undefined, true);
|
|
390
|
+
const billing = queryBillingSummary(db, period);
|
|
391
|
+
const rows = db.prepare(`
|
|
392
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) AS estimated_usd
|
|
393
|
+
FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp")}
|
|
394
|
+
GROUP BY agent ORDER BY agent
|
|
2312
395
|
`).all();
|
|
2313
|
-
|
|
2314
|
-
const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
|
|
2315
|
-
const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
|
|
2316
|
-
const rowDelta = row.estimated_usd - actualUsd;
|
|
2317
|
-
const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
|
|
2318
|
-
return {
|
|
2319
|
-
agent: row.agent,
|
|
2320
|
-
estimated_usd: row.estimated_usd,
|
|
2321
|
-
actual_usd: actualUsd,
|
|
2322
|
-
delta_usd: rowDelta,
|
|
2323
|
-
delta_pct: rowPct
|
|
2324
|
-
};
|
|
2325
|
-
}).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
|
|
2326
|
-
return {
|
|
396
|
+
return requireCurrentBillingDiffSummary({
|
|
2327
397
|
period,
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
398
|
+
basis: "unmatched_subtotals",
|
|
399
|
+
estimated_usd: usage.total_usd,
|
|
400
|
+
actual_usd: billing.total_usd,
|
|
401
|
+
usage_summary: usage,
|
|
402
|
+
billing_summary: billing,
|
|
403
|
+
delta_usd: null,
|
|
404
|
+
delta_pct: null,
|
|
2332
405
|
threshold_pct: thresholdPct,
|
|
2333
|
-
is_alert:
|
|
2334
|
-
comparable,
|
|
2335
|
-
incomparable_reason:
|
|
2336
|
-
by_agent,
|
|
2337
|
-
by_provider:
|
|
2338
|
-
};
|
|
2339
|
-
}
|
|
2340
|
-
function periodWhere3(period, column) {
|
|
2341
|
-
switch (period) {
|
|
2342
|
-
case "today":
|
|
2343
|
-
return `DATE(${column}) = DATE('now')`;
|
|
2344
|
-
case "yesterday":
|
|
2345
|
-
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
2346
|
-
case "week":
|
|
2347
|
-
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
2348
|
-
case "month":
|
|
2349
|
-
return `${column} >= DATE('now', 'start of month')`;
|
|
2350
|
-
case "year":
|
|
2351
|
-
return `${column} >= DATE('now', 'start of year')`;
|
|
2352
|
-
case "all":
|
|
2353
|
-
return "1=1";
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
// src/lib/periods.ts
|
|
2358
|
-
function ymd(date) {
|
|
2359
|
-
return date.toISOString().substring(0, 10);
|
|
406
|
+
is_alert: null,
|
|
407
|
+
comparable: false,
|
|
408
|
+
incomparable_reason: billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified",
|
|
409
|
+
by_agent: rows.map((row) => ({ ...row, basis: "stored_request_rows_subtotal", actual_usd: null, delta_usd: null, delta_pct: null })),
|
|
410
|
+
by_provider: billing.by_provider
|
|
411
|
+
});
|
|
2360
412
|
}
|
|
2361
|
-
function
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
413
|
+
function requireCurrentBillingDiffSummary(value) {
|
|
414
|
+
const invalid = () => {
|
|
415
|
+
throw new Error("BILLING_COMPARISON_UNAVAILABLE: The service must return scoped unmatched subtotals");
|
|
416
|
+
};
|
|
417
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
418
|
+
return invalid();
|
|
419
|
+
const s = value;
|
|
420
|
+
if (s.basis !== "unmatched_subtotals" || s.comparable !== false || s.delta_usd !== null || s.delta_pct !== null || s.is_alert !== null)
|
|
421
|
+
return invalid();
|
|
422
|
+
if (!["today", "yesterday", "week", "month", "year", "all"].includes(String(s.period)) || typeof s.threshold_pct !== "number" || !Number.isFinite(s.threshold_pct) || s.threshold_pct < 0)
|
|
423
|
+
return invalid();
|
|
424
|
+
let usage, billing;
|
|
425
|
+
try {
|
|
426
|
+
usage = requireCurrentCostSummary(s.usage_summary);
|
|
427
|
+
billing = requireCurrentBillingSummary(s.billing_summary);
|
|
428
|
+
} catch {
|
|
429
|
+
return invalid();
|
|
430
|
+
}
|
|
431
|
+
if (usage.period !== s.period || s.estimated_usd !== usage.total_usd || s.actual_usd !== billing.total_usd || billing.recorded_total_usd !== (billing.record_count ? billing.total_usd : null))
|
|
432
|
+
return invalid();
|
|
433
|
+
if (s.incomparable_reason !== (billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified"))
|
|
434
|
+
return invalid();
|
|
435
|
+
const providers = s.by_provider;
|
|
436
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers) || !billing.by_provider || typeof billing.by_provider !== "object")
|
|
437
|
+
return invalid();
|
|
438
|
+
const expected = Object.entries(billing.by_provider);
|
|
439
|
+
if (Object.keys(providers).length !== expected.length || expected.some(([key, amount]) => typeof amount !== "number" || !Number.isFinite(amount) || providers[key] !== amount))
|
|
440
|
+
return invalid();
|
|
441
|
+
if (!Array.isArray(s.by_agent) || s.by_agent.some((row) => !row || typeof row !== "object" || typeof row.agent !== "string" || row.basis !== "stored_request_rows_subtotal" || typeof row.estimated_usd !== "number" || !Number.isFinite(row.estimated_usd) || row.actual_usd !== null || row.delta_usd !== null || row.delta_pct !== null))
|
|
442
|
+
return invalid();
|
|
443
|
+
return value;
|
|
2383
444
|
}
|
|
2384
445
|
|
|
2385
446
|
// src/lib/brief.ts
|
|
2386
|
-
init_database();
|
|
2387
447
|
var ZERO_REQUEST_TOTALS = {
|
|
2388
448
|
sessions: 0,
|
|
2389
449
|
request_sessions: 0,
|
|
@@ -2427,15 +487,11 @@ function machineFilter(machine) {
|
|
|
2427
487
|
return;
|
|
2428
488
|
return machine;
|
|
2429
489
|
}
|
|
2430
|
-
function
|
|
2431
|
-
|
|
2432
|
-
return `DATE(timestamp) = DATE('now')`;
|
|
2433
|
-
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
490
|
+
function requestPeriodWhere(period) {
|
|
491
|
+
return calendarPeriodWhere(period, "timestamp");
|
|
2434
492
|
}
|
|
2435
|
-
function
|
|
2436
|
-
|
|
2437
|
-
return `DATE(started_at) = DATE('now')`;
|
|
2438
|
-
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
493
|
+
function sessionPeriodWhere(period) {
|
|
494
|
+
return calendarPeriodWhere(period, "started_at");
|
|
2439
495
|
}
|
|
2440
496
|
function baseRequestTotals(row) {
|
|
2441
497
|
return {
|
|
@@ -2506,7 +562,7 @@ function requestTotalsSql(where, machine) {
|
|
|
2506
562
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
2507
563
|
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
2508
564
|
MAX(timestamp) as last_active
|
|
2509
|
-
FROM
|
|
565
|
+
FROM economy_effective_requests
|
|
2510
566
|
WHERE ${where}${machineClause}
|
|
2511
567
|
`,
|
|
2512
568
|
params
|
|
@@ -2527,9 +583,9 @@ function sessionOnlyTotalsSql(where, machine) {
|
|
|
2527
583
|
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
2528
584
|
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
2529
585
|
MAX(started_at) as last_active
|
|
2530
|
-
FROM
|
|
586
|
+
FROM economy_effective_sessions
|
|
2531
587
|
WHERE ${where}${machineClause}
|
|
2532
|
-
AND NOT EXISTS (SELECT 1 FROM
|
|
588
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
2533
589
|
`,
|
|
2534
590
|
params
|
|
2535
591
|
};
|
|
@@ -2544,15 +600,16 @@ function querySessionOnlyTotals(db, where, params, machine) {
|
|
|
2544
600
|
}
|
|
2545
601
|
function queryPeriodTotals(db, period, machine) {
|
|
2546
602
|
const summary = querySummary(db, period, machine);
|
|
2547
|
-
const requests = queryRequestTotals(db,
|
|
2548
|
-
const sessions = querySessionOnlyTotals(db,
|
|
603
|
+
const requests = queryRequestTotals(db, requestPeriodWhere(period), [], machine);
|
|
604
|
+
const sessions = querySessionOnlyTotals(db, sessionPeriodWhere(period), [], machine);
|
|
2549
605
|
const totals = combineTotals(requests, sessions);
|
|
2550
606
|
return {
|
|
2551
607
|
...totals,
|
|
2552
608
|
sessions: summary.sessions,
|
|
2553
609
|
requests: summary.requests,
|
|
2554
610
|
total_tokens: summary.tokens,
|
|
2555
|
-
cost_usd: summary.total_usd
|
|
611
|
+
cost_usd: summary.total_usd,
|
|
612
|
+
cost_summary: summary
|
|
2556
613
|
};
|
|
2557
614
|
}
|
|
2558
615
|
function querySinceTotals(db, since, machine) {
|
|
@@ -2605,7 +662,7 @@ function queryMachineSinceRows(db, since, machine) {
|
|
|
2605
662
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
2606
663
|
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
2607
664
|
MAX(timestamp) as last_active
|
|
2608
|
-
FROM
|
|
665
|
+
FROM economy_effective_requests
|
|
2609
666
|
WHERE timestamp >= ?
|
|
2610
667
|
AND machine_id != ''
|
|
2611
668
|
${requestMachineClause}
|
|
@@ -2620,11 +677,11 @@ function queryMachineSinceRows(db, since, machine) {
|
|
|
2620
677
|
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
2621
678
|
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
2622
679
|
MAX(started_at) as last_active
|
|
2623
|
-
FROM
|
|
680
|
+
FROM economy_effective_sessions
|
|
2624
681
|
WHERE started_at >= ?
|
|
2625
682
|
AND machine_id != ''
|
|
2626
683
|
${sessionMachineClause}
|
|
2627
|
-
AND NOT EXISTS (SELECT 1 FROM
|
|
684
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
2628
685
|
GROUP BY machine_id
|
|
2629
686
|
`).all(...machine ? [since, machine] : [since]);
|
|
2630
687
|
const groups = new Map;
|
|
@@ -2662,13 +719,13 @@ function queryMachineLastData(db, machine) {
|
|
|
2662
719
|
const filter = machine ? " AND machine_id = ?" : "";
|
|
2663
720
|
const requestRows = db.prepare(`
|
|
2664
721
|
SELECT machine_id, MAX(timestamp) as last_at
|
|
2665
|
-
FROM
|
|
722
|
+
FROM economy_effective_requests
|
|
2666
723
|
WHERE machine_id != ''${filter}
|
|
2667
724
|
GROUP BY machine_id
|
|
2668
725
|
`).all(...machine ? [machine] : []);
|
|
2669
726
|
const sessionRows = db.prepare(`
|
|
2670
727
|
SELECT machine_id, MAX(started_at) as last_at
|
|
2671
|
-
FROM
|
|
728
|
+
FROM economy_effective_sessions
|
|
2672
729
|
WHERE machine_id != ''${filter}
|
|
2673
730
|
GROUP BY machine_id
|
|
2674
731
|
`).all(...machine ? [machine] : []);
|
|
@@ -2696,7 +753,7 @@ function queryAgentSinceRows(db, since, machine) {
|
|
|
2696
753
|
input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
|
|
2697
754
|
cost_usd,
|
|
2698
755
|
timestamp as last_active
|
|
2699
|
-
FROM
|
|
756
|
+
FROM economy_effective_requests
|
|
2700
757
|
WHERE timestamp >= ?${requestMachineClause}
|
|
2701
758
|
),
|
|
2702
759
|
session_only_rows AS (
|
|
@@ -2713,9 +770,9 @@ function queryAgentSinceRows(db, since, machine) {
|
|
|
2713
770
|
COALESCE(total_tokens, 0) as total_tokens,
|
|
2714
771
|
COALESCE(total_cost_usd, 0) as cost_usd,
|
|
2715
772
|
started_at as last_active
|
|
2716
|
-
FROM
|
|
773
|
+
FROM economy_effective_sessions
|
|
2717
774
|
WHERE started_at >= ?${sessionMachineClause}
|
|
2718
|
-
AND NOT EXISTS (SELECT 1 FROM
|
|
775
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
2719
776
|
),
|
|
2720
777
|
combined AS (
|
|
2721
778
|
SELECT * FROM request_rows
|
|
@@ -2759,12 +816,12 @@ function agentBreakdownToBriefRow(row) {
|
|
|
2759
816
|
last_active: row.last_active
|
|
2760
817
|
};
|
|
2761
818
|
}
|
|
2762
|
-
function
|
|
819
|
+
function normalizeAccountEmail(email) {
|
|
2763
820
|
return email.trim().toLowerCase();
|
|
2764
821
|
}
|
|
2765
822
|
function accountKey(row) {
|
|
2766
823
|
const agent = row.agent || row.account_tool || "unknown";
|
|
2767
|
-
const email =
|
|
824
|
+
const email = normalizeAccountEmail(row.account_email);
|
|
2768
825
|
if (email)
|
|
2769
826
|
return `${agent}:${email}`;
|
|
2770
827
|
if (row.account_name)
|
|
@@ -2775,7 +832,7 @@ function addAccountRow(groups, row) {
|
|
|
2775
832
|
if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
|
|
2776
833
|
return;
|
|
2777
834
|
const key = accountKey(row);
|
|
2778
|
-
const email =
|
|
835
|
+
const email = normalizeAccountEmail(row.account_email);
|
|
2779
836
|
const current = groups.get(key) ?? {
|
|
2780
837
|
account_key: key,
|
|
2781
838
|
account_tool: row.agent || row.account_tool || "unknown",
|
|
@@ -2837,8 +894,8 @@ function queryAccountSinceRows(db, since, machine) {
|
|
|
2837
894
|
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
2838
895
|
COALESCE(SUM(r.cost_usd), 0) as cost_usd,
|
|
2839
896
|
MAX(r.timestamp) as last_active
|
|
2840
|
-
FROM
|
|
2841
|
-
LEFT JOIN
|
|
897
|
+
FROM economy_effective_requests r
|
|
898
|
+
LEFT JOIN economy_effective_sessions s ON s.id = r.session_id
|
|
2842
899
|
WHERE r.timestamp >= ?${requestMachineClause}
|
|
2843
900
|
GROUP BY
|
|
2844
901
|
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
|
|
@@ -2867,9 +924,9 @@ function queryAccountSinceRows(db, since, machine) {
|
|
|
2867
924
|
COALESCE(SUM(s.total_tokens), 0) as total_tokens,
|
|
2868
925
|
COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
|
|
2869
926
|
MAX(s.started_at) as last_active
|
|
2870
|
-
FROM
|
|
927
|
+
FROM economy_effective_sessions s
|
|
2871
928
|
WHERE s.started_at >= ?${sessionMachineClause}
|
|
2872
|
-
AND NOT EXISTS (SELECT 1 FROM
|
|
929
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
|
|
2873
930
|
GROUP BY
|
|
2874
931
|
s.agent,
|
|
2875
932
|
s.account_key,
|
|
@@ -2913,7 +970,7 @@ function queryFreshness(db, now, machine, localSync) {
|
|
|
2913
970
|
const requestFilter = machine ? " AND machine_id = ?" : "";
|
|
2914
971
|
const requestRows = db.prepare(`
|
|
2915
972
|
SELECT machine_id, MAX(timestamp) as max_request_at
|
|
2916
|
-
FROM
|
|
973
|
+
FROM economy_effective_requests
|
|
2917
974
|
WHERE machine_id != ''${requestFilter}
|
|
2918
975
|
GROUP BY machine_id
|
|
2919
976
|
`).all(...machine ? [machine] : []);
|
|
@@ -3007,7 +1064,7 @@ function buildBrief(db, opts = {}) {
|
|
|
3007
1064
|
function queryProjectDetail(db, nameOrPath) {
|
|
3008
1065
|
const like = `%${nameOrPath}%`;
|
|
3009
1066
|
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
3010
|
-
FROM
|
|
1067
|
+
FROM economy_effective_sessions
|
|
3011
1068
|
WHERE project_name LIKE ? OR project_path LIKE ?
|
|
3012
1069
|
ORDER BY started_at DESC`).all(like, like);
|
|
3013
1070
|
if (sessions.length === 0)
|
|
@@ -3016,12 +1073,12 @@ function queryProjectDetail(db, nameOrPath) {
|
|
|
3016
1073
|
const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
|
|
3017
1074
|
const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
|
|
3018
1075
|
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
3019
|
-
FROM
|
|
1076
|
+
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
3020
1077
|
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
3021
1078
|
AND r.timestamp >= DATE('now', '-14 days')
|
|
3022
1079
|
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
3023
1080
|
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
3024
|
-
FROM
|
|
1081
|
+
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
3025
1082
|
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
3026
1083
|
GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
|
|
3027
1084
|
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 }));
|
|
@@ -3037,45 +1094,39 @@ function queryProjectDetail(db, nameOrPath) {
|
|
|
3037
1094
|
};
|
|
3038
1095
|
}
|
|
3039
1096
|
function exportDateWhere(column, period) {
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
return `DATE(${column}) = DATE('now')`;
|
|
3043
|
-
case "week":
|
|
3044
|
-
return `${column} >= DATE('now', '-7 days')`;
|
|
3045
|
-
case "all":
|
|
3046
|
-
return "1=1";
|
|
3047
|
-
default:
|
|
3048
|
-
return `${column} >= DATE('now', '-30 days')`;
|
|
1097
|
+
if (!["today", "yesterday", "week", "month", "year", "all"].includes(period)) {
|
|
1098
|
+
throw new RangeError("Invalid export period");
|
|
3049
1099
|
}
|
|
1100
|
+
return calendarPeriodWhere(period, column);
|
|
3050
1101
|
}
|
|
3051
1102
|
function queryExportRows(db, type, period) {
|
|
3052
1103
|
if (type === "requests") {
|
|
3053
1104
|
const where2 = exportDateWhere("timestamp", period);
|
|
3054
|
-
return db.prepare(`SELECT * FROM
|
|
1105
|
+
return db.prepare(`SELECT * FROM economy_effective_requests WHERE ${where2} ORDER BY timestamp ASC`).all();
|
|
3055
1106
|
}
|
|
3056
1107
|
const where = exportDateWhere("started_at", period);
|
|
3057
|
-
return db.prepare(`SELECT * FROM
|
|
1108
|
+
return db.prepare(`SELECT * FROM economy_effective_sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
3058
1109
|
}
|
|
3059
1110
|
function queryRangeStats(db, from, to) {
|
|
3060
1111
|
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
|
|
3061
1112
|
COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
|
|
3062
|
-
FROM
|
|
3063
|
-
const s = db.prepare(`SELECT COUNT(*) as sessions FROM
|
|
1113
|
+
FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
1114
|
+
const s = db.prepare(`SELECT COUNT(*) as sessions FROM economy_effective_sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
3064
1115
|
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
3065
1116
|
}
|
|
3066
1117
|
function queryForecast(db, now = new Date) {
|
|
3067
1118
|
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
3068
1119
|
const dayOfMonth = now.getDate();
|
|
3069
1120
|
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
3070
|
-
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM
|
|
1121
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
3071
1122
|
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
3072
1123
|
const projected = dailyAvg * daysInMonth;
|
|
3073
1124
|
const sevenDaysAgo = new Date(now);
|
|
3074
1125
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
3075
|
-
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM
|
|
1126
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
3076
1127
|
const last7DailyAvg = last7.cost / 7;
|
|
3077
1128
|
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
3078
|
-
FROM
|
|
1129
|
+
FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
|
|
3079
1130
|
return {
|
|
3080
1131
|
day_of_month: dayOfMonth,
|
|
3081
1132
|
days_in_month: daysInMonth,
|
|
@@ -3096,22 +1147,134 @@ function queryModelEfficiency(db) {
|
|
|
3096
1147
|
COALESCE(SUM(cache_create_tokens),0) as cache_write,
|
|
3097
1148
|
COUNT(*) as requests,
|
|
3098
1149
|
COALESCE(SUM(cost_usd),0) as cost
|
|
3099
|
-
FROM
|
|
1150
|
+
FROM economy_effective_requests GROUP BY model ORDER BY cost DESC`).all();
|
|
3100
1151
|
}
|
|
3101
1152
|
|
|
3102
|
-
// src/lib/contracts-client/storage.ts
|
|
3103
|
-
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
3104
|
-
|
|
3105
1153
|
// src/lib/cloud-storage.ts
|
|
1154
|
+
import {
|
|
1155
|
+
appConfigDiskValue,
|
|
1156
|
+
ClientTransportConfigurationError,
|
|
1157
|
+
clientTransportEnvKeys,
|
|
1158
|
+
createClientTransport,
|
|
1159
|
+
credentialDiskSources,
|
|
1160
|
+
credentialOverrideEnvKey,
|
|
1161
|
+
credentialPointerEnvKey,
|
|
1162
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
1163
|
+
keychainConfigValue
|
|
1164
|
+
} from "@hasna/contracts/client";
|
|
1165
|
+
import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
3106
1166
|
var ECONOMY_APP = "economy";
|
|
1167
|
+
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
1168
|
+
function envFlagSet(env, key) {
|
|
1169
|
+
const raw = env[key];
|
|
1170
|
+
if (raw === undefined)
|
|
1171
|
+
return false;
|
|
1172
|
+
const value = raw.trim().toLowerCase();
|
|
1173
|
+
return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
|
|
1174
|
+
}
|
|
1175
|
+
function localStorageExplicitlyOptedIn(env) {
|
|
1176
|
+
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
|
|
1177
|
+
}
|
|
1178
|
+
function economyAuthorityEnvKeys() {
|
|
1179
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
1180
|
+
return [
|
|
1181
|
+
...keys.apiUrlKeys,
|
|
1182
|
+
...keys.apiKeyKeys,
|
|
1183
|
+
credentialOverrideEnvKey(ECONOMY_APP),
|
|
1184
|
+
credentialPointerEnvKey(ECONOMY_APP),
|
|
1185
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
1186
|
+
];
|
|
1187
|
+
}
|
|
1188
|
+
function hasEconomyEnvAuthorityIntent(env) {
|
|
1189
|
+
return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
|
|
1190
|
+
}
|
|
1191
|
+
function hostedAuthorityConfigured(env, options = {}) {
|
|
1192
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
1193
|
+
if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
|
|
1194
|
+
return true;
|
|
1195
|
+
if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
|
|
1196
|
+
return true;
|
|
1197
|
+
return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
|
|
1198
|
+
}
|
|
1199
|
+
function economyResolverEnv(env) {
|
|
1200
|
+
const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
1201
|
+
if (blanks.length === 0)
|
|
1202
|
+
return env;
|
|
1203
|
+
const next = { ...env };
|
|
1204
|
+
for (const key of blanks)
|
|
1205
|
+
delete next[key];
|
|
1206
|
+
return next;
|
|
1207
|
+
}
|
|
1208
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
1209
|
+
function isAmbientEconomyEnv(env) {
|
|
1210
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
1211
|
+
return true;
|
|
1212
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
1213
|
+
}
|
|
1214
|
+
function economyResolverInputs(env, credentials = {}) {
|
|
1215
|
+
const normalised = economyResolverEnv(env);
|
|
1216
|
+
if (normalised === env)
|
|
1217
|
+
return { env: normalised, credentials };
|
|
1218
|
+
const keychain = { ...credentials.keychain };
|
|
1219
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
1220
|
+
keychain.enabled = isAmbientEconomyEnv(env);
|
|
1221
|
+
}
|
|
1222
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
1223
|
+
}
|
|
1224
|
+
function resolveEconomyStorageClient(name, env, overrides = {}) {
|
|
1225
|
+
const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
|
|
1226
|
+
const wired = createClientTransport(name, resolverEnv, {
|
|
1227
|
+
credentials,
|
|
1228
|
+
...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
1229
|
+
...overrides.headers ? { headers: overrides.headers } : {},
|
|
1230
|
+
...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
1231
|
+
...overrides.retry !== undefined ? { retry: overrides.retry } : {},
|
|
1232
|
+
...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
1233
|
+
});
|
|
1234
|
+
return {
|
|
1235
|
+
transport: "http",
|
|
1236
|
+
client: createHasnaStorageClient(name, wired.client),
|
|
1237
|
+
resolution: wired.resolution
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function localEconomyNotice() {
|
|
1241
|
+
return `economy: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
|
|
1242
|
+
}
|
|
1243
|
+
function credentialRequiredError(env, cause) {
|
|
1244
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
1245
|
+
const clientEnv = env;
|
|
1246
|
+
const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
|
|
1247
|
+
const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
|
|
1248
|
+
return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
|
|
1249
|
+
}
|
|
3107
1250
|
var cache;
|
|
1251
|
+
var localNoticePrinted = false;
|
|
3108
1252
|
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
3109
|
-
const
|
|
3110
|
-
|
|
1253
|
+
const clientEnv = env;
|
|
1254
|
+
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
1255
|
+
return { active: false, client: null };
|
|
1256
|
+
}
|
|
1257
|
+
let resolved;
|
|
1258
|
+
try {
|
|
1259
|
+
resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (error instanceof ClientTransportConfigurationError) {
|
|
1262
|
+
throw credentialRequiredError(env, error);
|
|
1263
|
+
}
|
|
1264
|
+
throw error;
|
|
1265
|
+
}
|
|
1266
|
+
return { active: true, client: resolved.client };
|
|
3111
1267
|
}
|
|
3112
1268
|
function economyCloudStorage(env = process.env) {
|
|
3113
|
-
if (cache === undefined)
|
|
1269
|
+
if (cache === undefined) {
|
|
3114
1270
|
cache = resolveEconomyCloudStorage(env);
|
|
1271
|
+
if (!cache.active && !localNoticePrinted) {
|
|
1272
|
+
localNoticePrinted = true;
|
|
1273
|
+
if (typeof process !== "undefined")
|
|
1274
|
+
process.stderr.write(`${localEconomyNotice()}
|
|
1275
|
+
`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
3115
1278
|
return cache;
|
|
3116
1279
|
}
|
|
3117
1280
|
function cleanQuery(query) {
|
|
@@ -3139,94 +1302,116 @@ async function cloudObject(storage, path, query) {
|
|
|
3139
1302
|
}
|
|
3140
1303
|
|
|
3141
1304
|
// src/lib/store/index.ts
|
|
1305
|
+
function configuredSubscription(input) {
|
|
1306
|
+
if (typeof input.account_key !== "string" || !input.account_key.trim()) {
|
|
1307
|
+
throw new TypeError("account_key must identify the provider account; accepted: nonempty account key");
|
|
1308
|
+
}
|
|
1309
|
+
const now = new Date().toISOString();
|
|
1310
|
+
const row = {
|
|
1311
|
+
...input,
|
|
1312
|
+
id: input.id ?? subscriptionId(input),
|
|
1313
|
+
monthly_fee_usd: input.monthly_fee_usd === undefined ? 0 : input.monthly_fee_usd,
|
|
1314
|
+
included_usage_usd: input.included_usage_usd === undefined ? 0 : input.included_usage_usd,
|
|
1315
|
+
fee_basis: input.monthly_fee_usd === undefined ? "unknown" : "configured",
|
|
1316
|
+
included_basis: input.included_usage_usd === undefined ? "unknown" : "configured",
|
|
1317
|
+
observed_station: input.observed_station ?? null,
|
|
1318
|
+
observed_at: now,
|
|
1319
|
+
created_at: now,
|
|
1320
|
+
updated_at: now
|
|
1321
|
+
};
|
|
1322
|
+
validateIngest({ subscriptions: [row] });
|
|
1323
|
+
return row;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
3142
1326
|
class LocalStore {
|
|
3143
1327
|
transport = "local";
|
|
3144
1328
|
_db;
|
|
3145
|
-
db() {
|
|
1329
|
+
async db() {
|
|
3146
1330
|
if (!this._db) {
|
|
1331
|
+
const { openDatabase } = await import("./chunks/sqlite-store-fq4natnf.js");
|
|
3147
1332
|
this._db = openDatabase();
|
|
3148
1333
|
ensurePricingSeeded(this._db);
|
|
3149
1334
|
}
|
|
3150
1335
|
return this._db;
|
|
3151
1336
|
}
|
|
3152
1337
|
async summary(period, machine) {
|
|
3153
|
-
return querySummary(this.db(), period, machine);
|
|
1338
|
+
return querySummary(await this.db(), period, machine);
|
|
3154
1339
|
}
|
|
3155
1340
|
async sessions(filter) {
|
|
3156
|
-
return querySessions(this.db(), filter);
|
|
1341
|
+
return querySessions(await this.db(), filter);
|
|
3157
1342
|
}
|
|
3158
1343
|
async topSessions(n, agent, since) {
|
|
3159
|
-
return queryTopSessions(this.db(), n, agent, since);
|
|
1344
|
+
return queryTopSessions(await this.db(), n, agent, since);
|
|
3160
1345
|
}
|
|
3161
1346
|
async sessionDetail(id) {
|
|
3162
|
-
return getSessionDetail(this.db(), id);
|
|
1347
|
+
return getSessionDetail(await this.db(), id);
|
|
3163
1348
|
}
|
|
3164
1349
|
async modelBreakdown(query = {}) {
|
|
3165
|
-
return query.since ? queryModelBreakdownSince(this.db(), query.since) : queryModelBreakdown(this.db());
|
|
1350
|
+
return query.since ? queryModelBreakdownSince(await this.db(), query.since) : queryModelBreakdown(await this.db());
|
|
3166
1351
|
}
|
|
3167
1352
|
async agentBreakdown(query = {}) {
|
|
3168
|
-
return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
1353
|
+
return query.since ? queryAgentBreakdownSince(await this.db(), query.since) : queryAgentBreakdown(await this.db(), query.period ?? "all", query.machine);
|
|
3169
1354
|
}
|
|
3170
1355
|
async projectBreakdown(query = {}) {
|
|
3171
|
-
return query.since ? queryProjectBreakdownSince(this.db(), query.since, query.machine) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
1356
|
+
return query.since ? queryProjectBreakdownSince(await this.db(), query.since, query.machine) : queryProjectBreakdown(await this.db(), query.period ?? "all", query.machine);
|
|
3172
1357
|
}
|
|
3173
1358
|
async accountBreakdown(query = {}) {
|
|
3174
|
-
return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
1359
|
+
return query.since ? queryAccountBreakdownSince(await this.db(), query.since) : queryAccountBreakdown(await this.db(), query.period ?? "all", query.machine);
|
|
3175
1360
|
}
|
|
3176
1361
|
async costCenterBreakdown(query = {}) {
|
|
3177
|
-
return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
|
|
1362
|
+
return queryCostCenterBreakdown(await this.db(), query.period ?? "all", {
|
|
3178
1363
|
kind: query.kind,
|
|
3179
1364
|
machine: query.machine,
|
|
3180
1365
|
since: query.since
|
|
3181
1366
|
});
|
|
3182
1367
|
}
|
|
3183
1368
|
async accounts(period) {
|
|
3184
|
-
return queryAccountBreakdown(this.db(), period);
|
|
1369
|
+
return queryAccountBreakdown(await this.db(), period);
|
|
3185
1370
|
}
|
|
3186
1371
|
async daily(days, machine) {
|
|
3187
|
-
return queryDailyBreakdown(this.db(), days, machine);
|
|
1372
|
+
return queryDailyBreakdown(await this.db(), days, machine);
|
|
3188
1373
|
}
|
|
3189
1374
|
async machines() {
|
|
3190
|
-
return listMachines(this.db());
|
|
1375
|
+
return listMachines(await this.db());
|
|
3191
1376
|
}
|
|
3192
1377
|
async fleet(period) {
|
|
3193
1378
|
return {
|
|
3194
|
-
summary: querySummary(this.db(), period, undefined, true),
|
|
3195
|
-
machines: listMachines(this.db(), period),
|
|
3196
|
-
registry: listMachineRegistry(this.db())
|
|
1379
|
+
summary: querySummary(await this.db(), period, undefined, true),
|
|
1380
|
+
machines: listMachines(await this.db(), period),
|
|
1381
|
+
registry: listMachineRegistry(await this.db())
|
|
3197
1382
|
};
|
|
3198
1383
|
}
|
|
3199
1384
|
async billingSummary(period) {
|
|
3200
|
-
return queryBillingSummary(this.db(), period);
|
|
1385
|
+
return queryBillingSummary(await this.db(), period);
|
|
3201
1386
|
}
|
|
3202
1387
|
async billingDiff(period) {
|
|
3203
|
-
return queryBillingDiff(this.db(), period);
|
|
1388
|
+
return queryBillingDiff(await this.db(), period);
|
|
3204
1389
|
}
|
|
3205
1390
|
async usage(period, agent) {
|
|
3206
|
-
const snapshots = queryUsageSnapshots(this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
3207
|
-
const summary = querySummary(this.db(), period, undefined, true, agent);
|
|
1391
|
+
const snapshots = queryUsageSnapshots(await this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
1392
|
+
const summary = querySummary(await this.db(), period, undefined, true, agent);
|
|
3208
1393
|
return { snapshots, summary };
|
|
3209
1394
|
}
|
|
3210
1395
|
async savings(period, agent) {
|
|
3211
|
-
return querySavingsSummary(this.db(), period, agent);
|
|
1396
|
+
return querySavingsSummary(await this.db(), period, agent);
|
|
3212
1397
|
}
|
|
3213
1398
|
async listBudgets() {
|
|
3214
|
-
return getBudgetStatuses(this.db());
|
|
1399
|
+
return getBudgetStatuses(await this.db());
|
|
3215
1400
|
}
|
|
3216
1401
|
async listGoals() {
|
|
3217
|
-
return getGoalStatuses(this.db());
|
|
1402
|
+
return getGoalStatuses(await this.db());
|
|
3218
1403
|
}
|
|
3219
1404
|
async listPricing() {
|
|
3220
|
-
return listModelPricing(this.db());
|
|
1405
|
+
return listModelPricing(await this.db());
|
|
3221
1406
|
}
|
|
3222
1407
|
async listSubscriptions() {
|
|
3223
|
-
return listSubscriptions(this.db());
|
|
1408
|
+
return listSubscriptions(await this.db());
|
|
3224
1409
|
}
|
|
3225
1410
|
async listProjects() {
|
|
3226
|
-
return queryProjectBreakdown(this.db());
|
|
1411
|
+
return queryProjectBreakdown(await this.db());
|
|
3227
1412
|
}
|
|
3228
1413
|
async brief(query = {}) {
|
|
3229
|
-
return buildBrief(this.db(), {
|
|
1414
|
+
return buildBrief(await this.db(), {
|
|
3230
1415
|
since: query.since,
|
|
3231
1416
|
machine: query.machine,
|
|
3232
1417
|
currentMachineId: query.currentMachineId,
|
|
@@ -3234,71 +1419,59 @@ class LocalStore {
|
|
|
3234
1419
|
});
|
|
3235
1420
|
}
|
|
3236
1421
|
async projectDetail(nameOrPath) {
|
|
3237
|
-
return queryProjectDetail(this.db(), nameOrPath);
|
|
1422
|
+
return queryProjectDetail(await this.db(), nameOrPath);
|
|
3238
1423
|
}
|
|
3239
1424
|
async exportRows(type, period) {
|
|
3240
|
-
return queryExportRows(this.db(), type, period);
|
|
1425
|
+
return queryExportRows(await this.db(), type, period);
|
|
3241
1426
|
}
|
|
3242
1427
|
async rangeStats(from, to) {
|
|
3243
|
-
return queryRangeStats(this.db(), from, to);
|
|
1428
|
+
return queryRangeStats(await this.db(), from, to);
|
|
3244
1429
|
}
|
|
3245
1430
|
async forecast() {
|
|
3246
|
-
return queryForecast(this.db());
|
|
1431
|
+
return queryForecast(await this.db());
|
|
3247
1432
|
}
|
|
3248
1433
|
async efficiency() {
|
|
3249
|
-
return queryModelEfficiency(this.db());
|
|
1434
|
+
return queryModelEfficiency(await this.db());
|
|
3250
1435
|
}
|
|
3251
1436
|
async recentRequests(since) {
|
|
3252
|
-
return queryRequestsSince(this.db(), since);
|
|
1437
|
+
return queryRequestsSince(await this.db(), since);
|
|
3253
1438
|
}
|
|
3254
1439
|
async estimate(input) {
|
|
3255
|
-
return estimateCostFromRows(listModelPricing(this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
1440
|
+
return estimateCostFromRows(listModelPricing(await this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
3256
1441
|
}
|
|
3257
1442
|
async setBudget(input) {
|
|
3258
1443
|
const now = new Date().toISOString();
|
|
3259
1444
|
const budget = { id: randomUUID(), ...input, created_at: now, updated_at: now };
|
|
3260
|
-
upsertBudget(this.db(), budget);
|
|
1445
|
+
upsertBudget(await this.db(), budget);
|
|
3261
1446
|
return budget.id;
|
|
3262
1447
|
}
|
|
3263
1448
|
async removeBudget(id) {
|
|
3264
|
-
deleteBudget(this.db(), id);
|
|
1449
|
+
deleteBudget(await this.db(), id);
|
|
3265
1450
|
}
|
|
3266
1451
|
async setGoal(input) {
|
|
3267
1452
|
const now = new Date().toISOString();
|
|
3268
|
-
upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
|
|
1453
|
+
upsertGoal(await this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
|
|
3269
1454
|
}
|
|
3270
1455
|
async removeGoal(id) {
|
|
3271
|
-
deleteGoal(this.db(), id);
|
|
1456
|
+
deleteGoal(await this.db(), id);
|
|
3272
1457
|
}
|
|
3273
1458
|
async setPricing(input) {
|
|
3274
|
-
upsertModelPricing(this.db(), { ...input, updated_at: new Date().toISOString() });
|
|
1459
|
+
upsertModelPricing(await this.db(), { ...input, updated_at: new Date().toISOString() });
|
|
3275
1460
|
}
|
|
3276
1461
|
async removePricing(model) {
|
|
3277
|
-
deleteModelPricing(this.db(), model);
|
|
1462
|
+
deleteModelPricing(await this.db(), model);
|
|
3278
1463
|
}
|
|
3279
1464
|
async setSubscription(input) {
|
|
3280
|
-
const
|
|
3281
|
-
const
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
plan: input.plan,
|
|
3285
|
-
agent: input.agent,
|
|
3286
|
-
monthly_fee_usd: input.monthly_fee_usd,
|
|
3287
|
-
included_usage_usd: input.included_usage_usd,
|
|
3288
|
-
billing_cycle_start: input.billing_cycle_start,
|
|
3289
|
-
reset_policy: input.reset_policy,
|
|
3290
|
-
active: input.active,
|
|
3291
|
-
created_at: now,
|
|
3292
|
-
updated_at: now
|
|
3293
|
-
};
|
|
3294
|
-
upsertSubscription(this.db(), row);
|
|
3295
|
-
return row;
|
|
1465
|
+
const row = configuredSubscription(input);
|
|
1466
|
+
const db = await this.db();
|
|
1467
|
+
upsertSubscription(db, row);
|
|
1468
|
+
return db.get("SELECT * FROM subscriptions WHERE id = ?", row.id);
|
|
3296
1469
|
}
|
|
3297
1470
|
async removeSubscription(id) {
|
|
3298
|
-
deleteSubscription(this.db(), id);
|
|
1471
|
+
deleteSubscription(await this.db(), id);
|
|
3299
1472
|
}
|
|
3300
1473
|
async addProject(path, name) {
|
|
3301
|
-
upsertProject(this.db(), {
|
|
1474
|
+
upsertProject(await this.db(), {
|
|
3302
1475
|
id: randomUUID(),
|
|
3303
1476
|
path,
|
|
3304
1477
|
name,
|
|
@@ -3308,16 +1481,16 @@ class LocalStore {
|
|
|
3308
1481
|
});
|
|
3309
1482
|
}
|
|
3310
1483
|
async renameProject(path, name) {
|
|
3311
|
-
const existing = getProject(this.db(), path);
|
|
1484
|
+
const existing = getProject(await this.db(), path);
|
|
3312
1485
|
if (!existing)
|
|
3313
1486
|
throw new Error("Project not found");
|
|
3314
|
-
upsertProject(this.db(), { ...existing, name });
|
|
1487
|
+
upsertProject(await this.db(), { ...existing, name });
|
|
3315
1488
|
}
|
|
3316
1489
|
async removeProject(path) {
|
|
3317
|
-
deleteProject(this.db(), path);
|
|
1490
|
+
deleteProject(await this.db(), path);
|
|
3318
1491
|
}
|
|
3319
1492
|
async sendFeedback(input) {
|
|
3320
|
-
insertFeedback(this.db(), {
|
|
1493
|
+
insertFeedback(await this.db(), {
|
|
3321
1494
|
message: input.message,
|
|
3322
1495
|
email: input.email ?? null,
|
|
3323
1496
|
category: input.category ?? "general",
|
|
@@ -3341,7 +1514,7 @@ class ApiStore {
|
|
|
3341
1514
|
this.cloud = cloud;
|
|
3342
1515
|
}
|
|
3343
1516
|
async summary(period, machine) {
|
|
3344
|
-
return cloudObject(this.cloud, "/summary", q({ period, machine }));
|
|
1517
|
+
return requireCurrentCostSummary(await cloudObject(this.cloud, "/summary", q({ period, machine })));
|
|
3345
1518
|
}
|
|
3346
1519
|
async sessions(filter) {
|
|
3347
1520
|
return cloudListItems(this.cloud, "sessions", q({ ...filter }));
|
|
@@ -3389,22 +1562,23 @@ class ApiStore {
|
|
|
3389
1562
|
async fleet(period) {
|
|
3390
1563
|
const data = await cloudObject(this.cloud, "/fleet", q({ period }));
|
|
3391
1564
|
return {
|
|
3392
|
-
summary: data.summary,
|
|
1565
|
+
summary: requireCurrentCostSummary(data.summary),
|
|
3393
1566
|
machines: data.machines ?? [],
|
|
3394
1567
|
registry: data.registry ?? []
|
|
3395
1568
|
};
|
|
3396
1569
|
}
|
|
3397
1570
|
async billingSummary(period) {
|
|
3398
|
-
return cloudObject(this.cloud, "/billing", q({ period }));
|
|
1571
|
+
return requireCurrentBillingSummary(await cloudObject(this.cloud, "/billing", q({ period })));
|
|
3399
1572
|
}
|
|
3400
1573
|
async billingDiff(period) {
|
|
3401
|
-
return cloudObject(this.cloud, "/billing/diff", q({ period }));
|
|
1574
|
+
return requireCurrentBillingDiffSummary(await cloudObject(this.cloud, "/billing/diff", q({ period })));
|
|
3402
1575
|
}
|
|
3403
1576
|
async usage(period, agent) {
|
|
3404
|
-
|
|
1577
|
+
const data = await cloudObject(this.cloud, "/usage", q({ period, agent }));
|
|
1578
|
+
return { ...data, summary: requireCurrentCostSummary(data.summary) };
|
|
3405
1579
|
}
|
|
3406
1580
|
async savings(period, agent) {
|
|
3407
|
-
return cloudObject(this.cloud, "/savings", q({ period, agent }));
|
|
1581
|
+
return requireCurrentSavingsReport(await cloudObject(this.cloud, "/savings", q({ period, agent })));
|
|
3408
1582
|
}
|
|
3409
1583
|
async listBudgets() {
|
|
3410
1584
|
return cloudListItems(this.cloud, "budgets");
|
|
@@ -3478,22 +1652,8 @@ class ApiStore {
|
|
|
3478
1652
|
await this.cloud.client.delete("pricing", model);
|
|
3479
1653
|
}
|
|
3480
1654
|
async setSubscription(input) {
|
|
3481
|
-
const
|
|
3482
|
-
|
|
3483
|
-
id: input.id ?? randomUUID(),
|
|
3484
|
-
provider: input.provider,
|
|
3485
|
-
plan: input.plan,
|
|
3486
|
-
agent: input.agent,
|
|
3487
|
-
monthly_fee_usd: input.monthly_fee_usd,
|
|
3488
|
-
included_usage_usd: input.included_usage_usd,
|
|
3489
|
-
billing_cycle_start: input.billing_cycle_start,
|
|
3490
|
-
reset_policy: input.reset_policy,
|
|
3491
|
-
active: input.active,
|
|
3492
|
-
created_at: now,
|
|
3493
|
-
updated_at: now
|
|
3494
|
-
};
|
|
3495
|
-
await this.cloud.client.create("subscriptions", row);
|
|
3496
|
-
return row;
|
|
1655
|
+
const row = configuredSubscription(input);
|
|
1656
|
+
return this.cloud.client.create("subscriptions", row);
|
|
3497
1657
|
}
|
|
3498
1658
|
async removeSubscription(id) {
|
|
3499
1659
|
await this.cloud.client.delete("subscriptions", id);
|
|
@@ -3523,9 +1683,725 @@ function getStore(env = process.env) {
|
|
|
3523
1683
|
function isCloudStore(env = process.env) {
|
|
3524
1684
|
return economyCloudStorage(env).active;
|
|
3525
1685
|
}
|
|
1686
|
+
// src/lib/ingest-journal-client.ts
|
|
1687
|
+
import { z as z2 } from "zod";
|
|
1688
|
+
|
|
1689
|
+
// src/lib/ingest-journal.ts
|
|
1690
|
+
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
1691
|
+
import { createHash } from "crypto";
|
|
1692
|
+
import { join, resolve } from "path";
|
|
1693
|
+
var MAX_COMMANDS = 1000;
|
|
1694
|
+
var MAX_BYTES = 64 * 1024 * 1024;
|
|
1695
|
+
var RECEIPT_RESERVE = 4096;
|
|
1696
|
+
var MAINTENANCE_RESERVE = 16384;
|
|
1697
|
+
var sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
1698
|
+
|
|
1699
|
+
class PendingIngestError extends Error {
|
|
1700
|
+
pending_keys;
|
|
1701
|
+
code = "INGEST_RECONCILIATION_REQUIRED";
|
|
1702
|
+
constructor(pending_keys) {
|
|
1703
|
+
super("Pending ingestion requires hosted receipt reconciliation; no command was replayed");
|
|
1704
|
+
this.pending_keys = pending_keys;
|
|
1705
|
+
this.name = "PendingIngestError";
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function definitiveIngestRefusal(status, body) {
|
|
1709
|
+
const reasons = { 400: "invalid_ingest", 409: "ingest_conflict", 413: "ingest_too_large", 422: "unprocessable_ingest" };
|
|
1710
|
+
if (!Object.hasOwn(reasons, status))
|
|
1711
|
+
return null;
|
|
1712
|
+
const legacy = body?.expected === "legacy_aggregate_requires_migration";
|
|
1713
|
+
return {
|
|
1714
|
+
status,
|
|
1715
|
+
code: legacy ? "legacy_aggregate_requires_migration" : "ingest_refused",
|
|
1716
|
+
reason: legacy ? "legacy_aggregate_requires_migration" : reasons[status]
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function getIngestJournalPath() {
|
|
1720
|
+
return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
|
|
1721
|
+
}
|
|
1722
|
+
function targetId(cloud) {
|
|
1723
|
+
const url = new URL(cloud.client.transport.baseUrl);
|
|
1724
|
+
if (url.username || url.password || url.search || url.hash)
|
|
1725
|
+
throw new Error("Ingest authority must not contain URL credentials or capabilities");
|
|
1726
|
+
return sha(url.toString().replace(/\/+$/, ""));
|
|
1727
|
+
}
|
|
1728
|
+
function secureDirectory(path) {
|
|
1729
|
+
mkdirSync(path, { recursive: true, mode: 448 });
|
|
1730
|
+
const stat = lstatSync(path);
|
|
1731
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
1732
|
+
throw new Error("Ingest journal must be a private owner directory");
|
|
1733
|
+
}
|
|
1734
|
+
function readJson(path, max) {
|
|
1735
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1736
|
+
try {
|
|
1737
|
+
const st = fstatSync(fd);
|
|
1738
|
+
if (!st.isFile() || st.size > max || (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid())
|
|
1739
|
+
throw new Error("Unsafe ingest journal record");
|
|
1740
|
+
return JSON.parse(readFileSync(fd, "utf8"));
|
|
1741
|
+
} finally {
|
|
1742
|
+
closeSync(fd);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function createExact(path, value) {
|
|
1746
|
+
const bytes = JSON.stringify(value);
|
|
1747
|
+
let fd;
|
|
1748
|
+
try {
|
|
1749
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
if (error.code === "EEXIST")
|
|
1752
|
+
return false;
|
|
1753
|
+
throw error;
|
|
1754
|
+
}
|
|
1755
|
+
try {
|
|
1756
|
+
writeFileSync(fd, bytes);
|
|
1757
|
+
fsyncSync(fd);
|
|
1758
|
+
} finally {
|
|
1759
|
+
closeSync(fd);
|
|
1760
|
+
}
|
|
1761
|
+
if (JSON.stringify(readJson(path, MAX_INGEST_BYTES + 4096)) !== bytes)
|
|
1762
|
+
throw new Error("Ingest journal readback failed");
|
|
1763
|
+
const directory = openSync(resolve(path, ".."), constants.O_RDONLY);
|
|
1764
|
+
try {
|
|
1765
|
+
fsyncSync(directory);
|
|
1766
|
+
} finally {
|
|
1767
|
+
closeSync(directory);
|
|
1768
|
+
}
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
function unwrap(raw) {
|
|
1772
|
+
return raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
|
|
1773
|
+
}
|
|
1774
|
+
function verifyIngestReceipt(raw, command) {
|
|
1775
|
+
const receipt = unwrap(raw);
|
|
1776
|
+
if (!receipt || typeof receipt !== "object" || !receipt.ingested || typeof receipt.ingested !== "object" || Array.isArray(receipt.ingested) || !Number.isSafeInteger(receipt.total) || receipt.total < 0 || typeof receipt.operation_key !== "string" || !/^[a-f0-9]{64}$/.test(receipt.operation_key) || typeof receipt.replayed !== "boolean")
|
|
1777
|
+
throw new Error("Invalid ingest receipt");
|
|
1778
|
+
let sum = 0;
|
|
1779
|
+
for (const [table, count] of Object.entries(receipt.ingested)) {
|
|
1780
|
+
if (!Array.isArray(command[table]) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0 || count > command[table].length)
|
|
1781
|
+
throw new Error("Invalid ingest receipt");
|
|
1782
|
+
sum += count;
|
|
1783
|
+
}
|
|
1784
|
+
if (sum !== receipt.total)
|
|
1785
|
+
throw new Error("Invalid ingest receipt");
|
|
1786
|
+
const projected = {
|
|
1787
|
+
ingested: Object.fromEntries(Object.entries(receipt.ingested)),
|
|
1788
|
+
total: receipt.total,
|
|
1789
|
+
operation_key: receipt.operation_key,
|
|
1790
|
+
replayed: receipt.replayed,
|
|
1791
|
+
stale: receipt.stale ?? 0,
|
|
1792
|
+
unchanged: receipt.unchanged ?? 0
|
|
1793
|
+
};
|
|
1794
|
+
const rows = Object.values(command).reduce((n, v) => n + (Array.isArray(v) ? v.length : 0), 0);
|
|
1795
|
+
if (![projected.stale, projected.unchanged].every((n) => Number.isSafeInteger(n) && n >= 0 && n <= rows) || Buffer.byteLength(JSON.stringify(projected)) > RECEIPT_RESERVE - 256)
|
|
1796
|
+
throw new Error("Invalid ingest receipt");
|
|
1797
|
+
return projected;
|
|
1798
|
+
}
|
|
1799
|
+
function sameReceipt(left, right) {
|
|
1800
|
+
const meaning = (value) => ({ ingested: value?.ingested, total: value?.total, operation_key: value?.operation_key, stale: value?.stale ?? 0, unchanged: value?.unchanged ?? 0 });
|
|
1801
|
+
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
1802
|
+
}
|
|
1803
|
+
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
1804
|
+
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
1805
|
+
const raw = unwrap(await cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs }));
|
|
1806
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
1807
|
+
throw new Error("Invalid reconciliation response");
|
|
1808
|
+
if (raw.status === "absent")
|
|
1809
|
+
return null;
|
|
1810
|
+
if (raw.status !== "committed")
|
|
1811
|
+
throw new Error("Invalid reconciliation response");
|
|
1812
|
+
return raw.receipt;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
class IngestJournal {
|
|
1816
|
+
directory;
|
|
1817
|
+
target;
|
|
1818
|
+
inventoryBytes = 0;
|
|
1819
|
+
freshlyStaged = new Set;
|
|
1820
|
+
constructor(cloud, path = getIngestJournalPath()) {
|
|
1821
|
+
this.target = targetId(cloud);
|
|
1822
|
+
secureDirectory(path);
|
|
1823
|
+
this.directory = join(realpathSync(path), this.target);
|
|
1824
|
+
secureDirectory(this.directory);
|
|
1825
|
+
}
|
|
1826
|
+
names() {
|
|
1827
|
+
const names = [];
|
|
1828
|
+
const directory = opendirSync(this.directory);
|
|
1829
|
+
try {
|
|
1830
|
+
for (let entry;entry = directory.readSync(); ) {
|
|
1831
|
+
if (entry.name === ".lease" || entry.name === ".maintenance" || entry.name === ".authority.json" || entry.name === ".recovery")
|
|
1832
|
+
continue;
|
|
1833
|
+
names.push(entry.name);
|
|
1834
|
+
if (names.length > MAX_COMMANDS * 2)
|
|
1835
|
+
throw new Error("Ingest journal capacity reached; preserve and reconcile records");
|
|
1836
|
+
}
|
|
1837
|
+
} finally {
|
|
1838
|
+
directory.closeSync();
|
|
1839
|
+
}
|
|
1840
|
+
this.inventoryBytes = 0;
|
|
1841
|
+
const set = new Set(names);
|
|
1842
|
+
for (const name of names) {
|
|
1843
|
+
if (!/^[a-f0-9]{64}\.(intent|receipt|refusal)\.json$/.test(name))
|
|
1844
|
+
throw new Error("Unexpected ingest journal entry");
|
|
1845
|
+
if (!name.endsWith(".intent.json") && !set.has(name.replace(/\.(receipt|refusal)\./, ".intent.")))
|
|
1846
|
+
throw new Error("Orphaned ingest outcome requires maintenance recovery");
|
|
1847
|
+
if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
|
|
1848
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
1849
|
+
const stat = lstatSync(join(this.directory, name));
|
|
1850
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
|
|
1851
|
+
throw new Error("Unsafe ingest journal record");
|
|
1852
|
+
if (stat.size > MAX_INGEST_BYTES + 4096)
|
|
1853
|
+
throw new Error("Oversized ingest journal record");
|
|
1854
|
+
this.inventoryBytes += stat.size;
|
|
1855
|
+
if (name.endsWith(".refusal.json"))
|
|
1856
|
+
this.refusal(this.intent(name.replace(".refusal.", ".intent.")).command);
|
|
1857
|
+
}
|
|
1858
|
+
return names;
|
|
1859
|
+
}
|
|
1860
|
+
file(key, kind) {
|
|
1861
|
+
return join(this.directory, `${sha(key)}.${kind}.json`);
|
|
1862
|
+
}
|
|
1863
|
+
intent(name) {
|
|
1864
|
+
const record = readJson(join(this.directory, name), MAX_INGEST_BYTES + 4096);
|
|
1865
|
+
if (record.version !== 1 || record.target !== this.target || !record.command || typeof record.command.idempotency_key !== "string" || record.body_hash !== canonicalIngestBodyHash(record.command) || name !== `${sha(record.command.idempotency_key)}.intent.json`)
|
|
1866
|
+
throw new Error("Invalid ingest journal intent");
|
|
1867
|
+
validateIngest(record.command);
|
|
1868
|
+
return record;
|
|
1869
|
+
}
|
|
1870
|
+
status() {
|
|
1871
|
+
const names = this.names(), intents = names.filter((n) => n.endsWith(".intent.json"));
|
|
1872
|
+
const set = new Set(names);
|
|
1873
|
+
const completed = intents.filter((name) => set.has(name.replace(".intent.", ".receipt."))).length;
|
|
1874
|
+
const rejected = intents.filter((name) => set.has(name.replace(".intent.", ".refusal."))).length;
|
|
1875
|
+
const bytes = this.inventoryBytes;
|
|
1876
|
+
return { target: this.target, commands: intents.length, completed, rejected, pending: intents.length - completed - rejected, bytes, reserved_bytes: bytes + (intents.length - completed - rejected) * RECEIPT_RESERVE + MAINTENANCE_RESERVE };
|
|
1877
|
+
}
|
|
1878
|
+
canStage(command, status = this.status()) {
|
|
1879
|
+
const name = `${sha(command.idempotency_key)}.intent.json`;
|
|
1880
|
+
try {
|
|
1881
|
+
if (this.intent(name).body_hash !== canonicalIngestBodyHash(command))
|
|
1882
|
+
throw new Error("Ingest command key is already bound to different content");
|
|
1883
|
+
return true;
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
if (error.code !== "ENOENT")
|
|
1886
|
+
throw error;
|
|
1887
|
+
}
|
|
1888
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
|
|
1889
|
+
return status.commands < MAX_COMMANDS && status.reserved_bytes + Buffer.byteLength(JSON.stringify(record)) + RECEIPT_RESERVE <= MAX_BYTES;
|
|
1890
|
+
}
|
|
1891
|
+
receipt(command) {
|
|
1892
|
+
if (this.refusal(command))
|
|
1893
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
1894
|
+
const record = readJson(this.file(command.idempotency_key, "receipt"), RECEIPT_RESERVE);
|
|
1895
|
+
if (record.version !== 1 || record.target !== this.target || record.body_hash !== canonicalIngestBodyHash(command))
|
|
1896
|
+
throw new Error("Invalid receipt journal binding");
|
|
1897
|
+
return verifyIngestReceipt(record.receipt, command);
|
|
1898
|
+
}
|
|
1899
|
+
stage(command) {
|
|
1900
|
+
validateIngest(command);
|
|
1901
|
+
const path = this.file(command.idempotency_key, "intent");
|
|
1902
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
|
|
1903
|
+
if (!this.canStage(command))
|
|
1904
|
+
throw new Error("Ingest journal capacity reached; run economy ingest journal maintain; originals preserved");
|
|
1905
|
+
if (createExact(path, record)) {
|
|
1906
|
+
this.freshlyStaged.add(command.idempotency_key);
|
|
1907
|
+
return true;
|
|
1908
|
+
}
|
|
1909
|
+
this.freshlyStaged.delete(command.idempotency_key);
|
|
1910
|
+
const old = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
1911
|
+
if (old.body_hash !== record.body_hash)
|
|
1912
|
+
throw new Error("Ingest command key is already bound to different content");
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
accept(command, raw) {
|
|
1916
|
+
if (this.refusal(command))
|
|
1917
|
+
throw new Error("A refused command cannot accept a receipt");
|
|
1918
|
+
this.freshlyStaged.delete(command.idempotency_key);
|
|
1919
|
+
const receipt = verifyIngestReceipt(raw, command);
|
|
1920
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
1921
|
+
if (intent.body_hash !== canonicalIngestBodyHash(command))
|
|
1922
|
+
throw new Error("Ingest receipt content conflicts with exact intent");
|
|
1923
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), receipt };
|
|
1924
|
+
const path = this.file(command.idempotency_key, "receipt");
|
|
1925
|
+
if (!this.names().includes(`${sha(command.idempotency_key)}.receipt.json`) && this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
|
|
1926
|
+
throw new Error("Receipt capacity exhausted; exact intent retained for reconciliation");
|
|
1927
|
+
if (!createExact(path, record)) {
|
|
1928
|
+
const old = readJson(path, MAX_INGEST_BYTES);
|
|
1929
|
+
if (old.body_hash !== record.body_hash || old.target !== this.target || !sameReceipt(old.receipt, receipt))
|
|
1930
|
+
throw new Error("Ingest receipt journal conflict");
|
|
1931
|
+
}
|
|
1932
|
+
return receipt;
|
|
1933
|
+
}
|
|
1934
|
+
recordRefusal(command, refusal) {
|
|
1935
|
+
if (!this.freshlyStaged.delete(command.idempotency_key))
|
|
1936
|
+
throw new Error("Refusal requires this invocation's fresh command");
|
|
1937
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
1938
|
+
if (intent.body_hash !== canonicalIngestBodyHash(command))
|
|
1939
|
+
throw new Error("Refusal conflicts with exact intent");
|
|
1940
|
+
const expected = definitiveIngestRefusal(refusal.status, refusal.code === "legacy_aggregate_requires_migration" ? { expected: refusal.code } : null);
|
|
1941
|
+
if (!expected || stableJson(expected) !== stableJson(refusal))
|
|
1942
|
+
throw new Error("Invalid definitive refusal");
|
|
1943
|
+
const names = this.names();
|
|
1944
|
+
if (names.includes(`${sha(command.idempotency_key)}.receipt.json`))
|
|
1945
|
+
throw new Error("A completed command cannot be refused");
|
|
1946
|
+
const record = { version: 1, target: this.target, body_hash: intent.body_hash, refusal };
|
|
1947
|
+
if (Buffer.byteLength(JSON.stringify(record)) > RECEIPT_RESERVE || this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
|
|
1948
|
+
throw new Error("Refusal capacity exhausted; exact intent retained");
|
|
1949
|
+
if (!createExact(this.file(command.idempotency_key, "refusal"), record))
|
|
1950
|
+
throw new Error("Existing refusal requires inspection");
|
|
1951
|
+
this.refusal(command);
|
|
1952
|
+
}
|
|
1953
|
+
refusal(command) {
|
|
1954
|
+
let record;
|
|
1955
|
+
try {
|
|
1956
|
+
record = readJson(this.file(command.idempotency_key, "refusal"), RECEIPT_RESERVE);
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
if (error.code === "ENOENT")
|
|
1959
|
+
return null;
|
|
1960
|
+
throw error;
|
|
1961
|
+
}
|
|
1962
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`), r = record?.refusal;
|
|
1963
|
+
const expected = definitiveIngestRefusal(r?.status, r?.code === "legacy_aggregate_requires_migration" ? { expected: r.code } : null);
|
|
1964
|
+
if (record?.version !== 1 || record.target !== this.target || record.body_hash !== intent.body_hash || intent.body_hash !== canonicalIngestBodyHash(command) || !expected || stableJson(expected) !== stableJson(r))
|
|
1965
|
+
throw new Error("Invalid refusal journal binding");
|
|
1966
|
+
try {
|
|
1967
|
+
lstatSync(this.file(command.idempotency_key, "receipt"));
|
|
1968
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
if (error.code !== "ENOENT")
|
|
1971
|
+
throw error;
|
|
1972
|
+
}
|
|
1973
|
+
return expected;
|
|
1974
|
+
}
|
|
1975
|
+
async reconcileCommand(cloud, command, timeoutMs = 1e4) {
|
|
1976
|
+
const receipt = await this.lookupReceipt(cloud, command, timeoutMs);
|
|
1977
|
+
return receipt ? this.accept(command, receipt) : null;
|
|
1978
|
+
}
|
|
1979
|
+
assertCloud(cloud) {
|
|
1980
|
+
if (targetId(cloud) !== this.target)
|
|
1981
|
+
throw new Error("Ingest journal authority changed");
|
|
1982
|
+
}
|
|
1983
|
+
async lookupReceipt(cloud, command, timeoutMs = 1e4) {
|
|
1984
|
+
this.assertCloud(cloud);
|
|
1985
|
+
const receipt = await lookupJournalReceipt(cloud, command.idempotency_key, canonicalIngestBodyHash(command), timeoutMs);
|
|
1986
|
+
return receipt === null ? null : verifyIngestReceipt(receipt, command);
|
|
1987
|
+
}
|
|
1988
|
+
async reconcilePending(cloud) {
|
|
1989
|
+
const names = this.names(), unresolved = [];
|
|
1990
|
+
const deadline = performance.now() + 30000;
|
|
1991
|
+
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
1992
|
+
const intent = this.intent(name);
|
|
1993
|
+
if (names.includes(`${sha(intent.command.idempotency_key)}.refusal.json`)) {
|
|
1994
|
+
this.refusal(intent.command);
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
if (names.includes(`${sha(intent.command.idempotency_key)}.receipt.json`)) {
|
|
1998
|
+
const record = readJson(this.file(intent.command.idempotency_key, "receipt"), MAX_INGEST_BYTES);
|
|
1999
|
+
if (record.target !== this.target || record.body_hash !== intent.body_hash)
|
|
2000
|
+
throw new Error("Invalid receipt journal binding");
|
|
2001
|
+
verifyIngestReceipt(record.receipt, intent.command);
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
if (performance.now() >= deadline) {
|
|
2005
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
try {
|
|
2009
|
+
if (!await this.reconcileCommand(cloud, intent.command, Math.max(1, Math.min(1e4, deadline - performance.now()))))
|
|
2010
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2011
|
+
} catch {
|
|
2012
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (unresolved.length)
|
|
2016
|
+
throw new PendingIngestError(unresolved);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// src/lib/ingest-journal-maintenance.ts
|
|
2021
|
+
import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
2022
|
+
import { createHash as createHash2 } from "crypto";
|
|
2023
|
+
import { join as join2 } from "path";
|
|
2024
|
+
var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
2025
|
+
var digest = /^[a-f0-9]{64}$/;
|
|
2026
|
+
var sha2 = (bytes) => createHash2("sha256").update(bytes).digest("hex");
|
|
2027
|
+
|
|
2028
|
+
class JournalMaintenanceError extends Error {
|
|
2029
|
+
reason;
|
|
2030
|
+
target;
|
|
2031
|
+
code = "INGEST_JOURNAL_MAINTENANCE_REQUIRED";
|
|
2032
|
+
constructor(reason, target) {
|
|
2033
|
+
super(`Ingest journal maintenance required (${reason}); run economy ingest journal maintain; originals or Trash recovery state preserved`);
|
|
2034
|
+
this.reason = reason;
|
|
2035
|
+
this.target = target;
|
|
2036
|
+
this.name = "JournalMaintenanceError";
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
function exists(path) {
|
|
2040
|
+
try {
|
|
2041
|
+
lstatSync2(path);
|
|
2042
|
+
return true;
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
if (error.code === "ENOENT")
|
|
2045
|
+
return false;
|
|
2046
|
+
throw error;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
function snapshot(path) {
|
|
2050
|
+
const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
|
|
2051
|
+
try {
|
|
2052
|
+
const stat = fstatSync2(fd);
|
|
2053
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 4 * 1024 * 1024 + 4096 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
2054
|
+
throw new Error("Unsafe maintenance source");
|
|
2055
|
+
const bytes = readFileSync2(fd), after = fstatSync2(fd);
|
|
2056
|
+
if (after.ctimeMs !== stat.ctimeMs || bytes.length !== stat.size)
|
|
2057
|
+
throw new Error("Maintenance source changed");
|
|
2058
|
+
return { sha256: sha2(bytes), sizeBytes: stat.size, mode: stat.mode & 511, device: String(stat.dev), inode: String(stat.ino), ctime: stat.ctimeMs };
|
|
2059
|
+
} finally {
|
|
2060
|
+
closeSync2(fd);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
function sameSource(path, expected) {
|
|
2064
|
+
const { capture_sha256: _capture, ...original } = expected;
|
|
2065
|
+
if (JSON.stringify(snapshot(path)) !== JSON.stringify(original))
|
|
2066
|
+
throw new Error("Maintenance source changed; preserve for inspection");
|
|
2067
|
+
}
|
|
2068
|
+
function validAnchor(value, target) {
|
|
2069
|
+
const a = value;
|
|
2070
|
+
if (a?.version !== 1 || a.target !== target || !/^[A-Za-z0-9._:-]{1,200}$/.test(a.key) || !digest.test(a.body_hash) || !digest.test(a.receipt?.operation_key))
|
|
2071
|
+
throw new Error("Invalid journal authority anchor");
|
|
2072
|
+
return a;
|
|
2073
|
+
}
|
|
2074
|
+
async function confirm(cloud, anchor, deadline) {
|
|
2075
|
+
if (Date.now() >= deadline)
|
|
2076
|
+
throw new Error("Maintenance deadline reached");
|
|
2077
|
+
const current = await lookupJournalReceipt(cloud, anchor.key, anchor.body_hash, Math.max(1, Math.min(1e4, deadline - Date.now())));
|
|
2078
|
+
if (current === null || !sameReceipt(current, anchor.receipt))
|
|
2079
|
+
throw new Error("Exact Economy receipt is unavailable or changed");
|
|
2080
|
+
}
|
|
2081
|
+
async function verifyJournalAuthority(journal, cloud) {
|
|
2082
|
+
journal.assertCloud(cloud);
|
|
2083
|
+
const path = join2(journal.directory, ".authority.json");
|
|
2084
|
+
if (!exists(path)) {
|
|
2085
|
+
const names = journal.names();
|
|
2086
|
+
const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
|
|
2087
|
+
if (!name)
|
|
2088
|
+
return;
|
|
2089
|
+
const intent = journal.intent(name), receipt = journal.receipt(intent.command);
|
|
2090
|
+
createExact(path, { version: 1, target: journal.target, key: intent.command.idempotency_key, body_hash: intent.body_hash, receipt });
|
|
2091
|
+
}
|
|
2092
|
+
await confirm(cloud, validAnchor(readJson(path, 4096), journal.target), Date.now() + 1e4);
|
|
2093
|
+
}
|
|
2094
|
+
function recoveryFactory(cloud, env = process.env) {
|
|
2095
|
+
return async (operationRoot, checkpoint, deadline) => {
|
|
2096
|
+
if (env.HASNA_ECONOMY_INGEST_RECOVERY !== "trash")
|
|
2097
|
+
throw new Error("recovery_not_configured");
|
|
2098
|
+
const economy = new URL(cloud.client.transport.baseUrl);
|
|
2099
|
+
const fleet = economy.origin === "https://api.hasna.com" && /^\/economy(?:\/v1)?\/?$/.test(economy.pathname);
|
|
2100
|
+
if (!fleet && !env.HASNA_TRASH_API_URL)
|
|
2101
|
+
throw new Error("self_host_trash_authority_required");
|
|
2102
|
+
const sdk = await import("@hasna/trash/sdk");
|
|
2103
|
+
if (sdk.VERSION !== "0.2.3")
|
|
2104
|
+
throw new Error("trash_version_mismatch");
|
|
2105
|
+
const boundedFetch = (input, init) => {
|
|
2106
|
+
const remaining = deadline - Date.now();
|
|
2107
|
+
if (remaining <= 0)
|
|
2108
|
+
throw new Error("maintenance_deadline");
|
|
2109
|
+
const signal = AbortSignal.timeout(remaining);
|
|
2110
|
+
return fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal, redirect: "error" });
|
|
2111
|
+
};
|
|
2112
|
+
return Object.assign(sdk.createTrash({ env, operationRoot, checkpoint, fetchImpl: boundedFetch, transferFetch: boundedFetch, lockTimeoutMs: 100 }), { snapshot: sdk.snapshotIdentity });
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
class IngestJournalMaintenance {
|
|
2117
|
+
journal;
|
|
2118
|
+
cloud;
|
|
2119
|
+
factory;
|
|
2120
|
+
control;
|
|
2121
|
+
operationRoot;
|
|
2122
|
+
constructor(journal, cloud, factory = recoveryFactory(cloud)) {
|
|
2123
|
+
this.journal = journal;
|
|
2124
|
+
this.cloud = cloud;
|
|
2125
|
+
this.factory = factory;
|
|
2126
|
+
journal.assertCloud(cloud);
|
|
2127
|
+
this.control = join2(journal.directory, ".maintenance");
|
|
2128
|
+
this.operationRoot = join2(journal.directory, ".recovery");
|
|
2129
|
+
}
|
|
2130
|
+
manifest() {
|
|
2131
|
+
secureDirectory(this.control);
|
|
2132
|
+
const names = readdirSync(this.control);
|
|
2133
|
+
if (names.some((n) => !["manifest.json", "intent.operation.json", "receipt.operation.json"].includes(n)))
|
|
2134
|
+
throw new Error("Unexpected maintenance entry");
|
|
2135
|
+
let size = 0;
|
|
2136
|
+
for (const name of names)
|
|
2137
|
+
size += lstatSync2(join2(this.control, name)).size;
|
|
2138
|
+
if (size > MAINTENANCE_RESERVE)
|
|
2139
|
+
throw new Error("Maintenance metadata exceeds reserve");
|
|
2140
|
+
const m = validAnchor(readJson(join2(this.control, "manifest.json"), 8192), this.journal.target);
|
|
2141
|
+
for (const part of [m.intent, m.receipt_file])
|
|
2142
|
+
if (!part || !digest.test(part.sha256) || !digest.test(part.capture_sha256 ?? "") || !Number.isSafeInteger(part.sizeBytes) || part.sizeBytes < 1 || part.sizeBytes > 4198400 || part.mode !== 384 || !/^\d+$/.test(part.device) || !/^\d+$/.test(part.inode) || !Number.isFinite(part.ctime))
|
|
2143
|
+
throw new Error("Invalid maintenance snapshot");
|
|
2144
|
+
return m;
|
|
2145
|
+
}
|
|
2146
|
+
pending(backend) {
|
|
2147
|
+
const pending = backend.pending(4);
|
|
2148
|
+
if (pending.length > 3 || pending.some((p) => !uuid.test(p.id) || p.phase === "unreadable"))
|
|
2149
|
+
throw new Error("Unexpected Trash recovery state");
|
|
2150
|
+
return pending;
|
|
2151
|
+
}
|
|
2152
|
+
async backend(deadline) {
|
|
2153
|
+
const backend = await this.factory(this.operationRoot, async (point, operation) => {
|
|
2154
|
+
if (operation.path === this.control)
|
|
2155
|
+
return;
|
|
2156
|
+
const m = this.manifest();
|
|
2157
|
+
const kind = operation.path === this.journal.file(m.key, "receipt") ? "receipt" : operation.path === this.journal.file(m.key, "intent") ? "intent" : null;
|
|
2158
|
+
if (!kind || operation.kind !== "capture" || !uuid.test(operation.id) || operation.id !== operation.entryId)
|
|
2159
|
+
throw new Error("Unexpected Trash operation");
|
|
2160
|
+
const expected = kind === "receipt" ? m.receipt_file : m.intent;
|
|
2161
|
+
if (point === "afterSnapshot") {
|
|
2162
|
+
if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
|
|
2163
|
+
throw new Error("Trash snapshot mismatch");
|
|
2164
|
+
sameSource(operation.path, expected);
|
|
2165
|
+
const path = join2(this.control, `${kind}.operation.json`);
|
|
2166
|
+
createExact(path, { id: operation.id });
|
|
2167
|
+
if (readJson(path, 1024).id !== operation.id)
|
|
2168
|
+
throw new Error("Conflicting Trash operation");
|
|
2169
|
+
}
|
|
2170
|
+
if (point === "beforeStage") {
|
|
2171
|
+
await confirm(this.cloud, m, deadline);
|
|
2172
|
+
sameSource(operation.path, expected);
|
|
2173
|
+
}
|
|
2174
|
+
}, deadline);
|
|
2175
|
+
const status = await backend.doctor();
|
|
2176
|
+
if (!status.station || !uuid.test(status.station.id) || !Number.isInteger(status.retentionDays) || status.retentionDays < 1)
|
|
2177
|
+
throw new Error("Trash station authority unavailable");
|
|
2178
|
+
return backend;
|
|
2179
|
+
}
|
|
2180
|
+
async entry(backend, id, path, expected) {
|
|
2181
|
+
const e = await backend.api.get(id);
|
|
2182
|
+
if (e.id !== id || e.originalPath !== path || e.kind !== "file" || e.sha256 !== expected.capture_sha256 || e.sizeBytes !== expected.sizeBytes || e.mode !== expected.mode || e.state !== "trashed")
|
|
2183
|
+
throw new Error("Trash preservation readback refused");
|
|
2184
|
+
if (exists(path))
|
|
2185
|
+
throw new Error("Retired source reappeared; preserve for inspection");
|
|
2186
|
+
}
|
|
2187
|
+
async pairFile(backend, m, kind, deadline) {
|
|
2188
|
+
await confirm(this.cloud, m, deadline);
|
|
2189
|
+
const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
|
|
2190
|
+
const saved = join2(this.control, `${kind}.operation.json`);
|
|
2191
|
+
let id;
|
|
2192
|
+
if (exists(saved))
|
|
2193
|
+
id = readJson(saved, 1024).id;
|
|
2194
|
+
const pending = this.pending(backend);
|
|
2195
|
+
const matches = pending.filter((p) => p.path === path);
|
|
2196
|
+
if (matches.length > 1 || id && !uuid.test(id))
|
|
2197
|
+
throw new Error("Conflicting Trash operation");
|
|
2198
|
+
if (matches[0]) {
|
|
2199
|
+
if (id && id !== matches[0].id)
|
|
2200
|
+
throw new Error("Conflicting Trash operation");
|
|
2201
|
+
id = matches[0].id;
|
|
2202
|
+
const operation = backend.journal.read(id);
|
|
2203
|
+
if (operation.kind !== "capture" || operation.path !== path || operation.entryId !== id)
|
|
2204
|
+
throw new Error("Invalid Trash operation binding");
|
|
2205
|
+
if (exists(path))
|
|
2206
|
+
sameSource(path, expected);
|
|
2207
|
+
await backend.recover(id, { takeAbandonedLock: true });
|
|
2208
|
+
} else if (!id) {
|
|
2209
|
+
sameSource(path, expected);
|
|
2210
|
+
const status = await backend.doctor();
|
|
2211
|
+
const e = await backend.put(path, { retentionDays: status.retentionDays });
|
|
2212
|
+
id = e.id;
|
|
2213
|
+
if (!exists(saved) || readJson(saved, 1024).id !== id)
|
|
2214
|
+
throw new Error("Trash did not persist its capture binding");
|
|
2215
|
+
}
|
|
2216
|
+
await this.entry(backend, id, path, expected);
|
|
2217
|
+
}
|
|
2218
|
+
async recover() {
|
|
2219
|
+
if (!exists(this.control) && !exists(this.operationRoot))
|
|
2220
|
+
return;
|
|
2221
|
+
if (exists(this.operationRoot))
|
|
2222
|
+
secureDirectory(this.operationRoot);
|
|
2223
|
+
if (!exists(this.control) && readdirSync(this.operationRoot).every((n) => n === ".locks"))
|
|
2224
|
+
return;
|
|
2225
|
+
const deadline = Date.now() + 30000;
|
|
2226
|
+
try {
|
|
2227
|
+
await this.finish(await this.backend(deadline), deadline);
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
if (error instanceof JournalMaintenanceError)
|
|
2230
|
+
throw error;
|
|
2231
|
+
throw new JournalMaintenanceError("recovery_unresolved", this.journal.target);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
async finish(backend, deadline) {
|
|
2235
|
+
const pending = this.pending(backend);
|
|
2236
|
+
const control = pending.filter((p) => p.path === this.control);
|
|
2237
|
+
if (control.length) {
|
|
2238
|
+
if (control.length !== 1 || pending.length !== 1)
|
|
2239
|
+
throw new Error("Conflicting maintenance cleanup");
|
|
2240
|
+
const op = backend.journal.read(control[0].id);
|
|
2241
|
+
if (op.kind !== "capture" || op.path !== this.control)
|
|
2242
|
+
throw new Error("Invalid maintenance cleanup");
|
|
2243
|
+
await backend.recover(op.id, { takeAbandonedLock: true });
|
|
2244
|
+
if (exists(this.control) || this.pending(backend).length)
|
|
2245
|
+
throw new Error("Maintenance cleanup unresolved");
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
if (!exists(this.control)) {
|
|
2249
|
+
if (pending.length)
|
|
2250
|
+
throw new Error("Orphaned maintenance operation");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
if (readdirSync(this.control).length === 0) {
|
|
2254
|
+
if (pending.length)
|
|
2255
|
+
throw new Error("Orphaned maintenance operation");
|
|
2256
|
+
const status2 = await backend.doctor();
|
|
2257
|
+
await backend.put(this.control, { retentionDays: status2.retentionDays });
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
const m = this.manifest();
|
|
2261
|
+
if (pending.some((p) => p.path !== this.journal.file(m.key, "receipt") && p.path !== this.journal.file(m.key, "intent")))
|
|
2262
|
+
throw new Error("Unexpected recovery path");
|
|
2263
|
+
await this.pairFile(backend, m, "receipt", deadline);
|
|
2264
|
+
await this.pairFile(backend, m, "intent", deadline);
|
|
2265
|
+
if (this.pending(backend).length)
|
|
2266
|
+
throw new Error("Unfinished pair capture");
|
|
2267
|
+
await confirm(this.cloud, m, deadline);
|
|
2268
|
+
const status = await backend.doctor();
|
|
2269
|
+
await backend.put(this.control, { retentionDays: status.retentionDays });
|
|
2270
|
+
if (exists(this.control) || this.pending(backend).length)
|
|
2271
|
+
throw new Error("Maintenance cleanup unresolved");
|
|
2272
|
+
}
|
|
2273
|
+
async maintain(limit = 32) {
|
|
2274
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 32)
|
|
2275
|
+
throw new Error("Journal maintenance limit must be 1\u201332");
|
|
2276
|
+
await this.recover();
|
|
2277
|
+
await verifyJournalAuthority(this.journal, this.cloud);
|
|
2278
|
+
const deadline = Date.now() + 30000;
|
|
2279
|
+
let retired = 0, bytes = 0;
|
|
2280
|
+
let backend;
|
|
2281
|
+
try {
|
|
2282
|
+
const names = this.journal.names();
|
|
2283
|
+
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
2284
|
+
if (retired >= limit || Date.now() >= deadline || retired > 0 && bytes >= 1048576)
|
|
2285
|
+
break;
|
|
2286
|
+
if (!names.includes(name.replace(".intent.", ".receipt.")))
|
|
2287
|
+
continue;
|
|
2288
|
+
const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
|
|
2289
|
+
backend ??= await this.backend(deadline);
|
|
2290
|
+
const m = {
|
|
2291
|
+
version: 1,
|
|
2292
|
+
target: this.journal.target,
|
|
2293
|
+
key: intent.command.idempotency_key,
|
|
2294
|
+
body_hash: intent.body_hash,
|
|
2295
|
+
receipt,
|
|
2296
|
+
intent: snapshot(join2(this.journal.directory, name)),
|
|
2297
|
+
receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
|
|
2298
|
+
};
|
|
2299
|
+
await confirm(this.cloud, m, deadline);
|
|
2300
|
+
m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
|
|
2301
|
+
m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
|
|
2302
|
+
secureDirectory(this.control);
|
|
2303
|
+
if (!createExact(join2(this.control, "manifest.json"), m))
|
|
2304
|
+
throw new Error("Existing maintenance transaction");
|
|
2305
|
+
await this.finish(backend, deadline);
|
|
2306
|
+
retired++;
|
|
2307
|
+
bytes += m.intent.sizeBytes + m.receipt_file.sizeBytes;
|
|
2308
|
+
}
|
|
2309
|
+
return { retired, remaining: this.journal.status().completed };
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
if (error instanceof JournalMaintenanceError)
|
|
2312
|
+
throw error;
|
|
2313
|
+
const reason = error instanceof Error && ["recovery_not_configured", "self_host_trash_authority_required", "trash_version_mismatch"].includes(error.message) ? error.message : backend ? "recovery_unresolved" : "recovery_authority_unavailable";
|
|
2314
|
+
throw new JournalMaintenanceError(reason, this.journal.target);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
async admit(command) {
|
|
2318
|
+
const status = this.journal.status();
|
|
2319
|
+
const fits = this.journal.canStage(command, status);
|
|
2320
|
+
if (status.commands < 64 && status.reserved_bytes < 50331648 && fits)
|
|
2321
|
+
return;
|
|
2322
|
+
try {
|
|
2323
|
+
await this.maintain();
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
if (exists(this.control) || !this.journal.canStage(command))
|
|
2326
|
+
throw error;
|
|
2327
|
+
}
|
|
2328
|
+
if (!this.journal.canStage(command))
|
|
2329
|
+
throw new JournalMaintenanceError("capacity", this.journal.target);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/lib/ingest-journal-operation.ts
|
|
2334
|
+
import { existsSync, readdirSync as readdirSync2 } from "fs";
|
|
2335
|
+
import { z } from "zod";
|
|
2336
|
+
|
|
2337
|
+
// src/lib/ingest-journal-lock.ts
|
|
2338
|
+
import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
|
|
2339
|
+
import { join as join3 } from "path";
|
|
2340
|
+
async function withIngestJournalLock(directory, action) {
|
|
2341
|
+
if (process.platform !== "linux" && process.platform !== "darwin")
|
|
2342
|
+
throw new Error("Ingest journal locking requires Linux or macOS");
|
|
2343
|
+
const { dlopen, FFIType } = await import("bun:ffi");
|
|
2344
|
+
const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
|
|
2345
|
+
flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
|
|
2346
|
+
});
|
|
2347
|
+
const path = join3(directory, ".lease");
|
|
2348
|
+
let fd;
|
|
2349
|
+
try {
|
|
2350
|
+
fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
|
|
2351
|
+
const stat = fstatSync3(fd), current = lstatSync3(path);
|
|
2352
|
+
if (!stat.isFile() || stat.size !== 0 || stat.nlink !== 1 || stat.ino !== current.ino || stat.dev !== current.dev || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
2353
|
+
throw new Error("Unsafe ingest journal lease");
|
|
2354
|
+
if (library.symbols.flock(fd, 6) !== 0) {
|
|
2355
|
+
const error = new Error("Ingest journal is busy; retry after the current operation finishes");
|
|
2356
|
+
Object.assign(error, { code: "INGEST_JOURNAL_BUSY" });
|
|
2357
|
+
throw error;
|
|
2358
|
+
}
|
|
2359
|
+
return await action();
|
|
2360
|
+
} finally {
|
|
2361
|
+
if (fd !== undefined)
|
|
2362
|
+
closeSync3(fd);
|
|
2363
|
+
library.close();
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// src/lib/ingest-journal-operation.ts
|
|
2368
|
+
var ingestJournalOperationSchema = z.discriminatedUnion("action", [
|
|
2369
|
+
z.object({ action: z.literal("status") }).strict(),
|
|
2370
|
+
z.object({ action: z.literal("reconcile") }).strict(),
|
|
2371
|
+
z.object({ action: z.literal("maintain"), confirm: z.literal(true), limit: z.number().int().min(1).max(32).default(32) }).strict()
|
|
2372
|
+
]);
|
|
2373
|
+
async function runIngestJournalOperation(cloud, input) {
|
|
2374
|
+
const options = ingestJournalOperationSchema.parse(input);
|
|
2375
|
+
const journal = new IngestJournal(cloud);
|
|
2376
|
+
return withIngestJournalLock(journal.directory, async () => {
|
|
2377
|
+
const maintenance = new IngestJournalMaintenance(journal, cloud);
|
|
2378
|
+
const unfinished = existsSync(maintenance.control) || existsSync(maintenance.operationRoot) && readdirSync2(maintenance.operationRoot).some((n) => n !== ".locks");
|
|
2379
|
+
if (options.action === "status" && unfinished)
|
|
2380
|
+
throw new JournalMaintenanceError("recovery_pending", journal.target);
|
|
2381
|
+
if (options.action !== "status") {
|
|
2382
|
+
await maintenance.recover();
|
|
2383
|
+
if (options.action === "reconcile")
|
|
2384
|
+
await journal.reconcilePending(cloud);
|
|
2385
|
+
await verifyJournalAuthority(journal, cloud);
|
|
2386
|
+
}
|
|
2387
|
+
const sweep = options.action === "maintain" ? await maintenance.maintain(options.limit) : { retired: 0 };
|
|
2388
|
+
return { schema: "economy.ingest-journal.v1", action: options.action, ...journal.status(), ...sweep };
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// src/lib/ingest-journal-client.ts
|
|
2393
|
+
async function ingestJournal(input) {
|
|
2394
|
+
const operation = ingestJournalOperationSchema.parse(input);
|
|
2395
|
+
const cloud = economyCloudStorage();
|
|
2396
|
+
if (!cloud.active)
|
|
2397
|
+
throw Object.assign(new Error("Journal operations require the hosted Economy authority"), { code: "INGEST_HOSTED_AUTHORITY_REQUIRED" });
|
|
2398
|
+
return runIngestJournalOperation(cloud, operation);
|
|
2399
|
+
}
|
|
3526
2400
|
export {
|
|
3527
2401
|
isCloudStore,
|
|
3528
2402
|
isAgent,
|
|
2403
|
+
ingestJournalOperationSchema,
|
|
2404
|
+
ingestJournal,
|
|
3529
2405
|
getStore,
|
|
3530
2406
|
LocalStore,
|
|
3531
2407
|
COST_BASIS,
|