mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-28 18:19:28 +00:00
fix: stop reporting cache-hit rate and cost across all UI surfaces
Cost estimates and cache read/write token reporting are unreliable on providers that don't surface cached_tokens (e.g. ollama-cloud, which doesn't implement prompt_tokens_details.cached_tokens), producing misleading near-zero 'cache hit' readouts and cost figures. Remove cost + cache-hit reporting from every user-facing surface; keep input/output/total token counts (provider-agnostic and accurate) and the Nous account billing UI (real account money, separate from per-conversation estimates). Surfaces: - CLI /usage + model-info: drop cost lines + cache read/write token lines - Gateway /usage + /model: drop cost + cache lines - tui_gateway/server.py: stop emitting cost_usd / cache_read in usage and subagent.complete payloads - TUI (Ink): drop cost from status bar (+ showCost plumbing), /usage panel, thinking rollup, agents overlay (incl. compare view); keep token counts - Desktop Command Center: drop cost stat, per-model cost, actual-cost hint Underlying estimate_usage_cost / format_cost / insights cost columns are left intact but no longer surfaced (display-only change, reversible).
This commit is contained in:
parent
3e99ec0ff9
commit
446b8e2392
15 changed files with 22 additions and 234 deletions
|
|
@ -455,20 +455,6 @@ function formatTokens(value: null | number | undefined): string {
|
||||||
return num.toLocaleString()
|
return num.toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCost(value: null | number | undefined): string {
|
|
||||||
const num = Number(value || 0)
|
|
||||||
|
|
||||||
if (num === 0) {
|
|
||||||
return '$0.00'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (num < 0.01) {
|
|
||||||
return '<$0.01'
|
|
||||||
}
|
|
||||||
|
|
||||||
return `$${num.toFixed(2)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatInteger(value: null | number | undefined): string {
|
function formatInteger(value: null | number | undefined): string {
|
||||||
return Number(value ?? 0).toLocaleString()
|
return Number(value ?? 0).toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
@ -525,18 +511,13 @@ function UsagePanel({ error, loading, onRefresh, period, usage }: UsagePanelProp
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-4 border-b border-(--ui-stroke-tertiary) pb-5 sm:grid-cols-4">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-4 border-b border-(--ui-stroke-tertiary) pb-5 sm:grid-cols-3">
|
||||||
<UsageStat label={cc.statSessions} value={formatInteger(totals.total_sessions)} />
|
<UsageStat label={cc.statSessions} value={formatInteger(totals.total_sessions)} />
|
||||||
<UsageStat label={cc.statApiCalls} value={formatInteger(totals.total_api_calls)} />
|
<UsageStat label={cc.statApiCalls} value={formatInteger(totals.total_api_calls)} />
|
||||||
<UsageStat
|
<UsageStat
|
||||||
label={cc.statTokens}
|
label={cc.statTokens}
|
||||||
value={`${formatTokens(totals.total_input)} / ${formatTokens(totals.total_output)}`}
|
value={`${formatTokens(totals.total_input)} / ${formatTokens(totals.total_output)}`}
|
||||||
/>
|
/>
|
||||||
<UsageStat
|
|
||||||
hint={totals.total_actual_cost > 0 ? cc.actualCost(formatCost(totals.total_actual_cost)) : undefined}
|
|
||||||
label={cc.statCost}
|
|
||||||
value={formatCost(totals.total_estimated_cost)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -596,7 +577,7 @@ function UsagePanel({ error, loading, onRefresh, period, usage }: UsagePanelProp
|
||||||
rows={byModel.slice(0, 6).map(entry => ({
|
rows={byModel.slice(0, 6).map(entry => ({
|
||||||
key: entry.model,
|
key: entry.model,
|
||||||
label: entry.model,
|
label: entry.model,
|
||||||
value: `${formatTokens((entry.input_tokens || 0) + (entry.output_tokens || 0))} · ${formatCost(entry.estimated_cost)}`
|
value: `${formatTokens((entry.input_tokens || 0) + (entry.output_tokens || 0))}`
|
||||||
}))}
|
}))}
|
||||||
title={cc.topModels}
|
title={cc.topModels}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
30
cli.py
30
cli.py
|
|
@ -7488,8 +7488,6 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
||||||
if mi:
|
if mi:
|
||||||
if mi.max_output:
|
if mi.max_output:
|
||||||
_cprint(f" Max output: {mi.max_output:,} tokens")
|
_cprint(f" Max output: {mi.max_output:,} tokens")
|
||||||
if mi.has_cost_data():
|
|
||||||
_cprint(f" Cost: {mi.format_cost()}")
|
|
||||||
_cprint(f" Capabilities: {mi.format_capabilities()}")
|
_cprint(f" Capabilities: {mi.format_capabilities()}")
|
||||||
|
|
||||||
cache_enabled = (
|
cache_enabled = (
|
||||||
|
|
@ -7796,8 +7794,6 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
||||||
if mi:
|
if mi:
|
||||||
if mi.max_output:
|
if mi.max_output:
|
||||||
_cprint(f" Max output: {mi.max_output:,} tokens")
|
_cprint(f" Max output: {mi.max_output:,} tokens")
|
||||||
if mi.has_cost_data():
|
|
||||||
_cprint(f" Cost: {mi.format_cost()}")
|
|
||||||
_cprint(f" Capabilities: {mi.format_capabilities()}")
|
_cprint(f" Capabilities: {mi.format_capabilities()}")
|
||||||
|
|
||||||
# Cache notice
|
# Cache notice
|
||||||
|
|
@ -9113,8 +9109,6 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
||||||
# ── Session token usage ─────────────────────────────────────
|
# ── Session token usage ─────────────────────────────────────
|
||||||
input_tokens = getattr(agent, "session_input_tokens", 0) or 0
|
input_tokens = getattr(agent, "session_input_tokens", 0) or 0
|
||||||
output_tokens = getattr(agent, "session_output_tokens", 0) or 0
|
output_tokens = getattr(agent, "session_output_tokens", 0) or 0
|
||||||
cache_read_tokens = getattr(agent, "session_cache_read_tokens", 0) or 0
|
|
||||||
cache_write_tokens = getattr(agent, "session_cache_write_tokens", 0) or 0
|
|
||||||
reasoning_tokens = getattr(agent, "session_reasoning_tokens", 0) or 0
|
reasoning_tokens = getattr(agent, "session_reasoning_tokens", 0) or 0
|
||||||
prompt = agent.session_prompt_tokens
|
prompt = agent.session_prompt_tokens
|
||||||
completion = agent.session_completion_tokens
|
completion = agent.session_completion_tokens
|
||||||
|
|
@ -9127,25 +9121,12 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
||||||
compressions = compressor.compression_count
|
compressions = compressor.compression_count
|
||||||
|
|
||||||
msg_count = len(self.conversation_history)
|
msg_count = len(self.conversation_history)
|
||||||
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),
|
|
||||||
)
|
|
||||||
elapsed = format_duration_compact((datetime.now() - self.session_start).total_seconds())
|
elapsed = format_duration_compact((datetime.now() - self.session_start).total_seconds())
|
||||||
|
|
||||||
print(" 📊 Session Token Usage")
|
print(" 📊 Session Token Usage")
|
||||||
print(f" {'─' * 40}")
|
print(f" {'─' * 40}")
|
||||||
print(f" Model: {agent.model}")
|
print(f" Model: {agent.model}")
|
||||||
print(f" Input tokens: {input_tokens:>10,}")
|
print(f" Input tokens: {input_tokens:>10,}")
|
||||||
print(f" Cache read tokens: {cache_read_tokens:>10,}")
|
|
||||||
print(f" Cache write tokens: {cache_write_tokens:>10,}")
|
|
||||||
print(f" Output tokens: {output_tokens:>10,}")
|
print(f" Output tokens: {output_tokens:>10,}")
|
||||||
if reasoning_tokens:
|
if reasoning_tokens:
|
||||||
print(f" ↳ Reasoning (subset): {reasoning_tokens:>10,}")
|
print(f" ↳ Reasoning (subset): {reasoning_tokens:>10,}")
|
||||||
|
|
@ -9154,21 +9135,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
||||||
print(f" Total tokens: {total:>10,}")
|
print(f" Total tokens: {total:>10,}")
|
||||||
print(f" API calls: {calls:>10,}")
|
print(f" API calls: {calls:>10,}")
|
||||||
print(f" Session duration: {elapsed:>10}")
|
print(f" Session duration: {elapsed:>10}")
|
||||||
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" Total cost: {'n/a':>10}")
|
|
||||||
print(f" {'─' * 40}")
|
print(f" {'─' * 40}")
|
||||||
print(f" Current context: {last_prompt:,} / {ctx_len:,} ({pct:.0f}%)")
|
print(f" Current context: {last_prompt:,} / {ctx_len:,} ({pct:.0f}%)")
|
||||||
print(f" Messages: {msg_count}")
|
print(f" Messages: {msg_count}")
|
||||||
print(f" Compressions: {compressions}")
|
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
|
# Account limits -- fetched off-thread with a hard timeout so slow
|
||||||
# provider APIs don't hang the prompt.
|
# provider APIs don't hang the prompt.
|
||||||
|
|
|
||||||
|
|
@ -1391,8 +1391,6 @@ class GatewaySlashCommandsMixin:
|
||||||
if mi:
|
if mi:
|
||||||
if mi.max_output:
|
if mi.max_output:
|
||||||
lines.append(t("gateway.model.max_output_label", tokens=f"{mi.max_output:,}"))
|
lines.append(t("gateway.model.max_output_label", tokens=f"{mi.max_output:,}"))
|
||||||
if mi.has_cost_data():
|
|
||||||
lines.append(t("gateway.model.cost_label", cost=mi.format_cost()))
|
|
||||||
lines.append(t("gateway.model.capabilities_label", capabilities=mi.format_capabilities()))
|
lines.append(t("gateway.model.capabilities_label", capabilities=mi.format_capabilities()))
|
||||||
if result.warning_message:
|
if result.warning_message:
|
||||||
lines.append(t("gateway.model.warning_prefix", warning=result.warning_message))
|
lines.append(t("gateway.model.warning_prefix", warning=result.warning_message))
|
||||||
|
|
@ -1628,8 +1626,6 @@ class GatewaySlashCommandsMixin:
|
||||||
if mi:
|
if mi:
|
||||||
if mi.max_output:
|
if mi.max_output:
|
||||||
lines.append(t("gateway.model.max_output_label", tokens=f"{mi.max_output:,}"))
|
lines.append(t("gateway.model.max_output_label", tokens=f"{mi.max_output:,}"))
|
||||||
if mi.has_cost_data():
|
|
||||||
lines.append(t("gateway.model.cost_label", cost=mi.format_cost()))
|
|
||||||
lines.append(t("gateway.model.capabilities_label", capabilities=mi.format_capabilities()))
|
lines.append(t("gateway.model.capabilities_label", capabilities=mi.format_capabilities()))
|
||||||
|
|
||||||
# Cache notice
|
# Cache notice
|
||||||
|
|
@ -3514,42 +3510,14 @@ class GatewaySlashCommandsMixin:
|
||||||
# Session token usage — detailed breakdown matching CLI
|
# Session token usage — detailed breakdown matching CLI
|
||||||
input_tokens = getattr(agent, "session_input_tokens", 0) or 0
|
input_tokens = getattr(agent, "session_input_tokens", 0) or 0
|
||||||
output_tokens = getattr(agent, "session_output_tokens", 0) or 0
|
output_tokens = getattr(agent, "session_output_tokens", 0) or 0
|
||||||
cache_read = getattr(agent, "session_cache_read_tokens", 0) or 0
|
|
||||||
cache_write = getattr(agent, "session_cache_write_tokens", 0) or 0
|
|
||||||
|
|
||||||
lines.append(t("gateway.usage.header_session"))
|
lines.append(t("gateway.usage.header_session"))
|
||||||
lines.append(t("gateway.usage.label_model", model=agent.model))
|
lines.append(t("gateway.usage.label_model", model=agent.model))
|
||||||
lines.append(t("gateway.usage.label_input_tokens", count=f"{input_tokens:,}"))
|
lines.append(t("gateway.usage.label_input_tokens", count=f"{input_tokens:,}"))
|
||||||
if cache_read:
|
|
||||||
lines.append(t("gateway.usage.label_cache_read", count=f"{cache_read:,}"))
|
|
||||||
if cache_write:
|
|
||||||
lines.append(t("gateway.usage.label_cache_write", count=f"{cache_write:,}"))
|
|
||||||
lines.append(t("gateway.usage.label_output_tokens", count=f"{output_tokens:,}"))
|
lines.append(t("gateway.usage.label_output_tokens", count=f"{output_tokens:,}"))
|
||||||
lines.append(t("gateway.usage.label_total", count=f"{agent.session_total_tokens:,}"))
|
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))
|
lines.append(t("gateway.usage.label_api_calls", count=agent.session_api_calls))
|
||||||
|
|
||||||
# Cost estimation
|
|
||||||
try:
|
|
||||||
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
|
|
||||||
|
|
||||||
# Context window and compressions
|
# Context window and compressions
|
||||||
ctx = agent.context_compressor
|
ctx = agent.context_compressor
|
||||||
if ctx.last_prompt_tokens:
|
if ctx.last_prompt_tokens:
|
||||||
|
|
|
||||||
|
|
@ -500,7 +500,7 @@ class TestCLIStatusBar:
|
||||||
|
|
||||||
|
|
||||||
class TestCLIUsageReport:
|
class TestCLIUsageReport:
|
||||||
def test_show_usage_includes_estimated_cost(self, capsys):
|
def test_show_usage_omits_cost_reporting(self, capsys):
|
||||||
cli_obj = _attach_agent(
|
cli_obj = _attach_agent(
|
||||||
_make_cli(),
|
_make_cli(),
|
||||||
prompt_tokens=10_230,
|
prompt_tokens=10_230,
|
||||||
|
|
@ -516,52 +516,19 @@ class TestCLIUsageReport:
|
||||||
cli_obj._show_usage()
|
cli_obj._show_usage()
|
||||||
output = capsys.readouterr().out
|
output = capsys.readouterr().out
|
||||||
|
|
||||||
|
# Token counts and session metadata still shown.
|
||||||
assert "Model:" in output
|
assert "Model:" in output
|
||||||
assert "Cost status:" in output
|
assert "Input tokens:" in output
|
||||||
assert "Cost source:" in output
|
assert "Output tokens:" in output
|
||||||
assert "Total cost:" in output
|
assert "Total tokens:" in output
|
||||||
assert "$" in output
|
|
||||||
assert "0.064" in output
|
|
||||||
assert "Session duration:" in output
|
assert "Session duration:" in output
|
||||||
assert "Compressions:" in output
|
assert "Compressions:" in output
|
||||||
|
# Cost and cache-hit reporting is removed everywhere.
|
||||||
def test_show_usage_marks_unknown_pricing(self, capsys):
|
assert "Total cost:" not in output
|
||||||
cli_obj = _attach_agent(
|
assert "Cost status:" not in output
|
||||||
_make_cli(model="local/my-custom-model"),
|
assert "Cost source:" not in output
|
||||||
prompt_tokens=1_000,
|
assert "Cache read tokens:" not in output
|
||||||
completion_tokens=500,
|
assert "Cache write tokens:" not in output
|
||||||
total_tokens=1_500,
|
|
||||||
api_calls=1,
|
|
||||||
context_tokens=1_000,
|
|
||||||
context_length=32_000,
|
|
||||||
)
|
|
||||||
cli_obj.verbose = False
|
|
||||||
|
|
||||||
cli_obj._show_usage()
|
|
||||||
output = capsys.readouterr().out
|
|
||||||
|
|
||||||
assert "Total cost:" in output
|
|
||||||
assert "n/a" in output
|
|
||||||
assert "Pricing unknown for local/my-custom-model" in output
|
|
||||||
|
|
||||||
def test_zero_priced_provider_models_stay_unknown(self, capsys):
|
|
||||||
cli_obj = _attach_agent(
|
|
||||||
_make_cli(model="glm-5"),
|
|
||||||
prompt_tokens=1_000,
|
|
||||||
completion_tokens=500,
|
|
||||||
total_tokens=1_500,
|
|
||||||
api_calls=1,
|
|
||||||
context_tokens=1_000,
|
|
||||||
context_length=32_000,
|
|
||||||
)
|
|
||||||
cli_obj.verbose = False
|
|
||||||
|
|
||||||
cli_obj._show_usage()
|
|
||||||
output = capsys.readouterr().out
|
|
||||||
|
|
||||||
assert "Total cost:" in output
|
|
||||||
assert "n/a" in output
|
|
||||||
assert "Pricing unknown for glm-5" in output
|
|
||||||
|
|
||||||
|
|
||||||
class TestStatusBarWidthSource:
|
class TestStatusBarWidthSource:
|
||||||
|
|
|
||||||
|
|
@ -76,20 +76,20 @@ class TestUsageCachedAgent:
|
||||||
runner = _make_runner(SK, cached_agent=agent)
|
runner = _make_runner(SK, cached_agent=agent)
|
||||||
event = MagicMock()
|
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)
|
result = await runner._handle_usage_command(event)
|
||||||
|
|
||||||
assert "claude-sonnet-4.6" in result
|
assert "claude-sonnet-4.6" in result
|
||||||
assert "35,000" in result # input tokens
|
assert "35,000" in result # input tokens
|
||||||
assert "10,000" in result # output tokens
|
assert "10,000" in result # output tokens
|
||||||
assert "5,000" in result # cache read
|
|
||||||
assert "2,000" in result # cache write
|
|
||||||
assert "50,000" in result # total
|
assert "50,000" in result # total
|
||||||
assert "$0.1234" in result
|
|
||||||
assert "30,000" in result # context
|
assert "30,000" in result # context
|
||||||
assert "Compressions: 1" in result
|
assert "Compressions: 1" in result
|
||||||
|
# Cost and cache-hit reporting is removed everywhere.
|
||||||
|
assert "$" not in result
|
||||||
|
assert "Cache read" not in result
|
||||||
|
assert "Cache write" not in result
|
||||||
|
assert "Cost" not in result
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_running_agent_preferred_over_cache(self):
|
async def test_running_agent_preferred_over_cache(self):
|
||||||
|
|
@ -161,20 +161,6 @@ class TestUsageCachedAgent:
|
||||||
assert "Cache read" not in result
|
assert "Cache read" not in result
|
||||||
assert "Cache write" not in result
|
assert "Cache write" not in result
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_cost_included_status(self):
|
|
||||||
"""Subscription-included providers show 'included' instead of dollar amount."""
|
|
||||||
agent = _make_mock_agent(provider="openai-codex")
|
|
||||||
runner = _make_runner(SK, cached_agent=agent)
|
|
||||||
event = MagicMock()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class TestUsageAccountSection:
|
class TestUsageAccountSection:
|
||||||
"""Account-limits section appended to /usage output (PR #2486)."""
|
"""Account-limits section appended to /usage output (PR #2486)."""
|
||||||
|
|
|
||||||
|
|
@ -2790,8 +2790,6 @@ def _get_usage(agent) -> dict:
|
||||||
"model": getattr(agent, "model", "") or "",
|
"model": getattr(agent, "model", "") or "",
|
||||||
"input": g("session_input_tokens", "session_prompt_tokens"),
|
"input": g("session_input_tokens", "session_prompt_tokens"),
|
||||||
"output": g("session_output_tokens", "session_completion_tokens"),
|
"output": g("session_output_tokens", "session_completion_tokens"),
|
||||||
"cache_read": g("session_cache_read_tokens"),
|
|
||||||
"cache_write": g("session_cache_write_tokens"),
|
|
||||||
"reasoning": g("session_reasoning_tokens"),
|
"reasoning": g("session_reasoning_tokens"),
|
||||||
"prompt": g("session_prompt_tokens"),
|
"prompt": g("session_prompt_tokens"),
|
||||||
"completion": g("session_completion_tokens"),
|
"completion": g("session_completion_tokens"),
|
||||||
|
|
@ -2815,25 +2813,6 @@ def _get_usage(agent) -> dict:
|
||||||
usage["active_subagents"] = _async_active_count()
|
usage["active_subagents"] = _async_active_count()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
|
||||||
from agent.usage_pricing import CanonicalUsage, estimate_usage_cost
|
|
||||||
|
|
||||||
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
|
# Dev-only live credits-spent readout (L0 usage-aware-credits). Gated on
|
||||||
# HERMES_DEV_CREDITS so the payload stays clean when the flag is off.
|
# HERMES_DEV_CREDITS so the payload stays clean when the flag is off.
|
||||||
if is_truthy_value(os.environ.get("HERMES_DEV_CREDITS")):
|
if is_truthy_value(os.environ.get("HERMES_DEV_CREDITS")):
|
||||||
|
|
@ -3289,11 +3268,6 @@ def _on_tool_progress(
|
||||||
payload[int_key] = int(val)
|
payload[int_key] = int(val)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
if _kwargs.get("cost_usd") is not None:
|
|
||||||
try:
|
|
||||||
payload["cost_usd"] = float(_kwargs["cost_usd"])
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
if _kwargs.get("files_read"):
|
if _kwargs.get("files_read"):
|
||||||
payload["files_read"] = [str(p) for p in _kwargs["files_read"]]
|
payload["files_read"] = [str(p) for p in _kwargs["files_read"]]
|
||||||
if _kwargs.get("files_written"):
|
if _kwargs.get("files_written"):
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,6 @@ export interface UiState {
|
||||||
|
|
||||||
sections: SectionVisibility
|
sections: SectionVisibility
|
||||||
sessionTitle: string
|
sessionTitle: string
|
||||||
showCost: boolean
|
|
||||||
showReasoning: boolean
|
showReasoning: boolean
|
||||||
indicatorStyle: IndicatorStyle
|
indicatorStyle: IndicatorStyle
|
||||||
sid: null | string
|
sid: null | string
|
||||||
|
|
|
||||||
|
|
@ -593,22 +593,15 @@ export const sessionCommands: SlashCommand[] = [
|
||||||
}
|
}
|
||||||
|
|
||||||
const f = (v: number | undefined) => (v ?? 0).toLocaleString()
|
const f = (v: number | undefined) => (v ?? 0).toLocaleString()
|
||||||
const cost = r.cost_usd != null ? `${r.cost_status === 'estimated' ? '~' : ''}$${r.cost_usd.toFixed(4)}` : null
|
|
||||||
|
|
||||||
const rows: [string, string][] = [
|
const rows: [string, string][] = [
|
||||||
['Model', r.model ?? ''],
|
['Model', r.model ?? ''],
|
||||||
['Input tokens', f(r.input)],
|
['Input tokens', f(r.input)],
|
||||||
['Cache read tokens', f(r.cache_read)],
|
|
||||||
['Cache write tokens', f(r.cache_write)],
|
|
||||||
['Output tokens', f(r.output)],
|
['Output tokens', f(r.output)],
|
||||||
['Total tokens', f(r.total)],
|
['Total tokens', f(r.total)],
|
||||||
['API calls', f(r.calls)]
|
['API calls', f(r.calls)]
|
||||||
]
|
]
|
||||||
|
|
||||||
if (cost) {
|
|
||||||
rows.push(['Cost', cost])
|
|
||||||
}
|
|
||||||
|
|
||||||
const sections: PanelSection[] = [{ rows }]
|
const sections: PanelSection[] = [{ rows }]
|
||||||
|
|
||||||
if (r.context_max) {
|
if (r.context_max) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ const buildUiState = (): UiState => ({
|
||||||
pasteCollapseChars: 2000,
|
pasteCollapseChars: 2000,
|
||||||
sections: {},
|
sections: {},
|
||||||
sessionTitle: '',
|
sessionTitle: '',
|
||||||
showCost: false,
|
|
||||||
showReasoning: false,
|
showReasoning: false,
|
||||||
sid: null,
|
sid: null,
|
||||||
status: 'summoning hermes…',
|
status: 'summoning hermes…',
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,6 @@ export const applyDisplay = (
|
||||||
pasteCollapseLines: _pasteCollapseLinesFromConfig(cfg),
|
pasteCollapseLines: _pasteCollapseLinesFromConfig(cfg),
|
||||||
pasteCollapseChars: _pasteCollapseCharsFromConfig(cfg),
|
pasteCollapseChars: _pasteCollapseCharsFromConfig(cfg),
|
||||||
sections: resolveSections(d.sections),
|
sections: resolveSections(d.sections),
|
||||||
showCost: !!d.show_cost,
|
|
||||||
showReasoning: !!d.show_reasoning,
|
showReasoning: !!d.show_reasoning,
|
||||||
statusBar: normalizeStatusBar(d.tui_statusbar),
|
statusBar: normalizeStatusBar(d.tui_statusbar),
|
||||||
streaming: d.streaming !== false
|
streaming: d.streaming !== false
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import {
|
||||||
buildSubagentTree,
|
buildSubagentTree,
|
||||||
descendantIds,
|
descendantIds,
|
||||||
flattenTree,
|
flattenTree,
|
||||||
fmtCost,
|
|
||||||
fmtDuration,
|
fmtDuration,
|
||||||
fmtTokens,
|
fmtTokens,
|
||||||
formatSummary,
|
formatSummary,
|
||||||
|
|
@ -407,8 +406,6 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||||
const outputTokens = item.outputTokens ?? 0
|
const outputTokens = item.outputTokens ?? 0
|
||||||
const localTokens = inputTokens + outputTokens
|
const localTokens = inputTokens + outputTokens
|
||||||
const subtreeTokens = agg.inputTokens + agg.outputTokens - localTokens
|
const subtreeTokens = agg.inputTokens + agg.outputTokens - localTokens
|
||||||
const localCost = item.costUsd ?? 0
|
|
||||||
const subtreeCost = agg.costUsd - localCost
|
|
||||||
|
|
||||||
const filesRead = item.filesRead ?? []
|
const filesRead = item.filesRead ?? []
|
||||||
const filesWritten = item.filesWritten ?? []
|
const filesWritten = item.filesWritten ?? []
|
||||||
|
|
@ -442,7 +439,7 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||||
{item.apiCalls ? <Field name="api calls" t={t} value={String(item.apiCalls)} /> : null}
|
{item.apiCalls ? <Field name="api calls" t={t} value={String(item.apiCalls)} /> : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{localTokens > 0 || localCost > 0 ? (
|
{localTokens > 0 ? (
|
||||||
<OverlaySection defaultOpen t={t} title="Budget">
|
<OverlaySection defaultOpen t={t} title="Budget">
|
||||||
{localTokens > 0 ? (
|
{localTokens > 0 ? (
|
||||||
<Field
|
<Field
|
||||||
|
|
@ -457,19 +454,6 @@ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme })
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{localCost > 0 ? (
|
|
||||||
<Field
|
|
||||||
name="cost"
|
|
||||||
t={t}
|
|
||||||
value={
|
|
||||||
<>
|
|
||||||
{fmtCost(localCost)}
|
|
||||||
{subtreeCost >= 0.01 ? ` · subtree +${fmtCost(subtreeCost)}` : ''}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{subtreeTokens > 0 ? <Field name="subtree tokens" t={t} value={`+${fmtTokens(subtreeTokens)}`} /> : null}
|
{subtreeTokens > 0 ? <Field name="subtree tokens" t={t} value={`+${fmtTokens(subtreeTokens)}`} /> : null}
|
||||||
</OverlaySection>
|
</OverlaySection>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -650,7 +634,6 @@ function DiffView({
|
||||||
|
|
||||||
const round = (n: number) => String(Math.round(n))
|
const round = (n: number) => String(Math.round(n))
|
||||||
const sumTokens = (x: typeof aTotals) => x.inputTokens + x.outputTokens
|
const sumTokens = (x: typeof aTotals) => x.inputTokens + x.outputTokens
|
||||||
const dollars = (n: number) => fmtCost(n) || '$0.00'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" flexGrow={1} paddingX={1} paddingY={1}>
|
<Box flexDirection="column" flexGrow={1} paddingX={1} paddingY={1}>
|
||||||
|
|
@ -683,7 +666,6 @@ function DiffView({
|
||||||
{diffMetricLine('duration', aTotals.totalDuration, bTotals.totalDuration, n => `${n.toFixed(1)}s`)}
|
{diffMetricLine('duration', aTotals.totalDuration, bTotals.totalDuration, n => `${n.toFixed(1)}s`)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={t.color.text}>{diffMetricLine('tokens', sumTokens(aTotals), sumTokens(bTotals), fmtTokens)}</Text>
|
<Text color={t.color.text}>{diffMetricLine('tokens', sumTokens(aTotals), sumTokens(bTotals), fmtTokens)}</Text>
|
||||||
<Text color={t.color.text}>{diffMetricLine('cost', aTotals.costUsd, bTotals.costUsd, dollars)}</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,6 @@ export interface StatusBarSegments {
|
||||||
bg: boolean
|
bg: boolean
|
||||||
compactCtx: boolean
|
compactCtx: boolean
|
||||||
compressions: boolean
|
compressions: boolean
|
||||||
cost: boolean
|
|
||||||
duration: boolean
|
duration: boolean
|
||||||
subagents: boolean
|
subagents: boolean
|
||||||
voice: boolean
|
voice: boolean
|
||||||
|
|
@ -264,8 +263,7 @@ export function statusBarSegments(cols: number): StatusBarSegments {
|
||||||
compressions: w >= 80,
|
compressions: w >= 80,
|
||||||
voice: w >= 84,
|
voice: w >= 84,
|
||||||
bg: w >= 88,
|
bg: w >= 88,
|
||||||
subagents: w >= 92,
|
subagents: w >= 92
|
||||||
cost: w >= 96
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -420,7 +418,6 @@ export function StatusRule({
|
||||||
lastTurnEndedAt,
|
lastTurnEndedAt,
|
||||||
liveSessionCount,
|
liveSessionCount,
|
||||||
sessionStartedAt,
|
sessionStartedAt,
|
||||||
showCost,
|
|
||||||
turnStartedAt,
|
turnStartedAt,
|
||||||
voiceLabel,
|
voiceLabel,
|
||||||
onSessionCountClick,
|
onSessionCountClick,
|
||||||
|
|
@ -494,7 +491,6 @@ export function StatusRule({
|
||||||
|
|
||||||
const sessionCountText = liveSessionCount > 0 ? statusSessionCountLabel(liveSessionCount) : ''
|
const sessionCountText = liveSessionCount > 0 ? statusSessionCountLabel(liveSessionCount) : ''
|
||||||
const compressions = typeof usage.compressions === 'number' ? usage.compressions : 0
|
const compressions = typeof usage.compressions === 'number' ? usage.compressions : 0
|
||||||
const costText = typeof usage.cost_usd === 'number' ? `$${usage.cost_usd.toFixed(4)}` : ''
|
|
||||||
// Dev-only readout (HERMES_DEV_CREDITS). The server omits the key entirely unless the
|
// Dev-only readout (HERMES_DEV_CREDITS). The server omits the key entirely unless the
|
||||||
// flag is on, so this segment self-hides for normal users. micros→cents is allowed money
|
// flag is on, so this segment self-hides for normal users. micros→cents is allowed money
|
||||||
// math (display formatting) — never parseFloat a *_usd. Signed: a mid-session top-up that
|
// math (display formatting) — never parseFloat a *_usd. Signed: a mid-session top-up that
|
||||||
|
|
@ -516,8 +512,7 @@ export function StatusRule({
|
||||||
const showBg = segs.bg && bgCount > 0 && fits(SEP + stringWidth(`${bgCount} bg`))
|
const showBg = segs.bg && bgCount > 0 && fits(SEP + stringWidth(`${bgCount} bg`))
|
||||||
const subagentCount = typeof usage.active_subagents === 'number' ? usage.active_subagents : 0
|
const subagentCount = typeof usage.active_subagents === 'number' ? usage.active_subagents : 0
|
||||||
const showSubagents = segs.subagents && subagentCount > 0 && fits(SEP + stringWidth(`⛓ ${subagentCount}`))
|
const showSubagents = segs.subagents && subagentCount > 0 && fits(SEP + stringWidth(`⛓ ${subagentCount}`))
|
||||||
const showCostSeg = segs.cost && showCost && !!costText && fits(SEP + stringWidth(costText))
|
// Dev-gated readout (HERMES_DEV_CREDITS), lowest priority,
|
||||||
// No segs flag / no showCost coupling — it's a server-gated dev readout, lowest priority,
|
|
||||||
// so it consumes tail budget LAST and drops first on a narrow terminal.
|
// so it consumes tail budget LAST and drops first on a narrow terminal.
|
||||||
const showDevCredits = !!devCreditsText && fits(SEP + stringWidth(devCreditsText))
|
const showDevCredits = !!devCreditsText && fits(SEP + stringWidth(devCreditsText))
|
||||||
|
|
||||||
|
|
@ -629,12 +624,6 @@ export function StatusRule({
|
||||||
⛓ {subagentCount}
|
⛓ {subagentCount}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{showCostSeg ? (
|
|
||||||
<Text color={t.color.muted} wrap="truncate-end">
|
|
||||||
{' │ '}
|
|
||||||
{costText}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
{showDevCredits ? (
|
{showDevCredits ? (
|
||||||
<Text color={t.color.accent} wrap="truncate-end">
|
<Text color={t.color.accent} wrap="truncate-end">
|
||||||
{' │ '}
|
{' │ '}
|
||||||
|
|
@ -772,7 +761,6 @@ interface StatusRuleProps {
|
||||||
indicatorStyle?: IndicatorStyle
|
indicatorStyle?: IndicatorStyle
|
||||||
notice?: Notice | null
|
notice?: Notice | null
|
||||||
sessionStartedAt?: null | number
|
sessionStartedAt?: null | number
|
||||||
showCost: boolean
|
|
||||||
status: string
|
status: string
|
||||||
statusColor: string
|
statusColor: string
|
||||||
t: Theme
|
t: Theme
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,6 @@ const StatusRulePane = memo(function StatusRulePane({
|
||||||
notice={ui.notice}
|
notice={ui.notice}
|
||||||
onSessionCountClick={() => patchOverlayState({ sessions: true })}
|
onSessionCountClick={() => patchOverlayState({ sessions: true })}
|
||||||
sessionStartedAt={status.sessionStartedAt}
|
sessionStartedAt={status.sessionStartedAt}
|
||||||
showCost={ui.showCost}
|
|
||||||
status={ui.status}
|
status={ui.status}
|
||||||
statusColor={status.statusColor}
|
statusColor={status.statusColor}
|
||||||
t={ui.theme}
|
t={ui.theme}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { THINKING_COT_MAX } from '../config/limits.js'
|
||||||
import { sectionMode } from '../domain/details.js'
|
import { sectionMode } from '../domain/details.js'
|
||||||
import {
|
import {
|
||||||
buildSubagentTree,
|
buildSubagentTree,
|
||||||
fmtCost,
|
|
||||||
fmtTokens,
|
fmtTokens,
|
||||||
formatSummary as formatSpawnSummary,
|
formatSummary as formatSpawnSummary,
|
||||||
hotnessBucket,
|
hotnessBucket,
|
||||||
|
|
@ -361,12 +360,6 @@ function SubagentAccordion({
|
||||||
rollupBits.push(`${fmtTokens(localTokens)} tok`)
|
rollupBits.push(`${fmtTokens(localTokens)} tok`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const localCost = item.costUsd ?? 0
|
|
||||||
|
|
||||||
if (localCost > 0) {
|
|
||||||
rollupBits.push(fmtCost(localCost))
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesLocal = (item.filesWritten?.length ?? 0) + (item.filesRead?.length ?? 0)
|
const filesLocal = (item.filesWritten?.length ?? 0) + (item.filesRead?.length ?? 0)
|
||||||
|
|
||||||
if (filesLocal > 0) {
|
if (filesLocal > 0) {
|
||||||
|
|
@ -380,12 +373,6 @@ function SubagentAccordion({
|
||||||
rollupBits.push(`+${subtreeTools}t sub`)
|
rollupBits.push(`+${subtreeTools}t sub`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const subCost = aggregate.costUsd - localCost
|
|
||||||
|
|
||||||
if (subCost >= 0.01) {
|
|
||||||
rollupBits.push(`+${fmtCost(subCost)} sub`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aggregate.activeCount > 0 && item.status !== 'running') {
|
if (aggregate.activeCount > 0 && item.status !== 'running') {
|
||||||
rollupBits.push(`⚡${aggregate.activeCount}`)
|
rollupBits.push(`⚡${aggregate.activeCount}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,10 +252,6 @@ export function formatSummary(totals: SubagentAggregate): string {
|
||||||
pieces.push(`${fmtTokens(tokens)} tok`)
|
pieces.push(`${fmtTokens(tokens)} tok`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totals.costUsd > 0) {
|
|
||||||
pieces.push(fmtCost(totals.costUsd))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totals.activeCount > 0) {
|
if (totals.activeCount > 0) {
|
||||||
pieces.push(`⚡${totals.activeCount}`)
|
pieces.push(`⚡${totals.activeCount}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue