@hasna/economy 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +1 -2
  2. package/README.md +5 -13
  3. package/dist/cli/commands/completion.d.ts +2 -0
  4. package/dist/cli/commands/completion.d.ts.map +1 -0
  5. package/dist/cli/commands/extras.d.ts +4 -0
  6. package/dist/cli/commands/extras.d.ts.map +1 -0
  7. package/dist/cli/commands/menubar.d.ts.map +1 -1
  8. package/dist/cli/commands/notification.d.ts +8 -0
  9. package/dist/cli/commands/notification.d.ts.map +1 -0
  10. package/dist/cli/commands/todos.d.ts +26 -0
  11. package/dist/cli/commands/todos.d.ts.map +1 -0
  12. package/dist/cli/commands/tui.d.ts +10 -0
  13. package/dist/cli/commands/tui.d.ts.map +1 -0
  14. package/dist/cli/commands/watch.d.ts +1 -0
  15. package/dist/cli/commands/watch.d.ts.map +1 -1
  16. package/dist/cli/index.js +5649 -708
  17. package/dist/db/database.d.ts +45 -3
  18. package/dist/db/database.d.ts.map +1 -1
  19. package/dist/db/pg-migrations.d.ts.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1576 -142
  23. package/dist/ingest/billing.d.ts +27 -0
  24. package/dist/ingest/billing.d.ts.map +1 -0
  25. package/dist/ingest/claude-quota.d.ts +5 -0
  26. package/dist/ingest/claude-quota.d.ts.map +1 -0
  27. package/dist/ingest/claude.d.ts +13 -2
  28. package/dist/ingest/claude.d.ts.map +1 -1
  29. package/dist/ingest/codex-quota.d.ts +5 -0
  30. package/dist/ingest/codex-quota.d.ts.map +1 -0
  31. package/dist/ingest/codex.d.ts +2 -1
  32. package/dist/ingest/codex.d.ts.map +1 -1
  33. package/dist/ingest/cursor.d.ts +6 -0
  34. package/dist/ingest/cursor.d.ts.map +1 -0
  35. package/dist/ingest/gemini.d.ts +2 -1
  36. package/dist/ingest/gemini.d.ts.map +1 -1
  37. package/dist/ingest/hermes.d.ts +6 -0
  38. package/dist/ingest/hermes.d.ts.map +1 -0
  39. package/dist/ingest/opencode.d.ts +7 -0
  40. package/dist/ingest/opencode.d.ts.map +1 -0
  41. package/dist/ingest/otel.d.ts +20 -0
  42. package/dist/ingest/otel.d.ts.map +1 -0
  43. package/dist/ingest/pi.d.ts +7 -0
  44. package/dist/ingest/pi.d.ts.map +1 -0
  45. package/dist/ingest/plugin.d.ts +17 -0
  46. package/dist/ingest/plugin.d.ts.map +1 -0
  47. package/dist/lib/accounts.d.ts +11 -0
  48. package/dist/lib/accounts.d.ts.map +1 -0
  49. package/dist/lib/agents.d.ts +11 -0
  50. package/dist/lib/agents.d.ts.map +1 -0
  51. package/dist/lib/billing-diff.d.ts +22 -0
  52. package/dist/lib/billing-diff.d.ts.map +1 -0
  53. package/dist/lib/cloud-sync.d.ts +35 -0
  54. package/dist/lib/cloud-sync.d.ts.map +1 -0
  55. package/dist/lib/config.d.ts.map +1 -1
  56. package/dist/lib/gatherer.d.ts.map +1 -1
  57. package/dist/lib/model-config.d.ts.map +1 -1
  58. package/dist/lib/open-projects.d.ts +19 -0
  59. package/dist/lib/open-projects.d.ts.map +1 -0
  60. package/dist/lib/package-metadata.d.ts +8 -0
  61. package/dist/lib/package-metadata.d.ts.map +1 -0
  62. package/dist/lib/paths.d.ts +20 -0
  63. package/dist/lib/paths.d.ts.map +1 -0
  64. package/dist/lib/pricing.d.ts +3 -3
  65. package/dist/lib/pricing.d.ts.map +1 -1
  66. package/dist/lib/savings.d.ts +17 -0
  67. package/dist/lib/savings.d.ts.map +1 -0
  68. package/dist/lib/serve-auth.d.ts +4 -0
  69. package/dist/lib/serve-auth.d.ts.map +1 -0
  70. package/dist/lib/spikes.d.ts +18 -0
  71. package/dist/lib/spikes.d.ts.map +1 -0
  72. package/dist/lib/sync-all.d.ts +28 -0
  73. package/dist/lib/sync-all.d.ts.map +1 -0
  74. package/dist/lib/watch-paths.d.ts +3 -0
  75. package/dist/lib/watch-paths.d.ts.map +1 -0
  76. package/dist/lib/webhooks.d.ts +1 -1
  77. package/dist/lib/webhooks.d.ts.map +1 -1
  78. package/dist/mcp/index.js +3063 -482
  79. package/dist/otel/index.d.ts +3 -0
  80. package/dist/otel/index.d.ts.map +1 -0
  81. package/dist/otel/index.js +1423 -0
  82. package/dist/server/index.d.ts +1 -0
  83. package/dist/server/index.js +3550 -269
  84. package/dist/server/serve.d.ts +10 -2
  85. package/dist/server/serve.d.ts.map +1 -1
  86. package/dist/types/index.d.ts +102 -6
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/package.json +9 -4
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var __export = (target, all) => {
14
14
  });
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+ var __require = import.meta.require;
17
18
 
18
19
  // src/lib/pricing.ts
19
20
  var exports_pricing = {};
@@ -27,88 +28,503 @@ __export(exports_pricing, {
27
28
  DEFAULT_PRICING: () => DEFAULT_PRICING
28
29
  });
