@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/server/index.js
CHANGED
|
@@ -29,99 +29,486 @@ __export(exports_pricing, {
|
|
|
29
29
|
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
30
30
|
});
|
|
31
31
|
function normalizeModelName(raw) {
|
|
32
|
-
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;
|
|
33
68
|
}
|
|
34
69
|
function ensurePricingSeeded(db) {
|
|
35
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);
|
|
36
169
|
}
|
|
37
170
|
function getPricingFromDb(db, model) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
45
|
-
cacheWritePer1M: row.cache_write_per_1m
|
|
46
|
-
};
|
|
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);
|
|
47
177
|
}
|
|
48
178
|
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
};
|
|
55
195
|
}
|
|
56
196
|
function getPricing(model) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
197
|
+
if (isFreeModel(model))
|
|
198
|
+
return FREE_PRICING;
|
|
199
|
+
return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
|
|
200
|
+
}
|
|
201
|
+
function isFreeModel(model) {
|
|
202
|
+
return model.trim().toLowerCase().endsWith(":free");
|
|
65
203
|
}
|
|
66
|
-
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
204
|
+
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
67
205
|
const pricing = getPricing(model);
|
|
68
206
|
if (!pricing)
|
|
69
207
|
return 0;
|
|
70
|
-
return (
|
|
208
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
71
209
|
}
|
|
72
|
-
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) {
|
|
73
211
|
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
74
212
|
if (!pricing)
|
|
75
213
|
return 0;
|
|
76
|
-
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;
|
|
77
234
|
}
|
|
78
|
-
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;
|
|
79
236
|
var init_pricing = __esm(() => {
|
|
80
237
|
init_database();
|
|
81
238
|
DEFAULT_PRICING = {
|
|
82
|
-
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
83
|
-
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
84
|
-
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
85
|
-
"claude-
|
|
86
|
-
"claude-
|
|
87
|
-
"claude-
|
|
88
|
-
"claude-
|
|
89
|
-
"claude-
|
|
90
|
-
"claude-3-
|
|
91
|
-
"claude-
|
|
92
|
-
"claude-3-haiku": { inputPer1M: 0.
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"gemini-
|
|
96
|
-
"gemini-
|
|
97
|
-
"gemini-1
|
|
98
|
-
"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 },
|
|
99
270
|
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
100
271
|
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
101
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 },
|
|
102
330
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
103
|
-
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
104
331
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
105
332
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
106
333
|
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
107
334
|
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
108
|
-
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
109
|
-
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
110
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
111
335
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
"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 },
|
|
115
338
|
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
116
|
-
"qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
117
339
|
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
118
340
|
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
119
341
|
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
120
|
-
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
121
|
-
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
122
342
|
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
123
343
|
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
124
|
-
"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
|
+
}
|
|
125
512
|
};
|
|
126
513
|
});
|
|
127
514
|
|
|
@@ -190,6 +577,8 @@ function initSchema(db) {
|
|
|
190
577
|
output_tokens INTEGER DEFAULT 0,
|
|
191
578
|
cache_read_tokens INTEGER DEFAULT 0,
|
|
192
579
|
cache_create_tokens INTEGER DEFAULT 0,
|
|
580
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
581
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
193
582
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
194
583
|
duration_ms INTEGER DEFAULT 0,
|
|
195
584
|
timestamp TEXT NOT NULL,
|
|
@@ -260,6 +649,8 @@ function initSchema(db) {
|
|
|
260
649
|
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
261
650
|
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
262
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,
|
|
263
654
|
updated_at TEXT NOT NULL
|
|
264
655
|
);
|
|
265
656
|
|
|
@@ -284,12 +675,100 @@ function initSchema(db) {
|
|
|
284
675
|
|
|
285
676
|
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
286
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);
|
|
287
728
|
`);
|
|
288
729
|
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
289
730
|
if (!cols.some((c) => c.name === "machine_id")) {
|
|
290
731
|
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
291
732
|
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
292
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
|
+
}
|
|
293
772
|
db.exec(`
|
|
294
773
|
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
295
774
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
@@ -328,21 +807,24 @@ function sessionPeriodWhere(period) {
|
|
|
328
807
|
}
|
|
329
808
|
}
|
|
330
809
|
function upsertRequest(db, req) {
|
|
810
|
+
const now = req.updated_at ?? new Date().toISOString();
|
|
331
811
|
db.prepare(`
|
|
332
812
|
INSERT OR REPLACE INTO requests
|
|
333
813
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
334
|
-
cache_read_tokens, cache_create_tokens,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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 ?? "");
|
|
338
819
|
}
|
|
339
820
|
function upsertSession(db, session) {
|
|
821
|
+
const now = session.updated_at ?? new Date().toISOString();
|
|
340
822
|
db.prepare(`
|
|
341
823
|
INSERT OR REPLACE INTO sessions
|
|
342
824
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
343
|
-
total_cost_usd, total_tokens, request_count, machine_id)
|
|
344
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
345
|
-
`).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 ?? "");
|
|
346
828
|
}
|
|
347
829
|
function rollupSession(db, sessionId) {
|
|
348
830
|
db.prepare(`
|
|
@@ -394,10 +876,10 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
394
876
|
}
|
|
395
877
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
396
878
|
}
|
|
397
|
-
function querySummary(db, period, machine) {
|
|
879
|
+
function querySummary(db, period, machine, allMachines = false) {
|
|
398
880
|
const rWhere = periodWhere(period);
|
|
399
881
|
const sWhere = sessionPeriodWhere(period);
|
|
400
|
-
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
882
|
+
const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
401
883
|
const r = db.prepare(`
|
|
402
884
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
403
885
|
COUNT(*) as requests,
|
|
@@ -600,6 +1082,26 @@ function getIngestState(db, source, key) {
|
|
|
600
1082
|
function setIngestState(db, source, key, value) {
|
|
601
1083
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
602
1084
|
}
|
|
1085
|
+
function upsertBillingDaily(db, row) {
|
|
1086
|
+
db.prepare(`
|
|
1087
|
+
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
1088
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1089
|
+
`).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
|
|
1090
|
+
}
|
|
1091
|
+
function clearBillingRange(db, provider, fromDate, toDate) {
|
|
1092
|
+
db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
|
|
1093
|
+
}
|
|
1094
|
+
function queryBillingSummary(db, period) {
|
|
1095
|
+
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";
|
|
1096
|
+
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
1097
|
+
const by_provider = {};
|
|
1098
|
+
let total = 0;
|
|
1099
|
+
for (const r of rows) {
|
|
1100
|
+
by_provider[r.provider] = r.cost;
|
|
1101
|
+
total += r.cost;
|
|
1102
|
+
}
|
|
1103
|
+
return { total_usd: total, by_provider };
|
|
1104
|
+
}
|
|
603
1105
|
function listMachines(db) {
|
|
604
1106
|
return db.prepare(`
|
|
605
1107
|
SELECT
|
|
@@ -617,9 +1119,9 @@ function listMachines(db) {
|
|
|
617
1119
|
function upsertModelPricing(db, p) {
|
|
618
1120
|
db.prepare(`
|
|
619
1121
|
INSERT OR REPLACE INTO model_pricing
|
|
620
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
|
|
621
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
622
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
|
|
1122
|
+
(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)
|
|
1123
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1124
|
+
`).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);
|
|
623
1125
|
}
|
|
624
1126
|
function getModelPricing(db, model) {
|
|
625
1127
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
@@ -642,14 +1144,644 @@ function seedModelPricing(db, defaults) {
|
|
|
642
1144
|
output_per_1m: p.outputPer1M,
|
|
643
1145
|
cache_read_per_1m: p.cacheReadPer1M,
|
|
644
1146
|
cache_write_per_1m: p.cacheWritePer1M,
|
|
1147
|
+
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1148
|
+
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
645
1149
|
updated_at: now
|
|
646
1150
|
});
|
|
647
1151
|
}
|
|
648
1152
|
}
|
|
1153
|
+
function upsertSubscription(db, sub) {
|
|
1154
|
+
db.prepare(`
|
|
1155
|
+
INSERT OR REPLACE INTO subscriptions
|
|
1156
|
+
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
1157
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1158
|
+
`).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);
|
|
1159
|
+
}
|
|
1160
|
+
function upsertUsageSnapshot(db, snap) {
|
|
1161
|
+
const now = snap.updated_at ?? new Date().toISOString();
|
|
1162
|
+
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
1163
|
+
db.prepare(`
|
|
1164
|
+
INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
|
|
1165
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1166
|
+
`).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
|
|
1167
|
+
}
|
|
1168
|
+
function queryUsageSnapshots(db, opts = {}) {
|
|
1169
|
+
const conditions = [];
|
|
1170
|
+
const params = [];
|
|
1171
|
+
if (opts.agent) {
|
|
1172
|
+
conditions.push("agent = ?");
|
|
1173
|
+
params.push(opts.agent);
|
|
1174
|
+
}
|
|
1175
|
+
if (opts.date) {
|
|
1176
|
+
conditions.push("date = ?");
|
|
1177
|
+
params.push(opts.date);
|
|
1178
|
+
}
|
|
1179
|
+
if (opts.since) {
|
|
1180
|
+
conditions.push("date >= ?");
|
|
1181
|
+
params.push(opts.since);
|
|
1182
|
+
}
|
|
1183
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1184
|
+
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1185
|
+
}
|
|
1186
|
+
function listMachineRegistry(db) {
|
|
1187
|
+
return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
|
|
1188
|
+
}
|
|
1189
|
+
function dedupeRequests(db) {
|
|
1190
|
+
const dupes = db.prepare(`
|
|
1191
|
+
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1192
|
+
FROM requests
|
|
1193
|
+
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1194
|
+
GROUP BY source_request_id, agent
|
|
1195
|
+
HAVING cnt > 1
|
|
1196
|
+
`).all();
|
|
1197
|
+
let removed = 0;
|
|
1198
|
+
for (const row of dupes) {
|
|
1199
|
+
const result = db.prepare(`
|
|
1200
|
+
DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
|
|
1201
|
+
`).run(row.source_request_id, row.agent, row.keep_id);
|
|
1202
|
+
removed += result.changes;
|
|
1203
|
+
}
|
|
1204
|
+
return removed;
|
|
1205
|
+
}
|
|
649
1206
|
var init_database = () => {};
|
|
650
1207
|
|
|
1208
|
+
// src/db/pg-migrations.ts
|
|
1209
|
+
var exports_pg_migrations = {};
|
|
1210
|
+
__export(exports_pg_migrations, {
|
|
1211
|
+
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1212
|
+
});
|
|
1213
|
+
var PG_MIGRATIONS;
|
|
1214
|
+
var init_pg_migrations = __esm(() => {
|
|
1215
|
+
PG_MIGRATIONS = [
|
|
1216
|
+
`CREATE TABLE IF NOT EXISTS requests (
|
|
1217
|
+
id TEXT PRIMARY KEY,
|
|
1218
|
+
agent TEXT NOT NULL,
|
|
1219
|
+
session_id TEXT NOT NULL,
|
|
1220
|
+
model TEXT NOT NULL,
|
|
1221
|
+
input_tokens INTEGER DEFAULT 0,
|
|
1222
|
+
output_tokens INTEGER DEFAULT 0,
|
|
1223
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
1224
|
+
cache_create_tokens INTEGER DEFAULT 0,
|
|
1225
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
1226
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
1227
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1228
|
+
duration_ms INTEGER DEFAULT 0,
|
|
1229
|
+
timestamp TEXT NOT NULL,
|
|
1230
|
+
source_request_id TEXT,
|
|
1231
|
+
machine_id TEXT DEFAULT ''
|
|
1232
|
+
)`,
|
|
1233
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1234
|
+
id TEXT PRIMARY KEY,
|
|
1235
|
+
agent TEXT NOT NULL,
|
|
1236
|
+
project_path TEXT DEFAULT '',
|
|
1237
|
+
project_name TEXT DEFAULT '',
|
|
1238
|
+
started_at TEXT NOT NULL,
|
|
1239
|
+
ended_at TEXT,
|
|
1240
|
+
total_cost_usd REAL DEFAULT 0,
|
|
1241
|
+
total_tokens INTEGER DEFAULT 0,
|
|
1242
|
+
request_count INTEGER DEFAULT 0,
|
|
1243
|
+
machine_id TEXT DEFAULT ''
|
|
1244
|
+
)`,
|
|
1245
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
1246
|
+
id TEXT PRIMARY KEY,
|
|
1247
|
+
path TEXT UNIQUE NOT NULL,
|
|
1248
|
+
name TEXT NOT NULL,
|
|
1249
|
+
description TEXT,
|
|
1250
|
+
tags TEXT DEFAULT '[]',
|
|
1251
|
+
created_at TEXT NOT NULL
|
|
1252
|
+
)`,
|
|
1253
|
+
`CREATE TABLE IF NOT EXISTS budgets (
|
|
1254
|
+
id TEXT PRIMARY KEY,
|
|
1255
|
+
project_path TEXT,
|
|
1256
|
+
agent TEXT,
|
|
1257
|
+
period TEXT NOT NULL,
|
|
1258
|
+
limit_usd REAL NOT NULL,
|
|
1259
|
+
alert_at_percent INTEGER DEFAULT 80,
|
|
1260
|
+
created_at TEXT NOT NULL,
|
|
1261
|
+
updated_at TEXT NOT NULL
|
|
1262
|
+
)`,
|
|
1263
|
+
`CREATE TABLE IF NOT EXISTS goals (
|
|
1264
|
+
id TEXT PRIMARY KEY,
|
|
1265
|
+
period TEXT NOT NULL,
|
|
1266
|
+
project_path TEXT,
|
|
1267
|
+
agent TEXT,
|
|
1268
|
+
limit_usd REAL NOT NULL,
|
|
1269
|
+
created_at TEXT NOT NULL,
|
|
1270
|
+
updated_at TEXT NOT NULL
|
|
1271
|
+
)`,
|
|
1272
|
+
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
1273
|
+
source TEXT NOT NULL,
|
|
1274
|
+
key TEXT NOT NULL,
|
|
1275
|
+
value TEXT NOT NULL,
|
|
1276
|
+
PRIMARY KEY (source, key)
|
|
1277
|
+
)`,
|
|
1278
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1279
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1280
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1281
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1282
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1283
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1284
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1285
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1286
|
+
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
1287
|
+
model TEXT PRIMARY KEY,
|
|
1288
|
+
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
1289
|
+
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
1290
|
+
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
1291
|
+
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1292
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
1293
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
1294
|
+
updated_at TEXT NOT NULL
|
|
1295
|
+
)`,
|
|
1296
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
|
|
1297
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
|
|
1298
|
+
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
|
|
1299
|
+
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
|
|
1300
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1301
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1302
|
+
message TEXT NOT NULL,
|
|
1303
|
+
email TEXT,
|
|
1304
|
+
category TEXT DEFAULT 'general',
|
|
1305
|
+
version TEXT,
|
|
1306
|
+
machine_id TEXT,
|
|
1307
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1308
|
+
)`,
|
|
1309
|
+
`CREATE TABLE IF NOT EXISTS billing_daily (
|
|
1310
|
+
date TEXT NOT NULL,
|
|
1311
|
+
provider TEXT NOT NULL,
|
|
1312
|
+
description TEXT DEFAULT '',
|
|
1313
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1314
|
+
updated_at TEXT NOT NULL,
|
|
1315
|
+
PRIMARY KEY (date, provider, description)
|
|
1316
|
+
)`,
|
|
1317
|
+
`CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
|
|
1318
|
+
`CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
|
|
1319
|
+
`CREATE TABLE IF NOT EXISTS subscriptions (
|
|
1320
|
+
id TEXT PRIMARY KEY,
|
|
1321
|
+
agent TEXT,
|
|
1322
|
+
provider TEXT NOT NULL,
|
|
1323
|
+
plan TEXT NOT NULL,
|
|
1324
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1325
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
1326
|
+
billing_cycle_start TEXT,
|
|
1327
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
1328
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
1329
|
+
created_at TEXT NOT NULL,
|
|
1330
|
+
updated_at TEXT NOT NULL
|
|
1331
|
+
)`,
|
|
1332
|
+
`CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
1333
|
+
id TEXT PRIMARY KEY,
|
|
1334
|
+
agent TEXT NOT NULL,
|
|
1335
|
+
date TEXT NOT NULL,
|
|
1336
|
+
metric TEXT NOT NULL,
|
|
1337
|
+
value REAL NOT NULL DEFAULT 0,
|
|
1338
|
+
unit TEXT DEFAULT '',
|
|
1339
|
+
machine_id TEXT DEFAULT '',
|
|
1340
|
+
updated_at TEXT NOT NULL
|
|
1341
|
+
)`,
|
|
1342
|
+
`CREATE TABLE IF NOT EXISTS savings_daily (
|
|
1343
|
+
date TEXT NOT NULL,
|
|
1344
|
+
agent TEXT DEFAULT '',
|
|
1345
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
1346
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1347
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
1348
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
1349
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
1350
|
+
updated_at TEXT NOT NULL,
|
|
1351
|
+
PRIMARY KEY (date, agent)
|
|
1352
|
+
)`,
|
|
1353
|
+
`CREATE TABLE IF NOT EXISTS machines (
|
|
1354
|
+
machine_id TEXT PRIMARY KEY,
|
|
1355
|
+
hostname TEXT NOT NULL,
|
|
1356
|
+
last_seen_at TEXT,
|
|
1357
|
+
last_push_at TEXT,
|
|
1358
|
+
last_pull_at TEXT,
|
|
1359
|
+
economy_version TEXT,
|
|
1360
|
+
updated_at TEXT NOT NULL
|
|
1361
|
+
)`,
|
|
1362
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
1363
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1364
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1365
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1366
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1367
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1368
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1369
|
+
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
1370
|
+
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`
|
|
1371
|
+
];
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
// src/ingest/billing.ts
|
|
1375
|
+
var exports_billing = {};
|
|
1376
|
+
__export(exports_billing, {
|
|
1377
|
+
syncOpenAIBilling: () => syncOpenAIBilling,
|
|
1378
|
+
syncGeminiBilling: () => syncGeminiBilling,
|
|
1379
|
+
syncAnthropicBilling: () => syncAnthropicBilling
|
|
1380
|
+
});
|
|
1381
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
1382
|
+
function getAnthropicAdminKey() {
|
|
1383
|
+
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
1384
|
+
}
|
|
1385
|
+
function getOpenAIAdminKey() {
|
|
1386
|
+
return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
|
|
1387
|
+
}
|
|
1388
|
+
function getGeminiBillingExportPath() {
|
|
1389
|
+
return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
|
|
1390
|
+
}
|
|
1391
|
+
function toISODate(d) {
|
|
1392
|
+
return d.toISOString().substring(0, 10);
|
|
1393
|
+
}
|
|
1394
|
+
function parseDate(value) {
|
|
1395
|
+
if (typeof value !== "string" || !value.trim())
|
|
1396
|
+
return null;
|
|
1397
|
+
const d = new Date(value);
|
|
1398
|
+
if (Number.isNaN(d.getTime()))
|
|
1399
|
+
return value.substring(0, 10);
|
|
1400
|
+
return toISODate(d);
|
|
1401
|
+
}
|
|
1402
|
+
function parseCsv(content) {
|
|
1403
|
+
const lines = content.split(/\r?\n/).filter((line) => line.trim());
|
|
1404
|
+
if (lines.length < 2)
|
|
1405
|
+
return [];
|
|
1406
|
+
const headers = parseCsvLine(lines[0]).map((h) => h.trim());
|
|
1407
|
+
return lines.slice(1).map((line) => {
|
|
1408
|
+
const values = parseCsvLine(line);
|
|
1409
|
+
return Object.fromEntries(headers.map((header, i) => [header, values[i]?.trim() ?? ""]));
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
function parseCsvLine(line) {
|
|
1413
|
+
const values = [];
|
|
1414
|
+
let value = "";
|
|
1415
|
+
let quoted = false;
|
|
1416
|
+
for (let i = 0;i < line.length; i++) {
|
|
1417
|
+
const char = line[i];
|
|
1418
|
+
if (char === '"') {
|
|
1419
|
+
if (quoted && line[i + 1] === '"') {
|
|
1420
|
+
value += '"';
|
|
1421
|
+
i++;
|
|
1422
|
+
} else {
|
|
1423
|
+
quoted = !quoted;
|
|
1424
|
+
}
|
|
1425
|
+
} else if (char === "," && !quoted) {
|
|
1426
|
+
values.push(value);
|
|
1427
|
+
value = "";
|
|
1428
|
+
} else {
|
|
1429
|
+
value += char;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
values.push(value);
|
|
1433
|
+
return values;
|
|
1434
|
+
}
|
|
1435
|
+
function parseBillingRows(content) {
|
|
1436
|
+
const trimmed = content.trim();
|
|
1437
|
+
if (!trimmed)
|
|
1438
|
+
return [];
|
|
1439
|
+
try {
|
|
1440
|
+
const parsed = JSON.parse(trimmed);
|
|
1441
|
+
if (Array.isArray(parsed))
|
|
1442
|
+
return parsed;
|
|
1443
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed["rows"])) {
|
|
1444
|
+
return parsed["rows"];
|
|
1445
|
+
}
|
|
1446
|
+
} catch {}
|
|
1447
|
+
const jsonlRows = [];
|
|
1448
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
1449
|
+
try {
|
|
1450
|
+
const parsed = JSON.parse(line);
|
|
1451
|
+
if (parsed && typeof parsed === "object")
|
|
1452
|
+
jsonlRows.push(parsed);
|
|
1453
|
+
} catch {
|
|
1454
|
+
jsonlRows.length = 0;
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (jsonlRows.length > 0)
|
|
1459
|
+
return jsonlRows;
|
|
1460
|
+
return parseCsv(content);
|
|
1461
|
+
}
|
|
1462
|
+
async function syncAnthropicBilling(db, opts = {}) {
|
|
1463
|
+
const key = getAnthropicAdminKey();
|
|
1464
|
+
if (!key)
|
|
1465
|
+
throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
|
|
1466
|
+
const now = new Date;
|
|
1467
|
+
const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
|
|
1468
|
+
const days = opts.days ?? 31;
|
|
1469
|
+
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
1470
|
+
const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
|
|
1471
|
+
const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
|
|
1472
|
+
let totalUsd = 0;
|
|
1473
|
+
const buckets = [];
|
|
1474
|
+
let nextPage;
|
|
1475
|
+
do {
|
|
1476
|
+
const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
|
|
1477
|
+
url.searchParams.set("starting_at", startIso);
|
|
1478
|
+
url.searchParams.set("ending_at", endIso);
|
|
1479
|
+
url.searchParams.set("bucket_width", "1d");
|
|
1480
|
+
url.searchParams.set("limit", "31");
|
|
1481
|
+
url.searchParams.append("group_by[]", "description");
|
|
1482
|
+
if (nextPage)
|
|
1483
|
+
url.searchParams.set("page", nextPage);
|
|
1484
|
+
const res = await fetch(url.toString(), {
|
|
1485
|
+
headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
|
|
1486
|
+
});
|
|
1487
|
+
const data = await res.json();
|
|
1488
|
+
if (data.error)
|
|
1489
|
+
throw new Error(`Anthropic API: ${data.error.message}`);
|
|
1490
|
+
if (data.data)
|
|
1491
|
+
buckets.push(...data.data);
|
|
1492
|
+
nextPage = data.has_more ? data.next_page : undefined;
|
|
1493
|
+
} while (nextPage);
|
|
1494
|
+
const fromDateStr = toISODate(start);
|
|
1495
|
+
const toDateStr = toISODate(new Date(end.getTime() - 1000));
|
|
1496
|
+
clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
|
|
1497
|
+
const updatedAt = new Date().toISOString();
|
|
1498
|
+
for (const bucket of buckets) {
|
|
1499
|
+
const date = bucket.starting_at.substring(0, 10);
|
|
1500
|
+
for (const r of bucket.results) {
|
|
1501
|
+
const usd = Number(r.amount) / 100;
|
|
1502
|
+
if (usd === 0)
|
|
1503
|
+
continue;
|
|
1504
|
+
const desc = (r.description ?? "unknown").substring(0, 200);
|
|
1505
|
+
upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
|
|
1506
|
+
totalUsd += usd;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return { days: buckets.length, totalUsd };
|
|
1510
|
+
}
|
|
1511
|
+
async function syncOpenAIBilling(db, opts = {}) {
|
|
1512
|
+
const key = getOpenAIAdminKey();
|
|
1513
|
+
if (!key)
|
|
1514
|
+
throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
|
|
1515
|
+
const now = new Date;
|
|
1516
|
+
const end = opts.toDate ? new Date(opts.toDate) : now;
|
|
1517
|
+
const days = opts.days ?? 31;
|
|
1518
|
+
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
1519
|
+
const startSec = Math.floor(start.getTime() / 1000);
|
|
1520
|
+
const endSec = Math.floor(end.getTime() / 1000);
|
|
1521
|
+
let totalUsd = 0;
|
|
1522
|
+
const buckets = [];
|
|
1523
|
+
let nextPage;
|
|
1524
|
+
do {
|
|
1525
|
+
const url = new URL("https://api.openai.com/v1/organization/costs");
|
|
1526
|
+
url.searchParams.set("start_time", String(startSec));
|
|
1527
|
+
url.searchParams.set("end_time", String(endSec));
|
|
1528
|
+
url.searchParams.set("bucket_width", "1d");
|
|
1529
|
+
url.searchParams.set("limit", "31");
|
|
1530
|
+
url.searchParams.append("group_by[]", "line_item");
|
|
1531
|
+
if (nextPage)
|
|
1532
|
+
url.searchParams.set("page", nextPage);
|
|
1533
|
+
const res = await fetch(url.toString(), {
|
|
1534
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
1535
|
+
});
|
|
1536
|
+
const data = await res.json();
|
|
1537
|
+
if (data.error)
|
|
1538
|
+
throw new Error(`OpenAI API: ${data.error.message}`);
|
|
1539
|
+
if (data.data)
|
|
1540
|
+
buckets.push(...data.data);
|
|
1541
|
+
nextPage = data.has_more ? data.next_page : undefined;
|
|
1542
|
+
} while (nextPage);
|
|
1543
|
+
const fromDateStr = toISODate(start);
|
|
1544
|
+
const toDateStr = toISODate(new Date(end.getTime() - 1000));
|
|
1545
|
+
clearBillingRange(db, "openai", fromDateStr, toDateStr);
|
|
1546
|
+
const updatedAt = new Date().toISOString();
|
|
1547
|
+
for (const bucket of buckets) {
|
|
1548
|
+
const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
|
|
1549
|
+
for (const r of bucket.results) {
|
|
1550
|
+
const usd = Number(r.amount?.value ?? 0);
|
|
1551
|
+
if (usd === 0)
|
|
1552
|
+
continue;
|
|
1553
|
+
const desc = (r.line_item ?? "unknown").substring(0, 200);
|
|
1554
|
+
upsertBillingDaily(db, { date, provider: "openai", description: desc, cost_usd: usd, updated_at: updatedAt });
|
|
1555
|
+
totalUsd += usd;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return { days: buckets.length, totalUsd };
|
|
1559
|
+
}
|
|
1560
|
+
async function syncGeminiBilling(db, opts = {}) {
|
|
1561
|
+
const exportPath = getGeminiBillingExportPath();
|
|
1562
|
+
if (!exportPath) {
|
|
1563
|
+
return {
|
|
1564
|
+
days: 0,
|
|
1565
|
+
totalUsd: 0,
|
|
1566
|
+
skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH, HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH, or GEMINI_BILLING_EXPORT_PATH)"
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const now = new Date;
|
|
1570
|
+
const end = opts.toDate ? new Date(opts.toDate) : now;
|
|
1571
|
+
const days = opts.days ?? 31;
|
|
1572
|
+
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
1573
|
+
const fromDateStr = toISODate(start);
|
|
1574
|
+
const toDateStr = toISODate(end);
|
|
1575
|
+
const rows = parseBillingRows(readFileSync10(exportPath, "utf-8"));
|
|
1576
|
+
clearBillingRange(db, "gemini", fromDateStr, toDateStr);
|
|
1577
|
+
const updatedAt = new Date().toISOString();
|
|
1578
|
+
let totalUsd = 0;
|
|
1579
|
+
const seenDays = new Set;
|
|
1580
|
+
for (const row of rows) {
|
|
1581
|
+
const date = parseDate(row["date"] ?? row["usage_start_time"] ?? row["start_time"] ?? row["invoice.month"]);
|
|
1582
|
+
if (!date || date < fromDateStr || date > toDateStr)
|
|
1583
|
+
continue;
|
|
1584
|
+
const rawCost = row["cost_usd"] ?? row["costUsd"] ?? row["cost"] ?? row["amount"];
|
|
1585
|
+
const costUsd = Number(rawCost);
|
|
1586
|
+
if (!Number.isFinite(costUsd) || costUsd === 0)
|
|
1587
|
+
continue;
|
|
1588
|
+
const service = row["service.description"] ?? row["service"] ?? row["provider"] ?? "";
|
|
1589
|
+
const sku = row["sku.description"] ?? row["sku"] ?? row["description"] ?? "Gemini API";
|
|
1590
|
+
const description = `${String(service || "Google AI")}: ${String(sku)}`.substring(0, 200);
|
|
1591
|
+
upsertBillingDaily(db, { date, provider: "gemini", description, cost_usd: costUsd, updated_at: updatedAt });
|
|
1592
|
+
totalUsd += costUsd;
|
|
1593
|
+
seenDays.add(date);
|
|
1594
|
+
}
|
|
1595
|
+
return { days: seenDays.size, totalUsd };
|
|
1596
|
+
}
|
|
1597
|
+
var init_billing = __esm(() => {
|
|
1598
|
+
init_database();
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
// src/lib/open-projects.ts
|
|
1602
|
+
var exports_open_projects = {};
|
|
1603
|
+
__export(exports_open_projects, {
|
|
1604
|
+
syncOpenProjectsRegistry: () => syncOpenProjectsRegistry
|
|
1605
|
+
});
|
|
1606
|
+
async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
1607
|
+
let listProjects2 = listActiveProjects;
|
|
1608
|
+
if (!listProjects2) {
|
|
1609
|
+
const projectsApi = await import("@hasna/projects");
|
|
1610
|
+
listProjects2 = projectsApi.listProjects;
|
|
1611
|
+
}
|
|
1612
|
+
const projects = listProjects2({ status: "active", limit: 5000 });
|
|
1613
|
+
let imported = 0;
|
|
1614
|
+
let skipped = 0;
|
|
1615
|
+
for (const project of projects) {
|
|
1616
|
+
if (!project.path) {
|
|
1617
|
+
skipped++;
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
upsertProject(db, {
|
|
1621
|
+
id: project.id,
|
|
1622
|
+
path: project.path,
|
|
1623
|
+
name: project.name,
|
|
1624
|
+
description: project.description,
|
|
1625
|
+
tags: project.tags ?? [],
|
|
1626
|
+
created_at: project.created_at
|
|
1627
|
+
});
|
|
1628
|
+
imported++;
|
|
1629
|
+
}
|
|
1630
|
+
return { imported, skipped };
|
|
1631
|
+
}
|
|
1632
|
+
var init_open_projects = __esm(() => {
|
|
1633
|
+
init_database();
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
// src/lib/config.ts
|
|
1637
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1638
|
+
import { dirname, join as join9 } from "path";
|
|
1639
|
+
function getConfigPath() {
|
|
1640
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join9(getDataDir(), "config.json");
|
|
1641
|
+
}
|
|
1642
|
+
function loadConfig() {
|
|
1643
|
+
try {
|
|
1644
|
+
const configPath = getConfigPath();
|
|
1645
|
+
if (existsSync10(configPath)) {
|
|
1646
|
+
const raw = readFileSync11(configPath, "utf-8");
|
|
1647
|
+
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
1648
|
+
}
|
|
1649
|
+
} catch {}
|
|
1650
|
+
return { ...DEFAULTS };
|
|
1651
|
+
}
|
|
1652
|
+
var DEFAULTS;
|
|
1653
|
+
var init_config = __esm(() => {
|
|
1654
|
+
init_database();
|
|
1655
|
+
DEFAULTS = {
|
|
1656
|
+
port: 3456,
|
|
1657
|
+
"default-period": "today",
|
|
1658
|
+
"auto-sync": true,
|
|
1659
|
+
"sync-interval": 30,
|
|
1660
|
+
"alert-thresholds": [5, 10, 25, 50, 100],
|
|
1661
|
+
"webhook-url": null
|
|
1662
|
+
};
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
// src/lib/spikes.ts
|
|
1666
|
+
function detectCostSpikes(dailyTotals, opts) {
|
|
1667
|
+
const windowDays = opts?.windowDays ?? 7;
|
|
1668
|
+
const multiplier = opts?.multiplier ?? 2;
|
|
1669
|
+
const sorted = [...dailyTotals].sort((a, b) => a.date.localeCompare(b.date));
|
|
1670
|
+
const spikes = [];
|
|
1671
|
+
for (let i = windowDays;i < sorted.length; i++) {
|
|
1672
|
+
const window = sorted.slice(i - windowDays, i);
|
|
1673
|
+
const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
|
|
1674
|
+
const current = sorted[i];
|
|
1675
|
+
if (avg > 0 && current.cost_usd > avg * multiplier) {
|
|
1676
|
+
spikes.push({
|
|
1677
|
+
date: current.date,
|
|
1678
|
+
cost_usd: current.cost_usd,
|
|
1679
|
+
average_usd: avg,
|
|
1680
|
+
ratio: current.cost_usd / avg
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return spikes;
|
|
1685
|
+
}
|
|
1686
|
+
function queryRecentSpikes(db, days = 14) {
|
|
1687
|
+
const rows = db.prepare(`
|
|
1688
|
+
SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1689
|
+
FROM requests
|
|
1690
|
+
WHERE timestamp >= DATE('now', '-' || ? || ' days')
|
|
1691
|
+
GROUP BY DATE(timestamp)
|
|
1692
|
+
ORDER BY date ASC
|
|
1693
|
+
`).all(days);
|
|
1694
|
+
return detectCostSpikes(rows);
|
|
1695
|
+
}
|
|
1696
|
+
function getTodaySpike(db) {
|
|
1697
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
1698
|
+
const spikes = queryRecentSpikes(db, 14);
|
|
1699
|
+
return spikes.find((s) => s.date === today) ?? null;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/lib/webhooks.ts
|
|
1703
|
+
var exports_webhooks = {};
|
|
1704
|
+
__export(exports_webhooks, {
|
|
1705
|
+
checkAndFireWebhooks: () => checkAndFireWebhooks
|
|
1706
|
+
});
|
|
1707
|
+
async function checkAndFireWebhooks(db) {
|
|
1708
|
+
const config = loadConfig();
|
|
1709
|
+
const url = config["webhook-url"];
|
|
1710
|
+
if (!url)
|
|
1711
|
+
return;
|
|
1712
|
+
const statuses = getBudgetStatuses(db);
|
|
1713
|
+
for (const b of statuses) {
|
|
1714
|
+
if (!b.is_over_alert)
|
|
1715
|
+
continue;
|
|
1716
|
+
const key = `webhook-budget-${b.id}-${b.period}`;
|
|
1717
|
+
const lastFired = getIngestState(db, "webhook", key);
|
|
1718
|
+
const pctBucket = Math.floor(b.percent_used / 10) * 10;
|
|
1719
|
+
if (lastFired === String(pctBucket))
|
|
1720
|
+
continue;
|
|
1721
|
+
const delivered = await fireWebhook(url, {
|
|
1722
|
+
event: "budget_alert",
|
|
1723
|
+
budget_id: b.id,
|
|
1724
|
+
project: b.project_path ?? "global",
|
|
1725
|
+
period: b.period,
|
|
1726
|
+
spend: b.current_spend_usd,
|
|
1727
|
+
limit: b.limit_usd,
|
|
1728
|
+
percent: Math.round(b.percent_used * 10) / 10
|
|
1729
|
+
});
|
|
1730
|
+
if (delivered)
|
|
1731
|
+
setIngestState(db, "webhook", key, String(pctBucket));
|
|
1732
|
+
}
|
|
1733
|
+
const spike = getTodaySpike(db);
|
|
1734
|
+
if (spike) {
|
|
1735
|
+
const key = `webhook-spike-${spike.date}`;
|
|
1736
|
+
if (getIngestState(db, "webhook", key) !== "1") {
|
|
1737
|
+
const delivered = await fireWebhook(url, {
|
|
1738
|
+
event: "cost_spike",
|
|
1739
|
+
date: spike.date,
|
|
1740
|
+
cost_usd: spike.cost_usd,
|
|
1741
|
+
average_usd: spike.average_usd,
|
|
1742
|
+
ratio: Math.round(spike.ratio * 100) / 100
|
|
1743
|
+
});
|
|
1744
|
+
if (delivered)
|
|
1745
|
+
setIngestState(db, "webhook", key, "1");
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async function fireWebhook(url, payload) {
|
|
1750
|
+
try {
|
|
1751
|
+
const response = await fetch(url, {
|
|
1752
|
+
method: "POST",
|
|
1753
|
+
headers: { "Content-Type": "application/json" },
|
|
1754
|
+
body: JSON.stringify(payload),
|
|
1755
|
+
signal: AbortSignal.timeout(5000)
|
|
1756
|
+
});
|
|
1757
|
+
return response.ok;
|
|
1758
|
+
} catch {
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
var init_webhooks = __esm(() => {
|
|
1763
|
+
init_config();
|
|
1764
|
+
init_database();
|
|
1765
|
+
});
|
|
1766
|
+
|
|
651
1767
|
// src/server/serve.ts
|
|
652
1768
|
init_database();
|
|
1769
|
+
init_pricing();
|
|
1770
|
+
|
|
1771
|
+
// src/lib/agents.ts
|
|
1772
|
+
var AGENTS = [
|
|
1773
|
+
"claude",
|
|
1774
|
+
"takumi",
|
|
1775
|
+
"codex",
|
|
1776
|
+
"gemini",
|
|
1777
|
+
"opencode",
|
|
1778
|
+
"cursor",
|
|
1779
|
+
"pi",
|
|
1780
|
+
"hermes"
|
|
1781
|
+
];
|
|
1782
|
+
function isAgent(value) {
|
|
1783
|
+
return AGENTS.includes(value);
|
|
1784
|
+
}
|
|
653
1785
|
|
|
654
1786
|
// src/ingest/claude.ts
|
|
655
1787
|
init_database();
|
|
@@ -657,17 +1789,120 @@ init_pricing();
|
|
|
657
1789
|
import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
658
1790
|
import { homedir as homedir2 } from "os";
|
|
659
1791
|
import { join as join2, basename } from "path";
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1792
|
+
|
|
1793
|
+
// src/lib/savings.ts
|
|
1794
|
+
function periodWhere2(period, column) {
|
|
1795
|
+
switch (period) {
|
|
1796
|
+
case "today":
|
|
1797
|
+
return `DATE(${column}) = DATE('now')`;
|
|
1798
|
+
case "yesterday":
|
|
1799
|
+
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
1800
|
+
case "week":
|
|
1801
|
+
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1802
|
+
case "month":
|
|
1803
|
+
return `${column} >= DATE('now', 'start of month')`;
|
|
1804
|
+
case "year":
|
|
1805
|
+
return `${column} >= DATE('now', 'start of year')`;
|
|
1806
|
+
case "all":
|
|
1807
|
+
return "1=1";
|
|
1808
|
+
}
|
|
667
1809
|
}
|
|
668
|
-
function
|
|
669
|
-
const
|
|
670
|
-
|
|
1810
|
+
function prorateMonthlyFee(monthlyFee, period) {
|
|
1811
|
+
const now = new Date;
|
|
1812
|
+
const dayOfMonth = now.getDate();
|
|
1813
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1814
|
+
switch (period) {
|
|
1815
|
+
case "today":
|
|
1816
|
+
case "yesterday":
|
|
1817
|
+
return monthlyFee / daysInMonth;
|
|
1818
|
+
case "week":
|
|
1819
|
+
return monthlyFee / daysInMonth * 7;
|
|
1820
|
+
case "month":
|
|
1821
|
+
return monthlyFee;
|
|
1822
|
+
case "year":
|
|
1823
|
+
return monthlyFee * 12;
|
|
1824
|
+
case "all":
|
|
1825
|
+
return monthlyFee;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
|
|
1829
|
+
return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
|
|
1830
|
+
}
|
|
1831
|
+
function querySavingsSummary(db, period, agent) {
|
|
1832
|
+
const where = periodWhere2(period, "timestamp");
|
|
1833
|
+
const agentClause = agent ? " AND agent = ?" : "";
|
|
1834
|
+
const params = agent ? [agent] : [];
|
|
1835
|
+
const apiRow = db.prepare(`
|
|
1836
|
+
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1837
|
+
FROM requests
|
|
1838
|
+
WHERE ${where}${agentClause}
|
|
1839
|
+
AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
|
|
1840
|
+
`).get(...params);
|
|
1841
|
+
const includedRow = db.prepare(`
|
|
1842
|
+
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1843
|
+
FROM requests
|
|
1844
|
+
WHERE ${where}${agentClause}
|
|
1845
|
+
AND cost_basis = 'subscription_included'
|
|
1846
|
+
`).get(...params);
|
|
1847
|
+
const subWhere = periodWhere2(period, "date");
|
|
1848
|
+
const onDemandRow = db.prepare(`
|
|
1849
|
+
SELECT COALESCE(SUM(value), 0) as total
|
|
1850
|
+
FROM usage_snapshots
|
|
1851
|
+
WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
|
|
1852
|
+
AND metric = 'on_demand_usd'
|
|
1853
|
+
`).get(...params);
|
|
1854
|
+
const subs = db.prepare(`
|
|
1855
|
+
SELECT COALESCE(SUM(monthly_fee_usd), 0) as total
|
|
1856
|
+
FROM subscriptions
|
|
1857
|
+
WHERE active = 1${agent ? " AND agent = ?" : ""}
|
|
1858
|
+
`).get(...agent ? [agent] : []);
|
|
1859
|
+
const subscriptionFee = prorateMonthlyFee(subs.total, period);
|
|
1860
|
+
const apiEquivalent = apiRow.total + includedRow.total;
|
|
1861
|
+
const onDemand = onDemandRow.total;
|
|
1862
|
+
const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
|
|
1863
|
+
const byAgent = {};
|
|
1864
|
+
if (!agent) {
|
|
1865
|
+
for (const row of db.prepare(`
|
|
1866
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) as api_eq
|
|
1867
|
+
FROM requests WHERE ${where}
|
|
1868
|
+
GROUP BY agent
|
|
1869
|
+
`).all()) {
|
|
1870
|
+
byAgent[row.agent] = {
|
|
1871
|
+
api_equivalent_usd: row.api_eq,
|
|
1872
|
+
saved_usd: row.api_eq
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return {
|
|
1877
|
+
period,
|
|
1878
|
+
api_equivalent_usd: apiEquivalent,
|
|
1879
|
+
subscription_fee_usd: subscriptionFee,
|
|
1880
|
+
included_consumed_usd: includedRow.total,
|
|
1881
|
+
on_demand_usd: onDemand,
|
|
1882
|
+
saved_usd: saved,
|
|
1883
|
+
by_agent: byAgent
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function defaultCostBasisForAgent(agent) {
|
|
1887
|
+
if (agent === "claude")
|
|
1888
|
+
return "metered_api";
|
|
1889
|
+
if (agent === "cursor")
|
|
1890
|
+
return "subscription_included";
|
|
1891
|
+
return "estimated";
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/ingest/claude.ts
|
|
1895
|
+
function autoDetectProject(cwd, projects) {
|
|
1896
|
+
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
1897
|
+
}
|
|
1898
|
+
var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
1899
|
+
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
1900
|
+
function dirNameToPath(dirName) {
|
|
1901
|
+
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
1902
|
+
}
|
|
1903
|
+
function collectJsonlFiles(projectDir) {
|
|
1904
|
+
const files = [];
|
|
1905
|
+
function walk(dir) {
|
|
671
1906
|
try {
|
|
672
1907
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
673
1908
|
if (entry.isDirectory())
|
|
@@ -680,11 +1915,11 @@ function collectJsonlFiles(projectDir) {
|
|
|
680
1915
|
walk(projectDir);
|
|
681
1916
|
return files;
|
|
682
1917
|
}
|
|
683
|
-
async function ingestClaude(db, verbose = false,
|
|
684
|
-
return ingestJsonlProjects(db,
|
|
1918
|
+
async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
|
|
1919
|
+
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
685
1920
|
}
|
|
686
|
-
async function ingestTakumi(db, verbose = false) {
|
|
687
|
-
return ingestJsonlProjects(db,
|
|
1921
|
+
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
1922
|
+
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
688
1923
|
}
|
|
689
1924
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
690
1925
|
if (!existsSync2(projectsDir)) {
|
|
@@ -745,13 +1980,21 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
745
1980
|
continue;
|
|
746
1981
|
const inputTokens = usage.input_tokens ?? 0;
|
|
747
1982
|
const outputTokens = usage.output_tokens ?? 0;
|
|
748
|
-
const
|
|
1983
|
+
const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
|
|
1984
|
+
const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
1985
|
+
const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
|
|
749
1986
|
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
750
1987
|
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
751
|
-
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
1988
|
+
if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
|
|
752
1989
|
continue;
|
|
753
|
-
|
|
754
|
-
|
|
1990
|
+
let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
|
|
1991
|
+
costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
|
|
1992
|
+
const serverToolUse = usage.server_tool_use;
|
|
1993
|
+
if (serverToolUse?.web_search_requests) {
|
|
1994
|
+
costUsd += serverToolUse.web_search_requests * 0.01;
|
|
1995
|
+
}
|
|
1996
|
+
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1997
|
+
const reqId = `${agentName}-${sourceRequestId}`;
|
|
755
1998
|
upsertRequest(db, {
|
|
756
1999
|
id: reqId,
|
|
757
2000
|
agent: agentName,
|
|
@@ -761,10 +2004,13 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
761
2004
|
output_tokens: outputTokens,
|
|
762
2005
|
cache_read_tokens: cacheReadTokens,
|
|
763
2006
|
cache_create_tokens: cacheWriteTokens,
|
|
2007
|
+
cache_create_5m_tokens: cacheWrite5mTokens,
|
|
2008
|
+
cache_create_1h_tokens: cacheWrite1hTokens,
|
|
764
2009
|
cost_usd: costUsd,
|
|
2010
|
+
cost_basis: defaultCostBasisForAgent(agentName),
|
|
765
2011
|
duration_ms: 0,
|
|
766
2012
|
timestamp,
|
|
767
|
-
source_request_id:
|
|
2013
|
+
source_request_id: sourceRequestId,
|
|
768
2014
|
machine_id: machineId
|
|
769
2015
|
});
|
|
770
2016
|
if (!touchedSessions.has(sessionId)) {
|
|
@@ -799,82 +2045,254 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
799
2045
|
}
|
|
800
2046
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
801
2047
|
}
|
|
2048
|
+
function applyClaudeModifiers(costUsd, model, usage, entry) {
|
|
2049
|
+
let multiplier = 1;
|
|
2050
|
+
const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
|
|
2051
|
+
if (speed === "fast" && model.includes("opus-4-6")) {
|
|
2052
|
+
multiplier *= 6;
|
|
2053
|
+
}
|
|
2054
|
+
const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
|
|
2055
|
+
if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
|
|
2056
|
+
multiplier *= 1.1;
|
|
2057
|
+
}
|
|
2058
|
+
return costUsd * multiplier;
|
|
2059
|
+
}
|
|
2060
|
+
function supportsClaudeDataResidencyPricing(model) {
|
|
2061
|
+
const normalized = normalizeModelName(model);
|
|
2062
|
+
const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
2063
|
+
if (!match)
|
|
2064
|
+
return false;
|
|
2065
|
+
const major = Number(match[2]);
|
|
2066
|
+
const minor = match[3] ? Number(match[3]) : 0;
|
|
2067
|
+
return major > 4 || major === 4 && minor >= 6;
|
|
2068
|
+
}
|
|
802
2069
|
|
|
803
2070
|
// src/ingest/codex.ts
|
|
804
2071
|
init_database();
|
|
2072
|
+
init_pricing();
|
|
805
2073
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
806
2074
|
import { homedir as homedir3 } from "os";
|
|
807
2075
|
import { join as join3, basename as basename2 } from "path";
|
|
808
2076
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
809
|
-
var
|
|
810
|
-
var
|
|
2077
|
+
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
2078
|
+
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
2079
|
+
var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
|
|
2080
|
+
function codexDbPath() {
|
|
2081
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
2082
|
+
}
|
|
2083
|
+
function codexConfigPath() {
|
|
2084
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
2085
|
+
}
|
|
2086
|
+
function readCodexModel() {
|
|
2087
|
+
const configPath = codexConfigPath();
|
|
2088
|
+
if (!existsSync3(configPath))
|
|
2089
|
+
return "gpt-5-codex";
|
|
2090
|
+
try {
|
|
2091
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
2092
|
+
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
2093
|
+
return match?.[1] ?? "gpt-5-codex";
|
|
2094
|
+
} catch {
|
|
2095
|
+
return "gpt-5-codex";
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
function buildThreadQuery(codexDb) {
|
|
2099
|
+
const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
|
|
2100
|
+
const modelSelect = cols.has("model") ? "model" : "NULL AS model";
|
|
2101
|
+
const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
|
|
2102
|
+
const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
|
|
2103
|
+
return `
|
|
2104
|
+
SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
|
|
2105
|
+
${providerSelect}, ${modelSelect}
|
|
2106
|
+
FROM threads WHERE tokens_used > 0
|
|
2107
|
+
`;
|
|
2108
|
+
}
|
|
2109
|
+
function readTokenEvents(rolloutPath) {
|
|
2110
|
+
if (!rolloutPath || !existsSync3(rolloutPath))
|
|
2111
|
+
return [];
|
|
2112
|
+
const events = [];
|
|
2113
|
+
const seen = new Set;
|
|
2114
|
+
for (const line of readFileSync2(rolloutPath, "utf-8").split(`
|
|
2115
|
+
`)) {
|
|
2116
|
+
if (!line.trim())
|
|
2117
|
+
continue;
|
|
2118
|
+
let entry;
|
|
2119
|
+
try {
|
|
2120
|
+
entry = JSON.parse(line);
|
|
2121
|
+
} catch {
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2124
|
+
if (!entry || typeof entry !== "object")
|
|
2125
|
+
continue;
|
|
2126
|
+
const payload = entry["payload"];
|
|
2127
|
+
if (!payload || payload["type"] !== "token_count")
|
|
2128
|
+
continue;
|
|
2129
|
+
const info = payload["info"];
|
|
2130
|
+
const usage = info?.["last_token_usage"];
|
|
2131
|
+
if (!usage)
|
|
2132
|
+
continue;
|
|
2133
|
+
const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2134
|
+
if (total <= 0)
|
|
2135
|
+
continue;
|
|
2136
|
+
const key = JSON.stringify(usage);
|
|
2137
|
+
if (seen.has(key))
|
|
2138
|
+
continue;
|
|
2139
|
+
seen.add(key);
|
|
2140
|
+
const timestamp = entry["timestamp"];
|
|
2141
|
+
events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
|
|
2142
|
+
}
|
|
2143
|
+
return events;
|
|
2144
|
+
}
|
|
2145
|
+
function fallbackEvents(totalTokens) {
|
|
2146
|
+
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
2147
|
+
return [{
|
|
2148
|
+
usage: {
|
|
2149
|
+
input_tokens: inputTokens,
|
|
2150
|
+
cached_input_tokens: 0,
|
|
2151
|
+
output_tokens: totalTokens - inputTokens,
|
|
2152
|
+
total_tokens: totalTokens
|
|
2153
|
+
}
|
|
2154
|
+
}];
|
|
2155
|
+
}
|
|
811
2156
|
async function ingestCodex(db, verbose = false) {
|
|
812
|
-
|
|
2157
|
+
const dbPath = codexDbPath();
|
|
2158
|
+
if (!existsSync3(dbPath)) {
|
|
813
2159
|
if (verbose)
|
|
814
|
-
console.log("Codex DB not found:",
|
|
815
|
-
return { sessions: 0 };
|
|
2160
|
+
console.log("Codex DB not found:", dbPath);
|
|
2161
|
+
return { sessions: 0, requests: 0 };
|
|
816
2162
|
}
|
|
817
2163
|
const machineId = getMachineId();
|
|
818
2164
|
let codexDb = null;
|
|
819
2165
|
let ingested = 0;
|
|
2166
|
+
let requests = 0;
|
|
820
2167
|
try {
|
|
821
|
-
codexDb = new BunDatabase(
|
|
822
|
-
const threads = codexDb.prepare(
|
|
2168
|
+
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
2169
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
823
2170
|
for (const thread of threads) {
|
|
824
|
-
const
|
|
825
|
-
const
|
|
826
|
-
|
|
2171
|
+
const model = thread.model ?? readCodexModel();
|
|
2172
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
2173
|
+
const processed = getIngestState(db, "codex", thread.id);
|
|
2174
|
+
if (processed === stateValue)
|
|
827
2175
|
continue;
|
|
828
|
-
const costUsd = 0;
|
|
829
2176
|
const projectPath = thread.cwd ?? "";
|
|
830
2177
|
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
2178
|
+
const sessionId = `codex-${thread.id}`;
|
|
831
2179
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
832
2180
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
833
2181
|
upsertSession(db, {
|
|
834
|
-
id:
|
|
2182
|
+
id: sessionId,
|
|
835
2183
|
agent: "codex",
|
|
836
2184
|
project_path: projectPath,
|
|
837
2185
|
project_name: projectName,
|
|
838
2186
|
started_at: startedAt,
|
|
839
2187
|
ended_at: endedAt,
|
|
840
|
-
total_cost_usd:
|
|
841
|
-
total_tokens:
|
|
842
|
-
request_count:
|
|
2188
|
+
total_cost_usd: 0,
|
|
2189
|
+
total_tokens: 0,
|
|
2190
|
+
request_count: 0,
|
|
843
2191
|
machine_id: machineId
|
|
844
2192
|
});
|
|
845
|
-
|
|
2193
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
2194
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2195
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2196
|
+
tokenEvents.forEach((event, index) => {
|
|
2197
|
+
const usage = event.usage;
|
|
2198
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
2199
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2200
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2201
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2202
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2203
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2204
|
+
const requestId = `${sessionId}-${index}`;
|
|
2205
|
+
upsertRequest(db, {
|
|
2206
|
+
id: requestId,
|
|
2207
|
+
agent: "codex",
|
|
2208
|
+
session_id: sessionId,
|
|
2209
|
+
model,
|
|
2210
|
+
input_tokens: inputTokens,
|
|
2211
|
+
output_tokens: outputTokens,
|
|
2212
|
+
cache_read_tokens: cacheReadTokens,
|
|
2213
|
+
cache_create_tokens: 0,
|
|
2214
|
+
cost_usd: costUsd,
|
|
2215
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2216
|
+
duration_ms: 0,
|
|
2217
|
+
timestamp,
|
|
2218
|
+
source_request_id: requestId,
|
|
2219
|
+
machine_id: machineId
|
|
2220
|
+
});
|
|
2221
|
+
requests++;
|
|
2222
|
+
});
|
|
2223
|
+
rollupSession(db, sessionId);
|
|
2224
|
+
setIngestState(db, "codex", thread.id, stateValue);
|
|
846
2225
|
ingested++;
|
|
847
2226
|
if (verbose)
|
|
848
|
-
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens
|
|
2227
|
+
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
|
|
849
2228
|
}
|
|
850
2229
|
} finally {
|
|
851
2230
|
codexDb?.close();
|
|
852
2231
|
}
|
|
853
|
-
return { sessions: ingested };
|
|
2232
|
+
return { sessions: ingested, requests };
|
|
854
2233
|
}
|
|
855
2234
|
|
|
856
2235
|
// src/ingest/gemini.ts
|
|
857
2236
|
init_database();
|
|
2237
|
+
init_pricing();
|
|
858
2238
|
import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
859
2239
|
import { homedir as homedir4 } from "os";
|
|
860
|
-
import { join as join4 } from "path";
|
|
861
|
-
var
|
|
2240
|
+
import { join as join4, basename as basename3 } from "path";
|
|
2241
|
+
var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
2242
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
|
|
2243
|
+
function geminiTmpDir() {
|
|
2244
|
+
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
2245
|
+
}
|
|
2246
|
+
function geminiHistoryDir() {
|
|
2247
|
+
return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
|
|
2248
|
+
}
|
|
2249
|
+
function numberField(...values) {
|
|
2250
|
+
for (const value of values) {
|
|
2251
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2252
|
+
return value;
|
|
2253
|
+
}
|
|
2254
|
+
return 0;
|
|
2255
|
+
}
|
|
2256
|
+
function listProjectDirs(...roots) {
|
|
2257
|
+
const dirs = new Set;
|
|
2258
|
+
for (const root of roots) {
|
|
2259
|
+
if (!existsSync4(root))
|
|
2260
|
+
continue;
|
|
2261
|
+
try {
|
|
2262
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
2263
|
+
if (entry.isDirectory())
|
|
2264
|
+
dirs.add(join4(root, entry.name));
|
|
2265
|
+
}
|
|
2266
|
+
} catch {}
|
|
2267
|
+
}
|
|
2268
|
+
return [...dirs];
|
|
2269
|
+
}
|
|
2270
|
+
function projectRoot(projectDir, chatData) {
|
|
2271
|
+
if (chatData.projectPath)
|
|
2272
|
+
return chatData.projectPath;
|
|
2273
|
+
if (chatData.project_path)
|
|
2274
|
+
return chatData.project_path;
|
|
2275
|
+
const rootFile = join4(projectDir, ".project_root");
|
|
2276
|
+
try {
|
|
2277
|
+
if (existsSync4(rootFile))
|
|
2278
|
+
return readFileSync3(rootFile, "utf-8").trim();
|
|
2279
|
+
} catch {}
|
|
2280
|
+
return "";
|
|
2281
|
+
}
|
|
862
2282
|
async function ingestGemini(db, verbose) {
|
|
863
|
-
|
|
2283
|
+
const tmpDir = geminiTmpDir();
|
|
2284
|
+
const historyDir = geminiHistoryDir();
|
|
2285
|
+
if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
|
|
864
2286
|
if (verbose)
|
|
865
|
-
console.log("Gemini tmp
|
|
866
|
-
return { sessions: 0 };
|
|
2287
|
+
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
2288
|
+
return { sessions: 0, requests: 0 };
|
|
867
2289
|
}
|
|
868
2290
|
const machineId = getMachineId();
|
|
869
2291
|
let totalSessions = 0;
|
|
2292
|
+
let totalRequests = 0;
|
|
870
2293
|
const touchedSessions = new Set;
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
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));
|
|
874
|
-
} catch {
|
|
875
|
-
return { sessions: 0 };
|
|
876
|
-
}
|
|
877
|
-
for (const projectDir of projectHashDirs) {
|
|
2294
|
+
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2295
|
+
for (const projectDir of projectDirs) {
|
|
878
2296
|
const chatsDir = join4(projectDir, "chats");
|
|
879
2297
|
if (!existsSync4(chatsDir))
|
|
880
2298
|
continue;
|
|
@@ -901,17 +2319,19 @@ async function ingestGemini(db, verbose) {
|
|
|
901
2319
|
} catch {
|
|
902
2320
|
continue;
|
|
903
2321
|
}
|
|
904
|
-
const sessionId = chatData.sessionId;
|
|
2322
|
+
const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
|
|
905
2323
|
if (!sessionId)
|
|
906
2324
|
continue;
|
|
907
2325
|
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
2326
|
+
const projectPath = projectRoot(projectDir, chatData);
|
|
2327
|
+
const projectName = projectPath ? basename3(projectPath) : "";
|
|
908
2328
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
909
2329
|
if (!existing) {
|
|
910
2330
|
const session = {
|
|
911
2331
|
id: sessionId,
|
|
912
2332
|
agent: "gemini",
|
|
913
|
-
project_path:
|
|
914
|
-
project_name:
|
|
2333
|
+
project_path: projectPath,
|
|
2334
|
+
project_name: projectName,
|
|
915
2335
|
started_at: startTime,
|
|
916
2336
|
ended_at: chatData.lastUpdated ?? null,
|
|
917
2337
|
total_cost_usd: 0,
|
|
@@ -920,26 +2340,1074 @@ async function ingestGemini(db, verbose) {
|
|
|
920
2340
|
machine_id: machineId
|
|
921
2341
|
};
|
|
922
2342
|
upsertSession(db, session);
|
|
923
|
-
touchedSessions.add(sessionId);
|
|
924
2343
|
totalSessions++;
|
|
925
2344
|
}
|
|
2345
|
+
touchedSessions.add(sessionId);
|
|
2346
|
+
for (const [index, message] of (chatData.messages ?? []).entries()) {
|
|
2347
|
+
const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
|
|
2348
|
+
if (!usage)
|
|
2349
|
+
continue;
|
|
2350
|
+
const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
|
|
2351
|
+
if (!model)
|
|
2352
|
+
continue;
|
|
2353
|
+
const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
|
|
2354
|
+
const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
|
|
2355
|
+
const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
|
|
2356
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2357
|
+
const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
|
|
2358
|
+
const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
|
|
2359
|
+
const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
|
|
2360
|
+
if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
|
|
2361
|
+
continue;
|
|
2362
|
+
const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2363
|
+
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
2364
|
+
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
2365
|
+
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
2366
|
+
upsertRequest(db, {
|
|
2367
|
+
id: requestId,
|
|
2368
|
+
agent: "gemini",
|
|
2369
|
+
session_id: sessionId,
|
|
2370
|
+
model,
|
|
2371
|
+
input_tokens: inputTokens,
|
|
2372
|
+
output_tokens: outputTokens,
|
|
2373
|
+
cache_read_tokens: cacheReadTokens,
|
|
2374
|
+
cache_create_tokens: 0,
|
|
2375
|
+
cost_usd: costUsd,
|
|
2376
|
+
cost_basis: defaultCostBasisForAgent("gemini"),
|
|
2377
|
+
duration_ms: 0,
|
|
2378
|
+
timestamp,
|
|
2379
|
+
source_request_id: message.id ?? requestId,
|
|
2380
|
+
machine_id: machineId
|
|
2381
|
+
});
|
|
2382
|
+
totalRequests++;
|
|
2383
|
+
}
|
|
926
2384
|
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
927
2385
|
}
|
|
928
2386
|
}
|
|
929
2387
|
for (const sessionId of touchedSessions) {
|
|
930
2388
|
rollupSession(db, sessionId);
|
|
931
2389
|
}
|
|
932
|
-
return { sessions: totalSessions };
|
|
2390
|
+
return { sessions: totalSessions, requests: totalRequests };
|
|
933
2391
|
}
|
|
934
2392
|
|
|
935
|
-
// src/
|
|
2393
|
+
// src/ingest/opencode.ts
|
|
2394
|
+
init_database();
|
|
936
2395
|
init_pricing();
|
|
2396
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
2397
|
+
import { homedir as homedir5 } from "os";
|
|
2398
|
+
import { join as join5 } from "path";
|
|
2399
|
+
var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
|
|
2400
|
+
function walkJsonFiles(dir, acc = []) {
|
|
2401
|
+
if (!existsSync5(dir))
|
|
2402
|
+
return acc;
|
|
2403
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
2404
|
+
const p = join5(dir, entry.name);
|
|
2405
|
+
if (entry.isDirectory())
|
|
2406
|
+
walkJsonFiles(p, acc);
|
|
2407
|
+
else if (entry.name.endsWith(".json"))
|
|
2408
|
+
acc.push(p);
|
|
2409
|
+
}
|
|
2410
|
+
return acc;
|
|
2411
|
+
}
|
|
2412
|
+
function parseSessionIdFromPath(filePath) {
|
|
2413
|
+
const parts = filePath.split("/");
|
|
2414
|
+
const msgIdx = parts.indexOf("message");
|
|
2415
|
+
if (msgIdx >= 0 && parts[msgIdx + 1])
|
|
2416
|
+
return parts[msgIdx + 1];
|
|
2417
|
+
const sessionIdx = parts.indexOf("session");
|
|
2418
|
+
if (sessionIdx >= 0 && parts[parts.length - 1]?.endsWith(".json")) {
|
|
2419
|
+
return parts[parts.length - 1].replace(/\.json$/, "");
|
|
2420
|
+
}
|
|
2421
|
+
return null;
|
|
2422
|
+
}
|
|
2423
|
+
async function ingestOpenCode(db, verbose = false) {
|
|
2424
|
+
const messageDir = join5(OPENCODE_STORAGE, "message");
|
|
2425
|
+
const files = walkJsonFiles(messageDir);
|
|
2426
|
+
let requests = 0;
|
|
2427
|
+
const touched = new Set;
|
|
2428
|
+
const machineId = getMachineId();
|
|
2429
|
+
const now = new Date().toISOString();
|
|
2430
|
+
for (const file of files) {
|
|
2431
|
+
const mtime = statSync4(file).mtimeMs;
|
|
2432
|
+
const stateKey = file;
|
|
2433
|
+
const prev = getIngestState(db, "opencode", stateKey);
|
|
2434
|
+
if (prev && Number(prev) >= mtime)
|
|
2435
|
+
continue;
|
|
2436
|
+
let parsed;
|
|
2437
|
+
try {
|
|
2438
|
+
parsed = JSON.parse(readFileSync4(file, "utf-8"));
|
|
2439
|
+
} catch {
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
if (parsed.role !== "assistant")
|
|
2443
|
+
continue;
|
|
2444
|
+
const usage = parsed.usage;
|
|
2445
|
+
if (!usage)
|
|
2446
|
+
continue;
|
|
2447
|
+
const sessionId = parseSessionIdFromPath(file) ?? `opencode-${statSync4(file).ino}`;
|
|
2448
|
+
const model = normalizeModelName(parsed.model ?? "unknown");
|
|
2449
|
+
const input = usage.inputTokens ?? 0;
|
|
2450
|
+
const output = usage.outputTokens ?? 0;
|
|
2451
|
+
const cacheRead = usage.cacheReadTokens ?? 0;
|
|
2452
|
+
const cacheWrite = usage.cacheWriteTokens ?? 0;
|
|
2453
|
+
if (input + output + cacheRead + cacheWrite === 0 && !usage.cost)
|
|
2454
|
+
continue;
|
|
2455
|
+
const timestamp = usage && parsed.time?.created ? new Date(parsed.time.created).toISOString() : new Date(statSync4(file).mtime).toISOString();
|
|
2456
|
+
const sourceId = file.replace(OPENCODE_STORAGE, "");
|
|
2457
|
+
const reqId = `opencode-${sourceId}`;
|
|
2458
|
+
const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
|
|
2459
|
+
upsertRequest(db, {
|
|
2460
|
+
id: reqId,
|
|
2461
|
+
agent: "opencode",
|
|
2462
|
+
session_id: sessionId,
|
|
2463
|
+
model,
|
|
2464
|
+
input_tokens: input,
|
|
2465
|
+
output_tokens: output,
|
|
2466
|
+
cache_read_tokens: cacheRead,
|
|
2467
|
+
cache_create_tokens: cacheWrite,
|
|
2468
|
+
cost_usd: costUsd,
|
|
2469
|
+
cost_basis: defaultCostBasisForAgent("opencode"),
|
|
2470
|
+
duration_ms: 0,
|
|
2471
|
+
timestamp,
|
|
2472
|
+
source_request_id: sourceId,
|
|
2473
|
+
machine_id: machineId,
|
|
2474
|
+
updated_at: now
|
|
2475
|
+
});
|
|
2476
|
+
requests++;
|
|
2477
|
+
if (!touched.has(sessionId)) {
|
|
2478
|
+
upsertSession(db, {
|
|
2479
|
+
id: sessionId,
|
|
2480
|
+
agent: "opencode",
|
|
2481
|
+
project_path: "",
|
|
2482
|
+
project_name: "",
|
|
2483
|
+
started_at: timestamp,
|
|
2484
|
+
ended_at: null,
|
|
2485
|
+
total_cost_usd: 0,
|
|
2486
|
+
total_tokens: 0,
|
|
2487
|
+
request_count: 0,
|
|
2488
|
+
machine_id: machineId,
|
|
2489
|
+
updated_at: now
|
|
2490
|
+
});
|
|
2491
|
+
touched.add(sessionId);
|
|
2492
|
+
}
|
|
2493
|
+
setIngestState(db, "opencode", stateKey, String(mtime));
|
|
2494
|
+
if (verbose)
|
|
2495
|
+
console.log(` opencode: ${reqId} ${model} $${costUsd.toFixed(4)}`);
|
|
2496
|
+
}
|
|
2497
|
+
for (const sid of touched)
|
|
2498
|
+
rollupSession(db, sid);
|
|
2499
|
+
return { files: files.length, requests, sessions: touched.size };
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// src/ingest/cursor.ts
|
|
2503
|
+
init_database();
|
|
2504
|
+
function getCursorSessionToken() {
|
|
2505
|
+
return process.env["CURSOR_SESSION_TOKEN"] ?? process.env["CURSOR_API_TOKEN"] ?? null;
|
|
2506
|
+
}
|
|
2507
|
+
async function cursorFetch(path, token) {
|
|
2508
|
+
try {
|
|
2509
|
+
const res = await fetch(`https://cursor.com${path}`, {
|
|
2510
|
+
headers: {
|
|
2511
|
+
Cookie: `WorkosCursorSessionToken=${token}`,
|
|
2512
|
+
Accept: "application/json"
|
|
2513
|
+
},
|
|
2514
|
+
signal: AbortSignal.timeout(1e4)
|
|
2515
|
+
});
|
|
2516
|
+
if (!res.ok)
|
|
2517
|
+
return null;
|
|
2518
|
+
return await res.json();
|
|
2519
|
+
} catch {
|
|
2520
|
+
return null;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
async function ingestCursor(db, verbose = false) {
|
|
2524
|
+
const token = getCursorSessionToken();
|
|
2525
|
+
if (!token) {
|
|
2526
|
+
if (verbose)
|
|
2527
|
+
console.log(" cursor: skipped \u2014 set CURSOR_SESSION_TOKEN");
|
|
2528
|
+
return { requests: 0, snapshots: 0 };
|
|
2529
|
+
}
|
|
2530
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
2531
|
+
const prev = getIngestState(db, "cursor", `sync-${today}`);
|
|
2532
|
+
if (prev)
|
|
2533
|
+
return { requests: 0, snapshots: 0 };
|
|
2534
|
+
const machineId = getMachineId();
|
|
2535
|
+
const now = new Date().toISOString();
|
|
2536
|
+
let snapshots = 0;
|
|
2537
|
+
const usage = await cursorFetch("/api/usage", token);
|
|
2538
|
+
if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
|
|
2539
|
+
upsertUsageSnapshot(db, {
|
|
2540
|
+
agent: "cursor",
|
|
2541
|
+
date: today,
|
|
2542
|
+
metric: "premium_requests_used",
|
|
2543
|
+
value: usage.premiumRequests,
|
|
2544
|
+
unit: "count",
|
|
2545
|
+
machine_id: machineId
|
|
2546
|
+
});
|
|
2547
|
+
upsertUsageSnapshot(db, {
|
|
2548
|
+
agent: "cursor",
|
|
2549
|
+
date: today,
|
|
2550
|
+
metric: "premium_requests_limit",
|
|
2551
|
+
value: usage.maxPremiumRequests,
|
|
2552
|
+
unit: "count",
|
|
2553
|
+
machine_id: machineId
|
|
2554
|
+
});
|
|
2555
|
+
snapshots += 2;
|
|
2556
|
+
}
|
|
2557
|
+
const summary = await cursorFetch("/api/usage-summary", token);
|
|
2558
|
+
const onDemand = summary?.individualUsage?.spend ?? summary?.teamUsage?.spend ?? 0;
|
|
2559
|
+
const included = summary?.individualUsage?.includedSpend ?? 0;
|
|
2560
|
+
if (onDemand > 0) {
|
|
2561
|
+
upsertUsageSnapshot(db, {
|
|
2562
|
+
agent: "cursor",
|
|
2563
|
+
date: today,
|
|
2564
|
+
metric: "on_demand_usd",
|
|
2565
|
+
value: onDemand,
|
|
2566
|
+
unit: "usd",
|
|
2567
|
+
machine_id: machineId
|
|
2568
|
+
});
|
|
2569
|
+
snapshots++;
|
|
2570
|
+
}
|
|
2571
|
+
if (included > 0) {
|
|
2572
|
+
upsertUsageSnapshot(db, {
|
|
2573
|
+
agent: "cursor",
|
|
2574
|
+
date: today,
|
|
2575
|
+
metric: "included_consumed_usd",
|
|
2576
|
+
value: included,
|
|
2577
|
+
unit: "usd",
|
|
2578
|
+
machine_id: machineId
|
|
2579
|
+
});
|
|
2580
|
+
snapshots++;
|
|
2581
|
+
}
|
|
2582
|
+
const sessionId = `cursor-${today}-${machineId}`;
|
|
2583
|
+
if (onDemand + included > 0) {
|
|
2584
|
+
upsertSession(db, {
|
|
2585
|
+
id: sessionId,
|
|
2586
|
+
agent: "cursor",
|
|
2587
|
+
project_path: "",
|
|
2588
|
+
project_name: "Cursor subscription",
|
|
2589
|
+
started_at: `${today}T00:00:00.000Z`,
|
|
2590
|
+
ended_at: now,
|
|
2591
|
+
total_cost_usd: onDemand + included,
|
|
2592
|
+
total_tokens: 0,
|
|
2593
|
+
request_count: 1,
|
|
2594
|
+
machine_id: machineId,
|
|
2595
|
+
updated_at: now
|
|
2596
|
+
});
|
|
2597
|
+
upsertRequest(db, {
|
|
2598
|
+
id: `cursor-${today}-${machineId}-usage`,
|
|
2599
|
+
agent: "cursor",
|
|
2600
|
+
session_id: sessionId,
|
|
2601
|
+
model: "cursor-subscription",
|
|
2602
|
+
input_tokens: 0,
|
|
2603
|
+
output_tokens: 0,
|
|
2604
|
+
cache_read_tokens: 0,
|
|
2605
|
+
cache_create_tokens: 0,
|
|
2606
|
+
cost_usd: onDemand + included,
|
|
2607
|
+
cost_basis: "subscription_included",
|
|
2608
|
+
duration_ms: 0,
|
|
2609
|
+
timestamp: now,
|
|
2610
|
+
source_request_id: today,
|
|
2611
|
+
machine_id: machineId,
|
|
2612
|
+
updated_at: now
|
|
2613
|
+
});
|
|
2614
|
+
rollupSession(db, sessionId);
|
|
2615
|
+
}
|
|
2616
|
+
setIngestState(db, "cursor", `sync-${today}`, now);
|
|
2617
|
+
if (verbose)
|
|
2618
|
+
console.log(` cursor: on-demand $${onDemand.toFixed(2)}, included $${included.toFixed(2)}`);
|
|
2619
|
+
return { requests: onDemand + included > 0 ? 1 : 0, snapshots };
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// src/ingest/pi.ts
|
|
2623
|
+
init_database();
|
|
2624
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
2625
|
+
import { homedir as homedir6 } from "os";
|
|
2626
|
+
import { join as join6 } from "path";
|
|
2627
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
|
|
2628
|
+
function walkSessions(dir, acc = []) {
|
|
2629
|
+
if (!existsSync6(dir))
|
|
2630
|
+
return acc;
|
|
2631
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
2632
|
+
const p = join6(dir, entry.name);
|
|
2633
|
+
if (entry.isDirectory())
|
|
2634
|
+
walkSessions(p, acc);
|
|
2635
|
+
else if (entry.name.endsWith(".json"))
|
|
2636
|
+
acc.push(p);
|
|
2637
|
+
}
|
|
2638
|
+
return acc;
|
|
2639
|
+
}
|
|
2640
|
+
async function ingestPi(db, verbose = false) {
|
|
2641
|
+
const files = walkSessions(PI_SESSION_DIR);
|
|
2642
|
+
let requests = 0;
|
|
2643
|
+
const touched = new Set;
|
|
2644
|
+
const machineId = getMachineId();
|
|
2645
|
+
const now = new Date().toISOString();
|
|
2646
|
+
for (const file of files) {
|
|
2647
|
+
const mtime = statSync5(file).mtimeMs;
|
|
2648
|
+
const prev = getIngestState(db, "pi", file);
|
|
2649
|
+
if (prev && Number(prev) >= mtime)
|
|
2650
|
+
continue;
|
|
2651
|
+
let data;
|
|
2652
|
+
try {
|
|
2653
|
+
data = JSON.parse(readFileSync5(file, "utf-8"));
|
|
2654
|
+
} catch {
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
const sessionId = data.id ?? file.replace(/\.json$/, "").split("/").pop() ?? `pi-${statSync5(file).ino}`;
|
|
2658
|
+
const turns = data.turns ?? data.messages?.filter((m) => m.role === "assistant") ?? [];
|
|
2659
|
+
for (let i = 0;i < turns.length; i++) {
|
|
2660
|
+
const turn = turns[i];
|
|
2661
|
+
const usage = turn.usage;
|
|
2662
|
+
if (!usage)
|
|
2663
|
+
continue;
|
|
2664
|
+
const input = usage.input ?? 0;
|
|
2665
|
+
const output = usage.output ?? 0;
|
|
2666
|
+
if (input + output === 0 && !usage.cost)
|
|
2667
|
+
continue;
|
|
2668
|
+
const model = turn.model ?? turn.provider ?? "unknown";
|
|
2669
|
+
const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
|
|
2670
|
+
const reqId = `pi-${sessionId}-${i}`;
|
|
2671
|
+
upsertRequest(db, {
|
|
2672
|
+
id: reqId,
|
|
2673
|
+
agent: "pi",
|
|
2674
|
+
session_id: sessionId,
|
|
2675
|
+
model,
|
|
2676
|
+
input_tokens: input,
|
|
2677
|
+
output_tokens: output,
|
|
2678
|
+
cache_read_tokens: usage.cacheRead ?? 0,
|
|
2679
|
+
cache_create_tokens: usage.cacheWrite ?? 0,
|
|
2680
|
+
cost_usd: usage.cost ?? 0,
|
|
2681
|
+
cost_basis: defaultCostBasisForAgent("pi"),
|
|
2682
|
+
duration_ms: 0,
|
|
2683
|
+
timestamp,
|
|
2684
|
+
source_request_id: `${sessionId}-${i}`,
|
|
2685
|
+
machine_id: machineId,
|
|
2686
|
+
updated_at: now
|
|
2687
|
+
});
|
|
2688
|
+
requests++;
|
|
2689
|
+
}
|
|
2690
|
+
if (turns.length > 0) {
|
|
2691
|
+
upsertSession(db, {
|
|
2692
|
+
id: sessionId,
|
|
2693
|
+
agent: "pi",
|
|
2694
|
+
project_path: "",
|
|
2695
|
+
project_name: "",
|
|
2696
|
+
started_at: turns[0]?.timestamp ?? now,
|
|
2697
|
+
ended_at: null,
|
|
2698
|
+
total_cost_usd: 0,
|
|
2699
|
+
total_tokens: 0,
|
|
2700
|
+
request_count: 0,
|
|
2701
|
+
machine_id: machineId,
|
|
2702
|
+
updated_at: now
|
|
2703
|
+
});
|
|
2704
|
+
touched.add(sessionId);
|
|
2705
|
+
}
|
|
2706
|
+
setIngestState(db, "pi", file, String(mtime));
|
|
2707
|
+
if (verbose)
|
|
2708
|
+
console.log(` pi: ${sessionId} (${turns.length} turns)`);
|
|
2709
|
+
}
|
|
2710
|
+
for (const sid of touched)
|
|
2711
|
+
rollupSession(db, sid);
|
|
2712
|
+
return { files: files.length, requests, sessions: touched.size };
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// src/ingest/hermes.ts
|
|
2716
|
+
init_database();
|
|
2717
|
+
import { existsSync as existsSync7, statSync as statSync6 } from "fs";
|
|
2718
|
+
import { homedir as homedir7 } from "os";
|
|
2719
|
+
import { join as join7 } from "path";
|
|
2720
|
+
var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
|
|
2721
|
+
function mapCostBasis(billingMode) {
|
|
2722
|
+
if (billingMode === "subscription")
|
|
2723
|
+
return "subscription_included";
|
|
2724
|
+
if (billingMode === "api")
|
|
2725
|
+
return "metered_api";
|
|
2726
|
+
return defaultCostBasisForAgent("hermes");
|
|
2727
|
+
}
|
|
2728
|
+
async function ingestHermes(db, verbose = false) {
|
|
2729
|
+
if (!existsSync7(HERMES_DB)) {
|
|
2730
|
+
return { sessions: 0, requests: 0 };
|
|
2731
|
+
}
|
|
2732
|
+
const { Database: Sqlite } = await import("bun:sqlite");
|
|
2733
|
+
const hermes = new Sqlite(HERMES_DB, { readonly: true });
|
|
2734
|
+
const rows = hermes.prepare(`
|
|
2735
|
+
SELECT id, source, model, started_at, ended_at,
|
|
2736
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
2737
|
+
reasoning_tokens, estimated_cost_usd, actual_cost_usd, billing_mode, parent_session_id
|
|
2738
|
+
FROM sessions
|
|
2739
|
+
ORDER BY started_at DESC
|
|
2740
|
+
`).all();
|
|
2741
|
+
const stateKey = "state.db";
|
|
2742
|
+
const mtime = statSyncSafe(HERMES_DB);
|
|
2743
|
+
const prev = getIngestState(db, "hermes", stateKey);
|
|
2744
|
+
if (prev && Number(prev) >= mtime && rows.length === 0) {
|
|
2745
|
+
hermes.close();
|
|
2746
|
+
return { sessions: 0, requests: 0 };
|
|
2747
|
+
}
|
|
2748
|
+
const machineId = getMachineId();
|
|
2749
|
+
const now = new Date().toISOString();
|
|
2750
|
+
let requests = 0;
|
|
2751
|
+
for (const row of rows) {
|
|
2752
|
+
const sessionId = `hermes-${row.id}`;
|
|
2753
|
+
const startedAt = new Date(row.started_at * 1000).toISOString();
|
|
2754
|
+
const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
|
|
2755
|
+
const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
|
|
2756
|
+
const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
|
|
2757
|
+
upsertSession(db, {
|
|
2758
|
+
id: sessionId,
|
|
2759
|
+
agent: "hermes",
|
|
2760
|
+
project_path: row.source ?? "",
|
|
2761
|
+
project_name: row.source ?? "",
|
|
2762
|
+
started_at: startedAt,
|
|
2763
|
+
ended_at: endedAt,
|
|
2764
|
+
total_cost_usd: cost,
|
|
2765
|
+
total_tokens: tokens,
|
|
2766
|
+
request_count: 1,
|
|
2767
|
+
machine_id: machineId,
|
|
2768
|
+
updated_at: now
|
|
2769
|
+
});
|
|
2770
|
+
const reqId = `hermes-${row.id}-rollup`;
|
|
2771
|
+
upsertRequest(db, {
|
|
2772
|
+
id: reqId,
|
|
2773
|
+
agent: "hermes",
|
|
2774
|
+
session_id: sessionId,
|
|
2775
|
+
model: row.model ?? "unknown",
|
|
2776
|
+
input_tokens: row.input_tokens,
|
|
2777
|
+
output_tokens: row.output_tokens + row.reasoning_tokens,
|
|
2778
|
+
cache_read_tokens: row.cache_read_tokens,
|
|
2779
|
+
cache_create_tokens: row.cache_write_tokens,
|
|
2780
|
+
cost_usd: cost,
|
|
2781
|
+
cost_basis: mapCostBasis(row.billing_mode),
|
|
2782
|
+
duration_ms: 0,
|
|
2783
|
+
timestamp: endedAt ?? startedAt,
|
|
2784
|
+
source_request_id: row.id,
|
|
2785
|
+
machine_id: machineId,
|
|
2786
|
+
updated_at: now
|
|
2787
|
+
});
|
|
2788
|
+
requests++;
|
|
2789
|
+
rollupSession(db, sessionId);
|
|
2790
|
+
if (verbose)
|
|
2791
|
+
console.log(` hermes: ${sessionId} $${cost.toFixed(4)}`);
|
|
2792
|
+
}
|
|
2793
|
+
setIngestState(db, "hermes", stateKey, String(mtime));
|
|
2794
|
+
hermes.close();
|
|
2795
|
+
return { sessions: rows.length, requests };
|
|
2796
|
+
}
|
|
2797
|
+
function statSyncSafe(path) {
|
|
2798
|
+
try {
|
|
2799
|
+
return statSync6(path).mtimeMs;
|
|
2800
|
+
} catch {
|
|
2801
|
+
return 0;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/ingest/claude-quota.ts
|
|
2806
|
+
init_database();
|
|
2807
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
2808
|
+
|
|
2809
|
+
// src/lib/paths.ts
|
|
2810
|
+
import { homedir as homedir8 } from "os";
|
|
2811
|
+
import { join as join8 } from "path";
|
|
2812
|
+
function getHomeDir() {
|
|
2813
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir8();
|
|
2814
|
+
}
|
|
2815
|
+
function agentPaths() {
|
|
2816
|
+
const home = getHomeDir();
|
|
2817
|
+
return {
|
|
2818
|
+
claudeProjects: join8(home, ".claude", "projects"),
|
|
2819
|
+
claudeCredentials: join8(home, ".claude", ".credentials.json"),
|
|
2820
|
+
takumiProjects: join8(home, ".takumi", "projects"),
|
|
2821
|
+
codexDir: join8(home, ".codex"),
|
|
2822
|
+
codexDb: join8(home, ".codex", "state_5.sqlite"),
|
|
2823
|
+
codexAuth: join8(home, ".codex", "auth.json"),
|
|
2824
|
+
codexConfig: join8(home, ".codex", "config.toml"),
|
|
2825
|
+
geminiTmp: join8(home, ".gemini", "tmp"),
|
|
2826
|
+
geminiHistory: join8(home, ".gemini", "history"),
|
|
2827
|
+
opencodeMessages: join8(home, ".local", "share", "opencode", "storage", "message"),
|
|
2828
|
+
piSessions: join8(home, ".pi", "agent", "sessions"),
|
|
2829
|
+
hermesDir: join8(home, ".hermes"),
|
|
2830
|
+
hermesDb: join8(home, ".hermes", "state.db")
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// src/ingest/claude-quota.ts
|
|
2835
|
+
var CREDENTIALS_PATH = agentPaths().claudeCredentials;
|
|
2836
|
+
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
2837
|
+
var OAUTH_BETA = "oauth-2025-04-20";
|
|
2838
|
+
function readClaudeToken() {
|
|
2839
|
+
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
2840
|
+
if (fromEnv)
|
|
2841
|
+
return { token: fromEnv };
|
|
2842
|
+
if (!existsSync8(CREDENTIALS_PATH))
|
|
2843
|
+
return null;
|
|
2844
|
+
try {
|
|
2845
|
+
const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
|
|
2846
|
+
const oauth = creds.claudeAiOauth;
|
|
2847
|
+
if (!oauth?.accessToken)
|
|
2848
|
+
return null;
|
|
2849
|
+
return {
|
|
2850
|
+
token: oauth.accessToken,
|
|
2851
|
+
subscriptionType: oauth.subscriptionType,
|
|
2852
|
+
rateLimitTier: oauth.rateLimitTier
|
|
2853
|
+
};
|
|
2854
|
+
} catch {
|
|
2855
|
+
return null;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
function inferMonthlyFee(subscriptionType, rateLimitTier) {
|
|
2859
|
+
const tier = `${subscriptionType ?? ""} ${rateLimitTier ?? ""}`.toLowerCase();
|
|
2860
|
+
if (tier.includes("max") && tier.includes("20"))
|
|
2861
|
+
return 200;
|
|
2862
|
+
if (tier.includes("max"))
|
|
2863
|
+
return 100;
|
|
2864
|
+
if (tier.includes("pro"))
|
|
2865
|
+
return 20;
|
|
2866
|
+
if (tier.includes("team"))
|
|
2867
|
+
return 30;
|
|
2868
|
+
return 20;
|
|
2869
|
+
}
|
|
2870
|
+
async function fetchClaudeOAuthUsage(token) {
|
|
2871
|
+
try {
|
|
2872
|
+
const res = await fetch(USAGE_URL, {
|
|
2873
|
+
headers: {
|
|
2874
|
+
Authorization: `Bearer ${token}`,
|
|
2875
|
+
"anthropic-beta": OAUTH_BETA,
|
|
2876
|
+
Accept: "application/json"
|
|
2877
|
+
},
|
|
2878
|
+
signal: AbortSignal.timeout(1e4)
|
|
2879
|
+
});
|
|
2880
|
+
if (!res.ok)
|
|
2881
|
+
return null;
|
|
2882
|
+
return await res.json();
|
|
2883
|
+
} catch {
|
|
2884
|
+
return null;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
async function ingestClaudeQuota(db, verbose = false) {
|
|
2888
|
+
const auth = readClaudeToken();
|
|
2889
|
+
if (!auth) {
|
|
2890
|
+
if (verbose)
|
|
2891
|
+
console.log(" claude quota: skipped \u2014 no OAuth token (~/.claude/.credentials.json or CLAUDE_OAUTH_TOKEN)");
|
|
2892
|
+
return { snapshots: 0 };
|
|
2893
|
+
}
|
|
2894
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
2895
|
+
const prev = getIngestState(db, "claude", `quota-${today}`);
|
|
2896
|
+
if (prev)
|
|
2897
|
+
return { snapshots: 0 };
|
|
2898
|
+
const usage = await fetchClaudeOAuthUsage(auth.token);
|
|
2899
|
+
if (!usage) {
|
|
2900
|
+
if (verbose)
|
|
2901
|
+
console.log(" claude quota: OAuth usage endpoint unavailable");
|
|
2902
|
+
return { snapshots: 0 };
|
|
2903
|
+
}
|
|
2904
|
+
const machineId = getMachineId();
|
|
2905
|
+
let snapshots = 0;
|
|
2906
|
+
const windows = [
|
|
2907
|
+
["five_hour_utilization", usage.five_hour],
|
|
2908
|
+
["seven_day_utilization", usage.seven_day],
|
|
2909
|
+
["seven_day_sonnet_utilization", usage.seven_day_sonnet],
|
|
2910
|
+
["seven_day_opus_utilization", usage.seven_day_opus]
|
|
2911
|
+
];
|
|
2912
|
+
for (const [metric, window] of windows) {
|
|
2913
|
+
if (window?.utilization == null)
|
|
2914
|
+
continue;
|
|
2915
|
+
upsertUsageSnapshot(db, {
|
|
2916
|
+
agent: "claude",
|
|
2917
|
+
date: today,
|
|
2918
|
+
metric,
|
|
2919
|
+
value: Math.round(window.utilization * 1000) / 10,
|
|
2920
|
+
unit: "percent",
|
|
2921
|
+
machine_id: machineId
|
|
2922
|
+
});
|
|
2923
|
+
snapshots++;
|
|
2924
|
+
if (window.resets_at) {
|
|
2925
|
+
upsertUsageSnapshot(db, {
|
|
2926
|
+
agent: "claude",
|
|
2927
|
+
date: today,
|
|
2928
|
+
metric: `${metric}_resets_at`,
|
|
2929
|
+
value: Date.parse(window.resets_at),
|
|
2930
|
+
unit: "epoch_ms",
|
|
2931
|
+
machine_id: machineId
|
|
2932
|
+
});
|
|
2933
|
+
snapshots++;
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
if (usage.extra_usage?.spend != null) {
|
|
2937
|
+
upsertUsageSnapshot(db, {
|
|
2938
|
+
agent: "claude",
|
|
2939
|
+
date: today,
|
|
2940
|
+
metric: "on_demand_usd",
|
|
2941
|
+
value: usage.extra_usage.spend,
|
|
2942
|
+
unit: "usd",
|
|
2943
|
+
machine_id: machineId
|
|
2944
|
+
});
|
|
2945
|
+
snapshots++;
|
|
2946
|
+
}
|
|
2947
|
+
if (usage.extra_usage?.limit != null) {
|
|
2948
|
+
upsertUsageSnapshot(db, {
|
|
2949
|
+
agent: "claude",
|
|
2950
|
+
date: today,
|
|
2951
|
+
metric: "on_demand_limit_usd",
|
|
2952
|
+
value: usage.extra_usage.limit,
|
|
2953
|
+
unit: "usd",
|
|
2954
|
+
machine_id: machineId
|
|
2955
|
+
});
|
|
2956
|
+
snapshots++;
|
|
2957
|
+
}
|
|
2958
|
+
const monthlyFee = inferMonthlyFee(auth.subscriptionType, auth.rateLimitTier);
|
|
2959
|
+
const now = new Date().toISOString();
|
|
2960
|
+
upsertSubscription(db, {
|
|
2961
|
+
id: "anthropic-claude-oauth",
|
|
2962
|
+
provider: "anthropic",
|
|
2963
|
+
agent: "claude",
|
|
2964
|
+
plan: auth.rateLimitTier ?? auth.subscriptionType ?? "claude_pro",
|
|
2965
|
+
monthly_fee_usd: monthlyFee,
|
|
2966
|
+
included_usage_usd: monthlyFee,
|
|
2967
|
+
billing_cycle_start: null,
|
|
2968
|
+
reset_policy: "monthly",
|
|
2969
|
+
active: 1,
|
|
2970
|
+
created_at: now,
|
|
2971
|
+
updated_at: now
|
|
2972
|
+
});
|
|
2973
|
+
setIngestState(db, "claude", `quota-${today}`, new Date().toISOString());
|
|
2974
|
+
if (verbose)
|
|
2975
|
+
console.log(` claude quota: ${snapshots} snapshots`);
|
|
2976
|
+
return { snapshots };
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// src/ingest/codex-quota.ts
|
|
2980
|
+
init_database();
|
|
2981
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
2982
|
+
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
2983
|
+
function readCodexAuth() {
|
|
2984
|
+
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
2985
|
+
if (fromEnv)
|
|
2986
|
+
return { token: fromEnv, authMode: "chatgpt" };
|
|
2987
|
+
const authPath = agentPaths().codexAuth;
|
|
2988
|
+
if (!existsSync9(authPath))
|
|
2989
|
+
return null;
|
|
2990
|
+
try {
|
|
2991
|
+
const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
|
|
2992
|
+
const token = auth.tokens?.access_token;
|
|
2993
|
+
if (!token)
|
|
2994
|
+
return null;
|
|
2995
|
+
return {
|
|
2996
|
+
token,
|
|
2997
|
+
accountId: auth.tokens?.account_id,
|
|
2998
|
+
authMode: auth.auth_mode ?? "chatgpt"
|
|
2999
|
+
};
|
|
3000
|
+
} catch {
|
|
3001
|
+
return null;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
function planMonthlyFee(planType) {
|
|
3005
|
+
const plan = (planType ?? "").toLowerCase();
|
|
3006
|
+
if (plan.includes("pro"))
|
|
3007
|
+
return 200;
|
|
3008
|
+
if (plan.includes("plus"))
|
|
3009
|
+
return 20;
|
|
3010
|
+
if (plan.includes("team"))
|
|
3011
|
+
return 30;
|
|
3012
|
+
return 20;
|
|
3013
|
+
}
|
|
3014
|
+
async function fetchCodexUsage(token, accountId) {
|
|
3015
|
+
try {
|
|
3016
|
+
const headers = {
|
|
3017
|
+
Authorization: `Bearer ${token}`,
|
|
3018
|
+
Accept: "application/json",
|
|
3019
|
+
"User-Agent": "economy-cli"
|
|
3020
|
+
};
|
|
3021
|
+
if (accountId)
|
|
3022
|
+
headers["ChatGPT-Account-Id"] = accountId;
|
|
3023
|
+
const res = await fetch(WHAM_USAGE_URL, {
|
|
3024
|
+
headers,
|
|
3025
|
+
signal: AbortSignal.timeout(1e4)
|
|
3026
|
+
});
|
|
3027
|
+
if (!res.ok)
|
|
3028
|
+
return null;
|
|
3029
|
+
return await res.json();
|
|
3030
|
+
} catch {
|
|
3031
|
+
return null;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
async function ingestCodexQuota(db, verbose = false) {
|
|
3035
|
+
const auth = readCodexAuth();
|
|
3036
|
+
if (!auth) {
|
|
3037
|
+
if (verbose)
|
|
3038
|
+
console.log(" codex quota: skipped \u2014 no ~/.codex/auth.json or CODEX_OAUTH_TOKEN");
|
|
3039
|
+
return { snapshots: 0 };
|
|
3040
|
+
}
|
|
3041
|
+
if (auth.authMode === "api_key" || auth.authMode === "api") {
|
|
3042
|
+
if (verbose)
|
|
3043
|
+
console.log(" codex quota: skipped \u2014 API key mode (no subscription quota)");
|
|
3044
|
+
return { snapshots: 0 };
|
|
3045
|
+
}
|
|
3046
|
+
const today = new Date().toISOString().substring(0, 10);
|
|
3047
|
+
const prev = getIngestState(db, "codex", `quota-${today}`);
|
|
3048
|
+
if (prev)
|
|
3049
|
+
return { snapshots: 0 };
|
|
3050
|
+
const usage = await fetchCodexUsage(auth.token, auth.accountId);
|
|
3051
|
+
if (!usage) {
|
|
3052
|
+
if (verbose)
|
|
3053
|
+
console.log(" codex quota: wham/usage endpoint unavailable");
|
|
3054
|
+
return { snapshots: 0 };
|
|
3055
|
+
}
|
|
3056
|
+
const machineId = getMachineId();
|
|
3057
|
+
let snapshots = 0;
|
|
3058
|
+
const now = new Date().toISOString();
|
|
3059
|
+
const windows = [
|
|
3060
|
+
["five_hour_utilization", usage.rate_limit?.primary_window],
|
|
3061
|
+
["seven_day_utilization", usage.rate_limit?.secondary_window]
|
|
3062
|
+
];
|
|
3063
|
+
for (const [metric, window] of windows) {
|
|
3064
|
+
if (window?.used_percent == null)
|
|
3065
|
+
continue;
|
|
3066
|
+
upsertUsageSnapshot(db, {
|
|
3067
|
+
agent: "codex",
|
|
3068
|
+
date: today,
|
|
3069
|
+
metric,
|
|
3070
|
+
value: window.used_percent,
|
|
3071
|
+
unit: "percent",
|
|
3072
|
+
machine_id: machineId
|
|
3073
|
+
});
|
|
3074
|
+
snapshots++;
|
|
3075
|
+
if (window.reset_at) {
|
|
3076
|
+
upsertUsageSnapshot(db, {
|
|
3077
|
+
agent: "codex",
|
|
3078
|
+
date: today,
|
|
3079
|
+
metric: `${metric}_resets_at`,
|
|
3080
|
+
value: window.reset_at * 1000,
|
|
3081
|
+
unit: "epoch_ms",
|
|
3082
|
+
machine_id: machineId
|
|
3083
|
+
});
|
|
3084
|
+
snapshots++;
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
if (usage.credits?.balance != null) {
|
|
3088
|
+
upsertUsageSnapshot(db, {
|
|
3089
|
+
agent: "codex",
|
|
3090
|
+
date: today,
|
|
3091
|
+
metric: "credits_balance_usd",
|
|
3092
|
+
value: usage.credits.balance,
|
|
3093
|
+
unit: "usd",
|
|
3094
|
+
machine_id: machineId
|
|
3095
|
+
});
|
|
3096
|
+
snapshots++;
|
|
3097
|
+
}
|
|
3098
|
+
const monthlyFee = planMonthlyFee(usage.plan_type);
|
|
3099
|
+
upsertSubscription(db, {
|
|
3100
|
+
id: "openai-codex-oauth",
|
|
3101
|
+
provider: "openai",
|
|
3102
|
+
agent: "codex",
|
|
3103
|
+
plan: usage.plan_type ?? "chatgpt_plus",
|
|
3104
|
+
monthly_fee_usd: monthlyFee,
|
|
3105
|
+
included_usage_usd: monthlyFee,
|
|
3106
|
+
billing_cycle_start: null,
|
|
3107
|
+
reset_policy: "monthly",
|
|
3108
|
+
active: 1,
|
|
3109
|
+
created_at: now,
|
|
3110
|
+
updated_at: now
|
|
3111
|
+
});
|
|
3112
|
+
setIngestState(db, "codex", `quota-${today}`, now);
|
|
3113
|
+
if (verbose)
|
|
3114
|
+
console.log(` codex quota: ${snapshots} snapshots (${usage.plan_type ?? "unknown plan"})`);
|
|
3115
|
+
return { snapshots };
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
// src/lib/sync-all.ts
|
|
3119
|
+
init_database();
|
|
3120
|
+
|
|
3121
|
+
// src/lib/cloud-sync.ts
|
|
3122
|
+
init_database();
|
|
3123
|
+
|
|
3124
|
+
// src/lib/package-metadata.ts
|
|
3125
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3126
|
+
var cachedMetadata = null;
|
|
3127
|
+
function getPackageMetadata() {
|
|
3128
|
+
if (cachedMetadata)
|
|
3129
|
+
return cachedMetadata;
|
|
3130
|
+
const raw = readFileSync9(new URL("../../package.json", import.meta.url), "utf8");
|
|
3131
|
+
const parsed = JSON.parse(raw);
|
|
3132
|
+
cachedMetadata = {
|
|
3133
|
+
name: parsed.name ?? "@hasna/economy",
|
|
3134
|
+
version: parsed.version ?? "0.0.0"
|
|
3135
|
+
};
|
|
3136
|
+
return cachedMetadata;
|
|
3137
|
+
}
|
|
3138
|
+
var packageMetadata = getPackageMetadata();
|
|
3139
|
+
|
|
3140
|
+
// src/lib/cloud-sync.ts
|
|
3141
|
+
var CLOUD_TABLES = [
|
|
3142
|
+
"requests",
|
|
3143
|
+
"sessions",
|
|
3144
|
+
"projects",
|
|
3145
|
+
"budgets",
|
|
3146
|
+
"goals",
|
|
3147
|
+
"model_pricing",
|
|
3148
|
+
"billing_daily",
|
|
3149
|
+
"subscriptions",
|
|
3150
|
+
"usage_snapshots",
|
|
3151
|
+
"savings_daily",
|
|
3152
|
+
"machines",
|
|
3153
|
+
"ingest_state"
|
|
3154
|
+
];
|
|
3155
|
+
function getCloudDatabaseUrl() {
|
|
3156
|
+
return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
|
|
3157
|
+
}
|
|
3158
|
+
function isCloudAutoEnabled() {
|
|
3159
|
+
return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
|
|
3160
|
+
}
|
|
3161
|
+
function getCloudPullIntervalMinutes() {
|
|
3162
|
+
const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
|
|
3163
|
+
if (!raw)
|
|
3164
|
+
return 15;
|
|
3165
|
+
const n = Number(raw);
|
|
3166
|
+
return Number.isFinite(n) && n > 0 ? n : 15;
|
|
3167
|
+
}
|
|
3168
|
+
async function getCloudPg() {
|
|
3169
|
+
const url = getCloudDatabaseUrl();
|
|
3170
|
+
if (!url) {
|
|
3171
|
+
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
3172
|
+
}
|
|
3173
|
+
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
3174
|
+
return new PgAdapterAsync(url);
|
|
3175
|
+
}
|
|
3176
|
+
async function runCloudMigrations(cloud) {
|
|
3177
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3178
|
+
for (const sql of PG_MIGRATIONS2) {
|
|
3179
|
+
await cloud.run(sql);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
function isCloudIncrementalEnabled() {
|
|
3183
|
+
return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
|
|
3184
|
+
}
|
|
3185
|
+
async function cloudPush(opts) {
|
|
3186
|
+
const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
|
|
3187
|
+
const cloud = await getCloudPg();
|
|
3188
|
+
const local = new SqliteAdapter(getDbPath());
|
|
3189
|
+
await runCloudMigrations(cloud);
|
|
3190
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3191
|
+
let rows = 0;
|
|
3192
|
+
if (isCloudIncrementalEnabled()) {
|
|
3193
|
+
ensureSyncMetaTable(local);
|
|
3194
|
+
const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
|
|
3195
|
+
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
3196
|
+
} else {
|
|
3197
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3198
|
+
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3199
|
+
}
|
|
3200
|
+
touchMachineRegistry(local, "push");
|
|
3201
|
+
local.close();
|
|
3202
|
+
await cloud.close();
|
|
3203
|
+
return { rows, machine: getMachineId() };
|
|
3204
|
+
}
|
|
3205
|
+
async function cloudPull(opts) {
|
|
3206
|
+
const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
|
|
3207
|
+
const cloud = await getCloudPg();
|
|
3208
|
+
const local = new SqliteAdapter(getDbPath());
|
|
3209
|
+
await runCloudMigrations(cloud);
|
|
3210
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3211
|
+
let rows = 0;
|
|
3212
|
+
if (isCloudIncrementalEnabled()) {
|
|
3213
|
+
ensureSyncMetaTable(local);
|
|
3214
|
+
const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
|
|
3215
|
+
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
3216
|
+
} else {
|
|
3217
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3218
|
+
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3219
|
+
}
|
|
3220
|
+
touchMachineRegistry(local, "pull");
|
|
3221
|
+
local.close();
|
|
3222
|
+
await cloud.close();
|
|
3223
|
+
setLastCloudPull();
|
|
3224
|
+
return { rows, machine: getMachineId() };
|
|
3225
|
+
}
|
|
3226
|
+
function setLastCloudPull(at = new Date().toISOString()) {
|
|
3227
|
+
const db = openDatabase();
|
|
3228
|
+
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
3229
|
+
}
|
|
3230
|
+
function getLastCloudPull() {
|
|
3231
|
+
const db = openDatabase();
|
|
3232
|
+
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
3233
|
+
return row?.value ?? null;
|
|
3234
|
+
}
|
|
3235
|
+
function shouldPullFromCloud() {
|
|
3236
|
+
if (!getCloudDatabaseUrl())
|
|
3237
|
+
return false;
|
|
3238
|
+
const last = getLastCloudPull();
|
|
3239
|
+
if (!last)
|
|
3240
|
+
return true;
|
|
3241
|
+
const ageMs = Date.now() - new Date(last).getTime();
|
|
3242
|
+
return ageMs > getCloudPullIntervalMinutes() * 60000;
|
|
3243
|
+
}
|
|
3244
|
+
async function maybePullFromCloud() {
|
|
3245
|
+
if (!shouldPullFromCloud())
|
|
3246
|
+
return false;
|
|
3247
|
+
try {
|
|
3248
|
+
await cloudPull();
|
|
3249
|
+
return true;
|
|
3250
|
+
} catch {
|
|
3251
|
+
return false;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
async function maybePushAfterIngest() {
|
|
3255
|
+
if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
|
|
3256
|
+
return false;
|
|
3257
|
+
try {
|
|
3258
|
+
await cloudPush();
|
|
3259
|
+
return true;
|
|
3260
|
+
} catch {
|
|
3261
|
+
return false;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
function touchMachineRegistry(db, direction) {
|
|
3265
|
+
const now = new Date().toISOString();
|
|
3266
|
+
const machine = getMachineId();
|
|
3267
|
+
db.prepare(`
|
|
3268
|
+
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
3269
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3270
|
+
ON CONFLICT(machine_id) DO UPDATE SET
|
|
3271
|
+
hostname = excluded.hostname,
|
|
3272
|
+
last_seen_at = excluded.last_seen_at,
|
|
3273
|
+
last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
|
|
3274
|
+
last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
|
|
3275
|
+
economy_version = excluded.economy_version,
|
|
3276
|
+
updated_at = excluded.updated_at
|
|
3277
|
+
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
// src/lib/sync-all.ts
|
|
3281
|
+
async function syncAll(db, opts = {}) {
|
|
3282
|
+
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
3283
|
+
const all = !anySpecific;
|
|
3284
|
+
await maybePullFromCloud();
|
|
3285
|
+
const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
|
|
3286
|
+
if (all || opts.claude) {
|
|
3287
|
+
result.claude = await ingestClaude(db, opts.verbose);
|
|
3288
|
+
result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
|
|
3289
|
+
}
|
|
3290
|
+
if (all || opts.takumi)
|
|
3291
|
+
result.takumi = await ingestTakumi(db, opts.verbose);
|
|
3292
|
+
if (all || opts.codex) {
|
|
3293
|
+
result.codex = await ingestCodex(db, opts.verbose);
|
|
3294
|
+
result.codexQuota = await ingestCodexQuota(db, opts.verbose);
|
|
3295
|
+
}
|
|
3296
|
+
if (all || opts.gemini)
|
|
3297
|
+
result.gemini = await ingestGemini(db, opts.verbose);
|
|
3298
|
+
if (all || opts.opencode)
|
|
3299
|
+
result.opencode = await ingestOpenCode(db, opts.verbose);
|
|
3300
|
+
if (all || opts.cursor)
|
|
3301
|
+
result.cursor = await ingestCursor(db, opts.verbose);
|
|
3302
|
+
if (all || opts.pi)
|
|
3303
|
+
result.pi = await ingestPi(db, opts.verbose);
|
|
3304
|
+
if (all || opts.hermes)
|
|
3305
|
+
result.hermes = await ingestHermes(db, opts.verbose);
|
|
3306
|
+
result.deduped = dedupeRequests(db);
|
|
3307
|
+
result.cloudPushed = await maybePushAfterIngest();
|
|
3308
|
+
return result;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
// src/lib/billing-diff.ts
|
|
3312
|
+
init_database();
|
|
3313
|
+
var PROVIDER_TO_AGENT = {
|
|
3314
|
+
anthropic: "claude",
|
|
3315
|
+
openai: "codex",
|
|
3316
|
+
gemini: "gemini",
|
|
3317
|
+
google: "gemini"
|
|
3318
|
+
};
|
|
3319
|
+
function queryBillingDiff(db, period, thresholdPct = 15) {
|
|
3320
|
+
const estimated = querySummary(db, period, undefined, true);
|
|
3321
|
+
const actual = queryBillingSummary(db, period);
|
|
3322
|
+
const delta = estimated.total_usd - actual.total_usd;
|
|
3323
|
+
const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
|
|
3324
|
+
const agentRows = db.prepare(`
|
|
3325
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
|
|
3326
|
+
FROM requests
|
|
3327
|
+
WHERE ${periodWhere3(period, "timestamp")}
|
|
3328
|
+
GROUP BY agent
|
|
3329
|
+
`).all();
|
|
3330
|
+
const by_agent = agentRows.map((row) => {
|
|
3331
|
+
const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
|
|
3332
|
+
const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
|
|
3333
|
+
const rowDelta = row.estimated_usd - actualUsd;
|
|
3334
|
+
const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
|
|
3335
|
+
return {
|
|
3336
|
+
agent: row.agent,
|
|
3337
|
+
estimated_usd: row.estimated_usd,
|
|
3338
|
+
actual_usd: actualUsd,
|
|
3339
|
+
delta_usd: rowDelta,
|
|
3340
|
+
delta_pct: rowPct
|
|
3341
|
+
};
|
|
3342
|
+
}).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
|
|
3343
|
+
return {
|
|
3344
|
+
period,
|
|
3345
|
+
estimated_usd: estimated.total_usd,
|
|
3346
|
+
actual_usd: actual.total_usd,
|
|
3347
|
+
delta_usd: delta,
|
|
3348
|
+
delta_pct: deltaPct,
|
|
3349
|
+
threshold_pct: thresholdPct,
|
|
3350
|
+
is_alert: Math.abs(deltaPct) > thresholdPct,
|
|
3351
|
+
by_agent,
|
|
3352
|
+
by_provider: actual.by_provider
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
function periodWhere3(period, column) {
|
|
3356
|
+
switch (period) {
|
|
3357
|
+
case "today":
|
|
3358
|
+
return `DATE(${column}) = DATE('now')`;
|
|
3359
|
+
case "yesterday":
|
|
3360
|
+
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
3361
|
+
case "week":
|
|
3362
|
+
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
3363
|
+
case "month":
|
|
3364
|
+
return `${column} >= DATE('now', 'start of month')`;
|
|
3365
|
+
case "year":
|
|
3366
|
+
return `${column} >= DATE('now', 'start of year')`;
|
|
3367
|
+
case "all":
|
|
3368
|
+
return "1=1";
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
// src/server/serve.ts
|
|
3373
|
+
init_database();
|
|
3374
|
+
|
|
3375
|
+
// src/lib/serve-auth.ts
|
|
3376
|
+
function getServeApiToken() {
|
|
3377
|
+
return process.env["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
|
|
3378
|
+
}
|
|
3379
|
+
function getServeBindHost() {
|
|
3380
|
+
const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
|
|
3381
|
+
if (explicit)
|
|
3382
|
+
return explicit;
|
|
3383
|
+
return getServeApiToken() ? "127.0.0.1" : "0.0.0.0";
|
|
3384
|
+
}
|
|
3385
|
+
function isAuthorizedRequest(req, path) {
|
|
3386
|
+
const token = getServeApiToken();
|
|
3387
|
+
if (!token)
|
|
3388
|
+
return true;
|
|
3389
|
+
if (path === "/health")
|
|
3390
|
+
return true;
|
|
3391
|
+
const auth = req.headers.get("Authorization");
|
|
3392
|
+
if (auth === `Bearer ${token}`)
|
|
3393
|
+
return true;
|
|
3394
|
+
if (req.headers.get("X-Economy-Token") === token)
|
|
3395
|
+
return true;
|
|
3396
|
+
return false;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// src/server/serve.ts
|
|
937
3400
|
import { randomUUID } from "crypto";
|
|
3401
|
+
import { existsSync as existsSync11 } from "fs";
|
|
3402
|
+
import { resolve, sep } from "path";
|
|
938
3403
|
var CORS = {
|
|
939
3404
|
"Access-Control-Allow-Origin": "*",
|
|
940
3405
|
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
941
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
3406
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
|
|
942
3407
|
};
|
|
3408
|
+
var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
3409
|
+
var SYNC_SOURCES = ["all", ...AGENTS];
|
|
3410
|
+
var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
943
3411
|
function json(data, status = 200) {
|
|
944
3412
|
return new Response(JSON.stringify(data), {
|
|
945
3413
|
status,
|
|
@@ -966,6 +3434,56 @@ function normalizeBudgetPeriod(value) {
|
|
|
966
3434
|
return "monthly";
|
|
967
3435
|
}
|
|
968
3436
|
}
|
|
3437
|
+
function finiteNumber(value) {
|
|
3438
|
+
const n = Number(value);
|
|
3439
|
+
return Number.isFinite(n) ? n : null;
|
|
3440
|
+
}
|
|
3441
|
+
async function jsonBody(req) {
|
|
3442
|
+
const body = await req.json().catch(() => null);
|
|
3443
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
3444
|
+
}
|
|
3445
|
+
function optionalString(value) {
|
|
3446
|
+
return typeof value === "string" ? value : null;
|
|
3447
|
+
}
|
|
3448
|
+
function optionalAgent(value) {
|
|
3449
|
+
if (value == null || value === "")
|
|
3450
|
+
return null;
|
|
3451
|
+
return typeof value === "string" && AGENTS.includes(value) ? value : undefined;
|
|
3452
|
+
}
|
|
3453
|
+
function stringArray(value) {
|
|
3454
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
3455
|
+
}
|
|
3456
|
+
function dashboardPath(root, pathname) {
|
|
3457
|
+
let decoded;
|
|
3458
|
+
try {
|
|
3459
|
+
decoded = decodeURIComponent(pathname);
|
|
3460
|
+
} catch {
|
|
3461
|
+
return null;
|
|
3462
|
+
}
|
|
3463
|
+
const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
3464
|
+
const rootPath = resolve(root);
|
|
3465
|
+
const filePath = resolve(rootPath, relativePath);
|
|
3466
|
+
return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
|
|
3467
|
+
}
|
|
3468
|
+
function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
3469
|
+
return async function fetch2(req) {
|
|
3470
|
+
const url = new URL(req.url);
|
|
3471
|
+
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
3472
|
+
return apiHandler(req);
|
|
3473
|
+
}
|
|
3474
|
+
if (existsSync11(dashboardDir)) {
|
|
3475
|
+
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
3476
|
+
if (filePath && existsSync11(filePath)) {
|
|
3477
|
+
return new Response(Bun.file(filePath));
|
|
3478
|
+
}
|
|
3479
|
+
const indexPath = dashboardPath(dashboardDir, "/");
|
|
3480
|
+
if (indexPath && existsSync11(indexPath)) {
|
|
3481
|
+
return new Response(Bun.file(indexPath));
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
return apiHandler(req);
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
969
3487
|
function applyFields(obj, fields) {
|
|
970
3488
|
if (!fields || fields.length === 0)
|
|
971
3489
|
return obj;
|
|
@@ -978,6 +3496,8 @@ function createHandler(db) {
|
|
|
978
3496
|
const method = req.method;
|
|
979
3497
|
if (method === "OPTIONS")
|
|
980
3498
|
return new Response(null, { status: 204, headers: CORS });
|
|
3499
|
+
if (!isAuthorizedRequest(req, path))
|
|
3500
|
+
return err("Unauthorized", 401);
|
|
981
3501
|
if (path === "/health")
|
|
982
3502
|
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
983
3503
|
if (path === "/api/summary" && method === "GET") {
|
|
@@ -988,6 +3508,15 @@ function createHandler(db) {
|
|
|
988
3508
|
if (path === "/api/machines" && method === "GET") {
|
|
989
3509
|
return ok(listMachines(db), { current_machine: getMachineId() });
|
|
990
3510
|
}
|
|
3511
|
+
if (path === "/api/fleet" && method === "GET") {
|
|
3512
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
3513
|
+
return ok({
|
|
3514
|
+
summary: querySummary(db, period, undefined, true),
|
|
3515
|
+
machines: listMachines(db),
|
|
3516
|
+
registry: listMachineRegistry(db),
|
|
3517
|
+
current_machine: getMachineId()
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
991
3520
|
if (path === "/api/daily" && method === "GET") {
|
|
992
3521
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
993
3522
|
return ok(queryDailyBreakdown(db, days));
|
|
@@ -1021,6 +3550,41 @@ function createHandler(db) {
|
|
|
1021
3550
|
if (path === "/api/models" && method === "GET") {
|
|
1022
3551
|
return ok(queryModelBreakdown(db));
|
|
1023
3552
|
}
|
|
3553
|
+
if (path === "/api/billing" && method === "GET") {
|
|
3554
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
3555
|
+
return ok(queryBillingSummary(db, period));
|
|
3556
|
+
}
|
|
3557
|
+
if (path === "/api/billing/diff" && method === "GET") {
|
|
3558
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
3559
|
+
const threshold = Number(url.searchParams.get("threshold") ?? 15);
|
|
3560
|
+
return ok(queryBillingDiff(db, period, Number.isFinite(threshold) ? threshold : 15));
|
|
3561
|
+
}
|
|
3562
|
+
if (path === "/api/billing/sync" && method === "POST") {
|
|
3563
|
+
const body = await jsonBody(req) ?? {};
|
|
3564
|
+
const days = Number(body["days"] ?? 31);
|
|
3565
|
+
if (!Number.isFinite(days) || days <= 0 || days > 366)
|
|
3566
|
+
return err("days must be between 1 and 366");
|
|
3567
|
+
const providers = Array.isArray(body["providers"]) ? body["providers"] : ["anthropic", "openai", "gemini"];
|
|
3568
|
+
const allowedProviders = new Set(["anthropic", "openai", "gemini"]);
|
|
3569
|
+
if (providers.some((provider) => !allowedProviders.has(provider)))
|
|
3570
|
+
return err("invalid billing provider");
|
|
3571
|
+
const results = {};
|
|
3572
|
+
const { syncAnthropicBilling: syncAnthropicBilling2, syncOpenAIBilling: syncOpenAIBilling2, syncGeminiBilling: syncGeminiBilling2 } = await Promise.resolve().then(() => (init_billing(), exports_billing));
|
|
3573
|
+
async function capture(provider, fn) {
|
|
3574
|
+
try {
|
|
3575
|
+
results[provider] = await fn();
|
|
3576
|
+
} catch (e) {
|
|
3577
|
+
results[provider] = { error: e instanceof Error ? e.message : String(e) };
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
if (providers.includes("anthropic"))
|
|
3581
|
+
await capture("anthropic", () => syncAnthropicBilling2(db, { days }));
|
|
3582
|
+
if (providers.includes("openai"))
|
|
3583
|
+
await capture("openai", () => syncOpenAIBilling2(db, { days }));
|
|
3584
|
+
if (providers.includes("gemini"))
|
|
3585
|
+
await capture("gemini", () => syncGeminiBilling2(db, { days }));
|
|
3586
|
+
return ok(results);
|
|
3587
|
+
}
|
|
1024
3588
|
if (path === "/api/projects" && method === "GET") {
|
|
1025
3589
|
return ok(queryProjectBreakdown(db));
|
|
1026
3590
|
}
|
|
@@ -1032,38 +3596,54 @@ function createHandler(db) {
|
|
|
1032
3596
|
return ok(getBudgetStatuses(db));
|
|
1033
3597
|
}
|
|
1034
3598
|
if (path === "/api/budgets" && method === "POST") {
|
|
1035
|
-
const body = await req
|
|
3599
|
+
const body = await jsonBody(req);
|
|
3600
|
+
if (!body)
|
|
3601
|
+
return err("invalid JSON body");
|
|
3602
|
+
const limitUsd = finiteNumber(body["limit_usd"]);
|
|
3603
|
+
const alertAtPercent = finiteNumber(body["alert_at_percent"] ?? 80);
|
|
3604
|
+
if (limitUsd == null || limitUsd <= 0)
|
|
3605
|
+
return err("limit_usd must be a positive number");
|
|
3606
|
+
if (alertAtPercent == null || alertAtPercent <= 0 || alertAtPercent > 100)
|
|
3607
|
+
return err("alert_at_percent must be between 1 and 100");
|
|
3608
|
+
const agent = optionalAgent(body["agent"]);
|
|
3609
|
+
if (agent === undefined)
|
|
3610
|
+
return err(AGENT_ERROR);
|
|
1036
3611
|
const now = new Date().toISOString();
|
|
1037
|
-
|
|
3612
|
+
const budget = {
|
|
1038
3613
|
id: randomUUID(),
|
|
1039
3614
|
project_path: body["project_path"] ?? null,
|
|
1040
|
-
agent
|
|
3615
|
+
agent,
|
|
1041
3616
|
period: normalizeBudgetPeriod(body["period"]),
|
|
1042
|
-
limit_usd:
|
|
1043
|
-
alert_at_percent:
|
|
3617
|
+
limit_usd: limitUsd,
|
|
3618
|
+
alert_at_percent: alertAtPercent,
|
|
1044
3619
|
created_at: now,
|
|
1045
3620
|
updated_at: now
|
|
1046
|
-
}
|
|
1047
|
-
|
|
3621
|
+
};
|
|
3622
|
+
upsertBudget(db, budget);
|
|
3623
|
+
return ok(getBudgetStatuses(db).find((b) => b.id === budget.id) ?? budget);
|
|
1048
3624
|
}
|
|
1049
3625
|
const budgetMatch = path.match(/^\/api\/budgets\/(.+)$/);
|
|
1050
3626
|
if (budgetMatch && method === "DELETE") {
|
|
1051
|
-
deleteBudget(db, budgetMatch[1]);
|
|
3627
|
+
deleteBudget(db, decodeURIComponent(budgetMatch[1]));
|
|
1052
3628
|
return ok({ ok: true });
|
|
1053
3629
|
}
|
|
1054
3630
|
if (path === "/api/project-registry" && method === "GET") {
|
|
1055
3631
|
return ok(listProjects(db));
|
|
1056
3632
|
}
|
|
1057
3633
|
if (path === "/api/project-registry" && method === "POST") {
|
|
1058
|
-
const body = await req
|
|
1059
|
-
|
|
1060
|
-
|
|
3634
|
+
const body = await jsonBody(req);
|
|
3635
|
+
if (!body)
|
|
3636
|
+
return err("invalid JSON body");
|
|
3637
|
+
const { basename: basename4 } = await import("path");
|
|
3638
|
+
const projPath = optionalString(body["path"])?.trim();
|
|
3639
|
+
if (!projPath)
|
|
3640
|
+
return err("path is required");
|
|
1061
3641
|
upsertProject(db, {
|
|
1062
3642
|
id: randomUUID(),
|
|
1063
3643
|
path: projPath,
|
|
1064
|
-
name: body["name"] ??
|
|
1065
|
-
description: body["description"]
|
|
1066
|
-
tags: body["tags"]
|
|
3644
|
+
name: optionalString(body["name"]) ?? basename4(projPath),
|
|
3645
|
+
description: optionalString(body["description"]),
|
|
3646
|
+
tags: stringArray(body["tags"]),
|
|
1067
3647
|
created_at: new Date().toISOString()
|
|
1068
3648
|
});
|
|
1069
3649
|
return ok({ ok: true });
|
|
@@ -1077,16 +3657,33 @@ function createHandler(db) {
|
|
|
1077
3657
|
return ok(listModelPricing(db));
|
|
1078
3658
|
}
|
|
1079
3659
|
if (path === "/api/pricing" && method === "POST") {
|
|
1080
|
-
const body = await req
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
3660
|
+
const body = await jsonBody(req);
|
|
3661
|
+
if (!body)
|
|
3662
|
+
return err("invalid JSON body");
|
|
3663
|
+
const model = String(body["model"] ?? "").trim();
|
|
3664
|
+
if (!model)
|
|
3665
|
+
return err("model is required");
|
|
3666
|
+
const input = finiteNumber(body["input_per_1m"]);
|
|
3667
|
+
const output = finiteNumber(body["output_per_1m"]);
|
|
3668
|
+
const cacheRead = finiteNumber(body["cache_read_per_1m"] ?? 0);
|
|
3669
|
+
const cacheWrite = finiteNumber(body["cache_write_per_1m"] ?? 0);
|
|
3670
|
+
const cacheWrite1h = finiteNumber(body["cache_write_1h_per_1m"] ?? 0);
|
|
3671
|
+
const cacheStorage = finiteNumber(body["cache_storage_per_1m_hour"] ?? 0);
|
|
3672
|
+
if ([input, output, cacheRead, cacheWrite, cacheWrite1h, cacheStorage].some((v) => v == null || v < 0)) {
|
|
3673
|
+
return err("pricing values must be non-negative numbers");
|
|
3674
|
+
}
|
|
3675
|
+
const pricing = {
|
|
3676
|
+
model,
|
|
3677
|
+
input_per_1m: input,
|
|
3678
|
+
output_per_1m: output,
|
|
3679
|
+
cache_read_per_1m: cacheRead,
|
|
3680
|
+
cache_write_per_1m: cacheWrite,
|
|
3681
|
+
cache_write_1h_per_1m: cacheWrite1h,
|
|
3682
|
+
cache_storage_per_1m_hour: cacheStorage,
|
|
1087
3683
|
updated_at: new Date().toISOString()
|
|
1088
|
-
}
|
|
1089
|
-
|
|
3684
|
+
};
|
|
3685
|
+
upsertModelPricing(db, pricing);
|
|
3686
|
+
return ok(pricing);
|
|
1090
3687
|
}
|
|
1091
3688
|
const pricingMatch = path.match(/^\/api\/pricing\/(.+)$/);
|
|
1092
3689
|
if (pricingMatch && method === "DELETE") {
|
|
@@ -1094,22 +3691,43 @@ function createHandler(db) {
|
|
|
1094
3691
|
return ok({ ok: true });
|
|
1095
3692
|
}
|
|
1096
3693
|
if (path === "/api/sync" && method === "POST") {
|
|
1097
|
-
const body = await req
|
|
3694
|
+
const body = await jsonBody(req) ?? {};
|
|
1098
3695
|
const sources = body["sources"] ?? "all";
|
|
3696
|
+
if (!SYNC_SOURCES.includes(sources))
|
|
3697
|
+
return err("invalid sync source");
|
|
1099
3698
|
const results = {};
|
|
1100
|
-
if (sources === "all"
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
3699
|
+
if (sources === "all") {
|
|
3700
|
+
try {
|
|
3701
|
+
const { syncOpenProjectsRegistry: syncOpenProjectsRegistry2 } = await Promise.resolve().then(() => (init_open_projects(), exports_open_projects));
|
|
3702
|
+
results["projects"] = await syncOpenProjectsRegistry2(db);
|
|
3703
|
+
} catch {}
|
|
3704
|
+
}
|
|
3705
|
+
const selected = sources === "all" ? {} : { [sources]: true };
|
|
3706
|
+
const syncResult = await syncAll(db, selected);
|
|
3707
|
+
Object.assign(results, syncResult);
|
|
3708
|
+
try {
|
|
3709
|
+
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
3710
|
+
await checkAndFireWebhooks2(db);
|
|
3711
|
+
} catch {}
|
|
1108
3712
|
return ok(results);
|
|
1109
3713
|
}
|
|
3714
|
+
if (path === "/api/usage" && method === "GET") {
|
|
3715
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
3716
|
+
const agent = url.searchParams.get("agent") ?? undefined;
|
|
3717
|
+
const since = period === "month" ? new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().substring(0, 10) : undefined;
|
|
3718
|
+
return ok({
|
|
3719
|
+
snapshots: queryUsageSnapshots(db, { agent: agent && isAgent(agent) ? agent : undefined, since }),
|
|
3720
|
+
summary: querySummary(db, period, undefined, true)
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
if (path === "/api/savings" && method === "GET") {
|
|
3724
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
3725
|
+
const agent = url.searchParams.get("agent") ?? undefined;
|
|
3726
|
+
return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
|
|
3727
|
+
}
|
|
1110
3728
|
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
1111
3729
|
if (sessionRequestsMatch && method === "GET") {
|
|
1112
|
-
const sessionId = sessionRequestsMatch[1];
|
|
3730
|
+
const sessionId = decodeURIComponent(sessionRequestsMatch[1]);
|
|
1113
3731
|
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sessionId, `${sessionId}%`);
|
|
1114
3732
|
if (!session)
|
|
1115
3733
|
return err("Session not found", 404);
|
|
@@ -1120,75 +3738,55 @@ function createHandler(db) {
|
|
|
1120
3738
|
return ok(getGoalStatuses(db));
|
|
1121
3739
|
}
|
|
1122
3740
|
if (path === "/api/goals" && method === "POST") {
|
|
1123
|
-
const body = await req
|
|
3741
|
+
const body = await jsonBody(req);
|
|
3742
|
+
if (!body)
|
|
3743
|
+
return err("invalid JSON body");
|
|
3744
|
+
const period = body["period"] ?? "month";
|
|
3745
|
+
if (!["day", "week", "month", "year"].includes(String(period)))
|
|
3746
|
+
return err("period must be day, week, month, or year");
|
|
3747
|
+
const limitUsd = finiteNumber(body["limit_usd"]);
|
|
3748
|
+
if (limitUsd == null || limitUsd <= 0)
|
|
3749
|
+
return err("limit_usd must be a positive number");
|
|
3750
|
+
const agent = optionalAgent(body["agent"]);
|
|
3751
|
+
if (agent === undefined)
|
|
3752
|
+
return err(AGENT_ERROR);
|
|
1124
3753
|
const now = new Date().toISOString();
|
|
1125
|
-
|
|
3754
|
+
const goal = {
|
|
1126
3755
|
id: randomUUID(),
|
|
1127
|
-
period
|
|
1128
|
-
project_path: body["project_path"]
|
|
1129
|
-
agent
|
|
1130
|
-
limit_usd:
|
|
3756
|
+
period,
|
|
3757
|
+
project_path: optionalString(body["project_path"]),
|
|
3758
|
+
agent,
|
|
3759
|
+
limit_usd: limitUsd,
|
|
1131
3760
|
created_at: now,
|
|
1132
3761
|
updated_at: now
|
|
1133
|
-
}
|
|
1134
|
-
|
|
3762
|
+
};
|
|
3763
|
+
upsertGoal(db, goal);
|
|
3764
|
+
return ok(getGoalStatuses(db).find((g) => g.id === goal.id) ?? goal);
|
|
1135
3765
|
}
|
|
1136
3766
|
const goalMatch = path.match(/^\/api\/goals\/(.+)$/);
|
|
1137
3767
|
if (goalMatch && method === "DELETE") {
|
|
1138
|
-
deleteGoal(db, goalMatch[1]);
|
|
3768
|
+
deleteGoal(db, decodeURIComponent(goalMatch[1]));
|
|
1139
3769
|
return ok({ ok: true });
|
|
1140
3770
|
}
|
|
1141
3771
|
return err("Not found", 404);
|
|
1142
3772
|
};
|
|
1143
3773
|
}
|
|
1144
|
-
function startServer(port = 3456) {
|
|
1145
|
-
const db = openDatabase();
|
|
3774
|
+
function startServer(port = 3456, options = {}) {
|
|
3775
|
+
const db = options.db ?? openDatabase();
|
|
1146
3776
|
ensurePricingSeeded(db);
|
|
1147
3777
|
const apiHandler = createHandler(db);
|
|
1148
|
-
const
|
|
1149
|
-
Bun.serve({
|
|
3778
|
+
const hostname2 = options.hostname ?? getServeBindHost();
|
|
3779
|
+
const server = Bun.serve({
|
|
1150
3780
|
port,
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
1154
|
-
return apiHandler(req);
|
|
1155
|
-
}
|
|
1156
|
-
try {
|
|
1157
|
-
const { existsSync: existsSync5 } = await import("fs");
|
|
1158
|
-
if (existsSync5(dashboardDir)) {
|
|
1159
|
-
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
1160
|
-
const fullPath = dashboardDir + filePath;
|
|
1161
|
-
if (existsSync5(fullPath)) {
|
|
1162
|
-
return new Response(Bun.file(fullPath));
|
|
1163
|
-
}
|
|
1164
|
-
const indexPath = dashboardDir + "/index.html";
|
|
1165
|
-
if (existsSync5(indexPath)) {
|
|
1166
|
-
return new Response(Bun.file(indexPath));
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
} catch {}
|
|
1170
|
-
return apiHandler(req);
|
|
1171
|
-
}
|
|
3781
|
+
hostname: hostname2,
|
|
3782
|
+
fetch: createServerFetch(apiHandler, options.dashboardDir)
|
|
1172
3783
|
});
|
|
1173
|
-
|
|
3784
|
+
const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
|
|
3785
|
+
const log = options.log ?? console.log;
|
|
3786
|
+
log(`economy-serve listening on ${address}`);
|
|
3787
|
+
return server;
|
|
1174
3788
|
}
|
|
1175
3789
|
|
|
1176
|
-
// src/lib/package-metadata.ts
|
|
1177
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
1178
|
-
var cachedMetadata = null;
|
|
1179
|
-
function getPackageMetadata() {
|
|
1180
|
-
if (cachedMetadata)
|
|
1181
|
-
return cachedMetadata;
|
|
1182
|
-
const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
|
|
1183
|
-
const parsed = JSON.parse(raw);
|
|
1184
|
-
cachedMetadata = {
|
|
1185
|
-
name: parsed.name ?? "@hasna/economy",
|
|
1186
|
-
version: parsed.version ?? "0.0.0"
|
|
1187
|
-
};
|
|
1188
|
-
return cachedMetadata;
|
|
1189
|
-
}
|
|
1190
|
-
var packageMetadata = getPackageMetadata();
|
|
1191
|
-
|
|
1192
3790
|
// src/server/index.ts
|
|
1193
3791
|
function printHelp() {
|
|
1194
3792
|
console.log(`Usage: economy-serve [options]
|
|
@@ -1203,17 +3801,19 @@ Options:
|
|
|
1203
3801
|
function resolvePort(argv) {
|
|
1204
3802
|
for (let i = 0;i < argv.length; i++) {
|
|
1205
3803
|
const arg = argv[i];
|
|
1206
|
-
if (
|
|
1207
|
-
const
|
|
1208
|
-
if (!
|
|
1209
|
-
throw new Error(`Invalid port: ${
|
|
1210
|
-
|
|
1211
|
-
return value2;
|
|
3804
|
+
if (arg === "--port" || arg === "-p") {
|
|
3805
|
+
const raw = argv[i + 1];
|
|
3806
|
+
if (!raw)
|
|
3807
|
+
throw new Error(`Invalid port: ${raw ?? ""}`);
|
|
3808
|
+
return parsePort(raw, "port");
|
|
1212
3809
|
}
|
|
1213
3810
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
3811
|
+
return parsePort(process.env["ECONOMY_PORT"] ?? "3456", "ECONOMY_PORT");
|
|
3812
|
+
}
|
|
3813
|
+
function parsePort(raw, label) {
|
|
3814
|
+
const value = Number(raw);
|
|
3815
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
3816
|
+
throw new Error(`Invalid ${label}: ${raw}`);
|
|
1217
3817
|
}
|
|
1218
3818
|
return value;
|
|
1219
3819
|
}
|