fix(ai): keep GCP Ollama lane on safe models
All checks were successful
CD Pipeline / tests (push) Successful in 54s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Successful in 3m25s
CD Pipeline / post-deploy-checks (push) Successful in 1m50s

This commit is contained in:
Your Name
2026-05-05 23:37:08 +08:00
parent 1ba36697ca
commit e208798531
6 changed files with 201 additions and 12 deletions

View File

@@ -29,6 +29,54 @@ from src.services.model_registry import get_model_registry
logger = structlog.get_logger(__name__)
settings = get_settings()
_GCP_SAFE_MODELS = {
"gemma3:4b",
}
def _normalized_url(value: str | None) -> str:
return (value or "").rstrip("/")
def _is_gcp_alert_lane(endpoint_url: str) -> bool:
"""Return true for the CPU-only GCP-A/B synchronous alert lane."""
endpoint = _normalized_url(endpoint_url)
return endpoint in {
_normalized_url(getattr(settings, "OLLAMA_URL", "")),
_normalized_url(getattr(settings, "OLLAMA_SECONDARY_URL", "")),
}
def _resolve_model_for_endpoint(
*,
requested_model: str,
endpoint_url: str,
context: dict | None,
) -> str:
"""
Keep GCP-A/B on the fast alert model unless explicitly allowed.
The GCP hosts currently expose CPU-only Ollama. Loading 7B/14B/32B models on
that lane blocks synchronous alerts long enough to fall through to Gemini.
Heavy/deep workloads must use 111 or the future AwoooP Inference Gateway.
"""
model_name = requested_model.strip()
context = context or {}
allow_gcp_heavy = bool(context.get("allow_gcp_heavy_model"))
if _is_gcp_alert_lane(endpoint_url) and not allow_gcp_heavy and model_name not in _GCP_SAFE_MODELS:
alert_model = str(getattr(settings, "ALERT_OLLAMA_MODEL", "gemma3:4b")).strip() or "gemma3:4b"
logger.warning(
"ollama_gcp_heavy_model_coerced",
endpoint=endpoint_url,
requested_model=model_name,
safe_model=alert_model,
task_type=context.get("task_type"),
)
return alert_model
return model_name
class OllamaProvider:
"""
@@ -77,7 +125,13 @@ class OllamaProvider:
client = await self._get_client()
registry = get_model_registry()
model_name = str((context or {}).get("ollama_model") or registry.get_model("ollama", "rca")).strip()
endpoint_url = self._endpoint_url()
requested_model = str((context or {}).get("ollama_model") or registry.get_model("ollama", "rca")).strip()
model_name = _resolve_model_for_endpoint(
requested_model=requested_model,
endpoint_url=endpoint_url,
context=context,
)
options = registry.get_provider_options("ollama")
# P0 2026-04-04 Claude Code: per-task timeoutOption C 分情境)
@@ -89,7 +143,6 @@ class OllamaProvider:
else:
read_timeout = float(settings.OPENCLAW_TIMEOUT)
endpoint_url = self._endpoint_url()
response = await client.post(
f"{endpoint_url}/api/generate",
json={

View File

@@ -40,6 +40,7 @@ from src.models.nvidia import (
from src.services.langfuse_client import ( # 2026-03-29 ogt: P1-1 Langfuse 整合
LangfuseTraceContext,
)
from src.services.ollama_endpoint_resolver import resolve_ollama_endpoint
logger = structlog.get_logger(__name__)
settings = get_settings()
@@ -822,8 +823,8 @@ class NvidiaProvider:
NVIDIA_REQUESTS_TOTAL.labels(status="error", tool_name="chat").inc()
import traceback
logger.warning(
"nvidia_chat_failed",
error=str(e),
"nvidia_chat_failed",
error=str(e),
error_type=type(e).__name__,
stacktrace=traceback.format_exc()
)
@@ -845,7 +846,7 @@ class OllamaToolProvider:
取代 NVIDIA 雲端 NIM。延遲從 44s 降至 ~5s。
模型: llama3.1:8b (tool calling 最穩定的 8B 模型)
Endpoint: OLLAMA_URL/v1/chat/completions (OpenAI 相容格式)
Endpoint: local tool lane /v1/chat/completions (OpenAI 相容格式)
"""
def __init__(self) -> None:
@@ -872,10 +873,14 @@ class OllamaToolProvider:
) -> list[ToolCallValidationResult]:
return [tc for tc in tool_calls if self.is_high_risk_tool(tc.tool_name)]
def _base_url(self) -> str:
"""Tool-calling/Hermes models stay off the GCP alert lane."""
return resolve_ollama_endpoint("hermes").rstrip("/")
async def health_check(self) -> bool:
try:
client = await self._get_client()
base_url = settings.OLLAMA_URL.rstrip("/")
base_url = self._base_url()
resp = await client.get(f"{base_url}/api/tags", timeout=5.0)
return resp.status_code == 200
except Exception:
@@ -892,7 +897,7 @@ class OllamaToolProvider:
"""Ollama /v1/chat/completions tool calling"""
start_time = time.perf_counter()
model = model or settings.OLLAMA_TOOL_MODEL
base_url = settings.OLLAMA_URL.rstrip("/")
base_url = self._base_url()
url = f"{base_url}/v1/chat/completions"
# 轉換 tools 為 dict 格式(同 NvidiaProvider
@@ -988,7 +993,7 @@ class OllamaToolProvider:
async def chat(self, prompt: str, model: str = "", temperature: float = 0.7, max_tokens: int = 512) -> str:
"""簡單 chat非 tool calling 路徑,保持 INvidiaProvider 相容)"""
model = model or settings.OLLAMA_TOOL_MODEL
base_url = settings.OLLAMA_URL.rstrip("/")
base_url = self._base_url()
try:
client = await self._get_client()
resp = await client.post(
@@ -1010,7 +1015,7 @@ _provider: NvidiaProvider | None = None
_ollama_tool_provider: OllamaToolProvider | None = None
def get_nvidia_provider() -> "NvidiaProvider | OllamaToolProvider":
def get_nvidia_provider() -> NvidiaProvider | OllamaToolProvider:
"""
取得 Tool Calling Provider 單例。
USE_OLLAMA_TOOL_CALLING=True (預設) → OllamaToolProvider (本機,~5s)

View File

@@ -534,6 +534,8 @@ class OpenClawService:
# 從 ModelRegistry 取得模型配置
registry = get_model_registry()
model_name = registry.get_model("ollama", "rca")
if ollama_only:
model_name = getattr(settings, "ALERT_OLLAMA_MODEL", "gemma3:4b")
options = registry.get_provider_options("ollama")
timeout_seconds = max(
float(settings.OPENCLAW_TIMEOUT),