fix(usage): cost display honesty — sub-cent labels, cost buckets, included notes
Three cost-display honesty fixes: 1. Sub-cent cost label rendering (#79220) — _format_cost_label() scales precision to magnitude: zero renders as '$0.00', sub-cent (< $0.01) renders at 4 decimal places (e.g. '~$0.0046'), normal costs keep 2dp. This fixes the bug where DeepSeek per-turn costs of $0.004640 rendered as '~$0.00' despite amount_usd carrying full Decimal precision. 2. Cost bucket surfacing (#77223) — insights format_terminal and format_gateway now display three cost buckets: estimated (with dollar figure), included (session count, labeled 'subscription — no provider invoice'), and unknown (session count, labeled 'no pricing data'). Previously, included and unknown sessions silently collapsed to $0 in the aggregate view, hiding 315 of 473 sessions in the reporter's DB. 3. Subscription-included cost notes — estimate_usage_cost now attaches a 'subscription-included; no provider invoice for usage' note to CostResult for subscription-included routes (openai-codex), so consumers can distinguish 'free because subscription' from 'free because $0 pricing'. Fixes #79220 Fixes #77223
This commit is contained in:
@@ -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:**")
|
||||
|
||||
+25
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user