fix: eliminate v3/v4 double-counting, count assistant/message usage only
- findSessionFiles now picks newest schema version per session dir (v3 file is a migrated subset of v4; reading both doubled ~5.9k turns) - handleLine only counts type=assistant/message with data.usage (compaction/summary re-reports summarizer spend separately) - verified exact match vs independent ground truth: 16/16 days, all 13 model rows, grand total 3,725,441,198 tokens
This commit is contained in:
parent
6ee09e215e
commit
60643d673c
5 changed files with 75 additions and 27 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1 +1,5 @@
|
|||
node_modules/\n.DS_Store
|
||||
node_modules/
|
||||
.DS_Store
|
||||
|
||||
# accuracy audit scripts (dev-only)
|
||||
audit/
|
||||
|
|
|
|||
|
|
@ -4,19 +4,35 @@ import os from "node:os";
|
|||
import { spawn } from "node:child_process";
|
||||
const fileCache = /* @__PURE__ */ new Map();
|
||||
function findSessionFiles(dir) {
|
||||
const results = [];
|
||||
const byDir = /* @__PURE__ */ new Map();
|
||||
const collect = (d) => {
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const entries = fs.readdirSync(d, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const fullPath = path.join(d, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findSessionFiles(fullPath));
|
||||
collect(fullPath);
|
||||
} else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl")) {
|
||||
results.push(fullPath);
|
||||
if (!byDir.has(d)) byDir.set(d, []);
|
||||
byDir.get(d).push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
collect(dir);
|
||||
const results = [];
|
||||
for (const files of byDir.values()) {
|
||||
if (files.length === 1) {
|
||||
results.push(files[0]);
|
||||
continue;
|
||||
}
|
||||
const ranked = files.map((f) => {
|
||||
const m = path.basename(f).match(/\.v(\d+)\.jsonl/);
|
||||
return { f, v: m ? parseInt(m[1], 10) : 0 };
|
||||
}).sort((a, b) => b.v - a.v);
|
||||
results.push(ranked[0].f);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
async function parseSessionFile(filePath) {
|
||||
|
|
@ -65,8 +81,9 @@ async function parseSessionFile(filePath) {
|
|||
if (line.includes('"usage"')) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
if (obj.type !== "assistant/message") return;
|
||||
const time = obj.time || obj.data && obj.data.time;
|
||||
const usage = obj.usage || obj.data && obj.data.usage;
|
||||
const usage = obj.data && obj.data.usage || obj.usage;
|
||||
if (time && usage) {
|
||||
const d = new Date(time);
|
||||
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "token-contribution",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "GitHub-style token usage contribution calendar and analytics for DeepSeek Harness",
|
||||
"type": "module",
|
||||
"main": "./lib/index.js",
|
||||
|
|
|
|||
|
|
@ -78,20 +78,43 @@ interface CacheEntry {
|
|||
const fileCache = new Map<string, CacheEntry>();
|
||||
|
||||
function findSessionFiles(dir: string): string[] {
|
||||
const results: string[] = [];
|
||||
// Group files by session directory. DSH keeps a migrated copy of each
|
||||
// session (session.v3.jsonl.zstd alongside session.v4.jsonl.zstd) that
|
||||
// contains the SAME records — reading both double-counts every turn.
|
||||
// Prefer the newest schema version per directory.
|
||||
const byDir = new Map<string, string[]>();
|
||||
const collect = (d: string) => {
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const entries = fs.readdirSync(d, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const fullPath = path.join(d, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findSessionFiles(fullPath));
|
||||
collect(fullPath);
|
||||
} else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl")) {
|
||||
results.push(fullPath);
|
||||
if (!byDir.has(d)) byDir.set(d, []);
|
||||
byDir.get(d)!.push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore unreadable dirs
|
||||
}
|
||||
};
|
||||
collect(dir);
|
||||
const results: string[] = [];
|
||||
for (const files of byDir.values()) {
|
||||
if (files.length === 1) {
|
||||
results.push(files[0]);
|
||||
continue;
|
||||
}
|
||||
// Prefer highest session version (v4 > v3)
|
||||
const ranked = files
|
||||
.map((f) => {
|
||||
const m = path.basename(f).match(/\.v(\d+)\.jsonl/);
|
||||
return { f, v: m ? parseInt(m[1], 10) : 0 };
|
||||
})
|
||||
.sort((a, b) => b.v - a.v);
|
||||
results.push(ranked[0].f);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
|
@ -140,8 +163,12 @@ async function parseSessionFile(filePath: string): Promise<SessionSummary | null
|
|||
if (line.includes('"usage"')) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
// Only assistant/message records carry real billed usage.
|
||||
// compaction/summary records re-report the summarizer call and
|
||||
// assistant/attempt lines carry no data.usage at all.
|
||||
if (obj.type !== "assistant/message") return;
|
||||
const time = obj.time || (obj.data && obj.data.time);
|
||||
const usage = obj.usage || (obj.data && obj.data.usage);
|
||||
const usage = (obj.data && obj.data.usage) || obj.usage;
|
||||
if (time && usage) {
|
||||
const d = new Date(time);
|
||||
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue