diff --git a/agent/insights.py b/agent/insights.py index 6962d13427..b1b9a06ef6 100644 --- a/agent/insights.py +++ b/agent/insights.py @@ -21,16 +21,28 @@ import sqlite3 import time from collections import Counter, defaultdict from datetime import datetime +from decimal import Decimal from typing import Any, Dict, List, Optional from agent.usage_pricing import ( CanonicalUsage, estimate_usage_cost, + format_cost_label, format_duration_compact, has_known_pricing, ) +def _fmt_est_cost(est_cost: float) -> str: + """Format an aggregate estimated cost via the shared cost-label helper. + + Routes through ``format_cost_label`` so sub-cent aggregates render at + 4dp instead of collapsing to "~$0.00" (#79220 bug class — the same + dishonesty this module's cost buckets exist to fix, #77223). + """ + return format_cost_label(Decimal(str(est_cost))) + + def _estimate_cost( @@ -1010,7 +1022,7 @@ class InsightsEngine: lines.append(" 💰 Cost") lines.append(" " + "─" * 56) if est_cost > 0: - lines.append(f" Estimated: ~${est_cost:.2f}") + lines.append(f" Estimated: {_fmt_est_cost(est_cost)}") if included_sessions > 0: lines.append( f" Included: {included_sessions} session(s) " @@ -1143,7 +1155,7 @@ class InsightsEngine: unknown = o.get("unknown_cost_sessions", 0) cost_parts: list[str] = [] if est_cost > 0: - cost_parts.append(f"~${est_cost:.2f} estimated") + cost_parts.append(f"{_fmt_est_cost(est_cost)} estimated") if included > 0: cost_parts.append(f"{included} included (subscription)") if unknown > 0: diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 6ef29b5ff0..d3e294f7b4 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -23,22 +23,30 @@ _NOUS_DEFAULT_BASE_URL = "https://inference-api.nousresearch.com/v1" _SUBCENT_THRESHOLD = Decimal("0.01") -def _format_cost_label(amount: Decimal) -> str: +def format_cost_label(amount: Decimal) -> str: """Format a cost amount as a display label. Scales precision to magnitude: - Zero → "$0.00" - - Sub-cent (< $0.01) → "~$0.0046" (4 dp, always non-zero) + - Sub-cent (< $0.01) → "~$0.0046" (4 dp; below $0.00005 falls back + to "~$<0.0001" so the label never reads as zero) - Normal → "~$1.23" (2 dp) This fixes #79220 where sub-cent per-turn costs on cheap models (DeepSeek, etc.) rendered as "$0.00" despite amount_usd carrying full Decimal precision. + + Shared by per-response cost labels (estimate_usage_cost) and the + insights cost-bucket formatters — keep both surfaces on this one + implementation so sub-cent honesty can't regress on one of them. """ if amount == _ZERO: return "$0.00" if amount < _SUBCENT_THRESHOLD: - return f"~${amount:.4f}" + label = f"~${amount:.4f}" + # 4dp truncation of a positive amount below $0.00005 would render + # "~$0.0000" — a zero-looking label, the exact #79220 dishonesty. + return label if label != "~$0.0000" else "~$<0.0001" return f"~${amount:.2f}" CostStatus = Literal["actual", "estimated", "included", "unknown"] @@ -1402,7 +1410,7 @@ def estimate_usage_cost( amount += Decimal(usage.request_count) * entry.request_cost status: CostStatus = "estimated" - label = _format_cost_label(amount) + label = format_cost_label(amount) if entry.source == "none" and amount == _ZERO: status = "included" label = "included" diff --git a/tests/agent/test_insights.py b/tests/agent/test_insights.py index 2a3b12157a..1c19abf43d 100644 --- a/tests/agent/test_insights.py +++ b/tests/agent/test_insights.py @@ -545,8 +545,8 @@ class TestTerminalFormatting: - def test_terminal_format_hides_cost_for_custom_models(self, db): - """Cost display is hidden entirely — custom models no longer show 'N/A' either.""" + def test_terminal_format_unknown_bucket_for_custom_models(self, db): + """Custom models with no pricing surface as the Unknown bucket (#77223).""" db.create_session(session_id="s1", source="cli", model="my-custom-model") db.update_token_counts("s1", input_tokens=1000, output_tokens=500) db._conn.commit() @@ -557,9 +557,11 @@ class TestTerminalFormatting: assert "N/A" not in text assert "custom/self-hosted" not in text - # Cost section now surfaces unknown-cost sessions (#77223) instead - # of hiding them — a custom model with no pricing data should show - # "Unknown: 1 session(s)" rather than silently reporting $0. + # Cost section surfaces unknown-cost sessions (#77223) instead of + # hiding them — a custom model with no pricing data shows in the + # Unknown bucket rather than silently reporting $0. + assert "Unknown" in text + assert "no pricing data" in text class TestGatewayFormatting: @@ -572,7 +574,7 @@ class TestGatewayFormatting: assert len(gateway_text) < len(terminal_text) - def test_gateway_format_hides_cost(self, populated_db): + def test_gateway_format_hides_cache_details(self, populated_db): """Gateway format omits internal cache details. Dollar figures now appear when there are estimated/included/unknown @@ -705,6 +707,30 @@ class TestEdgeCases: assert "included" in text.lower() assert "subscription" in text.lower() + def test_sub_cent_aggregate_estimated_cost_not_zero(self, db): + """A sub-cent aggregate must not render 'Estimated: ~$0.00' (#79220). + + The insights formatters share format_cost_label with per-response + labels; a cheap-model period totaling $0.0046 shows 4dp, not $0.00. + """ + db.create_session(session_id="est", source="cli", model="model-a") + db.update_token_counts( + "est", input_tokens=100, model="model-a", + billing_provider="custom", + estimated_cost_usd=0.0046, actual_cost_usd=0.0, + cost_status="estimated", cost_source="provider", api_call_count=1, + ) + + engine = InsightsEngine(db) + report = engine.generate(days=30) + terminal_text = engine.format_terminal(report) + gateway_text = engine.format_gateway(report) + + assert "~$0.00\n" not in terminal_text + assert "~$0.0046" in terminal_text + assert "~$0.00 estimated" not in gateway_text + assert "~$0.0046 estimated" in gateway_text + def test_cost_buckets_displayed_in_gateway_format(self, db): """#77223: included/estimated/unknown cost buckets surface in gateway.""" db.create_session(session_id="est", source="cli", model="model-a") @@ -730,7 +756,7 @@ class TestEdgeCases: assert "~$2.25" in text assert "included" in text.lower() - def test_no_cost_section_when_all_zero(self, db): + def test_unknown_bucket_shown_for_costless_session(self, db): """A session with no model still shows unknown cost bucket (#77223). The unknown bucket is surfaced so users can see they have sessions diff --git a/tests/agent/test_usage_pricing.py b/tests/agent/test_usage_pricing.py index d2bb8c4df7..fa0fcef5a6 100644 --- a/tests/agent/test_usage_pricing.py +++ b/tests/agent/test_usage_pricing.py @@ -2,7 +2,7 @@ from types import SimpleNamespace from agent.usage_pricing import ( CanonicalUsage, - _format_cost_label, + format_cost_label, estimate_usage_cost, get_pricing_entry, normalize_usage, @@ -383,37 +383,47 @@ class TestFormatCostLabel: """Tests for magnitude-scaled cost label formatting.""" def test_zero_renders_as_dollar_zero(self): - assert _format_cost_label(Decimal("0")) == "$0.00" + assert format_cost_label(Decimal("0")) == "$0.00" def test_sub_cent_renders_4dp(self): """Costs below $0.01 render at 4 decimal places (#79220).""" - label = _format_cost_label(Decimal("0.004640")) + label = format_cost_label(Decimal("0.004640")) assert label == "~$0.0046" # Must NOT be $0.00 assert "$0.00" != label def test_exactly_one_cent_renders_2dp(self): """$0.01 renders at 2dp.""" - assert _format_cost_label(Decimal("0.01")) == "~$0.01" + assert format_cost_label(Decimal("0.01")) == "~$0.01" def test_normal_cost_renders_2dp(self): - assert _format_cost_label(Decimal("1.23")) == "~$1.23" + assert format_cost_label(Decimal("1.23")) == "~$1.23" def test_large_cost_renders_2dp(self): - assert _format_cost_label(Decimal("42.50")) == "~$42.50" + assert format_cost_label(Decimal("42.50")) == "~$42.50" def test_very_small_sub_cent(self): """Even very small costs render non-zero.""" - label = _format_cost_label(Decimal("0.0001")) + label = format_cost_label(Decimal("0.0001")) assert label == "~$0.0001" assert label != "$0.00" + def test_below_4dp_floor_never_reads_zero(self): + """Amounts below $0.00005 must not render as '~$0.0000' (#79220). + + 4dp truncation of a positive amount would produce a zero-looking + label — the exact dishonesty the formatter exists to fix. + """ + label = format_cost_label(Decimal("0.00004")) + assert label == "~$<0.0001" + assert "0.0000" not in label.replace("<0.0001", "") + def test_sub_cent_deepseek_scenario(self): """Reproduce the #79220 reproduction: DeepSeek at $0.004640.""" # DeepSeek V4 Pro: 8K input + 1.2K output + 32K cache read # = $0.004640 per turn amount = Decimal("0.004640") - label = _format_cost_label(amount) + label = format_cost_label(amount) assert "0.0046" in label assert label != "$0.00" assert label != "~$0.00"