@hasna/economy 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +12 -9
- 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-49f12css.js +124 -0
- package/dist/chunks/index-d480p2wh.js +1322 -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-p35cyhc4.js +4 -0
- package/dist/chunks/index-r1jv3kpy.js +451 -0
- package/dist/chunks/index-w3bvv0ns.js +26 -0
- package/dist/chunks/index-xsah93s2.js +2085 -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-2knem98x.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-khmnkat2.js +22 -0
- package/dist/chunks/watch-4y064yys.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 +1358 -10114
- package/dist/db/cloud.d.ts +7 -9
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +14 -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-legacy-schema.d.ts +12 -0
- package/dist/db/pg-legacy-schema.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 +1171 -2465
- 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.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 +39 -13
- package/dist/lib/accounts-store.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +1 -1
- package/dist/lib/autosync-gate.d.ts +12 -5
- package/dist/lib/autosync-gate.d.ts.map +1 -1
- 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 +15 -0
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.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/store/index.d.ts +25 -21
- 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 +9 -16
- package/dist/mcp/agent-registry.d.ts.map +1 -1
- package/dist/mcp/index.js +2291 -3458
- 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 +1960 -289
- package/dist/server/human-auth.d.ts +20 -0
- package/dist/server/human-auth.d.ts.map +1 -0
- package/dist/server/index.js +10639 -6720
- 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 -16
- 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 +8038 -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 +1 -1
- package/docs/configuration.md +5 -5
- 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 +60 -4
- package/docs/mcp.md +2 -2
- package/docs/native-budget-save.md +84 -0
- package/docs/native-human-auth.md +102 -0
- package/docs/postgresql-legacy-schema-repair.md +30 -0
- package/docs/rest-api.md +2 -2
- package/package.json +25 -13
- package/dist/lib/test-hermetic-accounts.d.ts +0 -7
- package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
- package/dist/lib/test-hermetic-fleet-env.d.ts +0 -4
- package/dist/lib/test-hermetic-fleet-env.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,2096 +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 { homedir as pathsResolverHomedir } from "os";
|
|
598
|
-
import { join as pathsResolverJoin } from "path";
|
|
599
|
-
function pathsResolverAssertApp(app) {
|
|
600
|
-
if (typeof app !== "string" || app.length === 0) {
|
|
601
|
-
throw new TypeError("paths: app must be a non-empty string");
|
|
602
|
-
}
|
|
603
|
-
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
604
|
-
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
function pathsResolverAssertKind(kind) {
|
|
608
|
-
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
609
|
-
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
function pathsResolverBaseDir(kind, options) {
|
|
613
|
-
pathsResolverAssertKind(kind);
|
|
614
|
-
const env = options.env ?? process.env;
|
|
615
|
-
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
616
|
-
if (typeof override === "string" && override.length > 0)
|
|
617
|
-
return override;
|
|
618
|
-
const home = options.home ?? pathsResolverHomedir();
|
|
619
|
-
const platform2 = options.platform ?? process.platform;
|
|
620
|
-
if (platform2 === "darwin") {
|
|
621
|
-
switch (kind) {
|
|
622
|
-
case "config":
|
|
623
|
-
case "data":
|
|
624
|
-
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
625
|
-
case "cache":
|
|
626
|
-
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
627
|
-
case "state":
|
|
628
|
-
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
switch (kind) {
|
|
632
|
-
case "config":
|
|
633
|
-
return pathsResolverJoin(home, ".config", "hasna");
|
|
634
|
-
case "data":
|
|
635
|
-
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
636
|
-
case "state":
|
|
637
|
-
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
638
|
-
case "cache":
|
|
639
|
-
return pathsResolverJoin(home, ".cache", "hasna");
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
function pathsResolverResolve(kind, options) {
|
|
643
|
-
pathsResolverAssertApp(options.app);
|
|
644
|
-
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
645
|
-
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
646
|
-
}
|
|
647
|
-
function dataDir(options) {
|
|
648
|
-
return pathsResolverResolve("data", options);
|
|
649
|
-
}
|
|
650
|
-
function normalizeMachineId(value) {
|
|
651
|
-
const id = value?.trim().toLowerCase().split(".")[0];
|
|
652
|
-
return id && id.length > 0 ? id : null;
|
|
653
|
-
}
|
|
654
|
-
function macHostMachineId() {
|
|
655
|
-
if (platform() !== "darwin")
|
|
656
|
-
return null;
|
|
657
|
-
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
658
|
-
try {
|
|
659
|
-
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
660
|
-
encoding: "utf8",
|
|
661
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
662
|
-
timeout: 1000
|
|
663
|
-
});
|
|
664
|
-
const id = normalizeMachineId(value);
|
|
665
|
-
if (id && id !== "mac" && id !== "localhost")
|
|
666
|
-
return id;
|
|
667
|
-
} catch {}
|
|
668
|
-
}
|
|
669
|
-
return null;
|
|
670
|
-
}
|
|
671
|
-
function getMachineId() {
|
|
672
|
-
const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
|
|
673
|
-
if (envMachine)
|
|
674
|
-
return envMachine;
|
|
675
|
-
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
676
|
-
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
677
|
-
return macHostMachineId() ?? hostMachine;
|
|
678
|
-
return hostMachine;
|
|
679
|
-
}
|
|
680
|
-
function getHomeDir() {
|
|
681
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
682
|
-
if (!home)
|
|
683
|
-
throw new Error("Could not resolve the user home directory");
|
|
684
|
-
return home;
|
|
685
|
-
}
|
|
686
|
-
function getResolverDataRoot() {
|
|
687
|
-
return dataDir({
|
|
688
|
-
app: "economy",
|
|
689
|
-
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
function getLegacyDataRoot() {
|
|
693
|
-
return join(getHomeDir(), ".hasna", "economy");
|
|
694
|
-
}
|
|
695
|
-
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
696
|
-
const dataOverride = env.HASNA_DATA_HOME;
|
|
697
|
-
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
698
|
-
return true;
|
|
699
|
-
return existsSync(join(resolved, "economy.db"));
|
|
700
|
-
}
|
|
701
|
-
function getExactDataRoot() {
|
|
702
|
-
const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
|
|
703
|
-
if (dir && dir.trim())
|
|
704
|
-
return dir.trim();
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
function getDataDir() {
|
|
708
|
-
const exact = getExactDataRoot();
|
|
709
|
-
const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
|
|
710
|
-
const oldDir = join(getHomeDir(), ".economy");
|
|
711
|
-
if (existsSync(oldDir) && !existsSync(effective)) {
|
|
712
|
-
mkdirSync(effective, { recursive: true });
|
|
713
|
-
for (const file of readdirSync(oldDir)) {
|
|
714
|
-
const oldPath = join(oldDir, file);
|
|
715
|
-
if (statSync(oldPath).isFile()) {
|
|
716
|
-
copyFileSync(oldPath, join(effective, file));
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
mkdirSync(effective, { recursive: true });
|
|
721
|
-
return effective;
|
|
722
|
-
}
|
|
723
|
-
function getDbPath() {
|
|
724
|
-
if (process.env["HASNA_ECONOMY_DB_PATH"])
|
|
725
|
-
return process.env["HASNA_ECONOMY_DB_PATH"];
|
|
726
|
-
if (process.env["ECONOMY_DB"])
|
|
727
|
-
return process.env["ECONOMY_DB"];
|
|
728
|
-
return join(getDataDir(), "economy.db");
|
|
729
|
-
}
|
|
730
|
-
function isSqliteBusyError(error) {
|
|
731
|
-
const candidate = error;
|
|
732
|
-
const code = typeof candidate.code === "string" ? candidate.code : "";
|
|
733
|
-
const message = typeof candidate.message === "string" ? candidate.message : String(error);
|
|
734
|
-
return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
|
|
735
|
-
}
|
|
736
|
-
function retryDelayMs(attempt) {
|
|
737
|
-
return Math.min(1000, 50 * 2 ** attempt);
|
|
738
|
-
}
|
|
739
|
-
function withSqliteBusyRetry(operation, context) {
|
|
740
|
-
const maxAttempts = 8;
|
|
741
|
-
let lastError;
|
|
742
|
-
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
743
|
-
try {
|
|
744
|
-
return operation();
|
|
745
|
-
} catch (error) {
|
|
746
|
-
if (!isSqliteBusyError(error))
|
|
747
|
-
throw error;
|
|
748
|
-
lastError = error;
|
|
749
|
-
if (attempt === maxAttempts - 1)
|
|
750
|
-
break;
|
|
751
|
-
Bun.sleepSync(retryDelayMs(attempt));
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
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 });
|
|
755
|
-
}
|
|
756
|
-
function openDatabase(dbPath, skipSeed = false) {
|
|
757
|
-
const path = dbPath ?? getDbPath();
|
|
758
|
-
if (path !== ":memory:") {
|
|
759
|
-
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
760
|
-
if (dir && !existsSync(dir))
|
|
761
|
-
mkdirSync(dir, { recursive: true });
|
|
762
|
-
}
|
|
763
|
-
const db = withSqliteBusyRetry(() => {
|
|
764
|
-
const opened = new SqliteAdapter(path);
|
|
765
|
-
try {
|
|
766
|
-
opened.exec("PRAGMA busy_timeout = 10000");
|
|
767
|
-
opened.exec("PRAGMA journal_mode = WAL");
|
|
768
|
-
opened.exec("PRAGMA foreign_keys = ON");
|
|
769
|
-
initSchema(opened);
|
|
770
|
-
return opened;
|
|
771
|
-
} catch (error) {
|
|
772
|
-
try {
|
|
773
|
-
opened.close();
|
|
774
|
-
} catch {}
|
|
775
|
-
throw error;
|
|
776
|
-
}
|
|
777
|
-
}, `opening ${path}`);
|
|
778
|
-
if (!skipSeed) {
|
|
779
|
-
Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
|
|
780
|
-
}
|
|
781
|
-
return db;
|
|
782
|
-
}
|
|
783
|
-
function quoteSqlIdent(identifier) {
|
|
784
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
785
|
-
}
|
|
786
|
-
function hasColumn(db, table, column) {
|
|
787
|
-
const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
|
|
788
|
-
return columns.some((c) => c.name === column);
|
|
789
|
-
}
|
|
790
|
-
function addColumnIfMissing(db, table, column, definition) {
|
|
791
|
-
if (hasColumn(db, table, column))
|
|
792
|
-
return false;
|
|
793
|
-
try {
|
|
794
|
-
db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
|
|
795
|
-
return true;
|
|
796
|
-
} catch (error) {
|
|
797
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
798
|
-
if (/duplicate column name/i.test(message))
|
|
799
|
-
return true;
|
|
800
|
-
throw error;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
function initSchema(db) {
|
|
804
|
-
db.exec(`
|
|
805
|
-
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
806
|
-
id TEXT PRIMARY KEY,
|
|
807
|
-
kind TEXT NOT NULL,
|
|
808
|
-
name TEXT NOT NULL,
|
|
809
|
-
repo_path TEXT,
|
|
810
|
-
labels_json TEXT DEFAULT '{}',
|
|
811
|
-
created_at TEXT NOT NULL,
|
|
812
|
-
updated_at TEXT NOT NULL
|
|
813
|
-
);
|
|
814
|
-
|
|
815
|
-
CREATE TABLE IF NOT EXISTS requests (
|
|
816
|
-
id TEXT PRIMARY KEY,
|
|
817
|
-
agent TEXT NOT NULL,
|
|
818
|
-
session_id TEXT NOT NULL,
|
|
819
|
-
model TEXT NOT NULL,
|
|
820
|
-
input_tokens INTEGER DEFAULT 0,
|
|
821
|
-
output_tokens INTEGER DEFAULT 0,
|
|
822
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
823
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
824
|
-
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
825
|
-
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
826
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
827
|
-
duration_ms INTEGER DEFAULT 0,
|
|
828
|
-
timestamp TEXT NOT NULL,
|
|
829
|
-
source_request_id TEXT,
|
|
830
|
-
machine_id TEXT DEFAULT '',
|
|
831
|
-
cost_center_id TEXT,
|
|
832
|
-
account_key TEXT DEFAULT '',
|
|
833
|
-
account_tool TEXT DEFAULT '',
|
|
834
|
-
account_name TEXT DEFAULT '',
|
|
835
|
-
account_email TEXT DEFAULT '',
|
|
836
|
-
account_source TEXT DEFAULT ''
|
|
837
|
-
);
|
|
838
|
-
|
|
839
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
840
|
-
id TEXT PRIMARY KEY,
|
|
841
|
-
agent TEXT NOT NULL,
|
|
842
|
-
project_path TEXT DEFAULT '',
|
|
843
|
-
project_name TEXT DEFAULT '',
|
|
844
|
-
started_at TEXT NOT NULL,
|
|
845
|
-
ended_at TEXT,
|
|
846
|
-
total_cost_usd REAL DEFAULT 0,
|
|
847
|
-
total_tokens INTEGER DEFAULT 0,
|
|
848
|
-
request_count INTEGER DEFAULT 0,
|
|
849
|
-
machine_id TEXT DEFAULT '',
|
|
850
|
-
cost_center_id TEXT,
|
|
851
|
-
account_key TEXT DEFAULT '',
|
|
852
|
-
account_tool TEXT DEFAULT '',
|
|
853
|
-
account_name TEXT DEFAULT '',
|
|
854
|
-
account_email TEXT DEFAULT '',
|
|
855
|
-
account_source TEXT DEFAULT ''
|
|
856
|
-
);
|
|
857
|
-
|
|
858
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
859
|
-
id TEXT PRIMARY KEY,
|
|
860
|
-
path TEXT UNIQUE NOT NULL,
|
|
861
|
-
name TEXT NOT NULL,
|
|
862
|
-
description TEXT,
|
|
863
|
-
tags TEXT DEFAULT '[]',
|
|
864
|
-
created_at TEXT NOT NULL
|
|
865
|
-
);
|
|
866
|
-
|
|
867
|
-
CREATE TABLE IF NOT EXISTS budgets (
|
|
868
|
-
id TEXT PRIMARY KEY,
|
|
869
|
-
project_path TEXT,
|
|
870
|
-
agent TEXT,
|
|
871
|
-
cost_center_id TEXT,
|
|
872
|
-
period TEXT NOT NULL,
|
|
873
|
-
limit_usd REAL NOT NULL,
|
|
874
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
875
|
-
created_at TEXT NOT NULL,
|
|
876
|
-
updated_at TEXT NOT NULL
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
CREATE TABLE IF NOT EXISTS goals (
|
|
880
|
-
id TEXT PRIMARY KEY,
|
|
881
|
-
period TEXT NOT NULL,
|
|
882
|
-
project_path TEXT,
|
|
883
|
-
agent TEXT,
|
|
884
|
-
limit_usd REAL NOT NULL,
|
|
885
|
-
created_at TEXT NOT NULL,
|
|
886
|
-
updated_at TEXT NOT NULL
|
|
887
|
-
);
|
|
888
|
-
|
|
889
|
-
CREATE TABLE IF NOT EXISTS ingest_state (
|
|
890
|
-
source TEXT NOT NULL,
|
|
891
|
-
key TEXT NOT NULL,
|
|
892
|
-
value TEXT NOT NULL,
|
|
893
|
-
PRIMARY KEY (source, key)
|
|
894
|
-
);
|
|
895
|
-
|
|
896
|
-
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
|
|
897
|
-
CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
|
|
898
|
-
CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
|
|
899
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
900
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
901
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
902
|
-
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
903
|
-
|
|
904
|
-
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
905
|
-
model TEXT PRIMARY KEY,
|
|
906
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
907
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
908
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
909
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
910
|
-
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
911
|
-
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
912
|
-
updated_at TEXT NOT NULL
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
CREATE TABLE IF NOT EXISTS feedback (
|
|
916
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
917
|
-
message TEXT NOT NULL,
|
|
918
|
-
email TEXT,
|
|
919
|
-
category TEXT DEFAULT 'general',
|
|
920
|
-
version TEXT,
|
|
921
|
-
machine_id TEXT,
|
|
922
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
923
|
-
);
|
|
924
|
-
|
|
925
|
-
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
926
|
-
date TEXT NOT NULL,
|
|
927
|
-
provider TEXT NOT NULL,
|
|
928
|
-
description TEXT DEFAULT '',
|
|
929
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
930
|
-
updated_at TEXT NOT NULL,
|
|
931
|
-
PRIMARY KEY (date, provider, description)
|
|
932
|
-
);
|
|
933
|
-
|
|
934
|
-
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
935
|
-
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
936
|
-
|
|
937
|
-
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
938
|
-
id TEXT PRIMARY KEY,
|
|
939
|
-
agent TEXT,
|
|
940
|
-
provider TEXT NOT NULL,
|
|
941
|
-
plan TEXT NOT NULL,
|
|
942
|
-
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
943
|
-
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
944
|
-
billing_cycle_start TEXT,
|
|
945
|
-
reset_policy TEXT DEFAULT 'monthly',
|
|
946
|
-
active INTEGER NOT NULL DEFAULT 1,
|
|
947
|
-
created_at TEXT NOT NULL,
|
|
948
|
-
updated_at TEXT NOT NULL
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
952
|
-
id TEXT PRIMARY KEY,
|
|
953
|
-
agent TEXT NOT NULL,
|
|
954
|
-
date TEXT NOT NULL,
|
|
955
|
-
metric TEXT NOT NULL,
|
|
956
|
-
value REAL NOT NULL DEFAULT 0,
|
|
957
|
-
unit TEXT DEFAULT '',
|
|
958
|
-
machine_id TEXT DEFAULT '',
|
|
959
|
-
updated_at TEXT NOT NULL
|
|
960
|
-
);
|
|
961
|
-
|
|
962
|
-
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
963
|
-
date TEXT NOT NULL,
|
|
964
|
-
agent TEXT DEFAULT '',
|
|
965
|
-
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
966
|
-
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
967
|
-
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
968
|
-
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
969
|
-
saved_usd REAL NOT NULL DEFAULT 0,
|
|
970
|
-
updated_at TEXT NOT NULL,
|
|
971
|
-
PRIMARY KEY (date, agent)
|
|
972
|
-
);
|
|
973
|
-
|
|
974
|
-
CREATE TABLE IF NOT EXISTS machines (
|
|
975
|
-
machine_id TEXT PRIMARY KEY,
|
|
976
|
-
hostname TEXT NOT NULL,
|
|
977
|
-
last_seen_at TEXT,
|
|
978
|
-
last_push_at TEXT,
|
|
979
|
-
last_pull_at TEXT,
|
|
980
|
-
economy_version TEXT,
|
|
981
|
-
updated_at TEXT NOT NULL
|
|
982
|
-
);
|
|
983
|
-
|
|
984
|
-
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
985
|
-
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
986
|
-
`);
|
|
987
|
-
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
988
|
-
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
989
|
-
if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
|
|
990
|
-
db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
991
|
-
}
|
|
992
|
-
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
993
|
-
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
994
|
-
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
995
|
-
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
996
|
-
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
997
|
-
}
|
|
998
|
-
addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
|
|
999
|
-
addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
|
|
1000
|
-
addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
|
|
1001
|
-
if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
|
|
1002
|
-
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
1003
|
-
}
|
|
1004
|
-
addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
|
|
1005
|
-
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
1006
|
-
addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
|
|
1007
|
-
}
|
|
1008
|
-
addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
|
|
1009
|
-
if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
|
|
1010
|
-
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
1011
|
-
}
|
|
1012
|
-
addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
|
|
1013
|
-
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
1014
|
-
addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
|
|
1015
|
-
}
|
|
1016
|
-
addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
|
|
1017
|
-
addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
|
|
1018
|
-
db.exec(`
|
|
1019
|
-
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
1020
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
1021
|
-
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
1022
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
1023
|
-
CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
|
|
1024
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
|
|
1025
|
-
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
1026
|
-
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
1027
|
-
`);
|
|
1028
|
-
}
|
|
1029
|
-
function periodWhere(period) {
|
|
1030
|
-
switch (period) {
|
|
1031
|
-
case "today":
|
|
1032
|
-
return `DATE(timestamp) = DATE('now')`;
|
|
1033
|
-
case "yesterday":
|
|
1034
|
-
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
1035
|
-
case "week":
|
|
1036
|
-
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1037
|
-
case "month":
|
|
1038
|
-
return `timestamp >= DATE('now', 'start of month')`;
|
|
1039
|
-
case "year":
|
|
1040
|
-
return `timestamp >= DATE('now', 'start of year')`;
|
|
1041
|
-
case "all":
|
|
1042
|
-
return "1=1";
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
function sessionPeriodWhere(period) {
|
|
1046
|
-
switch (period) {
|
|
1047
|
-
case "today":
|
|
1048
|
-
return `DATE(started_at) = DATE('now')`;
|
|
1049
|
-
case "yesterday":
|
|
1050
|
-
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
1051
|
-
case "week":
|
|
1052
|
-
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1053
|
-
case "month":
|
|
1054
|
-
return `started_at >= DATE('now', 'start of month')`;
|
|
1055
|
-
case "year":
|
|
1056
|
-
return `started_at >= DATE('now', 'start of year')`;
|
|
1057
|
-
case "all":
|
|
1058
|
-
return "1=1";
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
function requestPeriodWhere(period) {
|
|
1062
|
-
switch (period) {
|
|
1063
|
-
case "today":
|
|
1064
|
-
return `DATE(timestamp) = DATE('now')`;
|
|
1065
|
-
case "yesterday":
|
|
1066
|
-
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
1067
|
-
case "week":
|
|
1068
|
-
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1069
|
-
case "month":
|
|
1070
|
-
return `timestamp >= DATE('now', 'start of month')`;
|
|
1071
|
-
case "year":
|
|
1072
|
-
return `timestamp >= DATE('now', 'start of year')`;
|
|
1073
|
-
case "all":
|
|
1074
|
-
return "1=1";
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
function querySessions(db, filter = {}) {
|
|
1078
|
-
const conditions = [];
|
|
1079
|
-
const params = [];
|
|
1080
|
-
if (filter.agent) {
|
|
1081
|
-
conditions.push("agent = ?");
|
|
1082
|
-
params.push(filter.agent);
|
|
1083
|
-
}
|
|
1084
|
-
if (filter.project) {
|
|
1085
|
-
conditions.push("project_path LIKE ?");
|
|
1086
|
-
params.push(`%${filter.project}%`);
|
|
1087
|
-
}
|
|
1088
|
-
if (filter.account) {
|
|
1089
|
-
const q = `%${filter.account}%`;
|
|
1090
|
-
conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
|
|
1091
|
-
params.push(q, q, q);
|
|
1092
|
-
}
|
|
1093
|
-
if (filter.since) {
|
|
1094
|
-
conditions.push("started_at >= ?");
|
|
1095
|
-
params.push(filter.since);
|
|
1096
|
-
}
|
|
1097
|
-
if (filter.machine) {
|
|
1098
|
-
conditions.push("machine_id = ?");
|
|
1099
|
-
params.push(filter.machine);
|
|
1100
|
-
}
|
|
1101
|
-
if (filter.search) {
|
|
1102
|
-
const q = `%${filter.search}%`;
|
|
1103
|
-
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
1104
|
-
params.push(q, q, `${filter.search}%`);
|
|
1105
|
-
}
|
|
1106
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1107
|
-
const limit = filter.limit ?? 50;
|
|
1108
|
-
const offset = filter.offset ?? 0;
|
|
1109
|
-
return db.prepare(`
|
|
1110
|
-
SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
1111
|
-
`).all(...params, limit, offset);
|
|
1112
|
-
}
|
|
1113
|
-
function queryTopSessions(db, n = 10, agent, since) {
|
|
1114
|
-
const conditions = [];
|
|
1115
|
-
const params = [];
|
|
1116
|
-
if (agent) {
|
|
1117
|
-
conditions.push("agent = ?");
|
|
1118
|
-
params.push(agent);
|
|
1119
|
-
}
|
|
1120
|
-
if (since) {
|
|
1121
|
-
conditions.push("started_at >= ?");
|
|
1122
|
-
params.push(since);
|
|
1123
|
-
}
|
|
1124
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1125
|
-
return db.prepare(`SELECT * FROM sessions ${where} ORDER BY total_cost_usd DESC LIMIT ?`).all(...params, n);
|
|
1126
|
-
}
|
|
1127
|
-
function querySummary(db, period, machine, allMachines = false, agent) {
|
|
1128
|
-
const rWhere = periodWhere(period);
|
|
1129
|
-
const sWhere = sessionPeriodWhere(period);
|
|
1130
|
-
const machineClause = !allMachines && machine ? " AND machine_id = ?" : "";
|
|
1131
|
-
const agentClause = agent ? " AND agent = ?" : "";
|
|
1132
|
-
const params = [];
|
|
1133
|
-
if (!allMachines && machine)
|
|
1134
|
-
params.push(machine);
|
|
1135
|
-
if (agent)
|
|
1136
|
-
params.push(agent);
|
|
1137
|
-
const r = db.prepare(`
|
|
1138
|
-
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
1139
|
-
COUNT(*) as requests,
|
|
1140
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
1141
|
-
FROM requests WHERE ${rWhere}${machineClause}${agentClause}
|
|
1142
|
-
`).get(...params);
|
|
1143
|
-
const codexTotals = db.prepare(`
|
|
1144
|
-
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1145
|
-
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
1146
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1147
|
-
COUNT(*) as sessions
|
|
1148
|
-
FROM sessions
|
|
1149
|
-
WHERE ${sWhere}${machineClause}${agentClause}
|
|
1150
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1151
|
-
`).get(...params);
|
|
1152
|
-
const requestSessionCount = db.prepare(`
|
|
1153
|
-
SELECT COUNT(DISTINCT session_id) as sessions
|
|
1154
|
-
FROM requests
|
|
1155
|
-
WHERE ${rWhere}${machineClause}${agentClause}
|
|
1156
|
-
`).get(...params);
|
|
1157
|
-
return {
|
|
1158
|
-
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
1159
|
-
requests: r.requests + codexTotals.requests,
|
|
1160
|
-
tokens: r.tokens + codexTotals.tokens,
|
|
1161
|
-
sessions: requestSessionCount.sessions + codexTotals.sessions,
|
|
1162
|
-
period
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
function queryModelBreakdown(db) {
|
|
1166
|
-
return db.prepare(`
|
|
1167
|
-
SELECT model, agent,
|
|
1168
|
-
COUNT(*) as requests,
|
|
1169
|
-
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
1170
|
-
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
1171
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1172
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1173
|
-
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
1174
|
-
`).all();
|
|
1175
|
-
}
|
|
1176
|
-
function queryAgentBreakdown(db, period = "all", machine) {
|
|
1177
|
-
const requestWhere = requestPeriodWhere(period);
|
|
1178
|
-
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1179
|
-
const machineParams = machine ? [machine] : [];
|
|
1180
|
-
const groups = new Map;
|
|
1181
|
-
const requestRows = db.prepare(`
|
|
1182
|
-
SELECT agent,
|
|
1183
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1184
|
-
COUNT(*) as requests,
|
|
1185
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1186
|
-
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1187
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1188
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1189
|
-
COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1190
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
1191
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1192
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1193
|
-
MAX(timestamp) as last_active
|
|
1194
|
-
FROM requests
|
|
1195
|
-
WHERE ${requestWhere}${machineClause}
|
|
1196
|
-
GROUP BY agent
|
|
1197
|
-
ORDER BY api_equivalent_usd DESC
|
|
1198
|
-
`).all(...machineParams);
|
|
1199
|
-
for (const row of requestRows) {
|
|
1200
|
-
groups.set(row.agent, row);
|
|
1201
|
-
}
|
|
1202
|
-
const sessionWhere = sessionPeriodWhere(period);
|
|
1203
|
-
const sessionOnlyRows = db.prepare(`
|
|
1204
|
-
SELECT agent,
|
|
1205
|
-
COUNT(*) as sessions,
|
|
1206
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1207
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1208
|
-
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1209
|
-
MAX(started_at) as last_active
|
|
1210
|
-
FROM sessions
|
|
1211
|
-
WHERE ${sessionWhere}${machineClause}
|
|
1212
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1213
|
-
GROUP BY agent
|
|
1214
|
-
`).all(...machineParams);
|
|
1215
|
-
for (const row of sessionOnlyRows) {
|
|
1216
|
-
const existing = groups.get(row.agent) ?? {
|
|
1217
|
-
agent: row.agent,
|
|
1218
|
-
sessions: 0,
|
|
1219
|
-
requests: 0,
|
|
1220
|
-
total_tokens: 0,
|
|
1221
|
-
api_equivalent_usd: 0,
|
|
1222
|
-
billable_usd: 0,
|
|
1223
|
-
metered_api_usd: 0,
|
|
1224
|
-
subscription_included_usd: 0,
|
|
1225
|
-
estimated_usd: 0,
|
|
1226
|
-
unknown_usd: 0,
|
|
1227
|
-
cost_usd: 0,
|
|
1228
|
-
last_active: ""
|
|
1229
|
-
};
|
|
1230
|
-
existing.sessions += row.sessions;
|
|
1231
|
-
existing.requests += row.requests;
|
|
1232
|
-
existing.total_tokens += row.total_tokens;
|
|
1233
|
-
existing.api_equivalent_usd += row.cost_usd;
|
|
1234
|
-
existing.estimated_usd += row.cost_usd;
|
|
1235
|
-
existing.cost_usd += row.cost_usd;
|
|
1236
|
-
if (!existing.last_active || row.last_active > existing.last_active)
|
|
1237
|
-
existing.last_active = row.last_active;
|
|
1238
|
-
groups.set(row.agent, existing);
|
|
1239
|
-
}
|
|
1240
|
-
return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
|
|
1241
|
-
}
|
|
1242
|
-
function pathProjectLabel(projectPath) {
|
|
1243
|
-
if (!projectPath)
|
|
1244
|
-
return "";
|
|
1245
|
-
const segments = projectPath.split("/").filter(Boolean);
|
|
1246
|
-
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
1247
|
-
for (const seg of segments) {
|
|
1248
|
-
if (projectPrefix.test(seg))
|
|
1249
|
-
return seg;
|
|
1250
|
-
}
|
|
1251
|
-
const generic = new Set([
|
|
1252
|
-
"web",
|
|
1253
|
-
"app",
|
|
1254
|
-
"apps",
|
|
1255
|
-
"packages",
|
|
1256
|
-
"src",
|
|
1257
|
-
"lib",
|
|
1258
|
-
"server",
|
|
1259
|
-
"client",
|
|
1260
|
-
"api",
|
|
1261
|
-
"frontend",
|
|
1262
|
-
"backend",
|
|
1263
|
-
"home",
|
|
1264
|
-
"users",
|
|
1265
|
-
"workspace",
|
|
1266
|
-
"workspaces",
|
|
1267
|
-
"hasna"
|
|
1268
|
-
]);
|
|
1269
|
-
for (let i = segments.length - 1;i >= 0; i--) {
|
|
1270
|
-
if (!generic.has(segments[i].toLowerCase()))
|
|
1271
|
-
return segments[i];
|
|
1272
|
-
}
|
|
1273
|
-
return null;
|
|
1274
|
-
}
|
|
1275
|
-
function isRepoLikeLabel(label) {
|
|
1276
|
-
return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
|
|
1277
|
-
}
|
|
1278
|
-
function labelForPath(projectPath, projectName) {
|
|
1279
|
-
const pathLabel = pathProjectLabel(projectPath);
|
|
1280
|
-
if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
|
|
1281
|
-
return pathLabel;
|
|
1282
|
-
if (projectName && projectName.trim() !== "")
|
|
1283
|
-
return projectName;
|
|
1284
|
-
if (pathLabel)
|
|
1285
|
-
return pathLabel;
|
|
1286
|
-
return projectPath;
|
|
1287
|
-
}
|
|
1288
|
-
function groupKeyForPath(projectPath, projectName) {
|
|
1289
|
-
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1290
|
-
}
|
|
1291
|
-
function laterTimestamp(current, candidate) {
|
|
1292
|
-
if (!candidate)
|
|
1293
|
-
return current;
|
|
1294
|
-
return candidate > current ? candidate : current;
|
|
1295
|
-
}
|
|
1296
|
-
function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
|
|
1297
|
-
const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
|
|
1298
|
-
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1299
|
-
const sessionMachineParams = machine ? [machine, machine] : [];
|
|
1300
|
-
const requestMachineParams = machine ? [machine] : [];
|
|
1301
|
-
const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1302
|
-
const sessionOnlyMachineParams = machine ? [machine] : [];
|
|
1303
|
-
const sessions = db.prepare(`
|
|
1304
|
-
SELECT id, project_path, project_name
|
|
1305
|
-
FROM sessions
|
|
1306
|
-
WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
|
|
1307
|
-
`).all(...sessionMachineParams);
|
|
1308
|
-
const groups = new Map;
|
|
1309
|
-
const groupKeyBySession = new Map;
|
|
1310
|
-
for (const s of sessions) {
|
|
1311
|
-
const label = labelForPath(s.project_path, s.project_name);
|
|
1312
|
-
if (!label)
|
|
1313
|
-
continue;
|
|
1314
|
-
const key = groupKeyForPath(s.project_path, s.project_name);
|
|
1315
|
-
const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
|
|
1316
|
-
if (!g.samplePath)
|
|
1317
|
-
g.samplePath = s.project_path;
|
|
1318
|
-
groups.set(key, g);
|
|
1319
|
-
groupKeyBySession.set(s.id, key);
|
|
1320
|
-
}
|
|
1321
|
-
const groupFor = (sessionId) => {
|
|
1322
|
-
const key = groupKeyBySession.get(sessionId);
|
|
1323
|
-
return key === undefined ? undefined : groups.get(key);
|
|
1324
|
-
};
|
|
1325
|
-
const requestRows = db.prepare(`
|
|
1326
|
-
SELECT session_id,
|
|
1327
|
-
COUNT(*) as requests,
|
|
1328
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1329
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1330
|
-
MAX(timestamp) as last_active
|
|
1331
|
-
FROM requests
|
|
1332
|
-
WHERE ${requestWhere}${requestMachineClause}
|
|
1333
|
-
GROUP BY session_id
|
|
1334
|
-
`).all(...requestScopeParams, ...requestMachineParams);
|
|
1335
|
-
for (const row of requestRows) {
|
|
1336
|
-
const g = groupFor(row.session_id);
|
|
1337
|
-
if (!g)
|
|
1338
|
-
continue;
|
|
1339
|
-
g.sessions += 1;
|
|
1340
|
-
g.requests += row.requests;
|
|
1341
|
-
g.cost_usd += row.cost_usd;
|
|
1342
|
-
g.total_tokens += row.total_tokens;
|
|
1343
|
-
g.last_active = laterTimestamp(g.last_active, row.last_active);
|
|
1344
|
-
}
|
|
1345
|
-
const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
|
|
1346
|
-
const sessionOnlyRows = db.prepare(`
|
|
1347
|
-
SELECT id,
|
|
1348
|
-
COALESCE(request_count, 0) as request_count,
|
|
1349
|
-
COALESCE(total_tokens, 0) as total_tokens,
|
|
1350
|
-
COALESCE(total_cost_usd, 0) as total_cost_usd,
|
|
1351
|
-
started_at
|
|
1352
|
-
FROM sessions
|
|
1353
|
-
WHERE ${sessionWhere}${sessionOnlyMachineClause}
|
|
1354
|
-
`).all(...sessionScopeParams, ...sessionOnlyMachineParams);
|
|
1355
|
-
for (const row of sessionOnlyRows) {
|
|
1356
|
-
if (sessionsWithRequests.has(row.id))
|
|
1357
|
-
continue;
|
|
1358
|
-
const g = groupFor(row.id);
|
|
1359
|
-
if (!g)
|
|
1360
|
-
continue;
|
|
1361
|
-
g.sessions += 1;
|
|
1362
|
-
g.requests += row.request_count;
|
|
1363
|
-
g.total_tokens += row.total_tokens;
|
|
1364
|
-
g.cost_usd += row.total_cost_usd;
|
|
1365
|
-
g.last_active = laterTimestamp(g.last_active, row.started_at);
|
|
1366
|
-
}
|
|
1367
|
-
const result = [];
|
|
1368
|
-
for (const g of groups.values()) {
|
|
1369
|
-
if (g.sessions === 0)
|
|
1370
|
-
continue;
|
|
1371
|
-
result.push({
|
|
1372
|
-
project_path: g.samplePath,
|
|
1373
|
-
project_name: g.label,
|
|
1374
|
-
sessions: g.sessions,
|
|
1375
|
-
requests: g.requests,
|
|
1376
|
-
total_tokens: g.total_tokens,
|
|
1377
|
-
cost_usd: g.cost_usd,
|
|
1378
|
-
last_active: g.last_active
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1382
|
-
return result;
|
|
1383
|
-
}
|
|
1384
|
-
function queryProjectBreakdown(db, period = "all", machine) {
|
|
1385
|
-
return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
|
|
1386
|
-
}
|
|
1387
|
-
function normalizeAccountEmail(email) {
|
|
1388
|
-
return (email ?? "").trim().toLowerCase();
|
|
1389
|
-
}
|
|
1390
|
-
function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
|
|
1391
|
-
const identityAgent = (agent || "").trim();
|
|
1392
|
-
const normalizedEmail = normalizeAccountEmail(accountEmail);
|
|
1393
|
-
if (identityAgent && normalizedEmail)
|
|
1394
|
-
return `${identityAgent}:${normalizedEmail}`;
|
|
1395
|
-
if (identityAgent && accountName)
|
|
1396
|
-
return `${identityAgent}:${accountName}`;
|
|
1397
|
-
if (accountKey)
|
|
1398
|
-
return accountKey;
|
|
1399
|
-
return identityAgent ? `${identityAgent}:unknown` : "";
|
|
1400
|
-
}
|
|
1401
|
-
function addAccountBreakdownRow(groups, row, sessionOnly) {
|
|
1402
|
-
const agent = row.agent || row.account_tool;
|
|
1403
|
-
const email = normalizeAccountEmail(row.account_email);
|
|
1404
|
-
const accountName = row.account_name || email || row.account_key;
|
|
1405
|
-
const key = accountIdentityKey(agent, row.account_key, accountName, email);
|
|
1406
|
-
if (!key)
|
|
1407
|
-
return;
|
|
1408
|
-
const group = groups.get(key) ?? {
|
|
1409
|
-
account_key: key,
|
|
1410
|
-
account_tool: agent,
|
|
1411
|
-
account_name: accountName,
|
|
1412
|
-
account_email: email || null,
|
|
1413
|
-
account_source: row.account_source || "unknown",
|
|
1414
|
-
sessionIds: new Set,
|
|
1415
|
-
requests: 0,
|
|
1416
|
-
total_tokens: 0,
|
|
1417
|
-
api_equivalent_usd: 0,
|
|
1418
|
-
metered_api_usd: 0,
|
|
1419
|
-
subscription_included_usd: 0,
|
|
1420
|
-
estimated_usd: 0,
|
|
1421
|
-
unknown_usd: 0,
|
|
1422
|
-
last_active: ""
|
|
1423
|
-
};
|
|
1424
|
-
if (!group.account_email && email)
|
|
1425
|
-
group.account_email = email;
|
|
1426
|
-
if (!group.account_name && accountName)
|
|
1427
|
-
group.account_name = accountName;
|
|
1428
|
-
if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
|
|
1429
|
-
group.account_source = row.account_source;
|
|
1430
|
-
}
|
|
1431
|
-
if (row.session_id)
|
|
1432
|
-
group.sessionIds.add(row.session_id);
|
|
1433
|
-
group.requests += row.requests;
|
|
1434
|
-
group.total_tokens += row.total_tokens;
|
|
1435
|
-
group.api_equivalent_usd += row.cost_usd;
|
|
1436
|
-
if (sessionOnly) {
|
|
1437
|
-
group.estimated_usd += row.cost_usd;
|
|
1438
|
-
} else if (row.cost_basis === "metered_api") {
|
|
1439
|
-
group.metered_api_usd += row.cost_usd;
|
|
1440
|
-
} else if (row.cost_basis === "subscription_included") {
|
|
1441
|
-
group.subscription_included_usd += row.cost_usd;
|
|
1442
|
-
} else if (row.cost_basis === "unknown") {
|
|
1443
|
-
group.unknown_usd += row.cost_usd;
|
|
1444
|
-
} else {
|
|
1445
|
-
group.estimated_usd += row.cost_usd;
|
|
1446
|
-
}
|
|
1447
|
-
if (!group.last_active || row.last_active > group.last_active)
|
|
1448
|
-
group.last_active = row.last_active;
|
|
1449
|
-
groups.set(key, group);
|
|
1450
|
-
}
|
|
1451
|
-
function queryAccountBreakdown(db, period = "all", machine) {
|
|
1452
|
-
const requestWhere = requestPeriodWhere(period);
|
|
1453
|
-
const sessionWhere = sessionPeriodWhere(period);
|
|
1454
|
-
const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
|
|
1455
|
-
const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
|
|
1456
|
-
const requestMachineParams = machine ? [machine] : [];
|
|
1457
|
-
const sessionMachineParams = machine ? [machine] : [];
|
|
1458
|
-
const groups = new Map;
|
|
1459
|
-
const requestRows = db.prepare(`
|
|
1460
|
-
SELECT
|
|
1461
|
-
r.session_id as session_id,
|
|
1462
|
-
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
|
|
1463
|
-
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
|
|
1464
|
-
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
|
|
1465
|
-
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
|
|
1466
|
-
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
|
|
1467
|
-
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1468
|
-
1 as requests,
|
|
1469
|
-
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1470
|
-
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1471
|
-
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1472
|
-
r.timestamp as last_active
|
|
1473
|
-
FROM requests r
|
|
1474
|
-
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1475
|
-
WHERE ${requestWhere}${requestMachineClause}
|
|
1476
|
-
AND (
|
|
1477
|
-
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
|
|
1478
|
-
OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
|
|
1479
|
-
OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
|
|
1480
|
-
OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
|
|
1481
|
-
)
|
|
1482
|
-
`).all(...requestMachineParams);
|
|
1483
|
-
for (const row of requestRows)
|
|
1484
|
-
addAccountBreakdownRow(groups, row, false);
|
|
1485
|
-
const sessionOnlyRows = db.prepare(`
|
|
1486
|
-
SELECT
|
|
1487
|
-
s.id as session_id,
|
|
1488
|
-
s.agent as agent,
|
|
1489
|
-
s.account_key as account_key,
|
|
1490
|
-
s.account_tool as account_tool,
|
|
1491
|
-
s.account_name as account_name,
|
|
1492
|
-
s.account_email as account_email,
|
|
1493
|
-
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1494
|
-
COALESCE(s.request_count, 0) as requests,
|
|
1495
|
-
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1496
|
-
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1497
|
-
'estimated' as cost_basis,
|
|
1498
|
-
s.started_at as last_active
|
|
1499
|
-
FROM sessions s
|
|
1500
|
-
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1501
|
-
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1502
|
-
AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
|
|
1503
|
-
`).all(...sessionMachineParams);
|
|
1504
|
-
for (const row of sessionOnlyRows)
|
|
1505
|
-
addAccountBreakdownRow(groups, row, true);
|
|
1506
|
-
const result = [...groups.values()].map((group) => ({
|
|
1507
|
-
account_key: group.account_key,
|
|
1508
|
-
account_tool: group.account_tool,
|
|
1509
|
-
account_name: group.account_name,
|
|
1510
|
-
account_email: group.account_email,
|
|
1511
|
-
account_source: group.account_source,
|
|
1512
|
-
sessions: group.sessionIds.size,
|
|
1513
|
-
requests: group.requests,
|
|
1514
|
-
total_tokens: group.total_tokens,
|
|
1515
|
-
api_equivalent_usd: group.api_equivalent_usd,
|
|
1516
|
-
billable_usd: group.metered_api_usd,
|
|
1517
|
-
metered_api_usd: group.metered_api_usd,
|
|
1518
|
-
subscription_included_usd: group.subscription_included_usd,
|
|
1519
|
-
estimated_usd: group.estimated_usd,
|
|
1520
|
-
unknown_usd: group.unknown_usd,
|
|
1521
|
-
cost_usd: group.api_equivalent_usd,
|
|
1522
|
-
last_active: group.last_active
|
|
1523
|
-
}));
|
|
1524
|
-
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1525
|
-
return result;
|
|
1526
|
-
}
|
|
1527
|
-
function kindFromCostCenterId(id) {
|
|
1528
|
-
const prefix = id.split(":", 1)[0] ?? "";
|
|
1529
|
-
return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
|
|
1530
|
-
}
|
|
1531
|
-
function addCostCenterBreakdownRow(groups, row, sessionOnly) {
|
|
1532
|
-
if (!row.cost_center_id)
|
|
1533
|
-
return;
|
|
1534
|
-
const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
|
|
1535
|
-
const group = groups.get(row.cost_center_id) ?? {
|
|
1536
|
-
cost_center_id: row.cost_center_id,
|
|
1537
|
-
kind: derivedKind,
|
|
1538
|
-
name: row.name || row.cost_center_id,
|
|
1539
|
-
repo_path: row.repo_path ?? null,
|
|
1540
|
-
labels_json: row.labels_json || "{}",
|
|
1541
|
-
sessionIds: new Set,
|
|
1542
|
-
requests: 0,
|
|
1543
|
-
total_tokens: 0,
|
|
1544
|
-
api_equivalent_usd: 0,
|
|
1545
|
-
metered_api_usd: 0,
|
|
1546
|
-
subscription_included_usd: 0,
|
|
1547
|
-
estimated_usd: 0,
|
|
1548
|
-
unknown_usd: 0,
|
|
1549
|
-
last_active: ""
|
|
1550
|
-
};
|
|
1551
|
-
group.kind = derivedKind;
|
|
1552
|
-
if (row.name)
|
|
1553
|
-
group.name = row.name;
|
|
1554
|
-
if (row.repo_path && !group.repo_path)
|
|
1555
|
-
group.repo_path = row.repo_path;
|
|
1556
|
-
if (row.labels_json && row.labels_json !== "{}")
|
|
1557
|
-
group.labels_json = row.labels_json;
|
|
1558
|
-
if (row.session_id)
|
|
1559
|
-
group.sessionIds.add(row.session_id);
|
|
1560
|
-
group.requests += row.requests;
|
|
1561
|
-
group.total_tokens += row.total_tokens;
|
|
1562
|
-
group.api_equivalent_usd += row.cost_usd;
|
|
1563
|
-
if (sessionOnly) {
|
|
1564
|
-
group.estimated_usd += row.cost_usd;
|
|
1565
|
-
} else if (row.cost_basis === "metered_api") {
|
|
1566
|
-
group.metered_api_usd += row.cost_usd;
|
|
1567
|
-
} else if (row.cost_basis === "subscription_included") {
|
|
1568
|
-
group.subscription_included_usd += row.cost_usd;
|
|
1569
|
-
} else if (row.cost_basis === "unknown") {
|
|
1570
|
-
group.unknown_usd += row.cost_usd;
|
|
1571
|
-
} else {
|
|
1572
|
-
group.estimated_usd += row.cost_usd;
|
|
1573
|
-
}
|
|
1574
|
-
if (!group.last_active || row.last_active > group.last_active)
|
|
1575
|
-
group.last_active = row.last_active;
|
|
1576
|
-
groups.set(row.cost_center_id, group);
|
|
1577
|
-
}
|
|
1578
|
-
function queryCostCenterBreakdown(db, period = "all", filter = {}) {
|
|
1579
|
-
const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
|
|
1580
|
-
const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
|
|
1581
|
-
const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
|
|
1582
|
-
const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
|
|
1583
|
-
const requestParams = [];
|
|
1584
|
-
const sessionParams = [];
|
|
1585
|
-
if (filter.since) {
|
|
1586
|
-
requestParams.push(filter.since);
|
|
1587
|
-
sessionParams.push(filter.since);
|
|
1588
|
-
}
|
|
1589
|
-
if (filter.machine) {
|
|
1590
|
-
requestParams.push(filter.machine);
|
|
1591
|
-
sessionParams.push(filter.machine);
|
|
1592
|
-
}
|
|
1593
|
-
const groups = new Map;
|
|
1594
|
-
const requestRows = db.prepare(`
|
|
1595
|
-
SELECT
|
|
1596
|
-
COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
|
|
1597
|
-
COALESCE(cc.kind, '') as kind,
|
|
1598
|
-
COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
|
|
1599
|
-
cc.repo_path as repo_path,
|
|
1600
|
-
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1601
|
-
r.session_id as session_id,
|
|
1602
|
-
1 as requests,
|
|
1603
|
-
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1604
|
-
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1605
|
-
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1606
|
-
r.timestamp as last_active
|
|
1607
|
-
FROM requests r
|
|
1608
|
-
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1609
|
-
LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
|
|
1610
|
-
WHERE ${requestWhere}${requestMachineClause}
|
|
1611
|
-
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
|
|
1612
|
-
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
|
|
1613
|
-
`).all(...requestParams);
|
|
1614
|
-
for (const row of requestRows)
|
|
1615
|
-
addCostCenterBreakdownRow(groups, row, false);
|
|
1616
|
-
const sessionOnlyRows = db.prepare(`
|
|
1617
|
-
SELECT
|
|
1618
|
-
s.cost_center_id as cost_center_id,
|
|
1619
|
-
COALESCE(cc.kind, '') as kind,
|
|
1620
|
-
COALESCE(cc.name, s.cost_center_id) as name,
|
|
1621
|
-
cc.repo_path as repo_path,
|
|
1622
|
-
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1623
|
-
s.id as session_id,
|
|
1624
|
-
COALESCE(s.request_count, 0) as requests,
|
|
1625
|
-
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1626
|
-
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1627
|
-
'estimated' as cost_basis,
|
|
1628
|
-
s.started_at as last_active
|
|
1629
|
-
FROM sessions s
|
|
1630
|
-
LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
|
|
1631
|
-
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1632
|
-
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1633
|
-
AND s.cost_center_id IS NOT NULL
|
|
1634
|
-
AND s.cost_center_id != ''
|
|
1635
|
-
`).all(...sessionParams);
|
|
1636
|
-
for (const row of sessionOnlyRows)
|
|
1637
|
-
addCostCenterBreakdownRow(groups, row, true);
|
|
1638
|
-
const result = [...groups.values()].map((group) => ({
|
|
1639
|
-
cost_center_id: group.cost_center_id,
|
|
1640
|
-
kind: group.kind,
|
|
1641
|
-
name: group.name,
|
|
1642
|
-
repo_path: group.repo_path,
|
|
1643
|
-
labels_json: group.labels_json,
|
|
1644
|
-
sessions: group.sessionIds.size,
|
|
1645
|
-
requests: group.requests,
|
|
1646
|
-
total_tokens: group.total_tokens,
|
|
1647
|
-
api_equivalent_usd: group.api_equivalent_usd,
|
|
1648
|
-
billable_usd: group.metered_api_usd,
|
|
1649
|
-
metered_api_usd: group.metered_api_usd,
|
|
1650
|
-
subscription_included_usd: group.subscription_included_usd,
|
|
1651
|
-
estimated_usd: group.estimated_usd,
|
|
1652
|
-
unknown_usd: group.unknown_usd,
|
|
1653
|
-
cost_usd: group.api_equivalent_usd,
|
|
1654
|
-
last_active: group.last_active
|
|
1655
|
-
}));
|
|
1656
|
-
const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
|
|
1657
|
-
filtered.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1658
|
-
return filtered;
|
|
1659
|
-
}
|
|
1660
|
-
function queryProjectBreakdownSince(db, since, machine) {
|
|
1661
|
-
return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
|
|
1662
|
-
}
|
|
1663
|
-
function queryAgentBreakdownSince(db, since) {
|
|
1664
|
-
return db.prepare(`
|
|
1665
|
-
SELECT agent,
|
|
1666
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1667
|
-
COUNT(*) as requests,
|
|
1668
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1669
|
-
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1670
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1671
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1672
|
-
MAX(timestamp) as last_active
|
|
1673
|
-
FROM requests
|
|
1674
|
-
WHERE timestamp >= ?
|
|
1675
|
-
GROUP BY agent
|
|
1676
|
-
ORDER BY api_equivalent_usd DESC
|
|
1677
|
-
`).all(since);
|
|
1678
|
-
}
|
|
1679
|
-
function queryModelBreakdownSince(db, since) {
|
|
1680
|
-
return db.prepare(`
|
|
1681
|
-
SELECT model, agent,
|
|
1682
|
-
COUNT(*) as requests,
|
|
1683
|
-
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
1684
|
-
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
1685
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1686
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1687
|
-
FROM requests WHERE timestamp >= ?
|
|
1688
|
-
GROUP BY model, agent ORDER BY cost_usd DESC
|
|
1689
|
-
`).all(since);
|
|
1690
|
-
}
|
|
1691
|
-
function queryAccountBreakdownSince(db, since) {
|
|
1692
|
-
return db.prepare(`
|
|
1693
|
-
WITH request_rows AS (
|
|
1694
|
-
SELECT
|
|
1695
|
-
r.session_id as session_id,
|
|
1696
|
-
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
|
|
1697
|
-
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
|
|
1698
|
-
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
|
|
1699
|
-
LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
|
|
1700
|
-
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1701
|
-
1 as requests,
|
|
1702
|
-
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1703
|
-
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1704
|
-
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1705
|
-
r.timestamp as last_active
|
|
1706
|
-
FROM requests r
|
|
1707
|
-
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1708
|
-
WHERE r.timestamp >= ?
|
|
1709
|
-
AND (
|
|
1710
|
-
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
|
|
1711
|
-
OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
|
|
1712
|
-
OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
|
|
1713
|
-
OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
|
|
1714
|
-
)
|
|
1715
|
-
),
|
|
1716
|
-
session_only_rows AS (
|
|
1717
|
-
SELECT
|
|
1718
|
-
s.id as session_id,
|
|
1719
|
-
COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
|
|
1720
|
-
s.account_key as raw_account_key,
|
|
1721
|
-
s.account_name as raw_account_name,
|
|
1722
|
-
LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
|
|
1723
|
-
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1724
|
-
COALESCE(s.request_count, 0) as requests,
|
|
1725
|
-
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1726
|
-
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1727
|
-
'estimated' as cost_basis,
|
|
1728
|
-
s.started_at as last_active
|
|
1729
|
-
FROM sessions s
|
|
1730
|
-
WHERE s.started_at >= ?
|
|
1731
|
-
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1732
|
-
AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
|
|
1733
|
-
),
|
|
1734
|
-
normalized AS (
|
|
1735
|
-
SELECT
|
|
1736
|
-
CASE
|
|
1737
|
-
WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
|
|
1738
|
-
WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
|
|
1739
|
-
ELSE raw_account_key
|
|
1740
|
-
END as account_key,
|
|
1741
|
-
account_agent as account_tool,
|
|
1742
|
-
raw_account_name as account_name,
|
|
1743
|
-
raw_account_email as account_email,
|
|
1744
|
-
account_source,
|
|
1745
|
-
session_id,
|
|
1746
|
-
requests,
|
|
1747
|
-
total_tokens,
|
|
1748
|
-
cost_usd,
|
|
1749
|
-
cost_basis,
|
|
1750
|
-
last_active
|
|
1751
|
-
FROM request_rows
|
|
1752
|
-
UNION ALL
|
|
1753
|
-
SELECT
|
|
1754
|
-
CASE
|
|
1755
|
-
WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
|
|
1756
|
-
WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
|
|
1757
|
-
ELSE raw_account_key
|
|
1758
|
-
END as account_key,
|
|
1759
|
-
account_agent as account_tool,
|
|
1760
|
-
raw_account_name as account_name,
|
|
1761
|
-
raw_account_email as account_email,
|
|
1762
|
-
account_source,
|
|
1763
|
-
session_id,
|
|
1764
|
-
requests,
|
|
1765
|
-
total_tokens,
|
|
1766
|
-
cost_usd,
|
|
1767
|
-
cost_basis,
|
|
1768
|
-
last_active
|
|
1769
|
-
FROM session_only_rows
|
|
1770
|
-
)
|
|
1771
|
-
SELECT account_key,
|
|
1772
|
-
account_tool,
|
|
1773
|
-
COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
|
|
1774
|
-
NULLIF(account_email, '') as account_email,
|
|
1775
|
-
COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
|
|
1776
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1777
|
-
COALESCE(SUM(requests), 0) as requests,
|
|
1778
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1779
|
-
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1780
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1781
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1782
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1783
|
-
COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1784
|
-
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
1785
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1786
|
-
MAX(last_active) as last_active
|
|
1787
|
-
FROM normalized
|
|
1788
|
-
WHERE account_key != ''
|
|
1789
|
-
GROUP BY account_key, account_tool, account_email
|
|
1790
|
-
ORDER BY api_equivalent_usd DESC
|
|
1791
|
-
`).all(since, since);
|
|
1792
|
-
}
|
|
1793
|
-
function queryDailyBreakdown(db, days = 30, machine) {
|
|
1794
|
-
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1795
|
-
const params = machine ? [`-${days}`, machine] : [`-${days}`];
|
|
1796
|
-
return db.prepare(`
|
|
1797
|
-
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1798
|
-
FROM requests
|
|
1799
|
-
WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
|
|
1800
|
-
GROUP BY DATE(timestamp), agent
|
|
1801
|
-
ORDER BY date ASC
|
|
1802
|
-
`).all(...params);
|
|
1803
|
-
}
|
|
1804
|
-
function upsertProject(db, project) {
|
|
1805
|
-
db.prepare(`
|
|
1806
|
-
INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
|
|
1807
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
1808
|
-
`).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
|
|
1809
|
-
}
|
|
1810
|
-
function getProject(db, path) {
|
|
1811
|
-
const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
|
|
1812
|
-
if (!row)
|
|
1813
|
-
return null;
|
|
1814
|
-
return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
|
|
1815
|
-
}
|
|
1816
|
-
function deleteProject(db, path) {
|
|
1817
|
-
db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
|
|
1818
|
-
}
|
|
1819
|
-
function upsertBudget(db, budget) {
|
|
1820
|
-
db.prepare(`
|
|
1821
|
-
INSERT OR REPLACE INTO budgets
|
|
1822
|
-
(id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1823
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1824
|
-
`).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);
|
|
1825
|
-
}
|
|
1826
|
-
function listBudgets(db) {
|
|
1827
|
-
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
1828
|
-
}
|
|
1829
|
-
function deleteBudget(db, id) {
|
|
1830
|
-
db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
|
|
1831
|
-
}
|
|
1832
|
-
function getBudgetStatuses(db) {
|
|
1833
|
-
const budgets = listBudgets(db);
|
|
1834
|
-
return budgets.map((b) => {
|
|
1835
|
-
const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
|
|
1836
|
-
let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
|
|
1837
|
-
const params = [];
|
|
1838
|
-
if (b.project_path) {
|
|
1839
|
-
spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
|
|
1840
|
-
params.push(b.project_path);
|
|
1841
|
-
}
|
|
1842
|
-
if (b.agent) {
|
|
1843
|
-
spendQuery += ` AND agent = ?`;
|
|
1844
|
-
params.push(b.agent);
|
|
1845
|
-
}
|
|
1846
|
-
if (b.cost_center_id) {
|
|
1847
|
-
spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
|
|
1848
|
-
params.push(b.cost_center_id);
|
|
1849
|
-
}
|
|
1850
|
-
const row = db.prepare(spendQuery).get(...params);
|
|
1851
|
-
const spend = row.spend;
|
|
1852
|
-
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
1853
|
-
return {
|
|
1854
|
-
...b,
|
|
1855
|
-
current_spend_usd: spend,
|
|
1856
|
-
percent_used: percent,
|
|
1857
|
-
is_over_limit: percent >= 100,
|
|
1858
|
-
is_over_alert: percent >= b.alert_at_percent
|
|
1859
|
-
};
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
function upsertGoal(db, goal) {
|
|
1863
|
-
db.prepare(`
|
|
1864
|
-
INSERT OR REPLACE INTO goals
|
|
1865
|
-
(id, period, project_path, agent, limit_usd, created_at, updated_at)
|
|
1866
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1867
|
-
`).run(goal.id, goal.period, goal.project_path ?? null, goal.agent ?? null, goal.limit_usd, goal.created_at, goal.updated_at);
|
|
1868
|
-
}
|
|
1869
|
-
function deleteGoal(db, id) {
|
|
1870
|
-
db.prepare(`DELETE FROM goals WHERE id = ?`).run(id);
|
|
1871
|
-
}
|
|
1872
|
-
function listGoals(db) {
|
|
1873
|
-
return db.prepare(`SELECT * FROM goals ORDER BY created_at DESC`).all();
|
|
1874
|
-
}
|
|
1875
|
-
function getGoalStatuses(db) {
|
|
1876
|
-
const goals = listGoals(db);
|
|
1877
|
-
return goals.map((g) => {
|
|
1878
|
-
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')";
|
|
1879
|
-
let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
|
|
1880
|
-
const params = [];
|
|
1881
|
-
if (g.project_path) {
|
|
1882
|
-
spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
|
|
1883
|
-
params.push(g.project_path);
|
|
1884
|
-
}
|
|
1885
|
-
if (g.agent) {
|
|
1886
|
-
spendQuery += ` AND agent = ?`;
|
|
1887
|
-
params.push(g.agent);
|
|
1888
|
-
}
|
|
1889
|
-
const row = db.prepare(spendQuery).get(...params);
|
|
1890
|
-
const spend = row.spend;
|
|
1891
|
-
const percent = g.limit_usd > 0 ? spend / g.limit_usd * 100 : 0;
|
|
1892
|
-
return {
|
|
1893
|
-
...g,
|
|
1894
|
-
current_spend_usd: spend,
|
|
1895
|
-
percent_used: percent,
|
|
1896
|
-
is_on_track: percent < 70,
|
|
1897
|
-
is_at_risk: percent >= 70 && percent <= 100,
|
|
1898
|
-
is_over: percent > 100
|
|
1899
|
-
};
|
|
1900
|
-
});
|
|
1901
|
-
}
|
|
1902
|
-
function queryRequestsSince(db, since) {
|
|
1903
|
-
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
1904
|
-
}
|
|
1905
|
-
function insertFeedback(db, record) {
|
|
1906
|
-
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);
|
|
1907
|
-
}
|
|
1908
|
-
function billingPeriodWhere(period) {
|
|
1909
|
-
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";
|
|
1910
|
-
}
|
|
1911
|
-
function queryBillingSummary(db, period) {
|
|
1912
|
-
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${billingPeriodWhere(period)} GROUP BY provider`).all();
|
|
1913
|
-
const by_provider = {};
|
|
1914
|
-
let total = 0;
|
|
1915
|
-
for (const r of rows) {
|
|
1916
|
-
by_provider[r.provider] = r.cost;
|
|
1917
|
-
total += r.cost;
|
|
1918
|
-
}
|
|
1919
|
-
return { total_usd: total, by_provider };
|
|
1920
|
-
}
|
|
1921
|
-
function countBillingRecords(db, period) {
|
|
1922
|
-
const row = db.prepare(`SELECT COUNT(*) as c FROM billing_daily WHERE ${billingPeriodWhere(period)}`).get();
|
|
1923
|
-
return row?.c ?? 0;
|
|
1924
|
-
}
|
|
1925
|
-
function getSessionDetail(db, id) {
|
|
1926
|
-
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
|
|
1927
|
-
if (!session)
|
|
1928
|
-
return null;
|
|
1929
|
-
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
1930
|
-
return { session, requests };
|
|
1931
|
-
}
|
|
1932
|
-
function listMachines(db, period = "all") {
|
|
1933
|
-
const rWhere = requestPeriodWhere(period);
|
|
1934
|
-
const sWhere = sessionPeriodWhere(period);
|
|
1935
|
-
return db.prepare(`
|
|
1936
|
-
WITH request_stats AS (
|
|
1937
|
-
SELECT
|
|
1938
|
-
machine_id,
|
|
1939
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1940
|
-
COUNT(*) as requests,
|
|
1941
|
-
COALESCE(SUM(cost_usd), 0) as total_cost_usd,
|
|
1942
|
-
MAX(timestamp) as last_active
|
|
1943
|
-
FROM requests
|
|
1944
|
-
WHERE machine_id != ''
|
|
1945
|
-
AND ${rWhere}
|
|
1946
|
-
GROUP BY machine_id
|
|
1947
|
-
),
|
|
1948
|
-
session_only_stats AS (
|
|
1949
|
-
SELECT
|
|
1950
|
-
machine_id,
|
|
1951
|
-
COUNT(*) as sessions,
|
|
1952
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1953
|
-
COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
|
|
1954
|
-
MAX(started_at) as last_active
|
|
1955
|
-
FROM sessions
|
|
1956
|
-
WHERE machine_id != ''
|
|
1957
|
-
AND ${sWhere}
|
|
1958
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1959
|
-
GROUP BY machine_id
|
|
1960
|
-
),
|
|
1961
|
-
combined AS (
|
|
1962
|
-
SELECT * FROM request_stats
|
|
1963
|
-
UNION ALL
|
|
1964
|
-
SELECT * FROM session_only_stats
|
|
1965
|
-
)
|
|
1966
|
-
SELECT
|
|
1967
|
-
machine_id,
|
|
1968
|
-
COALESCE(SUM(sessions), 0) as sessions,
|
|
1969
|
-
COALESCE(SUM(requests), 0) as requests,
|
|
1970
|
-
COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
|
|
1971
|
-
MAX(last_active) as last_active
|
|
1972
|
-
FROM combined
|
|
1973
|
-
GROUP BY machine_id
|
|
1974
|
-
ORDER BY total_cost_usd DESC
|
|
1975
|
-
`).all();
|
|
1976
|
-
}
|
|
1977
|
-
function upsertModelPricing(db, p) {
|
|
1978
|
-
db.prepare(`
|
|
1979
|
-
INSERT OR REPLACE INTO model_pricing
|
|
1980
|
-
(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)
|
|
1981
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1982
|
-
`).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);
|
|
1983
|
-
}
|
|
1984
|
-
function getModelPricing(db, model) {
|
|
1985
|
-
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
1986
|
-
}
|
|
1987
|
-
function listModelPricing(db) {
|
|
1988
|
-
return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
|
|
1989
|
-
}
|
|
1990
|
-
function deleteModelPricing(db, model) {
|
|
1991
|
-
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
1992
|
-
}
|
|
1993
|
-
function seedModelPricing(db, defaults) {
|
|
1994
|
-
const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
|
|
1995
|
-
const now = new Date().toISOString();
|
|
1996
|
-
for (const [model, p] of Object.entries(defaults)) {
|
|
1997
|
-
if (existing.has(model))
|
|
1998
|
-
continue;
|
|
1999
|
-
upsertModelPricing(db, {
|
|
2000
|
-
model,
|
|
2001
|
-
input_per_1m: p.inputPer1M,
|
|
2002
|
-
output_per_1m: p.outputPer1M,
|
|
2003
|
-
cache_read_per_1m: p.cacheReadPer1M,
|
|
2004
|
-
cache_write_per_1m: p.cacheWritePer1M,
|
|
2005
|
-
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
2006
|
-
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
2007
|
-
updated_at: now
|
|
2008
|
-
});
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
function upsertSubscription(db, sub) {
|
|
2012
|
-
db.prepare(`
|
|
2013
|
-
INSERT OR REPLACE INTO subscriptions
|
|
2014
|
-
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
2015
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2016
|
-
`).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);
|
|
2017
|
-
}
|
|
2018
|
-
function listSubscriptions(db) {
|
|
2019
|
-
return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
|
|
2020
|
-
}
|
|
2021
|
-
function deleteSubscription(db, id) {
|
|
2022
|
-
db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
|
|
2023
|
-
}
|
|
2024
|
-
function queryUsageSnapshots(db, opts = {}) {
|
|
2025
|
-
const conditions = [];
|
|
2026
|
-
const params = [];
|
|
2027
|
-
if (opts.agent) {
|
|
2028
|
-
conditions.push("agent = ?");
|
|
2029
|
-
params.push(opts.agent);
|
|
2030
|
-
}
|
|
2031
|
-
if (opts.date) {
|
|
2032
|
-
conditions.push("date = ?");
|
|
2033
|
-
params.push(opts.date);
|
|
2034
|
-
}
|
|
2035
|
-
if (opts.since) {
|
|
2036
|
-
conditions.push("date >= ?");
|
|
2037
|
-
params.push(opts.since);
|
|
2038
|
-
}
|
|
2039
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2040
|
-
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
2041
|
-
}
|
|
2042
|
-
function listMachineRegistry(db) {
|
|
2043
|
-
return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
|
|
2044
|
-
}
|
|
2045
|
-
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
2046
|
-
var init_database = __esm(() => {
|
|
2047
|
-
init_sqlite_adapter();
|
|
2048
|
-
PATHS_RESOLVER_KIND_ENV = {
|
|
2049
|
-
config: "HASNA_CONFIG_HOME",
|
|
2050
|
-
data: "HASNA_DATA_HOME",
|
|
2051
|
-
state: "HASNA_STATE_HOME",
|
|
2052
|
-
cache: "HASNA_CACHE_HOME"
|
|
2053
|
-
};
|
|
2054
|
-
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2055
|
-
});
|
|
2056
|
-
|
|
2057
|
-
// src/lib/agents.ts
|
|
2058
|
-
var AGENTS = [
|
|
2059
|
-
"claude",
|
|
2060
|
-
"takumi",
|
|
2061
|
-
"codex",
|
|
2062
|
-
"gemini",
|
|
2063
|
-
"opencode",
|
|
2064
|
-
"cursor",
|
|
2065
|
-
"pi",
|
|
2066
|
-
"hermes"
|
|
2067
|
-
];
|
|
2068
|
-
var COST_BASIS = [
|
|
2069
|
-
"metered_api",
|
|
2070
|
-
"subscription_included",
|
|
2071
|
-
"estimated",
|
|
2072
|
-
"unknown"
|
|
2073
|
-
];
|
|
2074
|
-
function isAgent(value) {
|
|
2075
|
-
return AGENTS.includes(value);
|
|
2076
|
-
}
|
|
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";
|
|
2077
59
|
// src/lib/store/index.ts
|
|
2078
|
-
init_database();
|
|
2079
|
-
init_pricing();
|
|
2080
60
|
import { randomUUID } from "crypto";
|
|
2081
61
|
// package.json
|
|
2082
62
|
var package_default = {
|
|
2083
63
|
name: "@hasna/economy",
|
|
2084
|
-
version: "0.
|
|
64
|
+
version: "0.5.1",
|
|
2085
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",
|
|
2086
66
|
repository: {
|
|
2087
67
|
type: "git",
|
|
2088
|
-
url: "
|
|
68
|
+
url: "https://github.com/hasna/apps.git",
|
|
69
|
+
directory: "apps/economy"
|
|
2089
70
|
},
|
|
2090
71
|
bugs: {
|
|
2091
|
-
url: "https://github.com/hasna/
|
|
72
|
+
url: "https://github.com/hasna/apps/issues"
|
|
2092
73
|
},
|
|
2093
|
-
homepage: "https://github.com/hasna/economy#readme",
|
|
74
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
|
|
2094
75
|
type: "module",
|
|
2095
76
|
main: "dist/index.js",
|
|
2096
77
|
types: "dist/index.d.ts",
|
|
@@ -2119,14 +100,15 @@ var package_default = {
|
|
|
2119
100
|
"CHANGELOG.md",
|
|
2120
101
|
"SECURITY.md",
|
|
2121
102
|
"CONTRIBUTING.md",
|
|
2122
|
-
"CODE_OF_CONDUCT.md"
|
|
103
|
+
"CODE_OF_CONDUCT.md",
|
|
104
|
+
"!dist/lib/test-hermetic-*"
|
|
2123
105
|
],
|
|
2124
106
|
scripts: {
|
|
2125
|
-
build:
|
|
2126
|
-
"build:cli":
|
|
2127
|
-
"build:mcp":
|
|
2128
|
-
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
2129
|
-
"build:lib":
|
|
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]"',
|
|
2130
112
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
2131
113
|
prepack: "bun run build && bun run artifact-scan",
|
|
2132
114
|
typecheck: "tsc --noEmit",
|
|
@@ -2163,7 +145,8 @@ var package_default = {
|
|
|
2163
145
|
chalk: "^5.4.1",
|
|
2164
146
|
commander: "^13.1.0",
|
|
2165
147
|
pg: "8.13.1",
|
|
2166
|
-
zod: "^3.24.2"
|
|
148
|
+
zod: "^3.24.2",
|
|
149
|
+
"@hasna/trash": "0.2.3"
|
|
2167
150
|
},
|
|
2168
151
|
devDependencies: {
|
|
2169
152
|
"@types/bun": "1.3.14",
|
|
@@ -2173,7 +156,16 @@ var package_default = {
|
|
|
2173
156
|
},
|
|
2174
157
|
trustedDependencies: [
|
|
2175
158
|
"@hasna/projects"
|
|
2176
|
-
]
|
|
159
|
+
],
|
|
160
|
+
overrides: {
|
|
161
|
+
"fast-uri": "3.1.8",
|
|
162
|
+
hono: "4.13.7",
|
|
163
|
+
"ip-address": "10.7.1",
|
|
164
|
+
nanoid: "5.1.16",
|
|
165
|
+
"path-to-regexp": "8.4.0",
|
|
166
|
+
undici: "6.28.1",
|
|
167
|
+
"express-rate-limit": "8.7.0"
|
|
168
|
+
}
|
|
2177
169
|
};
|
|
2178
170
|
|
|
2179
171
|
// src/lib/package-metadata.ts
|
|
@@ -2190,254 +182,277 @@ function getPackageMetadata() {
|
|
|
2190
182
|
var packageMetadata = getPackageMetadata();
|
|
2191
183
|
|
|
2192
184
|
// src/lib/savings.ts
|
|
2193
|
-
function
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
const now = new Date;
|
|
2211
|
-
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
2212
|
-
switch (period) {
|
|
2213
|
-
case "today":
|
|
2214
|
-
case "yesterday":
|
|
2215
|
-
return monthlyFee / daysInMonth;
|
|
2216
|
-
case "week":
|
|
2217
|
-
return monthlyFee / daysInMonth * 7;
|
|
2218
|
-
case "month":
|
|
2219
|
-
return monthlyFee;
|
|
2220
|
-
case "year":
|
|
2221
|
-
return monthlyFee * 12;
|
|
2222
|
-
case "all":
|
|
2223
|
-
return monthlyFee;
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
function proratedIncludedConsumed(includedUsage, includedCap, period) {
|
|
2227
|
-
const cap = prorateMonthlyFee(includedCap, period);
|
|
2228
|
-
return cap > 0 ? Math.min(includedUsage, cap) : includedUsage;
|
|
2229
|
-
}
|
|
2230
|
-
function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
|
|
2231
|
-
return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
|
|
185
|
+
function requireCurrentSavingsReport(value) {
|
|
186
|
+
if (!value || typeof value !== "object")
|
|
187
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid savings response");
|
|
188
|
+
const report = value;
|
|
189
|
+
if (report.comparison_basis !== "included_usage_vs_current_plan_estimate" || !["estimated", "not_applicable", "unavailable"].includes(String(report.comparison_status))) {
|
|
190
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: The service must support the current subscription comparison contract");
|
|
191
|
+
}
|
|
192
|
+
for (const field of ["api_equivalent_usd", "subscription_fee_usd", "on_demand_usd", "saved_usd", "net_comparison_usd"]) {
|
|
193
|
+
const number = report[field];
|
|
194
|
+
if (number !== null && (typeof number !== "number" || !Number.isFinite(number))) {
|
|
195
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid monetary field " + field);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (report.comparison_status === "unavailable" && (report.saved_usd !== null || report.net_comparison_usd !== null)) {
|
|
199
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Unavailable comparison carries a monetary claim");
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
2232
202
|
}
|
|
2233
|
-
function
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
const
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
COALESCE(SUM(included_usage_usd), 0) as included
|
|
2260
|
-
FROM subscriptions
|
|
2261
|
-
WHERE active = 1${agent ? " AND (agent = ? OR agent IS NULL)" : ""}
|
|
2262
|
-
`).get(...agent ? [agent] : []);
|
|
2263
|
-
const subscriptionFee = prorateMonthlyFee(subs.fee, period);
|
|
2264
|
-
const apiEquivalent = apiRow.total + includedRow.total;
|
|
2265
|
-
const includedConsumed = proratedIncludedConsumed(includedRow.total, subs.included, period);
|
|
2266
|
-
const onDemand = onDemandRow.total;
|
|
2267
|
-
const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
|
|
2268
|
-
const byAgent = {};
|
|
2269
|
-
if (!agent) {
|
|
2270
|
-
const onDemandByAgent = new Map;
|
|
2271
|
-
for (const row of db.prepare(`
|
|
2272
|
-
SELECT agent, COALESCE(SUM(value), 0) as total
|
|
2273
|
-
FROM usage_snapshots
|
|
2274
|
-
WHERE ${subWhere}
|
|
2275
|
-
AND metric = 'on_demand_usd'
|
|
2276
|
-
GROUP BY agent
|
|
2277
|
-
`).all()) {
|
|
2278
|
-
onDemandByAgent.set(row.agent, row.total);
|
|
203
|
+
function prorateMonthlyFee(monthlyFee, period, now = new Date) {
|
|
204
|
+
if (!Number.isFinite(monthlyFee) || monthlyFee < 0)
|
|
205
|
+
throw new RangeError("Monthly fee must be finite and nonnegative");
|
|
206
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
207
|
+
if (bounds.start_date === null)
|
|
208
|
+
return null;
|
|
209
|
+
let cursor = new Date(bounds.start_date + "T00:00:00.000Z");
|
|
210
|
+
const end = new Date(bounds.end_date_exclusive + "T00:00:00.000Z");
|
|
211
|
+
let fee = 0;
|
|
212
|
+
while (cursor < end) {
|
|
213
|
+
const monthEnd = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 1));
|
|
214
|
+
const next = new Date(Math.min(end.getTime(), monthEnd.getTime()));
|
|
215
|
+
const days = (next.getTime() - cursor.getTime()) / 86400000;
|
|
216
|
+
const monthDays = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 0)).getUTCDate();
|
|
217
|
+
fee += monthlyFee / monthDays * days;
|
|
218
|
+
cursor = next;
|
|
219
|
+
}
|
|
220
|
+
return fee;
|
|
221
|
+
}
|
|
222
|
+
function feeFor(plans, period, now) {
|
|
223
|
+
let known = 0;
|
|
224
|
+
let complete = true;
|
|
225
|
+
for (const plan of plans) {
|
|
226
|
+
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) {
|
|
227
|
+
complete = false;
|
|
228
|
+
continue;
|
|
2279
229
|
}
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
230
|
+
const amount = prorateMonthlyFee(plan.monthly_fee_usd, period, now);
|
|
231
|
+
if (amount === null || !Number.isFinite(amount) || !Number.isFinite(known + amount))
|
|
232
|
+
complete = false;
|
|
233
|
+
else
|
|
234
|
+
known += amount;
|
|
235
|
+
}
|
|
236
|
+
return { known, total: complete ? known : null };
|
|
237
|
+
}
|
|
238
|
+
function summarizeOnDemand(observations, period, now = new Date) {
|
|
239
|
+
const rows = observations.filter((row) => row.metric === "on_demand_usd");
|
|
240
|
+
if (!rows.length)
|
|
241
|
+
return { usd: null, status: "not_observed" };
|
|
242
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
243
|
+
const start = bounds.start_date ? Date.parse(bounds.start_date + "T00:00:00Z") : -Infinity;
|
|
244
|
+
const end = bounds.end_date_exclusive ? Date.parse(bounds.end_date_exclusive + "T00:00:00Z") : Infinity;
|
|
245
|
+
const windows = new Map;
|
|
246
|
+
for (const row of rows) {
|
|
247
|
+
const from = Date.parse(row.window_start ?? "");
|
|
248
|
+
const until = Date.parse(row.window_end ?? "");
|
|
249
|
+
const at = Date.parse(row.source_timestamp ?? row.observed_at ?? "");
|
|
250
|
+
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") {
|
|
251
|
+
return { usd: null, status: "unavailable" };
|
|
2290
252
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
253
|
+
if (until <= start || from >= end)
|
|
254
|
+
continue;
|
|
255
|
+
if (from < start || until > end)
|
|
256
|
+
return { usd: null, status: "unavailable" };
|
|
257
|
+
const key = JSON.stringify([row.provider, row.account_key, from, until]);
|
|
258
|
+
const previous = windows.get(key);
|
|
259
|
+
if (previous && previous.row.value_kind !== row.value_kind)
|
|
260
|
+
return { usd: null, status: "unavailable" };
|
|
261
|
+
if (previous && ((row.value_kind === "delta" || at === previous.at) && previous.row.value !== row.value)) {
|
|
262
|
+
return { usd: null, status: "unavailable" };
|
|
263
|
+
}
|
|
264
|
+
if (!previous || at > previous.at)
|
|
265
|
+
windows.set(key, { row, at, start: from, end: until });
|
|
266
|
+
}
|
|
267
|
+
const byAccount = new Map;
|
|
268
|
+
let total = 0;
|
|
269
|
+
for (const window of windows.values()) {
|
|
270
|
+
const key = JSON.stringify([window.row.provider, window.row.account_key]);
|
|
271
|
+
const intervals = byAccount.get(key) ?? [];
|
|
272
|
+
if (intervals.some((other) => window.start < other.end && window.end > other.start)) {
|
|
273
|
+
return { usd: null, status: "unavailable" };
|
|
274
|
+
}
|
|
275
|
+
intervals.push(window);
|
|
276
|
+
byAccount.set(key, intervals);
|
|
277
|
+
total += window.row.value;
|
|
278
|
+
}
|
|
279
|
+
return windows.size ? { usd: total, status: "observed" } : { usd: null, status: "not_observed" };
|
|
280
|
+
}
|
|
281
|
+
function buildSavingsSummary(period, usage, plans, observations, agent, now = new Date, prices = []) {
|
|
282
|
+
const selected = usage.filter((row) => !agent || row.agent === agent);
|
|
283
|
+
const activePlans = plans.filter((row) => row.active === 1);
|
|
284
|
+
const selectedPlans = activePlans.filter((row) => !agent || row.agent === agent);
|
|
285
|
+
const sharedPlans = activePlans.filter((row) => !row.agent || !row.account_key);
|
|
286
|
+
const fee = feeFor(selectedPlans, period, now);
|
|
287
|
+
const unallocated = feeFor(sharedPlans, period, now);
|
|
288
|
+
const reasons = new Set;
|
|
289
|
+
let priced = 0, metered = 0, estimated = 0, included = 0, unknownPrices = 0, unknownBilling = 0, estimatedSessions = 0;
|
|
290
|
+
for (const row of selected) {
|
|
291
|
+
if (row.session_count) {
|
|
292
|
+
estimatedSessions += row.session_count;
|
|
293
|
+
reasons.add("billing_route_unknown");
|
|
294
|
+
}
|
|
295
|
+
if (!hasKnownUsagePrice(row, prices)) {
|
|
296
|
+
unknownPrices += row.requests;
|
|
297
|
+
reasons.add("model_price_unknown");
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
priced += row.cost_usd;
|
|
301
|
+
if (row.cost_basis === "metered_api")
|
|
302
|
+
metered += row.cost_usd;
|
|
303
|
+
else if (row.cost_basis === "subscription_included") {
|
|
304
|
+
included += row.cost_usd;
|
|
305
|
+
const matches = selectedPlans.filter((plan) => !!row.account_key && plan.account_key === row.account_key);
|
|
306
|
+
if (matches.length === 0)
|
|
307
|
+
reasons.add("subscription_account_unmapped");
|
|
308
|
+
if (matches.length > 1)
|
|
309
|
+
reasons.add("subscription_account_ambiguous");
|
|
310
|
+
} else {
|
|
311
|
+
estimated += row.cost_usd;
|
|
312
|
+
unknownBilling += row.requests;
|
|
313
|
+
reasons.add("billing_route_unknown");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (unknownPrices)
|
|
317
|
+
reasons.add("model_price_unknown");
|
|
318
|
+
if (unknownBilling)
|
|
319
|
+
reasons.add("billing_route_unknown");
|
|
320
|
+
if (fee.total === null)
|
|
321
|
+
reasons.add(period === "all" ? "historical_plan_fees_unavailable" : "plan_fee_unknown");
|
|
322
|
+
if (included > 0 && !agent && sharedPlans.some((plan) => !plan.account_key))
|
|
323
|
+
reasons.add("subscription_account_unmapped");
|
|
324
|
+
const demand = summarizeOnDemand(observations.filter((row) => !agent || row.agent === agent), period, now);
|
|
325
|
+
const applicable = selectedPlans.length > 0 || selected.some((row) => row.cost_basis === "subscription_included");
|
|
326
|
+
const net = reasons.size ? null : applicable ? included - fee.known : 0;
|
|
327
|
+
const byAgent = {};
|
|
328
|
+
if (!agent) {
|
|
329
|
+
const agents = new Set([...usage.map((row) => row.agent), ...activePlans.flatMap((row) => row.agent ? [row.agent] : [])]);
|
|
330
|
+
for (const name of [...agents].sort()) {
|
|
331
|
+
const result = buildSavingsSummary(period, usage, plans, observations, name, now, prices);
|
|
332
|
+
const { by_agent: _, ...scalars } = result;
|
|
333
|
+
byAgent[name] = scalars;
|
|
2326
334
|
}
|
|
2327
335
|
}
|
|
2328
336
|
return {
|
|
2329
337
|
period,
|
|
2330
|
-
api_equivalent_usd:
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
338
|
+
api_equivalent_usd: reasons.has("model_price_unknown") ? null : priced,
|
|
339
|
+
priced_usage_value_usd: priced,
|
|
340
|
+
recorded_metered_api_usd: metered,
|
|
341
|
+
estimated_usage_value_usd: estimated,
|
|
342
|
+
subscription_fee_usd: fee.total,
|
|
343
|
+
known_subscription_fee_usd: fee.known,
|
|
344
|
+
unallocated_subscription_fee_usd: unallocated.total,
|
|
345
|
+
included_consumed_usd: included,
|
|
346
|
+
on_demand_usd: demand.usd,
|
|
347
|
+
on_demand_status: demand.status,
|
|
348
|
+
saved_usd: net === null ? null : Math.max(0, net),
|
|
349
|
+
net_comparison_usd: net,
|
|
350
|
+
comparison_status: reasons.size ? "unavailable" : applicable ? "estimated" : "not_applicable",
|
|
351
|
+
comparison_basis: "included_usage_vs_current_plan_estimate",
|
|
352
|
+
unavailable_reasons: [...reasons].sort(),
|
|
353
|
+
unknown_price_requests: unknownPrices,
|
|
354
|
+
unknown_billing_requests: unknownBilling,
|
|
355
|
+
estimated_only_sessions: estimatedSessions,
|
|
2335
356
|
by_agent: byAgent
|
|
2336
357
|
};
|
|
2337
358
|
}
|
|
359
|
+
function querySavingsSummary(db, period, agent) {
|
|
360
|
+
const now = new Date;
|
|
361
|
+
const usage = db.prepare(`
|
|
362
|
+
SELECT agent, account_key, cost_basis, model,
|
|
363
|
+
CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END AS zero_cost,
|
|
364
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd, COUNT(*) AS requests,
|
|
365
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
366
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_create_tokens),0) AS cache_create_tokens,
|
|
367
|
+
COALESCE(SUM(cache_create_1h_tokens),0) AS cache_create_1h_tokens
|
|
368
|
+
FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp", now)}
|
|
369
|
+
GROUP BY agent, account_key, cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END
|
|
370
|
+
`).all();
|
|
371
|
+
const plans = db.prepare("SELECT * FROM subscriptions WHERE active = 1").all();
|
|
372
|
+
usage.push(...db.prepare(`
|
|
373
|
+
SELECT s.agent, s.account_key, 'estimated' AS cost_basis,
|
|
374
|
+
COALESCE(SUM(s.total_cost_usd), 0) AS cost_usd,
|
|
375
|
+
COALESCE(SUM(s.request_count), 0) AS requests, COUNT(*) AS session_count
|
|
376
|
+
FROM economy_effective_sessions s WHERE ${sessionOverlapWhere(period, "s", now)}
|
|
377
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
|
|
378
|
+
GROUP BY s.agent, s.account_key
|
|
379
|
+
`).all());
|
|
380
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
381
|
+
const windowWhere = bounds.start_date === null ? "1=1" : `(
|
|
382
|
+
(window_start IS NOT NULL AND window_end IS NOT NULL
|
|
383
|
+
AND window_start < '${bounds.end_date_exclusive}T00:00:00.000Z'
|
|
384
|
+
AND window_end > '${bounds.start_date}T00:00:00.000Z')
|
|
385
|
+
OR ((window_start IS NULL OR window_end IS NULL) AND ${calendarPeriodWhere(period, "date", now)})
|
|
386
|
+
)`;
|
|
387
|
+
const observations = db.prepare(`
|
|
388
|
+
SELECT * FROM usage_snapshots WHERE metric = 'on_demand_usd'
|
|
389
|
+
AND ${windowWhere}
|
|
390
|
+
`).all();
|
|
391
|
+
return buildSavingsSummary(period, usage, plans, observations, agent, now, listModelPricing(db));
|
|
392
|
+
}
|
|
2338
393
|
|
|
2339
394
|
// src/lib/billing-diff.ts
|
|
2340
|
-
init_database();
|
|
2341
|
-
var PROVIDER_TO_AGENT = {
|
|
2342
|
-
anthropic: "claude",
|
|
2343
|
-
openai: "codex",
|
|
2344
|
-
gemini: "gemini",
|
|
2345
|
-
google: "gemini"
|
|
2346
|
-
};
|
|
2347
|
-
function billingDeltaPct(estimatedUsd, actualUsd) {
|
|
2348
|
-
if (!(actualUsd > 0))
|
|
2349
|
-
return null;
|
|
2350
|
-
return (estimatedUsd - actualUsd) / actualUsd * 100;
|
|
2351
|
-
}
|
|
2352
395
|
function queryBillingDiff(db, period, thresholdPct = 15) {
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
const
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
const agentRows = db.prepare(`
|
|
2362
|
-
SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
|
|
2363
|
-
FROM requests
|
|
2364
|
-
WHERE ${periodWhere3(period, "timestamp")}
|
|
2365
|
-
GROUP BY agent
|
|
396
|
+
if (!Number.isFinite(thresholdPct) || thresholdPct < 0)
|
|
397
|
+
throw new RangeError("thresholdPct must be a finite nonnegative number");
|
|
398
|
+
const usage = querySummary(db, period, undefined, true);
|
|
399
|
+
const billing = queryBillingSummary(db, period);
|
|
400
|
+
const rows = db.prepare(`
|
|
401
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) AS estimated_usd
|
|
402
|
+
FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp")}
|
|
403
|
+
GROUP BY agent ORDER BY agent
|
|
2366
404
|
`).all();
|
|
2367
|
-
|
|
2368
|
-
const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
|
|
2369
|
-
const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
|
|
2370
|
-
const rowDelta = row.estimated_usd - actualUsd;
|
|
2371
|
-
const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
|
|
2372
|
-
return {
|
|
2373
|
-
agent: row.agent,
|
|
2374
|
-
estimated_usd: row.estimated_usd,
|
|
2375
|
-
actual_usd: actualUsd,
|
|
2376
|
-
delta_usd: rowDelta,
|
|
2377
|
-
delta_pct: rowPct
|
|
2378
|
-
};
|
|
2379
|
-
}).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
|
|
2380
|
-
return {
|
|
405
|
+
return requireCurrentBillingDiffSummary({
|
|
2381
406
|
period,
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
407
|
+
basis: "unmatched_subtotals",
|
|
408
|
+
estimated_usd: usage.total_usd,
|
|
409
|
+
actual_usd: billing.total_usd,
|
|
410
|
+
usage_summary: usage,
|
|
411
|
+
billing_summary: billing,
|
|
412
|
+
delta_usd: null,
|
|
413
|
+
delta_pct: null,
|
|
2386
414
|
threshold_pct: thresholdPct,
|
|
2387
|
-
is_alert:
|
|
2388
|
-
comparable,
|
|
2389
|
-
incomparable_reason:
|
|
2390
|
-
by_agent,
|
|
2391
|
-
by_provider:
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
function periodWhere3(period, column) {
|
|
2395
|
-
switch (period) {
|
|
2396
|
-
case "today":
|
|
2397
|
-
return `DATE(${column}) = DATE('now')`;
|
|
2398
|
-
case "yesterday":
|
|
2399
|
-
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
2400
|
-
case "week":
|
|
2401
|
-
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
2402
|
-
case "month":
|
|
2403
|
-
return `${column} >= DATE('now', 'start of month')`;
|
|
2404
|
-
case "year":
|
|
2405
|
-
return `${column} >= DATE('now', 'start of year')`;
|
|
2406
|
-
case "all":
|
|
2407
|
-
return "1=1";
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
// src/lib/periods.ts
|
|
2412
|
-
function ymd(date) {
|
|
2413
|
-
return date.toISOString().substring(0, 10);
|
|
415
|
+
is_alert: null,
|
|
416
|
+
comparable: false,
|
|
417
|
+
incomparable_reason: billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified",
|
|
418
|
+
by_agent: rows.map((row) => ({ ...row, basis: "stored_request_rows_subtotal", actual_usd: null, delta_usd: null, delta_pct: null })),
|
|
419
|
+
by_provider: billing.by_provider
|
|
420
|
+
});
|
|
2414
421
|
}
|
|
2415
|
-
function
|
|
2416
|
-
const
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
422
|
+
function requireCurrentBillingDiffSummary(value) {
|
|
423
|
+
const invalid = () => {
|
|
424
|
+
throw new Error("BILLING_COMPARISON_UNAVAILABLE: The service must return scoped unmatched subtotals");
|
|
425
|
+
};
|
|
426
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
427
|
+
return invalid();
|
|
428
|
+
const s = value;
|
|
429
|
+
if (s.basis !== "unmatched_subtotals" || s.comparable !== false || s.delta_usd !== null || s.delta_pct !== null || s.is_alert !== null)
|
|
430
|
+
return invalid();
|
|
431
|
+
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)
|
|
432
|
+
return invalid();
|
|
433
|
+
let usage, billing;
|
|
434
|
+
try {
|
|
435
|
+
usage = requireCurrentCostSummary(s.usage_summary);
|
|
436
|
+
billing = requireCurrentBillingSummary(s.billing_summary);
|
|
437
|
+
} catch {
|
|
438
|
+
return invalid();
|
|
439
|
+
}
|
|
440
|
+
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))
|
|
441
|
+
return invalid();
|
|
442
|
+
if (s.incomparable_reason !== (billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified"))
|
|
443
|
+
return invalid();
|
|
444
|
+
const providers = s.by_provider;
|
|
445
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers) || !billing.by_provider || typeof billing.by_provider !== "object")
|
|
446
|
+
return invalid();
|
|
447
|
+
const expected = Object.entries(billing.by_provider);
|
|
448
|
+
if (Object.keys(providers).length !== expected.length || expected.some(([key, amount]) => typeof amount !== "number" || !Number.isFinite(amount) || providers[key] !== amount))
|
|
449
|
+
return invalid();
|
|
450
|
+
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))
|
|
451
|
+
return invalid();
|
|
452
|
+
return value;
|
|
2437
453
|
}
|
|
2438
454
|
|
|
2439
455
|
// src/lib/brief.ts
|
|
2440
|
-
init_database();
|
|
2441
456
|
var ZERO_REQUEST_TOTALS = {
|
|
2442
457
|
sessions: 0,
|
|
2443
458
|
request_sessions: 0,
|
|
@@ -2481,15 +496,11 @@ function machineFilter(machine) {
|
|
|
2481
496
|
return;
|
|
2482
497
|
return machine;
|
|
2483
498
|
}
|
|
2484
|
-
function
|
|
2485
|
-
|
|
2486
|
-
return `DATE(timestamp) = DATE('now')`;
|
|
2487
|
-
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
499
|
+
function requestPeriodWhere(period) {
|
|
500
|
+
return calendarPeriodWhere(period, "timestamp");
|
|
2488
501
|
}
|
|
2489
|
-
function
|
|
2490
|
-
|
|
2491
|
-
return `DATE(started_at) = DATE('now')`;
|
|
2492
|
-
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
502
|
+
function sessionPeriodWhere(period) {
|
|
503
|
+
return calendarPeriodWhere(period, "started_at");
|
|
2493
504
|
}
|
|
2494
505
|
function baseRequestTotals(row) {
|
|
2495
506
|
return {
|
|
@@ -2560,7 +571,7 @@ function requestTotalsSql(where, machine) {
|
|
|
2560
571
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
2561
572
|
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
2562
573
|
MAX(timestamp) as last_active
|
|
2563
|
-
FROM
|
|
574
|
+
FROM economy_effective_requests
|
|
2564
575
|
WHERE ${where}${machineClause}
|
|
2565
576
|
`,
|
|
2566
577
|
params
|
|
@@ -2581,9 +592,9 @@ function sessionOnlyTotalsSql(where, machine) {
|
|
|
2581
592
|
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
2582
593
|
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
2583
594
|
MAX(started_at) as last_active
|
|
2584
|
-
FROM
|
|
595
|
+
FROM economy_effective_sessions
|
|
2585
596
|
WHERE ${where}${machineClause}
|
|
2586
|
-
AND NOT EXISTS (SELECT 1 FROM
|
|
597
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
2587
598
|
`,
|
|
2588
599
|
params
|
|
2589
600
|
};
|
|
@@ -2598,15 +609,16 @@ function querySessionOnlyTotals(db, where, params, machine) {
|
|
|
2598
609
|
}
|
|
2599
610
|
function queryPeriodTotals(db, period, machine) {
|
|
2600
611
|
const summary = querySummary(db, period, machine);
|
|
2601
|
-
const requests = queryRequestTotals(db,
|
|
2602
|
-
const sessions = querySessionOnlyTotals(db,
|
|
612
|
+
const requests = queryRequestTotals(db, requestPeriodWhere(period), [], machine);
|
|
613
|
+
const sessions = querySessionOnlyTotals(db, sessionPeriodWhere(period), [], machine);
|
|
2603
614
|
const totals = combineTotals(requests, sessions);
|
|
2604
615
|
return {
|
|
2605
616
|
...totals,
|
|
2606
617
|
sessions: summary.sessions,
|
|
2607
618
|
requests: summary.requests,
|
|
2608
619
|
total_tokens: summary.tokens,
|
|
2609
|
-
cost_usd: summary.total_usd
|
|
620
|
+
cost_usd: summary.total_usd,
|
|
621
|
+
cost_summary: summary
|
|
2610
622
|
};
|
|
2611
623
|
}
|
|
2612
624
|
function querySinceTotals(db, since, machine) {
|
|
@@ -2659,7 +671,7 @@ function queryMachineSinceRows(db, since, machine) {
|
|
|
2659
671
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
2660
672
|
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
2661
673
|
MAX(timestamp) as last_active
|
|
2662
|
-
FROM
|
|
674
|
+
FROM economy_effective_requests
|
|
2663
675
|
WHERE timestamp >= ?
|
|
2664
676
|
AND machine_id != ''
|
|
2665
677
|
${requestMachineClause}
|
|
@@ -2674,11 +686,11 @@ function queryMachineSinceRows(db, since, machine) {
|
|
|
2674
686
|
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
2675
687
|
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
2676
688
|
MAX(started_at) as last_active
|
|
2677
|
-
FROM
|
|
689
|
+
FROM economy_effective_sessions
|
|
2678
690
|
WHERE started_at >= ?
|
|
2679
691
|
AND machine_id != ''
|
|
2680
692
|
${sessionMachineClause}
|
|
2681
|
-
AND NOT EXISTS (SELECT 1 FROM
|
|
693
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
2682
694
|
GROUP BY machine_id
|
|
2683
695
|
`).all(...machine ? [since, machine] : [since]);
|
|
2684
696
|
const groups = new Map;
|
|
@@ -2716,13 +728,13 @@ function queryMachineLastData(db, machine) {
|
|
|
2716
728
|
const filter = machine ? " AND machine_id = ?" : "";
|
|
2717
729
|
const requestRows = db.prepare(`
|
|
2718
730
|
SELECT machine_id, MAX(timestamp) as last_at
|
|
2719
|
-
FROM
|
|
731
|
+
FROM economy_effective_requests
|
|
2720
732
|
WHERE machine_id != ''${filter}
|
|
2721
733
|
GROUP BY machine_id
|
|
2722
734
|
`).all(...machine ? [machine] : []);
|
|
2723
735
|
const sessionRows = db.prepare(`
|
|
2724
736
|
SELECT machine_id, MAX(started_at) as last_at
|
|
2725
|
-
FROM
|
|
737
|
+
FROM economy_effective_sessions
|
|
2726
738
|
WHERE machine_id != ''${filter}
|
|
2727
739
|
GROUP BY machine_id
|
|
2728
740
|
`).all(...machine ? [machine] : []);
|
|
@@ -2750,7 +762,7 @@ function queryAgentSinceRows(db, since, machine) {
|
|
|
2750
762
|
input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
|
|
2751
763
|
cost_usd,
|
|
2752
764
|
timestamp as last_active
|
|
2753
|
-
FROM
|
|
765
|
+
FROM economy_effective_requests
|
|
2754
766
|
WHERE timestamp >= ?${requestMachineClause}
|
|
2755
767
|
),
|
|
2756
768
|
session_only_rows AS (
|
|
@@ -2767,9 +779,9 @@ function queryAgentSinceRows(db, since, machine) {
|
|
|
2767
779
|
COALESCE(total_tokens, 0) as total_tokens,
|
|
2768
780
|
COALESCE(total_cost_usd, 0) as cost_usd,
|
|
2769
781
|
started_at as last_active
|
|
2770
|
-
FROM
|
|
782
|
+
FROM economy_effective_sessions
|
|
2771
783
|
WHERE started_at >= ?${sessionMachineClause}
|
|
2772
|
-
AND NOT EXISTS (SELECT 1 FROM
|
|
784
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
2773
785
|
),
|
|
2774
786
|
combined AS (
|
|
2775
787
|
SELECT * FROM request_rows
|
|
@@ -2813,12 +825,12 @@ function agentBreakdownToBriefRow(row) {
|
|
|
2813
825
|
last_active: row.last_active
|
|
2814
826
|
};
|
|
2815
827
|
}
|
|
2816
|
-
function
|
|
828
|
+
function normalizeAccountEmail(email) {
|
|
2817
829
|
return email.trim().toLowerCase();
|
|
2818
830
|
}
|
|
2819
831
|
function accountKey(row) {
|
|
2820
832
|
const agent = row.agent || row.account_tool || "unknown";
|
|
2821
|
-
const email =
|
|
833
|
+
const email = normalizeAccountEmail(row.account_email);
|
|
2822
834
|
if (email)
|
|
2823
835
|
return `${agent}:${email}`;
|
|
2824
836
|
if (row.account_name)
|
|
@@ -2829,7 +841,7 @@ function addAccountRow(groups, row) {
|
|
|
2829
841
|
if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
|
|
2830
842
|
return;
|
|
2831
843
|
const key = accountKey(row);
|
|
2832
|
-
const email =
|
|
844
|
+
const email = normalizeAccountEmail(row.account_email);
|
|
2833
845
|
const current = groups.get(key) ?? {
|
|
2834
846
|
account_key: key,
|
|
2835
847
|
account_tool: row.agent || row.account_tool || "unknown",
|
|
@@ -2891,8 +903,8 @@ function queryAccountSinceRows(db, since, machine) {
|
|
|
2891
903
|
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
2892
904
|
COALESCE(SUM(r.cost_usd), 0) as cost_usd,
|
|
2893
905
|
MAX(r.timestamp) as last_active
|
|
2894
|
-
FROM
|
|
2895
|
-
LEFT JOIN
|
|
906
|
+
FROM economy_effective_requests r
|
|
907
|
+
LEFT JOIN economy_effective_sessions s ON s.id = r.session_id
|
|
2896
908
|
WHERE r.timestamp >= ?${requestMachineClause}
|
|
2897
909
|
GROUP BY
|
|
2898
910
|
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
|
|
@@ -2921,9 +933,9 @@ function queryAccountSinceRows(db, since, machine) {
|
|
|
2921
933
|
COALESCE(SUM(s.total_tokens), 0) as total_tokens,
|
|
2922
934
|
COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
|
|
2923
935
|
MAX(s.started_at) as last_active
|
|
2924
|
-
FROM
|
|
936
|
+
FROM economy_effective_sessions s
|
|
2925
937
|
WHERE s.started_at >= ?${sessionMachineClause}
|
|
2926
|
-
AND NOT EXISTS (SELECT 1 FROM
|
|
938
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
|
|
2927
939
|
GROUP BY
|
|
2928
940
|
s.agent,
|
|
2929
941
|
s.account_key,
|
|
@@ -2967,7 +979,7 @@ function queryFreshness(db, now, machine, localSync) {
|
|
|
2967
979
|
const requestFilter = machine ? " AND machine_id = ?" : "";
|
|
2968
980
|
const requestRows = db.prepare(`
|
|
2969
981
|
SELECT machine_id, MAX(timestamp) as max_request_at
|
|
2970
|
-
FROM
|
|
982
|
+
FROM economy_effective_requests
|
|
2971
983
|
WHERE machine_id != ''${requestFilter}
|
|
2972
984
|
GROUP BY machine_id
|
|
2973
985
|
`).all(...machine ? [machine] : []);
|
|
@@ -3061,7 +1073,7 @@ function buildBrief(db, opts = {}) {
|
|
|
3061
1073
|
function queryProjectDetail(db, nameOrPath) {
|
|
3062
1074
|
const like = `%${nameOrPath}%`;
|
|
3063
1075
|
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
3064
|
-
FROM
|
|
1076
|
+
FROM economy_effective_sessions
|
|
3065
1077
|
WHERE project_name LIKE ? OR project_path LIKE ?
|
|
3066
1078
|
ORDER BY started_at DESC`).all(like, like);
|
|
3067
1079
|
if (sessions.length === 0)
|
|
@@ -3070,12 +1082,12 @@ function queryProjectDetail(db, nameOrPath) {
|
|
|
3070
1082
|
const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
|
|
3071
1083
|
const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
|
|
3072
1084
|
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
3073
|
-
FROM
|
|
1085
|
+
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
3074
1086
|
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
3075
1087
|
AND r.timestamp >= DATE('now', '-14 days')
|
|
3076
1088
|
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
3077
1089
|
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
3078
|
-
FROM
|
|
1090
|
+
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
3079
1091
|
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
3080
1092
|
GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
|
|
3081
1093
|
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 }));
|
|
@@ -3091,45 +1103,39 @@ function queryProjectDetail(db, nameOrPath) {
|
|
|
3091
1103
|
};
|
|
3092
1104
|
}
|
|
3093
1105
|
function exportDateWhere(column, period) {
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
return `DATE(${column}) = DATE('now')`;
|
|
3097
|
-
case "week":
|
|
3098
|
-
return `${column} >= DATE('now', '-7 days')`;
|
|
3099
|
-
case "all":
|
|
3100
|
-
return "1=1";
|
|
3101
|
-
default:
|
|
3102
|
-
return `${column} >= DATE('now', '-30 days')`;
|
|
1106
|
+
if (!["today", "yesterday", "week", "month", "year", "all"].includes(period)) {
|
|
1107
|
+
throw new RangeError("Invalid export period");
|
|
3103
1108
|
}
|
|
1109
|
+
return calendarPeriodWhere(period, column);
|
|
3104
1110
|
}
|
|
3105
1111
|
function queryExportRows(db, type, period) {
|
|
3106
1112
|
if (type === "requests") {
|
|
3107
1113
|
const where2 = exportDateWhere("timestamp", period);
|
|
3108
|
-
return db.prepare(`SELECT * FROM
|
|
1114
|
+
return db.prepare(`SELECT * FROM economy_effective_requests WHERE ${where2} ORDER BY timestamp ASC`).all();
|
|
3109
1115
|
}
|
|
3110
1116
|
const where = exportDateWhere("started_at", period);
|
|
3111
|
-
return db.prepare(`SELECT * FROM
|
|
1117
|
+
return db.prepare(`SELECT * FROM economy_effective_sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
3112
1118
|
}
|
|
3113
1119
|
function queryRangeStats(db, from, to) {
|
|
3114
1120
|
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
|
|
3115
1121
|
COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
|
|
3116
|
-
FROM
|
|
3117
|
-
const s = db.prepare(`SELECT COUNT(*) as sessions FROM
|
|
1122
|
+
FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
1123
|
+
const s = db.prepare(`SELECT COUNT(*) as sessions FROM economy_effective_sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
3118
1124
|
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
3119
1125
|
}
|
|
3120
1126
|
function queryForecast(db, now = new Date) {
|
|
3121
1127
|
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
3122
1128
|
const dayOfMonth = now.getDate();
|
|
3123
1129
|
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
3124
|
-
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM
|
|
1130
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
3125
1131
|
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
3126
1132
|
const projected = dailyAvg * daysInMonth;
|
|
3127
1133
|
const sevenDaysAgo = new Date(now);
|
|
3128
1134
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
3129
|
-
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM
|
|
1135
|
+
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));
|
|
3130
1136
|
const last7DailyAvg = last7.cost / 7;
|
|
3131
1137
|
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
3132
|
-
FROM
|
|
1138
|
+
FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
|
|
3133
1139
|
return {
|
|
3134
1140
|
day_of_month: dayOfMonth,
|
|
3135
1141
|
days_in_month: daysInMonth,
|
|
@@ -3150,7 +1156,7 @@ function queryModelEfficiency(db) {
|
|
|
3150
1156
|
COALESCE(SUM(cache_create_tokens),0) as cache_write,
|
|
3151
1157
|
COUNT(*) as requests,
|
|
3152
1158
|
COALESCE(SUM(cost_usd),0) as cost
|
|
3153
|
-
FROM
|
|
1159
|
+
FROM economy_effective_requests GROUP BY model ORDER BY cost DESC`).all();
|
|
3154
1160
|
}
|
|
3155
1161
|
|
|
3156
1162
|
// src/lib/cloud-storage.ts
|
|
@@ -3167,18 +1173,6 @@ import {
|
|
|
3167
1173
|
} from "@hasna/contracts/client";
|
|
3168
1174
|
import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
3169
1175
|
var ECONOMY_APP = "economy";
|
|
3170
|
-
var RETIRED_STORAGE_MODE_KEYS = [
|
|
3171
|
-
"HASNA_ECONOMY_STORAGE_MODE",
|
|
3172
|
-
"HASNA_ECONOMY_MODE",
|
|
3173
|
-
"ECONOMY_STORAGE_MODE",
|
|
3174
|
-
"ECONOMY_MODE"
|
|
3175
|
-
];
|
|
3176
|
-
function assertNoRetiredStorageMode(env) {
|
|
3177
|
-
const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
3178
|
-
if (!legacyKey)
|
|
3179
|
-
return;
|
|
3180
|
-
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client routes through the HTTP API resolved by @hasna/contracts ` + `(HASNA_ECONOMY_API_URL + HASNA_ECONOMY_API_KEY, the Keychain, or ` + `~/.hasna/economy/config/credentials), or serves the local SQLite store only when ` + `HASNA_ECONOMY_LOCAL=1 is set.`);
|
|
3181
|
-
}
|
|
3182
1176
|
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
3183
1177
|
function envFlagSet(env, key) {
|
|
3184
1178
|
const raw = env[key];
|
|
@@ -3253,7 +1247,7 @@ function resolveEconomyStorageClient(name, env, overrides = {}) {
|
|
|
3253
1247
|
};
|
|
3254
1248
|
}
|
|
3255
1249
|
function localEconomyNotice() {
|
|
3256
|
-
return `economy:
|
|
1250
|
+
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.`;
|
|
3257
1251
|
}
|
|
3258
1252
|
function credentialRequiredError(env, cause) {
|
|
3259
1253
|
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
@@ -3265,7 +1259,6 @@ function credentialRequiredError(env, cause) {
|
|
|
3265
1259
|
var cache;
|
|
3266
1260
|
var localNoticePrinted = false;
|
|
3267
1261
|
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
3268
|
-
assertNoRetiredStorageMode(env);
|
|
3269
1262
|
const clientEnv = env;
|
|
3270
1263
|
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
3271
1264
|
return { active: false, client: null };
|
|
@@ -3318,94 +1311,116 @@ async function cloudObject(storage, path, query) {
|
|
|
3318
1311
|
}
|
|
3319
1312
|
|
|
3320
1313
|
// src/lib/store/index.ts
|
|
1314
|
+
function configuredSubscription(input) {
|
|
1315
|
+
if (typeof input.account_key !== "string" || !input.account_key.trim()) {
|
|
1316
|
+
throw new TypeError("account_key must identify the provider account; accepted: nonempty account key");
|
|
1317
|
+
}
|
|
1318
|
+
const now = new Date().toISOString();
|
|
1319
|
+
const row = {
|
|
1320
|
+
...input,
|
|
1321
|
+
id: input.id ?? subscriptionId(input),
|
|
1322
|
+
monthly_fee_usd: input.monthly_fee_usd === undefined ? 0 : input.monthly_fee_usd,
|
|
1323
|
+
included_usage_usd: input.included_usage_usd === undefined ? 0 : input.included_usage_usd,
|
|
1324
|
+
fee_basis: input.monthly_fee_usd === undefined ? "unknown" : "configured",
|
|
1325
|
+
included_basis: input.included_usage_usd === undefined ? "unknown" : "configured",
|
|
1326
|
+
observed_station: input.observed_station ?? null,
|
|
1327
|
+
observed_at: now,
|
|
1328
|
+
created_at: now,
|
|
1329
|
+
updated_at: now
|
|
1330
|
+
};
|
|
1331
|
+
validateIngest({ subscriptions: [row] });
|
|
1332
|
+
return row;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
3321
1335
|
class LocalStore {
|
|
3322
1336
|
transport = "local";
|
|
3323
1337
|
_db;
|
|
3324
|
-
db() {
|
|
1338
|
+
async db() {
|
|
3325
1339
|
if (!this._db) {
|
|
1340
|
+
const { openDatabase } = await import("./chunks/sqlite-store-fq4natnf.js");
|
|
3326
1341
|
this._db = openDatabase();
|
|
3327
1342
|
ensurePricingSeeded(this._db);
|
|
3328
1343
|
}
|
|
3329
1344
|
return this._db;
|
|
3330
1345
|
}
|
|
3331
1346
|
async summary(period, machine) {
|
|
3332
|
-
return querySummary(this.db(), period, machine);
|
|
1347
|
+
return querySummary(await this.db(), period, machine);
|
|
3333
1348
|
}
|
|
3334
1349
|
async sessions(filter) {
|
|
3335
|
-
return querySessions(this.db(), filter);
|
|
1350
|
+
return querySessions(await this.db(), filter);
|
|
3336
1351
|
}
|
|
3337
1352
|
async topSessions(n, agent, since) {
|
|
3338
|
-
return queryTopSessions(this.db(), n, agent, since);
|
|
1353
|
+
return queryTopSessions(await this.db(), n, agent, since);
|
|
3339
1354
|
}
|
|
3340
1355
|
async sessionDetail(id) {
|
|
3341
|
-
return getSessionDetail(this.db(), id);
|
|
1356
|
+
return getSessionDetail(await this.db(), id);
|
|
3342
1357
|
}
|
|
3343
1358
|
async modelBreakdown(query = {}) {
|
|
3344
|
-
return query.since ? queryModelBreakdownSince(this.db(), query.since) : queryModelBreakdown(this.db());
|
|
1359
|
+
return query.since ? queryModelBreakdownSince(await this.db(), query.since) : queryModelBreakdown(await this.db());
|
|
3345
1360
|
}
|
|
3346
1361
|
async agentBreakdown(query = {}) {
|
|
3347
|
-
return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
1362
|
+
return query.since ? queryAgentBreakdownSince(await this.db(), query.since) : queryAgentBreakdown(await this.db(), query.period ?? "all", query.machine);
|
|
3348
1363
|
}
|
|
3349
1364
|
async projectBreakdown(query = {}) {
|
|
3350
|
-
return query.since ? queryProjectBreakdownSince(this.db(), query.since, query.machine) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
1365
|
+
return query.since ? queryProjectBreakdownSince(await this.db(), query.since, query.machine) : queryProjectBreakdown(await this.db(), query.period ?? "all", query.machine);
|
|
3351
1366
|
}
|
|
3352
1367
|
async accountBreakdown(query = {}) {
|
|
3353
|
-
return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
1368
|
+
return query.since ? queryAccountBreakdownSince(await this.db(), query.since) : queryAccountBreakdown(await this.db(), query.period ?? "all", query.machine);
|
|
3354
1369
|
}
|
|
3355
1370
|
async costCenterBreakdown(query = {}) {
|
|
3356
|
-
return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
|
|
1371
|
+
return queryCostCenterBreakdown(await this.db(), query.period ?? "all", {
|
|
3357
1372
|
kind: query.kind,
|
|
3358
1373
|
machine: query.machine,
|
|
3359
1374
|
since: query.since
|
|
3360
1375
|
});
|
|
3361
1376
|
}
|
|
3362
1377
|
async accounts(period) {
|
|
3363
|
-
return queryAccountBreakdown(this.db(), period);
|
|
1378
|
+
return queryAccountBreakdown(await this.db(), period);
|
|
3364
1379
|
}
|
|
3365
1380
|
async daily(days, machine) {
|
|
3366
|
-
return queryDailyBreakdown(this.db(), days, machine);
|
|
1381
|
+
return queryDailyBreakdown(await this.db(), days, machine);
|
|
3367
1382
|
}
|
|
3368
1383
|
async machines() {
|
|
3369
|
-
return listMachines(this.db());
|
|
1384
|
+
return listMachines(await this.db());
|
|
3370
1385
|
}
|
|
3371
1386
|
async fleet(period) {
|
|
3372
1387
|
return {
|
|
3373
|
-
summary: querySummary(this.db(), period, undefined, true),
|
|
3374
|
-
machines: listMachines(this.db(), period),
|
|
3375
|
-
registry: listMachineRegistry(this.db())
|
|
1388
|
+
summary: querySummary(await this.db(), period, undefined, true),
|
|
1389
|
+
machines: listMachines(await this.db(), period),
|
|
1390
|
+
registry: listMachineRegistry(await this.db())
|
|
3376
1391
|
};
|
|
3377
1392
|
}
|
|
3378
1393
|
async billingSummary(period) {
|
|
3379
|
-
return queryBillingSummary(this.db(), period);
|
|
1394
|
+
return queryBillingSummary(await this.db(), period);
|
|
3380
1395
|
}
|
|
3381
1396
|
async billingDiff(period) {
|
|
3382
|
-
return queryBillingDiff(this.db(), period);
|
|
1397
|
+
return queryBillingDiff(await this.db(), period);
|
|
3383
1398
|
}
|
|
3384
1399
|
async usage(period, agent) {
|
|
3385
|
-
const snapshots = queryUsageSnapshots(this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
3386
|
-
const summary = querySummary(this.db(), period, undefined, true, agent);
|
|
1400
|
+
const snapshots = queryUsageSnapshots(await this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
1401
|
+
const summary = querySummary(await this.db(), period, undefined, true, agent);
|
|
3387
1402
|
return { snapshots, summary };
|
|
3388
1403
|
}
|
|
3389
1404
|
async savings(period, agent) {
|
|
3390
|
-
return querySavingsSummary(this.db(), period, agent);
|
|
1405
|
+
return querySavingsSummary(await this.db(), period, agent);
|
|
3391
1406
|
}
|
|
3392
1407
|
async listBudgets() {
|
|
3393
|
-
return getBudgetStatuses(this.db());
|
|
1408
|
+
return getBudgetStatuses(await this.db());
|
|
3394
1409
|
}
|
|
3395
1410
|
async listGoals() {
|
|
3396
|
-
return getGoalStatuses(this.db());
|
|
1411
|
+
return getGoalStatuses(await this.db());
|
|
3397
1412
|
}
|
|
3398
1413
|
async listPricing() {
|
|
3399
|
-
return listModelPricing(this.db());
|
|
1414
|
+
return listModelPricing(await this.db());
|
|
3400
1415
|
}
|
|
3401
1416
|
async listSubscriptions() {
|
|
3402
|
-
return listSubscriptions(this.db());
|
|
1417
|
+
return listSubscriptions(await this.db());
|
|
3403
1418
|
}
|
|
3404
1419
|
async listProjects() {
|
|
3405
|
-
return queryProjectBreakdown(this.db());
|
|
1420
|
+
return queryProjectBreakdown(await this.db());
|
|
3406
1421
|
}
|
|
3407
1422
|
async brief(query = {}) {
|
|
3408
|
-
return buildBrief(this.db(), {
|
|
1423
|
+
return buildBrief(await this.db(), {
|
|
3409
1424
|
since: query.since,
|
|
3410
1425
|
machine: query.machine,
|
|
3411
1426
|
currentMachineId: query.currentMachineId,
|
|
@@ -3413,71 +1428,59 @@ class LocalStore {
|
|
|
3413
1428
|
});
|
|
3414
1429
|
}
|
|
3415
1430
|
async projectDetail(nameOrPath) {
|
|
3416
|
-
return queryProjectDetail(this.db(), nameOrPath);
|
|
1431
|
+
return queryProjectDetail(await this.db(), nameOrPath);
|
|
3417
1432
|
}
|
|
3418
1433
|
async exportRows(type, period) {
|
|
3419
|
-
return queryExportRows(this.db(), type, period);
|
|
1434
|
+
return queryExportRows(await this.db(), type, period);
|
|
3420
1435
|
}
|
|
3421
1436
|
async rangeStats(from, to) {
|
|
3422
|
-
return queryRangeStats(this.db(), from, to);
|
|
1437
|
+
return queryRangeStats(await this.db(), from, to);
|
|
3423
1438
|
}
|
|
3424
1439
|
async forecast() {
|
|
3425
|
-
return queryForecast(this.db());
|
|
1440
|
+
return queryForecast(await this.db());
|
|
3426
1441
|
}
|
|
3427
1442
|
async efficiency() {
|
|
3428
|
-
return queryModelEfficiency(this.db());
|
|
1443
|
+
return queryModelEfficiency(await this.db());
|
|
3429
1444
|
}
|
|
3430
1445
|
async recentRequests(since) {
|
|
3431
|
-
return queryRequestsSince(this.db(), since);
|
|
1446
|
+
return queryRequestsSince(await this.db(), since);
|
|
3432
1447
|
}
|
|
3433
1448
|
async estimate(input) {
|
|
3434
|
-
return estimateCostFromRows(listModelPricing(this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
1449
|
+
return estimateCostFromRows(listModelPricing(await this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
3435
1450
|
}
|
|
3436
1451
|
async setBudget(input) {
|
|
3437
1452
|
const now = new Date().toISOString();
|
|
3438
1453
|
const budget = { id: randomUUID(), ...input, created_at: now, updated_at: now };
|
|
3439
|
-
upsertBudget(this.db(), budget);
|
|
1454
|
+
upsertBudget(await this.db(), budget);
|
|
3440
1455
|
return budget.id;
|
|
3441
1456
|
}
|
|
3442
1457
|
async removeBudget(id) {
|
|
3443
|
-
deleteBudget(this.db(), id);
|
|
1458
|
+
deleteBudget(await this.db(), id);
|
|
3444
1459
|
}
|
|
3445
1460
|
async setGoal(input) {
|
|
3446
1461
|
const now = new Date().toISOString();
|
|
3447
|
-
upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
|
|
1462
|
+
upsertGoal(await this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
|
|
3448
1463
|
}
|
|
3449
1464
|
async removeGoal(id) {
|
|
3450
|
-
deleteGoal(this.db(), id);
|
|
1465
|
+
deleteGoal(await this.db(), id);
|
|
3451
1466
|
}
|
|
3452
1467
|
async setPricing(input) {
|
|
3453
|
-
upsertModelPricing(this.db(), { ...input, updated_at: new Date().toISOString() });
|
|
1468
|
+
upsertModelPricing(await this.db(), { ...input, updated_at: new Date().toISOString() });
|
|
3454
1469
|
}
|
|
3455
1470
|
async removePricing(model) {
|
|
3456
|
-
deleteModelPricing(this.db(), model);
|
|
1471
|
+
deleteModelPricing(await this.db(), model);
|
|
3457
1472
|
}
|
|
3458
1473
|
async setSubscription(input) {
|
|
3459
|
-
const
|
|
3460
|
-
const
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
plan: input.plan,
|
|
3464
|
-
agent: input.agent,
|
|
3465
|
-
monthly_fee_usd: input.monthly_fee_usd,
|
|
3466
|
-
included_usage_usd: input.included_usage_usd,
|
|
3467
|
-
billing_cycle_start: input.billing_cycle_start,
|
|
3468
|
-
reset_policy: input.reset_policy,
|
|
3469
|
-
active: input.active,
|
|
3470
|
-
created_at: now,
|
|
3471
|
-
updated_at: now
|
|
3472
|
-
};
|
|
3473
|
-
upsertSubscription(this.db(), row);
|
|
3474
|
-
return row;
|
|
1474
|
+
const row = configuredSubscription(input);
|
|
1475
|
+
const db = await this.db();
|
|
1476
|
+
upsertSubscription(db, row);
|
|
1477
|
+
return db.get("SELECT * FROM subscriptions WHERE id = ?", row.id);
|
|
3475
1478
|
}
|
|
3476
1479
|
async removeSubscription(id) {
|
|
3477
|
-
deleteSubscription(this.db(), id);
|
|
1480
|
+
deleteSubscription(await this.db(), id);
|
|
3478
1481
|
}
|
|
3479
1482
|
async addProject(path, name) {
|
|
3480
|
-
upsertProject(this.db(), {
|
|
1483
|
+
upsertProject(await this.db(), {
|
|
3481
1484
|
id: randomUUID(),
|
|
3482
1485
|
path,
|
|
3483
1486
|
name,
|
|
@@ -3487,16 +1490,16 @@ class LocalStore {
|
|
|
3487
1490
|
});
|
|
3488
1491
|
}
|
|
3489
1492
|
async renameProject(path, name) {
|
|
3490
|
-
const existing = getProject(this.db(), path);
|
|
1493
|
+
const existing = getProject(await this.db(), path);
|
|
3491
1494
|
if (!existing)
|
|
3492
1495
|
throw new Error("Project not found");
|
|
3493
|
-
upsertProject(this.db(), { ...existing, name });
|
|
1496
|
+
upsertProject(await this.db(), { ...existing, name });
|
|
3494
1497
|
}
|
|
3495
1498
|
async removeProject(path) {
|
|
3496
|
-
deleteProject(this.db(), path);
|
|
1499
|
+
deleteProject(await this.db(), path);
|
|
3497
1500
|
}
|
|
3498
1501
|
async sendFeedback(input) {
|
|
3499
|
-
insertFeedback(this.db(), {
|
|
1502
|
+
insertFeedback(await this.db(), {
|
|
3500
1503
|
message: input.message,
|
|
3501
1504
|
email: input.email ?? null,
|
|
3502
1505
|
category: input.category ?? "general",
|
|
@@ -3520,7 +1523,7 @@ class ApiStore {
|
|
|
3520
1523
|
this.cloud = cloud;
|
|
3521
1524
|
}
|
|
3522
1525
|
async summary(period, machine) {
|
|
3523
|
-
return cloudObject(this.cloud, "/summary", q({ period, machine }));
|
|
1526
|
+
return requireCurrentCostSummary(await cloudObject(this.cloud, "/summary", q({ period, machine })));
|
|
3524
1527
|
}
|
|
3525
1528
|
async sessions(filter) {
|
|
3526
1529
|
return cloudListItems(this.cloud, "sessions", q({ ...filter }));
|
|
@@ -3568,22 +1571,23 @@ class ApiStore {
|
|
|
3568
1571
|
async fleet(period) {
|
|
3569
1572
|
const data = await cloudObject(this.cloud, "/fleet", q({ period }));
|
|
3570
1573
|
return {
|
|
3571
|
-
summary: data.summary,
|
|
1574
|
+
summary: requireCurrentCostSummary(data.summary),
|
|
3572
1575
|
machines: data.machines ?? [],
|
|
3573
1576
|
registry: data.registry ?? []
|
|
3574
1577
|
};
|
|
3575
1578
|
}
|
|
3576
1579
|
async billingSummary(period) {
|
|
3577
|
-
return cloudObject(this.cloud, "/billing", q({ period }));
|
|
1580
|
+
return requireCurrentBillingSummary(await cloudObject(this.cloud, "/billing", q({ period })));
|
|
3578
1581
|
}
|
|
3579
1582
|
async billingDiff(period) {
|
|
3580
|
-
return cloudObject(this.cloud, "/billing/diff", q({ period }));
|
|
1583
|
+
return requireCurrentBillingDiffSummary(await cloudObject(this.cloud, "/billing/diff", q({ period })));
|
|
3581
1584
|
}
|
|
3582
1585
|
async usage(period, agent) {
|
|
3583
|
-
|
|
1586
|
+
const data = await cloudObject(this.cloud, "/usage", q({ period, agent }));
|
|
1587
|
+
return { ...data, summary: requireCurrentCostSummary(data.summary) };
|
|
3584
1588
|
}
|
|
3585
1589
|
async savings(period, agent) {
|
|
3586
|
-
return cloudObject(this.cloud, "/savings", q({ period, agent }));
|
|
1590
|
+
return requireCurrentSavingsReport(await cloudObject(this.cloud, "/savings", q({ period, agent })));
|
|
3587
1591
|
}
|
|
3588
1592
|
async listBudgets() {
|
|
3589
1593
|
return cloudListItems(this.cloud, "budgets");
|
|
@@ -3657,22 +1661,8 @@ class ApiStore {
|
|
|
3657
1661
|
await this.cloud.client.delete("pricing", model);
|
|
3658
1662
|
}
|
|
3659
1663
|
async setSubscription(input) {
|
|
3660
|
-
const
|
|
3661
|
-
|
|
3662
|
-
id: input.id ?? randomUUID(),
|
|
3663
|
-
provider: input.provider,
|
|
3664
|
-
plan: input.plan,
|
|
3665
|
-
agent: input.agent,
|
|
3666
|
-
monthly_fee_usd: input.monthly_fee_usd,
|
|
3667
|
-
included_usage_usd: input.included_usage_usd,
|
|
3668
|
-
billing_cycle_start: input.billing_cycle_start,
|
|
3669
|
-
reset_policy: input.reset_policy,
|
|
3670
|
-
active: input.active,
|
|
3671
|
-
created_at: now,
|
|
3672
|
-
updated_at: now
|
|
3673
|
-
};
|
|
3674
|
-
await this.cloud.client.create("subscriptions", row);
|
|
3675
|
-
return row;
|
|
1664
|
+
const row = configuredSubscription(input);
|
|
1665
|
+
return this.cloud.client.create("subscriptions", row);
|
|
3676
1666
|
}
|
|
3677
1667
|
async removeSubscription(id) {
|
|
3678
1668
|
await this.cloud.client.delete("subscriptions", id);
|
|
@@ -3702,9 +1692,725 @@ function getStore(env = process.env) {
|
|
|
3702
1692
|
function isCloudStore(env = process.env) {
|
|
3703
1693
|
return economyCloudStorage(env).active;
|
|
3704
1694
|
}
|
|
1695
|
+
// src/lib/ingest-journal-client.ts
|
|
1696
|
+
import { z as z2 } from "zod";
|
|
1697
|
+
|
|
1698
|
+
// src/lib/ingest-journal.ts
|
|
1699
|
+
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
1700
|
+
import { createHash } from "crypto";
|
|
1701
|
+
import { join, resolve } from "path";
|
|
1702
|
+
var MAX_COMMANDS = 1000;
|
|
1703
|
+
var MAX_BYTES = 64 * 1024 * 1024;
|
|
1704
|
+
var RECEIPT_RESERVE = 4096;
|
|
1705
|
+
var MAINTENANCE_RESERVE = 16384;
|
|
1706
|
+
var sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
1707
|
+
|
|
1708
|
+
class PendingIngestError extends Error {
|
|
1709
|
+
pending_keys;
|
|
1710
|
+
code = "INGEST_RECONCILIATION_REQUIRED";
|
|
1711
|
+
constructor(pending_keys) {
|
|
1712
|
+
super("Pending ingestion requires hosted receipt reconciliation; no command was replayed");
|
|
1713
|
+
this.pending_keys = pending_keys;
|
|
1714
|
+
this.name = "PendingIngestError";
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function definitiveIngestRefusal(status, body) {
|
|
1718
|
+
const reasons = { 400: "invalid_ingest", 409: "ingest_conflict", 413: "ingest_too_large", 422: "unprocessable_ingest" };
|
|
1719
|
+
if (!Object.hasOwn(reasons, status))
|
|
1720
|
+
return null;
|
|
1721
|
+
const legacy = body?.expected === "legacy_aggregate_requires_migration";
|
|
1722
|
+
return {
|
|
1723
|
+
status,
|
|
1724
|
+
code: legacy ? "legacy_aggregate_requires_migration" : "ingest_refused",
|
|
1725
|
+
reason: legacy ? "legacy_aggregate_requires_migration" : reasons[status]
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
function getIngestJournalPath() {
|
|
1729
|
+
return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
|
|
1730
|
+
}
|
|
1731
|
+
function targetId(cloud) {
|
|
1732
|
+
const url = new URL(cloud.client.transport.baseUrl);
|
|
1733
|
+
if (url.username || url.password || url.search || url.hash)
|
|
1734
|
+
throw new Error("Ingest authority must not contain URL credentials or capabilities");
|
|
1735
|
+
return sha(url.toString().replace(/\/+$/, ""));
|
|
1736
|
+
}
|
|
1737
|
+
function secureDirectory(path) {
|
|
1738
|
+
mkdirSync(path, { recursive: true, mode: 448 });
|
|
1739
|
+
const stat = lstatSync(path);
|
|
1740
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
1741
|
+
throw new Error("Ingest journal must be a private owner directory");
|
|
1742
|
+
}
|
|
1743
|
+
function readJson(path, max) {
|
|
1744
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1745
|
+
try {
|
|
1746
|
+
const st = fstatSync(fd);
|
|
1747
|
+
if (!st.isFile() || st.size > max || (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid())
|
|
1748
|
+
throw new Error("Unsafe ingest journal record");
|
|
1749
|
+
return JSON.parse(readFileSync(fd, "utf8"));
|
|
1750
|
+
} finally {
|
|
1751
|
+
closeSync(fd);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
function createExact(path, value) {
|
|
1755
|
+
const bytes = JSON.stringify(value);
|
|
1756
|
+
let fd;
|
|
1757
|
+
try {
|
|
1758
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
if (error.code === "EEXIST")
|
|
1761
|
+
return false;
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1764
|
+
try {
|
|
1765
|
+
writeFileSync(fd, bytes);
|
|
1766
|
+
fsyncSync(fd);
|
|
1767
|
+
} finally {
|
|
1768
|
+
closeSync(fd);
|
|
1769
|
+
}
|
|
1770
|
+
if (JSON.stringify(readJson(path, MAX_INGEST_BYTES + 4096)) !== bytes)
|
|
1771
|
+
throw new Error("Ingest journal readback failed");
|
|
1772
|
+
const directory = openSync(resolve(path, ".."), constants.O_RDONLY);
|
|
1773
|
+
try {
|
|
1774
|
+
fsyncSync(directory);
|
|
1775
|
+
} finally {
|
|
1776
|
+
closeSync(directory);
|
|
1777
|
+
}
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
function unwrap(raw) {
|
|
1781
|
+
return raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
|
|
1782
|
+
}
|
|
1783
|
+
function verifyIngestReceipt(raw, command) {
|
|
1784
|
+
const receipt = unwrap(raw);
|
|
1785
|
+
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")
|
|
1786
|
+
throw new Error("Invalid ingest receipt");
|
|
1787
|
+
let sum = 0;
|
|
1788
|
+
for (const [table, count] of Object.entries(receipt.ingested)) {
|
|
1789
|
+
if (!Array.isArray(command[table]) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0 || count > command[table].length)
|
|
1790
|
+
throw new Error("Invalid ingest receipt");
|
|
1791
|
+
sum += count;
|
|
1792
|
+
}
|
|
1793
|
+
if (sum !== receipt.total)
|
|
1794
|
+
throw new Error("Invalid ingest receipt");
|
|
1795
|
+
const projected = {
|
|
1796
|
+
ingested: Object.fromEntries(Object.entries(receipt.ingested)),
|
|
1797
|
+
total: receipt.total,
|
|
1798
|
+
operation_key: receipt.operation_key,
|
|
1799
|
+
replayed: receipt.replayed,
|
|
1800
|
+
stale: receipt.stale ?? 0,
|
|
1801
|
+
unchanged: receipt.unchanged ?? 0
|
|
1802
|
+
};
|
|
1803
|
+
const rows = Object.values(command).reduce((n, v) => n + (Array.isArray(v) ? v.length : 0), 0);
|
|
1804
|
+
if (![projected.stale, projected.unchanged].every((n) => Number.isSafeInteger(n) && n >= 0 && n <= rows) || Buffer.byteLength(JSON.stringify(projected)) > RECEIPT_RESERVE - 256)
|
|
1805
|
+
throw new Error("Invalid ingest receipt");
|
|
1806
|
+
return projected;
|
|
1807
|
+
}
|
|
1808
|
+
function sameReceipt(left, right) {
|
|
1809
|
+
const meaning = (value) => ({ ingested: value?.ingested, total: value?.total, operation_key: value?.operation_key, stale: value?.stale ?? 0, unchanged: value?.unchanged ?? 0 });
|
|
1810
|
+
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
1811
|
+
}
|
|
1812
|
+
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
1813
|
+
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
1814
|
+
const raw = unwrap(await cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs }));
|
|
1815
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
1816
|
+
throw new Error("Invalid reconciliation response");
|
|
1817
|
+
if (raw.status === "absent")
|
|
1818
|
+
return null;
|
|
1819
|
+
if (raw.status !== "committed")
|
|
1820
|
+
throw new Error("Invalid reconciliation response");
|
|
1821
|
+
return raw.receipt;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
class IngestJournal {
|
|
1825
|
+
directory;
|
|
1826
|
+
target;
|
|
1827
|
+
inventoryBytes = 0;
|
|
1828
|
+
freshlyStaged = new Set;
|
|
1829
|
+
constructor(cloud, path = getIngestJournalPath()) {
|
|
1830
|
+
this.target = targetId(cloud);
|
|
1831
|
+
secureDirectory(path);
|
|
1832
|
+
this.directory = join(realpathSync(path), this.target);
|
|
1833
|
+
secureDirectory(this.directory);
|
|
1834
|
+
}
|
|
1835
|
+
names() {
|
|
1836
|
+
const names = [];
|
|
1837
|
+
const directory = opendirSync(this.directory);
|
|
1838
|
+
try {
|
|
1839
|
+
for (let entry;entry = directory.readSync(); ) {
|
|
1840
|
+
if (entry.name === ".lease" || entry.name === ".maintenance" || entry.name === ".authority.json" || entry.name === ".recovery")
|
|
1841
|
+
continue;
|
|
1842
|
+
names.push(entry.name);
|
|
1843
|
+
if (names.length > MAX_COMMANDS * 2)
|
|
1844
|
+
throw new Error("Ingest journal capacity reached; preserve and reconcile records");
|
|
1845
|
+
}
|
|
1846
|
+
} finally {
|
|
1847
|
+
directory.closeSync();
|
|
1848
|
+
}
|
|
1849
|
+
this.inventoryBytes = 0;
|
|
1850
|
+
const set = new Set(names);
|
|
1851
|
+
for (const name of names) {
|
|
1852
|
+
if (!/^[a-f0-9]{64}\.(intent|receipt|refusal)\.json$/.test(name))
|
|
1853
|
+
throw new Error("Unexpected ingest journal entry");
|
|
1854
|
+
if (!name.endsWith(".intent.json") && !set.has(name.replace(/\.(receipt|refusal)\./, ".intent.")))
|
|
1855
|
+
throw new Error("Orphaned ingest outcome requires maintenance recovery");
|
|
1856
|
+
if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
|
|
1857
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
1858
|
+
const stat = lstatSync(join(this.directory, name));
|
|
1859
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
|
|
1860
|
+
throw new Error("Unsafe ingest journal record");
|
|
1861
|
+
if (stat.size > MAX_INGEST_BYTES + 4096)
|
|
1862
|
+
throw new Error("Oversized ingest journal record");
|
|
1863
|
+
this.inventoryBytes += stat.size;
|
|
1864
|
+
if (name.endsWith(".refusal.json"))
|
|
1865
|
+
this.refusal(this.intent(name.replace(".refusal.", ".intent.")).command);
|
|
1866
|
+
}
|
|
1867
|
+
return names;
|
|
1868
|
+
}
|
|
1869
|
+
file(key, kind) {
|
|
1870
|
+
return join(this.directory, `${sha(key)}.${kind}.json`);
|
|
1871
|
+
}
|
|
1872
|
+
intent(name) {
|
|
1873
|
+
const record = readJson(join(this.directory, name), MAX_INGEST_BYTES + 4096);
|
|
1874
|
+
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`)
|
|
1875
|
+
throw new Error("Invalid ingest journal intent");
|
|
1876
|
+
validateIngest(record.command);
|
|
1877
|
+
return record;
|
|
1878
|
+
}
|
|
1879
|
+
status() {
|
|
1880
|
+
const names = this.names(), intents = names.filter((n) => n.endsWith(".intent.json"));
|
|
1881
|
+
const set = new Set(names);
|
|
1882
|
+
const completed = intents.filter((name) => set.has(name.replace(".intent.", ".receipt."))).length;
|
|
1883
|
+
const rejected = intents.filter((name) => set.has(name.replace(".intent.", ".refusal."))).length;
|
|
1884
|
+
const bytes = this.inventoryBytes;
|
|
1885
|
+
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 };
|
|
1886
|
+
}
|
|
1887
|
+
canStage(command, status = this.status()) {
|
|
1888
|
+
const name = `${sha(command.idempotency_key)}.intent.json`;
|
|
1889
|
+
try {
|
|
1890
|
+
if (this.intent(name).body_hash !== canonicalIngestBodyHash(command))
|
|
1891
|
+
throw new Error("Ingest command key is already bound to different content");
|
|
1892
|
+
return true;
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
if (error.code !== "ENOENT")
|
|
1895
|
+
throw error;
|
|
1896
|
+
}
|
|
1897
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
|
|
1898
|
+
return status.commands < MAX_COMMANDS && status.reserved_bytes + Buffer.byteLength(JSON.stringify(record)) + RECEIPT_RESERVE <= MAX_BYTES;
|
|
1899
|
+
}
|
|
1900
|
+
receipt(command) {
|
|
1901
|
+
if (this.refusal(command))
|
|
1902
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
1903
|
+
const record = readJson(this.file(command.idempotency_key, "receipt"), RECEIPT_RESERVE);
|
|
1904
|
+
if (record.version !== 1 || record.target !== this.target || record.body_hash !== canonicalIngestBodyHash(command))
|
|
1905
|
+
throw new Error("Invalid receipt journal binding");
|
|
1906
|
+
return verifyIngestReceipt(record.receipt, command);
|
|
1907
|
+
}
|
|
1908
|
+
stage(command) {
|
|
1909
|
+
validateIngest(command);
|
|
1910
|
+
const path = this.file(command.idempotency_key, "intent");
|
|
1911
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
|
|
1912
|
+
if (!this.canStage(command))
|
|
1913
|
+
throw new Error("Ingest journal capacity reached; run economy ingest journal maintain; originals preserved");
|
|
1914
|
+
if (createExact(path, record)) {
|
|
1915
|
+
this.freshlyStaged.add(command.idempotency_key);
|
|
1916
|
+
return true;
|
|
1917
|
+
}
|
|
1918
|
+
this.freshlyStaged.delete(command.idempotency_key);
|
|
1919
|
+
const old = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
1920
|
+
if (old.body_hash !== record.body_hash)
|
|
1921
|
+
throw new Error("Ingest command key is already bound to different content");
|
|
1922
|
+
return false;
|
|
1923
|
+
}
|
|
1924
|
+
accept(command, raw) {
|
|
1925
|
+
if (this.refusal(command))
|
|
1926
|
+
throw new Error("A refused command cannot accept a receipt");
|
|
1927
|
+
this.freshlyStaged.delete(command.idempotency_key);
|
|
1928
|
+
const receipt = verifyIngestReceipt(raw, command);
|
|
1929
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
1930
|
+
if (intent.body_hash !== canonicalIngestBodyHash(command))
|
|
1931
|
+
throw new Error("Ingest receipt content conflicts with exact intent");
|
|
1932
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), receipt };
|
|
1933
|
+
const path = this.file(command.idempotency_key, "receipt");
|
|
1934
|
+
if (!this.names().includes(`${sha(command.idempotency_key)}.receipt.json`) && this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
|
|
1935
|
+
throw new Error("Receipt capacity exhausted; exact intent retained for reconciliation");
|
|
1936
|
+
if (!createExact(path, record)) {
|
|
1937
|
+
const old = readJson(path, MAX_INGEST_BYTES);
|
|
1938
|
+
if (old.body_hash !== record.body_hash || old.target !== this.target || !sameReceipt(old.receipt, receipt))
|
|
1939
|
+
throw new Error("Ingest receipt journal conflict");
|
|
1940
|
+
}
|
|
1941
|
+
return receipt;
|
|
1942
|
+
}
|
|
1943
|
+
recordRefusal(command, refusal) {
|
|
1944
|
+
if (!this.freshlyStaged.delete(command.idempotency_key))
|
|
1945
|
+
throw new Error("Refusal requires this invocation's fresh command");
|
|
1946
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
1947
|
+
if (intent.body_hash !== canonicalIngestBodyHash(command))
|
|
1948
|
+
throw new Error("Refusal conflicts with exact intent");
|
|
1949
|
+
const expected = definitiveIngestRefusal(refusal.status, refusal.code === "legacy_aggregate_requires_migration" ? { expected: refusal.code } : null);
|
|
1950
|
+
if (!expected || stableJson(expected) !== stableJson(refusal))
|
|
1951
|
+
throw new Error("Invalid definitive refusal");
|
|
1952
|
+
const names = this.names();
|
|
1953
|
+
if (names.includes(`${sha(command.idempotency_key)}.receipt.json`))
|
|
1954
|
+
throw new Error("A completed command cannot be refused");
|
|
1955
|
+
const record = { version: 1, target: this.target, body_hash: intent.body_hash, refusal };
|
|
1956
|
+
if (Buffer.byteLength(JSON.stringify(record)) > RECEIPT_RESERVE || this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
|
|
1957
|
+
throw new Error("Refusal capacity exhausted; exact intent retained");
|
|
1958
|
+
if (!createExact(this.file(command.idempotency_key, "refusal"), record))
|
|
1959
|
+
throw new Error("Existing refusal requires inspection");
|
|
1960
|
+
this.refusal(command);
|
|
1961
|
+
}
|
|
1962
|
+
refusal(command) {
|
|
1963
|
+
let record;
|
|
1964
|
+
try {
|
|
1965
|
+
record = readJson(this.file(command.idempotency_key, "refusal"), RECEIPT_RESERVE);
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
if (error.code === "ENOENT")
|
|
1968
|
+
return null;
|
|
1969
|
+
throw error;
|
|
1970
|
+
}
|
|
1971
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`), r = record?.refusal;
|
|
1972
|
+
const expected = definitiveIngestRefusal(r?.status, r?.code === "legacy_aggregate_requires_migration" ? { expected: r.code } : null);
|
|
1973
|
+
if (record?.version !== 1 || record.target !== this.target || record.body_hash !== intent.body_hash || intent.body_hash !== canonicalIngestBodyHash(command) || !expected || stableJson(expected) !== stableJson(r))
|
|
1974
|
+
throw new Error("Invalid refusal journal binding");
|
|
1975
|
+
try {
|
|
1976
|
+
lstatSync(this.file(command.idempotency_key, "receipt"));
|
|
1977
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
if (error.code !== "ENOENT")
|
|
1980
|
+
throw error;
|
|
1981
|
+
}
|
|
1982
|
+
return expected;
|
|
1983
|
+
}
|
|
1984
|
+
async reconcileCommand(cloud, command, timeoutMs = 1e4) {
|
|
1985
|
+
const receipt = await this.lookupReceipt(cloud, command, timeoutMs);
|
|
1986
|
+
return receipt ? this.accept(command, receipt) : null;
|
|
1987
|
+
}
|
|
1988
|
+
assertCloud(cloud) {
|
|
1989
|
+
if (targetId(cloud) !== this.target)
|
|
1990
|
+
throw new Error("Ingest journal authority changed");
|
|
1991
|
+
}
|
|
1992
|
+
async lookupReceipt(cloud, command, timeoutMs = 1e4) {
|
|
1993
|
+
this.assertCloud(cloud);
|
|
1994
|
+
const receipt = await lookupJournalReceipt(cloud, command.idempotency_key, canonicalIngestBodyHash(command), timeoutMs);
|
|
1995
|
+
return receipt === null ? null : verifyIngestReceipt(receipt, command);
|
|
1996
|
+
}
|
|
1997
|
+
async reconcilePending(cloud) {
|
|
1998
|
+
const names = this.names(), unresolved = [];
|
|
1999
|
+
const deadline = performance.now() + 30000;
|
|
2000
|
+
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
2001
|
+
const intent = this.intent(name);
|
|
2002
|
+
if (names.includes(`${sha(intent.command.idempotency_key)}.refusal.json`)) {
|
|
2003
|
+
this.refusal(intent.command);
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
if (names.includes(`${sha(intent.command.idempotency_key)}.receipt.json`)) {
|
|
2007
|
+
const record = readJson(this.file(intent.command.idempotency_key, "receipt"), MAX_INGEST_BYTES);
|
|
2008
|
+
if (record.target !== this.target || record.body_hash !== intent.body_hash)
|
|
2009
|
+
throw new Error("Invalid receipt journal binding");
|
|
2010
|
+
verifyIngestReceipt(record.receipt, intent.command);
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
if (performance.now() >= deadline) {
|
|
2014
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
if (!await this.reconcileCommand(cloud, intent.command, Math.max(1, Math.min(1e4, deadline - performance.now()))))
|
|
2019
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2020
|
+
} catch {
|
|
2021
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
if (unresolved.length)
|
|
2025
|
+
throw new PendingIngestError(unresolved);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/lib/ingest-journal-maintenance.ts
|
|
2030
|
+
import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
2031
|
+
import { createHash as createHash2 } from "crypto";
|
|
2032
|
+
import { join as join2 } from "path";
|
|
2033
|
+
var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
2034
|
+
var digest = /^[a-f0-9]{64}$/;
|
|
2035
|
+
var sha2 = (bytes) => createHash2("sha256").update(bytes).digest("hex");
|
|
2036
|
+
|
|
2037
|
+
class JournalMaintenanceError extends Error {
|
|
2038
|
+
reason;
|
|
2039
|
+
target;
|
|
2040
|
+
code = "INGEST_JOURNAL_MAINTENANCE_REQUIRED";
|
|
2041
|
+
constructor(reason, target) {
|
|
2042
|
+
super(`Ingest journal maintenance required (${reason}); run economy ingest journal maintain; originals or Trash recovery state preserved`);
|
|
2043
|
+
this.reason = reason;
|
|
2044
|
+
this.target = target;
|
|
2045
|
+
this.name = "JournalMaintenanceError";
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
function exists(path) {
|
|
2049
|
+
try {
|
|
2050
|
+
lstatSync2(path);
|
|
2051
|
+
return true;
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
if (error.code === "ENOENT")
|
|
2054
|
+
return false;
|
|
2055
|
+
throw error;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
function snapshot(path) {
|
|
2059
|
+
const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
|
|
2060
|
+
try {
|
|
2061
|
+
const stat = fstatSync2(fd);
|
|
2062
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 4 * 1024 * 1024 + 4096 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
2063
|
+
throw new Error("Unsafe maintenance source");
|
|
2064
|
+
const bytes = readFileSync2(fd), after = fstatSync2(fd);
|
|
2065
|
+
if (after.ctimeMs !== stat.ctimeMs || bytes.length !== stat.size)
|
|
2066
|
+
throw new Error("Maintenance source changed");
|
|
2067
|
+
return { sha256: sha2(bytes), sizeBytes: stat.size, mode: stat.mode & 511, device: String(stat.dev), inode: String(stat.ino), ctime: stat.ctimeMs };
|
|
2068
|
+
} finally {
|
|
2069
|
+
closeSync2(fd);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function sameSource(path, expected) {
|
|
2073
|
+
const { capture_sha256: _capture, ...original } = expected;
|
|
2074
|
+
if (JSON.stringify(snapshot(path)) !== JSON.stringify(original))
|
|
2075
|
+
throw new Error("Maintenance source changed; preserve for inspection");
|
|
2076
|
+
}
|
|
2077
|
+
function validAnchor(value, target) {
|
|
2078
|
+
const a = value;
|
|
2079
|
+
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))
|
|
2080
|
+
throw new Error("Invalid journal authority anchor");
|
|
2081
|
+
return a;
|
|
2082
|
+
}
|
|
2083
|
+
async function confirm(cloud, anchor, deadline) {
|
|
2084
|
+
if (Date.now() >= deadline)
|
|
2085
|
+
throw new Error("Maintenance deadline reached");
|
|
2086
|
+
const current = await lookupJournalReceipt(cloud, anchor.key, anchor.body_hash, Math.max(1, Math.min(1e4, deadline - Date.now())));
|
|
2087
|
+
if (current === null || !sameReceipt(current, anchor.receipt))
|
|
2088
|
+
throw new Error("Exact Economy receipt is unavailable or changed");
|
|
2089
|
+
}
|
|
2090
|
+
async function verifyJournalAuthority(journal, cloud) {
|
|
2091
|
+
journal.assertCloud(cloud);
|
|
2092
|
+
const path = join2(journal.directory, ".authority.json");
|
|
2093
|
+
if (!exists(path)) {
|
|
2094
|
+
const names = journal.names();
|
|
2095
|
+
const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
|
|
2096
|
+
if (!name)
|
|
2097
|
+
return;
|
|
2098
|
+
const intent = journal.intent(name), receipt = journal.receipt(intent.command);
|
|
2099
|
+
createExact(path, { version: 1, target: journal.target, key: intent.command.idempotency_key, body_hash: intent.body_hash, receipt });
|
|
2100
|
+
}
|
|
2101
|
+
await confirm(cloud, validAnchor(readJson(path, 4096), journal.target), Date.now() + 1e4);
|
|
2102
|
+
}
|
|
2103
|
+
function recoveryFactory(cloud, env = process.env) {
|
|
2104
|
+
return async (operationRoot, checkpoint, deadline) => {
|
|
2105
|
+
if (env.HASNA_ECONOMY_INGEST_RECOVERY !== "trash")
|
|
2106
|
+
throw new Error("recovery_not_configured");
|
|
2107
|
+
const economy = new URL(cloud.client.transport.baseUrl);
|
|
2108
|
+
const fleet = economy.origin === "https://api.hasna.com" && /^\/economy(?:\/v1)?\/?$/.test(economy.pathname);
|
|
2109
|
+
if (!fleet && !env.HASNA_TRASH_API_URL)
|
|
2110
|
+
throw new Error("self_host_trash_authority_required");
|
|
2111
|
+
const sdk = await import("@hasna/trash/sdk");
|
|
2112
|
+
if (sdk.VERSION !== "0.2.3")
|
|
2113
|
+
throw new Error("trash_version_mismatch");
|
|
2114
|
+
const boundedFetch = (input, init) => {
|
|
2115
|
+
const remaining = deadline - Date.now();
|
|
2116
|
+
if (remaining <= 0)
|
|
2117
|
+
throw new Error("maintenance_deadline");
|
|
2118
|
+
const signal = AbortSignal.timeout(remaining);
|
|
2119
|
+
return fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal, redirect: "error" });
|
|
2120
|
+
};
|
|
2121
|
+
return Object.assign(sdk.createTrash({ env, operationRoot, checkpoint, fetchImpl: boundedFetch, transferFetch: boundedFetch, lockTimeoutMs: 100 }), { snapshot: sdk.snapshotIdentity });
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
class IngestJournalMaintenance {
|
|
2126
|
+
journal;
|
|
2127
|
+
cloud;
|
|
2128
|
+
factory;
|
|
2129
|
+
control;
|
|
2130
|
+
operationRoot;
|
|
2131
|
+
constructor(journal, cloud, factory = recoveryFactory(cloud)) {
|
|
2132
|
+
this.journal = journal;
|
|
2133
|
+
this.cloud = cloud;
|
|
2134
|
+
this.factory = factory;
|
|
2135
|
+
journal.assertCloud(cloud);
|
|
2136
|
+
this.control = join2(journal.directory, ".maintenance");
|
|
2137
|
+
this.operationRoot = join2(journal.directory, ".recovery");
|
|
2138
|
+
}
|
|
2139
|
+
manifest() {
|
|
2140
|
+
secureDirectory(this.control);
|
|
2141
|
+
const names = readdirSync(this.control);
|
|
2142
|
+
if (names.some((n) => !["manifest.json", "intent.operation.json", "receipt.operation.json"].includes(n)))
|
|
2143
|
+
throw new Error("Unexpected maintenance entry");
|
|
2144
|
+
let size = 0;
|
|
2145
|
+
for (const name of names)
|
|
2146
|
+
size += lstatSync2(join2(this.control, name)).size;
|
|
2147
|
+
if (size > MAINTENANCE_RESERVE)
|
|
2148
|
+
throw new Error("Maintenance metadata exceeds reserve");
|
|
2149
|
+
const m = validAnchor(readJson(join2(this.control, "manifest.json"), 8192), this.journal.target);
|
|
2150
|
+
for (const part of [m.intent, m.receipt_file])
|
|
2151
|
+
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))
|
|
2152
|
+
throw new Error("Invalid maintenance snapshot");
|
|
2153
|
+
return m;
|
|
2154
|
+
}
|
|
2155
|
+
pending(backend) {
|
|
2156
|
+
const pending = backend.pending(4);
|
|
2157
|
+
if (pending.length > 3 || pending.some((p) => !uuid.test(p.id) || p.phase === "unreadable"))
|
|
2158
|
+
throw new Error("Unexpected Trash recovery state");
|
|
2159
|
+
return pending;
|
|
2160
|
+
}
|
|
2161
|
+
async backend(deadline) {
|
|
2162
|
+
const backend = await this.factory(this.operationRoot, async (point, operation) => {
|
|
2163
|
+
if (operation.path === this.control)
|
|
2164
|
+
return;
|
|
2165
|
+
const m = this.manifest();
|
|
2166
|
+
const kind = operation.path === this.journal.file(m.key, "receipt") ? "receipt" : operation.path === this.journal.file(m.key, "intent") ? "intent" : null;
|
|
2167
|
+
if (!kind || operation.kind !== "capture" || !uuid.test(operation.id) || operation.id !== operation.entryId)
|
|
2168
|
+
throw new Error("Unexpected Trash operation");
|
|
2169
|
+
const expected = kind === "receipt" ? m.receipt_file : m.intent;
|
|
2170
|
+
if (point === "afterSnapshot") {
|
|
2171
|
+
if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
|
|
2172
|
+
throw new Error("Trash snapshot mismatch");
|
|
2173
|
+
sameSource(operation.path, expected);
|
|
2174
|
+
const path = join2(this.control, `${kind}.operation.json`);
|
|
2175
|
+
createExact(path, { id: operation.id });
|
|
2176
|
+
if (readJson(path, 1024).id !== operation.id)
|
|
2177
|
+
throw new Error("Conflicting Trash operation");
|
|
2178
|
+
}
|
|
2179
|
+
if (point === "beforeStage") {
|
|
2180
|
+
await confirm(this.cloud, m, deadline);
|
|
2181
|
+
sameSource(operation.path, expected);
|
|
2182
|
+
}
|
|
2183
|
+
}, deadline);
|
|
2184
|
+
const status = await backend.doctor();
|
|
2185
|
+
if (!status.station || !uuid.test(status.station.id) || !Number.isInteger(status.retentionDays) || status.retentionDays < 1)
|
|
2186
|
+
throw new Error("Trash station authority unavailable");
|
|
2187
|
+
return backend;
|
|
2188
|
+
}
|
|
2189
|
+
async entry(backend, id, path, expected) {
|
|
2190
|
+
const e = await backend.api.get(id);
|
|
2191
|
+
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")
|
|
2192
|
+
throw new Error("Trash preservation readback refused");
|
|
2193
|
+
if (exists(path))
|
|
2194
|
+
throw new Error("Retired source reappeared; preserve for inspection");
|
|
2195
|
+
}
|
|
2196
|
+
async pairFile(backend, m, kind, deadline) {
|
|
2197
|
+
await confirm(this.cloud, m, deadline);
|
|
2198
|
+
const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
|
|
2199
|
+
const saved = join2(this.control, `${kind}.operation.json`);
|
|
2200
|
+
let id;
|
|
2201
|
+
if (exists(saved))
|
|
2202
|
+
id = readJson(saved, 1024).id;
|
|
2203
|
+
const pending = this.pending(backend);
|
|
2204
|
+
const matches = pending.filter((p) => p.path === path);
|
|
2205
|
+
if (matches.length > 1 || id && !uuid.test(id))
|
|
2206
|
+
throw new Error("Conflicting Trash operation");
|
|
2207
|
+
if (matches[0]) {
|
|
2208
|
+
if (id && id !== matches[0].id)
|
|
2209
|
+
throw new Error("Conflicting Trash operation");
|
|
2210
|
+
id = matches[0].id;
|
|
2211
|
+
const operation = backend.journal.read(id);
|
|
2212
|
+
if (operation.kind !== "capture" || operation.path !== path || operation.entryId !== id)
|
|
2213
|
+
throw new Error("Invalid Trash operation binding");
|
|
2214
|
+
if (exists(path))
|
|
2215
|
+
sameSource(path, expected);
|
|
2216
|
+
await backend.recover(id, { takeAbandonedLock: true });
|
|
2217
|
+
} else if (!id) {
|
|
2218
|
+
sameSource(path, expected);
|
|
2219
|
+
const status = await backend.doctor();
|
|
2220
|
+
const e = await backend.put(path, { retentionDays: status.retentionDays });
|
|
2221
|
+
id = e.id;
|
|
2222
|
+
if (!exists(saved) || readJson(saved, 1024).id !== id)
|
|
2223
|
+
throw new Error("Trash did not persist its capture binding");
|
|
2224
|
+
}
|
|
2225
|
+
await this.entry(backend, id, path, expected);
|
|
2226
|
+
}
|
|
2227
|
+
async recover() {
|
|
2228
|
+
if (!exists(this.control) && !exists(this.operationRoot))
|
|
2229
|
+
return;
|
|
2230
|
+
if (exists(this.operationRoot))
|
|
2231
|
+
secureDirectory(this.operationRoot);
|
|
2232
|
+
if (!exists(this.control) && readdirSync(this.operationRoot).every((n) => n === ".locks"))
|
|
2233
|
+
return;
|
|
2234
|
+
const deadline = Date.now() + 30000;
|
|
2235
|
+
try {
|
|
2236
|
+
await this.finish(await this.backend(deadline), deadline);
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
if (error instanceof JournalMaintenanceError)
|
|
2239
|
+
throw error;
|
|
2240
|
+
throw new JournalMaintenanceError("recovery_unresolved", this.journal.target);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
async finish(backend, deadline) {
|
|
2244
|
+
const pending = this.pending(backend);
|
|
2245
|
+
const control = pending.filter((p) => p.path === this.control);
|
|
2246
|
+
if (control.length) {
|
|
2247
|
+
if (control.length !== 1 || pending.length !== 1)
|
|
2248
|
+
throw new Error("Conflicting maintenance cleanup");
|
|
2249
|
+
const op = backend.journal.read(control[0].id);
|
|
2250
|
+
if (op.kind !== "capture" || op.path !== this.control)
|
|
2251
|
+
throw new Error("Invalid maintenance cleanup");
|
|
2252
|
+
await backend.recover(op.id, { takeAbandonedLock: true });
|
|
2253
|
+
if (exists(this.control) || this.pending(backend).length)
|
|
2254
|
+
throw new Error("Maintenance cleanup unresolved");
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
if (!exists(this.control)) {
|
|
2258
|
+
if (pending.length)
|
|
2259
|
+
throw new Error("Orphaned maintenance operation");
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
if (readdirSync(this.control).length === 0) {
|
|
2263
|
+
if (pending.length)
|
|
2264
|
+
throw new Error("Orphaned maintenance operation");
|
|
2265
|
+
const status2 = await backend.doctor();
|
|
2266
|
+
await backend.put(this.control, { retentionDays: status2.retentionDays });
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
const m = this.manifest();
|
|
2270
|
+
if (pending.some((p) => p.path !== this.journal.file(m.key, "receipt") && p.path !== this.journal.file(m.key, "intent")))
|
|
2271
|
+
throw new Error("Unexpected recovery path");
|
|
2272
|
+
await this.pairFile(backend, m, "receipt", deadline);
|
|
2273
|
+
await this.pairFile(backend, m, "intent", deadline);
|
|
2274
|
+
if (this.pending(backend).length)
|
|
2275
|
+
throw new Error("Unfinished pair capture");
|
|
2276
|
+
await confirm(this.cloud, m, deadline);
|
|
2277
|
+
const status = await backend.doctor();
|
|
2278
|
+
await backend.put(this.control, { retentionDays: status.retentionDays });
|
|
2279
|
+
if (exists(this.control) || this.pending(backend).length)
|
|
2280
|
+
throw new Error("Maintenance cleanup unresolved");
|
|
2281
|
+
}
|
|
2282
|
+
async maintain(limit = 32) {
|
|
2283
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 32)
|
|
2284
|
+
throw new Error("Journal maintenance limit must be 1\u201332");
|
|
2285
|
+
await this.recover();
|
|
2286
|
+
await verifyJournalAuthority(this.journal, this.cloud);
|
|
2287
|
+
const deadline = Date.now() + 30000;
|
|
2288
|
+
let retired = 0, bytes = 0;
|
|
2289
|
+
let backend;
|
|
2290
|
+
try {
|
|
2291
|
+
const names = this.journal.names();
|
|
2292
|
+
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
2293
|
+
if (retired >= limit || Date.now() >= deadline || retired > 0 && bytes >= 1048576)
|
|
2294
|
+
break;
|
|
2295
|
+
if (!names.includes(name.replace(".intent.", ".receipt.")))
|
|
2296
|
+
continue;
|
|
2297
|
+
const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
|
|
2298
|
+
backend ??= await this.backend(deadline);
|
|
2299
|
+
const m = {
|
|
2300
|
+
version: 1,
|
|
2301
|
+
target: this.journal.target,
|
|
2302
|
+
key: intent.command.idempotency_key,
|
|
2303
|
+
body_hash: intent.body_hash,
|
|
2304
|
+
receipt,
|
|
2305
|
+
intent: snapshot(join2(this.journal.directory, name)),
|
|
2306
|
+
receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
|
|
2307
|
+
};
|
|
2308
|
+
await confirm(this.cloud, m, deadline);
|
|
2309
|
+
m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
|
|
2310
|
+
m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
|
|
2311
|
+
secureDirectory(this.control);
|
|
2312
|
+
if (!createExact(join2(this.control, "manifest.json"), m))
|
|
2313
|
+
throw new Error("Existing maintenance transaction");
|
|
2314
|
+
await this.finish(backend, deadline);
|
|
2315
|
+
retired++;
|
|
2316
|
+
bytes += m.intent.sizeBytes + m.receipt_file.sizeBytes;
|
|
2317
|
+
}
|
|
2318
|
+
return { retired, remaining: this.journal.status().completed };
|
|
2319
|
+
} catch (error) {
|
|
2320
|
+
if (error instanceof JournalMaintenanceError)
|
|
2321
|
+
throw error;
|
|
2322
|
+
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";
|
|
2323
|
+
throw new JournalMaintenanceError(reason, this.journal.target);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
async admit(command) {
|
|
2327
|
+
const status = this.journal.status();
|
|
2328
|
+
const fits = this.journal.canStage(command, status);
|
|
2329
|
+
if (status.commands < 64 && status.reserved_bytes < 50331648 && fits)
|
|
2330
|
+
return;
|
|
2331
|
+
try {
|
|
2332
|
+
await this.maintain();
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
if (exists(this.control) || !this.journal.canStage(command))
|
|
2335
|
+
throw error;
|
|
2336
|
+
}
|
|
2337
|
+
if (!this.journal.canStage(command))
|
|
2338
|
+
throw new JournalMaintenanceError("capacity", this.journal.target);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/lib/ingest-journal-operation.ts
|
|
2343
|
+
import { existsSync, readdirSync as readdirSync2 } from "fs";
|
|
2344
|
+
import { z } from "zod";
|
|
2345
|
+
|
|
2346
|
+
// src/lib/ingest-journal-lock.ts
|
|
2347
|
+
import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
|
|
2348
|
+
import { join as join3 } from "path";
|
|
2349
|
+
async function withIngestJournalLock(directory, action) {
|
|
2350
|
+
if (process.platform !== "linux" && process.platform !== "darwin")
|
|
2351
|
+
throw new Error("Ingest journal locking requires Linux or macOS");
|
|
2352
|
+
const { dlopen, FFIType } = await import("bun:ffi");
|
|
2353
|
+
const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
|
|
2354
|
+
flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
|
|
2355
|
+
});
|
|
2356
|
+
const path = join3(directory, ".lease");
|
|
2357
|
+
let fd;
|
|
2358
|
+
try {
|
|
2359
|
+
fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
|
|
2360
|
+
const stat = fstatSync3(fd), current = lstatSync3(path);
|
|
2361
|
+
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())
|
|
2362
|
+
throw new Error("Unsafe ingest journal lease");
|
|
2363
|
+
if (library.symbols.flock(fd, 6) !== 0) {
|
|
2364
|
+
const error = new Error("Ingest journal is busy; retry after the current operation finishes");
|
|
2365
|
+
Object.assign(error, { code: "INGEST_JOURNAL_BUSY" });
|
|
2366
|
+
throw error;
|
|
2367
|
+
}
|
|
2368
|
+
return await action();
|
|
2369
|
+
} finally {
|
|
2370
|
+
if (fd !== undefined)
|
|
2371
|
+
closeSync3(fd);
|
|
2372
|
+
library.close();
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// src/lib/ingest-journal-operation.ts
|
|
2377
|
+
var ingestJournalOperationSchema = z.discriminatedUnion("action", [
|
|
2378
|
+
z.object({ action: z.literal("status") }).strict(),
|
|
2379
|
+
z.object({ action: z.literal("reconcile") }).strict(),
|
|
2380
|
+
z.object({ action: z.literal("maintain"), confirm: z.literal(true), limit: z.number().int().min(1).max(32).default(32) }).strict()
|
|
2381
|
+
]);
|
|
2382
|
+
async function runIngestJournalOperation(cloud, input) {
|
|
2383
|
+
const options = ingestJournalOperationSchema.parse(input);
|
|
2384
|
+
const journal = new IngestJournal(cloud);
|
|
2385
|
+
return withIngestJournalLock(journal.directory, async () => {
|
|
2386
|
+
const maintenance = new IngestJournalMaintenance(journal, cloud);
|
|
2387
|
+
const unfinished = existsSync(maintenance.control) || existsSync(maintenance.operationRoot) && readdirSync2(maintenance.operationRoot).some((n) => n !== ".locks");
|
|
2388
|
+
if (options.action === "status" && unfinished)
|
|
2389
|
+
throw new JournalMaintenanceError("recovery_pending", journal.target);
|
|
2390
|
+
if (options.action !== "status") {
|
|
2391
|
+
await maintenance.recover();
|
|
2392
|
+
if (options.action === "reconcile")
|
|
2393
|
+
await journal.reconcilePending(cloud);
|
|
2394
|
+
await verifyJournalAuthority(journal, cloud);
|
|
2395
|
+
}
|
|
2396
|
+
const sweep = options.action === "maintain" ? await maintenance.maintain(options.limit) : { retired: 0 };
|
|
2397
|
+
return { schema: "economy.ingest-journal.v1", action: options.action, ...journal.status(), ...sweep };
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// src/lib/ingest-journal-client.ts
|
|
2402
|
+
async function ingestJournal(input) {
|
|
2403
|
+
const operation = ingestJournalOperationSchema.parse(input);
|
|
2404
|
+
const cloud = economyCloudStorage();
|
|
2405
|
+
if (!cloud.active)
|
|
2406
|
+
throw Object.assign(new Error("Journal operations require the hosted Economy authority"), { code: "INGEST_HOSTED_AUTHORITY_REQUIRED" });
|
|
2407
|
+
return runIngestJournalOperation(cloud, operation);
|
|
2408
|
+
}
|
|
3705
2409
|
export {
|
|
3706
2410
|
isCloudStore,
|
|
3707
2411
|
isAgent,
|
|
2412
|
+
ingestJournalOperationSchema,
|
|
2413
|
+
ingestJournal,
|
|
3708
2414
|
getStore,
|
|
3709
2415
|
LocalStore,
|
|
3710
2416
|
COST_BASIS,
|