diff --git a/agent/insights.py b/agent/insights.py index 34e78a6ff9..6962d13427 100644 --- a/agent/insights.py +++ b/agent/insights.py @@ -1000,6 +1000,29 @@ class InsightsEngine: lines.append(f" Avg msgs/session: {o['avg_messages_per_session']:.1f}") lines.append("") + # Cost breakdown — surface the three buckets so subscription-included + # and unknown-cost sessions are visible instead of silently collapsing + # to $0. See #77223. + est_cost = o.get("estimated_cost", 0.0) + included_sessions = o.get("included_cost_sessions", 0) + unknown_sessions = o.get("unknown_cost_sessions", 0) + if est_cost > 0 or included_sessions > 0 or unknown_sessions > 0: + lines.append(" 💰 Cost") + lines.append(" " + "─" * 56) + if est_cost > 0: + lines.append(f" Estimated: ~${est_cost:.2f}") + if included_sessions > 0: + lines.append( + f" Included: {included_sessions} session(s) " + f"(subscription — no provider invoice)" + ) + if unknown_sessions > 0: + lines.append( + f" Unknown: {unknown_sessions} session(s) " + f"(no pricing data)" + ) + lines.append("") + # Model breakdown if report["models"]: lines.append(" 🤖 Models Used") @@ -1114,6 +1137,21 @@ class InsightsEngine: lines.append(f"**Active time:** ~{format_duration_compact(o['total_hours'] * 3600)} | **Avg session:** ~{format_duration_compact(o['avg_session_duration'])}") lines.append("") + # Cost breakdown — surface buckets so included/unknown are visible + est_cost = o.get("estimated_cost", 0.0) + included = o.get("included_cost_sessions", 0) + unknown = o.get("unknown_cost_sessions", 0) + cost_parts: list[str] = [] + if est_cost > 0: + cost_parts.append(f"~${est_cost:.2f} estimated") + if included > 0: + cost_parts.append(f"{included} included (subscription)") + if unknown > 0: + cost_parts.append(f"{unknown} unknown") + if cost_parts: + lines.append(f"**Cost:** {' | '.join(cost_parts)}") + lines.append("") + # Models (top 5) if report["models"]: lines.append("**🤖 Models:**") diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 592f6742b6..6ef29b5ff0 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -18,6 +18,29 @@ _ZERO = Decimal("0") _ONE_MILLION = Decimal("1000000") _NOUS_DEFAULT_BASE_URL = "https://inference-api.nousresearch.com/v1" +# Sub-cent cost threshold: below $0.01, render at 4 decimal places so +# the display is non-zero (e.g. $0.0046 instead of $0.00). See #79220. +_SUBCENT_THRESHOLD = Decimal("0.01") + + +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) + - 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. + """ + if amount == _ZERO: + return "$0.00" + if amount < _SUBCENT_THRESHOLD: + return f"~${amount:.4f}" + return f"~${amount:.2f}" + CostStatus = Literal["actual", "estimated", "included", "unknown"] CostSource = Literal[ "provider_cost_api", @@ -1334,6 +1357,7 @@ def estimate_usage_cost( source="none", label="included", pricing_version="included-route", + notes=("subscription-included; no provider invoice for usage",), ) entry = get_pricing_entry(model_name, provider=provider, base_url=base_url, api_key=api_key) @@ -1378,7 +1402,7 @@ def estimate_usage_cost( amount += Decimal(usage.request_count) * entry.request_cost status: CostStatus = "estimated" - label = f"~${amount:.2f}" + 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 2d42ab62e1..2a3b12157a 100644 --- a/tests/agent/test_insights.py +++ b/tests/agent/test_insights.py @@ -557,7 +557,9 @@ class TestTerminalFormatting: assert "N/A" not in text assert "custom/self-hosted" not in text - assert "Cost" 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. class TestGatewayFormatting: @@ -571,12 +573,16 @@ class TestGatewayFormatting: def test_gateway_format_hides_cost(self, populated_db): - """Gateway format omits dollar figures and internal cache details.""" + """Gateway format omits internal cache details. + + Dollar figures now appear when there are estimated/included/unknown + cost buckets (#77223) — the old assertion that '$' is absent is no + longer correct because surfacing cost buckets is the fix. + """ engine = InsightsEngine(populated_db) report = engine.generate(days=30) text = engine.format_gateway(report) - assert "$" not in text assert "cache" not in text.lower() @@ -670,3 +676,74 @@ class TestEdgeCases: # Actually the condition is > 1 platforms OR non-cli, so single cli won't show + def test_cost_buckets_displayed_in_terminal_format(self, db): + """#77223: included/estimated/unknown cost buckets surface in terminal.""" + # Estimated cost session + 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=1.50, actual_cost_usd=1.0, + cost_status="estimated", cost_source="provider", api_call_count=1, + ) + # Included cost session (subscription) + db.create_session(session_id="inc", source="cli", model="gpt-5.4-mini") + db.update_token_counts( + "inc", input_tokens=200, model="gpt-5.4-mini", + billing_provider="openai-codex", + estimated_cost_usd=0.0, actual_cost_usd=0.0, + cost_status="included", cost_source="none", api_call_count=1, + ) + + engine = InsightsEngine(db) + report = engine.generate(days=30) + text = engine.format_terminal(report) + + # The cost section should appear with all three buckets + assert "💰 Cost" in text + assert "~$1.50" in text # estimated + assert "included" in text.lower() + assert "subscription" in text.lower() + + 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") + db.update_token_counts( + "est", input_tokens=100, model="model-a", + billing_provider="custom", + estimated_cost_usd=2.25, actual_cost_usd=0.0, + cost_status="estimated", cost_source="provider", api_call_count=1, + ) + db.create_session(session_id="inc", source="cli", model="gpt-5.4-mini") + db.update_token_counts( + "inc", input_tokens=200, model="gpt-5.4-mini", + billing_provider="openai-codex", + estimated_cost_usd=0.0, actual_cost_usd=0.0, + cost_status="included", cost_source="none", api_call_count=1, + ) + + engine = InsightsEngine(db) + report = engine.generate(days=30) + text = engine.format_gateway(report) + + assert "**Cost:**" in text + assert "~$2.25" in text + assert "included" in text.lower() + + def test_no_cost_section_when_all_zero(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 + with no pricing data, rather than silently reporting $0. + """ + db.create_session(session_id="s1", source="cli", model="test") + db._conn.commit() + + engine = InsightsEngine(db) + report = engine.generate(days=30) + text = engine.format_terminal(report) + # The session has no cost data, so it falls in the "unknown" bucket. + assert "💰 Cost" in text + assert "Unknown" in text + + diff --git a/tests/agent/test_usage_pricing.py b/tests/agent/test_usage_pricing.py index 7939faaabf..d2bb8c4df7 100644 --- a/tests/agent/test_usage_pricing.py +++ b/tests/agent/test_usage_pricing.py @@ -2,11 +2,13 @@ from types import SimpleNamespace from agent.usage_pricing import ( CanonicalUsage, + _format_cost_label, estimate_usage_cost, get_pricing_entry, normalize_usage, resolve_billing_route, ) +from decimal import Decimal @@ -370,3 +372,77 @@ def test_normalize_usage_native_anthropic_no_cache_observability(caplog): assert result.input_tokens == 100 assert result.cache_read_tokens == 50 assert result.cache_write_tokens == 10 + + +# --------------------------------------------------------------------------- +# Cost label formatting (#79220: sub-cent costs render as $0.00) +# --------------------------------------------------------------------------- + + +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" + + def test_sub_cent_renders_4dp(self): + """Costs below $0.01 render at 4 decimal places (#79220).""" + 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" + + def test_normal_cost_renders_2dp(self): + 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" + + def test_very_small_sub_cent(self): + """Even very small costs render non-zero.""" + label = _format_cost_label(Decimal("0.0001")) + assert label == "~$0.0001" + assert label != "$0.00" + + 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) + assert "0.0046" in label + assert label != "$0.00" + assert label != "~$0.00" + + +# --------------------------------------------------------------------------- +# Subscription-included cost notes +# --------------------------------------------------------------------------- + + +class TestSubscriptionIncludedNotes: + """Subscription-included costs should carry a note clarifying no invoice.""" + + def test_included_cost_has_note(self): + """estimate_usage_cost for subscription-included route includes a note.""" + # openai-codex is subscription_included + usage = CanonicalUsage( + input_tokens=1000, + output_tokens=500, + cache_read_tokens=0, + cache_write_tokens=0, + reasoning_tokens=0, + ) + result = estimate_usage_cost( + "gpt-5.4-mini", + usage, + provider="openai-codex", + ) + assert result.status == "included" + assert result.amount_usd == Decimal("0") + assert len(result.notes) > 0 + assert any("subscription" in note.lower() for note in result.notes)