claude-finops 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/finops/api.py CHANGED
@@ -58,12 +58,34 @@ def flatten(rows):
58
58
  return buf.getvalue()
59
59
 
60
60
 
61
+ # A browser that navigates away, reloads, or is closed mid-response resets the
62
+ # socket. That is the client's normal behaviour, not our error, but socketserver
63
+ # prints a full traceback for it — pages of noise in server.log that look like a
64
+ # crash. These two are the only disconnect shapes it produces.
65
+ DISCONNECTS = (ConnectionResetError, BrokenPipeError, ConnectionAbortedError)
66
+
67
+
68
+ class Server(ThreadingHTTPServer):
69
+ def handle_error(self, request, client_address):
70
+ import sys
71
+ if not isinstance(sys.exception(), DISCONNECTS):
72
+ super().handle_error(request, client_address)
73
+
74
+
61
75
  class Handler(BaseHTTPRequestHandler):
62
76
  protocol_version = "HTTP/1.1"
63
77
 
64
78
  def log_message(self, *a):
65
79
  pass
66
80
 
81
+ def handle_one_request(self):
82
+ # The reset can also land while we are still writing the response, past
83
+ # the point handle_error covers.
84
+ try:
85
+ super().handle_one_request()
86
+ except DISCONNECTS:
87
+ self.close_connection = True
88
+
67
89
  def send_json(self, obj, code=200):
68
90
  body = json.dumps(obj, default=str).encode()
69
91
  self.send_response(code)
@@ -423,7 +445,7 @@ def serve(port=8787, db=DB_PATH, background=None):
423
445
  if background and hasattr(os, "fork"):
424
446
  detach(port)
425
447
  A = Analytics(db)
426
- srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)
448
+ srv = Server(("127.0.0.1", port), Handler)
427
449
  print(f"Claude FinOps Command Center -> http://127.0.0.1:{port}")
428
450
  print(f" warehouse: {db}")
429
451
  print(f" data : {A.first_day} .. {A.last_day}")
@@ -686,7 +686,7 @@ class Diagnoser:
686
686
  rows = a.q(f"""SELECT r.session_id, s.title, pj.name project, COUNT(*) steps,
687
687
  MAX(r.context_tokens) peak, AVG(r.context_tokens) avg_ctx,
688
688
  SUM(r.billable_tokens) tokens, SUM(r.est_cost_usd) cost,
689
- SUM(CASE WHEN r.context_tokens > {base} THEN r.context_tokens - {base} ELSE 0 END) over_base,
689
+ COALESCE(SUM(CASE WHEN r.context_tokens > {base} THEN r.context_tokens - {base} ELSE 0 END), 0) over_base,
690
690
  SUM(CASE WHEN r.context_tokens > {self.CTX_WARN} THEN 1 ELSE 0 END) heavy_steps,
691
691
  COUNT(DISTINCT r.prompt_id) prompts, SUM(r.is_sidechain) side
692
692
  FROM requests r JOIN sessions s ON s.id=r.session_id JOIN projects pj ON pj.id=r.project_id
@@ -698,8 +698,12 @@ class Diagnoser:
698
698
  for r in rows:
699
699
  sid = r["session_id"]
700
700
  fixes = []
701
- r["avoidable_tokens"] = r["over_base"]
702
- r["avoidable_cost"] = r["over_base"] * rate
701
+ # A session with no priced requests sums to NULL, not 0 — and one
702
+ # NULL used to take the whole diagnose page down with a 500.
703
+ over = r["over_base"] or 0
704
+ r["over_base"] = over
705
+ r["avoidable_tokens"] = over
706
+ r["avoidable_cost"] = over * rate
703
707
  if not compacts.get(sid):
704
708
  fixes.append(f"Never compacted. {r['heavy_steps']:,} steps ran above "
705
709
  f"{self.CTX_WARN // 1000}K context; /compact (or /clear between the "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-finops",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Local FinOps dashboard for Claude Code: what you used, what it cost, why it cost that much, and what to change. Reads your own transcripts, no API key, no data leaves the machine.",
5
5
  "bin": {
6
6
  "claude-finops": "bin/claude-finops.js"