tokentracker-cli 0.63.1 → 0.64.1
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/dashboard/dist/assets/{ActivityHeatmap-3rWViAnK.js → ActivityHeatmap-WJZDXbwS.js} +1 -1
- package/dashboard/dist/assets/{Card-CjtoUN-V.js → Card-BLMSo0Yk.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-B6riL6Zc.js +19 -0
- package/dashboard/dist/assets/{DevicePage-DWgc0ITo.js → DevicePage-DB89vSmn.js} +1 -1
- package/dashboard/dist/assets/{DialogTitle-BOWoDnVq.js → DialogTitle-B1RHm8WW.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CK1EopuS.js → FadeIn-7JSzFnw8.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-C1CldHWg.js → HeaderGithubStar-BLtS5CM-.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-DKiEo4LO.js → IpCheckPage-Bd-0YOCq.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BCzXJP9I.js → LandingPage-2EhY8k2J.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardAvatar-Dw-KfosP.js → LeaderboardAvatar-CsPY9Amf.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-XQBrmlgA.js → LeaderboardPage-DC76VQpf.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfileModal-8xPG0MRj.js → LeaderboardProfileModal-C6fllhU2.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-DHa73U-g.js → LeaderboardProfilePage-C42UiGkG.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-BTF8cZ-P.js → LimitsPage-CwxWe18U.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-BqLH4XMA.js → LocalOnlyNotice-CWBhqZ0X.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-OZf1-OnV.js → LoginPage-Dw0rFhaO.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-BQ03eBHT.js → PopoverPopup-D5d98dJF.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-CqSU4VnK.js → ResetPasswordPage-Ca1vYCmL.js} +1 -1
- package/dashboard/dist/assets/{Select-C2miulxl.js → Select-TG8sTI5Z.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-CvU6qFNt.js → SelectItemText-ChAwxrlu.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-CHO9pTCC.js +1 -0
- package/dashboard/dist/assets/{SkillsPage-DKHdur0w.js → SkillsPage-D4gqRDz5.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Dh_FKo1H.js → WidgetsPage-B4hDqpOL.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-BIdXJTOA.js → WrappedPage-_5A_J24M.js} +1 -1
- package/dashboard/dist/assets/{agent-logos-CWBw3t4a.js → agent-logos-BxXu3XPn.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-D1Qv9-hJ.js → arrow-up-right-B1VUIoVh.js} +1 -1
- package/dashboard/dist/assets/{download-9hc1LZwP.js → download-D6ma9Qrl.js} +1 -1
- package/dashboard/dist/assets/{info-Bw9sw3vW.js → info-K9Ps997U.js} +1 -1
- package/dashboard/dist/assets/{main-BN1NzjX3.js → main-CnThe5nE.js} +30 -13
- package/dashboard/dist/assets/main-DEU_X1Ba.css +1 -0
- package/dashboard/dist/assets/{use-limits-display-prefs-CpHg_a82.js → use-limits-display-prefs-lH7AACmv.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-BCMRmnLD.js → use-native-settings-C43_rEQP.js} +1 -1
- package/dashboard/dist/assets/use-quality-per-dollar-pref-DsVwHWvg.js +1 -0
- package/dashboard/dist/assets/{use-usage-limits-CVu3Jff0.js → use-usage-limits-D6QquT0m.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-3hjYtt-n.js → useCurrency-CCGK2Rdz.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-BNfQujzc.js → useScrollLock-BtA-ptIe.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +4 -2
- package/src/lib/local-api.js +25 -0
- package/src/lib/opencode-go-limits.js +253 -20
- package/src/lib/outcomes-engine.js +230 -0
- package/src/lib/pricing/curated-overrides.json +4 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/sqlite-reader.js +59 -0
- package/src/lib/usage-limits.js +4 -4
- package/dashboard/dist/assets/DashboardPage-C8Zm5caR.js +0 -19
- package/dashboard/dist/assets/SettingsPage-DYiO30s5.js +0 -1
- package/dashboard/dist/assets/main-iuBgdxqX.css +0 -1
- package/dashboard/dist/assets/pencil-C8mN_ljR.js +0 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Quality-per-dollar / Effective-Tokens engine (opt-in, sidecar)
|
|
5
|
+
//
|
|
6
|
+
// TokenTracker answers "what did the tokens COST?". This optional layer adds
|
|
7
|
+
// "what did they BUY?" by joining an opt-in, vendor-neutral `outcomes.jsonl`
|
|
8
|
+
// sidecar to the existing token/$ rows by model + time.
|
|
9
|
+
//
|
|
10
|
+
// Design invariants (see GitHub #229):
|
|
11
|
+
// 1. SIDECAR, never the hot path. This module only ever READS its own file
|
|
12
|
+
// and the already-parsed queue rows. It never touches queue.jsonl, its
|
|
13
|
+
// schema, or the sync path. A bug here cannot corrupt token/$ data.
|
|
14
|
+
// 2. DEGRADES TO COST-ONLY. No outcomes file -> readOutcomesData returns []
|
|
15
|
+
// and every join yields empty, so callers render exactly as today.
|
|
16
|
+
// 3. METADATA-ONLY. sanitizeOutcome whitelists a tiny set of scalar fields
|
|
17
|
+
// and drops everything else, so a mis-populated sidecar can NEVER surface
|
|
18
|
+
// PR bodies, diffs, or message text into the dashboard.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const fs = require("node:fs");
|
|
22
|
+
const os = require("node:os");
|
|
23
|
+
const path = require("node:path");
|
|
24
|
+
const { computeRowCost } = require("./pricing");
|
|
25
|
+
|
|
26
|
+
function resolveOutcomesPath() {
|
|
27
|
+
return path.join(os.homedir(), ".tokentracker", "tracker", "outcomes.jsonl");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The ONLY fields ever lifted off an outcome record. Anything not on this list
|
|
31
|
+
// — bodies, diffs, messages, arbitrary blobs — is dropped at read time. This is
|
|
32
|
+
// the enforcement point for the metadata-only privacy invariant.
|
|
33
|
+
function sanitizeOutcome(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
35
|
+
const timestamp = typeof raw.timestamp === "string" ? raw.timestamp.trim() : "";
|
|
36
|
+
if (!timestamp) return null; // an outcome with no time can't be joined to $ rows
|
|
37
|
+
|
|
38
|
+
// `accepted` is the gate signal: a merged PR / passed task. Strictly boolean
|
|
39
|
+
// — anything other than literal true counts as not-accepted (rework).
|
|
40
|
+
const accepted = raw.accepted === true;
|
|
41
|
+
|
|
42
|
+
const model =
|
|
43
|
+
typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : "unknown";
|
|
44
|
+
// `tool` mirrors the queue row's `source` (claude, codex, cursor, …). Accept
|
|
45
|
+
// either key so writers can use whichever they have on hand.
|
|
46
|
+
const toolRaw =
|
|
47
|
+
(typeof raw.tool === "string" && raw.tool.trim() && raw.tool) ||
|
|
48
|
+
(typeof raw.source === "string" && raw.source.trim() && raw.source) ||
|
|
49
|
+
"";
|
|
50
|
+
const tool = toolRaw ? String(toolRaw).trim() : "unknown";
|
|
51
|
+
|
|
52
|
+
const out = { timestamp, model, tool, accepted };
|
|
53
|
+
// task_type is an optional, low-cardinality label (e.g. "feature", "bugfix").
|
|
54
|
+
if (typeof raw.task_type === "string" && raw.task_type.trim()) {
|
|
55
|
+
out.task_type = raw.task_type.trim();
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readOutcomesData(outcomesPath) {
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = fs.readFileSync(outcomesPath, "utf8");
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// ENOENT is the normal "user hasn't opted in" case — silent. Anything else
|
|
66
|
+
// is surfaced once so a genuinely unreadable file isn't hidden forever.
|
|
67
|
+
if (e && e.code !== "ENOENT") {
|
|
68
|
+
console.error("[outcomes] read failed:", (e && e.message) || e);
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const out = [];
|
|
73
|
+
let malformed = 0;
|
|
74
|
+
for (const line of raw.split("\n")) {
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
if (!trimmed) continue;
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(trimmed);
|
|
80
|
+
} catch {
|
|
81
|
+
malformed += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const rec = sanitizeOutcome(parsed);
|
|
85
|
+
if (rec) out.push(rec);
|
|
86
|
+
else malformed += 1;
|
|
87
|
+
}
|
|
88
|
+
if (malformed > 0) {
|
|
89
|
+
console.error(`[outcomes] skipped ${malformed} malformed/invalid line(s) in ${outcomesPath}`);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function dayOf(value) {
|
|
95
|
+
return typeof value === "string" ? value.slice(0, 10) : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function inWindow(dayStr, from, to) {
|
|
99
|
+
if (from && dayStr && dayStr < from) return false;
|
|
100
|
+
if (to && dayStr && dayStr > to) return false;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function bumpCost(map, key, cost, tokens) {
|
|
105
|
+
let agg = map.get(key);
|
|
106
|
+
if (!agg) {
|
|
107
|
+
agg = { cost_usd: 0, total_tokens: 0 };
|
|
108
|
+
map.set(key, agg);
|
|
109
|
+
}
|
|
110
|
+
agg.cost_usd += cost;
|
|
111
|
+
agg.total_tokens += tokens;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function bumpOutcome(map, key, accepted) {
|
|
115
|
+
let agg = map.get(key);
|
|
116
|
+
if (!agg) {
|
|
117
|
+
agg = { accepted: 0, outcomes: 0 };
|
|
118
|
+
map.set(key, agg);
|
|
119
|
+
}
|
|
120
|
+
agg.outcomes += 1;
|
|
121
|
+
if (accepted) agg.accepted += 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function combineRow(key, cost, out) {
|
|
125
|
+
const cost_usd = cost ? cost.cost_usd : 0;
|
|
126
|
+
const total_tokens = cost ? cost.total_tokens : 0;
|
|
127
|
+
const accepted = out ? out.accepted : 0;
|
|
128
|
+
const outcomes = out ? out.outcomes : 0;
|
|
129
|
+
const acceptance_rate = outcomes > 0 ? accepted / outcomes : null;
|
|
130
|
+
// quality per dollar = accepted, gate-passing outcomes ÷ dollars spent.
|
|
131
|
+
// Null (not zero) when we can't form the ratio — no spend or no outcomes —
|
|
132
|
+
// so the UI can distinguish "0 quality" from "not enough data".
|
|
133
|
+
const quality_per_dollar = cost_usd > 0 && outcomes > 0 ? accepted / cost_usd : null;
|
|
134
|
+
// Effective Tokens: the share of tokens that produced accepted work.
|
|
135
|
+
const effective_tokens = acceptance_rate === null ? null : total_tokens * acceptance_rate;
|
|
136
|
+
const effective_cost_usd = acceptance_rate === null ? null : cost_usd * acceptance_rate;
|
|
137
|
+
return {
|
|
138
|
+
key,
|
|
139
|
+
cost_usd,
|
|
140
|
+
total_tokens,
|
|
141
|
+
accepted,
|
|
142
|
+
outcomes,
|
|
143
|
+
acceptance_rate,
|
|
144
|
+
quality_per_dollar,
|
|
145
|
+
effective_tokens,
|
|
146
|
+
effective_cost_usd,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function combine(costMap, outMap) {
|
|
151
|
+
const keys = new Set([...costMap.keys(), ...outMap.keys()]);
|
|
152
|
+
const rows = [];
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
rows.push(combineRow(key, costMap.get(key), outMap.get(key)));
|
|
155
|
+
}
|
|
156
|
+
// Highest quality-per-dollar first; rows without a ratio sink to the bottom.
|
|
157
|
+
rows.sort((a, b) => {
|
|
158
|
+
const av = a.quality_per_dollar;
|
|
159
|
+
const bv = b.quality_per_dollar;
|
|
160
|
+
if (av === null && bv === null) return b.outcomes - a.outcomes;
|
|
161
|
+
if (av === null) return 1;
|
|
162
|
+
if (bv === null) return -1;
|
|
163
|
+
return bv - av;
|
|
164
|
+
});
|
|
165
|
+
return rows;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Join outcomes to token/$ rows by model + tool, within an optional [from,to]
|
|
170
|
+
* day window. Returns per-model and per-tool quality-per-dollar / ET, plus
|
|
171
|
+
* totals. Pure: no I/O, no mutation of inputs.
|
|
172
|
+
*
|
|
173
|
+
* @param {Array} queueRows already-parsed queue rows ({ source, model, hour_start, …token fields })
|
|
174
|
+
* @param {Array} outcomes sanitized outcome records (from readOutcomesData)
|
|
175
|
+
* @param {{from?:string,to?:string}} [window]
|
|
176
|
+
*/
|
|
177
|
+
function computeQualityPerDollar(queueRows, outcomes, { from = "", to = "" } = {}) {
|
|
178
|
+
const modelCost = new Map();
|
|
179
|
+
const toolCost = new Map();
|
|
180
|
+
for (const row of queueRows || []) {
|
|
181
|
+
if (!row || typeof row !== "object") continue; // tolerate a stray null/garbage line
|
|
182
|
+
const day = dayOf(row.hour_start);
|
|
183
|
+
if ((from || to) && !inWindow(day, from, to)) continue;
|
|
184
|
+
const model = (row && row.model) || "unknown";
|
|
185
|
+
const tool = (row && row.source) || "unknown";
|
|
186
|
+
const cost = computeRowCost(row) || 0;
|
|
187
|
+
const tokens = Number((row && row.total_tokens) || 0);
|
|
188
|
+
bumpCost(modelCost, model, cost, tokens);
|
|
189
|
+
bumpCost(toolCost, tool, cost, tokens);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const modelOut = new Map();
|
|
193
|
+
const toolOut = new Map();
|
|
194
|
+
for (const o of outcomes || []) {
|
|
195
|
+
const day = dayOf(o && o.timestamp);
|
|
196
|
+
if ((from || to) && !inWindow(day, from, to)) continue;
|
|
197
|
+
bumpOutcome(modelOut, (o && o.model) || "unknown", !!(o && o.accepted));
|
|
198
|
+
bumpOutcome(toolOut, (o && o.tool) || "unknown", !!(o && o.accepted));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const by_model = combine(modelCost, modelOut);
|
|
202
|
+
const by_tool = combine(toolCost, toolOut);
|
|
203
|
+
|
|
204
|
+
let totalCost = 0;
|
|
205
|
+
let totalTokens = 0;
|
|
206
|
+
for (const v of modelCost.values()) {
|
|
207
|
+
totalCost += v.cost_usd;
|
|
208
|
+
totalTokens += v.total_tokens;
|
|
209
|
+
}
|
|
210
|
+
let totalAccepted = 0;
|
|
211
|
+
let totalOutcomes = 0;
|
|
212
|
+
for (const v of modelOut.values()) {
|
|
213
|
+
totalAccepted += v.accepted;
|
|
214
|
+
totalOutcomes += v.outcomes;
|
|
215
|
+
}
|
|
216
|
+
const totals = combineRow(
|
|
217
|
+
"__all__",
|
|
218
|
+
{ cost_usd: totalCost, total_tokens: totalTokens },
|
|
219
|
+
{ accepted: totalAccepted, outcomes: totalOutcomes },
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return { available: outcomes && outcomes.length > 0, by_model, by_tool, totals };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
resolveOutcomesPath,
|
|
227
|
+
sanitizeOutcome,
|
|
228
|
+
readOutcomesData,
|
|
229
|
+
computeQualityPerDollar,
|
|
230
|
+
};
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"composer-2-fast": { "input": 1.5, "output": 7.5, "cache_read": 0.15 },
|
|
18
18
|
"MiniMax-M2.7": { "input": 0.3, "output": 1.2, "cache_read": 0.06, "cache_write": 0.375 },
|
|
19
19
|
"MiniMax-M2.7-highspeed":{ "input": 0.6, "output": 2.4, "cache_read": 0.06, "cache_write": 0.375 },
|
|
20
|
+
"minimax-m3": { "input": 0.3, "output": 1.2, "cache_read": 0.06, "cache_write": 0 },
|
|
20
21
|
"deepseek-v4-flash":{ "input": 0.14, "output": 0.28, "cache_read": 0.0028, "cache_write": 0.14 },
|
|
21
22
|
"deepseek-v4-pro": { "input": 0.435,"output": 0.87, "cache_read": 0.003625, "cache_write": 0.435 },
|
|
22
23
|
"deepseek-chat": { "input": 0.14, "output": 0.28, "cache_read": 0.0028, "cache_write": 0.14 },
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"kimi-k2.5": { "input": 0.6, "output": 2, "cache_read": 0.15 },
|
|
34
35
|
"kimi-k2.5-free": { "input": 0, "output": 0, "cache_read": 0 },
|
|
35
36
|
"kimi-k2.6": { "input": 0.95, "output": 4, "cache_read": 0.16 },
|
|
37
|
+
"kimi-k2.7-code": { "input": 0.95, "output": 4, "cache_read": 0.19 },
|
|
36
38
|
"glm-5.2": { "input": 1.4, "output": 4.4, "cache_read": 0.26 },
|
|
37
39
|
"glm-5.1": { "input": 1.4, "output": 4.4, "cache_read": 0.26 },
|
|
38
40
|
"glm-5": { "input": 1.0, "output": 3.2, "cache_read": 0.2 },
|
|
@@ -62,10 +64,12 @@
|
|
|
62
64
|
{ "match": "hy3", "ref": "hy3-preview-agent" },
|
|
63
65
|
{ "match": "composer", "ref": "composer-1" },
|
|
64
66
|
{ "match": "claude-opus-4-8", "ref": "claude-opus-4-8" },
|
|
67
|
+
{ "match": "minimax-m3", "ref": "minimax-m3" },
|
|
65
68
|
{ "match": "minimax-m2.7-highspeed", "ref": "MiniMax-M2.7-highspeed" },
|
|
66
69
|
{ "match": "minimax-m2.7", "ref": "MiniMax-M2.7" },
|
|
67
70
|
{ "match": "deepseek-v4-flash", "ref": "deepseek-v4-flash" },
|
|
68
71
|
{ "match": "deepseek-v4-pro", "ref": "deepseek-v4-pro" },
|
|
72
|
+
{ "match": "kimi-k2.7-code", "ref": "kimi-k2.7-code" },
|
|
69
73
|
{ "match": "kimi-k2.6", "ref": "kimi-k2.6" },
|
|
70
74
|
{ "match": "kimi", "ref": "kimi-k2.5" },
|
|
71
75
|
{ "match": "glm-4.5-airx", "ref": "glm-4.5-airx" },
|