29
30
  function normalizeModelName(raw) {
30
- return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
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
- const normalized = normalizeModelName(model);
37
- const row = getModelPricing(db, normalized);
38
- if (row) {
39
- return {
40
- inputPer1M: row.input_per_1m,
41
- outputPer1M: row.output_per_1m,
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
- for (const r of allRows) {
48
- if (normalized.startsWith(r.model)) {
49
- return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
50
- }
51
- }
52
- return null;
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
- const normalized = normalizeModelName(model);
56
- if (DEFAULT_PRICING[normalized])
57
- return DEFAULT_PRICING[normalized] ?? null;
58
- for (const key of Object.keys(DEFAULT_PRICING)) {
59
- if (normalized.startsWith(key))
60
- return DEFAULT_PRICING[key] ?? null;
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 (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
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 (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
213
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
75
214
  }
76
- var DEFAULT_PRICING;
215
+ function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
216
+ if (isFreeModel(model))
217
+ return 0;
218
+ let effective = pricing;
219
+ const promptTier = bestModelMatch(model, Object.entries(GEMINI_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(QWEN_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(MINIMAX_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(XAI_PROMPT_TIERS)) ?? exactModelMatch(model, Object.entries(OPENAI_PROMPT_TIERS));
220
+ if (promptTier) {
221
+ const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
222
+ if (billablePromptTokens > promptTier.threshold) {
223
+ effective = {
224
+ ...pricing,
225
+ inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
226
+ outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
227
+ cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
228
+ cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
229
+ };
230
+ }
231
+ }
232
+ return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
233
+ }
234
+ var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS;
77
235
  var init_pricing = __esm(() => {
78
236
  init_database();
79
237
  DEFAULT_PRICING = {
80
- "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
81
- "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
82
- "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
83
- "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
84
- "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
85
- "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
238
+ "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
239
+ "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
240
+ "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
241
+ "claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
242
+ "claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
243
+ "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
244
+ "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
245
+ "claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
246
+ "claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
247
+ "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
248
+ "claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
249
+ "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
250
+ "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
251
+ "gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
252
+ "gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
253
+ "gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
254
+ "gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
255
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
256
+ "gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
257
+ "gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
258
+ "gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
259
+ "gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
260
+ "google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
261
+ "google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
262
+ "google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
263
+ "google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
264
+ "google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
265
+ "google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
266
+ "google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
267
+ "gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
268
+ "gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
269
+ "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
270
+ "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
271
+ "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
272
+ "gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
273
+ "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
274
+ "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
275
+ "gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
276
+ "gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
277
+ "gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
278
+ "gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
279
+ "gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
280
+ "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
281
+ "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
282
+ o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
283
+ "o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
284
+ o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
285
+ "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
286
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
287
+ "qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
288
+ "qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
289
+ "qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
290
+ "qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
291
+ "qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
292
+ "qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
293
+ "qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
294
+ "qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
295
+ "qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
296
+ "qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
297
+ "minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
298
+ "minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
299
+ "minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
300
+ "minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
301
+ "minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
302
+ "grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
303
+ "grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
304
+ "grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
305
+ "grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
306
+ "grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
307
+ "grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
308
+ "grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
309
+ "grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
310
+ "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
311
+ "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
312
+ "glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
313
+ "glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
314
+ "z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
315
+ "z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
316
+ "kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
317
+ "kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
318
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
319
+ "moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
320
+ "moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
321
+ "moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
322
+ };
323
+ LEGACY_DEFAULT_PRICING = {
86
324
  "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
87
- "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
88
- "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
89
- "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
325
+ "claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
326
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
327
+ "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
90
328
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
91
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
92
- "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
- "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
329
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
95
330
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
96
331
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
97
- "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
98
- "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
99
- o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
332
+ "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
333
+ "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
100
334
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
101
- o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
102
- "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
103
- "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 },
337
+ "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
338
+ "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
339
+ "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
340
+ "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
341
+ "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
342
+ "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
343
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
344
+ o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
345
+ };
346
+ ADDITIONAL_LEGACY_DEFAULT_PRICING = {
347
+ "gemini-2.5-pro": [
348
+ { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
349
+ ],
350
+ "qwen3.6-plus": [
351
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
352
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
353
+ ],
354
+ "qwen3.6-flash": [
355
+ { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
356
+ ],
357
+ "qwen3.6-max-preview": [
358
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
359
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
360
+ ],
361
+ "qwen/qwen3.6-plus": [
362
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
363
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
364
+ ],
365
+ "qwen/qwen3.6-flash": [
366
+ { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
367
+ ],
368
+ "qwen/qwen3.6-max-preview": [
369
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
370
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
371
+ ]
372
+ };
373
+ REMOVED_DEFAULT_PRICING = {
374
+ "claude-3-5-sonnet": [
375
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
376
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
377
+ ],
378
+ "claude-3-sonnet": [
379
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
380
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
381
+ ],
382
+ "gemini-3.1-pro": [
383
+ { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
384
+ { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
385
+ ],
386
+ "gemini-1.5-pro": [
387
+ { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
388
+ ],
389
+ "gemini-1.5-flash": [
390
+ { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
391
+ ],
392
+ "gpt-5.3-chat": [
393
+ { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
394
+ { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
395
+ ],
396
+ "qwen3.6": [
397
+ { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
398
+ ]
399
+ };
400
+ FREE_PRICING = {
401
+ inputPer1M: 0,
402
+ outputPer1M: 0,
403
+ cacheReadPer1M: 0,
404
+ cacheWritePer1M: 0,
405
+ cacheWrite1hPer1M: 0,
406
+ cacheStoragePer1MHour: 0
407
+ };
408
+ GEMINI_PROMPT_TIERS = {
409
+ "gemini-3.1-pro-preview": {
410
+ threshold: 200000,
411
+ inputPer1M: 4,
412
+ outputPer1M: 18,
413
+ cacheReadPer1M: 0.4
414
+ },
415
+ "gemini-2.5-pro": {
416
+ threshold: 200000,
417
+ inputPer1M: 2.5,
418
+ outputPer1M: 15,
419
+ cacheReadPer1M: 0.25
420
+ }
421
+ };
422
+ OPENAI_PROMPT_TIERS = {
423
+ "gpt-5.5": {
424
+ threshold: 272000,
425
+ inputMultiplier: 2,
426
+ outputMultiplier: 1.5,
427
+ cacheReadMultiplier: 2
428
+ },
429
+ "gpt-5.4-pro": {
430
+ threshold: 272000,
431
+ inputMultiplier: 2,
432
+ outputMultiplier: 1.5,
433
+ cacheReadMultiplier: 2
434
+ },
435
+ "gpt-5.4": {
436
+ threshold: 272000,
437
+ inputMultiplier: 2,
438
+ outputMultiplier: 1.5,
439
+ cacheReadMultiplier: 2
440
+ }
441
+ };
442
+ QWEN_PROMPT_TIERS = {
443
+ "qwen3.6-plus": {
444
+ threshold: 256000,
445
+ inputPer1M: 1.3,
446
+ outputPer1M: 3.9,
447
+ cacheReadPer1M: 0.13,
448
+ cacheWritePer1M: 1.625
449
+ },
450
+ "qwen3.6-flash": {
451
+ threshold: 256000,
452
+ inputPer1M: 1,
453
+ outputPer1M: 4,
454
+ cacheReadPer1M: 0.1,
455
+ cacheWritePer1M: 1.25
456
+ },
457
+ "qwen3.6-max-preview": {
458
+ threshold: 128000,
459
+ inputPer1M: 1.6,
460
+ outputPer1M: 9.6,
461
+ cacheReadPer1M: 0.16,
462
+ cacheWritePer1M: 2
463
+ }
464
+ };
465
+ MINIMAX_PROMPT_TIERS = {
466
+ "minimax/minimax-m1": {
467
+ threshold: Number.POSITIVE_INFINITY
468
+ },
469
+ "minimax-m1": {
470
+ threshold: 200000,
471
+ inputPer1M: 1.3
472
+ }
473
+ };
474
+ XAI_PROMPT_TIERS = {
475
+ "grok-4.3": {
476
+ threshold: 200000,
477
+ inputPer1M: 2.5,
478
+ outputPer1M: 5,
479
+ cacheReadPer1M: 0.4
480
+ },
481
+ "grok-latest": {
482
+ threshold: 200000,
483
+ inputPer1M: 2.5,
484
+ outputPer1M: 5,
485
+ cacheReadPer1M: 0.4
486
+ },
487
+ "grok-4.20": {
488
+ threshold: 200000,
489
+ inputPer1M: 2.5,
490
+ outputPer1M: 5,
491
+ cacheReadPer1M: 0.4
492
+ },
493
+ "grok-4-1-fast": {
494
+ threshold: 128000,
495
+ inputPer1M: 0.4,
496
+ outputPer1M: 1,
497
+ cacheReadPer1M: 0
498
+ },
499
+ "grok-4-fast": {
500
+ threshold: 128000,
501
+ inputPer1M: 0.4,
502
+ outputPer1M: 1,
503
+ cacheReadPer1M: 0
504
+ },
505
+ "grok-4": {
506
+ threshold: 128000,
507
+ inputPer1M: 6,
508
+ outputPer1M: 30,
509
+ cacheReadPer1M: 0
510
+ }
104
511
  };
105
512
  });
106
513
 
107
514
  // src/db/database.ts
108
515
  import { SqliteAdapter as Database } from "@hasna/cloud";
109
516
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
517
+ import { hostname } from "os";
110
518
  import { homedir } from "os";
111
519
  import { join } from "path";
520
+ function getMachineId() {
521
+ if (process.env["ECONOMY_MACHINE_ID"])
522
+ return process.env["ECONOMY_MACHINE_ID"];
523
+ const h = hostname().toLowerCase();
524
+ if (h.startsWith("spark") || h.startsWith("apple"))
525
+ return h.split(".")[0];
526
+ return h.split(".")[0];
527
+ }
112
528
  function getDataDir() {
113
529
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
114
530
  const newDir = join(home, ".hasna", "economy");
@@ -141,6 +557,7 @@ function openDatabase(dbPath, skipSeed = false) {
141
557
  }
142
558
  const db = new Database(path);
143
559
  db.exec("PRAGMA journal_mode = WAL");
560
+ db.exec("PRAGMA busy_timeout = 5000");
144
561
  db.exec("PRAGMA foreign_keys = ON");
145
562
  initSchema(db);
146
563
  if (!skipSeed) {
@@ -159,10 +576,18 @@ function initSchema(db) {
159
576
  output_tokens INTEGER DEFAULT 0,
160
577
  cache_read_tokens INTEGER DEFAULT 0,
161
578
  cache_create_tokens INTEGER DEFAULT 0,
579
+ cache_create_5m_tokens INTEGER DEFAULT 0,
580
+ cache_create_1h_tokens INTEGER DEFAULT 0,
162
581
  cost_usd REAL NOT NULL DEFAULT 0,
163
582
  duration_ms INTEGER DEFAULT 0,
164
583
  timestamp TEXT NOT NULL,
165
- source_request_id TEXT
584
+ source_request_id TEXT,
585
+ machine_id TEXT DEFAULT '',
586
+ account_key TEXT DEFAULT '',
587
+ account_tool TEXT DEFAULT '',
588
+ account_name TEXT DEFAULT '',
589
+ account_email TEXT DEFAULT '',
590
+ account_source TEXT DEFAULT ''
166
591
  );
167
592
 
168
593
  CREATE TABLE IF NOT EXISTS sessions (
@@ -174,7 +599,13 @@ function initSchema(db) {
174
599
  ended_at TEXT,
175
600
  total_cost_usd REAL DEFAULT 0,
176
601
  total_tokens INTEGER DEFAULT 0,
177
- request_count INTEGER DEFAULT 0
602
+ request_count INTEGER DEFAULT 0,
603
+ machine_id TEXT DEFAULT '',
604
+ account_key TEXT DEFAULT '',
605
+ account_tool TEXT DEFAULT '',
606
+ account_name TEXT DEFAULT '',
607
+ account_email TEXT DEFAULT '',
608
+ account_source TEXT DEFAULT ''
178
609
  );
179
610
 
180
611
  CREATE TABLE IF NOT EXISTS projects (
@@ -227,6 +658,8 @@ function initSchema(db) {
227
658
  output_per_1m REAL NOT NULL DEFAULT 0,
228
659
  cache_read_per_1m REAL NOT NULL DEFAULT 0,
229
660
  cache_write_per_1m REAL NOT NULL DEFAULT 0,
661
+ cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
662
+ cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
230
663
  updated_at TEXT NOT NULL
231
664
  );
232
665
 
@@ -239,6 +672,127 @@ function initSchema(db) {
239
672
  machine_id TEXT,
240
673
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
241
674
  );
675
+
676
+ CREATE TABLE IF NOT EXISTS billing_daily (
677
+ date TEXT NOT NULL,
678
+ provider TEXT NOT NULL,
679
+ description TEXT DEFAULT '',
680
+ cost_usd REAL NOT NULL DEFAULT 0,
681
+ updated_at TEXT NOT NULL,
682
+ PRIMARY KEY (date, provider, description)
683
+ );
684
+
685
+ CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
686
+ CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
687
+
688
+ CREATE TABLE IF NOT EXISTS subscriptions (
689
+ id TEXT PRIMARY KEY,
690
+ agent TEXT,
691
+ provider TEXT NOT NULL,
692
+ plan TEXT NOT NULL,
693
+ monthly_fee_usd REAL NOT NULL DEFAULT 0,
694
+ included_usage_usd REAL NOT NULL DEFAULT 0,
695
+ billing_cycle_start TEXT,
696
+ reset_policy TEXT DEFAULT 'monthly',
697
+ active INTEGER NOT NULL DEFAULT 1,
698
+ created_at TEXT NOT NULL,
699
+ updated_at TEXT NOT NULL
700
+ );
701
+
702
+ CREATE TABLE IF NOT EXISTS usage_snapshots (
703
+ id TEXT PRIMARY KEY,
704
+ agent TEXT NOT NULL,
705
+ date TEXT NOT NULL,
706
+ metric TEXT NOT NULL,
707
+ value REAL NOT NULL DEFAULT 0,
708
+ unit TEXT DEFAULT '',
709
+ machine_id TEXT DEFAULT '',
710
+ updated_at TEXT NOT NULL
711
+ );
712
+
713
+ CREATE TABLE IF NOT EXISTS savings_daily (
714
+ date TEXT NOT NULL,
715
+ agent TEXT DEFAULT '',
716
+ api_equivalent_usd REAL NOT NULL DEFAULT 0,
717
+ subscription_fee_usd REAL NOT NULL DEFAULT 0,
718
+ included_consumed_usd REAL NOT NULL DEFAULT 0,
719
+ on_demand_usd REAL NOT NULL DEFAULT 0,
720
+ saved_usd REAL NOT NULL DEFAULT 0,
721
+ updated_at TEXT NOT NULL,
722
+ PRIMARY KEY (date, agent)
723
+ );
724
+
725
+ CREATE TABLE IF NOT EXISTS machines (
726
+ machine_id TEXT PRIMARY KEY,
727
+ hostname TEXT NOT NULL,
728
+ last_seen_at TEXT,
729
+ last_push_at TEXT,
730
+ last_pull_at TEXT,
731
+ economy_version TEXT,
732
+ updated_at TEXT NOT NULL
733
+ );
734
+
735
+ CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
736
+ CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
737
+ `);
738
+ const cols = db.prepare(`PRAGMA table_info(requests)`).all();
739
+ if (!cols.some((c) => c.name === "machine_id")) {
740
+ db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
741
+ db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
742
+ }
743
+ if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
744
+ db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
745
+ db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
746
+ }
747
+ if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
748
+ db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
749
+ }
750
+ if (!cols.some((c) => c.name === "cost_basis")) {
751
+ db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
752
+ }
753
+ if (!cols.some((c) => c.name === "attribution_tag")) {
754
+ db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
755
+ }
756
+ if (!cols.some((c) => c.name === "updated_at")) {
757
+ db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
758
+ db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
759
+ }
760
+ if (!cols.some((c) => c.name === "synced_at")) {
761
+ db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
762
+ }
763
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
764
+ if (!cols.some((c) => c.name === column)) {
765
+ db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
766
+ }
767
+ }
768
+ const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
769
+ if (!sessionCols.some((c) => c.name === "attribution_tag")) {
770
+ db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
771
+ }
772
+ if (!sessionCols.some((c) => c.name === "updated_at")) {
773
+ db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
774
+ db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
775
+ }
776
+ if (!sessionCols.some((c) => c.name === "synced_at")) {
777
+ db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
778
+ }
779
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
780
+ if (!sessionCols.some((c) => c.name === column)) {
781
+ db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
782
+ }
783
+ }
784
+ const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
785
+ if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
786
+ db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
787
+ }
788
+ if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
789
+ db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
790
+ }
791
+ db.exec(`
792
+ CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
793
+ CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
794
+ CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
795
+ CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
242
796
  `);
243
797
  }
244
798
  function periodWhere(period) {
@@ -248,11 +802,11 @@ function periodWhere(period) {
248
802
  case "yesterday":
249
803
  return `DATE(timestamp) = DATE('now', '-1 day')`;
250
804
  case "week":
251
- return `timestamp >= DATE('now', '-7 days')`;
805
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
252
806
  case "month":
253
- return `timestamp >= DATE('now', '-30 days')`;
807
+ return `timestamp >= DATE('now', 'start of month')`;
254
808
  case "year":
255
- return `timestamp >= DATE('now', '-365 days')`;
809
+ return `timestamp >= DATE('now', 'start of year')`;
256
810
  case "all":
257
811
  return "1=1";
258
812
  }
@@ -264,31 +818,52 @@ function sessionPeriodWhere(period) {
264
818
  case "yesterday":
265
819
  return `DATE(started_at) = DATE('now', '-1 day')`;
266
820
  case "week":
267
- return `started_at >= DATE('now', '-7 days')`;
821
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
268
822
  case "month":
269
- return `started_at >= DATE('now', '-30 days')`;
823
+ return `started_at >= DATE('now', 'start of month')`;
270
824
  case "year":
271
- return `started_at >= DATE('now', '-365 days')`;
825
+ return `started_at >= DATE('now', 'start of year')`;
826
+ case "all":
827
+ return "1=1";
828
+ }
829
+ }
830
+ function requestPeriodWhere(period) {
831
+ switch (period) {
832
+ case "today":
833
+ return `DATE(timestamp) = DATE('now')`;
834
+ case "yesterday":
835
+ return `DATE(timestamp) = DATE('now', '-1 day')`;
836
+ case "week":
837
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
838
+ case "month":
839
+ return `timestamp >= DATE('now', 'start of month')`;
840
+ case "year":
841
+ return `timestamp >= DATE('now', 'start of year')`;
272
842
  case "all":
273
843
  return "1=1";
274
844
  }
275
845
  }
276
846
  function upsertRequest(db, req) {
847
+ const now = req.updated_at ?? new Date().toISOString();
277
848
  db.prepare(`
278
849
  INSERT OR REPLACE INTO requests
279
850
  (id, agent, session_id, model, input_tokens, output_tokens,
280
- cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
281
- timestamp, source_request_id)
282
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
283
- `).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.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
851
+ cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
852
+ cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
853
+ source_request_id, machine_id, attribution_tag, account_key, account_tool,
854
+ account_name, account_email, account_source, updated_at, synced_at)
855
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
856
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
284
857
  }
285
858
  function upsertSession(db, session) {
859
+ const now = session.updated_at ?? new Date().toISOString();
286
860
  db.prepare(`
287
861
  INSERT OR REPLACE INTO sessions
288
862
  (id, agent, project_path, project_name, started_at, ended_at,
289
- total_cost_usd, total_tokens, request_count)
290
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
291
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count);
863
+ total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
864
+ account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
865
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
866
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
292
867
  }
293
868
  function rollupSession(db, sessionId) {
294
869
  db.prepare(`
@@ -299,9 +874,24 @@ function rollupSession(db, sessionId) {
299
874
  ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
300
875
  started_at = CASE WHEN started_at = '' OR started_at IS NULL
301
876
  THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
302
- ELSE started_at END
877
+ ELSE started_at END,
878
+ account_key = CASE WHEN account_key = '' OR account_key IS NULL
879
+ THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
880
+ ELSE account_key END,
881
+ account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
882
+ THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
883
+ ELSE account_tool END,
884
+ account_name = CASE WHEN account_name = '' OR account_name IS NULL
885
+ THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
886
+ ELSE account_name END,
887
+ account_email = CASE WHEN account_email = '' OR account_email IS NULL
888
+ THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
889
+ ELSE account_email END,
890
+ account_source = CASE WHEN account_source = '' OR account_source IS NULL
891
+ THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
892
+ ELSE account_source END
303
893
  WHERE id = ?
304
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
894
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
305
895
  }
306
896
  function querySessions(db, filter = {}) {
307
897
  const conditions = [];
@@ -314,10 +904,19 @@ function querySessions(db, filter = {}) {
314
904
  conditions.push("project_path LIKE ?");
315
905
  params.push(`%${filter.project}%`);
316
906
  }
907
+ if (filter.account) {
908
+ const q = `%${filter.account}%`;
909
+ conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
910
+ params.push(q, q, q);
911
+ }
317
912
  if (filter.since) {
318
913
  conditions.push("started_at >= ?");
319
914
  params.push(filter.since);
320
915
  }
916
+ if (filter.machine) {
917
+ conditions.push("machine_id = ?");
918
+ params.push(filter.machine);
919
+ }
321
920
  if (filter.search) {
322
921
  const q = `%${filter.search}%`;
323
922
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -336,24 +935,25 @@ function queryTopSessions(db, n = 10, agent) {
336
935
  }
337
936
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
338
937
  }
339
- function querySummary(db, period) {
938
+ function querySummary(db, period, machine, allMachines = false) {
340
939
  const rWhere = periodWhere(period);
341
940
  const sWhere = sessionPeriodWhere(period);
941
+ const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
342
942
  const r = db.prepare(`
343
943
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
344
944
  COUNT(*) as requests,
345
945
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
346
- FROM requests WHERE ${rWhere}
946
+ FROM requests WHERE ${rWhere}${machineClause}
347
947
  `).get();
348
948
  const codexTotals = db.prepare(`
349
949
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
350
950
  COALESCE(SUM(total_tokens), 0) as tokens,
351
951
  COUNT(*) as sessions
352
952
  FROM sessions
353
- WHERE ${sWhere}
953
+ WHERE ${sWhere}${machineClause}
354
954
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
355
955
  `).get();
356
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
956
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
357
957
  return {
358
958
  total_usd: r.total_usd + codexTotals.cost_usd,
359
959
  requests: r.requests,
@@ -373,23 +973,213 @@ function queryModelBreakdown(db) {
373
973
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
374
974
  `).all();
375
975
  }
376
- function queryProjectBreakdown(db) {
377
- return db.prepare(`
378
- SELECT
379
- s.project_path,
380
- COALESCE(p.name, s.project_name) as project_name,
381
- COUNT(DISTINCT s.id) as sessions,
382
- COUNT(r.id) as requests,
383
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
384
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
385
- MAX(s.started_at) as last_active
386
- FROM sessions s
387
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
388
- LEFT JOIN requests r ON r.session_id = s.id
389
- WHERE s.project_path != '' OR s.project_name != ''
390
- GROUP BY s.project_path
391
- ORDER BY cost_usd DESC
976
+ function queryAgentBreakdown(db, period = "all") {
977
+ const requestWhere = requestPeriodWhere(period);
978
+ const groups = new Map;
979
+ const requestRows = db.prepare(`
980
+ SELECT agent,
981
+ COUNT(DISTINCT session_id) as sessions,
982
+ COUNT(*) as requests,
983
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
984
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
985
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
986
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
987
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
988
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
989
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
990
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
991
+ MAX(timestamp) as last_active
992
+ FROM requests
993
+ WHERE ${requestWhere}
994
+ GROUP BY agent
995
+ ORDER BY api_equivalent_usd DESC
996
+ `).all();
997
+ for (const row of requestRows) {
998
+ groups.set(row.agent, row);
999
+ }
1000
+ const sessionWhere = sessionPeriodWhere(period);
1001
+ const sessionOnlyRows = db.prepare(`
1002
+ SELECT agent,
1003
+ COUNT(*) as sessions,
1004
+ COALESCE(SUM(request_count), 0) as requests,
1005
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1006
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1007
+ MAX(started_at) as last_active
1008
+ FROM sessions
1009
+ WHERE ${sessionWhere}
1010
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1011
+ GROUP BY agent
1012
+ `).all();
1013
+ for (const row of sessionOnlyRows) {
1014
+ const existing = groups.get(row.agent) ?? {
1015
+ agent: row.agent,
1016
+ sessions: 0,
1017
+ requests: 0,
1018
+ total_tokens: 0,
1019
+ api_equivalent_usd: 0,
1020
+ billable_usd: 0,
1021
+ metered_api_usd: 0,
1022
+ subscription_included_usd: 0,
1023
+ estimated_usd: 0,
1024
+ unknown_usd: 0,
1025
+ cost_usd: 0,
1026
+ last_active: ""
1027
+ };
1028
+ existing.sessions += row.sessions;
1029
+ existing.requests += row.requests;
1030
+ existing.total_tokens += row.total_tokens;
1031
+ existing.api_equivalent_usd += row.cost_usd;
1032
+ existing.estimated_usd += row.cost_usd;
1033
+ existing.cost_usd += row.cost_usd;
1034
+ if (!existing.last_active || row.last_active > existing.last_active)
1035
+ existing.last_active = row.last_active;
1036
+ groups.set(row.agent, existing);
1037
+ }
1038
+ return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1039
+ }
1040
+ function labelForPath(projectPath, projectName) {
1041
+ if (projectName && projectName.trim() !== "")
1042
+ return projectName;
1043
+ if (!projectPath)
1044
+ return "";
1045
+ const segments = projectPath.split("/").filter(Boolean);
1046
+ const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
1047
+ for (const seg of segments) {
1048
+ if (projectPrefix.test(seg))
1049
+ return seg;
1050
+ }
1051
+ const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
1052
+ for (let i = segments.length - 1;i >= 0; i--) {
1053
+ if (!generic.has(segments[i].toLowerCase()))
1054
+ return segments[i];
1055
+ }
1056
+ return segments[segments.length - 1] ?? projectPath;
1057
+ }
1058
+ function queryProjectBreakdown(db, period = "all") {
1059
+ const where = sessionPeriodWhere(period);
1060
+ const sessions = db.prepare(`
1061
+ SELECT id, project_path, project_name, total_cost_usd, started_at
1062
+ FROM sessions
1063
+ WHERE ${where}
1064
+ AND (project_path != '' OR project_name != '')
392
1065
  `).all();
1066
+ const groups = new Map;
1067
+ for (const s of sessions) {
1068
+ const label = labelForPath(s.project_path, s.project_name);
1069
+ if (!label)
1070
+ continue;
1071
+ const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
1072
+ g.sessionIds.push(s.id);
1073
+ g.totalCost += s.total_cost_usd || 0;
1074
+ if (!g.lastActive || s.started_at > g.lastActive)
1075
+ g.lastActive = s.started_at;
1076
+ if (!g.samplePath)
1077
+ g.samplePath = s.project_path;
1078
+ groups.set(label, g);
1079
+ }
1080
+ const result = [];
1081
+ for (const [label, g] of groups.entries()) {
1082
+ const placeholders = g.sessionIds.map(() => "?").join(",");
1083
+ const reqStats = placeholders.length ? db.prepare(`
1084
+ SELECT
1085
+ COUNT(*) as requests,
1086
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1087
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
1088
+ FROM requests WHERE session_id IN (${placeholders})
1089
+ `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
1090
+ result.push({
1091
+ project_path: g.samplePath,
1092
+ project_name: label,
1093
+ sessions: g.sessionIds.length,
1094
+ requests: reqStats.requests,
1095
+ total_tokens: reqStats.total_tokens,
1096
+ cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
1097
+ last_active: g.lastActive
1098
+ });
1099
+ }
1100
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
1101
+ return result;
1102
+ }
1103
+ function queryAccountBreakdown(db, period = "all") {
1104
+ const sWhere = sessionPeriodWhere(period);
1105
+ const sessions = db.prepare(`
1106
+ SELECT id, account_key, account_tool, account_name, account_email, account_source,
1107
+ total_cost_usd, total_tokens, request_count, started_at
1108
+ FROM sessions
1109
+ WHERE ${sWhere}
1110
+ AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
1111
+ `).all();
1112
+ const groups = new Map;
1113
+ for (const session of sessions) {
1114
+ const key = session.account_key || `${session.account_tool}:${session.account_name}`;
1115
+ if (!key || key === ":")
1116
+ continue;
1117
+ const group = groups.get(key) ?? {
1118
+ sessionIds: [],
1119
+ account_tool: session.account_tool,
1120
+ account_name: session.account_name,
1121
+ account_email: session.account_email || null,
1122
+ account_source: session.account_source || "unknown",
1123
+ totalCost: 0,
1124
+ totalTokens: 0,
1125
+ requests: 0,
1126
+ lastActive: ""
1127
+ };
1128
+ group.sessionIds.push(session.id);
1129
+ group.totalCost += session.total_cost_usd || 0;
1130
+ group.totalTokens += session.total_tokens || 0;
1131
+ group.requests += session.request_count || 0;
1132
+ if (!group.lastActive || session.started_at > group.lastActive)
1133
+ group.lastActive = session.started_at;
1134
+ groups.set(key, group);
1135
+ }
1136
+ const result = [];
1137
+ for (const [key, group] of groups.entries()) {
1138
+ const placeholders = group.sessionIds.map(() => "?").join(",");
1139
+ const reqStats = placeholders ? db.prepare(`
1140
+ SELECT
1141
+ COUNT(*) as requests,
1142
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1143
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1144
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1145
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1146
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1147
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
1148
+ FROM requests WHERE session_id IN (${placeholders})
1149
+ `).get(...group.sessionIds) : {
1150
+ requests: 0,
1151
+ cost_usd: 0,
1152
+ total_tokens: 0,
1153
+ metered_api_usd: 0,
1154
+ subscription_included_usd: 0,
1155
+ estimated_usd: 0,
1156
+ unknown_usd: 0
1157
+ };
1158
+ const hasRequestCosts = reqStats.requests > 0;
1159
+ const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
1160
+ const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
1161
+ const billableUsd = reqStats.metered_api_usd;
1162
+ result.push({
1163
+ account_key: key,
1164
+ account_tool: group.account_tool,
1165
+ account_name: group.account_name,
1166
+ account_email: group.account_email,
1167
+ account_source: group.account_source,
1168
+ sessions: group.sessionIds.length,
1169
+ requests: reqStats.requests || group.requests,
1170
+ total_tokens: reqStats.total_tokens || group.totalTokens,
1171
+ api_equivalent_usd: apiEquivalentUsd,
1172
+ billable_usd: billableUsd,
1173
+ metered_api_usd: reqStats.metered_api_usd,
1174
+ subscription_included_usd: reqStats.subscription_included_usd,
1175
+ estimated_usd: estimatedUsd,
1176
+ unknown_usd: reqStats.unknown_usd,
1177
+ cost_usd: apiEquivalentUsd,
1178
+ last_active: group.lastActive
1179
+ });
1180
+ }
1181
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
1182
+ return result;
393
1183
  }
394
1184
  function queryDailyBreakdown(db, days = 30) {
395
1185
  return db.prepare(`
@@ -507,12 +1297,46 @@ function setIngestState(db, source, key, value) {
507
1297
  function queryRequestsSince(db, since) {
508
1298
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
509
1299
  }
1300
+ function upsertBillingDaily(db, row) {
1301
+ db.prepare(`
1302
+ INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
1303
+ VALUES (?, ?, ?, ?, ?)
1304
+ `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
1305
+ }
1306
+ function clearBillingRange(db, provider, fromDate, toDate) {
1307
+ db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
1308
+ }
1309
+ function queryBillingSummary(db, period) {
1310
+ 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";
1311
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
1312
+ const by_provider = {};
1313
+ let total = 0;
1314
+ for (const r of rows) {
1315
+ by_provider[r.provider] = r.cost;
1316
+ total += r.cost;
1317
+ }
1318
+ return { total_usd: total, by_provider };
1319
+ }
1320
+ function listMachines(db) {
1321
+ return db.prepare(`
1322
+ SELECT
1323
+ s.machine_id,
1324
+ COUNT(DISTINCT s.id) as sessions,
1325
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
1326
+ COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
1327
+ MAX(s.started_at) as last_active
1328
+ FROM sessions s
1329
+ WHERE s.machine_id != ''
1330
+ GROUP BY s.machine_id
1331
+ ORDER BY total_cost_usd DESC
1332
+ `).all();
1333
+ }
510
1334
  function upsertModelPricing(db, p) {
511
1335
  db.prepare(`
512
1336
  INSERT OR REPLACE INTO model_pricing
513
- (model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
514
- VALUES (?, ?, ?, ?, ?, ?)
515
- `).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
1337
+ (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)
1338
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1339
+ `).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.cache_write_1h_per_1m ?? 0, p.cache_storage_per_1m_hour ?? 0, p.updated_at);
516
1340
  }
517
1341
  function getModelPricing(db, model) {
518
1342
  return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
@@ -524,23 +1348,104 @@ function deleteModelPricing(db, model) {
524
1348
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
525
1349
  }
526
1350
  function seedModelPricing(db, defaults) {
527
- const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
528
- if (existing.count > 0)
529
- return;
1351
+ const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
530
1352
  const now = new Date().toISOString();
531
1353
  for (const [model, p] of Object.entries(defaults)) {
1354
+ if (existing.has(model))
1355
+ continue;
532
1356
  upsertModelPricing(db, {
533
1357
  model,
534
1358
  input_per_1m: p.inputPer1M,
535
1359
  output_per_1m: p.outputPer1M,
536
1360
  cache_read_per_1m: p.cacheReadPer1M,
537
1361
  cache_write_per_1m: p.cacheWritePer1M,
1362
+ cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
1363
+ cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
538
1364
  updated_at: now
539
1365
  });
540
1366
  }
541
1367
  }
1368
+ function upsertSubscription(db, sub) {
1369
+ db.prepare(`
1370
+ INSERT OR REPLACE INTO subscriptions
1371
+ (id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
1372
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1373
+ `).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);
1374
+ }
1375
+ function listSubscriptions(db) {
1376
+ return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
1377
+ }
1378
+ function deleteSubscription(db, id) {
1379
+ db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
1380
+ }
1381
+ function upsertUsageSnapshot(db, snap) {
1382
+ const now = snap.updated_at ?? new Date().toISOString();
1383
+ const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
1384
+ db.prepare(`
1385
+ INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
1386
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1387
+ `).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
1388
+ }
1389
+ function queryUsageSnapshots(db, opts = {}) {
1390
+ const conditions = [];
1391
+ const params = [];
1392
+ if (opts.agent) {
1393
+ conditions.push("agent = ?");
1394
+ params.push(opts.agent);
1395
+ }
1396
+ if (opts.date) {
1397
+ conditions.push("date = ?");
1398
+ params.push(opts.date);
1399
+ }
1400
+ if (opts.since) {
1401
+ conditions.push("date >= ?");
1402
+ params.push(opts.since);
1403
+ }
1404
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1405
+ return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
1406
+ }
1407
+ function listMachineRegistry(db) {
1408
+ return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
1409
+ }
1410
+ function dedupeRequests(db) {
1411
+ const dupes = db.prepare(`
1412
+ SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
1413
+ FROM requests
1414
+ WHERE source_request_id != '' AND source_request_id IS NOT NULL
1415
+ GROUP BY source_request_id, agent
1416
+ HAVING cnt > 1
1417
+ `).all();
1418
+ let removed = 0;
1419
+ for (const row of dupes) {
1420
+ const result = db.prepare(`
1421
+ DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
1422
+ `).run(row.source_request_id, row.agent, row.keep_id);
1423
+ removed += result.changes;
1424
+ }
1425
+ return removed;
1426
+ }
542
1427
  var init_database = () => {};
543
1428
 
1429
+ // src/lib/agents.ts
1430
+ var AGENTS = [
1431
+ "claude",
1432
+ "takumi",
1433
+ "codex",
1434
+ "gemini",
1435
+ "opencode",
1436
+ "cursor",
1437
+ "pi",
1438
+ "hermes"
1439
+ ];
1440
+ var COST_BASIS = [
1441
+ "metered_api",
1442
+ "subscription_included",
1443
+ "estimated",
1444
+ "unknown"
1445
+ ];
1446
+ function isAgent(value) {
1447
+ return AGENTS.includes(value);
1448
+ }
544
1449
  // src/index.ts
545
1450
  init_database();
546
1451
  init_pricing();
@@ -548,6 +1453,9 @@ init_pricing();
548
1453
  // src/lib/gatherer.ts
549
1454
  init_database();
550
1455
  var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
1456
+ function hasCostData(summary) {
1457
+ return summary.total_usd > 0 || summary.sessions > 0 || summary.requests > 0 || summary.tokens > 0;
1458
+ }
551
1459
  var gatherTrainingData = async (options = {}) => {
552
1460
  const limit = options.limit ?? 500;
553
1461
  const examples = [];
@@ -557,6 +1465,8 @@ var gatherTrainingData = async (options = {}) => {
557
1465
  for (const period of periods) {
558
1466
  try {
559
1467
  const s = querySummary(db, period);
1468
+ if (!hasCostData(s))
1469
+ continue;
560
1470
  examples.push({
561
1471
  messages: [
562
1472
  { role: "system", content: SYSTEM_PROMPT },
@@ -729,22 +1639,26 @@ ${goals.map((g) => `- ${g.period} goal (${g.project_path ?? g.agent ?? "global"}
729
1639
  // src/lib/model-config.ts
730
1640
  init_database();
731
1641
  import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
732
- import { join as join2 } from "path";
1642
+ import { dirname, join as join2 } from "path";
733
1643
  var DEFAULT_MODEL = "gpt-4o-mini";
734
- var CONFIG_PATH = join2(getDataDir(), "config.json");
1644
+ function getModelConfigPath() {
1645
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join2(getDataDir(), "config.json");
1646
+ }
735
1647
  function loadConfig() {
736
1648
  try {
737
- if (existsSync2(CONFIG_PATH)) {
738
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1649
+ const configPath = getModelConfigPath();
1650
+ if (existsSync2(configPath)) {
1651
+ return JSON.parse(readFileSync(configPath, "utf-8"));
739
1652
  }
740
1653
  } catch {}
741
1654
  return {};
742
1655
  }
743
1656
  function saveConfig(config) {
744
- const dir = getDataDir();
1657
+ const configPath = getModelConfigPath();
1658
+ const dir = dirname(configPath);
745
1659
  if (!existsSync2(dir))
746
1660
  mkdirSync2(dir, { recursive: true });
747
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
1661
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + `
748
1662
  `);
749
1663
  }
750
1664
  function getActiveModel() {
@@ -760,16 +1674,181 @@ function clearActiveModel() {
760
1674
  delete config.activeModel;
761
1675
  saveConfig(config);
762
1676
  }
1677
+ // src/lib/open-projects.ts
1678
+ init_database();
1679
+ async function syncOpenProjectsRegistry(db, listActiveProjects) {
1680
+ let listProjects2 = listActiveProjects;
1681
+ if (!listProjects2) {
1682
+ const projectsApi = await import("@hasna/projects");
1683
+ listProjects2 = projectsApi.listProjects;
1684
+ }
1685
+ const projects = listProjects2({ status: "active", limit: 5000 });
1686
+ let imported = 0;
1687
+ let skipped = 0;
1688
+ for (const project of projects) {
1689
+ if (!project.path) {
1690
+ skipped++;
1691
+ continue;
1692
+ }
1693
+ upsertProject(db, {
1694
+ id: project.id,
1695
+ path: project.path,
1696
+ name: project.name,
1697
+ description: project.description,
1698
+ tags: project.tags ?? [],
1699
+ created_at: project.created_at
1700
+ });
1701
+ imported++;
1702
+ }
1703
+ return { imported, skipped };
1704
+ }
763
1705
  // src/ingest/claude.ts
764
1706
  init_database();
765
1707
  init_pricing();
766
1708
  import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
767
1709
  import { homedir as homedir2 } from "os";
768
1710
  import { join as join3, basename } from "path";
1711
+
1712
+ // src/lib/savings.ts
1713
+ function defaultCostBasisForAgent(agent) {
1714
+ if (agent === "claude")
1715
+ return "metered_api";
1716
+ if (agent === "cursor")
1717
+ return "subscription_included";
1718
+ return "estimated";
1719
+ }
1720
+
1721
+ // src/lib/accounts.ts
1722
+ var AGENT_ACCOUNT_TOOLS = {
1723
+ claude: ["claude"],
1724
+ takumi: ["takumi", "claude"],
1725
+ codex: ["codex"],
1726
+ gemini: ["gemini"],
1727
+ opencode: ["opencode"],
1728
+ cursor: ["cursor"],
1729
+ pi: ["pi"],
1730
+ hermes: ["hermes"]
1731
+ };
1732
+ function accountKey(tool, name) {
1733
+ return `${tool}:${name}`;
1734
+ }
1735
+ function normalizeDir(value) {
1736
+ return value.replace(/\/+$/, "");
1737
+ }
1738
+ function fromProfile(profile, source) {
1739
+ return {
1740
+ account_key: accountKey(profile.tool, profile.name),
1741
+ account_tool: profile.tool,
1742
+ account_name: profile.name,
1743
+ ...profile.email ? { account_email: profile.email } : {},
1744
+ account_source: source
1745
+ };
1746
+ }
1747
+ function fromOverride(raw, agent) {
1748
+ const value = raw.trim();
1749
+ if (!value)
1750
+ return null;
1751
+ const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
1752
+ const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
1753
+ if (!tool || !name)
1754
+ return null;
1755
+ return {
1756
+ account_key: accountKey(tool, name),
1757
+ account_tool: tool,
1758
+ account_name: name,
1759
+ account_source: "override"
1760
+ };
1761
+ }
1762
+ function envOverride(agent, env) {
1763
+ const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1764
+ const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
1765
+ if (raw)
1766
+ return fromOverride(raw, agent);
1767
+ const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
1768
+ const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
1769
+ if (!tool || !name)
1770
+ return null;
1771
+ return {
1772
+ account_key: accountKey(tool, name),
1773
+ account_tool: tool,
1774
+ account_name: name,
1775
+ account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
1776
+ account_source: "override"
1777
+ };
1778
+ }
1779
+ function knownToolIds(api) {
1780
+ try {
1781
+ return new Set(api.listTools().map((tool) => tool.id));
1782
+ } catch {
1783
+ return new Set;
1784
+ }
1785
+ }
1786
+ function profileForEnvDir(api, tool, env) {
1787
+ const configuredDir = env[tool.envVar];
1788
+ if (!configuredDir)
1789
+ return null;
1790
+ const normalized = normalizeDir(configuredDir);
1791
+ try {
1792
+ return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
1793
+ } catch {
1794
+ return null;
1795
+ }
1796
+ }
1797
+ async function resolveAccountForAgent(agent, env = process.env) {
1798
+ const override = envOverride(agent, env);
1799
+ if (override)
1800
+ return override;
1801
+ let api;
1802
+ try {
1803
+ api = await import("@hasna/accounts");
1804
+ } catch {
1805
+ return null;
1806
+ }
1807
+ const toolIds = knownToolIds(api);
1808
+ for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
1809
+ if (!toolIds.has(toolId))
1810
+ continue;
1811
+ let tool;
1812
+ try {
1813
+ tool = api.getTool(toolId);
1814
+ } catch {
1815
+ continue;
1816
+ }
1817
+ const envProfile = profileForEnvDir(api, tool, env);
1818
+ if (envProfile)
1819
+ return fromProfile(envProfile, "env");
1820
+ try {
1821
+ const applied = api.appliedProfile(toolId);
1822
+ if (applied)
1823
+ return fromProfile(applied, "applied");
1824
+ } catch {}
1825
+ try {
1826
+ const current = api.currentProfile(toolId);
1827
+ if (current)
1828
+ return fromProfile(current, "current");
1829
+ } catch {}
1830
+ }
1831
+ return null;
1832
+ }
1833
+ function withAccount(record, account) {
1834
+ if (!account)
1835
+ return record;
1836
+ return {
1837
+ ...record,
1838
+ account_key: account.account_key,
1839
+ account_tool: account.account_tool,
1840
+ account_name: account.account_name,
1841
+ account_email: account.account_email ?? "",
1842
+ account_source: account.account_source
1843
+ };
1844
+ }
1845
+
1846
+ // src/ingest/claude.ts
769
1847
  function autoDetectProject(cwd, projects) {
770
1848
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
771
1849
  }
772
- var PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
1850
+ var CLAUDE_PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
1851
+ var TAKUMI_PROJECTS_DIR = join3(homedir2(), ".takumi", "projects");
773
1852
  function dirNameToPath(dirName) {
774
1853
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
775
1854
  }
@@ -788,30 +1867,38 @@ function collectJsonlFiles(projectDir) {
788
1867
  walk(projectDir);
789
1868
  return files;
790
1869
  }
791
- async function ingestClaude(db, verbose = false, _telemetryDir) {
792
- if (!existsSync3(PROJECTS_DIR)) {
1870
+ async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
1871
+ return ingestJsonlProjects(db, projectsDir, "claude", verbose);
1872
+ }
1873
+ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
1874
+ return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
1875
+ }
1876
+ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
1877
+ if (!existsSync3(projectsDir)) {
793
1878
  if (verbose)
794
- console.log("Claude projects dir not found:", PROJECTS_DIR);
1879
+ console.log(`${agentName} projects dir not found:`, projectsDir);
795
1880
  return { files: 0, requests: 0, sessions: 0 };
796
1881
  }
1882
+ const machineId = getMachineId();
797
1883
  let totalFiles = 0;
798
1884
  let totalRequests = 0;
799
1885
  const touchedSessions = new Set;
800
1886
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
801
- const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
1887
+ const account = await resolveAccountForAgent(agentName);
1888
+ const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
802
1889
  for (const projectDirEntry of projectDirs) {
803
- const projectDirPath = join3(PROJECTS_DIR, projectDirEntry.name);
1890
+ const projectDirPath = join3(projectsDir, projectDirEntry.name);
804
1891
  const projectPath = dirNameToPath(projectDirEntry.name);
805
1892
  const jsonlFiles = collectJsonlFiles(projectDirPath);
806
1893
  for (const filePath of jsonlFiles) {
807
- const stateKey = filePath.replace(PROJECTS_DIR, "");
1894
+ const stateKey = filePath.replace(projectsDir, "");
808
1895
  let fileMtime = "0";
809
1896
  try {
810
1897
  fileMtime = statSync2(filePath).mtimeMs.toString();
811
1898
  } catch {
812
1899
  continue;
813
1900
  }
814
- const processed = getIngestState(db, "claude", stateKey);
1901
+ const processed = getIngestState(db, agentName, stateKey);
815
1902
  if (processed === fileMtime)
816
1903
  continue;
817
1904
  let lines;
@@ -846,27 +1933,39 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
846
1933
  continue;
847
1934
  const inputTokens = usage.input_tokens ?? 0;
848
1935
  const outputTokens = usage.output_tokens ?? 0;
849
- const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
1936
+ const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
1937
+ const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
1938
+ const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
850
1939
  const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
851
1940
  const timestamp = entry.timestamp ?? new Date().toISOString();
852
- if (inputTokens + outputTokens + cacheWriteTokens === 0)
1941
+ if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
853
1942
  continue;
854
- const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
855
- const reqId = `claude-${sessionId}-${timestamp}`;
856
- upsertRequest(db, {
1943
+ let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
1944
+ costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
1945
+ const serverToolUse = usage.server_tool_use;
1946
+ if (serverToolUse?.web_search_requests) {
1947
+ costUsd += serverToolUse.web_search_requests * 0.01;
1948
+ }
1949
+ const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
1950
+ const reqId = `${agentName}-${sourceRequestId}`;
1951
+ upsertRequest(db, withAccount({
857
1952
  id: reqId,
858
- agent: "claude",
1953
+ agent: agentName,
859
1954
  session_id: sessionId,
860
1955
  model,
861
1956
  input_tokens: inputTokens,
862
1957
  output_tokens: outputTokens,
863
1958
  cache_read_tokens: cacheReadTokens,
864
1959
  cache_create_tokens: cacheWriteTokens,
1960
+ cache_create_5m_tokens: cacheWrite5mTokens,
1961
+ cache_create_1h_tokens: cacheWrite1hTokens,
865
1962
  cost_usd: costUsd,
1963
+ cost_basis: defaultCostBasisForAgent(agentName),
866
1964
  duration_ms: 0,
867
1965
  timestamp,
868
- source_request_id: reqId
869
- });
1966
+ source_request_id: sourceRequestId,
1967
+ machine_id: machineId
1968
+ }, account));
870
1969
  if (!touchedSessions.has(sessionId)) {
871
1970
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
872
1971
  if (!existing) {
@@ -874,22 +1973,23 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
874
1973
  const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
875
1974
  const session = {
876
1975
  id: sessionId,
877
- agent: "claude",
1976
+ agent: agentName,
878
1977
  project_path: detectedProject ? detectedProject.path : effectiveCwd,
879
1978
  project_name: detectedProject ? detectedProject.name : "",
880
1979
  started_at: timestamp,
881
1980
  ended_at: null,
882
1981
  total_cost_usd: 0,
883
1982
  total_tokens: 0,
884
- request_count: 0
1983
+ request_count: 0,
1984
+ machine_id: machineId
885
1985
  };
886
- upsertSession(db, session);
1986
+ upsertSession(db, withAccount(session, account));
887
1987
  }
888
1988
  touchedSessions.add(sessionId);
889
1989
  }
890
1990
  totalRequests++;
891
1991
  }
892
- setIngestState(db, "claude", stateKey, fileMtime);
1992
+ setIngestState(db, agentName, stateKey, fileMtime);
893
1993
  totalFiles++;
894
1994
  }
895
1995
  }
@@ -898,79 +1998,397 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
898
1998
  }
899
1999
  return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
900
2000
  }
2001
+ function applyClaudeModifiers(costUsd, model, usage, entry) {
2002
+ let multiplier = 1;
2003
+ const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
2004
+ if (speed === "fast" && model.includes("opus-4-6")) {
2005
+ multiplier *= 6;
2006
+ }
2007
+ const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
2008
+ if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
2009
+ multiplier *= 1.1;
2010
+ }
2011
+ return costUsd * multiplier;
2012
+ }
2013
+ function supportsClaudeDataResidencyPricing(model) {
2014
+ const normalized = normalizeModelName(model);
2015
+ const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
2016
+ if (!match)
2017
+ return false;
2018
+ const major = Number(match[2]);
2019
+ const minor = match[3] ? Number(match[3]) : 0;
2020
+ return major > 4 || major === 4 && minor >= 6;
2021
+ }
901
2022
  // src/ingest/codex.ts
902
2023
  init_database();
2024
+ init_pricing();
903
2025
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
904
2026
  import { homedir as homedir3 } from "os";
905
2027
  import { join as join4, basename as basename2 } from "path";
906
- import { Database as Database2 } from "bun:sqlite";
907
- var CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
908
- var CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
2028
+ import { Database as BunDatabase } from "bun:sqlite";
2029
+ var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
2030
+ var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
2031
+ var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
2032
+ function codexDbPath() {
2033
+ return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
2034
+ }
2035
+ function codexConfigPath() {
2036
+ return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
2037
+ }
909
2038
  function readCodexModel() {
910
- if (!existsSync4(CODEX_CONFIG_PATH))
911
- return "gpt-5.3-codex";
2039
+ const configPath = codexConfigPath();
2040
+ if (!existsSync4(configPath))
2041
+ return "gpt-5-codex";
912
2042
  try {
913
- const content = readFileSync3(CODEX_CONFIG_PATH, "utf-8");
2043
+ const content = readFileSync3(configPath, "utf-8");
914
2044
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
915
- return match?.[1] ?? "gpt-5.3-codex";
2045
+ return match?.[1] ?? "gpt-5-codex";
916
2046
  } catch {
917
- return "gpt-5.3-codex";
2047
+ return "gpt-5-codex";
918
2048
  }
919
2049
  }
2050
+ function buildThreadQuery(codexDb) {
2051
+ const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
2052
+ const modelSelect = cols.has("model") ? "model" : "NULL AS model";
2053
+ const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
2054
+ const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
2055
+ return `
2056
+ SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
2057
+ ${providerSelect}, ${modelSelect}
2058
+ FROM threads WHERE tokens_used > 0
2059
+ `;
2060
+ }
2061
+ function readTokenEvents(rolloutPath) {
2062
+ if (!rolloutPath || !existsSync4(rolloutPath))
2063
+ return [];
2064
+ const fallbackUsages = new Map;
2065
+ let fallbackTimestamp;
2066
+ let aggregate = null;
2067
+ for (const line of readFileSync3(rolloutPath, "utf-8").split(`
2068
+ `)) {
2069
+ if (!line.trim())
2070
+ continue;
2071
+ let entry;
2072
+ try {
2073
+ entry = JSON.parse(line);
2074
+ } catch {
2075
+ continue;
2076
+ }
2077
+ if (!entry || typeof entry !== "object")
2078
+ continue;
2079
+ const payload = entry["payload"];
2080
+ if (!payload || payload["type"] !== "token_count")
2081
+ continue;
2082
+ const info = payload["info"];
2083
+ const timestamp = entry["timestamp"];
2084
+ const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
2085
+ const totalUsage = info?.["total_token_usage"];
2086
+ if (totalUsage && tokenTotal(totalUsage) > 0) {
2087
+ aggregate = { usage: totalUsage, timestamp: entryTimestamp };
2088
+ continue;
2089
+ }
2090
+ const usage = info?.["last_token_usage"];
2091
+ if (!usage)
2092
+ continue;
2093
+ if (tokenTotal(usage) <= 0)
2094
+ continue;
2095
+ const key = JSON.stringify(usage);
2096
+ if (!fallbackUsages.has(key))
2097
+ fallbackUsages.set(key, usage);
2098
+ fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
2099
+ }
2100
+ if (aggregate)
2101
+ return [aggregate];
2102
+ if (fallbackUsages.size === 0)
2103
+ return [];
2104
+ return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
2105
+ }
2106
+ function tokenTotal(usage) {
2107
+ return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2108
+ }
2109
+ function sumTokenUsages(usages) {
2110
+ const result = {
2111
+ input_tokens: 0,
2112
+ cached_input_tokens: 0,
2113
+ output_tokens: 0,
2114
+ reasoning_output_tokens: 0,
2115
+ total_tokens: 0
2116
+ };
2117
+ for (const usage of usages) {
2118
+ result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
2119
+ result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
2120
+ result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
2121
+ result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
2122
+ result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
2123
+ }
2124
+ return result;
2125
+ }
2126
+ function fallbackEvents(totalTokens) {
2127
+ const inputTokens = Math.floor(totalTokens * 0.6);
2128
+ return [{
2129
+ usage: {
2130
+ input_tokens: inputTokens,
2131
+ cached_input_tokens: 0,
2132
+ output_tokens: totalTokens - inputTokens,
2133
+ total_tokens: totalTokens
2134
+ }
2135
+ }];
2136
+ }
920
2137
  async function ingestCodex(db, verbose = false) {
921
- if (!existsSync4(CODEX_DB_PATH)) {
2138
+ const dbPath = codexDbPath();
2139
+ if (!existsSync4(dbPath)) {
922
2140
  if (verbose)
923
- console.log("Codex DB not found:", CODEX_DB_PATH);
924
- return { sessions: 0 };
2141
+ console.log("Codex DB not found:", dbPath);
2142
+ return { sessions: 0, requests: 0 };
925
2143
  }
2144
+ const machineId = getMachineId();
926
2145
  let codexDb = null;
927
2146
  let ingested = 0;
2147
+ let requests = 0;
2148
+ const account = await resolveAccountForAgent("codex");
928
2149
  try {
929
- codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
930
- const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
2150
+ codexDb = new BunDatabase(dbPath, { readonly: true });
2151
+ const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
931
2152
  for (const thread of threads) {
932
- const stateKey = thread.id;
933
- const processed = getIngestState(db, "codex", stateKey);
934
- if (processed === "done")
2153
+ const model = thread.model ?? readCodexModel();
2154
+ const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
2155
+ const processed = getIngestState(db, "codex", thread.id);
2156
+ if (processed === stateValue)
935
2157
  continue;
936
- const costUsd = 0;
937
2158
  const projectPath = thread.cwd ?? "";
938
2159
  const projectName = projectPath ? basename2(projectPath) : "unknown";
2160
+ const sessionId = `codex-${thread.id}`;
939
2161
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
940
2162
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
941
- upsertSession(db, {
942
- id: `codex-${thread.id}`,
2163
+ upsertSession(db, withAccount({
2164
+ id: sessionId,
943
2165
  agent: "codex",
944
2166
  project_path: projectPath,
945
2167
  project_name: projectName,
946
2168
  started_at: startedAt,
947
2169
  ended_at: endedAt,
948
- total_cost_usd: costUsd,
949
- total_tokens: thread.tokens_used,
950
- request_count: 1
2170
+ total_cost_usd: 0,
2171
+ total_tokens: 0,
2172
+ request_count: 0,
2173
+ machine_id: machineId
2174
+ }, account));
2175
+ const events = readTokenEvents(thread.rollout_path);
2176
+ const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
2177
+ const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
2178
+ db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
2179
+ tokenEvents.forEach((event, index) => {
2180
+ const usage = event.usage;
2181
+ const inputTotal = usage.input_tokens ?? 0;
2182
+ const cacheReadTokens = usage.cached_input_tokens ?? 0;
2183
+ const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
2184
+ const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
2185
+ const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
2186
+ const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
2187
+ const requestId = `${sessionId}-${index}`;
2188
+ upsertRequest(db, withAccount({
2189
+ id: requestId,
2190
+ agent: "codex",
2191
+ session_id: sessionId,
2192
+ model,
2193
+ input_tokens: inputTokens,
2194
+ output_tokens: outputTokens,
2195
+ cache_read_tokens: cacheReadTokens,
2196
+ cache_create_tokens: 0,
2197
+ cost_usd: costUsd,
2198
+ cost_basis: defaultCostBasisForAgent("codex"),
2199
+ duration_ms: 0,
2200
+ timestamp,
2201
+ source_request_id: requestId,
2202
+ machine_id: machineId
2203
+ }, account));
2204
+ requests++;
951
2205
  });
952
- setIngestState(db, "codex", stateKey, "done");
2206
+ rollupSession(db, sessionId);
2207
+ setIngestState(db, "codex", thread.id, stateValue);
953
2208
  ingested++;
954
2209
  if (verbose)
955
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
2210
+ console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
956
2211
  }
957
2212
  } finally {
958
2213
  codexDb?.close();
959
2214
  }
960
- return { sessions: ingested };
2215
+ return { sessions: ingested, requests };
2216
+ }
2217
+ // src/ingest/gemini.ts
2218
+ init_database();
2219
+ init_pricing();
2220
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
2221
+ import { homedir as homedir4 } from "os";
2222
+ import { join as join5, basename as basename3 } from "path";
2223
+ var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
2224
+ var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir4(), ".gemini", "history");
2225
+ function geminiTmpDir() {
2226
+ return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
2227
+ }
2228
+ function geminiHistoryDir() {
2229
+ return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
2230
+ }
2231
+ function numberField(...values) {
2232
+ for (const value of values) {
2233
+ if (typeof value === "number" && Number.isFinite(value))
2234
+ return value;
2235
+ }
2236
+ return 0;
2237
+ }
2238
+ function listProjectDirs(...roots) {
2239
+ const dirs = new Set;
2240
+ for (const root of roots) {
2241
+ if (!existsSync5(root))
2242
+ continue;
2243
+ try {
2244
+ for (const entry of readdirSync3(root, { withFileTypes: true })) {
2245
+ if (entry.isDirectory())
2246
+ dirs.add(join5(root, entry.name));
2247
+ }
2248
+ } catch {}
2249
+ }
2250
+ return [...dirs];
2251
+ }
2252
+ function projectRoot(projectDir, chatData) {
2253
+ if (chatData.projectPath)
2254
+ return chatData.projectPath;
2255
+ if (chatData.project_path)
2256
+ return chatData.project_path;
2257
+ const rootFile = join5(projectDir, ".project_root");
2258
+ try {
2259
+ if (existsSync5(rootFile))
2260
+ return readFileSync4(rootFile, "utf-8").trim();
2261
+ } catch {}
2262
+ return "";
2263
+ }
2264
+ async function ingestGemini(db, verbose) {
2265
+ const tmpDir = geminiTmpDir();
2266
+ const historyDir = geminiHistoryDir();
2267
+ if (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
2268
+ if (verbose)
2269
+ console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
2270
+ return { sessions: 0, requests: 0 };
2271
+ }
2272
+ const machineId = getMachineId();
2273
+ let totalSessions = 0;
2274
+ let totalRequests = 0;
2275
+ const touchedSessions = new Set;
2276
+ const account = await resolveAccountForAgent("gemini");
2277
+ const projectDirs = listProjectDirs(tmpDir, historyDir);
2278
+ for (const projectDir of projectDirs) {
2279
+ const chatsDir = join5(projectDir, "chats");
2280
+ if (!existsSync5(chatsDir))
2281
+ continue;
2282
+ let chatFiles = [];
2283
+ try {
2284
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
2285
+ } catch {
2286
+ continue;
2287
+ }
2288
+ for (const filePath of chatFiles) {
2289
+ const stateKey = filePath.replace(homedir4(), "~");
2290
+ let fileMtime = "0";
2291
+ try {
2292
+ fileMtime = statSync3(filePath).mtimeMs.toString();
2293
+ } catch {
2294
+ continue;
2295
+ }
2296
+ const processed = getIngestState(db, "gemini", stateKey);
2297
+ if (processed === fileMtime)
2298
+ continue;
2299
+ let chatData;
2300
+ try {
2301
+ chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
2302
+ } catch {
2303
+ continue;
2304
+ }
2305
+ const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
2306
+ if (!sessionId)
2307
+ continue;
2308
+ const startTime = chatData.startTime ?? new Date().toISOString();
2309
+ const projectPath = projectRoot(projectDir, chatData);
2310
+ const projectName = projectPath ? basename3(projectPath) : "";
2311
+ const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
2312
+ if (!existing) {
2313
+ const session = {
2314
+ id: sessionId,
2315
+ agent: "gemini",
2316
+ project_path: projectPath,
2317
+ project_name: projectName,
2318
+ started_at: startTime,
2319
+ ended_at: chatData.lastUpdated ?? null,
2320
+ total_cost_usd: 0,
2321
+ total_tokens: 0,
2322
+ request_count: 0,
2323
+ machine_id: machineId
2324
+ };
2325
+ upsertSession(db, withAccount(session, account));
2326
+ totalSessions++;
2327
+ }
2328
+ touchedSessions.add(sessionId);
2329
+ for (const [index, message] of (chatData.messages ?? []).entries()) {
2330
+ const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
2331
+ if (!usage)
2332
+ continue;
2333
+ const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
2334
+ if (!model)
2335
+ continue;
2336
+ const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
2337
+ const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
2338
+ const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
2339
+ const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
2340
+ const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
2341
+ const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
2342
+ const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
2343
+ if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
2344
+ continue;
2345
+ const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
2346
+ const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
2347
+ const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
2348
+ const requestId = `gemini-${sessionId}-${message.id ?? index}`;
2349
+ upsertRequest(db, withAccount({
2350
+ id: requestId,
2351
+ agent: "gemini",
2352
+ session_id: sessionId,
2353
+ model,
2354
+ input_tokens: inputTokens,
2355
+ output_tokens: outputTokens,
2356
+ cache_read_tokens: cacheReadTokens,
2357
+ cache_create_tokens: 0,
2358
+ cost_usd: costUsd,
2359
+ cost_basis: defaultCostBasisForAgent("gemini"),
2360
+ duration_ms: 0,
2361
+ timestamp,
2362
+ source_request_id: message.id ?? requestId,
2363
+ machine_id: machineId
2364
+ }, account));
2365
+ totalRequests++;
2366
+ }
2367
+ setIngestState(db, "gemini", stateKey, fileMtime);
2368
+ }
2369
+ }
2370
+ for (const sessionId of touchedSessions) {
2371
+ rollupSession(db, sessionId);
2372
+ }
2373
+ return { sessions: totalSessions, requests: totalRequests };
961
2374
  }
962
2375
  export {
2376
+ upsertUsageSnapshot,
2377
+ upsertSubscription,
963
2378
  upsertSession,
964
2379
  upsertRequest,
965
2380
  upsertProject,
966
2381
  upsertModelPricing,
967
2382
  upsertGoal,
968
2383
  upsertBudget,
2384
+ upsertBillingDaily,
2385
+ syncOpenProjectsRegistry,
969
2386
  setIngestState,
970
2387
  setActiveModel,
971
2388
  seedModelPricing,
972
2389
  rollupSession,
973
2390
  readCodexModel,
2391
+ queryUsageSnapshots,
974
2392
  queryTopSessions,
975
2393
  querySummary,
976
2394
  querySessions,
@@ -978,18 +2396,29 @@ export {
978
2396
  queryProjectBreakdown,
979
2397
  queryModelBreakdown,
980
2398
  queryDailyBreakdown,
2399
+ queryBillingSummary,
2400
+ queryAgentBreakdown,
2401
+ queryAccountBreakdown,
981
2402
  openDatabase,
982
2403
  normalizeModelName,
2404
+ listSubscriptions,
983
2405
  listProjects,
984
2406
  listModelPricing,
2407
+ listMachines,
2408
+ listMachineRegistry,
985
2409
  listGoals,
986
2410
  listBudgets,
2411
+ isAgent,
2412
+ ingestTakumi,
2413
+ ingestJsonlProjects,
2414
+ ingestGemini,
987
2415
  ingestCodex,
988
2416
  ingestClaude,
989
2417
  getProject,
990
2418
  getPricingFromDb,
991
2419
  getPricing,
992
2420
  getModelPricing,
2421
+ getMachineId,
993
2422
  getIngestState,
994
2423
  getGoalStatuses,
995
2424
  getDbPath,
@@ -998,13 +2427,18 @@ export {
998
2427
  getActiveModel,
999
2428
  gatherTrainingData,
1000
2429
  ensurePricingSeeded,
2430
+ deleteSubscription,
1001
2431
  deleteProject,
1002
2432
  deleteModelPricing,
1003
2433
  deleteGoal,
1004
2434
  deleteBudget,
2435
+ dedupeRequests,
1005
2436
  computeCostFromDb,
1006
2437
  computeCost,
2438
+ clearBillingRange,
1007
2439
  clearActiveModel,
1008
2440
  DEFAULT_PRICING,
1009
- DEFAULT_MODEL
2441
+ DEFAULT_MODEL,
2442
+ COST_BASIS,
2443
+ AGENTS
1010
2444
  };