# Plan de test — Capacités des LLM (vision, thinking) ## 1. Vérification de /v1/models ```bash # Vérifier que les champs sont bien présents curl -s http://localhost:3014/v1/models | python3 -c " import json, sys data = json.load(sys.stdin)['data'] for m in data: ctx = m.get('context_length', '?') max_tok = m.get('max_tokens', '?') max_in = m.get('max_input_tokens', '?') caps = m.get('capabilities', []) print(f'{m[\"id\"]:45s} | ctx={str(ctx):>8s} | max_tok={str(max_tok):>8s} | caps={caps}') " # Forcer le refresh curl -s "http://localhost:3014/v1/models?refresh=true" | python3 -c "..." ``` **Checklist :** - [ ] Tous les modèles ont `context_length` (pas `?` ni `None`) - [ ] Tous les modèles ont `max_tokens` et `max_input_tokens` - [ ] Les modèles ont `capabilities` (pas vide, min `["completion"]`) - [ ] Le preset "Ornith 1.0 35b Q4 MOE" a des champs valides --- ## 2. Test de vision (multimodal) ### 2.1 — Modèles censés supporter la vision (selon `/v1/models`) | Modèle Ollama | Vision annoncée | |---------------|----------------| | qwen3.5:9b | vision | | qwen3.5:0.8b | vision | | qwen3.5:4b | vision | | qwen3.5:2b | vision | | qwen3.5 (latest) | vision | | gemma4:12b | vision | | ministral-3:14b | vision | ```bash # Script de test (enregistre comme test_vision.sh) for MODEL in qwen3.5:9b qwen3.5:4b qwen3.5:2b gemma4:12b ministral-3:14b; do echo "=== Test vision: $MODEL ===" curl -s http://localhost:3014/v1/chat/completions \ -H "Content-Type: application/json" \ -d "{ \"model\": \"$MODEL\", \"messages\": [{ \"role\": \"user\", \"content\": \"Décris ce que tu vois dans l'image.\" }], \"max_tokens\": 100 }" | python3 -c " import json, sys d = json.load(sys.stdin) if 'error' in d: print(f' ERREUR: {d[\"error\"]}') else: content = d['choices'][0]['message']['content'] print(f' REPONSE: {content[:200]}') " echo "" done ``` ### 2.2 — Test avec image réelle ```bash # Encoder une image en base64 (necessite une image de test) IMAGE_B64=$(base64 -w0 /path/to/test-image.jpg) curl -s http://localhost:3014/v1/chat/completions \ -H "Content-Type: application/json" \ -d "{ \"model\": \"qwen3.5:9b\", \"messages\": [{ \"role\": \"user\", \"content\": [ {\"type\": \"text\", \"text\": \"Décris cette image en une phrase.\"}, {\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/jpeg;base64,$IMAGE_B64\"}} ] }], \"max_tokens\": 200 }" | python3 -m json.tool ``` --- ## 3. Test de thinking/reasoning ### 3.1 — Modèles censés supporter le thinking | Modèle | Thinking annoncé | |--------|-----------------| | qwen3.5:9b | thinking | | qwen3.5:0.8b | thinking | | qwen3.5:4b | thinking | | qwen3.5:2b | thinking | | qwen3.5 (latest) | thinking | | qwen3:8b | thinking | | deepseek-r1:14b | thinking | | deepseek-r1 | thinking | | gemma4:12b | thinking | | gemma4:26b | thinking | | gemma4:e4b | thinking | | magistral | thinking | | gpt-oss | thinking | | gemma4 (latest) | thinking | ```bash # Test de raisonnement for MODEL in deepseek-r1:14b qwen3.5:9b qwen3:8b gemma4:12b; do echo "=== Test thinking: $MODEL ===" curl -s http://localhost:3014/v1/chat/completions \ -H "Content-Type: application/json" \ -d "{ \"model\": \"$MODEL\", \"messages\": [{ \"role\": \"user\", \"content\": \"Si un train part de Paris à 100 km/h et un autre de Lyon à 150 km/h, distance 400 km, combien de temps avant qu'ils se croisent ? Explique ton raisonnement étape par étape.\" }], \"max_tokens\": 500 }" | python3 -c " import json, sys d = json.load(sys.stdin) if 'error' in d: print(f' ERREUR: {d[\"error\"]}') else: content = d['choices'][0]['message']['content'] print(f' REPONSE: {content[:300]}') # Vérifier si la réponse contient un raisonnement structuré has_reasoning = any(kw in content.lower() for kw in ['étape', 'd\'abord', 'premièrement', 'step', 'first', 'raisonn']) print(f' STRUCTURE_VISIBLE: {has_reasoning}') " echo "" done ``` ### 3.2 — Test thinking avec le paramètre OpenAI `reasoning_effort` > Pour les modèles Jean avec `reasoning: "on"` (Ornith, qwen3.6-27b-mtp, etc.) ```bash curl -s http://localhost:3014/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "ornith-35b-q6", "messages": [{"role": "user", "content": "Combien de 'r' y a-t-il dans le mot \"strawberry\" ?"}], "max_tokens": 300 }' | python3 -c " import json, sys d = json.load(sys.stdin) print(json.dumps(d, indent=2)[:2000]) " # Le modèle devrait répondre : 3 (s-t-r-a-w-b-e-r-r-y → 3 'r') # Si thinking est actif, il devrait montrer son raisonnement ``` --- ## 4. Test des modèles sans certaines capacités ```bash # Tester un modèle sans vision — lui envoyer une image devrait échouer curl -s http://localhost:3014/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "mistral", "messages": [{ "role": "user", "content": [ {"type": "text", "text": "Décris cette image."}, {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}} ] }], "max_tokens": 100 }' | python3 -c " import json, sys d = json.load(sys.stdin) print(f'Status attendu: ERREUR → Reçu: {\"error\" in d}') " ``` --- ## 5. Test de routing (Jean ↔ Ollama) ```bash # Tester un modèle Jean (doit déclencher le switch VRAM) curl -s http://localhost:3014/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "ornith-35b-q6", "messages": [{"role": "user", "content": "Bonjour, es-tu fonctionnel ?"}], "max_tokens": 50 }' | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('choices',[{}])[0].get('message',{}).get('content','ERREUR:'+json.dumps(d))[:300])" # Puis tester un modèle Ollama (doit déclencher le switch inverse) curl -s http://localhost:3014/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "mistral", "messages": [{"role": "user", "content": "Bonjour, es-tu fonctionnel ?"}], "max_tokens": 50 }' | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('choices',[{}])[0].get('message',{}).get('content','ERREUR:'+json.dumps(d))[:300])" ``` --- ## 6. Synthèse | Check | Méthode | |-------|---------| | `/v1/models` a `max_tokens` + `max_input_tokens` | curl + jq | | `/v1/models` a `capabilities` non-vide | curl + jq | | Preset "Ornith 1.0 35b Q4 MOE" existe avec champs valides | curl + jq | | Modèle `vision=true` accepte image sans erreur | curl + image base64 | | Modèle `vision=false` rejette image | curl + image base64 | | Modèle `thinking=true` montre raisonnement structuré | curl + analyse réponse | | Switch Jean→Ollama fonctionne | 2 requêtes séquentielles | | `?refresh=true` force la mise à jour du cache | 2 requêtes avec timestamp | --- ## 7. Mise à jour OpenFox ```bash openfox update # télécharge la nouvelle version kill $(lsof -t -i :10369) # tue l'ancien processus (sinon EADDRINUSE) openfox # redémarre ``` --- ## 8. Mise à jour jean-router sur effi-server ```bash # Depuis la machine locale scp jean-router/app.py effi-server:/home/effi/effi/jean-router/app.py # Sur effi-server ssh effi-server cd /home/effi/effi docker build -t effi-jean-router ./jean-router docker stop jean-router && docker rm jean-router docker run -d --name jean-router --network host \ --env-file /home/effi/effi/.env \ -e JEAN_WEB_HOST=192.168.1.11 \ -e OLLAMA_DIRECT_HOST=192.168.1.11 \ effi-jean-router ```