repl: :cost meta surface (Phase 7 commit #5)
User-facing reporter of the per-session accumulator. Three shapes:
:cost one-line summary (calls / tokens / cost)
:cost detail per-model + per-category breakdown
:cost reset zero the meter; clears warn flags
All read-only against ctx.usage_totals; no broker calls.
R6 — annotation uses the per-slot is_local sticky flag, NOT a fragile
cost==0 heuristic. Summary line classifies:
cloud only -> "cost=$X.XXXXXX"
cloud + local mix -> "cost=$X.XXXXXX (cloud only; local: tokens
but no cost field)"
local only -> "cost=$X.XXXXXX (local only; no cost field)"
R7 — :cost detail rows sort by (cost desc, model asc, category asc).
Three-level key for deterministic output across equal-cost rows
(table.sort is unstable; identical costs would otherwise reorder).
R10 — all dollar values use $%.6f formatting. Sub-cent precision is
critical: a Haiku call can cost $0.000028; $%.4f would round it to
$0.0000 — indistinguishable from local $0.
Column width widened to %-26s to fit fully-qualified cloud model
names (e.g. "anthropic/claude-haiku-4.5" = 25 chars).
E2E verified against live cloud + local broker:
:cost (empty session) -> "0 calls, $0.000000"
...after mixed-mode session...
:cost -> "5 calls, prompt=472 / completion=26
tokens, cost=$0.000377 (cloud only;
local: tokens but no cost field)"
:cost detail -> 4 rows: main cloud $0.000219, probe
cloud $0.000128, delegate cloud
$0.000030, main local $0.000000
(local). Sort by cost desc within
model.
:cost reset -> "cost meter reset"; subsequent
:cost shows zeros.
All 5 categories appeared in the same session: main (twice — cloud
+ local), delegate, probe (x2 from :safety check). Warn-threshold
firing already verified in commit #3 + #4.
HELP gains 3 :cost lines.
Regression: test_safety 87/87, test_router_model 31/31, repl loads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,9 @@ Meta commands:
|
||||
:tree off clear the [project] block
|
||||
:diff [<git-args>] git diff <args> -> inject as [diff ...] exec_output
|
||||
examples: :diff :diff --cached :diff main..feature
|
||||
:cost summary of session token/cost usage
|
||||
:cost detail per-model + per-category breakdown
|
||||
:cost reset zero the cost meter (also clears warn flags)
|
||||
:highlight [on|off|status]
|
||||
toggle tree-sitter syntax highlighting on assistant
|
||||
code fences (requires external tree-sitter CLI +
|
||||
@@ -2024,6 +2027,78 @@ function M.run(config)
|
||||
-- Phase 6: :diff meta — `git diff <args>` (B1-clean), appends as
|
||||
-- [diff <args>]\n<output> exec_output. Reads cwd at invocation
|
||||
-- time (R6: differs from :tree's scan-time cwd capture). Empty
|
||||
-- Phase 7: :cost meta — read-only reporter of ctx.usage_totals.
|
||||
-- :cost summary line
|
||||
-- :cost detail per-model + per-category breakdown
|
||||
-- :cost reset zero the meter (also clears warn flags)
|
||||
-- R7 sort: (cost desc, model asc, category asc) — table.sort is
|
||||
-- unstable, so the 3-level key ensures deterministic output.
|
||||
-- R10: $%.6f for sub-cent precision (cloud costs can be ~3e-05).
|
||||
-- R6: annotation uses the per-slot is_local sticky flag rather
|
||||
-- than a fragile cost==0 heuristic.
|
||||
meta.cost = function(args)
|
||||
local sub = ((args or ""):match("^%s*(%S*)") or ""):lower()
|
||||
if sub == "reset" then
|
||||
ctx:reset_usage()
|
||||
renderer.status("cost meter reset")
|
||||
return
|
||||
end
|
||||
local total_cost = ctx:total_cost()
|
||||
local total_p, total_c = ctx:total_tokens()
|
||||
local has_local, has_cloud = false, false
|
||||
for _, m in pairs(ctx.usage_totals or {}) do
|
||||
for _, c in pairs(m) do
|
||||
if c.is_local then has_local = true end
|
||||
if c.cost > 0 then has_cloud = true end
|
||||
end
|
||||
end
|
||||
local label
|
||||
if has_local and has_cloud then
|
||||
label = "(cloud only; local: tokens but no cost field)"
|
||||
elseif has_local and not has_cloud then
|
||||
label = "(local only; no cost field)"
|
||||
else
|
||||
label = ""
|
||||
end
|
||||
if sub == "" then
|
||||
local calls = 0
|
||||
for _, m in pairs(ctx.usage_totals or {}) do
|
||||
for _, c in pairs(m) do calls = calls + c.calls end
|
||||
end
|
||||
renderer.status(("session usage: %d calls, prompt=%d / completion=%d tokens, cost=$%.6f %s"):format(
|
||||
calls, total_p, total_c, total_cost, label))
|
||||
return
|
||||
end
|
||||
if sub == "detail" then
|
||||
local rows = {}
|
||||
for model, cats in pairs(ctx.usage_totals or {}) do
|
||||
for category, c in pairs(cats) do
|
||||
rows[#rows + 1] = {
|
||||
model = model, category = category,
|
||||
prompt = c.prompt, completion = c.completion,
|
||||
calls = c.calls, cost = c.cost,
|
||||
is_local = c.is_local,
|
||||
}
|
||||
end
|
||||
end
|
||||
if #rows == 0 then renderer.status("(no usage recorded)"); return end
|
||||
-- R7: 3-level deterministic sort
|
||||
table.sort(rows, function(a, b)
|
||||
if a.cost ~= b.cost then return a.cost > b.cost end
|
||||
if a.model ~= b.model then return a.model < b.model end
|
||||
return a.category < b.category
|
||||
end)
|
||||
renderer.status(("session usage detail (total=$%.6f, %d/%d tokens):"):format(
|
||||
total_cost, total_p, total_c))
|
||||
for _, r in ipairs(rows) do
|
||||
io.write((" %-26s %-18s %3d calls, %6d / %6d tokens, $%.6f%s\n"):format(
|
||||
r.model, r.category, r.calls, r.prompt, r.completion, r.cost,
|
||||
r.is_local and " (local)" or ""))
|
||||
end
|
||||
return
|
||||
end
|
||||
renderer.status("usage: :cost [detail|reset]")
|
||||
end
|
||||
-- diff or git failure emits status and skips — never pollutes
|
||||
-- context with empty or error noise.
|
||||
meta.diff = function(args)
|
||||
|
||||
Reference in New Issue
Block a user