g3usage 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 +92 -0
- package/bin/g3usage.js +7 -0
- package/package.json +42 -0
- package/src/aggregate.js +205 -0
- package/src/cli.js +316 -0
- package/src/load.js +200 -0
- package/src/pricing.js +72 -0
- package/src/table.js +103 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 g3usage contributors
|
|
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
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# g3usage
|
|
2
|
+
|
|
3
|
+
**ccusage for Grok Build** — analyze local Grok Build token usage and estimate costs.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx g3usage@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install globally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g g3usage
|
|
15
|
+
g3usage
|
|
16
|
+
g3usage monthly
|
|
17
|
+
g3usage session --json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
From this repo (dev):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node bin/g3usage.js daily
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What it does
|
|
27
|
+
|
|
28
|
+
Reads **local** session logs from:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
$GROK_HOME/sessions/*/*/ # default: ~/.grok/sessions/
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For each session it uses:
|
|
35
|
+
|
|
36
|
+
| File | Role |
|
|
37
|
+
|------|------|
|
|
38
|
+
| `updates.jsonl` | `_meta.totalTokens` (cumulative context size per event) |
|
|
39
|
+
| `summary.json` | session id, cwd, title, model, timestamps |
|
|
40
|
+
| `signals.json` | models used, context peak fallback |
|
|
41
|
+
|
|
42
|
+
Nothing is uploaded. Same idea as [ccusage](https://github.com/ccusage/ccusage), scoped to Grok Build only.
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
| Command | Description |
|
|
47
|
+
|---------|-------------|
|
|
48
|
+
| `daily` (default) | Group by date |
|
|
49
|
+
| `weekly` | Group by ISO week |
|
|
50
|
+
| `monthly` | Group by month |
|
|
51
|
+
| `session` | One row per session |
|
|
52
|
+
|
|
53
|
+
### Options
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
-j, --json JSON output
|
|
57
|
+
-s, --since <date> From date (YYYY-MM-DD or YYYYMMDD)
|
|
58
|
+
-u, --until <date> Until date (inclusive)
|
|
59
|
+
-z, --timezone <tz> IANA timezone for grouping
|
|
60
|
+
--no-cost Hide cost column
|
|
61
|
+
--path <dir> Grok home override (default ~/.grok)
|
|
62
|
+
-h, --help
|
|
63
|
+
-v, --version
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Token estimates
|
|
67
|
+
|
|
68
|
+
Grok Build currently logs **cumulative context size** (`totalTokens`), not billed input/output/cache splits.
|
|
69
|
+
|
|
70
|
+
g3usage estimates per user turn (`promptId`):
|
|
71
|
+
|
|
72
|
+
- **Input** ≈ first `totalTokens` seen in the turn (context at start)
|
|
73
|
+
- **Output** ≈ growth during the turn (`peak − first`)
|
|
74
|
+
|
|
75
|
+
Growth includes tool results, so output is an **upper-bound** estimate. Treat numbers as directional, not invoices.
|
|
76
|
+
|
|
77
|
+
## Cost estimates
|
|
78
|
+
|
|
79
|
+
Costs use public xAI API rates (see `src/pricing.js`). If you use **SuperGrok / X Premium+** subscription auth, you may not be billed per token — figures are **API-equivalent**.
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm install -g .
|
|
85
|
+
# then:
|
|
86
|
+
g3usage
|
|
87
|
+
g3usage session -s 2026-07-01
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/bin/g3usage.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "g3usage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Analyze Grok Build token usage and estimated costs from local session data (ccusage for Grok)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"g3usage": "bin/g3usage.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node bin/g3usage.js",
|
|
17
|
+
"prepublishOnly": "node bin/g3usage.js --version"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"grok",
|
|
24
|
+
"xai",
|
|
25
|
+
"grok-build",
|
|
26
|
+
"usage",
|
|
27
|
+
"tokens",
|
|
28
|
+
"cli",
|
|
29
|
+
"ccusage",
|
|
30
|
+
"cost"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/YOUR_USER/g3usage.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/YOUR_USER/g3usage/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/YOUR_USER/g3usage#readme"
|
|
42
|
+
}
|
package/src/aggregate.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date helpers + report aggregation (daily / weekly / monthly / session).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function parseISO(s) {
|
|
6
|
+
if (!s) return null;
|
|
7
|
+
const d = new Date(s);
|
|
8
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Format a Date in a timezone as YYYY-MM-DD */
|
|
12
|
+
export function dateKey(date, timeZone) {
|
|
13
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
14
|
+
timeZone,
|
|
15
|
+
year: "numeric",
|
|
16
|
+
month: "2-digit",
|
|
17
|
+
day: "2-digit",
|
|
18
|
+
}).format(date);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Monday-start ISO week key: YYYY-Www */
|
|
22
|
+
export function weekKey(date, timeZone) {
|
|
23
|
+
// Use local calendar date in TZ, then compute ISO week on that Y-M-D as UTC noon
|
|
24
|
+
const ymd = dateKey(date, timeZone);
|
|
25
|
+
const [y, m, d] = ymd.split("-").map(Number);
|
|
26
|
+
const utc = new Date(Date.UTC(y, m - 1, d, 12));
|
|
27
|
+
// ISO week: Thursday determines the year
|
|
28
|
+
const day = utc.getUTCDay() || 7; // Mon=1..Sun=7
|
|
29
|
+
utc.setUTCDate(utc.getUTCDate() + 4 - day);
|
|
30
|
+
const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
|
|
31
|
+
const weekNo = Math.ceil(((utc - yearStart) / 86400000 + 1) / 7);
|
|
32
|
+
const weekYear = utc.getUTCFullYear();
|
|
33
|
+
return `${weekYear}-W${String(weekNo).padStart(2, "0")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function monthKey(date, timeZone) {
|
|
37
|
+
return dateKey(date, timeZone).slice(0, 7); // YYYY-MM
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveTimezone(tz) {
|
|
41
|
+
if (tz) return tz;
|
|
42
|
+
try {
|
|
43
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
44
|
+
} catch {
|
|
45
|
+
return "UTC";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Filter records by inclusive date range (YYYY-MM-DD or YYYYMMDD).
|
|
51
|
+
* Uses updatedAt (fallback createdAt).
|
|
52
|
+
*/
|
|
53
|
+
export function filterByDate(records, { since, until, timeZone } = {}) {
|
|
54
|
+
const tz = resolveTimezone(timeZone);
|
|
55
|
+
const norm = (s) => {
|
|
56
|
+
if (!s) return null;
|
|
57
|
+
const digits = String(s).replace(/-/g, "");
|
|
58
|
+
if (!/^\d{8}$/.test(digits)) return null;
|
|
59
|
+
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
|
|
60
|
+
};
|
|
61
|
+
const sinceK = norm(since);
|
|
62
|
+
const untilK = norm(until);
|
|
63
|
+
|
|
64
|
+
return records.filter((r) => {
|
|
65
|
+
const d = parseISO(r.updatedAt || r.createdAt);
|
|
66
|
+
if (!d) return !sinceK && !untilK;
|
|
67
|
+
const k = dateKey(d, tz);
|
|
68
|
+
if (sinceK && k < sinceK) return false;
|
|
69
|
+
if (untilK && k > untilK) return false;
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function emptyBucket() {
|
|
75
|
+
return {
|
|
76
|
+
inputTokens: 0,
|
|
77
|
+
outputTokens: 0,
|
|
78
|
+
cacheCreateTokens: 0,
|
|
79
|
+
cacheReadTokens: 0,
|
|
80
|
+
totalTokens: 0,
|
|
81
|
+
peakTokens: 0,
|
|
82
|
+
cost: 0,
|
|
83
|
+
models: new Set(),
|
|
84
|
+
sessions: 0,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function addToBucket(b, r) {
|
|
89
|
+
b.inputTokens += r.inputTokens;
|
|
90
|
+
b.outputTokens += r.outputTokens;
|
|
91
|
+
b.cacheCreateTokens += r.cacheCreateTokens;
|
|
92
|
+
b.cacheReadTokens += r.cacheReadTokens;
|
|
93
|
+
b.totalTokens += r.totalTokens;
|
|
94
|
+
b.peakTokens += r.peakTokens;
|
|
95
|
+
b.cost += r.cost;
|
|
96
|
+
b.models.add(r.model);
|
|
97
|
+
for (const m of r.models || []) b.models.add(m);
|
|
98
|
+
b.sessions += 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function finalize(map) {
|
|
102
|
+
return [...map.entries()]
|
|
103
|
+
.map(([key, b]) => ({
|
|
104
|
+
key,
|
|
105
|
+
inputTokens: b.inputTokens,
|
|
106
|
+
outputTokens: b.outputTokens,
|
|
107
|
+
cacheCreateTokens: b.cacheCreateTokens,
|
|
108
|
+
cacheReadTokens: b.cacheReadTokens,
|
|
109
|
+
totalTokens: b.totalTokens,
|
|
110
|
+
peakTokens: b.peakTokens,
|
|
111
|
+
cost: b.cost,
|
|
112
|
+
models: [...b.models].sort(),
|
|
113
|
+
sessions: b.sessions,
|
|
114
|
+
}))
|
|
115
|
+
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function aggregateBy(records, keyFn) {
|
|
119
|
+
const map = new Map();
|
|
120
|
+
for (const r of records) {
|
|
121
|
+
const d = parseISO(r.updatedAt || r.createdAt);
|
|
122
|
+
if (!d) continue;
|
|
123
|
+
const key = keyFn(d, r);
|
|
124
|
+
let b = map.get(key);
|
|
125
|
+
if (!b) {
|
|
126
|
+
b = emptyBucket();
|
|
127
|
+
map.set(key, b);
|
|
128
|
+
}
|
|
129
|
+
addToBucket(b, r);
|
|
130
|
+
}
|
|
131
|
+
return finalize(map);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function dailyReport(records, timeZone) {
|
|
135
|
+
const tz = resolveTimezone(timeZone);
|
|
136
|
+
return aggregateBy(records, (d) => dateKey(d, tz));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function weeklyReport(records, timeZone) {
|
|
140
|
+
const tz = resolveTimezone(timeZone);
|
|
141
|
+
return aggregateBy(records, (d) => weekKey(d, tz));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function monthlyReport(records, timeZone) {
|
|
145
|
+
const tz = resolveTimezone(timeZone);
|
|
146
|
+
return aggregateBy(records, (d) => monthKey(d, tz));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function sessionReport(records, timeZone) {
|
|
150
|
+
const tz = resolveTimezone(timeZone);
|
|
151
|
+
return records
|
|
152
|
+
.map((r) => {
|
|
153
|
+
const d = parseISO(r.updatedAt || r.createdAt);
|
|
154
|
+
return {
|
|
155
|
+
key: r.sessionId,
|
|
156
|
+
date: d ? dateKey(d, tz) : null,
|
|
157
|
+
title: r.title,
|
|
158
|
+
cwd: r.cwd,
|
|
159
|
+
model: r.model,
|
|
160
|
+
models: r.models,
|
|
161
|
+
inputTokens: r.inputTokens,
|
|
162
|
+
outputTokens: r.outputTokens,
|
|
163
|
+
cacheCreateTokens: r.cacheCreateTokens,
|
|
164
|
+
cacheReadTokens: r.cacheReadTokens,
|
|
165
|
+
totalTokens: r.totalTokens,
|
|
166
|
+
peakTokens: r.peakTokens,
|
|
167
|
+
cost: r.cost,
|
|
168
|
+
sessions: 1,
|
|
169
|
+
prompts: r.prompts,
|
|
170
|
+
};
|
|
171
|
+
})
|
|
172
|
+
.sort((a, b) => {
|
|
173
|
+
const ad = a.date || "";
|
|
174
|
+
const bd = b.date || "";
|
|
175
|
+
if (ad !== bd) return ad < bd ? -1 : 1;
|
|
176
|
+
return a.key < b.key ? -1 : 1;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function totals(rows) {
|
|
181
|
+
const t = emptyBucket();
|
|
182
|
+
for (const r of rows) {
|
|
183
|
+
t.inputTokens += r.inputTokens || 0;
|
|
184
|
+
t.outputTokens += r.outputTokens || 0;
|
|
185
|
+
t.cacheCreateTokens += r.cacheCreateTokens || 0;
|
|
186
|
+
t.cacheReadTokens += r.cacheReadTokens || 0;
|
|
187
|
+
t.totalTokens += r.totalTokens || 0;
|
|
188
|
+
t.peakTokens += r.peakTokens || 0;
|
|
189
|
+
t.cost += r.cost || 0;
|
|
190
|
+
t.sessions += r.sessions || 0;
|
|
191
|
+
for (const m of r.models || []) t.models.add(m);
|
|
192
|
+
if (r.model) t.models.add(r.model);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
inputTokens: t.inputTokens,
|
|
196
|
+
outputTokens: t.outputTokens,
|
|
197
|
+
cacheCreateTokens: t.cacheCreateTokens,
|
|
198
|
+
cacheReadTokens: t.cacheReadTokens,
|
|
199
|
+
totalTokens: t.totalTokens,
|
|
200
|
+
peakTokens: t.peakTokens,
|
|
201
|
+
cost: t.cost,
|
|
202
|
+
models: [...t.models].sort(),
|
|
203
|
+
sessions: t.sessions,
|
|
204
|
+
};
|
|
205
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { loadSessions, grokHome, sessionsRoot } from "./load.js";
|
|
2
|
+
import {
|
|
3
|
+
filterByDate,
|
|
4
|
+
dailyReport,
|
|
5
|
+
weeklyReport,
|
|
6
|
+
monthlyReport,
|
|
7
|
+
sessionReport,
|
|
8
|
+
totals,
|
|
9
|
+
resolveTimezone,
|
|
10
|
+
} from "./aggregate.js";
|
|
11
|
+
import {
|
|
12
|
+
printTable,
|
|
13
|
+
formatNum,
|
|
14
|
+
formatCost,
|
|
15
|
+
c,
|
|
16
|
+
} from "./table.js";
|
|
17
|
+
|
|
18
|
+
const VERSION = "0.1.0";
|
|
19
|
+
|
|
20
|
+
const HELP = `
|
|
21
|
+
${c.bold("g3usage")} — Grok Build usage analysis (ccusage for Grok)
|
|
22
|
+
|
|
23
|
+
${c.bold("USAGE")}
|
|
24
|
+
g3usage [daily] [options]
|
|
25
|
+
g3usage <command> [options]
|
|
26
|
+
|
|
27
|
+
${c.bold("COMMANDS")}
|
|
28
|
+
daily Show usage grouped by date (default)
|
|
29
|
+
weekly Show usage grouped by week
|
|
30
|
+
monthly Show usage grouped by month
|
|
31
|
+
session Show usage grouped by session
|
|
32
|
+
|
|
33
|
+
${c.bold("OPTIONS")}
|
|
34
|
+
-j, --json Output JSON
|
|
35
|
+
-s, --since <date> Filter from date (YYYY-MM-DD or YYYYMMDD)
|
|
36
|
+
-u, --until <date> Filter until date (inclusive)
|
|
37
|
+
-z, --timezone <tz> IANA timezone for date grouping
|
|
38
|
+
--no-cost Hide cost column
|
|
39
|
+
--path <dir> Override Grok home (default: $GROK_HOME or ~/.grok)
|
|
40
|
+
-h, --help Show help
|
|
41
|
+
-v, --version Show version
|
|
42
|
+
|
|
43
|
+
${c.bold("NOTES")}
|
|
44
|
+
Reads local session logs from ~/.grok/sessions (never uploads data).
|
|
45
|
+
Token input/output is estimated from cumulative context sizes in
|
|
46
|
+
updates.jsonl — Grok does not currently log billed I/O splits.
|
|
47
|
+
Cost is API-equivalent; SuperGrok subscription usage may not bill per token.
|
|
48
|
+
`.trim();
|
|
49
|
+
|
|
50
|
+
function parseArgs(argv) {
|
|
51
|
+
const opts = {
|
|
52
|
+
command: "daily",
|
|
53
|
+
json: false,
|
|
54
|
+
since: null,
|
|
55
|
+
until: null,
|
|
56
|
+
timezone: null,
|
|
57
|
+
noCost: false,
|
|
58
|
+
path: null,
|
|
59
|
+
help: false,
|
|
60
|
+
version: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const commands = new Set(["daily", "weekly", "monthly", "session"]);
|
|
64
|
+
const args = [...argv];
|
|
65
|
+
|
|
66
|
+
if (args[0] && !args[0].startsWith("-") && commands.has(args[0])) {
|
|
67
|
+
opts.command = args.shift();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
while (args.length) {
|
|
71
|
+
const a = args.shift();
|
|
72
|
+
switch (a) {
|
|
73
|
+
case "-h":
|
|
74
|
+
case "--help":
|
|
75
|
+
opts.help = true;
|
|
76
|
+
break;
|
|
77
|
+
case "-v":
|
|
78
|
+
case "--version":
|
|
79
|
+
opts.version = true;
|
|
80
|
+
break;
|
|
81
|
+
case "-j":
|
|
82
|
+
case "--json":
|
|
83
|
+
opts.json = true;
|
|
84
|
+
break;
|
|
85
|
+
case "--no-cost":
|
|
86
|
+
opts.noCost = true;
|
|
87
|
+
break;
|
|
88
|
+
case "-s":
|
|
89
|
+
case "--since":
|
|
90
|
+
opts.since = args.shift();
|
|
91
|
+
break;
|
|
92
|
+
case "-u":
|
|
93
|
+
case "--until":
|
|
94
|
+
opts.until = args.shift();
|
|
95
|
+
break;
|
|
96
|
+
case "-z":
|
|
97
|
+
case "--timezone":
|
|
98
|
+
opts.timezone = args.shift();
|
|
99
|
+
break;
|
|
100
|
+
case "--path":
|
|
101
|
+
opts.path = args.shift();
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
if (a.startsWith("-")) {
|
|
105
|
+
throw new Error(`Unknown option: ${a}\n\n${HELP}`);
|
|
106
|
+
}
|
|
107
|
+
if (commands.has(a)) {
|
|
108
|
+
opts.command = a;
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error(`Unknown command: ${a}\n\n${HELP}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return opts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function shortPath(p, max = 36) {
|
|
119
|
+
if (!p) return "";
|
|
120
|
+
const home = process.env.HOME || "";
|
|
121
|
+
let s = home && p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
122
|
+
if (s.length <= max) return s;
|
|
123
|
+
return "…" + s.slice(-(max - 1));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function rowCells(row, { noCost, mode }) {
|
|
127
|
+
const models =
|
|
128
|
+
(row.models && row.models.length
|
|
129
|
+
? row.models
|
|
130
|
+
: row.model
|
|
131
|
+
? [row.model]
|
|
132
|
+
: []
|
|
133
|
+
)
|
|
134
|
+
.map((m) => m)
|
|
135
|
+
.join(", ");
|
|
136
|
+
|
|
137
|
+
if (mode === "session") {
|
|
138
|
+
const label = row.title
|
|
139
|
+
? `${row.key.slice(0, 8)}… ${row.title.slice(0, 28)}`
|
|
140
|
+
: row.key.slice(0, 13);
|
|
141
|
+
const cells = [
|
|
142
|
+
row.date || "?",
|
|
143
|
+
label,
|
|
144
|
+
models || "—",
|
|
145
|
+
formatNum(row.inputTokens),
|
|
146
|
+
formatNum(row.outputTokens),
|
|
147
|
+
formatNum(row.totalTokens),
|
|
148
|
+
];
|
|
149
|
+
if (!noCost) cells.push(formatCost(row.cost));
|
|
150
|
+
return cells;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const cells = [
|
|
154
|
+
row.key,
|
|
155
|
+
models || "—",
|
|
156
|
+
formatNum(row.inputTokens),
|
|
157
|
+
formatNum(row.outputTokens),
|
|
158
|
+
formatNum(row.totalTokens),
|
|
159
|
+
String(row.sessions ?? ""),
|
|
160
|
+
];
|
|
161
|
+
if (!noCost) cells.push(formatCost(row.cost));
|
|
162
|
+
return cells;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function headersFor(mode, noCost) {
|
|
166
|
+
if (mode === "session") {
|
|
167
|
+
const h = ["Date", "Session", "Models", "Input", "Output", "Total"];
|
|
168
|
+
if (!noCost) h.push("Cost (USD)");
|
|
169
|
+
return h;
|
|
170
|
+
}
|
|
171
|
+
const label =
|
|
172
|
+
mode === "monthly" ? "Month" : mode === "weekly" ? "Week" : "Date";
|
|
173
|
+
const h = [label, "Models", "Input", "Output", "Total", "Sessions"];
|
|
174
|
+
if (!noCost) h.push("Cost (USD)");
|
|
175
|
+
return h;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function alignFor(mode, noCost) {
|
|
179
|
+
if (mode === "session") {
|
|
180
|
+
return ["left", "left", "left", "right", "right", "right", "right"].slice(
|
|
181
|
+
0,
|
|
182
|
+
noCost ? 6 : 7,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return ["left", "left", "right", "right", "right", "right", "right"].slice(
|
|
186
|
+
0,
|
|
187
|
+
noCost ? 6 : 7,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function printReport(mode, rows, opts) {
|
|
192
|
+
const t = totals(rows);
|
|
193
|
+
const titleMap = {
|
|
194
|
+
daily: "Grok Build Usage Report - Daily",
|
|
195
|
+
weekly: "Grok Build Usage Report - Weekly",
|
|
196
|
+
monthly: "Grok Build Usage Report - Monthly",
|
|
197
|
+
session: "Grok Build Usage Report - Session",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const headers = headersFor(mode, opts.noCost);
|
|
201
|
+
const tableRows = rows.map((r) => rowCells(r, { noCost: opts.noCost, mode }));
|
|
202
|
+
|
|
203
|
+
let footer;
|
|
204
|
+
if (mode === "session") {
|
|
205
|
+
footer = [
|
|
206
|
+
"Total",
|
|
207
|
+
`${t.sessions} sessions`,
|
|
208
|
+
t.models.join(", "),
|
|
209
|
+
formatNum(t.inputTokens),
|
|
210
|
+
formatNum(t.outputTokens),
|
|
211
|
+
formatNum(t.totalTokens),
|
|
212
|
+
];
|
|
213
|
+
if (!opts.noCost) footer.push(formatCost(t.cost));
|
|
214
|
+
} else {
|
|
215
|
+
footer = [
|
|
216
|
+
"Total",
|
|
217
|
+
t.models.join(", "),
|
|
218
|
+
formatNum(t.inputTokens),
|
|
219
|
+
formatNum(t.outputTokens),
|
|
220
|
+
formatNum(t.totalTokens),
|
|
221
|
+
String(t.sessions),
|
|
222
|
+
];
|
|
223
|
+
if (!opts.noCost) footer.push(formatCost(t.cost));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
printTable({
|
|
227
|
+
title: titleMap[mode],
|
|
228
|
+
headers,
|
|
229
|
+
rows: tableRows,
|
|
230
|
+
align: alignFor(mode, opts.noCost),
|
|
231
|
+
footer,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!opts.noCost) {
|
|
235
|
+
console.log(
|
|
236
|
+
c.dim(
|
|
237
|
+
" Costs are API-equivalent estimates. SuperGrok / subscription usage may not bill per token.",
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
console.log(
|
|
242
|
+
c.dim(
|
|
243
|
+
" Input/Output estimated from cumulative context sizes in local session logs.",
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
console.log();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function main(argv) {
|
|
250
|
+
const opts = parseArgs(argv);
|
|
251
|
+
|
|
252
|
+
if (opts.help) {
|
|
253
|
+
console.log(HELP);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (opts.version) {
|
|
257
|
+
console.log(`g3usage ${VERSION}`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const home = opts.path || grokHome();
|
|
262
|
+
const root = sessionsRoot(home);
|
|
263
|
+
|
|
264
|
+
if (!opts.json) {
|
|
265
|
+
// quiet load message only when empty later
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let records = loadSessions({ home });
|
|
269
|
+
const tz = resolveTimezone(opts.timezone);
|
|
270
|
+
records = filterByDate(records, {
|
|
271
|
+
since: opts.since,
|
|
272
|
+
until: opts.until,
|
|
273
|
+
timeZone: tz,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
let rows;
|
|
277
|
+
switch (opts.command) {
|
|
278
|
+
case "weekly":
|
|
279
|
+
rows = weeklyReport(records, tz);
|
|
280
|
+
break;
|
|
281
|
+
case "monthly":
|
|
282
|
+
rows = monthlyReport(records, tz);
|
|
283
|
+
break;
|
|
284
|
+
case "session":
|
|
285
|
+
rows = sessionReport(records, tz);
|
|
286
|
+
break;
|
|
287
|
+
case "daily":
|
|
288
|
+
default:
|
|
289
|
+
rows = dailyReport(records, tz);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (opts.json) {
|
|
293
|
+
const payload = {
|
|
294
|
+
command: opts.command,
|
|
295
|
+
timezone: tz,
|
|
296
|
+
grokHome: home,
|
|
297
|
+
sessionsPath: root,
|
|
298
|
+
totals: totals(rows),
|
|
299
|
+
rows,
|
|
300
|
+
};
|
|
301
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (rows.length === 0) {
|
|
306
|
+
console.log(
|
|
307
|
+
c.yellow(
|
|
308
|
+
`\nNo Grok Build usage found under ${root}\n` +
|
|
309
|
+
`Run some sessions with Grok Build first, or pass --path.\n`,
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
printReport(opts.command, rows, opts);
|
|
316
|
+
}
|
package/src/load.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { estimateCost } from "./pricing.js";
|
|
5
|
+
|
|
6
|
+
export function grokHome() {
|
|
7
|
+
return process.env.GROK_HOME || path.join(os.homedir(), ".grok");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function sessionsRoot(home = grokHome()) {
|
|
11
|
+
return path.join(home, "sessions");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readJson(file) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse updates.jsonl for per-prompt token estimates.
|
|
24
|
+
*
|
|
25
|
+
* Grok exposes cumulative context size as `_meta.totalTokens`, not billed
|
|
26
|
+
* input/output. We approximate:
|
|
27
|
+
* - input ≈ first totalTokens observed in a prompt (context at turn start)
|
|
28
|
+
* - output ≈ max(0, peak - first) within that prompt (growth during the turn)
|
|
29
|
+
*
|
|
30
|
+
* Growth includes tool results, so "output" is an upper-bound estimate.
|
|
31
|
+
*/
|
|
32
|
+
function parseUpdates(updatesPath) {
|
|
33
|
+
if (!fs.existsSync(updatesPath)) {
|
|
34
|
+
return { inputTokens: 0, outputTokens: 0, peakTokens: 0, prompts: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @type {Map<string, { first: number, peak: number }>} */
|
|
38
|
+
const byPrompt = new Map();
|
|
39
|
+
let peakTokens = 0;
|
|
40
|
+
let lineNo = 0;
|
|
41
|
+
|
|
42
|
+
const text = fs.readFileSync(updatesPath, "utf8");
|
|
43
|
+
for (const line of text.split("\n")) {
|
|
44
|
+
lineNo++;
|
|
45
|
+
if (!line.trim()) continue;
|
|
46
|
+
let o;
|
|
47
|
+
try {
|
|
48
|
+
o = JSON.parse(line);
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const meta = o?.params?._meta;
|
|
53
|
+
if (!meta || typeof meta.totalTokens !== "number") continue;
|
|
54
|
+
|
|
55
|
+
const tt = meta.totalTokens;
|
|
56
|
+
if (tt > peakTokens) peakTokens = tt;
|
|
57
|
+
|
|
58
|
+
const pid = meta.promptId || `_line_${lineNo}`;
|
|
59
|
+
let entry = byPrompt.get(pid);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
// first observation for this turn (chronological — file is append-only)
|
|
62
|
+
entry = { first: tt, peak: tt };
|
|
63
|
+
byPrompt.set(pid, entry);
|
|
64
|
+
} else if (tt > entry.peak) {
|
|
65
|
+
entry.peak = tt;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let inputTokens = 0;
|
|
70
|
+
let outputTokens = 0;
|
|
71
|
+
for (const { first, peak } of byPrompt.values()) {
|
|
72
|
+
inputTokens += first;
|
|
73
|
+
outputTokens += Math.max(0, peak - first);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
inputTokens,
|
|
78
|
+
outputTokens,
|
|
79
|
+
peakTokens,
|
|
80
|
+
prompts: byPrompt.size,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function listSessionDirs(root) {
|
|
85
|
+
if (!fs.existsSync(root)) return [];
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const project of fs.readdirSync(root, { withFileTypes: true })) {
|
|
88
|
+
if (!project.isDirectory()) continue;
|
|
89
|
+
const projectPath = path.join(root, project.name);
|
|
90
|
+
for (const sid of fs.readdirSync(projectPath, { withFileTypes: true })) {
|
|
91
|
+
if (!sid.isDirectory()) continue;
|
|
92
|
+
out.push(path.join(projectPath, sid.name));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @typedef {object} UsageRecord
|
|
100
|
+
* @property {string} sessionId
|
|
101
|
+
* @property {string|null} cwd
|
|
102
|
+
* @property {string|null} title
|
|
103
|
+
* @property {string} model
|
|
104
|
+
* @property {string[]} models
|
|
105
|
+
* @property {string|null} createdAt ISO
|
|
106
|
+
* @property {string|null} updatedAt ISO
|
|
107
|
+
* @property {number} inputTokens
|
|
108
|
+
* @property {number} outputTokens
|
|
109
|
+
* @property {number} cacheReadTokens always 0 for now (not in local logs)
|
|
110
|
+
* @property {number} cacheCreateTokens always 0
|
|
111
|
+
* @property {number} totalTokens input+output estimate
|
|
112
|
+
* @property {number} peakTokens max context size in session
|
|
113
|
+
* @property {number} cost USD estimate
|
|
114
|
+
* @property {string|null} agentName
|
|
115
|
+
* @property {number} prompts
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/** @returns {UsageRecord[]} */
|
|
119
|
+
export function loadSessions({ home = grokHome() } = {}) {
|
|
120
|
+
const root = sessionsRoot(home);
|
|
121
|
+
const dirs = listSessionDirs(root);
|
|
122
|
+
/** @type {UsageRecord[]} */
|
|
123
|
+
const records = [];
|
|
124
|
+
|
|
125
|
+
for (const dir of dirs) {
|
|
126
|
+
const summary = readJson(path.join(dir, "summary.json")) || {};
|
|
127
|
+
const signals = readJson(path.join(dir, "signals.json")) || {};
|
|
128
|
+
const usage = parseUpdates(path.join(dir, "updates.jsonl"));
|
|
129
|
+
|
|
130
|
+
// Prefer signals peak when updates empty/missing
|
|
131
|
+
const peakTokens = Math.max(
|
|
132
|
+
usage.peakTokens,
|
|
133
|
+
signals.contextTokensUsed || 0,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const models = new Set();
|
|
137
|
+
if (Array.isArray(signals.modelsUsed)) {
|
|
138
|
+
for (const m of signals.modelsUsed) if (m) models.add(m);
|
|
139
|
+
}
|
|
140
|
+
if (signals.primaryModelId) models.add(signals.primaryModelId);
|
|
141
|
+
if (summary.current_model_id) models.add(summary.current_model_id);
|
|
142
|
+
|
|
143
|
+
const model =
|
|
144
|
+
signals.primaryModelId ||
|
|
145
|
+
summary.current_model_id ||
|
|
146
|
+
[...models][0] ||
|
|
147
|
+
"unknown";
|
|
148
|
+
|
|
149
|
+
// If no prompt breakdown, fall back to peak as "total" with no split
|
|
150
|
+
let { inputTokens, outputTokens, prompts } = usage;
|
|
151
|
+
if (prompts === 0 && peakTokens > 0) {
|
|
152
|
+
// unknown split — attribute all to input (context snapshot)
|
|
153
|
+
inputTokens = peakTokens;
|
|
154
|
+
outputTokens = 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const totalTokens = inputTokens + outputTokens;
|
|
158
|
+
const cost = estimateCost(model, {
|
|
159
|
+
input: inputTokens,
|
|
160
|
+
output: outputTokens,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const sessionId =
|
|
164
|
+
summary?.info?.id || path.basename(dir);
|
|
165
|
+
|
|
166
|
+
const createdAt =
|
|
167
|
+
summary.created_at || summary.updated_at || null;
|
|
168
|
+
const updatedAt =
|
|
169
|
+
summary.last_active_at || summary.updated_at || createdAt;
|
|
170
|
+
|
|
171
|
+
// Skip empty ghost sessions
|
|
172
|
+
if (totalTokens === 0 && peakTokens === 0 && !summary.num_messages) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
records.push({
|
|
177
|
+
sessionId,
|
|
178
|
+
cwd: summary?.info?.cwd || null,
|
|
179
|
+
title:
|
|
180
|
+
summary.generated_title ||
|
|
181
|
+
summary.session_summary ||
|
|
182
|
+
null,
|
|
183
|
+
model,
|
|
184
|
+
models: [...models],
|
|
185
|
+
createdAt,
|
|
186
|
+
updatedAt,
|
|
187
|
+
inputTokens,
|
|
188
|
+
outputTokens,
|
|
189
|
+
cacheReadTokens: 0,
|
|
190
|
+
cacheCreateTokens: 0,
|
|
191
|
+
totalTokens,
|
|
192
|
+
peakTokens,
|
|
193
|
+
cost,
|
|
194
|
+
agentName: summary.agent_name || null,
|
|
195
|
+
prompts,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return records;
|
|
200
|
+
}
|
package/src/pricing.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token pricing ($ / 1M tokens) for API-equivalent cost estimates.
|
|
3
|
+
*
|
|
4
|
+
* Sources:
|
|
5
|
+
* - xAI API: https://docs.x.ai/developers/pricing
|
|
6
|
+
* - Cursor Composer 2.5: https://cursor.com/blog/composer-2-5
|
|
7
|
+
* and https://cursor.com/docs/models/cursor-composer-2-5
|
|
8
|
+
* - Cursor model table: https://cursor.com/docs/models-and-pricing
|
|
9
|
+
*
|
|
10
|
+
* SuperGrok / subscription sessions may not bill per token —
|
|
11
|
+
* reported costs are API-equivalent estimates only.
|
|
12
|
+
*/
|
|
13
|
+
export const PRICING = {
|
|
14
|
+
// xAI Chat API — https://docs.x.ai/developers/pricing
|
|
15
|
+
"grok-4.5": { input: 2.0, cachedInput: 0.5, output: 6.0 },
|
|
16
|
+
"grok-4.3": { input: 1.25, cachedInput: 0.2, output: 2.5 },
|
|
17
|
+
"grok-4.20-multi-agent-0309": { input: 1.25, cachedInput: 0.2, output: 2.5 },
|
|
18
|
+
"grok-4.20-0309-reasoning": { input: 1.25, cachedInput: 0.2, output: 2.5 },
|
|
19
|
+
"grok-4.20-0309-non-reasoning": { input: 1.25, cachedInput: 0.2, output: 2.5 },
|
|
20
|
+
"grok-4": { input: 3.0, cachedInput: 0.75, output: 15.0 },
|
|
21
|
+
"grok-4-0709": { input: 3.0, cachedInput: 0.75, output: 15.0 },
|
|
22
|
+
"grok-4-fast": { input: 0.2, cachedInput: 0.05, output: 0.5 },
|
|
23
|
+
"grok-4-1-fast": { input: 0.2, cachedInput: 0.05, output: 0.5 },
|
|
24
|
+
"grok-code-fast-1": { input: 0.2, cachedInput: 0.05, output: 1.5 },
|
|
25
|
+
"grok-3": { input: 3.0, cachedInput: 0.75, output: 15.0 },
|
|
26
|
+
"grok-3-mini": { input: 0.3, cachedInput: 0.075, output: 0.5 },
|
|
27
|
+
|
|
28
|
+
// xAI Code API
|
|
29
|
+
"grok-build-0.1": { input: 1.0, cachedInput: 0.2, output: 2.0 },
|
|
30
|
+
// Build UI alias often maps to 4.5 / grok-build-latest
|
|
31
|
+
"grok-build": { input: 2.0, cachedInput: 0.5, output: 6.0 },
|
|
32
|
+
"grok-build-latest": { input: 2.0, cachedInput: 0.5, output: 6.0 },
|
|
33
|
+
|
|
34
|
+
// Cursor Composer 2.5 (also used inside Grok Build as grok-composer-*)
|
|
35
|
+
// Standard: $0.50 in / $2.50 out; Fast (default): $3 in / $15 out
|
|
36
|
+
"composer-2.5": { input: 0.5, cachedInput: 0.2, output: 2.5 },
|
|
37
|
+
"composer-2.5-fast": { input: 3.0, cachedInput: 0.5, output: 15.0 },
|
|
38
|
+
"grok-composer-2.5": { input: 0.5, cachedInput: 0.2, output: 2.5 },
|
|
39
|
+
"grok-composer-2.5-fast": { input: 3.0, cachedInput: 0.5, output: 15.0 },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const DEFAULT = { input: 2.0, cachedInput: 0.5, output: 6.0 };
|
|
43
|
+
|
|
44
|
+
export function priceFor(model) {
|
|
45
|
+
if (!model) return DEFAULT;
|
|
46
|
+
if (PRICING[model]) return PRICING[model];
|
|
47
|
+
|
|
48
|
+
const lower = String(model).toLowerCase();
|
|
49
|
+
|
|
50
|
+
// Prefer exact-ish substring matches for composer variants before generic prefixes
|
|
51
|
+
if (lower.includes("composer-2.5-fast") || lower.includes("composer-2.5_fast")) {
|
|
52
|
+
return PRICING["composer-2.5-fast"];
|
|
53
|
+
}
|
|
54
|
+
if (lower.includes("composer-2.5") || lower.includes("composer_2.5")) {
|
|
55
|
+
return PRICING["composer-2.5"];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// fuzzy: match prefix keys longest-first
|
|
59
|
+
const keys = Object.keys(PRICING).sort((a, b) => b.length - a.length);
|
|
60
|
+
for (const k of keys) {
|
|
61
|
+
if (model === k || model.startsWith(k) || k.startsWith(model)) {
|
|
62
|
+
return PRICING[k];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return DEFAULT;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Estimate USD cost from token counts. */
|
|
69
|
+
export function estimateCost(model, { input = 0, output = 0, cacheRead = 0 } = {}) {
|
|
70
|
+
const p = priceFor(model);
|
|
71
|
+
return (input * p.input + output * p.output + cacheRead * p.cachedInput) / 1_000_000;
|
|
72
|
+
}
|
package/src/table.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Minimal table renderer (no deps). */
|
|
2
|
+
|
|
3
|
+
const useColor =
|
|
4
|
+
process.stdout.isTTY &&
|
|
5
|
+
!process.env.NO_COLOR &&
|
|
6
|
+
process.env.FORCE_COLOR !== "0";
|
|
7
|
+
|
|
8
|
+
const c = {
|
|
9
|
+
bold: (s) => (useColor ? `\x1b[1m${s}\x1b[0m` : s),
|
|
10
|
+
dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
|
|
11
|
+
cyan: (s) => (useColor ? `\x1b[36m${s}\x1b[0m` : s),
|
|
12
|
+
green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s),
|
|
13
|
+
yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function formatNum(n) {
|
|
17
|
+
if (n == null || Number.isNaN(n)) return "0";
|
|
18
|
+
return Math.round(n).toLocaleString("en-US");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatCost(n) {
|
|
22
|
+
if (n == null || Number.isNaN(n)) return "$0.00";
|
|
23
|
+
if (n > 0 && n < 0.01) return `$${n.toFixed(4)}`;
|
|
24
|
+
return `$${n.toFixed(2)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pad(str, width, align = "left") {
|
|
28
|
+
const s = String(str);
|
|
29
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
30
|
+
const padLen = Math.max(0, width - visible.length);
|
|
31
|
+
if (align === "right") return " ".repeat(padLen) + s;
|
|
32
|
+
return s + " ".repeat(padLen);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function visibleLen(s) {
|
|
36
|
+
return String(s).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} opts
|
|
41
|
+
* @param {string} opts.title
|
|
42
|
+
* @param {string[]} opts.headers
|
|
43
|
+
* @param {string[][]} opts.rows
|
|
44
|
+
* @param {('left'|'right')[]} [opts.align]
|
|
45
|
+
* @param {string[]} [opts.footer]
|
|
46
|
+
*/
|
|
47
|
+
export function printTable({ title, headers, rows, align = [], footer }) {
|
|
48
|
+
const cols = headers.length;
|
|
49
|
+
const widths = headers.map((h, i) => {
|
|
50
|
+
let w = visibleLen(h);
|
|
51
|
+
for (const row of rows) w = Math.max(w, visibleLen(row[i] ?? ""));
|
|
52
|
+
if (footer) w = Math.max(w, visibleLen(footer[i] ?? ""));
|
|
53
|
+
return Math.min(w, 48);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const rule = (left, mid, right, fill = "─") =>
|
|
57
|
+
left + widths.map((w) => fill.repeat(w + 2)).join(mid) + right;
|
|
58
|
+
|
|
59
|
+
const line = (cells, bold = false) => {
|
|
60
|
+
const parts = cells.map((cell, i) => {
|
|
61
|
+
let text = String(cell ?? "");
|
|
62
|
+
// truncate long cells
|
|
63
|
+
const vis = visibleLen(text);
|
|
64
|
+
if (vis > widths[i]) {
|
|
65
|
+
const plain = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
66
|
+
text = plain.slice(0, Math.max(0, widths[i] - 1)) + "…";
|
|
67
|
+
}
|
|
68
|
+
const a = align[i] || (i === 0 ? "left" : "right");
|
|
69
|
+
return pad(text, widths[i], a);
|
|
70
|
+
});
|
|
71
|
+
const body = "│ " + parts.join(" │ ") + " │";
|
|
72
|
+
return bold ? c.bold(body) : body;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const boxTitle = title;
|
|
76
|
+
const inner = Math.max(
|
|
77
|
+
boxTitle.length + 2,
|
|
78
|
+
widths.reduce((a, w) => a + w + 3, 1),
|
|
79
|
+
);
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(c.cyan("╭" + "─".repeat(inner) + "╮"));
|
|
82
|
+
console.log(c.cyan("│") + c.bold(pad(` ${boxTitle}`, inner)) + c.cyan("│"));
|
|
83
|
+
console.log(c.cyan("╰" + "─".repeat(inner) + "╯"));
|
|
84
|
+
console.log();
|
|
85
|
+
|
|
86
|
+
console.log(rule("┌", "┬", "┐"));
|
|
87
|
+
console.log(line(headers, true));
|
|
88
|
+
console.log(rule("├", "┼", "┤"));
|
|
89
|
+
for (const row of rows) console.log(line(row));
|
|
90
|
+
if (footer) {
|
|
91
|
+
console.log(rule("├", "┼", "┤"));
|
|
92
|
+
console.log(line(footer, true));
|
|
93
|
+
}
|
|
94
|
+
console.log(rule("└", "┴", "┘"));
|
|
95
|
+
console.log();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function modelsCell(models) {
|
|
99
|
+
if (!models?.length) return "";
|
|
100
|
+
return models.map((m) => `- ${m}`).join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { c };
|