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:
@@ -10,8 +10,8 @@ Effi est un assistant IA personnel **100% auto-hébergé**, avec mémoire persis
|
|||||||
|
|
||||||
| Machine | IP | Rôle | Services |
|
| Machine | IP | Rôle | Services |
|
||||||
|---------|-----|------|----------|
|
|---------|-----|------|----------|
|
||||||
| **effi-server** | `192.168.1.27` | Serveur Docker principal | OpenWebUI, Qdrant, n8n, jean-router |
|
| **effi-server** | `192.168.1.27` | Serveur Docker principal | OpenWebUI, Qdrant, n8n, llm-proxy, TTS |
|
||||||
| **folzo-Corp** | `192.168.1.11` | LLM + STT + TTS + Gen | Jean, Ollama, ComfyUI, effi-stt, openedai-speech, kreuzberg |
|
| **folzo-Corp** | `192.168.1.11` | Station GPU (LLM + STT) | llama.cpp, Ollama, ComfyUI, effi-stt, kreuzberg |
|
||||||
| **NAS Synology** | `192.168.1.25` | Monitoring | Beszel Hub |
|
| **NAS Synology** | `192.168.1.25` | Monitoring | Beszel Hub |
|
||||||
|
|
||||||
### Schéma Global
|
### Schéma Global
|
||||||
@@ -24,33 +24,26 @@ Effi est un assistant IA personnel **100% auto-hébergé**, avec mémoire persis
|
|||||||
│ │ │
|
│ │ │
|
||||||
│ Monitoring │ Monitoring │ Monitoring
|
│ Monitoring │ Monitoring │ Monitoring
|
||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
┌─────────────────────────────────┐ ┌─────────────────────────────────────────┐
|
┌───────────────────────────────────────┐ ┌─────────────────────────────────────────┐
|
||||||
│ EFFI SERVER (192.168.1.27) │ │ FOLZO-CORP (192.168.1.11) │
|
│ EFFI SERVER (192.168.1.27) │ │ FOLZO-CORP (192.168.1.11) │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ ┌──────────────┐ ┌──────────┐ │ │ ┌──────────────┐ ┌──────────────┐ │
|
│ ┌──────────────┐ ┌────────────────┐ │ │ ┌──────────────┐ ┌──────────────┐ │
|
||||||
│ │ Open WebUI │ │ Qdrant │ │ │ │ Jean │ │ Ollama │ │
|
│ │ Open WebUI │ │ llm-proxy │ │ │ │ llama.cpp │ │ Ollama │ │
|
||||||
│ │ Port 3000 │ │ Port 6333│ │ │ │ Port 8080 │ │ Port 11434 │ │
|
│ │ Port 3000 │ │ :3011 Ollama │ │ │ │ Port 8080 │ │ Port 11434 │ │
|
||||||
│ └──────────────┘ └──────────┘ │ │ │ llama.cpp │ │ Vision │ │
|
│ └──────────────┘ │ :3012 llama.cpp│ │ │ │ (modèle │ │ (vision, │ │
|
||||||
│ │ │ │ Ornith 35B │ │ mistral, │ │
|
│ ┌──────────────┐ │ :3015 STT │ │ │ │ principal) │ │ embeddings) │ │
|
||||||
│ ┌──────────────┐ ┌──────────┐ │ │ │ Qwen3.6... │ │ qwen3.5:9b │ │
|
│ │ Qdrant │ │ :3016 TTS │ │ │ └──────────────┘ └──────────────┘ │
|
||||||
│ │ n8n │ │jean-router│ │ │ └──────────────┘ └──────────────┘ │
|
│ │ Port 6333 │ │ + Wake-on-LAN │ │ │ │
|
||||||
│ │ Port 5678 │ │ Port 3014│ │ │ │
|
│ └──────────────┘ └────────────────┘ │ │ ┌──────────────┐ ┌──────────────┐ │
|
||||||
│ └──────────────┘ └──────────┘ │ │ ┌──────────────┐ ┌──────────────┐ │
|
|
||||||
│ │ │ │ ComfyUI │ │ effi-stt │ │
|
│ │ │ │ ComfyUI │ │ effi-stt │ │
|
||||||
│ ┌──────────────┐ ┌──────────┐ │ │ │ Port 8188 │ │ Port 8200 │ │
|
│ ┌──────────────┐ ┌──────────────┐ │ │ │ Port 8188 │ │ Port 8200 │ │
|
||||||
│ │ Beszel Agent │ │ Watchtower│ │ │ │ Image/Audio │ │ Whisper STT │ │
|
│ │ n8n │ │ openedai │ │ │ │ Image/Audio │ │ Whisper STT │ │
|
||||||
│ └──────────────┘ └──────────┘ │ │ └──────────────┘ └──────────────┘ │
|
│ │ Port 5678 │ │ TTS :8100 │ │ │ └──────────────┘ └──────────────┘ │
|
||||||
└─────────────────────────────────┘ │ │
|
│ └──────────────┘ └──────────────┘ │ │ │
|
||||||
│ ┌──────────────┐ ┌──────────────┐ │
|
└───────────────────────────────────────┘ └─────────────────────────────────────────┘
|
||||||
│ │ openedai │ │ kreuzberg │ │
|
|
||||||
│ │ Port 8100 │ │ Port 8300 │ │
|
|
||||||
│ │ TTS │ │ Doc Extract │ │
|
|
||||||
│ └──────────────┘ └──────────────┘ │
|
|
||||||
│ RX 7800 XT (VRAM 16GB) │
|
|
||||||
└─────────────────────────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note :** jean-router gère le routing dynamique entre Jean et Ollama avec gestion automatique de la VRAM partagée. Voir [ARCHITECTURE-JEAN-ROUTER.md](docs/ARCHITECTURE-JEAN-ROUTER.md) pour les détails.
|
**Note :** `llm-proxy` est le point d'entrée unique vers la station GPU. Il fait du Wake-on-LAN : si le PC est éteint, il envoie un paquet magic avant d'attendre que le service cible soit prêt. Voir [WOL et démarrage auto](./docs/WOL-ET-DEMARRAGE-AUTO.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -60,16 +53,13 @@ Effi est un assistant IA personnel **100% auto-hébergé**, avec mémoire persis
|
|||||||
|
|
||||||
| Service | Port | Image | Rôle | RAM/VRAM | Auto-unload |
|
| Service | Port | Image | Rôle | RAM/VRAM | Auto-unload |
|
||||||
|---------|------|-------|------|----------|-------------|
|
|---------|------|-------|------|----------|-------------|
|
||||||
| **Jean** | 8080 | Native | LLM inference (llama.cpp, presets dynamiques) | ~28GB | Arrêt via API |
|
| **llama.cpp** | 8080 | Native | LLM principal (qwen3.8-27b) | ~28GB | Démarré au boot |
|
||||||
| **Ollama** | 11434 | Native | LLM vision (mistral, qwen3.5:9b) | ~4-6GB | 5min après dernier usage |
|
| **Ollama** | 11434 | Native | LLM vision + embeddings | ~4-6GB | 5min après dernier usage |
|
||||||
| **ComfyUI** | 8188 | `yanwk/comfyui-boot:rocm` | Image + Audio generation | 18GB shm | ✅ après 10min idle |
|
| **ComfyUI** | 8188 | `yanwk/comfyui-boot:rocm` | Image + Audio generation | 18GB shm | ✅ après 10min idle |
|
||||||
| **effi-stt** | 8200 | Build local | Whisper STT | ~20MB | ✅ après 5min idle |
|
| **effi-stt** | 8200 | Build local | Whisper STT | ~20MB | ✅ après 5min idle |
|
||||||
| **openedai-speech** | 8100 | `ghcr.io/matatonic/openedai-speech` | TTS | ~15MB | Non (GPU) |
|
|
||||||
| **kreuzberg** | 8300 | `ghcr.io/kreuzberg-dev/kreuzberg:latest` | Document extraction | ~5MB | ✅ après 10min idle |
|
| **kreuzberg** | 8300 | `ghcr.io/kreuzberg-dev/kreuzberg:latest` | Document extraction | ~5MB | ✅ après 10min idle |
|
||||||
| **beszel-agent** | - | `henrygd/beszel-agent:latest` | Monitoring | ~5MB | Auto-start |
|
| **beszel-agent** | - | `henrygd/beszel-agent:latest` | Monitoring | ~5MB | Auto-start |
|
||||||
|
|
||||||
**Note :** Jean et Ollama partagent la VRAM (RX 7800 XT, 16GB). jean-router gère automatiquement le switch pour éviter la contention mémoire.
|
|
||||||
|
|
||||||
### EffiServer (192.168.1.27)
|
### EffiServer (192.168.1.27)
|
||||||
|
|
||||||
| Service | Port | Image | Rôle |
|
| Service | Port | Image | Rôle |
|
||||||
@@ -77,9 +67,8 @@ Effi est un assistant IA personnel **100% auto-hébergé**, avec mémoire persis
|
|||||||
| **Open WebUI** | 3000 | `ghcr.io/open-webui/open-webui:main` | Interface IA |
|
| **Open WebUI** | 3000 | `ghcr.io/open-webui/open-webui:main` | Interface IA |
|
||||||
| **Qdrant** | 6333 | `qdrant/qdrant:latest` | Vector DB (RAG) |
|
| **Qdrant** | 6333 | `qdrant/qdrant:latest` | Vector DB (RAG) |
|
||||||
| **n8n** | 5678 | `docker.n8n.io/n8nio/n8n:latest` | Automatisation |
|
| **n8n** | 5678 | `docker.n8n.io/n8nio/n8n:latest` | Automatisation |
|
||||||
| **jean-router** | 3014 | Build local | Routing dynamique Jean/Ollama + gestion VRAM |
|
| **llm-proxy** | 3011/3012/3015/3016 | Build local | Proxy unique + Wake-on-LAN |
|
||||||
| **jean-proxy** | 3012 | Build local | Wake-on-LAN + Jean proxy |
|
| **openedai-speech** | 8100 | `ghcr.io/matatonic/openedai-speech` | TTS |
|
||||||
| **wol-proxy** | 3011 | Build local | Wake-on-LAN + Ollama proxy |
|
|
||||||
| **beszel-agent** | - | `henrygd/beszel-agent:latest` | Monitoring |
|
| **beszel-agent** | - | `henrygd/beszel-agent:latest` | Monitoring |
|
||||||
|
|
||||||
### NAS Synology (192.168.1.25)
|
### NAS Synology (192.168.1.25)
|
||||||
@@ -94,35 +83,24 @@ Effi est un assistant IA personnel **100% auto-hébergé**, avec mémoire persis
|
|||||||
|
|
||||||
### 💬 Chat IA
|
### 💬 Chat IA
|
||||||
- **Interface**: Open WebUI
|
- **Interface**: Open WebUI
|
||||||
- **LLM principal**: Jean (llama.cpp) avec presets dynamiques
|
- **LLM principal**: llama.cpp (qwen3.8-27b) via `llm-proxy:3012`
|
||||||
- **ornith-35b-q6** (35B, Q6_K, reasoning) — modèle principal
|
- **LLM vision / embeddings**: Ollama via `llm-proxy:3011`
|
||||||
- **qwen3.6-27b-mtp** (27B, IQ4_XS, reasoning, MTP)
|
- **Wake-on-LAN automatique**: le proxy réveille la station GPU si elle est éteinte
|
||||||
- **qwen3.6-35b-moe-compact** (35B MoE, compact)
|
|
||||||
- **qwen3.6-35b-moe-compact-reason** (35B MoE, reasoning)
|
|
||||||
- **qwen3.6-35b-moe-nano** (35B MoE, nano)
|
|
||||||
- **LLM vision**: Ollama (mistral, qwen3.5:9b) pour les tâches avec images
|
|
||||||
- **Routing automatique**: jean-router détecte le modèle demandé et route vers Jean ou Ollama
|
|
||||||
- **Gestion VRAM**: Switch automatique entre Jean et Ollama pour éviter la contention mémoire
|
|
||||||
|
|
||||||
**Note :** Nouveaux presets Jean apparaissent automatiquement dans OpenWebUI sans configuration. Voir [ARCHITECTURE-JEAN-ROUTER.md](docs/ARCHITECTURE-JEAN-ROUTER.md) pour les détails.
|
### 🔌 llm-proxy — Proxy unique + Wake-on-LAN
|
||||||
|
- **4 routes** : Ollama (`:3011`), llama.cpp (`:3012`), STT (`:3015`), TTS (`:3016`)
|
||||||
### 🚀 jean-router — Routing dynamique LLM
|
- **Wake-on-LAN** : si le PC est éteint, envoie 3 paquets magic (MAC `04:D9:F5:D1:EC:DF`) puis attend le service
|
||||||
- **Découverte automatique**: Interroge Jean et Ollama pour lister les modèles disponibles
|
- **Anti-spam WOL** : un seul réveil en cours à la fois, pas de re-envoi pendant l'attente
|
||||||
- **Routing intelligent**: Route vers Jean (presets) ou Ollama (vision) selon le modèle demandé
|
- **API OpenAI-compatible** : `/v1/models` et `/v1/chat/completions` sur les routes LLM
|
||||||
- **Switch automatique**: Change de preset Jean si nécessaire (via API web)
|
- **Streaming** : passthrough des réponses SSE
|
||||||
- **Gestion VRAM**:
|
|
||||||
- Arrête Jean quand on passe sur Ollama (libère VRAM)
|
|
||||||
- Garde Ollama en mémoire 5 min avant déchargement automatique
|
|
||||||
- Évite la contention mémoire entre les deux backends
|
|
||||||
- **API OpenAI-compatible**: `/v1/models` et `/v1/chat/completions`
|
|
||||||
|
|
||||||
### 🎤 Pipeline Vocal
|
### 🎤 Pipeline Vocal
|
||||||
- **STT**: Whisper large-v3 via effi-stt (CPU, déchargement auto)
|
- **STT**: Whisper large-v3 via effi-stt (CPU, déchargement auto)
|
||||||
- **TTS**: XTTS v2 via openedai-speech (GPU ROCm)
|
- **TTS**: openedai-speech (serveur, voix Piper + XTTS)
|
||||||
|
|
||||||
### 🖼️ Génération Images/Audio
|
### 🖼️ Génération Images/Audio
|
||||||
- **ComfyUI**: Workflows pour image et audio
|
- **ComfyUI**: Workflows pour image et audio
|
||||||
- **Modèle audio**: ACE-Step ( génération musicale)
|
- **Modèle audio**: ACE-Step (génération musicale)
|
||||||
- **Modèles image**: Multiples checkpoints disponibles
|
- **Modèles image**: Multiples checkpoints disponibles
|
||||||
|
|
||||||
### 📄 Extraction Documents
|
### 📄 Extraction Documents
|
||||||
@@ -148,12 +126,6 @@ Outil custom pour interroger Beszel depuis OpenWebUI via Effi.
|
|||||||
- `get_system_stats` → retourne CPU, RAM, GPU (%)
|
- `get_system_stats` → retourne CPU, RAM, GPU (%)
|
||||||
- Données temps réel depuis `info` (cpu, mp=gpu, g=gpu, dt=timestamp)
|
- Données temps réel depuis `info` (cpu, mp=gpu, g=gpu, dt=timestamp)
|
||||||
|
|
||||||
**Améliorations futures** (pas encore implémentées):
|
|
||||||
- Support historique: données des N dernières minutes
|
|
||||||
- Filtres par période
|
|
||||||
- Graphiques/Métriques avancés
|
|
||||||
- Alertes automatisées via le tool
|
|
||||||
|
|
||||||
**Documentation**: [Tool Beszel OpenWebUI](./docs/TOOL-BESZEL-OPENWEBUI.md)
|
**Documentation**: [Tool Beszel OpenWebUI](./docs/TOOL-BESZEL-OPENWEBUI.md)
|
||||||
|
|
||||||
### Scripts Memory
|
### Scripts Memory
|
||||||
@@ -166,22 +138,14 @@ Outil custom pour interroger Beszel depuis OpenWebUI via Effi.
|
|||||||
|
|
||||||
### Commandes Utiles
|
### Commandes Utiles
|
||||||
|
|
||||||
| Script | Condition | Action |
|
|
||||||
|--------|-----------|--------|
|
|
||||||
| comfyui-memory-manager.py | 10min idle + VRAM<1% + RAM>5GB | Restart container |
|
|
||||||
| kreuzberg-memory-manager.py | 10min idle + RAM>2GB | Restart container |
|
|
||||||
| effi-stt | 5min idle | Déchargement modèle Whisper |
|
|
||||||
|
|
||||||
### Commandes Utiles
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Statut services
|
# Statut services
|
||||||
sudo systemctl status comfyui-memory-manager kreuzberg-memory-manager
|
|
||||||
docker ps
|
docker ps
|
||||||
|
docker logs -f llm-proxy
|
||||||
|
|
||||||
# Logs
|
# Tester le proxy (PC allumé)
|
||||||
journalctl -u comfyui-memory-manager -f
|
curl http://192.168.1.27:3012/health
|
||||||
docker logs -f comfyui
|
curl http://192.168.1.27:3011/api/version
|
||||||
|
|
||||||
# Métriques Beszel
|
# Métriques Beszel
|
||||||
python3 scripts/beszel_client.py --system folzocorp --check stats
|
python3 scripts/beszel_client.py --system folzocorp --check stats
|
||||||
@@ -191,8 +155,7 @@ python3 scripts/beszel_client.py --system folzocorp --check stats
|
|||||||
|
|
||||||
## 📚 Documentation
|
## 📚 Documentation
|
||||||
|
|
||||||
- [Architecture jean-router](./docs/ARCHITECTURE-JEAN-ROUTER.md) — Routing dynamique LLM + gestion VRAM
|
- [WOL et démarrage auto](./docs/WOL-ET-DEMARRAGE-AUTO.md)
|
||||||
- [Session 2026-07-20](./docs/05-journal/SESSION-2026-07-20-JEAN-ROUTER.md) — Création de jean-router
|
|
||||||
- [Installation Beszel](./docs/BESZEL-INSTALLATION.md)
|
- [Installation Beszel](./docs/BESZEL-INSTALLATION.md)
|
||||||
- [Gestion Mémoire](./docs/MEMOIRE-GESTION.md)
|
- [Gestion Mémoire](./docs/MEMOIRE-GESTION.md)
|
||||||
- [Session 2026-04-12](./docs/SESSION-2026-04-12-RAM-BESZEL.md)
|
- [Session 2026-04-12](./docs/SESSION-2026-04-12-RAM-BESZEL.md)
|
||||||
@@ -205,11 +168,11 @@ python3 scripts/beszel_client.py --system folzocorp --check stats
|
|||||||
|
|
||||||
| Domaine | Technologies |
|
| Domaine | Technologies |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| **LLM** | Jean (llama.cpp), Ollama (vision) |
|
| **LLM** | llama.cpp (principal), Ollama (vision + embeddings) |
|
||||||
| **Routing** | jean-router (Flask, routing dynamique + gestion VRAM) |
|
| **Proxy / WOL** | llm-proxy (Flask, proxy multi-cibles + Wake-on-LAN) |
|
||||||
| **Interface** | Open WebUI |
|
| **Interface** | Open WebUI |
|
||||||
| **STT** | Faster Whisper (CPU) |
|
| **STT** | Faster Whisper (CPU) |
|
||||||
| **TTS** | XTTS v2 (ROCm GPU) |
|
| **TTS** | openedai-speech (Piper + XTTS) |
|
||||||
| **Image/Audio** | ComfyUI + ACE-Step |
|
| **Image/Audio** | ComfyUI + ACE-Step |
|
||||||
| **Vector DB** | Qdrant |
|
| **Vector DB** | Qdrant |
|
||||||
| **Document** | Kreuzberg (docling) |
|
| **Document** | Kreuzberg (docling) |
|
||||||
@@ -223,6 +186,10 @@ python3 scripts/beszel_client.py --system folzocorp --check stats
|
|||||||
|
|
||||||
```
|
```
|
||||||
effi/
|
effi/
|
||||||
|
├── llm-proxy/ # Proxy unique + Wake-on-LAN
|
||||||
|
│ ├── app.py
|
||||||
|
│ └── Dockerfile
|
||||||
|
│
|
||||||
├── beszel/ # Configuration Beszel
|
├── beszel/ # Configuration Beszel
|
||||||
│ ├── docker-compose-hub.yml # Hub (Synology)
|
│ ├── docker-compose-hub.yml # Hub (Synology)
|
||||||
│ ├── docker-compose-agent-folzocorp.yml # Agent FolzoCorp
|
│ ├── docker-compose-agent-folzocorp.yml # Agent FolzoCorp
|
||||||
@@ -252,75 +219,38 @@ effi/
|
|||||||
├── systemd/ # Services systemd
|
├── systemd/ # Services systemd
|
||||||
│ ├── comfyui-memory-manager.service
|
│ ├── comfyui-memory-manager.service
|
||||||
│ ├── kreuzberg-memory-manager.service
|
│ ├── kreuzberg-memory-manager.service
|
||||||
│ ├── beszel-agent-wake.service
|
│ └── beszel-agent-wake.service
|
||||||
│ └── ollama.service # Ollama LLM (folzo-Corp)
|
|
||||||
│
|
│
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation
|
||||||
│ ├── SESSION-2026-04-12-RAM-BESZEL.md
|
│ ├── WOL-ET-DEMARRAGE-AUTO.md
|
||||||
│ ├── BESZEL-INSTALLATION.md
|
│ ├── BESZEL-INSTALLATION.md
|
||||||
│ ├── MEMOIRE-GESTION.md
|
│ ├── MEMOIRE-GESTION.md
|
||||||
│ └── TOOL-BESZEL-OPENWEBUI.md
|
│ └── TOOL-BESZEL-OPENWEBUI.md
|
||||||
│
|
│
|
||||||
└── docker-compose*.yml # Divers compose files (legacy/backup)
|
└── docker-compose.yml # Stack principale (effi-server)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 Historique
|
## 📝 Historique
|
||||||
|
|
||||||
**Juillet 2026** - jean-router : routing dynamique LLM + gestion VRAM
|
**Août 2026** - Simplification : proxy unique + Wake-on-LAN
|
||||||
|
|
||||||
### jean-router ✅
|
### llm-proxy ✅
|
||||||
- Création de jean-router (Flask) pour routing dynamique entre Jean et Ollama
|
- Un seul service remplace jean-router, jean-proxy, jean-proxy-moe, wol-proxy
|
||||||
- Découverte automatique des presets Jean via `/api/presets`
|
- 4 routes : Ollama (`:3011`), llama.cpp (`:3012`), STT (`:3015`), TTS (`:3016`)
|
||||||
- Découverte automatique des modèles Ollama via `/api/tags`
|
- Wake-on-LAN intégré avec anti-spam (un seul réveil en cours)
|
||||||
- Nouveaux presets Jean automatiquement visibles dans OpenWebUI
|
- Suppression de tout le code Jean (plus besoin de routing dynamique)
|
||||||
- Gestion VRAM : arrêt Jean quand on passe sur Ollama, timer 5min pour Ollama
|
- OpenWebUI pointe vers `llm-proxy:3011` (Ollama) et `llm-proxy:3012` (llama.cpp)
|
||||||
- Suppression de LiteLLM (une couche de moins, config statique obsolète)
|
|
||||||
- OpenWebUI pointe directement vers jean-router:3014
|
|
||||||
|
|
||||||
### Jean (llama.cpp) ✅
|
**Juillet 2026** - Routing dynamique (abandonné)
|
||||||
- Migration vers Jean comme backend LLM principal
|
- jean-router : routing dynamique Jean/Ollama + gestion VRAM (supprimé)
|
||||||
- Presets dynamiques : ornith-35b-q6, qwen3.6-27b-mtp, qwen3.6-35b-moe-compact...
|
|
||||||
- Service systemd avec démarrage au boot (sans login)
|
|
||||||
- API web pour gestion (start/stop/switch presets)
|
|
||||||
|
|
||||||
**Avril 2026** - Optimisation RAM + Monitoring + Tool OpenWebUI
|
**Avril 2026** - Optimisation RAM + Monitoring + Tool OpenWebUI
|
||||||
|
|
||||||
### RAM Management ✅
|
|
||||||
- Scripts déchargement mémoire avec restart container (ComfyUI, Kreuzberg)
|
- Scripts déchargement mémoire avec restart container (ComfyUI, Kreuzberg)
|
||||||
- Timeout inactivité 300s pour effi-stt avec déchargement Whisper
|
- Timeout inactivité 300s pour effi-stt avec déchargement Whisper
|
||||||
- Services systemd pour gestion automatique
|
- Beszel Monitoring (Hub NAS + agents FolzoCorp/EffiServer)
|
||||||
|
- Tool OpenWebUI Beszel
|
||||||
### Beszel Monitoring ✅
|
|
||||||
- Hub sur NAS Synology (http://192.168.1.25:8090)
|
|
||||||
- Agents sur FolzoCorp + EffiServer + NAS Synology
|
|
||||||
- Service restart après veille (beszel-agent-wake.service)
|
|
||||||
|
|
||||||
### Tool OpenWebUI Beszel ✅
|
|
||||||
- Outil custom pour interrogation Beszel depuis OpenWebUI
|
|
||||||
- Endpoint PocketBase: `/api/collections/users/auth-with-password`
|
|
||||||
- Données extraites du champ `info` (cpu, mp, g, dt, la, etc.)
|
|
||||||
- Pattern Valves Pydantic pour configuration
|
|
||||||
|
|
||||||
### LLM Optimization (gemma4:e4b) ✅
|
|
||||||
- Installation gemma4:e4b (9.6GB, 128K context, MMLU 82.6%)
|
|
||||||
- Service systemd Ollama propre (User=folzo, auto-start)
|
|
||||||
- Variables: OLLAMA_FLASH_ATTENTION=1, OLLAMA_KV_CACHE_TYPE=q8_0, OLLAMA_CONTEXT_LENGTH=32768
|
|
||||||
- Note: gemma4:26b (18GB) ne tient pas dans 17GB VRAM RX 7800 XT
|
|
||||||
|
|
||||||
### RAG Optimization ✅
|
|
||||||
- effi-server: RAG_SYSTEM_CONTEXT=True (meilleur KV cache pour RAG)
|
|
||||||
- effi-server: ENABLE_RAG_HYBRID_SEARCH=True
|
|
||||||
|
|
||||||
**Améliorations futures**:
|
|
||||||
- Support historique (données des N dernières minutes)
|
|
||||||
- Filtres par période
|
|
||||||
- Graphiques et alertes automatisées
|
|
||||||
|
|
||||||
**Mars 2026** - Génération Audio
|
|
||||||
- Intégration ACE-Step pour génération musicale
|
|
||||||
- ComfyUI avec ROCm
|
|
||||||
|
|
||||||
**Février 2026** - Stack Vocale
|
**Février 2026** - Stack Vocale
|
||||||
- effi-stt avec Whisper large-v3
|
- effi-stt avec Whisper large-v3
|
||||||
@@ -335,4 +265,4 @@ effi/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Dernière mise à jour : Avril 2026*
|
*Dernière mise à jour : Août 2026*
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
# ═══════════════════════════════════════════════════════════
|
|
||||||
# OVERRIDE jean-router — Routing dynamique Jean + Ollama
|
|
||||||
#
|
|
||||||
# Usage (depuis /home/effi/effi sur serveur Effi) :
|
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.litellm.yml up -d --build
|
|
||||||
#
|
|
||||||
# jean-router remplace LiteLLM :
|
|
||||||
# - Interroge dynamiquement Jean pour lister les presets
|
|
||||||
# - Route vers Jean ou Ollama selon le modèle demandé
|
|
||||||
# - Switch automatique de preset quand nécessaire
|
|
||||||
# - Nouveaux presets Jean automatiquement visibles dans OpenWebUI
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
# open-webui — repointe vers jean-router (API OpenAI directe)
|
|
||||||
# jean-router expose dynamiquement les presets Jean + modèles Ollama
|
|
||||||
# Plus besoin de LiteLLM, routing direct vers jean-router:3014
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
open-webui:
|
|
||||||
environment:
|
|
||||||
- OLLAMA_BASE_URL=http://host.docker.internal:3011
|
|
||||||
- OPENAI_API_BASE_URL=http://host.docker.internal:3014/v1
|
|
||||||
- OPENAI_API_KEY=any
|
|
||||||
- ENABLE_OPENAI_API=True
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
# jean-proxy — Reverse-proxy vers JeanOrchestrator (.11:8080) + WOL
|
|
||||||
# L'orchestrateur gere le switch auto MoE/27B.
|
|
||||||
# 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}
|
|
||||||
- HEALTH_PATH=/health
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
# jean-router — Proxy dynamique qui interroge Jean pour
|
|
||||||
# lister les presets et route les requêtes vers Jean
|
|
||||||
# ou Ollama selon le modèle demandé.
|
|
||||||
# Ecoute :3014 sur le host (network_mode: host)
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
jean-router:
|
|
||||||
build: ./jean-router
|
|
||||||
container_name: jean-router
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
environment:
|
|
||||||
- JEAN_WEB_HOST=${OLLAMA_HOST}
|
|
||||||
- JEAN_WEB_PORT=8090
|
|
||||||
- JEAN_PROXY_HOST=localhost
|
|
||||||
- JEAN_PROXY_PORT=3012
|
|
||||||
- OLLAMA_PROXY_HOST=localhost
|
|
||||||
- OLLAMA_PROXY_PORT=3011
|
|
||||||
- OLLAMA_DIRECT_HOST=${OLLAMA_HOST}
|
|
||||||
- OLLAMA_DIRECT_PORT=11434
|
|
||||||
- ENABLE_OLLAMA=true
|
|
||||||
- ENABLE_VRAM_MANAGEMENT=true
|
|
||||||
- OLLAMA_KEEP_ALIVE_MINUTES=5
|
|
||||||
- LISTEN_PORT=3014
|
|
||||||
- SWITCH_TIMEOUT=90
|
|
||||||
- CHECK_INTERVAL=2
|
|
||||||
- JEAN_STOP_TIMEOUT=30
|
|
||||||
- OLLAMA_UNLOAD_TIMEOUT=15
|
|
||||||
+24
-43
@@ -6,44 +6,20 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
# 0. VALKEY - Backend de rate limiting pour SearXNG
|
# 0. SEARXNG - Meta-search Engine (Web Search)
|
||||||
# → Port interne uniquement (non exposé)
|
# → http://192.168.1.27:11112
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
valkey:
|
|
||||||
image: valkey/valkey:7-alpine
|
|
||||||
container_name: effi-valkey
|
|
||||||
restart: unless-stopped
|
|
||||||
command: valkey-server /usr/local/etc/valkey/valkey.conf
|
|
||||||
volumes:
|
|
||||||
- ./searxng-config/valkey.conf:/usr/local/etc/valkey/valkey.conf:ro
|
|
||||||
- valkey-data:/data/valkey-data
|
|
||||||
networks:
|
|
||||||
- effi-net
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
# 1. SEARXNG - Meta-search Engine (API JSON)
|
|
||||||
# → http://192.168.1.27:6060
|
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
searxng:
|
searxng:
|
||||||
image: searxng/searxng:latest
|
image: searxng/searxng:latest
|
||||||
container_name: effi-searxng
|
container_name: effi-searxng
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "6060:8080"
|
- "11112:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./searxng-config/settings.yml:/etc/searxng/settings.yml:ro
|
- ./searxng-data:/etc/searxng:rw
|
||||||
environment:
|
environment:
|
||||||
- SEARXNG_BASE_URL=http://192.168.1.27:6060/
|
- SEARXNG_BASE_URL=http://192.168.1.27:11112/
|
||||||
- SEARXNG_SECRET=${SEARXNG_SECRET_KEY}
|
- SEARXNG_SECRET=${SEARXNG_SECRET_KEY}
|
||||||
- SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 30s
|
|
||||||
depends_on:
|
|
||||||
- valkey
|
|
||||||
networks:
|
networks:
|
||||||
- effi-net
|
- effi-net
|
||||||
|
|
||||||
@@ -61,40 +37,46 @@ services:
|
|||||||
- "3000:8080"
|
- "3000:8080"
|
||||||
environment:
|
environment:
|
||||||
- OLLAMA_BASE_URL=http://host.docker.internal:3011
|
- OLLAMA_BASE_URL=http://host.docker.internal:3011
|
||||||
|
- OPENAI_API_BASE_URL=http://host.docker.internal:3012/v1
|
||||||
|
- OPENAI_API_KEY=any
|
||||||
|
- ENABLE_OPENAI_API=True
|
||||||
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
|
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
|
||||||
- VECTOR_DB=qdrant
|
- VECTOR_DB=qdrant
|
||||||
- QDRANT_URI=http://qdrant:6333
|
- QDRANT_URI=http://qdrant:6333
|
||||||
- DEFAULT_LOCALE=fr-FR
|
- DEFAULT_LOCALE=fr-FR
|
||||||
- ENABLE_MEMORIES=true
|
- ENABLE_MEMORIES=true
|
||||||
- AUDIO_STT_ENGINE=openai
|
- AUDIO_STT_ENGINE=openai
|
||||||
- AUDIO_STT_OPENAI_API_BASE_URL=http://192.168.1.11:8200/v1
|
- AUDIO_STT_OPENAI_API_BASE_URL=http://host.docker.internal:3015/v1
|
||||||
- AUDIO_STT_OPENAI_API_KEY=anything
|
- AUDIO_STT_OPENAI_API_KEY=anything
|
||||||
- AUDIO_STT_MODEL=Systran/faster-whisper-large-v3
|
- AUDIO_STT_MODEL=Systran/faster-whisper-large-v3
|
||||||
volumes:
|
volumes:
|
||||||
- open-webui-data:/app/backend/data
|
- open-webui-data:/app/backend/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- qdrant
|
- qdrant
|
||||||
- wol-proxy
|
- llm-proxy
|
||||||
networks:
|
networks:
|
||||||
- effi-net
|
- effi-net
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
# 2. WOL PROXY - Réveil PC Gaming avant appel Ollama
|
# 2. LLM PROXY - Proxy unique vers le PC Gaming + Wake-on-LAN
|
||||||
# → Proxy :3011 → PC Gaming :11434
|
# :3011 → Ollama (.11:11434)
|
||||||
|
# :3012 → llama.cpp (.11:8080)
|
||||||
|
# :3015 → STT (.11:8200)
|
||||||
|
# :3016 → TTS (serveur local :8100, pas de WOL)
|
||||||
|
# Si le PC est éteint, un paquet WoL est envoyé avant l'attente.
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
wol-proxy:
|
llm-proxy:
|
||||||
build: ./wol-proxy
|
build: ./llm-proxy
|
||||||
container_name: wol-proxy
|
container_name: llm-proxy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
environment:
|
environment:
|
||||||
- TARGET_HOST=${OLLAMA_HOST}
|
- PC_HOST=${OLLAMA_HOST}
|
||||||
- TARGET_PORT=11434
|
- PC_MAC=${PC_GAMING_MAC}
|
||||||
- TARGET_MAC=${PC_GAMING_MAC}
|
|
||||||
- LISTEN_PORT=3011
|
|
||||||
- WAKE_TIMEOUT=${WAKE_TIMEOUT:-180}
|
|
||||||
- CHECK_INTERVAL=5
|
|
||||||
- BROADCAST_IP=${BROADCAST_IP}
|
- BROADCAST_IP=${BROADCAST_IP}
|
||||||
|
- WAKE_TIMEOUT=${WAKE_TIMEOUT:-300}
|
||||||
|
- SERVICE_TIMEOUT=120
|
||||||
|
- CHECK_INTERVAL=5
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
# 3. MCPO - Proxy outils MCP (Obsidian, etc.)
|
# 3. MCPO - Proxy outils MCP (Obsidian, etc.)
|
||||||
@@ -289,4 +271,3 @@ volumes:
|
|||||||
open-webui-data:
|
open-webui-data:
|
||||||
pipelines-data:
|
pipelines-data:
|
||||||
whisper-cache:
|
whisper-cache:
|
||||||
redis-data:
|
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
# Session 2026-07-20 : jean-router et gestion VRAM dynamique
|
|
||||||
|
|
||||||
## Résumé
|
|
||||||
|
|
||||||
Création de **jean-router**, un proxy dynamique qui remplace LiteLLM pour router intelligemment entre Jean (llama.cpp) et Ollama, avec gestion automatique de la VRAM partagée.
|
|
||||||
|
|
||||||
## Contexte
|
|
||||||
|
|
||||||
### Problème initial
|
|
||||||
- OpenWebUI ne voyait pas les modèles Jean (Ornith) car configuré uniquement pour Ollama
|
|
||||||
- LiteLLM avait une configuration statique des modèles (codée en dur dans `litellm/config.yaml`)
|
|
||||||
- Nouveaux presets Jean nécessitaient de modifier manuellement la config LiteLLM
|
|
||||||
- Jean et Ollama partagent la même VRAM (RX 7800 XT, 16GB) → contention mémoire
|
|
||||||
|
|
||||||
### Architecture avant
|
|
||||||
```
|
|
||||||
OpenWebUI → LiteLLM:4000 (config statique)
|
|
||||||
├→ jean-proxy:3012 → Jean (.11:8080)
|
|
||||||
└→ wol-proxy:3011 → Ollama (.11:11434)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problèmes :**
|
|
||||||
- LiteLLM ne faisait pas de pass-through de `/v1/models`
|
|
||||||
- Configuration statique : `qwen3.6-reason`, `qwen3.6-27b`, etc. codés en dur
|
|
||||||
- Jean faisait tourner **Ornith** mais LiteLLM attendait Qwen3.6
|
|
||||||
- Pas de gestion VRAM : Jean et Ollama pouvaient charger simultanément → OOM
|
|
||||||
|
|
||||||
## Solution : jean-router
|
|
||||||
|
|
||||||
### Architecture après
|
|
||||||
```
|
|
||||||
OpenWebUI → jean-router:3014 (routing dynamique)
|
|
||||||
├→ jean-proxy:3012 → Jean (.11:8080)
|
|
||||||
└→ wol-proxy:3011 → Ollama (.11:11434)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fonctionnalités
|
|
||||||
|
|
||||||
**1. Découverte dynamique des modèles**
|
|
||||||
- Interroge `/api/presets` de Jean (port 8090) pour lister les presets disponibles
|
|
||||||
- Interroge `/api/tags` d'Ollama (port 11434) pour lister les modèles
|
|
||||||
- Expose tout via `/v1/models` (API OpenAI-compatible)
|
|
||||||
- **Nouveaux presets Jean automatiquement visibles dans OpenWebUI**
|
|
||||||
|
|
||||||
**2. Routing intelligent**
|
|
||||||
- Si le modèle demandé est un preset Jean → switch automatique + route vers Jean
|
|
||||||
- Si c'est un modèle Ollama → route vers Ollama
|
|
||||||
- Sinon → erreur 404
|
|
||||||
|
|
||||||
**3. Gestion VRAM (RX 7800 XT, 16GB partagés)**
|
|
||||||
|
|
||||||
| Backend | Modèle typique | VRAM utilisée |
|
|
||||||
|---------|---------------|---------------|
|
|
||||||
| Jean | Ornith 35B Q6_K | ~28.5GB (déborde en RAM) |
|
|
||||||
| Ollama | mistral/qwen3.5 | ~4-6GB |
|
|
||||||
|
|
||||||
**Logique de gestion :**
|
|
||||||
|
|
||||||
**Switch Jean → Ollama :**
|
|
||||||
1. `stop_jean()` arrête Jean via son API web (port 8090)
|
|
||||||
2. Attend confirmation que Jean est arrêté (max 30s)
|
|
||||||
3. Route la requête vers Ollama
|
|
||||||
|
|
||||||
**Switch Ollama → Jean :**
|
|
||||||
1. Programme le déchargement d'Ollama dans 5 minutes (timer)
|
|
||||||
2. `start_jean()` redémarre Jean
|
|
||||||
3. Attend que le modèle soit chargé (max 90s)
|
|
||||||
4. Route la requête vers Jean
|
|
||||||
|
|
||||||
**Timer Ollama :**
|
|
||||||
- Ollama reste en mémoire pendant **5 minutes** après le dernier usage
|
|
||||||
- Chaque requête vers Ollama annule le timer
|
|
||||||
- Si retour sur Ollama avant expiration → déchargement annulé
|
|
||||||
- Après 5 min d'inactivité → déchargement automatique pour libérer VRAM
|
|
||||||
|
|
||||||
## Fichiers créés/modifiés
|
|
||||||
|
|
||||||
### Nouveau : `jean-router/`
|
|
||||||
- `app.py` (320 lignes) : Service Flask avec routing dynamique + gestion VRAM
|
|
||||||
- `Dockerfile` : Container Python 3.12-slim
|
|
||||||
|
|
||||||
### Modifiés :
|
|
||||||
- `docker-compose.litellm.yml` :
|
|
||||||
- Ajout service jean-router
|
|
||||||
- Suppression service litellm
|
|
||||||
- OpenWebUI pointe directement vers jean-router:3014
|
|
||||||
- `litellm/config.yaml` : Supprimé (plus nécessaire)
|
|
||||||
|
|
||||||
### Configuration jean-router
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
- JEAN_WEB_HOST=${OLLAMA_HOST}
|
|
||||||
- JEAN_WEB_PORT=8090
|
|
||||||
- JEAN_PROXY_HOST=localhost
|
|
||||||
- JEAN_PROXY_PORT=3012
|
|
||||||
- OLLAMA_PROXY_HOST=localhost
|
|
||||||
- OLLAMA_PROXY_PORT=3011
|
|
||||||
- OLLAMA_DIRECT_HOST=${OLLAMA_HOST}
|
|
||||||
- OLLAMA_DIRECT_PORT=11434
|
|
||||||
- ENABLE_OLLAMA=true
|
|
||||||
- ENABLE_VRAM_MANAGEMENT=true
|
|
||||||
- OLLAMA_KEEP_ALIVE_MINUTES=5
|
|
||||||
- LISTEN_PORT=3014
|
|
||||||
- SWITCH_TIMEOUT=90
|
|
||||||
- CHECK_INTERVAL=2
|
|
||||||
- JEAN_STOP_TIMEOUT=30
|
|
||||||
- OLLAMA_UNLOAD_TIMEOUT=15
|
|
||||||
```
|
|
||||||
|
|
||||||
## Déploiement sur effi-server
|
|
||||||
|
|
||||||
### Initialisation du repo git
|
|
||||||
Le dossier `/home/effi/effi` n'était pas un repo git. Initialisation :
|
|
||||||
```bash
|
|
||||||
cd /home/effi/effi
|
|
||||||
git init
|
|
||||||
git remote add origin https://gitea.folzocorp.fr/Folzo/Effi.git
|
|
||||||
git fetch origin
|
|
||||||
git checkout -b main origin/main
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note :** Conflits avec fichiers existants (docker-compose.yml, jean-proxy/, etc.) résolus en les déplaçant temporairement dans `/tmp/effi-backup/`.
|
|
||||||
|
|
||||||
### Commandes de déploiement
|
|
||||||
```bash
|
|
||||||
cd /home/effi/effi
|
|
||||||
git pull
|
|
||||||
docker compose -f docker-compose.yml -f docker-compose.litellm.yml up -d --build jean-router
|
|
||||||
docker compose -f docker-compose.yml -f docker-compose.litellm.yml up -d --force-recreate open-webui
|
|
||||||
docker stop litellm && docker rm litellm # Suppression LiteLLM
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tests et validation
|
|
||||||
|
|
||||||
### Modèles visibles dans OpenWebUI
|
|
||||||
|
|
||||||
**Jean (5 presets) :**
|
|
||||||
- ornith-35b-q6 (actif, Q6_K, reasoning)
|
|
||||||
- qwen3.6-27b-mtp (IQ4_XS, reasoning)
|
|
||||||
- qwen3.6-35b-moe-compact
|
|
||||||
- qwen3.6-35b-moe-compact-reason (reasoning)
|
|
||||||
- qwen3.6-35b-moe-nano
|
|
||||||
|
|
||||||
**Ollama :**
|
|
||||||
- mistral (7.2B, Q4_K_M, tools)
|
|
||||||
- qwen3.5:9b (9.7B, Q4_K_M, vision, thinking)
|
|
||||||
|
|
||||||
### Logs observés
|
|
||||||
|
|
||||||
**Switch Jean → Ollama :**
|
|
||||||
```
|
|
||||||
[VRAM] Switch Jean → Ollama
|
|
||||||
[VRAM] Arrêt de Jean pour libérer la VRAM...
|
|
||||||
[VRAM] Jean arrêté via API web
|
|
||||||
[VRAM] Jean confirmé arrêté
|
|
||||||
[Router] → Ollama (gemma4:e4b)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Switch Ollama → Jean :**
|
|
||||||
```
|
|
||||||
[VRAM] Switch Ollama → Jean
|
|
||||||
[VRAM] Déchargement Ollama programmé dans 5 minutes
|
|
||||||
[VRAM] Démarrage de Jean...
|
|
||||||
[VRAM] Jean prêt après 45s
|
|
||||||
[Router] → Jean (ornith-35b-q6)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Timer Ollama :**
|
|
||||||
```
|
|
||||||
[VRAM] Timer écoulé (5min), déchargement Ollama...
|
|
||||||
[VRAM] Ollama déchargé via API
|
|
||||||
```
|
|
||||||
|
|
||||||
## Avantages
|
|
||||||
|
|
||||||
1. **Dynamique** : Nouveaux presets Jean automatiquement visibles sans modifier de config
|
|
||||||
2. **Simplifié** : Une couche de moins (LiteLLM supprimé)
|
|
||||||
3. **VRAM-safe** : Pas de contention mémoire entre Jean et Ollama
|
|
||||||
4. **Intelligent** : Ollama reste en mémoire 5 min pour les usages répétés
|
|
||||||
5. **Transparent** : OpenWebUI voit tous les modèles via un seul endpoint
|
|
||||||
|
|
||||||
## Limitations et points d'attention
|
|
||||||
|
|
||||||
### 1. Jean n'a pas d'auto-unload natif
|
|
||||||
- Ollama décharge automatiquement après `OLLAMA_KEEP_ALIVE` (défaut 5min)
|
|
||||||
- Jean garde son modèle chargé tant que le service tourne
|
|
||||||
- **Solution actuelle** : Arrêt complet de Jean via API web
|
|
||||||
|
|
||||||
### 2. Latence de switch
|
|
||||||
- Jean → Ollama : ~3s (arrêt Jean)
|
|
||||||
- Ollama → Jean : ~45-90s (démarrage + chargement modèle)
|
|
||||||
- **Impact** : Première requête après switch plus lente
|
|
||||||
|
|
||||||
### 3. Timer Ollama non-persistant
|
|
||||||
- Si jean-router redémarre, le timer est perdu
|
|
||||||
- Ollama restera en mémoire indéfiniment (jusqu'à son propre timeout)
|
|
||||||
- **Acceptable** : Ollama gère son propre `keep_alive`
|
|
||||||
|
|
||||||
### 4. Pas de failover
|
|
||||||
- Si Jean échoue (timeout, erreur), pas de fallback automatique vers Ollama
|
|
||||||
- **À considérer** : Ajouter un fallback dans jean-router si besoin
|
|
||||||
|
|
||||||
## Commits Gitea
|
|
||||||
|
|
||||||
```
|
|
||||||
8b7c4fb feat: add VRAM management for Jean/Ollama switching
|
|
||||||
20767c2 feat: keep Ollama models in memory for 5 minutes
|
|
||||||
babe4ee chore: add OLLAMA_KEEP_ALIVE_MINUTES=5 to docker-compose
|
|
||||||
43aacc0 refactor: OpenWebUI points directly to jean-router (no LiteLLM needed)
|
|
||||||
856ba4a refactor: remove LiteLLM, jean-router handles all routing
|
|
||||||
fcad415 feat: jean-router — routing dynamique Jean + Ollama
|
|
||||||
b441f34 fix: jean-router syntax error (missing except block)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Réflexions et décisions
|
|
||||||
|
|
||||||
### Pourquoi supprimer LiteLLM ?
|
|
||||||
- LiteLLM ne faisait pas de pass-through de `/v1/models`
|
|
||||||
- Configuration statique incompatible avec les presets dynamiques de Jean
|
|
||||||
- Couche supplémentaire inutile : jean-router fait le même travail
|
|
||||||
- **Décision** : OpenWebUI pointe directement vers jean-router:3014
|
|
||||||
|
|
||||||
### Pourquoi garder Ollama en mémoire 5 min ?
|
|
||||||
- Ollama sert principalement pour la vision (modèles que Jean ne supporte pas)
|
|
||||||
- Usage typique : quelques requêtes vision consécutives, puis retour à Jean
|
|
||||||
- 5 min = bon compromis entre réactivité et libération VRAM
|
|
||||||
- **Configurable** : `OLLAMA_KEEP_ALIVE_MINUTES` dans docker-compose
|
|
||||||
|
|
||||||
### Pourquoi arrêter Jean complètement ?
|
|
||||||
- Jean n'a pas d'API pour décharger le modèle sans arrêter le service
|
|
||||||
- Le modèle reste en VRAM tant que llama-server tourne
|
|
||||||
- **Alternative envisagée** : `systemctl stop jean` (mais nécessite sudo)
|
|
||||||
- **Solution retenue** : API web de Jean (`/api/stop`) plus simple
|
|
||||||
|
|
||||||
### Architecture future possible
|
|
||||||
- **JeanOrchestrator** : Jean pourrait gérer lui-même le switch entre presets
|
|
||||||
- **VRAM monitoring** : jean-router pourrait vérifier la VRAM disponible avant de router
|
|
||||||
- **Fallback automatique** : Si Jean échoue, router vers Ollama
|
|
||||||
- **Metrics** : Exposer des statistiques (temps de switch, VRAM utilisée, etc.)
|
|
||||||
|
|
||||||
## Prochaines étapes
|
|
||||||
|
|
||||||
1. **Monitorer** : Vérifier que la gestion VRAM fonctionne bien en production
|
|
||||||
2. **Ajuster** : Modifier `OLLAMA_KEEP_ALIVE_MINUTES` selon l'usage réel
|
|
||||||
3. **Documenter** : Mettre à jour `docs/ARCHITECTURE-LITELLM.md` (à renommer `ARCHITECTURE-JEAN-ROUTER.md`)
|
|
||||||
4. **Tester** : Valider que les nouveaux presets Jean apparaissent bien automatiquement
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
**jean-router** remplace avantageusement LiteLLM avec :
|
|
||||||
- Routing dynamique basé sur les presets Jean
|
|
||||||
- Gestion automatique de la VRAM partagée
|
|
||||||
- Architecture simplifiée (une couche de moins)
|
|
||||||
- Nouveaux modèles automatiquement disponibles
|
|
||||||
|
|
||||||
L'architecture est maintenant prête pour Effi : quand un nouveau preset Jean est créé, il apparaît immédiatement dans OpenWebUI sans aucune configuration supplémentaire.
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
# Architecture LLM — jean-router (routing dynamique + gestion VRAM)
|
|
||||||
|
|
||||||
## Vue d'ensemble
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ OpenWebUI (:3000) │
|
|
||||||
│ http://192.168.1.27:3000 │
|
|
||||||
└────────────────────────┬────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ jean-router (:3014) │
|
|
||||||
│ Routing dynamique + Gestion VRAM automatique │
|
|
||||||
│ │
|
|
||||||
│ • Interroge Jean /api/presets (découverte dynamique) │
|
|
||||||
│ • Interroge Ollama /api/tags (modèles vision) │
|
|
||||||
│ • Switch automatique de preset Jean │
|
|
||||||
│ • Arrête Jean quand on passe sur Ollama (libère VRAM) │
|
|
||||||
│ • Garde Ollama 5 min en mémoire avant déchargement │
|
|
||||||
└────────┬──────────────────────────────────────┬────────────────┘
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌────────────────────────┐ ┌────────────────────────┐
|
|
||||||
│ jean-proxy (:3012) │ │ wol-proxy (:3011) │
|
|
||||||
│ + WOL si PC éteint │ │ + WOL si PC éteint │
|
|
||||||
└────────┬───────────────┘ └────────┬───────────────┘
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌────────────────────────┐ ┌────────────────────────┐
|
|
||||||
│ Jean (.11:8080) │ │ Ollama (.11:11434) │
|
|
||||||
│ llama.cpp server │ │ Modèles vision │
|
|
||||||
│ Presets dynamiques │ │ mistral, qwen3.5 │
|
|
||||||
│ Ornith, Qwen3.6... │ │ │
|
|
||||||
└────────────────────────┘ └────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Composants
|
|
||||||
|
|
||||||
### jean-router (`/jean-router/`) — Proxy dynamique
|
|
||||||
|
|
||||||
**Rôle :** Router intelligemment entre Jean et Ollama avec gestion VRAM
|
|
||||||
|
|
||||||
**Fonctionnalités :**
|
|
||||||
1. **Découverte dynamique** : Interroge `/api/presets` (Jean) et `/api/tags` (Ollama)
|
|
||||||
2. **Routing intelligent** : Route vers Jean ou Ollama selon le modèle demandé
|
|
||||||
3. **Switch automatique** : Change de preset Jean si nécessaire
|
|
||||||
4. **Gestion VRAM** : Évite la contention mémoire entre Jean et Ollama
|
|
||||||
|
|
||||||
**Endpoints :**
|
|
||||||
- `GET /v1/models` : Liste tous les modèles (Jean + Ollama)
|
|
||||||
- `POST /v1/chat/completions` : Route vers le bon backend
|
|
||||||
|
|
||||||
**Configuration :**
|
|
||||||
```bash
|
|
||||||
JEAN_WEB_HOST=192.168.1.11 # Jean Web UI (API presets)
|
|
||||||
JEAN_WEB_PORT=8090
|
|
||||||
JEAN_PROXY_HOST=localhost # jean-proxy (backend Jean)
|
|
||||||
JEAN_PROXY_PORT=3012
|
|
||||||
OLLAMA_PROXY_HOST=localhost # wol-proxy (backend Ollama)
|
|
||||||
OLLAMA_PROXY_PORT=3011
|
|
||||||
OLLAMA_DIRECT_HOST=192.168.1.11 # Ollama direct (pour unload)
|
|
||||||
OLLAMA_DIRECT_PORT=11434
|
|
||||||
ENABLE_OLLAMA=true
|
|
||||||
ENABLE_VRAM_MANAGEMENT=true
|
|
||||||
OLLAMA_KEEP_ALIVE_MINUTES=5 # Ollama reste 5 min en mémoire
|
|
||||||
SWITCH_TIMEOUT=90 # Timeout chargement Jean (s)
|
|
||||||
JEAN_STOP_TIMEOUT=30 # Timeout arrêt Jean (s)
|
|
||||||
LISTEN_PORT=3014
|
|
||||||
```
|
|
||||||
|
|
||||||
### jean-proxy (`/jean-proxy/`) — Reverse-proxy Jean + WOL
|
|
||||||
|
|
||||||
**Rôle :** Proxy vers Jean avec Wake-on-LAN si le PC gaming est éteint
|
|
||||||
|
|
||||||
**Fonctionnalités :**
|
|
||||||
- Écoute `:3012` sur le host (`network_mode: host`)
|
|
||||||
- Health-check : `GET /health` sur Jean
|
|
||||||
- Si cible DOWN → envoi paquets WOL vers `TARGET_MAC`
|
|
||||||
- Attente jusqu'à `WAKE_TIMEOUT` (60s)
|
|
||||||
- Route toutes les requêtes vers `http://${OLLAMA_HOST}:8080`
|
|
||||||
|
|
||||||
**Configuration :**
|
|
||||||
```bash
|
|
||||||
TARGET_HOST=192.168.1.11
|
|
||||||
TARGET_PORT=8080
|
|
||||||
TARGET_MAC=04:D9:F5:D1:EC:DF
|
|
||||||
LISTEN_PORT=3012
|
|
||||||
WAKE_TIMEOUT=60
|
|
||||||
CHECK_INTERVAL=5
|
|
||||||
BROADCAST_IP=192.168.1.255
|
|
||||||
HEALTH_PATH=/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### wol-proxy (`/wol-proxy/`) — Reverse-proxy Ollama + WOL
|
|
||||||
|
|
||||||
**Rôle :** Proxy vers Ollama avec Wake-on-LAN
|
|
||||||
|
|
||||||
**Fonctionnalités :**
|
|
||||||
- Écoute `:3011` sur le host
|
|
||||||
- Health-check : `GET /api/version` sur Ollama
|
|
||||||
- Si cible DOWN → envoi paquets WOL
|
|
||||||
- Route toutes les requêtes vers `http://${OLLAMA_HOST}:11434`
|
|
||||||
|
|
||||||
**Configuration :**
|
|
||||||
```bash
|
|
||||||
TARGET_HOST=192.168.1.11
|
|
||||||
TARGET_PORT=11434
|
|
||||||
TARGET_MAC=04:D9:F5:D1:EC:DF
|
|
||||||
LISTEN_PORT=3011
|
|
||||||
WAKE_TIMEOUT=180
|
|
||||||
CHECK_INTERVAL=5
|
|
||||||
BROADCAST_IP=192.168.1.255
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gestion VRAM
|
|
||||||
|
|
||||||
### Problème
|
|
||||||
Jean et Ollama partagent la même carte graphique (RX 7800 XT, 16GB VRAM). Charger les deux simultanément cause une contention mémoire.
|
|
||||||
|
|
||||||
| Backend | Modèle typique | VRAM utilisée |
|
|
||||||
|---------|---------------|---------------|
|
|
||||||
| Jean | Ornith 35B Q6_K | ~28.5GB (déborde en RAM) |
|
|
||||||
| Ollama | mistral 7B Q4_K_M | ~4GB |
|
|
||||||
| Ollama | qwen3.5:9b Q4_K_M | ~6GB |
|
|
||||||
|
|
||||||
### Solution : Switch automatique
|
|
||||||
|
|
||||||
**Switch Jean → Ollama :**
|
|
||||||
```
|
|
||||||
1. Arrêt de Jean via API web (/api/stop)
|
|
||||||
2. Attente confirmation (max 30s)
|
|
||||||
3. Route requête vers Ollama
|
|
||||||
```
|
|
||||||
|
|
||||||
**Switch Ollama → Jean :**
|
|
||||||
```
|
|
||||||
1. Programme déchargement Ollama dans 5 min (timer)
|
|
||||||
2. Démarrage de Jean via API web (/api/start)
|
|
||||||
3. Attente chargement modèle (max 90s)
|
|
||||||
4. Route requête vers Jean
|
|
||||||
```
|
|
||||||
|
|
||||||
**Timer Ollama :**
|
|
||||||
- Ollama reste en mémoire pendant **5 minutes** après le dernier usage
|
|
||||||
- Chaque requête vers Ollama annule le timer
|
|
||||||
- Si retour sur Ollama avant expiration → déchargement annulé
|
|
||||||
- Après 5 min d'inactivité → déchargement automatique
|
|
||||||
|
|
||||||
### Logs observés
|
|
||||||
|
|
||||||
**Switch Jean → Ollama :**
|
|
||||||
```
|
|
||||||
[VRAM] Switch Jean → Ollama
|
|
||||||
[VRAM] Arrêt de Jean pour libérer la VRAM...
|
|
||||||
[VRAM] Jean arrêté via API web
|
|
||||||
[VRAM] Jean confirmé arrêté
|
|
||||||
[Router] → Ollama (gemma4:e4b)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Switch Ollama → Jean :**
|
|
||||||
```
|
|
||||||
[VRAM] Switch Ollama → Jean
|
|
||||||
[VRAM] Déchargement Ollama programmé dans 5 minutes
|
|
||||||
[VRAM] Démarrage de Jean...
|
|
||||||
[VRAM] Jean prêt après 45s
|
|
||||||
[Router] → Jean (ornith-35b-q6)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Timer Ollama :**
|
|
||||||
```
|
|
||||||
[VRAM] Timer écoulé (5min), déchargement Ollama...
|
|
||||||
[VRAM] Ollama déchargé via API
|
|
||||||
```
|
|
||||||
|
|
||||||
## Modèles disponibles
|
|
||||||
|
|
||||||
### Jean (presets dynamiques)
|
|
||||||
Les presets sont découverts automatiquement via `/api/presets` :
|
|
||||||
- **ornith-35b-q6** (actif, Q6_K, reasoning)
|
|
||||||
- **qwen3.6-27b-mtp** (IQ4_XS, reasoning)
|
|
||||||
- **qwen3.6-35b-moe-compact**
|
|
||||||
- **qwen3.6-35b-moe-compact-reason** (reasoning)
|
|
||||||
- **qwen3.6-35b-moe-nano**
|
|
||||||
|
|
||||||
**Note :** Nouveaux presets Jean apparaissent automatiquement dans OpenWebUI sans configuration.
|
|
||||||
|
|
||||||
### Ollama (modèles vision)
|
|
||||||
- **mistral** (7.2B, Q4_K_M, tools)
|
|
||||||
- **qwen3.5:9b** (9.7B, Q4_K_M, vision, thinking)
|
|
||||||
|
|
||||||
**Note :** Ollama sert principalement pour les modèles avec vision que Jean ne supporte pas.
|
|
||||||
|
|
||||||
## Déploiement
|
|
||||||
|
|
||||||
### Commandes (sur effi-server .27)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/effi/effi
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# Build et démarrage jean-router
|
|
||||||
docker compose -f docker-compose.yml -f docker-compose.litellm.yml up -d --build jean-router
|
|
||||||
|
|
||||||
# Recréer open-webui avec nouvelle config
|
|
||||||
docker compose -f docker-compose.yml -f docker-compose.litellm.yml up -d --force-recreate open-webui
|
|
||||||
|
|
||||||
# Vérifier
|
|
||||||
docker logs jean-router --tail 20
|
|
||||||
curl -s http://localhost:3014/v1/models | jq
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
**1. Liste des modèles :**
|
|
||||||
```bash
|
|
||||||
curl -s http://localhost:3014/v1/models | python3 -m json.tool
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Requête Jean :**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3014/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "ornith-35b-q6",
|
|
||||||
"messages": [{"role": "user", "content": "Bonjour"}],
|
|
||||||
"max_tokens": 50
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Requête Ollama :**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3014/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "mistral",
|
|
||||||
"messages": [{"role": "user", "content": "Hello"}],
|
|
||||||
"max_tokens": 30
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Vérifier logs VRAM :**
|
|
||||||
```bash
|
|
||||||
docker logs jean-router -f | grep -E "VRAM|Router"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Avantages vs LiteLLM
|
|
||||||
|
|
||||||
| Aspect | LiteLLM | jean-router |
|
|
||||||
|--------|---------|-------------|
|
|
||||||
| Configuration | Statique (config.yaml) | Dynamique (API Jean) |
|
|
||||||
| Nouveaux presets | Modifier config + restart | Automatique |
|
|
||||||
| Pass-through /v1/models | Non | Oui |
|
|
||||||
| Gestion VRAM | Aucune | Automatique |
|
|
||||||
| Couches | 2 (LiteLLM + jean-proxy) | 1 (jean-router) |
|
|
||||||
| Complexité | Élevée | Faible |
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
### 1. Latence de switch
|
|
||||||
- Jean → Ollama : ~3s (arrêt Jean)
|
|
||||||
- Ollama → Jean : ~45-90s (démarrage + chargement modèle)
|
|
||||||
- **Impact** : Première requête après switch plus lente
|
|
||||||
|
|
||||||
### 2. Timer Ollama non-persistant
|
|
||||||
- Si jean-router redémarre, le timer est perdu
|
|
||||||
- Ollama restera en mémoire indéfiniment (jusqu'à son propre timeout)
|
|
||||||
- **Acceptable** : Ollama gère son propre `keep_alive`
|
|
||||||
|
|
||||||
### 3. Pas de failover
|
|
||||||
- Si Jean échoue (timeout, erreur), pas de fallback automatique vers Ollama
|
|
||||||
- **À considérer** : Ajouter un fallback dans jean-router si besoin
|
|
||||||
|
|
||||||
### 4. Jean n'a pas d'auto-unload natif
|
|
||||||
- Ollama décharge automatiquement après `OLLAMA_KEEP_ALIVE` (défaut 5min)
|
|
||||||
- Jean garde son modèle chargé tant que le service tourne
|
|
||||||
- **Solution actuelle** : Arrêt complet de Jean via API web
|
|
||||||
|
|
||||||
## Évolution possible
|
|
||||||
|
|
||||||
### Court terme
|
|
||||||
- **Monitorer** : Vérifier que la gestion VRAM fonctionne bien en production
|
|
||||||
- **Ajuster** : Modifier `OLLAMA_KEEP_ALIVE_MINUTES` selon l'usage réel
|
|
||||||
- **Tester** : Valider que les nouveaux presets Jean apparaissent bien automatiquement
|
|
||||||
|
|
||||||
### Moyen terme
|
|
||||||
- **VRAM monitoring** : jean-router pourrait vérifier la VRAM disponible avant de router
|
|
||||||
- **Fallback automatique** : Si Jean échoue, router vers Ollama
|
|
||||||
- **Metrics** : Exposer des statistiques (temps de switch, VRAM utilisée, etc.)
|
|
||||||
|
|
||||||
### Long terme
|
|
||||||
- **JeanOrchestrator** : Jean pourrait gérer lui-même le switch entre presets
|
|
||||||
- **Multi-GPU** : Si plusieurs GPU, router vers celui avec le plus de VRAM libre
|
|
||||||
- **Load balancing** : Si plusieurs instances Jean/Ollama, répartir la charge
|
|
||||||
|
|
||||||
## Historique
|
|
||||||
|
|
||||||
**2026-06-27** : Architecture initiale avec LiteLLM
|
|
||||||
- Routing statique par nom de modèle
|
|
||||||
- Configuration codée en dur dans `litellm/config.yaml`
|
|
||||||
|
|
||||||
**2026-07-20** : Migration vers jean-router
|
|
||||||
- Routing dynamique basé sur les presets Jean
|
|
||||||
- Gestion automatique de la VRAM
|
|
||||||
- Suppression de LiteLLM (une couche de moins)
|
|
||||||
- Nouveaux modèles automatiquement disponibles
|
|
||||||
|
|
||||||
## Références
|
|
||||||
|
|
||||||
- **Code jean-router** : `/jean-router/app.py`
|
|
||||||
- **Docker compose** : `/docker-compose.litellm.yml`
|
|
||||||
- **Journal de session** : `/docs/05-journal/SESSION-2026-07-20-JEAN-ROUTER.md`
|
|
||||||
- **Repo Gitea** : https://gitea.folzocorp.fr/Folzo/Effi
|
|
||||||
@@ -8,11 +8,18 @@ Guide pour s'assurer que tous les services Effi démarrent automatiquement aprè
|
|||||||
|
|
||||||
| Service | Port | Docker | Dépendance WOL |
|
| Service | Port | Docker | Dépendance WOL |
|
||||||
|---------|------|--------|----------------|
|
|---------|------|--------|----------------|
|
||||||
|
| **llama.cpp** | 8080 | Native (systemd) | Oui |
|
||||||
| **Ollama** | 11434 | ✅ | Oui |
|
| **Ollama** | 11434 | ✅ | Oui |
|
||||||
| **Effi-STT** | 8200 | ✅ | Oui |
|
| **Effi-STT** | 8200 | ✅ | Oui |
|
||||||
| **openedai-speech** | 8100 | ✅ | Oui |
|
|
||||||
| **ComfyUI** | 8188 | ✅ | Oui |
|
| **ComfyUI** | 8188 | ✅ | Oui |
|
||||||
|
|
||||||
|
> **Note :** le TTS (openedai-speech, port 8100) tourne désormais sur l'effi-server,
|
||||||
|
> pas sur folzo-Corp. Il n'a donc pas besoin de WOL.
|
||||||
|
|
||||||
|
Le réveil est géré automatiquement par **`llm-proxy`** (effi-server) : au premier appel
|
||||||
|
vers une cible éteinte, il envoie 3 paquets magic (MAC du PC) puis attend que le service
|
||||||
|
soit prêt. Aucun script manuel nécessaire.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Configuration du démarrage automatique
|
## 🚀 Configuration du démarrage automatique
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
# Roadmap : wol-proxy → Orchestrateur Ollama
|
|
||||||
|
|
||||||
## Vision
|
|
||||||
|
|
||||||
Transformer le wol-proxy en point d'entree central pour TOUS les appels a Ollama,
|
|
||||||
avec des fonctions d'orchestration.
|
|
||||||
|
|
||||||
## Fonctionnalites futures
|
|
||||||
|
|
||||||
### 1. Logs centralises
|
|
||||||
- Enregistrer chaque requete : modele, prompt (taille), duree, status
|
|
||||||
- Detecter les erreurs recurrentes (timeout, 502, VRAM full)
|
|
||||||
- Tableau de bord des performances par modele
|
|
||||||
|
|
||||||
### 2. Temporisation / File d attente
|
|
||||||
- Si Open WebUI utilise deja Ollama (GPU occupe), mettre en file d attente
|
|
||||||
- Empecher les conflits VRAM entre services
|
|
||||||
- File FIFO avec timeout max
|
|
||||||
|
|
||||||
### 3. Orchestration multi-services
|
|
||||||
- Open WebUI (port 3000)
|
|
||||||
- Hermes Agent (profil local)
|
|
||||||
- Scripts de test automatises
|
|
||||||
- Futurs outils (n8n, pipelines RAG...)
|
|
||||||
|
|
||||||
### 4. Cache intelligent
|
|
||||||
- Mettre en cache les reponses identiques (system prompt identique)
|
|
||||||
- Cache-hit pricing comme DeepSeek
|
|
||||||
- Invalidation par duree ou par modele
|
|
||||||
|
|
||||||
### 5. Mode degraded
|
|
||||||
- Si PC gaming en veille : renvoyer une reponse "PC en veille, reessayez"
|
|
||||||
- Planifier un reveil automatique (cron)
|
|
||||||
- Fallback vers un petit modele local sur le J5040 si PC indisponible
|
|
||||||
|
|
||||||
### 6. Healthcheck / Monitoring
|
|
||||||
- Ping regulier du PC pour savoir s'il est eveille
|
|
||||||
- Statistiques : temps de reveil moyen, duree des requetes
|
|
||||||
- Alertes si PC injoignable depuis plus de X minutes
|
|
||||||
|
|
||||||
## Stack technique envisagee
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Proposition d evolution du docker-compose
|
|
||||||
wol-orchestrator:
|
|
||||||
build: ./wol-orchestrator
|
|
||||||
volumes:
|
|
||||||
- ./logs:/var/log/orchestrator
|
|
||||||
- ./cache:/var/cache/orchestrator
|
|
||||||
environment:
|
|
||||||
- REDIS_URL=redis://redis:6379 # File d attente
|
|
||||||
- LOG_LEVEL=info
|
|
||||||
- WAKE_TIMEOUT=180
|
|
||||||
- REQUEST_TIMEOUT=300
|
|
||||||
- MAX_QUEUE=10
|
|
||||||
```
|
|
||||||
|
|
||||||
## Priorite
|
|
||||||
|
|
||||||
| Fonction | Priorite | Effort | Impact |
|
|
||||||
|----------|----------|--------|--------|
|
|
||||||
| Logs | Haute | Faible | Utile tout de suite |
|
|
||||||
| File attente | Haute | Moyen | Evite conflits VRAM |
|
|
||||||
| Cache | Moyenne | Moyen | Economise tokens |
|
|
||||||
| Orchestration | Moyenne | Important | Architecture propre |
|
|
||||||
| Mode degrade | Basse | Moyen | Securite |
|
|
||||||
| Monitoring | Basse | Faible | Visibilite |
|
|
||||||
|
|
||||||
## Note
|
|
||||||
|
|
||||||
Idee originale de Folzo (juin 2026) — a implementer avec OpenCode Go ou un
|
|
||||||
langage adapte (Go, Rust ou Python selon les besoins de performance).
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# dépendances
|
|
||||||
RUN pip install --no-cache-dir flask requests
|
|
||||||
|
|
||||||
COPY app.py .
|
|
||||||
|
|
||||||
ENV LISTEN_PORT=3012
|
|
||||||
EXPOSE 3012
|
|
||||||
|
|
||||||
CMD ["python", "app.py"]
|
|
||||||
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,553 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Jean Router — Proxy dynamique pour Jean presets
|
|
||||||
Interroge Jean pour lister les presets disponibles et route les requêtes
|
|
||||||
vers le bon preset en faisant le switch automatique si nécessaire.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from flask import Flask, request, Response, jsonify
|
|
||||||
|
|
||||||
JEAN_WEB_HOST = os.getenv("JEAN_WEB_HOST", "192.168.1.11")
|
|
||||||
JEAN_WEB_PORT = int(os.getenv("JEAN_WEB_PORT", "8090"))
|
|
||||||
JEAN_PROXY_HOST = os.getenv("JEAN_PROXY_HOST", "localhost")
|
|
||||||
JEAN_PROXY_PORT = int(os.getenv("JEAN_PROXY_PORT", "3012"))
|
|
||||||
OLLAMA_PROXY_HOST = os.getenv("OLLAMA_PROXY_HOST", "localhost")
|
|
||||||
OLLAMA_PROXY_PORT = int(os.getenv("OLLAMA_PROXY_PORT", "3011"))
|
|
||||||
OLLAMA_DIRECT_HOST = os.getenv("OLLAMA_DIRECT_HOST", JEAN_WEB_HOST)
|
|
||||||
OLLAMA_DIRECT_PORT = int(os.getenv("OLLAMA_DIRECT_PORT", "11434"))
|
|
||||||
ENABLE_OLLAMA = os.getenv("ENABLE_OLLAMA", "true").lower() == "true"
|
|
||||||
ENABLE_VRAM_MANAGEMENT = os.getenv("ENABLE_VRAM_MANAGEMENT", "true").lower() == "true"
|
|
||||||
OLLAMA_KEEP_ALIVE_MINUTES = int(os.getenv("OLLAMA_KEEP_ALIVE_MINUTES", "5"))
|
|
||||||
LISTEN_PORT = int(os.getenv("LISTEN_PORT", "3014"))
|
|
||||||
SWITCH_TIMEOUT = int(os.getenv("SWITCH_TIMEOUT", "90"))
|
|
||||||
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "2"))
|
|
||||||
JEAN_STOP_TIMEOUT = int(os.getenv("JEAN_STOP_TIMEOUT", "30"))
|
|
||||||
OLLAMA_UNLOAD_TIMEOUT = int(os.getenv("OLLAMA_UNLOAD_TIMEOUT", "15"))
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
presets_cache = {"data": [], "timestamp": 0}
|
|
||||||
ollama_cache = {"data": [], "timestamp": 0}
|
|
||||||
jean_ctx_cache = {}
|
|
||||||
CACHE_TTL = 30
|
|
||||||
switch_lock = threading.Lock()
|
|
||||||
vram_lock = threading.Lock()
|
|
||||||
vram_state = {"active_backend": "jean"}
|
|
||||||
ollama_unload_timer = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_presets(force_refresh=False):
|
|
||||||
"""Récupère la liste des presets depuis Jean"""
|
|
||||||
now = time.time()
|
|
||||||
if not force_refresh and presets_cache["data"] and (now - presets_cache["timestamp"]) < CACHE_TTL:
|
|
||||||
return presets_cache["data"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = f"http://{JEAN_WEB_HOST}:{JEAN_WEB_PORT}/api/presets"
|
|
||||||
r = requests.get(url, timeout=3)
|
|
||||||
if r.ok:
|
|
||||||
presets_cache["data"] = r.json()
|
|
||||||
presets_cache["timestamp"] = now
|
|
||||||
return presets_cache["data"]
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[Presets] Erreur récupération: {e}")
|
|
||||||
|
|
||||||
return presets_cache["data"]
|
|
||||||
|
|
||||||
|
|
||||||
def get_ollama_models(force_refresh=False):
|
|
||||||
"""Récupère la liste des modèles depuis Ollama"""
|
|
||||||
if not ENABLE_OLLAMA:
|
|
||||||
return []
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
if not force_refresh and ollama_cache["data"] and (now - ollama_cache["timestamp"]) < CACHE_TTL:
|
|
||||||
return ollama_cache["data"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = f"http://{OLLAMA_PROXY_HOST}:{OLLAMA_PROXY_PORT}/api/tags"
|
|
||||||
r = requests.get(url, timeout=3)
|
|
||||||
if r.ok:
|
|
||||||
data = r.json().get("models", [])
|
|
||||||
ollama_cache["data"] = data
|
|
||||||
ollama_cache["timestamp"] = now
|
|
||||||
return data
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[Ollama] Erreur récupération: {e}")
|
|
||||||
|
|
||||||
return ollama_cache["data"]
|
|
||||||
|
|
||||||
|
|
||||||
def get_jean_context_lengths():
|
|
||||||
"""Récupère les context_length depuis Jean /v1/models (avec cache)"""
|
|
||||||
preset_ctx_map = {
|
|
||||||
"ornith-35b-q6": 64000,
|
|
||||||
"qwen3.6-27b-mtp": 55000,
|
|
||||||
"qwen3.6-35b-moe-compact": 131072,
|
|
||||||
"qwen3.6-35b-moe-compact-reason": 131072,
|
|
||||||
"qwen3.6-35b-moe-nano": 100000,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/v1/models"
|
|
||||||
r = requests.get(url, timeout=2)
|
|
||||||
if r.ok:
|
|
||||||
models = r.json().get("data", [])
|
|
||||||
for m in models:
|
|
||||||
meta = m.get("meta", {})
|
|
||||||
n_ctx = meta.get("n_ctx")
|
|
||||||
if n_ctx:
|
|
||||||
alias = m.get("id", "")
|
|
||||||
jean_ctx_cache[alias] = n_ctx
|
|
||||||
log.debug(f"[Jean] Context length pour {alias}: {n_ctx}")
|
|
||||||
except Exception as e:
|
|
||||||
log.debug(f"[Jean] Impossible de récupérer context_length: {e}")
|
|
||||||
|
|
||||||
for preset_id, ctx in preset_ctx_map.items():
|
|
||||||
if preset_id not in jean_ctx_cache:
|
|
||||||
jean_ctx_cache[preset_id] = ctx
|
|
||||||
|
|
||||||
return jean_ctx_cache
|
|
||||||
|
|
||||||
|
|
||||||
def get_active_preset():
|
|
||||||
"""Retourne le preset actuellement actif"""
|
|
||||||
presets = get_presets()
|
|
||||||
for p in presets:
|
|
||||||
if p.get("active"):
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def switch_preset(preset_id: str) -> bool:
|
|
||||||
"""Switch vers un preset spécifique par son ID"""
|
|
||||||
presets = get_presets(force_refresh=True)
|
|
||||||
|
|
||||||
for i, p in enumerate(presets):
|
|
||||||
if p["id"] == preset_id:
|
|
||||||
if p.get("active"):
|
|
||||||
log.info(f"[Switch] Preset {preset_id} déjà actif")
|
|
||||||
return True
|
|
||||||
|
|
||||||
log.info(f"[Switch] Activation preset {preset_id} (index {i+1})")
|
|
||||||
try:
|
|
||||||
url = f"http://{JEAN_WEB_HOST}:{JEAN_WEB_PORT}/api/switch"
|
|
||||||
r = requests.post(url, json={"n": i + 1}, timeout=5)
|
|
||||||
if not r.ok:
|
|
||||||
log.error(f"[Switch] Erreur API: {r.text}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f"[Switch] Erreur requête: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.info(f"[Switch] Attente chargement modèle...")
|
|
||||||
deadline = time.time() + SWITCH_TIMEOUT
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
health_url = f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/health"
|
|
||||||
hr = requests.get(health_url, timeout=2)
|
|
||||||
if hr.ok:
|
|
||||||
models_url = f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/v1/models"
|
|
||||||
mr = requests.get(models_url, timeout=2)
|
|
||||||
if mr.ok:
|
|
||||||
models = mr.json().get("data", [])
|
|
||||||
if models:
|
|
||||||
log.info(f"[Switch] Modèle chargé après {int(time.time() - deadline + SWITCH_TIMEOUT)}s")
|
|
||||||
presets_cache["timestamp"] = 0
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
|
||||||
|
|
||||||
log.error(f"[Switch] Timeout après {SWITCH_TIMEOUT}s")
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.error(f"[Switch] Preset {preset_id} non trouvé")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def stop_jean() -> bool:
|
|
||||||
"""Arrête le service Jean pour libérer la VRAM"""
|
|
||||||
log.info("[VRAM] Arrêt de Jean pour libérer la VRAM...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = f"http://{JEAN_WEB_HOST}:{JEAN_WEB_PORT}/api/stop"
|
|
||||||
r = requests.post(url, timeout=5)
|
|
||||||
if r.ok:
|
|
||||||
log.info("[VRAM] Jean arrêté via API web")
|
|
||||||
else:
|
|
||||||
log.warning(f"[VRAM] API stop a retourné {r.status_code}, tentative via health check")
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[VRAM] Erreur API stop: {e}")
|
|
||||||
|
|
||||||
deadline = time.time() + JEAN_STOP_TIMEOUT
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
health_url = f"http://{JEAN_WEB_HOST}:{JEAN_WEB_PORT}/health"
|
|
||||||
r = requests.get(health_url, timeout=2)
|
|
||||||
if not r.ok:
|
|
||||||
log.info("[VRAM] Jean confirmé arrêté")
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
log.info("[VRAM] Jean confirmé arrêté (connexion refusée)")
|
|
||||||
return True
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
|
||||||
|
|
||||||
log.error(f"[VRAM] Timeout: Jean ne s'est pas arrêté après {JEAN_STOP_TIMEOUT}s")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def start_jean() -> bool:
|
|
||||||
"""Démarre le service Jean"""
|
|
||||||
log.info("[VRAM] Démarrage de Jean...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = f"http://{JEAN_WEB_HOST}:{JEAN_WEB_PORT}/api/start"
|
|
||||||
r = requests.post(url, timeout=5)
|
|
||||||
if r.ok:
|
|
||||||
log.info("[VRAM] Jean démarré via API web")
|
|
||||||
else:
|
|
||||||
log.warning(f"[VRAM] API start a retourné {r.status_code}")
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[VRAM] Erreur API start: {e}")
|
|
||||||
|
|
||||||
log.info("[VRAM] Attente du chargement du modèle Jean...")
|
|
||||||
deadline = time.time() + SWITCH_TIMEOUT
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
health_url = f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/health"
|
|
||||||
hr = requests.get(health_url, timeout=2)
|
|
||||||
if hr.ok:
|
|
||||||
models_url = f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/v1/models"
|
|
||||||
mr = requests.get(models_url, timeout=2)
|
|
||||||
if mr.ok:
|
|
||||||
models = mr.json().get("data", [])
|
|
||||||
if models:
|
|
||||||
log.info(f"[VRAM] Jean prêt après {int(time.time() - deadline + SWITCH_TIMEOUT)}s")
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
|
||||||
|
|
||||||
log.error(f"[VRAM] Timeout: Jean n'a pas démarré après {SWITCH_TIMEOUT}s")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def unload_ollama() -> bool:
|
|
||||||
"""Force le déchargement des modèles Ollama pour libérer la VRAM"""
|
|
||||||
log.info("[VRAM] Déchargement des modèles Ollama...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = f"http://{OLLAMA_DIRECT_HOST}:{OLLAMA_DIRECT_PORT}/api/generate"
|
|
||||||
payload = {
|
|
||||||
"model": "mistral",
|
|
||||||
"keep_alive": 0,
|
|
||||||
"prompt": ""
|
|
||||||
}
|
|
||||||
r = requests.post(url, json=payload, timeout=5)
|
|
||||||
if r.ok:
|
|
||||||
log.info("[VRAM] Ollama déchargé via API")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
log.warning(f"[VRAM] API unload a retourné {r.status_code}")
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[VRAM] Erreur API unload: {e}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = f"http://{OLLAMA_DIRECT_HOST}:{OLLAMA_DIRECT_PORT}/api/ps"
|
|
||||||
r = requests.get(url, timeout=3)
|
|
||||||
if r.ok:
|
|
||||||
models = r.json().get("models", [])
|
|
||||||
if not models:
|
|
||||||
log.info("[VRAM] Ollama confirmé déchargé (aucun modèle actif)")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
log.warning(f"[VRAM] Ollama a encore {len(models)} modèle(s) chargé(s)")
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[VRAM] Impossible de vérifier /api/ps: {e}")
|
|
||||||
|
|
||||||
time.sleep(2)
|
|
||||||
log.info("[VRAM] Ollama supposé déchargé (timeout)")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def schedule_ollama_unload():
|
|
||||||
"""Programme le déchargement d'Ollama après OLLAMA_KEEP_ALIVE_MINUTES"""
|
|
||||||
global ollama_unload_timer
|
|
||||||
|
|
||||||
def delayed_unload():
|
|
||||||
log.info(f"[VRAM] Timer écoulé ({OLLAMA_KEEP_ALIVE_MINUTES}min), déchargement Ollama...")
|
|
||||||
unload_ollama()
|
|
||||||
|
|
||||||
if ollama_unload_timer:
|
|
||||||
ollama_unload_timer.cancel()
|
|
||||||
log.info("[VRAM] Timer précédent annulé")
|
|
||||||
|
|
||||||
ollama_unload_timer = threading.Timer(
|
|
||||||
OLLAMA_KEEP_ALIVE_MINUTES * 60,
|
|
||||||
delayed_unload
|
|
||||||
)
|
|
||||||
ollama_unload_timer.daemon = True
|
|
||||||
ollama_unload_timer.start()
|
|
||||||
log.info(f"[VRAM] Déchargement Ollama programmé dans {OLLAMA_KEEP_ALIVE_MINUTES} minutes")
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_ollama_unload():
|
|
||||||
"""Annule le déchargement programmé d'Ollama"""
|
|
||||||
global ollama_unload_timer
|
|
||||||
if ollama_unload_timer:
|
|
||||||
ollama_unload_timer.cancel()
|
|
||||||
ollama_unload_timer = None
|
|
||||||
log.info("[VRAM] Timer de déchargement Ollama annulé")
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_vram_for_backend(target_backend: str) -> bool:
|
|
||||||
"""
|
|
||||||
S'assure que la VRAM est disponible pour le backend cible.
|
|
||||||
- Si target=ollama et jean actif → arrête Jean, annule timer unload Ollama
|
|
||||||
- Si target=jean et ollama actif → programme déchargement Ollama dans X min, démarre Jean
|
|
||||||
"""
|
|
||||||
if not ENABLE_VRAM_MANAGEMENT:
|
|
||||||
return True
|
|
||||||
|
|
||||||
with vram_lock:
|
|
||||||
current = vram_state["active_backend"]
|
|
||||||
|
|
||||||
if current == target_backend:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if target_backend == "ollama" and current == "jean":
|
|
||||||
log.info("[VRAM] Switch Jean → Ollama")
|
|
||||||
cancel_ollama_unload()
|
|
||||||
if not stop_jean():
|
|
||||||
return False
|
|
||||||
vram_state["active_backend"] = "ollama"
|
|
||||||
return True
|
|
||||||
|
|
||||||
elif target_backend == "jean" and current == "ollama":
|
|
||||||
log.info("[VRAM] Switch Ollama → Jean")
|
|
||||||
schedule_ollama_unload()
|
|
||||||
if not start_jean():
|
|
||||||
return False
|
|
||||||
vram_state["active_backend"] = "jean"
|
|
||||||
return True
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/v1/models", methods=["GET"])
|
|
||||||
def list_models():
|
|
||||||
"""Expose tous les presets Jean + modèles Ollama comme modèles OpenAI"""
|
|
||||||
presets = get_presets()
|
|
||||||
jean_ctx = get_jean_context_lengths()
|
|
||||||
models = []
|
|
||||||
|
|
||||||
for p in presets:
|
|
||||||
model_id = p["id"]
|
|
||||||
model_name = p.get("name", model_id)
|
|
||||||
|
|
||||||
model_info = {
|
|
||||||
"id": model_id,
|
|
||||||
"object": "model",
|
|
||||||
"created": int(time.time()),
|
|
||||||
"owned_by": "jean",
|
|
||||||
"permission": [],
|
|
||||||
"root": model_id,
|
|
||||||
"parent": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
meta = {}
|
|
||||||
if p.get("quant"):
|
|
||||||
meta["quantization"] = p["quant"]
|
|
||||||
if p.get("reasoning"):
|
|
||||||
meta["reasoning"] = p["reasoning"]
|
|
||||||
if p.get("active"):
|
|
||||||
meta["active"] = True
|
|
||||||
if p.get("bench"):
|
|
||||||
meta["bench"] = p["bench"]
|
|
||||||
|
|
||||||
ctx_len = jean_ctx.get(model_id)
|
|
||||||
if ctx_len:
|
|
||||||
meta["context_length"] = ctx_len
|
|
||||||
model_info["context_length"] = ctx_len
|
|
||||||
model_info["context_window"] = ctx_len
|
|
||||||
model_info["max_model_len"] = ctx_len
|
|
||||||
|
|
||||||
if meta:
|
|
||||||
model_info["meta"] = meta
|
|
||||||
|
|
||||||
models.append(model_info)
|
|
||||||
|
|
||||||
if ENABLE_OLLAMA:
|
|
||||||
ollama_models = get_ollama_models()
|
|
||||||
for m in ollama_models:
|
|
||||||
model_id = m.get("name", "").replace(":latest", "")
|
|
||||||
if not model_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
model_info = {
|
|
||||||
"id": model_id,
|
|
||||||
"object": "model",
|
|
||||||
"created": int(time.time()),
|
|
||||||
"owned_by": "ollama",
|
|
||||||
"permission": [],
|
|
||||||
"root": model_id,
|
|
||||||
"parent": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
details = m.get("details", {})
|
|
||||||
meta = {}
|
|
||||||
if details.get("parameter_size"):
|
|
||||||
meta["parameter_size"] = details["parameter_size"]
|
|
||||||
if details.get("quantization_level"):
|
|
||||||
meta["quantization"] = details["quantization_level"]
|
|
||||||
if details.get("family"):
|
|
||||||
meta["family"] = details["family"]
|
|
||||||
if details.get("context_length"):
|
|
||||||
ctx_len = details["context_length"]
|
|
||||||
meta["context_length"] = ctx_len
|
|
||||||
model_info["context_length"] = ctx_len
|
|
||||||
model_info["context_window"] = ctx_len
|
|
||||||
model_info["max_model_len"] = ctx_len
|
|
||||||
if m.get("capabilities"):
|
|
||||||
meta["capabilities"] = m["capabilities"]
|
|
||||||
|
|
||||||
if meta:
|
|
||||||
model_info["meta"] = meta
|
|
||||||
|
|
||||||
models.append(model_info)
|
|
||||||
|
|
||||||
return jsonify({"object": "list", "data": models})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/v1/chat/completions", methods=["POST"])
|
|
||||||
def chat_completions():
|
|
||||||
"""Route les requêtes chat vers Jean ou Ollama selon le modèle"""
|
|
||||||
data = request.get_json()
|
|
||||||
model_id = data.get("model")
|
|
||||||
|
|
||||||
if not model_id:
|
|
||||||
return jsonify({"error": "model parameter required"}), 400
|
|
||||||
|
|
||||||
presets = get_presets()
|
|
||||||
jean_preset_ids = [p["id"] for p in presets]
|
|
||||||
|
|
||||||
if model_id in jean_preset_ids:
|
|
||||||
if not ensure_vram_for_backend("jean"):
|
|
||||||
return jsonify({"error": "Failed to prepare VRAM for Jean"}), 503
|
|
||||||
|
|
||||||
with switch_lock:
|
|
||||||
active = get_active_preset()
|
|
||||||
if not active or active["id"] != model_id:
|
|
||||||
log.info(f"[Router] Switch Jean requis: {active['id'] if active else 'none'} → {model_id}")
|
|
||||||
if not switch_preset(model_id):
|
|
||||||
return jsonify({"error": f"Failed to switch to preset {model_id}"}), 503
|
|
||||||
|
|
||||||
upstream_url = f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/v1/chat/completions"
|
|
||||||
log.info(f"[Router] → Jean ({model_id})")
|
|
||||||
elif ENABLE_OLLAMA:
|
|
||||||
ollama_models = get_ollama_models()
|
|
||||||
ollama_ids = [m.get("name", "").replace(":latest", "") for m in ollama_models]
|
|
||||||
|
|
||||||
if model_id in ollama_ids:
|
|
||||||
if not ensure_vram_for_backend("ollama"):
|
|
||||||
return jsonify({"error": "Failed to prepare VRAM for Ollama"}), 503
|
|
||||||
|
|
||||||
cancel_ollama_unload()
|
|
||||||
|
|
||||||
upstream_url = f"http://{OLLAMA_PROXY_HOST}:{OLLAMA_PROXY_PORT}/v1/chat/completions"
|
|
||||||
log.info(f"[Router] → Ollama ({model_id})")
|
|
||||||
else:
|
|
||||||
return jsonify({"error": f"Unknown model: {model_id}"}), 404
|
|
||||||
else:
|
|
||||||
return jsonify({"error": f"Unknown model: {model_id}"}), 404
|
|
||||||
|
|
||||||
headers = dict(request.headers)
|
|
||||||
headers.pop("Host", None)
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = requests.request(
|
|
||||||
method=request.method,
|
|
||||||
url=upstream_url,
|
|
||||||
headers=headers,
|
|
||||||
json=data,
|
|
||||||
stream=True,
|
|
||||||
timeout=None,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f"[Router] Erreur upstream: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 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 proxy(path):
|
|
||||||
"""Proxy générique pour les autres endpoints"""
|
|
||||||
if path.startswith("v1/chat/completions"):
|
|
||||||
return chat_completions()
|
|
||||||
|
|
||||||
upstream_url = f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/{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:
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
log.info(f"[Router] Démarrage sur port {LISTEN_PORT}")
|
|
||||||
log.info(f"[Router] Jean Web: {JEAN_WEB_HOST}:{JEAN_WEB_PORT}")
|
|
||||||
log.info(f"[Router] Jean Proxy: {JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}")
|
|
||||||
if ENABLE_OLLAMA:
|
|
||||||
log.info(f"[Router] Ollama Proxy: {OLLAMA_PROXY_HOST}:{OLLAMA_PROXY_PORT}")
|
|
||||||
else:
|
|
||||||
log.info(f"[Router] Ollama: désactivé")
|
|
||||||
app.run(host="0.0.0.0", port=LISTEN_PORT, threaded=True)
|
|
||||||
@@ -6,7 +6,6 @@ RUN pip install --no-cache-dir flask requests
|
|||||||
|
|
||||||
COPY app.py .
|
COPY app.py .
|
||||||
|
|
||||||
ENV LISTEN_PORT=3014
|
EXPOSE 3011 3012 3015 3016
|
||||||
EXPOSE 3014
|
|
||||||
|
|
||||||
CMD ["python", "app.py"]
|
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)
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
upstream searxng {
|
|
||||||
server effi-searxng:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 6060;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://searxng;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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"]
|
|
||||||
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
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)
|
|
||||||
Reference in New Issue
Block a user