From 677680034ae50dc9c271a665bd6fc26673ec1093 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Tue, 16 Jun 2026 19:44:26 +0530 Subject: [PATCH] Revert "gateway: capture real provider-reported cost (openrouter usage accounting)" This reverts commit 85546bb9e2a2f82f13af8f4969803201ec27c3b8. --- agent/agent_init.py | 6 - agent/conversation_loop.py | 47 +---- agent/transports/chat_completions.py | 7 - agent/usage_pricing.py | 67 ------- cli.py | 28 +-- gateway/slash_commands.py | 35 ++-- .../model-providers/openrouter/__init__.py | 7 - run_agent.py | 3 - tests/agent/test_provider_cost_capture.py | 186 ------------------ tests/cli/test_cli_status_bar.py | 29 ++- tests/gateway/test_usage_command.py | 70 ++----- tests/providers/test_provider_profiles.py | 4 +- tui_gateway/server.py | 24 ++- 13 files changed, 86 insertions(+), 427 deletions(-) delete mode 100644 tests/agent/test_provider_cost_capture.py diff --git a/agent/agent_init.py b/agent/agent_init.py index b22c2ac8f7a..2c2ded871e5 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -1598,12 +1598,6 @@ def init_agent( agent.session_cache_write_tokens = 0 agent.session_reasoning_tokens = 0 agent.session_estimated_cost_usd = 0.0 - # Provider-REPORTED cost only (e.g. OpenRouter usage.cost). None means - # "nothing reported" — distinct from a real $0.00. - agent.session_actual_cost_usd = None - # Per-model session usage rows for /usage: {model: {calls, input, output, - # cache_read, cache_write, cost_usd|None}}. - agent.session_model_usage = {} agent.session_cost_status = "unknown" agent.session_cost_source = "none" diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 54dd866aa8e..379a038a9e0 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -57,11 +57,7 @@ from agent.process_bootstrap import _install_safe_stdio from agent.prompt_caching import apply_anthropic_cache_control from agent.retry_utils import jittered_backoff from agent.trajectory import has_incomplete_scratchpad -from agent.usage_pricing import ( - estimate_usage_cost, - extract_provider_cost_usd, - normalize_usage, -) +from agent.usage_pricing import estimate_usage_cost, normalize_usage from hermes_constants import PARTIAL_STREAM_STUB_ID from hermes_logging import set_session_context from tools.skill_provenance import set_current_write_origin @@ -1809,37 +1805,6 @@ def run_conversation( agent.session_cost_status = cost_result.status agent.session_cost_source = cost_result.source - # ── Real provider-REPORTED cost (never estimated) ── - # OpenRouter usage accounting returns ``usage.cost`` on the - # response when the request carries usage:{include:true} - # (added on OpenRouter routes). When the provider reports - # nothing, this stays None — absent, NOT zero — so cost - # displays hide instead of showing a fabricated $0.00. - reported_cost_usd = extract_provider_cost_usd(response.usage) - if reported_cost_usd is not None: - _prev_actual = getattr(agent, "session_actual_cost_usd", None) - agent.session_actual_cost_usd = (_prev_actual or 0.0) + reported_cost_usd - agent.session_cost_status = "actual" - agent.session_cost_source = "provider_cost_api" - - # Per-model session breakdown for /usage — counts are always - # real; cost_usd only accumulates provider-reported values - # and stays None when the provider reports nothing. - _model_usage = getattr(agent, "session_model_usage", None) - if _model_usage is None: - _model_usage = agent.session_model_usage = {} - _mrow = _model_usage.setdefault(agent.model, { - "calls": 0, "input": 0, "output": 0, - "cache_read": 0, "cache_write": 0, "cost_usd": None, - }) - _mrow["calls"] += 1 - _mrow["input"] += canonical_usage.input_tokens - _mrow["output"] += canonical_usage.output_tokens - _mrow["cache_read"] += canonical_usage.cache_read_tokens - _mrow["cache_write"] += canonical_usage.cache_write_tokens - if reported_cost_usd is not None: - _mrow["cost_usd"] = (_mrow["cost_usd"] or 0.0) + reported_cost_usd - # Persist token counts to session DB for /insights. # Do this for every platform with a session_id so non-CLI # sessions (gateway, cron, delegated runs) cannot lose @@ -1866,14 +1831,8 @@ def run_conversation( reasoning_tokens=canonical_usage.reasoning_tokens, estimated_cost_usd=float(cost_result.amount_usd) if cost_result.amount_usd is not None else None, - # Provider-reported per-call cost delta. NULL - # (not 0) when the provider reported nothing — - # the SQL CASE keeps actual_cost_usd untouched. - actual_cost_usd=reported_cost_usd, - cost_status="actual" - if reported_cost_usd is not None else cost_result.status, - cost_source="provider_cost_api" - if reported_cost_usd is not None else cost_result.source, + cost_status=cost_result.status, + cost_source=cost_result.source, billing_provider=agent.provider, billing_base_url=agent.base_url, billing_mode="subscription_included" diff --git a/agent/transports/chat_completions.py b/agent/transports/chat_completions.py index b428fec0ec9..c0b2a13d250 100644 --- a/agent/transports/chat_completions.py +++ b/agent/transports/chat_completions.py @@ -388,13 +388,6 @@ class ChatCompletionsTransport(ProviderTransport): if provider_prefs and is_openrouter: extra_body["provider"] = provider_prefs - # OpenRouter usage accounting — response `usage.cost` carries the REAL - # charged cost (credits are 1:1 USD). Parity with the profile path in - # plugins/model-providers/openrouter/__init__.py; this branch only runs - # when the OpenRouter profile isn't loaded. - if is_openrouter: - extra_body["usage"] = {"include": True} - # Pareto Code router plugin — model-gated. Same shape as the # profile path in plugins/model-providers/openrouter/__init__.py; # this branch only runs when the OpenRouter profile isn't loaded. diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 33ebd229862..95bb11df521 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -852,73 +852,6 @@ def estimate_usage_cost( ) -def _finite_nonneg_number(value: Any) -> Optional[float]: - """Return ``value`` as a float when it is a real, finite, non-negative - number (int/float, not bool); otherwise None.""" - if isinstance(value, bool) or not isinstance(value, (int, float)): - return None - try: - f = float(value) - except (TypeError, ValueError): - return None - if f != f or f in (float("inf"), float("-inf")) or f < 0: - return None - return f - - -def extract_provider_cost_usd(response_usage: Any) -> Optional[float]: - """Provider-REPORTED cost (USD) from a response ``usage`` object, or None. - - Reads the ``usage.cost`` field that OpenRouter's usage accounting returns - (``usage: {"include": true}`` request param; OpenRouter credits are 1:1 - USD). OpenRouter-compatible aggregators use the same field. This NEVER - estimates: when the provider reports nothing, the result is None — callers - must treat None as "no cost data", not zero. A reported ``0`` is a real - zero (e.g. free-tier models) and is returned as ``0.0``. - """ - if response_usage is None: - return None - cost = getattr(response_usage, "cost", None) - if cost is None and isinstance(response_usage, dict): - cost = response_usage.get("cost") - return _finite_nonneg_number(cost) - - -def real_session_cost_usd(agent: Any) -> Optional[float]: - """Session-cumulative provider-REPORTED cost in USD, or None. - - Combines the two real sources Hermes has — no estimation, ever: - - ``agent.session_actual_cost_usd``: per-response ``usage.cost`` - accumulator (OpenRouter usage accounting). - - Nous ``x-nous-credits-*`` header delta via - ``agent.get_credits_spent_micros()`` (account-level spend since the - session first saw a header; clamped at 0 so a mid-session top-up - doesn't render a negative cost). - - Returns None when neither source has reported anything — callers must - hide their cost display in that case rather than showing $0.00. - """ - total: Optional[float] = None - - actual = _finite_nonneg_number(getattr(agent, "session_actual_cost_usd", None)) - if actual is not None: - total = actual - - try: - spent_micros = agent.get_credits_spent_micros() - except Exception: - spent_micros = None - if spent_micros is not None: - try: - spent_usd = max(0, int(spent_micros)) / 1_000_000 - except (TypeError, ValueError): - spent_usd = None - if spent_usd is not None: - total = (total or 0.0) + spent_usd - - return total - - def has_known_pricing( model_name: str, provider: Optional[str] = None, diff --git a/cli.py b/cli.py index 8a13a7953ff..bc4f4a76bef 100644 --- a/cli.py +++ b/cli.py @@ -8301,13 +8301,14 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin): compressions = compressor.compression_count msg_count = len(self.conversation_history) - # Cost — provider-REPORTED only (OpenRouter usage.cost accumulator - # and/or Nous credits-header delta). No estimation: an unreported - # cost shows as "not reported", never a fabricated dollar figure. - from agent.usage_pricing import real_session_cost_usd, resolve_billing_route - real_cost_usd = real_session_cost_usd(agent) - _billing_route = resolve_billing_route( + cost_result = estimate_usage_cost( agent.model, + CanonicalUsage( + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_tokens=cache_read_tokens, + cache_write_tokens=cache_write_tokens, + ), provider=getattr(agent, "provider", None), base_url=getattr(agent, "base_url", None), ) @@ -8327,16 +8328,21 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin): print(f" Total tokens: {total:>10,}") print(f" API calls: {calls:>10,}") print(f" Session duration: {elapsed:>10}") - if real_cost_usd is not None: - print(f" Cost (provider-reported): ${real_cost_usd:>9.4f}") - elif _billing_route.billing_mode == "subscription_included": - print(f" Cost: {'included':>11}") + print(f" Cost status: {cost_result.status:>10}") + print(f" Cost source: {cost_result.source:>10}") + if cost_result.amount_usd is not None: + prefix = "~" if cost_result.status == "estimated" else "" + print(f" Total cost: {prefix}${float(cost_result.amount_usd):>10.4f}") + elif cost_result.status == "included": + print(f" Total cost: {'included':>10}") else: - print(f" Cost: {'not reported by provider':>23}") + print(f" Total cost: {'n/a':>10}") print(f" {'─' * 40}") print(f" Current context: {last_prompt:,} / {ctx_len:,} ({pct:.0f}%)") print(f" Messages: {msg_count}") print(f" Compressions: {compressions}") + if cost_result.status == "unknown": + print(f" Note: Pricing unknown for {agent.model}") # Account limits -- fetched off-thread with a hard timeout so slow # provider APIs don't hang the prompt. diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 5f5fe9a155f..92db5b42f0c 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -3215,24 +3215,25 @@ class GatewaySlashCommandsMixin: lines.append(t("gateway.usage.label_total", count=f"{agent.session_total_tokens:,}")) lines.append(t("gateway.usage.label_api_calls", count=agent.session_api_calls)) - # Cost — provider-REPORTED only (OpenRouter usage.cost accumulator - # and/or Nous credits-header delta). No estimation: when nothing - # was reported the line is omitted entirely, never shown as $0.00. - # Subscription-included routes (a billing fact, not a price guess) - # still show "included". + # Cost estimation try: - from agent.usage_pricing import real_session_cost_usd, resolve_billing_route - real_cost = real_session_cost_usd(agent) - if real_cost is not None: - lines.append(t("gateway.usage.label_cost", prefix="", amount=f"{real_cost:.4f}")) - else: - route = resolve_billing_route( - agent.model, - provider=getattr(agent, "provider", None), - base_url=getattr(agent, "base_url", None), - ) - if route.billing_mode == "subscription_included": - lines.append(t("gateway.usage.label_cost_included")) + from agent.usage_pricing import CanonicalUsage, estimate_usage_cost + cost_result = estimate_usage_cost( + agent.model, + CanonicalUsage( + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_tokens=cache_read, + cache_write_tokens=cache_write, + ), + provider=getattr(agent, "provider", None), + base_url=getattr(agent, "base_url", None), + ) + if cost_result.amount_usd is not None: + prefix = "~" if cost_result.status == "estimated" else "" + lines.append(t("gateway.usage.label_cost", prefix=prefix, amount=f"{float(cost_result.amount_usd):.4f}")) + elif cost_result.status == "included": + lines.append(t("gateway.usage.label_cost_included")) except Exception: pass diff --git a/plugins/model-providers/openrouter/__init__.py b/plugins/model-providers/openrouter/__init__.py index 7a65aa51487..6566d604fc5 100644 --- a/plugins/model-providers/openrouter/__init__.py +++ b/plugins/model-providers/openrouter/__init__.py @@ -82,13 +82,6 @@ class OpenRouterProfile(ProviderProfile): if prefs: body["provider"] = prefs - # Usage accounting — makes OpenRouter return the REAL cost it charged - # in the response `usage.cost` field (credits are 1:1 USD), instead of - # Hermes having to estimate from a pricing table. Captured by - # agent.usage_pricing.extract_provider_cost_usd in the conversation - # loop. https://openrouter.ai/docs/use-cases/usage-accounting - body["usage"] = {"include": True} - # Pareto Code router — model-gated. The plugins block is only # meaningful for openrouter/pareto-code; sending it on any other # model has no documented effect and would be confusing in logs. diff --git a/run_agent.py b/run_agent.py index d766336b3be..94d3be3e674 100644 --- a/run_agent.py +++ b/run_agent.py @@ -636,9 +636,6 @@ class AIAgent: self.session_reasoning_tokens = 0 self.session_api_calls = 0 self.session_estimated_cost_usd = 0.0 - # Provider-REPORTED cost only — None means "nothing reported". - self.session_actual_cost_usd = None - self.session_model_usage = {} self.session_cost_status = "unknown" self.session_cost_source = "none" diff --git a/tests/agent/test_provider_cost_capture.py b/tests/agent/test_provider_cost_capture.py deleted file mode 100644 index 5399d81a907..00000000000 --- a/tests/agent/test_provider_cost_capture.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Real provider-reported cost capture — never estimated, absent ≠ zero. - -Covers the three fixture shapes from the cost-tracking fix: - - OpenRouter usage accounting: response ``usage.cost`` present → accumulates. - - Nous: ``x-nous-credits-*`` headers present → header delta accumulates. - - Provider reports nothing → cost stays None/absent (NOT zero-as-real). -""" - -from types import SimpleNamespace - -import pytest - -from agent.usage_pricing import extract_provider_cost_usd, real_session_cost_usd - - -# ── extract_provider_cost_usd — the per-response REAL cost reader ──────────── - - -class TestExtractProviderCost: - def test_openrouter_usage_cost_attr(self): - usage = SimpleNamespace(prompt_tokens=10, completion_tokens=5, cost=0.001234) - assert extract_provider_cost_usd(usage) == pytest.approx(0.001234) - - def test_dict_shaped_usage(self): - assert extract_provider_cost_usd({"cost": 0.5}) == pytest.approx(0.5) - - def test_reported_zero_is_real_zero(self): - # Free-tier models really cost $0 — distinct from "not reported". - usage = SimpleNamespace(cost=0) - assert extract_provider_cost_usd(usage) == 0.0 - - def test_absent_cost_is_none_not_zero(self): - usage = SimpleNamespace(prompt_tokens=10, completion_tokens=5) - assert extract_provider_cost_usd(usage) is None - assert extract_provider_cost_usd({"prompt_tokens": 10}) is None - - def test_none_usage_is_none(self): - assert extract_provider_cost_usd(None) is None - - def test_garbage_cost_values_are_none(self): - for bad in ("0.01", True, float("nan"), float("inf"), -0.5, [], {}): - assert extract_provider_cost_usd(SimpleNamespace(cost=bad)) is None, bad - - -# ── real_session_cost_usd — the session accumulator surface ───────────────── - - -class _FakeAgent: - def __init__(self, actual=None, credits_micros=None): - self.session_actual_cost_usd = actual - self._credits_micros = credits_micros - - def get_credits_spent_micros(self): - return self._credits_micros - - -class TestRealSessionCost: - def test_nothing_reported_is_none(self): - assert real_session_cost_usd(_FakeAgent()) is None - - def test_openrouter_accumulator_only(self): - assert real_session_cost_usd(_FakeAgent(actual=0.42)) == pytest.approx(0.42) - - def test_nous_credits_delta_only(self): - # 123_400 micros = $0.1234 - assert real_session_cost_usd( - _FakeAgent(credits_micros=123_400) - ) == pytest.approx(0.1234) - - def test_both_sources_sum(self): - assert real_session_cost_usd( - _FakeAgent(actual=0.10, credits_micros=200_000) - ) == pytest.approx(0.30) - - def test_negative_credits_delta_clamped(self): - # A mid-session top-up makes the delta negative — never show negative cost. - assert real_session_cost_usd(_FakeAgent(credits_micros=-50_000)) == 0.0 - - def test_agent_without_credits_method(self): - agent = SimpleNamespace(session_actual_cost_usd=None) - assert real_session_cost_usd(agent) is None - - def test_non_numeric_actual_ignored(self): - agent = _FakeAgent() - agent.session_actual_cost_usd = "0.42" # corrupted attr → ignore - assert real_session_cost_usd(agent) is None - - -# ── Nous header fixture → real accumulator (full _capture_credits path) ───── - - -def _nous_headers(remaining_micros: int) -> dict: - return { - "x-nous-credits-version": "1", - "x-nous-credits-remaining-micros": str(remaining_micros), - "x-nous-credits-remaining-usd": f"{remaining_micros / 1_000_000:.2f}", - "x-nous-credits-subscription-micros": str(remaining_micros), - "x-nous-credits-subscription-usd": f"{remaining_micros / 1_000_000:.2f}", - "x-nous-credits-rollover-micros": "0", - "x-nous-credits-purchased-micros": "0", - "x-nous-credits-purchased-usd": "0.00", - "x-nous-credits-denominator-kind": "none", - "x-nous-credits-paid-access": "true", - "x-nous-credits-as-of-ms": "1717000000000", - } - - -def _bare_nous_agent(): - """Minimal AIAgent shell exercising the real _capture_credits path.""" - from run_agent import AIAgent - - agent = object.__new__(AIAgent) - agent.provider = "nous" - agent._credits_state = None - agent._credits_session_start_micros = None - agent.notice_callback = None - agent.notice_clear_callback = None - agent.session_actual_cost_usd = None - return agent - - -class TestNousHeaderAccumulation: - def test_headers_accumulate_into_real_session_cost(self, monkeypatch): - monkeypatch.delenv("HERMES_DEV_CREDITS_FIXTURE", raising=False) - agent = _bare_nous_agent() - - # First response latches the session-start balance ($10.00). - agent._capture_credits(SimpleNamespace(headers=_nous_headers(10_000_000))) - assert real_session_cost_usd(agent) == 0.0 # real zero: headers seen, $0 spent - - # Second response: balance dropped by $0.25 → real reported spend. - agent._capture_credits(SimpleNamespace(headers=_nous_headers(9_750_000))) - assert real_session_cost_usd(agent) == pytest.approx(0.25) - - def test_no_headers_means_no_cost(self, monkeypatch): - monkeypatch.delenv("HERMES_DEV_CREDITS_FIXTURE", raising=False) - agent = _bare_nous_agent() - agent._capture_credits(SimpleNamespace(headers={"content-type": "application/json"})) - assert real_session_cost_usd(agent) is None - - -# ── OpenRouter request param — usage accounting must be requested ──────────── - - -class TestOpenRouterUsageParam: - def test_profile_extra_body_requests_usage_accounting(self): - import importlib.util - from pathlib import Path - - from providers import get_provider_profile - - profile = get_provider_profile("openrouter") - if profile is None: - # Force plugin discovery in minimal test envs. - plugin = Path(__file__).resolve().parents[2] / "plugins" / "model-providers" / "openrouter" / "__init__.py" - spec = importlib.util.spec_from_file_location("_or_plugin", plugin) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - profile = mod.openrouter - - body = profile.build_extra_body(session_id="s-1") - assert body["usage"] == {"include": True} - - def test_legacy_transport_path_requests_usage_accounting(self): - from agent.transports.chat_completions import ChatCompletionsTransport - - transport = ChatCompletionsTransport() - kwargs = transport.build_kwargs( - model="anthropic/claude-sonnet-4.6", - messages=[{"role": "user", "content": "hi"}], - tools=None, - is_openrouter=True, - ) - assert kwargs["extra_body"]["usage"] == {"include": True} - - def test_non_openrouter_does_not_send_usage_param(self): - from agent.transports.chat_completions import ChatCompletionsTransport - - transport = ChatCompletionsTransport() - kwargs = transport.build_kwargs( - model="deepseek-chat", - messages=[{"role": "user", "content": "hi"}], - tools=None, - is_openrouter=False, - ) - assert "usage" not in (kwargs.get("extra_body") or {}) diff --git a/tests/cli/test_cli_status_bar.py b/tests/cli/test_cli_status_bar.py index a266d9a4054..36587bff722 100644 --- a/tests/cli/test_cli_status_bar.py +++ b/tests/cli/test_cli_status_bar.py @@ -457,7 +457,7 @@ class TestCLIStatusBar: class TestCLIUsageReport: - def test_show_usage_reports_real_provider_cost(self, capsys): + def test_show_usage_includes_estimated_cost(self, capsys): cli_obj = _attach_agent( _make_cli(), prompt_tokens=10_230, @@ -469,22 +469,20 @@ class TestCLIUsageReport: compressions=1, ) cli_obj.verbose = False - # Provider-reported cost (e.g. OpenRouter usage accounting accumulator). - cli_obj.agent.session_actual_cost_usd = 0.0640 cli_obj._show_usage() output = capsys.readouterr().out assert "Model:" in output - assert "Cost (provider-reported):" in output + assert "Cost status:" in output + assert "Cost source:" in output + assert "Total cost:" in output assert "$" in output assert "0.064" in output assert "Session duration:" in output assert "Compressions:" in output - def test_show_usage_unreported_cost_is_not_a_dollar_figure(self, capsys): - """No estimation: when the provider reports nothing, /usage must NOT - fabricate a dollar amount — not even $0.00.""" + def test_show_usage_marks_unknown_pricing(self, capsys): cli_obj = _attach_agent( _make_cli(model="local/my-custom-model"), prompt_tokens=1_000, @@ -499,15 +497,13 @@ class TestCLIUsageReport: cli_obj._show_usage() output = capsys.readouterr().out - assert "not reported by provider" in output - assert "Cost (provider-reported):" not in output - assert "$0.00" not in output + assert "Total cost:" in output + assert "n/a" in output + assert "Pricing unknown for local/my-custom-model" in output - def test_show_usage_never_estimates_even_with_known_pricing(self, capsys): - """A model with a pricing-table entry must still show NO cost when the - provider reported nothing (hard requirement: real cost only).""" + def test_zero_priced_provider_models_stay_unknown(self, capsys): cli_obj = _attach_agent( - _make_cli(model="anthropic/claude-sonnet-4-6"), + _make_cli(model="glm-5"), prompt_tokens=1_000, completion_tokens=500, total_tokens=1_500, @@ -520,8 +516,9 @@ class TestCLIUsageReport: cli_obj._show_usage() output = capsys.readouterr().out - assert "not reported by provider" in output - assert "Cost (provider-reported):" not in output + assert "Total cost:" in output + assert "n/a" in output + assert "Pricing unknown for glm-5" in output class TestStatusBarWidthSource: diff --git a/tests/gateway/test_usage_command.py b/tests/gateway/test_usage_command.py index 66e81913a7a..9fbb80e3123 100644 --- a/tests/gateway/test_usage_command.py +++ b/tests/gateway/test_usage_command.py @@ -21,16 +21,11 @@ def _make_mock_agent(**overrides): "session_output_tokens": 10_000, "session_cache_read_tokens": 5_000, "session_cache_write_tokens": 2_000, - # Real provider-reported cost: None = nothing reported (the default). - "session_actual_cost_usd": None, } defaults.update(overrides) for k, v in defaults.items(): setattr(agent, k, v) - # No Nous credits headers seen unless a test overrides this. - agent.get_credits_spent_micros = MagicMock(return_value=None) - # Rate limit state rl = MagicMock() rl.has_data = True @@ -77,11 +72,13 @@ class TestUsageCachedAgent: @pytest.mark.asyncio async def test_cached_agent_shows_detailed_usage(self): - agent = _make_mock_agent(session_actual_cost_usd=0.1234) + agent = _make_mock_agent() runner = _make_runner(SK, cached_agent=agent) event = MagicMock() - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): + with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"), \ + patch("agent.usage_pricing.estimate_usage_cost") as mock_cost: + mock_cost.return_value = MagicMock(amount_usd=0.1234, status="estimated") result = await runner._handle_usage_command(event) assert "claude-sonnet-4.6" in result @@ -102,7 +99,9 @@ class TestUsageCachedAgent: runner = _make_runner(SK, agent=running, cached_agent=cached) event = MagicMock() - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): + with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"), \ + patch("agent.usage_pricing.estimate_usage_cost") as mock_cost: + mock_cost.return_value = MagicMock(amount_usd=None, status="unknown") result = await runner._handle_usage_command(event) assert "80,000" in result # running agent's total @@ -118,7 +117,9 @@ class TestUsageCachedAgent: runner._running_agents[SK] = _AGENT_PENDING_SENTINEL event = MagicMock() - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): + with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"), \ + patch("agent.usage_pricing.estimate_usage_cost") as mock_cost: + mock_cost.return_value = MagicMock(amount_usd=None, status="unknown") result = await runner._handle_usage_command(event) assert "claude-sonnet-4.6" in result @@ -152,7 +153,9 @@ class TestUsageCachedAgent: runner = _make_runner(SK, cached_agent=agent) event = MagicMock() - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): + with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"), \ + patch("agent.usage_pricing.estimate_usage_cost") as mock_cost: + mock_cost.return_value = MagicMock(amount_usd=None, status="unknown") result = await runner._handle_usage_command(event) assert "Cache read" not in result @@ -165,7 +168,9 @@ class TestUsageCachedAgent: runner = _make_runner(SK, cached_agent=agent) event = MagicMock() - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): + with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"), \ + patch("agent.usage_pricing.estimate_usage_cost") as mock_cost: + mock_cost.return_value = MagicMock(amount_usd=None, status="included") result = await runner._handle_usage_command(event) assert "Cost: included" in result @@ -194,7 +199,9 @@ class TestUsageAccountSection: "Session: 85% remaining (15% used)", ], ) - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): + with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"), \ + patch("agent.usage_pricing.estimate_usage_cost") as mock_cost: + mock_cost.return_value = MagicMock(amount_usd=None, status="included") result = await runner._handle_usage_command(event) assert "📊 **Session Token Usage**" in result @@ -249,42 +256,3 @@ class TestUsageAccountSection: assert account_call["kwargs"]["base_url"] == "https://chatgpt.com/backend-api/codex" assert "📊 **Session Info**" in result assert "📈 **Account limits**" in result - - -class TestUsageRealCostOnly: - """Cost lines are provider-REPORTED only — never estimated, never $0.00.""" - - @pytest.mark.asyncio - async def test_unreported_cost_renders_no_cost_line(self): - agent = _make_mock_agent() # openrouter, nothing reported - runner = _make_runner(SK, cached_agent=agent) - event = MagicMock() - - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): - result = await runner._handle_usage_command(event) - - assert "Cost:" not in result - assert "$0.00" not in result - - @pytest.mark.asyncio - async def test_nous_credits_delta_renders_as_cost(self): - agent = _make_mock_agent(provider="nous", model="Hermes-4.1-405B") - agent.get_credits_spent_micros = MagicMock(return_value=123_400) - runner = _make_runner(SK, cached_agent=agent) - event = MagicMock() - - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): - result = await runner._handle_usage_command(event) - - assert "$0.1234" in result - - @pytest.mark.asyncio - async def test_openrouter_reported_cost_renders(self): - agent = _make_mock_agent(session_actual_cost_usd=0.9876) - runner = _make_runner(SK, cached_agent=agent) - event = MagicMock() - - with patch("agent.rate_limit_tracker.format_rate_limit_compact", return_value="RPM: 50/60"): - result = await runner._handle_usage_command(event) - - assert "$0.9876" in result diff --git a/tests/providers/test_provider_profiles.py b/tests/providers/test_provider_profiles.py index 5f1364ff537..3eb23403048 100644 --- a/tests/providers/test_provider_profiles.py +++ b/tests/providers/test_provider_profiles.py @@ -110,9 +110,7 @@ class TestOpenRouterProfile: def test_extra_body_no_prefs(self): p = get_provider_profile("openrouter") body = p.build_extra_body() - # Usage accounting is always requested (real provider-reported cost); - # nothing else should appear without prefs/session. - assert body == {"usage": {"include": True}} + assert body == {} def test_pareto_min_coding_score_emitted_for_pareto_model(self): """min_coding_score → plugins block when model is openrouter/pareto-code.""" diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 423545e429f..e1e851912e4 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -2296,17 +2296,23 @@ def _get_usage(agent) -> dict: usage["context_max"] = ctx_max usage["context_percent"] = max(0, min(100, round(ctx_used / ctx_max * 100))) usage["compressions"] = getattr(comp, "compression_count", 0) or 0 - # Cost: provider-REPORTED only (OpenRouter usage.cost accumulator and/or - # the Nous x-nous-credits-* header delta). Never estimated. When nothing - # was reported `cost_usd` is ABSENT (not $0.00) and the TUI hides its - # cost segment — that's the intended UX. try: - from agent.usage_pricing import real_session_cost_usd + from agent.usage_pricing import CanonicalUsage, estimate_usage_cost - real_cost = real_session_cost_usd(agent) - if real_cost is not None: - usage["cost_usd"] = real_cost - usage["cost_status"] = "actual" + cost = estimate_usage_cost( + usage["model"], + CanonicalUsage( + input_tokens=usage["input"], + output_tokens=usage["output"], + cache_read_tokens=usage["cache_read"], + cache_write_tokens=usage["cache_write"], + ), + provider=getattr(agent, "provider", None), + base_url=getattr(agent, "base_url", None), + ) + usage["cost_status"] = cost.status + if cost.amount_usd is not None: + usage["cost_usd"] = float(cost.amount_usd) except Exception: pass # Dev-only live credits-spent readout (L0 usage-aware-credits). Gated on