@hasna/economy 0.2.20 → 0.2.22
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/LICENSE +1 -2
- package/README.md +5 -13
- 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.map +1 -1
- 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 +26 -0
- package/dist/cli/commands/todos.d.ts.map +1 -0
- package/dist/cli/commands/tui.d.ts +10 -0
- package/dist/cli/commands/tui.d.ts.map +1 -0
- package/dist/cli/commands/watch.d.ts +1 -0
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +5649 -708
- package/dist/db/database.d.ts +45 -3
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1576 -142
- 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 +13 -2
- package/dist/ingest/claude.d.ts.map +1 -1
- 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 +2 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts +6 -0
- package/dist/ingest/cursor.d.ts.map +1 -0
- package/dist/ingest/gemini.d.ts +2 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +6 -0
- package/dist/ingest/hermes.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 +20 -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/billing-diff.d.ts +22 -0
- package/dist/lib/billing-diff.d.ts.map +1 -0
- package/dist/lib/cloud-sync.d.ts +35 -0
- package/dist/lib/cloud-sync.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/model-config.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +19 -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/pricing.d.ts +3 -3
- package/dist/lib/pricing.d.ts.map +1 -1
- 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/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 +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +3063 -482
- package/dist/otel/index.d.ts +3 -0
- package/dist/otel/index.d.ts.map +1 -0
- package/dist/otel/index.js +1423 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +3550 -269
- package/dist/server/serve.d.ts +10 -2
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +102 -6
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +9 -4
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = import.meta.require;
|
|
17
18
|
|
|
18
19
|
// src/lib/pricing.ts
|
|
19
20
|
var exports_pricing = {};
|
|
@@ -27,88 +28,503 @@ __export(exports_pricing, {
|
|
|
27
28
|
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
28
29
|
});
|
|
29
30
|
function normalizeModelName(raw) {
|
|
30
|
-
return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "")
|
|
31
|
+
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
32
|
+
}
|
|
33
|
+
function normalizeModelNamePreservingProvider(raw) {
|
|
34
|
+
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
35
|
+
}
|
|
36
|
+
function modelLookupKeys(raw) {
|
|
37
|
+
const withProvider = normalizeModelNamePreservingProvider(raw);
|
|
38
|
+
const withoutProvider = normalizeModelName(raw);
|
|
39
|
+
return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
|
|
40
|
+
}
|
|
41
|
+
function bestPrefixMatch(normalized, entries) {
|
|
42
|
+
let best = null;
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const [key] = entry;
|
|
45
|
+
if (normalized !== key && !normalized.startsWith(`${key}-`))
|
|
46
|
+
continue;
|
|
47
|
+
if (!best || key.length > best[0].length)
|
|
48
|
+
best = entry;
|
|
49
|
+
}
|
|
50
|
+
return best?.[1] ?? null;
|
|
51
|
+
}
|
|
52
|
+
function bestModelMatch(model, entries) {
|
|
53
|
+
for (const key of modelLookupKeys(model)) {
|
|
54
|
+
const match = bestPrefixMatch(key, entries);
|
|
55
|
+
if (match)
|
|
56
|
+
return match;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function exactModelMatch(model, entries) {
|
|
61
|
+
for (const key of modelLookupKeys(model)) {
|
|
62
|
+
const match = entries.find(([entryKey]) => entryKey === key);
|
|
63
|
+
if (match)
|
|
64
|
+
return match[1];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
31
67
|
}
|
|
32
68
|
function ensurePricingSeeded(db) {
|
|
33
69
|
seedModelPricing(db, DEFAULT_PRICING);
|
|
70
|
+
repairLegacySeededPricing(db);
|
|
71
|
+
repairMissingDefaultCacheWrite1h(db);
|
|
72
|
+
repairMissingDefaultCacheStorage(db);
|
|
73
|
+
removeDeprecatedDefaultPricing(db);
|
|
74
|
+
}
|
|
75
|
+
function repairLegacySeededPricing(db) {
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
const legacyModels = new Set([
|
|
78
|
+
...Object.keys(LEGACY_DEFAULT_PRICING),
|
|
79
|
+
...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
|
|
80
|
+
]);
|
|
81
|
+
for (const model of legacyModels) {
|
|
82
|
+
const current = getModelPricing(db, model);
|
|
83
|
+
const next = DEFAULT_PRICING[model];
|
|
84
|
+
if (!current || !next)
|
|
85
|
+
continue;
|
|
86
|
+
const legacy = LEGACY_DEFAULT_PRICING[model];
|
|
87
|
+
const legacyRows = [
|
|
88
|
+
...legacy ? [legacy] : [],
|
|
89
|
+
...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
|
|
90
|
+
];
|
|
91
|
+
if (!legacyRows.some((row) => samePricing(current, row)))
|
|
92
|
+
continue;
|
|
93
|
+
upsertModelPricing(db, {
|
|
94
|
+
model,
|
|
95
|
+
input_per_1m: next.inputPer1M,
|
|
96
|
+
output_per_1m: next.outputPer1M,
|
|
97
|
+
cache_read_per_1m: next.cacheReadPer1M,
|
|
98
|
+
cache_write_per_1m: next.cacheWritePer1M,
|
|
99
|
+
cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
|
|
100
|
+
cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
|
|
101
|
+
updated_at: now
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function repairMissingDefaultCacheWrite1h(db) {
|
|
106
|
+
const now = new Date().toISOString();
|
|
107
|
+
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
108
|
+
if (!next.cacheWrite1hPer1M)
|
|
109
|
+
continue;
|
|
110
|
+
const current = getModelPricing(db, model);
|
|
111
|
+
if (!current)
|
|
112
|
+
continue;
|
|
113
|
+
if ((current.cache_write_1h_per_1m ?? 0) !== 0)
|
|
114
|
+
continue;
|
|
115
|
+
if (!sameBasePricing(current, next))
|
|
116
|
+
continue;
|
|
117
|
+
upsertModelPricing(db, {
|
|
118
|
+
model,
|
|
119
|
+
input_per_1m: current.input_per_1m,
|
|
120
|
+
output_per_1m: current.output_per_1m,
|
|
121
|
+
cache_read_per_1m: current.cache_read_per_1m,
|
|
122
|
+
cache_write_per_1m: current.cache_write_per_1m,
|
|
123
|
+
cache_write_1h_per_1m: next.cacheWrite1hPer1M,
|
|
124
|
+
cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
|
|
125
|
+
updated_at: now
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function repairMissingDefaultCacheStorage(db) {
|
|
130
|
+
const now = new Date().toISOString();
|
|
131
|
+
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
132
|
+
if (!next.cacheStoragePer1MHour)
|
|
133
|
+
continue;
|
|
134
|
+
const current = getModelPricing(db, model);
|
|
135
|
+
if (!current)
|
|
136
|
+
continue;
|
|
137
|
+
if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
|
|
138
|
+
continue;
|
|
139
|
+
if (!sameBasePricing(current, next))
|
|
140
|
+
continue;
|
|
141
|
+
upsertModelPricing(db, {
|
|
142
|
+
model,
|
|
143
|
+
input_per_1m: current.input_per_1m,
|
|
144
|
+
output_per_1m: current.output_per_1m,
|
|
145
|
+
cache_read_per_1m: current.cache_read_per_1m,
|
|
146
|
+
cache_write_per_1m: current.cache_write_per_1m,
|
|
147
|
+
cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
|
|
148
|
+
cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
|
|
149
|
+
updated_at: now
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function removeDeprecatedDefaultPricing(db) {
|
|
154
|
+
for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
|
|
155
|
+
const current = getModelPricing(db, model);
|
|
156
|
+
if (!current)
|
|
157
|
+
continue;
|
|
158
|
+
if (!removedRows.some((row) => samePricing(current, row)))
|
|
159
|
+
continue;
|
|
160
|
+
deleteModelPricing(db, model);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function sameBasePricing(row, pricing) {
|
|
164
|
+
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;
|
|
165
|
+
}
|
|
166
|
+
function samePricing(row, pricing) {
|
|
167
|
+
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);
|
|
34
168
|
}
|
|
35
169
|
function getPricingFromDb(db, model) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
43
|
-
cacheWritePer1M: row.cache_write_per_1m
|
|
44
|
-
};
|
|
170
|
+
if (isFreeModel(model))
|
|
171
|
+
return FREE_PRICING;
|
|
172
|
+
for (const key of modelLookupKeys(model)) {
|
|
173
|
+
const row = getModelPricing(db, key);
|
|
174
|
+
if (row)
|
|
175
|
+
return modelPricingFromDbRow(row);
|
|
45
176
|
}
|
|
46
177
|
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
178
|
+
const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
|
|
179
|
+
if (!match)
|
|
180
|
+
return null;
|
|
181
|
+
return modelPricingFromDbRow(match);
|
|
182
|
+
}
|
|
183
|
+
function modelPricingFromDbRow(row) {
|
|
184
|
+
const seeded = DEFAULT_PRICING[row.model];
|
|
185
|
+
const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
|
|
186
|
+
return {
|
|
187
|
+
inputPer1M: row.input_per_1m,
|
|
188
|
+
outputPer1M: row.output_per_1m,
|
|
189
|
+
cacheReadPer1M: row.cache_read_per_1m,
|
|
190
|
+
cacheWritePer1M: row.cache_write_per_1m,
|
|
191
|
+
cacheWrite1hPer1M,
|
|
192
|
+
cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
|
|
193
|
+
};
|
|
53
194
|
}
|
|
54
195
|
function getPricing(model) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
196
|
+
if (isFreeModel(model))
|
|
197
|
+
return FREE_PRICING;
|
|
198
|
+
return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
|
|
199
|
+
}
|
|
200
|
+
function isFreeModel(model) {
|
|
201
|
+
return model.trim().toLowerCase().endsWith(":free");
|
|
63
202
|
}
|
|
64
|
-
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
203
|
+
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
65
204
|
const pricing = getPricing(model);
|
|
66
205
|
if (!pricing)
|
|
67
206
|
return 0;
|
|
68
|
-
return (
|
|
207
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
69
208
|
}
|
|
70
|
-
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
209
|
+
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
71
210
|
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
72
211
|
if (!pricing)
|
|
73
212
|
return 0;
|
|
74
|
-
return (
|
|
213
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
75
214
|
}
|
|
76
|
-
|
|
215
|
+
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
216
|
+
if (isFreeModel(model))
|
|
217
|
+
return 0;
|
|
218
|
+
let effective = pricing;
|
|
219
|
+
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));
|
|
220
|
+
if (promptTier) {
|
|
221
|
+
const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
|
|
222
|
+
if (billablePromptTokens > promptTier.threshold) {
|
|
223
|
+
effective = {
|
|
224
|
+
...pricing,
|
|
225
|
+
inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
|
|
226
|
+
outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
|
|
227
|
+
cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
|
|
228
|
+
cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
233
|
+
}
|
|
234
|
+
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;
|
|
77
235
|
var init_pricing = __esm(() => {
|
|
78
236
|
init_database();
|
|
79
237
|
DEFAULT_PRICING = {
|
|
80
|
-
"claude-opus-4-
|
|
81
|
-
"claude-opus-4-
|
|
82
|
-
"claude-
|
|
83
|
-
"claude-
|
|
84
|
-
"claude-
|
|
85
|
-
"claude-
|
|
238
|
+
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
239
|
+
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
240
|
+
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
241
|
+
"claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
242
|
+
"claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
243
|
+
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
244
|
+
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
245
|
+
"claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
246
|
+
"claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
247
|
+
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
|
|
248
|
+
"claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
|
|
249
|
+
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
250
|
+
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
|
|
251
|
+
"gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
252
|
+
"gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
253
|
+
"gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
254
|
+
"gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
255
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
256
|
+
"gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
257
|
+
"gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
258
|
+
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
259
|
+
"gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
260
|
+
"google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
|
|
261
|
+
"google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
262
|
+
"google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
263
|
+
"google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
|
|
264
|
+
"google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
|
|
265
|
+
"google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
|
|
266
|
+
"google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
|
|
267
|
+
"gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
268
|
+
"gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
269
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
270
|
+
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
271
|
+
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
272
|
+
"gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
273
|
+
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
274
|
+
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
275
|
+
"gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
276
|
+
"gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
277
|
+
"gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
278
|
+
"gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
|
|
279
|
+
"gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
280
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
281
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
282
|
+
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
283
|
+
"o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
284
|
+
o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
285
|
+
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
286
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
|
|
287
|
+
"qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
288
|
+
"qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
289
|
+
"qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
290
|
+
"qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
291
|
+
"qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
292
|
+
"qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
293
|
+
"qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
294
|
+
"qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
295
|
+
"qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
296
|
+
"qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
297
|
+
"minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
298
|
+
"minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
299
|
+
"minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
300
|
+
"minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
301
|
+
"minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
302
|
+
"grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
303
|
+
"grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
304
|
+
"grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
305
|
+
"grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
306
|
+
"grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
307
|
+
"grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
308
|
+
"grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
309
|
+
"grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
310
|
+
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
311
|
+
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
|
|
312
|
+
"glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
|
|
313
|
+
"glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
314
|
+
"z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
|
|
315
|
+
"z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
|
|
316
|
+
"kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
|
|
317
|
+
"kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
|
|
318
|
+
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
319
|
+
"moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
320
|
+
"moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
|
|
321
|
+
"moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
322
|
+
};
|
|
323
|
+
LEGACY_DEFAULT_PRICING = {
|
|
86
324
|
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
87
|
-
"claude-
|
|
88
|
-
"
|
|
89
|
-
"
|
|
325
|
+
"claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
326
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
|
|
327
|
+
"gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
90
328
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
91
|
-
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
92
|
-
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
93
|
-
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
94
329
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
95
330
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
96
331
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
97
|
-
"gpt-
|
|
98
|
-
"gpt-
|
|
99
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
332
|
+
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
333
|
+
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
100
334
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
"
|
|
335
|
+
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
336
|
+
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
337
|
+
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
338
|
+
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
339
|
+
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
340
|
+
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
341
|
+
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
342
|
+
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
343
|
+
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
344
|
+
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
|
|
345
|
+
};
|
|
346
|
+
ADDITIONAL_LEGACY_DEFAULT_PRICING = {
|
|
347
|
+
"gemini-2.5-pro": [
|
|
348
|
+
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
349
|
+
],
|
|
350
|
+
"qwen3.6-plus": [
|
|
351
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
352
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
353
|
+
],
|
|
354
|
+
"qwen3.6-flash": [
|
|
355
|
+
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
356
|
+
],
|
|
357
|
+
"qwen3.6-max-preview": [
|
|
358
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
359
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
360
|
+
],
|
|
361
|
+
"qwen/qwen3.6-plus": [
|
|
362
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
363
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
364
|
+
],
|
|
365
|
+
"qwen/qwen3.6-flash": [
|
|
366
|
+
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
367
|
+
],
|
|
368
|
+
"qwen/qwen3.6-max-preview": [
|
|
369
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
370
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
371
|
+
]
|
|
372
|
+
};
|
|
373
|
+
REMOVED_DEFAULT_PRICING = {
|
|
374
|
+
"claude-3-5-sonnet": [
|
|
375
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
376
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
377
|
+
],
|
|
378
|
+
"claude-3-sonnet": [
|
|
379
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
380
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
381
|
+
],
|
|
382
|
+
"gemini-3.1-pro": [
|
|
383
|
+
{ inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
384
|
+
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
|
|
385
|
+
],
|
|
386
|
+
"gemini-1.5-pro": [
|
|
387
|
+
{ inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
388
|
+
],
|
|
389
|
+
"gemini-1.5-flash": [
|
|
390
|
+
{ inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
391
|
+
],
|
|
392
|
+
"gpt-5.3-chat": [
|
|
393
|
+
{ inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
394
|
+
{ inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
|
|
395
|
+
],
|
|
396
|
+
"qwen3.6": [
|
|
397
|
+
{ inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
FREE_PRICING = {
|
|
401
|
+
inputPer1M: 0,
|
|
402
|
+
outputPer1M: 0,
|
|
403
|
+
cacheReadPer1M: 0,
|
|
404
|
+
cacheWritePer1M: 0,
|
|
405
|
+
cacheWrite1hPer1M: 0,
|
|
406
|
+
cacheStoragePer1MHour: 0
|
|
407
|
+
};
|
|
408
|
+
GEMINI_PROMPT_TIERS = {
|
|
409
|
+
"gemini-3.1-pro-preview": {
|
|
410
|
+
threshold: 200000,
|
|
411
|
+
inputPer1M: 4,
|
|
412
|
+
outputPer1M: 18,
|
|
413
|
+
cacheReadPer1M: 0.4
|
|
414
|
+
},
|
|
415
|
+
"gemini-2.5-pro": {
|
|
416
|
+
threshold: 200000,
|
|
417
|
+
inputPer1M: 2.5,
|
|
418
|
+
outputPer1M: 15,
|
|
419
|
+
cacheReadPer1M: 0.25
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
OPENAI_PROMPT_TIERS = {
|
|
423
|
+
"gpt-5.5": {
|
|
424
|
+
threshold: 272000,
|
|
425
|
+
inputMultiplier: 2,
|
|
426
|
+
outputMultiplier: 1.5,
|
|
427
|
+
cacheReadMultiplier: 2
|
|
428
|
+
},
|
|
429
|
+
"gpt-5.4-pro": {
|
|
430
|
+
threshold: 272000,
|
|
431
|
+
inputMultiplier: 2,
|
|
432
|
+
outputMultiplier: 1.5,
|
|
433
|
+
cacheReadMultiplier: 2
|
|
434
|
+
},
|
|
435
|
+
"gpt-5.4": {
|
|
436
|
+
threshold: 272000,
|
|
437
|
+
inputMultiplier: 2,
|
|
438
|
+
outputMultiplier: 1.5,
|
|
439
|
+
cacheReadMultiplier: 2
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
QWEN_PROMPT_TIERS = {
|
|
443
|
+
"qwen3.6-plus": {
|
|
444
|
+
threshold: 256000,
|
|
445
|
+
inputPer1M: 1.3,
|
|
446
|
+
outputPer1M: 3.9,
|
|
447
|
+
cacheReadPer1M: 0.13,
|
|
448
|
+
cacheWritePer1M: 1.625
|
|
449
|
+
},
|
|
450
|
+
"qwen3.6-flash": {
|
|
451
|
+
threshold: 256000,
|
|
452
|
+
inputPer1M: 1,
|
|
453
|
+
outputPer1M: 4,
|
|
454
|
+
cacheReadPer1M: 0.1,
|
|
455
|
+
cacheWritePer1M: 1.25
|
|
456
|
+
},
|
|
457
|
+
"qwen3.6-max-preview": {
|
|
458
|
+
threshold: 128000,
|
|
459
|
+
inputPer1M: 1.6,
|
|
460
|
+
outputPer1M: 9.6,
|
|
461
|
+
cacheReadPer1M: 0.16,
|
|
462
|
+
cacheWritePer1M: 2
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
MINIMAX_PROMPT_TIERS = {
|
|
466
|
+
"minimax/minimax-m1": {
|
|
467
|
+
threshold: Number.POSITIVE_INFINITY
|
|
468
|
+
},
|
|
469
|
+
"minimax-m1": {
|
|
470
|
+
threshold: 200000,
|
|
471
|
+
inputPer1M: 1.3
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
XAI_PROMPT_TIERS = {
|
|
475
|
+
"grok-4.3": {
|
|
476
|
+
threshold: 200000,
|
|
477
|
+
inputPer1M: 2.5,
|
|
478
|
+
outputPer1M: 5,
|
|
479
|
+
cacheReadPer1M: 0.4
|
|
480
|
+
},
|
|
481
|
+
"grok-latest": {
|
|
482
|
+
threshold: 200000,
|
|
483
|
+
inputPer1M: 2.5,
|
|
484
|
+
outputPer1M: 5,
|
|
485
|
+
cacheReadPer1M: 0.4
|
|
486
|
+
},
|
|
487
|
+
"grok-4.20": {
|
|
488
|
+
threshold: 200000,
|
|
489
|
+
inputPer1M: 2.5,
|
|
490
|
+
outputPer1M: 5,
|
|
491
|
+
cacheReadPer1M: 0.4
|
|
492
|
+
},
|
|
493
|
+
"grok-4-1-fast": {
|
|
494
|
+
threshold: 128000,
|
|
495
|
+
inputPer1M: 0.4,
|
|
496
|
+
outputPer1M: 1,
|
|
497
|
+
cacheReadPer1M: 0
|
|
498
|
+
},
|
|
499
|
+
"grok-4-fast": {
|
|
500
|
+
threshold: 128000,
|
|
501
|
+
inputPer1M: 0.4,
|
|
502
|
+
outputPer1M: 1,
|
|
503
|
+
cacheReadPer1M: 0
|
|
504
|
+
},
|
|
505
|
+
"grok-4": {
|
|
506
|
+
threshold: 128000,
|
|
507
|
+
inputPer1M: 6,
|
|
508
|
+
outputPer1M: 30,
|
|
509
|
+
cacheReadPer1M: 0
|
|
510
|
+
}
|
|
104
511
|
};
|
|
105
512
|
});
|
|
106
513
|
|
|
107
514
|
// src/db/database.ts
|
|
108
515
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
109
516
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
517
|
+
import { hostname } from "os";
|
|
110
518
|
import { homedir } from "os";
|
|
111
519
|
import { join } from "path";
|
|
520
|
+
function getMachineId() {
|
|
521
|
+
if (process.env["ECONOMY_MACHINE_ID"])
|
|
522
|
+
return process.env["ECONOMY_MACHINE_ID"];
|
|
523
|
+
const h = hostname().toLowerCase();
|
|
524
|
+
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
525
|
+
return h.split(".")[0];
|
|
526
|
+
return h.split(".")[0];
|
|
527
|
+
}
|
|
112
528
|
function getDataDir() {
|
|
113
529
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
114
530
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -141,6 +557,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
141
557
|
}
|
|
142
558
|
const db = new Database(path);
|
|
143
559
|
db.exec("PRAGMA journal_mode = WAL");
|
|
560
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
144
561
|
db.exec("PRAGMA foreign_keys = ON");
|
|
145
562
|
initSchema(db);
|
|
146
563
|
if (!skipSeed) {
|
|
@@ -159,10 +576,18 @@ function initSchema(db) {
|
|
|
159
576
|
output_tokens INTEGER DEFAULT 0,
|
|
160
577
|
cache_read_tokens INTEGER DEFAULT 0,
|
|
161
578
|
cache_create_tokens INTEGER DEFAULT 0,
|
|
579
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
580
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
162
581
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
163
582
|
duration_ms INTEGER DEFAULT 0,
|
|
164
583
|
timestamp TEXT NOT NULL,
|
|
165
|
-
source_request_id TEXT
|
|
584
|
+
source_request_id TEXT,
|
|
585
|
+
machine_id TEXT DEFAULT '',
|
|
586
|
+
account_key TEXT DEFAULT '',
|
|
587
|
+
account_tool TEXT DEFAULT '',
|
|
588
|
+
account_name TEXT DEFAULT '',
|
|
589
|
+
account_email TEXT DEFAULT '',
|
|
590
|
+
account_source TEXT DEFAULT ''
|
|
166
591
|
);
|
|
167
592
|
|
|
168
593
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -174,7 +599,13 @@ function initSchema(db) {
|
|
|
174
599
|
ended_at TEXT,
|
|
175
600
|
total_cost_usd REAL DEFAULT 0,
|
|
176
601
|
total_tokens INTEGER DEFAULT 0,
|
|
177
|
-
request_count INTEGER DEFAULT 0
|
|
602
|
+
request_count INTEGER DEFAULT 0,
|
|
603
|
+
machine_id TEXT DEFAULT '',
|
|
604
|
+
account_key TEXT DEFAULT '',
|
|
605
|
+
account_tool TEXT DEFAULT '',
|
|
606
|
+
account_name TEXT DEFAULT '',
|
|
607
|
+
account_email TEXT DEFAULT '',
|
|
608
|
+
account_source TEXT DEFAULT ''
|
|
178
609
|
);
|
|
179
610
|
|
|
180
611
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -227,6 +658,8 @@ function initSchema(db) {
|
|
|
227
658
|
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
228
659
|
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
229
660
|
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
661
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
662
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
230
663
|
updated_at TEXT NOT NULL
|
|
231
664
|
);
|
|
232
665
|
|
|
@@ -239,6 +672,127 @@ function initSchema(db) {
|
|
|
239
672
|
machine_id TEXT,
|
|
240
673
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
241
674
|
);
|
|
675
|
+
|
|
676
|
+
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
677
|
+
date TEXT NOT NULL,
|
|
678
|
+
provider TEXT NOT NULL,
|
|
679
|
+
description TEXT DEFAULT '',
|
|
680
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
681
|
+
updated_at TEXT NOT NULL,
|
|
682
|
+
PRIMARY KEY (date, provider, description)
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
686
|
+
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
687
|
+
|
|
688
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
689
|
+
id TEXT PRIMARY KEY,
|
|
690
|
+
agent TEXT,
|
|
691
|
+
provider TEXT NOT NULL,
|
|
692
|
+
plan TEXT NOT NULL,
|
|
693
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
694
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
695
|
+
billing_cycle_start TEXT,
|
|
696
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
697
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
698
|
+
created_at TEXT NOT NULL,
|
|
699
|
+
updated_at TEXT NOT NULL
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
703
|
+
id TEXT PRIMARY KEY,
|
|
704
|
+
agent TEXT NOT NULL,
|
|
705
|
+
date TEXT NOT NULL,
|
|
706
|
+
metric TEXT NOT NULL,
|
|
707
|
+
value REAL NOT NULL DEFAULT 0,
|
|
708
|
+
unit TEXT DEFAULT '',
|
|
709
|
+
machine_id TEXT DEFAULT '',
|
|
710
|
+
updated_at TEXT NOT NULL
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
714
|
+
date TEXT NOT NULL,
|
|
715
|
+
agent TEXT DEFAULT '',
|
|
716
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
717
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
718
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
719
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
720
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
721
|
+
updated_at TEXT NOT NULL,
|
|
722
|
+
PRIMARY KEY (date, agent)
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
726
|
+
machine_id TEXT PRIMARY KEY,
|
|
727
|
+
hostname TEXT NOT NULL,
|
|
728
|
+
last_seen_at TEXT,
|
|
729
|
+
last_push_at TEXT,
|
|
730
|
+
last_pull_at TEXT,
|
|
731
|
+
economy_version TEXT,
|
|
732
|
+
updated_at TEXT NOT NULL
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
736
|
+
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
737
|
+
`);
|
|
738
|
+
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
739
|
+
if (!cols.some((c) => c.name === "machine_id")) {
|
|
740
|
+
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
741
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
742
|
+
}
|
|
743
|
+
if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
|
|
744
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
|
|
745
|
+
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
746
|
+
}
|
|
747
|
+
if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
|
|
748
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
|
|
749
|
+
}
|
|
750
|
+
if (!cols.some((c) => c.name === "cost_basis")) {
|
|
751
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
|
|
752
|
+
}
|
|
753
|
+
if (!cols.some((c) => c.name === "attribution_tag")) {
|
|
754
|
+
db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
755
|
+
}
|
|
756
|
+
if (!cols.some((c) => c.name === "updated_at")) {
|
|
757
|
+
db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
758
|
+
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
759
|
+
}
|
|
760
|
+
if (!cols.some((c) => c.name === "synced_at")) {
|
|
761
|
+
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
762
|
+
}
|
|
763
|
+
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
764
|
+
if (!cols.some((c) => c.name === column)) {
|
|
765
|
+
db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
769
|
+
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
770
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
771
|
+
}
|
|
772
|
+
if (!sessionCols.some((c) => c.name === "updated_at")) {
|
|
773
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
774
|
+
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
775
|
+
}
|
|
776
|
+
if (!sessionCols.some((c) => c.name === "synced_at")) {
|
|
777
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
778
|
+
}
|
|
779
|
+
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
780
|
+
if (!sessionCols.some((c) => c.name === column)) {
|
|
781
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
785
|
+
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
786
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
787
|
+
}
|
|
788
|
+
if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
|
|
789
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
|
|
790
|
+
}
|
|
791
|
+
db.exec(`
|
|
792
|
+
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
793
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
794
|
+
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
795
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
242
796
|
`);
|
|
243
797
|
}
|
|
244
798
|
function periodWhere(period) {
|
|
@@ -248,11 +802,11 @@ function periodWhere(period) {
|
|
|
248
802
|
case "yesterday":
|
|
249
803
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
250
804
|
case "week":
|
|
251
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
805
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
252
806
|
case "month":
|
|
253
|
-
return `timestamp >= DATE('now', '
|
|
807
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
254
808
|
case "year":
|
|
255
|
-
return `timestamp >= DATE('now', '
|
|
809
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
256
810
|
case "all":
|
|
257
811
|
return "1=1";
|
|
258
812
|
}
|
|
@@ -264,31 +818,52 @@ function sessionPeriodWhere(period) {
|
|
|
264
818
|
case "yesterday":
|
|
265
819
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
266
820
|
case "week":
|
|
267
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
821
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
268
822
|
case "month":
|
|
269
|
-
return `started_at >= DATE('now', '
|
|
823
|
+
return `started_at >= DATE('now', 'start of month')`;
|
|
270
824
|
case "year":
|
|
271
|
-
return `started_at >= DATE('now', '
|
|
825
|
+
return `started_at >= DATE('now', 'start of year')`;
|
|
826
|
+
case "all":
|
|
827
|
+
return "1=1";
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function requestPeriodWhere(period) {
|
|
831
|
+
switch (period) {
|
|
832
|
+
case "today":
|
|
833
|
+
return `DATE(timestamp) = DATE('now')`;
|
|
834
|
+
case "yesterday":
|
|
835
|
+
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
836
|
+
case "week":
|
|
837
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
838
|
+
case "month":
|
|
839
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
840
|
+
case "year":
|
|
841
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
272
842
|
case "all":
|
|
273
843
|
return "1=1";
|
|
274
844
|
}
|
|
275
845
|
}
|
|
276
846
|
function upsertRequest(db, req) {
|
|
847
|
+
const now = req.updated_at ?? new Date().toISOString();
|
|
277
848
|
db.prepare(`
|
|
278
849
|
INSERT OR REPLACE INTO requests
|
|
279
850
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
280
|
-
cache_read_tokens, cache_create_tokens,
|
|
281
|
-
timestamp,
|
|
282
|
-
|
|
283
|
-
|
|
851
|
+
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
852
|
+
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
853
|
+
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
854
|
+
account_name, account_email, account_source, updated_at, synced_at)
|
|
855
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
856
|
+
`).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.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 ?? "");
|
|
284
857
|
}
|
|
285
858
|
function upsertSession(db, session) {
|
|
859
|
+
const now = session.updated_at ?? new Date().toISOString();
|
|
286
860
|
db.prepare(`
|
|
287
861
|
INSERT OR REPLACE INTO sessions
|
|
288
862
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
289
|
-
total_cost_usd, total_tokens, request_count
|
|
290
|
-
|
|
291
|
-
|
|
863
|
+
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
864
|
+
account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
865
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
866
|
+
`).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.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
|
|
292
867
|
}
|
|
293
868
|
function rollupSession(db, sessionId) {
|
|
294
869
|
db.prepare(`
|
|
@@ -299,9 +874,24 @@ function rollupSession(db, sessionId) {
|
|
|
299
874
|
ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
|
|
300
875
|
started_at = CASE WHEN started_at = '' OR started_at IS NULL
|
|
301
876
|
THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
|
|
302
|
-
ELSE started_at END
|
|
877
|
+
ELSE started_at END,
|
|
878
|
+
account_key = CASE WHEN account_key = '' OR account_key IS NULL
|
|
879
|
+
THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
880
|
+
ELSE account_key END,
|
|
881
|
+
account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
|
|
882
|
+
THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
883
|
+
ELSE account_tool END,
|
|
884
|
+
account_name = CASE WHEN account_name = '' OR account_name IS NULL
|
|
885
|
+
THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
886
|
+
ELSE account_name END,
|
|
887
|
+
account_email = CASE WHEN account_email = '' OR account_email IS NULL
|
|
888
|
+
THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
889
|
+
ELSE account_email END,
|
|
890
|
+
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
891
|
+
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
892
|
+
ELSE account_source END
|
|
303
893
|
WHERE id = ?
|
|
304
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
894
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
305
895
|
}
|
|
306
896
|
function querySessions(db, filter = {}) {
|
|
307
897
|
const conditions = [];
|
|
@@ -314,10 +904,19 @@ function querySessions(db, filter = {}) {
|
|
|
314
904
|
conditions.push("project_path LIKE ?");
|
|
315
905
|
params.push(`%${filter.project}%`);
|
|
316
906
|
}
|
|
907
|
+
if (filter.account) {
|
|
908
|
+
const q = `%${filter.account}%`;
|
|
909
|
+
conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
|
|
910
|
+
params.push(q, q, q);
|
|
911
|
+
}
|
|
317
912
|
if (filter.since) {
|
|
318
913
|
conditions.push("started_at >= ?");
|
|
319
914
|
params.push(filter.since);
|
|
320
915
|
}
|
|
916
|
+
if (filter.machine) {
|
|
917
|
+
conditions.push("machine_id = ?");
|
|
918
|
+
params.push(filter.machine);
|
|
919
|
+
}
|
|
321
920
|
if (filter.search) {
|
|
322
921
|
const q = `%${filter.search}%`;
|
|
323
922
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -336,24 +935,25 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
336
935
|
}
|
|
337
936
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
338
937
|
}
|
|
339
|
-
function querySummary(db, period) {
|
|
938
|
+
function querySummary(db, period, machine, allMachines = false) {
|
|
340
939
|
const rWhere = periodWhere(period);
|
|
341
940
|
const sWhere = sessionPeriodWhere(period);
|
|
941
|
+
const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
342
942
|
const r = db.prepare(`
|
|
343
943
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
344
944
|
COUNT(*) as requests,
|
|
345
945
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
346
|
-
FROM requests WHERE ${rWhere}
|
|
946
|
+
FROM requests WHERE ${rWhere}${machineClause}
|
|
347
947
|
`).get();
|
|
348
948
|
const codexTotals = db.prepare(`
|
|
349
949
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
350
950
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
351
951
|
COUNT(*) as sessions
|
|
352
952
|
FROM sessions
|
|
353
|
-
WHERE ${sWhere}
|
|
953
|
+
WHERE ${sWhere}${machineClause}
|
|
354
954
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
355
955
|
`).get();
|
|
356
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
956
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
|
|
357
957
|
return {
|
|
358
958
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
359
959
|
requests: r.requests,
|
|
@@ -373,23 +973,213 @@ function queryModelBreakdown(db) {
|
|
|
373
973
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
374
974
|
`).all();
|
|
375
975
|
}
|
|
376
|
-
function
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
976
|
+
function queryAgentBreakdown(db, period = "all") {
|
|
977
|
+
const requestWhere = requestPeriodWhere(period);
|
|
978
|
+
const groups = new Map;
|
|
979
|
+
const requestRows = db.prepare(`
|
|
980
|
+
SELECT agent,
|
|
981
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
982
|
+
COUNT(*) as requests,
|
|
983
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
984
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
985
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
986
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
987
|
+
COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
988
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
989
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
990
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
991
|
+
MAX(timestamp) as last_active
|
|
992
|
+
FROM requests
|
|
993
|
+
WHERE ${requestWhere}
|
|
994
|
+
GROUP BY agent
|
|
995
|
+
ORDER BY api_equivalent_usd DESC
|
|
996
|
+
`).all();
|
|
997
|
+
for (const row of requestRows) {
|
|
998
|
+
groups.set(row.agent, row);
|
|
999
|
+
}
|
|
1000
|
+
const sessionWhere = sessionPeriodWhere(period);
|
|
1001
|
+
const sessionOnlyRows = db.prepare(`
|
|
1002
|
+
SELECT agent,
|
|
1003
|
+
COUNT(*) as sessions,
|
|
1004
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1005
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1006
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1007
|
+
MAX(started_at) as last_active
|
|
1008
|
+
FROM sessions
|
|
1009
|
+
WHERE ${sessionWhere}
|
|
1010
|
+
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1011
|
+
GROUP BY agent
|
|
1012
|
+
`).all();
|
|
1013
|
+
for (const row of sessionOnlyRows) {
|
|
1014
|
+
const existing = groups.get(row.agent) ?? {
|
|
1015
|
+
agent: row.agent,
|
|
1016
|
+
sessions: 0,
|
|
1017
|
+
requests: 0,
|
|
1018
|
+
total_tokens: 0,
|
|
1019
|
+
api_equivalent_usd: 0,
|
|
1020
|
+
billable_usd: 0,
|
|
1021
|
+
metered_api_usd: 0,
|
|
1022
|
+
subscription_included_usd: 0,
|
|
1023
|
+
estimated_usd: 0,
|
|
1024
|
+
unknown_usd: 0,
|
|
1025
|
+
cost_usd: 0,
|
|
1026
|
+
last_active: ""
|
|
1027
|
+
};
|
|
1028
|
+
existing.sessions += row.sessions;
|
|
1029
|
+
existing.requests += row.requests;
|
|
1030
|
+
existing.total_tokens += row.total_tokens;
|
|
1031
|
+
existing.api_equivalent_usd += row.cost_usd;
|
|
1032
|
+
existing.estimated_usd += row.cost_usd;
|
|
1033
|
+
existing.cost_usd += row.cost_usd;
|
|
1034
|
+
if (!existing.last_active || row.last_active > existing.last_active)
|
|
1035
|
+
existing.last_active = row.last_active;
|
|
1036
|
+
groups.set(row.agent, existing);
|
|
1037
|
+
}
|
|
1038
|
+
return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
|
|
1039
|
+
}
|
|
1040
|
+
function labelForPath(projectPath, projectName) {
|
|
1041
|
+
if (projectName && projectName.trim() !== "")
|
|
1042
|
+
return projectName;
|
|
1043
|
+
if (!projectPath)
|
|
1044
|
+
return "";
|
|
1045
|
+
const segments = projectPath.split("/").filter(Boolean);
|
|
1046
|
+
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
1047
|
+
for (const seg of segments) {
|
|
1048
|
+
if (projectPrefix.test(seg))
|
|
1049
|
+
return seg;
|
|
1050
|
+
}
|
|
1051
|
+
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
1052
|
+
for (let i = segments.length - 1;i >= 0; i--) {
|
|
1053
|
+
if (!generic.has(segments[i].toLowerCase()))
|
|
1054
|
+
return segments[i];
|
|
1055
|
+
}
|
|
1056
|
+
return segments[segments.length - 1] ?? projectPath;
|
|
1057
|
+
}
|
|
1058
|
+
function queryProjectBreakdown(db, period = "all") {
|
|
1059
|
+
const where = sessionPeriodWhere(period);
|
|
1060
|
+
const sessions = db.prepare(`
|
|
1061
|
+
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
1062
|
+
FROM sessions
|
|
1063
|
+
WHERE ${where}
|
|
1064
|
+
AND (project_path != '' OR project_name != '')
|
|
392
1065
|
`).all();
|
|
1066
|
+
const groups = new Map;
|
|
1067
|
+
for (const s of sessions) {
|
|
1068
|
+
const label = labelForPath(s.project_path, s.project_name);
|
|
1069
|
+
if (!label)
|
|
1070
|
+
continue;
|
|
1071
|
+
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
1072
|
+
g.sessionIds.push(s.id);
|
|
1073
|
+
g.totalCost += s.total_cost_usd || 0;
|
|
1074
|
+
if (!g.lastActive || s.started_at > g.lastActive)
|
|
1075
|
+
g.lastActive = s.started_at;
|
|
1076
|
+
if (!g.samplePath)
|
|
1077
|
+
g.samplePath = s.project_path;
|
|
1078
|
+
groups.set(label, g);
|
|
1079
|
+
}
|
|
1080
|
+
const result = [];
|
|
1081
|
+
for (const [label, g] of groups.entries()) {
|
|
1082
|
+
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
1083
|
+
const reqStats = placeholders.length ? db.prepare(`
|
|
1084
|
+
SELECT
|
|
1085
|
+
COUNT(*) as requests,
|
|
1086
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1087
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
1088
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
1089
|
+
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
1090
|
+
result.push({
|
|
1091
|
+
project_path: g.samplePath,
|
|
1092
|
+
project_name: label,
|
|
1093
|
+
sessions: g.sessionIds.length,
|
|
1094
|
+
requests: reqStats.requests,
|
|
1095
|
+
total_tokens: reqStats.total_tokens,
|
|
1096
|
+
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
1097
|
+
last_active: g.lastActive
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1101
|
+
return result;
|
|
1102
|
+
}
|
|
1103
|
+
function queryAccountBreakdown(db, period = "all") {
|
|
1104
|
+
const sWhere = sessionPeriodWhere(period);
|
|
1105
|
+
const sessions = db.prepare(`
|
|
1106
|
+
SELECT id, account_key, account_tool, account_name, account_email, account_source,
|
|
1107
|
+
total_cost_usd, total_tokens, request_count, started_at
|
|
1108
|
+
FROM sessions
|
|
1109
|
+
WHERE ${sWhere}
|
|
1110
|
+
AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
|
|
1111
|
+
`).all();
|
|
1112
|
+
const groups = new Map;
|
|
1113
|
+
for (const session of sessions) {
|
|
1114
|
+
const key = session.account_key || `${session.account_tool}:${session.account_name}`;
|
|
1115
|
+
if (!key || key === ":")
|
|
1116
|
+
continue;
|
|
1117
|
+
const group = groups.get(key) ?? {
|
|
1118
|
+
sessionIds: [],
|
|
1119
|
+
account_tool: session.account_tool,
|
|
1120
|
+
account_name: session.account_name,
|
|
1121
|
+
account_email: session.account_email || null,
|
|
1122
|
+
account_source: session.account_source || "unknown",
|
|
1123
|
+
totalCost: 0,
|
|
1124
|
+
totalTokens: 0,
|
|
1125
|
+
requests: 0,
|
|
1126
|
+
lastActive: ""
|
|
1127
|
+
};
|
|
1128
|
+
group.sessionIds.push(session.id);
|
|
1129
|
+
group.totalCost += session.total_cost_usd || 0;
|
|
1130
|
+
group.totalTokens += session.total_tokens || 0;
|
|
1131
|
+
group.requests += session.request_count || 0;
|
|
1132
|
+
if (!group.lastActive || session.started_at > group.lastActive)
|
|
1133
|
+
group.lastActive = session.started_at;
|
|
1134
|
+
groups.set(key, group);
|
|
1135
|
+
}
|
|
1136
|
+
const result = [];
|
|
1137
|
+
for (const [key, group] of groups.entries()) {
|
|
1138
|
+
const placeholders = group.sessionIds.map(() => "?").join(",");
|
|
1139
|
+
const reqStats = placeholders ? db.prepare(`
|
|
1140
|
+
SELECT
|
|
1141
|
+
COUNT(*) as requests,
|
|
1142
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1143
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1144
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1145
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1146
|
+
COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1147
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
|
|
1148
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
1149
|
+
`).get(...group.sessionIds) : {
|
|
1150
|
+
requests: 0,
|
|
1151
|
+
cost_usd: 0,
|
|
1152
|
+
total_tokens: 0,
|
|
1153
|
+
metered_api_usd: 0,
|
|
1154
|
+
subscription_included_usd: 0,
|
|
1155
|
+
estimated_usd: 0,
|
|
1156
|
+
unknown_usd: 0
|
|
1157
|
+
};
|
|
1158
|
+
const hasRequestCosts = reqStats.requests > 0;
|
|
1159
|
+
const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
|
|
1160
|
+
const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
|
|
1161
|
+
const billableUsd = reqStats.metered_api_usd;
|
|
1162
|
+
result.push({
|
|
1163
|
+
account_key: key,
|
|
1164
|
+
account_tool: group.account_tool,
|
|
1165
|
+
account_name: group.account_name,
|
|
1166
|
+
account_email: group.account_email,
|
|
1167
|
+
account_source: group.account_source,
|
|
1168
|
+
sessions: group.sessionIds.length,
|
|
1169
|
+
requests: reqStats.requests || group.requests,
|
|
1170
|
+
total_tokens: reqStats.total_tokens || group.totalTokens,
|
|
1171
|
+
api_equivalent_usd: apiEquivalentUsd,
|
|
1172
|
+
billable_usd: billableUsd,
|
|
1173
|
+
metered_api_usd: reqStats.metered_api_usd,
|
|
1174
|
+
subscription_included_usd: reqStats.subscription_included_usd,
|
|
1175
|
+
estimated_usd: estimatedUsd,
|
|
1176
|
+
unknown_usd: reqStats.unknown_usd,
|
|
1177
|
+
cost_usd: apiEquivalentUsd,
|
|
1178
|
+
last_active: group.lastActive
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1182
|
+
return result;
|
|
393
1183
|
}
|
|
394
1184
|
function queryDailyBreakdown(db, days = 30) {
|
|
395
1185
|
return db.prepare(`
|
|
@@ -507,12 +1297,46 @@ function setIngestState(db, source, key, value) {
|
|
|
507
1297
|
function queryRequestsSince(db, since) {
|
|
508
1298
|
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
509
1299
|
}
|
|
1300
|
+
function upsertBillingDaily(db, row) {
|
|
1301
|
+
db.prepare(`
|
|
1302
|
+
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
1303
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1304
|
+
`).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
|
|
1305
|
+
}
|
|
1306
|
+
function clearBillingRange(db, provider, fromDate, toDate) {
|
|
1307
|
+
db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
|
|
1308
|
+
}
|
|
1309
|
+
function queryBillingSummary(db, period) {
|
|
1310
|
+
const where = 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";
|
|
1311
|
+
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
1312
|
+
const by_provider = {};
|
|
1313
|
+
let total = 0;
|
|
1314
|
+
for (const r of rows) {
|
|
1315
|
+
by_provider[r.provider] = r.cost;
|
|
1316
|
+
total += r.cost;
|
|
1317
|
+
}
|
|
1318
|
+
return { total_usd: total, by_provider };
|
|
1319
|
+
}
|
|
1320
|
+
function listMachines(db) {
|
|
1321
|
+
return db.prepare(`
|
|
1322
|
+
SELECT
|
|
1323
|
+
s.machine_id,
|
|
1324
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
1325
|
+
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
1326
|
+
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
1327
|
+
MAX(s.started_at) as last_active
|
|
1328
|
+
FROM sessions s
|
|
1329
|
+
WHERE s.machine_id != ''
|
|
1330
|
+
GROUP BY s.machine_id
|
|
1331
|
+
ORDER BY total_cost_usd DESC
|
|
1332
|
+
`).all();
|
|
1333
|
+
}
|
|
510
1334
|
function upsertModelPricing(db, p) {
|
|
511
1335
|
db.prepare(`
|
|
512
1336
|
INSERT OR REPLACE INTO model_pricing
|
|
513
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
|
|
514
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
515
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
|
|
1337
|
+
(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)
|
|
1338
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1339
|
+
`).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);
|
|
516
1340
|
}
|
|
517
1341
|
function getModelPricing(db, model) {
|
|
518
1342
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
@@ -524,23 +1348,104 @@ function deleteModelPricing(db, model) {
|
|
|
524
1348
|
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
525
1349
|
}
|
|
526
1350
|
function seedModelPricing(db, defaults) {
|
|
527
|
-
const existing = db.prepare(`SELECT
|
|
528
|
-
if (existing.count > 0)
|
|
529
|
-
return;
|
|
1351
|
+
const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
|
|
530
1352
|
const now = new Date().toISOString();
|
|
531
1353
|
for (const [model, p] of Object.entries(defaults)) {
|
|
1354
|
+
if (existing.has(model))
|
|
1355
|
+
continue;
|
|
532
1356
|
upsertModelPricing(db, {
|
|
533
1357
|
model,
|
|
534
1358
|
input_per_1m: p.inputPer1M,
|
|
535
1359
|
output_per_1m: p.outputPer1M,
|
|
536
1360
|
cache_read_per_1m: p.cacheReadPer1M,
|
|
537
1361
|
cache_write_per_1m: p.cacheWritePer1M,
|
|
1362
|
+
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1363
|
+
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
538
1364
|
updated_at: now
|
|
539
1365
|
});
|
|
540
1366
|
}
|
|
541
1367
|
}
|
|
1368
|
+
function upsertSubscription(db, sub) {
|
|
1369
|
+
db.prepare(`
|
|
1370
|
+
INSERT OR REPLACE INTO subscriptions
|
|
1371
|
+
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
1372
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1373
|
+
`).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);
|
|
1374
|
+
}
|
|
1375
|
+
function listSubscriptions(db) {
|
|
1376
|
+
return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
|
|
1377
|
+
}
|
|
1378
|
+
function deleteSubscription(db, id) {
|
|
1379
|
+
db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
|
|
1380
|
+
}
|
|
1381
|
+
function upsertUsageSnapshot(db, snap) {
|
|
1382
|
+
const now = snap.updated_at ?? new Date().toISOString();
|
|
1383
|
+
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
1384
|
+
db.prepare(`
|
|
1385
|
+
INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
|
|
1386
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1387
|
+
`).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
|
|
1388
|
+
}
|
|
1389
|
+
function queryUsageSnapshots(db, opts = {}) {
|
|
1390
|
+
const conditions = [];
|
|
1391
|
+
const params = [];
|
|
1392
|
+
if (opts.agent) {
|
|
1393
|
+
conditions.push("agent = ?");
|
|
1394
|
+
params.push(opts.agent);
|
|
1395
|
+
}
|
|
1396
|
+
if (opts.date) {
|
|
1397
|
+
conditions.push("date = ?");
|
|
1398
|
+
params.push(opts.date);
|
|
1399
|
+
}
|
|
1400
|
+
if (opts.since) {
|
|
1401
|
+
conditions.push("date >= ?");
|
|
1402
|
+
params.push(opts.since);
|
|
1403
|
+
}
|
|
1404
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1405
|
+
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1406
|
+
}
|
|
1407
|
+
function listMachineRegistry(db) {
|
|
1408
|
+
return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
|
|
1409
|
+
}
|
|
1410
|
+
function dedupeRequests(db) {
|
|
1411
|
+
const dupes = db.prepare(`
|
|
1412
|
+
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1413
|
+
FROM requests
|
|
1414
|
+
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1415
|
+
GROUP BY source_request_id, agent
|
|
1416
|
+
HAVING cnt > 1
|
|
1417
|
+
`).all();
|
|
1418
|
+
let removed = 0;
|
|
1419
|
+
for (const row of dupes) {
|
|
1420
|
+
const result = db.prepare(`
|
|
1421
|
+
DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
|
|
1422
|
+
`).run(row.source_request_id, row.agent, row.keep_id);
|
|
1423
|
+
removed += result.changes;
|
|
1424
|
+
}
|
|
1425
|
+
return removed;
|
|
1426
|
+
}
|
|
542
1427
|
var init_database = () => {};
|
|
543
1428
|
|
|
1429
|
+
// src/lib/agents.ts
|
|
1430
|
+
var AGENTS = [
|
|
1431
|
+
"claude",
|
|
1432
|
+
"takumi",
|
|
1433
|
+
"codex",
|
|
1434
|
+
"gemini",
|
|
1435
|
+
"opencode",
|
|
1436
|
+
"cursor",
|
|
1437
|
+
"pi",
|
|
1438
|
+
"hermes"
|
|
1439
|
+
];
|
|
1440
|
+
var COST_BASIS = [
|
|
1441
|
+
"metered_api",
|
|
1442
|
+
"subscription_included",
|
|
1443
|
+
"estimated",
|
|
1444
|
+
"unknown"
|
|
1445
|
+
];
|
|
1446
|
+
function isAgent(value) {
|
|
1447
|
+
return AGENTS.includes(value);
|
|
1448
|
+
}
|
|
544
1449
|
// src/index.ts
|
|
545
1450
|
init_database();
|
|
546
1451
|
init_pricing();
|
|
@@ -548,6 +1453,9 @@ init_pricing();
|
|
|
548
1453
|
// src/lib/gatherer.ts
|
|
549
1454
|
init_database();
|
|
550
1455
|
var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
|
|
1456
|
+
function hasCostData(summary) {
|
|
1457
|
+
return summary.total_usd > 0 || summary.sessions > 0 || summary.requests > 0 || summary.tokens > 0;
|
|
1458
|
+
}
|
|
551
1459
|
var gatherTrainingData = async (options = {}) => {
|
|
552
1460
|
const limit = options.limit ?? 500;
|
|
553
1461
|
const examples = [];
|
|
@@ -557,6 +1465,8 @@ var gatherTrainingData = async (options = {}) => {
|
|
|
557
1465
|
for (const period of periods) {
|
|
558
1466
|
try {
|
|
559
1467
|
const s = querySummary(db, period);
|
|
1468
|
+
if (!hasCostData(s))
|
|
1469
|
+
continue;
|
|
560
1470
|
examples.push({
|
|
561
1471
|
messages: [
|
|
562
1472
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
@@ -729,22 +1639,26 @@ ${goals.map((g) => `- ${g.period} goal (${g.project_path ?? g.agent ?? "global"}
|
|
|
729
1639
|
// src/lib/model-config.ts
|
|
730
1640
|
init_database();
|
|
731
1641
|
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
732
|
-
import { join as join2 } from "path";
|
|
1642
|
+
import { dirname, join as join2 } from "path";
|
|
733
1643
|
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
734
|
-
|
|
1644
|
+
function getModelConfigPath() {
|
|
1645
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join2(getDataDir(), "config.json");
|
|
1646
|
+
}
|
|
735
1647
|
function loadConfig() {
|
|
736
1648
|
try {
|
|
737
|
-
|
|
738
|
-
|
|
1649
|
+
const configPath = getModelConfigPath();
|
|
1650
|
+
if (existsSync2(configPath)) {
|
|
1651
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
739
1652
|
}
|
|
740
1653
|
} catch {}
|
|
741
1654
|
return {};
|
|
742
1655
|
}
|
|
743
1656
|
function saveConfig(config) {
|
|
744
|
-
const
|
|
1657
|
+
const configPath = getModelConfigPath();
|
|
1658
|
+
const dir = dirname(configPath);
|
|
745
1659
|
if (!existsSync2(dir))
|
|
746
1660
|
mkdirSync2(dir, { recursive: true });
|
|
747
|
-
writeFileSync(
|
|
1661
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
748
1662
|
`);
|
|
749
1663
|
}
|
|
750
1664
|
function getActiveModel() {
|
|
@@ -760,16 +1674,181 @@ function clearActiveModel() {
|
|
|
760
1674
|
delete config.activeModel;
|
|
761
1675
|
saveConfig(config);
|
|
762
1676
|
}
|
|
1677
|
+
// src/lib/open-projects.ts
|
|
1678
|
+
init_database();
|
|
1679
|
+
async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
1680
|
+
let listProjects2 = listActiveProjects;
|
|
1681
|
+
if (!listProjects2) {
|
|
1682
|
+
const projectsApi = await import("@hasna/projects");
|
|
1683
|
+
listProjects2 = projectsApi.listProjects;
|
|
1684
|
+
}
|
|
1685
|
+
const projects = listProjects2({ status: "active", limit: 5000 });
|
|
1686
|
+
let imported = 0;
|
|
1687
|
+
let skipped = 0;
|
|
1688
|
+
for (const project of projects) {
|
|
1689
|
+
if (!project.path) {
|
|
1690
|
+
skipped++;
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
upsertProject(db, {
|
|
1694
|
+
id: project.id,
|
|
1695
|
+
path: project.path,
|
|
1696
|
+
name: project.name,
|
|
1697
|
+
description: project.description,
|
|
1698
|
+
tags: project.tags ?? [],
|
|
1699
|
+
created_at: project.created_at
|
|
1700
|
+
});
|
|
1701
|
+
imported++;
|
|
1702
|
+
}
|
|
1703
|
+
return { imported, skipped };
|
|
1704
|
+
}
|
|
763
1705
|
// src/ingest/claude.ts
|
|
764
1706
|
init_database();
|
|
765
1707
|
init_pricing();
|
|
766
1708
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
767
1709
|
import { homedir as homedir2 } from "os";
|
|
768
1710
|
import { join as join3, basename } from "path";
|
|
1711
|
+
|
|
1712
|
+
// src/lib/savings.ts
|
|
1713
|
+
function defaultCostBasisForAgent(agent) {
|
|
1714
|
+
if (agent === "claude")
|
|
1715
|
+
return "metered_api";
|
|
1716
|
+
if (agent === "cursor")
|
|
1717
|
+
return "subscription_included";
|
|
1718
|
+
return "estimated";
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// src/lib/accounts.ts
|
|
1722
|
+
var AGENT_ACCOUNT_TOOLS = {
|
|
1723
|
+
claude: ["claude"],
|
|
1724
|
+
takumi: ["takumi", "claude"],
|
|
1725
|
+
codex: ["codex"],
|
|
1726
|
+
gemini: ["gemini"],
|
|
1727
|
+
opencode: ["opencode"],
|
|
1728
|
+
cursor: ["cursor"],
|
|
1729
|
+
pi: ["pi"],
|
|
1730
|
+
hermes: ["hermes"]
|
|
1731
|
+
};
|
|
1732
|
+
function accountKey(tool, name) {
|
|
1733
|
+
return `${tool}:${name}`;
|
|
1734
|
+
}
|
|
1735
|
+
function normalizeDir(value) {
|
|
1736
|
+
return value.replace(/\/+$/, "");
|
|
1737
|
+
}
|
|
1738
|
+
function fromProfile(profile, source) {
|
|
1739
|
+
return {
|
|
1740
|
+
account_key: accountKey(profile.tool, profile.name),
|
|
1741
|
+
account_tool: profile.tool,
|
|
1742
|
+
account_name: profile.name,
|
|
1743
|
+
...profile.email ? { account_email: profile.email } : {},
|
|
1744
|
+
account_source: source
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
function fromOverride(raw, agent) {
|
|
1748
|
+
const value = raw.trim();
|
|
1749
|
+
if (!value)
|
|
1750
|
+
return null;
|
|
1751
|
+
const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
|
|
1752
|
+
const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
|
|
1753
|
+
if (!tool || !name)
|
|
1754
|
+
return null;
|
|
1755
|
+
return {
|
|
1756
|
+
account_key: accountKey(tool, name),
|
|
1757
|
+
account_tool: tool,
|
|
1758
|
+
account_name: name,
|
|
1759
|
+
account_source: "override"
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
function envOverride(agent, env) {
|
|
1763
|
+
const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
1764
|
+
const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
|
|
1765
|
+
if (raw)
|
|
1766
|
+
return fromOverride(raw, agent);
|
|
1767
|
+
const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
|
|
1768
|
+
const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
|
|
1769
|
+
if (!tool || !name)
|
|
1770
|
+
return null;
|
|
1771
|
+
return {
|
|
1772
|
+
account_key: accountKey(tool, name),
|
|
1773
|
+
account_tool: tool,
|
|
1774
|
+
account_name: name,
|
|
1775
|
+
account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
|
|
1776
|
+
account_source: "override"
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
function knownToolIds(api) {
|
|
1780
|
+
try {
|
|
1781
|
+
return new Set(api.listTools().map((tool) => tool.id));
|
|
1782
|
+
} catch {
|
|
1783
|
+
return new Set;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
function profileForEnvDir(api, tool, env) {
|
|
1787
|
+
const configuredDir = env[tool.envVar];
|
|
1788
|
+
if (!configuredDir)
|
|
1789
|
+
return null;
|
|
1790
|
+
const normalized = normalizeDir(configuredDir);
|
|
1791
|
+
try {
|
|
1792
|
+
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
1793
|
+
} catch {
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
async function resolveAccountForAgent(agent, env = process.env) {
|
|
1798
|
+
const override = envOverride(agent, env);
|
|
1799
|
+
if (override)
|
|
1800
|
+
return override;
|
|
1801
|
+
let api;
|
|
1802
|
+
try {
|
|
1803
|
+
api = await import("@hasna/accounts");
|
|
1804
|
+
} catch {
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
const toolIds = knownToolIds(api);
|
|
1808
|
+
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
1809
|
+
if (!toolIds.has(toolId))
|
|
1810
|
+
continue;
|
|
1811
|
+
let tool;
|
|
1812
|
+
try {
|
|
1813
|
+
tool = api.getTool(toolId);
|
|
1814
|
+
} catch {
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
const envProfile = profileForEnvDir(api, tool, env);
|
|
1818
|
+
if (envProfile)
|
|
1819
|
+
return fromProfile(envProfile, "env");
|
|
1820
|
+
try {
|
|
1821
|
+
const applied = api.appliedProfile(toolId);
|
|
1822
|
+
if (applied)
|
|
1823
|
+
return fromProfile(applied, "applied");
|
|
1824
|
+
} catch {}
|
|
1825
|
+
try {
|
|
1826
|
+
const current = api.currentProfile(toolId);
|
|
1827
|
+
if (current)
|
|
1828
|
+
return fromProfile(current, "current");
|
|
1829
|
+
} catch {}
|
|
1830
|
+
}
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
function withAccount(record, account) {
|
|
1834
|
+
if (!account)
|
|
1835
|
+
return record;
|
|
1836
|
+
return {
|
|
1837
|
+
...record,
|
|
1838
|
+
account_key: account.account_key,
|
|
1839
|
+
account_tool: account.account_tool,
|
|
1840
|
+
account_name: account.account_name,
|
|
1841
|
+
account_email: account.account_email ?? "",
|
|
1842
|
+
account_source: account.account_source
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// src/ingest/claude.ts
|
|
769
1847
|
function autoDetectProject(cwd, projects) {
|
|
770
1848
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
771
1849
|
}
|
|
772
|
-
var
|
|
1850
|
+
var CLAUDE_PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
|
|
1851
|
+
var TAKUMI_PROJECTS_DIR = join3(homedir2(), ".takumi", "projects");
|
|
773
1852
|
function dirNameToPath(dirName) {
|
|
774
1853
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
775
1854
|
}
|
|
@@ -788,30 +1867,38 @@ function collectJsonlFiles(projectDir) {
|
|
|
788
1867
|
walk(projectDir);
|
|
789
1868
|
return files;
|
|
790
1869
|
}
|
|
791
|
-
async function ingestClaude(db, verbose = false,
|
|
792
|
-
|
|
1870
|
+
async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
|
|
1871
|
+
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
1872
|
+
}
|
|
1873
|
+
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
1874
|
+
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
1875
|
+
}
|
|
1876
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
1877
|
+
if (!existsSync3(projectsDir)) {
|
|
793
1878
|
if (verbose)
|
|
794
|
-
console.log(
|
|
1879
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
795
1880
|
return { files: 0, requests: 0, sessions: 0 };
|
|
796
1881
|
}
|
|
1882
|
+
const machineId = getMachineId();
|
|
797
1883
|
let totalFiles = 0;
|
|
798
1884
|
let totalRequests = 0;
|
|
799
1885
|
const touchedSessions = new Set;
|
|
800
1886
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
801
|
-
const
|
|
1887
|
+
const account = await resolveAccountForAgent(agentName);
|
|
1888
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
802
1889
|
for (const projectDirEntry of projectDirs) {
|
|
803
|
-
const projectDirPath = join3(
|
|
1890
|
+
const projectDirPath = join3(projectsDir, projectDirEntry.name);
|
|
804
1891
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
805
1892
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
806
1893
|
for (const filePath of jsonlFiles) {
|
|
807
|
-
const stateKey = filePath.replace(
|
|
1894
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
808
1895
|
let fileMtime = "0";
|
|
809
1896
|
try {
|
|
810
1897
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
811
1898
|
} catch {
|
|
812
1899
|
continue;
|
|
813
1900
|
}
|
|
814
|
-
const processed = getIngestState(db,
|
|
1901
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
815
1902
|
if (processed === fileMtime)
|
|
816
1903
|
continue;
|
|
817
1904
|
let lines;
|
|
@@ -846,27 +1933,39 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
846
1933
|
continue;
|
|
847
1934
|
const inputTokens = usage.input_tokens ?? 0;
|
|
848
1935
|
const outputTokens = usage.output_tokens ?? 0;
|
|
849
|
-
const
|
|
1936
|
+
const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
|
|
1937
|
+
const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
1938
|
+
const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
|
|
850
1939
|
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
851
1940
|
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
852
|
-
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
1941
|
+
if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
|
|
853
1942
|
continue;
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1943
|
+
let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
|
|
1944
|
+
costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
|
|
1945
|
+
const serverToolUse = usage.server_tool_use;
|
|
1946
|
+
if (serverToolUse?.web_search_requests) {
|
|
1947
|
+
costUsd += serverToolUse.web_search_requests * 0.01;
|
|
1948
|
+
}
|
|
1949
|
+
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1950
|
+
const reqId = `${agentName}-${sourceRequestId}`;
|
|
1951
|
+
upsertRequest(db, withAccount({
|
|
857
1952
|
id: reqId,
|
|
858
|
-
agent:
|
|
1953
|
+
agent: agentName,
|
|
859
1954
|
session_id: sessionId,
|
|
860
1955
|
model,
|
|
861
1956
|
input_tokens: inputTokens,
|
|
862
1957
|
output_tokens: outputTokens,
|
|
863
1958
|
cache_read_tokens: cacheReadTokens,
|
|
864
1959
|
cache_create_tokens: cacheWriteTokens,
|
|
1960
|
+
cache_create_5m_tokens: cacheWrite5mTokens,
|
|
1961
|
+
cache_create_1h_tokens: cacheWrite1hTokens,
|
|
865
1962
|
cost_usd: costUsd,
|
|
1963
|
+
cost_basis: defaultCostBasisForAgent(agentName),
|
|
866
1964
|
duration_ms: 0,
|
|
867
1965
|
timestamp,
|
|
868
|
-
source_request_id:
|
|
869
|
-
|
|
1966
|
+
source_request_id: sourceRequestId,
|
|
1967
|
+
machine_id: machineId
|
|
1968
|
+
}, account));
|
|
870
1969
|
if (!touchedSessions.has(sessionId)) {
|
|
871
1970
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
872
1971
|
if (!existing) {
|
|
@@ -874,22 +1973,23 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
874
1973
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
875
1974
|
const session = {
|
|
876
1975
|
id: sessionId,
|
|
877
|
-
agent:
|
|
1976
|
+
agent: agentName,
|
|
878
1977
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
879
1978
|
project_name: detectedProject ? detectedProject.name : "",
|
|
880
1979
|
started_at: timestamp,
|
|
881
1980
|
ended_at: null,
|
|
882
1981
|
total_cost_usd: 0,
|
|
883
1982
|
total_tokens: 0,
|
|
884
|
-
request_count: 0
|
|
1983
|
+
request_count: 0,
|
|
1984
|
+
machine_id: machineId
|
|
885
1985
|
};
|
|
886
|
-
upsertSession(db, session);
|
|
1986
|
+
upsertSession(db, withAccount(session, account));
|
|
887
1987
|
}
|
|
888
1988
|
touchedSessions.add(sessionId);
|
|
889
1989
|
}
|
|
890
1990
|
totalRequests++;
|
|
891
1991
|
}
|
|
892
|
-
setIngestState(db,
|
|
1992
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
893
1993
|
totalFiles++;
|
|
894
1994
|
}
|
|
895
1995
|
}
|
|
@@ -898,79 +1998,397 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
898
1998
|
}
|
|
899
1999
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
900
2000
|
}
|
|
2001
|
+
function applyClaudeModifiers(costUsd, model, usage, entry) {
|
|
2002
|
+
let multiplier = 1;
|
|
2003
|
+
const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
|
|
2004
|
+
if (speed === "fast" && model.includes("opus-4-6")) {
|
|
2005
|
+
multiplier *= 6;
|
|
2006
|
+
}
|
|
2007
|
+
const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
|
|
2008
|
+
if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
|
|
2009
|
+
multiplier *= 1.1;
|
|
2010
|
+
}
|
|
2011
|
+
return costUsd * multiplier;
|
|
2012
|
+
}
|
|
2013
|
+
function supportsClaudeDataResidencyPricing(model) {
|
|
2014
|
+
const normalized = normalizeModelName(model);
|
|
2015
|
+
const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
2016
|
+
if (!match)
|
|
2017
|
+
return false;
|
|
2018
|
+
const major = Number(match[2]);
|
|
2019
|
+
const minor = match[3] ? Number(match[3]) : 0;
|
|
2020
|
+
return major > 4 || major === 4 && minor >= 6;
|
|
2021
|
+
}
|
|
901
2022
|
// src/ingest/codex.ts
|
|
902
2023
|
init_database();
|
|
2024
|
+
init_pricing();
|
|
903
2025
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
904
2026
|
import { homedir as homedir3 } from "os";
|
|
905
2027
|
import { join as join4, basename as basename2 } from "path";
|
|
906
|
-
import { Database as
|
|
907
|
-
var
|
|
908
|
-
var
|
|
2028
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
2029
|
+
var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
|
|
2030
|
+
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
|
|
2031
|
+
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2032
|
+
function codexDbPath() {
|
|
2033
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
2034
|
+
}
|
|
2035
|
+
function codexConfigPath() {
|
|
2036
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
2037
|
+
}
|
|
909
2038
|
function readCodexModel() {
|
|
910
|
-
|
|
911
|
-
|
|
2039
|
+
const configPath = codexConfigPath();
|
|
2040
|
+
if (!existsSync4(configPath))
|
|
2041
|
+
return "gpt-5-codex";
|
|
912
2042
|
try {
|
|
913
|
-
const content = readFileSync3(
|
|
2043
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
914
2044
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
915
|
-
return match?.[1] ?? "gpt-5
|
|
2045
|
+
return match?.[1] ?? "gpt-5-codex";
|
|
916
2046
|
} catch {
|
|
917
|
-
return "gpt-5
|
|
2047
|
+
return "gpt-5-codex";
|
|
918
2048
|
}
|
|
919
2049
|
}
|
|
2050
|
+
function buildThreadQuery(codexDb) {
|
|
2051
|
+
const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
|
|
2052
|
+
const modelSelect = cols.has("model") ? "model" : "NULL AS model";
|
|
2053
|
+
const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
|
|
2054
|
+
const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
|
|
2055
|
+
return `
|
|
2056
|
+
SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
|
|
2057
|
+
${providerSelect}, ${modelSelect}
|
|
2058
|
+
FROM threads WHERE tokens_used > 0
|
|
2059
|
+
`;
|
|
2060
|
+
}
|
|
2061
|
+
function readTokenEvents(rolloutPath) {
|
|
2062
|
+
if (!rolloutPath || !existsSync4(rolloutPath))
|
|
2063
|
+
return [];
|
|
2064
|
+
const fallbackUsages = new Map;
|
|
2065
|
+
let fallbackTimestamp;
|
|
2066
|
+
let aggregate = null;
|
|
2067
|
+
for (const line of readFileSync3(rolloutPath, "utf-8").split(`
|
|
2068
|
+
`)) {
|
|
2069
|
+
if (!line.trim())
|
|
2070
|
+
continue;
|
|
2071
|
+
let entry;
|
|
2072
|
+
try {
|
|
2073
|
+
entry = JSON.parse(line);
|
|
2074
|
+
} catch {
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
if (!entry || typeof entry !== "object")
|
|
2078
|
+
continue;
|
|
2079
|
+
const payload = entry["payload"];
|
|
2080
|
+
if (!payload || payload["type"] !== "token_count")
|
|
2081
|
+
continue;
|
|
2082
|
+
const info = payload["info"];
|
|
2083
|
+
const timestamp = entry["timestamp"];
|
|
2084
|
+
const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
|
|
2085
|
+
const totalUsage = info?.["total_token_usage"];
|
|
2086
|
+
if (totalUsage && tokenTotal(totalUsage) > 0) {
|
|
2087
|
+
aggregate = { usage: totalUsage, timestamp: entryTimestamp };
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
const usage = info?.["last_token_usage"];
|
|
2091
|
+
if (!usage)
|
|
2092
|
+
continue;
|
|
2093
|
+
if (tokenTotal(usage) <= 0)
|
|
2094
|
+
continue;
|
|
2095
|
+
const key = JSON.stringify(usage);
|
|
2096
|
+
if (!fallbackUsages.has(key))
|
|
2097
|
+
fallbackUsages.set(key, usage);
|
|
2098
|
+
fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
|
|
2099
|
+
}
|
|
2100
|
+
if (aggregate)
|
|
2101
|
+
return [aggregate];
|
|
2102
|
+
if (fallbackUsages.size === 0)
|
|
2103
|
+
return [];
|
|
2104
|
+
return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
|
|
2105
|
+
}
|
|
2106
|
+
function tokenTotal(usage) {
|
|
2107
|
+
return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2108
|
+
}
|
|
2109
|
+
function sumTokenUsages(usages) {
|
|
2110
|
+
const result = {
|
|
2111
|
+
input_tokens: 0,
|
|
2112
|
+
cached_input_tokens: 0,
|
|
2113
|
+
output_tokens: 0,
|
|
2114
|
+
reasoning_output_tokens: 0,
|
|
2115
|
+
total_tokens: 0
|
|
2116
|
+
};
|
|
2117
|
+
for (const usage of usages) {
|
|
2118
|
+
result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
|
|
2119
|
+
result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
|
|
2120
|
+
result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2121
|
+
result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
|
|
2122
|
+
result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
|
|
2123
|
+
}
|
|
2124
|
+
return result;
|
|
2125
|
+
}
|
|
2126
|
+
function fallbackEvents(totalTokens) {
|
|
2127
|
+
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
2128
|
+
return [{
|
|
2129
|
+
usage: {
|
|
2130
|
+
input_tokens: inputTokens,
|
|
2131
|
+
cached_input_tokens: 0,
|
|
2132
|
+
output_tokens: totalTokens - inputTokens,
|
|
2133
|
+
total_tokens: totalTokens
|
|
2134
|
+
}
|
|
2135
|
+
}];
|
|
2136
|
+
}
|
|
920
2137
|
async function ingestCodex(db, verbose = false) {
|
|
921
|
-
|
|
2138
|
+
const dbPath = codexDbPath();
|
|
2139
|
+
if (!existsSync4(dbPath)) {
|
|
922
2140
|
if (verbose)
|
|
923
|
-
console.log("Codex DB not found:",
|
|
924
|
-
return { sessions: 0 };
|
|
2141
|
+
console.log("Codex DB not found:", dbPath);
|
|
2142
|
+
return { sessions: 0, requests: 0 };
|
|
925
2143
|
}
|
|
2144
|
+
const machineId = getMachineId();
|
|
926
2145
|
let codexDb = null;
|
|
927
2146
|
let ingested = 0;
|
|
2147
|
+
let requests = 0;
|
|
2148
|
+
const account = await resolveAccountForAgent("codex");
|
|
928
2149
|
try {
|
|
929
|
-
codexDb = new
|
|
930
|
-
const threads = codexDb.prepare(
|
|
2150
|
+
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
2151
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
931
2152
|
for (const thread of threads) {
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
|
|
2153
|
+
const model = thread.model ?? readCodexModel();
|
|
2154
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
2155
|
+
const processed = getIngestState(db, "codex", thread.id);
|
|
2156
|
+
if (processed === stateValue)
|
|
935
2157
|
continue;
|
|
936
|
-
const costUsd = 0;
|
|
937
2158
|
const projectPath = thread.cwd ?? "";
|
|
938
2159
|
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
2160
|
+
const sessionId = `codex-${thread.id}`;
|
|
939
2161
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
940
2162
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
941
|
-
upsertSession(db, {
|
|
942
|
-
id:
|
|
2163
|
+
upsertSession(db, withAccount({
|
|
2164
|
+
id: sessionId,
|
|
943
2165
|
agent: "codex",
|
|
944
2166
|
project_path: projectPath,
|
|
945
2167
|
project_name: projectName,
|
|
946
2168
|
started_at: startedAt,
|
|
947
2169
|
ended_at: endedAt,
|
|
948
|
-
total_cost_usd:
|
|
949
|
-
total_tokens:
|
|
950
|
-
request_count:
|
|
2170
|
+
total_cost_usd: 0,
|
|
2171
|
+
total_tokens: 0,
|
|
2172
|
+
request_count: 0,
|
|
2173
|
+
machine_id: machineId
|
|
2174
|
+
}, account));
|
|
2175
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
2176
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2177
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2178
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2179
|
+
tokenEvents.forEach((event, index) => {
|
|
2180
|
+
const usage = event.usage;
|
|
2181
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
2182
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2183
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2184
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2185
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2186
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2187
|
+
const requestId = `${sessionId}-${index}`;
|
|
2188
|
+
upsertRequest(db, withAccount({
|
|
2189
|
+
id: requestId,
|
|
2190
|
+
agent: "codex",
|
|
2191
|
+
session_id: sessionId,
|
|
2192
|
+
model,
|
|
2193
|
+
input_tokens: inputTokens,
|
|
2194
|
+
output_tokens: outputTokens,
|
|
2195
|
+
cache_read_tokens: cacheReadTokens,
|
|
2196
|
+
cache_create_tokens: 0,
|
|
2197
|
+
cost_usd: costUsd,
|
|
2198
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2199
|
+
duration_ms: 0,
|
|
2200
|
+
timestamp,
|
|
2201
|
+
source_request_id: requestId,
|
|
2202
|
+
machine_id: machineId
|
|
2203
|
+
}, account));
|
|
2204
|
+
requests++;
|
|
951
2205
|
});
|
|
952
|
-
|
|
2206
|
+
rollupSession(db, sessionId);
|
|
2207
|
+
setIngestState(db, "codex", thread.id, stateValue);
|
|
953
2208
|
ingested++;
|
|
954
2209
|
if (verbose)
|
|
955
|
-
console.log(`Codex session ${thread.id}: ${
|
|
2210
|
+
console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
956
2211
|
}
|
|
957
2212
|
} finally {
|
|
958
2213
|
codexDb?.close();
|
|
959
2214
|
}
|
|
960
|
-
return { sessions: ingested };
|
|
2215
|
+
return { sessions: ingested, requests };
|
|
2216
|
+
}
|
|
2217
|
+
// src/ingest/gemini.ts
|
|
2218
|
+
init_database();
|
|
2219
|
+
init_pricing();
|
|
2220
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
2221
|
+
import { homedir as homedir4 } from "os";
|
|
2222
|
+
import { join as join5, basename as basename3 } from "path";
|
|
2223
|
+
var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
|
|
2224
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir4(), ".gemini", "history");
|
|
2225
|
+
function geminiTmpDir() {
|
|
2226
|
+
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
2227
|
+
}
|
|
2228
|
+
function geminiHistoryDir() {
|
|
2229
|
+
return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
|
|
2230
|
+
}
|
|
2231
|
+
function numberField(...values) {
|
|
2232
|
+
for (const value of values) {
|
|
2233
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2234
|
+
return value;
|
|
2235
|
+
}
|
|
2236
|
+
return 0;
|
|
2237
|
+
}
|
|
2238
|
+
function listProjectDirs(...roots) {
|
|
2239
|
+
const dirs = new Set;
|
|
2240
|
+
for (const root of roots) {
|
|
2241
|
+
if (!existsSync5(root))
|
|
2242
|
+
continue;
|
|
2243
|
+
try {
|
|
2244
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
2245
|
+
if (entry.isDirectory())
|
|
2246
|
+
dirs.add(join5(root, entry.name));
|
|
2247
|
+
}
|
|
2248
|
+
} catch {}
|
|
2249
|
+
}
|
|
2250
|
+
return [...dirs];
|
|
2251
|
+
}
|
|
2252
|
+
function projectRoot(projectDir, chatData) {
|
|
2253
|
+
if (chatData.projectPath)
|
|
2254
|
+
return chatData.projectPath;
|
|
2255
|
+
if (chatData.project_path)
|
|
2256
|
+
return chatData.project_path;
|
|
2257
|
+
const rootFile = join5(projectDir, ".project_root");
|
|
2258
|
+
try {
|
|
2259
|
+
if (existsSync5(rootFile))
|
|
2260
|
+
return readFileSync4(rootFile, "utf-8").trim();
|
|
2261
|
+
} catch {}
|
|
2262
|
+
return "";
|
|
2263
|
+
}
|
|
2264
|
+
async function ingestGemini(db, verbose) {
|
|
2265
|
+
const tmpDir = geminiTmpDir();
|
|
2266
|
+
const historyDir = geminiHistoryDir();
|
|
2267
|
+
if (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
|
|
2268
|
+
if (verbose)
|
|
2269
|
+
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
2270
|
+
return { sessions: 0, requests: 0 };
|
|
2271
|
+
}
|
|
2272
|
+
const machineId = getMachineId();
|
|
2273
|
+
let totalSessions = 0;
|
|
2274
|
+
let totalRequests = 0;
|
|
2275
|
+
const touchedSessions = new Set;
|
|
2276
|
+
const account = await resolveAccountForAgent("gemini");
|
|
2277
|
+
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2278
|
+
for (const projectDir of projectDirs) {
|
|
2279
|
+
const chatsDir = join5(projectDir, "chats");
|
|
2280
|
+
if (!existsSync5(chatsDir))
|
|
2281
|
+
continue;
|
|
2282
|
+
let chatFiles = [];
|
|
2283
|
+
try {
|
|
2284
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
|
|
2285
|
+
} catch {
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
for (const filePath of chatFiles) {
|
|
2289
|
+
const stateKey = filePath.replace(homedir4(), "~");
|
|
2290
|
+
let fileMtime = "0";
|
|
2291
|
+
try {
|
|
2292
|
+
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
2293
|
+
} catch {
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
const processed = getIngestState(db, "gemini", stateKey);
|
|
2297
|
+
if (processed === fileMtime)
|
|
2298
|
+
continue;
|
|
2299
|
+
let chatData;
|
|
2300
|
+
try {
|
|
2301
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
2302
|
+
} catch {
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
|
|
2306
|
+
if (!sessionId)
|
|
2307
|
+
continue;
|
|
2308
|
+
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
2309
|
+
const projectPath = projectRoot(projectDir, chatData);
|
|
2310
|
+
const projectName = projectPath ? basename3(projectPath) : "";
|
|
2311
|
+
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
2312
|
+
if (!existing) {
|
|
2313
|
+
const session = {
|
|
2314
|
+
id: sessionId,
|
|
2315
|
+
agent: "gemini",
|
|
2316
|
+
project_path: projectPath,
|
|
2317
|
+
project_name: projectName,
|
|
2318
|
+
started_at: startTime,
|
|
2319
|
+
ended_at: chatData.lastUpdated ?? null,
|
|
2320
|
+
total_cost_usd: 0,
|
|
2321
|
+
total_tokens: 0,
|
|
2322
|
+
request_count: 0,
|
|
2323
|
+
machine_id: machineId
|
|
2324
|
+
};
|
|
2325
|
+
upsertSession(db, withAccount(session, account));
|
|
2326
|
+
totalSessions++;
|
|
2327
|
+
}
|
|
2328
|
+
touchedSessions.add(sessionId);
|
|
2329
|
+
for (const [index, message] of (chatData.messages ?? []).entries()) {
|
|
2330
|
+
const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
|
|
2331
|
+
if (!usage)
|
|
2332
|
+
continue;
|
|
2333
|
+
const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
|
|
2334
|
+
if (!model)
|
|
2335
|
+
continue;
|
|
2336
|
+
const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
|
|
2337
|
+
const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
|
|
2338
|
+
const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
|
|
2339
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2340
|
+
const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
|
|
2341
|
+
const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
|
|
2342
|
+
const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
|
|
2343
|
+
if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
|
|
2344
|
+
continue;
|
|
2345
|
+
const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2346
|
+
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
2347
|
+
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
2348
|
+
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
2349
|
+
upsertRequest(db, withAccount({
|
|
2350
|
+
id: requestId,
|
|
2351
|
+
agent: "gemini",
|
|
2352
|
+
session_id: sessionId,
|
|
2353
|
+
model,
|
|
2354
|
+
input_tokens: inputTokens,
|
|
2355
|
+
output_tokens: outputTokens,
|
|
2356
|
+
cache_read_tokens: cacheReadTokens,
|
|
2357
|
+
cache_create_tokens: 0,
|
|
2358
|
+
cost_usd: costUsd,
|
|
2359
|
+
cost_basis: defaultCostBasisForAgent("gemini"),
|
|
2360
|
+
duration_ms: 0,
|
|
2361
|
+
timestamp,
|
|
2362
|
+
source_request_id: message.id ?? requestId,
|
|
2363
|
+
machine_id: machineId
|
|
2364
|
+
}, account));
|
|
2365
|
+
totalRequests++;
|
|
2366
|
+
}
|
|
2367
|
+
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
for (const sessionId of touchedSessions) {
|
|
2371
|
+
rollupSession(db, sessionId);
|
|
2372
|
+
}
|
|
2373
|
+
return { sessions: totalSessions, requests: totalRequests };
|
|
961
2374
|
}
|
|
962
2375
|
export {
|
|
2376
|
+
upsertUsageSnapshot,
|
|
2377
|
+
upsertSubscription,
|
|
963
2378
|
upsertSession,
|
|
964
2379
|
upsertRequest,
|
|
965
2380
|
upsertProject,
|
|
966
2381
|
upsertModelPricing,
|
|
967
2382
|
upsertGoal,
|
|
968
2383
|
upsertBudget,
|
|
2384
|
+
upsertBillingDaily,
|
|
2385
|
+
syncOpenProjectsRegistry,
|
|
969
2386
|
setIngestState,
|
|
970
2387
|
setActiveModel,
|
|
971
2388
|
seedModelPricing,
|
|
972
2389
|
rollupSession,
|
|
973
2390
|
readCodexModel,
|
|
2391
|
+
queryUsageSnapshots,
|
|
974
2392
|
queryTopSessions,
|
|
975
2393
|
querySummary,
|
|
976
2394
|
querySessions,
|
|
@@ -978,18 +2396,29 @@ export {
|
|
|
978
2396
|
queryProjectBreakdown,
|
|
979
2397
|
queryModelBreakdown,
|
|
980
2398
|
queryDailyBreakdown,
|
|
2399
|
+
queryBillingSummary,
|
|
2400
|
+
queryAgentBreakdown,
|
|
2401
|
+
queryAccountBreakdown,
|
|
981
2402
|
openDatabase,
|
|
982
2403
|
normalizeModelName,
|
|
2404
|
+
listSubscriptions,
|
|
983
2405
|
listProjects,
|
|
984
2406
|
listModelPricing,
|
|
2407
|
+
listMachines,
|
|
2408
|
+
listMachineRegistry,
|
|
985
2409
|
listGoals,
|
|
986
2410
|
listBudgets,
|
|
2411
|
+
isAgent,
|
|
2412
|
+
ingestTakumi,
|
|
2413
|
+
ingestJsonlProjects,
|
|
2414
|
+
ingestGemini,
|
|
987
2415
|
ingestCodex,
|
|
988
2416
|
ingestClaude,
|
|
989
2417
|
getProject,
|
|
990
2418
|
getPricingFromDb,
|
|
991
2419
|
getPricing,
|
|
992
2420
|
getModelPricing,
|
|
2421
|
+
getMachineId,
|
|
993
2422
|
getIngestState,
|
|
994
2423
|
getGoalStatuses,
|
|
995
2424
|
getDbPath,
|
|
@@ -998,13 +2427,18 @@ export {
|
|
|
998
2427
|
getActiveModel,
|
|
999
2428
|
gatherTrainingData,
|
|
1000
2429
|
ensurePricingSeeded,
|
|
2430
|
+
deleteSubscription,
|
|
1001
2431
|
deleteProject,
|
|
1002
2432
|
deleteModelPricing,
|
|
1003
2433
|
deleteGoal,
|
|
1004
2434
|
deleteBudget,
|
|
2435
|
+
dedupeRequests,
|
|
1005
2436
|
computeCostFromDb,
|
|
1006
2437
|
computeCost,
|
|
2438
|
+
clearBillingRange,
|
|
1007
2439
|
clearActiveModel,
|
|
1008
2440
|
DEFAULT_PRICING,
|
|
1009
|
-
DEFAULT_MODEL
|
|
2441
|
+
DEFAULT_MODEL,
|
|
2442
|
+
COST_BASIS,
|
|
2443
|
+
AGENTS
|
|
1010
2444
|
};
|