refactor: remplacer les proxies Jean par un unique llm-proxy + WOL
- llm-proxy: 1 service, 4 routes (3011 Ollama, 3012 llama.cpp, 3015 STT, 3016 TTS) - Wake-on-LAN integre avec anti-spam (3 paquets magic, timeout 300s) - suppression de jean-router, jean-proxy, wol-proxy, litellm - OpenWebUI repointe vers le proxy (OpenAI 3012 + Ollama 3011 + STT 3015) - docs a jour (README, WOL)
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir flask requests
|
||||
|
||||
COPY app.py .
|
||||
|
||||
EXPOSE 3011 3012 3015 3016
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
@@ -0,0 +1,165 @@
|
||||
import os
|
||||
import time
|
||||
import socket
|
||||
import logging
|
||||
import threading
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from flask import Flask, Response, request
|
||||
|
||||
PC_HOST = os.getenv("PC_HOST", "192.168.1.11")
|
||||
PC_MAC = os.getenv("PC_MAC", "")
|
||||
BROADCAST_IP = os.getenv("BROADCAST_IP", "192.168.1.255")
|
||||
WAKE_TIMEOUT = int(os.getenv("WAKE_TIMEOUT", "300"))
|
||||
SERVICE_TIMEOUT = int(os.getenv("SERVICE_TIMEOUT", "120"))
|
||||
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "5"))
|
||||
|
||||
ROUTES = [
|
||||
{"name": "ollama", "port": int(os.getenv("OLLAMA_PORT", "11434")),
|
||||
"listen": int(os.getenv("LISTEN_OLLAMA", "3011")), "health": "/api/version", "wol": True},
|
||||
{"name": "llamacpp", "port": int(os.getenv("LLAMACPP_PORT", "8080")),
|
||||
"listen": int(os.getenv("LISTEN_LLAMACPP", "3012")), "health": "/health", "wol": True},
|
||||
{"name": "stt", "port": int(os.getenv("STT_PORT", "8200")),
|
||||
"listen": int(os.getenv("LISTEN_STT", "3015")), "health": "/health", "wol": True},
|
||||
{"name": "tts", "host": os.getenv("TTS_HOST", "127.0.0.1"),
|
||||
"port": int(os.getenv("TTS_PORT", "8100")),
|
||||
"listen": int(os.getenv("LISTEN_TTS", "3016")), "health": "/v1/models", "wol": False},
|
||||
]
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
log = logging.getLogger("llm-proxy")
|
||||
|
||||
wol_lock = threading.Lock()
|
||||
wol_state = {"last_sent": 0.0}
|
||||
|
||||
METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
|
||||
|
||||
|
||||
def send_wol():
|
||||
mac = PC_MAC.replace(":", "").replace("-", "")
|
||||
if len(mac) != 12:
|
||||
raise ValueError("MAC invalide: %s" % PC_MAC)
|
||||
data = bytes.fromhex("FF" * 6 + mac * 16)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
for i in range(3):
|
||||
s.sendto(data, (BROADCAST_IP, 9))
|
||||
time.sleep(0.5)
|
||||
log.info(f"[WoL] 3 paquets magic envoyes vers {PC_MAC} via {BROADCAST_IP}:9")
|
||||
|
||||
|
||||
def service_alive(route) -> bool:
|
||||
host = route.get("host", PC_HOST)
|
||||
try:
|
||||
r = requests.get(f"http://{host}:{route['port']}{route['health']}", timeout=3)
|
||||
return r.ok
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def pc_alive() -> bool:
|
||||
return any(service_alive(r) for r in ROUTES if r["wol"])
|
||||
|
||||
|
||||
def wait_for(route, timeout: int) -> bool:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if service_alive(route):
|
||||
return True
|
||||
log.info(f"[{route['name']}] en attente du service... ({int(deadline - time.time())}s restantes)")
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
return service_alive(route)
|
||||
|
||||
|
||||
def ensure_up(route):
|
||||
name = route["name"]
|
||||
if service_alive(route):
|
||||
return
|
||||
if not route["wol"]:
|
||||
raise RuntimeError(f"service {name} inatteignable ({route.get('host', PC_HOST)}:{route['port']})")
|
||||
with wol_lock:
|
||||
if pc_alive():
|
||||
log.info(f"[WoL] PC deja UP, attente du service {name}")
|
||||
if not wait_for(route, SERVICE_TIMEOUT):
|
||||
raise RuntimeError(f"service {name} ne repond pas apres {SERVICE_TIMEOUT}s")
|
||||
return
|
||||
now = time.time()
|
||||
if now - wol_state["last_sent"] > WAKE_TIMEOUT:
|
||||
log.info(f"[WoL] PC DOWN -> envoi WOL vers {PC_MAC}")
|
||||
send_wol()
|
||||
wol_state["last_sent"] = now
|
||||
else:
|
||||
log.info(f"[WoL] reveil deja en cours, attente sans re-envoi")
|
||||
deadline = time.time() + WAKE_TIMEOUT
|
||||
while time.time() < deadline:
|
||||
if pc_alive():
|
||||
log.info(f"[WoL] PC UP apres reveil")
|
||||
break
|
||||
log.info(f"[WoL] en attente du PC... ({int(deadline - time.time())}s restantes)")
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
else:
|
||||
raise RuntimeError(f"PC ne repond pas apres {WAKE_TIMEOUT}s")
|
||||
if not wait_for(route, SERVICE_TIMEOUT):
|
||||
raise RuntimeError(f"service {name} ne repond pas apres le reveil du PC")
|
||||
|
||||
|
||||
def proxy_request(route, path: str):
|
||||
try:
|
||||
ensure_up(route)
|
||||
except Exception as e:
|
||||
return Response(str(e), status=503)
|
||||
host = route.get("host", PC_HOST)
|
||||
base = f"http://{host}:{route['port']}"
|
||||
upstream_url = urljoin(base + "/", path)
|
||||
headers = dict(request.headers)
|
||||
headers.pop("Host", None)
|
||||
try:
|
||||
resp = requests.request(
|
||||
method=request.method,
|
||||
url=upstream_url,
|
||||
headers=headers,
|
||||
data=request.get_data(),
|
||||
cookies=request.cookies,
|
||||
params=request.args,
|
||||
allow_redirects=False,
|
||||
stream=True,
|
||||
timeout=None,
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f"[{route['name']}] erreur upstream: {e}")
|
||||
return Response(str(e), status=502)
|
||||
|
||||
excluded = {"content-encoding", "content-length", "transfer-encoding", "connection"}
|
||||
response_headers = [
|
||||
(n, v) for n, v in resp.headers.items() if n.lower() not in excluded
|
||||
]
|
||||
return Response(
|
||||
resp.raw,
|
||||
status=resp.status_code,
|
||||
headers=response_headers,
|
||||
direct_passthrough=True,
|
||||
)
|
||||
|
||||
|
||||
def make_app(route):
|
||||
app = Flask(route["name"])
|
||||
|
||||
@app.route("/", defaults={"path": ""}, methods=METHODS)
|
||||
@app.route("/<path:path>", methods=METHODS)
|
||||
def handle(path):
|
||||
return proxy_request(route, path)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def serve(route):
|
||||
make_app(route).run(host="0.0.0.0", port=route["listen"], threaded=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for r in ROUTES:
|
||||
log.info(f"[{r['name']}] :{r['listen']} -> {r.get('host', PC_HOST)}:{r['port']} (wol={r['wol']})")
|
||||
threading.Thread(target=serve, args=(r,), daemon=True).start()
|
||||
while True:
|
||||
time.sleep(60)
|
||||
Reference in New Issue
Block a user