claude-finops 0.4.0 → 0.4.2
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 +23 -1
- package/finops/diagnose.py +7 -3
- package/package.json +1 -1
- package/run.py +30 -0
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 =
|
|
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}")
|
package/finops/diagnose.py
CHANGED
|
@@ -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
|
-
|
|
702
|
-
|
|
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.
|
|
3
|
+
"version": "0.4.2",
|
|
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"
|
package/run.py
CHANGED
|
@@ -93,6 +93,7 @@ HELP = """Claude FinOps Command Center
|
|
|
93
93
|
claude-finops --set-key store a provider API key (prompts, never echoes)
|
|
94
94
|
claude-finops --keys list which provider keys are configured
|
|
95
95
|
claude-finops --share write ../claude-finops.zip (code only, never your data)
|
|
96
|
+
claude-finops --version print the installed version, and whether a newer one is out
|
|
96
97
|
claude-finops --help this message
|
|
97
98
|
|
|
98
99
|
Environment:
|
|
@@ -104,6 +105,33 @@ Environment:
|
|
|
104
105
|
"""
|
|
105
106
|
|
|
106
107
|
|
|
108
|
+
def version():
|
|
109
|
+
"""Which copy is this, and is it current?
|
|
110
|
+
|
|
111
|
+
The second half matters more than the first: people run a global install and
|
|
112
|
+
a repo checkout side by side, and the usual confusion is not "what version
|
|
113
|
+
am I on" but "why does the one I am looking at not have the feature".
|
|
114
|
+
"""
|
|
115
|
+
from finops.update import check, disabled, _key
|
|
116
|
+
# Asking outright is worth a fresh request: a day-old cached answer is the
|
|
117
|
+
# one thing this command must not give you.
|
|
118
|
+
u = check(force=True)
|
|
119
|
+
print(f" claude-finops {u['current'] or 'unknown'}")
|
|
120
|
+
print(f" installed at {ROOT}")
|
|
121
|
+
if u.get("update_available"):
|
|
122
|
+
print(f" update {u['latest']} is out - {u['command']}")
|
|
123
|
+
elif disabled():
|
|
124
|
+
print(" update check is off (NO_UPDATE_NOTIFIER)")
|
|
125
|
+
elif u.get("latest") and _key(u["current"]) > _key(u["latest"]):
|
|
126
|
+
# A checkout mid-release is ahead of what is published. Saying "up to
|
|
127
|
+
# date" there would hide exactly the gap you are looking for.
|
|
128
|
+
print(f" update ahead of npm (published latest is {u['latest']})")
|
|
129
|
+
elif u.get("latest"):
|
|
130
|
+
print(f" update up to date (npm latest is {u['latest']})")
|
|
131
|
+
else:
|
|
132
|
+
print(" update could not reach the npm registry")
|
|
133
|
+
|
|
134
|
+
|
|
107
135
|
def where():
|
|
108
136
|
from finops.paths import (HOME_DIR, DB_PATH, LOCAL_SETTINGS_PATH, SECRETS_PATH,
|
|
109
137
|
LOGFILE, CONFIG_DIR)
|
|
@@ -180,6 +208,8 @@ def main():
|
|
|
180
208
|
migrate()
|
|
181
209
|
if "--help" in args or "-h" in args:
|
|
182
210
|
return print(HELP)
|
|
211
|
+
if "--version" in args or "-v" in args or "-V" in args:
|
|
212
|
+
return version()
|
|
183
213
|
if "--where" in args:
|
|
184
214
|
return where()
|
|
185
215
|
if "--keys" in args:
|