Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8723,6 +8723,8 @@ def process_command(self, command: str) -> bool:
self._manual_compress(cmd_original)
elif canonical == "usage":
self._show_usage()
elif canonical == "codex-usage":
self._handle_codex_usage_command(cmd_original)
elif canonical == "credits":
self._show_credits()
elif canonical == "billing":
Expand Down Expand Up @@ -9727,6 +9729,21 @@ def _show_usage(self):
# Console quietness is enforced by hermes_logging not
# installing a console StreamHandler in non-verbose mode.

def _handle_codex_usage_command(self, cmd_original: str) -> None:
"""Show OpenAI Codex / ChatGPT subscription usage for the signed-in account."""
from agent.account_usage import fetch_account_usage, render_account_usage_lines

snapshot = fetch_account_usage("openai-codex")
if not snapshot or not snapshot.available:
self._console_print(" [yellow]No OpenAI Codex usage data available.[/]")
self._console_print(" Make sure you're signed in with the OpenAI Codex / ChatGPT account first.")
return

self._console_print()
for line in render_account_usage_lines(snapshot):
self._console_print(f" {line}")
self._console_print()

def _print_nous_credits_block(self) -> bool:
"""Print the Nous credits magnitudes + monthly-grant gauge when a Nous account
is logged in. Returns True if it printed anything.
Expand Down
3 changes: 3 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9919,6 +9919,9 @@ async def _do_undo():
if canonical == "usage":
return await self._handle_usage_command(event)

if canonical == "codex-usage":
return await self._handle_codex_usage_command(event)

if canonical == "credits":
return await self._handle_credits_command(event)

Expand Down
16 changes: 16 additions & 0 deletions gateway/slash_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4090,6 +4090,22 @@ async def _handle_usage_command(self, event: MessageEvent) -> str:
return "\n".join(parts)
return t("gateway.usage.no_data")

async def _handle_codex_usage_command(self, event: MessageEvent) -> str:
"""Show OpenAI Codex / ChatGPT subscription usage for the signed-in account."""
try:
snapshot = await asyncio.to_thread(fetch_account_usage, "openai-codex")
except Exception as exc:
logger.error("Codex usage lookup failed: %s", exc, exc_info=True)
return f"OpenAI Codex usage lookup failed: {exc}"

if not snapshot or not snapshot.available:
return (
"No OpenAI Codex usage data available. "
"Make sure you're signed in with the OpenAI Codex / ChatGPT account first."
)

return "\n".join(render_account_usage_lines(snapshot, markdown=True))

async def _handle_insights_command(self, event: MessageEvent) -> str:
"""Handle /insights command -- show usage insights and analytics."""
args = event.get_command_args().strip()
Expand Down
4 changes: 3 additions & 1 deletion hermes_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ class CommandDef:
CommandDef("restart", "Gracefully restart the gateway after draining active runs", "Session",
gateway_only=True),
CommandDef("usage", "Show token usage and rate limits for the current session", "Info"),
CommandDef("codex-usage", "Show OpenAI Codex / ChatGPT subscription usage and reset time", "Info"),
CommandDef("credits", "Show Nous credit balance and top up", "Info"),
CommandDef("billing", "Manage Nous terminal billing — buy credits, auto-reload, limits", "Info",
cli_only=True),
Expand Down Expand Up @@ -1163,7 +1164,8 @@ def discord_skill_commands_by_category(
# - moa: high-cost slash mode, available through /hermes moa to avoid
# displacing existing native Slack slash commands at the 50-command cap.
# - debug: the log/report upload surface; reached via /hermes debug on Slack.
_SLACK_VIA_HERMES_ONLY = frozenset({"credits", "billing", "moa", "debug"})
# - codex-usage: subscription-quota lookup; reached via /hermes codex-usage.
_SLACK_VIA_HERMES_ONLY = frozenset({"credits", "billing", "moa", "debug", "codex-usage"})


def _sanitize_slack_name(raw: str) -> str:
Expand Down
50 changes: 50 additions & 0 deletions tests/cli/test_codex_usage_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock

from agent.account_usage import AccountUsageSnapshot, AccountUsageWindow
from cli import HermesCLI


def _snapshot() -> AccountUsageSnapshot:
return AccountUsageSnapshot(
provider="openai-codex",
source="usage_api",
fetched_at=datetime.now(timezone.utc),
plan="Pro",
windows=(
AccountUsageWindow(
label="Session",
used_percent=15,
reset_at=datetime(2026, 5, 28, 16, 30, tzinfo=timezone.utc),
),
),
)


def test_codex_usage_command_renders_subscription_usage(monkeypatch):
cli = HermesCLI.__new__(HermesCLI)
cli._app = None
cli._console_print = MagicMock()
monkeypatch.setattr(
"agent.account_usage.fetch_account_usage",
lambda provider, **kwargs: _snapshot(),
)

cli._handle_codex_usage_command("/codex-usage")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls the private handler directly. Add a dispatch-level assertion through process_command("/codex-usage") so the registry and CLI routing added by this PR are covered together.


rendered = "\n".join(
str(call.args[0]) if call.args else ""
for call in cli._console_print.call_args_list
)
assert "openai-codex (Pro)" in rendered
assert "Session: 85% remaining (15% used)" in rendered
assert "resets" in rendered


def test_codex_usage_command_dispatches_through_registry():
cli = HermesCLI.__new__(HermesCLI)
cli._pending_resume_sessions = None
cli._handle_codex_usage_command = MagicMock()

assert cli.process_command("/codex-usage") is True
cli._handle_codex_usage_command.assert_called_once_with("/codex-usage")
27 changes: 27 additions & 0 deletions tests/gateway/test_usage_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,33 @@ async def _fake_to_thread(fn, *args, **kwargs):
assert "📈 **Account limits**" in result


class TestCodexUsageCommand:
@pytest.mark.asyncio
async def test_codex_usage_command_returns_markdown_usage(self, monkeypatch):
runner = _make_runner(SK)
event = MagicMock()
snapshot = MagicMock(available=True)

monkeypatch.setattr(
"gateway.slash_commands.fetch_account_usage",
lambda provider, base_url=None, api_key=None: snapshot if provider == "openai-codex" else None,
)
monkeypatch.setattr(
"gateway.slash_commands.render_account_usage_lines",
lambda snap, markdown=False: [
"📈 **Account limits**",
"Provider: openai-codex (Pro)",
"Session: 85% remaining (15% used) • resets in 2h",
],
)

result = await runner._handle_codex_usage_command(event)

assert "openai-codex (Pro)" in result
assert "85% remaining" in result
assert "resets in 2h" in result


class TestUsageContextBreakdown:
"""The /usage output includes the per-category context breakdown."""

Expand Down
1 change: 1 addition & 0 deletions website/docs/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
| `/help` | Show this help message |
| `/version` | Show Hermes Agent version, build, and environment info. |
| `/usage` | Show token usage, cost breakdown, session duration, and — when available from the active provider — an **Account limits** section with remaining quota / credits / plan usage pulled live from the provider's API. |
| `/codex-usage` | Show your OpenAI Codex / ChatGPT subscription usage, remaining percentage, and reset time. |
| `/credits` | Show your Nous credit balance and a top-up handoff link. |
| `/billing` | CLI terminal-billing flow for Nous — view balance, buy credits, and manage auto-reload / monthly limits. |
| `/insights` | Show usage insights and analytics (last 30 days) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Hermes 有两个斜杠命令入口,均由 `hermes_cli/commands.py` 中的中
| `/help` | 显示帮助信息 |
| `/version` | 显示 Hermes Agent 版本、构建及环境信息。 |
| `/usage` | 显示 token 用量、费用明细、会话时长,以及——当活动提供商支持时——从提供商 API 实时拉取的**账户限额**部分,包含剩余配额/积分/套餐用量。 |
| `/codex-usage` | 显示你的 OpenAI Codex / ChatGPT 订阅用量、剩余额度百分比和重置时间。 |
| `/credits` | 显示你的 Nous 积分余额和充值跳转链接。 |
| `/billing` | Nous 的 CLI 终端计费流程——查看余额、购买积分并管理自动充值 / 月度限额。 |
| `/insights` | 显示用量洞察和分析(最近 30 天) |
Expand Down