fix(code-review): keep Gemini fallback opt-in
Some checks failed
CD Pipeline / tests (push) Successful in 2m2s
Code Review / ai-code-review (push) Successful in 27s
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-05-05 14:38:44 +08:00
parent 3b73cc7f94
commit fc1a6196df
5 changed files with 355 additions and 9 deletions

View File

@@ -20,6 +20,7 @@ import structlog
from src.core.config import get_settings
from src.services.model_registry import get_model
from src.services.ollama_endpoint_resolver import resolve_ollama_endpoint
logger = structlog.get_logger(__name__)
settings = get_settings()
@@ -75,18 +76,24 @@ class LocalCodeReviewService:
redis = None
diff_size = len(diff.encode())
use_gemini = diff_size > _MAX_DIFF_BYTES
allow_cloud_fallback = settings.LOCAL_CODE_REVIEW_ALLOW_GEMINI_FALLBACK
if use_gemini:
if diff_size > _MAX_DIFF_BYTES and allow_cloud_fallback:
result = await self._review_with_gemini(pr_id, repo, title, diff)
else:
if diff_size > _MAX_DIFF_BYTES:
logger.info(
"pr_review_large_diff_using_ollama_truncated",
pr_id=pr_id,
diff_size_bytes=diff_size,
cloud_fallback_enabled=allow_cloud_fallback,
)
result = await self._review_with_ollama(pr_id, repo, title, diff)
if result is None:
# Ollama 失敗 → fallback Gemini
if result is None and allow_cloud_fallback:
result = await self._review_with_gemini(pr_id, repo, title, diff)
if result is None:
return None
result = self._cloud_fallback_disabled_result(pr_id, repo, title)
result["diff_size_bytes"] = diff_size
@@ -116,7 +123,7 @@ class LocalCodeReviewService:
try:
http = await self._get_http()
resp = await http.post(
f"{settings.OLLAMA_URL}/api/generate",
f"{resolve_ollama_endpoint('code_review')}/api/generate",
json={
"model": _MODEL_OLLAMA,
"prompt": prompt,
@@ -138,6 +145,14 @@ class LocalCodeReviewService:
async def _review_with_gemini(
self, pr_id: str, repo: str, title: str, diff: str
) -> dict[str, Any] | None:
if not settings.LOCAL_CODE_REVIEW_ALLOW_GEMINI_FALLBACK:
logger.warning(
"pr_review_gemini_fallback_disabled",
pr_id=pr_id,
repo=repo,
)
return None
try:
from src.services.openclaw import get_openclaw
openclaw = get_openclaw()
@@ -156,12 +171,37 @@ class LocalCodeReviewService:
logger.error("pr_review_gemini_failed", pr_id=pr_id, error=str(e))
return None
def _cloud_fallback_disabled_result(
self,
pr_id: str,
repo: str,
title: str,
) -> dict[str, Any]:
logger.warning(
"pr_review_cloud_fallback_skipped",
pr_id=pr_id,
repo=repo,
title=title,
reason="LOCAL_CODE_REVIEW_ALLOW_GEMINI_FALLBACK=false",
)
return {
"review_text": (
"⚠️ Code ReviewGCP-B/Ollama 審查未完成,"
"已依成本策略跳過 Gemini fallback。"
),
"issues_count": 1,
"model": _MODEL_OLLAMA,
"provider": "ollama_unavailable",
"cloud_fallback_skipped": True,
}
async def _save_to_db(
self, pr_id: str, repo: str, title: str, diff_size: int, result: dict
) -> None:
try:
from src.db.base import get_db_context
from sqlalchemy import text
from src.db.base import get_db_context
async with get_db_context() as db:
await db.execute(
text("""
@@ -206,7 +246,7 @@ class LocalCodeReviewService:
try:
http = await self._get_http()
resp = await http.post(
f"{settings.OLLAMA_URL}/api/generate",
f"{resolve_ollama_endpoint('code_review')}/api/generate",
json={
"model": _MODEL_OLLAMA,
"prompt": prompt,

View File

@@ -0,0 +1,101 @@
"""
Ollama endpoint resolver for non-critical workload placement.
ADR-110 gives AWOOOI three Ollama endpoints. This resolver is intentionally
small: it chooses the preferred endpoint by workload class, while health-aware
failover remains owned by ollama_failover_manager.py.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal, Protocol
from src.core.config import settings
OllamaWorkloadType = Literal[
"interactive",
"healthcheck",
"batch",
"embedding",
"rag",
"code_review",
"shadow",
"canary",
"local_required",
"privacy_sensitive",
"dr",
]
_GCP_B_PREFERRED_WORKLOADS = {
"batch",
"embedding",
"rag",
"code_review",
"shadow",
"canary",
}
_LOCAL_PREFERRED_WORKLOADS = {
"local_required",
"privacy_sensitive",
"dr",
}
class _OllamaSettings(Protocol):
OLLAMA_URL: str
OLLAMA_SECONDARY_URL: str
OLLAMA_FALLBACK_URL: str
@dataclass(frozen=True)
class OllamaEndpointSelection:
url: str
provider_name: str
workload_type: OllamaWorkloadType
reason: str
def resolve_ollama_selection(
workload_type: OllamaWorkloadType = "interactive",
*,
config: _OllamaSettings | None = None,
) -> OllamaEndpointSelection:
"""Return the preferred Ollama endpoint for a workload class."""
cfg = config or settings
primary = cfg.OLLAMA_URL
secondary = cfg.OLLAMA_SECONDARY_URL
fallback = cfg.OLLAMA_FALLBACK_URL
if workload_type in _GCP_B_PREFERRED_WORKLOADS and secondary:
return OllamaEndpointSelection(
url=secondary,
provider_name="ollama_gcp_b",
workload_type=workload_type,
reason="gcp_b_batch_lane",
)
if workload_type in _LOCAL_PREFERRED_WORKLOADS and fallback:
return OllamaEndpointSelection(
url=fallback,
provider_name="ollama_local",
workload_type=workload_type,
reason="local_privacy_or_dr_lane",
)
return OllamaEndpointSelection(
url=primary,
provider_name="ollama_gcp_a",
workload_type=workload_type,
reason="primary_interactive_lane",
)
def resolve_ollama_endpoint(
workload_type: OllamaWorkloadType = "interactive",
*,
config: _OllamaSettings | None = None,
) -> str:
"""Return only the preferred Ollama base URL."""
return resolve_ollama_selection(workload_type, config=config).url