m64_04b2a07e4eac / server.py
112 lines, 4.8 KBraw
1"""deepseek-flash-openrouter-gb10: a tiny OpenAI-compatible chat server (stdlib only).23POST /v1/chat/completions is forwarded to http://openrouter.ai/api/v1/chat/completions4with the model forced to deepseek/deepseek-v4.1-flash. The request goes through the5m64 gate (HTTP_PROXY, set by m64): the gate adds the OpenRouter key and sends it on6over TLS, so the key never enters this container. Streaming responses (SSE) are relayed7chunk by chunk as they arrive. GET /v1/models lists the one model; GET /health is 200.8"""910import json11import os12import sys13import urllib.error14import urllib.request15from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer1617PORT = int(os.environ.get("M64_PORT", "9600"))18UPSTREAM = "http://openrouter.ai/api/v1/chat/completions" # plain http: the gate does TLS19UPSTREAM_MODEL = "deepseek/deepseek-v4.1-flash"20PUBLIC_MODEL = "deepseek-v4.1-flash"21MAX_BODY = 32 * 1024 * 102422TIMEOUT_S = 900 # between two bytes from upstream (a long reasoning pause is fine)23CHUNK = 16 * 10242425# urllib's ProxyHandler reads HTTP_PROXY from the environment: every call goes via the gate.26OPENER = urllib.request.build_opener(urllib.request.ProxyHandler())272829class Handler(BaseHTTPRequestHandler):30 protocol_version = "HTTP/1.1"31 server_version = "dsflash-capsule"3233 def log_message(self, fmt, *args): # one short line per request on stderr (not /health)34 if self.path == "/health":35 return36 sys.stderr.write("%s %s\n" % (self.command, fmt % args))3738 def _json(self, status, obj):39 data = json.dumps(obj).encode()40 self.send_response(status)41 self.send_header("Content-Type", "application/json")42 self.send_header("Content-Length", str(len(data)))43 self.end_headers()44 self.wfile.write(data)4546 def do_GET(self):47 if self.path == "/health":48 return self._json(200, {"status": "ok"})49 if self.path == "/v1/models":50 return self._json(200, {"object": "list", "data": [51 {"id": PUBLIC_MODEL, "object": "model", "created": 0, "owned_by": "openrouter",52 "upstream": UPSTREAM_MODEL},53 ]})54 return self._json(404, {"error": "not found"})5556 def do_POST(self):57 if self.path != "/v1/chat/completions":58 return self._json(404, {"error": "not found"})59 try:60 length = int(self.headers.get("Content-Length") or 0)61 except ValueError:62 return self._json(400, {"error": "bad Content-Length"})63 if length <= 0 or length > MAX_BODY:64 return self._json(400 if length <= 0 else 413, {"error": "body missing or too large"})65 try:66 body = json.loads(self.rfile.read(length))67 except ValueError:68 return self._json(400, {"error": "body must be JSON"})69 if not isinstance(body, dict):70 return self._json(400, {"error": "body must be a JSON object"})71 body["model"] = UPSTREAM_MODEL72 req = urllib.request.Request(UPSTREAM, data=json.dumps(body).encode(), method="POST", headers={73 "Content-Type": "application/json",74 "Accept": "text/event-stream" if body.get("stream") is True else "application/json",75 "X-Title": "m64 deepseek-flash-openrouter-gb10",76 # No Authorization: the gate sets it (Bearer <key>) for openrouter.ai.77 })78 try:79 resp = OPENER.open(req, timeout=TIMEOUT_S)80 except urllib.error.HTTPError as e:81 resp = e # relay OpenRouter's own error status and body82 except (urllib.error.URLError, OSError) as e:83 return self._json(502, {"error": f"upstream unreachable: {type(e).__name__}"})84 with resp:85 self.send_response(resp.status if hasattr(resp, "status") else resp.code)86 self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json"))87 self.send_header("Cache-Control", "no-cache")88 self.send_header("Transfer-Encoding", "chunked")89 self.end_headers()90 try:91 while True:92 # read1: whatever has arrived (up to CHUNK), without waiting to fill it.93 data = resp.read1(CHUNK) if hasattr(resp, "read1") else resp.read(CHUNK)94 if not data:95 break96 self.wfile.write(b"%x\r\n%s\r\n" % (len(data), data))97 self.wfile.flush()98 self.wfile.write(b"0\r\n\r\n")99 except (BrokenPipeError, ConnectionResetError):100 self.close_connection = True101102103def main():104 server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)105 server.daemon_threads = True106 with open(os.environ["M64_STATUS"], "a") as f:107 f.write(json.dumps({"phase": "ready", "detail": f"forwarding to {UPSTREAM_MODEL}"}) + "\n")108 server.serve_forever()109110111if __name__ == "__main__":112 main()