@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/mcp/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
|
-
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
5
4
|
var __returnValue = (v) => v;
|
|
6
5
|
function __exportSetter(name, newValue) {
|
|
7
6
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -30,88 +29,503 @@ __export(exports_pricing, {
|
|
|
30
29
|
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
31
30
|
});
|
|
32
31
|
function normalizeModelName(raw) {
|
|
33
|
-
return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "")
|
|
32
|
+
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
33
|
+
}
|
|
34
|
+
function normalizeModelNamePreservingProvider(raw) {
|
|
35
|
+
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
36
|
+
}
|
|
37
|
+
function modelLookupKeys(raw) {
|
|
38
|
+
const withProvider = normalizeModelNamePreservingProvider(raw);
|
|
39
|
+
const withoutProvider = normalizeModelName(raw);
|
|
40
|
+
return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
|
|
41
|
+
}
|
|
42
|
+
function bestPrefixMatch(normalized, entries) {
|
|
43
|
+
let best = null;
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const [key] = entry;
|
|
46
|
+
if (normalized !== key && !normalized.startsWith(`${key}-`))
|
|
47
|
+
continue;
|
|
48
|
+
if (!best || key.length > best[0].length)
|
|
49
|
+
best = entry;
|
|
50
|
+
}
|
|
51
|
+
return best?.[1] ?? null;
|
|
52
|
+
}
|
|
53
|
+
function bestModelMatch(model, entries) {
|
|
54
|
+
for (const key of modelLookupKeys(model)) {
|
|
55
|
+
const match = bestPrefixMatch(key, entries);
|
|
56
|
+
if (match)
|
|
57
|
+
return match;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function exactModelMatch(model, entries) {
|
|
62
|
+
for (const key of modelLookupKeys(model)) {
|
|
63
|
+
const match = entries.find(([entryKey]) => entryKey === key);
|
|
64
|
+
if (match)
|
|
65
|
+
return match[1];
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
34
68
|
}
|
|
35
69
|
function ensurePricingSeeded(db) {
|
|
36
70
|
seedModelPricing(db, DEFAULT_PRICING);
|
|
71
|
+
repairLegacySeededPricing(db);
|
|
72
|
+
repairMissingDefaultCacheWrite1h(db);
|
|
73
|
+
repairMissingDefaultCacheStorage(db);
|
|
74
|
+
removeDeprecatedDefaultPricing(db);
|
|
75
|
+
}
|
|
76
|
+
function repairLegacySeededPricing(db) {
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
const legacyModels = new Set([
|
|
79
|
+
...Object.keys(LEGACY_DEFAULT_PRICING),
|
|
80
|
+
...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
|
|
81
|
+
]);
|
|
82
|
+
for (const model of legacyModels) {
|
|
83
|
+
const current = getModelPricing(db, model);
|
|
84
|
+
const next = DEFAULT_PRICING[model];
|
|
85
|
+
if (!current || !next)
|
|
86
|
+
continue;
|
|
87
|
+
const legacy = LEGACY_DEFAULT_PRICING[model];
|
|
88
|
+
const legacyRows = [
|
|
89
|
+
...legacy ? [legacy] : [],
|
|
90
|
+
...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
|
|
91
|
+
];
|
|
92
|
+
if (!legacyRows.some((row) => samePricing(current, row)))
|
|
93
|
+
continue;
|
|
94
|
+
upsertModelPricing(db, {
|
|
95
|
+
model,
|
|
96
|
+
input_per_1m: next.inputPer1M,
|
|
97
|
+
output_per_1m: next.outputPer1M,
|
|
98
|
+
cache_read_per_1m: next.cacheReadPer1M,
|
|
99
|
+
cache_write_per_1m: next.cacheWritePer1M,
|
|
100
|
+
cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
|
|
101
|
+
cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
|
|
102
|
+
updated_at: now
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function repairMissingDefaultCacheWrite1h(db) {
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
109
|
+
if (!next.cacheWrite1hPer1M)
|
|
110
|
+
continue;
|
|
111
|
+
const current = getModelPricing(db, model);
|
|
112
|
+
if (!current)
|
|
113
|
+
continue;
|
|
114
|
+
if ((current.cache_write_1h_per_1m ?? 0) !== 0)
|
|
115
|
+
continue;
|
|
116
|
+
if (!sameBasePricing(current, next))
|
|
117
|
+
continue;
|
|
118
|
+
upsertModelPricing(db, {
|
|
119
|
+
model,
|
|
120
|
+
input_per_1m: current.input_per_1m,
|
|
121
|
+
output_per_1m: current.output_per_1m,
|
|
122
|
+
cache_read_per_1m: current.cache_read_per_1m,
|
|
123
|
+
cache_write_per_1m: current.cache_write_per_1m,
|
|
124
|
+
cache_write_1h_per_1m: next.cacheWrite1hPer1M,
|
|
125
|
+
cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
|
|
126
|
+
updated_at: now
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function repairMissingDefaultCacheStorage(db) {
|
|
131
|
+
const now = new Date().toISOString();
|
|
132
|
+
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
133
|
+
if (!next.cacheStoragePer1MHour)
|
|
134
|
+
continue;
|
|
135
|
+
const current = getModelPricing(db, model);
|
|
136
|
+
if (!current)
|
|
137
|
+
continue;
|
|
138
|
+
if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
|
|
139
|
+
continue;
|
|
140
|
+
if (!sameBasePricing(current, next))
|
|
141
|
+
continue;
|
|
142
|
+
upsertModelPricing(db, {
|
|
143
|
+
model,
|
|
144
|
+
input_per_1m: current.input_per_1m,
|
|
145
|
+
output_per_1m: current.output_per_1m,
|
|
146
|
+
cache_read_per_1m: current.cache_read_per_1m,
|
|
147
|
+
cache_write_per_1m: current.cache_write_per_1m,
|
|
148
|
+
cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
|
|
149
|
+
cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
|
|
150
|
+
updated_at: now
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function removeDeprecatedDefaultPricing(db) {
|
|
155
|
+
for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
|
|
156
|
+
const current = getModelPricing(db, model);
|
|
157
|
+
if (!current)
|
|
158
|
+
continue;
|
|
159
|
+
if (!removedRows.some((row) => samePricing(current, row)))
|
|
160
|
+
continue;
|
|
161
|
+
deleteModelPricing(db, model);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function sameBasePricing(row, pricing) {
|
|
165
|
+
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;
|
|
166
|
+
}
|
|
167
|
+
function samePricing(row, pricing) {
|
|
168
|
+
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);
|
|
37
169
|
}
|
|
38
170
|
function getPricingFromDb(db, model) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
46
|
-
cacheWritePer1M: row.cache_write_per_1m
|
|
47
|
-
};
|
|
171
|
+
if (isFreeModel(model))
|
|
172
|
+
return FREE_PRICING;
|
|
173
|
+
for (const key of modelLookupKeys(model)) {
|
|
174
|
+
const row = getModelPricing(db, key);
|
|
175
|
+
if (row)
|
|
176
|
+
return modelPricingFromDbRow(row);
|
|
48
177
|
}
|
|
49
178
|
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
179
|
+
const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
|
|
180
|
+
if (!match)
|
|
181
|
+
return null;
|
|
182
|
+
return modelPricingFromDbRow(match);
|
|
183
|
+
}
|
|
184
|
+
function modelPricingFromDbRow(row) {
|
|
185
|
+
const seeded = DEFAULT_PRICING[row.model];
|
|
186
|
+
const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
|
|
187
|
+
return {
|
|
188
|
+
inputPer1M: row.input_per_1m,
|
|
189
|
+
outputPer1M: row.output_per_1m,
|
|
190
|
+
cacheReadPer1M: row.cache_read_per_1m,
|
|
191
|
+
cacheWritePer1M: row.cache_write_per_1m,
|
|
192
|
+
cacheWrite1hPer1M,
|
|
193
|
+
cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
|
|
194
|
+
};
|
|
56
195
|
}
|
|
57
196
|
function getPricing(model) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for (const key of Object.keys(DEFAULT_PRICING)) {
|
|
62
|
-
if (normalized.startsWith(key))
|
|
63
|
-
return DEFAULT_PRICING[key] ?? null;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
197
|
+
if (isFreeModel(model))
|
|
198
|
+
return FREE_PRICING;
|
|
199
|
+
return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
|
|
66
200
|
}
|
|
67
|
-
function
|
|
201
|
+
function isFreeModel(model) {
|
|
202
|
+
return model.trim().toLowerCase().endsWith(":free");
|
|
203
|
+
}
|
|
204
|
+
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
68
205
|
const pricing = getPricing(model);
|
|
69
206
|
if (!pricing)
|
|
70
207
|
return 0;
|
|
71
|
-
return (
|
|
208
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
72
209
|
}
|
|
73
|
-
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
210
|
+
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
74
211
|
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
75
212
|
if (!pricing)
|
|
76
213
|
return 0;
|
|
77
|
-
return (
|
|
214
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
215
|
+
}
|
|
216
|
+
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
217
|
+
if (isFreeModel(model))
|
|
218
|
+
return 0;
|
|
219
|
+
let effective = pricing;
|
|
220
|
+
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));
|
|
221
|
+
if (promptTier) {
|
|
222
|
+
const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
|
|
223
|
+
if (billablePromptTokens > promptTier.threshold) {
|
|
224
|
+
effective = {
|
|
225
|
+
...pricing,
|
|
226
|
+
inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
|
|
227
|
+
outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
|
|
228
|
+
cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
|
|
229
|
+
cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
78
234
|
}
|
|
79
|
-
var DEFAULT_PRICING;
|
|
235
|
+
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;
|
|
80
236
|
var init_pricing = __esm(() => {
|
|
81
237
|
init_database();
|
|
82
238
|
DEFAULT_PRICING = {
|
|
83
|
-
"claude-opus-4-
|
|
84
|
-
"claude-opus-4-
|
|
85
|
-
"claude-
|
|
86
|
-
"claude-
|
|
87
|
-
"claude-
|
|
88
|
-
"claude-
|
|
239
|
+
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
240
|
+
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
241
|
+
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
242
|
+
"claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
243
|
+
"claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
244
|
+
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
245
|
+
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
246
|
+
"claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
247
|
+
"claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
248
|
+
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
|
|
249
|
+
"claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
|
|
250
|
+
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
251
|
+
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
|
|
252
|
+
"gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
253
|
+
"gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
254
|
+
"gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
255
|
+
"gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
256
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
257
|
+
"gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
258
|
+
"gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
259
|
+
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
260
|
+
"gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
261
|
+
"google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
|
|
262
|
+
"google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
263
|
+
"google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
264
|
+
"google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
|
|
265
|
+
"google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
|
|
266
|
+
"google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
|
|
267
|
+
"google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
|
|
268
|
+
"gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
269
|
+
"gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
270
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
271
|
+
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
272
|
+
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
273
|
+
"gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
274
|
+
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
275
|
+
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
276
|
+
"gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
277
|
+
"gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
278
|
+
"gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
279
|
+
"gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
|
|
280
|
+
"gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
281
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
282
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
283
|
+
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
284
|
+
"o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
285
|
+
o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
286
|
+
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
287
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
|
|
288
|
+
"qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
289
|
+
"qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
290
|
+
"qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
291
|
+
"qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
292
|
+
"qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
293
|
+
"qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
294
|
+
"qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
295
|
+
"qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
296
|
+
"qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
297
|
+
"qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
298
|
+
"minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
299
|
+
"minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
300
|
+
"minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
301
|
+
"minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
302
|
+
"minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
303
|
+
"grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
304
|
+
"grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
305
|
+
"grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
306
|
+
"grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
307
|
+
"grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
308
|
+
"grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
309
|
+
"grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
310
|
+
"grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
311
|
+
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
312
|
+
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
|
|
313
|
+
"glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
|
|
314
|
+
"glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
315
|
+
"z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
|
|
316
|
+
"z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
|
|
317
|
+
"kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
|
|
318
|
+
"kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
|
|
319
|
+
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
320
|
+
"moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
321
|
+
"moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
|
|
322
|
+
"moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
323
|
+
};
|
|
324
|
+
LEGACY_DEFAULT_PRICING = {
|
|
89
325
|
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
90
|
-
"claude-
|
|
91
|
-
"
|
|
92
|
-
"
|
|
326
|
+
"claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
327
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
|
|
328
|
+
"gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
93
329
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
94
|
-
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
95
|
-
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
96
|
-
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
330
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
98
331
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
99
332
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
100
|
-
"gpt-
|
|
101
|
-
"gpt-
|
|
102
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
333
|
+
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
334
|
+
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
103
335
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
"
|
|
336
|
+
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
337
|
+
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
338
|
+
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
339
|
+
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
340
|
+
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
341
|
+
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
342
|
+
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
343
|
+
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
344
|
+
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
345
|
+
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
|
|
346
|
+
};
|
|
347
|
+
ADDITIONAL_LEGACY_DEFAULT_PRICING = {
|
|
348
|
+
"gemini-2.5-pro": [
|
|
349
|
+
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
350
|
+
],
|
|
351
|
+
"qwen3.6-plus": [
|
|
352
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
353
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
354
|
+
],
|
|
355
|
+
"qwen3.6-flash": [
|
|
356
|
+
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
357
|
+
],
|
|
358
|
+
"qwen3.6-max-preview": [
|
|
359
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
360
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
361
|
+
],
|
|
362
|
+
"qwen/qwen3.6-plus": [
|
|
363
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
364
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
365
|
+
],
|
|
366
|
+
"qwen/qwen3.6-flash": [
|
|
367
|
+
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
368
|
+
],
|
|
369
|
+
"qwen/qwen3.6-max-preview": [
|
|
370
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
371
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
372
|
+
]
|
|
373
|
+
};
|
|
374
|
+
REMOVED_DEFAULT_PRICING = {
|
|
375
|
+
"claude-3-5-sonnet": [
|
|
376
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
377
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
378
|
+
],
|
|
379
|
+
"claude-3-sonnet": [
|
|
380
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
381
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
382
|
+
],
|
|
383
|
+
"gemini-3.1-pro": [
|
|
384
|
+
{ inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
385
|
+
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
|
|
386
|
+
],
|
|
387
|
+
"gemini-1.5-pro": [
|
|
388
|
+
{ inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
389
|
+
],
|
|
390
|
+
"gemini-1.5-flash": [
|
|
391
|
+
{ inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
392
|
+
],
|
|
393
|
+
"gpt-5.3-chat": [
|
|
394
|
+
{ inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
395
|
+
{ inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
|
|
396
|
+
],
|
|
397
|
+
"qwen3.6": [
|
|
398
|
+
{ inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
399
|
+
]
|
|
400
|
+
};
|
|
401
|
+
FREE_PRICING = {
|
|
402
|
+
inputPer1M: 0,
|
|
403
|
+
outputPer1M: 0,
|
|
404
|
+
cacheReadPer1M: 0,
|
|
405
|
+
cacheWritePer1M: 0,
|
|
406
|
+
cacheWrite1hPer1M: 0,
|
|
407
|
+
cacheStoragePer1MHour: 0
|
|
408
|
+
};
|
|
409
|
+
GEMINI_PROMPT_TIERS = {
|
|
410
|
+
"gemini-3.1-pro-preview": {
|
|
411
|
+
threshold: 200000,
|
|
412
|
+
inputPer1M: 4,
|
|
413
|
+
outputPer1M: 18,
|
|
414
|
+
cacheReadPer1M: 0.4
|
|
415
|
+
},
|
|
416
|
+
"gemini-2.5-pro": {
|
|
417
|
+
threshold: 200000,
|
|
418
|
+
inputPer1M: 2.5,
|
|
419
|
+
outputPer1M: 15,
|
|
420
|
+
cacheReadPer1M: 0.25
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
OPENAI_PROMPT_TIERS = {
|
|
424
|
+
"gpt-5.5": {
|
|
425
|
+
threshold: 272000,
|
|
426
|
+
inputMultiplier: 2,
|
|
427
|
+
outputMultiplier: 1.5,
|
|
428
|
+
cacheReadMultiplier: 2
|
|
429
|
+
},
|
|
430
|
+
"gpt-5.4-pro": {
|
|
431
|
+
threshold: 272000,
|
|
432
|
+
inputMultiplier: 2,
|
|
433
|
+
outputMultiplier: 1.5,
|
|
434
|
+
cacheReadMultiplier: 2
|
|
435
|
+
},
|
|
436
|
+
"gpt-5.4": {
|
|
437
|
+
threshold: 272000,
|
|
438
|
+
inputMultiplier: 2,
|
|
439
|
+
outputMultiplier: 1.5,
|
|
440
|
+
cacheReadMultiplier: 2
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
QWEN_PROMPT_TIERS = {
|
|
444
|
+
"qwen3.6-plus": {
|
|
445
|
+
threshold: 256000,
|
|
446
|
+
inputPer1M: 1.3,
|
|
447
|
+
outputPer1M: 3.9,
|
|
448
|
+
cacheReadPer1M: 0.13,
|
|
449
|
+
cacheWritePer1M: 1.625
|
|
450
|
+
},
|
|
451
|
+
"qwen3.6-flash": {
|
|
452
|
+
threshold: 256000,
|
|
453
|
+
inputPer1M: 1,
|
|
454
|
+
outputPer1M: 4,
|
|
455
|
+
cacheReadPer1M: 0.1,
|
|
456
|
+
cacheWritePer1M: 1.25
|
|
457
|
+
},
|
|
458
|
+
"qwen3.6-max-preview": {
|
|
459
|
+
threshold: 128000,
|
|
460
|
+
inputPer1M: 1.6,
|
|
461
|
+
outputPer1M: 9.6,
|
|
462
|
+
cacheReadPer1M: 0.16,
|
|
463
|
+
cacheWritePer1M: 2
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
MINIMAX_PROMPT_TIERS = {
|
|
467
|
+
"minimax/minimax-m1": {
|
|
468
|
+
threshold: Number.POSITIVE_INFINITY
|
|
469
|
+
},
|
|
470
|
+
"minimax-m1": {
|
|
471
|
+
threshold: 200000,
|
|
472
|
+
inputPer1M: 1.3
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
XAI_PROMPT_TIERS = {
|
|
476
|
+
"grok-4.3": {
|
|
477
|
+
threshold: 200000,
|
|
478
|
+
inputPer1M: 2.5,
|
|
479
|
+
outputPer1M: 5,
|
|
480
|
+
cacheReadPer1M: 0.4
|
|
481
|
+
},
|
|
482
|
+
"grok-latest": {
|
|
483
|
+
threshold: 200000,
|
|
484
|
+
inputPer1M: 2.5,
|
|
485
|
+
outputPer1M: 5,
|
|
486
|
+
cacheReadPer1M: 0.4
|
|
487
|
+
},
|
|
488
|
+
"grok-4.20": {
|
|
489
|
+
threshold: 200000,
|
|
490
|
+
inputPer1M: 2.5,
|
|
491
|
+
outputPer1M: 5,
|
|
492
|
+
cacheReadPer1M: 0.4
|
|
493
|
+
},
|
|
494
|
+
"grok-4-1-fast": {
|
|
495
|
+
threshold: 128000,
|
|
496
|
+
inputPer1M: 0.4,
|
|
497
|
+
outputPer1M: 1,
|
|
498
|
+
cacheReadPer1M: 0
|
|
499
|
+
},
|
|
500
|
+
"grok-4-fast": {
|
|
501
|
+
threshold: 128000,
|
|
502
|
+
inputPer1M: 0.4,
|
|
503
|
+
outputPer1M: 1,
|
|
504
|
+
cacheReadPer1M: 0
|
|
505
|
+
},
|
|
506
|
+
"grok-4": {
|
|
507
|
+
threshold: 128000,
|
|
508
|
+
inputPer1M: 6,
|
|
509
|
+
outputPer1M: 30,
|
|
510
|
+
cacheReadPer1M: 0
|
|
511
|
+
}
|
|
107
512
|
};
|
|
108
513
|
});
|
|
109
514
|
|
|
110
515
|
// src/db/database.ts
|
|
111
516
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
112
517
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
518
|
+
import { hostname } from "os";
|
|
113
519
|
import { homedir } from "os";
|
|
114
520
|
import { join } from "path";
|
|
521
|
+
function getMachineId() {
|
|
522
|
+
if (process.env["ECONOMY_MACHINE_ID"])
|
|
523
|
+
return process.env["ECONOMY_MACHINE_ID"];
|
|
524
|
+
const h = hostname().toLowerCase();
|
|
525
|
+
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
526
|
+
return h.split(".")[0];
|
|
527
|
+
return h.split(".")[0];
|
|
528
|
+
}
|
|
115
529
|
function getDataDir() {
|
|
116
530
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
117
531
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -144,6 +558,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
144
558
|
}
|
|
145
559
|
const db = new Database(path);
|
|
146
560
|
db.exec("PRAGMA journal_mode = WAL");
|
|
561
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
147
562
|
db.exec("PRAGMA foreign_keys = ON");
|
|
148
563
|
initSchema(db);
|
|
149
564
|
if (!skipSeed) {
|
|
@@ -162,10 +577,18 @@ function initSchema(db) {
|
|
|
162
577
|
output_tokens INTEGER DEFAULT 0,
|
|
163
578
|
cache_read_tokens INTEGER DEFAULT 0,
|
|
164
579
|
cache_create_tokens INTEGER DEFAULT 0,
|
|
580
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
581
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
165
582
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
166
583
|
duration_ms INTEGER DEFAULT 0,
|
|
167
584
|
timestamp TEXT NOT NULL,
|
|
168
|
-
source_request_id TEXT
|
|
585
|
+
source_request_id TEXT,
|
|
586
|
+
machine_id TEXT DEFAULT '',
|
|
587
|
+
account_key TEXT DEFAULT '',
|
|
588
|
+
account_tool TEXT DEFAULT '',
|
|
589
|
+
account_name TEXT DEFAULT '',
|
|
590
|
+
account_email TEXT DEFAULT '',
|
|
591
|
+
account_source TEXT DEFAULT ''
|
|
169
592
|
);
|
|
170
593
|
|
|
171
594
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -177,7 +600,13 @@ function initSchema(db) {
|
|
|
177
600
|
ended_at TEXT,
|
|
178
601
|
total_cost_usd REAL DEFAULT 0,
|
|
179
602
|
total_tokens INTEGER DEFAULT 0,
|
|
180
|
-
request_count INTEGER DEFAULT 0
|
|
603
|
+
request_count INTEGER DEFAULT 0,
|
|
604
|
+
machine_id TEXT DEFAULT '',
|
|
605
|
+
account_key TEXT DEFAULT '',
|
|
606
|
+
account_tool TEXT DEFAULT '',
|
|
607
|
+
account_name TEXT DEFAULT '',
|
|
608
|
+
account_email TEXT DEFAULT '',
|
|
609
|
+
account_source TEXT DEFAULT ''
|
|
181
610
|
);
|
|
182
611
|
|
|
183
612
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -230,6 +659,8 @@ function initSchema(db) {
|
|
|
230
659
|
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
231
660
|
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
232
661
|
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
662
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
663
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
233
664
|
updated_at TEXT NOT NULL
|
|
234
665
|
);
|
|
235
666
|
|
|
@@ -242,6 +673,127 @@ function initSchema(db) {
|
|
|
242
673
|
machine_id TEXT,
|
|
243
674
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
244
675
|
);
|
|
676
|
+
|
|
677
|
+
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
678
|
+
date TEXT NOT NULL,
|
|
679
|
+
provider TEXT NOT NULL,
|
|
680
|
+
description TEXT DEFAULT '',
|
|
681
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
682
|
+
updated_at TEXT NOT NULL,
|
|
683
|
+
PRIMARY KEY (date, provider, description)
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
687
|
+
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
688
|
+
|
|
689
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
690
|
+
id TEXT PRIMARY KEY,
|
|
691
|
+
agent TEXT,
|
|
692
|
+
provider TEXT NOT NULL,
|
|
693
|
+
plan TEXT NOT NULL,
|
|
694
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
695
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
696
|
+
billing_cycle_start TEXT,
|
|
697
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
698
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
699
|
+
created_at TEXT NOT NULL,
|
|
700
|
+
updated_at TEXT NOT NULL
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
704
|
+
id TEXT PRIMARY KEY,
|
|
705
|
+
agent TEXT NOT NULL,
|
|
706
|
+
date TEXT NOT NULL,
|
|
707
|
+
metric TEXT NOT NULL,
|
|
708
|
+
value REAL NOT NULL DEFAULT 0,
|
|
709
|
+
unit TEXT DEFAULT '',
|
|
710
|
+
machine_id TEXT DEFAULT '',
|
|
711
|
+
updated_at TEXT NOT NULL
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
715
|
+
date TEXT NOT NULL,
|
|
716
|
+
agent TEXT DEFAULT '',
|
|
717
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
718
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
719
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
720
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
721
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
722
|
+
updated_at TEXT NOT NULL,
|
|
723
|
+
PRIMARY KEY (date, agent)
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
727
|
+
machine_id TEXT PRIMARY KEY,
|
|
728
|
+
hostname TEXT NOT NULL,
|
|
729
|
+
last_seen_at TEXT,
|
|
730
|
+
last_push_at TEXT,
|
|
731
|
+
last_pull_at TEXT,
|
|
732
|
+
economy_version TEXT,
|
|
733
|
+
updated_at TEXT NOT NULL
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
737
|
+
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
738
|
+
`);
|
|
739
|
+
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
740
|
+
if (!cols.some((c) => c.name === "machine_id")) {
|
|
741
|
+
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
742
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
743
|
+
}
|
|
744
|
+
if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
|
|
745
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
|
|
746
|
+
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
747
|
+
}
|
|
748
|
+
if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
|
|
749
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
|
|
750
|
+
}
|
|
751
|
+
if (!cols.some((c) => c.name === "cost_basis")) {
|
|
752
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
|
|
753
|
+
}
|
|
754
|
+
if (!cols.some((c) => c.name === "attribution_tag")) {
|
|
755
|
+
db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
756
|
+
}
|
|
757
|
+
if (!cols.some((c) => c.name === "updated_at")) {
|
|
758
|
+
db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
759
|
+
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
760
|
+
}
|
|
761
|
+
if (!cols.some((c) => c.name === "synced_at")) {
|
|
762
|
+
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
763
|
+
}
|
|
764
|
+
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
765
|
+
if (!cols.some((c) => c.name === column)) {
|
|
766
|
+
db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
770
|
+
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
771
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
772
|
+
}
|
|
773
|
+
if (!sessionCols.some((c) => c.name === "updated_at")) {
|
|
774
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
775
|
+
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
776
|
+
}
|
|
777
|
+
if (!sessionCols.some((c) => c.name === "synced_at")) {
|
|
778
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
779
|
+
}
|
|
780
|
+
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
781
|
+
if (!sessionCols.some((c) => c.name === column)) {
|
|
782
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
786
|
+
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
787
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
788
|
+
}
|
|
789
|
+
if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
|
|
790
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
|
|
791
|
+
}
|
|
792
|
+
db.exec(`
|
|
793
|
+
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
794
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
795
|
+
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
796
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
245
797
|
`);
|
|
246
798
|
}
|
|
247
799
|
function periodWhere(period) {
|
|
@@ -251,11 +803,11 @@ function periodWhere(period) {
|
|
|
251
803
|
case "yesterday":
|
|
252
804
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
253
805
|
case "week":
|
|
254
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
806
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
255
807
|
case "month":
|
|
256
|
-
return `timestamp >= DATE('now', '
|
|
808
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
257
809
|
case "year":
|
|
258
|
-
return `timestamp >= DATE('now', '
|
|
810
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
259
811
|
case "all":
|
|
260
812
|
return "1=1";
|
|
261
813
|
}
|
|
@@ -267,31 +819,52 @@ function sessionPeriodWhere(period) {
|
|
|
267
819
|
case "yesterday":
|
|
268
820
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
269
821
|
case "week":
|
|
270
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
822
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
823
|
+
case "month":
|
|
824
|
+
return `started_at >= DATE('now', 'start of month')`;
|
|
825
|
+
case "year":
|
|
826
|
+
return `started_at >= DATE('now', 'start of year')`;
|
|
827
|
+
case "all":
|
|
828
|
+
return "1=1";
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function requestPeriodWhere(period) {
|
|
832
|
+
switch (period) {
|
|
833
|
+
case "today":
|
|
834
|
+
return `DATE(timestamp) = DATE('now')`;
|
|
835
|
+
case "yesterday":
|
|
836
|
+
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
837
|
+
case "week":
|
|
838
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
271
839
|
case "month":
|
|
272
|
-
return `
|
|
840
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
273
841
|
case "year":
|
|
274
|
-
return `
|
|
842
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
275
843
|
case "all":
|
|
276
844
|
return "1=1";
|
|
277
845
|
}
|
|
278
846
|
}
|
|
279
847
|
function upsertRequest(db, req) {
|
|
848
|
+
const now = req.updated_at ?? new Date().toISOString();
|
|
280
849
|
db.prepare(`
|
|
281
850
|
INSERT OR REPLACE INTO requests
|
|
282
851
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
283
|
-
cache_read_tokens, cache_create_tokens,
|
|
284
|
-
timestamp,
|
|
285
|
-
|
|
286
|
-
|
|
852
|
+
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
853
|
+
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
854
|
+
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
855
|
+
account_name, account_email, account_source, updated_at, synced_at)
|
|
856
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
857
|
+
`).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 ?? "");
|
|
287
858
|
}
|
|
288
859
|
function upsertSession(db, session) {
|
|
860
|
+
const now = session.updated_at ?? new Date().toISOString();
|
|
289
861
|
db.prepare(`
|
|
290
862
|
INSERT OR REPLACE INTO sessions
|
|
291
863
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
292
|
-
total_cost_usd, total_tokens, request_count
|
|
293
|
-
|
|
294
|
-
|
|
864
|
+
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
865
|
+
account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
866
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
867
|
+
`).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 ?? "");
|
|
295
868
|
}
|
|
296
869
|
function rollupSession(db, sessionId) {
|
|
297
870
|
db.prepare(`
|
|
@@ -302,9 +875,24 @@ function rollupSession(db, sessionId) {
|
|
|
302
875
|
ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
|
|
303
876
|
started_at = CASE WHEN started_at = '' OR started_at IS NULL
|
|
304
877
|
THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
|
|
305
|
-
ELSE started_at END
|
|
878
|
+
ELSE started_at END,
|
|
879
|
+
account_key = CASE WHEN account_key = '' OR account_key IS NULL
|
|
880
|
+
THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
881
|
+
ELSE account_key END,
|
|
882
|
+
account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
|
|
883
|
+
THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
884
|
+
ELSE account_tool END,
|
|
885
|
+
account_name = CASE WHEN account_name = '' OR account_name IS NULL
|
|
886
|
+
THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
887
|
+
ELSE account_name END,
|
|
888
|
+
account_email = CASE WHEN account_email = '' OR account_email IS NULL
|
|
889
|
+
THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
890
|
+
ELSE account_email END,
|
|
891
|
+
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
892
|
+
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
893
|
+
ELSE account_source END
|
|
306
894
|
WHERE id = ?
|
|
307
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
895
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
308
896
|
}
|
|
309
897
|
function querySessions(db, filter = {}) {
|
|
310
898
|
const conditions = [];
|
|
@@ -317,10 +905,19 @@ function querySessions(db, filter = {}) {
|
|
|
317
905
|
conditions.push("project_path LIKE ?");
|
|
318
906
|
params.push(`%${filter.project}%`);
|
|
319
907
|
}
|
|
908
|
+
if (filter.account) {
|
|
909
|
+
const q = `%${filter.account}%`;
|
|
910
|
+
conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
|
|
911
|
+
params.push(q, q, q);
|
|
912
|
+
}
|
|
320
913
|
if (filter.since) {
|
|
321
914
|
conditions.push("started_at >= ?");
|
|
322
915
|
params.push(filter.since);
|
|
323
916
|
}
|
|
917
|
+
if (filter.machine) {
|
|
918
|
+
conditions.push("machine_id = ?");
|
|
919
|
+
params.push(filter.machine);
|
|
920
|
+
}
|
|
324
921
|
if (filter.search) {
|
|
325
922
|
const q = `%${filter.search}%`;
|
|
326
923
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -339,24 +936,25 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
339
936
|
}
|
|
340
937
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
341
938
|
}
|
|
342
|
-
function querySummary(db, period) {
|
|
939
|
+
function querySummary(db, period, machine, allMachines = false) {
|
|
343
940
|
const rWhere = periodWhere(period);
|
|
344
941
|
const sWhere = sessionPeriodWhere(period);
|
|
942
|
+
const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
345
943
|
const r = db.prepare(`
|
|
346
944
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
347
945
|
COUNT(*) as requests,
|
|
348
946
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
349
|
-
FROM requests WHERE ${rWhere}
|
|
947
|
+
FROM requests WHERE ${rWhere}${machineClause}
|
|
350
948
|
`).get();
|
|
351
949
|
const codexTotals = db.prepare(`
|
|
352
950
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
353
951
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
354
952
|
COUNT(*) as sessions
|
|
355
953
|
FROM sessions
|
|
356
|
-
WHERE ${sWhere}
|
|
954
|
+
WHERE ${sWhere}${machineClause}
|
|
357
955
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
358
956
|
`).get();
|
|
359
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
957
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
|
|
360
958
|
return {
|
|
361
959
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
362
960
|
requests: r.requests,
|
|
@@ -376,23 +974,213 @@ function queryModelBreakdown(db) {
|
|
|
376
974
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
377
975
|
`).all();
|
|
378
976
|
}
|
|
379
|
-
function
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
977
|
+
function queryAgentBreakdown(db, period = "all") {
|
|
978
|
+
const requestWhere = requestPeriodWhere(period);
|
|
979
|
+
const groups = new Map;
|
|
980
|
+
const requestRows = db.prepare(`
|
|
981
|
+
SELECT agent,
|
|
982
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
983
|
+
COUNT(*) as requests,
|
|
984
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
985
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
986
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
987
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
988
|
+
COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
989
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
990
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
991
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
992
|
+
MAX(timestamp) as last_active
|
|
993
|
+
FROM requests
|
|
994
|
+
WHERE ${requestWhere}
|
|
995
|
+
GROUP BY agent
|
|
996
|
+
ORDER BY api_equivalent_usd DESC
|
|
997
|
+
`).all();
|
|
998
|
+
for (const row of requestRows) {
|
|
999
|
+
groups.set(row.agent, row);
|
|
1000
|
+
}
|
|
1001
|
+
const sessionWhere = sessionPeriodWhere(period);
|
|
1002
|
+
const sessionOnlyRows = db.prepare(`
|
|
1003
|
+
SELECT agent,
|
|
1004
|
+
COUNT(*) as sessions,
|
|
1005
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1006
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1007
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1008
|
+
MAX(started_at) as last_active
|
|
1009
|
+
FROM sessions
|
|
1010
|
+
WHERE ${sessionWhere}
|
|
1011
|
+
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1012
|
+
GROUP BY agent
|
|
1013
|
+
`).all();
|
|
1014
|
+
for (const row of sessionOnlyRows) {
|
|
1015
|
+
const existing = groups.get(row.agent) ?? {
|
|
1016
|
+
agent: row.agent,
|
|
1017
|
+
sessions: 0,
|
|
1018
|
+
requests: 0,
|
|
1019
|
+
total_tokens: 0,
|
|
1020
|
+
api_equivalent_usd: 0,
|
|
1021
|
+
billable_usd: 0,
|
|
1022
|
+
metered_api_usd: 0,
|
|
1023
|
+
subscription_included_usd: 0,
|
|
1024
|
+
estimated_usd: 0,
|
|
1025
|
+
unknown_usd: 0,
|
|
1026
|
+
cost_usd: 0,
|
|
1027
|
+
last_active: ""
|
|
1028
|
+
};
|
|
1029
|
+
existing.sessions += row.sessions;
|
|
1030
|
+
existing.requests += row.requests;
|
|
1031
|
+
existing.total_tokens += row.total_tokens;
|
|
1032
|
+
existing.api_equivalent_usd += row.cost_usd;
|
|
1033
|
+
existing.estimated_usd += row.cost_usd;
|
|
1034
|
+
existing.cost_usd += row.cost_usd;
|
|
1035
|
+
if (!existing.last_active || row.last_active > existing.last_active)
|
|
1036
|
+
existing.last_active = row.last_active;
|
|
1037
|
+
groups.set(row.agent, existing);
|
|
1038
|
+
}
|
|
1039
|
+
return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
|
|
1040
|
+
}
|
|
1041
|
+
function labelForPath(projectPath, projectName) {
|
|
1042
|
+
if (projectName && projectName.trim() !== "")
|
|
1043
|
+
return projectName;
|
|
1044
|
+
if (!projectPath)
|
|
1045
|
+
return "";
|
|
1046
|
+
const segments = projectPath.split("/").filter(Boolean);
|
|
1047
|
+
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
1048
|
+
for (const seg of segments) {
|
|
1049
|
+
if (projectPrefix.test(seg))
|
|
1050
|
+
return seg;
|
|
1051
|
+
}
|
|
1052
|
+
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
1053
|
+
for (let i = segments.length - 1;i >= 0; i--) {
|
|
1054
|
+
if (!generic.has(segments[i].toLowerCase()))
|
|
1055
|
+
return segments[i];
|
|
1056
|
+
}
|
|
1057
|
+
return segments[segments.length - 1] ?? projectPath;
|
|
1058
|
+
}
|
|
1059
|
+
function queryProjectBreakdown(db, period = "all") {
|
|
1060
|
+
const where = sessionPeriodWhere(period);
|
|
1061
|
+
const sessions = db.prepare(`
|
|
1062
|
+
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
1063
|
+
FROM sessions
|
|
1064
|
+
WHERE ${where}
|
|
1065
|
+
AND (project_path != '' OR project_name != '')
|
|
1066
|
+
`).all();
|
|
1067
|
+
const groups = new Map;
|
|
1068
|
+
for (const s of sessions) {
|
|
1069
|
+
const label = labelForPath(s.project_path, s.project_name);
|
|
1070
|
+
if (!label)
|
|
1071
|
+
continue;
|
|
1072
|
+
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
1073
|
+
g.sessionIds.push(s.id);
|
|
1074
|
+
g.totalCost += s.total_cost_usd || 0;
|
|
1075
|
+
if (!g.lastActive || s.started_at > g.lastActive)
|
|
1076
|
+
g.lastActive = s.started_at;
|
|
1077
|
+
if (!g.samplePath)
|
|
1078
|
+
g.samplePath = s.project_path;
|
|
1079
|
+
groups.set(label, g);
|
|
1080
|
+
}
|
|
1081
|
+
const result = [];
|
|
1082
|
+
for (const [label, g] of groups.entries()) {
|
|
1083
|
+
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
1084
|
+
const reqStats = placeholders.length ? db.prepare(`
|
|
1085
|
+
SELECT
|
|
1086
|
+
COUNT(*) as requests,
|
|
1087
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1088
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
1089
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
1090
|
+
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
1091
|
+
result.push({
|
|
1092
|
+
project_path: g.samplePath,
|
|
1093
|
+
project_name: label,
|
|
1094
|
+
sessions: g.sessionIds.length,
|
|
1095
|
+
requests: reqStats.requests,
|
|
1096
|
+
total_tokens: reqStats.total_tokens,
|
|
1097
|
+
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
1098
|
+
last_active: g.lastActive
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1102
|
+
return result;
|
|
1103
|
+
}
|
|
1104
|
+
function queryAccountBreakdown(db, period = "all") {
|
|
1105
|
+
const sWhere = sessionPeriodWhere(period);
|
|
1106
|
+
const sessions = db.prepare(`
|
|
1107
|
+
SELECT id, account_key, account_tool, account_name, account_email, account_source,
|
|
1108
|
+
total_cost_usd, total_tokens, request_count, started_at
|
|
1109
|
+
FROM sessions
|
|
1110
|
+
WHERE ${sWhere}
|
|
1111
|
+
AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
|
|
395
1112
|
`).all();
|
|
1113
|
+
const groups = new Map;
|
|
1114
|
+
for (const session of sessions) {
|
|
1115
|
+
const key = session.account_key || `${session.account_tool}:${session.account_name}`;
|
|
1116
|
+
if (!key || key === ":")
|
|
1117
|
+
continue;
|
|
1118
|
+
const group = groups.get(key) ?? {
|
|
1119
|
+
sessionIds: [],
|
|
1120
|
+
account_tool: session.account_tool,
|
|
1121
|
+
account_name: session.account_name,
|
|
1122
|
+
account_email: session.account_email || null,
|
|
1123
|
+
account_source: session.account_source || "unknown",
|
|
1124
|
+
totalCost: 0,
|
|
1125
|
+
totalTokens: 0,
|
|
1126
|
+
requests: 0,
|
|
1127
|
+
lastActive: ""
|
|
1128
|
+
};
|
|
1129
|
+
group.sessionIds.push(session.id);
|
|
1130
|
+
group.totalCost += session.total_cost_usd || 0;
|
|
1131
|
+
group.totalTokens += session.total_tokens || 0;
|
|
1132
|
+
group.requests += session.request_count || 0;
|
|
1133
|
+
if (!group.lastActive || session.started_at > group.lastActive)
|
|
1134
|
+
group.lastActive = session.started_at;
|
|
1135
|
+
groups.set(key, group);
|
|
1136
|
+
}
|
|
1137
|
+
const result = [];
|
|
1138
|
+
for (const [key, group] of groups.entries()) {
|
|
1139
|
+
const placeholders = group.sessionIds.map(() => "?").join(",");
|
|
1140
|
+
const reqStats = placeholders ? db.prepare(`
|
|
1141
|
+
SELECT
|
|
1142
|
+
COUNT(*) as requests,
|
|
1143
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1144
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1145
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1146
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1147
|
+
COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1148
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
|
|
1149
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
1150
|
+
`).get(...group.sessionIds) : {
|
|
1151
|
+
requests: 0,
|
|
1152
|
+
cost_usd: 0,
|
|
1153
|
+
total_tokens: 0,
|
|
1154
|
+
metered_api_usd: 0,
|
|
1155
|
+
subscription_included_usd: 0,
|
|
1156
|
+
estimated_usd: 0,
|
|
1157
|
+
unknown_usd: 0
|
|
1158
|
+
};
|
|
1159
|
+
const hasRequestCosts = reqStats.requests > 0;
|
|
1160
|
+
const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
|
|
1161
|
+
const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
|
|
1162
|
+
const billableUsd = reqStats.metered_api_usd;
|
|
1163
|
+
result.push({
|
|
1164
|
+
account_key: key,
|
|
1165
|
+
account_tool: group.account_tool,
|
|
1166
|
+
account_name: group.account_name,
|
|
1167
|
+
account_email: group.account_email,
|
|
1168
|
+
account_source: group.account_source,
|
|
1169
|
+
sessions: group.sessionIds.length,
|
|
1170
|
+
requests: reqStats.requests || group.requests,
|
|
1171
|
+
total_tokens: reqStats.total_tokens || group.totalTokens,
|
|
1172
|
+
api_equivalent_usd: apiEquivalentUsd,
|
|
1173
|
+
billable_usd: billableUsd,
|
|
1174
|
+
metered_api_usd: reqStats.metered_api_usd,
|
|
1175
|
+
subscription_included_usd: reqStats.subscription_included_usd,
|
|
1176
|
+
estimated_usd: estimatedUsd,
|
|
1177
|
+
unknown_usd: reqStats.unknown_usd,
|
|
1178
|
+
cost_usd: apiEquivalentUsd,
|
|
1179
|
+
last_active: group.lastActive
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1183
|
+
return result;
|
|
396
1184
|
}
|
|
397
1185
|
function queryDailyBreakdown(db, days = 30) {
|
|
398
1186
|
return db.prepare(`
|
|
@@ -403,9 +1191,19 @@ function queryDailyBreakdown(db, days = 30) {
|
|
|
403
1191
|
ORDER BY date ASC
|
|
404
1192
|
`).all(`-${days}`);
|
|
405
1193
|
}
|
|
1194
|
+
function upsertBudget(db, budget) {
|
|
1195
|
+
db.prepare(`
|
|
1196
|
+
INSERT OR REPLACE INTO budgets
|
|
1197
|
+
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1198
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1199
|
+
`).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
|
|
1200
|
+
}
|
|
406
1201
|
function listBudgets(db) {
|
|
407
1202
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
408
1203
|
}
|
|
1204
|
+
function deleteBudget(db, id) {
|
|
1205
|
+
db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
|
|
1206
|
+
}
|
|
409
1207
|
function getBudgetStatuses(db) {
|
|
410
1208
|
const budgets = listBudgets(db);
|
|
411
1209
|
return budgets.map((b) => {
|
|
@@ -479,109 +1277,313 @@ function getIngestState(db, source, key) {
|
|
|
479
1277
|
function setIngestState(db, source, key, value) {
|
|
480
1278
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
481
1279
|
}
|
|
1280
|
+
function queryBillingSummary(db, period) {
|
|
1281
|
+
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";
|
|
1282
|
+
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
1283
|
+
const by_provider = {};
|
|
1284
|
+
let total = 0;
|
|
1285
|
+
for (const r of rows) {
|
|
1286
|
+
by_provider[r.provider] = r.cost;
|
|
1287
|
+
total += r.cost;
|
|
1288
|
+
}
|
|
1289
|
+
return { total_usd: total, by_provider };
|
|
1290
|
+
}
|
|
1291
|
+
function listMachines(db) {
|
|
1292
|
+
return db.prepare(`
|
|
1293
|
+
SELECT
|
|
1294
|
+
s.machine_id,
|
|
1295
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
1296
|
+
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
1297
|
+
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
1298
|
+
MAX(s.started_at) as last_active
|
|
1299
|
+
FROM sessions s
|
|
1300
|
+
WHERE s.machine_id != ''
|
|
1301
|
+
GROUP BY s.machine_id
|
|
1302
|
+
ORDER BY total_cost_usd DESC
|
|
1303
|
+
`).all();
|
|
1304
|
+
}
|
|
482
1305
|
function upsertModelPricing(db, p) {
|
|
483
1306
|
db.prepare(`
|
|
484
1307
|
INSERT OR REPLACE INTO model_pricing
|
|
485
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
|
|
486
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
487
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
|
|
1308
|
+
(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)
|
|
1309
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1310
|
+
`).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);
|
|
488
1311
|
}
|
|
489
1312
|
function getModelPricing(db, model) {
|
|
490
1313
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
491
1314
|
}
|
|
1315
|
+
function listModelPricing(db) {
|
|
1316
|
+
return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
|
|
1317
|
+
}
|
|
1318
|
+
function deleteModelPricing(db, model) {
|
|
1319
|
+
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
1320
|
+
}
|
|
492
1321
|
function seedModelPricing(db, defaults) {
|
|
493
|
-
const existing = db.prepare(`SELECT
|
|
494
|
-
if (existing.count > 0)
|
|
495
|
-
return;
|
|
1322
|
+
const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
|
|
496
1323
|
const now = new Date().toISOString();
|
|
497
1324
|
for (const [model, p] of Object.entries(defaults)) {
|
|
1325
|
+
if (existing.has(model))
|
|
1326
|
+
continue;
|
|
498
1327
|
upsertModelPricing(db, {
|
|
499
1328
|
model,
|
|
500
1329
|
input_per_1m: p.inputPer1M,
|
|
501
1330
|
output_per_1m: p.outputPer1M,
|
|
502
1331
|
cache_read_per_1m: p.cacheReadPer1M,
|
|
503
1332
|
cache_write_per_1m: p.cacheWritePer1M,
|
|
1333
|
+
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1334
|
+
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
504
1335
|
updated_at: now
|
|
505
1336
|
});
|
|
506
1337
|
}
|
|
507
1338
|
}
|
|
1339
|
+
function upsertSubscription(db, sub) {
|
|
1340
|
+
db.prepare(`
|
|
1341
|
+
INSERT OR REPLACE INTO subscriptions
|
|
1342
|
+
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
1343
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1344
|
+
`).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);
|
|
1345
|
+
}
|
|
1346
|
+
function upsertUsageSnapshot(db, snap) {
|
|
1347
|
+
const now = snap.updated_at ?? new Date().toISOString();
|
|
1348
|
+
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
1349
|
+
db.prepare(`
|
|
1350
|
+
INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
|
|
1351
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1352
|
+
`).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
|
|
1353
|
+
}
|
|
1354
|
+
function queryUsageSnapshots(db, opts = {}) {
|
|
1355
|
+
const conditions = [];
|
|
1356
|
+
const params = [];
|
|
1357
|
+
if (opts.agent) {
|
|
1358
|
+
conditions.push("agent = ?");
|
|
1359
|
+
params.push(opts.agent);
|
|
1360
|
+
}
|
|
1361
|
+
if (opts.date) {
|
|
1362
|
+
conditions.push("date = ?");
|
|
1363
|
+
params.push(opts.date);
|
|
1364
|
+
}
|
|
1365
|
+
if (opts.since) {
|
|
1366
|
+
conditions.push("date >= ?");
|
|
1367
|
+
params.push(opts.since);
|
|
1368
|
+
}
|
|
1369
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1370
|
+
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1371
|
+
}
|
|
1372
|
+
function dedupeRequests(db) {
|
|
1373
|
+
const dupes = db.prepare(`
|
|
1374
|
+
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1375
|
+
FROM requests
|
|
1376
|
+
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1377
|
+
GROUP BY source_request_id, agent
|
|
1378
|
+
HAVING cnt > 1
|
|
1379
|
+
`).all();
|
|
1380
|
+
let removed = 0;
|
|
1381
|
+
for (const row of dupes) {
|
|
1382
|
+
const result = db.prepare(`
|
|
1383
|
+
DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
|
|
1384
|
+
`).run(row.source_request_id, row.agent, row.keep_id);
|
|
1385
|
+
removed += result.changes;
|
|
1386
|
+
}
|
|
1387
|
+
return removed;
|
|
1388
|
+
}
|
|
508
1389
|
var init_database = () => {};
|
|
509
1390
|
|
|
510
|
-
//
|
|
511
|
-
var
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1391
|
+
// src/db/pg-migrations.ts
|
|
1392
|
+
var exports_pg_migrations = {};
|
|
1393
|
+
__export(exports_pg_migrations, {
|
|
1394
|
+
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1395
|
+
});
|
|
1396
|
+
var PG_MIGRATIONS;
|
|
1397
|
+
var init_pg_migrations = __esm(() => {
|
|
1398
|
+
PG_MIGRATIONS = [
|
|
1399
|
+
`CREATE TABLE IF NOT EXISTS requests (
|
|
1400
|
+
id TEXT PRIMARY KEY,
|
|
1401
|
+
agent TEXT NOT NULL,
|
|
1402
|
+
session_id TEXT NOT NULL,
|
|
1403
|
+
model TEXT NOT NULL,
|
|
1404
|
+
input_tokens INTEGER DEFAULT 0,
|
|
1405
|
+
output_tokens INTEGER DEFAULT 0,
|
|
1406
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
1407
|
+
cache_create_tokens INTEGER DEFAULT 0,
|
|
1408
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
1409
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
1410
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1411
|
+
duration_ms INTEGER DEFAULT 0,
|
|
1412
|
+
timestamp TEXT NOT NULL,
|
|
1413
|
+
source_request_id TEXT,
|
|
1414
|
+
machine_id TEXT DEFAULT '',
|
|
1415
|
+
account_key TEXT DEFAULT '',
|
|
1416
|
+
account_tool TEXT DEFAULT '',
|
|
1417
|
+
account_name TEXT DEFAULT '',
|
|
1418
|
+
account_email TEXT DEFAULT '',
|
|
1419
|
+
account_source TEXT DEFAULT ''
|
|
1420
|
+
)`,
|
|
1421
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1422
|
+
id TEXT PRIMARY KEY,
|
|
1423
|
+
agent TEXT NOT NULL,
|
|
1424
|
+
project_path TEXT DEFAULT '',
|
|
1425
|
+
project_name TEXT DEFAULT '',
|
|
1426
|
+
started_at TEXT NOT NULL,
|
|
1427
|
+
ended_at TEXT,
|
|
1428
|
+
total_cost_usd REAL DEFAULT 0,
|
|
1429
|
+
total_tokens INTEGER DEFAULT 0,
|
|
1430
|
+
request_count INTEGER DEFAULT 0,
|
|
1431
|
+
machine_id TEXT DEFAULT '',
|
|
1432
|
+
account_key TEXT DEFAULT '',
|
|
1433
|
+
account_tool TEXT DEFAULT '',
|
|
1434
|
+
account_name TEXT DEFAULT '',
|
|
1435
|
+
account_email TEXT DEFAULT '',
|
|
1436
|
+
account_source TEXT DEFAULT ''
|
|
1437
|
+
)`,
|
|
1438
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
1439
|
+
id TEXT PRIMARY KEY,
|
|
1440
|
+
path TEXT UNIQUE NOT NULL,
|
|
1441
|
+
name TEXT NOT NULL,
|
|
1442
|
+
description TEXT,
|
|
1443
|
+
tags TEXT DEFAULT '[]',
|
|
1444
|
+
created_at TEXT NOT NULL
|
|
1445
|
+
)`,
|
|
1446
|
+
`CREATE TABLE IF NOT EXISTS budgets (
|
|
1447
|
+
id TEXT PRIMARY KEY,
|
|
1448
|
+
project_path TEXT,
|
|
1449
|
+
agent TEXT,
|
|
1450
|
+
period TEXT NOT NULL,
|
|
1451
|
+
limit_usd REAL NOT NULL,
|
|
1452
|
+
alert_at_percent INTEGER DEFAULT 80,
|
|
1453
|
+
created_at TEXT NOT NULL,
|
|
1454
|
+
updated_at TEXT NOT NULL
|
|
1455
|
+
)`,
|
|
1456
|
+
`CREATE TABLE IF NOT EXISTS goals (
|
|
1457
|
+
id TEXT PRIMARY KEY,
|
|
1458
|
+
period TEXT NOT NULL,
|
|
1459
|
+
project_path TEXT,
|
|
1460
|
+
agent TEXT,
|
|
1461
|
+
limit_usd REAL NOT NULL,
|
|
1462
|
+
created_at TEXT NOT NULL,
|
|
1463
|
+
updated_at TEXT NOT NULL
|
|
1464
|
+
)`,
|
|
1465
|
+
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
1466
|
+
source TEXT NOT NULL,
|
|
1467
|
+
key TEXT NOT NULL,
|
|
1468
|
+
value TEXT NOT NULL,
|
|
1469
|
+
PRIMARY KEY (source, key)
|
|
1470
|
+
)`,
|
|
1471
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1472
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1473
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1474
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1475
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1476
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1477
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1478
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1479
|
+
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
1480
|
+
model TEXT PRIMARY KEY,
|
|
1481
|
+
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
1482
|
+
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
1483
|
+
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
1484
|
+
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1485
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
1486
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
1487
|
+
updated_at TEXT NOT NULL
|
|
1488
|
+
)`,
|
|
1489
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
|
|
1490
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
|
|
1491
|
+
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
|
|
1492
|
+
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
|
|
1493
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1494
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1495
|
+
message TEXT NOT NULL,
|
|
1496
|
+
email TEXT,
|
|
1497
|
+
category TEXT DEFAULT 'general',
|
|
1498
|
+
version TEXT,
|
|
1499
|
+
machine_id TEXT,
|
|
1500
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1501
|
+
)`,
|
|
1502
|
+
`CREATE TABLE IF NOT EXISTS billing_daily (
|
|
1503
|
+
date TEXT NOT NULL,
|
|
1504
|
+
provider TEXT NOT NULL,
|
|
1505
|
+
description TEXT DEFAULT '',
|
|
1506
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1507
|
+
updated_at TEXT NOT NULL,
|
|
1508
|
+
PRIMARY KEY (date, provider, description)
|
|
1509
|
+
)`,
|
|
1510
|
+
`CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
|
|
1511
|
+
`CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
|
|
1512
|
+
`CREATE TABLE IF NOT EXISTS subscriptions (
|
|
1513
|
+
id TEXT PRIMARY KEY,
|
|
1514
|
+
agent TEXT,
|
|
1515
|
+
provider TEXT NOT NULL,
|
|
1516
|
+
plan TEXT NOT NULL,
|
|
1517
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1518
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
1519
|
+
billing_cycle_start TEXT,
|
|
1520
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
1521
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
1522
|
+
created_at TEXT NOT NULL,
|
|
1523
|
+
updated_at TEXT NOT NULL
|
|
1524
|
+
)`,
|
|
1525
|
+
`CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
1526
|
+
id TEXT PRIMARY KEY,
|
|
1527
|
+
agent TEXT NOT NULL,
|
|
1528
|
+
date TEXT NOT NULL,
|
|
1529
|
+
metric TEXT NOT NULL,
|
|
1530
|
+
value REAL NOT NULL DEFAULT 0,
|
|
1531
|
+
unit TEXT DEFAULT '',
|
|
1532
|
+
machine_id TEXT DEFAULT '',
|
|
1533
|
+
updated_at TEXT NOT NULL
|
|
1534
|
+
)`,
|
|
1535
|
+
`CREATE TABLE IF NOT EXISTS savings_daily (
|
|
1536
|
+
date TEXT NOT NULL,
|
|
1537
|
+
agent TEXT DEFAULT '',
|
|
1538
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
1539
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1540
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
1541
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
1542
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
1543
|
+
updated_at TEXT NOT NULL,
|
|
1544
|
+
PRIMARY KEY (date, agent)
|
|
1545
|
+
)`,
|
|
1546
|
+
`CREATE TABLE IF NOT EXISTS machines (
|
|
1547
|
+
machine_id TEXT PRIMARY KEY,
|
|
1548
|
+
hostname TEXT NOT NULL,
|
|
1549
|
+
last_seen_at TEXT,
|
|
1550
|
+
last_push_at TEXT,
|
|
1551
|
+
last_pull_at TEXT,
|
|
1552
|
+
economy_version TEXT,
|
|
1553
|
+
updated_at TEXT NOT NULL
|
|
1554
|
+
)`,
|
|
1555
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
1556
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1557
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1558
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1559
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1560
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1561
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1562
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1563
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1564
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1565
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1566
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1567
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1568
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1569
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1570
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1571
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1572
|
+
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
1573
|
+
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
|
|
1574
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
|
|
1575
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
|
|
1576
|
+
];
|
|
578
1577
|
});
|
|
579
1578
|
|
|
580
1579
|
// src/mcp/index.ts
|
|
581
1580
|
init_database();
|
|
582
|
-
|
|
1581
|
+
init_pg_migrations();
|
|
1582
|
+
import { randomUUID } from "crypto";
|
|
1583
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
583
1584
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
584
|
-
import {
|
|
1585
|
+
import { registerCloudTools } from "@hasna/cloud";
|
|
1586
|
+
import { z } from "zod";
|
|
585
1587
|
|
|
586
1588
|
// src/ingest/claude.ts
|
|
587
1589
|
init_database();
|
|
@@ -589,12 +1591,240 @@ init_pricing();
|
|
|
589
1591
|
import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
590
1592
|
import { homedir as homedir2 } from "os";
|
|
591
1593
|
import { join as join2, basename } from "path";
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1594
|
+
|
|
1595
|
+
// src/lib/savings.ts
|
|
1596
|
+
function periodWhere2(period, column) {
|
|
1597
|
+
switch (period) {
|
|
1598
|
+
case "today":
|
|
1599
|
+
return `DATE(${column}) = DATE('now')`;
|
|
1600
|
+
case "yesterday":
|
|
1601
|
+
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
1602
|
+
case "week":
|
|
1603
|
+
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1604
|
+
case "month":
|
|
1605
|
+
return `${column} >= DATE('now', 'start of month')`;
|
|
1606
|
+
case "year":
|
|
1607
|
+
return `${column} >= DATE('now', 'start of year')`;
|
|
1608
|
+
case "all":
|
|
1609
|
+
return "1=1";
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
function prorateMonthlyFee(monthlyFee, period) {
|
|
1613
|
+
const now = new Date;
|
|
1614
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1615
|
+
switch (period) {
|
|
1616
|
+
case "today":
|
|
1617
|
+
case "yesterday":
|
|
1618
|
+
return monthlyFee / daysInMonth;
|
|
1619
|
+
case "week":
|
|
1620
|
+
return monthlyFee / daysInMonth * 7;
|
|
1621
|
+
case "month":
|
|
1622
|
+
return monthlyFee;
|
|
1623
|
+
case "year":
|
|
1624
|
+
return monthlyFee * 12;
|
|
1625
|
+
case "all":
|
|
1626
|
+
return monthlyFee;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
|
|
1630
|
+
return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
|
|
1631
|
+
}
|
|
1632
|
+
function querySavingsSummary(db, period, agent) {
|
|
1633
|
+
const where = periodWhere2(period, "timestamp");
|
|
1634
|
+
const agentClause = agent ? " AND agent = ?" : "";
|
|
1635
|
+
const params = agent ? [agent] : [];
|
|
1636
|
+
const apiRow = db.prepare(`
|
|
1637
|
+
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1638
|
+
FROM requests
|
|
1639
|
+
WHERE ${where}${agentClause}
|
|
1640
|
+
AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
|
|
1641
|
+
`).get(...params);
|
|
1642
|
+
const includedRow = db.prepare(`
|
|
1643
|
+
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1644
|
+
FROM requests
|
|
1645
|
+
WHERE ${where}${agentClause}
|
|
1646
|
+
AND cost_basis = 'subscription_included'
|
|
1647
|
+
`).get(...params);
|
|
1648
|
+
const subWhere = periodWhere2(period, "date");
|
|
1649
|
+
const onDemandRow = db.prepare(`
|
|
1650
|
+
SELECT COALESCE(SUM(value), 0) as total
|
|
1651
|
+
FROM usage_snapshots
|
|
1652
|
+
WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
|
|
1653
|
+
AND metric = 'on_demand_usd'
|
|
1654
|
+
`).get(...params);
|
|
1655
|
+
const subs = db.prepare(`
|
|
1656
|
+
SELECT COALESCE(SUM(monthly_fee_usd), 0) as total
|
|
1657
|
+
FROM subscriptions
|
|
1658
|
+
WHERE active = 1${agent ? " AND agent = ?" : ""}
|
|
1659
|
+
`).get(...agent ? [agent] : []);
|
|
1660
|
+
const subscriptionFee = prorateMonthlyFee(subs.total, period);
|
|
1661
|
+
const apiEquivalent = apiRow.total + includedRow.total;
|
|
1662
|
+
const onDemand = onDemandRow.total;
|
|
1663
|
+
const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
|
|
1664
|
+
const byAgent = {};
|
|
1665
|
+
if (!agent) {
|
|
1666
|
+
for (const row of db.prepare(`
|
|
1667
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) as api_eq
|
|
1668
|
+
FROM requests WHERE ${where}
|
|
1669
|
+
GROUP BY agent
|
|
1670
|
+
`).all()) {
|
|
1671
|
+
byAgent[row.agent] = {
|
|
1672
|
+
api_equivalent_usd: row.api_eq,
|
|
1673
|
+
saved_usd: row.api_eq
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
period,
|
|
1679
|
+
api_equivalent_usd: apiEquivalent,
|
|
1680
|
+
subscription_fee_usd: subscriptionFee,
|
|
1681
|
+
included_consumed_usd: includedRow.total,
|
|
1682
|
+
on_demand_usd: onDemand,
|
|
1683
|
+
saved_usd: saved,
|
|
1684
|
+
by_agent: byAgent
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function defaultCostBasisForAgent(agent) {
|
|
1688
|
+
if (agent === "claude")
|
|
1689
|
+
return "metered_api";
|
|
1690
|
+
if (agent === "cursor")
|
|
1691
|
+
return "subscription_included";
|
|
1692
|
+
return "estimated";
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/lib/accounts.ts
|
|
1696
|
+
var AGENT_ACCOUNT_TOOLS = {
|
|
1697
|
+
claude: ["claude"],
|
|
1698
|
+
takumi: ["takumi", "claude"],
|
|
1699
|
+
codex: ["codex"],
|
|
1700
|
+
gemini: ["gemini"],
|
|
1701
|
+
opencode: ["opencode"],
|
|
1702
|
+
cursor: ["cursor"],
|
|
1703
|
+
pi: ["pi"],
|
|
1704
|
+
hermes: ["hermes"]
|
|
1705
|
+
};
|
|
1706
|
+
function accountKey(tool, name) {
|
|
1707
|
+
return `${tool}:${name}`;
|
|
1708
|
+
}
|
|
1709
|
+
function normalizeDir(value) {
|
|
1710
|
+
return value.replace(/\/+$/, "");
|
|
1711
|
+
}
|
|
1712
|
+
function fromProfile(profile, source) {
|
|
1713
|
+
return {
|
|
1714
|
+
account_key: accountKey(profile.tool, profile.name),
|
|
1715
|
+
account_tool: profile.tool,
|
|
1716
|
+
account_name: profile.name,
|
|
1717
|
+
...profile.email ? { account_email: profile.email } : {},
|
|
1718
|
+
account_source: source
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
function fromOverride(raw, agent) {
|
|
1722
|
+
const value = raw.trim();
|
|
1723
|
+
if (!value)
|
|
1724
|
+
return null;
|
|
1725
|
+
const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
|
|
1726
|
+
const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
|
|
1727
|
+
if (!tool || !name)
|
|
1728
|
+
return null;
|
|
1729
|
+
return {
|
|
1730
|
+
account_key: accountKey(tool, name),
|
|
1731
|
+
account_tool: tool,
|
|
1732
|
+
account_name: name,
|
|
1733
|
+
account_source: "override"
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
function envOverride(agent, env) {
|
|
1737
|
+
const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
1738
|
+
const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
|
|
1739
|
+
if (raw)
|
|
1740
|
+
return fromOverride(raw, agent);
|
|
1741
|
+
const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
|
|
1742
|
+
const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
|
|
1743
|
+
if (!tool || !name)
|
|
1744
|
+
return null;
|
|
1745
|
+
return {
|
|
1746
|
+
account_key: accountKey(tool, name),
|
|
1747
|
+
account_tool: tool,
|
|
1748
|
+
account_name: name,
|
|
1749
|
+
account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
|
|
1750
|
+
account_source: "override"
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function knownToolIds(api) {
|
|
1754
|
+
try {
|
|
1755
|
+
return new Set(api.listTools().map((tool) => tool.id));
|
|
1756
|
+
} catch {
|
|
1757
|
+
return new Set;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function profileForEnvDir(api, tool, env) {
|
|
1761
|
+
const configuredDir = env[tool.envVar];
|
|
1762
|
+
if (!configuredDir)
|
|
1763
|
+
return null;
|
|
1764
|
+
const normalized = normalizeDir(configuredDir);
|
|
1765
|
+
try {
|
|
1766
|
+
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
1767
|
+
} catch {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
async function resolveAccountForAgent(agent, env = process.env) {
|
|
1772
|
+
const override = envOverride(agent, env);
|
|
1773
|
+
if (override)
|
|
1774
|
+
return override;
|
|
1775
|
+
let api;
|
|
1776
|
+
try {
|
|
1777
|
+
api = await import("@hasna/accounts");
|
|
1778
|
+
} catch {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
const toolIds = knownToolIds(api);
|
|
1782
|
+
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
1783
|
+
if (!toolIds.has(toolId))
|
|
1784
|
+
continue;
|
|
1785
|
+
let tool;
|
|
1786
|
+
try {
|
|
1787
|
+
tool = api.getTool(toolId);
|
|
1788
|
+
} catch {
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
const envProfile = profileForEnvDir(api, tool, env);
|
|
1792
|
+
if (envProfile)
|
|
1793
|
+
return fromProfile(envProfile, "env");
|
|
1794
|
+
try {
|
|
1795
|
+
const applied = api.appliedProfile(toolId);
|
|
1796
|
+
if (applied)
|
|
1797
|
+
return fromProfile(applied, "applied");
|
|
1798
|
+
} catch {}
|
|
1799
|
+
try {
|
|
1800
|
+
const current = api.currentProfile(toolId);
|
|
1801
|
+
if (current)
|
|
1802
|
+
return fromProfile(current, "current");
|
|
1803
|
+
} catch {}
|
|
1804
|
+
}
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
function withAccount(record, account) {
|
|
1808
|
+
if (!account)
|
|
1809
|
+
return record;
|
|
1810
|
+
return {
|
|
1811
|
+
...record,
|
|
1812
|
+
account_key: account.account_key,
|
|
1813
|
+
account_tool: account.account_tool,
|
|
1814
|
+
account_name: account.account_name,
|
|
1815
|
+
account_email: account.account_email ?? "",
|
|
1816
|
+
account_source: account.account_source
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/ingest/claude.ts
|
|
1821
|
+
function autoDetectProject(cwd, projects) {
|
|
1822
|
+
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
1823
|
+
}
|
|
1824
|
+
var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
1825
|
+
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
1826
|
+
function dirNameToPath(dirName) {
|
|
1827
|
+
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
598
1828
|
}
|
|
599
1829
|
function collectJsonlFiles(projectDir) {
|
|
600
1830
|
const files = [];
|
|
@@ -611,30 +1841,38 @@ function collectJsonlFiles(projectDir) {
|
|
|
611
1841
|
walk(projectDir);
|
|
612
1842
|
return files;
|
|
613
1843
|
}
|
|
614
|
-
async function ingestClaude(db, verbose = false,
|
|
615
|
-
|
|
1844
|
+
async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
|
|
1845
|
+
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
1846
|
+
}
|
|
1847
|
+
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
1848
|
+
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
1849
|
+
}
|
|
1850
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
1851
|
+
if (!existsSync2(projectsDir)) {
|
|
616
1852
|
if (verbose)
|
|
617
|
-
console.log(
|
|
1853
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
618
1854
|
return { files: 0, requests: 0, sessions: 0 };
|
|
619
1855
|
}
|
|
1856
|
+
const machineId = getMachineId();
|
|
620
1857
|
let totalFiles = 0;
|
|
621
1858
|
let totalRequests = 0;
|
|
622
1859
|
const touchedSessions = new Set;
|
|
623
1860
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
624
|
-
const
|
|
1861
|
+
const account = await resolveAccountForAgent(agentName);
|
|
1862
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
625
1863
|
for (const projectDirEntry of projectDirs) {
|
|
626
|
-
const projectDirPath = join2(
|
|
1864
|
+
const projectDirPath = join2(projectsDir, projectDirEntry.name);
|
|
627
1865
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
628
1866
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
629
1867
|
for (const filePath of jsonlFiles) {
|
|
630
|
-
const stateKey = filePath.replace(
|
|
1868
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
631
1869
|
let fileMtime = "0";
|
|
632
1870
|
try {
|
|
633
1871
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
634
1872
|
} catch {
|
|
635
1873
|
continue;
|
|
636
1874
|
}
|
|
637
|
-
const processed = getIngestState(db,
|
|
1875
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
638
1876
|
if (processed === fileMtime)
|
|
639
1877
|
continue;
|
|
640
1878
|
let lines;
|
|
@@ -669,27 +1907,39 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
669
1907
|
continue;
|
|
670
1908
|
const inputTokens = usage.input_tokens ?? 0;
|
|
671
1909
|
const outputTokens = usage.output_tokens ?? 0;
|
|
672
|
-
const
|
|
1910
|
+
const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
|
|
1911
|
+
const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
1912
|
+
const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
|
|
673
1913
|
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
674
1914
|
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
675
|
-
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
1915
|
+
if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
|
|
676
1916
|
continue;
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1917
|
+
let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
|
|
1918
|
+
costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
|
|
1919
|
+
const serverToolUse = usage.server_tool_use;
|
|
1920
|
+
if (serverToolUse?.web_search_requests) {
|
|
1921
|
+
costUsd += serverToolUse.web_search_requests * 0.01;
|
|
1922
|
+
}
|
|
1923
|
+
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1924
|
+
const reqId = `${agentName}-${sourceRequestId}`;
|
|
1925
|
+
upsertRequest(db, withAccount({
|
|
680
1926
|
id: reqId,
|
|
681
|
-
agent:
|
|
1927
|
+
agent: agentName,
|
|
682
1928
|
session_id: sessionId,
|
|
683
1929
|
model,
|
|
684
1930
|
input_tokens: inputTokens,
|
|
685
1931
|
output_tokens: outputTokens,
|
|
686
1932
|
cache_read_tokens: cacheReadTokens,
|
|
687
1933
|
cache_create_tokens: cacheWriteTokens,
|
|
1934
|
+
cache_create_5m_tokens: cacheWrite5mTokens,
|
|
1935
|
+
cache_create_1h_tokens: cacheWrite1hTokens,
|
|
688
1936
|
cost_usd: costUsd,
|
|
1937
|
+
cost_basis: defaultCostBasisForAgent(agentName),
|
|
689
1938
|
duration_ms: 0,
|
|
690
1939
|
timestamp,
|
|
691
|
-
source_request_id:
|
|
692
|
-
|
|
1940
|
+
source_request_id: sourceRequestId,
|
|
1941
|
+
machine_id: machineId
|
|
1942
|
+
}, account));
|
|
693
1943
|
if (!touchedSessions.has(sessionId)) {
|
|
694
1944
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
695
1945
|
if (!existing) {
|
|
@@ -697,22 +1947,23 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
697
1947
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
698
1948
|
const session = {
|
|
699
1949
|
id: sessionId,
|
|
700
|
-
agent:
|
|
1950
|
+
agent: agentName,
|
|
701
1951
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
702
1952
|
project_name: detectedProject ? detectedProject.name : "",
|
|
703
1953
|
started_at: timestamp,
|
|
704
1954
|
ended_at: null,
|
|
705
1955
|
total_cost_usd: 0,
|
|
706
1956
|
total_tokens: 0,
|
|
707
|
-
request_count: 0
|
|
1957
|
+
request_count: 0,
|
|
1958
|
+
machine_id: machineId
|
|
708
1959
|
};
|
|
709
|
-
upsertSession(db, session);
|
|
1960
|
+
upsertSession(db, withAccount(session, account));
|
|
710
1961
|
}
|
|
711
1962
|
touchedSessions.add(sessionId);
|
|
712
1963
|
}
|
|
713
1964
|
totalRequests++;
|
|
714
1965
|
}
|
|
715
|
-
setIngestState(db,
|
|
1966
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
716
1967
|
totalFiles++;
|
|
717
1968
|
}
|
|
718
1969
|
}
|
|
@@ -721,79 +1972,286 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
721
1972
|
}
|
|
722
1973
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
723
1974
|
}
|
|
1975
|
+
function applyClaudeModifiers(costUsd, model, usage, entry) {
|
|
1976
|
+
let multiplier = 1;
|
|
1977
|
+
const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
|
|
1978
|
+
if (speed === "fast" && model.includes("opus-4-6")) {
|
|
1979
|
+
multiplier *= 6;
|
|
1980
|
+
}
|
|
1981
|
+
const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
|
|
1982
|
+
if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
|
|
1983
|
+
multiplier *= 1.1;
|
|
1984
|
+
}
|
|
1985
|
+
return costUsd * multiplier;
|
|
1986
|
+
}
|
|
1987
|
+
function supportsClaudeDataResidencyPricing(model) {
|
|
1988
|
+
const normalized = normalizeModelName(model);
|
|
1989
|
+
const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
1990
|
+
if (!match)
|
|
1991
|
+
return false;
|
|
1992
|
+
const major = Number(match[2]);
|
|
1993
|
+
const minor = match[3] ? Number(match[3]) : 0;
|
|
1994
|
+
return major > 4 || major === 4 && minor >= 6;
|
|
1995
|
+
}
|
|
724
1996
|
|
|
725
1997
|
// src/ingest/codex.ts
|
|
726
1998
|
init_database();
|
|
1999
|
+
init_pricing();
|
|
727
2000
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
728
2001
|
import { homedir as homedir3 } from "os";
|
|
729
2002
|
import { join as join3, basename as basename2 } from "path";
|
|
730
|
-
import { Database as
|
|
731
|
-
var
|
|
732
|
-
var
|
|
2003
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
2004
|
+
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
2005
|
+
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
2006
|
+
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2007
|
+
function codexDbPath() {
|
|
2008
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
2009
|
+
}
|
|
2010
|
+
function codexConfigPath() {
|
|
2011
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
2012
|
+
}
|
|
2013
|
+
function readCodexModel() {
|
|
2014
|
+
const configPath = codexConfigPath();
|
|
2015
|
+
if (!existsSync3(configPath))
|
|
2016
|
+
return "gpt-5-codex";
|
|
2017
|
+
try {
|
|
2018
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
2019
|
+
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
2020
|
+
return match?.[1] ?? "gpt-5-codex";
|
|
2021
|
+
} catch {
|
|
2022
|
+
return "gpt-5-codex";
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
function buildThreadQuery(codexDb) {
|
|
2026
|
+
const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
|
|
2027
|
+
const modelSelect = cols.has("model") ? "model" : "NULL AS model";
|
|
2028
|
+
const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
|
|
2029
|
+
const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
|
|
2030
|
+
return `
|
|
2031
|
+
SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
|
|
2032
|
+
${providerSelect}, ${modelSelect}
|
|
2033
|
+
FROM threads WHERE tokens_used > 0
|
|
2034
|
+
`;
|
|
2035
|
+
}
|
|
2036
|
+
function readTokenEvents(rolloutPath) {
|
|
2037
|
+
if (!rolloutPath || !existsSync3(rolloutPath))
|
|
2038
|
+
return [];
|
|
2039
|
+
const fallbackUsages = new Map;
|
|
2040
|
+
let fallbackTimestamp;
|
|
2041
|
+
let aggregate = null;
|
|
2042
|
+
for (const line of readFileSync2(rolloutPath, "utf-8").split(`
|
|
2043
|
+
`)) {
|
|
2044
|
+
if (!line.trim())
|
|
2045
|
+
continue;
|
|
2046
|
+
let entry;
|
|
2047
|
+
try {
|
|
2048
|
+
entry = JSON.parse(line);
|
|
2049
|
+
} catch {
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
if (!entry || typeof entry !== "object")
|
|
2053
|
+
continue;
|
|
2054
|
+
const payload = entry["payload"];
|
|
2055
|
+
if (!payload || payload["type"] !== "token_count")
|
|
2056
|
+
continue;
|
|
2057
|
+
const info = payload["info"];
|
|
2058
|
+
const timestamp = entry["timestamp"];
|
|
2059
|
+
const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
|
|
2060
|
+
const totalUsage = info?.["total_token_usage"];
|
|
2061
|
+
if (totalUsage && tokenTotal(totalUsage) > 0) {
|
|
2062
|
+
aggregate = { usage: totalUsage, timestamp: entryTimestamp };
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
const usage = info?.["last_token_usage"];
|
|
2066
|
+
if (!usage)
|
|
2067
|
+
continue;
|
|
2068
|
+
if (tokenTotal(usage) <= 0)
|
|
2069
|
+
continue;
|
|
2070
|
+
const key = JSON.stringify(usage);
|
|
2071
|
+
if (!fallbackUsages.has(key))
|
|
2072
|
+
fallbackUsages.set(key, usage);
|
|
2073
|
+
fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
|
|
2074
|
+
}
|
|
2075
|
+
if (aggregate)
|
|
2076
|
+
return [aggregate];
|
|
2077
|
+
if (fallbackUsages.size === 0)
|
|
2078
|
+
return [];
|
|
2079
|
+
return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
|
|
2080
|
+
}
|
|
2081
|
+
function tokenTotal(usage) {
|
|
2082
|
+
return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2083
|
+
}
|
|
2084
|
+
function sumTokenUsages(usages) {
|
|
2085
|
+
const result = {
|
|
2086
|
+
input_tokens: 0,
|
|
2087
|
+
cached_input_tokens: 0,
|
|
2088
|
+
output_tokens: 0,
|
|
2089
|
+
reasoning_output_tokens: 0,
|
|
2090
|
+
total_tokens: 0
|
|
2091
|
+
};
|
|
2092
|
+
for (const usage of usages) {
|
|
2093
|
+
result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
|
|
2094
|
+
result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
|
|
2095
|
+
result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2096
|
+
result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
|
|
2097
|
+
result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
|
|
2098
|
+
}
|
|
2099
|
+
return result;
|
|
2100
|
+
}
|
|
2101
|
+
function fallbackEvents(totalTokens) {
|
|
2102
|
+
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
2103
|
+
return [{
|
|
2104
|
+
usage: {
|
|
2105
|
+
input_tokens: inputTokens,
|
|
2106
|
+
cached_input_tokens: 0,
|
|
2107
|
+
output_tokens: totalTokens - inputTokens,
|
|
2108
|
+
total_tokens: totalTokens
|
|
2109
|
+
}
|
|
2110
|
+
}];
|
|
2111
|
+
}
|
|
733
2112
|
async function ingestCodex(db, verbose = false) {
|
|
734
|
-
|
|
2113
|
+
const dbPath = codexDbPath();
|
|
2114
|
+
if (!existsSync3(dbPath)) {
|
|
735
2115
|
if (verbose)
|
|
736
|
-
console.log("Codex DB not found:",
|
|
737
|
-
return { sessions: 0 };
|
|
2116
|
+
console.log("Codex DB not found:", dbPath);
|
|
2117
|
+
return { sessions: 0, requests: 0 };
|
|
738
2118
|
}
|
|
2119
|
+
const machineId = getMachineId();
|
|
739
2120
|
let codexDb = null;
|
|
740
2121
|
let ingested = 0;
|
|
2122
|
+
let requests = 0;
|
|
2123
|
+
const account = await resolveAccountForAgent("codex");
|
|
741
2124
|
try {
|
|
742
|
-
codexDb = new
|
|
743
|
-
const threads = codexDb.prepare(
|
|
2125
|
+
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
2126
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
744
2127
|
for (const thread of threads) {
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
|
|
2128
|
+
const model = thread.model ?? readCodexModel();
|
|
2129
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
2130
|
+
const processed = getIngestState(db, "codex", thread.id);
|
|
2131
|
+
if (processed === stateValue)
|
|
748
2132
|
continue;
|
|
749
|
-
const costUsd = 0;
|
|
750
2133
|
const projectPath = thread.cwd ?? "";
|
|
751
2134
|
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
2135
|
+
const sessionId = `codex-${thread.id}`;
|
|
752
2136
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
753
2137
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
754
|
-
upsertSession(db, {
|
|
755
|
-
id:
|
|
2138
|
+
upsertSession(db, withAccount({
|
|
2139
|
+
id: sessionId,
|
|
756
2140
|
agent: "codex",
|
|
757
2141
|
project_path: projectPath,
|
|
758
2142
|
project_name: projectName,
|
|
759
2143
|
started_at: startedAt,
|
|
760
2144
|
ended_at: endedAt,
|
|
761
|
-
total_cost_usd:
|
|
762
|
-
total_tokens:
|
|
763
|
-
request_count:
|
|
2145
|
+
total_cost_usd: 0,
|
|
2146
|
+
total_tokens: 0,
|
|
2147
|
+
request_count: 0,
|
|
2148
|
+
machine_id: machineId
|
|
2149
|
+
}, account));
|
|
2150
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
2151
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2152
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2153
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2154
|
+
tokenEvents.forEach((event, index) => {
|
|
2155
|
+
const usage = event.usage;
|
|
2156
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
2157
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2158
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2159
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2160
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2161
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2162
|
+
const requestId = `${sessionId}-${index}`;
|
|
2163
|
+
upsertRequest(db, withAccount({
|
|
2164
|
+
id: requestId,
|
|
2165
|
+
agent: "codex",
|
|
2166
|
+
session_id: sessionId,
|
|
2167
|
+
model,
|
|
2168
|
+
input_tokens: inputTokens,
|
|
2169
|
+
output_tokens: outputTokens,
|
|
2170
|
+
cache_read_tokens: cacheReadTokens,
|
|
2171
|
+
cache_create_tokens: 0,
|
|
2172
|
+
cost_usd: costUsd,
|
|
2173
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2174
|
+
duration_ms: 0,
|
|
2175
|
+
timestamp,
|
|
2176
|
+
source_request_id: requestId,
|
|
2177
|
+
machine_id: machineId
|
|
2178
|
+
}, account));
|
|
2179
|
+
requests++;
|
|
764
2180
|
});
|
|
765
|
-
|
|
2181
|
+
rollupSession(db, sessionId);
|
|
2182
|
+
setIngestState(db, "codex", thread.id, stateValue);
|
|
766
2183
|
ingested++;
|
|
767
2184
|
if (verbose)
|
|
768
|
-
console.log(`Codex session ${thread.id}: ${
|
|
2185
|
+
console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
769
2186
|
}
|
|
770
2187
|
} finally {
|
|
771
2188
|
codexDb?.close();
|
|
772
2189
|
}
|
|
773
|
-
return { sessions: ingested };
|
|
2190
|
+
return { sessions: ingested, requests };
|
|
774
2191
|
}
|
|
775
2192
|
|
|
776
2193
|
// src/ingest/gemini.ts
|
|
777
2194
|
init_database();
|
|
2195
|
+
init_pricing();
|
|
778
2196
|
import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
779
2197
|
import { homedir as homedir4 } from "os";
|
|
780
|
-
import { join as join4 } from "path";
|
|
781
|
-
var
|
|
2198
|
+
import { join as join4, basename as basename3 } from "path";
|
|
2199
|
+
var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
2200
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
|
|
2201
|
+
function geminiTmpDir() {
|
|
2202
|
+
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
2203
|
+
}
|
|
2204
|
+
function geminiHistoryDir() {
|
|
2205
|
+
return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
|
|
2206
|
+
}
|
|
2207
|
+
function numberField(...values) {
|
|
2208
|
+
for (const value of values) {
|
|
2209
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2210
|
+
return value;
|
|
2211
|
+
}
|
|
2212
|
+
return 0;
|
|
2213
|
+
}
|
|
2214
|
+
function listProjectDirs(...roots) {
|
|
2215
|
+
const dirs = new Set;
|
|
2216
|
+
for (const root of roots) {
|
|
2217
|
+
if (!existsSync4(root))
|
|
2218
|
+
continue;
|
|
2219
|
+
try {
|
|
2220
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
2221
|
+
if (entry.isDirectory())
|
|
2222
|
+
dirs.add(join4(root, entry.name));
|
|
2223
|
+
}
|
|
2224
|
+
} catch {}
|
|
2225
|
+
}
|
|
2226
|
+
return [...dirs];
|
|
2227
|
+
}
|
|
2228
|
+
function projectRoot(projectDir, chatData) {
|
|
2229
|
+
if (chatData.projectPath)
|
|
2230
|
+
return chatData.projectPath;
|
|
2231
|
+
if (chatData.project_path)
|
|
2232
|
+
return chatData.project_path;
|
|
2233
|
+
const rootFile = join4(projectDir, ".project_root");
|
|
2234
|
+
try {
|
|
2235
|
+
if (existsSync4(rootFile))
|
|
2236
|
+
return readFileSync3(rootFile, "utf-8").trim();
|
|
2237
|
+
} catch {}
|
|
2238
|
+
return "";
|
|
2239
|
+
}
|
|
782
2240
|
async function ingestGemini(db, verbose) {
|
|
783
|
-
|
|
2241
|
+
const tmpDir = geminiTmpDir();
|
|
2242
|
+
const historyDir = geminiHistoryDir();
|
|
2243
|
+
if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
|
|
784
2244
|
if (verbose)
|
|
785
|
-
console.log("Gemini tmp
|
|
786
|
-
return { sessions: 0 };
|
|
2245
|
+
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
2246
|
+
return { sessions: 0, requests: 0 };
|
|
787
2247
|
}
|
|
2248
|
+
const machineId = getMachineId();
|
|
788
2249
|
let totalSessions = 0;
|
|
2250
|
+
let totalRequests = 0;
|
|
789
2251
|
const touchedSessions = new Set;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
} catch {
|
|
794
|
-
return { sessions: 0 };
|
|
795
|
-
}
|
|
796
|
-
for (const projectDir of projectHashDirs) {
|
|
2252
|
+
const account = await resolveAccountForAgent("gemini");
|
|
2253
|
+
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2254
|
+
for (const projectDir of projectDirs) {
|
|
797
2255
|
const chatsDir = join4(projectDir, "chats");
|
|
798
2256
|
if (!existsSync4(chatsDir))
|
|
799
2257
|
continue;
|
|
@@ -820,41 +2278,1089 @@ async function ingestGemini(db, verbose) {
|
|
|
820
2278
|
} catch {
|
|
821
2279
|
continue;
|
|
822
2280
|
}
|
|
823
|
-
const sessionId = chatData.sessionId;
|
|
2281
|
+
const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
|
|
824
2282
|
if (!sessionId)
|
|
825
2283
|
continue;
|
|
826
2284
|
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
2285
|
+
const projectPath = projectRoot(projectDir, chatData);
|
|
2286
|
+
const projectName = projectPath ? basename3(projectPath) : "";
|
|
827
2287
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
828
2288
|
if (!existing) {
|
|
829
2289
|
const session = {
|
|
830
2290
|
id: sessionId,
|
|
831
2291
|
agent: "gemini",
|
|
832
|
-
project_path:
|
|
833
|
-
project_name:
|
|
2292
|
+
project_path: projectPath,
|
|
2293
|
+
project_name: projectName,
|
|
834
2294
|
started_at: startTime,
|
|
835
2295
|
ended_at: chatData.lastUpdated ?? null,
|
|
836
2296
|
total_cost_usd: 0,
|
|
837
2297
|
total_tokens: 0,
|
|
838
|
-
request_count: 0
|
|
2298
|
+
request_count: 0,
|
|
2299
|
+
machine_id: machineId
|
|
839
2300
|
};
|
|
840
|
-
upsertSession(db, session);
|
|
841
|
-
touchedSessions.add(sessionId);
|
|
2301
|
+
upsertSession(db, withAccount(session, account));
|
|
842
2302
|
totalSessions++;
|
|
843
2303
|
}
|
|
2304
|
+
touchedSessions.add(sessionId);
|
|
2305
|
+
for (const [index, message] of (chatData.messages ?? []).entries()) {
|
|
2306
|
+
const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
|
|
2307
|
+
if (!usage)
|
|
2308
|
+
continue;
|
|
2309
|
+
const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
|
|
2310
|
+
if (!model)
|
|
2311
|
+
continue;
|
|
2312
|
+
const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
|
|
2313
|
+
const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
|
|
2314
|
+
const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
|
|
2315
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2316
|
+
const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
|
|
2317
|
+
const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
|
|
2318
|
+
const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
|
|
2319
|
+
if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
|
|
2320
|
+
continue;
|
|
2321
|
+
const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2322
|
+
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
2323
|
+
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
2324
|
+
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
2325
|
+
upsertRequest(db, withAccount({
|
|
2326
|
+
id: requestId,
|
|
2327
|
+
agent: "gemini",
|
|
2328
|
+
session_id: sessionId,
|
|
2329
|
+
model,
|
|
2330
|
+
input_tokens: inputTokens,
|
|
2331
|
+
output_tokens: outputTokens,
|
|
2332
|
+
cache_read_tokens: cacheReadTokens,
|
|
2333
|
+
cache_create_tokens: 0,
|
|
2334
|
+
cost_usd: costUsd,
|
|
2335
|
+
cost_basis: defaultCostBasisForAgent("gemini"),
|
|
2336
|
+
duration_ms: 0,
|
|
2337
|
+
timestamp,
|
|
2338
|
+
source_request_id: message.id ?? requestId,
|
|
2339
|
+
machine_id: machineId
|
|
2340
|
+
}, account));
|
|
2341
|
+
totalRequests++;
|
|
2342
|
+
}
|
|
844
2343
|
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
845
2344
|
}
|
|
846
2345
|
}
|
|
847
2346
|
for (const sessionId of touchedSessions) {
|
|
848
2347
|
rollupSession(db, sessionId);
|
|
849
2348
|
}
|
|
850
|
-
return { sessions: totalSessions };
|
|
2349
|
+
return { sessions: totalSessions, requests: totalRequests };
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// src/ingest/opencode.ts
|
|
2353
|
+
init_database();
|
|
2354
|
+
init_pricing();
|
|
2355
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
2356
|
+
import { homedir as homedir5 } from "os";
|
|
2357
|
+
import { join as join5 } from "path";
|
|
2358
|
+
var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
|
|
2359
|
+
function walkJsonFiles(dir, acc = []) {
|
|
2360
|
+
if (!existsSync5(dir))
|
|
2361
|
+
return acc;
|
|
2362
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
2363
|
+
const p = join5(dir, entry.name);
|
|
2364
|
+
if (entry.isDirectory())
|
|
2365
|
+
walkJsonFiles(p, acc);
|
|
2366
|
+
else if (entry.name.endsWith(".json"))
|
|
2367
|
+
acc.push(p);
|
|
2368
|
+
}
|
|
2369
|
+
return acc;
|
|
2370
|
+
}
|
|
2371
|
+
function parseSessionIdFromPath(filePath) {
|
|
2372
|
+
const parts = filePath.split("/");
|
|
2373
|
+
const msgIdx = parts.indexOf("message");
|
|
2374
|
+
if (msgIdx >= 0 && parts[msgIdx + 1])
|
|
2375
|
+
return parts[msgIdx + 1];
|
|
2376
|
+
const sessionIdx = parts.indexOf("session");
|
|
2377
|
+
if (sessionIdx >= 0 && parts[parts.length - 1]?.endsWith(".json")) {
|
|
2378
|
+
return parts[parts.length - 1].replace(/\.json$/, "");
|
|
2379
|
+
}
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
async function ingestOpenCode(db, verbose = false) {
|
|
2383
|
+
const messageDir = join5(OPENCODE_STORAGE, "message");
|
|
2384
|
+
const files = walkJsonFiles(messageDir);
|
|
2385
|
+
let requests = 0;
|
|
2386
|
+
const touched = new Set;
|
|
2387
|
+
const machineId = getMachineId();
|
|
2388
|
+
const now = new Date().toISOString();
|
|
2389
|
+
const account = await resolveAccountForAgent("opencode");
|
|
2390
|
+
for (const file of files) {
|
|
2391
|
+
const mtime = statSync4(file).mtimeMs;
|
|
2392
|
+
const stateKey = file;
|
|
2393
|
+
const prev = getIngestState(db, "opencode", stateKey);
|
|
2394
|
+
if (prev && Number(prev) >= mtime)
|
|
2395
|
+
continue;
|
|
2396
|
+
let parsed;
|
|
2397
|
+
try {
|
|
2398
|
+
parsed = JSON.parse(readFileSync4(file, "utf-8"));
|
|
2399
|
+
} catch {
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
if (parsed.role !== "assistant")
|
|
2403
|
+
continue;
|
|
2404
|
+
const usage = parsed.usage;
|
|
2405
|
+
if (!usage)
|
|
2406
|
+
continue;
|
|
2407
|
+
const sessionId = parseSessionIdFromPath(file) ?? `opencode-${statSync4(file).ino}`;
|
|
2408
|
+
const model = normalizeModelName(parsed.model ?? "unknown");
|
|
2409
|
+
const input = usage.inputTokens ?? 0;
|
|
2410
|
+
const output = usage.outputTokens ?? 0;
|
|
2411
|
+
const cacheRead = usage.cacheReadTokens ?? 0;
|
|
2412
|
+
const cacheWrite = usage.cacheWriteTokens ?? 0;
|
|
2413
|
+
if (input + output + cacheRead + cacheWrite === 0 && !usage.cost)
|
|
2414
|
+
continue;
|
|
2415
|
+
const timestamp = usage && parsed.time?.created ? new Date(parsed.time.created).toISOString() : new Date(statSync4(file).mtime).toISOString();
|
|
2416
|
+
const sourceId = file.replace(OPENCODE_STORAGE, "");
|
|
2417
|
+
const reqId = `opencode-${sourceId}`;
|
|
2418
|
+
const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
|
|
2419
|
+
upsertRequest(db, withAccount({
|
|
2420
|
+
id: reqId,
|
|
2421
|
+
agent: "opencode",
|
|
2422
|
+
session_id: sessionId,
|
|
2423
|
+
model,
|
|
2424
|
+
input_tokens: input,
|
|
2425
|
+
output_tokens: output,
|
|
2426
|
+
cache_read_tokens: cacheRead,
|
|
2427
|
+
cache_create_tokens: cacheWrite,
|
|
2428
|
+
cost_usd: costUsd,
|
|
2429
|
+
cost_basis: defaultCostBasisForAgent("opencode"),
|
|
2430
|
+
duration_ms: 0,
|
|
2431
|
+
timestamp,
|
|
2432
|
+
source_request_id: sourceId,
|
|
2433
|
+
machine_id: machineId,
|
|
2434
|
+
updated_at: now
|
|
2435
|
+
}, account));
|
|
2436
|
+
requests++;
|
|
2437
|
+
if (!touched.has(sessionId)) {
|
|
2438
|
+
upsertSession(db, withAccount({
|
|
2439
|
+
id: sessionId,
|
|
2440
|
+
agent: "opencode",
|
|
2441
|
+
project_path: "",
|
|
2442
|
+
project_name: "",
|
|
2443
|
+
started_at: timestamp,
|
|
2444
|
+
ended_at: null,
|
|
2445
|
+
total_cost_usd: 0,
|
|
2446
|
+
total_tokens: 0,
|
|
2447
|
+
request_count: 0,
|
|
2448
|
+
machine_id: machineId,
|
|
2449
|
+
updated_at: now
|
|
2450
|
+
}, account));
|
|
2451
|
+
touched.add(sessionId);
|
|
2452
|
+
}
|
|
2453
|
+
setIngestState(db, "opencode", stateKey, String(mtime));
|
|
2454
|
+
if (verbose)
|
|
2455
|
+
console.log(` opencode: ${reqId} ${model} $${costUsd.toFixed(4)}`);
|
|
2456
|
+
}
|
|
2457
|
+
for (const sid of touched)
|
|
2458
|
+
rollupSession(db, sid);
|
|
2459
|
+
return { files: files.length, requests, sessions: touched.size };
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// src/ingest/cursor.ts
|
|
2463
|
+
init_database();
|
|
2464
|
+
function getCursorSessionToken() {
|
|
2465
|
+
return process.env["CURSOR_SESSION_TOKEN"] ?? process.env["CURSOR_API_TOKEN"] ?? null;
|
|
2466
|
+
}
|
|
2467
|
+
async function cursorFetch(path, token) {
|
|
2468
|
+
try {
|
|
2469
|
+
const res = await fetch(`https://cursor.com${path}`, {
|
|
2470
|
+
headers: {
|
|
2471
|
+
Cookie: `WorkosCursorSessionToken=${token}`,
|
|
2472
|
+
Accept: "application/json"
|
|
2473
|
+
},
|
|
2474
|
+
signal: AbortSignal.timeout(1e4)
|
|
2475
|
+
});
|
|
2476
|
+
if (!res.ok)
|
|
2477
|
+
return null;
|
|
2478
|
+
return await res.json();
|
|
2479
|
+
} catch {
|
|
2480
|
+
return null;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
async function ingestCursor(db, verbose = false) {
|
|
2484
|
+
const token = getCursorSessionToken();
|
|
2485
|
+
if (!token) {
|
|
2486
|
+
if (verbose)
|
|
2487
|
+
console.log(" cursor: skipped \u2014 set CURSOR_SESSION_TOKEN");
|
|
2488
|
+
return { requests: 0, snapshots: 0 };
|
|
2489
|
+
}
|
|
2490
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
2491
|
+
const prev = getIngestState(db, "cursor", `sync-${today}`);
|
|
2492
|
+
if (prev)
|
|
2493
|
+
return { requests: 0, snapshots: 0 };
|
|
2494
|
+
const machineId = getMachineId();
|
|
2495
|
+
const now = new Date().toISOString();
|
|
2496
|
+
let snapshots = 0;
|
|
2497
|
+
const account = await resolveAccountForAgent("cursor");
|
|
2498
|
+
const usage = await cursorFetch("/api/usage", token);
|
|
2499
|
+
if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
|
|
2500
|
+
upsertUsageSnapshot(db, {
|
|
2501
|
+
agent: "cursor",
|
|
2502
|
+
date: today,
|
|
2503
|
+
metric: "premium_requests_used",
|
|
2504
|
+
value: usage.premiumRequests,
|
|
2505
|
+
unit: "count",
|
|
2506
|
+
machine_id: machineId
|
|
2507
|
+
});
|
|
2508
|
+
upsertUsageSnapshot(db, {
|
|
2509
|
+
agent: "cursor",
|
|
2510
|
+
date: today,
|
|
2511
|
+
metric: "premium_requests_limit",
|
|
2512
|
+
value: usage.maxPremiumRequests,
|
|
2513
|
+
unit: "count",
|
|
2514
|
+
machine_id: machineId
|
|
2515
|
+
});
|
|
2516
|
+
snapshots += 2;
|
|
2517
|
+
}
|
|
2518
|
+
const summary = await cursorFetch("/api/usage-summary", token);
|
|
2519
|
+
const onDemand = summary?.individualUsage?.spend ?? summary?.teamUsage?.spend ?? 0;
|
|
2520
|
+
const included = summary?.individualUsage?.includedSpend ?? 0;
|
|
2521
|
+
if (onDemand > 0) {
|
|
2522
|
+
upsertUsageSnapshot(db, {
|
|
2523
|
+
agent: "cursor",
|
|
2524
|
+
date: today,
|
|
2525
|
+
metric: "on_demand_usd",
|
|
2526
|
+
value: onDemand,
|
|
2527
|
+
unit: "usd",
|
|
2528
|
+
machine_id: machineId
|
|
2529
|
+
});
|
|
2530
|
+
snapshots++;
|
|
2531
|
+
}
|
|
2532
|
+
if (included > 0) {
|
|
2533
|
+
upsertUsageSnapshot(db, {
|
|
2534
|
+
agent: "cursor",
|
|
2535
|
+
date: today,
|
|
2536
|
+
metric: "included_consumed_usd",
|
|
2537
|
+
value: included,
|
|
2538
|
+
unit: "usd",
|
|
2539
|
+
machine_id: machineId
|
|
2540
|
+
});
|
|
2541
|
+
snapshots++;
|
|
2542
|
+
}
|
|
2543
|
+
const sessionId = `cursor-${today}-${machineId}`;
|
|
2544
|
+
if (onDemand + included > 0) {
|
|
2545
|
+
upsertSession(db, withAccount({
|
|
2546
|
+
id: sessionId,
|
|
2547
|
+
agent: "cursor",
|
|
2548
|
+
project_path: "",
|
|
2549
|
+
project_name: "Cursor subscription",
|
|
2550
|
+
started_at: `${today}T00:00:00.000Z`,
|
|
2551
|
+
ended_at: now,
|
|
2552
|
+
total_cost_usd: onDemand + included,
|
|
2553
|
+
total_tokens: 0,
|
|
2554
|
+
request_count: 1,
|
|
2555
|
+
machine_id: machineId,
|
|
2556
|
+
updated_at: now
|
|
2557
|
+
}, account));
|
|
2558
|
+
upsertRequest(db, withAccount({
|
|
2559
|
+
id: `cursor-${today}-${machineId}-usage`,
|
|
2560
|
+
agent: "cursor",
|
|
2561
|
+
session_id: sessionId,
|
|
2562
|
+
model: "cursor-subscription",
|
|
2563
|
+
input_tokens: 0,
|
|
2564
|
+
output_tokens: 0,
|
|
2565
|
+
cache_read_tokens: 0,
|
|
2566
|
+
cache_create_tokens: 0,
|
|
2567
|
+
cost_usd: onDemand + included,
|
|
2568
|
+
cost_basis: "subscription_included",
|
|
2569
|
+
duration_ms: 0,
|
|
2570
|
+
timestamp: now,
|
|
2571
|
+
source_request_id: today,
|
|
2572
|
+
machine_id: machineId,
|
|
2573
|
+
updated_at: now
|
|
2574
|
+
}, account));
|
|
2575
|
+
rollupSession(db, sessionId);
|
|
2576
|
+
}
|
|
2577
|
+
setIngestState(db, "cursor", `sync-${today}`, now);
|
|
2578
|
+
if (verbose)
|
|
2579
|
+
console.log(` cursor: on-demand $${onDemand.toFixed(2)}, included $${included.toFixed(2)}`);
|
|
2580
|
+
return { requests: onDemand + included > 0 ? 1 : 0, snapshots };
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// src/ingest/pi.ts
|
|
2584
|
+
init_database();
|
|
2585
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
2586
|
+
import { homedir as homedir6 } from "os";
|
|
2587
|
+
import { join as join6 } from "path";
|
|
2588
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
|
|
2589
|
+
function walkSessions(dir, acc = []) {
|
|
2590
|
+
if (!existsSync6(dir))
|
|
2591
|
+
return acc;
|
|
2592
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
2593
|
+
const p = join6(dir, entry.name);
|
|
2594
|
+
if (entry.isDirectory())
|
|
2595
|
+
walkSessions(p, acc);
|
|
2596
|
+
else if (entry.name.endsWith(".json"))
|
|
2597
|
+
acc.push(p);
|
|
2598
|
+
}
|
|
2599
|
+
return acc;
|
|
2600
|
+
}
|
|
2601
|
+
async function ingestPi(db, verbose = false) {
|
|
2602
|
+
const files = walkSessions(PI_SESSION_DIR);
|
|
2603
|
+
let requests = 0;
|
|
2604
|
+
const touched = new Set;
|
|
2605
|
+
const machineId = getMachineId();
|
|
2606
|
+
const now = new Date().toISOString();
|
|
2607
|
+
const account = await resolveAccountForAgent("pi");
|
|
2608
|
+
for (const file of files) {
|
|
2609
|
+
const mtime = statSync5(file).mtimeMs;
|
|
2610
|
+
const prev = getIngestState(db, "pi", file);
|
|
2611
|
+
if (prev && Number(prev) >= mtime)
|
|
2612
|
+
continue;
|
|
2613
|
+
let data;
|
|
2614
|
+
try {
|
|
2615
|
+
data = JSON.parse(readFileSync5(file, "utf-8"));
|
|
2616
|
+
} catch {
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
const sessionId = data.id ?? file.replace(/\.json$/, "").split("/").pop() ?? `pi-${statSync5(file).ino}`;
|
|
2620
|
+
const turns = data.turns ?? data.messages?.filter((m) => m.role === "assistant") ?? [];
|
|
2621
|
+
for (let i = 0;i < turns.length; i++) {
|
|
2622
|
+
const turn = turns[i];
|
|
2623
|
+
const usage = turn.usage;
|
|
2624
|
+
if (!usage)
|
|
2625
|
+
continue;
|
|
2626
|
+
const input = usage.input ?? 0;
|
|
2627
|
+
const output = usage.output ?? 0;
|
|
2628
|
+
if (input + output === 0 && !usage.cost)
|
|
2629
|
+
continue;
|
|
2630
|
+
const model = turn.model ?? turn.provider ?? "unknown";
|
|
2631
|
+
const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
|
|
2632
|
+
const reqId = `pi-${sessionId}-${i}`;
|
|
2633
|
+
upsertRequest(db, withAccount({
|
|
2634
|
+
id: reqId,
|
|
2635
|
+
agent: "pi",
|
|
2636
|
+
session_id: sessionId,
|
|
2637
|
+
model,
|
|
2638
|
+
input_tokens: input,
|
|
2639
|
+
output_tokens: output,
|
|
2640
|
+
cache_read_tokens: usage.cacheRead ?? 0,
|
|
2641
|
+
cache_create_tokens: usage.cacheWrite ?? 0,
|
|
2642
|
+
cost_usd: usage.cost ?? 0,
|
|
2643
|
+
cost_basis: defaultCostBasisForAgent("pi"),
|
|
2644
|
+
duration_ms: 0,
|
|
2645
|
+
timestamp,
|
|
2646
|
+
source_request_id: `${sessionId}-${i}`,
|
|
2647
|
+
machine_id: machineId,
|
|
2648
|
+
updated_at: now
|
|
2649
|
+
}, account));
|
|
2650
|
+
requests++;
|
|
2651
|
+
}
|
|
2652
|
+
if (turns.length > 0) {
|
|
2653
|
+
upsertSession(db, withAccount({
|
|
2654
|
+
id: sessionId,
|
|
2655
|
+
agent: "pi",
|
|
2656
|
+
project_path: "",
|
|
2657
|
+
project_name: "",
|
|
2658
|
+
started_at: turns[0]?.timestamp ?? now,
|
|
2659
|
+
ended_at: null,
|
|
2660
|
+
total_cost_usd: 0,
|
|
2661
|
+
total_tokens: 0,
|
|
2662
|
+
request_count: 0,
|
|
2663
|
+
machine_id: machineId,
|
|
2664
|
+
updated_at: now
|
|
2665
|
+
}, account));
|
|
2666
|
+
touched.add(sessionId);
|
|
2667
|
+
}
|
|
2668
|
+
setIngestState(db, "pi", file, String(mtime));
|
|
2669
|
+
if (verbose)
|
|
2670
|
+
console.log(` pi: ${sessionId} (${turns.length} turns)`);
|
|
2671
|
+
}
|
|
2672
|
+
for (const sid of touched)
|
|
2673
|
+
rollupSession(db, sid);
|
|
2674
|
+
return { files: files.length, requests, sessions: touched.size };
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/ingest/hermes.ts
|
|
2678
|
+
init_database();
|
|
2679
|
+
import { existsSync as existsSync7, statSync as statSync6 } from "fs";
|
|
2680
|
+
import { homedir as homedir7 } from "os";
|
|
2681
|
+
import { join as join7 } from "path";
|
|
2682
|
+
var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
|
|
2683
|
+
function mapCostBasis(billingMode) {
|
|
2684
|
+
if (billingMode === "subscription")
|
|
2685
|
+
return "subscription_included";
|
|
2686
|
+
if (billingMode === "api")
|
|
2687
|
+
return "metered_api";
|
|
2688
|
+
return defaultCostBasisForAgent("hermes");
|
|
2689
|
+
}
|
|
2690
|
+
async function ingestHermes(db, verbose = false) {
|
|
2691
|
+
if (!existsSync7(HERMES_DB)) {
|
|
2692
|
+
return { sessions: 0, requests: 0 };
|
|
2693
|
+
}
|
|
2694
|
+
const { Database: Sqlite } = await import("bun:sqlite");
|
|
2695
|
+
const hermes = new Sqlite(HERMES_DB, { readonly: true });
|
|
2696
|
+
const rows = hermes.prepare(`
|
|
2697
|
+
SELECT id, source, model, started_at, ended_at,
|
|
2698
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
2699
|
+
reasoning_tokens, estimated_cost_usd, actual_cost_usd, billing_mode, parent_session_id
|
|
2700
|
+
FROM sessions
|
|
2701
|
+
ORDER BY started_at DESC
|
|
2702
|
+
`).all();
|
|
2703
|
+
const stateKey = "state.db";
|
|
2704
|
+
const mtime = statSyncSafe(HERMES_DB);
|
|
2705
|
+
const prev = getIngestState(db, "hermes", stateKey);
|
|
2706
|
+
if (prev && Number(prev) >= mtime && rows.length === 0) {
|
|
2707
|
+
hermes.close();
|
|
2708
|
+
return { sessions: 0, requests: 0 };
|
|
2709
|
+
}
|
|
2710
|
+
const machineId = getMachineId();
|
|
2711
|
+
const now = new Date().toISOString();
|
|
2712
|
+
let requests = 0;
|
|
2713
|
+
const account = await resolveAccountForAgent("hermes");
|
|
2714
|
+
for (const row of rows) {
|
|
2715
|
+
const sessionId = `hermes-${row.id}`;
|
|
2716
|
+
const startedAt = new Date(row.started_at * 1000).toISOString();
|
|
2717
|
+
const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
|
|
2718
|
+
const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
|
|
2719
|
+
const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
|
|
2720
|
+
upsertSession(db, withAccount({
|
|
2721
|
+
id: sessionId,
|
|
2722
|
+
agent: "hermes",
|
|
2723
|
+
project_path: row.source ?? "",
|
|
2724
|
+
project_name: row.source ?? "",
|
|
2725
|
+
started_at: startedAt,
|
|
2726
|
+
ended_at: endedAt,
|
|
2727
|
+
total_cost_usd: cost,
|
|
2728
|
+
total_tokens: tokens,
|
|
2729
|
+
request_count: 1,
|
|
2730
|
+
machine_id: machineId,
|
|
2731
|
+
updated_at: now
|
|
2732
|
+
}, account));
|
|
2733
|
+
const reqId = `hermes-${row.id}-rollup`;
|
|
2734
|
+
upsertRequest(db, withAccount({
|
|
2735
|
+
id: reqId,
|
|
2736
|
+
agent: "hermes",
|
|
2737
|
+
session_id: sessionId,
|
|
2738
|
+
model: row.model ?? "unknown",
|
|
2739
|
+
input_tokens: row.input_tokens,
|
|
2740
|
+
output_tokens: row.output_tokens + row.reasoning_tokens,
|
|
2741
|
+
cache_read_tokens: row.cache_read_tokens,
|
|
2742
|
+
cache_create_tokens: row.cache_write_tokens,
|
|
2743
|
+
cost_usd: cost,
|
|
2744
|
+
cost_basis: mapCostBasis(row.billing_mode),
|
|
2745
|
+
duration_ms: 0,
|
|
2746
|
+
timestamp: endedAt ?? startedAt,
|
|
2747
|
+
source_request_id: row.id,
|
|
2748
|
+
machine_id: machineId,
|
|
2749
|
+
updated_at: now
|
|
2750
|
+
}, account));
|
|
2751
|
+
requests++;
|
|
2752
|
+
rollupSession(db, sessionId);
|
|
2753
|
+
if (verbose)
|
|
2754
|
+
console.log(` hermes: ${sessionId} $${cost.toFixed(4)}`);
|
|
2755
|
+
}
|
|
2756
|
+
setIngestState(db, "hermes", stateKey, String(mtime));
|
|
2757
|
+
hermes.close();
|
|
2758
|
+
return { sessions: rows.length, requests };
|
|
2759
|
+
}
|
|
2760
|
+
function statSyncSafe(path) {
|
|
2761
|
+
try {
|
|
2762
|
+
return statSync6(path).mtimeMs;
|
|
2763
|
+
} catch {
|
|
2764
|
+
return 0;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
// src/ingest/claude-quota.ts
|
|
2769
|
+
init_database();
|
|
2770
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
2771
|
+
|
|
2772
|
+
// src/lib/paths.ts
|
|
2773
|
+
import { homedir as homedir8 } from "os";
|
|
2774
|
+
import { join as join8 } from "path";
|
|
2775
|
+
function getHomeDir() {
|
|
2776
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir8();
|
|
2777
|
+
}
|
|
2778
|
+
function agentPaths() {
|
|
2779
|
+
const home = getHomeDir();
|
|
2780
|
+
return {
|
|
2781
|
+
claudeProjects: join8(home, ".claude", "projects"),
|
|
2782
|
+
claudeCredentials: join8(home, ".claude", ".credentials.json"),
|
|
2783
|
+
takumiProjects: join8(home, ".takumi", "projects"),
|
|
2784
|
+
codexDir: join8(home, ".codex"),
|
|
2785
|
+
codexDb: join8(home, ".codex", "state_5.sqlite"),
|
|
2786
|
+
codexAuth: join8(home, ".codex", "auth.json"),
|
|
2787
|
+
codexConfig: join8(home, ".codex", "config.toml"),
|
|
2788
|
+
geminiTmp: join8(home, ".gemini", "tmp"),
|
|
2789
|
+
geminiHistory: join8(home, ".gemini", "history"),
|
|
2790
|
+
opencodeMessages: join8(home, ".local", "share", "opencode", "storage", "message"),
|
|
2791
|
+
piSessions: join8(home, ".pi", "agent", "sessions"),
|
|
2792
|
+
hermesDir: join8(home, ".hermes"),
|
|
2793
|
+
hermesDb: join8(home, ".hermes", "state.db")
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
// src/ingest/claude-quota.ts
|
|
2798
|
+
var CREDENTIALS_PATH = agentPaths().claudeCredentials;
|
|
2799
|
+
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
2800
|
+
var OAUTH_BETA = "oauth-2025-04-20";
|
|
2801
|
+
function readClaudeToken() {
|
|
2802
|
+
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
2803
|
+
if (fromEnv)
|
|
2804
|
+
return { token: fromEnv };
|
|
2805
|
+
if (!existsSync8(CREDENTIALS_PATH))
|
|
2806
|
+
return null;
|
|
2807
|
+
try {
|
|
2808
|
+
const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
|
|
2809
|
+
const oauth = creds.claudeAiOauth;
|
|
2810
|
+
if (!oauth?.accessToken)
|
|
2811
|
+
return null;
|
|
2812
|
+
return {
|
|
2813
|
+
token: oauth.accessToken,
|
|
2814
|
+
subscriptionType: oauth.subscriptionType,
|
|
2815
|
+
rateLimitTier: oauth.rateLimitTier
|
|
2816
|
+
};
|
|
2817
|
+
} catch {
|
|
2818
|
+
return null;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
function inferMonthlyFee(subscriptionType, rateLimitTier) {
|
|
2822
|
+
const tier = `${subscriptionType ?? ""} ${rateLimitTier ?? ""}`.toLowerCase();
|
|
2823
|
+
if (tier.includes("max") && tier.includes("20"))
|
|
2824
|
+
return 200;
|
|
2825
|
+
if (tier.includes("max"))
|
|
2826
|
+
return 100;
|
|
2827
|
+
if (tier.includes("pro"))
|
|
2828
|
+
return 20;
|
|
2829
|
+
if (tier.includes("team"))
|
|
2830
|
+
return 30;
|
|
2831
|
+
return 20;
|
|
2832
|
+
}
|
|
2833
|
+
async function fetchClaudeOAuthUsage(token) {
|
|
2834
|
+
try {
|
|
2835
|
+
const res = await fetch(USAGE_URL, {
|
|
2836
|
+
headers: {
|
|
2837
|
+
Authorization: `Bearer ${token}`,
|
|
2838
|
+
"anthropic-beta": OAUTH_BETA,
|
|
2839
|
+
Accept: "application/json"
|
|
2840
|
+
},
|
|
2841
|
+
signal: AbortSignal.timeout(1e4)
|
|
2842
|
+
});
|
|
2843
|
+
if (!res.ok)
|
|
2844
|
+
return null;
|
|
2845
|
+
return await res.json();
|
|
2846
|
+
} catch {
|
|
2847
|
+
return null;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
async function ingestClaudeQuota(db, verbose = false) {
|
|
2851
|
+
const auth = readClaudeToken();
|
|
2852
|
+
if (!auth) {
|
|
2853
|
+
if (verbose)
|
|
2854
|
+
console.log(" claude quota: skipped \u2014 no OAuth token (~/.claude/.credentials.json or CLAUDE_OAUTH_TOKEN)");
|
|
2855
|
+
return { snapshots: 0 };
|
|
2856
|
+
}
|
|
2857
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
2858
|
+
const prev = getIngestState(db, "claude", `quota-${today}`);
|
|
2859
|
+
if (prev)
|
|
2860
|
+
return { snapshots: 0 };
|
|
2861
|
+
const usage = await fetchClaudeOAuthUsage(auth.token);
|
|
2862
|
+
if (!usage) {
|
|
2863
|
+
if (verbose)
|
|
2864
|
+
console.log(" claude quota: OAuth usage endpoint unavailable");
|
|
2865
|
+
return { snapshots: 0 };
|
|
2866
|
+
}
|
|
2867
|
+
const machineId = getMachineId();
|
|
2868
|
+
let snapshots = 0;
|
|
2869
|
+
const windows = [
|
|
2870
|
+
["five_hour_utilization", usage.five_hour],
|
|
2871
|
+
["seven_day_utilization", usage.seven_day],
|
|
2872
|
+
["seven_day_sonnet_utilization", usage.seven_day_sonnet],
|
|
2873
|
+
["seven_day_opus_utilization", usage.seven_day_opus]
|
|
2874
|
+
];
|
|
2875
|
+
for (const [metric, window] of windows) {
|
|
2876
|
+
if (window?.utilization == null)
|
|
2877
|
+
continue;
|
|
2878
|
+
upsertUsageSnapshot(db, {
|
|
2879
|
+
agent: "claude",
|
|
2880
|
+
date: today,
|
|
2881
|
+
metric,
|
|
2882
|
+
value: Math.round(window.utilization * 1000) / 10,
|
|
2883
|
+
unit: "percent",
|
|
2884
|
+
machine_id: machineId
|
|
2885
|
+
});
|
|
2886
|
+
snapshots++;
|
|
2887
|
+
if (window.resets_at) {
|
|
2888
|
+
upsertUsageSnapshot(db, {
|
|
2889
|
+
agent: "claude",
|
|
2890
|
+
date: today,
|
|
2891
|
+
metric: `${metric}_resets_at`,
|
|
2892
|
+
value: Date.parse(window.resets_at),
|
|
2893
|
+
unit: "epoch_ms",
|
|
2894
|
+
machine_id: machineId
|
|
2895
|
+
});
|
|
2896
|
+
snapshots++;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
if (usage.extra_usage?.spend != null) {
|
|
2900
|
+
upsertUsageSnapshot(db, {
|
|
2901
|
+
agent: "claude",
|
|
2902
|
+
date: today,
|
|
2903
|
+
metric: "on_demand_usd",
|
|
2904
|
+
value: usage.extra_usage.spend,
|
|
2905
|
+
unit: "usd",
|
|
2906
|
+
machine_id: machineId
|
|
2907
|
+
});
|
|
2908
|
+
snapshots++;
|
|
2909
|
+
}
|
|
2910
|
+
if (usage.extra_usage?.limit != null) {
|
|
2911
|
+
upsertUsageSnapshot(db, {
|
|
2912
|
+
agent: "claude",
|
|
2913
|
+
date: today,
|
|
2914
|
+
metric: "on_demand_limit_usd",
|
|
2915
|
+
value: usage.extra_usage.limit,
|
|
2916
|
+
unit: "usd",
|
|
2917
|
+
machine_id: machineId
|
|
2918
|
+
});
|
|
2919
|
+
snapshots++;
|
|
2920
|
+
}
|
|
2921
|
+
const monthlyFee = inferMonthlyFee(auth.subscriptionType, auth.rateLimitTier);
|
|
2922
|
+
const now = new Date().toISOString();
|
|
2923
|
+
upsertSubscription(db, {
|
|
2924
|
+
id: "anthropic-claude-oauth",
|
|
2925
|
+
provider: "anthropic",
|
|
2926
|
+
agent: "claude",
|
|
2927
|
+
plan: auth.rateLimitTier ?? auth.subscriptionType ?? "claude_pro",
|
|
2928
|
+
monthly_fee_usd: monthlyFee,
|
|
2929
|
+
included_usage_usd: monthlyFee,
|
|
2930
|
+
billing_cycle_start: null,
|
|
2931
|
+
reset_policy: "monthly",
|
|
2932
|
+
active: 1,
|
|
2933
|
+
created_at: now,
|
|
2934
|
+
updated_at: now
|
|
2935
|
+
});
|
|
2936
|
+
setIngestState(db, "claude", `quota-${today}`, new Date().toISOString());
|
|
2937
|
+
if (verbose)
|
|
2938
|
+
console.log(` claude quota: ${snapshots} snapshots`);
|
|
2939
|
+
return { snapshots };
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// src/ingest/codex-quota.ts
|
|
2943
|
+
init_database();
|
|
2944
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
2945
|
+
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
2946
|
+
function readCodexAuth() {
|
|
2947
|
+
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
2948
|
+
if (fromEnv)
|
|
2949
|
+
return { token: fromEnv, authMode: "chatgpt" };
|
|
2950
|
+
const authPath = agentPaths().codexAuth;
|
|
2951
|
+
if (!existsSync9(authPath))
|
|
2952
|
+
return null;
|
|
2953
|
+
try {
|
|
2954
|
+
const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
|
|
2955
|
+
const token = auth.tokens?.access_token;
|
|
2956
|
+
if (!token)
|
|
2957
|
+
return null;
|
|
2958
|
+
return {
|
|
2959
|
+
token,
|
|
2960
|
+
accountId: auth.tokens?.account_id,
|
|
2961
|
+
authMode: auth.auth_mode ?? "chatgpt"
|
|
2962
|
+
};
|
|
2963
|
+
} catch {
|
|
2964
|
+
return null;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
function planMonthlyFee(planType) {
|
|
2968
|
+
const plan = (planType ?? "").toLowerCase();
|
|
2969
|
+
if (plan.includes("pro"))
|
|
2970
|
+
return 200;
|
|
2971
|
+
if (plan.includes("plus"))
|
|
2972
|
+
return 20;
|
|
2973
|
+
if (plan.includes("team"))
|
|
2974
|
+
return 30;
|
|
2975
|
+
return 20;
|
|
2976
|
+
}
|
|
2977
|
+
async function fetchCodexUsage(token, accountId) {
|
|
2978
|
+
try {
|
|
2979
|
+
const headers = {
|
|
2980
|
+
Authorization: `Bearer ${token}`,
|
|
2981
|
+
Accept: "application/json",
|
|
2982
|
+
"User-Agent": "economy-cli"
|
|
2983
|
+
};
|
|
2984
|
+
if (accountId)
|
|
2985
|
+
headers["ChatGPT-Account-Id"] = accountId;
|
|
2986
|
+
const res = await fetch(WHAM_USAGE_URL, {
|
|
2987
|
+
headers,
|
|
2988
|
+
signal: AbortSignal.timeout(1e4)
|
|
2989
|
+
});
|
|
2990
|
+
if (!res.ok)
|
|
2991
|
+
return null;
|
|
2992
|
+
return await res.json();
|
|
2993
|
+
} catch {
|
|
2994
|
+
return null;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
async function ingestCodexQuota(db, verbose = false) {
|
|
2998
|
+
const auth = readCodexAuth();
|
|
2999
|
+
if (!auth) {
|
|
3000
|
+
if (verbose)
|
|
3001
|
+
console.log(" codex quota: skipped \u2014 no ~/.codex/auth.json or CODEX_OAUTH_TOKEN");
|
|
3002
|
+
return { snapshots: 0 };
|
|
3003
|
+
}
|
|
3004
|
+
if (auth.authMode === "api_key" || auth.authMode === "api") {
|
|
3005
|
+
if (verbose)
|
|
3006
|
+
console.log(" codex quota: skipped \u2014 API key mode (no subscription quota)");
|
|
3007
|
+
return { snapshots: 0 };
|
|
3008
|
+
}
|
|
3009
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
3010
|
+
const prev = getIngestState(db, "codex", `quota-${today}`);
|
|
3011
|
+
if (prev)
|
|
3012
|
+
return { snapshots: 0 };
|
|
3013
|
+
const usage = await fetchCodexUsage(auth.token, auth.accountId);
|
|
3014
|
+
if (!usage) {
|
|
3015
|
+
if (verbose)
|
|
3016
|
+
console.log(" codex quota: wham/usage endpoint unavailable");
|
|
3017
|
+
return { snapshots: 0 };
|
|
3018
|
+
}
|
|
3019
|
+
const machineId = getMachineId();
|
|
3020
|
+
let snapshots = 0;
|
|
3021
|
+
const now = new Date().toISOString();
|
|
3022
|
+
const windows = [
|
|
3023
|
+
["five_hour_utilization", usage.rate_limit?.primary_window],
|
|
3024
|
+
["seven_day_utilization", usage.rate_limit?.secondary_window]
|
|
3025
|
+
];
|
|
3026
|
+
for (const [metric, window] of windows) {
|
|
3027
|
+
if (window?.used_percent == null)
|
|
3028
|
+
continue;
|
|
3029
|
+
upsertUsageSnapshot(db, {
|
|
3030
|
+
agent: "codex",
|
|
3031
|
+
date: today,
|
|
3032
|
+
metric,
|
|
3033
|
+
value: window.used_percent,
|
|
3034
|
+
unit: "percent",
|
|
3035
|
+
machine_id: machineId
|
|
3036
|
+
});
|
|
3037
|
+
snapshots++;
|
|
3038
|
+
if (window.reset_at) {
|
|
3039
|
+
upsertUsageSnapshot(db, {
|
|
3040
|
+
agent: "codex",
|
|
3041
|
+
date: today,
|
|
3042
|
+
metric: `${metric}_resets_at`,
|
|
3043
|
+
value: window.reset_at * 1000,
|
|
3044
|
+
unit: "epoch_ms",
|
|
3045
|
+
machine_id: machineId
|
|
3046
|
+
});
|
|
3047
|
+
snapshots++;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
if (usage.credits?.balance != null) {
|
|
3051
|
+
upsertUsageSnapshot(db, {
|
|
3052
|
+
agent: "codex",
|
|
3053
|
+
date: today,
|
|
3054
|
+
metric: "credits_balance_usd",
|
|
3055
|
+
value: usage.credits.balance,
|
|
3056
|
+
unit: "usd",
|
|
3057
|
+
machine_id: machineId
|
|
3058
|
+
});
|
|
3059
|
+
snapshots++;
|
|
3060
|
+
}
|
|
3061
|
+
const monthlyFee = planMonthlyFee(usage.plan_type);
|
|
3062
|
+
upsertSubscription(db, {
|
|
3063
|
+
id: "openai-codex-oauth",
|
|
3064
|
+
provider: "openai",
|
|
3065
|
+
agent: "codex",
|
|
3066
|
+
plan: usage.plan_type ?? "chatgpt_plus",
|
|
3067
|
+
monthly_fee_usd: monthlyFee,
|
|
3068
|
+
included_usage_usd: monthlyFee,
|
|
3069
|
+
billing_cycle_start: null,
|
|
3070
|
+
reset_policy: "monthly",
|
|
3071
|
+
active: 1,
|
|
3072
|
+
created_at: now,
|
|
3073
|
+
updated_at: now
|
|
3074
|
+
});
|
|
3075
|
+
setIngestState(db, "codex", `quota-${today}`, now);
|
|
3076
|
+
if (verbose)
|
|
3077
|
+
console.log(` codex quota: ${snapshots} snapshots (${usage.plan_type ?? "unknown plan"})`);
|
|
3078
|
+
return { snapshots };
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// src/lib/sync-all.ts
|
|
3082
|
+
init_database();
|
|
3083
|
+
|
|
3084
|
+
// src/lib/cloud-sync.ts
|
|
3085
|
+
init_database();
|
|
3086
|
+
|
|
3087
|
+
// src/lib/package-metadata.ts
|
|
3088
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3089
|
+
var cachedMetadata = null;
|
|
3090
|
+
function getPackageMetadata() {
|
|
3091
|
+
if (cachedMetadata)
|
|
3092
|
+
return cachedMetadata;
|
|
3093
|
+
const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
|
|
3094
|
+
const parsed = JSON.parse(raw);
|
|
3095
|
+
cachedMetadata = {
|
|
3096
|
+
name: parsed.name ?? "@hasna/economy",
|
|
3097
|
+
version: parsed.version ?? "0.0.0"
|
|
3098
|
+
};
|
|
3099
|
+
return cachedMetadata;
|
|
3100
|
+
}
|
|
3101
|
+
var packageMetadata = getPackageMetadata();
|
|
3102
|
+
|
|
3103
|
+
// src/lib/cloud-sync.ts
|
|
3104
|
+
var CLOUD_TABLES = [
|
|
3105
|
+
"requests",
|
|
3106
|
+
"sessions",
|
|
3107
|
+
"projects",
|
|
3108
|
+
"budgets",
|
|
3109
|
+
"goals",
|
|
3110
|
+
"model_pricing",
|
|
3111
|
+
"billing_daily",
|
|
3112
|
+
"subscriptions",
|
|
3113
|
+
"usage_snapshots",
|
|
3114
|
+
"savings_daily",
|
|
3115
|
+
"machines",
|
|
3116
|
+
"ingest_state"
|
|
3117
|
+
];
|
|
3118
|
+
function getCloudDatabaseUrl() {
|
|
3119
|
+
return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
|
|
3120
|
+
}
|
|
3121
|
+
function isCloudAutoEnabled() {
|
|
3122
|
+
return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
|
|
3123
|
+
}
|
|
3124
|
+
function getCloudPullIntervalMinutes() {
|
|
3125
|
+
const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
|
|
3126
|
+
if (!raw)
|
|
3127
|
+
return 15;
|
|
3128
|
+
const n = Number(raw);
|
|
3129
|
+
return Number.isFinite(n) && n > 0 ? n : 15;
|
|
3130
|
+
}
|
|
3131
|
+
async function getCloudPg() {
|
|
3132
|
+
const url = getCloudDatabaseUrl();
|
|
3133
|
+
if (!url) {
|
|
3134
|
+
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
3135
|
+
}
|
|
3136
|
+
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
3137
|
+
return new PgAdapterAsync(url);
|
|
3138
|
+
}
|
|
3139
|
+
async function runCloudMigrations(cloud) {
|
|
3140
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3141
|
+
for (const sql of PG_MIGRATIONS2) {
|
|
3142
|
+
await cloud.run(sql);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
async function cloudPush(opts) {
|
|
3146
|
+
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
3147
|
+
const cloud = await getCloudPg();
|
|
3148
|
+
const local = new SqliteAdapter(getDbPath());
|
|
3149
|
+
await runCloudMigrations(cloud);
|
|
3150
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3151
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3152
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3153
|
+
touchMachineRegistry(local, "push");
|
|
3154
|
+
local.close();
|
|
3155
|
+
await cloud.close();
|
|
3156
|
+
return { rows, machine: getMachineId() };
|
|
3157
|
+
}
|
|
3158
|
+
async function cloudPull(opts) {
|
|
3159
|
+
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3160
|
+
const cloud = await getCloudPg();
|
|
3161
|
+
const local = new SqliteAdapter(getDbPath());
|
|
3162
|
+
await runCloudMigrations(cloud);
|
|
3163
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3164
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3165
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3166
|
+
touchMachineRegistry(local, "pull");
|
|
3167
|
+
local.close();
|
|
3168
|
+
await cloud.close();
|
|
3169
|
+
setLastCloudPull();
|
|
3170
|
+
return { rows, machine: getMachineId() };
|
|
3171
|
+
}
|
|
3172
|
+
function setLastCloudPull(at = new Date().toISOString()) {
|
|
3173
|
+
const db = openDatabase();
|
|
3174
|
+
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
3175
|
+
}
|
|
3176
|
+
function getLastCloudPull() {
|
|
3177
|
+
const db = openDatabase();
|
|
3178
|
+
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
3179
|
+
return row?.value ?? null;
|
|
3180
|
+
}
|
|
3181
|
+
function shouldPullFromCloud() {
|
|
3182
|
+
if (!getCloudDatabaseUrl())
|
|
3183
|
+
return false;
|
|
3184
|
+
const last = getLastCloudPull();
|
|
3185
|
+
if (!last)
|
|
3186
|
+
return true;
|
|
3187
|
+
const ageMs = Date.now() - new Date(last).getTime();
|
|
3188
|
+
return ageMs > getCloudPullIntervalMinutes() * 60000;
|
|
3189
|
+
}
|
|
3190
|
+
async function maybePullFromCloud() {
|
|
3191
|
+
if (!shouldPullFromCloud())
|
|
3192
|
+
return false;
|
|
3193
|
+
try {
|
|
3194
|
+
await cloudPull();
|
|
3195
|
+
return true;
|
|
3196
|
+
} catch {
|
|
3197
|
+
return false;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
async function maybePushAfterIngest() {
|
|
3201
|
+
if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
|
|
3202
|
+
return false;
|
|
3203
|
+
try {
|
|
3204
|
+
await cloudPush();
|
|
3205
|
+
return true;
|
|
3206
|
+
} catch {
|
|
3207
|
+
return false;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
function touchMachineRegistry(db, direction) {
|
|
3211
|
+
const now = new Date().toISOString();
|
|
3212
|
+
const machine = getMachineId();
|
|
3213
|
+
db.prepare(`
|
|
3214
|
+
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
3215
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3216
|
+
ON CONFLICT(machine_id) DO UPDATE SET
|
|
3217
|
+
hostname = excluded.hostname,
|
|
3218
|
+
last_seen_at = excluded.last_seen_at,
|
|
3219
|
+
last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
|
|
3220
|
+
last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
|
|
3221
|
+
economy_version = excluded.economy_version,
|
|
3222
|
+
updated_at = excluded.updated_at
|
|
3223
|
+
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
// src/lib/sync-all.ts
|
|
3227
|
+
async function syncAll(db, opts = {}) {
|
|
3228
|
+
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
3229
|
+
const all = !anySpecific;
|
|
3230
|
+
await maybePullFromCloud();
|
|
3231
|
+
const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
|
|
3232
|
+
if (all || opts.claude) {
|
|
3233
|
+
result.claude = await ingestClaude(db, opts.verbose);
|
|
3234
|
+
result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
|
|
3235
|
+
}
|
|
3236
|
+
if (all || opts.takumi)
|
|
3237
|
+
result.takumi = await ingestTakumi(db, opts.verbose);
|
|
3238
|
+
if (all || opts.codex) {
|
|
3239
|
+
result.codex = await ingestCodex(db, opts.verbose);
|
|
3240
|
+
result.codexQuota = await ingestCodexQuota(db, opts.verbose);
|
|
3241
|
+
}
|
|
3242
|
+
if (all || opts.gemini)
|
|
3243
|
+
result.gemini = await ingestGemini(db, opts.verbose);
|
|
3244
|
+
if (all || opts.opencode)
|
|
3245
|
+
result.opencode = await ingestOpenCode(db, opts.verbose);
|
|
3246
|
+
if (all || opts.cursor)
|
|
3247
|
+
result.cursor = await ingestCursor(db, opts.verbose);
|
|
3248
|
+
if (all || opts.pi)
|
|
3249
|
+
result.pi = await ingestPi(db, opts.verbose);
|
|
3250
|
+
if (all || opts.hermes)
|
|
3251
|
+
result.hermes = await ingestHermes(db, opts.verbose);
|
|
3252
|
+
result.deduped = dedupeRequests(db);
|
|
3253
|
+
result.cloudPushed = await maybePushAfterIngest();
|
|
3254
|
+
return result;
|
|
851
3255
|
}
|
|
852
3256
|
|
|
3257
|
+
// src/lib/agents.ts
|
|
3258
|
+
var AGENTS = [
|
|
3259
|
+
"claude",
|
|
3260
|
+
"takumi",
|
|
3261
|
+
"codex",
|
|
3262
|
+
"gemini",
|
|
3263
|
+
"opencode",
|
|
3264
|
+
"cursor",
|
|
3265
|
+
"pi",
|
|
3266
|
+
"hermes"
|
|
3267
|
+
];
|
|
3268
|
+
|
|
853
3269
|
// src/mcp/index.ts
|
|
3270
|
+
init_database();
|
|
3271
|
+
init_pricing();
|
|
854
3272
|
init_pricing();
|
|
3273
|
+
function printHelp() {
|
|
3274
|
+
console.log(`Usage: economy-mcp [options]
|
|
3275
|
+
|
|
3276
|
+
Runs the ${packageMetadata.name} MCP stdio server.
|
|
3277
|
+
|
|
3278
|
+
Options:
|
|
3279
|
+
-V, --version output the version number
|
|
3280
|
+
-h, --help display help for command`);
|
|
3281
|
+
}
|
|
3282
|
+
var args = process.argv.slice(2);
|
|
3283
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3284
|
+
printHelp();
|
|
3285
|
+
process.exit(0);
|
|
3286
|
+
}
|
|
3287
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
3288
|
+
console.log(packageMetadata.version);
|
|
3289
|
+
process.exit(0);
|
|
3290
|
+
}
|
|
855
3291
|
var db = openDatabase();
|
|
856
3292
|
ensurePricingSeeded(db);
|
|
857
|
-
var server = new
|
|
3293
|
+
var server = new McpServer({
|
|
3294
|
+
name: "economy",
|
|
3295
|
+
version: packageMetadata.version
|
|
3296
|
+
});
|
|
3297
|
+
var _econAgents = new Map;
|
|
3298
|
+
var TOOL_NAMES = [
|
|
3299
|
+
"get_cost_summary",
|
|
3300
|
+
"get_sessions",
|
|
3301
|
+
"get_top_sessions",
|
|
3302
|
+
"get_model_breakdown",
|
|
3303
|
+
"get_project_breakdown",
|
|
3304
|
+
"get_agent_breakdown",
|
|
3305
|
+
"get_account_breakdown",
|
|
3306
|
+
"get_budget_status",
|
|
3307
|
+
"set_budget",
|
|
3308
|
+
"remove_budget",
|
|
3309
|
+
"get_pricing",
|
|
3310
|
+
"set_pricing",
|
|
3311
|
+
"remove_pricing",
|
|
3312
|
+
"get_daily",
|
|
3313
|
+
"get_billing_summary",
|
|
3314
|
+
"get_session_detail",
|
|
3315
|
+
"get_usage",
|
|
3316
|
+
"get_savings",
|
|
3317
|
+
"estimate_cost",
|
|
3318
|
+
"sync",
|
|
3319
|
+
"search_tools",
|
|
3320
|
+
"describe_tools",
|
|
3321
|
+
"get_goals",
|
|
3322
|
+
"set_goal",
|
|
3323
|
+
"remove_goal",
|
|
3324
|
+
"list_machines",
|
|
3325
|
+
"register_agent",
|
|
3326
|
+
"heartbeat",
|
|
3327
|
+
"set_focus",
|
|
3328
|
+
"list_agents",
|
|
3329
|
+
"send_feedback"
|
|
3330
|
+
];
|
|
3331
|
+
var TOOL_DESCRIPTIONS = {
|
|
3332
|
+
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
3333
|
+
get_sessions: `agent(${AGENTS.join("|")}), project(partial), machine?(hostname), limit(20) -> compact session table`,
|
|
3334
|
+
get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
|
|
3335
|
+
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
3336
|
+
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
3337
|
+
get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
|
|
3338
|
+
get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
|
|
3339
|
+
get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
|
|
3340
|
+
get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
|
|
3341
|
+
set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
|
|
3342
|
+
remove_budget: "id -> delete budget",
|
|
3343
|
+
get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
|
|
3344
|
+
set_pricing: "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? -> create/update pricing",
|
|
3345
|
+
remove_pricing: "model -> delete pricing row",
|
|
3346
|
+
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
3347
|
+
get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
|
|
3348
|
+
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
3349
|
+
get_usage: `period(today|week|month), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
|
|
3350
|
+
get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
|
|
3351
|
+
estimate_cost: "model, input_tokens?, output_tokens? -> pre-flight token cost estimate",
|
|
3352
|
+
sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
|
|
3353
|
+
search_tools: "query substring -> tool name list",
|
|
3354
|
+
describe_tools: "names[] -> one-line parameter hints",
|
|
3355
|
+
get_goals: "no params -> goal progress summary",
|
|
3356
|
+
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
3357
|
+
remove_goal: "id -> delete goal",
|
|
3358
|
+
register_agent: "name, session_id? -> register agent session",
|
|
3359
|
+
heartbeat: "agent_id -> update last_seen_at",
|
|
3360
|
+
set_focus: "agent_id, project_id? -> set active project context",
|
|
3361
|
+
list_agents: "no params -> registered agent list",
|
|
3362
|
+
send_feedback: "message, email?, category? -> save feedback locally"
|
|
3363
|
+
};
|
|
858
3364
|
var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
859
3365
|
var fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
860
3366
|
function fmtSession(s) {
|
|
@@ -863,264 +3369,339 @@ function fmtSession(s) {
|
|
|
863
3369
|
const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
|
|
864
3370
|
const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
|
|
865
3371
|
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
866
|
-
return `${id} ${agent.padEnd(
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
{
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
{ name: "set_goal", description: "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", inputSchema: { type: "object", properties: { period: { type: "string" }, limit_usd: { type: "number" }, project_path: { type: "string" }, agent: { type: "string" } }, required: ["period", "limit_usd"] } },
|
|
882
|
-
{ name: "remove_goal", description: "Delete a goal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
883
|
-
{ name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
|
|
884
|
-
{ name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
|
|
885
|
-
{ name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
|
|
886
|
-
{ name: "list_agents", description: "List all registered agents.", inputSchema: { type: "object", properties: {} } },
|
|
887
|
-
{ name: "send_feedback", description: "Send feedback about this service.", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } }
|
|
888
|
-
];
|
|
889
|
-
var TOOL_DESCRIPTIONS = {
|
|
890
|
-
get_cost_summary: "period(today|week|month|year|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
|
|
891
|
-
get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
|
|
892
|
-
get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
|
|
893
|
-
get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
|
|
894
|
-
get_project_breakdown: "no params \u2192 project_name, sessions, cost",
|
|
895
|
-
get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
|
|
896
|
-
get_daily: "days(30) \u2192 daily cost table grouped by date and agent",
|
|
897
|
-
get_session_detail: "session_id(prefix ok) \u2192 per-request breakdown with model, tokens, cost",
|
|
898
|
-
sync: "sources(all|claude|codex|gemini) \u2192 {files, requests, sessions} ingested",
|
|
899
|
-
get_goals: "no params \u2192 period, scope, limit, spent, percent, status(ON TRACK/AT RISK/OVER)",
|
|
900
|
-
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? \u2192 creates/updates goal",
|
|
901
|
-
remove_goal: "id \u2192 deletes goal"
|
|
902
|
-
};
|
|
903
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
904
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
905
|
-
const { name, arguments: args } = req.params;
|
|
906
|
-
const a = args ?? {};
|
|
907
|
-
try {
|
|
908
|
-
switch (name) {
|
|
909
|
-
case "search_tools": {
|
|
910
|
-
const q = a["query"]?.toLowerCase();
|
|
911
|
-
const names = TOOLS.map((t) => t.name);
|
|
912
|
-
const matches = q ? names.filter((n) => n.includes(q)) : names;
|
|
913
|
-
return { content: [{ type: "text", text: matches.join(", ") }] };
|
|
914
|
-
}
|
|
915
|
-
case "describe_tools": {
|
|
916
|
-
const names = a["names"] ?? [];
|
|
917
|
-
const result = names.map((n) => `${n}: ${TOOL_DESCRIPTIONS[n] ?? "see tool schema"}`).join(`
|
|
918
|
-
`);
|
|
919
|
-
return { content: [{ type: "text", text: result }] };
|
|
920
|
-
}
|
|
921
|
-
case "get_cost_summary": {
|
|
922
|
-
const period = a["period"] ?? "today";
|
|
923
|
-
const s = querySummary(db, period);
|
|
924
|
-
const text = [
|
|
925
|
-
`period: ${period}`,
|
|
926
|
-
`cost: ${fmtUsd(s.total_usd)}`,
|
|
927
|
-
`sessions: ${s.sessions}`,
|
|
928
|
-
`requests: ${s.requests.toLocaleString()}`,
|
|
929
|
-
`tokens: ${fmtTok(s.tokens)}`,
|
|
930
|
-
`summary: You've spent ${fmtUsd(s.total_usd)} ${period === "all" ? "total" : period} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
|
|
931
|
-
].join(`
|
|
3372
|
+
return `${id} ${agent.padEnd(9)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
3373
|
+
}
|
|
3374
|
+
function text(text2) {
|
|
3375
|
+
return { content: [{ type: "text", text: text2 }] };
|
|
3376
|
+
}
|
|
3377
|
+
function textError(message) {
|
|
3378
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
3379
|
+
}
|
|
3380
|
+
server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
|
|
3381
|
+
const q = query?.toLowerCase();
|
|
3382
|
+
const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
|
|
3383
|
+
return text(matches.join(", "));
|
|
3384
|
+
});
|
|
3385
|
+
server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
|
|
3386
|
+
const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
|
|
932
3387
|
`);
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
`) }
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
3388
|
+
return text(result);
|
|
3389
|
+
});
|
|
3390
|
+
server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
|
|
3391
|
+
const resolved = period ?? "today";
|
|
3392
|
+
const s = querySummary(db, resolved, machine);
|
|
3393
|
+
const machineLabel = machine ? ` on ${machine}` : "";
|
|
3394
|
+
return text([
|
|
3395
|
+
`period: ${resolved}${machineLabel}`,
|
|
3396
|
+
`cost: ${fmtUsd(s.total_usd)}`,
|
|
3397
|
+
`sessions: ${s.sessions}`,
|
|
3398
|
+
`requests: ${s.requests.toLocaleString()}`,
|
|
3399
|
+
`tokens: ${fmtTok(s.tokens)}`,
|
|
3400
|
+
`summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved}${machineLabel} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
|
|
3401
|
+
].join(`
|
|
3402
|
+
`));
|
|
3403
|
+
});
|
|
3404
|
+
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
|
|
3405
|
+
agent: z.enum(AGENTS).optional(),
|
|
3406
|
+
project: z.string().optional(),
|
|
3407
|
+
machine: z.string().optional(),
|
|
3408
|
+
limit: z.number().int().positive().max(100).optional()
|
|
3409
|
+
}, async ({ agent, project, machine, limit }) => {
|
|
3410
|
+
const sessions = querySessions(db, {
|
|
3411
|
+
agent,
|
|
3412
|
+
project,
|
|
3413
|
+
machine,
|
|
3414
|
+
limit: limit ?? 20
|
|
3415
|
+
});
|
|
3416
|
+
const lines = ["id agent cost tokens project"];
|
|
3417
|
+
for (const session of sessions)
|
|
3418
|
+
lines.push(fmtSession(session));
|
|
3419
|
+
return text(lines.join(`
|
|
3420
|
+
`));
|
|
3421
|
+
});
|
|
3422
|
+
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
3423
|
+
n: z.number().int().positive().max(100).optional(),
|
|
3424
|
+
agent: z.enum(AGENTS).optional()
|
|
3425
|
+
}, async ({ n, agent }) => {
|
|
3426
|
+
const sessions = queryTopSessions(db, n ?? 10, agent);
|
|
3427
|
+
const lines = ["rank id agent cost tokens project"];
|
|
3428
|
+
sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
|
|
3429
|
+
return text(lines.join(`
|
|
3430
|
+
`));
|
|
3431
|
+
});
|
|
3432
|
+
server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
|
|
3433
|
+
const rows = queryModelBreakdown(db);
|
|
3434
|
+
const lines = ["model agent reqs tokens cost"];
|
|
3435
|
+
for (const row of rows) {
|
|
3436
|
+
lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["agent"]).padEnd(10)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
3437
|
+
}
|
|
3438
|
+
return text(lines.join(`
|
|
3439
|
+
`));
|
|
3440
|
+
});
|
|
3441
|
+
server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all).", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
3442
|
+
const rows = queryProjectBreakdown(db, period ?? "all");
|
|
3443
|
+
const lines = ["project sessions tokens cost"];
|
|
3444
|
+
for (const row of rows) {
|
|
3445
|
+
const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
|
|
3446
|
+
lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
3447
|
+
}
|
|
3448
|
+
return text(lines.join(`
|
|
3449
|
+
`));
|
|
3450
|
+
});
|
|
3451
|
+
server.tool("get_agent_breakdown", "Cost per coding agent. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
3452
|
+
const rows = queryAgentBreakdown(db, period ?? "all");
|
|
3453
|
+
if (rows.length === 0)
|
|
3454
|
+
return text("No agent usage yet.");
|
|
3455
|
+
const lines = ["agent sessions requests tokens api_eq billable included"];
|
|
3456
|
+
for (const row of rows) {
|
|
3457
|
+
lines.push(`${String(row["agent"]).slice(0, 10).padEnd(11)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["api_equivalent_usd"] ?? row["cost_usd"])).padEnd(10)}` + `${fmtUsd(Number(row["billable_usd"] ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row["subscription_included_usd"] ?? 0))}`);
|
|
3458
|
+
}
|
|
3459
|
+
return text(lines.join(`
|
|
3460
|
+
`));
|
|
3461
|
+
});
|
|
3462
|
+
server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
3463
|
+
const rows = queryAccountBreakdown(db, period ?? "all");
|
|
3464
|
+
if (rows.length === 0)
|
|
3465
|
+
return text("No account-attributed sessions yet.");
|
|
3466
|
+
const lines = ["account sessions requests tokens api_eq billable included"];
|
|
3467
|
+
for (const row of rows) {
|
|
3468
|
+
const label = String(row["account_key"] || row["account_name"] || "\u2014").slice(0, 20);
|
|
3469
|
+
lines.push(`${label.padEnd(21)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["api_equivalent_usd"] ?? row["cost_usd"])).padEnd(10)}` + `${fmtUsd(Number(row["billable_usd"] ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row["subscription_included_usd"] ?? 0))}`);
|
|
3470
|
+
}
|
|
3471
|
+
return text(lines.join(`
|
|
3472
|
+
`));
|
|
3473
|
+
});
|
|
3474
|
+
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
|
|
3475
|
+
const budgets = getBudgetStatuses(db);
|
|
3476
|
+
if (budgets.length === 0)
|
|
3477
|
+
return text("No budgets set.");
|
|
3478
|
+
const lines = ["scope period spent limit used% status"];
|
|
3479
|
+
for (const budget of budgets) {
|
|
3480
|
+
const scope = String(budget["project_path"] ?? "global").slice(0, 20);
|
|
3481
|
+
const pct = Number(budget["percent_used"]).toFixed(1);
|
|
3482
|
+
const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
|
|
3483
|
+
lines.push(`${scope.padEnd(21)}${String(budget["period"]).padEnd(9)}${fmtUsd(Number(budget["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(budget["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
|
|
3484
|
+
}
|
|
3485
|
+
return text(lines.join(`
|
|
3486
|
+
`));
|
|
3487
|
+
});
|
|
3488
|
+
server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
|
|
3489
|
+
period: z.enum(["daily", "weekly", "monthly"]),
|
|
3490
|
+
limit_usd: z.number().positive(),
|
|
3491
|
+
project_path: z.string().optional(),
|
|
3492
|
+
agent: z.enum(AGENTS).optional(),
|
|
3493
|
+
alert_at_percent: z.number().positive().max(100).optional()
|
|
3494
|
+
}, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
|
|
3495
|
+
const now = new Date().toISOString();
|
|
3496
|
+
const id = randomUUID();
|
|
3497
|
+
upsertBudget(db, {
|
|
3498
|
+
id,
|
|
3499
|
+
project_path: project_path ?? null,
|
|
3500
|
+
agent: agent ?? null,
|
|
3501
|
+
period,
|
|
3502
|
+
limit_usd,
|
|
3503
|
+
alert_at_percent: alert_at_percent ?? 80,
|
|
3504
|
+
created_at: now,
|
|
3505
|
+
updated_at: now
|
|
3506
|
+
});
|
|
3507
|
+
return text(`Budget set: ${id}`);
|
|
3508
|
+
});
|
|
3509
|
+
server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
|
|
3510
|
+
deleteBudget(db, id);
|
|
3511
|
+
return text("Budget removed.");
|
|
3512
|
+
});
|
|
3513
|
+
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
|
|
3514
|
+
const rows = listModelPricing(db);
|
|
3515
|
+
const lines = ["model input output cache-r cache-w cache-1h storage-h"];
|
|
3516
|
+
for (const row of rows) {
|
|
3517
|
+
lines.push(`${row.model.slice(0, 30).padEnd(31)}` + `${fmtUsd(row.input_per_1m).padEnd(9)}` + `${fmtUsd(row.output_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_read_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_1h_per_1m ?? 0).padEnd(9)}` + `${fmtUsd(row.cache_storage_per_1m_hour ?? 0)}`);
|
|
3518
|
+
}
|
|
3519
|
+
return text(lines.join(`
|
|
3520
|
+
`));
|
|
3521
|
+
});
|
|
3522
|
+
server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
|
|
3523
|
+
model: z.string().min(1),
|
|
3524
|
+
input_per_1m: z.number().nonnegative(),
|
|
3525
|
+
output_per_1m: z.number().nonnegative(),
|
|
3526
|
+
cache_read_per_1m: z.number().nonnegative().optional(),
|
|
3527
|
+
cache_write_per_1m: z.number().nonnegative().optional(),
|
|
3528
|
+
cache_write_1h_per_1m: z.number().nonnegative().optional(),
|
|
3529
|
+
cache_storage_per_1m_hour: z.number().nonnegative().optional()
|
|
3530
|
+
}, async (input) => {
|
|
3531
|
+
const model = input.model.trim();
|
|
3532
|
+
if (!model)
|
|
3533
|
+
return textError("model is required");
|
|
3534
|
+
upsertModelPricing(db, {
|
|
3535
|
+
model,
|
|
3536
|
+
input_per_1m: input.input_per_1m,
|
|
3537
|
+
output_per_1m: input.output_per_1m,
|
|
3538
|
+
cache_read_per_1m: input.cache_read_per_1m ?? 0,
|
|
3539
|
+
cache_write_per_1m: input.cache_write_per_1m ?? 0,
|
|
3540
|
+
cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
|
|
3541
|
+
cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0,
|
|
3542
|
+
updated_at: new Date().toISOString()
|
|
3543
|
+
});
|
|
3544
|
+
return text(`Pricing set: ${model}`);
|
|
3545
|
+
});
|
|
3546
|
+
server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
|
|
3547
|
+
deleteModelPricing(db, model);
|
|
3548
|
+
return text("Pricing removed.");
|
|
3549
|
+
});
|
|
3550
|
+
server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
|
|
3551
|
+
const rows = queryDailyBreakdown(db, days ?? 30);
|
|
3552
|
+
const byDate = new Map;
|
|
3553
|
+
for (const row of rows) {
|
|
3554
|
+
const date = String(row["date"]);
|
|
3555
|
+
const agent = String(row["agent"]);
|
|
3556
|
+
const entry = byDate.get(date) ?? Object.fromEntries(AGENTS.map((name) => [name, 0]));
|
|
3557
|
+
entry[agent] = (entry[agent] ?? 0) + Number(row["cost_usd"]);
|
|
3558
|
+
byDate.set(date, entry);
|
|
3559
|
+
}
|
|
3560
|
+
const lines = [`date ${AGENTS.map((agent) => agent.slice(0, 8).padEnd(10)).join("")}total`];
|
|
3561
|
+
for (const [date, costs] of [...byDate.entries()].sort()) {
|
|
3562
|
+
const total = Object.values(costs).reduce((sum, value) => sum + value, 0);
|
|
3563
|
+
lines.push(`${date} ${AGENTS.map((agent) => fmtUsd(costs[agent] ?? 0).padEnd(10)).join("")}${fmtUsd(total)}`);
|
|
3564
|
+
}
|
|
3565
|
+
return text(lines.join(`
|
|
3566
|
+
`));
|
|
3567
|
+
});
|
|
3568
|
+
server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period: z.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
3569
|
+
const summary = queryBillingSummary(db, period ?? "month");
|
|
3570
|
+
const lines = ["provider billed"];
|
|
3571
|
+
for (const [provider, cost] of Object.entries(summary.by_provider)) {
|
|
3572
|
+
lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
|
|
3573
|
+
}
|
|
3574
|
+
lines.push(`total ${fmtUsd(summary.total_usd)}`);
|
|
3575
|
+
return text(lines.join(`
|
|
3576
|
+
`));
|
|
3577
|
+
});
|
|
3578
|
+
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
|
|
3579
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
|
|
3580
|
+
if (!session)
|
|
3581
|
+
return textError(`Session not found: ${session_id}`);
|
|
3582
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
|
|
3583
|
+
const lines = [
|
|
3584
|
+
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
3585
|
+
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
3586
|
+
`cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
|
|
3587
|
+
"",
|
|
3588
|
+
"time model input output cache-r cache-5m cache-1h cost"
|
|
3589
|
+
];
|
|
3590
|
+
for (const request of requests) {
|
|
3591
|
+
lines.push(`${String(request["timestamp"]).slice(11, 19)} ` + `${String(request["model"]).slice(0, 22).padEnd(23)}` + `${fmtTok(Number(request["input_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["output_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_read_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_create_5m_tokens"] ?? request["cache_create_tokens"] ?? 0)).padEnd(9)}` + `${fmtTok(Number(request["cache_create_1h_tokens"] ?? 0)).padEnd(9)}` + `${fmtUsd(Number(request["cost_usd"]))}`);
|
|
3592
|
+
}
|
|
3593
|
+
return text(lines.join(`
|
|
3594
|
+
`));
|
|
3595
|
+
});
|
|
3596
|
+
server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
|
|
3597
|
+
const selected = sources ?? "all";
|
|
3598
|
+
const opts = selected === "all" ? {} : { [selected]: true };
|
|
3599
|
+
const result = await syncAll(db, opts);
|
|
3600
|
+
return text(JSON.stringify(result, null, 2));
|
|
3601
|
+
});
|
|
3602
|
+
server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month", { period: z.enum(["today", "week", "month"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
3603
|
+
const p = period ?? "month";
|
|
3604
|
+
const snaps = queryUsageSnapshots(db, { agent });
|
|
3605
|
+
const summary = querySummary(db, p, undefined, true);
|
|
3606
|
+
return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
|
|
3607
|
+
});
|
|
3608
|
+
server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
3609
|
+
return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
|
|
3610
|
+
});
|
|
3611
|
+
server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z.string(), input_tokens: z.number().optional(), output_tokens: z.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
|
|
3612
|
+
const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
|
|
3613
|
+
return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
|
|
3614
|
+
});
|
|
3615
|
+
server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
|
|
3616
|
+
const goals = getGoalStatuses(db);
|
|
3617
|
+
if (goals.length === 0)
|
|
3618
|
+
return text("No goals set.");
|
|
3619
|
+
const lines = ["period scope limit spent used% status"];
|
|
3620
|
+
for (const goal of goals) {
|
|
3621
|
+
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
3622
|
+
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
3623
|
+
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
3624
|
+
lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
|
|
3625
|
+
}
|
|
3626
|
+
return text(lines.join(`
|
|
3627
|
+
`));
|
|
3628
|
+
});
|
|
3629
|
+
server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
|
|
3630
|
+
period: z.enum(["day", "week", "month", "year"]),
|
|
3631
|
+
limit_usd: z.number().positive(),
|
|
3632
|
+
project_path: z.string().optional(),
|
|
3633
|
+
agent: z.enum(AGENTS).optional()
|
|
3634
|
+
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
3635
|
+
const now = new Date().toISOString();
|
|
3636
|
+
upsertGoal(db, {
|
|
3637
|
+
id: randomUUID(),
|
|
3638
|
+
period,
|
|
3639
|
+
project_path: project_path ?? null,
|
|
3640
|
+
agent: agent ?? null,
|
|
3641
|
+
limit_usd,
|
|
3642
|
+
created_at: now,
|
|
3643
|
+
updated_at: now
|
|
3644
|
+
});
|
|
3645
|
+
return text(`Goal set: ${period} $${limit_usd}`);
|
|
3646
|
+
});
|
|
3647
|
+
server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
|
|
3648
|
+
deleteGoal(db, id);
|
|
3649
|
+
return text("Goal removed.");
|
|
3650
|
+
});
|
|
3651
|
+
server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
|
|
3652
|
+
const machines = listMachines(db);
|
|
3653
|
+
if (machines.length === 0)
|
|
3654
|
+
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
3655
|
+
const lines = ["machine sessions requests cost last_active"];
|
|
3656
|
+
for (const m of machines) {
|
|
3657
|
+
lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
|
|
3658
|
+
}
|
|
3659
|
+
lines.push(`
|
|
3660
|
+
current machine: ${getMachineId()}`);
|
|
3661
|
+
return text(lines.join(`
|
|
3662
|
+
`));
|
|
3663
|
+
});
|
|
3664
|
+
server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
|
|
3665
|
+
const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
|
|
3666
|
+
if (existing) {
|
|
3667
|
+
existing.last_seen_at = new Date().toISOString();
|
|
3668
|
+
return text(JSON.stringify(existing));
|
|
3669
|
+
}
|
|
3670
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
3671
|
+
const agent = { id, name, last_seen_at: new Date().toISOString() };
|
|
3672
|
+
_econAgents.set(id, agent);
|
|
3673
|
+
return text(JSON.stringify(agent));
|
|
3674
|
+
});
|
|
3675
|
+
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
3676
|
+
const agent = _econAgents.get(agent_id);
|
|
3677
|
+
if (!agent)
|
|
3678
|
+
return textError("Agent not found");
|
|
3679
|
+
agent.last_seen_at = new Date().toISOString();
|
|
3680
|
+
return text(`\u2665 ${agent.name}`);
|
|
3681
|
+
});
|
|
3682
|
+
server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
|
|
3683
|
+
const agent = _econAgents.get(agent_id);
|
|
3684
|
+
if (!agent)
|
|
3685
|
+
return textError("Agent not found");
|
|
3686
|
+
agent.project_id = project_id ?? undefined;
|
|
3687
|
+
return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
|
|
3688
|
+
});
|
|
3689
|
+
server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
|
|
3690
|
+
server.tool("send_feedback", "Send feedback about this service.", {
|
|
3691
|
+
message: z.string(),
|
|
3692
|
+
email: z.string().optional(),
|
|
3693
|
+
category: z.enum(["bug", "feature", "general"]).optional()
|
|
3694
|
+
}, async ({ message, email, category }) => {
|
|
3695
|
+
try {
|
|
3696
|
+
db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
|
|
3697
|
+
return text("Feedback saved. Thank you!");
|
|
3698
|
+
} catch (error) {
|
|
3699
|
+
return textError(String(error));
|
|
1122
3700
|
}
|
|
1123
3701
|
});
|
|
1124
|
-
var _econAgents = new Map;
|
|
1125
3702
|
var transport = new StdioServerTransport;
|
|
3703
|
+
registerCloudTools(server, "economy", {
|
|
3704
|
+
dbPath: getDbPath(),
|
|
3705
|
+
migrations: PG_MIGRATIONS
|
|
3706
|
+
});
|
|
1126
3707
|
await server.connect(transport);
|