@hasna/economy 0.3.9 → 0.3.10
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 +12 -0
- package/dist/cli/brains.d.ts +3 -0
- package/dist/cli/brains.d.ts.map +1 -0
- package/dist/cli/commands/brief.d.ts +11 -0
- package/dist/cli/commands/brief.d.ts.map +1 -0
- package/dist/cli/commands/completion.d.ts +2 -0
- package/dist/cli/commands/completion.d.ts.map +1 -0
- package/dist/cli/commands/extras.d.ts +4 -0
- package/dist/cli/commands/extras.d.ts.map +1 -0
- package/dist/cli/commands/menubar.d.ts +7 -0
- package/dist/cli/commands/menubar.d.ts.map +1 -0
- package/dist/cli/commands/notification.d.ts +8 -0
- package/dist/cli/commands/notification.d.ts.map +1 -0
- package/dist/cli/commands/todos.d.ts +29 -0
- package/dist/cli/commands/todos.d.ts.map +1 -0
- package/dist/cli/commands/tui.d.ts +26 -0
- package/dist/cli/commands/tui.d.ts.map +1 -0
- package/dist/cli/commands/watch.d.ts +10 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +12493 -0
- package/dist/db/cloud.d.ts +47 -0
- package/dist/db/cloud.d.ts.map +1 -0
- package/dist/db/database.d.ts +157 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/dialect.d.ts +13 -0
- package/dist/db/dialect.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +30 -0
- package/dist/db/pg-migrate.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts +7 -0
- package/dist/db/pg-migrations.d.ts.map +1 -0
- package/dist/db/pg-sync-worker.d.ts +2 -0
- package/dist/db/pg-sync-worker.d.ts.map +1 -0
- package/dist/db/sqlite-adapter.d.ts +36 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +43 -0
- package/dist/db/sync-pg.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3833 -0
- package/dist/ingest/billing.d.ts +27 -0
- package/dist/ingest/billing.d.ts.map +1 -0
- package/dist/ingest/claude-quota.d.ts +5 -0
- package/dist/ingest/claude-quota.d.ts.map +1 -0
- package/dist/ingest/claude.d.ts +18 -0
- package/dist/ingest/claude.d.ts.map +1 -0
- package/dist/ingest/codex-quota.d.ts +5 -0
- package/dist/ingest/codex-quota.d.ts.map +1 -0
- package/dist/ingest/codex.d.ts +8 -0
- package/dist/ingest/codex.d.ts.map +1 -0
- package/dist/ingest/cursor.d.ts +6 -0
- package/dist/ingest/cursor.d.ts.map +1 -0
- package/dist/ingest/gemini.d.ts +6 -0
- package/dist/ingest/gemini.d.ts.map +1 -0
- package/dist/ingest/hermes.d.ts +6 -0
- package/dist/ingest/hermes.d.ts.map +1 -0
- package/dist/ingest/loops.d.ts +6 -0
- package/dist/ingest/loops.d.ts.map +1 -0
- package/dist/ingest/opencode.d.ts +7 -0
- package/dist/ingest/opencode.d.ts.map +1 -0
- package/dist/ingest/otel.d.ts +33 -0
- package/dist/ingest/otel.d.ts.map +1 -0
- package/dist/ingest/pi.d.ts +7 -0
- package/dist/ingest/pi.d.ts.map +1 -0
- package/dist/ingest/plugin.d.ts +17 -0
- package/dist/ingest/plugin.d.ts.map +1 -0
- package/dist/lib/accounts.d.ts +11 -0
- package/dist/lib/accounts.d.ts.map +1 -0
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.d.ts.map +1 -0
- package/dist/lib/analytics.d.ts +68 -0
- package/dist/lib/analytics.d.ts.map +1 -0
- package/dist/lib/billing-diff.d.ts +61 -0
- package/dist/lib/billing-diff.d.ts.map +1 -0
- package/dist/lib/brief.d.ts +72 -0
- package/dist/lib/brief.d.ts.map +1 -0
- package/dist/lib/cloud-storage.d.ts +48 -0
- package/dist/lib/cloud-storage.d.ts.map +1 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/contracts-client/index.d.ts +3 -0
- package/dist/lib/contracts-client/index.d.ts.map +1 -0
- package/dist/lib/contracts-client/mode.d.ts +20 -0
- package/dist/lib/contracts-client/mode.d.ts.map +1 -0
- package/dist/lib/contracts-client/storage.d.ts +83 -0
- package/dist/lib/contracts-client/storage.d.ts.map +1 -0
- package/dist/lib/contracts-client/transport.d.ts +148 -0
- package/dist/lib/contracts-client/transport.d.ts.map +1 -0
- package/dist/lib/gatherer.d.ts +21 -0
- package/dist/lib/gatherer.d.ts.map +1 -0
- package/dist/lib/model-config.d.ts +8 -0
- package/dist/lib/model-config.d.ts.map +1 -0
- package/dist/lib/open-projects.d.ts +27 -0
- package/dist/lib/open-projects.d.ts.map +1 -0
- package/dist/lib/package-metadata.d.ts +8 -0
- package/dist/lib/package-metadata.d.ts.map +1 -0
- package/dist/lib/paths.d.ts +20 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/periods.d.ts +6 -0
- package/dist/lib/periods.d.ts.map +1 -0
- package/dist/lib/pricing.d.ts +13 -0
- package/dist/lib/pricing.d.ts.map +1 -0
- package/dist/lib/savings.d.ts +17 -0
- package/dist/lib/savings.d.ts.map +1 -0
- package/dist/lib/serve-auth.d.ts +4 -0
- package/dist/lib/serve-auth.d.ts.map +1 -0
- package/dist/lib/spikes.d.ts +18 -0
- package/dist/lib/spikes.d.ts.map +1 -0
- package/dist/lib/store/index.d.ts +303 -0
- package/dist/lib/store/index.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +28 -0
- package/dist/lib/sync-all.d.ts.map +1 -0
- package/dist/lib/watch-paths.d.ts +3 -0
- package/dist/lib/watch-paths.d.ts.map +1 -0
- package/dist/lib/webhooks.d.ts +3 -0
- package/dist/lib/webhooks.d.ts.map +1 -0
- package/dist/mcp/http.d.ts +14 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +6274 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/otel/index.d.ts +3 -0
- package/dist/otel/index.d.ts.map +1 -0
- package/dist/otel/index.js +1464 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8327 -0
- package/dist/server/pg-sync-worker.js +31 -0
- package/dist/server/serve.d.ts +42 -0
- package/dist/server/serve.d.ts.map +1 -0
- package/dist/types/index.d.ts +238 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1464 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
|
|
19
|
+
// src/db/sqlite-adapter.ts
|
|
20
|
+
import { Database } from "bun:sqlite";
|
|
21
|
+
|
|
22
|
+
class SqliteAdapter {
|
|
23
|
+
db;
|
|
24
|
+
constructor(path) {
|
|
25
|
+
this.db = new Database(path, { create: true });
|
|
26
|
+
this.db.exec("PRAGMA journal_mode=WAL");
|
|
27
|
+
this.db.exec("PRAGMA foreign_keys=ON");
|
|
28
|
+
}
|
|
29
|
+
run(sql, ...params) {
|
|
30
|
+
const result = this.db.prepare(sql).run(...params);
|
|
31
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
32
|
+
}
|
|
33
|
+
get(sql, ...params) {
|
|
34
|
+
return this.db.prepare(sql).get(...params);
|
|
35
|
+
}
|
|
36
|
+
all(sql, ...params) {
|
|
37
|
+
return this.db.prepare(sql).all(...params);
|
|
38
|
+
}
|
|
39
|
+
exec(sql) {
|
|
40
|
+
this.db.exec(sql);
|
|
41
|
+
}
|
|
42
|
+
query(sql) {
|
|
43
|
+
return this.db.query(sql);
|
|
44
|
+
}
|
|
45
|
+
prepare(sql) {
|
|
46
|
+
const stmt = this.db.prepare(sql);
|
|
47
|
+
return {
|
|
48
|
+
run(...params) {
|
|
49
|
+
const r = stmt.run(...params);
|
|
50
|
+
return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
|
|
51
|
+
},
|
|
52
|
+
get(...params) {
|
|
53
|
+
return stmt.get(...params);
|
|
54
|
+
},
|
|
55
|
+
all(...params) {
|
|
56
|
+
return stmt.all(...params);
|
|
57
|
+
},
|
|
58
|
+
finalize() {
|
|
59
|
+
stmt.finalize();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
close() {
|
|
64
|
+
this.db.close();
|
|
65
|
+
}
|
|
66
|
+
transaction(fn) {
|
|
67
|
+
return this.db.transaction(fn)();
|
|
68
|
+
}
|
|
69
|
+
get raw() {
|
|
70
|
+
return this.db;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
var init_sqlite_adapter = () => {};
|
|
74
|
+
|
|
75
|
+
// src/lib/pricing.ts
|
|
76
|
+
var exports_pricing = {};
|
|
77
|
+
__export(exports_pricing, {
|
|
78
|
+
normalizeModelName: () => normalizeModelName,
|
|
79
|
+
getPricingFromRows: () => getPricingFromRows,
|
|
80
|
+
getPricingFromDb: () => getPricingFromDb,
|
|
81
|
+
getPricing: () => getPricing,
|
|
82
|
+
estimateCostFromRows: () => estimateCostFromRows,
|
|
83
|
+
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
84
|
+
computeCostFromDb: () => computeCostFromDb,
|
|
85
|
+
computeCost: () => computeCost,
|
|
86
|
+
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
87
|
+
});
|
|
88
|
+
function normalizeModelName(raw) {
|
|
89
|
+
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
90
|
+
}
|
|
91
|
+
function normalizeModelNamePreservingProvider(raw) {
|
|
92
|
+
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
93
|
+
}
|
|
94
|
+
function modelLookupKeys(raw) {
|
|
95
|
+
const withProvider = normalizeModelNamePreservingProvider(raw);
|
|
96
|
+
const withoutProvider = normalizeModelName(raw);
|
|
97
|
+
return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
|
|
98
|
+
}
|
|
99
|
+
function bestPrefixMatch(normalized, entries) {
|
|
100
|
+
let best = null;
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const [key] = entry;
|
|
103
|
+
if (normalized !== key && !normalized.startsWith(`${key}-`))
|
|
104
|
+
continue;
|
|
105
|
+
if (!best || key.length > best[0].length)
|
|
106
|
+
best = entry;
|
|
107
|
+
}
|
|
108
|
+
return best?.[1] ?? null;
|
|
109
|
+
}
|
|
110
|
+
function bestModelMatch(model, entries) {
|
|
111
|
+
for (const key of modelLookupKeys(model)) {
|
|
112
|
+
const match = bestPrefixMatch(key, entries);
|
|
113
|
+
if (match)
|
|
114
|
+
return match;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function exactModelMatch(model, entries) {
|
|
119
|
+
for (const key of modelLookupKeys(model)) {
|
|
120
|
+
const match = entries.find(([entryKey]) => entryKey === key);
|
|
121
|
+
if (match)
|
|
122
|
+
return match[1];
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function ensurePricingSeeded(db) {
|
|
127
|
+
seedModelPricing(db, DEFAULT_PRICING);
|
|
128
|
+
repairLegacySeededPricing(db);
|
|
129
|
+
repairMissingDefaultCacheWrite1h(db);
|
|
130
|
+
repairMissingDefaultCacheStorage(db);
|
|
131
|
+
removeDeprecatedDefaultPricing(db);
|
|
132
|
+
}
|
|
133
|
+
function repairLegacySeededPricing(db) {
|
|
134
|
+
const now = new Date().toISOString();
|
|
135
|
+
const legacyModels = new Set([
|
|
136
|
+
...Object.keys(LEGACY_DEFAULT_PRICING),
|
|
137
|
+
...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
|
|
138
|
+
]);
|
|
139
|
+
for (const model of legacyModels) {
|
|
140
|
+
const current = getModelPricing(db, model);
|
|
141
|
+
const next = DEFAULT_PRICING[model];
|
|
142
|
+
if (!current || !next)
|
|
143
|
+
continue;
|
|
144
|
+
const legacy = LEGACY_DEFAULT_PRICING[model];
|
|
145
|
+
const legacyRows = [
|
|
146
|
+
...legacy ? [legacy] : [],
|
|
147
|
+
...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
|
|
148
|
+
];
|
|
149
|
+
if (!legacyRows.some((row) => samePricing(current, row)))
|
|
150
|
+
continue;
|
|
151
|
+
upsertModelPricing(db, {
|
|
152
|
+
model,
|
|
153
|
+
input_per_1m: next.inputPer1M,
|
|
154
|
+
output_per_1m: next.outputPer1M,
|
|
155
|
+
cache_read_per_1m: next.cacheReadPer1M,
|
|
156
|
+
cache_write_per_1m: next.cacheWritePer1M,
|
|
157
|
+
cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
|
|
158
|
+
cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
|
|
159
|
+
updated_at: now
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function repairMissingDefaultCacheWrite1h(db) {
|
|
164
|
+
const now = new Date().toISOString();
|
|
165
|
+
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
166
|
+
if (!next.cacheWrite1hPer1M)
|
|
167
|
+
continue;
|
|
168
|
+
const current = getModelPricing(db, model);
|
|
169
|
+
if (!current)
|
|
170
|
+
continue;
|
|
171
|
+
if ((current.cache_write_1h_per_1m ?? 0) !== 0)
|
|
172
|
+
continue;
|
|
173
|
+
if (!sameBasePricing(current, next))
|
|
174
|
+
continue;
|
|
175
|
+
upsertModelPricing(db, {
|
|
176
|
+
model,
|
|
177
|
+
input_per_1m: current.input_per_1m,
|
|
178
|
+
output_per_1m: current.output_per_1m,
|
|
179
|
+
cache_read_per_1m: current.cache_read_per_1m,
|
|
180
|
+
cache_write_per_1m: current.cache_write_per_1m,
|
|
181
|
+
cache_write_1h_per_1m: next.cacheWrite1hPer1M,
|
|
182
|
+
cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
|
|
183
|
+
updated_at: now
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function repairMissingDefaultCacheStorage(db) {
|
|
188
|
+
const now = new Date().toISOString();
|
|
189
|
+
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
190
|
+
if (!next.cacheStoragePer1MHour)
|
|
191
|
+
continue;
|
|
192
|
+
const current = getModelPricing(db, model);
|
|
193
|
+
if (!current)
|
|
194
|
+
continue;
|
|
195
|
+
if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
|
|
196
|
+
continue;
|
|
197
|
+
if (!sameBasePricing(current, next))
|
|
198
|
+
continue;
|
|
199
|
+
upsertModelPricing(db, {
|
|
200
|
+
model,
|
|
201
|
+
input_per_1m: current.input_per_1m,
|
|
202
|
+
output_per_1m: current.output_per_1m,
|
|
203
|
+
cache_read_per_1m: current.cache_read_per_1m,
|
|
204
|
+
cache_write_per_1m: current.cache_write_per_1m,
|
|
205
|
+
cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
|
|
206
|
+
cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
|
|
207
|
+
updated_at: now
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function removeDeprecatedDefaultPricing(db) {
|
|
212
|
+
for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
|
|
213
|
+
const current = getModelPricing(db, model);
|
|
214
|
+
if (!current)
|
|
215
|
+
continue;
|
|
216
|
+
if (!removedRows.some((row) => samePricing(current, row)))
|
|
217
|
+
continue;
|
|
218
|
+
deleteModelPricing(db, model);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function sameBasePricing(row, pricing) {
|
|
222
|
+
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;
|
|
223
|
+
}
|
|
224
|
+
function samePricing(row, pricing) {
|
|
225
|
+
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);
|
|
226
|
+
}
|
|
227
|
+
function getPricingFromDb(db, model) {
|
|
228
|
+
if (isFreeModel(model))
|
|
229
|
+
return FREE_PRICING;
|
|
230
|
+
for (const key of modelLookupKeys(model)) {
|
|
231
|
+
const row = getModelPricing(db, key);
|
|
232
|
+
if (row)
|
|
233
|
+
return modelPricingFromDbRow(row);
|
|
234
|
+
}
|
|
235
|
+
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
236
|
+
const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
|
|
237
|
+
if (!match)
|
|
238
|
+
return null;
|
|
239
|
+
return modelPricingFromDbRow(match);
|
|
240
|
+
}
|
|
241
|
+
function modelPricingFromDbRow(row) {
|
|
242
|
+
const seeded = DEFAULT_PRICING[row.model];
|
|
243
|
+
const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
|
|
244
|
+
return {
|
|
245
|
+
inputPer1M: row.input_per_1m,
|
|
246
|
+
outputPer1M: row.output_per_1m,
|
|
247
|
+
cacheReadPer1M: row.cache_read_per_1m,
|
|
248
|
+
cacheWritePer1M: row.cache_write_per_1m,
|
|
249
|
+
cacheWrite1hPer1M,
|
|
250
|
+
cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function getPricing(model) {
|
|
254
|
+
if (isFreeModel(model))
|
|
255
|
+
return FREE_PRICING;
|
|
256
|
+
return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
|
|
257
|
+
}
|
|
258
|
+
function isFreeModel(model) {
|
|
259
|
+
return model.trim().toLowerCase().endsWith(":free");
|
|
260
|
+
}
|
|
261
|
+
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
262
|
+
const pricing = getPricing(model);
|
|
263
|
+
if (!pricing)
|
|
264
|
+
return 0;
|
|
265
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
266
|
+
}
|
|
267
|
+
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
268
|
+
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
269
|
+
if (!pricing)
|
|
270
|
+
return 0;
|
|
271
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
272
|
+
}
|
|
273
|
+
function getPricingFromRows(rows, model) {
|
|
274
|
+
if (isFreeModel(model))
|
|
275
|
+
return FREE_PRICING;
|
|
276
|
+
for (const key of modelLookupKeys(model)) {
|
|
277
|
+
const row = rows.find((r) => r.model === key);
|
|
278
|
+
if (row)
|
|
279
|
+
return modelPricingFromDbRow(row);
|
|
280
|
+
}
|
|
281
|
+
const match = bestModelMatch(model, rows.map((r) => [r.model, r]));
|
|
282
|
+
if (!match)
|
|
283
|
+
return null;
|
|
284
|
+
return modelPricingFromDbRow(match);
|
|
285
|
+
}
|
|
286
|
+
function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
287
|
+
const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
|
|
288
|
+
if (!pricing)
|
|
289
|
+
return 0;
|
|
290
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
291
|
+
}
|
|
292
|
+
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
293
|
+
if (isFreeModel(model))
|
|
294
|
+
return 0;
|
|
295
|
+
let effective = pricing;
|
|
296
|
+
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));
|
|
297
|
+
if (promptTier) {
|
|
298
|
+
const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
|
|
299
|
+
if (billablePromptTokens > promptTier.threshold) {
|
|
300
|
+
effective = {
|
|
301
|
+
...pricing,
|
|
302
|
+
inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
|
|
303
|
+
outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
|
|
304
|
+
cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
|
|
305
|
+
cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
310
|
+
}
|
|
311
|
+
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;
|
|
312
|
+
var init_pricing = __esm(() => {
|
|
313
|
+
init_database();
|
|
314
|
+
DEFAULT_PRICING = {
|
|
315
|
+
"claude-opus-4-8": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
316
|
+
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
317
|
+
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
318
|
+
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
319
|
+
"claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
320
|
+
"claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
321
|
+
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
322
|
+
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
323
|
+
"claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
324
|
+
"claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
325
|
+
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
|
|
326
|
+
"claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
|
|
327
|
+
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
328
|
+
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
|
|
329
|
+
"gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
330
|
+
"gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
331
|
+
"gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
332
|
+
"gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
333
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
334
|
+
"gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
335
|
+
"gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
336
|
+
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
337
|
+
"gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
338
|
+
"google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
|
|
339
|
+
"google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
340
|
+
"google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
341
|
+
"google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
|
|
342
|
+
"google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
|
|
343
|
+
"google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
|
|
344
|
+
"google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
|
|
345
|
+
"gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
346
|
+
"gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
347
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
348
|
+
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
349
|
+
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
350
|
+
"gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
351
|
+
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
352
|
+
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
353
|
+
"gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
354
|
+
"gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
355
|
+
"gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
356
|
+
"gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
|
|
357
|
+
"gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
358
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
359
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
360
|
+
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
361
|
+
"o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
362
|
+
o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
363
|
+
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
364
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
|
|
365
|
+
"qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
366
|
+
"qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
367
|
+
"qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
368
|
+
"qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
369
|
+
"qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
370
|
+
"qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
371
|
+
"qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
372
|
+
"qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
373
|
+
"qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
374
|
+
"qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
375
|
+
"minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
376
|
+
"minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
377
|
+
"minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
378
|
+
"minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
379
|
+
"minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
380
|
+
"grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
381
|
+
"grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
382
|
+
"grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
383
|
+
"grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
384
|
+
"grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
385
|
+
"grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
386
|
+
"grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
387
|
+
"grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
388
|
+
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
389
|
+
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
|
|
390
|
+
"glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
|
|
391
|
+
"glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
392
|
+
"z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
|
|
393
|
+
"z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
|
|
394
|
+
"kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
|
|
395
|
+
"kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
|
|
396
|
+
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
397
|
+
"moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
398
|
+
"moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
|
|
399
|
+
"moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
400
|
+
};
|
|
401
|
+
LEGACY_DEFAULT_PRICING = {
|
|
402
|
+
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
403
|
+
"claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
404
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
|
|
405
|
+
"gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
406
|
+
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
407
|
+
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
408
|
+
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
409
|
+
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
410
|
+
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
411
|
+
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
412
|
+
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
413
|
+
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
414
|
+
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
415
|
+
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
416
|
+
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
417
|
+
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
418
|
+
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
419
|
+
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
420
|
+
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
421
|
+
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
422
|
+
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
|
|
423
|
+
};
|
|
424
|
+
ADDITIONAL_LEGACY_DEFAULT_PRICING = {
|
|
425
|
+
"gemini-2.5-pro": [
|
|
426
|
+
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
427
|
+
],
|
|
428
|
+
"qwen3.6-plus": [
|
|
429
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
430
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
431
|
+
],
|
|
432
|
+
"qwen3.6-flash": [
|
|
433
|
+
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
434
|
+
],
|
|
435
|
+
"qwen3.6-max-preview": [
|
|
436
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
437
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
438
|
+
],
|
|
439
|
+
"qwen/qwen3.6-plus": [
|
|
440
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
441
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
442
|
+
],
|
|
443
|
+
"qwen/qwen3.6-flash": [
|
|
444
|
+
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
445
|
+
],
|
|
446
|
+
"qwen/qwen3.6-max-preview": [
|
|
447
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
448
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
449
|
+
]
|
|
450
|
+
};
|
|
451
|
+
REMOVED_DEFAULT_PRICING = {
|
|
452
|
+
"claude-3-5-sonnet": [
|
|
453
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
454
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
455
|
+
],
|
|
456
|
+
"claude-3-sonnet": [
|
|
457
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
458
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
459
|
+
],
|
|
460
|
+
"gemini-3.1-pro": [
|
|
461
|
+
{ inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
462
|
+
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
|
|
463
|
+
],
|
|
464
|
+
"gemini-1.5-pro": [
|
|
465
|
+
{ inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
466
|
+
],
|
|
467
|
+
"gemini-1.5-flash": [
|
|
468
|
+
{ inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
469
|
+
],
|
|
470
|
+
"gpt-5.3-chat": [
|
|
471
|
+
{ inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
472
|
+
{ inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
|
|
473
|
+
],
|
|
474
|
+
"qwen3.6": [
|
|
475
|
+
{ inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
476
|
+
]
|
|
477
|
+
};
|
|
478
|
+
FREE_PRICING = {
|
|
479
|
+
inputPer1M: 0,
|
|
480
|
+
outputPer1M: 0,
|
|
481
|
+
cacheReadPer1M: 0,
|
|
482
|
+
cacheWritePer1M: 0,
|
|
483
|
+
cacheWrite1hPer1M: 0,
|
|
484
|
+
cacheStoragePer1MHour: 0
|
|
485
|
+
};
|
|
486
|
+
GEMINI_PROMPT_TIERS = {
|
|
487
|
+
"gemini-3.1-pro-preview": {
|
|
488
|
+
threshold: 200000,
|
|
489
|
+
inputPer1M: 4,
|
|
490
|
+
outputPer1M: 18,
|
|
491
|
+
cacheReadPer1M: 0.4
|
|
492
|
+
},
|
|
493
|
+
"gemini-2.5-pro": {
|
|
494
|
+
threshold: 200000,
|
|
495
|
+
inputPer1M: 2.5,
|
|
496
|
+
outputPer1M: 15,
|
|
497
|
+
cacheReadPer1M: 0.25
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
OPENAI_PROMPT_TIERS = {
|
|
501
|
+
"gpt-5.5": {
|
|
502
|
+
threshold: 272000,
|
|
503
|
+
inputMultiplier: 2,
|
|
504
|
+
outputMultiplier: 1.5,
|
|
505
|
+
cacheReadMultiplier: 2
|
|
506
|
+
},
|
|
507
|
+
"gpt-5.4-pro": {
|
|
508
|
+
threshold: 272000,
|
|
509
|
+
inputMultiplier: 2,
|
|
510
|
+
outputMultiplier: 1.5,
|
|
511
|
+
cacheReadMultiplier: 2
|
|
512
|
+
},
|
|
513
|
+
"gpt-5.4": {
|
|
514
|
+
threshold: 272000,
|
|
515
|
+
inputMultiplier: 2,
|
|
516
|
+
outputMultiplier: 1.5,
|
|
517
|
+
cacheReadMultiplier: 2
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
QWEN_PROMPT_TIERS = {
|
|
521
|
+
"qwen3.6-plus": {
|
|
522
|
+
threshold: 256000,
|
|
523
|
+
inputPer1M: 1.3,
|
|
524
|
+
outputPer1M: 3.9,
|
|
525
|
+
cacheReadPer1M: 0.13,
|
|
526
|
+
cacheWritePer1M: 1.625
|
|
527
|
+
},
|
|
528
|
+
"qwen3.6-flash": {
|
|
529
|
+
threshold: 256000,
|
|
530
|
+
inputPer1M: 1,
|
|
531
|
+
outputPer1M: 4,
|
|
532
|
+
cacheReadPer1M: 0.1,
|
|
533
|
+
cacheWritePer1M: 1.25
|
|
534
|
+
},
|
|
535
|
+
"qwen3.6-max-preview": {
|
|
536
|
+
threshold: 128000,
|
|
537
|
+
inputPer1M: 1.6,
|
|
538
|
+
outputPer1M: 9.6,
|
|
539
|
+
cacheReadPer1M: 0.16,
|
|
540
|
+
cacheWritePer1M: 2
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
MINIMAX_PROMPT_TIERS = {
|
|
544
|
+
"minimax/minimax-m1": {
|
|
545
|
+
threshold: Number.POSITIVE_INFINITY
|
|
546
|
+
},
|
|
547
|
+
"minimax-m1": {
|
|
548
|
+
threshold: 200000,
|
|
549
|
+
inputPer1M: 1.3
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
XAI_PROMPT_TIERS = {
|
|
553
|
+
"grok-4.3": {
|
|
554
|
+
threshold: 200000,
|
|
555
|
+
inputPer1M: 2.5,
|
|
556
|
+
outputPer1M: 5,
|
|
557
|
+
cacheReadPer1M: 0.4
|
|
558
|
+
},
|
|
559
|
+
"grok-latest": {
|
|
560
|
+
threshold: 200000,
|
|
561
|
+
inputPer1M: 2.5,
|
|
562
|
+
outputPer1M: 5,
|
|
563
|
+
cacheReadPer1M: 0.4
|
|
564
|
+
},
|
|
565
|
+
"grok-4.20": {
|
|
566
|
+
threshold: 200000,
|
|
567
|
+
inputPer1M: 2.5,
|
|
568
|
+
outputPer1M: 5,
|
|
569
|
+
cacheReadPer1M: 0.4
|
|
570
|
+
},
|
|
571
|
+
"grok-4-1-fast": {
|
|
572
|
+
threshold: 128000,
|
|
573
|
+
inputPer1M: 0.4,
|
|
574
|
+
outputPer1M: 1,
|
|
575
|
+
cacheReadPer1M: 0
|
|
576
|
+
},
|
|
577
|
+
"grok-4-fast": {
|
|
578
|
+
threshold: 128000,
|
|
579
|
+
inputPer1M: 0.4,
|
|
580
|
+
outputPer1M: 1,
|
|
581
|
+
cacheReadPer1M: 0
|
|
582
|
+
},
|
|
583
|
+
"grok-4": {
|
|
584
|
+
threshold: 128000,
|
|
585
|
+
inputPer1M: 6,
|
|
586
|
+
outputPer1M: 30,
|
|
587
|
+
cacheReadPer1M: 0
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// src/db/database.ts
|
|
593
|
+
import { execFileSync } from "child_process";
|
|
594
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
595
|
+
import { hostname, platform } from "os";
|
|
596
|
+
import { homedir } from "os";
|
|
597
|
+
import { join } from "path";
|
|
598
|
+
function normalizeMachineId(value) {
|
|
599
|
+
const id = value?.trim().toLowerCase().split(".")[0];
|
|
600
|
+
return id && id.length > 0 ? id : null;
|
|
601
|
+
}
|
|
602
|
+
function macHostMachineId() {
|
|
603
|
+
if (platform() !== "darwin")
|
|
604
|
+
return null;
|
|
605
|
+
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
606
|
+
try {
|
|
607
|
+
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
608
|
+
encoding: "utf8",
|
|
609
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
610
|
+
timeout: 1000
|
|
611
|
+
});
|
|
612
|
+
const id = normalizeMachineId(value);
|
|
613
|
+
if (id && id !== "mac" && id !== "localhost")
|
|
614
|
+
return id;
|
|
615
|
+
} catch {}
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
function getMachineId() {
|
|
620
|
+
const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
|
|
621
|
+
if (envMachine)
|
|
622
|
+
return envMachine;
|
|
623
|
+
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
624
|
+
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
625
|
+
return macHostMachineId() ?? hostMachine;
|
|
626
|
+
return hostMachine;
|
|
627
|
+
}
|
|
628
|
+
function getDataDir() {
|
|
629
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
630
|
+
const newDir = join(home, ".hasna", "economy");
|
|
631
|
+
const oldDir = join(home, ".economy");
|
|
632
|
+
if (existsSync(oldDir) && !existsSync(newDir)) {
|
|
633
|
+
mkdirSync(newDir, { recursive: true });
|
|
634
|
+
for (const file of readdirSync(oldDir)) {
|
|
635
|
+
const oldPath = join(oldDir, file);
|
|
636
|
+
if (statSync(oldPath).isFile()) {
|
|
637
|
+
copyFileSync(oldPath, join(newDir, file));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
mkdirSync(newDir, { recursive: true });
|
|
642
|
+
return newDir;
|
|
643
|
+
}
|
|
644
|
+
function getDbPath() {
|
|
645
|
+
if (process.env["HASNA_ECONOMY_DB_PATH"])
|
|
646
|
+
return process.env["HASNA_ECONOMY_DB_PATH"];
|
|
647
|
+
if (process.env["ECONOMY_DB"])
|
|
648
|
+
return process.env["ECONOMY_DB"];
|
|
649
|
+
return join(getDataDir(), "economy.db");
|
|
650
|
+
}
|
|
651
|
+
function isSqliteBusyError(error) {
|
|
652
|
+
const candidate = error;
|
|
653
|
+
const code = typeof candidate.code === "string" ? candidate.code : "";
|
|
654
|
+
const message = typeof candidate.message === "string" ? candidate.message : String(error);
|
|
655
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
|
|
656
|
+
}
|
|
657
|
+
function retryDelayMs(attempt) {
|
|
658
|
+
return Math.min(1000, 50 * 2 ** attempt);
|
|
659
|
+
}
|
|
660
|
+
function withSqliteBusyRetry(operation, context) {
|
|
661
|
+
const maxAttempts = 8;
|
|
662
|
+
let lastError;
|
|
663
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
664
|
+
try {
|
|
665
|
+
return operation();
|
|
666
|
+
} catch (error) {
|
|
667
|
+
if (!isSqliteBusyError(error))
|
|
668
|
+
throw error;
|
|
669
|
+
lastError = error;
|
|
670
|
+
if (attempt === maxAttempts - 1)
|
|
671
|
+
break;
|
|
672
|
+
Bun.sleepSync(retryDelayMs(attempt));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
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 });
|
|
676
|
+
}
|
|
677
|
+
function openDatabase(dbPath, skipSeed = false) {
|
|
678
|
+
const path = dbPath ?? getDbPath();
|
|
679
|
+
if (path !== ":memory:") {
|
|
680
|
+
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
681
|
+
if (dir && !existsSync(dir))
|
|
682
|
+
mkdirSync(dir, { recursive: true });
|
|
683
|
+
}
|
|
684
|
+
const db = withSqliteBusyRetry(() => {
|
|
685
|
+
const opened = new SqliteAdapter(path);
|
|
686
|
+
try {
|
|
687
|
+
opened.exec("PRAGMA busy_timeout = 10000");
|
|
688
|
+
opened.exec("PRAGMA journal_mode = WAL");
|
|
689
|
+
opened.exec("PRAGMA foreign_keys = ON");
|
|
690
|
+
initSchema(opened);
|
|
691
|
+
return opened;
|
|
692
|
+
} catch (error) {
|
|
693
|
+
try {
|
|
694
|
+
opened.close();
|
|
695
|
+
} catch {}
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
}, `opening ${path}`);
|
|
699
|
+
if (!skipSeed) {
|
|
700
|
+
Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
|
|
701
|
+
}
|
|
702
|
+
return db;
|
|
703
|
+
}
|
|
704
|
+
function quoteSqlIdent(identifier) {
|
|
705
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
706
|
+
}
|
|
707
|
+
function hasColumn(db, table, column) {
|
|
708
|
+
const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
|
|
709
|
+
return columns.some((c) => c.name === column);
|
|
710
|
+
}
|
|
711
|
+
function addColumnIfMissing(db, table, column, definition) {
|
|
712
|
+
if (hasColumn(db, table, column))
|
|
713
|
+
return false;
|
|
714
|
+
try {
|
|
715
|
+
db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
|
|
716
|
+
return true;
|
|
717
|
+
} catch (error) {
|
|
718
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
719
|
+
if (/duplicate column name/i.test(message))
|
|
720
|
+
return true;
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function initSchema(db) {
|
|
725
|
+
db.exec(`
|
|
726
|
+
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
727
|
+
id TEXT PRIMARY KEY,
|
|
728
|
+
kind TEXT NOT NULL,
|
|
729
|
+
name TEXT NOT NULL,
|
|
730
|
+
repo_path TEXT,
|
|
731
|
+
labels_json TEXT DEFAULT '{}',
|
|
732
|
+
created_at TEXT NOT NULL,
|
|
733
|
+
updated_at TEXT NOT NULL
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
737
|
+
id TEXT PRIMARY KEY,
|
|
738
|
+
agent TEXT NOT NULL,
|
|
739
|
+
session_id TEXT NOT NULL,
|
|
740
|
+
model TEXT NOT NULL,
|
|
741
|
+
input_tokens INTEGER DEFAULT 0,
|
|
742
|
+
output_tokens INTEGER DEFAULT 0,
|
|
743
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
744
|
+
cache_create_tokens INTEGER DEFAULT 0,
|
|
745
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
746
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
747
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
748
|
+
duration_ms INTEGER DEFAULT 0,
|
|
749
|
+
timestamp TEXT NOT NULL,
|
|
750
|
+
source_request_id TEXT,
|
|
751
|
+
machine_id TEXT DEFAULT '',
|
|
752
|
+
cost_center_id TEXT,
|
|
753
|
+
account_key TEXT DEFAULT '',
|
|
754
|
+
account_tool TEXT DEFAULT '',
|
|
755
|
+
account_name TEXT DEFAULT '',
|
|
756
|
+
account_email TEXT DEFAULT '',
|
|
757
|
+
account_source TEXT DEFAULT ''
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
761
|
+
id TEXT PRIMARY KEY,
|
|
762
|
+
agent TEXT NOT NULL,
|
|
763
|
+
project_path TEXT DEFAULT '',
|
|
764
|
+
project_name TEXT DEFAULT '',
|
|
765
|
+
started_at TEXT NOT NULL,
|
|
766
|
+
ended_at TEXT,
|
|
767
|
+
total_cost_usd REAL DEFAULT 0,
|
|
768
|
+
total_tokens INTEGER DEFAULT 0,
|
|
769
|
+
request_count INTEGER DEFAULT 0,
|
|
770
|
+
machine_id TEXT DEFAULT '',
|
|
771
|
+
cost_center_id TEXT,
|
|
772
|
+
account_key TEXT DEFAULT '',
|
|
773
|
+
account_tool TEXT DEFAULT '',
|
|
774
|
+
account_name TEXT DEFAULT '',
|
|
775
|
+
account_email TEXT DEFAULT '',
|
|
776
|
+
account_source TEXT DEFAULT ''
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
780
|
+
id TEXT PRIMARY KEY,
|
|
781
|
+
path TEXT UNIQUE NOT NULL,
|
|
782
|
+
name TEXT NOT NULL,
|
|
783
|
+
description TEXT,
|
|
784
|
+
tags TEXT DEFAULT '[]',
|
|
785
|
+
created_at TEXT NOT NULL
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
CREATE TABLE IF NOT EXISTS budgets (
|
|
789
|
+
id TEXT PRIMARY KEY,
|
|
790
|
+
project_path TEXT,
|
|
791
|
+
agent TEXT,
|
|
792
|
+
cost_center_id TEXT,
|
|
793
|
+
period TEXT NOT NULL,
|
|
794
|
+
limit_usd REAL NOT NULL,
|
|
795
|
+
alert_at_percent INTEGER DEFAULT 80,
|
|
796
|
+
created_at TEXT NOT NULL,
|
|
797
|
+
updated_at TEXT NOT NULL
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
801
|
+
id TEXT PRIMARY KEY,
|
|
802
|
+
period TEXT NOT NULL,
|
|
803
|
+
project_path TEXT,
|
|
804
|
+
agent TEXT,
|
|
805
|
+
limit_usd REAL NOT NULL,
|
|
806
|
+
created_at TEXT NOT NULL,
|
|
807
|
+
updated_at TEXT NOT NULL
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
CREATE TABLE IF NOT EXISTS ingest_state (
|
|
811
|
+
source TEXT NOT NULL,
|
|
812
|
+
key TEXT NOT NULL,
|
|
813
|
+
value TEXT NOT NULL,
|
|
814
|
+
PRIMARY KEY (source, key)
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
|
|
818
|
+
CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
|
|
819
|
+
CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
|
|
820
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
821
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
822
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
823
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
824
|
+
|
|
825
|
+
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
826
|
+
model TEXT PRIMARY KEY,
|
|
827
|
+
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
828
|
+
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
829
|
+
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
830
|
+
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
831
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
832
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
833
|
+
updated_at TEXT NOT NULL
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
837
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
838
|
+
message TEXT NOT NULL,
|
|
839
|
+
email TEXT,
|
|
840
|
+
category TEXT DEFAULT 'general',
|
|
841
|
+
version TEXT,
|
|
842
|
+
machine_id TEXT,
|
|
843
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
847
|
+
date TEXT NOT NULL,
|
|
848
|
+
provider TEXT NOT NULL,
|
|
849
|
+
description TEXT DEFAULT '',
|
|
850
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
851
|
+
updated_at TEXT NOT NULL,
|
|
852
|
+
PRIMARY KEY (date, provider, description)
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
856
|
+
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
857
|
+
|
|
858
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
859
|
+
id TEXT PRIMARY KEY,
|
|
860
|
+
agent TEXT,
|
|
861
|
+
provider TEXT NOT NULL,
|
|
862
|
+
plan TEXT NOT NULL,
|
|
863
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
864
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
865
|
+
billing_cycle_start TEXT,
|
|
866
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
867
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
868
|
+
created_at TEXT NOT NULL,
|
|
869
|
+
updated_at TEXT NOT NULL
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
873
|
+
id TEXT PRIMARY KEY,
|
|
874
|
+
agent TEXT NOT NULL,
|
|
875
|
+
date TEXT NOT NULL,
|
|
876
|
+
metric TEXT NOT NULL,
|
|
877
|
+
value REAL NOT NULL DEFAULT 0,
|
|
878
|
+
unit TEXT DEFAULT '',
|
|
879
|
+
machine_id TEXT DEFAULT '',
|
|
880
|
+
updated_at TEXT NOT NULL
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
884
|
+
date TEXT NOT NULL,
|
|
885
|
+
agent TEXT DEFAULT '',
|
|
886
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
887
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
888
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
889
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
890
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
891
|
+
updated_at TEXT NOT NULL,
|
|
892
|
+
PRIMARY KEY (date, agent)
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
896
|
+
machine_id TEXT PRIMARY KEY,
|
|
897
|
+
hostname TEXT NOT NULL,
|
|
898
|
+
last_seen_at TEXT,
|
|
899
|
+
last_push_at TEXT,
|
|
900
|
+
last_pull_at TEXT,
|
|
901
|
+
economy_version TEXT,
|
|
902
|
+
updated_at TEXT NOT NULL
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
906
|
+
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
907
|
+
`);
|
|
908
|
+
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
909
|
+
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
910
|
+
if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
|
|
911
|
+
db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
912
|
+
}
|
|
913
|
+
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
914
|
+
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
915
|
+
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
916
|
+
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
917
|
+
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
918
|
+
}
|
|
919
|
+
addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
|
|
920
|
+
addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
|
|
921
|
+
addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
|
|
922
|
+
if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
|
|
923
|
+
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
924
|
+
}
|
|
925
|
+
addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
|
|
926
|
+
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
927
|
+
addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
|
|
928
|
+
}
|
|
929
|
+
addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
|
|
930
|
+
if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
|
|
931
|
+
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
932
|
+
}
|
|
933
|
+
addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
|
|
934
|
+
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
935
|
+
addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
|
|
936
|
+
}
|
|
937
|
+
addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
|
|
938
|
+
addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
|
|
939
|
+
db.exec(`
|
|
940
|
+
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
941
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
942
|
+
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
943
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
944
|
+
CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
|
|
945
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
|
|
946
|
+
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
947
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
948
|
+
`);
|
|
949
|
+
}
|
|
950
|
+
function upsertCostCenter(db, center) {
|
|
951
|
+
const updatedAt = center.updated_at ?? center.created_at;
|
|
952
|
+
db.prepare(`
|
|
953
|
+
INSERT OR REPLACE INTO cost_centers
|
|
954
|
+
(id, kind, name, repo_path, labels_json, created_at, updated_at)
|
|
955
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
956
|
+
`).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
|
|
957
|
+
}
|
|
958
|
+
function upsertRequest(db, req) {
|
|
959
|
+
const now = req.updated_at ?? new Date().toISOString();
|
|
960
|
+
db.prepare(`
|
|
961
|
+
INSERT OR REPLACE INTO requests
|
|
962
|
+
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
963
|
+
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
964
|
+
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
965
|
+
source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
|
|
966
|
+
account_name, account_email, account_source, updated_at, synced_at)
|
|
967
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
968
|
+
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.cost_center_id ?? null, req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
|
|
969
|
+
}
|
|
970
|
+
function upsertSession(db, session) {
|
|
971
|
+
const now = session.updated_at ?? new Date().toISOString();
|
|
972
|
+
db.prepare(`
|
|
973
|
+
INSERT OR REPLACE INTO sessions
|
|
974
|
+
(id, agent, project_path, project_name, started_at, ended_at,
|
|
975
|
+
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
976
|
+
cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
977
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
978
|
+
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.cost_center_id ?? null, session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
|
|
979
|
+
}
|
|
980
|
+
function rollupSession(db, sessionId) {
|
|
981
|
+
db.prepare(`
|
|
982
|
+
UPDATE sessions SET
|
|
983
|
+
total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
|
|
984
|
+
total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
|
|
985
|
+
request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
|
|
986
|
+
ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
|
|
987
|
+
started_at = CASE WHEN started_at = '' OR started_at IS NULL
|
|
988
|
+
THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
|
|
989
|
+
ELSE started_at END,
|
|
990
|
+
account_key = CASE WHEN account_key = '' OR account_key IS NULL
|
|
991
|
+
THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
992
|
+
ELSE account_key END,
|
|
993
|
+
account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
|
|
994
|
+
THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
995
|
+
ELSE account_tool END,
|
|
996
|
+
account_name = CASE WHEN account_name = '' OR account_name IS NULL
|
|
997
|
+
THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
998
|
+
ELSE account_name END,
|
|
999
|
+
account_email = CASE WHEN account_email = '' OR account_email IS NULL
|
|
1000
|
+
THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1001
|
+
ELSE account_email END,
|
|
1002
|
+
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
1003
|
+
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1004
|
+
ELSE account_source END,
|
|
1005
|
+
cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
|
|
1006
|
+
THEN COALESCE((SELECT cost_center_id FROM requests WHERE session_id = ? AND cost_center_id IS NOT NULL AND cost_center_id != '' ORDER BY timestamp DESC LIMIT 1), NULL)
|
|
1007
|
+
ELSE cost_center_id END
|
|
1008
|
+
WHERE id = ?
|
|
1009
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
1010
|
+
}
|
|
1011
|
+
function upsertModelPricing(db, p) {
|
|
1012
|
+
db.prepare(`
|
|
1013
|
+
INSERT OR REPLACE INTO model_pricing
|
|
1014
|
+
(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)
|
|
1015
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1016
|
+
`).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);
|
|
1017
|
+
}
|
|
1018
|
+
function getModelPricing(db, model) {
|
|
1019
|
+
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
1020
|
+
}
|
|
1021
|
+
function deleteModelPricing(db, model) {
|
|
1022
|
+
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
1023
|
+
}
|
|
1024
|
+
function seedModelPricing(db, defaults) {
|
|
1025
|
+
const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
|
|
1026
|
+
const now = new Date().toISOString();
|
|
1027
|
+
for (const [model, p] of Object.entries(defaults)) {
|
|
1028
|
+
if (existing.has(model))
|
|
1029
|
+
continue;
|
|
1030
|
+
upsertModelPricing(db, {
|
|
1031
|
+
model,
|
|
1032
|
+
input_per_1m: p.inputPer1M,
|
|
1033
|
+
output_per_1m: p.outputPer1M,
|
|
1034
|
+
cache_read_per_1m: p.cacheReadPer1M,
|
|
1035
|
+
cache_write_per_1m: p.cacheWritePer1M,
|
|
1036
|
+
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1037
|
+
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
1038
|
+
updated_at: now
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
var init_database = __esm(() => {
|
|
1043
|
+
init_sqlite_adapter();
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// src/otel/index.ts
|
|
1047
|
+
init_database();
|
|
1048
|
+
|
|
1049
|
+
// src/ingest/otel.ts
|
|
1050
|
+
init_database();
|
|
1051
|
+
|
|
1052
|
+
// src/lib/agents.ts
|
|
1053
|
+
var AGENTS = [
|
|
1054
|
+
"claude",
|
|
1055
|
+
"takumi",
|
|
1056
|
+
"codex",
|
|
1057
|
+
"gemini",
|
|
1058
|
+
"opencode",
|
|
1059
|
+
"cursor",
|
|
1060
|
+
"pi",
|
|
1061
|
+
"hermes"
|
|
1062
|
+
];
|
|
1063
|
+
function isAgent(value) {
|
|
1064
|
+
return AGENTS.includes(value);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/ingest/otel.ts
|
|
1068
|
+
function attrValue(attr) {
|
|
1069
|
+
if (!attr?.value)
|
|
1070
|
+
return;
|
|
1071
|
+
if (attr.value.stringValue != null)
|
|
1072
|
+
return attr.value.stringValue;
|
|
1073
|
+
if (attr.value.doubleValue != null)
|
|
1074
|
+
return attr.value.doubleValue;
|
|
1075
|
+
if (attr.value.intValue != null)
|
|
1076
|
+
return Number(attr.value.intValue);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
function attrsMap(attributes) {
|
|
1080
|
+
const out = {};
|
|
1081
|
+
for (const a of attributes ?? []) {
|
|
1082
|
+
if (!a.key)
|
|
1083
|
+
continue;
|
|
1084
|
+
const v = attrValue(a);
|
|
1085
|
+
if (v != null)
|
|
1086
|
+
out[a.key] = v;
|
|
1087
|
+
}
|
|
1088
|
+
return out;
|
|
1089
|
+
}
|
|
1090
|
+
var COST_CENTER_KINDS = ["loop", "app", "repo", "service", "team"];
|
|
1091
|
+
var PSEUDO_AGENTS = ["app", "service", "repo", "loop"];
|
|
1092
|
+
function isCostCenterKind(value) {
|
|
1093
|
+
return COST_CENTER_KINDS.includes(value);
|
|
1094
|
+
}
|
|
1095
|
+
function asString(value) {
|
|
1096
|
+
if (value == null)
|
|
1097
|
+
return;
|
|
1098
|
+
const text = String(value).trim();
|
|
1099
|
+
return text ? text : undefined;
|
|
1100
|
+
}
|
|
1101
|
+
function firstString(source, keys) {
|
|
1102
|
+
for (const key of keys) {
|
|
1103
|
+
const value = asString(source[key]);
|
|
1104
|
+
if (value)
|
|
1105
|
+
return value;
|
|
1106
|
+
}
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
function normalizeCostCenterKind(raw) {
|
|
1110
|
+
if (!raw)
|
|
1111
|
+
return;
|
|
1112
|
+
const value = raw.toLowerCase();
|
|
1113
|
+
return isCostCenterKind(value) ? value : undefined;
|
|
1114
|
+
}
|
|
1115
|
+
function normalizeAgent(raw, kind) {
|
|
1116
|
+
if (raw && isAgent(raw))
|
|
1117
|
+
return raw;
|
|
1118
|
+
if (raw && PSEUDO_AGENTS.includes(raw))
|
|
1119
|
+
return raw;
|
|
1120
|
+
if (kind && PSEUDO_AGENTS.includes(kind))
|
|
1121
|
+
return kind;
|
|
1122
|
+
return "service";
|
|
1123
|
+
}
|
|
1124
|
+
function slug(value) {
|
|
1125
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
1126
|
+
}
|
|
1127
|
+
function costCenterId(kind, name, explicitId) {
|
|
1128
|
+
if (explicitId)
|
|
1129
|
+
return explicitId;
|
|
1130
|
+
if (!kind || !name)
|
|
1131
|
+
return;
|
|
1132
|
+
return `${kind}:${slug(name)}`;
|
|
1133
|
+
}
|
|
1134
|
+
function projectNameFromPath(projectPath) {
|
|
1135
|
+
if (!projectPath)
|
|
1136
|
+
return;
|
|
1137
|
+
return projectPath.split("/").filter(Boolean).at(-1);
|
|
1138
|
+
}
|
|
1139
|
+
function extractAttribution(source) {
|
|
1140
|
+
const sourceKind = firstString(source, ["source", "source_kind", "ai.source"]);
|
|
1141
|
+
const rawKind = firstString(source, ["cost_center_kind", "cost_center.kind", "economy.cost_center.kind"]) ?? sourceKind;
|
|
1142
|
+
const costCenterKind = normalizeCostCenterKind(rawKind);
|
|
1143
|
+
const costCenter = firstString(source, [
|
|
1144
|
+
"cost_center",
|
|
1145
|
+
"cost_center_name",
|
|
1146
|
+
"cost_center.name",
|
|
1147
|
+
"economy.cost_center",
|
|
1148
|
+
"service.name",
|
|
1149
|
+
"app",
|
|
1150
|
+
"app.name",
|
|
1151
|
+
"repo",
|
|
1152
|
+
"repository"
|
|
1153
|
+
]);
|
|
1154
|
+
const projectPath = firstString(source, ["project_path", "project.path", "repo_path", "repository.path"]);
|
|
1155
|
+
const explicitId = firstString(source, ["cost_center_id", "cost_center.id", "economy.cost_center.id"]);
|
|
1156
|
+
const costBasisRaw = firstString(source, ["cost_basis", "economy.cost_basis"]);
|
|
1157
|
+
const costBasis = costBasisRaw && ["metered_api", "subscription_included", "estimated", "unknown"].includes(costBasisRaw) ? costBasisRaw : undefined;
|
|
1158
|
+
return {
|
|
1159
|
+
agent: normalizeAgent(firstString(source, ["agent", "ai.agent"]) ?? sourceKind, costCenterKind),
|
|
1160
|
+
cost_center: costCenter,
|
|
1161
|
+
cost_center_kind: costCenterKind,
|
|
1162
|
+
cost_center_id: costCenterId(costCenterKind, costCenter, explicitId),
|
|
1163
|
+
attribution_tag: firstString(source, ["attribution_tag", "attribution.tag", "economy.attribution_tag"]),
|
|
1164
|
+
project_path: projectPath,
|
|
1165
|
+
project_name: firstString(source, ["project_name", "project.name"]) ?? projectNameFromPath(projectPath) ?? costCenter,
|
|
1166
|
+
repo: firstString(source, ["repo", "repository", "repository.name"]),
|
|
1167
|
+
account_key: firstString(source, ["account_key", "account.key", "economy.account_key"]),
|
|
1168
|
+
account_tool: firstString(source, ["account_tool", "account.tool", "economy.account_tool"]),
|
|
1169
|
+
account_name: firstString(source, ["account_name", "account.name", "economy.account_name"]),
|
|
1170
|
+
account_email: firstString(source, ["account_email", "account.email", "economy.account_email"]),
|
|
1171
|
+
account_source: firstString(source, ["account_source", "account.source", "economy.account_source"]),
|
|
1172
|
+
cost_basis: costBasis
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function mergeAttribution(row, attribution) {
|
|
1176
|
+
return {
|
|
1177
|
+
...row,
|
|
1178
|
+
...Object.fromEntries(Object.entries(attribution).filter(([, value]) => value !== undefined && value !== ""))
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function metricKind(body) {
|
|
1182
|
+
const name = String(body["name"] ?? "").toLowerCase();
|
|
1183
|
+
if (name.includes("cost") || name.includes(".usd"))
|
|
1184
|
+
return "cost";
|
|
1185
|
+
if (name.includes("input") && name.includes("token"))
|
|
1186
|
+
return "input_tokens";
|
|
1187
|
+
if (name.includes("output") && name.includes("token"))
|
|
1188
|
+
return "output_tokens";
|
|
1189
|
+
if (name.endsWith(".token.usage") || name.includes("tokens.input"))
|
|
1190
|
+
return "input_tokens";
|
|
1191
|
+
if (name.includes("tokens.output"))
|
|
1192
|
+
return "output_tokens";
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
function parseOtlpMetrics(body) {
|
|
1196
|
+
if (!body || typeof body !== "object")
|
|
1197
|
+
return [];
|
|
1198
|
+
const resourceMetrics = body["resourceMetrics"];
|
|
1199
|
+
if (!Array.isArray(resourceMetrics))
|
|
1200
|
+
return [];
|
|
1201
|
+
const partial = new Map;
|
|
1202
|
+
for (const rm of resourceMetrics) {
|
|
1203
|
+
if (!rm || typeof rm !== "object")
|
|
1204
|
+
continue;
|
|
1205
|
+
const resourceAttrs = attrsMap(rm["resource"] ? rm["resource"].attributes : undefined);
|
|
1206
|
+
const scopeMetrics = rm["scopeMetrics"];
|
|
1207
|
+
if (!Array.isArray(scopeMetrics))
|
|
1208
|
+
continue;
|
|
1209
|
+
for (const sm of scopeMetrics) {
|
|
1210
|
+
if (!sm || typeof sm !== "object")
|
|
1211
|
+
continue;
|
|
1212
|
+
const metrics = sm["metrics"];
|
|
1213
|
+
if (!Array.isArray(metrics))
|
|
1214
|
+
continue;
|
|
1215
|
+
for (const metric of metrics) {
|
|
1216
|
+
if (!metric || typeof metric !== "object")
|
|
1217
|
+
continue;
|
|
1218
|
+
const kind = metricKind(metric);
|
|
1219
|
+
if (!kind)
|
|
1220
|
+
continue;
|
|
1221
|
+
const sum = metric["sum"];
|
|
1222
|
+
const gauge = metric["gauge"];
|
|
1223
|
+
const dataPoints = sum?.dataPoints ?? gauge?.dataPoints ?? [];
|
|
1224
|
+
for (const dp of dataPoints) {
|
|
1225
|
+
if (!dp || typeof dp !== "object")
|
|
1226
|
+
continue;
|
|
1227
|
+
const pointAttrs = attrsMap(dp.attributes);
|
|
1228
|
+
const merged = { ...resourceAttrs, ...pointAttrs };
|
|
1229
|
+
const attribution = extractAttribution(merged);
|
|
1230
|
+
const agent = attribution.agent;
|
|
1231
|
+
const sessionId = String(merged["session_id"] ?? merged["session.id"] ?? "otel-session");
|
|
1232
|
+
const model = String(merged["model"] ?? merged["ai.model"] ?? "unknown");
|
|
1233
|
+
const sourceId = String(merged["request_id"] ?? merged["event.id"] ?? `${sessionId}-${kind}`);
|
|
1234
|
+
const key = `${agent}:${sourceId}`;
|
|
1235
|
+
const row = mergeAttribution(partial.get(key) ?? {
|
|
1236
|
+
key,
|
|
1237
|
+
agent,
|
|
1238
|
+
session_id: sessionId,
|
|
1239
|
+
model,
|
|
1240
|
+
cost_usd: 0,
|
|
1241
|
+
input_tokens: 0,
|
|
1242
|
+
output_tokens: 0,
|
|
1243
|
+
timestamp: new Date().toISOString(),
|
|
1244
|
+
source_request_id: sourceId
|
|
1245
|
+
}, attribution);
|
|
1246
|
+
const asDouble = dp.asDouble;
|
|
1247
|
+
const asInt = dp.asInt;
|
|
1248
|
+
const value = asDouble ?? (asInt != null ? Number(asInt) : 0);
|
|
1249
|
+
if (kind === "cost")
|
|
1250
|
+
row.cost_usd = value;
|
|
1251
|
+
if (kind === "input_tokens")
|
|
1252
|
+
row.input_tokens = Math.round(value);
|
|
1253
|
+
if (kind === "output_tokens")
|
|
1254
|
+
row.output_tokens = Math.round(value);
|
|
1255
|
+
const timeUnixNano = dp.timeUnixNano;
|
|
1256
|
+
if (timeUnixNano) {
|
|
1257
|
+
row.timestamp = new Date(Number(timeUnixNano) / 1e6).toISOString();
|
|
1258
|
+
}
|
|
1259
|
+
partial.set(key, row);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return [...partial.values()].filter((r) => (r.cost_usd ?? 0) > 0 || (r.input_tokens ?? 0) + (r.output_tokens ?? 0) > 0).map((r) => ({
|
|
1265
|
+
agent: r.agent,
|
|
1266
|
+
session_id: r.session_id,
|
|
1267
|
+
model: r.model,
|
|
1268
|
+
cost_usd: r.cost_usd ?? 0,
|
|
1269
|
+
input_tokens: r.input_tokens ?? 0,
|
|
1270
|
+
output_tokens: r.output_tokens ?? 0,
|
|
1271
|
+
timestamp: r.timestamp,
|
|
1272
|
+
source_request_id: r.source_request_id,
|
|
1273
|
+
cost_basis: r.cost_basis,
|
|
1274
|
+
cost_center: r.cost_center,
|
|
1275
|
+
cost_center_kind: r.cost_center_kind,
|
|
1276
|
+
cost_center_id: r.cost_center_id,
|
|
1277
|
+
attribution_tag: r.attribution_tag,
|
|
1278
|
+
project_path: r.project_path,
|
|
1279
|
+
project_name: r.project_name,
|
|
1280
|
+
repo: r.repo,
|
|
1281
|
+
account_key: r.account_key,
|
|
1282
|
+
account_tool: r.account_tool,
|
|
1283
|
+
account_name: r.account_name,
|
|
1284
|
+
account_email: r.account_email,
|
|
1285
|
+
account_source: r.account_source
|
|
1286
|
+
}));
|
|
1287
|
+
}
|
|
1288
|
+
function parseSimpleIngest(body) {
|
|
1289
|
+
if (!body || typeof body !== "object")
|
|
1290
|
+
return null;
|
|
1291
|
+
const b = body;
|
|
1292
|
+
const attribution = extractAttribution(b);
|
|
1293
|
+
const cost = Number(b["cost_usd"] ?? 0);
|
|
1294
|
+
const input = Number(b["input_tokens"] ?? 0);
|
|
1295
|
+
const output = Number(b["output_tokens"] ?? 0);
|
|
1296
|
+
if (cost <= 0 && input + output <= 0)
|
|
1297
|
+
return null;
|
|
1298
|
+
const sessionId = String(b["session_id"] ?? "otel-session");
|
|
1299
|
+
const sourceId = String(b["request_id"] ?? b["source_request_id"] ?? `${sessionId}-${Date.now()}`);
|
|
1300
|
+
return {
|
|
1301
|
+
...attribution,
|
|
1302
|
+
agent: attribution.agent,
|
|
1303
|
+
session_id: sessionId,
|
|
1304
|
+
model: String(b["model"] ?? "unknown"),
|
|
1305
|
+
cost_usd: cost,
|
|
1306
|
+
input_tokens: input,
|
|
1307
|
+
output_tokens: output,
|
|
1308
|
+
timestamp: String(b["timestamp"] ?? new Date().toISOString()),
|
|
1309
|
+
source_request_id: sourceId
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function ensureCostCenter(db, row) {
|
|
1313
|
+
const id = row.cost_center_id ?? costCenterId(row.cost_center_kind, row.cost_center);
|
|
1314
|
+
if (!id || !row.cost_center_kind || !row.cost_center)
|
|
1315
|
+
return id;
|
|
1316
|
+
const labels = { source: "otel" };
|
|
1317
|
+
if (row.repo)
|
|
1318
|
+
labels["repo"] = row.repo;
|
|
1319
|
+
upsertCostCenter(db, {
|
|
1320
|
+
id,
|
|
1321
|
+
kind: row.cost_center_kind,
|
|
1322
|
+
name: row.cost_center,
|
|
1323
|
+
repo_path: row.project_path ?? null,
|
|
1324
|
+
labels_json: JSON.stringify(labels),
|
|
1325
|
+
created_at: row.timestamp
|
|
1326
|
+
});
|
|
1327
|
+
return id;
|
|
1328
|
+
}
|
|
1329
|
+
async function ingestOtelRows(db, rows) {
|
|
1330
|
+
const machineId = getMachineId();
|
|
1331
|
+
const sessions = new Set;
|
|
1332
|
+
let requests = 0;
|
|
1333
|
+
for (const row of rows) {
|
|
1334
|
+
const costCenterId2 = ensureCostCenter(db, row);
|
|
1335
|
+
const requestScope = slug(costCenterId2 ?? row.cost_center ?? row.project_path ?? row.session_id);
|
|
1336
|
+
const reqId = `otel-${row.agent}-${requestScope}-${row.source_request_id}`;
|
|
1337
|
+
upsertRequest(db, {
|
|
1338
|
+
id: reqId,
|
|
1339
|
+
agent: row.agent,
|
|
1340
|
+
session_id: row.session_id,
|
|
1341
|
+
model: row.model,
|
|
1342
|
+
input_tokens: row.input_tokens,
|
|
1343
|
+
output_tokens: row.output_tokens,
|
|
1344
|
+
cache_read_tokens: 0,
|
|
1345
|
+
cache_create_tokens: 0,
|
|
1346
|
+
cost_usd: row.cost_usd,
|
|
1347
|
+
cost_basis: row.cost_basis ?? "estimated",
|
|
1348
|
+
duration_ms: 0,
|
|
1349
|
+
timestamp: row.timestamp,
|
|
1350
|
+
source_request_id: row.source_request_id,
|
|
1351
|
+
machine_id: machineId,
|
|
1352
|
+
cost_center_id: costCenterId2 ?? null,
|
|
1353
|
+
attribution_tag: row.attribution_tag ?? "otel",
|
|
1354
|
+
account_key: row.account_key ?? "",
|
|
1355
|
+
account_tool: row.account_tool ?? "",
|
|
1356
|
+
account_name: row.account_name ?? "",
|
|
1357
|
+
account_email: row.account_email ?? "",
|
|
1358
|
+
account_source: row.account_source ?? ""
|
|
1359
|
+
});
|
|
1360
|
+
if (!sessions.has(row.session_id)) {
|
|
1361
|
+
upsertSession(db, {
|
|
1362
|
+
id: row.session_id,
|
|
1363
|
+
agent: row.agent,
|
|
1364
|
+
project_path: row.project_path ?? "",
|
|
1365
|
+
project_name: row.project_name ?? row.repo ?? row.cost_center ?? "",
|
|
1366
|
+
started_at: row.timestamp,
|
|
1367
|
+
ended_at: null,
|
|
1368
|
+
total_cost_usd: 0,
|
|
1369
|
+
total_tokens: 0,
|
|
1370
|
+
request_count: 0,
|
|
1371
|
+
machine_id: machineId,
|
|
1372
|
+
cost_center_id: costCenterId2 ?? null,
|
|
1373
|
+
attribution_tag: row.attribution_tag ?? "otel",
|
|
1374
|
+
account_key: row.account_key ?? "",
|
|
1375
|
+
account_tool: row.account_tool ?? "",
|
|
1376
|
+
account_name: row.account_name ?? "",
|
|
1377
|
+
account_email: row.account_email ?? "",
|
|
1378
|
+
account_source: row.account_source ?? ""
|
|
1379
|
+
});
|
|
1380
|
+
sessions.add(row.session_id);
|
|
1381
|
+
}
|
|
1382
|
+
requests++;
|
|
1383
|
+
}
|
|
1384
|
+
for (const sessionId of sessions)
|
|
1385
|
+
rollupSession(db, sessionId);
|
|
1386
|
+
return { requests, sessions: sessions.size };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/lib/package-metadata.ts
|
|
1390
|
+
import { readFileSync } from "fs";
|
|
1391
|
+
var cachedMetadata = null;
|
|
1392
|
+
function getPackageMetadata() {
|
|
1393
|
+
if (cachedMetadata)
|
|
1394
|
+
return cachedMetadata;
|
|
1395
|
+
const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
|
|
1396
|
+
const parsed = JSON.parse(raw);
|
|
1397
|
+
cachedMetadata = {
|
|
1398
|
+
name: parsed.name ?? "@hasna/economy",
|
|
1399
|
+
version: parsed.version ?? "0.0.0"
|
|
1400
|
+
};
|
|
1401
|
+
return cachedMetadata;
|
|
1402
|
+
}
|
|
1403
|
+
var packageMetadata = getPackageMetadata();
|
|
1404
|
+
|
|
1405
|
+
// src/otel/index.ts
|
|
1406
|
+
function resolvePort(argv) {
|
|
1407
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1408
|
+
if (argv[i] === "--port" || argv[i] === "-p") {
|
|
1409
|
+
return Number(argv[i + 1] ?? 4318);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return Number(process.env["ECONOMY_OTEL_PORT"] ?? 4318);
|
|
1413
|
+
}
|
|
1414
|
+
var args = process.argv.slice(2);
|
|
1415
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1416
|
+
console.log(`Usage: economy-otel [options]
|
|
1417
|
+
|
|
1418
|
+
OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into economy.db
|
|
1419
|
+
|
|
1420
|
+
Options:
|
|
1421
|
+
-p, --port <port> Port to bind (default: ECONOMY_OTEL_PORT or 4318)
|
|
1422
|
+
-h, --help display help for command
|
|
1423
|
+
|
|
1424
|
+
Environment:
|
|
1425
|
+
ECONOMY_OTEL_PORT Override the default port (4318)
|
|
1426
|
+
ECONOMY_OTEL_BIND Bind address (default: 127.0.0.1)
|
|
1427
|
+
|
|
1428
|
+
Endpoints:
|
|
1429
|
+
POST /v1/metrics OTLP JSON metrics
|
|
1430
|
+
POST /ingest Simplified single-event JSON
|
|
1431
|
+
GET /health Health check`);
|
|
1432
|
+
process.exit(0);
|
|
1433
|
+
}
|
|
1434
|
+
var port = resolvePort(args);
|
|
1435
|
+
var db = openDatabase();
|
|
1436
|
+
var server = Bun.serve({
|
|
1437
|
+
port,
|
|
1438
|
+
hostname: process.env["ECONOMY_OTEL_BIND"] ?? "127.0.0.1",
|
|
1439
|
+
async fetch(req) {
|
|
1440
|
+
const url = new URL(req.url);
|
|
1441
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
1442
|
+
return Response.json({ status: "ok", service: "economy-otel", version: packageMetadata.version });
|
|
1443
|
+
}
|
|
1444
|
+
if (req.method !== "POST") {
|
|
1445
|
+
return Response.json({ error: "method not allowed" }, { status: 405 });
|
|
1446
|
+
}
|
|
1447
|
+
let body;
|
|
1448
|
+
try {
|
|
1449
|
+
body = await req.json();
|
|
1450
|
+
} catch {
|
|
1451
|
+
return Response.json({ error: "invalid JSON" }, { status: 400 });
|
|
1452
|
+
}
|
|
1453
|
+
let rows = url.pathname === "/ingest" ? (() => {
|
|
1454
|
+
const one = parseSimpleIngest(body);
|
|
1455
|
+
return one ? [one] : [];
|
|
1456
|
+
})() : parseOtlpMetrics(body);
|
|
1457
|
+
if (rows.length === 0) {
|
|
1458
|
+
return Response.json({ ingested: 0, message: "no matching metrics" });
|
|
1459
|
+
}
|
|
1460
|
+
const result = await ingestOtelRows(db, rows);
|
|
1461
|
+
return Response.json({ ingested: result.requests, sessions: result.sessions });
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
console.log(`economy-otel listening on http://127.0.0.1:${server.port}`);
|