fix: skip missing code review ollama models
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s

This commit is contained in:
OoO
2026-06-18 14:34:53 +08:00
parent ba5fe06b13
commit c83fb4cfa9
3 changed files with 121 additions and 1 deletions

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.629"
SYSTEM_VERSION = "V10.630"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -142,6 +142,38 @@ def _code_review_ollama_host_reachable(host: str) -> bool:
return False
def _code_review_ollama_model_available(host: str, model: str) -> bool:
"""Skip a host/model pair when Ollama reports the requested model is absent."""
if not CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_ENABLED or not model:
return True
try:
resp = requests.get(
f"{str(host or '').rstrip('/')}/api/tags",
timeout=CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_TIMEOUT,
)
if resp.status_code != 200:
return True
names = {
str(item.get("name") or "")
for item in (resp.json().get("models") or [])
if isinstance(item, dict)
}
if model in names:
return True
if ":" not in model and f"{model}:latest" in names:
return True
logger.warning(
"[CodeReview] Ollama model preflight failed host=%s model=%s available=%s",
host,
model,
sorted(name for name in names if name)[:12],
)
return False
except Exception as exc:
logger.warning("[CodeReview] Ollama model preflight fail-open host=%s model=%s error=%s", host, model, exc)
return True
# ═══════════════════════════════════════════════════════════════════════════════
# Pipeline Class
# ═══════════════════════════════════════════════════════════════════════════════
@@ -374,6 +406,21 @@ class CodeReviewPipeline:
'max_chars': CODE_REVIEW_HERMES_MAX_CHARS,
'timeout_s': timeout_s},
) as _ctx:
_ctx.add_meta('host', host)
_ctx.add_meta('host_label', get_host_label(host))
if not _code_review_ollama_host_reachable(host):
last_error = (
"ollama host preflight failed "
f"host={host} timeout={CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_TIMEOUT}s"
)
_ctx.add_meta('preflight', 'api_version')
_ctx.set_error(last_error)
continue
if not _code_review_ollama_model_available(host, model_name):
last_error = f"ollama model preflight failed host={host} model={model_name}"
_ctx.add_meta('preflight', 'api_tags')
_ctx.set_error(last_error)
continue
ollama = OllamaService(host=host, model=model_name)
resp = ollama.generate(
prompt=prompt,
@@ -584,6 +631,13 @@ class CodeReviewPipeline:
if attempt_index == len(ollama_attempts):
_ctx.fallback_to_caller(fallback_caller)
continue
if not _code_review_ollama_model_available(host, model_name):
last_ollama_error = f"ollama model preflight failed host={host} model={model_name}"
_ctx.add_meta('preflight', 'api_tags')
_ctx.set_error(last_ollama_error)
if attempt_index == len(ollama_attempts):
_ctx.fallback_to_caller(fallback_caller)
continue
ollama = OllamaService(host=host, model=model_name)
resp = ollama.generate(
prompt=user_prompt,

View File

@@ -373,6 +373,72 @@ def test_openclaw_preflight_skips_dead_primary_before_generate(monkeypatch):
fake_elephant.generate.assert_not_called()
def test_openclaw_preflight_skips_primary_when_model_missing(monkeypatch):
"""GCP-A 可達但缺 primary model 時,直接進 GCP-B 的可用模型。"""
monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'false')
monkeypatch.setenv('CODE_REVIEW_OLLAMA_HOST_PREFLIGHT_ENABLED', 'true')
monkeypatch.setenv('GEMINI_API_KEY', 'test-key')
_stub_logger(monkeypatch)
svc_mod = _reload_pipeline()
import services.ollama_service as ollama_mod
monkeypatch.setattr(svc_mod, "_code_review_ollama_host_reachable", lambda host: True)
monkeypatch.setattr(
svc_mod,
"_code_review_ollama_model_available",
lambda host, model: not (
host == ollama_mod.OLLAMA_HOST_PRIMARY
and model == "qwen2.5-coder:7b"
),
)
calls = []
class FakeResp:
success = True
content = "SECONDARY-MODEL-OK"
error = None
input_tokens = 20
output_tokens = 8
def __init__(self, *, host, model):
self.host = host
self.model = model
class FakeOllama:
def __init__(self, host=None, model=None):
self.host = host
self.model = model
def generate(self, **kwargs):
calls.append({
"host": self.host,
"model": kwargs["model"],
"timeout": kwargs["timeout"],
})
return FakeResp(host=self.host, model=kwargs["model"])
monkeypatch.setattr(ollama_mod, "OllamaService", FakeOllama)
fake_claude = _stub_anthropic(monkeypatch, svc_mod, available=True)
fake_genai, fake_elephant = _stub_gemini_and_elephant(monkeypatch)
pipeline = _make_pipeline(svc_mod)
result = pipeline._openclaw_assess(
files={"services/foo.py": "def x(): pass"},
findings=[],
)
assert result == "SECONDARY-MODEL-OK"
assert calls == [{
"host": ollama_mod.OLLAMA_HOST_SECONDARY,
"model": "gemma3:4b",
"timeout": 25,
}]
fake_claude.generate.assert_not_called()
fake_genai.GenerativeModel.assert_not_called()
fake_elephant.generate.assert_not_called()
def test_openclaw_skips_111_and_cloud_by_default_when_gcp_pair_fails(monkeypatch):
"""GCP-A/B 都失敗時,預設不把 Code Review 重分析丟給 111 或雲端。"""
monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'false')