feat(p1z): centralize V4 reasoning model resolution (DGX-safe)
Remplace le default runtime dangereux `qwen2.5vl:7b` (absent du tunnel DGX -> 404) des chemins V4/reasoning par un helper central get_reasoning_model(). - core/detection/vlm_config.py : + get_reasoning_model() + DEFAULT_REASONING_MODEL (qwen2.5vl:7b-rpa). Ordre : RPA_REASONING_MODEL -> RPA_VLM_MODEL/VLM_MODEL -> default DGX-safe. Pas d'appel reseau (lazy, safe a l'import). - core/execution/input_handler.py, observe_reason_act.py (x3), core/cognition/vram_orchestrator.py : migration des 5 call-sites. - tests/unit/test_reasoning_model.py : 8 tests (default DGX-safe, ordre de resolution, non-regression wiring des 3 modules V4). Hors scope (signale lot P1.w) : DEFAULT_VLM_MODEL=gemma4:latest reste fallback de get_vlm_model(). Client gele non touche. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ Orchestrateur VRAM — gère le chargement/déchargement des modèles selon le m
|
|||||||
|
|
||||||
Deux modes :
|
Deux modes :
|
||||||
- SHADOW : streaming server + agent_chat actifs, VLM raisonnement déchargé
|
- SHADOW : streaming server + agent_chat actifs, VLM raisonnement déchargé
|
||||||
- REPLAY : VLM raisonnement (qwen2.5vl:7b) chargé, services non-essentiels stoppés
|
- REPLAY : VLM raisonnement (cf. get_reasoning_model) chargé, services non-essentiels stoppés
|
||||||
|
|
||||||
Bascule automatique ou manuelle selon le contexte.
|
Bascule automatique ou manuelle selon le contexte.
|
||||||
"""
|
"""
|
||||||
@@ -15,10 +15,12 @@ import time
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from core.detection.vlm_config import get_reasoning_model
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||||
REASONING_MODEL = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b")
|
REASONING_MODEL = get_reasoning_model()
|
||||||
MIN_VRAM_FOR_REASONING = 5.0 # Go minimum pour charger le modèle de raisonnement
|
MIN_VRAM_FOR_REASONING = 5.0 # Go minimum pour charger le modèle de raisonnement
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -261,6 +261,42 @@ def get_bbox_grounding_model() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# P1.z (2026-06-04) : résolution centralisée du modèle V4/reasoning, DGX-safe
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Modèle de raisonnement V4/ORA par défaut — DGX-safe.
|
||||||
|
# Les chemins reasoning (ORALoop, détection dialogue/popup, vram_orchestrator)
|
||||||
|
# font du VLM généraliste sur screenshot (JSON action/decision), pas du grounding
|
||||||
|
# bbox. Le default est aligné sur le modèle présent sur le tunnel DGX
|
||||||
|
# (qwen2.5vl:7b-rpa), PAS sur `qwen2.5vl:7b` brut qui est absent du DGX → 404.
|
||||||
|
DEFAULT_REASONING_MODEL = "qwen2.5vl:7b-rpa"
|
||||||
|
|
||||||
|
|
||||||
|
def get_reasoning_model() -> str:
|
||||||
|
"""Retourne le modèle pour les chemins V4/reasoning (ORALoop, détection
|
||||||
|
dialogue/popup, orchestration VRAM).
|
||||||
|
|
||||||
|
Distinct du grounding (get_grounding_profile / get_bbox_grounding_model) :
|
||||||
|
ici on raisonne en langage naturel + JSON sur un screenshot, pas de
|
||||||
|
coordonnées. Pas d'appel réseau (résolution lazy, safe à l'import).
|
||||||
|
|
||||||
|
Ordre de résolution :
|
||||||
|
1. RPA_REASONING_MODEL (dédié, prioritaire)
|
||||||
|
2. RPA_VLM_MODEL / VLM_MODEL (hérite de la config VLM existante)
|
||||||
|
3. DEFAULT_REASONING_MODEL (qwen2.5vl:7b-rpa, présent sur DGX)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Nom du modèle de raisonnement (ex: "qwen2.5vl:7b-rpa").
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
os.environ.get("RPA_REASONING_MODEL")
|
||||||
|
or os.environ.get("RPA_VLM_MODEL")
|
||||||
|
or os.environ.get("VLM_MODEL")
|
||||||
|
or DEFAULT_REASONING_MODEL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def needs_think_false(model_name: str) -> bool:
|
def needs_think_false(model_name: str) -> bool:
|
||||||
"""Détermine si un modèle nécessite think=false dans le payload.
|
"""Détermine si un modèle nécessite think=false dans le payload.
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import shutil
|
|||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from core.detection.vlm_config import get_reasoning_model
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -291,7 +293,7 @@ Si l'écran est normal sans action nécessaire, réponds action="nothing".
|
|||||||
Réponds UNIQUEMENT le JSON, pas d'explication."""
|
Réponds UNIQUEMENT le JSON, pas d'explication."""
|
||||||
|
|
||||||
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||||
model = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b")
|
model = get_reasoning_model()
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{ollama_url}/api/generate",
|
f"{ollama_url}/api/generate",
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import re
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
from core.detection.vlm_config import get_reasoning_model
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Import du contexte cognitif (mémoire de travail)
|
# Import du contexte cognitif (mémoire de travail)
|
||||||
@@ -407,7 +409,7 @@ Règles:
|
|||||||
|
|
||||||
# --- Appel VLM (Ollama) ---
|
# --- Appel VLM (Ollama) ---
|
||||||
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||||
model = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b")
|
model = get_reasoning_model()
|
||||||
|
|
||||||
print(f"🧠 [ORA/reason_instruction] Appel VLM {model}...")
|
print(f"🧠 [ORA/reason_instruction] Appel VLM {model}...")
|
||||||
|
|
||||||
@@ -1207,7 +1209,7 @@ Règles:
|
|||||||
image_b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
image_b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||||
|
|
||||||
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||||
model = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b")
|
model = get_reasoning_model()
|
||||||
|
|
||||||
resp = requests.post(f"{ollama_url}/api/generate", json={
|
resp = requests.post(f"{ollama_url}/api/generate", json={
|
||||||
"model": model,
|
"model": model,
|
||||||
@@ -1963,7 +1965,7 @@ Règles:
|
|||||||
)
|
)
|
||||||
|
|
||||||
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||||
model = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b")
|
model = get_reasoning_model()
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{ollama_url}/api/generate",
|
f"{ollama_url}/api/generate",
|
||||||
|
|||||||
82
tests/unit/test_reasoning_model.py
Normal file
82
tests/unit/test_reasoning_model.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""Tests P1.z — résolution centralisée du modèle V4/reasoning (DGX-safe).
|
||||||
|
|
||||||
|
Garantit que les chemins V4/reasoning (ORALoop, détection dialogue/popup,
|
||||||
|
vram_orchestrator) ne tombent plus sur le default brut `qwen2.5vl:7b` (absent
|
||||||
|
du tunnel DGX → 404) quand `RPA_REASONING_MODEL` n'est pas positionné.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.detection import vlm_config
|
||||||
|
|
||||||
|
|
||||||
|
# Le default historique dangereux : présent dans aucun modèle DGX → 404.
|
||||||
|
DANGEROUS_RAW_DEFAULT = "qwen2.5vl:7b"
|
||||||
|
|
||||||
|
# Les 3 env vars qui influencent la résolution reasoning.
|
||||||
|
_REASONING_ENVS = ("RPA_REASONING_MODEL", "RPA_VLM_MODEL", "VLM_MODEL")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def clean_reasoning_env(monkeypatch):
|
||||||
|
"""Neutralise toute env reasoning/VLM pour tester le default pur."""
|
||||||
|
for var in _REASONING_ENVS:
|
||||||
|
monkeypatch.delenv(var, raising=False)
|
||||||
|
return monkeypatch
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_is_dgx_safe(clean_reasoning_env):
|
||||||
|
"""Sans aucune env, le default ne doit PAS être `qwen2.5vl:7b` brut."""
|
||||||
|
model = vlm_config.get_reasoning_model()
|
||||||
|
assert model != DANGEROUS_RAW_DEFAULT
|
||||||
|
# Le default doit pointer vers un modèle présent sur DGX (tag -rpa).
|
||||||
|
assert model == vlm_config.DEFAULT_REASONING_MODEL
|
||||||
|
assert "-rpa" in model
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpa_reasoning_model_has_priority(clean_reasoning_env):
|
||||||
|
"""RPA_REASONING_MODEL prime sur tout le reste."""
|
||||||
|
clean_reasoning_env.setenv("RPA_REASONING_MODEL", "mon-modele:custom")
|
||||||
|
clean_reasoning_env.setenv("RPA_VLM_MODEL", "autre:vlm")
|
||||||
|
assert vlm_config.get_reasoning_model() == "mon-modele:custom"
|
||||||
|
|
||||||
|
|
||||||
|
def test_falls_back_to_vlm_model(clean_reasoning_env):
|
||||||
|
"""Sans RPA_REASONING_MODEL, on hérite de la config VLM existante."""
|
||||||
|
clean_reasoning_env.setenv("RPA_VLM_MODEL", "qwen2.5vl:7b-rpa")
|
||||||
|
assert vlm_config.get_reasoning_model() == "qwen2.5vl:7b-rpa"
|
||||||
|
|
||||||
|
|
||||||
|
def test_falls_back_to_legacy_vlm_model_alias(clean_reasoning_env):
|
||||||
|
"""L'alias de compat VLM_MODEL est honoré en dernier recours avant default."""
|
||||||
|
clean_reasoning_env.setenv("VLM_MODEL", "qwen2.5vl:7b-rpa")
|
||||||
|
assert vlm_config.get_reasoning_model() == "qwen2.5vl:7b-rpa"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"module_name",
|
||||||
|
[
|
||||||
|
"core.execution.input_handler",
|
||||||
|
"core.execution.observe_reason_act",
|
||||||
|
"core.cognition.vram_orchestrator",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_v4_modules_have_no_raw_hardcoded_default(module_name):
|
||||||
|
"""Non-régression : aucun module V4 ne hardcode plus
|
||||||
|
`os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b")`."""
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
source = open(module.__file__, encoding="utf-8").read()
|
||||||
|
assert 'RPA_REASONING_MODEL", "qwen2.5vl:7b"' not in source, (
|
||||||
|
f"{module_name} hardcode encore le default dangereux qwen2.5vl:7b"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vram_orchestrator_reasoning_model_is_dgx_safe(clean_reasoning_env):
|
||||||
|
"""Le constant module-level REASONING_MODEL, recalculé sans env,
|
||||||
|
ne doit pas valoir le default brut dangereux."""
|
||||||
|
from core.cognition import vram_orchestrator
|
||||||
|
importlib.reload(vram_orchestrator)
|
||||||
|
assert vram_orchestrator.REASONING_MODEL != DANGEROUS_RAW_DEFAULT
|
||||||
Reference in New Issue
Block a user