@qmxme/pi-stats 0.2.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/README.md +55 -0
- package/package.json +41 -0
- package/stats.ts +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @qmxme/pi-stats
|
|
2
|
+
|
|
3
|
+
Stats widget extension for [pi](https://github.com/badlogic/pi) that displays token usage, throughput, and response duration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Token throughput**: Tokens per second during response
|
|
8
|
+
- **Token usage**: Input → Output tokens per message
|
|
9
|
+
- **Cache tracking**: Cache read (↓) and write (↑) when applicable
|
|
10
|
+
- **Total tokens**: Cumulative token count
|
|
11
|
+
- **Duration**: Response time in seconds or minutes
|
|
12
|
+
|
|
13
|
+
## Widget Display
|
|
14
|
+
|
|
15
|
+
Example output in the UI status bar:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
12.3 tok/s | 1.2k → 450 | cache 500↓ 300↑ | total 2.1k | 3.2s
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi install npm:@qmxme/pi-stats
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With a pinned version:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi install npm:@qmxme/pi-stats@0.1.0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Project-local installation:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pi install npm:@qmxme/pi-stats -l
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Try without installing:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pi -e npm:@qmxme/pi-stats
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install # install build dependencies
|
|
49
|
+
npm run build # compile to dist/
|
|
50
|
+
npm run dev # watch mode
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qmxme/pi-stats",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Stats widget extension for pi - shows token throughput, usage, and duration",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/qmx/pi-stats"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./stats.ts",
|
|
11
|
+
"types": "./stats.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"stats.ts",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi",
|
|
18
|
+
"pi-package",
|
|
19
|
+
"extension",
|
|
20
|
+
"stats"
|
|
21
|
+
],
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./stats.ts"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"dev": "tsc --noEmit --watch",
|
|
30
|
+
"prepublishOnly": "npm run typecheck"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@mariozechner/pi-coding-agent": "^0.63.0",
|
|
37
|
+
"@types/node": "^22.10.7",
|
|
38
|
+
"typescript": "^5.7.3"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|
package/stats.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export default function (pi: ExtensionAPI) {
|
|
4
|
+
let streamingStart: number | undefined;
|
|
5
|
+
let initialOutputTokens: number = 0;
|
|
6
|
+
let widgetKey = "streaming-stats";
|
|
7
|
+
|
|
8
|
+
// Format numbers with K/M suffix
|
|
9
|
+
function formatNum(n: number): string {
|
|
10
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
11
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
12
|
+
return n.toString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Format duration
|
|
16
|
+
function formatDuration(ms: number): string {
|
|
17
|
+
const s = ms / 1000;
|
|
18
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
19
|
+
const m = Math.floor(s / 60);
|
|
20
|
+
const rem = (s % 60).toFixed(1);
|
|
21
|
+
return `${m}m${rem}s`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pi.on("message_start", async (event, ctx) => {
|
|
25
|
+
if (event.message.role === "assistant") {
|
|
26
|
+
streamingStart = Date.now();
|
|
27
|
+
initialOutputTokens = event.message.usage.output;
|
|
28
|
+
ctx.ui.setWidget(widgetKey, undefined);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
pi.on("message_end", async (event, ctx) => {
|
|
33
|
+
if (event.message.role === "assistant" && streamingStart) {
|
|
34
|
+
const elapsed = Date.now() - streamingStart;
|
|
35
|
+
const usage = event.message.usage;
|
|
36
|
+
|
|
37
|
+
// Calculate total output tokens generated in this turn
|
|
38
|
+
const outputTokens = usage.output - initialOutputTokens;
|
|
39
|
+
const tps = elapsed > 0 ? (outputTokens / elapsed) * 1000 : 0;
|
|
40
|
+
|
|
41
|
+
const total = usage.output + usage.input + usage.cacheRead + usage.cacheWrite;
|
|
42
|
+
|
|
43
|
+
// Build cache string, only showing non-zero values
|
|
44
|
+
let cacheStr = "";
|
|
45
|
+
const cacheParts: string[] = [];
|
|
46
|
+
if (usage.cacheRead > 0) cacheParts.push(`${formatNum(usage.cacheRead)}↓`);
|
|
47
|
+
if (usage.cacheWrite > 0) cacheParts.push(`${formatNum(usage.cacheWrite)}↑`);
|
|
48
|
+
if (cacheParts.length > 0) {
|
|
49
|
+
cacheStr = ` | cache ${cacheParts.join(" ")}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stats = `${tps.toFixed(1)} tok/s | ${formatNum(usage.input)} → ${formatNum(usage.output)}${cacheStr} | total ${formatNum(total)} | ${formatDuration(elapsed)}`;
|
|
53
|
+
|
|
54
|
+
ctx.ui.setWidget(widgetKey, (tui, theme) => {
|
|
55
|
+
const lines = [theme.fg("dim", stats)];
|
|
56
|
+
return {
|
|
57
|
+
render: () => lines,
|
|
58
|
+
invalidate: () => {},
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
streamingStart = undefined;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|