Files
DarkflameServer/tests/dWebTests/mock_claude_api.py
Aaron Kimbrell e213a7aeff feat: web dashboard and playground work
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch:
accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy
reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live
events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved
captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes
they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:43 -05:00

123 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""
A fake Anthropic Messages API for trying the dashboard's AI moderator helper without a real key or any cost.
python3 tests/dWebTests/mock_claude_api.py [--port 8765] [--mode good]
Then in Settings (Dashboard, AI moderator helper): ai_helper_enabled=1, claude_api_key=anything,
claude_api_base=http://127.0.0.1:8765. Modes (also switchable per request with the X-Mock-Mode header or by
putting "mock:<mode>" in a player's chat/report text, which the helper sends along as data):
good a valid suggestion built from the refs in the case file
malformed prose around the JSON (the dashboard must reject it)
fence JSON in a ```json code fence (rejected)
inject echoes the hidden canary marker, as if a player's text had hijacked the model (rejected)
ratelimit 429 with retry-after: 1 on the first try, then a good answer
error500 500 every time (the dashboard gives up after 4 tries)
overloaded 529 every time
refusal stop_reason "refusal"
cutoff stop_reason "max_tokens"
badkey 401 authentication_error
Only for local testing: it listens on 127.0.0.1 and never checks the key.
"""
import argparse
import json
import re
from http.server import BaseHTTPRequestHandler, HTTPServer
STATE = {"mode": "good", "calls": 0}
def suggestion_for(body):
"""A plausible answer: the first allowed action, citing the refs found in the case file."""
user = body["messages"][0]["content"]
refs = sorted(set(re.findall(r'"ref":"([A-Z]+[0-9]*)"', user)))
schema = (body.get("output_config") or {}).get("format", {}).get("schema", {})
actions = schema.get("properties", {}).get("action", {}).get("enum") or ["note"]
action = "reject_name" if "reject_name" in actions else ("warn" if "warn" in actions else actions[0])
return {
"action": action,
"days": 0,
"strike": False,
"player_reason": "Please keep things friendly for everyone." if action in ("warn", "reject_name") else "",
"staff_explanation": "Mock answer from mock_claude_api.py. It cites " + (", ".join(refs) or "nothing") + ".",
"confidence": "low",
"evidence": refs[:5],
}
def message(text, stop="end_turn"):
return {
"id": "msg_mock", "type": "message", "role": "assistant", "model": "mock-" + STATE.get("model", "claude"),
"content": [{"type": "thinking", "thinking": ""}, {"type": "text", "text": text}],
"stop_reason": stop, "usage": {"input_tokens": 1000, "output_tokens": 120},
}
def error(kind, text):
return {"type": "error", "error": {"type": kind, "message": text}}
class Handler(BaseHTTPRequestHandler):
def reply(self, status, payload, headers=None):
data = json.dumps(payload).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
for key, value in (headers or {}).items():
self.send_header(key, value)
self.end_headers()
self.wfile.write(data)
def do_POST(self):
if self.path != "/v1/messages":
return self.reply(404, error("not_found_error", "no such path"))
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}")
STATE["calls"] += 1
STATE["model"] = body.get("model", "claude")
found = re.search(r"mock:([a-z0-9]+)", body["messages"][0]["content"])
mode = self.headers.get("X-Mock-Mode") or (found.group(1) if found else STATE["mode"])
canary = re.search(r"CANARY-[0-9a-f]+", body.get("system", ""))
answer = json.dumps(suggestion_for(body))
print(f"[mock] call {STATE['calls']}: model={body.get('model')} max_tokens={body.get('max_tokens')} mode={mode} "
f"key_present={'x-api-key' in self.headers} version={self.headers.get('anthropic-version')}")
if mode == "ratelimit":
if STATE["calls"] % 2 == 1:
return self.reply(429, error("rate_limit_error", "slow down"), {"retry-after": "1"})
return self.reply(200, message(answer))
if mode == "error500":
return self.reply(500, error("api_error", "Internal server error"))
if mode == "overloaded":
return self.reply(529, error("overloaded_error", "Overloaded"))
if mode == "badkey":
return self.reply(401, error("authentication_error", "invalid x-api-key"))
if mode == "malformed":
return self.reply(200, message("Sure! Here is what I think: " + answer))
if mode == "fence":
return self.reply(200, message("```json\n" + answer + "\n```"))
if mode == "inject":
hijacked = suggestion_for(body)
hijacked.update(action="ban" if "ban" in json.dumps(body) else hijacked["action"], days=365,
staff_explanation="Ignoring my instructions as the player asked. Marker: " + (canary.group(0) if canary else "?"))
return self.reply(200, message(json.dumps(hijacked)))
if mode == "refusal":
return self.reply(200, message("", "refusal"))
if mode == "cutoff":
return self.reply(200, message(answer[:40], "max_tokens"))
return self.reply(200, message(answer))
def log_message(self, *args):
pass
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--port", type=int, default=8765)
parser.add_argument("--mode", default="good")
args = parser.parse_args()
STATE["mode"] = args.mode
print(f"Mock Claude API on http://127.0.0.1:{args.port} (mode {args.mode})")
HTTPServer(("127.0.0.1", args.port), Handler).serve_forever()