feat(dashboard): show Claude usage (tokens + requests) since server start
ClaudeBrain now returns per-reply token usage (Reply.usage from the API response), the dashboard accumulates it (monitor.add_claude_usage), and the header shows a "클로드 토큰" stat (input+output total, with a tooltip breaking down input/output tokens and request count). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -176,4 +176,9 @@ class ClaudeBrain:
|
||||
messages=msgs,
|
||||
)
|
||||
text = "".join(b.text for b in resp.content if b.type == "text")
|
||||
return Reply(text=text.strip(), ts=time.monotonic())
|
||||
usage = None
|
||||
u = getattr(resp, "usage", None)
|
||||
if u is not None:
|
||||
usage = {"input": getattr(u, "input_tokens", 0) or 0,
|
||||
"output": getattr(u, "output_tokens", 0) or 0}
|
||||
return Reply(text=text.strip(), ts=time.monotonic(), usage=usage)
|
||||
|
||||
@@ -475,6 +475,9 @@ class Dashboard:
|
||||
try:
|
||||
reply = self._submit(self.brain.respond(heard, None, list(self._history)))
|
||||
text = (reply.text or "").strip()
|
||||
u = getattr(reply, "usage", None)
|
||||
if u:
|
||||
self.monitor.add_claude_usage(u.get("input", 0), u.get("output", 0))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.exception("brain failed")
|
||||
self.monitor.log("error", f"두뇌 응답 실패: {exc}")
|
||||
@@ -723,6 +726,7 @@ PAGE = r"""<!DOCTYPE html>
|
||||
<div class="stats">
|
||||
<div class="stat"><b id="s-turns">0</b><span>대화 수</span></div>
|
||||
<div class="stat"><b id="s-errors">0</b><span>오류</span></div>
|
||||
<div class="stat" title="이 봇이 서버 시작 후 쓴 클로드 토큰"><b id="s-claude">0</b><span>클로드 토큰</span></div>
|
||||
<div class="stat"><b id="s-up">0초</b><span>가동시간</span></div>
|
||||
<div class="stat"><b id="s-conn">·</b><span>연결</span></div>
|
||||
</div>
|
||||
@@ -803,6 +807,7 @@ function fmtTime(wall){
|
||||
return d.toLocaleTimeString('ko-KR',{hour12:false}) +
|
||||
'.' + String(d.getMilliseconds()).padStart(3,'0');
|
||||
}
|
||||
function fmtNum(n){ n=n||0; if(n>=1e6) return (n/1e6).toFixed(2)+'M'; if(n>=1e3) return (n/1e3).toFixed(1)+'k'; return ''+n; }
|
||||
function fmtUptime(s){
|
||||
s = Math.floor(s||0);
|
||||
const h=Math.floor(s/3600), m=Math.floor(s%3600/60), sec=s%60;
|
||||
@@ -817,6 +822,10 @@ function renderStatus(s){
|
||||
$('s-turns').textContent = s.turns_total ?? 0;
|
||||
$('s-errors').textContent = s.errors_total ?? 0;
|
||||
$('s-up').textContent = fmtUptime(s.uptime_s);
|
||||
const ci=s.claude_input_tokens||0, co=s.claude_output_tokens||0, cr=s.claude_requests||0;
|
||||
const cel=$('s-claude');
|
||||
if(cel){ cel.textContent = fmtNum(ci+co);
|
||||
cel.parentElement.title = '클로드 사용량 (서버 시작 후) — 입력 '+ci.toLocaleString()+' · 출력 '+co.toLocaleString()+' 토큰 · 요청 '+cr+'회'; }
|
||||
const listening = s.listening;
|
||||
$('dot').className = 'dot ' + (listening ? 'live' : 'off');
|
||||
$('listen').textContent = listening ? '듣는 중' : (s.running ? '실행 중 (대기)' : '중지됨');
|
||||
|
||||
@@ -63,6 +63,7 @@ class Reply:
|
||||
|
||||
text: str
|
||||
ts: float
|
||||
usage: dict | None = None # Claude token usage for this reply, when known
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@@ -169,6 +169,10 @@ class Monitor:
|
||||
"components": {},
|
||||
"turns_total": 0,
|
||||
"errors_total": 0,
|
||||
# Claude usage since server start (this bot's own consumption).
|
||||
"claude_requests": 0,
|
||||
"claude_input_tokens": 0,
|
||||
"claude_output_tokens": 0,
|
||||
}
|
||||
self._lock = threading.Lock()
|
||||
self._subs: list["queue.Queue[str]"] = []
|
||||
@@ -192,6 +196,14 @@ class Monitor:
|
||||
s["uptime_s"] = round(_now_wall() - s["started_wall"], 1)
|
||||
return s
|
||||
|
||||
def add_claude_usage(self, input_tokens: int, output_tokens: int) -> None:
|
||||
"""Accumulate one Claude call's token usage for the dashboard."""
|
||||
with self._lock:
|
||||
self._status["claude_requests"] += 1
|
||||
self._status["claude_input_tokens"] += int(input_tokens or 0)
|
||||
self._status["claude_output_tokens"] += int(output_tokens or 0)
|
||||
self._broadcast({"type": "status", "status": self.status_snapshot()})
|
||||
|
||||
def log(self, level: str, message: str) -> None:
|
||||
"""A free-form lifecycle/error line (startup, disconnect, crash…)."""
|
||||
with self._lock:
|
||||
|
||||
Reference in New Issue
Block a user