motif64

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()