fix: skip missing code review ollama models
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s
This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user