motif64

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