mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
Revert "gateway: compact /usage with current-session per-model costs"
This reverts commit 364b93a4b9.
This commit is contained in:
parent
418ceaf8c1
commit
222126db1d
6 changed files with 7 additions and 416 deletions
|
|
@ -243,17 +243,6 @@ def nous_credits_lines(*, markdown: bool = False, timeout: float = 10.0) -> list
|
|||
renders from that fixture instead of the real portal (so the block + gauge are
|
||||
testable without a live account). Throwaway scaffolding.
|
||||
"""
|
||||
snapshot = _fetch_nous_credits_snapshot(timeout=timeout)
|
||||
return render_account_usage_lines(snapshot, markdown=markdown)
|
||||
|
||||
|
||||
def _fetch_nous_credits_snapshot(timeout: float = 10.0) -> Optional[AccountUsageSnapshot]:
|
||||
"""Auth-gate + portal fetch + snapshot build for the Nous credits block.
|
||||
|
||||
Shared by ``nous_credits_lines`` (full block) and
|
||||
``nous_credits_compact_line`` (one-liner). Honors the
|
||||
HERMES_DEV_CREDITS_FIXTURE dev override. Fail-open → None.
|
||||
"""
|
||||
# Dev fixture short-circuit — render /usage from the injected state, no portal.
|
||||
try:
|
||||
from agent.credits_tracker import dev_fixture_credits_state
|
||||
|
|
@ -262,16 +251,17 @@ def _fetch_nous_credits_snapshot(timeout: float = 10.0) -> Optional[AccountUsage
|
|||
except Exception:
|
||||
fixture = None
|
||||
if fixture is not None:
|
||||
return _snapshot_from_credits_state(fixture)
|
||||
snapshot = _snapshot_from_credits_state(fixture)
|
||||
return render_account_usage_lines(snapshot, markdown=markdown)
|
||||
|
||||
try:
|
||||
from hermes_cli.auth import get_provider_auth_state
|
||||
|
||||
tok = (get_provider_auth_state("nous") or {}).get("access_token")
|
||||
if not (isinstance(tok, str) and tok.strip()):
|
||||
return None
|
||||
return []
|
||||
except Exception:
|
||||
return None
|
||||
return []
|
||||
try:
|
||||
import concurrent.futures
|
||||
|
||||
|
|
@ -281,36 +271,13 @@ def _fetch_nous_credits_snapshot(timeout: float = 10.0) -> Optional[AccountUsage
|
|||
account = pool.submit(
|
||||
get_nous_portal_account_info, force_fresh=True
|
||||
).result(timeout=timeout)
|
||||
return build_nous_credits_snapshot(account)
|
||||
snapshot = build_nous_credits_snapshot(account)
|
||||
return render_account_usage_lines(snapshot, markdown=markdown)
|
||||
except Exception:
|
||||
# Fail-open (caller shows nothing), but leave a breadcrumb so a dead
|
||||
# /usage credits block is diagnosable in agent.log without a dev flag.
|
||||
logger.debug("credits ▸ /usage portal fetch/render failed (fail-open)", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def nous_credits_compact_line(*, timeout: float = 10.0) -> Optional[str]:
|
||||
"""One-line Nous credits summary for the compact /usage view, or None.
|
||||
|
||||
Condenses the snapshot's own detail strings (stable, locally-built
|
||||
formats) into ``Nous credits (Plan): Total usable: $X · Renews: …``.
|
||||
Same gating/fail-open semantics as ``nous_credits_lines``.
|
||||
"""
|
||||
snap = _fetch_nous_credits_snapshot(timeout=timeout)
|
||||
if snap is None or not snap.available:
|
||||
return None
|
||||
picked = [
|
||||
d for d in snap.details
|
||||
if d.startswith(("Total usable:", "Renews:", "Status:"))
|
||||
]
|
||||
if not picked:
|
||||
picked = [d for d in snap.details if not d.startswith("Manage / top up:")][:2]
|
||||
if not picked:
|
||||
return None
|
||||
title = snap.title
|
||||
if snap.plan:
|
||||
title += f" ({snap.plan})"
|
||||
return f"{title}: " + " · ".join(picked)
|
||||
return []
|
||||
|
||||
|
||||
def _snapshot_from_credits_state(state) -> Optional[AccountUsageSnapshot]:
|
||||
|
|
|
|||
|
|
@ -1695,36 +1695,6 @@ class SessionDB:
|
|||
|
||||
return self._execute_write(_do) or 0
|
||||
|
||||
def usage_totals(self, days: int = 30) -> Dict[str, Any]:
|
||||
"""Aggregate usage for sessions started in the last ``days``.
|
||||
|
||||
``reported_cost_usd`` sums only provider-REPORTED ``actual_cost_usd``
|
||||
(never estimates) and is None when no session in the window has a
|
||||
reported cost — callers must hide cost rather than print $0.00.
|
||||
"""
|
||||
cutoff = time.time() - days * 86400
|
||||
with self._lock:
|
||||
row = self._conn.execute(
|
||||
"""SELECT COUNT(*) AS sessions,
|
||||
COALESCE(SUM(input_tokens), 0)
|
||||
+ COALESCE(SUM(cache_read_tokens), 0)
|
||||
+ COALESCE(SUM(cache_write_tokens), 0) AS input_tokens,
|
||||
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
||||
COALESCE(SUM(api_call_count), 0) AS api_calls,
|
||||
SUM(actual_cost_usd) AS reported_cost_usd
|
||||
FROM sessions WHERE started_at >= ?""",
|
||||
(cutoff,),
|
||||
).fetchone()
|
||||
result = dict(row) if row else {}
|
||||
return {
|
||||
"days": days,
|
||||
"sessions": int(result.get("sessions") or 0),
|
||||
"input_tokens": int(result.get("input_tokens") or 0),
|
||||
"output_tokens": int(result.get("output_tokens") or 0),
|
||||
"api_calls": int(result.get("api_calls") or 0),
|
||||
"reported_cost_usd": result.get("reported_cost_usd"),
|
||||
}
|
||||
|
||||
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a session by ID."""
|
||||
with self._lock:
|
||||
|
|
|
|||
|
|
@ -184,37 +184,3 @@ class TestOpenRouterUsageParam:
|
|||
is_openrouter=False,
|
||||
)
|
||||
assert "usage" not in (kwargs.get("extra_body") or {})
|
||||
|
||||
|
||||
# ── nous_credits_compact_line — one-liner for the compact /usage page ───────
|
||||
|
||||
|
||||
class TestNousCreditsCompactLine:
|
||||
def test_condenses_snapshot_details(self, monkeypatch):
|
||||
import agent.account_usage as au
|
||||
|
||||
snap = au.AccountUsageSnapshot(
|
||||
provider="nous",
|
||||
source="portal-account",
|
||||
fetched_at=au._utc_now(),
|
||||
title="Nous credits",
|
||||
plan="Ultra",
|
||||
details=(
|
||||
"Subscription credits: $-0.79",
|
||||
"Top-up credits: $988.99",
|
||||
"Total usable: $988.99",
|
||||
"Renews: 2026-06-11T08:14:55.000Z",
|
||||
"Manage / top up: https://portal.nousresearch.com/billing",
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(au, "_fetch_nous_credits_snapshot", lambda timeout=10.0: snap)
|
||||
line = au.nous_credits_compact_line()
|
||||
assert line == (
|
||||
"Nous credits (Ultra): Total usable: $988.99 · Renews: 2026-06-11T08:14:55.000Z"
|
||||
)
|
||||
|
||||
def test_none_when_no_snapshot(self, monkeypatch):
|
||||
import agent.account_usage as au
|
||||
|
||||
monkeypatch.setattr(au, "_fetch_nous_credits_snapshot", lambda timeout=10.0: None)
|
||||
assert au.nous_credits_compact_line() is None
|
||||
|
|
|
|||
|
|
@ -158,37 +158,6 @@ class TestSessionLifecycle:
|
|||
assert session["api_call_count"] == 5
|
||||
assert session["input_tokens"] == 300
|
||||
|
||||
def test_update_token_counts_actual_cost_null_keeps_value(self, db):
|
||||
"""A NULL actual_cost_usd delta must not touch the stored REAL cost."""
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
db.update_token_counts("s1", input_tokens=100, actual_cost_usd=0.25)
|
||||
db.update_token_counts("s1", input_tokens=100, actual_cost_usd=None)
|
||||
db.update_token_counts("s1", input_tokens=100, actual_cost_usd=0.10)
|
||||
|
||||
session = db.get_session("s1")
|
||||
assert session["actual_cost_usd"] == pytest.approx(0.35)
|
||||
|
||||
def test_usage_totals_reported_cost_none_when_nothing_reported(self, db):
|
||||
"""usage_totals must distinguish 'no reported cost' (None) from $0."""
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
db.update_token_counts("s1", input_tokens=100, output_tokens=50, api_call_count=1)
|
||||
|
||||
totals = db.usage_totals(days=30)
|
||||
assert totals["sessions"] == 1
|
||||
assert totals["input_tokens"] == 100
|
||||
assert totals["output_tokens"] == 50
|
||||
assert totals["reported_cost_usd"] is None
|
||||
|
||||
def test_usage_totals_sums_reported_costs(self, db):
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
db.create_session(session_id="s2", source="tui")
|
||||
db.update_token_counts("s1", input_tokens=100, actual_cost_usd=0.20)
|
||||
db.update_token_counts("s2", input_tokens=300, actual_cost_usd=0.05)
|
||||
|
||||
totals = db.usage_totals(days=30)
|
||||
assert totals["sessions"] == 2
|
||||
assert totals["reported_cost_usd"] == pytest.approx(0.25)
|
||||
|
||||
def test_update_token_counts_backfills_model_when_null(self, db):
|
||||
db.create_session(session_id="s1", source="telegram")
|
||||
db.update_token_counts("s1", input_tokens=10, output_tokens=5, model="openai/gpt-5.4")
|
||||
|
|
|
|||
|
|
@ -7848,180 +7848,6 @@ def test_session_list_truncated_false_on_normal_paths(monkeypatch):
|
|||
assert filtered["result"]["truncated"] is False
|
||||
|
||||
|
||||
# ── /usage: compact in-process page with real-only costs ─────────────────────
|
||||
|
||||
|
||||
def _usage_agent(**overrides):
|
||||
"""SimpleNamespace agent with realistic session counters for /usage."""
|
||||
base = dict(
|
||||
model="anthropic/claude-sonnet-4.6",
|
||||
provider="openrouter",
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
session_input_tokens=35_000,
|
||||
session_output_tokens=10_000,
|
||||
session_cache_read_tokens=5_000,
|
||||
session_cache_write_tokens=2_000,
|
||||
session_reasoning_tokens=0,
|
||||
session_prompt_tokens=40_000,
|
||||
session_completion_tokens=10_000,
|
||||
session_total_tokens=50_000,
|
||||
session_api_calls=5,
|
||||
session_actual_cost_usd=None,
|
||||
session_model_usage={},
|
||||
context_compressor=None,
|
||||
)
|
||||
base.update(overrides)
|
||||
return types.SimpleNamespace(**base)
|
||||
|
||||
|
||||
def _mute_usage_externals(monkeypatch):
|
||||
import agent.account_usage as account_usage
|
||||
|
||||
monkeypatch.setattr(server, "_get_db", lambda: None)
|
||||
monkeypatch.setattr(account_usage, "nous_credits_compact_line", lambda **kw: None)
|
||||
|
||||
|
||||
def test_get_usage_cost_absent_when_provider_reports_nothing(monkeypatch):
|
||||
"""No estimation: even a model with known pricing gets NO cost_usd field."""
|
||||
agent = _usage_agent()
|
||||
usage = server._get_usage(agent)
|
||||
assert "cost_usd" not in usage
|
||||
assert usage["input"] == 35_000
|
||||
|
||||
|
||||
def test_get_usage_cost_present_when_provider_reported(monkeypatch):
|
||||
agent = _usage_agent(session_actual_cost_usd=0.4321)
|
||||
usage = server._get_usage(agent)
|
||||
assert usage["cost_usd"] == pytest.approx(0.4321)
|
||||
assert usage["cost_status"] == "actual"
|
||||
|
||||
|
||||
def test_get_usage_cost_from_nous_credits_delta(monkeypatch):
|
||||
agent = _usage_agent(provider="nous")
|
||||
agent.get_credits_spent_micros = lambda: 250_000 # $0.25 real header delta
|
||||
usage = server._get_usage(agent)
|
||||
assert usage["cost_usd"] == pytest.approx(0.25)
|
||||
|
||||
|
||||
def test_compact_usage_per_model_rows_and_real_cost(monkeypatch):
|
||||
_mute_usage_externals(monkeypatch)
|
||||
agent = _usage_agent(
|
||||
session_actual_cost_usd=0.42,
|
||||
session_model_usage={
|
||||
"anthropic/claude-sonnet-4.6": {
|
||||
"calls": 4, "input": 30_000, "output": 9_000,
|
||||
"cache_read": 5_000, "cache_write": 2_000, "cost_usd": 0.42,
|
||||
},
|
||||
"deepseek/deepseek-chat": {
|
||||
"calls": 1, "input": 5_000, "output": 1_000,
|
||||
"cache_read": 0, "cache_write": 0, "cost_usd": None,
|
||||
},
|
||||
},
|
||||
)
|
||||
text = server._compact_usage_text(_session(agent=agent))
|
||||
|
||||
assert "Session — anthropic/claude-sonnet-4.6 (openrouter)" in text
|
||||
sonnet_row = next(l for l in text.splitlines() if "claude-sonnet-4.6" in l and "reqs" in l)
|
||||
assert "reqs 4" in sonnet_row and "$0.4200" in sonnet_row
|
||||
deepseek_row = next(l for l in text.splitlines() if "deepseek-chat" in l)
|
||||
# Cost not reported for this model → no dollar figure on its row.
|
||||
assert "reqs 1" in deepseek_row and "$" not in deepseek_row
|
||||
assert "session cost: $0.4200 (provider-reported)" in text
|
||||
assert "/usage full" in text
|
||||
|
||||
|
||||
def test_compact_usage_absent_cost_never_renders_zero(monkeypatch):
|
||||
_mute_usage_externals(monkeypatch)
|
||||
agent = _usage_agent() # nothing reported
|
||||
text = server._compact_usage_text(_session(agent=agent))
|
||||
assert "session cost: not reported by provider" in text
|
||||
assert "$0.00" not in text
|
||||
|
||||
|
||||
def test_compact_usage_no_agent(monkeypatch):
|
||||
_mute_usage_externals(monkeypatch)
|
||||
text = server._compact_usage_text(_session(agent=None) | {"agent": None})
|
||||
assert "no API calls yet" in text
|
||||
|
||||
|
||||
def test_compact_usage_recent_summary_and_credits_line(monkeypatch):
|
||||
import agent.account_usage as account_usage
|
||||
|
||||
class _DB:
|
||||
def usage_totals(self, days=30):
|
||||
return {
|
||||
"days": 30, "sessions": 12, "input_tokens": 1_200_000,
|
||||
"output_tokens": 90_000, "api_calls": 64,
|
||||
"reported_cost_usd": 4.5678,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(server, "_get_db", lambda: _DB())
|
||||
monkeypatch.setattr(
|
||||
account_usage, "nous_credits_compact_line",
|
||||
lambda **kw: "Nous credits (Ultra): Total usable: $988.99 · Renews: 2026-06-11",
|
||||
)
|
||||
text = server._compact_usage_text(_session(agent=_usage_agent()))
|
||||
assert "Last 30d: 12 sessions" in text
|
||||
assert "reported cost $4.57" in text
|
||||
assert "Nous credits (Ultra)" in text
|
||||
|
||||
|
||||
def test_compact_usage_recent_summary_hides_unreported_cost(monkeypatch):
|
||||
_mute_usage_externals(monkeypatch)
|
||||
|
||||
class _DB:
|
||||
def usage_totals(self, days=30):
|
||||
return {
|
||||
"days": 30, "sessions": 3, "input_tokens": 10_000,
|
||||
"output_tokens": 2_000, "api_calls": 7,
|
||||
"reported_cost_usd": None,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(server, "_get_db", lambda: _DB())
|
||||
text = server._compact_usage_text(_session(agent=_usage_agent()))
|
||||
assert "Last 30d: 3 sessions" in text
|
||||
assert "reported cost" not in text
|
||||
|
||||
|
||||
def test_slash_exec_usage_is_answered_in_process(monkeypatch):
|
||||
"""/usage must not hit the slash worker (it has no live agent)."""
|
||||
_mute_usage_externals(monkeypatch)
|
||||
server._sessions["sid-usage"] = _session(agent=_usage_agent())
|
||||
try:
|
||||
resp = server.handle_request(
|
||||
{"id": "1", "method": "slash.exec",
|
||||
"params": {"session_id": "sid-usage", "command": "usage"}}
|
||||
)
|
||||
out = resp["result"]["output"]
|
||||
assert "Session — anthropic/claude-sonnet-4.6" in out
|
||||
# Worker untouched.
|
||||
assert server._sessions["sid-usage"]["slash_worker"] is None
|
||||
finally:
|
||||
server._sessions.pop("sid-usage", None)
|
||||
|
||||
|
||||
def test_slash_exec_usage_full_falls_through_to_worker(monkeypatch):
|
||||
ran = []
|
||||
|
||||
class _Worker:
|
||||
def run(self, cmd):
|
||||
ran.append(cmd)
|
||||
return "detailed legacy page"
|
||||
|
||||
sess = _session(agent=_usage_agent())
|
||||
sess["slash_worker"] = _Worker()
|
||||
server._sessions["sid-usage-full"] = sess
|
||||
try:
|
||||
monkeypatch.setattr(server, "_mirror_slash_side_effects", lambda *a: "")
|
||||
resp = server.handle_request(
|
||||
{"id": "1", "method": "slash.exec",
|
||||
"params": {"session_id": "sid-usage-full", "command": "usage full"}}
|
||||
)
|
||||
assert resp["result"]["output"] == "detailed legacy page"
|
||||
assert ran == ["usage full"]
|
||||
finally:
|
||||
server._sessions.pop("sid-usage-full", None)
|
||||
|
||||
def test_slash_worker_close_reaps_zombie_and_closes_fds():
|
||||
"""A hung worker is SIGKILLed, the zombie reaped, all pipes closed — once."""
|
||||
calls = {k: 0 for k in ("terminate", "kill", "wait", "stdin", "stdout", "stderr")}
|
||||
|
|
|
|||
|
|
@ -2321,102 +2321,6 @@ def _get_usage(agent) -> dict:
|
|||
return usage
|
||||
|
||||
|
||||
def _compact_usage_text(session: dict) -> str:
|
||||
"""Compact /usage page: current session per-model rows + a tight recent
|
||||
summary + a one-line Nous credits gauge.
|
||||
|
||||
Costs are provider-REPORTED only. When a provider reports nothing the
|
||||
cost is simply omitted (never rendered as $0.00). The detailed legacy
|
||||
page stays reachable via `/usage full` (slash-worker → CLI path).
|
||||
"""
|
||||
from agent.usage_pricing import format_token_count_compact as _fmt
|
||||
|
||||
agent = session.get("agent")
|
||||
lines: list[str] = []
|
||||
|
||||
calls = (getattr(agent, "session_api_calls", 0) or 0) if agent is not None else 0
|
||||
if agent is not None and calls > 0:
|
||||
u = _get_usage(agent)
|
||||
header = f"Session — {u['model']}"
|
||||
provider = getattr(agent, "provider", None)
|
||||
if provider:
|
||||
header += f" ({provider})"
|
||||
lines.append(header)
|
||||
|
||||
per_model = getattr(agent, "session_model_usage", None) or {}
|
||||
rows = list(per_model.items()) or [(
|
||||
u["model"],
|
||||
{
|
||||
"calls": u["calls"], "input": u["input"], "output": u["output"],
|
||||
"cache_read": u["cache_read"], "cache_write": u["cache_write"],
|
||||
"cost_usd": None,
|
||||
},
|
||||
)]
|
||||
name_w = max(len(name or "?") for name, _ in rows)
|
||||
for name, row in rows:
|
||||
cells = [
|
||||
f"{(name or '?'):<{name_w}}",
|
||||
f"reqs {row.get('calls', 0)}",
|
||||
f"in {_fmt(int(row.get('input', 0) or 0))}",
|
||||
f"out {_fmt(int(row.get('output', 0) or 0))}",
|
||||
]
|
||||
cache_read = int(row.get("cache_read", 0) or 0)
|
||||
if cache_read:
|
||||
cells.append(f"cache {_fmt(cache_read)}")
|
||||
cost = row.get("cost_usd")
|
||||
if cost is not None:
|
||||
cells.append(f"${cost:.4f}")
|
||||
lines.append(" " + " · ".join(cells))
|
||||
|
||||
ctx_pct = u.get("context_percent")
|
||||
tail = [f"total {_fmt(int(u['total'] or 0))} tokens", f"{u['calls']} calls"]
|
||||
if ctx_pct is not None:
|
||||
tail.append(f"context {ctx_pct}%")
|
||||
if u.get("compressions"):
|
||||
tail.append(f"compressions {u['compressions']}")
|
||||
lines.append(" " + " · ".join(tail))
|
||||
|
||||
cost_usd = u.get("cost_usd")
|
||||
if cost_usd is not None:
|
||||
lines.append(f" session cost: ${cost_usd:.4f} (provider-reported)")
|
||||
else:
|
||||
lines.append(" session cost: not reported by provider")
|
||||
else:
|
||||
lines.append("Session — no API calls yet")
|
||||
|
||||
# Tight recent summary from the session DB (real costs only).
|
||||
try:
|
||||
db = _get_db()
|
||||
totals = db.usage_totals(days=30) if db is not None else None
|
||||
except Exception:
|
||||
totals = None
|
||||
if totals and totals.get("sessions"):
|
||||
from agent.usage_pricing import format_token_count_compact as _fmt30
|
||||
|
||||
parts = [
|
||||
f"{totals['sessions']} sessions",
|
||||
f"in {_fmt30(totals['input_tokens'])}",
|
||||
f"out {_fmt30(totals['output_tokens'])}",
|
||||
]
|
||||
reported = totals.get("reported_cost_usd")
|
||||
if reported is not None:
|
||||
parts.append(f"reported cost ${float(reported):.2f}")
|
||||
lines.append("Last 30d: " + " · ".join(parts))
|
||||
|
||||
# Nous credits one-liner (account-level; independent of the live agent).
|
||||
try:
|
||||
from agent.account_usage import nous_credits_compact_line
|
||||
|
||||
credits_line = nous_credits_compact_line()
|
||||
except Exception:
|
||||
credits_line = None
|
||||
if credits_line:
|
||||
lines.append(credits_line)
|
||||
|
||||
lines.append("(/usage full — detailed page)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _probe_credentials(agent) -> str:
|
||||
"""Light credential check at session creation — returns warning or ''."""
|
||||
try:
|
||||
|
|
@ -9676,17 +9580,6 @@ def _(rid, params: dict) -> dict:
|
|||
except Exception as e:
|
||||
return _ok(rid, {"output": f"Plugin command error: {e}"})
|
||||
|
||||
# /usage — answered in-process from the LIVE agent's session counters.
|
||||
# The slash worker is a separate subprocess that resumes the session
|
||||
# WITHOUT an agent, so it can never see current-session tokens/costs
|
||||
# (it only printed the Nous credits block). `/usage full` still falls
|
||||
# through to the worker for the detailed CLI page.
|
||||
if _cmd_base == "usage" and _cmd_arg.strip().lower() not in {"full", "--full"}:
|
||||
try:
|
||||
return _ok(rid, {"output": _compact_usage_text(session)})
|
||||
except Exception:
|
||||
pass # fall through to the slash worker
|
||||
|
||||
worker = session.get("slash_worker")
|
||||
if not worker:
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue