burnrate-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -2
- package/dist/aggregate.js +213 -0
- package/dist/cli.js +387 -0
- package/dist/config.js +23 -0
- package/dist/dashboard/app.js +440 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +131 -0
- package/dist/dashboard/styles.css +200 -0
- package/dist/demo.js +123 -0
- package/dist/format.js +47 -0
- package/dist/notify.js +32 -0
- package/dist/parsers/claude.js +80 -0
- package/dist/parsers/codex.js +93 -0
- package/dist/paths.js +26 -0
- package/dist/pricing.js +84 -0
- package/dist/server.js +99 -0
- package/dist/store.js +216 -0
- package/dist/types.js +1 -0
- package/package.json +47 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emmanuel Idoko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,130 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🔥 Burnrate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**See what your coding agents really cost, and when you'll hit the wall.**
|
|
4
|
+
|
|
5
|
+
Burnrate is a local, zero-dependency spend and quota meter for **Claude Code** and **Codex CLI**. It reads the logs your agents already write and shows:
|
|
6
|
+
|
|
7
|
+
- a live **5-hour block forecast**: *"At $5.96/h you'll hit your budget at 17:44, 15 minutes before the reset."*
|
|
8
|
+
- **attribution** by session (titled by your first prompt), project, model and agent: *"that refactor cost $14.23"*
|
|
9
|
+
- **value for money** on subscriptions: *"Claude Max 20x → 3.5× value ($708 of API-equivalent usage for $200)"*
|
|
10
|
+
- a **status line** for Claude Code, a **menu-bar** plugin, a terminal summary and CSV export
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx burnrate-cli # opens http://127.0.0.1:4317
|
|
14
|
+
npx burnrate-cli demo # try it with 30 days of synthetic data
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
> Nothing leaves your machine: no account, no API key, no telemetry. The dashboard binds to `127.0.0.1` only.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why the numbers are right
|
|
24
|
+
|
|
25
|
+
Most usage calculators over-count. Claude Code writes **one log line per content block, each repeating the same `usage`**. In a real session we measured 228 assistant lines for 92 actual API responses, so naive summing inflates cost about 2.5×. Prompt-cache writes also cost **1.25× input for the 5-minute TTL and 2× for the 1-hour TTL**, and web searches cost $10 per 1,000.
|
|
26
|
+
|
|
27
|
+
Burnrate deduplicates by `message.id + requestId` and prices every component. Its test suite reproduces Claude Code's own recorded `cost-state` for a real session **to the 7th decimal place** ($15.0607934 Opus + $1.1404568 Haiku). See [`tests/pricing.test.ts`](tests/pricing.test.ts).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm i -g burnrate-cli # installs the `burnrate` command (or use npx burnrate-cli)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Node 20+. Works on macOS, Linux and Windows.
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
| Command | What it does |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `burnrate` | Scan logs and open the dashboard |
|
|
42
|
+
| `burnrate status` | Today, 7 days, month and projection, current block meter and forecast, top sessions |
|
|
43
|
+
| `burnrate statusline` | One line for Claude Code's status line (reads its JSON on stdin) |
|
|
44
|
+
| `burnrate bar` | xbar / SwiftBar menu-bar output |
|
|
45
|
+
| `burnrate sessions [--days 30] [--limit 20]` | Most expensive sessions |
|
|
46
|
+
| `burnrate export [--csv\|--json] [--days 30]` | Per-message usage with cost |
|
|
47
|
+
| `burnrate budget --block 40 --week 300 --month 1000` | Set budgets (`--clear` to remove) |
|
|
48
|
+
| `burnrate plan claude-max-20x` | Set your subscription to see the value multiple (`--name X --usd N` for custom) |
|
|
49
|
+
| `burnrate watch` | Desktop notifications at 80% and 100% of any budget |
|
|
50
|
+
| `burnrate demo` | Dashboard on synthetic data; your real data is untouched |
|
|
51
|
+
| `burnrate scan` / `reset` / `serve --port N` | Utilities |
|
|
52
|
+
|
|
53
|
+
Global options: `--path <dir>` (repeatable; prefix `codex:` for Codex logs), `--home <dir>`, `--no-titles`, `--no-color`.
|
|
54
|
+
|
|
55
|
+
### Claude Code status line
|
|
56
|
+
|
|
57
|
+
Add to `~/.claude/settings.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{ "statusLine": { "type": "command", "command": "burnrate statusline" } }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
◆ $3.12 session · $20.97 today · 75% block · $5.96/h · limit ~17:44
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### macOS menu bar (xbar / SwiftBar)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cp integrations/burnrate.1m.sh ~/Library/Application\ Support/xbar/plugins/
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
🔥 $20.97 75% ⚠︎
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## How limits work
|
|
78
|
+
|
|
79
|
+
Vendors don't publish exact subscription quotas, so Burnrate combines three signals:
|
|
80
|
+
|
|
81
|
+
1. **Your budget:** `burnrate budget --block 40` or *Budgets & plan* in the dashboard.
|
|
82
|
+
2. **Learned caps:** when a "limit reached" message appears in your Claude Code logs, Burnrate records how much you had spent in the 5 hours before it. With no budget set, it uses the smallest of your last three hits as the cap.
|
|
83
|
+
3. **Reported limits:** Codex logs its own 5-hour and weekly used-percentages and reset times, and Burnrate shows them as-is.
|
|
84
|
+
|
|
85
|
+
**Blocks** start at your first message after the previous block ended, floored to the hour, and last 5 hours. **Burn rate** is the spend over the last hour of the active block. The forecast is `now + (budget − spent) / burn rate`.
|
|
86
|
+
|
|
87
|
+
## Data & privacy
|
|
88
|
+
|
|
89
|
+
| What | Where |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Claude Code logs (read-only) | `~/.claude/projects/**/*.jsonl` (+ `$CLAUDE_CONFIG_DIR`), subagents included |
|
|
92
|
+
| Codex CLI logs (read-only) | `~/.codex/sessions/**/*.jsonl` (+ `$CODEX_HOME`) |
|
|
93
|
+
| Burnrate's cache & config | `~/.burnrate/` (`$BURNRATE_HOME`): `events.ndjson`, `state.json`, `meta.json`, `config.json`, optional `prices.json` |
|
|
94
|
+
|
|
95
|
+
Burnrate stores token counts, timestamps, model, project path and the **first 80 characters of each session's first prompt** (as its title). Use `--no-titles` to skip titles. Scans are incremental (byte offsets per file), so refreshing costs milliseconds.
|
|
96
|
+
|
|
97
|
+
**Custom prices.** Costs are *API-equivalent estimates* from public per-token prices (see [`src/pricing.ts`](src/pricing.ts)). To add or override a price, create `~/.burnrate/prices.json`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{ "my-model": { "input": 1, "output": 4, "cacheRead": 0.1 } }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install
|
|
107
|
+
npm test # vitest: pricing, parsers, incremental store, blocks & forecast
|
|
108
|
+
npm run build # tsc → dist/, copies the dashboard
|
|
109
|
+
node dist/cli.js demo
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
src/
|
|
114
|
+
cli.ts commands
|
|
115
|
+
parsers/ claude.ts · codex.ts — defensive line parsers
|
|
116
|
+
store.ts incremental ingest, dedupe, ~/.burnrate cache
|
|
117
|
+
pricing.ts price table + cost engine
|
|
118
|
+
aggregate.ts totals, 5-hour blocks, forecast, learned caps, breakdowns
|
|
119
|
+
server.ts localhost dashboard API (DNS-rebinding + CSRF guarded)
|
|
120
|
+
dashboard/ index.html · styles.css · app.js (vanilla, hand-built SVG charts)
|
|
121
|
+
demo.ts deterministic synthetic logs in both formats
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Docs: [Product requirements](docs/PRD.md) · [Design](docs/DESIGN.md)
|
|
125
|
+
|
|
126
|
+
**Releasing.** Bump `version` in `package.json` and push to `main`. The release workflow publishes to npm with provenance (trusted publishing, or an `NPM_TOKEN` secret), then creates the tag and GitHub release.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export const BLOCK_MS = 5 * 3600_000;
|
|
2
|
+
const HOUR = 3600_000;
|
|
3
|
+
const DAY = 86400_000;
|
|
4
|
+
export function localDate(ts) {
|
|
5
|
+
const d = new Date(ts);
|
|
6
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
7
|
+
}
|
|
8
|
+
function startOfDay(ts) {
|
|
9
|
+
const d = new Date(ts);
|
|
10
|
+
d.setHours(0, 0, 0, 0);
|
|
11
|
+
return d.getTime();
|
|
12
|
+
}
|
|
13
|
+
export function priceAll(events, prices) {
|
|
14
|
+
return events
|
|
15
|
+
.map((e) => {
|
|
16
|
+
const c = prices.cost(e);
|
|
17
|
+
return {
|
|
18
|
+
...e,
|
|
19
|
+
cost: c ?? 0,
|
|
20
|
+
priced: c !== null,
|
|
21
|
+
tokens: e.input + e.output + e.cacheRead + e.cacheWrite5m + e.cacheWrite1h,
|
|
22
|
+
};
|
|
23
|
+
})
|
|
24
|
+
.sort((a, b) => a.ts - b.ts);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Splits activity into 5-hour blocks the way subscription windows work:
|
|
28
|
+
* a block opens at the first message after the previous block ended (floored to the hour).
|
|
29
|
+
*/
|
|
30
|
+
export function computeBlocks(events) {
|
|
31
|
+
const blocks = [];
|
|
32
|
+
let cur = null;
|
|
33
|
+
for (const e of events) {
|
|
34
|
+
if (!cur || e.ts >= cur.end) {
|
|
35
|
+
const start = Math.floor(e.ts / HOUR) * HOUR;
|
|
36
|
+
cur = { start, end: start + BLOCK_MS, cost: 0, tokens: 0, events: 0, firstTs: e.ts, lastTs: e.ts };
|
|
37
|
+
blocks.push(cur);
|
|
38
|
+
}
|
|
39
|
+
cur.cost += e.cost;
|
|
40
|
+
cur.tokens += e.tokens;
|
|
41
|
+
cur.events++;
|
|
42
|
+
cur.lastTs = e.ts;
|
|
43
|
+
}
|
|
44
|
+
return blocks;
|
|
45
|
+
}
|
|
46
|
+
export function summarize(events, sessions, limits, rateLimits, cfg, prices, opts = {}) {
|
|
47
|
+
const now = opts.now ?? Date.now();
|
|
48
|
+
const days = opts.days ?? 30;
|
|
49
|
+
const priced = priceAll(events, prices).filter((e) => e.ts <= now + 60_000);
|
|
50
|
+
const today0 = startOfDay(now);
|
|
51
|
+
const month0 = new Date(new Date(now).getFullYear(), new Date(now).getMonth(), 1).getTime();
|
|
52
|
+
const daysInMonth = new Date(new Date(now).getFullYear(), new Date(now).getMonth() + 1, 0).getDate();
|
|
53
|
+
const dayOfMonth = new Date(now).getDate();
|
|
54
|
+
let today = 0, yesterday = 0, last7 = 0, mtd = 0, allTime = 0, tokensToday = 0, savings = 0;
|
|
55
|
+
const since30 = now - 30 * DAY;
|
|
56
|
+
for (const e of priced) {
|
|
57
|
+
allTime += e.cost;
|
|
58
|
+
if (e.ts >= today0) {
|
|
59
|
+
today += e.cost;
|
|
60
|
+
tokensToday += e.tokens;
|
|
61
|
+
}
|
|
62
|
+
else if (e.ts >= today0 - DAY)
|
|
63
|
+
yesterday += e.cost;
|
|
64
|
+
if (e.ts >= now - 7 * DAY)
|
|
65
|
+
last7 += e.cost;
|
|
66
|
+
if (e.ts >= month0)
|
|
67
|
+
mtd += e.cost;
|
|
68
|
+
if (e.ts >= since30 && e.priced) {
|
|
69
|
+
const p = prices.lookup(e.model);
|
|
70
|
+
savings += (e.cacheRead * (p.input - p.cacheRead)) / 1e6;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const avgDaily7 = last7 / 7;
|
|
74
|
+
const monthProjection = mtd + avgDaily7 * (daysInMonth - dayOfMonth + (1 - (now - today0) / DAY));
|
|
75
|
+
// Blocks & forecast
|
|
76
|
+
const blocks = computeBlocks(priced);
|
|
77
|
+
const learnedCaps = [];
|
|
78
|
+
// The vendor's window isn't visible to us, so estimate the cap as spend in the 5 hours before the hit.
|
|
79
|
+
const limitRows = limits.slice(-20).map((l) => {
|
|
80
|
+
const window = priced.filter((e) => e.ts > l.ts - BLOCK_MS && e.ts <= l.ts);
|
|
81
|
+
const blockCost = window.length ? window.reduce((s, e) => s + e.cost, 0) : null;
|
|
82
|
+
if (blockCost && l.agent === "claude-code")
|
|
83
|
+
learnedCaps.push(blockCost);
|
|
84
|
+
return { ...l, blockCost };
|
|
85
|
+
});
|
|
86
|
+
// Conservative: the smallest observed cap among the most recent three hits.
|
|
87
|
+
const learnedCap = learnedCaps.length ? Math.min(...learnedCaps.slice(-3)) : null;
|
|
88
|
+
const last = blocks[blocks.length - 1];
|
|
89
|
+
let block = null;
|
|
90
|
+
if (last) {
|
|
91
|
+
const active = now < last.end && now >= last.start;
|
|
92
|
+
const blockEvents = priced.filter((e) => e.ts >= last.start && e.ts < last.end);
|
|
93
|
+
const windowStart = Math.max(last.start, now - HOUR);
|
|
94
|
+
const recent = blockEvents.filter((e) => e.ts >= windowStart).reduce((s, e) => s + e.cost, 0);
|
|
95
|
+
const elapsedH = Math.max((now - windowStart) / HOUR, 1 / 6);
|
|
96
|
+
const burnPerHour = active ? recent / elapsedH : 0;
|
|
97
|
+
const remainingMs = active ? last.end - now : 0;
|
|
98
|
+
const budget = cfg.budgets.blockUsd ?? learnedCap ?? null;
|
|
99
|
+
const projected = last.cost + burnPerHour * (remainingMs / HOUR);
|
|
100
|
+
let hitsBudgetAt = null;
|
|
101
|
+
if (active && budget && burnPerHour > 0) {
|
|
102
|
+
const t = last.cost >= budget ? now : now + ((budget - last.cost) / burnPerHour) * HOUR;
|
|
103
|
+
if (t < last.end)
|
|
104
|
+
hitsBudgetAt = t;
|
|
105
|
+
}
|
|
106
|
+
const series = [];
|
|
107
|
+
let cum = 0, idx = 0;
|
|
108
|
+
const stop = active ? now : last.lastTs;
|
|
109
|
+
for (let t = last.start; t <= stop + 10 * 60_000; t += 10 * 60_000) {
|
|
110
|
+
while (idx < blockEvents.length && blockEvents[idx].ts < t)
|
|
111
|
+
cum += blockEvents[idx++].cost;
|
|
112
|
+
series.push({ t: Math.min(t, stop), cost: cum });
|
|
113
|
+
if (t >= stop)
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
block = {
|
|
117
|
+
start: last.start,
|
|
118
|
+
end: last.end,
|
|
119
|
+
active,
|
|
120
|
+
cost: last.cost,
|
|
121
|
+
tokens: last.tokens,
|
|
122
|
+
burnPerHour,
|
|
123
|
+
projected,
|
|
124
|
+
budget,
|
|
125
|
+
budgetSource: cfg.budgets.blockUsd ? "user" : learnedCap ? "learned" : null,
|
|
126
|
+
percent: budget ? last.cost / budget : null,
|
|
127
|
+
hitsBudgetAt,
|
|
128
|
+
remainingMs,
|
|
129
|
+
series,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Daily series
|
|
133
|
+
const dayMap = new Map();
|
|
134
|
+
for (let i = days - 1; i >= 0; i--)
|
|
135
|
+
dayMap.set(localDate(now - i * DAY), { total: 0, tokens: 0, byAgent: {} });
|
|
136
|
+
for (const e of priced) {
|
|
137
|
+
const row = dayMap.get(localDate(e.ts));
|
|
138
|
+
if (!row)
|
|
139
|
+
continue;
|
|
140
|
+
row.total += e.cost;
|
|
141
|
+
row.tokens += e.tokens;
|
|
142
|
+
row.byAgent[e.agent] = (row.byAgent[e.agent] ?? 0) + e.cost;
|
|
143
|
+
}
|
|
144
|
+
// Breakdown tables (within the selected range)
|
|
145
|
+
const rangeStart = startOfDay(now - (days - 1) * DAY);
|
|
146
|
+
const inRange = priced.filter((e) => e.ts >= rangeStart);
|
|
147
|
+
const agents = new Map();
|
|
148
|
+
const models = new Map();
|
|
149
|
+
const sess = new Map();
|
|
150
|
+
for (const e of inRange) {
|
|
151
|
+
const a = agents.get(e.agent) ?? { cost: 0, tokens: 0, events: 0 };
|
|
152
|
+
a.cost += e.cost;
|
|
153
|
+
a.tokens += e.tokens;
|
|
154
|
+
a.events++;
|
|
155
|
+
agents.set(e.agent, a);
|
|
156
|
+
const m = models.get(e.model) ?? { model: e.model, cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, priced: e.priced };
|
|
157
|
+
m.cost += e.cost;
|
|
158
|
+
m.input += e.input;
|
|
159
|
+
m.output += e.output;
|
|
160
|
+
m.cacheRead += e.cacheRead;
|
|
161
|
+
m.cacheWrite += e.cacheWrite5m + e.cacheWrite1h;
|
|
162
|
+
models.set(e.model, m);
|
|
163
|
+
const meta = sessions[e.session];
|
|
164
|
+
const s = sess.get(e.session) ?? {
|
|
165
|
+
id: e.session, title: meta?.title ?? null, agent: e.agent, project: meta?.project || e.project,
|
|
166
|
+
cost: 0, tokens: 0, start: e.ts, end: e.ts, models: [], modelSet: new Set(),
|
|
167
|
+
};
|
|
168
|
+
s.cost += e.cost;
|
|
169
|
+
s.tokens += e.tokens;
|
|
170
|
+
s.end = Math.max(s.end, e.ts);
|
|
171
|
+
s.start = Math.min(s.start, e.ts);
|
|
172
|
+
s.modelSet.add(e.model);
|
|
173
|
+
sess.set(e.session, s);
|
|
174
|
+
}
|
|
175
|
+
const sessionList = [...sess.values()].map(({ modelSet, ...s }) => ({ ...s, models: [...modelSet] }));
|
|
176
|
+
const projects = new Map();
|
|
177
|
+
for (const s of sessionList) {
|
|
178
|
+
const p = projects.get(s.project) ?? { project: s.project, cost: 0, tokens: 0, sessions: 0, lastTs: 0 };
|
|
179
|
+
p.cost += s.cost;
|
|
180
|
+
p.tokens += s.tokens;
|
|
181
|
+
p.sessions++;
|
|
182
|
+
p.lastTs = Math.max(p.lastTs, s.end);
|
|
183
|
+
projects.set(s.project, p);
|
|
184
|
+
}
|
|
185
|
+
const weekBudget = cfg.budgets.weekUsd ?? null;
|
|
186
|
+
const monthBudget = cfg.budgets.monthUsd ?? null;
|
|
187
|
+
const plan = cfg.plan
|
|
188
|
+
? { ...cfg.plan, valueMultiple: cfg.plan.monthlyUsd > 0 ? monthProjection / cfg.plan.monthlyUsd : null }
|
|
189
|
+
: null;
|
|
190
|
+
return {
|
|
191
|
+
generatedAt: now,
|
|
192
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
193
|
+
totals: {
|
|
194
|
+
today, yesterday, last7, mtd, monthProjection, allTime, tokensToday,
|
|
195
|
+
cacheSavings30d: savings,
|
|
196
|
+
firstEventTs: priced[0]?.ts ?? null,
|
|
197
|
+
},
|
|
198
|
+
plan,
|
|
199
|
+
block,
|
|
200
|
+
week: { cost: last7, budget: weekBudget, percent: weekBudget ? last7 / weekBudget : null },
|
|
201
|
+
month: { cost: mtd, budget: monthBudget, percent: monthBudget ? mtd / monthBudget : null, projected: monthProjection },
|
|
202
|
+
learnedCap,
|
|
203
|
+
daily: [...dayMap.entries()].map(([date, v]) => ({ date, ...v })),
|
|
204
|
+
agents: [...agents.entries()].map(([agent, v]) => ({ agent, ...v })).sort((a, b) => b.cost - a.cost),
|
|
205
|
+
models: [...models.values()].sort((a, b) => b.cost - a.cost),
|
|
206
|
+
projects: [...projects.values()].sort((a, b) => b.cost - a.cost),
|
|
207
|
+
sessions: sessionList.sort((a, b) => b.cost - a.cost).slice(0, 100),
|
|
208
|
+
limits: limitRows.reverse(),
|
|
209
|
+
rateLimits: Object.values(rateLimits),
|
|
210
|
+
unpricedModels: [...new Set(priced.filter((e) => !e.priced).map((e) => e.model))],
|
|
211
|
+
eventCount: priced.length,
|
|
212
|
+
};
|
|
213
|
+
}
|