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:
2026-03-18 22:29:19 -04:00
parent 0473eb0b50
commit 0083e80aca
2 changed files with 298 additions and 8 deletions
+32 -8
View File
@@ -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):
+266
View File
@@ -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 &lt;message&gt;</code> — Talk to God. Pray for items, smite your enemies, or say something offensive and get punished.</li>
<li><code>sudo &lt;request&gt;</code> — Natural language commands. "sudo give me a diamond sword" just works.</li>
<li><code>bug_log &lt;description&gt;</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 &middot; Whitelisted &middot; 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()