m64_174cfd55170a / server.py
92 lines, 3.9 KBraw
1"""openrouter-secrets: forward OpenAI-style chat requests to OpenRouter (stdlib only).23The interesting part is what this file does NOT do: it never reads4OPENROUTER_API_KEY and never sets an Authorization header. The request goes to5http://openrouter.ai through m64's gate ($HTTP_PROXY); the gate adds the key,6because capsule.toml asks it to in [[secrets.inject]], and sends it on over TLS.7"""89import json10import os11import urllib.error12import urllib.request13from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer1415PORT = int(os.environ["M64_PORT"])16# Plain http on purpose: the gate does TLS. https here would hide the request17# from the gate, so it could not add the key.18UPSTREAM = "http://openrouter.ai/api/v1/chat/completions"19UPSTREAM_MODEL = "deepseek/deepseek-v4.1-flash"20PUBLIC_MODEL = "deepseek-v4.1-flash"21MAX_BODY = 8 * 1024 * 10242223# ProxyHandler() reads HTTP_PROXY from the environment, so every call goes via the gate.24OPENER = urllib.request.build_opener(urllib.request.ProxyHandler())252627class Handler(BaseHTTPRequestHandler):28 protocol_version = "HTTP/1.1"2930 def log_message(self, fmt, *args):31 if self.path != "/health":32 super().log_message(fmt, *args)3334 def _json(self, status, obj):35 data = json.dumps(obj).encode()36 self.send_response(status)37 self.send_header("Content-Type", "application/json")38 self.send_header("Content-Length", str(len(data)))39 self.end_headers()40 self.wfile.write(data)4142 def do_GET(self):43 if self.path == "/health":44 return self._json(200, {"status": "ok"})45 if self.path == "/v1/models":46 return self._json(200, {"object": "list", "data": [{"id": PUBLIC_MODEL, "object": "model", "owned_by": "openrouter"}]})47 return self._json(404, {"error": "not found"})4849 def do_POST(self):50 if self.path != "/v1/chat/completions":51 return self._json(404, {"error": "not found"})52 length = int(self.headers.get("Content-Length") or 0)53 if not 0 < length <= MAX_BODY:54 return self._json(400, {"error": "body missing or too large"})55 try:56 body = json.loads(self.rfile.read(length))57 except ValueError:58 return self._json(400, {"error": "body must be JSON"})59 body["model"] = UPSTREAM_MODEL60 req = urllib.request.Request(UPSTREAM, data=json.dumps(body).encode(), method="POST",61 headers={"Content-Type": "application/json"}) # no Authorization: the gate adds it62 try:63 resp = OPENER.open(req, timeout=600)64 except urllib.error.HTTPError as e:65 resp = e # relay OpenRouter's own error (401 = no or bad key, 429 = rate limit)66 except (urllib.error.URLError, OSError) as e:67 return self._json(502, {"error": f"openrouter.ai unreachable ({type(e).__name__}); was the network granted?"})68 with resp:69 # Relay the answer as it arrives, so "stream": true works chunk by chunk.70 self.send_response(resp.status if hasattr(resp, "status") else resp.code)71 self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json"))72 self.send_header("Transfer-Encoding", "chunked")73 self.end_headers()74 try:75 while data := resp.read1(16384):76 self.wfile.write(b"%x\r\n%s\r\n" % (len(data), data))77 self.wfile.flush()78 self.wfile.write(b"0\r\n\r\n")79 except (BrokenPipeError, ConnectionResetError):80 self.close_connection = True818283def main():84 server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)85 server.daemon_threads = True86 with open(os.environ["M64_STATUS"], "a") as f:87 f.write(json.dumps({"phase": "ready", "detail": f"forwarding to {UPSTREAM_MODEL}"}) + "\n")88 server.serve_forever()899091if __name__ == "__main__":92 main()