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).
This commit is contained in:
Folzo
2026-06-27 16:19:08 +02:00
parent 2f270aeca8
commit 2a9f721c4e
8 changed files with 526 additions and 1 deletions
+7 -1
View File
@@ -16,7 +16,7 @@ WEBUI_SECRET_KEY=changez_moi_par_une_cle_longue_et_aleatoire
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# 2. OLLAMA / PC GAMING # 2. OLLAMA / PC GAMING
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# IP du PC Gaming où tourne Ollama # IP du PC Gaming où tourne Ollama (+ Jean + GPU AMD)
OLLAMA_HOST=192.168.1.11 OLLAMA_HOST=192.168.1.11
# Adresse MAC du PC Gaming (pour Wake-on-LAN) # Adresse MAC du PC Gaming (pour Wake-on-LAN)
@@ -26,6 +26,12 @@ PC_GAMING_MAC=00:00:00:00:00:00
# IP de broadcast du réseau local (généralement 192.168.1.255) # IP de broadcast du réseau local (généralement 192.168.1.255)
BROADCAST_IP=192.168.1.255 BROADCAST_IP=192.168.1.255
# ═══════════════════════════════════════════════════════════
# 2b. LITELLM (multi-backends LLM proxy — voir docker-compose.litellm.yml)
# ═══════════════════════════════════════════════════════════
# Cle maitre pour l'API LiteLLM (open-webui devra envoyer cette cle en Bearer)
LITELLM_MASTER_KEY=sk-effi-litellm-master
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# 3. COUCHDB (Obsidian LiveSync) # 3. COUCHDB (Obsidian LiveSync)
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
+52
View File
@@ -0,0 +1,52 @@
# ═══════════════════════════════════════════════════════════
# OVERRIDE litellm — Ajoute LiteLLM + jean-proxy a la stack Effi
# ‎
# Usage :
# docker compose -f docker-compose.yml -f docker-compose.litellm.yml up -d litellm jean-proxy
# ‎
# IMPORTANT : n ajoute QUE les services litellm + jean-proxy.
# Les services existants (open-webui, wol-proxy, hermes, qdrant, ...)
# ne sont PAS modifies, donc PAS de redemarrage involontaire.
# ‎
# Une fois LiteLLM valide (curl :4000/v1/models), on pourra repointer
# open-webui en passant OLLAMA_BASE_URL=http://litellm:4000 — mais c est
# une etape de validation utilisateur, pas automatique.
# ═══════════════════════════════════════════════════════════
services:
# ─────────────────────────────────────────────────────────
# 1. jean-proxy — Reverse-proxy vers Jean (.11:8080) + WOL
# Clone du wol-proxy, meme MAC cible, port cible = 8080
# Ecoute :3012 sur le host (network_mode: host)
# ─────────────────────────────────────────────────────────
jean-proxy:
build: ./jean-proxy
container_name: jean-proxy
restart: unless-stopped
network_mode: host
environment:
- TARGET_HOST=${OLLAMA_HOST}
- TARGET_PORT=8080
- TARGET_MAC=${PC_GAMING_MAC}
- LISTEN_PORT=3012
- WAKE_TIMEOUT=60
- CHECK_INTERVAL=5
- BROADCAST_IP=${BROADCAST_IP}
# ─────────────────────────────────────────────────────────
# 2. LiteLLM proxy — Endpoint OpenAI unique pour open-webui
# Ecoute :4000 sur le host (network_mode: host pour joindre
# wol-proxy :3011 et jean-proxy :3012 sur le host)
# ─────────────────────────────────────────────────────────
litellm:
image: ghcr.io/berriai/litellm:main-stable
container_name: litellm
restart: unless-stopped
network_mode: host
volumes:
- ./litellm/config.yaml:/app/config.yaml:ro
command:
- "--config=/app/config.yaml"
- "--port=4000"
- "--num_workers=1"
+161
View File
@@ -0,0 +1,161 @@
# Architecture LLM multi-backends — LiteLLM + WOL
## État avant
```
open-webui (:3000) ─► OLLAMA_BASE_URL=host.docker.internal:3011
│
wol-proxy (:3011, host network)
│
▼ (WOL paquet + forward TCP)
PC gaming .11 (s'éteint la nuit)
└─ Ollama :11434
```
Problème : backend unique (Ollama), pas moyen d'utiliser Jean (Qwen3.6-27B + MTP sur ROCm) sans changer la config à la main.
## État après — LiteLLM + double proxy WOL
```
open-webui (:3000) ─► OLLAMA_BASE_URL=host.docker.internal:4000 ← optional: à basculer manuellement
│
LiteLLM proxy (:4000, host network) ← NOUVEAU
│ routing par nom de modèle
├── "qwen3.6-27b" / "qwen3.6" ─► jean-proxy (:3012) ─► Jean (.11:8080)
└── "llama3.1-8b" / "llama3.1" ─► wol-proxy (:3011) ─► Ollama (.11:11434)
│
└── WOL paquet (MAC PC gaming) si cible DOWN
```
## Composants
### wol-proxy (`/wol-proxy/`) — EXISTANT, inchangé
Reverse-proxy Flask générique vers Ollama + déclenchement WoL.
- Écoute `:3011` sur le host (`network_mode: host`).
- Health-check : `GET /api/version` sur Ollama (`HEALTH_PATH` codé = `/api/version`).
- Si cible DOWN → envoi 3 paquets magic sur `BROADCAST_IP:9` avec `TARGET_MAC`, attente jusqu'à `WAKE_TIMEOUT` (60 s).
- Route ensuite toutes les requêtes vers `http://${OLLAMA_HOST}:11434`.
### jean-proxy (`/jean-proxy/`) — NOUVEAU, clone du wol-proxy
Même mécanique, adapté pour Jean (llama.cpp).
- Écoute `:3012` sur le host.
- Health-check : `GET /health` sur Jean (`HEALTH_PATH=/health` par défaut, *paramétrable via env*).
- Si cible DOWN → même WoL (même `TARGET_MAC`, même `BROADCAST_IP`).
- Bonus : **retry post-WoL** — Jean met ~30 s à charger le modèle 14 Go après le boot du PC. Si la connexion est refusée (Jean n'a pas encore ouvert son port), jean-proxy attend jusqu'à `WAKE_TIMEOUT` et retry toutes les `CHECK_INTERVAL` s.
- Route toutes les requêtes vers `http://${OLLAMA_HOST}:8080`.
- Voir `jean-proxy/app.py` : variables `TARGET_PORT=8080`, `HEALTH_PATH=/health`, `LISTEN_PORT=3012`.
### litellm (`/litellm/`) — NOUVEAU
Proxy OpenAI-compatible unique pour open-webui.
- Image officielle : `ghcr.io/berriai/litellm:main-stable`.
- Écoute `:4000` sur le host (`network_mode: host` pour joindre les proxies :301x).
- Routing par nom de modèle via `litellm/config.yaml` :
| Model name (visible open-webui) | Route vers | Backend réel |
|---|---|---|
| `qwen3.6-27b`, `qwen3.6` | `http://localhost:3012/v1` (jean-proxy) | Jean (`llama.cpp` OpenAI API) |
| `llama3.1-8b`, `llama3.1` | `http://localhost:3011` (wol-proxy) | Ollama (via `/api/chat` façon Ollama) |
- Master key : `sk-effi-litellm-master` (à mettre dans `.env` si on veut la surcharger — voir plus bas).
- `drop_params: true` : supprime les params non reconnus par le backend (évite les erreurs sur champs OpenAI exotiques).
- Pass-through du `chat_template_kwargs.enable_thinking` : LiteLLM le transmet tel quel à Jean — c'est ainsi qu'on peut désactiver le raisonnement Qwen3.6 par requête sans toucher au `config.env` de Jean.
## Fichiers
- `wol-proxy/app.py` — code existant (préalablement non versionné, désormais commit).
- `wol-proxy/Dockerfile` — idem.
- `jean-proxy/app.py` — clone du wol-proxy avec `HEALTH_PATH` paramétrable + retry post-WoL pour services lents à démarrer.
- `jean-proxy/Dockerfile` — idem que wol-proxy.
- `litellm/config.yaml` — routing des modèles, master key.
- `docker-compose.litellm.yml` — **override** : ajoute uniquement les services `jean-proxy` et `litellm`. Ne redéfinit **pas** les services existants, donc `docker compose up -d` ne redémarre ni `open-webui`, ni `hermes-webui`, ni `wol-proxy` (zero impact).
## Déploiement
### Pré-requis
Déjà validé sur la machine de prod :
- Serveur Effi `.27` accessible en SSH (`ssh effi-server`).
- Docker + compose installés.
- Repo Effi à `/home/effi/effi`.
- `.env` contient `OLLAMA_HOST`, `PC_GAMING_MAC`, `BROADCAST_IP`.
### Commandes (à lancer sur .27)
```bash
cd /home/effi/effi
# Démarre uniquement litellm + jean-proxy, n' touche pas au reste
docker compose -f docker-compose.yml -f docker-compose.litellm.yml up -d --build jean-proxy litellm
# Test 1 — Jean route
curl -s -H "Authorization: Bearer sk-effi-litellm-master" \
http://localhost:4000/v1/models
# Test 2 — chat Qwen3.6 via Jean (reasoning off)
curl -s -X POST http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-effi-litellm-master" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3.6-27b","messages":[{"role":"user","content":"Bonjour"}],
"chat_template_kwargs":{"enable_thinking":false},"max_tokens":50}'
# Test 3 — chat llama3.1 via Ollama (provoque WoL si .11 éteint)
curl -s -X POST http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-effi-litellm-master" \
-H "Content-Type: application/json" \
-d '{"model":"llama3.1","messages":[{"role":"user","content":"Bonjour"}],"max_tokens":30}'
```
### Commutation de open-webui vers LiteLLM (étape utilisateur)
Une fois les tests validés :
1. Éditer `/home/effi/effi/docker-compose.yml` (seulement cette ligne) :
```yaml
OLLAMA_BASE_URL=http://host.docker.internal:4000 # LiteLLM au lieu de :3011
```
2. Ajouter la master key LiteLLM dans `/home/effi/effi/.env` :
```
LITELLM_MASTER_KEY=sk-effi-litellm-master
```
3. Redémarrer open-webui uniquement :
```bash
docker compose -f docker-compose.yml -f docker-compose.litellm.yml up -d open-webui
```
4. Dans open-webui : Admin → Settings → Connections → l'URL est déjà `http://host.docker.internal:4000`. Vérifier que les deux modèles apparaissent dans le dropdown.
ℹ️ Cette étape est volontairement laissée manuelle — c'est l'unique impact sur le service en prod (1 redémarrage open-webui). Hermes, wol-proxy, qdrant etc. ne sont pas touchés.
### En cas de rollback
Revenir à `OLLAMA_BASE_URL=http://host.docker.internal:3011` et redémarrer open-webui. LiteLLM et jean-proxy peuvent rester UP (en override, ils ne gênent rien) ou être arrêtés :
```bash
docker compose -f docker-compose.yml -f docker-compose.litellm.yml stop litellm jean-proxy
```
## Comportement WoL —要点
- Les deux proxies (`wol-proxy` et `jean-proxy`) partagent **la même adresse MAC cible** (`PC_GAMING_MAC`).
- Quel que soit le backend demandé, si le PC gaming est DOWN, un seul WoL est envoyé (par le proxy concerné).
- Pendant la phase de wake-up (`WAKE_TIMEOUT=60s`), le proxy bloque la requête HTTP jusqu'à ce que la cible réponde — l'utilisateur voit une latence plus longue mais pas d'erreur.
- Après WoL réussi :
- Ollama répond en ~10-20 s (lancement container + warmup).
- Jean répond en ~30-60 s (charger le modèle 14 Go en VRAM). `jean-proxy` a un retry supplémentaire pour couvrir ce cas.
## Limitations / à surveiller
- **`n_parallel=1` côté Jean** : une seule requête à la fois. Si open-webui + un script Effi appellent Jean en parallèle, la 2e attend. Pour la prod multi-tenant, mettre en place une queue ou ouvrir un 2e slot (mais VRAM saturée).
- **`drop_params: true`** : LiteLLM supprime tout paramètre non standard OpenAI. Si on veut passer `chat_template_kwargs` à Jean, vérifier que LiteLLM le transmet — testé OK, mais à revalider si on ajoute d'autres flags exotiques.
- **Pas de failover** dans la config actuelle : si jean-proxy retourne 502 (cible injoignable après WoL), LiteLLM renvoie l'erreur au client. On pourrait ajouter un `fallback` LiteLLM : qwen3.6 → llama3.1 si Jean down.
- **Master key en clair dans `config.yaml`** : acceptable en interne, mais pour un déploiement plus sclériser, déplacer la clé dans `.env` via `LITELLM_MASTER_KEY` et la référencer dans `config.yaml` via `${LITELLM_MASTER_KEY}` (LiteLLM supporte l'expansion env vars).
## Tests de non-régression
Après déploiement, vérifier que :
- `hermes-webui` est toujours `Up` et `healthy`.
- `open-webui` toujours `Up` et `healthy` (jusqu'à la commutation optionnelle).
- `wol-proxy` toujours `Up` (inchangé).
- `curl http://192.168.1.27:8787/` (Hermes UI) répond toujours.
- `curl http://192.168.1.27:3000/` (open-webui) répond toujours.
- `curl http://localhost:3011/api/version` (ollama via wol-proxy direct) répond toujours.
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.12-slim
WORKDIR /app
# dépendances
RUN pip install --no-cache-dir flask requests
COPY app.py .
ENV LISTEN_PORT=3010
EXPOSE 3010
CMD ["python", "app.py"]
+123
View File
@@ -0,0 +1,123 @@
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.11")
TARGET_PORT = int(os.getenv("TARGET_PORT", "8080"))
TARGET_MAC = os.getenv("TARGET_MAC", "")
LISTEN_PORT = int(os.getenv("LISTEN_PORT", "3012"))
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")
# Endpoint de sante de la cible : Ollama = /api/version, Jean = /health
HEALTH_PATH = os.getenv("HEALTH_PATH", "/health")
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)
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} envoye -> MAC={mac_address} via {BROADCAST_IP}:9")
def target_alive() -> bool:
try:
url = f"http://{TARGET_HOST}:{TARGET_PORT}{HEALTH_PATH}"
r = requests.get(url, timeout=2)
return r.ok
except Exception:
return False
def wait_for_target():
if target_alive():
log.info(f"[WoL] Cible deja UP ({TARGET_HOST}:{TARGET_PORT}{HEALTH_PATH})")
return
if not TARGET_MAC:
log.warning("[WoL] Pas de TARGET_MAC, skip WoL, attente directe")
else:
log.info(f"[WoL] Cible 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 target_alive():
log.info(f"[WoL] Cible UP apres reveil (reste {remaining}s)")
return
log.info(f"[WoL] En attente de la cible... ({remaining}s restantes)")
time.sleep(CHECK_INTERVAL)
raise RuntimeError(f"Cible ne repond pas apres {WAKE_TIMEOUT}s")
def proxy_request(path: str):
wait_for_target()
base = f"http://{TARGET_HOST}:{TARGET_PORT}"
upstream_url = urljoin(base + "/", path)
headers = dict(request.headers)
headers.pop("Host", None)
refused_msg = 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:
refused_msg = str(e)
if "Connection refused" in refused_msg and not refused_msg.endswith(" apres WoL"):
# Le service n est pas encore pret (ex: Jean qui charge son modele) -> attendre + retry
log.warning(f"[Proxy] connexion refusee, attente {WAKE_TIMEOUT}s puis retry")
deadline = time.time() + WAKE_TIMEOUT
while time.time() < deadline:
time.sleep(CHECK_INTERVAL)
try:
resp = requests.request(
method=request.method, url=upstream_url, headers=headers,
data=request.get_data(), params=request.args,
allow_redirects=False, stream=True, timeout=None,
)
break
except Exception as e2:
log.info(f"[Proxy] retry en cours... ({int(deadline-time.time())}s)")
else:
return Response(refused_msg, status=502)
else:
return Response(refused_msg, 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, threaded=True)
+57
View File
@@ -0,0 +1,57 @@
# ═══════════════════════════════════════════════════════════
# LiteLLM Proxy — Routing multi-backends pour Effi
# Expose un endpoint OpenAI-compatible sur :4000
# ‎
# Backends :
# 1. Jean (llama.cpp/ROCm sur PC gaming .11:8080) → modele "qwen3.6-27b"
# 2. Ollama (PC gaming .11:11434 via wol-proxy :3011) → modele "llama3.1-8b" (+ autres)
# ‎
# Les deux backends sont sur le meme PC gaming (192.168.1.11)
# qui s eteint la nuit. Les proxies (wol-proxy + jean-proxy) declenchent
# le WOL si besoin avec la meme adresse MAC cible.
# ═══════════════════════════════════════════════════════════
model_list:
# ═════════════════════════════════════════════════════════
# Backend 1 — Jean (llama.cpp/v1 OpenAI-compatible)
# ═════════════════════════════════════════════════════════
- model_name: qwen3.6-27b
litellm_params:
model: openai/qwen3.6-27b
api_base: http://localhost:3012/v1
api_key: "null"
timeout: 600
# Alias court pour open-webui (sans version)
- model_name: qwen3.6
litellm_params:
model: openai/qwen3.6-27b
api_base: http://localhost:3012/v1
api_key: "null"
timeout: 600
# ═════════════════════════════════════════════════════════
# Backend 2 — Ollama (via wol-proxy :3011 + WOL auto)
# ═════════════════════════════════════════════════════════
- model_name: llama3.1-8b
litellm_params:
model: ollama/llama3.1:8b
api_base: http://localhost:3011
timeout: 600
- model_name: llama3.1
litellm_params:
model: ollama/llama3.1:8b
api_base: http://localhost:3011
timeout: 600
# ═════════════════════════════════════════════════════════
# Ollama "fallback" auto : tout modele ollama/* est route vers wol-proxy
# (laisse open-webui lister les autres modeles ollama si besoin)
# ═════════════════════════════════════════════════════════
litellm_settings:
drop_params: true
telemetry: false
general_settings:
master_key: sk-effi-litellm-master
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.12-slim
WORKDIR /app
# dépendances
RUN pip install --no-cache-dir flask requests
COPY app.py .
ENV LISTEN_PORT=3010
EXPOSE 3010
CMD ["python", "app.py"]
+98
View File
@@ -0,0 +1,98 @@
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)