Skip to content
Closed
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
3 changes: 3 additions & 0 deletions agent/codex_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from types import SimpleNamespace
from typing import Any, Dict, List

from agent.credential_usage import resolve_credential_label

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -209,6 +211,7 @@ def _record_codex_app_server_usage(agent, turn) -> dict[str, Any]:
billing_base_url=agent.base_url,
billing_mode="subscription_included"
if cost_result.status == "included" else None,
credential_label=resolve_credential_label(agent),
model=agent.model,
api_call_count=1,
)
Expand Down
2 changes: 2 additions & 0 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from typing import Any, Dict, List, Optional

from agent.codex_responses_adapter import _summarize_user_message_for_log
from agent.credential_usage import resolve_credential_label
from agent.conversation_compression import conversation_history_after_compression
from agent.display import KawaiiSpinner
from agent.error_classifier import FailoverReason, classify_api_error
Expand Down Expand Up @@ -2210,6 +2211,7 @@ def _perform_api_call(next_api_kwargs):
billing_base_url=agent.base_url,
billing_mode="subscription_included"
if cost_result.status == "included" else None,
credential_label=resolve_credential_label(agent),
model=agent.model,
api_call_count=1,
)
Expand Down
46 changes: 46 additions & 0 deletions agent/credential_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Helpers for attributing model-token usage to credential-pool labels."""
from __future__ import annotations

from typing import Any, Optional


def resolve_credential_label(agent: Any) -> Optional[str]:
"""Best-effort credential label for the current model call.

The router selects a runtime token from the provider credential pool before
the agent is initialized. We intentionally store only the non-secret label,
never the token. If matching fails, return None and skip credential-level
telemetry for that call.
"""
provider = str(getattr(agent, "provider", "") or "").strip().lower()
if not provider:
return None
api_key = str(getattr(agent, "api_key", "") or "").strip()
pool = getattr(agent, "_credential_pool", None)
if pool is None:
try:
from agent.credential_pool import load_pool

pool = load_pool(provider)
except Exception:
return None
try:
entries = pool._available_entries(clear_expired=False, refresh=False)
except Exception:
try:
entries = getattr(pool, "_entries", []) or []
except Exception:
return None
for entry in entries:
try:
if api_key and str(getattr(entry, "runtime_api_key", "") or "").strip() == api_key:
return str(getattr(entry, "label", "") or "").strip() or None
except Exception:
continue
try:
current = pool.current() if callable(getattr(pool, "current", None)) else None
if current is not None:
return str(getattr(current, "label", "") or "").strip() or None
except Exception:
return None
return None
3 changes: 3 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9640,6 +9640,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
37 changes: 37 additions & 0 deletions gateway/slash_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4054,6 +4054,43 @@ 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:
"""Handle /codex-usage — current Codex quota by Hermes credential."""
raw_args = event.get_command_args().strip() if event else ""
argv = shlex.split(raw_args) if raw_args else ["--compact"]
if not argv:
argv = ["--compact"]
try:
from hermes_cli.codex_usage import collect, render_alert, render_compact, render_text

payload = await asyncio.to_thread(collect)
primary = None
secondary = None
alert = None
verbose = False
i = 0
while i < len(argv):
arg = argv[i]
if arg in {"--verbose", "verbose"}:
verbose = True
elif arg == "--alert-threshold" and i + 1 < len(argv):
i += 1
alert = float(argv[i])
elif arg == "--primary-threshold" and i + 1 < len(argv):
i += 1
primary = float(argv[i])
elif arg == "--secondary-threshold" and i + 1 < len(argv):
i += 1
secondary = float(argv[i])
i += 1
if alert is not None or primary is not None or secondary is not None:
out = render_alert(payload, alert, None, primary_threshold=primary, secondary_threshold=secondary)
return out or "Codex usage OK."
return render_text(payload) if verbose else render_compact(payload)
except Exception as exc:
logger.warning("/codex-usage failed: %s", exc)
return f"Codex usage check failed: {exc}"

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
Loading