Files
Effi/wol-proxy/app.py
Folzo 2a9f721c4e feat: LiteLLM multi-backends + jean-proxy avec WoL
Architecture (voir docs/ARCHITECTURE-LITELLM.md):
  open-webui -> LiteLLM:4000
                 |- qwen3.6-27b -> jean-proxy:3012 -> Jean (.11:8080)
                 |- llama3.1-8b -> wol-proxy:3011  -> Ollama (.11:11434)

Nouveaux fichiers:
- wol-proxy/ (versionne l'app.py de prod deja en place)
- jean-proxy/ (clone du wol-proxy avec HEALTH_PATH=/health + retry post-WoL
  pour services qui mettent du temps a demarrer apres WoL, ex: Jean qui
  charge 14 Go en VRAM)
- litellm/config.yaml (routing par nom de modele, drop_params=true)
- docker-compose.litellm.yml (override ADDITIF: ajoute uniquement jean-proxy
  et litellm, ne redemarre PAS open-webui/wol-proxy/hermes)

Teste en prod sur .27: Jean + Ollama repondent via LiteLLM:4000 avec
path-through de chat_template_kwargs.enable_thinking (raisonnement Qwen3.6
desactivable par requete). Zero impact hermes-webui/wol-proxy.
Basculer open-webui reste une etape manuelle (step 3 de la doc).
2026-06-27 16:19:08 +02:00

99 lines
3.3 KiB
Python

import os
import time
import socket
import logging
from urllib.parse import urljoin
import requests
from flask import Flask, request, Response
TARGET_HOST = os.getenv("TARGET_HOST", "192.168.1.10")
TARGET_PORT = int(os.getenv("TARGET_PORT", "11434"))
TARGET_MAC = os.getenv("TARGET_MAC", "02:76:C6:01:48:C8")
LISTEN_PORT = int(os.getenv("LISTEN_PORT", "3010"))
WAKE_TIMEOUT = int(os.getenv("WAKE_TIMEOUT", "60"))
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "5"))
BROADCAST_IP = os.getenv("BROADCAST_IP", "192.168.1.255") # ← broadcast dirigé
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)
app = Flask(__name__)
def send_wol(mac_address: str):
mac = mac_address.replace(":", "").replace("-", "")
if len(mac) != 12:
raise ValueError("MAC invalide: %s" % mac_address)
data = bytes.fromhex("FF" * 6 + mac * 16)
# Envoyer 3 paquets pour fiabilité
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))
log.info(f"[WoL] Paquet magic #{i+1} envoyé → MAC={mac_address} via {BROADCAST_IP}:9")
def ollama_alive() -> bool:
try:
url = f"http://{TARGET_HOST}:{TARGET_PORT}/api/version"
r = requests.get(url, timeout=2)
return r.ok
except Exception:
return False
def wait_for_ollama():
if ollama_alive():
log.info("[WoL] Ollama déjà UP, skip WoL")
return
log.info(f"[WoL] Ollama DOWN → envoi WoL vers {TARGET_MAC}")
send_wol(TARGET_MAC)
deadline = time.time() + WAKE_TIMEOUT
while time.time() < deadline:
remaining = int(deadline - time.time())
if ollama_alive():
log.info(f"[WoL] Ollama UP après réveil (reste {remaining}s)")
return
log.info(f"[WoL] En attente d'Ollama... ({remaining}s restantes)")
time.sleep(CHECK_INTERVAL)
raise RuntimeError("Ollama ne répond pas après WoL")
def proxy_request(path: str):
wait_for_ollama()
base = f"http://{TARGET_HOST}:{TARGET_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,
allow_redirects=False,
stream=True,
timeout=60,
)
except Exception as e:
return Response(str(e), status=502)
excluded = {"content-encoding", "content-length", "transfer-encoding", "connection"}
response_headers = [
(name, value)
for name, value in resp.headers.items()
if name.lower() not in excluded
]
return Response(
resp.raw,
status=resp.status_code,
headers=response_headers,
direct_passthrough=True,
)
@app.route("/", defaults={"path": ""}, methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
def handle(path):
return proxy_request(path)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=LISTEN_PORT)