mirror of
https://github.com/JuliusBrussee/caveman.git
synced 2026-08-11 13:21:09 +02:00
feat(stats): limit-headroom meter — session budget saved % for subscription users
Claude Code Pro/Max users don't spend dollars, they spend a 5-hour / weekly usage limit. /caveman-stats now also reports the savings in their currency: 'Session budget saved: ~X% of your usage this session (est.)', and the lifetime view gets 'Est. budget saved: ~X% of tracked usage (est.)'. Honesty rules baked in: - % = saved / (saved + used) from tokens we actually count — nothing else. budgetSavedPct returns null (line omitted) when no savings are measured: honest zero, no claim. - no plan-limit sizes are assumed or hardcoded (Anthropic doesn't publish token quotas); the footer says so explicitly. - clearly labeled (est.); USD lines stay for API users. Checksums refreshed for the caveman-stats.js change. 5 new tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
This commit is contained in:
co-authored by
Claude Fable 5
parent
bceaa0cf6d
commit
8a5ab60ef0
@@ -182,6 +182,21 @@ function aggregateHistory(historyPath, sinceMs) {
|
||||
return { sessions: latestPerSession.size, outputTokens, estSavedTokens, estSavedUsd };
|
||||
}
|
||||
|
||||
// Limit-headroom meter. Subscription users (Claude Code Pro/Max) spend a
|
||||
// 5-hour/weekly usage limit, not dollars — for them the meaningful number is
|
||||
// what share of their would-be usage caveman freed. Computed ONLY from token
|
||||
// counts we actually have: saved / (saved + used). We never assume a plan's
|
||||
// limit size (Anthropic doesn't publish token quotas), so this is a share of
|
||||
// usage, never "% of your weekly limit". Returns a rounded percent, or null
|
||||
// when there is nothing measured to divide.
|
||||
function budgetSavedPct(savedTokens, usedTokens) {
|
||||
if (!Number.isFinite(savedTokens) || !Number.isFinite(usedTokens)) return null;
|
||||
if (savedTokens <= 0 || usedTokens < 0) return null;
|
||||
const total = savedTokens + usedTokens;
|
||||
if (total <= 0) return null;
|
||||
return Math.round((savedTokens / total) * 100);
|
||||
}
|
||||
|
||||
function humanizeTokens(n) {
|
||||
if (!Number.isFinite(n) || n <= 0) return '0';
|
||||
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
||||
@@ -196,11 +211,15 @@ function formatHistory({ sessions, outputTokens, estSavedTokens, estSavedUsd, si
|
||||
return `\nCaveman Stats — Lifetime${window}\n${sep}\nNo sessions logged yet — run /caveman-stats inside any session to start tracking.\n${sep}\n`;
|
||||
}
|
||||
const usdLine = estSavedUsd > 0 ? `Est. saved (USD): ~${formatUsd(estSavedUsd)}\n` : '';
|
||||
const pct = budgetSavedPct(estSavedTokens, outputTokens);
|
||||
const budgetLine = pct !== null
|
||||
? `Est. budget saved: ~${pct}% of tracked usage (est.)\n`
|
||||
: '';
|
||||
return `\nCaveman Stats — Lifetime${window}\n${sep}\n` +
|
||||
`Sessions: ${sessions.toLocaleString()}\n${sep}\n` +
|
||||
`Output tokens: ${outputTokens.toLocaleString()}\n` +
|
||||
`Est. tokens saved: ${estSavedTokens.toLocaleString()}\n` +
|
||||
usdLine + sep + '\n';
|
||||
budgetLine + usdLine + sep + '\n';
|
||||
}
|
||||
|
||||
// Single-line tweetable summary. Stays human-friendly when no ratio is known.
|
||||
@@ -250,9 +269,17 @@ function formatStats({ outputTokens, cacheReadTokens, turns, mode, model, sessio
|
||||
} else {
|
||||
footer = 'Savings est. from benchmarks/ (mean per-task). Actual varies by task.';
|
||||
}
|
||||
savings = `Est. without caveman: ${estNormal.toLocaleString()}\n` +
|
||||
// Limit-headroom framing for subscription (Pro/Max) users — see
|
||||
// budgetSavedPct. USD stays above for API users.
|
||||
const pct = budgetSavedPct(estSaved, outputTokens);
|
||||
let budgetLine = '';
|
||||
if (pct !== null) {
|
||||
budgetLine = `Session budget saved: ~${pct}% of your usage this session (est.)\n`;
|
||||
footer += ' Budget % = est. saved / (saved + used) tokens; no plan-limit size assumed.';
|
||||
}
|
||||
savings = (`Est. without caveman: ${estNormal.toLocaleString()}\n` +
|
||||
`Est. tokens saved: ${estSaved.toLocaleString()} (~${Math.round(ratio * 100)}%)\n` +
|
||||
usdLine.replace(/\n$/, '');
|
||||
usdLine + budgetLine).replace(/\n$/, '');
|
||||
} else if (mode && mode !== 'off') {
|
||||
savings = `No savings estimate for '${mode}' mode — only 'full' has benchmark data.`;
|
||||
} else {
|
||||
@@ -349,5 +376,5 @@ if (require.main === module) main();
|
||||
module.exports = {
|
||||
formatStats, formatShare, formatHistory, aggregateHistory, parseDuration, deriveSavings,
|
||||
parseSession, priceForModel, formatUsd, COMPRESSION, MODEL_OUTPUT_PRICE_PER_M,
|
||||
findCompressedPairs, summarizeCompressed, humanizeTokens,
|
||||
findCompressedPairs, summarizeCompressed, humanizeTokens, budgetSavedPct,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
c0b77891d1f8aaef3a7fbf76533ba63a42eaa96431fb7ece611ccff7474de2ba caveman-config.js
|
||||
52ba51f7c51749fda1e65c63060cec03ffad36c35856c01068c0f63fa33e5b40 caveman-activate.js
|
||||
a5a532fd57203aa96c5c6eeeef647df2770869343f31be009ba9da318da95301 caveman-mode-tracker.js
|
||||
be1129b422b7d3edfa8c97df3d8a81f1d1c94cf4e1fd7d6f3a253afebd93fed7 caveman-stats.js
|
||||
c841fa940e016f2a3000f814d51d9f96b0a37cc5cbaa01b63cb4f9820640960c caveman-stats.js
|
||||
d2deff457d0a5d8e1848193e6af6a68a0ebdba4fbdf250889400d5ea231e088f caveman-statusline.sh
|
||||
1690c639f05940cbff39e0383a27053898b30c224aa651043db29b2842cb524a caveman-statusline.ps1
|
||||
9f2601e8551653609f0b9d700c9bd75bffa43747d2db18ffe83fb73de5dc3607 cavecrew-model-overrides.js
|
||||
|
||||
@@ -460,5 +460,83 @@ test('mode tracker forwards --share to stats script', (tmp) => {
|
||||
assert.match(parsed.reason, /^🪨 Saved 650 output tokens/);
|
||||
});
|
||||
|
||||
// ── Limit-headroom meter (subscription users spend usage limit, not $) ─────
|
||||
|
||||
test('budgetSavedPct = saved / (saved + used), null when nothing saved', () => {
|
||||
const { budgetSavedPct } = require(STATS);
|
||||
assert.strictEqual(budgetSavedPct(650, 350), 65);
|
||||
assert.strictEqual(budgetSavedPct(1, 3), 25);
|
||||
assert.strictEqual(budgetSavedPct(0, 350), null); // no measured savings → no claim
|
||||
assert.strictEqual(budgetSavedPct(-5, 350), null);
|
||||
assert.strictEqual(budgetSavedPct(650, -1), null);
|
||||
assert.strictEqual(budgetSavedPct(NaN, 350), null);
|
||||
assert.strictEqual(budgetSavedPct(650, Infinity), null);
|
||||
});
|
||||
|
||||
test('shows session budget saved % (est.) alongside tokens when savings measured', (tmp) => {
|
||||
const sess = makeSession(tmp, [
|
||||
{ type: 'assistant', message: { model: 'claude-sonnet-4-7', usage: { output_tokens: 350 } } },
|
||||
]);
|
||||
const claudeDir = path.join(tmp, '.claude');
|
||||
fs.writeFileSync(path.join(claudeDir, '.caveman-active'), 'full');
|
||||
const out = execFileSync(process.execPath, [STATS, '--session-file', sess], {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, CLAUDE_CONFIG_DIR: claudeDir },
|
||||
});
|
||||
// saved 650 / (650 saved + 350 used) = 65%
|
||||
assert.match(out, /Session budget saved:\s+~65% of your usage this session \(est\.\)/);
|
||||
// Dollars stay for API users.
|
||||
assert.match(out, /Est\. saved \(USD\):/);
|
||||
// Honesty: the % must be labeled as estimate math, never a plan-limit claim.
|
||||
assert.match(out, /no plan-limit size assumed/);
|
||||
assert.ok(!/weekly limit|5-hour limit/i.test(out), 'must not fabricate Anthropic quota sizes');
|
||||
});
|
||||
|
||||
test('omits budget line when no savings estimate exists (lite mode)', (tmp) => {
|
||||
const sess = makeSession(tmp, [
|
||||
{ type: 'assistant', message: { usage: { output_tokens: 100 } } },
|
||||
]);
|
||||
const claudeDir = path.join(tmp, '.claude');
|
||||
fs.writeFileSync(path.join(claudeDir, '.caveman-active'), 'lite');
|
||||
const out = execFileSync(process.execPath, [STATS, '--session-file', sess], {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, CLAUDE_CONFIG_DIR: claudeDir },
|
||||
});
|
||||
assert.ok(!/budget saved/i.test(out), 'no measured savings → no budget % claim');
|
||||
});
|
||||
|
||||
test('--all lifetime output includes est. budget saved % when savings tracked', (tmp) => {
|
||||
const claudeDir = path.join(tmp, '.claude');
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
const history = [
|
||||
{ ts: Date.now(), session_id: 'a', output_tokens: 350, est_saved_tokens: 650, est_saved_usd: 0.01 },
|
||||
{ ts: Date.now(), session_id: 'b', output_tokens: 650, est_saved_tokens: 350, est_saved_usd: 0.005 },
|
||||
];
|
||||
fs.writeFileSync(
|
||||
path.join(claudeDir, '.caveman-history.jsonl'),
|
||||
history.map(h => JSON.stringify(h)).join('\n') + '\n',
|
||||
);
|
||||
const out = execFileSync(process.execPath, [STATS, '--all'], {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, CLAUDE_CONFIG_DIR: claudeDir },
|
||||
});
|
||||
// saved 1000 / (1000 saved + 1000 used) = 50%
|
||||
assert.match(out, /Est\. budget saved:\s+~50% of tracked usage \(est\.\)/);
|
||||
});
|
||||
|
||||
test('--all lifetime output omits budget line when nothing saved', (tmp) => {
|
||||
const claudeDir = path.join(tmp, '.claude');
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(claudeDir, '.caveman-history.jsonl'),
|
||||
JSON.stringify({ ts: Date.now(), session_id: 'a', output_tokens: 350, est_saved_tokens: 0, est_saved_usd: 0 }) + '\n',
|
||||
);
|
||||
const out = execFileSync(process.execPath, [STATS, '--all'], {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, CLAUDE_CONFIG_DIR: claudeDir },
|
||||
});
|
||||
assert.ok(!/budget saved/i.test(out), 'zero savings → honest zero, no % line');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user