@hasna/economy 0.2.20 → 0.2.21
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/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 +5134 -641
- package/dist/db/database.d.ts +41 -1
- 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 +1202 -135
- 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/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/http.d.ts +13 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.js +2752 -490
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/otel/index.d.ts +3 -0
- package/dist/otel/index.d.ts.map +1 -0
- package/dist/otel/index.js +1372 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +3095 -201
- package/dist/server/serve.d.ts +10 -2
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +59 -6
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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,13 @@ 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 ''
|
|
166
586
|
);
|
|
167
587
|
|
|
168
588
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -174,7 +594,8 @@ function initSchema(db) {
|
|
|
174
594
|
ended_at TEXT,
|
|
175
595
|
total_cost_usd REAL DEFAULT 0,
|
|
176
596
|
total_tokens INTEGER DEFAULT 0,
|
|
177
|
-
request_count INTEGER DEFAULT 0
|
|
597
|
+
request_count INTEGER DEFAULT 0,
|
|
598
|
+
machine_id TEXT DEFAULT ''
|
|
178
599
|
);
|
|
179
600
|
|
|
180
601
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -227,6 +648,8 @@ function initSchema(db) {
|
|
|
227
648
|
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
228
649
|
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
229
650
|
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
651
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
652
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
230
653
|
updated_at TEXT NOT NULL
|
|
231
654
|
);
|
|
232
655
|
|
|
@@ -239,6 +662,115 @@ function initSchema(db) {
|
|
|
239
662
|
machine_id TEXT,
|
|
240
663
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
241
664
|
);
|
|
665
|
+
|
|
666
|
+
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
667
|
+
date TEXT NOT NULL,
|
|
668
|
+
provider TEXT NOT NULL,
|
|
669
|
+
description TEXT DEFAULT '',
|
|
670
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
671
|
+
updated_at TEXT NOT NULL,
|
|
672
|
+
PRIMARY KEY (date, provider, description)
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
676
|
+
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
677
|
+
|
|
678
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
679
|
+
id TEXT PRIMARY KEY,
|
|
680
|
+
agent TEXT,
|
|
681
|
+
provider TEXT NOT NULL,
|
|
682
|
+
plan TEXT NOT NULL,
|
|
683
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
684
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
685
|
+
billing_cycle_start TEXT,
|
|
686
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
687
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
688
|
+
created_at TEXT NOT NULL,
|
|
689
|
+
updated_at TEXT NOT NULL
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
693
|
+
id TEXT PRIMARY KEY,
|
|
694
|
+
agent TEXT NOT NULL,
|
|
695
|
+
date TEXT NOT NULL,
|
|
696
|
+
metric TEXT NOT NULL,
|
|
697
|
+
value REAL NOT NULL DEFAULT 0,
|
|
698
|
+
unit TEXT DEFAULT '',
|
|
699
|
+
machine_id TEXT DEFAULT '',
|
|
700
|
+
updated_at TEXT NOT NULL
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
704
|
+
date TEXT NOT NULL,
|
|
705
|
+
agent TEXT DEFAULT '',
|
|
706
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
707
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
708
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
709
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
710
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
711
|
+
updated_at TEXT NOT NULL,
|
|
712
|
+
PRIMARY KEY (date, agent)
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
716
|
+
machine_id TEXT PRIMARY KEY,
|
|
717
|
+
hostname TEXT NOT NULL,
|
|
718
|
+
last_seen_at TEXT,
|
|
719
|
+
last_push_at TEXT,
|
|
720
|
+
last_pull_at TEXT,
|
|
721
|
+
economy_version TEXT,
|
|
722
|
+
updated_at TEXT NOT NULL
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
726
|
+
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
727
|
+
`);
|
|
728
|
+
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
729
|
+
if (!cols.some((c) => c.name === "machine_id")) {
|
|
730
|
+
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
731
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
732
|
+
}
|
|
733
|
+
if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
|
|
734
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
|
|
735
|
+
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
736
|
+
}
|
|
737
|
+
if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
|
|
738
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
|
|
739
|
+
}
|
|
740
|
+
if (!cols.some((c) => c.name === "cost_basis")) {
|
|
741
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
|
|
742
|
+
}
|
|
743
|
+
if (!cols.some((c) => c.name === "attribution_tag")) {
|
|
744
|
+
db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
745
|
+
}
|
|
746
|
+
if (!cols.some((c) => c.name === "updated_at")) {
|
|
747
|
+
db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
748
|
+
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
749
|
+
}
|
|
750
|
+
if (!cols.some((c) => c.name === "synced_at")) {
|
|
751
|
+
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
752
|
+
}
|
|
753
|
+
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
754
|
+
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
755
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
756
|
+
}
|
|
757
|
+
if (!sessionCols.some((c) => c.name === "updated_at")) {
|
|
758
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
759
|
+
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
760
|
+
}
|
|
761
|
+
if (!sessionCols.some((c) => c.name === "synced_at")) {
|
|
762
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
763
|
+
}
|
|
764
|
+
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
765
|
+
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
766
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
767
|
+
}
|
|
768
|
+
if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
|
|
769
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
|
|
770
|
+
}
|
|
771
|
+
db.exec(`
|
|
772
|
+
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
773
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
242
774
|
`);
|
|
243
775
|
}
|
|
244
776
|
function periodWhere(period) {
|
|
@@ -248,11 +780,11 @@ function periodWhere(period) {
|
|
|
248
780
|
case "yesterday":
|
|
249
781
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
250
782
|
case "week":
|
|
251
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
783
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
252
784
|
case "month":
|
|
253
|
-
return `timestamp >= DATE('now', '
|
|
785
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
254
786
|
case "year":
|
|
255
|
-
return `timestamp >= DATE('now', '
|
|
787
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
256
788
|
case "all":
|
|
257
789
|
return "1=1";
|
|
258
790
|
}
|
|
@@ -264,31 +796,34 @@ function sessionPeriodWhere(period) {
|
|
|
264
796
|
case "yesterday":
|
|
265
797
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
266
798
|
case "week":
|
|
267
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
799
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
268
800
|
case "month":
|
|
269
|
-
return `started_at >= DATE('now', '
|
|
801
|
+
return `started_at >= DATE('now', 'start of month')`;
|
|
270
802
|
case "year":
|
|
271
|
-
return `started_at >= DATE('now', '
|
|
803
|
+
return `started_at >= DATE('now', 'start of year')`;
|
|
272
804
|
case "all":
|
|
273
805
|
return "1=1";
|
|
274
806
|
}
|
|
275
807
|
}
|
|
276
808
|
function upsertRequest(db, req) {
|
|
809
|
+
const now = req.updated_at ?? new Date().toISOString();
|
|
277
810
|
db.prepare(`
|
|
278
811
|
INSERT OR REPLACE INTO requests
|
|
279
812
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
280
|
-
cache_read_tokens, cache_create_tokens,
|
|
281
|
-
timestamp,
|
|
282
|
-
|
|
283
|
-
|
|
813
|
+
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
814
|
+
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
815
|
+
source_request_id, machine_id, attribution_tag, updated_at, synced_at)
|
|
816
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
817
|
+
`).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"] ?? "", now, req.synced_at ?? "");
|
|
284
818
|
}
|
|
285
819
|
function upsertSession(db, session) {
|
|
820
|
+
const now = session.updated_at ?? new Date().toISOString();
|
|
286
821
|
db.prepare(`
|
|
287
822
|
INSERT OR REPLACE INTO sessions
|
|
288
823
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
289
|
-
total_cost_usd, total_tokens, request_count)
|
|
290
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
291
|
-
`).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);
|
|
824
|
+
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
|
|
825
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
826
|
+
`).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"] ?? "", now, session.synced_at ?? "");
|
|
292
827
|
}
|
|
293
828
|
function rollupSession(db, sessionId) {
|
|
294
829
|
db.prepare(`
|
|
@@ -318,6 +853,10 @@ function querySessions(db, filter = {}) {
|
|
|
318
853
|
conditions.push("started_at >= ?");
|
|
319
854
|
params.push(filter.since);
|
|
320
855
|
}
|
|
856
|
+
if (filter.machine) {
|
|
857
|
+
conditions.push("machine_id = ?");
|
|
858
|
+
params.push(filter.machine);
|
|
859
|
+
}
|
|
321
860
|
if (filter.search) {
|
|
322
861
|
const q = `%${filter.search}%`;
|
|
323
862
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -336,24 +875,25 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
336
875
|
}
|
|
337
876
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
338
877
|
}
|
|
339
|
-
function querySummary(db, period) {
|
|
878
|
+
function querySummary(db, period, machine, allMachines = false) {
|
|
340
879
|
const rWhere = periodWhere(period);
|
|
341
880
|
const sWhere = sessionPeriodWhere(period);
|
|
881
|
+
const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
342
882
|
const r = db.prepare(`
|
|
343
883
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
344
884
|
COUNT(*) as requests,
|
|
345
885
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
346
|
-
FROM requests WHERE ${rWhere}
|
|
886
|
+
FROM requests WHERE ${rWhere}${machineClause}
|
|
347
887
|
`).get();
|
|
348
888
|
const codexTotals = db.prepare(`
|
|
349
889
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
350
890
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
351
891
|
COUNT(*) as sessions
|
|
352
892
|
FROM sessions
|
|
353
|
-
WHERE ${sWhere}
|
|
893
|
+
WHERE ${sWhere}${machineClause}
|
|
354
894
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
355
895
|
`).get();
|
|
356
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
896
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
|
|
357
897
|
return {
|
|
358
898
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
359
899
|
requests: r.requests,
|
|
@@ -373,23 +913,66 @@ function queryModelBreakdown(db) {
|
|
|
373
913
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
374
914
|
`).all();
|
|
375
915
|
}
|
|
916
|
+
function labelForPath(projectPath, projectName) {
|
|
917
|
+
if (projectName && projectName.trim() !== "")
|
|
918
|
+
return projectName;
|
|
919
|
+
if (!projectPath)
|
|
920
|
+
return "";
|
|
921
|
+
const segments = projectPath.split("/").filter(Boolean);
|
|
922
|
+
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
923
|
+
for (const seg of segments) {
|
|
924
|
+
if (projectPrefix.test(seg))
|
|
925
|
+
return seg;
|
|
926
|
+
}
|
|
927
|
+
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
928
|
+
for (let i = segments.length - 1;i >= 0; i--) {
|
|
929
|
+
if (!generic.has(segments[i].toLowerCase()))
|
|
930
|
+
return segments[i];
|
|
931
|
+
}
|
|
932
|
+
return segments[segments.length - 1] ?? projectPath;
|
|
933
|
+
}
|
|
376
934
|
function queryProjectBreakdown(db) {
|
|
377
|
-
|
|
378
|
-
SELECT
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
382
|
-
COUNT(r.id) as requests,
|
|
383
|
-
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
384
|
-
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
385
|
-
MAX(s.started_at) as last_active
|
|
386
|
-
FROM sessions s
|
|
387
|
-
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
388
|
-
LEFT JOIN requests r ON r.session_id = s.id
|
|
389
|
-
WHERE s.project_path != '' OR s.project_name != ''
|
|
390
|
-
GROUP BY s.project_path
|
|
391
|
-
ORDER BY cost_usd DESC
|
|
935
|
+
const sessions = db.prepare(`
|
|
936
|
+
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
937
|
+
FROM sessions
|
|
938
|
+
WHERE project_path != '' OR project_name != ''
|
|
392
939
|
`).all();
|
|
940
|
+
const groups = new Map;
|
|
941
|
+
for (const s of sessions) {
|
|
942
|
+
const label = labelForPath(s.project_path, s.project_name);
|
|
943
|
+
if (!label)
|
|
944
|
+
continue;
|
|
945
|
+
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
946
|
+
g.sessionIds.push(s.id);
|
|
947
|
+
g.totalCost += s.total_cost_usd || 0;
|
|
948
|
+
if (!g.lastActive || s.started_at > g.lastActive)
|
|
949
|
+
g.lastActive = s.started_at;
|
|
950
|
+
if (!g.samplePath)
|
|
951
|
+
g.samplePath = s.project_path;
|
|
952
|
+
groups.set(label, g);
|
|
953
|
+
}
|
|
954
|
+
const result = [];
|
|
955
|
+
for (const [label, g] of groups.entries()) {
|
|
956
|
+
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
957
|
+
const reqStats = placeholders.length ? db.prepare(`
|
|
958
|
+
SELECT
|
|
959
|
+
COUNT(*) as requests,
|
|
960
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
961
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
962
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
963
|
+
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
964
|
+
result.push({
|
|
965
|
+
project_path: g.samplePath,
|
|
966
|
+
project_name: label,
|
|
967
|
+
sessions: g.sessionIds.length,
|
|
968
|
+
requests: reqStats.requests,
|
|
969
|
+
total_tokens: reqStats.total_tokens,
|
|
970
|
+
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
971
|
+
last_active: g.lastActive
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
975
|
+
return result;
|
|
393
976
|
}
|
|
394
977
|
function queryDailyBreakdown(db, days = 30) {
|
|
395
978
|
return db.prepare(`
|
|
@@ -507,12 +1090,46 @@ function setIngestState(db, source, key, value) {
|
|
|
507
1090
|
function queryRequestsSince(db, since) {
|
|
508
1091
|
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
509
1092
|
}
|
|
1093
|
+
function upsertBillingDaily(db, row) {
|
|
1094
|
+
db.prepare(`
|
|
1095
|
+
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
1096
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1097
|
+
`).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
|
|
1098
|
+
}
|
|
1099
|
+
function clearBillingRange(db, provider, fromDate, toDate) {
|
|
1100
|
+
db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
|
|
1101
|
+
}
|
|
1102
|
+
function queryBillingSummary(db, period) {
|
|
1103
|
+
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";
|
|
1104
|
+
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
1105
|
+
const by_provider = {};
|
|
1106
|
+
let total = 0;
|
|
1107
|
+
for (const r of rows) {
|
|
1108
|
+
by_provider[r.provider] = r.cost;
|
|
1109
|
+
total += r.cost;
|
|
1110
|
+
}
|
|
1111
|
+
return { total_usd: total, by_provider };
|
|
1112
|
+
}
|
|
1113
|
+
function listMachines(db) {
|
|
1114
|
+
return db.prepare(`
|
|
1115
|
+
SELECT
|
|
1116
|
+
s.machine_id,
|
|
1117
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
1118
|
+
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
1119
|
+
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
1120
|
+
MAX(s.started_at) as last_active
|
|
1121
|
+
FROM sessions s
|
|
1122
|
+
WHERE s.machine_id != ''
|
|
1123
|
+
GROUP BY s.machine_id
|
|
1124
|
+
ORDER BY total_cost_usd DESC
|
|
1125
|
+
`).all();
|
|
1126
|
+
}
|
|
510
1127
|
function upsertModelPricing(db, p) {
|
|
511
1128
|
db.prepare(`
|
|
512
1129
|
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);
|
|
1130
|
+
(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)
|
|
1131
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1132
|
+
`).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
1133
|
}
|
|
517
1134
|
function getModelPricing(db, model) {
|
|
518
1135
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
@@ -524,23 +1141,104 @@ function deleteModelPricing(db, model) {
|
|
|
524
1141
|
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
525
1142
|
}
|
|
526
1143
|
function seedModelPricing(db, defaults) {
|
|
527
|
-
const existing = db.prepare(`SELECT
|
|
528
|
-
if (existing.count > 0)
|
|
529
|
-
return;
|
|
1144
|
+
const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
|
|
530
1145
|
const now = new Date().toISOString();
|
|
531
1146
|
for (const [model, p] of Object.entries(defaults)) {
|
|
1147
|
+
if (existing.has(model))
|
|
1148
|
+
continue;
|
|
532
1149
|
upsertModelPricing(db, {
|
|
533
1150
|
model,
|
|
534
1151
|
input_per_1m: p.inputPer1M,
|
|
535
1152
|
output_per_1m: p.outputPer1M,
|
|
536
1153
|
cache_read_per_1m: p.cacheReadPer1M,
|
|
537
1154
|
cache_write_per_1m: p.cacheWritePer1M,
|
|
1155
|
+
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1156
|
+
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
538
1157
|
updated_at: now
|
|
539
1158
|
});
|
|
540
1159
|
}
|
|
541
1160
|
}
|
|
1161
|
+
function upsertSubscription(db, sub) {
|
|
1162
|
+
db.prepare(`
|
|
1163
|
+
INSERT OR REPLACE INTO subscriptions
|
|
1164
|
+
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
1165
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1166
|
+
`).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);
|
|
1167
|
+
}
|
|
1168
|
+
function listSubscriptions(db) {
|
|
1169
|
+
return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
|
|
1170
|
+
}
|
|
1171
|
+
function deleteSubscription(db, id) {
|
|
1172
|
+
db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
|
|
1173
|
+
}
|
|
1174
|
+
function upsertUsageSnapshot(db, snap) {
|
|
1175
|
+
const now = snap.updated_at ?? new Date().toISOString();
|
|
1176
|
+
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
1177
|
+
db.prepare(`
|
|
1178
|
+
INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
|
|
1179
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1180
|
+
`).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
|
|
1181
|
+
}
|
|
1182
|
+
function queryUsageSnapshots(db, opts = {}) {
|
|
1183
|
+
const conditions = [];
|
|
1184
|
+
const params = [];
|
|
1185
|
+
if (opts.agent) {
|
|
1186
|
+
conditions.push("agent = ?");
|
|
1187
|
+
params.push(opts.agent);
|
|
1188
|
+
}
|
|
1189
|
+
if (opts.date) {
|
|
1190
|
+
conditions.push("date = ?");
|
|
1191
|
+
params.push(opts.date);
|
|
1192
|
+
}
|
|
1193
|
+
if (opts.since) {
|
|
1194
|
+
conditions.push("date >= ?");
|
|
1195
|
+
params.push(opts.since);
|
|
1196
|
+
}
|
|
1197
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1198
|
+
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1199
|
+
}
|
|
1200
|
+
function listMachineRegistry(db) {
|
|
1201
|
+
return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
|
|
1202
|
+
}
|
|
1203
|
+
function dedupeRequests(db) {
|
|
1204
|
+
const dupes = db.prepare(`
|
|
1205
|
+
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1206
|
+
FROM requests
|
|
1207
|
+
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1208
|
+
GROUP BY source_request_id, agent
|
|
1209
|
+
HAVING cnt > 1
|
|
1210
|
+
`).all();
|
|
1211
|
+
let removed = 0;
|
|
1212
|
+
for (const row of dupes) {
|
|
1213
|
+
const result = db.prepare(`
|
|
1214
|
+
DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
|
|
1215
|
+
`).run(row.source_request_id, row.agent, row.keep_id);
|
|
1216
|
+
removed += result.changes;
|
|
1217
|
+
}
|
|
1218
|
+
return removed;
|
|
1219
|
+
}
|
|
542
1220
|
var init_database = () => {};
|
|
543
1221
|
|
|
1222
|
+
// src/lib/agents.ts
|
|
1223
|
+
var AGENTS = [
|
|
1224
|
+
"claude",
|
|
1225
|
+
"takumi",
|
|
1226
|
+
"codex",
|
|
1227
|
+
"gemini",
|
|
1228
|
+
"opencode",
|
|
1229
|
+
"cursor",
|
|
1230
|
+
"pi",
|
|
1231
|
+
"hermes"
|
|
1232
|
+
];
|
|
1233
|
+
var COST_BASIS = [
|
|
1234
|
+
"metered_api",
|
|
1235
|
+
"subscription_included",
|
|
1236
|
+
"estimated",
|
|
1237
|
+
"unknown"
|
|
1238
|
+
];
|
|
1239
|
+
function isAgent(value) {
|
|
1240
|
+
return AGENTS.includes(value);
|
|
1241
|
+
}
|
|
544
1242
|
// src/index.ts
|
|
545
1243
|
init_database();
|
|
546
1244
|
init_pricing();
|
|
@@ -548,6 +1246,9 @@ init_pricing();
|
|
|
548
1246
|
// src/lib/gatherer.ts
|
|
549
1247
|
init_database();
|
|
550
1248
|
var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
|
|
1249
|
+
function hasCostData(summary) {
|
|
1250
|
+
return summary.total_usd > 0 || summary.sessions > 0 || summary.requests > 0 || summary.tokens > 0;
|
|
1251
|
+
}
|
|
551
1252
|
var gatherTrainingData = async (options = {}) => {
|
|
552
1253
|
const limit = options.limit ?? 500;
|
|
553
1254
|
const examples = [];
|
|
@@ -557,6 +1258,8 @@ var gatherTrainingData = async (options = {}) => {
|
|
|
557
1258
|
for (const period of periods) {
|
|
558
1259
|
try {
|
|
559
1260
|
const s = querySummary(db, period);
|
|
1261
|
+
if (!hasCostData(s))
|
|
1262
|
+
continue;
|
|
560
1263
|
examples.push({
|
|
561
1264
|
messages: [
|
|
562
1265
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
@@ -729,22 +1432,26 @@ ${goals.map((g) => `- ${g.period} goal (${g.project_path ?? g.agent ?? "global"}
|
|
|
729
1432
|
// src/lib/model-config.ts
|
|
730
1433
|
init_database();
|
|
731
1434
|
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
732
|
-
import { join as join2 } from "path";
|
|
1435
|
+
import { dirname, join as join2 } from "path";
|
|
733
1436
|
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
734
|
-
|
|
1437
|
+
function getModelConfigPath() {
|
|
1438
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join2(getDataDir(), "config.json");
|
|
1439
|
+
}
|
|
735
1440
|
function loadConfig() {
|
|
736
1441
|
try {
|
|
737
|
-
|
|
738
|
-
|
|
1442
|
+
const configPath = getModelConfigPath();
|
|
1443
|
+
if (existsSync2(configPath)) {
|
|
1444
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
739
1445
|
}
|
|
740
1446
|
} catch {}
|
|
741
1447
|
return {};
|
|
742
1448
|
}
|
|
743
1449
|
function saveConfig(config) {
|
|
744
|
-
const
|
|
1450
|
+
const configPath = getModelConfigPath();
|
|
1451
|
+
const dir = dirname(configPath);
|
|
745
1452
|
if (!existsSync2(dir))
|
|
746
1453
|
mkdirSync2(dir, { recursive: true });
|
|
747
|
-
writeFileSync(
|
|
1454
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
748
1455
|
`);
|
|
749
1456
|
}
|
|
750
1457
|
function getActiveModel() {
|
|
@@ -760,16 +1467,56 @@ function clearActiveModel() {
|
|
|
760
1467
|
delete config.activeModel;
|
|
761
1468
|
saveConfig(config);
|
|
762
1469
|
}
|
|
1470
|
+
// src/lib/open-projects.ts
|
|
1471
|
+
init_database();
|
|
1472
|
+
async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
1473
|
+
let listProjects2 = listActiveProjects;
|
|
1474
|
+
if (!listProjects2) {
|
|
1475
|
+
const projectsApi = await import("@hasna/projects");
|
|
1476
|
+
listProjects2 = projectsApi.listProjects;
|
|
1477
|
+
}
|
|
1478
|
+
const projects = listProjects2({ status: "active", limit: 5000 });
|
|
1479
|
+
let imported = 0;
|
|
1480
|
+
let skipped = 0;
|
|
1481
|
+
for (const project of projects) {
|
|
1482
|
+
if (!project.path) {
|
|
1483
|
+
skipped++;
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
upsertProject(db, {
|
|
1487
|
+
id: project.id,
|
|
1488
|
+
path: project.path,
|
|
1489
|
+
name: project.name,
|
|
1490
|
+
description: project.description,
|
|
1491
|
+
tags: project.tags ?? [],
|
|
1492
|
+
created_at: project.created_at
|
|
1493
|
+
});
|
|
1494
|
+
imported++;
|
|
1495
|
+
}
|
|
1496
|
+
return { imported, skipped };
|
|
1497
|
+
}
|
|
763
1498
|
// src/ingest/claude.ts
|
|
764
1499
|
init_database();
|
|
765
1500
|
init_pricing();
|
|
766
1501
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
767
1502
|
import { homedir as homedir2 } from "os";
|
|
768
1503
|
import { join as join3, basename } from "path";
|
|
1504
|
+
|
|
1505
|
+
// src/lib/savings.ts
|
|
1506
|
+
function defaultCostBasisForAgent(agent) {
|
|
1507
|
+
if (agent === "claude")
|
|
1508
|
+
return "metered_api";
|
|
1509
|
+
if (agent === "cursor")
|
|
1510
|
+
return "subscription_included";
|
|
1511
|
+
return "estimated";
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/ingest/claude.ts
|
|
769
1515
|
function autoDetectProject(cwd, projects) {
|
|
770
1516
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
771
1517
|
}
|
|
772
|
-
var
|
|
1518
|
+
var CLAUDE_PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
|
|
1519
|
+
var TAKUMI_PROJECTS_DIR = join3(homedir2(), ".takumi", "projects");
|
|
773
1520
|
function dirNameToPath(dirName) {
|
|
774
1521
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
775
1522
|
}
|
|
@@ -788,30 +1535,37 @@ function collectJsonlFiles(projectDir) {
|
|
|
788
1535
|
walk(projectDir);
|
|
789
1536
|
return files;
|
|
790
1537
|
}
|
|
791
|
-
async function ingestClaude(db, verbose = false,
|
|
792
|
-
|
|
1538
|
+
async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
|
|
1539
|
+
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
1540
|
+
}
|
|
1541
|
+
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
1542
|
+
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
1543
|
+
}
|
|
1544
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
1545
|
+
if (!existsSync3(projectsDir)) {
|
|
793
1546
|
if (verbose)
|
|
794
|
-
console.log(
|
|
1547
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
795
1548
|
return { files: 0, requests: 0, sessions: 0 };
|
|
796
1549
|
}
|
|
1550
|
+
const machineId = getMachineId();
|
|
797
1551
|
let totalFiles = 0;
|
|
798
1552
|
let totalRequests = 0;
|
|
799
1553
|
const touchedSessions = new Set;
|
|
800
1554
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
801
|
-
const projectDirs = readdirSync2(
|
|
1555
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
802
1556
|
for (const projectDirEntry of projectDirs) {
|
|
803
|
-
const projectDirPath = join3(
|
|
1557
|
+
const projectDirPath = join3(projectsDir, projectDirEntry.name);
|
|
804
1558
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
805
1559
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
806
1560
|
for (const filePath of jsonlFiles) {
|
|
807
|
-
const stateKey = filePath.replace(
|
|
1561
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
808
1562
|
let fileMtime = "0";
|
|
809
1563
|
try {
|
|
810
1564
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
811
1565
|
} catch {
|
|
812
1566
|
continue;
|
|
813
1567
|
}
|
|
814
|
-
const processed = getIngestState(db,
|
|
1568
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
815
1569
|
if (processed === fileMtime)
|
|
816
1570
|
continue;
|
|
817
1571
|
let lines;
|
|
@@ -846,26 +1600,38 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
846
1600
|
continue;
|
|
847
1601
|
const inputTokens = usage.input_tokens ?? 0;
|
|
848
1602
|
const outputTokens = usage.output_tokens ?? 0;
|
|
849
|
-
const
|
|
1603
|
+
const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
|
|
1604
|
+
const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
1605
|
+
const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
|
|
850
1606
|
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
851
1607
|
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
852
|
-
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
1608
|
+
if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
|
|
853
1609
|
continue;
|
|
854
|
-
|
|
855
|
-
|
|
1610
|
+
let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
|
|
1611
|
+
costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
|
|
1612
|
+
const serverToolUse = usage.server_tool_use;
|
|
1613
|
+
if (serverToolUse?.web_search_requests) {
|
|
1614
|
+
costUsd += serverToolUse.web_search_requests * 0.01;
|
|
1615
|
+
}
|
|
1616
|
+
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1617
|
+
const reqId = `${agentName}-${sourceRequestId}`;
|
|
856
1618
|
upsertRequest(db, {
|
|
857
1619
|
id: reqId,
|
|
858
|
-
agent:
|
|
1620
|
+
agent: agentName,
|
|
859
1621
|
session_id: sessionId,
|
|
860
1622
|
model,
|
|
861
1623
|
input_tokens: inputTokens,
|
|
862
1624
|
output_tokens: outputTokens,
|
|
863
1625
|
cache_read_tokens: cacheReadTokens,
|
|
864
1626
|
cache_create_tokens: cacheWriteTokens,
|
|
1627
|
+
cache_create_5m_tokens: cacheWrite5mTokens,
|
|
1628
|
+
cache_create_1h_tokens: cacheWrite1hTokens,
|
|
865
1629
|
cost_usd: costUsd,
|
|
1630
|
+
cost_basis: defaultCostBasisForAgent(agentName),
|
|
866
1631
|
duration_ms: 0,
|
|
867
1632
|
timestamp,
|
|
868
|
-
source_request_id:
|
|
1633
|
+
source_request_id: sourceRequestId,
|
|
1634
|
+
machine_id: machineId
|
|
869
1635
|
});
|
|
870
1636
|
if (!touchedSessions.has(sessionId)) {
|
|
871
1637
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -874,14 +1640,15 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
874
1640
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
875
1641
|
const session = {
|
|
876
1642
|
id: sessionId,
|
|
877
|
-
agent:
|
|
1643
|
+
agent: agentName,
|
|
878
1644
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
879
1645
|
project_name: detectedProject ? detectedProject.name : "",
|
|
880
1646
|
started_at: timestamp,
|
|
881
1647
|
ended_at: null,
|
|
882
1648
|
total_cost_usd: 0,
|
|
883
1649
|
total_tokens: 0,
|
|
884
|
-
request_count: 0
|
|
1650
|
+
request_count: 0,
|
|
1651
|
+
machine_id: machineId
|
|
885
1652
|
};
|
|
886
1653
|
upsertSession(db, session);
|
|
887
1654
|
}
|
|
@@ -889,7 +1656,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
889
1656
|
}
|
|
890
1657
|
totalRequests++;
|
|
891
1658
|
}
|
|
892
|
-
setIngestState(db,
|
|
1659
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
893
1660
|
totalFiles++;
|
|
894
1661
|
}
|
|
895
1662
|
}
|
|
@@ -898,79 +1665,365 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
898
1665
|
}
|
|
899
1666
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
900
1667
|
}
|
|
1668
|
+
function applyClaudeModifiers(costUsd, model, usage, entry) {
|
|
1669
|
+
let multiplier = 1;
|
|
1670
|
+
const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
|
|
1671
|
+
if (speed === "fast" && model.includes("opus-4-6")) {
|
|
1672
|
+
multiplier *= 6;
|
|
1673
|
+
}
|
|
1674
|
+
const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
|
|
1675
|
+
if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
|
|
1676
|
+
multiplier *= 1.1;
|
|
1677
|
+
}
|
|
1678
|
+
return costUsd * multiplier;
|
|
1679
|
+
}
|
|
1680
|
+
function supportsClaudeDataResidencyPricing(model) {
|
|
1681
|
+
const normalized = normalizeModelName(model);
|
|
1682
|
+
const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
1683
|
+
if (!match)
|
|
1684
|
+
return false;
|
|
1685
|
+
const major = Number(match[2]);
|
|
1686
|
+
const minor = match[3] ? Number(match[3]) : 0;
|
|
1687
|
+
return major > 4 || major === 4 && minor >= 6;
|
|
1688
|
+
}
|
|
901
1689
|
// src/ingest/codex.ts
|
|
902
1690
|
init_database();
|
|
1691
|
+
init_pricing();
|
|
903
1692
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
904
1693
|
import { homedir as homedir3 } from "os";
|
|
905
1694
|
import { join as join4, basename as basename2 } from "path";
|
|
906
|
-
import { Database as
|
|
907
|
-
var
|
|
908
|
-
var
|
|
1695
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
1696
|
+
var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
|
|
1697
|
+
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
|
|
1698
|
+
var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
|
|
1699
|
+
function codexDbPath() {
|
|
1700
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
1701
|
+
}
|
|
1702
|
+
function codexConfigPath() {
|
|
1703
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
1704
|
+
}
|
|
909
1705
|
function readCodexModel() {
|
|
910
|
-
|
|
911
|
-
|
|
1706
|
+
const configPath = codexConfigPath();
|
|
1707
|
+
if (!existsSync4(configPath))
|
|
1708
|
+
return "gpt-5-codex";
|
|
912
1709
|
try {
|
|
913
|
-
const content = readFileSync3(
|
|
1710
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
914
1711
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
915
|
-
return match?.[1] ?? "gpt-5
|
|
1712
|
+
return match?.[1] ?? "gpt-5-codex";
|
|
916
1713
|
} catch {
|
|
917
|
-
return "gpt-5
|
|
1714
|
+
return "gpt-5-codex";
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function buildThreadQuery(codexDb) {
|
|
1718
|
+
const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
|
|
1719
|
+
const modelSelect = cols.has("model") ? "model" : "NULL AS model";
|
|
1720
|
+
const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
|
|
1721
|
+
const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
|
|
1722
|
+
return `
|
|
1723
|
+
SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
|
|
1724
|
+
${providerSelect}, ${modelSelect}
|
|
1725
|
+
FROM threads WHERE tokens_used > 0
|
|
1726
|
+
`;
|
|
1727
|
+
}
|
|
1728
|
+
function readTokenEvents(rolloutPath) {
|
|
1729
|
+
if (!rolloutPath || !existsSync4(rolloutPath))
|
|
1730
|
+
return [];
|
|
1731
|
+
const events = [];
|
|
1732
|
+
const seen = new Set;
|
|
1733
|
+
for (const line of readFileSync3(rolloutPath, "utf-8").split(`
|
|
1734
|
+
`)) {
|
|
1735
|
+
if (!line.trim())
|
|
1736
|
+
continue;
|
|
1737
|
+
let entry;
|
|
1738
|
+
try {
|
|
1739
|
+
entry = JSON.parse(line);
|
|
1740
|
+
} catch {
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
if (!entry || typeof entry !== "object")
|
|
1744
|
+
continue;
|
|
1745
|
+
const payload = entry["payload"];
|
|
1746
|
+
if (!payload || payload["type"] !== "token_count")
|
|
1747
|
+
continue;
|
|
1748
|
+
const info = payload["info"];
|
|
1749
|
+
const usage = info?.["last_token_usage"];
|
|
1750
|
+
if (!usage)
|
|
1751
|
+
continue;
|
|
1752
|
+
const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
1753
|
+
if (total <= 0)
|
|
1754
|
+
continue;
|
|
1755
|
+
const key = JSON.stringify(usage);
|
|
1756
|
+
if (seen.has(key))
|
|
1757
|
+
continue;
|
|
1758
|
+
seen.add(key);
|
|
1759
|
+
const timestamp = entry["timestamp"];
|
|
1760
|
+
events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
|
|
918
1761
|
}
|
|
1762
|
+
return events;
|
|
1763
|
+
}
|
|
1764
|
+
function fallbackEvents(totalTokens) {
|
|
1765
|
+
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
1766
|
+
return [{
|
|
1767
|
+
usage: {
|
|
1768
|
+
input_tokens: inputTokens,
|
|
1769
|
+
cached_input_tokens: 0,
|
|
1770
|
+
output_tokens: totalTokens - inputTokens,
|
|
1771
|
+
total_tokens: totalTokens
|
|
1772
|
+
}
|
|
1773
|
+
}];
|
|
919
1774
|
}
|
|
920
1775
|
async function ingestCodex(db, verbose = false) {
|
|
921
|
-
|
|
1776
|
+
const dbPath = codexDbPath();
|
|
1777
|
+
if (!existsSync4(dbPath)) {
|
|
922
1778
|
if (verbose)
|
|
923
|
-
console.log("Codex DB not found:",
|
|
924
|
-
return { sessions: 0 };
|
|
1779
|
+
console.log("Codex DB not found:", dbPath);
|
|
1780
|
+
return { sessions: 0, requests: 0 };
|
|
925
1781
|
}
|
|
1782
|
+
const machineId = getMachineId();
|
|
926
1783
|
let codexDb = null;
|
|
927
1784
|
let ingested = 0;
|
|
1785
|
+
let requests = 0;
|
|
928
1786
|
try {
|
|
929
|
-
codexDb = new
|
|
930
|
-
const threads = codexDb.prepare(
|
|
1787
|
+
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
1788
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
931
1789
|
for (const thread of threads) {
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
|
|
1790
|
+
const model = thread.model ?? readCodexModel();
|
|
1791
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
1792
|
+
const processed = getIngestState(db, "codex", thread.id);
|
|
1793
|
+
if (processed === stateValue)
|
|
935
1794
|
continue;
|
|
936
|
-
const costUsd = 0;
|
|
937
1795
|
const projectPath = thread.cwd ?? "";
|
|
938
1796
|
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
1797
|
+
const sessionId = `codex-${thread.id}`;
|
|
939
1798
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
940
1799
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
941
1800
|
upsertSession(db, {
|
|
942
|
-
id:
|
|
1801
|
+
id: sessionId,
|
|
943
1802
|
agent: "codex",
|
|
944
1803
|
project_path: projectPath,
|
|
945
1804
|
project_name: projectName,
|
|
946
1805
|
started_at: startedAt,
|
|
947
1806
|
ended_at: endedAt,
|
|
948
|
-
total_cost_usd:
|
|
949
|
-
total_tokens:
|
|
950
|
-
request_count:
|
|
1807
|
+
total_cost_usd: 0,
|
|
1808
|
+
total_tokens: 0,
|
|
1809
|
+
request_count: 0,
|
|
1810
|
+
machine_id: machineId
|
|
951
1811
|
});
|
|
952
|
-
|
|
1812
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
1813
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
1814
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
1815
|
+
tokenEvents.forEach((event, index) => {
|
|
1816
|
+
const usage = event.usage;
|
|
1817
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
1818
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
1819
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
1820
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
1821
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
1822
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
1823
|
+
const requestId = `${sessionId}-${index}`;
|
|
1824
|
+
upsertRequest(db, {
|
|
1825
|
+
id: requestId,
|
|
1826
|
+
agent: "codex",
|
|
1827
|
+
session_id: sessionId,
|
|
1828
|
+
model,
|
|
1829
|
+
input_tokens: inputTokens,
|
|
1830
|
+
output_tokens: outputTokens,
|
|
1831
|
+
cache_read_tokens: cacheReadTokens,
|
|
1832
|
+
cache_create_tokens: 0,
|
|
1833
|
+
cost_usd: costUsd,
|
|
1834
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
1835
|
+
duration_ms: 0,
|
|
1836
|
+
timestamp,
|
|
1837
|
+
source_request_id: requestId,
|
|
1838
|
+
machine_id: machineId
|
|
1839
|
+
});
|
|
1840
|
+
requests++;
|
|
1841
|
+
});
|
|
1842
|
+
rollupSession(db, sessionId);
|
|
1843
|
+
setIngestState(db, "codex", thread.id, stateValue);
|
|
953
1844
|
ingested++;
|
|
954
1845
|
if (verbose)
|
|
955
|
-
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens
|
|
1846
|
+
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
|
|
956
1847
|
}
|
|
957
1848
|
} finally {
|
|
958
1849
|
codexDb?.close();
|
|
959
1850
|
}
|
|
960
|
-
return { sessions: ingested };
|
|
1851
|
+
return { sessions: ingested, requests };
|
|
1852
|
+
}
|
|
1853
|
+
// src/ingest/gemini.ts
|
|
1854
|
+
init_database();
|
|
1855
|
+
init_pricing();
|
|
1856
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
1857
|
+
import { homedir as homedir4 } from "os";
|
|
1858
|
+
import { join as join5, basename as basename3 } from "path";
|
|
1859
|
+
var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
|
|
1860
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir4(), ".gemini", "history");
|
|
1861
|
+
function geminiTmpDir() {
|
|
1862
|
+
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
1863
|
+
}
|
|
1864
|
+
function geminiHistoryDir() {
|
|
1865
|
+
return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
|
|
1866
|
+
}
|
|
1867
|
+
function numberField(...values) {
|
|
1868
|
+
for (const value of values) {
|
|
1869
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1870
|
+
return value;
|
|
1871
|
+
}
|
|
1872
|
+
return 0;
|
|
1873
|
+
}
|
|
1874
|
+
function listProjectDirs(...roots) {
|
|
1875
|
+
const dirs = new Set;
|
|
1876
|
+
for (const root of roots) {
|
|
1877
|
+
if (!existsSync5(root))
|
|
1878
|
+
continue;
|
|
1879
|
+
try {
|
|
1880
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
1881
|
+
if (entry.isDirectory())
|
|
1882
|
+
dirs.add(join5(root, entry.name));
|
|
1883
|
+
}
|
|
1884
|
+
} catch {}
|
|
1885
|
+
}
|
|
1886
|
+
return [...dirs];
|
|
1887
|
+
}
|
|
1888
|
+
function projectRoot(projectDir, chatData) {
|
|
1889
|
+
if (chatData.projectPath)
|
|
1890
|
+
return chatData.projectPath;
|
|
1891
|
+
if (chatData.project_path)
|
|
1892
|
+
return chatData.project_path;
|
|
1893
|
+
const rootFile = join5(projectDir, ".project_root");
|
|
1894
|
+
try {
|
|
1895
|
+
if (existsSync5(rootFile))
|
|
1896
|
+
return readFileSync4(rootFile, "utf-8").trim();
|
|
1897
|
+
} catch {}
|
|
1898
|
+
return "";
|
|
1899
|
+
}
|
|
1900
|
+
async function ingestGemini(db, verbose) {
|
|
1901
|
+
const tmpDir = geminiTmpDir();
|
|
1902
|
+
const historyDir = geminiHistoryDir();
|
|
1903
|
+
if (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
|
|
1904
|
+
if (verbose)
|
|
1905
|
+
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
1906
|
+
return { sessions: 0, requests: 0 };
|
|
1907
|
+
}
|
|
1908
|
+
const machineId = getMachineId();
|
|
1909
|
+
let totalSessions = 0;
|
|
1910
|
+
let totalRequests = 0;
|
|
1911
|
+
const touchedSessions = new Set;
|
|
1912
|
+
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
1913
|
+
for (const projectDir of projectDirs) {
|
|
1914
|
+
const chatsDir = join5(projectDir, "chats");
|
|
1915
|
+
if (!existsSync5(chatsDir))
|
|
1916
|
+
continue;
|
|
1917
|
+
let chatFiles = [];
|
|
1918
|
+
try {
|
|
1919
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
|
|
1920
|
+
} catch {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
for (const filePath of chatFiles) {
|
|
1924
|
+
const stateKey = filePath.replace(homedir4(), "~");
|
|
1925
|
+
let fileMtime = "0";
|
|
1926
|
+
try {
|
|
1927
|
+
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
1928
|
+
} catch {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const processed = getIngestState(db, "gemini", stateKey);
|
|
1932
|
+
if (processed === fileMtime)
|
|
1933
|
+
continue;
|
|
1934
|
+
let chatData;
|
|
1935
|
+
try {
|
|
1936
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
1937
|
+
} catch {
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
|
|
1941
|
+
if (!sessionId)
|
|
1942
|
+
continue;
|
|
1943
|
+
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
1944
|
+
const projectPath = projectRoot(projectDir, chatData);
|
|
1945
|
+
const projectName = projectPath ? basename3(projectPath) : "";
|
|
1946
|
+
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
1947
|
+
if (!existing) {
|
|
1948
|
+
const session = {
|
|
1949
|
+
id: sessionId,
|
|
1950
|
+
agent: "gemini",
|
|
1951
|
+
project_path: projectPath,
|
|
1952
|
+
project_name: projectName,
|
|
1953
|
+
started_at: startTime,
|
|
1954
|
+
ended_at: chatData.lastUpdated ?? null,
|
|
1955
|
+
total_cost_usd: 0,
|
|
1956
|
+
total_tokens: 0,
|
|
1957
|
+
request_count: 0,
|
|
1958
|
+
machine_id: machineId
|
|
1959
|
+
};
|
|
1960
|
+
upsertSession(db, session);
|
|
1961
|
+
totalSessions++;
|
|
1962
|
+
}
|
|
1963
|
+
touchedSessions.add(sessionId);
|
|
1964
|
+
for (const [index, message] of (chatData.messages ?? []).entries()) {
|
|
1965
|
+
const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
|
|
1966
|
+
if (!usage)
|
|
1967
|
+
continue;
|
|
1968
|
+
const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
|
|
1969
|
+
if (!model)
|
|
1970
|
+
continue;
|
|
1971
|
+
const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
|
|
1972
|
+
const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
|
|
1973
|
+
const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
|
|
1974
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
1975
|
+
const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
|
|
1976
|
+
const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
|
|
1977
|
+
const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
|
|
1978
|
+
if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
|
|
1979
|
+
continue;
|
|
1980
|
+
const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
1981
|
+
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
1982
|
+
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
1983
|
+
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
1984
|
+
upsertRequest(db, {
|
|
1985
|
+
id: requestId,
|
|
1986
|
+
agent: "gemini",
|
|
1987
|
+
session_id: sessionId,
|
|
1988
|
+
model,
|
|
1989
|
+
input_tokens: inputTokens,
|
|
1990
|
+
output_tokens: outputTokens,
|
|
1991
|
+
cache_read_tokens: cacheReadTokens,
|
|
1992
|
+
cache_create_tokens: 0,
|
|
1993
|
+
cost_usd: costUsd,
|
|
1994
|
+
cost_basis: defaultCostBasisForAgent("gemini"),
|
|
1995
|
+
duration_ms: 0,
|
|
1996
|
+
timestamp,
|
|
1997
|
+
source_request_id: message.id ?? requestId,
|
|
1998
|
+
machine_id: machineId
|
|
1999
|
+
});
|
|
2000
|
+
totalRequests++;
|
|
2001
|
+
}
|
|
2002
|
+
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
for (const sessionId of touchedSessions) {
|
|
2006
|
+
rollupSession(db, sessionId);
|
|
2007
|
+
}
|
|
2008
|
+
return { sessions: totalSessions, requests: totalRequests };
|
|
961
2009
|
}
|
|
962
2010
|
export {
|
|
2011
|
+
upsertUsageSnapshot,
|
|
2012
|
+
upsertSubscription,
|
|
963
2013
|
upsertSession,
|
|
964
2014
|
upsertRequest,
|
|
965
2015
|
upsertProject,
|
|
966
2016
|
upsertModelPricing,
|
|
967
2017
|
upsertGoal,
|
|
968
2018
|
upsertBudget,
|
|
2019
|
+
upsertBillingDaily,
|
|
2020
|
+
syncOpenProjectsRegistry,
|
|
969
2021
|
setIngestState,
|
|
970
2022
|
setActiveModel,
|
|
971
2023
|
seedModelPricing,
|
|
972
2024
|
rollupSession,
|
|
973
2025
|
readCodexModel,
|
|
2026
|
+
queryUsageSnapshots,
|
|
974
2027
|
queryTopSessions,
|
|
975
2028
|
querySummary,
|
|
976
2029
|
querySessions,
|
|
@@ -978,18 +2031,27 @@ export {
|
|
|
978
2031
|
queryProjectBreakdown,
|
|
979
2032
|
queryModelBreakdown,
|
|
980
2033
|
queryDailyBreakdown,
|
|
2034
|
+
queryBillingSummary,
|
|
981
2035
|
openDatabase,
|
|
982
2036
|
normalizeModelName,
|
|
2037
|
+
listSubscriptions,
|
|
983
2038
|
listProjects,
|
|
984
2039
|
listModelPricing,
|
|
2040
|
+
listMachines,
|
|
2041
|
+
listMachineRegistry,
|
|
985
2042
|
listGoals,
|
|
986
2043
|
listBudgets,
|
|
2044
|
+
isAgent,
|
|
2045
|
+
ingestTakumi,
|
|
2046
|
+
ingestJsonlProjects,
|
|
2047
|
+
ingestGemini,
|
|
987
2048
|
ingestCodex,
|
|
988
2049
|
ingestClaude,
|
|
989
2050
|
getProject,
|
|
990
2051
|
getPricingFromDb,
|
|
991
2052
|
getPricing,
|
|
992
2053
|
getModelPricing,
|
|
2054
|
+
getMachineId,
|
|
993
2055
|
getIngestState,
|
|
994
2056
|
getGoalStatuses,
|
|
995
2057
|
getDbPath,
|
|
@@ -998,13 +2060,18 @@ export {
|
|
|
998
2060
|
getActiveModel,
|
|
999
2061
|
gatherTrainingData,
|
|
1000
2062
|
ensurePricingSeeded,
|
|
2063
|
+
deleteSubscription,
|
|
1001
2064
|
deleteProject,
|
|
1002
2065
|
deleteModelPricing,
|
|
1003
2066
|
deleteGoal,
|
|
1004
2067
|
deleteBudget,
|
|
2068
|
+
dedupeRequests,
|
|
1005
2069
|
computeCostFromDb,
|
|
1006
2070
|
computeCost,
|
|
2071
|
+
clearBillingRange,
|
|
1007
2072
|
clearActiveModel,
|
|
1008
2073
|
DEFAULT_PRICING,
|
|
1009
|
-
DEFAULT_MODEL
|
|
2074
|
+
DEFAULT_MODEL,
|
|
2075
|
+
COST_BASIS,
|
|
2076
|
+
AGENTS
|
|
1010
2077
|
};
|