fix(code-review): keep Gemini fallback opt-in
This commit is contained in:
@@ -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 Review:GCP-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,
|
||||
|
||||
101
apps/api/src/services/ollama_endpoint_resolver.py
Normal file
101
apps/api/src/services/ollama_endpoint_resolver.py
Normal 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
|
||||
Reference in New Issue
Block a user