feat(usage): multi-provider equivalent cost table

This commit is contained in:
claude
2026-08-22 01:06:02 +00:00
parent 979c93bdbb
commit 33e4c16782
2 changed files with 115 additions and 26 deletions
+50 -13
View File
@@ -91,6 +91,14 @@ STYLE_DEFAULTS: dict[str, tuple[int, str]] = {
# (env) or `.pr-review.json:cost_target` (per repo). # (env) or `.pr-review.json:cost_target` (per repo).
DEFAULT_PRICE_TARGET = "claude-sonnet-5" DEFAULT_PRICE_TARGET = "claude-sonnet-5"
# Default roster of paid providers shown in the equivalent-cost table when
# `.pr-review.json` does not pin `compare_against`. The pilot is free-tier only,
# so this list is the operator's budgeting signal — it answers "what would this
# have cost on a mainstream paid API?". Override per-repo via
# `.pr-review.json:compare_against` (capped at 12 entries; unknown keys are
# dropped with a stderr line at parse time).
DEFAULT_COMPARE_AGAINST = ("claude-sonnet-5", "gpt-5", "gemini-2.5-pro", "grok-4.5")
SYSTEM_PROMPT = """You are a senior, pragmatic code reviewer. Review the pull request diff below. SYSTEM_PROMPT = """You are a senior, pragmatic code reviewer. Review the pull request diff below.
Report ONLY real, actionable issues: correctness bugs, security problems, risky Report ONLY real, actionable issues: correctness bugs, security problems, risky
@@ -1211,9 +1219,13 @@ def findings_table(findings: list[dict]) -> str:
def _render_collapsible_usage(usage: dict | None, model: str, config: dict | None) -> str: def _render_collapsible_usage(usage: dict | None, model: str, config: dict | None) -> str:
"""Render the telemetry as a collapsible ``<details>`` block. """Render the telemetry as a collapsible ``<details>`` block.
Empty string when `usage` is None. The cost-equivalent line is always Empty string when `usage` is None. The equivalent-cost table is the
shown (it's the operator's budgeting signal). The `actual` line is shown operator's budgeting signal — the pilot runs on a free tier, so the
but the FREE-TIER note is collapsed into a single short clause. `actual` line is $0.00; the table shows what the same measured tokens
would bill on mainstream paid APIs (configurable via `compare_against`,
defaulting to ``DEFAULT_COMPARE_AGAINST``). The row matching `cost_target`
is bolded so the price target stands out. The whole table is omitted when
every row would be $0 (no work done).
""" """
if not usage: if not usage:
return "" return ""
@@ -1222,15 +1234,34 @@ def _render_collapsible_usage(usage: dict | None, model: str, config: dict | Non
actual = usage.get("cost") or 0.0 actual = usage.get("cost") or 0.0
actual_s = f"${actual:.4f}" if actual else "$0.00" actual_s = f"${actual:.4f}" if actual else "$0.00"
actual_note = " (headroom glm-5.2:cloud — free tier)" if not actual else "" actual_note = " (headroom glm-5.2:cloud — free tier)" if not actual else ""
price_key, price_err = _resolve_price_target(config) cost_target, price_err = _resolve_price_target(config)
from cost_model import PRICES if price_err:
eq = equivalent_cost(usage, price_key) # Surface config typos loudly but do not pollute the posted summary
eq_s = f"${eq:.4f}" if eq else "$0.00" # body — typos at the table-row level would render as English
eq_label = PRICES[price_key].name # mid-table and look like a model error.
eq_note = ( print(f"pragent: {price_err}", file=sys.stderr, flush=True)
f" _(price target: `{price_key}`; {price_err})_" # Lazy: cost_model has no dep on ai_review, and the ollama path
if price_err else "" # never reaches this branch.
) from cost_model import PRICES as _PRICES
cfg = config or {}
compare: list[str] = list(cfg.get("compare_against") or DEFAULT_COMPARE_AGAINST)
# Always include the resolved cost_target (env + config), even when the
# operator pinned a different `compare_against` roster — the price target
# row is the one maintainers eyeball against. Skip silently if the key
# isn't a known Price (e.g. a typo that slipped past stderr earlier).
if cost_target in _PRICES and cost_target not in compare:
compare.append(cost_target)
eq_rows: list[str] = []
for key in compare:
if key not in _PRICES:
continue
c = equivalent_cost(usage, key)
if c <= 0:
continue
label = _PRICES[key].name
cost_str = f"${c:.4f}" if c < 0.01 else f"${c:.2f}"
bold = "**" if key == cost_target else ""
eq_rows.append(f"| {bold}{label}{bold} | {cost_str} |")
in_tok = usage.get("input", 0) in_tok = usage.get("input", 0)
out_tok = usage.get("output", 0) out_tok = usage.get("output", 0)
reason_tok = usage.get("reasoning", 0) reason_tok = usage.get("reasoning", 0)
@@ -1251,10 +1282,16 @@ def _render_collapsible_usage(usage: dict | None, model: str, config: dict | Non
f"- **Total Tokens**: {fmt_tokens(in_tok)} in / {fmt_tokens(out_tok)} out " f"- **Total Tokens**: {fmt_tokens(in_tok)} in / {fmt_tokens(out_tok)} out "
f"({fmt_tokens(reason_tok)} reasoning, cache {fmt_tokens(cache_r)} read / " f"({fmt_tokens(reason_tok)} reasoning, cache {fmt_tokens(cache_r)} read / "
f"{fmt_tokens(cache_w)} write, {fmt_tokens(total)} total)", f"{fmt_tokens(cache_w)} write, {fmt_tokens(total)} total)",
f"- **Est. cost on {eq_label}**: {eq_s}{eq_note}",
f"- **Actual**: {actual_s}{actual_note}", f"- **Actual**: {actual_s}{actual_note}",
f"- **Scope**: {scope}", f"- **Scope**: {scope}",
] ]
if eq_rows:
lines.append("")
lines.append("- **Equivalent cost on paid providers** (this run's tokens):")
lines.append("")
lines.append("| Provider | Cost |")
lines.append("|---|---:|")
lines.extend(eq_rows)
# Multi-lens fan-out: surface the lens roster + summed steps so the user # Multi-lens fan-out: surface the lens roster + summed steps so the user
# can see which lenses contributed (and that triage didn't drop them all). # can see which lenses contributed (and that triage didn't drop them all).
lenses = usage.get("lenses") lenses = usage.get("lenses")
+65 -13
View File
@@ -1165,19 +1165,22 @@ def test_usage_block_shows_equivalent_provider_cost():
"cache_read": 0, "cache_write": 0, "total": 204000, "cache_read": 0, "cache_write": 0, "total": 204000,
"cost": 0.0, "steps": 6, "duration_s": 100.0} "cost": 0.0, "steps": 6, "duration_s": 100.0}
sec = ai_review._render_collapsible_usage(usage, "glm-5.2:cloud", config=None) sec = ai_review._render_collapsible_usage(usage, "glm-5.2:cloud", config=None)
# Two cost lines now: an equivalent (default Sonnet 5) AND the $0 actual. # New layout: equivalent-cost table instead of a single "Est. cost on …"
# line. The default compare_against is sonnet-5, gpt-5, gemini-2.5-pro,
# grok-4.5; cost_target defaults to sonnet-5 (bolded).
assert "🔋 AI Usage & Run Details" in sec assert "🔋 AI Usage & Run Details" in sec
assert "**Est. cost on Claude Sonnet 5**" in sec
assert "**Actual**: $0.00" in sec assert "**Actual**: $0.00" in sec
assert "free tier" in sec assert "free tier" in sec
# Equivalent should be > 0 for non-trivial token counts.
assert "$0.00" in sec # the actual line assert "$0.00" in sec # the actual line
# And a non-zero one for the equivalent. # Multi-provider table header present, default roster rendered, default
import re # cost_target (Sonnet 5) is the bolded row.
cost_lines = [ln for ln in sec.splitlines() if "cost on" in ln] assert "| Provider | Cost |" in sec
assert len(cost_lines) == 1 assert "**Claude Sonnet 5**" in sec
assert re.search(r"\$\d", cost_lines[0]) is not None assert "GPT-5" in sec
assert "$0.00" not in cost_lines[0] assert "Gemini 2.5 Pro" in sec
assert "Grok 4.5" in sec
# 200k * $2/MTok + 4k * $10/MTok → $0.44
assert "$0.44" in sec
def test_usage_block_honors_cost_target(monkeypatch): def test_usage_block_honors_cost_target(monkeypatch):
@@ -1204,17 +1207,22 @@ def test_usage_block_respects_repo_config_cost_target(monkeypatch):
assert "$0.0075" in sec assert "$0.0075" in sec
def test_usage_block_reports_unknown_price_target(): def test_usage_block_reports_unknown_price_target(capsys):
usage = {"input": 100, "output": 100, "reasoning": 0, usage = {"input": 100, "output": 100, "reasoning": 0,
"cache_read": 0, "cache_write": 0, "total": 200, "cache_read": 0, "cache_write": 0, "total": 200,
"cost": 0.0, "steps": 1, "duration_s": 1.0} "cost": 0.0, "steps": 1, "duration_s": 1.0}
sec = ai_review._render_collapsible_usage( sec = ai_review._render_collapsible_usage(
usage, "glm-5.2:cloud", config={"cost_target": "bogus-model"} usage, "glm-5.2:cloud", config={"cost_target": "bogus-model"}
) )
# Falls back to default + surfaces the error in the line. # Falls back to default. The error now goes to stderr (otherwise it would
# land mid-table and look like a model error in the posted summary).
assert "Claude Sonnet 5" in sec assert "Claude Sonnet 5" in sec
assert "unknown price target" in sec assert "**Claude Sonnet 5**" in sec # bolded as the resolved cost_target
assert "bogus-model" in sec assert "bogus-model" not in sec
assert "unknown price target" not in sec
err = capsys.readouterr().err
assert "unknown price target" in err
assert "bogus-model" in err
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1769,6 +1777,50 @@ def test_collapsible_usage_renders_humanized_tokens():
assert "17,303 (17.3K) out" in block assert "17,303 (17.3K) out" in block
# ---------------------------------------------------------------------------
# Multi-provider equivalent-cost table — Task 10
# ---------------------------------------------------------------------------
def test_collapsible_usage_renders_multi_provider_table():
usage = {"input": 1_000_000, "output": 1000, "reasoning": 0,
"cache_read": 0, "cache_write": 0, "total": 1_001_000,
"cost": 0.0, "steps": 1, "duration_s": 10.0}
block = _render_collapsible_usage(usage, "glm-5.2:cloud", config={"compare_against": ["claude-sonnet-5", "gpt-5"]})
assert "Claude Sonnet 5" in block
assert "GPT-5" in block
assert "| Provider | Cost |" in block
def test_collapsible_usage_uses_default_compare_against_when_absent():
usage = {"input": 1_000_000, "output": 0, "reasoning": 0,
"cache_read": 0, "cache_write": 0, "total": 1_000_000,
"cost": 0.0, "steps": 1, "duration_s": 5.0}
block = _render_collapsible_usage(usage, "glm-5.2:cloud", config={})
assert "Claude Sonnet 5" in block
assert "GPT-5" in block
assert "Gemini 2.5 Pro" in block
assert "Grok 4.5" in block
def test_collapsible_usage_bolds_cost_target_row():
usage = {"input": 1_000_000, "output": 0, "reasoning": 0,
"cache_read": 0, "cache_write": 0, "total": 1_000_000,
"cost": 0.0, "steps": 1, "duration_s": 5.0}
block = _render_collapsible_usage(usage, "glm-5.2:cloud", config={"cost_target": "gpt-5"})
assert "**GPT-5**" in block
assert "Claude Sonnet 5" in block # still in default compare set
def test_collapsible_usage_skips_zero_cost_rows():
usage = {"input": 0, "output": 0, "reasoning": 0,
"cache_read": 0, "cache_write": 0, "total": 0,
"cost": 0.0, "steps": 1, "duration_s": 1.0}
block = _render_collapsible_usage(usage, "glm-5.2:cloud", config={})
# With zero tokens, all costs are $0 — skip the entire table.
assert "| Provider | Cost |" not in block
def test_inline_comment_body_humanized_tokens(): def test_inline_comment_body_humanized_tokens():
# Value chosen > 1000 so fmt_tokens actually adds the comma + short suffix; # Value chosen > 1000 so fmt_tokens actually adds the comma + short suffix;
# the plan's 362 would render identically with or without fmt_tokens. # the plan's 362 would render identically with or without fmt_tokens.