@hasna/economy 0.2.17 → 0.2.19
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 +187 -13
- package/README.md +199 -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 +4845 -1001
- package/dist/db/database.d.ts +19 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1023 -108
- package/dist/ingest/billing.d.ts +9 -0
- package/dist/ingest/billing.d.ts.map +1 -1
- 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 +8 -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 +1 -0
- 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 +1 -0
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +6 -0
- package/dist/ingest/hermes.d.ts.map +1 -0
- package/dist/ingest/opencode.d.ts +7 -0
- package/dist/ingest/opencode.d.ts.map +1 -0
- package/dist/ingest/otel.d.ts +20 -0
- package/dist/ingest/otel.d.ts.map +1 -0
- package/dist/ingest/pi.d.ts +7 -0
- package/dist/ingest/pi.d.ts.map +1 -0
- package/dist/ingest/plugin.d.ts +17 -0
- package/dist/ingest/plugin.d.ts.map +1 -0
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.d.ts.map +1 -0
- package/dist/lib/billing-diff.d.ts +22 -0
- package/dist/lib/billing-diff.d.ts.map +1 -0
- package/dist/lib/cloud-sync.d.ts +35 -0
- package/dist/lib/cloud-sync.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/model-config.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +19 -0
- package/dist/lib/open-projects.d.ts.map +1 -0
- package/dist/lib/paths.d.ts +20 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/pricing.d.ts +2 -2
- 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.map +1 -1
- package/dist/mcp/http.d.ts +12 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.js +2518 -472
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/otel/index.d.ts +3 -0
- package/dist/otel/index.d.ts.map +1 -0
- package/dist/otel/index.js +1372 -0
- package/dist/server/index.js +2818 -218
- package/dist/server/serve.d.ts +9 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +56 -6
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/mcp/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __export = (target, all) => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = import.meta.require;
|
|
18
19
|
|
|
19
20
|
// src/lib/pricing.ts
|
|
20
21
|
var exports_pricing = {};
|
|
@@ -28,99 +29,486 @@ __export(exports_pricing, {
|
|
|
28
29
|
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
29
30
|
});
|
|
30
31
|
function normalizeModelName(raw) {
|
|
31
|
-
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;
|
|
32
68
|
}
|
|
33
69
|
function ensurePricingSeeded(db) {
|
|
34
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);
|
|
35
169
|
}
|
|
36
170
|
function getPricingFromDb(db, model) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
44
|
-
cacheWritePer1M: row.cache_write_per_1m
|
|
45
|
-
};
|
|
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);
|
|
46
177
|
}
|
|
47
178
|
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
};
|
|
54
195
|
}
|
|
55
196
|
function getPricing(model) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (const key of Object.keys(DEFAULT_PRICING)) {
|
|
60
|
-
if (normalized.startsWith(key))
|
|
61
|
-
return DEFAULT_PRICING[key] ?? null;
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
197
|
+
if (isFreeModel(model))
|
|
198
|
+
return FREE_PRICING;
|
|
199
|
+
return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
|
|
64
200
|
}
|
|
65
|
-
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) {
|
|
66
205
|
const pricing = getPricing(model);
|
|
67
206
|
if (!pricing)
|
|
68
207
|
return 0;
|
|
69
|
-
return (
|
|
208
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
70
209
|
}
|
|
71
|
-
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) {
|
|
72
211
|
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
73
212
|
if (!pricing)
|
|
74
213
|
return 0;
|
|
75
|
-
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;
|
|
76
234
|
}
|
|
77
|
-
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;
|
|
78
236
|
var init_pricing = __esm(() => {
|
|
79
237
|
init_database();
|
|
80
238
|
DEFAULT_PRICING = {
|
|
81
|
-
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
82
|
-
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
83
|
-
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
84
|
-
"claude-
|
|
85
|
-
"claude-
|
|
86
|
-
"claude-
|
|
87
|
-
"claude-
|
|
88
|
-
"claude-
|
|
89
|
-
"claude-3-
|
|
90
|
-
"claude-
|
|
91
|
-
"claude-3-haiku": { inputPer1M: 0.
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"gemini-
|
|
95
|
-
"gemini-
|
|
96
|
-
"gemini-1
|
|
97
|
-
"gemini-
|
|
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 },
|
|
98
270
|
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
99
271
|
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
100
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 = {
|
|
325
|
+
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
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 },
|
|
329
|
+
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
101
330
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
102
|
-
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
103
331
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
104
332
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
105
333
|
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
106
334
|
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
107
|
-
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
108
|
-
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
109
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
110
335
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
|
|
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 },
|
|
114
338
|
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
115
|
-
"qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
116
339
|
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
117
340
|
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
118
341
|
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
119
|
-
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
120
|
-
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
121
342
|
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
122
343
|
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
123
|
-
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, 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
|
+
}
|
|
124
512
|
};
|
|
125
513
|
});
|
|
126
514
|
|
|
@@ -189,6 +577,8 @@ function initSchema(db) {
|
|
|
189
577
|
output_tokens INTEGER DEFAULT 0,
|
|
190
578
|
cache_read_tokens INTEGER DEFAULT 0,
|
|
191
579
|
cache_create_tokens INTEGER DEFAULT 0,
|
|
580
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
581
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
192
582
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
193
583
|
duration_ms INTEGER DEFAULT 0,
|
|
194
584
|
timestamp TEXT NOT NULL,
|
|
@@ -259,6 +649,8 @@ function initSchema(db) {
|
|
|
259
649
|
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
260
650
|
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
261
651
|
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
652
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
653
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
262
654
|
updated_at TEXT NOT NULL
|
|
263
655
|
);
|
|
264
656
|
|
|
@@ -283,12 +675,100 @@ function initSchema(db) {
|
|
|
283
675
|
|
|
284
676
|
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
285
677
|
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
678
|
+
|
|
679
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
680
|
+
id TEXT PRIMARY KEY,
|
|
681
|
+
agent TEXT,
|
|
682
|
+
provider TEXT NOT NULL,
|
|
683
|
+
plan TEXT NOT NULL,
|
|
684
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
685
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
686
|
+
billing_cycle_start TEXT,
|
|
687
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
688
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
689
|
+
created_at TEXT NOT NULL,
|
|
690
|
+
updated_at TEXT NOT NULL
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
694
|
+
id TEXT PRIMARY KEY,
|
|
695
|
+
agent TEXT NOT NULL,
|
|
696
|
+
date TEXT NOT NULL,
|
|
697
|
+
metric TEXT NOT NULL,
|
|
698
|
+
value REAL NOT NULL DEFAULT 0,
|
|
699
|
+
unit TEXT DEFAULT '',
|
|
700
|
+
machine_id TEXT DEFAULT '',
|
|
701
|
+
updated_at TEXT NOT NULL
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
705
|
+
date TEXT NOT NULL,
|
|
706
|
+
agent TEXT DEFAULT '',
|
|
707
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
708
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
709
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
710
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
711
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
712
|
+
updated_at TEXT NOT NULL,
|
|
713
|
+
PRIMARY KEY (date, agent)
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
717
|
+
machine_id TEXT PRIMARY KEY,
|
|
718
|
+
hostname TEXT NOT NULL,
|
|
719
|
+
last_seen_at TEXT,
|
|
720
|
+
last_push_at TEXT,
|
|
721
|
+
last_pull_at TEXT,
|
|
722
|
+
economy_version TEXT,
|
|
723
|
+
updated_at TEXT NOT NULL
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
727
|
+
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
286
728
|
`);
|
|
287
729
|
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
288
730
|
if (!cols.some((c) => c.name === "machine_id")) {
|
|
289
731
|
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
290
732
|
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
291
733
|
}
|
|
734
|
+
if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
|
|
735
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
|
|
736
|
+
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
737
|
+
}
|
|
738
|
+
if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
|
|
739
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
|
|
740
|
+
}
|
|
741
|
+
if (!cols.some((c) => c.name === "cost_basis")) {
|
|
742
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
|
|
743
|
+
}
|
|
744
|
+
if (!cols.some((c) => c.name === "attribution_tag")) {
|
|
745
|
+
db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
746
|
+
}
|
|
747
|
+
if (!cols.some((c) => c.name === "updated_at")) {
|
|
748
|
+
db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
749
|
+
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
750
|
+
}
|
|
751
|
+
if (!cols.some((c) => c.name === "synced_at")) {
|
|
752
|
+
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
753
|
+
}
|
|
754
|
+
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
755
|
+
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
756
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
757
|
+
}
|
|
758
|
+
if (!sessionCols.some((c) => c.name === "updated_at")) {
|
|
759
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
760
|
+
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
761
|
+
}
|
|
762
|
+
if (!sessionCols.some((c) => c.name === "synced_at")) {
|
|
763
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
764
|
+
}
|
|
765
|
+
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
766
|
+
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
767
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
768
|
+
}
|
|
769
|
+
if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
|
|
770
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
|
|
771
|
+
}
|
|
292
772
|
db.exec(`
|
|
293
773
|
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
294
774
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
@@ -327,21 +807,24 @@ function sessionPeriodWhere(period) {
|
|
|
327
807
|
}
|
|
328
808
|
}
|
|
329
809
|
function upsertRequest(db, req) {
|
|
810
|
+
const now = req.updated_at ?? new Date().toISOString();
|
|
330
811
|
db.prepare(`
|
|
331
812
|
INSERT OR REPLACE INTO requests
|
|
332
813
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
333
|
-
cache_read_tokens, cache_create_tokens,
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
814
|
+
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
815
|
+
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
816
|
+
source_request_id, machine_id, attribution_tag, updated_at, synced_at)
|
|
817
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
818
|
+
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, req.synced_at ?? "");
|
|
337
819
|
}
|
|
338
820
|
function upsertSession(db, session) {
|
|
821
|
+
const now = session.updated_at ?? new Date().toISOString();
|
|
339
822
|
db.prepare(`
|
|
340
823
|
INSERT OR REPLACE INTO sessions
|
|
341
824
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
342
|
-
total_cost_usd, total_tokens, request_count, machine_id)
|
|
343
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
344
|
-
`).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 ?? "");
|
|
825
|
+
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
|
|
826
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
827
|
+
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, session.synced_at ?? "");
|
|
345
828
|
}
|
|
346
829
|
function rollupSession(db, sessionId) {
|
|
347
830
|
db.prepare(`
|
|
@@ -393,10 +876,10 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
393
876
|
}
|
|
394
877
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
395
878
|
}
|
|
396
|
-
function querySummary(db, period, machine) {
|
|
879
|
+
function querySummary(db, period, machine, allMachines = false) {
|
|
397
880
|
const rWhere = periodWhere(period);
|
|
398
881
|
const sWhere = sessionPeriodWhere(period);
|
|
399
|
-
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
882
|
+
const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
400
883
|
const r = db.prepare(`
|
|
401
884
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
402
885
|
COUNT(*) as requests,
|
|
@@ -501,9 +984,19 @@ function queryDailyBreakdown(db, days = 30) {
|
|
|
501
984
|
ORDER BY date ASC
|
|
502
985
|
`).all(`-${days}`);
|
|
503
986
|
}
|
|
987
|
+
function upsertBudget(db, budget) {
|
|
988
|
+
db.prepare(`
|
|
989
|
+
INSERT OR REPLACE INTO budgets
|
|
990
|
+
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
991
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
992
|
+
`).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);
|
|
993
|
+
}
|
|
504
994
|
function listBudgets(db) {
|
|
505
995
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
506
996
|
}
|
|
997
|
+
function deleteBudget(db, id) {
|
|
998
|
+
db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
|
|
999
|
+
}
|
|
507
1000
|
function getBudgetStatuses(db) {
|
|
508
1001
|
const budgets = listBudgets(db);
|
|
509
1002
|
return budgets.map((b) => {
|
|
@@ -577,6 +1070,17 @@ function getIngestState(db, source, key) {
|
|
|
577
1070
|
function setIngestState(db, source, key, value) {
|
|
578
1071
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
579
1072
|
}
|
|
1073
|
+
function queryBillingSummary(db, period) {
|
|
1074
|
+
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";
|
|
1075
|
+
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
1076
|
+
const by_provider = {};
|
|
1077
|
+
let total = 0;
|
|
1078
|
+
for (const r of rows) {
|
|
1079
|
+
by_provider[r.provider] = r.cost;
|
|
1080
|
+
total += r.cost;
|
|
1081
|
+
}
|
|
1082
|
+
return { total_usd: total, by_provider };
|
|
1083
|
+
}
|
|
580
1084
|
function listMachines(db) {
|
|
581
1085
|
return db.prepare(`
|
|
582
1086
|
SELECT
|
|
@@ -594,13 +1098,19 @@ function listMachines(db) {
|
|
|
594
1098
|
function upsertModelPricing(db, p) {
|
|
595
1099
|
db.prepare(`
|
|
596
1100
|
INSERT OR REPLACE INTO model_pricing
|
|
597
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
|
|
598
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
599
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
|
|
1101
|
+
(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)
|
|
1102
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1103
|
+
`).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);
|
|
600
1104
|
}
|
|
601
1105
|
function getModelPricing(db, model) {
|
|
602
1106
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
603
1107
|
}
|
|
1108
|
+
function listModelPricing(db) {
|
|
1109
|
+
return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
|
|
1110
|
+
}
|
|
1111
|
+
function deleteModelPricing(db, model) {
|
|
1112
|
+
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
1113
|
+
}
|
|
604
1114
|
function seedModelPricing(db, defaults) {
|
|
605
1115
|
const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
|
|
606
1116
|
const now = new Date().toISOString();
|
|
@@ -613,23 +1123,73 @@ function seedModelPricing(db, defaults) {
|
|
|
613
1123
|
output_per_1m: p.outputPer1M,
|
|
614
1124
|
cache_read_per_1m: p.cacheReadPer1M,
|
|
615
1125
|
cache_write_per_1m: p.cacheWritePer1M,
|
|
1126
|
+
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1127
|
+
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
616
1128
|
updated_at: now
|
|
617
1129
|
});
|
|
618
1130
|
}
|
|
619
1131
|
}
|
|
1132
|
+
function upsertSubscription(db, sub) {
|
|
1133
|
+
db.prepare(`
|
|
1134
|
+
INSERT OR REPLACE INTO subscriptions
|
|
1135
|
+
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
1136
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1137
|
+
`).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);
|
|
1138
|
+
}
|
|
1139
|
+
function upsertUsageSnapshot(db, snap) {
|
|
1140
|
+
const now = snap.updated_at ?? new Date().toISOString();
|
|
1141
|
+
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
1142
|
+
db.prepare(`
|
|
1143
|
+
INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
|
|
1144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1145
|
+
`).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
|
|
1146
|
+
}
|
|
1147
|
+
function queryUsageSnapshots(db, opts = {}) {
|
|
1148
|
+
const conditions = [];
|
|
1149
|
+
const params = [];
|
|
1150
|
+
if (opts.agent) {
|
|
1151
|
+
conditions.push("agent = ?");
|
|
1152
|
+
params.push(opts.agent);
|
|
1153
|
+
}
|
|
1154
|
+
if (opts.date) {
|
|
1155
|
+
conditions.push("date = ?");
|
|
1156
|
+
params.push(opts.date);
|
|
1157
|
+
}
|
|
1158
|
+
if (opts.since) {
|
|
1159
|
+
conditions.push("date >= ?");
|
|
1160
|
+
params.push(opts.since);
|
|
1161
|
+
}
|
|
1162
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1163
|
+
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1164
|
+
}
|
|
1165
|
+
function dedupeRequests(db) {
|
|
1166
|
+
const dupes = db.prepare(`
|
|
1167
|
+
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1168
|
+
FROM requests
|
|
1169
|
+
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1170
|
+
GROUP BY source_request_id, agent
|
|
1171
|
+
HAVING cnt > 1
|
|
1172
|
+
`).all();
|
|
1173
|
+
let removed = 0;
|
|
1174
|
+
for (const row of dupes) {
|
|
1175
|
+
const result = db.prepare(`
|
|
1176
|
+
DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
|
|
1177
|
+
`).run(row.source_request_id, row.agent, row.keep_id);
|
|
1178
|
+
removed += result.changes;
|
|
1179
|
+
}
|
|
1180
|
+
return removed;
|
|
1181
|
+
}
|
|
620
1182
|
var init_database = () => {};
|
|
621
1183
|
|
|
622
|
-
// src/mcp/index.ts
|
|
623
|
-
init_database();
|
|
624
|
-
import { randomUUID } from "crypto";
|
|
625
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
626
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
627
|
-
import { registerCloudTools } from "@hasna/cloud";
|
|
628
|
-
import { z } from "zod";
|
|
629
|
-
|
|
630
1184
|
// src/db/pg-migrations.ts
|
|
631
|
-
var
|
|
632
|
-
|
|
1185
|
+
var exports_pg_migrations = {};
|
|
1186
|
+
__export(exports_pg_migrations, {
|
|
1187
|
+
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1188
|
+
});
|
|
1189
|
+
var PG_MIGRATIONS;
|
|
1190
|
+
var init_pg_migrations = __esm(() => {
|
|
1191
|
+
PG_MIGRATIONS = [
|
|
1192
|
+
`CREATE TABLE IF NOT EXISTS requests (
|
|
633
1193
|
id TEXT PRIMARY KEY,
|
|
634
1194
|
agent TEXT NOT NULL,
|
|
635
1195
|
session_id TEXT NOT NULL,
|
|
@@ -638,13 +1198,15 @@ var PG_MIGRATIONS = [
|
|
|
638
1198
|
output_tokens INTEGER DEFAULT 0,
|
|
639
1199
|
cache_read_tokens INTEGER DEFAULT 0,
|
|
640
1200
|
cache_create_tokens INTEGER DEFAULT 0,
|
|
1201
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
1202
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
641
1203
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
642
1204
|
duration_ms INTEGER DEFAULT 0,
|
|
643
1205
|
timestamp TEXT NOT NULL,
|
|
644
1206
|
source_request_id TEXT,
|
|
645
1207
|
machine_id TEXT DEFAULT ''
|
|
646
1208
|
)`,
|
|
647
|
-
|
|
1209
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
648
1210
|
id TEXT PRIMARY KEY,
|
|
649
1211
|
agent TEXT NOT NULL,
|
|
650
1212
|
project_path TEXT DEFAULT '',
|
|
@@ -656,7 +1218,7 @@ var PG_MIGRATIONS = [
|
|
|
656
1218
|
request_count INTEGER DEFAULT 0,
|
|
657
1219
|
machine_id TEXT DEFAULT ''
|
|
658
1220
|
)`,
|
|
659
|
-
|
|
1221
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
660
1222
|
id TEXT PRIMARY KEY,
|
|
661
1223
|
path TEXT UNIQUE NOT NULL,
|
|
662
1224
|
name TEXT NOT NULL,
|
|
@@ -664,7 +1226,7 @@ var PG_MIGRATIONS = [
|
|
|
664
1226
|
tags TEXT DEFAULT '[]',
|
|
665
1227
|
created_at TEXT NOT NULL
|
|
666
1228
|
)`,
|
|
667
|
-
|
|
1229
|
+
`CREATE TABLE IF NOT EXISTS budgets (
|
|
668
1230
|
id TEXT PRIMARY KEY,
|
|
669
1231
|
project_path TEXT,
|
|
670
1232
|
agent TEXT,
|
|
@@ -674,7 +1236,7 @@ var PG_MIGRATIONS = [
|
|
|
674
1236
|
created_at TEXT NOT NULL,
|
|
675
1237
|
updated_at TEXT NOT NULL
|
|
676
1238
|
)`,
|
|
677
|
-
|
|
1239
|
+
`CREATE TABLE IF NOT EXISTS goals (
|
|
678
1240
|
id TEXT PRIMARY KEY,
|
|
679
1241
|
period TEXT NOT NULL,
|
|
680
1242
|
project_path TEXT,
|
|
@@ -683,29 +1245,35 @@ var PG_MIGRATIONS = [
|
|
|
683
1245
|
created_at TEXT NOT NULL,
|
|
684
1246
|
updated_at TEXT NOT NULL
|
|
685
1247
|
)`,
|
|
686
|
-
|
|
1248
|
+
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
687
1249
|
source TEXT NOT NULL,
|
|
688
1250
|
key TEXT NOT NULL,
|
|
689
1251
|
value TEXT NOT NULL,
|
|
690
1252
|
PRIMARY KEY (source, key)
|
|
691
1253
|
)`,
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1254
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1255
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1256
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1257
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1258
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1259
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1260
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1261
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1262
|
+
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
701
1263
|
model TEXT PRIMARY KEY,
|
|
702
1264
|
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
703
1265
|
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
704
1266
|
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
705
1267
|
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1268
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
1269
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
706
1270
|
updated_at TEXT NOT NULL
|
|
707
1271
|
)`,
|
|
708
|
-
|
|
1272
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
|
|
1273
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
|
|
1274
|
+
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
|
|
1275
|
+
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
|
|
1276
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
709
1277
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
710
1278
|
message TEXT NOT NULL,
|
|
711
1279
|
email TEXT,
|
|
@@ -713,15 +1281,208 @@ var PG_MIGRATIONS = [
|
|
|
713
1281
|
version TEXT,
|
|
714
1282
|
machine_id TEXT,
|
|
715
1283
|
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
716
|
-
)
|
|
717
|
-
|
|
1284
|
+
)`,
|
|
1285
|
+
`CREATE TABLE IF NOT EXISTS billing_daily (
|
|
1286
|
+
date TEXT NOT NULL,
|
|
1287
|
+
provider TEXT NOT NULL,
|
|
1288
|
+
description TEXT DEFAULT '',
|
|
1289
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1290
|
+
updated_at TEXT NOT NULL,
|
|
1291
|
+
PRIMARY KEY (date, provider, description)
|
|
1292
|
+
)`,
|
|
1293
|
+
`CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
|
|
1294
|
+
`CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
|
|
1295
|
+
`CREATE TABLE IF NOT EXISTS subscriptions (
|
|
1296
|
+
id TEXT PRIMARY KEY,
|
|
1297
|
+
agent TEXT,
|
|
1298
|
+
provider TEXT NOT NULL,
|
|
1299
|
+
plan TEXT NOT NULL,
|
|
1300
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1301
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
1302
|
+
billing_cycle_start TEXT,
|
|
1303
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
1304
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
1305
|
+
created_at TEXT NOT NULL,
|
|
1306
|
+
updated_at TEXT NOT NULL
|
|
1307
|
+
)`,
|
|
1308
|
+
`CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
1309
|
+
id TEXT PRIMARY KEY,
|
|
1310
|
+
agent TEXT NOT NULL,
|
|
1311
|
+
date TEXT NOT NULL,
|
|
1312
|
+
metric TEXT NOT NULL,
|
|
1313
|
+
value REAL NOT NULL DEFAULT 0,
|
|
1314
|
+
unit TEXT DEFAULT '',
|
|
1315
|
+
machine_id TEXT DEFAULT '',
|
|
1316
|
+
updated_at TEXT NOT NULL
|
|
1317
|
+
)`,
|
|
1318
|
+
`CREATE TABLE IF NOT EXISTS savings_daily (
|
|
1319
|
+
date TEXT NOT NULL,
|
|
1320
|
+
agent TEXT DEFAULT '',
|
|
1321
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
1322
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1323
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
1324
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
1325
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
1326
|
+
updated_at TEXT NOT NULL,
|
|
1327
|
+
PRIMARY KEY (date, agent)
|
|
1328
|
+
)`,
|
|
1329
|
+
`CREATE TABLE IF NOT EXISTS machines (
|
|
1330
|
+
machine_id TEXT PRIMARY KEY,
|
|
1331
|
+
hostname TEXT NOT NULL,
|
|
1332
|
+
last_seen_at TEXT,
|
|
1333
|
+
last_push_at TEXT,
|
|
1334
|
+
last_pull_at TEXT,
|
|
1335
|
+
economy_version TEXT,
|
|
1336
|
+
updated_at TEXT NOT NULL
|
|
1337
|
+
)`,
|
|
1338
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
1339
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1340
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1341
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1342
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1343
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1344
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1345
|
+
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
1346
|
+
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`
|
|
1347
|
+
];
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
// src/mcp/index.ts
|
|
1351
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1352
|
+
|
|
1353
|
+
// src/lib/package-metadata.ts
|
|
1354
|
+
import { readFileSync } from "fs";
|
|
1355
|
+
var cachedMetadata = null;
|
|
1356
|
+
function getPackageMetadata() {
|
|
1357
|
+
if (cachedMetadata)
|
|
1358
|
+
return cachedMetadata;
|
|
1359
|
+
const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
|
|
1360
|
+
const parsed = JSON.parse(raw);
|
|
1361
|
+
cachedMetadata = {
|
|
1362
|
+
name: parsed.name ?? "@hasna/economy",
|
|
1363
|
+
version: parsed.version ?? "0.0.0"
|
|
1364
|
+
};
|
|
1365
|
+
return cachedMetadata;
|
|
1366
|
+
}
|
|
1367
|
+
var packageMetadata = getPackageMetadata();
|
|
1368
|
+
|
|
1369
|
+
// src/mcp/server.ts
|
|
1370
|
+
init_database();
|
|
1371
|
+
init_pg_migrations();
|
|
1372
|
+
import { randomUUID } from "crypto";
|
|
1373
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1374
|
+
import { registerCloudTools } from "@hasna/cloud";
|
|
1375
|
+
import { z } from "zod";
|
|
718
1376
|
|
|
719
1377
|
// src/ingest/claude.ts
|
|
720
1378
|
init_database();
|
|
721
1379
|
init_pricing();
|
|
722
|
-
import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
1380
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
723
1381
|
import { homedir as homedir2 } from "os";
|
|
724
1382
|
import { join as join2, basename } from "path";
|
|
1383
|
+
|
|
1384
|
+
// src/lib/savings.ts
|
|
1385
|
+
function periodWhere2(period, column) {
|
|
1386
|
+
switch (period) {
|
|
1387
|
+
case "today":
|
|
1388
|
+
return `DATE(${column}) = DATE('now')`;
|
|
1389
|
+
case "yesterday":
|
|
1390
|
+
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
1391
|
+
case "week":
|
|
1392
|
+
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1393
|
+
case "month":
|
|
1394
|
+
return `${column} >= DATE('now', 'start of month')`;
|
|
1395
|
+
case "year":
|
|
1396
|
+
return `${column} >= DATE('now', 'start of year')`;
|
|
1397
|
+
case "all":
|
|
1398
|
+
return "1=1";
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function prorateMonthlyFee(monthlyFee, period) {
|
|
1402
|
+
const now = new Date;
|
|
1403
|
+
const dayOfMonth = now.getDate();
|
|
1404
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1405
|
+
switch (period) {
|
|
1406
|
+
case "today":
|
|
1407
|
+
case "yesterday":
|
|
1408
|
+
return monthlyFee / daysInMonth;
|
|
1409
|
+
case "week":
|
|
1410
|
+
return monthlyFee / daysInMonth * 7;
|
|
1411
|
+
case "month":
|
|
1412
|
+
return monthlyFee;
|
|
1413
|
+
case "year":
|
|
1414
|
+
return monthlyFee * 12;
|
|
1415
|
+
case "all":
|
|
1416
|
+
return monthlyFee;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
|
|
1420
|
+
return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
|
|
1421
|
+
}
|
|
1422
|
+
function querySavingsSummary(db, period, agent) {
|
|
1423
|
+
const where = periodWhere2(period, "timestamp");
|
|
1424
|
+
const agentClause = agent ? " AND agent = ?" : "";
|
|
1425
|
+
const params = agent ? [agent] : [];
|
|
1426
|
+
const apiRow = db.prepare(`
|
|
1427
|
+
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1428
|
+
FROM requests
|
|
1429
|
+
WHERE ${where}${agentClause}
|
|
1430
|
+
AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
|
|
1431
|
+
`).get(...params);
|
|
1432
|
+
const includedRow = db.prepare(`
|
|
1433
|
+
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1434
|
+
FROM requests
|
|
1435
|
+
WHERE ${where}${agentClause}
|
|
1436
|
+
AND cost_basis = 'subscription_included'
|
|
1437
|
+
`).get(...params);
|
|
1438
|
+
const subWhere = periodWhere2(period, "date");
|
|
1439
|
+
const onDemandRow = db.prepare(`
|
|
1440
|
+
SELECT COALESCE(SUM(value), 0) as total
|
|
1441
|
+
FROM usage_snapshots
|
|
1442
|
+
WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
|
|
1443
|
+
AND metric = 'on_demand_usd'
|
|
1444
|
+
`).get(...params);
|
|
1445
|
+
const subs = db.prepare(`
|
|
1446
|
+
SELECT COALESCE(SUM(monthly_fee_usd), 0) as total
|
|
1447
|
+
FROM subscriptions
|
|
1448
|
+
WHERE active = 1${agent ? " AND agent = ?" : ""}
|
|
1449
|
+
`).get(...agent ? [agent] : []);
|
|
1450
|
+
const subscriptionFee = prorateMonthlyFee(subs.total, period);
|
|
1451
|
+
const apiEquivalent = apiRow.total + includedRow.total;
|
|
1452
|
+
const onDemand = onDemandRow.total;
|
|
1453
|
+
const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
|
|
1454
|
+
const byAgent = {};
|
|
1455
|
+
if (!agent) {
|
|
1456
|
+
for (const row of db.prepare(`
|
|
1457
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) as api_eq
|
|
1458
|
+
FROM requests WHERE ${where}
|
|
1459
|
+
GROUP BY agent
|
|
1460
|
+
`).all()) {
|
|
1461
|
+
byAgent[row.agent] = {
|
|
1462
|
+
api_equivalent_usd: row.api_eq,
|
|
1463
|
+
saved_usd: row.api_eq
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return {
|
|
1468
|
+
period,
|
|
1469
|
+
api_equivalent_usd: apiEquivalent,
|
|
1470
|
+
subscription_fee_usd: subscriptionFee,
|
|
1471
|
+
included_consumed_usd: includedRow.total,
|
|
1472
|
+
on_demand_usd: onDemand,
|
|
1473
|
+
saved_usd: saved,
|
|
1474
|
+
by_agent: byAgent
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function defaultCostBasisForAgent(agent) {
|
|
1478
|
+
if (agent === "claude")
|
|
1479
|
+
return "metered_api";
|
|
1480
|
+
if (agent === "cursor")
|
|
1481
|
+
return "subscription_included";
|
|
1482
|
+
return "estimated";
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/ingest/claude.ts
|
|
725
1486
|
function autoDetectProject(cwd, projects) {
|
|
726
1487
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
727
1488
|
}
|
|
@@ -745,11 +1506,11 @@ function collectJsonlFiles(projectDir) {
|
|
|
745
1506
|
walk(projectDir);
|
|
746
1507
|
return files;
|
|
747
1508
|
}
|
|
748
|
-
async function ingestClaude(db, verbose = false,
|
|
749
|
-
return ingestJsonlProjects(db,
|
|
1509
|
+
async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
|
|
1510
|
+
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
750
1511
|
}
|
|
751
|
-
async function ingestTakumi(db, verbose = false) {
|
|
752
|
-
return ingestJsonlProjects(db,
|
|
1512
|
+
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
1513
|
+
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
753
1514
|
}
|
|
754
1515
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
755
1516
|
if (!existsSync2(projectsDir)) {
|
|
@@ -780,7 +1541,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
780
1541
|
continue;
|
|
781
1542
|
let lines;
|
|
782
1543
|
try {
|
|
783
|
-
lines =
|
|
1544
|
+
lines = readFileSync2(filePath, "utf-8").split(`
|
|
784
1545
|
`).filter((l) => l.trim());
|
|
785
1546
|
} catch {
|
|
786
1547
|
continue;
|
|
@@ -810,13 +1571,21 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
810
1571
|
continue;
|
|
811
1572
|
const inputTokens = usage.input_tokens ?? 0;
|
|
812
1573
|
const outputTokens = usage.output_tokens ?? 0;
|
|
813
|
-
const
|
|
1574
|
+
const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
|
|
1575
|
+
const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
1576
|
+
const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
|
|
814
1577
|
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
815
1578
|
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
816
|
-
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
1579
|
+
if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
|
|
817
1580
|
continue;
|
|
818
|
-
|
|
819
|
-
|
|
1581
|
+
let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
|
|
1582
|
+
costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
|
|
1583
|
+
const serverToolUse = usage.server_tool_use;
|
|
1584
|
+
if (serverToolUse?.web_search_requests) {
|
|
1585
|
+
costUsd += serverToolUse.web_search_requests * 0.01;
|
|
1586
|
+
}
|
|
1587
|
+
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1588
|
+
const reqId = `${agentName}-${sourceRequestId}`;
|
|
820
1589
|
upsertRequest(db, {
|
|
821
1590
|
id: reqId,
|
|
822
1591
|
agent: agentName,
|
|
@@ -826,10 +1595,13 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
826
1595
|
output_tokens: outputTokens,
|
|
827
1596
|
cache_read_tokens: cacheReadTokens,
|
|
828
1597
|
cache_create_tokens: cacheWriteTokens,
|
|
1598
|
+
cache_create_5m_tokens: cacheWrite5mTokens,
|
|
1599
|
+
cache_create_1h_tokens: cacheWrite1hTokens,
|
|
829
1600
|
cost_usd: costUsd,
|
|
1601
|
+
cost_basis: defaultCostBasisForAgent(agentName),
|
|
830
1602
|
duration_ms: 0,
|
|
831
1603
|
timestamp,
|
|
832
|
-
source_request_id:
|
|
1604
|
+
source_request_id: sourceRequestId,
|
|
833
1605
|
machine_id: machineId
|
|
834
1606
|
});
|
|
835
1607
|
if (!touchedSessions.has(sessionId)) {
|
|
@@ -864,82 +1636,254 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
864
1636
|
}
|
|
865
1637
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
866
1638
|
}
|
|
1639
|
+
function applyClaudeModifiers(costUsd, model, usage, entry) {
|
|
1640
|
+
let multiplier = 1;
|
|
1641
|
+
const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
|
|
1642
|
+
if (speed === "fast" && model.includes("opus-4-6")) {
|
|
1643
|
+
multiplier *= 6;
|
|
1644
|
+
}
|
|
1645
|
+
const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
|
|
1646
|
+
if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
|
|
1647
|
+
multiplier *= 1.1;
|
|
1648
|
+
}
|
|
1649
|
+
return costUsd * multiplier;
|
|
1650
|
+
}
|
|
1651
|
+
function supportsClaudeDataResidencyPricing(model) {
|
|
1652
|
+
const normalized = normalizeModelName(model);
|
|
1653
|
+
const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
1654
|
+
if (!match)
|
|
1655
|
+
return false;
|
|
1656
|
+
const major = Number(match[2]);
|
|
1657
|
+
const minor = match[3] ? Number(match[3]) : 0;
|
|
1658
|
+
return major > 4 || major === 4 && minor >= 6;
|
|
1659
|
+
}
|
|
867
1660
|
|
|
868
1661
|
// src/ingest/codex.ts
|
|
869
1662
|
init_database();
|
|
870
|
-
|
|
1663
|
+
init_pricing();
|
|
1664
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
871
1665
|
import { homedir as homedir3 } from "os";
|
|
872
1666
|
import { join as join3, basename as basename2 } from "path";
|
|
873
1667
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
874
|
-
var
|
|
875
|
-
var
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1668
|
+
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
1669
|
+
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
1670
|
+
var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
|
|
1671
|
+
function codexDbPath() {
|
|
1672
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
1673
|
+
}
|
|
1674
|
+
function codexConfigPath() {
|
|
1675
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
1676
|
+
}
|
|
1677
|
+
function readCodexModel() {
|
|
1678
|
+
const configPath = codexConfigPath();
|
|
1679
|
+
if (!existsSync3(configPath))
|
|
1680
|
+
return "gpt-5-codex";
|
|
885
1681
|
try {
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1682
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
1683
|
+
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
1684
|
+
return match?.[1] ?? "gpt-5-codex";
|
|
1685
|
+
} catch {
|
|
1686
|
+
return "gpt-5-codex";
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function buildThreadQuery(codexDb) {
|
|
1690
|
+
const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
|
|
1691
|
+
const modelSelect = cols.has("model") ? "model" : "NULL AS model";
|
|
1692
|
+
const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
|
|
1693
|
+
const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
|
|
1694
|
+
return `
|
|
1695
|
+
SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
|
|
1696
|
+
${providerSelect}, ${modelSelect}
|
|
1697
|
+
FROM threads WHERE tokens_used > 0
|
|
1698
|
+
`;
|
|
1699
|
+
}
|
|
1700
|
+
function readTokenEvents(rolloutPath) {
|
|
1701
|
+
if (!rolloutPath || !existsSync3(rolloutPath))
|
|
1702
|
+
return [];
|
|
1703
|
+
const events = [];
|
|
1704
|
+
const seen = new Set;
|
|
1705
|
+
for (const line of readFileSync3(rolloutPath, "utf-8").split(`
|
|
1706
|
+
`)) {
|
|
1707
|
+
if (!line.trim())
|
|
1708
|
+
continue;
|
|
1709
|
+
let entry;
|
|
1710
|
+
try {
|
|
1711
|
+
entry = JSON.parse(line);
|
|
1712
|
+
} catch {
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (!entry || typeof entry !== "object")
|
|
1716
|
+
continue;
|
|
1717
|
+
const payload = entry["payload"];
|
|
1718
|
+
if (!payload || payload["type"] !== "token_count")
|
|
1719
|
+
continue;
|
|
1720
|
+
const info = payload["info"];
|
|
1721
|
+
const usage = info?.["last_token_usage"];
|
|
1722
|
+
if (!usage)
|
|
1723
|
+
continue;
|
|
1724
|
+
const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
1725
|
+
if (total <= 0)
|
|
1726
|
+
continue;
|
|
1727
|
+
const key = JSON.stringify(usage);
|
|
1728
|
+
if (seen.has(key))
|
|
1729
|
+
continue;
|
|
1730
|
+
seen.add(key);
|
|
1731
|
+
const timestamp = entry["timestamp"];
|
|
1732
|
+
events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
|
|
1733
|
+
}
|
|
1734
|
+
return events;
|
|
1735
|
+
}
|
|
1736
|
+
function fallbackEvents(totalTokens) {
|
|
1737
|
+
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
1738
|
+
return [{
|
|
1739
|
+
usage: {
|
|
1740
|
+
input_tokens: inputTokens,
|
|
1741
|
+
cached_input_tokens: 0,
|
|
1742
|
+
output_tokens: totalTokens - inputTokens,
|
|
1743
|
+
total_tokens: totalTokens
|
|
1744
|
+
}
|
|
1745
|
+
}];
|
|
1746
|
+
}
|
|
1747
|
+
async function ingestCodex(db, verbose = false) {
|
|
1748
|
+
const dbPath = codexDbPath();
|
|
1749
|
+
if (!existsSync3(dbPath)) {
|
|
1750
|
+
if (verbose)
|
|
1751
|
+
console.log("Codex DB not found:", dbPath);
|
|
1752
|
+
return { sessions: 0, requests: 0 };
|
|
1753
|
+
}
|
|
1754
|
+
const machineId = getMachineId();
|
|
1755
|
+
let codexDb = null;
|
|
1756
|
+
let ingested = 0;
|
|
1757
|
+
let requests = 0;
|
|
1758
|
+
try {
|
|
1759
|
+
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
1760
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
1761
|
+
for (const thread of threads) {
|
|
1762
|
+
const model = thread.model ?? readCodexModel();
|
|
1763
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
1764
|
+
const processed = getIngestState(db, "codex", thread.id);
|
|
1765
|
+
if (processed === stateValue)
|
|
1766
|
+
continue;
|
|
1767
|
+
const projectPath = thread.cwd ?? "";
|
|
895
1768
|
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
1769
|
+
const sessionId = `codex-${thread.id}`;
|
|
896
1770
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
897
1771
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
898
1772
|
upsertSession(db, {
|
|
899
|
-
id:
|
|
1773
|
+
id: sessionId,
|
|
900
1774
|
agent: "codex",
|
|
901
1775
|
project_path: projectPath,
|
|
902
1776
|
project_name: projectName,
|
|
903
1777
|
started_at: startedAt,
|
|
904
1778
|
ended_at: endedAt,
|
|
905
|
-
total_cost_usd:
|
|
906
|
-
total_tokens:
|
|
907
|
-
request_count:
|
|
1779
|
+
total_cost_usd: 0,
|
|
1780
|
+
total_tokens: 0,
|
|
1781
|
+
request_count: 0,
|
|
908
1782
|
machine_id: machineId
|
|
909
1783
|
});
|
|
910
|
-
|
|
1784
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
1785
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
1786
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
1787
|
+
tokenEvents.forEach((event, index) => {
|
|
1788
|
+
const usage = event.usage;
|
|
1789
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
1790
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
1791
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
1792
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
1793
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
1794
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
1795
|
+
const requestId = `${sessionId}-${index}`;
|
|
1796
|
+
upsertRequest(db, {
|
|
1797
|
+
id: requestId,
|
|
1798
|
+
agent: "codex",
|
|
1799
|
+
session_id: sessionId,
|
|
1800
|
+
model,
|
|
1801
|
+
input_tokens: inputTokens,
|
|
1802
|
+
output_tokens: outputTokens,
|
|
1803
|
+
cache_read_tokens: cacheReadTokens,
|
|
1804
|
+
cache_create_tokens: 0,
|
|
1805
|
+
cost_usd: costUsd,
|
|
1806
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
1807
|
+
duration_ms: 0,
|
|
1808
|
+
timestamp,
|
|
1809
|
+
source_request_id: requestId,
|
|
1810
|
+
machine_id: machineId
|
|
1811
|
+
});
|
|
1812
|
+
requests++;
|
|
1813
|
+
});
|
|
1814
|
+
rollupSession(db, sessionId);
|
|
1815
|
+
setIngestState(db, "codex", thread.id, stateValue);
|
|
911
1816
|
ingested++;
|
|
912
1817
|
if (verbose)
|
|
913
|
-
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens
|
|
1818
|
+
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
|
|
914
1819
|
}
|
|
915
1820
|
} finally {
|
|
916
1821
|
codexDb?.close();
|
|
917
1822
|
}
|
|
918
|
-
return { sessions: ingested };
|
|
1823
|
+
return { sessions: ingested, requests };
|
|
919
1824
|
}
|
|
920
1825
|
|
|
921
1826
|
// src/ingest/gemini.ts
|
|
922
1827
|
init_database();
|
|
923
|
-
|
|
1828
|
+
init_pricing();
|
|
1829
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
924
1830
|
import { homedir as homedir4 } from "os";
|
|
925
|
-
import { join as join4 } from "path";
|
|
926
|
-
var
|
|
1831
|
+
import { join as join4, basename as basename3 } from "path";
|
|
1832
|
+
var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
1833
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
|
|
1834
|
+
function geminiTmpDir() {
|
|
1835
|
+
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
1836
|
+
}
|
|
1837
|
+
function geminiHistoryDir() {
|
|
1838
|
+
return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
|
|
1839
|
+
}
|
|
1840
|
+
function numberField(...values) {
|
|
1841
|
+
for (const value of values) {
|
|
1842
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1843
|
+
return value;
|
|
1844
|
+
}
|
|
1845
|
+
return 0;
|
|
1846
|
+
}
|
|
1847
|
+
function listProjectDirs(...roots) {
|
|
1848
|
+
const dirs = new Set;
|
|
1849
|
+
for (const root of roots) {
|
|
1850
|
+
if (!existsSync4(root))
|
|
1851
|
+
continue;
|
|
1852
|
+
try {
|
|
1853
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
1854
|
+
if (entry.isDirectory())
|
|
1855
|
+
dirs.add(join4(root, entry.name));
|
|
1856
|
+
}
|
|
1857
|
+
} catch {}
|
|
1858
|
+
}
|
|
1859
|
+
return [...dirs];
|
|
1860
|
+
}
|
|
1861
|
+
function projectRoot(projectDir, chatData) {
|
|
1862
|
+
if (chatData.projectPath)
|
|
1863
|
+
return chatData.projectPath;
|
|
1864
|
+
if (chatData.project_path)
|
|
1865
|
+
return chatData.project_path;
|
|
1866
|
+
const rootFile = join4(projectDir, ".project_root");
|
|
1867
|
+
try {
|
|
1868
|
+
if (existsSync4(rootFile))
|
|
1869
|
+
return readFileSync4(rootFile, "utf-8").trim();
|
|
1870
|
+
} catch {}
|
|
1871
|
+
return "";
|
|
1872
|
+
}
|
|
927
1873
|
async function ingestGemini(db, verbose) {
|
|
928
|
-
|
|
1874
|
+
const tmpDir = geminiTmpDir();
|
|
1875
|
+
const historyDir = geminiHistoryDir();
|
|
1876
|
+
if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
|
|
929
1877
|
if (verbose)
|
|
930
|
-
console.log("Gemini tmp
|
|
931
|
-
return { sessions: 0 };
|
|
1878
|
+
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
1879
|
+
return { sessions: 0, requests: 0 };
|
|
932
1880
|
}
|
|
933
1881
|
const machineId = getMachineId();
|
|
934
1882
|
let totalSessions = 0;
|
|
1883
|
+
let totalRequests = 0;
|
|
935
1884
|
const touchedSessions = new Set;
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
|
|
939
|
-
} catch {
|
|
940
|
-
return { sessions: 0 };
|
|
941
|
-
}
|
|
942
|
-
for (const projectDir of projectHashDirs) {
|
|
1885
|
+
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
1886
|
+
for (const projectDir of projectDirs) {
|
|
943
1887
|
const chatsDir = join4(projectDir, "chats");
|
|
944
1888
|
if (!existsSync4(chatsDir))
|
|
945
1889
|
continue;
|
|
@@ -962,21 +1906,23 @@ async function ingestGemini(db, verbose) {
|
|
|
962
1906
|
continue;
|
|
963
1907
|
let chatData;
|
|
964
1908
|
try {
|
|
965
|
-
chatData = JSON.parse(
|
|
1909
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
966
1910
|
} catch {
|
|
967
1911
|
continue;
|
|
968
1912
|
}
|
|
969
|
-
const sessionId = chatData.sessionId;
|
|
1913
|
+
const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
|
|
970
1914
|
if (!sessionId)
|
|
971
1915
|
continue;
|
|
972
1916
|
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
1917
|
+
const projectPath = projectRoot(projectDir, chatData);
|
|
1918
|
+
const projectName = projectPath ? basename3(projectPath) : "";
|
|
973
1919
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
974
1920
|
if (!existing) {
|
|
975
1921
|
const session = {
|
|
976
1922
|
id: sessionId,
|
|
977
1923
|
agent: "gemini",
|
|
978
|
-
project_path:
|
|
979
|
-
project_name:
|
|
1924
|
+
project_path: projectPath,
|
|
1925
|
+
project_name: projectName,
|
|
980
1926
|
started_at: startTime,
|
|
981
1927
|
ended_at: chatData.lastUpdated ?? null,
|
|
982
1928
|
total_cost_usd: 0,
|
|
@@ -985,358 +1931,1458 @@ async function ingestGemini(db, verbose) {
|
|
|
985
1931
|
machine_id: machineId
|
|
986
1932
|
};
|
|
987
1933
|
upsertSession(db, session);
|
|
988
|
-
touchedSessions.add(sessionId);
|
|
989
1934
|
totalSessions++;
|
|
990
1935
|
}
|
|
1936
|
+
touchedSessions.add(sessionId);
|
|
1937
|
+
for (const [index, message] of (chatData.messages ?? []).entries()) {
|
|
1938
|
+
const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
|
|
1939
|
+
if (!usage)
|
|
1940
|
+
continue;
|
|
1941
|
+
const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
|
|
1942
|
+
if (!model)
|
|
1943
|
+
continue;
|
|
1944
|
+
const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
|
|
1945
|
+
const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
|
|
1946
|
+
const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
|
|
1947
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
1948
|
+
const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
|
|
1949
|
+
const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
|
|
1950
|
+
const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
|
|
1951
|
+
if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
|
|
1952
|
+
continue;
|
|
1953
|
+
const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
1954
|
+
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
1955
|
+
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
1956
|
+
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
1957
|
+
upsertRequest(db, {
|
|
1958
|
+
id: requestId,
|
|
1959
|
+
agent: "gemini",
|
|
1960
|
+
session_id: sessionId,
|
|
1961
|
+
model,
|
|
1962
|
+
input_tokens: inputTokens,
|
|
1963
|
+
output_tokens: outputTokens,
|
|
1964
|
+
cache_read_tokens: cacheReadTokens,
|
|
1965
|
+
cache_create_tokens: 0,
|
|
1966
|
+
cost_usd: costUsd,
|
|
1967
|
+
cost_basis: defaultCostBasisForAgent("gemini"),
|
|
1968
|
+
duration_ms: 0,
|
|
1969
|
+
timestamp,
|
|
1970
|
+
source_request_id: message.id ?? requestId,
|
|
1971
|
+
machine_id: machineId
|
|
1972
|
+
});
|
|
1973
|
+
totalRequests++;
|
|
1974
|
+
}
|
|
991
1975
|
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
992
1976
|
}
|
|
993
1977
|
}
|
|
994
1978
|
for (const sessionId of touchedSessions) {
|
|
995
1979
|
rollupSession(db, sessionId);
|
|
996
1980
|
}
|
|
997
|
-
return { sessions: totalSessions };
|
|
1981
|
+
return { sessions: totalSessions, requests: totalRequests };
|
|
998
1982
|
}
|
|
999
1983
|
|
|
1000
|
-
// src/
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1984
|
+
// src/ingest/opencode.ts
|
|
1985
|
+
init_database();
|
|
1986
|
+
init_pricing();
|
|
1987
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
1988
|
+
import { homedir as homedir5 } from "os";
|
|
1989
|
+
import { join as join5 } from "path";
|
|
1990
|
+
var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
|
|
1991
|
+
function walkJsonFiles(dir, acc = []) {
|
|
1992
|
+
if (!existsSync5(dir))
|
|
1993
|
+
return acc;
|
|
1994
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1995
|
+
const p = join5(dir, entry.name);
|
|
1996
|
+
if (entry.isDirectory())
|
|
1997
|
+
walkJsonFiles(p, acc);
|
|
1998
|
+
else if (entry.name.endsWith(".json"))
|
|
1999
|
+
acc.push(p);
|
|
2000
|
+
}
|
|
2001
|
+
return acc;
|
|
2002
|
+
}
|
|
2003
|
+
function parseSessionIdFromPath(filePath) {
|
|
2004
|
+
const parts = filePath.split("/");
|
|
2005
|
+
const msgIdx = parts.indexOf("message");
|
|
2006
|
+
if (msgIdx >= 0 && parts[msgIdx + 1])
|
|
2007
|
+
return parts[msgIdx + 1];
|
|
2008
|
+
const sessionIdx = parts.indexOf("session");
|
|
2009
|
+
if (sessionIdx >= 0 && parts[parts.length - 1]?.endsWith(".json")) {
|
|
2010
|
+
return parts[parts.length - 1].replace(/\.json$/, "");
|
|
2011
|
+
}
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
async function ingestOpenCode(db, verbose = false) {
|
|
2015
|
+
const messageDir = join5(OPENCODE_STORAGE, "message");
|
|
2016
|
+
const files = walkJsonFiles(messageDir);
|
|
2017
|
+
let requests = 0;
|
|
2018
|
+
const touched = new Set;
|
|
2019
|
+
const machineId = getMachineId();
|
|
2020
|
+
const now = new Date().toISOString();
|
|
2021
|
+
for (const file of files) {
|
|
2022
|
+
const mtime = statSync4(file).mtimeMs;
|
|
2023
|
+
const stateKey = file;
|
|
2024
|
+
const prev = getIngestState(db, "opencode", stateKey);
|
|
2025
|
+
if (prev && Number(prev) >= mtime)
|
|
2026
|
+
continue;
|
|
2027
|
+
let parsed;
|
|
2028
|
+
try {
|
|
2029
|
+
parsed = JSON.parse(readFileSync5(file, "utf-8"));
|
|
2030
|
+
} catch {
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
if (parsed.role !== "assistant")
|
|
2034
|
+
continue;
|
|
2035
|
+
const usage = parsed.usage;
|
|
2036
|
+
if (!usage)
|
|
2037
|
+
continue;
|
|
2038
|
+
const sessionId = parseSessionIdFromPath(file) ?? `opencode-${statSync4(file).ino}`;
|
|
2039
|
+
const model = normalizeModelName(parsed.model ?? "unknown");
|
|
2040
|
+
const input = usage.inputTokens ?? 0;
|
|
2041
|
+
const output = usage.outputTokens ?? 0;
|
|
2042
|
+
const cacheRead = usage.cacheReadTokens ?? 0;
|
|
2043
|
+
const cacheWrite = usage.cacheWriteTokens ?? 0;
|
|
2044
|
+
if (input + output + cacheRead + cacheWrite === 0 && !usage.cost)
|
|
2045
|
+
continue;
|
|
2046
|
+
const timestamp = usage && parsed.time?.created ? new Date(parsed.time.created).toISOString() : new Date(statSync4(file).mtime).toISOString();
|
|
2047
|
+
const sourceId = file.replace(OPENCODE_STORAGE, "");
|
|
2048
|
+
const reqId = `opencode-${sourceId}`;
|
|
2049
|
+
const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
|
|
2050
|
+
upsertRequest(db, {
|
|
2051
|
+
id: reqId,
|
|
2052
|
+
agent: "opencode",
|
|
2053
|
+
session_id: sessionId,
|
|
2054
|
+
model,
|
|
2055
|
+
input_tokens: input,
|
|
2056
|
+
output_tokens: output,
|
|
2057
|
+
cache_read_tokens: cacheRead,
|
|
2058
|
+
cache_create_tokens: cacheWrite,
|
|
2059
|
+
cost_usd: costUsd,
|
|
2060
|
+
cost_basis: defaultCostBasisForAgent("opencode"),
|
|
2061
|
+
duration_ms: 0,
|
|
2062
|
+
timestamp,
|
|
2063
|
+
source_request_id: sourceId,
|
|
2064
|
+
machine_id: machineId,
|
|
2065
|
+
updated_at: now
|
|
2066
|
+
});
|
|
2067
|
+
requests++;
|
|
2068
|
+
if (!touched.has(sessionId)) {
|
|
2069
|
+
upsertSession(db, {
|
|
2070
|
+
id: sessionId,
|
|
2071
|
+
agent: "opencode",
|
|
2072
|
+
project_path: "",
|
|
2073
|
+
project_name: "",
|
|
2074
|
+
started_at: timestamp,
|
|
2075
|
+
ended_at: null,
|
|
2076
|
+
total_cost_usd: 0,
|
|
2077
|
+
total_tokens: 0,
|
|
2078
|
+
request_count: 0,
|
|
2079
|
+
machine_id: machineId,
|
|
2080
|
+
updated_at: now
|
|
2081
|
+
});
|
|
2082
|
+
touched.add(sessionId);
|
|
2083
|
+
}
|
|
2084
|
+
setIngestState(db, "opencode", stateKey, String(mtime));
|
|
2085
|
+
if (verbose)
|
|
2086
|
+
console.log(` opencode: ${reqId} ${model} $${costUsd.toFixed(4)}`);
|
|
2087
|
+
}
|
|
2088
|
+
for (const sid of touched)
|
|
2089
|
+
rollupSession(db, sid);
|
|
2090
|
+
return { files: files.length, requests, sessions: touched.size };
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// src/ingest/cursor.ts
|
|
2094
|
+
init_database();
|
|
2095
|
+
function getCursorSessionToken() {
|
|
2096
|
+
return process.env["CURSOR_SESSION_TOKEN"] ?? process.env["CURSOR_API_TOKEN"] ?? null;
|
|
2097
|
+
}
|
|
2098
|
+
async function cursorFetch(path, token) {
|
|
2099
|
+
try {
|
|
2100
|
+
const res = await fetch(`https://cursor.com${path}`, {
|
|
2101
|
+
headers: {
|
|
2102
|
+
Cookie: `WorkosCursorSessionToken=${token}`,
|
|
2103
|
+
Accept: "application/json"
|
|
2104
|
+
},
|
|
2105
|
+
signal: AbortSignal.timeout(1e4)
|
|
2106
|
+
});
|
|
2107
|
+
if (!res.ok)
|
|
2108
|
+
return null;
|
|
2109
|
+
return await res.json();
|
|
2110
|
+
} catch {
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
async function ingestCursor(db, verbose = false) {
|
|
2115
|
+
const token = getCursorSessionToken();
|
|
2116
|
+
if (!token) {
|
|
2117
|
+
if (verbose)
|
|
2118
|
+
console.log(" cursor: skipped \u2014 set CURSOR_SESSION_TOKEN");
|
|
2119
|
+
return { requests: 0, snapshots: 0 };
|
|
2120
|
+
}
|
|
2121
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
2122
|
+
const prev = getIngestState(db, "cursor", `sync-${today}`);
|
|
2123
|
+
if (prev)
|
|
2124
|
+
return { requests: 0, snapshots: 0 };
|
|
2125
|
+
const machineId = getMachineId();
|
|
2126
|
+
const now = new Date().toISOString();
|
|
2127
|
+
let snapshots = 0;
|
|
2128
|
+
const usage = await cursorFetch("/api/usage", token);
|
|
2129
|
+
if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
|
|
2130
|
+
upsertUsageSnapshot(db, {
|
|
2131
|
+
agent: "cursor",
|
|
2132
|
+
date: today,
|
|
2133
|
+
metric: "premium_requests_used",
|
|
2134
|
+
value: usage.premiumRequests,
|
|
2135
|
+
unit: "count",
|
|
2136
|
+
machine_id: machineId
|
|
2137
|
+
});
|
|
2138
|
+
upsertUsageSnapshot(db, {
|
|
2139
|
+
agent: "cursor",
|
|
2140
|
+
date: today,
|
|
2141
|
+
metric: "premium_requests_limit",
|
|
2142
|
+
value: usage.maxPremiumRequests,
|
|
2143
|
+
unit: "count",
|
|
2144
|
+
machine_id: machineId
|
|
2145
|
+
});
|
|
2146
|
+
snapshots += 2;
|
|
2147
|
+
}
|
|
2148
|
+
const summary = await cursorFetch("/api/usage-summary", token);
|
|
2149
|
+
const onDemand = summary?.individualUsage?.spend ?? summary?.teamUsage?.spend ?? 0;
|
|
2150
|
+
const included = summary?.individualUsage?.includedSpend ?? 0;
|
|
2151
|
+
if (onDemand > 0) {
|
|
2152
|
+
upsertUsageSnapshot(db, {
|
|
2153
|
+
agent: "cursor",
|
|
2154
|
+
date: today,
|
|
2155
|
+
metric: "on_demand_usd",
|
|
2156
|
+
value: onDemand,
|
|
2157
|
+
unit: "usd",
|
|
2158
|
+
machine_id: machineId
|
|
2159
|
+
});
|
|
2160
|
+
snapshots++;
|
|
2161
|
+
}
|
|
2162
|
+
if (included > 0) {
|
|
2163
|
+
upsertUsageSnapshot(db, {
|
|
2164
|
+
agent: "cursor",
|
|
2165
|
+
date: today,
|
|
2166
|
+
metric: "included_consumed_usd",
|
|
2167
|
+
value: included,
|
|
2168
|
+
unit: "usd",
|
|
2169
|
+
machine_id: machineId
|
|
2170
|
+
});
|
|
2171
|
+
snapshots++;
|
|
2172
|
+
}
|
|
2173
|
+
const sessionId = `cursor-${today}-${machineId}`;
|
|
2174
|
+
if (onDemand + included > 0) {
|
|
2175
|
+
upsertSession(db, {
|
|
2176
|
+
id: sessionId,
|
|
2177
|
+
agent: "cursor",
|
|
2178
|
+
project_path: "",
|
|
2179
|
+
project_name: "Cursor subscription",
|
|
2180
|
+
started_at: `${today}T00:00:00.000Z`,
|
|
2181
|
+
ended_at: now,
|
|
2182
|
+
total_cost_usd: onDemand + included,
|
|
2183
|
+
total_tokens: 0,
|
|
2184
|
+
request_count: 1,
|
|
2185
|
+
machine_id: machineId,
|
|
2186
|
+
updated_at: now
|
|
2187
|
+
});
|
|
2188
|
+
upsertRequest(db, {
|
|
2189
|
+
id: `cursor-${today}-${machineId}-usage`,
|
|
2190
|
+
agent: "cursor",
|
|
2191
|
+
session_id: sessionId,
|
|
2192
|
+
model: "cursor-subscription",
|
|
2193
|
+
input_tokens: 0,
|
|
2194
|
+
output_tokens: 0,
|
|
2195
|
+
cache_read_tokens: 0,
|
|
2196
|
+
cache_create_tokens: 0,
|
|
2197
|
+
cost_usd: onDemand + included,
|
|
2198
|
+
cost_basis: "subscription_included",
|
|
2199
|
+
duration_ms: 0,
|
|
2200
|
+
timestamp: now,
|
|
2201
|
+
source_request_id: today,
|
|
2202
|
+
machine_id: machineId,
|
|
2203
|
+
updated_at: now
|
|
2204
|
+
});
|
|
2205
|
+
rollupSession(db, sessionId);
|
|
2206
|
+
}
|
|
2207
|
+
setIngestState(db, "cursor", `sync-${today}`, now);
|
|
2208
|
+
if (verbose)
|
|
2209
|
+
console.log(` cursor: on-demand $${onDemand.toFixed(2)}, included $${included.toFixed(2)}`);
|
|
2210
|
+
return { requests: onDemand + included > 0 ? 1 : 0, snapshots };
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/ingest/pi.ts
|
|
2214
|
+
init_database();
|
|
2215
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
2216
|
+
import { homedir as homedir6 } from "os";
|
|
2217
|
+
import { join as join6 } from "path";
|
|
2218
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
|
|
2219
|
+
function walkSessions(dir, acc = []) {
|
|
2220
|
+
if (!existsSync6(dir))
|
|
2221
|
+
return acc;
|
|
2222
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
2223
|
+
const p = join6(dir, entry.name);
|
|
2224
|
+
if (entry.isDirectory())
|
|
2225
|
+
walkSessions(p, acc);
|
|
2226
|
+
else if (entry.name.endsWith(".json"))
|
|
2227
|
+
acc.push(p);
|
|
2228
|
+
}
|
|
2229
|
+
return acc;
|
|
2230
|
+
}
|
|
2231
|
+
async function ingestPi(db, verbose = false) {
|
|
2232
|
+
const files = walkSessions(PI_SESSION_DIR);
|
|
2233
|
+
let requests = 0;
|
|
2234
|
+
const touched = new Set;
|
|
2235
|
+
const machineId = getMachineId();
|
|
2236
|
+
const now = new Date().toISOString();
|
|
2237
|
+
for (const file of files) {
|
|
2238
|
+
const mtime = statSync5(file).mtimeMs;
|
|
2239
|
+
const prev = getIngestState(db, "pi", file);
|
|
2240
|
+
if (prev && Number(prev) >= mtime)
|
|
2241
|
+
continue;
|
|
2242
|
+
let data;
|
|
2243
|
+
try {
|
|
2244
|
+
data = JSON.parse(readFileSync6(file, "utf-8"));
|
|
2245
|
+
} catch {
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
const sessionId = data.id ?? file.replace(/\.json$/, "").split("/").pop() ?? `pi-${statSync5(file).ino}`;
|
|
2249
|
+
const turns = data.turns ?? data.messages?.filter((m) => m.role === "assistant") ?? [];
|
|
2250
|
+
for (let i = 0;i < turns.length; i++) {
|
|
2251
|
+
const turn = turns[i];
|
|
2252
|
+
const usage = turn.usage;
|
|
2253
|
+
if (!usage)
|
|
2254
|
+
continue;
|
|
2255
|
+
const input = usage.input ?? 0;
|
|
2256
|
+
const output = usage.output ?? 0;
|
|
2257
|
+
if (input + output === 0 && !usage.cost)
|
|
2258
|
+
continue;
|
|
2259
|
+
const model = turn.model ?? turn.provider ?? "unknown";
|
|
2260
|
+
const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
|
|
2261
|
+
const reqId = `pi-${sessionId}-${i}`;
|
|
2262
|
+
upsertRequest(db, {
|
|
2263
|
+
id: reqId,
|
|
2264
|
+
agent: "pi",
|
|
2265
|
+
session_id: sessionId,
|
|
2266
|
+
model,
|
|
2267
|
+
input_tokens: input,
|
|
2268
|
+
output_tokens: output,
|
|
2269
|
+
cache_read_tokens: usage.cacheRead ?? 0,
|
|
2270
|
+
cache_create_tokens: usage.cacheWrite ?? 0,
|
|
2271
|
+
cost_usd: usage.cost ?? 0,
|
|
2272
|
+
cost_basis: defaultCostBasisForAgent("pi"),
|
|
2273
|
+
duration_ms: 0,
|
|
2274
|
+
timestamp,
|
|
2275
|
+
source_request_id: `${sessionId}-${i}`,
|
|
2276
|
+
machine_id: machineId,
|
|
2277
|
+
updated_at: now
|
|
2278
|
+
});
|
|
2279
|
+
requests++;
|
|
2280
|
+
}
|
|
2281
|
+
if (turns.length > 0) {
|
|
2282
|
+
upsertSession(db, {
|
|
2283
|
+
id: sessionId,
|
|
2284
|
+
agent: "pi",
|
|
2285
|
+
project_path: "",
|
|
2286
|
+
project_name: "",
|
|
2287
|
+
started_at: turns[0]?.timestamp ?? now,
|
|
2288
|
+
ended_at: null,
|
|
2289
|
+
total_cost_usd: 0,
|
|
2290
|
+
total_tokens: 0,
|
|
2291
|
+
request_count: 0,
|
|
2292
|
+
machine_id: machineId,
|
|
2293
|
+
updated_at: now
|
|
2294
|
+
});
|
|
2295
|
+
touched.add(sessionId);
|
|
2296
|
+
}
|
|
2297
|
+
setIngestState(db, "pi", file, String(mtime));
|
|
2298
|
+
if (verbose)
|
|
2299
|
+
console.log(` pi: ${sessionId} (${turns.length} turns)`);
|
|
2300
|
+
}
|
|
2301
|
+
for (const sid of touched)
|
|
2302
|
+
rollupSession(db, sid);
|
|
2303
|
+
return { files: files.length, requests, sessions: touched.size };
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// src/ingest/hermes.ts
|
|
2307
|
+
init_database();
|
|
2308
|
+
import { existsSync as existsSync7, statSync as statSync6 } from "fs";
|
|
2309
|
+
import { homedir as homedir7 } from "os";
|
|
2310
|
+
import { join as join7 } from "path";
|
|
2311
|
+
var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
|
|
2312
|
+
function mapCostBasis(billingMode) {
|
|
2313
|
+
if (billingMode === "subscription")
|
|
2314
|
+
return "subscription_included";
|
|
2315
|
+
if (billingMode === "api")
|
|
2316
|
+
return "metered_api";
|
|
2317
|
+
return defaultCostBasisForAgent("hermes");
|
|
2318
|
+
}
|
|
2319
|
+
async function ingestHermes(db, verbose = false) {
|
|
2320
|
+
if (!existsSync7(HERMES_DB)) {
|
|
2321
|
+
return { sessions: 0, requests: 0 };
|
|
2322
|
+
}
|
|
2323
|
+
const { Database: Sqlite } = await import("bun:sqlite");
|
|
2324
|
+
const hermes = new Sqlite(HERMES_DB, { readonly: true });
|
|
2325
|
+
const rows = hermes.prepare(`
|
|
2326
|
+
SELECT id, source, model, started_at, ended_at,
|
|
2327
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
2328
|
+
reasoning_tokens, estimated_cost_usd, actual_cost_usd, billing_mode, parent_session_id
|
|
2329
|
+
FROM sessions
|
|
2330
|
+
ORDER BY started_at DESC
|
|
2331
|
+
`).all();
|
|
2332
|
+
const stateKey = "state.db";
|
|
2333
|
+
const mtime = statSyncSafe(HERMES_DB);
|
|
2334
|
+
const prev = getIngestState(db, "hermes", stateKey);
|
|
2335
|
+
if (prev && Number(prev) >= mtime && rows.length === 0) {
|
|
2336
|
+
hermes.close();
|
|
2337
|
+
return { sessions: 0, requests: 0 };
|
|
2338
|
+
}
|
|
2339
|
+
const machineId = getMachineId();
|
|
2340
|
+
const now = new Date().toISOString();
|
|
2341
|
+
let requests = 0;
|
|
2342
|
+
for (const row of rows) {
|
|
2343
|
+
const sessionId = `hermes-${row.id}`;
|
|
2344
|
+
const startedAt = new Date(row.started_at * 1000).toISOString();
|
|
2345
|
+
const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
|
|
2346
|
+
const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
|
|
2347
|
+
const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
|
|
2348
|
+
upsertSession(db, {
|
|
2349
|
+
id: sessionId,
|
|
2350
|
+
agent: "hermes",
|
|
2351
|
+
project_path: row.source ?? "",
|
|
2352
|
+
project_name: row.source ?? "",
|
|
2353
|
+
started_at: startedAt,
|
|
2354
|
+
ended_at: endedAt,
|
|
2355
|
+
total_cost_usd: cost,
|
|
2356
|
+
total_tokens: tokens,
|
|
2357
|
+
request_count: 1,
|
|
2358
|
+
machine_id: machineId,
|
|
2359
|
+
updated_at: now
|
|
2360
|
+
});
|
|
2361
|
+
const reqId = `hermes-${row.id}-rollup`;
|
|
2362
|
+
upsertRequest(db, {
|
|
2363
|
+
id: reqId,
|
|
2364
|
+
agent: "hermes",
|
|
2365
|
+
session_id: sessionId,
|
|
2366
|
+
model: row.model ?? "unknown",
|
|
2367
|
+
input_tokens: row.input_tokens,
|
|
2368
|
+
output_tokens: row.output_tokens + row.reasoning_tokens,
|
|
2369
|
+
cache_read_tokens: row.cache_read_tokens,
|
|
2370
|
+
cache_create_tokens: row.cache_write_tokens,
|
|
2371
|
+
cost_usd: cost,
|
|
2372
|
+
cost_basis: mapCostBasis(row.billing_mode),
|
|
2373
|
+
duration_ms: 0,
|
|
2374
|
+
timestamp: endedAt ?? startedAt,
|
|
2375
|
+
source_request_id: row.id,
|
|
2376
|
+
machine_id: machineId,
|
|
2377
|
+
updated_at: now
|
|
2378
|
+
});
|
|
2379
|
+
requests++;
|
|
2380
|
+
rollupSession(db, sessionId);
|
|
2381
|
+
if (verbose)
|
|
2382
|
+
console.log(` hermes: ${sessionId} $${cost.toFixed(4)}`);
|
|
2383
|
+
}
|
|
2384
|
+
setIngestState(db, "hermes", stateKey, String(mtime));
|
|
2385
|
+
hermes.close();
|
|
2386
|
+
return { sessions: rows.length, requests };
|
|
2387
|
+
}
|
|
2388
|
+
function statSyncSafe(path) {
|
|
2389
|
+
try {
|
|
2390
|
+
return statSync6(path).mtimeMs;
|
|
2391
|
+
} catch {
|
|
2392
|
+
return 0;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// src/ingest/claude-quota.ts
|
|
2397
|
+
init_database();
|
|
2398
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
2399
|
+
|
|
2400
|
+
// src/lib/paths.ts
|
|
2401
|
+
import { homedir as homedir8 } from "os";
|
|
2402
|
+
import { join as join8 } from "path";
|
|
2403
|
+
function getHomeDir() {
|
|
2404
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir8();
|
|
2405
|
+
}
|
|
2406
|
+
function agentPaths() {
|
|
2407
|
+
const home = getHomeDir();
|
|
2408
|
+
return {
|
|
2409
|
+
claudeProjects: join8(home, ".claude", "projects"),
|
|
2410
|
+
claudeCredentials: join8(home, ".claude", ".credentials.json"),
|
|
2411
|
+
takumiProjects: join8(home, ".takumi", "projects"),
|
|
2412
|
+
codexDir: join8(home, ".codex"),
|
|
2413
|
+
codexDb: join8(home, ".codex", "state_5.sqlite"),
|
|
2414
|
+
codexAuth: join8(home, ".codex", "auth.json"),
|
|
2415
|
+
codexConfig: join8(home, ".codex", "config.toml"),
|
|
2416
|
+
geminiTmp: join8(home, ".gemini", "tmp"),
|
|
2417
|
+
geminiHistory: join8(home, ".gemini", "history"),
|
|
2418
|
+
opencodeMessages: join8(home, ".local", "share", "opencode", "storage", "message"),
|
|
2419
|
+
piSessions: join8(home, ".pi", "agent", "sessions"),
|
|
2420
|
+
hermesDir: join8(home, ".hermes"),
|
|
2421
|
+
hermesDb: join8(home, ".hermes", "state.db")
|
|
1011
2422
|
};
|
|
1012
|
-
return cachedMetadata;
|
|
1013
2423
|
}
|
|
1014
|
-
var packageMetadata = getPackageMetadata();
|
|
1015
2424
|
|
|
1016
|
-
// src/
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
2425
|
+
// src/ingest/claude-quota.ts
|
|
2426
|
+
var CREDENTIALS_PATH = agentPaths().claudeCredentials;
|
|
2427
|
+
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
2428
|
+
var OAUTH_BETA = "oauth-2025-04-20";
|
|
2429
|
+
function readClaudeToken() {
|
|
2430
|
+
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
2431
|
+
if (fromEnv)
|
|
2432
|
+
return { token: fromEnv };
|
|
2433
|
+
if (!existsSync8(CREDENTIALS_PATH))
|
|
2434
|
+
return null;
|
|
2435
|
+
try {
|
|
2436
|
+
const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
|
|
2437
|
+
const oauth = creds.claudeAiOauth;
|
|
2438
|
+
if (!oauth?.accessToken)
|
|
2439
|
+
return null;
|
|
2440
|
+
return {
|
|
2441
|
+
token: oauth.accessToken,
|
|
2442
|
+
subscriptionType: oauth.subscriptionType,
|
|
2443
|
+
rateLimitTier: oauth.rateLimitTier
|
|
2444
|
+
};
|
|
2445
|
+
} catch {
|
|
2446
|
+
return null;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
function inferMonthlyFee(subscriptionType, rateLimitTier) {
|
|
2450
|
+
const tier = `${subscriptionType ?? ""} ${rateLimitTier ?? ""}`.toLowerCase();
|
|
2451
|
+
if (tier.includes("max") && tier.includes("20"))
|
|
2452
|
+
return 200;
|
|
2453
|
+
if (tier.includes("max"))
|
|
2454
|
+
return 100;
|
|
2455
|
+
if (tier.includes("pro"))
|
|
2456
|
+
return 20;
|
|
2457
|
+
if (tier.includes("team"))
|
|
2458
|
+
return 30;
|
|
2459
|
+
return 20;
|
|
2460
|
+
}
|
|
2461
|
+
async function fetchClaudeOAuthUsage(token) {
|
|
2462
|
+
try {
|
|
2463
|
+
const res = await fetch(USAGE_URL, {
|
|
2464
|
+
headers: {
|
|
2465
|
+
Authorization: `Bearer ${token}`,
|
|
2466
|
+
"anthropic-beta": OAUTH_BETA,
|
|
2467
|
+
Accept: "application/json"
|
|
2468
|
+
},
|
|
2469
|
+
signal: AbortSignal.timeout(1e4)
|
|
2470
|
+
});
|
|
2471
|
+
if (!res.ok)
|
|
2472
|
+
return null;
|
|
2473
|
+
return await res.json();
|
|
2474
|
+
} catch {
|
|
2475
|
+
return null;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
async function ingestClaudeQuota(db, verbose = false) {
|
|
2479
|
+
const auth = readClaudeToken();
|
|
2480
|
+
if (!auth) {
|
|
2481
|
+
if (verbose)
|
|
2482
|
+
console.log(" claude quota: skipped \u2014 no OAuth token (~/.claude/.credentials.json or CLAUDE_OAUTH_TOKEN)");
|
|
2483
|
+
return { snapshots: 0 };
|
|
2484
|
+
}
|
|
2485
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
2486
|
+
const prev = getIngestState(db, "claude", `quota-${today}`);
|
|
2487
|
+
if (prev)
|
|
2488
|
+
return { snapshots: 0 };
|
|
2489
|
+
const usage = await fetchClaudeOAuthUsage(auth.token);
|
|
2490
|
+
if (!usage) {
|
|
2491
|
+
if (verbose)
|
|
2492
|
+
console.log(" claude quota: OAuth usage endpoint unavailable");
|
|
2493
|
+
return { snapshots: 0 };
|
|
2494
|
+
}
|
|
2495
|
+
const machineId = getMachineId();
|
|
2496
|
+
let snapshots = 0;
|
|
2497
|
+
const windows = [
|
|
2498
|
+
["five_hour_utilization", usage.five_hour],
|
|
2499
|
+
["seven_day_utilization", usage.seven_day],
|
|
2500
|
+
["seven_day_sonnet_utilization", usage.seven_day_sonnet],
|
|
2501
|
+
["seven_day_opus_utilization", usage.seven_day_opus]
|
|
2502
|
+
];
|
|
2503
|
+
for (const [metric, window] of windows) {
|
|
2504
|
+
if (window?.utilization == null)
|
|
2505
|
+
continue;
|
|
2506
|
+
upsertUsageSnapshot(db, {
|
|
2507
|
+
agent: "claude",
|
|
2508
|
+
date: today,
|
|
2509
|
+
metric,
|
|
2510
|
+
value: Math.round(window.utilization * 1000) / 10,
|
|
2511
|
+
unit: "percent",
|
|
2512
|
+
machine_id: machineId
|
|
2513
|
+
});
|
|
2514
|
+
snapshots++;
|
|
2515
|
+
if (window.resets_at) {
|
|
2516
|
+
upsertUsageSnapshot(db, {
|
|
2517
|
+
agent: "claude",
|
|
2518
|
+
date: today,
|
|
2519
|
+
metric: `${metric}_resets_at`,
|
|
2520
|
+
value: Date.parse(window.resets_at),
|
|
2521
|
+
unit: "epoch_ms",
|
|
2522
|
+
machine_id: machineId
|
|
2523
|
+
});
|
|
2524
|
+
snapshots++;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
if (usage.extra_usage?.spend != null) {
|
|
2528
|
+
upsertUsageSnapshot(db, {
|
|
2529
|
+
agent: "claude",
|
|
2530
|
+
date: today,
|
|
2531
|
+
metric: "on_demand_usd",
|
|
2532
|
+
value: usage.extra_usage.spend,
|
|
2533
|
+
unit: "usd",
|
|
2534
|
+
machine_id: machineId
|
|
2535
|
+
});
|
|
2536
|
+
snapshots++;
|
|
2537
|
+
}
|
|
2538
|
+
if (usage.extra_usage?.limit != null) {
|
|
2539
|
+
upsertUsageSnapshot(db, {
|
|
2540
|
+
agent: "claude",
|
|
2541
|
+
date: today,
|
|
2542
|
+
metric: "on_demand_limit_usd",
|
|
2543
|
+
value: usage.extra_usage.limit,
|
|
2544
|
+
unit: "usd",
|
|
2545
|
+
machine_id: machineId
|
|
2546
|
+
});
|
|
2547
|
+
snapshots++;
|
|
2548
|
+
}
|
|
2549
|
+
const monthlyFee = inferMonthlyFee(auth.subscriptionType, auth.rateLimitTier);
|
|
2550
|
+
const now = new Date().toISOString();
|
|
2551
|
+
upsertSubscription(db, {
|
|
2552
|
+
id: "anthropic-claude-oauth",
|
|
2553
|
+
provider: "anthropic",
|
|
2554
|
+
agent: "claude",
|
|
2555
|
+
plan: auth.rateLimitTier ?? auth.subscriptionType ?? "claude_pro",
|
|
2556
|
+
monthly_fee_usd: monthlyFee,
|
|
2557
|
+
included_usage_usd: monthlyFee,
|
|
2558
|
+
billing_cycle_start: null,
|
|
2559
|
+
reset_policy: "monthly",
|
|
2560
|
+
active: 1,
|
|
2561
|
+
created_at: now,
|
|
2562
|
+
updated_at: now
|
|
2563
|
+
});
|
|
2564
|
+
setIngestState(db, "claude", `quota-${today}`, new Date().toISOString());
|
|
2565
|
+
if (verbose)
|
|
2566
|
+
console.log(` claude quota: ${snapshots} snapshots`);
|
|
2567
|
+
return { snapshots };
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/ingest/codex-quota.ts
|
|
2571
|
+
init_database();
|
|
2572
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
2573
|
+
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
2574
|
+
function readCodexAuth() {
|
|
2575
|
+
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
2576
|
+
if (fromEnv)
|
|
2577
|
+
return { token: fromEnv, authMode: "chatgpt" };
|
|
2578
|
+
const authPath = agentPaths().codexAuth;
|
|
2579
|
+
if (!existsSync9(authPath))
|
|
2580
|
+
return null;
|
|
2581
|
+
try {
|
|
2582
|
+
const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
|
|
2583
|
+
const token = auth.tokens?.access_token;
|
|
2584
|
+
if (!token)
|
|
2585
|
+
return null;
|
|
2586
|
+
return {
|
|
2587
|
+
token,
|
|
2588
|
+
accountId: auth.tokens?.account_id,
|
|
2589
|
+
authMode: auth.auth_mode ?? "chatgpt"
|
|
2590
|
+
};
|
|
2591
|
+
} catch {
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
function planMonthlyFee(planType) {
|
|
2596
|
+
const plan = (planType ?? "").toLowerCase();
|
|
2597
|
+
if (plan.includes("pro"))
|
|
2598
|
+
return 200;
|
|
2599
|
+
if (plan.includes("plus"))
|
|
2600
|
+
return 20;
|
|
2601
|
+
if (plan.includes("team"))
|
|
2602
|
+
return 30;
|
|
2603
|
+
return 20;
|
|
2604
|
+
}
|
|
2605
|
+
async function fetchCodexUsage(token, accountId) {
|
|
2606
|
+
try {
|
|
2607
|
+
const headers = {
|
|
2608
|
+
Authorization: `Bearer ${token}`,
|
|
2609
|
+
Accept: "application/json",
|
|
2610
|
+
"User-Agent": "economy-cli"
|
|
2611
|
+
};
|
|
2612
|
+
if (accountId)
|
|
2613
|
+
headers["ChatGPT-Account-Id"] = accountId;
|
|
2614
|
+
const res = await fetch(WHAM_USAGE_URL, {
|
|
2615
|
+
headers,
|
|
2616
|
+
signal: AbortSignal.timeout(1e4)
|
|
2617
|
+
});
|
|
2618
|
+
if (!res.ok)
|
|
2619
|
+
return null;
|
|
2620
|
+
return await res.json();
|
|
2621
|
+
} catch {
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
async function ingestCodexQuota(db, verbose = false) {
|
|
2626
|
+
const auth = readCodexAuth();
|
|
2627
|
+
if (!auth) {
|
|
2628
|
+
if (verbose)
|
|
2629
|
+
console.log(" codex quota: skipped \u2014 no ~/.codex/auth.json or CODEX_OAUTH_TOKEN");
|
|
2630
|
+
return { snapshots: 0 };
|
|
2631
|
+
}
|
|
2632
|
+
if (auth.authMode === "api_key" || auth.authMode === "api") {
|
|
2633
|
+
if (verbose)
|
|
2634
|
+
console.log(" codex quota: skipped \u2014 API key mode (no subscription quota)");
|
|
2635
|
+
return { snapshots: 0 };
|
|
2636
|
+
}
|
|
2637
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
2638
|
+
const prev = getIngestState(db, "codex", `quota-${today}`);
|
|
2639
|
+
if (prev)
|
|
2640
|
+
return { snapshots: 0 };
|
|
2641
|
+
const usage = await fetchCodexUsage(auth.token, auth.accountId);
|
|
2642
|
+
if (!usage) {
|
|
2643
|
+
if (verbose)
|
|
2644
|
+
console.log(" codex quota: wham/usage endpoint unavailable");
|
|
2645
|
+
return { snapshots: 0 };
|
|
2646
|
+
}
|
|
2647
|
+
const machineId = getMachineId();
|
|
2648
|
+
let snapshots = 0;
|
|
2649
|
+
const now = new Date().toISOString();
|
|
2650
|
+
const windows = [
|
|
2651
|
+
["five_hour_utilization", usage.rate_limit?.primary_window],
|
|
2652
|
+
["seven_day_utilization", usage.rate_limit?.secondary_window]
|
|
2653
|
+
];
|
|
2654
|
+
for (const [metric, window] of windows) {
|
|
2655
|
+
if (window?.used_percent == null)
|
|
2656
|
+
continue;
|
|
2657
|
+
upsertUsageSnapshot(db, {
|
|
2658
|
+
agent: "codex",
|
|
2659
|
+
date: today,
|
|
2660
|
+
metric,
|
|
2661
|
+
value: window.used_percent,
|
|
2662
|
+
unit: "percent",
|
|
2663
|
+
machine_id: machineId
|
|
2664
|
+
});
|
|
2665
|
+
snapshots++;
|
|
2666
|
+
if (window.reset_at) {
|
|
2667
|
+
upsertUsageSnapshot(db, {
|
|
2668
|
+
agent: "codex",
|
|
2669
|
+
date: today,
|
|
2670
|
+
metric: `${metric}_resets_at`,
|
|
2671
|
+
value: window.reset_at * 1000,
|
|
2672
|
+
unit: "epoch_ms",
|
|
2673
|
+
machine_id: machineId
|
|
2674
|
+
});
|
|
2675
|
+
snapshots++;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (usage.credits?.balance != null) {
|
|
2679
|
+
upsertUsageSnapshot(db, {
|
|
2680
|
+
agent: "codex",
|
|
2681
|
+
date: today,
|
|
2682
|
+
metric: "credits_balance_usd",
|
|
2683
|
+
value: usage.credits.balance,
|
|
2684
|
+
unit: "usd",
|
|
2685
|
+
machine_id: machineId
|
|
2686
|
+
});
|
|
2687
|
+
snapshots++;
|
|
2688
|
+
}
|
|
2689
|
+
const monthlyFee = planMonthlyFee(usage.plan_type);
|
|
2690
|
+
upsertSubscription(db, {
|
|
2691
|
+
id: "openai-codex-oauth",
|
|
2692
|
+
provider: "openai",
|
|
2693
|
+
agent: "codex",
|
|
2694
|
+
plan: usage.plan_type ?? "chatgpt_plus",
|
|
2695
|
+
monthly_fee_usd: monthlyFee,
|
|
2696
|
+
included_usage_usd: monthlyFee,
|
|
2697
|
+
billing_cycle_start: null,
|
|
2698
|
+
reset_policy: "monthly",
|
|
2699
|
+
active: 1,
|
|
2700
|
+
created_at: now,
|
|
2701
|
+
updated_at: now
|
|
2702
|
+
});
|
|
2703
|
+
setIngestState(db, "codex", `quota-${today}`, now);
|
|
2704
|
+
if (verbose)
|
|
2705
|
+
console.log(` codex quota: ${snapshots} snapshots (${usage.plan_type ?? "unknown plan"})`);
|
|
2706
|
+
return { snapshots };
|
|
2707
|
+
}
|
|
1020
2708
|
|
|
1021
|
-
|
|
2709
|
+
// src/lib/sync-all.ts
|
|
2710
|
+
init_database();
|
|
1022
2711
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
2712
|
+
// src/lib/cloud-sync.ts
|
|
2713
|
+
init_database();
|
|
2714
|
+
var CLOUD_TABLES = [
|
|
2715
|
+
"requests",
|
|
2716
|
+
"sessions",
|
|
2717
|
+
"projects",
|
|
2718
|
+
"budgets",
|
|
2719
|
+
"goals",
|
|
2720
|
+
"model_pricing",
|
|
2721
|
+
"billing_daily",
|
|
2722
|
+
"subscriptions",
|
|
2723
|
+
"usage_snapshots",
|
|
2724
|
+
"savings_daily",
|
|
2725
|
+
"machines",
|
|
2726
|
+
"ingest_state"
|
|
2727
|
+
];
|
|
2728
|
+
function getCloudDatabaseUrl() {
|
|
2729
|
+
return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
|
|
1026
2730
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
printHelp();
|
|
1030
|
-
process.exit(0);
|
|
2731
|
+
function isCloudAutoEnabled() {
|
|
2732
|
+
return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
|
|
1031
2733
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
2734
|
+
function getCloudPullIntervalMinutes() {
|
|
2735
|
+
const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
|
|
2736
|
+
if (!raw)
|
|
2737
|
+
return 15;
|
|
2738
|
+
const n = Number(raw);
|
|
2739
|
+
return Number.isFinite(n) && n > 0 ? n : 15;
|
|
1035
2740
|
}
|
|
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
|
-
|
|
2741
|
+
async function getCloudPg() {
|
|
2742
|
+
const url = getCloudDatabaseUrl();
|
|
2743
|
+
if (!url) {
|
|
2744
|
+
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
2745
|
+
}
|
|
2746
|
+
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
2747
|
+
return new PgAdapterAsync(url);
|
|
2748
|
+
}
|
|
2749
|
+
async function runCloudMigrations(cloud) {
|
|
2750
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
2751
|
+
for (const sql of PG_MIGRATIONS2) {
|
|
2752
|
+
await cloud.run(sql);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
function isCloudIncrementalEnabled() {
|
|
2756
|
+
return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
|
|
2757
|
+
}
|
|
2758
|
+
async function cloudPush(opts) {
|
|
2759
|
+
const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
|
|
2760
|
+
const cloud = await getCloudPg();
|
|
2761
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2762
|
+
await runCloudMigrations(cloud);
|
|
2763
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
2764
|
+
let rows = 0;
|
|
2765
|
+
if (isCloudIncrementalEnabled()) {
|
|
2766
|
+
ensureSyncMetaTable(local);
|
|
2767
|
+
const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
|
|
2768
|
+
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
2769
|
+
} else {
|
|
2770
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
2771
|
+
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2772
|
+
}
|
|
2773
|
+
touchMachineRegistry(local, "push");
|
|
2774
|
+
local.close();
|
|
2775
|
+
await cloud.close();
|
|
2776
|
+
return { rows, machine: getMachineId() };
|
|
2777
|
+
}
|
|
2778
|
+
async function cloudPull(opts) {
|
|
2779
|
+
const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
|
|
2780
|
+
const cloud = await getCloudPg();
|
|
2781
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2782
|
+
await runCloudMigrations(cloud);
|
|
2783
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
2784
|
+
let rows = 0;
|
|
2785
|
+
if (isCloudIncrementalEnabled()) {
|
|
2786
|
+
ensureSyncMetaTable(local);
|
|
2787
|
+
const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
|
|
2788
|
+
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
2789
|
+
} else {
|
|
2790
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
2791
|
+
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2792
|
+
}
|
|
2793
|
+
touchMachineRegistry(local, "pull");
|
|
2794
|
+
local.close();
|
|
2795
|
+
await cloud.close();
|
|
2796
|
+
setLastCloudPull();
|
|
2797
|
+
return { rows, machine: getMachineId() };
|
|
2798
|
+
}
|
|
2799
|
+
function setLastCloudPull(at = new Date().toISOString()) {
|
|
2800
|
+
const db = openDatabase();
|
|
2801
|
+
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
2802
|
+
}
|
|
2803
|
+
function getLastCloudPull() {
|
|
2804
|
+
const db = openDatabase();
|
|
2805
|
+
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
2806
|
+
return row?.value ?? null;
|
|
2807
|
+
}
|
|
2808
|
+
function shouldPullFromCloud() {
|
|
2809
|
+
if (!getCloudDatabaseUrl())
|
|
2810
|
+
return false;
|
|
2811
|
+
const last = getLastCloudPull();
|
|
2812
|
+
if (!last)
|
|
2813
|
+
return true;
|
|
2814
|
+
const ageMs = Date.now() - new Date(last).getTime();
|
|
2815
|
+
return ageMs > getCloudPullIntervalMinutes() * 60000;
|
|
2816
|
+
}
|
|
2817
|
+
async function maybePullFromCloud() {
|
|
2818
|
+
if (!shouldPullFromCloud())
|
|
2819
|
+
return false;
|
|
2820
|
+
try {
|
|
2821
|
+
await cloudPull();
|
|
2822
|
+
return true;
|
|
2823
|
+
} catch {
|
|
2824
|
+
return false;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
async function maybePushAfterIngest() {
|
|
2828
|
+
if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
|
|
2829
|
+
return false;
|
|
2830
|
+
try {
|
|
2831
|
+
await cloudPush();
|
|
2832
|
+
return true;
|
|
2833
|
+
} catch {
|
|
2834
|
+
return false;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
function touchMachineRegistry(db, direction) {
|
|
2838
|
+
const now = new Date().toISOString();
|
|
2839
|
+
const machine = getMachineId();
|
|
2840
|
+
db.prepare(`
|
|
2841
|
+
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
2842
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2843
|
+
ON CONFLICT(machine_id) DO UPDATE SET
|
|
2844
|
+
hostname = excluded.hostname,
|
|
2845
|
+
last_seen_at = excluded.last_seen_at,
|
|
2846
|
+
last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
|
|
2847
|
+
last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
|
|
2848
|
+
economy_version = excluded.economy_version,
|
|
2849
|
+
updated_at = excluded.updated_at
|
|
2850
|
+
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// src/lib/sync-all.ts
|
|
2854
|
+
async function syncAll(db, opts = {}) {
|
|
2855
|
+
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
2856
|
+
const all = !anySpecific;
|
|
2857
|
+
await maybePullFromCloud();
|
|
2858
|
+
const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
|
|
2859
|
+
if (all || opts.claude) {
|
|
2860
|
+
result.claude = await ingestClaude(db, opts.verbose);
|
|
2861
|
+
result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
|
|
2862
|
+
}
|
|
2863
|
+
if (all || opts.takumi)
|
|
2864
|
+
result.takumi = await ingestTakumi(db, opts.verbose);
|
|
2865
|
+
if (all || opts.codex) {
|
|
2866
|
+
result.codex = await ingestCodex(db, opts.verbose);
|
|
2867
|
+
result.codexQuota = await ingestCodexQuota(db, opts.verbose);
|
|
2868
|
+
}
|
|
2869
|
+
if (all || opts.gemini)
|
|
2870
|
+
result.gemini = await ingestGemini(db, opts.verbose);
|
|
2871
|
+
if (all || opts.opencode)
|
|
2872
|
+
result.opencode = await ingestOpenCode(db, opts.verbose);
|
|
2873
|
+
if (all || opts.cursor)
|
|
2874
|
+
result.cursor = await ingestCursor(db, opts.verbose);
|
|
2875
|
+
if (all || opts.pi)
|
|
2876
|
+
result.pi = await ingestPi(db, opts.verbose);
|
|
2877
|
+
if (all || opts.hermes)
|
|
2878
|
+
result.hermes = await ingestHermes(db, opts.verbose);
|
|
2879
|
+
result.deduped = dedupeRequests(db);
|
|
2880
|
+
result.cloudPushed = await maybePushAfterIngest();
|
|
2881
|
+
return result;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
// src/lib/agents.ts
|
|
2885
|
+
var AGENTS = [
|
|
2886
|
+
"claude",
|
|
2887
|
+
"takumi",
|
|
2888
|
+
"codex",
|
|
2889
|
+
"gemini",
|
|
2890
|
+
"opencode",
|
|
2891
|
+
"cursor",
|
|
2892
|
+
"pi",
|
|
2893
|
+
"hermes"
|
|
1064
2894
|
];
|
|
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
|
-
|
|
2895
|
+
|
|
2896
|
+
// src/mcp/server.ts
|
|
2897
|
+
init_database();
|
|
2898
|
+
init_pricing();
|
|
2899
|
+
init_pricing();
|
|
2900
|
+
var MCP_NAME = "economy";
|
|
2901
|
+
var DEFAULT_MCP_HTTP_PORT = 8815;
|
|
2902
|
+
function buildServer() {
|
|
2903
|
+
const db = openDatabase();
|
|
2904
|
+
ensurePricingSeeded(db);
|
|
2905
|
+
const server = new McpServer({
|
|
2906
|
+
name: MCP_NAME,
|
|
2907
|
+
version: packageMetadata.version
|
|
2908
|
+
});
|
|
2909
|
+
const _econAgents = new Map;
|
|
2910
|
+
const TOOL_NAMES = [
|
|
2911
|
+
"get_cost_summary",
|
|
2912
|
+
"get_sessions",
|
|
2913
|
+
"get_top_sessions",
|
|
2914
|
+
"get_model_breakdown",
|
|
2915
|
+
"get_project_breakdown",
|
|
2916
|
+
"get_budget_status",
|
|
2917
|
+
"set_budget",
|
|
2918
|
+
"remove_budget",
|
|
2919
|
+
"get_pricing",
|
|
2920
|
+
"set_pricing",
|
|
2921
|
+
"remove_pricing",
|
|
2922
|
+
"get_daily",
|
|
2923
|
+
"get_billing_summary",
|
|
2924
|
+
"get_session_detail",
|
|
2925
|
+
"sync",
|
|
2926
|
+
"search_tools",
|
|
2927
|
+
"describe_tools",
|
|
2928
|
+
"get_goals",
|
|
2929
|
+
"set_goal",
|
|
2930
|
+
"remove_goal",
|
|
2931
|
+
"list_machines",
|
|
2932
|
+
"register_agent",
|
|
2933
|
+
"heartbeat",
|
|
2934
|
+
"set_focus",
|
|
2935
|
+
"list_agents",
|
|
2936
|
+
"send_feedback"
|
|
2937
|
+
];
|
|
2938
|
+
const TOOL_DESCRIPTIONS = {
|
|
2939
|
+
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
2940
|
+
get_sessions: "agent(claude|takumi|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
|
|
2941
|
+
get_top_sessions: "n(10), agent(claude|takumi|codex|gemini) -> top sessions by cost",
|
|
2942
|
+
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
2943
|
+
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
2944
|
+
get_project_breakdown: "no params -> project_name, sessions, cost",
|
|
2945
|
+
get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
|
|
2946
|
+
set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
|
|
2947
|
+
remove_budget: "id -> delete budget",
|
|
2948
|
+
get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
|
|
2949
|
+
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",
|
|
2950
|
+
remove_pricing: "model -> delete pricing row",
|
|
2951
|
+
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
2952
|
+
get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
|
|
2953
|
+
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
2954
|
+
sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
|
|
2955
|
+
search_tools: "query substring -> tool name list",
|
|
2956
|
+
describe_tools: "names[] -> one-line parameter hints",
|
|
2957
|
+
get_goals: "no params -> goal progress summary",
|
|
2958
|
+
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
2959
|
+
remove_goal: "id -> delete goal",
|
|
2960
|
+
register_agent: "name, session_id? -> register agent session",
|
|
2961
|
+
heartbeat: "agent_id -> update last_seen_at",
|
|
2962
|
+
set_focus: "agent_id, project_id? -> set active project context",
|
|
2963
|
+
list_agents: "no params -> registered agent list",
|
|
2964
|
+
send_feedback: "message, email?, category? -> save feedback locally"
|
|
2965
|
+
};
|
|
2966
|
+
const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
2967
|
+
const 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);
|
|
2968
|
+
function fmtSession(s) {
|
|
2969
|
+
const id = String(s["id"] ?? "").slice(0, 8);
|
|
2970
|
+
const agent = String(s["agent"] ?? "");
|
|
2971
|
+
const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
|
|
2972
|
+
const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
|
|
2973
|
+
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
2974
|
+
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
2975
|
+
}
|
|
2976
|
+
function text(text2) {
|
|
2977
|
+
return { content: [{ type: "text", text: text2 }] };
|
|
2978
|
+
}
|
|
2979
|
+
function textError(message) {
|
|
2980
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
2981
|
+
}
|
|
2982
|
+
server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
|
|
2983
|
+
const q = query?.toLowerCase();
|
|
2984
|
+
const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
|
|
2985
|
+
return text(matches.join(", "));
|
|
2986
|
+
});
|
|
2987
|
+
server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
|
|
2988
|
+
const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
|
|
1110
2989
|
`);
|
|
1111
|
-
|
|
1112
|
-
});
|
|
1113
|
-
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 }) => {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
2990
|
+
return text(result);
|
|
2991
|
+
});
|
|
2992
|
+
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 }) => {
|
|
2993
|
+
const resolved = period ?? "today";
|
|
2994
|
+
const s = querySummary(db, resolved, machine);
|
|
2995
|
+
const machineLabel = machine ? ` on ${machine}` : "";
|
|
2996
|
+
return text([
|
|
2997
|
+
`period: ${resolved}${machineLabel}`,
|
|
2998
|
+
`cost: ${fmtUsd(s.total_usd)}`,
|
|
2999
|
+
`sessions: ${s.sessions}`,
|
|
3000
|
+
`requests: ${s.requests.toLocaleString()}`,
|
|
3001
|
+
`tokens: ${fmtTok(s.tokens)}`,
|
|
3002
|
+
`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)`
|
|
3003
|
+
].join(`
|
|
1125
3004
|
`));
|
|
1126
|
-
});
|
|
1127
|
-
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
|
|
1128
|
-
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
|
|
1129
|
-
project: z.string().optional(),
|
|
1130
|
-
machine: z.string().optional(),
|
|
1131
|
-
limit: z.number().int().positive().max(100).optional()
|
|
1132
|
-
}, async ({ agent, project, machine, limit }) => {
|
|
1133
|
-
const sessions = querySessions(db, {
|
|
1134
|
-
agent,
|
|
1135
|
-
project,
|
|
1136
|
-
machine,
|
|
1137
|
-
limit: limit ?? 20
|
|
1138
3005
|
});
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
3006
|
+
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
|
|
3007
|
+
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
|
|
3008
|
+
project: z.string().optional(),
|
|
3009
|
+
machine: z.string().optional(),
|
|
3010
|
+
limit: z.number().int().positive().max(100).optional()
|
|
3011
|
+
}, async ({ agent, project, machine, limit }) => {
|
|
3012
|
+
const sessions = querySessions(db, {
|
|
3013
|
+
agent,
|
|
3014
|
+
project,
|
|
3015
|
+
machine,
|
|
3016
|
+
limit: limit ?? 20
|
|
3017
|
+
});
|
|
3018
|
+
const lines = ["id agent cost tokens project"];
|
|
3019
|
+
for (const session of sessions)
|
|
3020
|
+
lines.push(fmtSession(session));
|
|
3021
|
+
return text(lines.join(`
|
|
1143
3022
|
`));
|
|
1144
|
-
});
|
|
1145
|
-
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
}, async ({ n, agent }) => {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
3023
|
+
});
|
|
3024
|
+
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
3025
|
+
n: z.number().int().positive().max(100).optional(),
|
|
3026
|
+
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional()
|
|
3027
|
+
}, async ({ n, agent }) => {
|
|
3028
|
+
const sessions = queryTopSessions(db, n ?? 10, agent);
|
|
3029
|
+
const lines = ["rank id agent cost tokens project"];
|
|
3030
|
+
sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
|
|
3031
|
+
return text(lines.join(`
|
|
1153
3032
|
`));
|
|
1154
|
-
});
|
|
1155
|
-
server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
3033
|
+
});
|
|
3034
|
+
server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
|
|
3035
|
+
const rows = queryModelBreakdown(db);
|
|
3036
|
+
const lines = ["model reqs tokens cost"];
|
|
3037
|
+
for (const row of rows) {
|
|
3038
|
+
lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
3039
|
+
}
|
|
3040
|
+
return text(lines.join(`
|
|
1162
3041
|
`));
|
|
1163
|
-
});
|
|
1164
|
-
server.tool("get_project_breakdown", "Cost per project. No params.", {}, async () => {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
3042
|
+
});
|
|
3043
|
+
server.tool("get_project_breakdown", "Cost per project. No params.", {}, async () => {
|
|
3044
|
+
const rows = queryProjectBreakdown(db);
|
|
3045
|
+
const lines = ["project sessions tokens cost"];
|
|
3046
|
+
for (const row of rows) {
|
|
3047
|
+
const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
|
|
3048
|
+
lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
3049
|
+
}
|
|
3050
|
+
return text(lines.join(`
|
|
1172
3051
|
`));
|
|
1173
|
-
});
|
|
1174
|
-
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
3052
|
+
});
|
|
3053
|
+
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
|
|
3054
|
+
const budgets = getBudgetStatuses(db);
|
|
3055
|
+
if (budgets.length === 0)
|
|
3056
|
+
return text("No budgets set.");
|
|
3057
|
+
const lines = ["scope period spent limit used% status"];
|
|
3058
|
+
for (const budget of budgets) {
|
|
3059
|
+
const scope = String(budget["project_path"] ?? "global").slice(0, 20);
|
|
3060
|
+
const pct = Number(budget["percent_used"]).toFixed(1);
|
|
3061
|
+
const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
|
|
3062
|
+
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}`);
|
|
3063
|
+
}
|
|
3064
|
+
return text(lines.join(`
|
|
1186
3065
|
`));
|
|
1187
|
-
});
|
|
1188
|
-
server.tool("
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
3066
|
+
});
|
|
3067
|
+
server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
|
|
3068
|
+
period: z.enum(["daily", "weekly", "monthly"]),
|
|
3069
|
+
limit_usd: z.number().positive(),
|
|
3070
|
+
project_path: z.string().optional(),
|
|
3071
|
+
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
|
|
3072
|
+
alert_at_percent: z.number().positive().max(100).optional()
|
|
3073
|
+
}, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
|
|
3074
|
+
const now = new Date().toISOString();
|
|
3075
|
+
const id = randomUUID();
|
|
3076
|
+
upsertBudget(db, {
|
|
3077
|
+
id,
|
|
3078
|
+
project_path: project_path ?? null,
|
|
3079
|
+
agent: agent ?? null,
|
|
3080
|
+
period,
|
|
3081
|
+
limit_usd,
|
|
3082
|
+
alert_at_percent: alert_at_percent ?? 80,
|
|
3083
|
+
created_at: now,
|
|
3084
|
+
updated_at: now
|
|
3085
|
+
});
|
|
3086
|
+
return text(`Budget set: ${id}`);
|
|
3087
|
+
});
|
|
3088
|
+
server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
|
|
3089
|
+
deleteBudget(db, id);
|
|
3090
|
+
return text("Budget removed.");
|
|
3091
|
+
});
|
|
3092
|
+
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
|
|
3093
|
+
const rows = listModelPricing(db);
|
|
3094
|
+
const lines = ["model input output cache-r cache-w cache-1h storage-h"];
|
|
3095
|
+
for (const row of rows) {
|
|
3096
|
+
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)}`);
|
|
3097
|
+
}
|
|
3098
|
+
return text(lines.join(`
|
|
1208
3099
|
`));
|
|
1209
|
-
});
|
|
1210
|
-
server.tool("
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
3100
|
+
});
|
|
3101
|
+
server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
|
|
3102
|
+
model: z.string().min(1),
|
|
3103
|
+
input_per_1m: z.number().nonnegative(),
|
|
3104
|
+
output_per_1m: z.number().nonnegative(),
|
|
3105
|
+
cache_read_per_1m: z.number().nonnegative().optional(),
|
|
3106
|
+
cache_write_per_1m: z.number().nonnegative().optional(),
|
|
3107
|
+
cache_write_1h_per_1m: z.number().nonnegative().optional(),
|
|
3108
|
+
cache_storage_per_1m_hour: z.number().nonnegative().optional()
|
|
3109
|
+
}, async (input) => {
|
|
3110
|
+
const model = input.model.trim();
|
|
3111
|
+
if (!model)
|
|
3112
|
+
return textError("model is required");
|
|
3113
|
+
upsertModelPricing(db, {
|
|
3114
|
+
model,
|
|
3115
|
+
input_per_1m: input.input_per_1m,
|
|
3116
|
+
output_per_1m: input.output_per_1m,
|
|
3117
|
+
cache_read_per_1m: input.cache_read_per_1m ?? 0,
|
|
3118
|
+
cache_write_per_1m: input.cache_write_per_1m ?? 0,
|
|
3119
|
+
cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
|
|
3120
|
+
cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0,
|
|
3121
|
+
updated_at: new Date().toISOString()
|
|
3122
|
+
});
|
|
3123
|
+
return text(`Pricing set: ${model}`);
|
|
3124
|
+
});
|
|
3125
|
+
server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
|
|
3126
|
+
deleteModelPricing(db, model);
|
|
3127
|
+
return text("Pricing removed.");
|
|
3128
|
+
});
|
|
3129
|
+
server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
|
|
3130
|
+
const rows = queryDailyBreakdown(db, days ?? 30);
|
|
3131
|
+
const byDate = new Map;
|
|
3132
|
+
for (const row of rows) {
|
|
3133
|
+
const date = String(row["date"]);
|
|
3134
|
+
const entry = byDate.get(date) ?? { claude: 0, takumi: 0, codex: 0, gemini: 0 };
|
|
3135
|
+
if (row["agent"] === "claude")
|
|
3136
|
+
entry.claude += Number(row["cost_usd"]);
|
|
3137
|
+
else if (row["agent"] === "takumi")
|
|
3138
|
+
entry.takumi += Number(row["cost_usd"]);
|
|
3139
|
+
else if (row["agent"] === "codex")
|
|
3140
|
+
entry.codex += Number(row["cost_usd"]);
|
|
3141
|
+
else if (row["agent"] === "gemini")
|
|
3142
|
+
entry.gemini += Number(row["cost_usd"]);
|
|
3143
|
+
byDate.set(date, entry);
|
|
3144
|
+
}
|
|
3145
|
+
const lines = ["date claude takumi codex gemini total"];
|
|
3146
|
+
for (const [date, costs] of [...byDate.entries()].sort()) {
|
|
3147
|
+
const total = costs.claude + costs.takumi + costs.codex + costs.gemini;
|
|
3148
|
+
lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.takumi).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
|
|
3149
|
+
}
|
|
3150
|
+
return text(lines.join(`
|
|
1226
3151
|
`));
|
|
1227
|
-
});
|
|
1228
|
-
server.tool("
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
const result = await ingestTakumi(db);
|
|
1237
|
-
parts.push(`takumi: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1238
|
-
}
|
|
1239
|
-
if (selected === "all" || selected === "codex") {
|
|
1240
|
-
const result = await ingestCodex(db);
|
|
1241
|
-
parts.push(`codex: ${result["sessions"]} sessions`);
|
|
1242
|
-
}
|
|
1243
|
-
if (selected === "all" || selected === "gemini") {
|
|
1244
|
-
const result = await ingestGemini(db);
|
|
1245
|
-
parts.push(`gemini: ${result["sessions"]} sessions`);
|
|
1246
|
-
}
|
|
1247
|
-
return text(parts.join(`
|
|
1248
|
-
`) || "done");
|
|
1249
|
-
});
|
|
1250
|
-
server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
|
|
1251
|
-
const goals = getGoalStatuses(db);
|
|
1252
|
-
if (goals.length === 0)
|
|
1253
|
-
return text("No goals set.");
|
|
1254
|
-
const lines = ["period scope limit spent used% status"];
|
|
1255
|
-
for (const goal of goals) {
|
|
1256
|
-
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
1257
|
-
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
1258
|
-
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
1259
|
-
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}`);
|
|
1260
|
-
}
|
|
1261
|
-
return text(lines.join(`
|
|
3152
|
+
});
|
|
3153
|
+
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 }) => {
|
|
3154
|
+
const summary = queryBillingSummary(db, period ?? "month");
|
|
3155
|
+
const lines = ["provider billed"];
|
|
3156
|
+
for (const [provider, cost] of Object.entries(summary.by_provider)) {
|
|
3157
|
+
lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
|
|
3158
|
+
}
|
|
3159
|
+
lines.push(`total ${fmtUsd(summary.total_usd)}`);
|
|
3160
|
+
return text(lines.join(`
|
|
1262
3161
|
`));
|
|
1263
|
-
});
|
|
1264
|
-
server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
|
|
1265
|
-
period: z.enum(["day", "week", "month", "year"]),
|
|
1266
|
-
limit_usd: z.number().nonnegative(),
|
|
1267
|
-
project_path: z.string().optional(),
|
|
1268
|
-
agent: z.string().optional()
|
|
1269
|
-
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
1270
|
-
const now = new Date().toISOString();
|
|
1271
|
-
upsertGoal(db, {
|
|
1272
|
-
id: randomUUID(),
|
|
1273
|
-
period,
|
|
1274
|
-
project_path: project_path ?? null,
|
|
1275
|
-
agent: agent ?? null,
|
|
1276
|
-
limit_usd,
|
|
1277
|
-
created_at: now,
|
|
1278
|
-
updated_at: now
|
|
1279
3162
|
});
|
|
1280
|
-
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1294
|
-
|
|
3163
|
+
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
|
|
3164
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
|
|
3165
|
+
if (!session)
|
|
3166
|
+
return textError(`Session not found: ${session_id}`);
|
|
3167
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
|
|
3168
|
+
const lines = [
|
|
3169
|
+
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
3170
|
+
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
3171
|
+
`cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
|
|
3172
|
+
"",
|
|
3173
|
+
"time model input output cache-r cache-5m cache-1h cost"
|
|
3174
|
+
];
|
|
3175
|
+
for (const request of requests) {
|
|
3176
|
+
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"]))}`);
|
|
3177
|
+
}
|
|
3178
|
+
return text(lines.join(`
|
|
3179
|
+
`));
|
|
3180
|
+
});
|
|
3181
|
+
server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
|
|
3182
|
+
const selected = sources ?? "all";
|
|
3183
|
+
const opts = selected === "all" ? {} : { [selected]: true };
|
|
3184
|
+
const result = await syncAll(db, opts);
|
|
3185
|
+
return text(JSON.stringify(result, null, 2));
|
|
3186
|
+
});
|
|
3187
|
+
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 }) => {
|
|
3188
|
+
const p = period ?? "month";
|
|
3189
|
+
const snaps = queryUsageSnapshots(db, { agent });
|
|
3190
|
+
const summary = querySummary(db, p, undefined, true);
|
|
3191
|
+
return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
|
|
3192
|
+
});
|
|
3193
|
+
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 }) => {
|
|
3194
|
+
return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
|
|
3195
|
+
});
|
|
3196
|
+
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 }) => {
|
|
3197
|
+
const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
|
|
3198
|
+
return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
|
|
3199
|
+
});
|
|
3200
|
+
server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
|
|
3201
|
+
const goals = getGoalStatuses(db);
|
|
3202
|
+
if (goals.length === 0)
|
|
3203
|
+
return text("No goals set.");
|
|
3204
|
+
const lines = ["period scope limit spent used% status"];
|
|
3205
|
+
for (const goal of goals) {
|
|
3206
|
+
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
3207
|
+
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
3208
|
+
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
3209
|
+
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}`);
|
|
3210
|
+
}
|
|
3211
|
+
return text(lines.join(`
|
|
3212
|
+
`));
|
|
3213
|
+
});
|
|
3214
|
+
server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
|
|
3215
|
+
period: z.enum(["day", "week", "month", "year"]),
|
|
3216
|
+
limit_usd: z.number().positive(),
|
|
3217
|
+
project_path: z.string().optional(),
|
|
3218
|
+
agent: z.string().optional()
|
|
3219
|
+
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
3220
|
+
const now = new Date().toISOString();
|
|
3221
|
+
upsertGoal(db, {
|
|
3222
|
+
id: randomUUID(),
|
|
3223
|
+
period,
|
|
3224
|
+
project_path: project_path ?? null,
|
|
3225
|
+
agent: agent ?? null,
|
|
3226
|
+
limit_usd,
|
|
3227
|
+
created_at: now,
|
|
3228
|
+
updated_at: now
|
|
3229
|
+
});
|
|
3230
|
+
return text(`Goal set: ${period} $${limit_usd}`);
|
|
3231
|
+
});
|
|
3232
|
+
server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
|
|
3233
|
+
deleteGoal(db, id);
|
|
3234
|
+
return text("Goal removed.");
|
|
3235
|
+
});
|
|
3236
|
+
server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
|
|
3237
|
+
const machines = listMachines(db);
|
|
3238
|
+
if (machines.length === 0)
|
|
3239
|
+
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
3240
|
+
const lines = ["machine sessions requests cost last_active"];
|
|
3241
|
+
for (const m of machines) {
|
|
3242
|
+
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"}`);
|
|
3243
|
+
}
|
|
3244
|
+
lines.push(`
|
|
1295
3245
|
current machine: ${getMachineId()}`);
|
|
1296
|
-
|
|
3246
|
+
return text(lines.join(`
|
|
1297
3247
|
`));
|
|
1298
|
-
});
|
|
1299
|
-
server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
});
|
|
1310
|
-
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
});
|
|
1317
|
-
server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
});
|
|
1324
|
-
server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
|
|
1325
|
-
server.tool("send_feedback", "Send feedback about this service.", {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}, async ({ message, email, category }) => {
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
3248
|
+
});
|
|
3249
|
+
server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
|
|
3250
|
+
const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
|
|
3251
|
+
if (existing) {
|
|
3252
|
+
existing.last_seen_at = new Date().toISOString();
|
|
3253
|
+
return text(JSON.stringify(existing));
|
|
3254
|
+
}
|
|
3255
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
3256
|
+
const agent = { id, name, last_seen_at: new Date().toISOString() };
|
|
3257
|
+
_econAgents.set(id, agent);
|
|
3258
|
+
return text(JSON.stringify(agent));
|
|
3259
|
+
});
|
|
3260
|
+
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
3261
|
+
const agent = _econAgents.get(agent_id);
|
|
3262
|
+
if (!agent)
|
|
3263
|
+
return textError("Agent not found");
|
|
3264
|
+
agent.last_seen_at = new Date().toISOString();
|
|
3265
|
+
return text(`\u2665 ${agent.name}`);
|
|
3266
|
+
});
|
|
3267
|
+
server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
|
|
3268
|
+
const agent = _econAgents.get(agent_id);
|
|
3269
|
+
if (!agent)
|
|
3270
|
+
return textError("Agent not found");
|
|
3271
|
+
agent.project_id = project_id ?? undefined;
|
|
3272
|
+
return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
|
|
3273
|
+
});
|
|
3274
|
+
server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
|
|
3275
|
+
server.tool("send_feedback", "Send feedback about this service.", {
|
|
3276
|
+
message: z.string(),
|
|
3277
|
+
email: z.string().optional(),
|
|
3278
|
+
category: z.enum(["bug", "feature", "general"]).optional()
|
|
3279
|
+
}, async ({ message, email, category }) => {
|
|
3280
|
+
try {
|
|
3281
|
+
db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
|
|
3282
|
+
return text("Feedback saved. Thank you!");
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
return textError(String(error));
|
|
3285
|
+
}
|
|
3286
|
+
});
|
|
3287
|
+
registerCloudTools(server, MCP_NAME, {
|
|
3288
|
+
dbPath: getDbPath(),
|
|
3289
|
+
migrations: PG_MIGRATIONS
|
|
3290
|
+
});
|
|
3291
|
+
return server;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
// src/mcp/http.ts
|
|
3295
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3296
|
+
function isHttpMode(argv = process.argv.slice(2)) {
|
|
3297
|
+
return argv.includes("--http") || process.env["MCP_HTTP"] === "1";
|
|
3298
|
+
}
|
|
3299
|
+
function resolveHttpPort(argv = process.argv.slice(2)) {
|
|
3300
|
+
for (let i = 0;i < argv.length; i++) {
|
|
3301
|
+
const arg = argv[i];
|
|
3302
|
+
if (arg === "--port" || arg === "-p") {
|
|
3303
|
+
const raw = argv[i + 1];
|
|
3304
|
+
if (!raw)
|
|
3305
|
+
throw new Error(`Invalid port: ${raw ?? ""}`);
|
|
3306
|
+
return parsePort(raw, "port");
|
|
3307
|
+
}
|
|
1335
3308
|
}
|
|
3309
|
+
const fromEnv = process.env["MCP_HTTP_PORT"];
|
|
3310
|
+
if (fromEnv)
|
|
3311
|
+
return parsePort(fromEnv, "MCP_HTTP_PORT");
|
|
3312
|
+
return DEFAULT_MCP_HTTP_PORT;
|
|
3313
|
+
}
|
|
3314
|
+
function parsePort(raw, label) {
|
|
3315
|
+
const value = Number(raw);
|
|
3316
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
3317
|
+
throw new Error(`Invalid ${label}: ${raw}`);
|
|
3318
|
+
}
|
|
3319
|
+
return value;
|
|
3320
|
+
}
|
|
3321
|
+
async function handleMcpHttpRequest(req) {
|
|
3322
|
+
const url = new URL(req.url);
|
|
3323
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
3324
|
+
return Response.json({ status: "ok", name: MCP_NAME });
|
|
3325
|
+
}
|
|
3326
|
+
if (url.pathname === "/mcp") {
|
|
3327
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
3328
|
+
sessionIdGenerator: undefined
|
|
3329
|
+
});
|
|
3330
|
+
const server = buildServer();
|
|
3331
|
+
await server.connect(transport);
|
|
3332
|
+
return transport.handleRequest(req);
|
|
3333
|
+
}
|
|
3334
|
+
return new Response("Not Found", { status: 404 });
|
|
3335
|
+
}
|
|
3336
|
+
function startHttpServer(options = {}) {
|
|
3337
|
+
const port = options.port ?? DEFAULT_MCP_HTTP_PORT;
|
|
3338
|
+
const hostname2 = options.hostname ?? "127.0.0.1";
|
|
3339
|
+
const log = options.log ?? console.error;
|
|
3340
|
+
const server = Bun.serve({
|
|
3341
|
+
port,
|
|
3342
|
+
hostname: hostname2,
|
|
3343
|
+
fetch: handleMcpHttpRequest
|
|
3344
|
+
});
|
|
3345
|
+
const address = `http://${hostname2}:${server.port}`;
|
|
3346
|
+
log(`${MCP_NAME}-mcp HTTP listening on ${address}/mcp (health: ${address}/health)`);
|
|
3347
|
+
return server;
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// src/mcp/index.ts
|
|
3351
|
+
function printHelp() {
|
|
3352
|
+
console.log(`Usage: economy-mcp [options]
|
|
3353
|
+
|
|
3354
|
+
Runs the ${packageMetadata.name} MCP server (stdio by default).
|
|
3355
|
+
|
|
3356
|
+
Options:
|
|
3357
|
+
--http Serve MCP over Streamable HTTP on 127.0.0.1
|
|
3358
|
+
-p, --port <port> HTTP port (default: MCP_HTTP_PORT or 8815)
|
|
3359
|
+
-V, --version output the version number
|
|
3360
|
+
-h, --help display help for command
|
|
3361
|
+
|
|
3362
|
+
Environment:
|
|
3363
|
+
MCP_HTTP=1 Enable HTTP mode
|
|
3364
|
+
MCP_HTTP_PORT Override default HTTP port (8815)`);
|
|
3365
|
+
}
|
|
3366
|
+
var args = process.argv.slice(2);
|
|
3367
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3368
|
+
printHelp();
|
|
3369
|
+
process.exit(0);
|
|
3370
|
+
}
|
|
3371
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
3372
|
+
console.log(packageMetadata.version);
|
|
3373
|
+
process.exit(0);
|
|
3374
|
+
}
|
|
3375
|
+
async function main() {
|
|
3376
|
+
if (isHttpMode(args)) {
|
|
3377
|
+
startHttpServer({ port: resolveHttpPort(args) });
|
|
3378
|
+
await new Promise(() => {});
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
const server = buildServer();
|
|
3382
|
+
const transport = new StdioServerTransport;
|
|
3383
|
+
await server.connect(transport);
|
|
3384
|
+
}
|
|
3385
|
+
main().catch((error) => {
|
|
3386
|
+
console.error("MCP server error:", error);
|
|
3387
|
+
process.exit(1);
|
|
1336
3388
|
});
|
|
1337
|
-
var transport = new StdioServerTransport;
|
|
1338
|
-
registerCloudTools(server, "economy", {
|
|
1339
|
-
dbPath: getDbPath(),
|
|
1340
|
-
migrations: PG_MIGRATIONS
|
|
1341
|
-
});
|
|
1342
|
-
await server.connect(transport);
|