mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
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>
123 lines
5.7 KiB
Python
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()
|