@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/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = import.meta.require;
|
|
17
18
|
|
|
18
19
|
// src/lib/pricing.ts
|
|
19
20
|
var exports_pricing = {};
|
|
@@ -27,99 +28,486 @@ __export(exports_pricing, {
|
|
|
27
28
|
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
28
29
|
});
|
|
29
30
|
function normalizeModelName(raw) {
|
|
30
|
-
return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "")
|
|
31
|
+
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
32
|
+
}
|
|
33
|
+
function normalizeModelNamePreservingProvider(raw) {
|
|
34
|
+
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
35
|
+
}
|
|
36
|
+
function modelLookupKeys(raw) {
|
|
37
|
+
const withProvider = normalizeModelNamePreservingProvider(raw);
|
|
38
|
+
const withoutProvider = normalizeModelName(raw);
|
|
39
|
+
return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
|
|
40
|
+
}
|
|
41
|
+
function bestPrefixMatch(normalized, entries) {
|
|
42
|
+
let best = null;
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const [key] = entry;
|
|
45
|
+
if (normalized !== key && !normalized.startsWith(`${key}-`))
|
|
46
|
+
continue;
|
|
47
|
+
if (!best || key.length > best[0].length)
|
|
48
|
+
best = entry;
|
|
49
|
+
}
|
|
50
|
+
return best?.[1] ?? null;
|
|
51
|
+
}
|
|
52
|
+
function bestModelMatch(model, entries) {
|
|
53
|
+
for (const key of modelLookupKeys(model)) {
|
|
54
|
+
const match = bestPrefixMatch(key, entries);
|
|
55
|
+
if (match)
|
|
56
|
+
return match;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function exactModelMatch(model, entries) {
|
|
61
|
+
for (const key of modelLookupKeys(model)) {
|
|
62
|
+
const match = entries.find(([entryKey]) => entryKey === key);
|
|
63
|
+
if (match)
|
|
64
|
+
return match[1];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
31
67
|
}
|
|
32
68
|
function ensurePricingSeeded(db) {
|
|
33
69
|
seedModelPricing(db, DEFAULT_PRICING);
|
|
70
|
+
repairLegacySeededPricing(db);
|
|
71
|
+
repairMissingDefaultCacheWrite1h(db);
|
|
72
|
+
repairMissingDefaultCacheStorage(db);
|
|
73
|
+
removeDeprecatedDefaultPricing(db);
|
|
74
|
+
}
|
|
75
|
+
function repairLegacySeededPricing(db) {
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
const legacyModels = new Set([
|
|
78
|
+
...Object.keys(LEGACY_DEFAULT_PRICING),
|
|
79
|
+
...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
|
|
80
|
+
]);
|
|
81
|
+
for (const model of legacyModels) {
|
|
82
|
+
const current = getModelPricing(db, model);
|
|
83
|
+
const next = DEFAULT_PRICING[model];
|
|
84
|
+
if (!current || !next)
|
|
85
|
+
continue;
|
|
86
|
+
const legacy = LEGACY_DEFAULT_PRICING[model];
|
|
87
|
+
const legacyRows = [
|
|
88
|
+
...legacy ? [legacy] : [],
|
|
89
|
+
...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
|
|
90
|
+
];
|
|
91
|
+
if (!legacyRows.some((row) => samePricing(current, row)))
|
|
92
|
+
continue;
|
|
93
|
+
upsertModelPricing(db, {
|
|
94
|
+
model,
|
|
95
|
+
input_per_1m: next.inputPer1M,
|
|
96
|
+
output_per_1m: next.outputPer1M,
|
|
97
|
+
cache_read_per_1m: next.cacheReadPer1M,
|
|
98
|
+
cache_write_per_1m: next.cacheWritePer1M,
|
|
99
|
+
cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
|
|
100
|
+
cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
|
|
101
|
+
updated_at: now
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function repairMissingDefaultCacheWrite1h(db) {
|
|
106
|
+
const now = new Date().toISOString();
|
|
107
|
+
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
108
|
+
if (!next.cacheWrite1hPer1M)
|
|
109
|
+
continue;
|
|
110
|
+
const current = getModelPricing(db, model);
|
|
111
|
+
if (!current)
|
|
112
|
+
continue;
|
|
113
|
+
if ((current.cache_write_1h_per_1m ?? 0) !== 0)
|
|
114
|
+
continue;
|
|
115
|
+
if (!sameBasePricing(current, next))
|
|
116
|
+
continue;
|
|
117
|
+
upsertModelPricing(db, {
|
|
118
|
+
model,
|
|
119
|
+
input_per_1m: current.input_per_1m,
|
|
120
|
+
output_per_1m: current.output_per_1m,
|
|
121
|
+
cache_read_per_1m: current.cache_read_per_1m,
|
|
122
|
+
cache_write_per_1m: current.cache_write_per_1m,
|
|
123
|
+
cache_write_1h_per_1m: next.cacheWrite1hPer1M,
|
|
124
|
+
cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
|
|
125
|
+
updated_at: now
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function repairMissingDefaultCacheStorage(db) {
|
|
130
|
+
const now = new Date().toISOString();
|
|
131
|
+
for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
|
|
132
|
+
if (!next.cacheStoragePer1MHour)
|
|
133
|
+
continue;
|
|
134
|
+
const current = getModelPricing(db, model);
|
|
135
|
+
if (!current)
|
|
136
|
+
continue;
|
|
137
|
+
if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
|
|
138
|
+
continue;
|
|
139
|
+
if (!sameBasePricing(current, next))
|
|
140
|
+
continue;
|
|
141
|
+
upsertModelPricing(db, {
|
|
142
|
+
model,
|
|
143
|
+
input_per_1m: current.input_per_1m,
|
|
144
|
+
output_per_1m: current.output_per_1m,
|
|
145
|
+
cache_read_per_1m: current.cache_read_per_1m,
|
|
146
|
+
cache_write_per_1m: current.cache_write_per_1m,
|
|
147
|
+
cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
|
|
148
|
+
cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
|
|
149
|
+
updated_at: now
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function removeDeprecatedDefaultPricing(db) {
|
|
154
|
+
for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
|
|
155
|
+
const current = getModelPricing(db, model);
|
|
156
|
+
if (!current)
|
|
157
|
+
continue;
|
|
158
|
+
if (!removedRows.some((row) => samePricing(current, row)))
|
|
159
|
+
continue;
|
|
160
|
+
deleteModelPricing(db, model);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function sameBasePricing(row, pricing) {
|
|
164
|
+
return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M;
|
|
165
|
+
}
|
|
166
|
+
function samePricing(row, pricing) {
|
|
167
|
+
return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M && (row.cache_write_1h_per_1m ?? 0) === (pricing.cacheWrite1hPer1M ?? 0) && (row.cache_storage_per_1m_hour ?? 0) === (pricing.cacheStoragePer1MHour ?? 0);
|
|
34
168
|
}
|
|
35
169
|
function getPricingFromDb(db, model) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
43
|
-
cacheWritePer1M: row.cache_write_per_1m
|
|
44
|
-
};
|
|
170
|
+
if (isFreeModel(model))
|
|
171
|
+
return FREE_PRICING;
|
|
172
|
+
for (const key of modelLookupKeys(model)) {
|
|
173
|
+
const row = getModelPricing(db, key);
|
|
174
|
+
if (row)
|
|
175
|
+
return modelPricingFromDbRow(row);
|
|
45
176
|
}
|
|
46
177
|
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
178
|
+
const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
|
|
179
|
+
if (!match)
|
|
180
|
+
return null;
|
|
181
|
+
return modelPricingFromDbRow(match);
|
|
182
|
+
}
|
|
183
|
+
function modelPricingFromDbRow(row) {
|
|
184
|
+
const seeded = DEFAULT_PRICING[row.model];
|
|
185
|
+
const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
|
|
186
|
+
return {
|
|
187
|
+
inputPer1M: row.input_per_1m,
|
|
188
|
+
outputPer1M: row.output_per_1m,
|
|
189
|
+
cacheReadPer1M: row.cache_read_per_1m,
|
|
190
|
+
cacheWritePer1M: row.cache_write_per_1m,
|
|
191
|
+
cacheWrite1hPer1M,
|
|
192
|
+
cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
|
|
193
|
+
};
|
|
53
194
|
}
|
|
54
195
|
function getPricing(model) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
196
|
+
if (isFreeModel(model))
|
|
197
|
+
return FREE_PRICING;
|
|
198
|
+
return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
|
|
199
|
+
}
|
|
200
|
+
function isFreeModel(model) {
|
|
201
|
+
return model.trim().toLowerCase().endsWith(":free");
|
|
63
202
|
}
|
|
64
|
-
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
203
|
+
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
65
204
|
const pricing = getPricing(model);
|
|
66
205
|
if (!pricing)
|
|
67
206
|
return 0;
|
|
68
|
-
return (
|
|
207
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
69
208
|
}
|
|
70
|
-
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
209
|
+
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
71
210
|
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
72
211
|
if (!pricing)
|
|
73
212
|
return 0;
|
|
74
|
-
return (
|
|
213
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
214
|
+
}
|
|
215
|
+
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
216
|
+
if (isFreeModel(model))
|
|
217
|
+
return 0;
|
|
218
|
+
let effective = pricing;
|
|
219
|
+
const promptTier = bestModelMatch(model, Object.entries(GEMINI_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(QWEN_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(MINIMAX_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(XAI_PROMPT_TIERS)) ?? exactModelMatch(model, Object.entries(OPENAI_PROMPT_TIERS));
|
|
220
|
+
if (promptTier) {
|
|
221
|
+
const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
|
|
222
|
+
if (billablePromptTokens > promptTier.threshold) {
|
|
223
|
+
effective = {
|
|
224
|
+
...pricing,
|
|
225
|
+
inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
|
|
226
|
+
outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
|
|
227
|
+
cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
|
|
228
|
+
cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
75
233
|
}
|
|
76
|
-
var DEFAULT_PRICING;
|
|
234
|
+
var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS;
|
|
77
235
|
var init_pricing = __esm(() => {
|
|
78
236
|
init_database();
|
|
79
237
|
DEFAULT_PRICING = {
|
|
80
|
-
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
81
|
-
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
82
|
-
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
83
|
-
"claude-
|
|
84
|
-
"claude-
|
|
85
|
-
"claude-
|
|
86
|
-
"claude-
|
|
87
|
-
"claude-
|
|
88
|
-
"claude-3-
|
|
89
|
-
"claude-
|
|
90
|
-
"claude-3-haiku": { inputPer1M: 0.
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"gemini-
|
|
94
|
-
"gemini-
|
|
95
|
-
"gemini-1
|
|
96
|
-
"gemini-
|
|
238
|
+
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
239
|
+
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
240
|
+
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
|
|
241
|
+
"claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
242
|
+
"claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
243
|
+
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
244
|
+
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
245
|
+
"claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
246
|
+
"claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
247
|
+
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
|
|
248
|
+
"claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
|
|
249
|
+
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
|
|
250
|
+
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
|
|
251
|
+
"gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
252
|
+
"gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
253
|
+
"gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
254
|
+
"gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
255
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
|
|
256
|
+
"gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
257
|
+
"gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
258
|
+
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
|
|
259
|
+
"gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
260
|
+
"google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
|
|
261
|
+
"google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
262
|
+
"google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
|
|
263
|
+
"google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
|
|
264
|
+
"google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
|
|
265
|
+
"google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
|
|
266
|
+
"google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
|
|
267
|
+
"gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
268
|
+
"gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
269
|
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
98
270
|
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
99
271
|
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
272
|
+
"gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
273
|
+
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
274
|
+
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
275
|
+
"gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
276
|
+
"gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
277
|
+
"gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
278
|
+
"gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
|
|
279
|
+
"gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
|
|
280
|
+
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
281
|
+
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
282
|
+
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
283
|
+
"o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
284
|
+
o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
285
|
+
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
286
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
|
|
287
|
+
"qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
288
|
+
"qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
289
|
+
"qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
290
|
+
"qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
291
|
+
"qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
292
|
+
"qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
|
|
293
|
+
"qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
|
|
294
|
+
"qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
295
|
+
"qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
|
|
296
|
+
"qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
297
|
+
"minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
298
|
+
"minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
|
|
299
|
+
"minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
300
|
+
"minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
301
|
+
"minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
302
|
+
"grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
303
|
+
"grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
304
|
+
"grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
305
|
+
"grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
306
|
+
"grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
|
|
307
|
+
"grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
308
|
+
"grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
309
|
+
"grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
|
|
310
|
+
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
|
|
311
|
+
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
|
|
312
|
+
"glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
|
|
313
|
+
"glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
314
|
+
"z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
|
|
315
|
+
"z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
|
|
316
|
+
"kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
|
|
317
|
+
"kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
|
|
318
|
+
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
319
|
+
"moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
|
|
320
|
+
"moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
|
|
321
|
+
"moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
322
|
+
};
|
|
323
|
+
LEGACY_DEFAULT_PRICING = {
|
|
324
|
+
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
325
|
+
"claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
326
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
|
|
327
|
+
"gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
328
|
+
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
100
329
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
101
|
-
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
102
330
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
103
331
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
104
332
|
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
105
333
|
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
106
|
-
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
107
|
-
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
108
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
109
334
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
|
|
335
|
+
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
336
|
+
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
113
337
|
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
114
|
-
"qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
115
338
|
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
116
339
|
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
117
340
|
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
118
|
-
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
119
|
-
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
120
341
|
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
121
342
|
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
122
|
-
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
343
|
+
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
344
|
+
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
|
|
345
|
+
};
|
|
346
|
+
ADDITIONAL_LEGACY_DEFAULT_PRICING = {
|
|
347
|
+
"gemini-2.5-pro": [
|
|
348
|
+
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
349
|
+
],
|
|
350
|
+
"qwen3.6-plus": [
|
|
351
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
352
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
353
|
+
],
|
|
354
|
+
"qwen3.6-flash": [
|
|
355
|
+
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
356
|
+
],
|
|
357
|
+
"qwen3.6-max-preview": [
|
|
358
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
359
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
360
|
+
],
|
|
361
|
+
"qwen/qwen3.6-plus": [
|
|
362
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
|
|
363
|
+
{ inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
|
|
364
|
+
],
|
|
365
|
+
"qwen/qwen3.6-flash": [
|
|
366
|
+
{ inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
|
|
367
|
+
],
|
|
368
|
+
"qwen/qwen3.6-max-preview": [
|
|
369
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
|
|
370
|
+
{ inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
|
|
371
|
+
]
|
|
372
|
+
};
|
|
373
|
+
REMOVED_DEFAULT_PRICING = {
|
|
374
|
+
"claude-3-5-sonnet": [
|
|
375
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
376
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
377
|
+
],
|
|
378
|
+
"claude-3-sonnet": [
|
|
379
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
|
|
380
|
+
{ inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
|
|
381
|
+
],
|
|
382
|
+
"gemini-3.1-pro": [
|
|
383
|
+
{ inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
|
|
384
|
+
{ inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
|
|
385
|
+
],
|
|
386
|
+
"gemini-1.5-pro": [
|
|
387
|
+
{ inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
388
|
+
],
|
|
389
|
+
"gemini-1.5-flash": [
|
|
390
|
+
{ inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
391
|
+
],
|
|
392
|
+
"gpt-5.3-chat": [
|
|
393
|
+
{ inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
|
|
394
|
+
{ inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
|
|
395
|
+
],
|
|
396
|
+
"qwen3.6": [
|
|
397
|
+
{ inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
FREE_PRICING = {
|
|
401
|
+
inputPer1M: 0,
|
|
402
|
+
outputPer1M: 0,
|
|
403
|
+
cacheReadPer1M: 0,
|
|
404
|
+
cacheWritePer1M: 0,
|
|
405
|
+
cacheWrite1hPer1M: 0,
|
|
406
|
+
cacheStoragePer1MHour: 0
|
|
407
|
+
};
|
|
408
|
+
GEMINI_PROMPT_TIERS = {
|
|
409
|
+
"gemini-3.1-pro-preview": {
|
|
410
|
+
threshold: 200000,
|
|
411
|
+
inputPer1M: 4,
|
|
412
|
+
outputPer1M: 18,
|
|
413
|
+
cacheReadPer1M: 0.4
|
|
414
|
+
},
|
|
415
|
+
"gemini-2.5-pro": {
|
|
416
|
+
threshold: 200000,
|
|
417
|
+
inputPer1M: 2.5,
|
|
418
|
+
outputPer1M: 15,
|
|
419
|
+
cacheReadPer1M: 0.25
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
OPENAI_PROMPT_TIERS = {
|
|
423
|
+
"gpt-5.5": {
|
|
424
|
+
threshold: 272000,
|
|
425
|
+
inputMultiplier: 2,
|
|
426
|
+
outputMultiplier: 1.5,
|
|
427
|
+
cacheReadMultiplier: 2
|
|
428
|
+
},
|
|
429
|
+
"gpt-5.4-pro": {
|
|
430
|
+
threshold: 272000,
|
|
431
|
+
inputMultiplier: 2,
|
|
432
|
+
outputMultiplier: 1.5,
|
|
433
|
+
cacheReadMultiplier: 2
|
|
434
|
+
},
|
|
435
|
+
"gpt-5.4": {
|
|
436
|
+
threshold: 272000,
|
|
437
|
+
inputMultiplier: 2,
|
|
438
|
+
outputMultiplier: 1.5,
|
|
439
|
+
cacheReadMultiplier: 2
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
QWEN_PROMPT_TIERS = {
|
|
443
|
+
"qwen3.6-plus": {
|
|
444
|
+
threshold: 256000,
|
|
445
|
+
inputPer1M: 1.3,
|
|
446
|
+
outputPer1M: 3.9,
|
|
447
|
+
cacheReadPer1M: 0.13,
|
|
448
|
+
cacheWritePer1M: 1.625
|
|
449
|
+
},
|
|
450
|
+
"qwen3.6-flash": {
|
|
451
|
+
threshold: 256000,
|
|
452
|
+
inputPer1M: 1,
|
|
453
|
+
outputPer1M: 4,
|
|
454
|
+
cacheReadPer1M: 0.1,
|
|
455
|
+
cacheWritePer1M: 1.25
|
|
456
|
+
},
|
|
457
|
+
"qwen3.6-max-preview": {
|
|
458
|
+
threshold: 128000,
|
|
459
|
+
inputPer1M: 1.6,
|
|
460
|
+
outputPer1M: 9.6,
|
|
461
|
+
cacheReadPer1M: 0.16,
|
|
462
|
+
cacheWritePer1M: 2
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
MINIMAX_PROMPT_TIERS = {
|
|
466
|
+
"minimax/minimax-m1": {
|
|
467
|
+
threshold: Number.POSITIVE_INFINITY
|
|
468
|
+
},
|
|
469
|
+
"minimax-m1": {
|
|
470
|
+
threshold: 200000,
|
|
471
|
+
inputPer1M: 1.3
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
XAI_PROMPT_TIERS = {
|
|
475
|
+
"grok-4.3": {
|
|
476
|
+
threshold: 200000,
|
|
477
|
+
inputPer1M: 2.5,
|
|
478
|
+
outputPer1M: 5,
|
|
479
|
+
cacheReadPer1M: 0.4
|
|
480
|
+
},
|
|
481
|
+
"grok-latest": {
|
|
482
|
+
threshold: 200000,
|
|
483
|
+
inputPer1M: 2.5,
|
|
484
|
+
outputPer1M: 5,
|
|
485
|
+
cacheReadPer1M: 0.4
|
|
486
|
+
},
|
|
487
|
+
"grok-4.20": {
|
|
488
|
+
threshold: 200000,
|
|
489
|
+
inputPer1M: 2.5,
|
|
490
|
+
outputPer1M: 5,
|
|
491
|
+
cacheReadPer1M: 0.4
|
|
492
|
+
},
|
|
493
|
+
"grok-4-1-fast": {
|
|
494
|
+
threshold: 128000,
|
|
495
|
+
inputPer1M: 0.4,
|
|
496
|
+
outputPer1M: 1,
|
|
497
|
+
cacheReadPer1M: 0
|
|
498
|
+
},
|
|
499
|
+
"grok-4-fast": {
|
|
500
|
+
threshold: 128000,
|
|
501
|
+
inputPer1M: 0.4,
|
|
502
|
+
outputPer1M: 1,
|
|
503
|
+
cacheReadPer1M: 0
|
|
504
|
+
},
|
|
505
|
+
"grok-4": {
|
|
506
|
+
threshold: 128000,
|
|
507
|
+
inputPer1M: 6,
|
|
508
|
+
outputPer1M: 30,
|
|
509
|
+
cacheReadPer1M: 0
|
|
510
|
+
}
|
|
123
511
|
};
|
|
124
512
|
});
|
|
125
513
|
|
|
@@ -188,6 +576,8 @@ function initSchema(db) {
|
|
|
188
576
|
output_tokens INTEGER DEFAULT 0,
|
|
189
577
|
cache_read_tokens INTEGER DEFAULT 0,
|
|
190
578
|
cache_create_tokens INTEGER DEFAULT 0,
|
|
579
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
580
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
191
581
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
192
582
|
duration_ms INTEGER DEFAULT 0,
|
|
193
583
|
timestamp TEXT NOT NULL,
|
|
@@ -258,6 +648,8 @@ function initSchema(db) {
|
|
|
258
648
|
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
259
649
|
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
260
650
|
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
651
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
652
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
261
653
|
updated_at TEXT NOT NULL
|
|
262
654
|
);
|
|
263
655
|
|
|
@@ -282,12 +674,100 @@ function initSchema(db) {
|
|
|
282
674
|
|
|
283
675
|
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
284
676
|
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
677
|
+
|
|
678
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
679
|
+
id TEXT PRIMARY KEY,
|
|
680
|
+
agent TEXT,
|
|
681
|
+
provider TEXT NOT NULL,
|
|
682
|
+
plan TEXT NOT NULL,
|
|
683
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
684
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
685
|
+
billing_cycle_start TEXT,
|
|
686
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
687
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
688
|
+
created_at TEXT NOT NULL,
|
|
689
|
+
updated_at TEXT NOT NULL
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
693
|
+
id TEXT PRIMARY KEY,
|
|
694
|
+
agent TEXT NOT NULL,
|
|
695
|
+
date TEXT NOT NULL,
|
|
696
|
+
metric TEXT NOT NULL,
|
|
697
|
+
value REAL NOT NULL DEFAULT 0,
|
|
698
|
+
unit TEXT DEFAULT '',
|
|
699
|
+
machine_id TEXT DEFAULT '',
|
|
700
|
+
updated_at TEXT NOT NULL
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
CREATE TABLE IF NOT EXISTS savings_daily (
|
|
704
|
+
date TEXT NOT NULL,
|
|
705
|
+
agent TEXT DEFAULT '',
|
|
706
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
707
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
708
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
709
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
710
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
711
|
+
updated_at TEXT NOT NULL,
|
|
712
|
+
PRIMARY KEY (date, agent)
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
716
|
+
machine_id TEXT PRIMARY KEY,
|
|
717
|
+
hostname TEXT NOT NULL,
|
|
718
|
+
last_seen_at TEXT,
|
|
719
|
+
last_push_at TEXT,
|
|
720
|
+
last_pull_at TEXT,
|
|
721
|
+
economy_version TEXT,
|
|
722
|
+
updated_at TEXT NOT NULL
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
726
|
+
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
285
727
|
`);
|
|
286
728
|
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
287
729
|
if (!cols.some((c) => c.name === "machine_id")) {
|
|
288
730
|
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
289
731
|
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
290
732
|
}
|
|
733
|
+
if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
|
|
734
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
|
|
735
|
+
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
736
|
+
}
|
|
737
|
+
if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
|
|
738
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
|
|
739
|
+
}
|
|
740
|
+
if (!cols.some((c) => c.name === "cost_basis")) {
|
|
741
|
+
db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
|
|
742
|
+
}
|
|
743
|
+
if (!cols.some((c) => c.name === "attribution_tag")) {
|
|
744
|
+
db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
745
|
+
}
|
|
746
|
+
if (!cols.some((c) => c.name === "updated_at")) {
|
|
747
|
+
db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
748
|
+
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
749
|
+
}
|
|
750
|
+
if (!cols.some((c) => c.name === "synced_at")) {
|
|
751
|
+
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
752
|
+
}
|
|
753
|
+
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
754
|
+
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
755
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
756
|
+
}
|
|
757
|
+
if (!sessionCols.some((c) => c.name === "updated_at")) {
|
|
758
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
759
|
+
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
760
|
+
}
|
|
761
|
+
if (!sessionCols.some((c) => c.name === "synced_at")) {
|
|
762
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
763
|
+
}
|
|
764
|
+
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
765
|
+
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
766
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
767
|
+
}
|
|
768
|
+
if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
|
|
769
|
+
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
|
|
770
|
+
}
|
|
291
771
|
db.exec(`
|
|
292
772
|
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
293
773
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
@@ -326,21 +806,24 @@ function sessionPeriodWhere(period) {
|
|
|
326
806
|
}
|
|
327
807
|
}
|
|
328
808
|
function upsertRequest(db, req) {
|
|
809
|
+
const now = req.updated_at ?? new Date().toISOString();
|
|
329
810
|
db.prepare(`
|
|
330
811
|
INSERT OR REPLACE INTO requests
|
|
331
812
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
332
|
-
cache_read_tokens, cache_create_tokens,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
813
|
+
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
814
|
+
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
815
|
+
source_request_id, machine_id, attribution_tag, updated_at, synced_at)
|
|
816
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
817
|
+
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, req.synced_at ?? "");
|
|
336
818
|
}
|
|
337
819
|
function upsertSession(db, session) {
|
|
820
|
+
const now = session.updated_at ?? new Date().toISOString();
|
|
338
821
|
db.prepare(`
|
|
339
822
|
INSERT OR REPLACE INTO sessions
|
|
340
823
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
341
|
-
total_cost_usd, total_tokens, request_count, machine_id)
|
|
342
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
343
|
-
`).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 ?? "");
|
|
824
|
+
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
|
|
825
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
826
|
+
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, session.synced_at ?? "");
|
|
344
827
|
}
|
|
345
828
|
function rollupSession(db, sessionId) {
|
|
346
829
|
db.prepare(`
|
|
@@ -392,10 +875,10 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
392
875
|
}
|
|
393
876
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
394
877
|
}
|
|
395
|
-
function querySummary(db, period, machine) {
|
|
878
|
+
function querySummary(db, period, machine, allMachines = false) {
|
|
396
879
|
const rWhere = periodWhere(period);
|
|
397
880
|
const sWhere = sessionPeriodWhere(period);
|
|
398
|
-
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
881
|
+
const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
399
882
|
const r = db.prepare(`
|
|
400
883
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
401
884
|
COUNT(*) as requests,
|
|
@@ -644,9 +1127,9 @@ function listMachines(db) {
|
|
|
644
1127
|
function upsertModelPricing(db, p) {
|
|
645
1128
|
db.prepare(`
|
|
646
1129
|
INSERT OR REPLACE INTO model_pricing
|
|
647
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
|
|
648
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
649
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
|
|
1130
|
+
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, cache_write_1h_per_1m, cache_storage_per_1m_hour, updated_at)
|
|
1131
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1132
|
+
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.cache_write_1h_per_1m ?? 0, p.cache_storage_per_1m_hour ?? 0, p.updated_at);
|
|
650
1133
|
}
|
|
651
1134
|
function getModelPricing(db, model) {
|
|
652
1135
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
@@ -669,12 +1152,93 @@ function seedModelPricing(db, defaults) {
|
|
|
669
1152
|
output_per_1m: p.outputPer1M,
|
|
670
1153
|
cache_read_per_1m: p.cacheReadPer1M,
|
|
671
1154
|
cache_write_per_1m: p.cacheWritePer1M,
|
|
1155
|
+
cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
|
|
1156
|
+
cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
|
|
672
1157
|
updated_at: now
|
|
673
1158
|
});
|
|
674
1159
|
}
|
|
675
1160
|
}
|
|
1161
|
+
function upsertSubscription(db, sub) {
|
|
1162
|
+
db.prepare(`
|
|
1163
|
+
INSERT OR REPLACE INTO subscriptions
|
|
1164
|
+
(id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
|
|
1165
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1166
|
+
`).run(sub.id, sub.agent, sub.provider, sub.plan, sub.monthly_fee_usd, sub.included_usage_usd, sub.billing_cycle_start, sub.reset_policy, sub.active, sub.created_at, sub.updated_at);
|
|
1167
|
+
}
|
|
1168
|
+
function listSubscriptions(db) {
|
|
1169
|
+
return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
|
|
1170
|
+
}
|
|
1171
|
+
function deleteSubscription(db, id) {
|
|
1172
|
+
db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
|
|
1173
|
+
}
|
|
1174
|
+
function upsertUsageSnapshot(db, snap) {
|
|
1175
|
+
const now = snap.updated_at ?? new Date().toISOString();
|
|
1176
|
+
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
1177
|
+
db.prepare(`
|
|
1178
|
+
INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
|
|
1179
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1180
|
+
`).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
|
|
1181
|
+
}
|
|
1182
|
+
function queryUsageSnapshots(db, opts = {}) {
|
|
1183
|
+
const conditions = [];
|
|
1184
|
+
const params = [];
|
|
1185
|
+
if (opts.agent) {
|
|
1186
|
+
conditions.push("agent = ?");
|
|
1187
|
+
params.push(opts.agent);
|
|
1188
|
+
}
|
|
1189
|
+
if (opts.date) {
|
|
1190
|
+
conditions.push("date = ?");
|
|
1191
|
+
params.push(opts.date);
|
|
1192
|
+
}
|
|
1193
|
+
if (opts.since) {
|
|
1194
|
+
conditions.push("date >= ?");
|
|
1195
|
+
params.push(opts.since);
|
|
1196
|
+
}
|
|
1197
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1198
|
+
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1199
|
+
}
|
|
1200
|
+
function listMachineRegistry(db) {
|
|
1201
|
+
return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
|
|
1202
|
+
}
|
|
1203
|
+
function dedupeRequests(db) {
|
|
1204
|
+
const dupes = db.prepare(`
|
|
1205
|
+
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1206
|
+
FROM requests
|
|
1207
|
+
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1208
|
+
GROUP BY source_request_id, agent
|
|
1209
|
+
HAVING cnt > 1
|
|
1210
|
+
`).all();
|
|
1211
|
+
let removed = 0;
|
|
1212
|
+
for (const row of dupes) {
|
|
1213
|
+
const result = db.prepare(`
|
|
1214
|
+
DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
|
|
1215
|
+
`).run(row.source_request_id, row.agent, row.keep_id);
|
|
1216
|
+
removed += result.changes;
|
|
1217
|
+
}
|
|
1218
|
+
return removed;
|
|
1219
|
+
}
|
|
676
1220
|
var init_database = () => {};
|
|
677
1221
|
|
|
1222
|
+
// src/lib/agents.ts
|
|
1223
|
+
var AGENTS = [
|
|
1224
|
+
"claude",
|
|
1225
|
+
"takumi",
|
|
1226
|
+
"codex",
|
|
1227
|
+
"gemini",
|
|
1228
|
+
"opencode",
|
|
1229
|
+
"cursor",
|
|
1230
|
+
"pi",
|
|
1231
|
+
"hermes"
|
|
1232
|
+
];
|
|
1233
|
+
var COST_BASIS = [
|
|
1234
|
+
"metered_api",
|
|
1235
|
+
"subscription_included",
|
|
1236
|
+
"estimated",
|
|
1237
|
+
"unknown"
|
|
1238
|
+
];
|
|
1239
|
+
function isAgent(value) {
|
|
1240
|
+
return AGENTS.includes(value);
|
|
1241
|
+
}
|
|
678
1242
|
// src/index.ts
|
|
679
1243
|
init_database();
|
|
680
1244
|
init_pricing();
|
|
@@ -682,6 +1246,9 @@ init_pricing();
|
|
|
682
1246
|
// src/lib/gatherer.ts
|
|
683
1247
|
init_database();
|
|
684
1248
|
var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
|
|
1249
|
+
function hasCostData(summary) {
|
|
1250
|
+
return summary.total_usd > 0 || summary.sessions > 0 || summary.requests > 0 || summary.tokens > 0;
|
|
1251
|
+
}
|
|
685
1252
|
var gatherTrainingData = async (options = {}) => {
|
|
686
1253
|
const limit = options.limit ?? 500;
|
|
687
1254
|
const examples = [];
|
|
@@ -691,6 +1258,8 @@ var gatherTrainingData = async (options = {}) => {
|
|
|
691
1258
|
for (const period of periods) {
|
|
692
1259
|
try {
|
|
693
1260
|
const s = querySummary(db, period);
|
|
1261
|
+
if (!hasCostData(s))
|
|
1262
|
+
continue;
|
|
694
1263
|
examples.push({
|
|
695
1264
|
messages: [
|
|
696
1265
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
@@ -863,22 +1432,26 @@ ${goals.map((g) => `- ${g.period} goal (${g.project_path ?? g.agent ?? "global"}
|
|
|
863
1432
|
// src/lib/model-config.ts
|
|
864
1433
|
init_database();
|
|
865
1434
|
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
866
|
-
import { join as join2 } from "path";
|
|
1435
|
+
import { dirname, join as join2 } from "path";
|
|
867
1436
|
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
868
|
-
|
|
1437
|
+
function getModelConfigPath() {
|
|
1438
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join2(getDataDir(), "config.json");
|
|
1439
|
+
}
|
|
869
1440
|
function loadConfig() {
|
|
870
1441
|
try {
|
|
871
|
-
|
|
872
|
-
|
|
1442
|
+
const configPath = getModelConfigPath();
|
|
1443
|
+
if (existsSync2(configPath)) {
|
|
1444
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
873
1445
|
}
|
|
874
1446
|
} catch {}
|
|
875
1447
|
return {};
|
|
876
1448
|
}
|
|
877
1449
|
function saveConfig(config) {
|
|
878
|
-
const
|
|
1450
|
+
const configPath = getModelConfigPath();
|
|
1451
|
+
const dir = dirname(configPath);
|
|
879
1452
|
if (!existsSync2(dir))
|
|
880
1453
|
mkdirSync2(dir, { recursive: true });
|
|
881
|
-
writeFileSync(
|
|
1454
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
882
1455
|
`);
|
|
883
1456
|
}
|
|
884
1457
|
function getActiveModel() {
|
|
@@ -894,12 +1467,51 @@ function clearActiveModel() {
|
|
|
894
1467
|
delete config.activeModel;
|
|
895
1468
|
saveConfig(config);
|
|
896
1469
|
}
|
|
1470
|
+
// src/lib/open-projects.ts
|
|
1471
|
+
init_database();
|
|
1472
|
+
async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
1473
|
+
let listProjects2 = listActiveProjects;
|
|
1474
|
+
if (!listProjects2) {
|
|
1475
|
+
const projectsApi = await import("@hasna/projects");
|
|
1476
|
+
listProjects2 = projectsApi.listProjects;
|
|
1477
|
+
}
|
|
1478
|
+
const projects = listProjects2({ status: "active", limit: 5000 });
|
|
1479
|
+
let imported = 0;
|
|
1480
|
+
let skipped = 0;
|
|
1481
|
+
for (const project of projects) {
|
|
1482
|
+
if (!project.path) {
|
|
1483
|
+
skipped++;
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
upsertProject(db, {
|
|
1487
|
+
id: project.id,
|
|
1488
|
+
path: project.path,
|
|
1489
|
+
name: project.name,
|
|
1490
|
+
description: project.description,
|
|
1491
|
+
tags: project.tags ?? [],
|
|
1492
|
+
created_at: project.created_at
|
|
1493
|
+
});
|
|
1494
|
+
imported++;
|
|
1495
|
+
}
|
|
1496
|
+
return { imported, skipped };
|
|
1497
|
+
}
|
|
897
1498
|
// src/ingest/claude.ts
|
|
898
1499
|
init_database();
|
|
899
1500
|
init_pricing();
|
|
900
1501
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
901
1502
|
import { homedir as homedir2 } from "os";
|
|
902
1503
|
import { join as join3, basename } from "path";
|
|
1504
|
+
|
|
1505
|
+
// src/lib/savings.ts
|
|
1506
|
+
function defaultCostBasisForAgent(agent) {
|
|
1507
|
+
if (agent === "claude")
|
|
1508
|
+
return "metered_api";
|
|
1509
|
+
if (agent === "cursor")
|
|
1510
|
+
return "subscription_included";
|
|
1511
|
+
return "estimated";
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/ingest/claude.ts
|
|
903
1515
|
function autoDetectProject(cwd, projects) {
|
|
904
1516
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
905
1517
|
}
|
|
@@ -923,11 +1535,11 @@ function collectJsonlFiles(projectDir) {
|
|
|
923
1535
|
walk(projectDir);
|
|
924
1536
|
return files;
|
|
925
1537
|
}
|
|
926
|
-
async function ingestClaude(db, verbose = false,
|
|
927
|
-
return ingestJsonlProjects(db,
|
|
1538
|
+
async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
|
|
1539
|
+
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
928
1540
|
}
|
|
929
|
-
async function ingestTakumi(db, verbose = false) {
|
|
930
|
-
return ingestJsonlProjects(db,
|
|
1541
|
+
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
1542
|
+
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
931
1543
|
}
|
|
932
1544
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
933
1545
|
if (!existsSync3(projectsDir)) {
|
|
@@ -988,13 +1600,21 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
988
1600
|
continue;
|
|
989
1601
|
const inputTokens = usage.input_tokens ?? 0;
|
|
990
1602
|
const outputTokens = usage.output_tokens ?? 0;
|
|
991
|
-
const
|
|
1603
|
+
const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
|
|
1604
|
+
const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
1605
|
+
const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
|
|
992
1606
|
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
993
1607
|
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
994
|
-
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
1608
|
+
if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
|
|
995
1609
|
continue;
|
|
996
|
-
|
|
997
|
-
|
|
1610
|
+
let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
|
|
1611
|
+
costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
|
|
1612
|
+
const serverToolUse = usage.server_tool_use;
|
|
1613
|
+
if (serverToolUse?.web_search_requests) {
|
|
1614
|
+
costUsd += serverToolUse.web_search_requests * 0.01;
|
|
1615
|
+
}
|
|
1616
|
+
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1617
|
+
const reqId = `${agentName}-${sourceRequestId}`;
|
|
998
1618
|
upsertRequest(db, {
|
|
999
1619
|
id: reqId,
|
|
1000
1620
|
agent: agentName,
|
|
@@ -1004,10 +1624,13 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1004
1624
|
output_tokens: outputTokens,
|
|
1005
1625
|
cache_read_tokens: cacheReadTokens,
|
|
1006
1626
|
cache_create_tokens: cacheWriteTokens,
|
|
1627
|
+
cache_create_5m_tokens: cacheWrite5mTokens,
|
|
1628
|
+
cache_create_1h_tokens: cacheWrite1hTokens,
|
|
1007
1629
|
cost_usd: costUsd,
|
|
1630
|
+
cost_basis: defaultCostBasisForAgent(agentName),
|
|
1008
1631
|
duration_ms: 0,
|
|
1009
1632
|
timestamp,
|
|
1010
|
-
source_request_id:
|
|
1633
|
+
source_request_id: sourceRequestId,
|
|
1011
1634
|
machine_id: machineId
|
|
1012
1635
|
});
|
|
1013
1636
|
if (!touchedSessions.has(sessionId)) {
|
|
@@ -1042,70 +1665,351 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1042
1665
|
}
|
|
1043
1666
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
1044
1667
|
}
|
|
1668
|
+
function applyClaudeModifiers(costUsd, model, usage, entry) {
|
|
1669
|
+
let multiplier = 1;
|
|
1670
|
+
const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
|
|
1671
|
+
if (speed === "fast" && model.includes("opus-4-6")) {
|
|
1672
|
+
multiplier *= 6;
|
|
1673
|
+
}
|
|
1674
|
+
const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
|
|
1675
|
+
if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
|
|
1676
|
+
multiplier *= 1.1;
|
|
1677
|
+
}
|
|
1678
|
+
return costUsd * multiplier;
|
|
1679
|
+
}
|
|
1680
|
+
function supportsClaudeDataResidencyPricing(model) {
|
|
1681
|
+
const normalized = normalizeModelName(model);
|
|
1682
|
+
const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
1683
|
+
if (!match)
|
|
1684
|
+
return false;
|
|
1685
|
+
const major = Number(match[2]);
|
|
1686
|
+
const minor = match[3] ? Number(match[3]) : 0;
|
|
1687
|
+
return major > 4 || major === 4 && minor >= 6;
|
|
1688
|
+
}
|
|
1045
1689
|
// src/ingest/codex.ts
|
|
1046
1690
|
init_database();
|
|
1691
|
+
init_pricing();
|
|
1047
1692
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1048
1693
|
import { homedir as homedir3 } from "os";
|
|
1049
1694
|
import { join as join4, basename as basename2 } from "path";
|
|
1050
1695
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
1051
|
-
var
|
|
1052
|
-
var
|
|
1696
|
+
var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
|
|
1697
|
+
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
|
|
1698
|
+
var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
|
|
1699
|
+
function codexDbPath() {
|
|
1700
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
1701
|
+
}
|
|
1702
|
+
function codexConfigPath() {
|
|
1703
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
1704
|
+
}
|
|
1053
1705
|
function readCodexModel() {
|
|
1054
|
-
|
|
1055
|
-
|
|
1706
|
+
const configPath = codexConfigPath();
|
|
1707
|
+
if (!existsSync4(configPath))
|
|
1708
|
+
return "gpt-5-codex";
|
|
1056
1709
|
try {
|
|
1057
|
-
const content = readFileSync3(
|
|
1710
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
1058
1711
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
1059
|
-
return match?.[1] ?? "gpt-5
|
|
1712
|
+
return match?.[1] ?? "gpt-5-codex";
|
|
1060
1713
|
} catch {
|
|
1061
|
-
return "gpt-5
|
|
1714
|
+
return "gpt-5-codex";
|
|
1062
1715
|
}
|
|
1063
1716
|
}
|
|
1717
|
+
function buildThreadQuery(codexDb) {
|
|
1718
|
+
const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
|
|
1719
|
+
const modelSelect = cols.has("model") ? "model" : "NULL AS model";
|
|
1720
|
+
const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
|
|
1721
|
+
const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
|
|
1722
|
+
return `
|
|
1723
|
+
SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
|
|
1724
|
+
${providerSelect}, ${modelSelect}
|
|
1725
|
+
FROM threads WHERE tokens_used > 0
|
|
1726
|
+
`;
|
|
1727
|
+
}
|
|
1728
|
+
function readTokenEvents(rolloutPath) {
|
|
1729
|
+
if (!rolloutPath || !existsSync4(rolloutPath))
|
|
1730
|
+
return [];
|
|
1731
|
+
const events = [];
|
|
1732
|
+
const seen = new Set;
|
|
1733
|
+
for (const line of readFileSync3(rolloutPath, "utf-8").split(`
|
|
1734
|
+
`)) {
|
|
1735
|
+
if (!line.trim())
|
|
1736
|
+
continue;
|
|
1737
|
+
let entry;
|
|
1738
|
+
try {
|
|
1739
|
+
entry = JSON.parse(line);
|
|
1740
|
+
} catch {
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
if (!entry || typeof entry !== "object")
|
|
1744
|
+
continue;
|
|
1745
|
+
const payload = entry["payload"];
|
|
1746
|
+
if (!payload || payload["type"] !== "token_count")
|
|
1747
|
+
continue;
|
|
1748
|
+
const info = payload["info"];
|
|
1749
|
+
const usage = info?.["last_token_usage"];
|
|
1750
|
+
if (!usage)
|
|
1751
|
+
continue;
|
|
1752
|
+
const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
1753
|
+
if (total <= 0)
|
|
1754
|
+
continue;
|
|
1755
|
+
const key = JSON.stringify(usage);
|
|
1756
|
+
if (seen.has(key))
|
|
1757
|
+
continue;
|
|
1758
|
+
seen.add(key);
|
|
1759
|
+
const timestamp = entry["timestamp"];
|
|
1760
|
+
events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
|
|
1761
|
+
}
|
|
1762
|
+
return events;
|
|
1763
|
+
}
|
|
1764
|
+
function fallbackEvents(totalTokens) {
|
|
1765
|
+
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
1766
|
+
return [{
|
|
1767
|
+
usage: {
|
|
1768
|
+
input_tokens: inputTokens,
|
|
1769
|
+
cached_input_tokens: 0,
|
|
1770
|
+
output_tokens: totalTokens - inputTokens,
|
|
1771
|
+
total_tokens: totalTokens
|
|
1772
|
+
}
|
|
1773
|
+
}];
|
|
1774
|
+
}
|
|
1064
1775
|
async function ingestCodex(db, verbose = false) {
|
|
1065
|
-
|
|
1776
|
+
const dbPath = codexDbPath();
|
|
1777
|
+
if (!existsSync4(dbPath)) {
|
|
1066
1778
|
if (verbose)
|
|
1067
|
-
console.log("Codex DB not found:",
|
|
1068
|
-
return { sessions: 0 };
|
|
1779
|
+
console.log("Codex DB not found:", dbPath);
|
|
1780
|
+
return { sessions: 0, requests: 0 };
|
|
1069
1781
|
}
|
|
1070
1782
|
const machineId = getMachineId();
|
|
1071
1783
|
let codexDb = null;
|
|
1072
1784
|
let ingested = 0;
|
|
1785
|
+
let requests = 0;
|
|
1073
1786
|
try {
|
|
1074
|
-
codexDb = new BunDatabase(
|
|
1075
|
-
const threads = codexDb.prepare(
|
|
1787
|
+
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
1788
|
+
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
1076
1789
|
for (const thread of threads) {
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1790
|
+
const model = thread.model ?? readCodexModel();
|
|
1791
|
+
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
1792
|
+
const processed = getIngestState(db, "codex", thread.id);
|
|
1793
|
+
if (processed === stateValue)
|
|
1080
1794
|
continue;
|
|
1081
|
-
const costUsd = 0;
|
|
1082
1795
|
const projectPath = thread.cwd ?? "";
|
|
1083
1796
|
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
1797
|
+
const sessionId = `codex-${thread.id}`;
|
|
1084
1798
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
1085
1799
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
1086
1800
|
upsertSession(db, {
|
|
1087
|
-
id:
|
|
1801
|
+
id: sessionId,
|
|
1088
1802
|
agent: "codex",
|
|
1089
1803
|
project_path: projectPath,
|
|
1090
1804
|
project_name: projectName,
|
|
1091
1805
|
started_at: startedAt,
|
|
1092
1806
|
ended_at: endedAt,
|
|
1093
|
-
total_cost_usd:
|
|
1094
|
-
total_tokens:
|
|
1095
|
-
request_count:
|
|
1807
|
+
total_cost_usd: 0,
|
|
1808
|
+
total_tokens: 0,
|
|
1809
|
+
request_count: 0,
|
|
1096
1810
|
machine_id: machineId
|
|
1097
1811
|
});
|
|
1098
|
-
|
|
1812
|
+
const events = readTokenEvents(thread.rollout_path);
|
|
1813
|
+
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
1814
|
+
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
1815
|
+
tokenEvents.forEach((event, index) => {
|
|
1816
|
+
const usage = event.usage;
|
|
1817
|
+
const inputTotal = usage.input_tokens ?? 0;
|
|
1818
|
+
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
1819
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
1820
|
+
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
1821
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
1822
|
+
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
1823
|
+
const requestId = `${sessionId}-${index}`;
|
|
1824
|
+
upsertRequest(db, {
|
|
1825
|
+
id: requestId,
|
|
1826
|
+
agent: "codex",
|
|
1827
|
+
session_id: sessionId,
|
|
1828
|
+
model,
|
|
1829
|
+
input_tokens: inputTokens,
|
|
1830
|
+
output_tokens: outputTokens,
|
|
1831
|
+
cache_read_tokens: cacheReadTokens,
|
|
1832
|
+
cache_create_tokens: 0,
|
|
1833
|
+
cost_usd: costUsd,
|
|
1834
|
+
cost_basis: defaultCostBasisForAgent("codex"),
|
|
1835
|
+
duration_ms: 0,
|
|
1836
|
+
timestamp,
|
|
1837
|
+
source_request_id: requestId,
|
|
1838
|
+
machine_id: machineId
|
|
1839
|
+
});
|
|
1840
|
+
requests++;
|
|
1841
|
+
});
|
|
1842
|
+
rollupSession(db, sessionId);
|
|
1843
|
+
setIngestState(db, "codex", thread.id, stateValue);
|
|
1099
1844
|
ingested++;
|
|
1100
1845
|
if (verbose)
|
|
1101
|
-
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens
|
|
1846
|
+
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
|
|
1102
1847
|
}
|
|
1103
1848
|
} finally {
|
|
1104
1849
|
codexDb?.close();
|
|
1105
1850
|
}
|
|
1106
|
-
return { sessions: ingested };
|
|
1851
|
+
return { sessions: ingested, requests };
|
|
1852
|
+
}
|
|
1853
|
+
// src/ingest/gemini.ts
|
|
1854
|
+
init_database();
|
|
1855
|
+
init_pricing();
|
|
1856
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
1857
|
+
import { homedir as homedir4 } from "os";
|
|
1858
|
+
import { join as join5, basename as basename3 } from "path";
|
|
1859
|
+
var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
|
|
1860
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir4(), ".gemini", "history");
|
|
1861
|
+
function geminiTmpDir() {
|
|
1862
|
+
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
1863
|
+
}
|
|
1864
|
+
function geminiHistoryDir() {
|
|
1865
|
+
return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
|
|
1866
|
+
}
|
|
1867
|
+
function numberField(...values) {
|
|
1868
|
+
for (const value of values) {
|
|
1869
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1870
|
+
return value;
|
|
1871
|
+
}
|
|
1872
|
+
return 0;
|
|
1873
|
+
}
|
|
1874
|
+
function listProjectDirs(...roots) {
|
|
1875
|
+
const dirs = new Set;
|
|
1876
|
+
for (const root of roots) {
|
|
1877
|
+
if (!existsSync5(root))
|
|
1878
|
+
continue;
|
|
1879
|
+
try {
|
|
1880
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
1881
|
+
if (entry.isDirectory())
|
|
1882
|
+
dirs.add(join5(root, entry.name));
|
|
1883
|
+
}
|
|
1884
|
+
} catch {}
|
|
1885
|
+
}
|
|
1886
|
+
return [...dirs];
|
|
1887
|
+
}
|
|
1888
|
+
function projectRoot(projectDir, chatData) {
|
|
1889
|
+
if (chatData.projectPath)
|
|
1890
|
+
return chatData.projectPath;
|
|
1891
|
+
if (chatData.project_path)
|
|
1892
|
+
return chatData.project_path;
|
|
1893
|
+
const rootFile = join5(projectDir, ".project_root");
|
|
1894
|
+
try {
|
|
1895
|
+
if (existsSync5(rootFile))
|
|
1896
|
+
return readFileSync4(rootFile, "utf-8").trim();
|
|
1897
|
+
} catch {}
|
|
1898
|
+
return "";
|
|
1899
|
+
}
|
|
1900
|
+
async function ingestGemini(db, verbose) {
|
|
1901
|
+
const tmpDir = geminiTmpDir();
|
|
1902
|
+
const historyDir = geminiHistoryDir();
|
|
1903
|
+
if (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
|
|
1904
|
+
if (verbose)
|
|
1905
|
+
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
1906
|
+
return { sessions: 0, requests: 0 };
|
|
1907
|
+
}
|
|
1908
|
+
const machineId = getMachineId();
|
|
1909
|
+
let totalSessions = 0;
|
|
1910
|
+
let totalRequests = 0;
|
|
1911
|
+
const touchedSessions = new Set;
|
|
1912
|
+
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
1913
|
+
for (const projectDir of projectDirs) {
|
|
1914
|
+
const chatsDir = join5(projectDir, "chats");
|
|
1915
|
+
if (!existsSync5(chatsDir))
|
|
1916
|
+
continue;
|
|
1917
|
+
let chatFiles = [];
|
|
1918
|
+
try {
|
|
1919
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
|
|
1920
|
+
} catch {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
for (const filePath of chatFiles) {
|
|
1924
|
+
const stateKey = filePath.replace(homedir4(), "~");
|
|
1925
|
+
let fileMtime = "0";
|
|
1926
|
+
try {
|
|
1927
|
+
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
1928
|
+
} catch {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const processed = getIngestState(db, "gemini", stateKey);
|
|
1932
|
+
if (processed === fileMtime)
|
|
1933
|
+
continue;
|
|
1934
|
+
let chatData;
|
|
1935
|
+
try {
|
|
1936
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
1937
|
+
} catch {
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
|
|
1941
|
+
if (!sessionId)
|
|
1942
|
+
continue;
|
|
1943
|
+
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
1944
|
+
const projectPath = projectRoot(projectDir, chatData);
|
|
1945
|
+
const projectName = projectPath ? basename3(projectPath) : "";
|
|
1946
|
+
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
1947
|
+
if (!existing) {
|
|
1948
|
+
const session = {
|
|
1949
|
+
id: sessionId,
|
|
1950
|
+
agent: "gemini",
|
|
1951
|
+
project_path: projectPath,
|
|
1952
|
+
project_name: projectName,
|
|
1953
|
+
started_at: startTime,
|
|
1954
|
+
ended_at: chatData.lastUpdated ?? null,
|
|
1955
|
+
total_cost_usd: 0,
|
|
1956
|
+
total_tokens: 0,
|
|
1957
|
+
request_count: 0,
|
|
1958
|
+
machine_id: machineId
|
|
1959
|
+
};
|
|
1960
|
+
upsertSession(db, session);
|
|
1961
|
+
totalSessions++;
|
|
1962
|
+
}
|
|
1963
|
+
touchedSessions.add(sessionId);
|
|
1964
|
+
for (const [index, message] of (chatData.messages ?? []).entries()) {
|
|
1965
|
+
const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
|
|
1966
|
+
if (!usage)
|
|
1967
|
+
continue;
|
|
1968
|
+
const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
|
|
1969
|
+
if (!model)
|
|
1970
|
+
continue;
|
|
1971
|
+
const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
|
|
1972
|
+
const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
|
|
1973
|
+
const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
|
|
1974
|
+
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
1975
|
+
const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
|
|
1976
|
+
const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
|
|
1977
|
+
const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
|
|
1978
|
+
if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
|
|
1979
|
+
continue;
|
|
1980
|
+
const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
1981
|
+
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
1982
|
+
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
1983
|
+
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
1984
|
+
upsertRequest(db, {
|
|
1985
|
+
id: requestId,
|
|
1986
|
+
agent: "gemini",
|
|
1987
|
+
session_id: sessionId,
|
|
1988
|
+
model,
|
|
1989
|
+
input_tokens: inputTokens,
|
|
1990
|
+
output_tokens: outputTokens,
|
|
1991
|
+
cache_read_tokens: cacheReadTokens,
|
|
1992
|
+
cache_create_tokens: 0,
|
|
1993
|
+
cost_usd: costUsd,
|
|
1994
|
+
cost_basis: defaultCostBasisForAgent("gemini"),
|
|
1995
|
+
duration_ms: 0,
|
|
1996
|
+
timestamp,
|
|
1997
|
+
source_request_id: message.id ?? requestId,
|
|
1998
|
+
machine_id: machineId
|
|
1999
|
+
});
|
|
2000
|
+
totalRequests++;
|
|
2001
|
+
}
|
|
2002
|
+
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
for (const sessionId of touchedSessions) {
|
|
2006
|
+
rollupSession(db, sessionId);
|
|
2007
|
+
}
|
|
2008
|
+
return { sessions: totalSessions, requests: totalRequests };
|
|
1107
2009
|
}
|
|
1108
2010
|
export {
|
|
2011
|
+
upsertUsageSnapshot,
|
|
2012
|
+
upsertSubscription,
|
|
1109
2013
|
upsertSession,
|
|
1110
2014
|
upsertRequest,
|
|
1111
2015
|
upsertProject,
|
|
@@ -1113,11 +2017,13 @@ export {
|
|
|
1113
2017
|
upsertGoal,
|
|
1114
2018
|
upsertBudget,
|
|
1115
2019
|
upsertBillingDaily,
|
|
2020
|
+
syncOpenProjectsRegistry,
|
|
1116
2021
|
setIngestState,
|
|
1117
2022
|
setActiveModel,
|
|
1118
2023
|
seedModelPricing,
|
|
1119
2024
|
rollupSession,
|
|
1120
2025
|
readCodexModel,
|
|
2026
|
+
queryUsageSnapshots,
|
|
1121
2027
|
queryTopSessions,
|
|
1122
2028
|
querySummary,
|
|
1123
2029
|
querySessions,
|
|
@@ -1128,12 +2034,17 @@ export {
|
|
|
1128
2034
|
queryBillingSummary,
|
|
1129
2035
|
openDatabase,
|
|
1130
2036
|
normalizeModelName,
|
|
2037
|
+
listSubscriptions,
|
|
1131
2038
|
listProjects,
|
|
1132
2039
|
listModelPricing,
|
|
1133
2040
|
listMachines,
|
|
2041
|
+
listMachineRegistry,
|
|
1134
2042
|
listGoals,
|
|
1135
2043
|
listBudgets,
|
|
2044
|
+
isAgent,
|
|
1136
2045
|
ingestTakumi,
|
|
2046
|
+
ingestJsonlProjects,
|
|
2047
|
+
ingestGemini,
|
|
1137
2048
|
ingestCodex,
|
|
1138
2049
|
ingestClaude,
|
|
1139
2050
|
getProject,
|
|
@@ -1149,14 +2060,18 @@ export {
|
|
|
1149
2060
|
getActiveModel,
|
|
1150
2061
|
gatherTrainingData,
|
|
1151
2062
|
ensurePricingSeeded,
|
|
2063
|
+
deleteSubscription,
|
|
1152
2064
|
deleteProject,
|
|
1153
2065
|
deleteModelPricing,
|
|
1154
2066
|
deleteGoal,
|
|
1155
2067
|
deleteBudget,
|
|
2068
|
+
dedupeRequests,
|
|
1156
2069
|
computeCostFromDb,
|
|
1157
2070
|
computeCost,
|
|
1158
2071
|
clearBillingRange,
|
|
1159
2072
|
clearActiveModel,
|
|
1160
2073
|
DEFAULT_PRICING,
|
|
1161
|
-
DEFAULT_MODEL
|
|
2074
|
+
DEFAULT_MODEL,
|
|
2075
|
+
COST_BASIS,
|
|
2076
|
+
AGENTS
|
|
1162
2077
|
};
|