diff --git a/apps/api/src/services/ai_providers/ollama.py b/apps/api/src/services/ai_providers/ollama.py index 66a8442e..6fe8317e 100644 --- a/apps/api/src/services/ai_providers/ollama.py +++ b/apps/api/src/services/ai_providers/ollama.py @@ -389,3 +389,38 @@ class Ollama188Provider(OllamaProvider): return resp.status_code == 200 except Exception: return False + + +class OllamaGcpBProvider(OllamaProvider): + """ + GCP-B Secondary Ollama Provider + + 繼承 OllamaProvider,使用 OLLAMA_SECONDARY_URL(34.21.145.224:11434)。 + ADR-110 三層容災:GCP-A → GCP-B → Local(111)。 + OllamaFailoverManager 回傳 provider_name="ollama_gcp_b" 時由此 Provider 執行。 + + 2026-05-04 ogt + Claude Sonnet 4.6: ADR-110 GCP-B 容災補全 + 根因:AIProviderRegistry 缺少 "ollama_gcp_b" → not_registered → 跳 Gemini + """ + + @property + def name(self) -> str: + return "ollama_gcp_b" + + @property + def is_enabled(self) -> bool: + return bool(getattr(settings, "OLLAMA_SECONDARY_URL", "")) + + def _endpoint_url(self) -> str: + return getattr(settings, "OLLAMA_SECONDARY_URL", "") + + async def health_check(self) -> bool: + url = getattr(settings, "OLLAMA_SECONDARY_URL", "") + if not url: + return False + try: + client = await self._get_client() + resp = await client.get(f"{url}/api/tags", timeout=5.0) + return resp.status_code == 200 + except Exception: + return False diff --git a/apps/api/src/services/ai_router.py b/apps/api/src/services/ai_router.py index 52473668..8e05b911 100644 --- a/apps/api/src/services/ai_router.py +++ b/apps/api/src/services/ai_router.py @@ -77,6 +77,12 @@ class AIProviderEnum(str, Enum): # P1.1b OllamaFailoverManager 使用 provider_name="ollama_188", # 但 AIProviderEnum 沒有此值 → P1.2 整合時 lookup 失敗 OLLAMA_188 = "ollama_188" # 188 CPU-only 備援節點(P1.1b) + # 2026-05-04 ogt + Claude Sonnet 4.6: ADR-110 GCP 三層容災 + # OllamaFailoverManager 回傳 provider_name="ollama_gcp_a"/"ollama_gcp_b"/"ollama_local" + # 缺少 enum 值 → AIProviderEnum(primary_str) 拋 ValueError → fallback chain 清空 → 直跳 Gemini + OLLAMA_GCP_A = "ollama_gcp_a" # GCP-A 34.143.170.20 Primary + OLLAMA_GCP_B = "ollama_gcp_b" # GCP-B 34.21.145.224 Secondary + OLLAMA_LOCAL = "ollama_local" # 192.168.0.111 Local Fallback GEMINI = "gemini" CLAUDE = "claude" # 2026-04-02 ogt: C1 修復 — 對齊 Registry 實際名稱 @@ -92,6 +98,10 @@ PROVIDER_LATENCY_BUDGET: dict[AIProviderEnum, int] = { AIProviderEnum.OLLAMA: 60000, # 本地,允許較長處理時間 # 2026-04-25 critic-fix Part2 B2 by Claude Engineer-C2 — 188 CPU-only 推理較慢 AIProviderEnum.OLLAMA_188: 120000, # 120s budget for CPU inference + # 2026-05-04 ogt: ADR-110 GCP 三層容災 — GCP NVMe SSD 推理快,60s 足夠 + AIProviderEnum.OLLAMA_GCP_A: 60000, + AIProviderEnum.OLLAMA_GCP_B: 60000, + AIProviderEnum.OLLAMA_LOCAL: 90000, # 111 本地 HDD 稍慢 AIProviderEnum.GEMINI: 30000, # 雲端,較低延遲 AIProviderEnum.CLAUDE: 30000, # 雲端,較低延遲 # 2026-04-02 ogt: C1 修復 — 對齊 Registry 名稱 @@ -1294,13 +1304,21 @@ _executor: AIRouterExecutor | None = None def _init_registry() -> AIProviderRegistry: """初始化 Provider Registry (首次呼叫時自動註冊所有 Provider)""" - from src.services.ai_providers.ollama import OllamaProvider, Ollama188Provider # 2026-04-26 Wave5 B1-fix by Claude Engineer-A4 + from src.services.ai_providers.ollama import ( + OllamaProvider, + Ollama188Provider, + OllamaGcpBProvider, # 2026-05-04 ADR-110 GCP-B + ) from src.services.ai_providers.gemini import GeminiProvider from src.services.ai_providers.claude import ClaudeProvider from src.services.ai_providers.openclaw_nemo import OpenClawNemoProvider registry = AIProviderRegistry() - registry.register(OllamaProvider()) + + # GCP-A Primary(name="ollama",OLLAMA_URL) + ollama_gcp_a = OllamaProvider() + registry.register(ollama_gcp_a) + registry.register(GeminiProvider()) registry.register(ClaudeProvider()) registry.register(OpenClawNemoProvider()) @@ -1310,9 +1328,19 @@ def _init_registry() -> AIProviderRegistry: registry.register(NemotronProvider()) # 2026-04-26 Wave5 B1-fix by Claude Engineer-A4 — 補登 OLLAMA_188 備援 provider - # 修復:原本 failover_manager 決策返回 "ollama_188",但 executor 查不到 → not_registered - # → 188 從未被打到。必須明確 register 才能讓 executor.execute() 路由到 188。 - registry.register(Ollama188Provider()) + ollama_local = Ollama188Provider() + registry.register(ollama_local) + + # 2026-05-04 ogt + Claude Sonnet 4.6: ADR-110 GCP 三層容災修復 + # 根因:OllamaFailoverManager 回傳 "ollama_gcp_a"/"ollama_gcp_b"/"ollama_local" + # 但 registry 無這些名稱 → not_registered → 整條 Ollama 鏈跳過 → 直接跳 Gemini + # 修復: + # "ollama_gcp_a" alias → 同 OllamaProvider(OLLAMA_URL = GCP-A) + # "ollama_gcp_b" → 新 OllamaGcpBProvider(OLLAMA_SECONDARY_URL = GCP-B) + # "ollama_local" alias → 同 Ollama188Provider(OLLAMA_FALLBACK_URL = 111) + registry._providers["ollama_gcp_a"] = ollama_gcp_a + registry.register(OllamaGcpBProvider()) + registry._providers["ollama_local"] = ollama_local return registry