Persistent Haiku cost tracking, Sethian whitelist web app
- Haiku cost persists to /var/log/mc_anthropic_cost.json (survives restarts) - Status printer reads persistent cost file instead of journalctl - Seeded at $3.08 estimated cumulative spend - Whitelist app: Sethian Dark theme, mission description, server info Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -106,8 +106,7 @@ def get_bot_stats():
|
||||
|
||||
|
||||
def get_gemini_usage():
|
||||
"""Track Gemini API calls. Reads/writes a local JSON counter."""
|
||||
# Count Gemini calls from bot log
|
||||
"""Track Gemini API calls from bot log."""
|
||||
gemini_calls = remote_cmd(f"grep -c 'Gemini.*Generated' {BOT_LOG} 2>/dev/null")
|
||||
gemini_errors = remote_cmd(f"grep -c 'Gemini.*Error' {BOT_LOG} 2>/dev/null")
|
||||
|
||||
@@ -120,7 +119,6 @@ def get_gemini_usage():
|
||||
except:
|
||||
errors = 0
|
||||
|
||||
# Estimate cost
|
||||
total_input_tokens = calls * EST_INPUT_TOKENS_PER_CALL
|
||||
total_output_tokens = calls * EST_OUTPUT_TOKENS_PER_CALL
|
||||
input_cost = (total_input_tokens / 1_000_000) * GEMINI_INPUT_COST_PER_M
|
||||
@@ -136,6 +134,21 @@ def get_gemini_usage():
|
||||
}
|
||||
|
||||
|
||||
def get_haiku_usage():
|
||||
"""Track Claude Haiku API spend from persistent cost file."""
|
||||
raw = remote_cmd("cat /var/log/mc_anthropic_cost.json 2>/dev/null")
|
||||
cost = 0.0
|
||||
budget = 5.0
|
||||
if raw and raw.startswith("{"):
|
||||
try:
|
||||
import json as _json
|
||||
data = _json.loads(raw)
|
||||
cost = data.get("total_cost", 0.0)
|
||||
except:
|
||||
pass
|
||||
return {"cost": cost, "budget": budget}
|
||||
|
||||
|
||||
def get_dataset_size():
|
||||
"""Get current seed dataset size."""
|
||||
try:
|
||||
@@ -195,15 +208,25 @@ def build_receipt():
|
||||
p.text(f" Last: {last_msg}\n")
|
||||
p.text("-" * COLS + "\n")
|
||||
|
||||
# Gemini API
|
||||
# Haiku API (main cost)
|
||||
haiku = get_haiku_usage()
|
||||
p.set(font='b', align='left', bold=True)
|
||||
p.text("CLAUDE HAIKU API (dev God)\n")
|
||||
p.set(font='b', align='left', bold=False)
|
||||
p.set(font='b', align='left', bold=True)
|
||||
p.text(f" Spent: ${haiku['cost']:.4f}\n")
|
||||
p.set(font='b', align='left', bold=False)
|
||||
p.text(f" Budget: ${haiku['budget']:.2f}\n")
|
||||
p.text(f" Remaining: ${haiku['budget'] - haiku['cost']:.4f}\n")
|
||||
p.text("-" * COLS + "\n")
|
||||
|
||||
# Gemini API (bot prompts)
|
||||
gemini = get_gemini_usage()
|
||||
p.set(font='b', align='left', bold=True)
|
||||
p.text("GEMINI API (flash-lite)\n")
|
||||
p.text("GEMINI API (bot prompts)\n")
|
||||
p.set(font='b', align='left', bold=False)
|
||||
p.text(f" Calls: {gemini['calls']}\n")
|
||||
p.text(f" Errors: {gemini['errors']}\n")
|
||||
p.text(f" Est input tokens: {gemini['est_input_tokens']:,}\n")
|
||||
p.text(f" Est output tokens: {gemini['est_output_tokens']:,}\n")
|
||||
p.set(font='b', align='left', bold=True)
|
||||
p.text(f" Est cost: ${gemini['est_cost_usd']:.4f}\n")
|
||||
p.set(font='b', align='left', bold=False)
|
||||
@@ -278,6 +301,7 @@ def main():
|
||||
dataset_size = get_dataset_size()
|
||||
dev_audit, prod_audit, bug_count = get_audit_stats()
|
||||
num_bots, num_sends, last_msg = get_bot_stats()
|
||||
haiku = get_haiku_usage()
|
||||
statuses = get_service_status()
|
||||
|
||||
print(f"Dataset: {dataset_size} seed examples")
|
||||
@@ -285,9 +309,9 @@ def main():
|
||||
print(f"Prod audit: {prod_audit} entries")
|
||||
print(f"Bug reports: {bug_count}")
|
||||
print(f"Bots: {num_bots} active, {num_sends} messages sent")
|
||||
print(f"Haiku: ${haiku['cost']:.4f} / ${haiku['budget']:.2f} ({haiku['budget'] - haiku['cost']:.4f} remaining)")
|
||||
print(f"Gemini: {gemini['calls']} calls, {gemini['errors']} errors, ${gemini['est_cost_usd']:.4f}")
|
||||
print(f"Services: {statuses}")
|
||||
print(f"Threshold: ${COST_PRINT_THRESHOLD} (would print: {should_print(current_cost) or force})")
|
||||
return
|
||||
|
||||
if not force and not should_print(current_cost):
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minecraft whitelist web app — lightweight self-service whitelisting.
|
||||
Sethian Dark theme. minecraft.sethpc.xyz
|
||||
"""
|
||||
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
PORT = 8099
|
||||
INVITE_KEY = os.environ.get("INVITE_KEY", "REDACTED_INVITE_KEY")
|
||||
|
||||
SERVERS = [
|
||||
{"name": "Paper AI", "host": "127.0.0.1", "rcon_port": 25577, "rcon_pass": "REDACTED_RCON", "show": True, "address": "sethpc.xyz:25567", "desc": "Full AI stack — pray to God, sudo commands, divine interventions"},
|
||||
{"name": "Shrink World", "host": "127.0.0.1", "rcon_port": 25576, "rcon_pass": "REDACTED_RCON", "show": True, "address": "sethpc.xyz:25566", "desc": "Survival challenge — border shrinks on death, 5x creepers, pray for help"},
|
||||
{"name": "Vanilla", "host": "127.0.0.1", "rcon_port": 25575, "rcon_pass": "REDACTED_RCON", "show": False, "address": None, "desc": None},
|
||||
]
|
||||
|
||||
WHITELIST_LOG = "/var/log/mc_whitelist.log"
|
||||
|
||||
LOGO_URL = "https://storage.googleapis.com/sethfreiberg.com/sethflix/favicon.png"
|
||||
|
||||
|
||||
def rcon_command(cmd, host, port, password):
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
s.connect((host, port))
|
||||
def send_packet(req_id, ptype, payload):
|
||||
data = struct.pack("<ii", req_id, ptype) + payload.encode("utf-8") + b"\x00\x00"
|
||||
s.sendall(struct.pack("<i", len(data)) + data)
|
||||
def recv_packet():
|
||||
raw = s.recv(4)
|
||||
if len(raw) < 4: return None
|
||||
length = struct.unpack("<i", raw)[0]
|
||||
data = s.recv(length)
|
||||
return data[8:-2].decode("utf-8", errors="replace")
|
||||
send_packet(1, 3, password)
|
||||
time.sleep(0.1)
|
||||
recv_packet()
|
||||
send_packet(2, 2, cmd)
|
||||
time.sleep(0.2)
|
||||
result = recv_packet()
|
||||
s.close()
|
||||
return result or ""
|
||||
except Exception as e:
|
||||
return f"ERROR: {e}"
|
||||
|
||||
|
||||
def whitelist_player(username):
|
||||
results = {}
|
||||
for srv in SERVERS:
|
||||
result = rcon_command(f"whitelist add {username}", srv["host"], srv["rcon_port"], srv["rcon_pass"])
|
||||
results[srv["name"]] = result.strip()
|
||||
return results
|
||||
|
||||
|
||||
def is_valid_username(name):
|
||||
return bool(re.match(r'^[a-zA-Z0-9_]{3,16}$', name))
|
||||
|
||||
|
||||
PAGE = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Minecraft AI Server — sethpc.xyz</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #1a1a1a; color: #e0e0e0; min-height: 100vh; }}
|
||||
a {{ color: #D35400; text-decoration: none; }}
|
||||
a:hover {{ color: #e65c00; }}
|
||||
|
||||
.header {{ background: #000; padding: 1rem; text-align: center; border-bottom: 2px solid #D35400; }}
|
||||
.header img {{ width: 40px; vertical-align: middle; border: none; }}
|
||||
.header span {{ font-size: 1.2rem; font-weight: 700; color: #D35400; margin-left: 0.5rem; vertical-align: middle; }}
|
||||
|
||||
.container {{ max-width: 600px; margin: 0 auto; padding: 1.5rem; }}
|
||||
|
||||
.hero {{ text-align: center; margin: 2rem 0 1.5rem; }}
|
||||
.hero h1 {{ font-size: 1.6rem; color: #D35400; margin-bottom: 0.5rem; }}
|
||||
.hero p {{ color: #ccc; font-size: 0.95rem; line-height: 1.5; }}
|
||||
|
||||
.mission {{ background: #252525; border: 1px solid #333; border-radius: 8px;
|
||||
padding: 1.2rem; margin: 1.5rem 0; }}
|
||||
.mission h2 {{ color: #D35400; font-size: 1rem; margin-bottom: 0.5rem; }}
|
||||
.mission p {{ color: #ccc; font-size: 0.85rem; line-height: 1.5; margin-bottom: 0.5rem; }}
|
||||
.mission ul {{ color: #ccc; font-size: 0.85rem; line-height: 1.6; padding-left: 1.2rem; }}
|
||||
.mission code {{ background: #333; padding: 0.15rem 0.4rem; border-radius: 3px; color: #D35400; font-size: 0.85rem; }}
|
||||
|
||||
.card {{ background: #252525; border: 1px solid #333; border-radius: 8px;
|
||||
padding: 1.5rem; margin: 1.5rem 0; }}
|
||||
.card h2 {{ color: #D35400; font-size: 1.1rem; margin-bottom: 1rem; text-align: center; }}
|
||||
|
||||
label {{ display: block; font-size: 0.85rem; color: #aaa; margin-bottom: 0.3rem; margin-top: 1rem; }}
|
||||
input {{ width: 100%; padding: 0.6rem; border-radius: 6px; border: 1px solid #444;
|
||||
background: #2a2a2a; color: #fff; font-size: 1rem; }}
|
||||
input:focus {{ outline: none; border-color: #D35400; box-shadow: 0 0 5px rgba(211,84,0,0.5); }}
|
||||
|
||||
button {{ width: 100%; padding: 0.7rem; border-radius: 6px; border: none;
|
||||
background: #D35400; color: #fff; font-size: 1rem; font-weight: 600;
|
||||
cursor: pointer; margin-top: 1.5rem; }}
|
||||
button:hover {{ background: #e65c00; }}
|
||||
|
||||
.error {{ color: #ff6b6b; font-size: 0.85rem; margin-top: 0.5rem; text-align: center; }}
|
||||
|
||||
.success {{ background: #1a3a1a; border: 1px solid #2d7a2d; border-radius: 8px;
|
||||
padding: 1.2rem; margin-top: 1.5rem; }}
|
||||
.success h2 {{ color: #4caf50; font-size: 1.1rem; margin-bottom: 0.5rem; }}
|
||||
|
||||
.server {{ background: #252525; border: 1px solid #333; border-left: 3px solid #D35400;
|
||||
border-radius: 6px; padding: 0.8rem; margin-top: 0.8rem; }}
|
||||
.server .name {{ color: #D35400; font-weight: 600; font-size: 0.95rem; }}
|
||||
.server .addr {{ font-family: monospace; color: #fff; font-size: 1.1rem; margin: 0.3rem 0;
|
||||
background: #1a1a1a; padding: 0.3rem 0.6rem; border-radius: 4px; display: inline-block; }}
|
||||
.server .desc {{ color: #999; font-size: 0.8rem; margin-top: 0.3rem; }}
|
||||
|
||||
.commands {{ background: #252525; border: 1px solid #333; border-radius: 8px;
|
||||
padding: 1rem; margin-top: 1rem; }}
|
||||
.commands h3 {{ color: #D35400; font-size: 0.9rem; margin-bottom: 0.5rem; }}
|
||||
.cmd {{ font-family: monospace; background: #1a1a1a; padding: 0.2rem 0.5rem;
|
||||
border-radius: 3px; color: #D35400; }}
|
||||
.commands p {{ font-size: 0.8rem; color: #bbb; margin-bottom: 0.3rem; }}
|
||||
|
||||
.footer {{ text-align: center; color: #555; font-size: 0.75rem; margin: 2rem 0 1rem; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<img src="{logo}" alt="logo" class="noborder">
|
||||
<span>sethpc.xyz</span>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="hero">
|
||||
<h1>Minecraft AI Server</h1>
|
||||
<p>An AI runs on this server that listens to in-game chat and does things in the world based on what you say.</p>
|
||||
</div>
|
||||
|
||||
<div class="mission">
|
||||
<h2>What Is This?</h2>
|
||||
<p>There's an AI character playing <strong>God</strong> on the server. It runs on local hardware — no cloud, no OpenAI — using a small open-source model we're actively training.</p>
|
||||
<p>Every interaction you have gets logged as training data to improve the model. The more you play, the smarter it gets.</p>
|
||||
<h2 style="margin-top: 0.8rem;">What Can You Do?</h2>
|
||||
<ul>
|
||||
<li><code>pray <message></code> — Talk to God. Pray for items, smite your enemies, or say something offensive and get punished.</li>
|
||||
<li><code>sudo <request></code> — Natural language commands. "sudo give me a diamond sword" just works.</li>
|
||||
<li><code>bug_log <description></code> — Report when something goes wrong. Helps us fix the AI.</li>
|
||||
</ul>
|
||||
<h2 style="margin-top: 0.8rem;">What We Need</h2>
|
||||
<p>Try to break it. Ask for weird things. Confuse it. Phrase things in ways nobody would expect. Every interaction — good or bad — makes the model better.</p>
|
||||
</div>
|
||||
|
||||
{content}
|
||||
|
||||
<div class="footer">
|
||||
Java Edition 1.21.x · Whitelisted · Hosted on a homelab
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
FORM = """
|
||||
<div class="card">
|
||||
<h2>Join the Server</h2>
|
||||
<form method="POST">
|
||||
<label>Minecraft Java Edition Username</label>
|
||||
<input name="username" placeholder="Your username" required autofocus>
|
||||
<label>Invite Key</label>
|
||||
<input name="key" type="password" placeholder="Paste the key you were given" required>
|
||||
<button type="submit">Get Whitelisted</button>
|
||||
</form>
|
||||
{error}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def success_content(username, results):
|
||||
servers = ""
|
||||
for srv in SERVERS:
|
||||
if not srv["show"]:
|
||||
continue
|
||||
servers += f"""
|
||||
<div class="server">
|
||||
<div class="name">{srv['name']}</div>
|
||||
<div class="addr">{srv['address']}</div>
|
||||
<div class="desc">{srv['desc']}</div>
|
||||
</div>"""
|
||||
|
||||
return f"""
|
||||
<div class="success">
|
||||
<h2>Welcome, {html.escape(username)}!</h2>
|
||||
<p style="color:#ccc; font-size:0.9rem;">You're whitelisted. Add these servers in Minecraft:</p>
|
||||
{servers}
|
||||
</div>
|
||||
|
||||
<div class="commands">
|
||||
<h3>Quick Start</h3>
|
||||
<p><span class="cmd">pray lord give me tools</span> — ask God for help</p>
|
||||
<p><span class="cmd">sudo give me a diamond sword</span> — direct command translation</p>
|
||||
<p><span class="cmd">sudo set time to day</span> — world commands work too</p>
|
||||
<p><span class="cmd">bug_log it gave me the wrong item</span> — report issues</p>
|
||||
</div>"""
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
content = FORM.format(error="")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(PAGE.format(content=content, logo=LOGO_URL).encode())
|
||||
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length).decode()
|
||||
params = parse_qs(body)
|
||||
username = params.get("username", [""])[0].strip()
|
||||
key = params.get("key", [""])[0].strip()
|
||||
|
||||
if key != INVITE_KEY:
|
||||
content = FORM.format(error='<p class="error">Invalid invite key.</p>')
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(PAGE.format(content=content, logo=LOGO_URL).encode())
|
||||
return
|
||||
|
||||
if not is_valid_username(username):
|
||||
content = FORM.format(error='<p class="error">Invalid username. 3-16 characters, letters/numbers/underscore only.</p>')
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(PAGE.format(content=content, logo=LOGO_URL).encode())
|
||||
return
|
||||
|
||||
results = whitelist_player(username)
|
||||
try:
|
||||
with open(WHITELIST_LOG, "a") as f:
|
||||
f.write(json.dumps({"time": time.strftime("%Y-%m-%dT%H:%M:%SZ"), "username": username, "results": results}) + "\n")
|
||||
except:
|
||||
pass
|
||||
|
||||
content = success_content(username, results)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(PAGE.format(content=content, logo=LOGO_URL).encode())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Whitelist app running on port {PORT}")
|
||||
print(f"Invite key: {INVITE_KEY}")
|
||||
HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
|
||||
Reference in New Issue
Block a user