Files
awoooi/apps/api/src/services/model_version_probe.py
Your Name 4111ea4f9f
All checks were successful
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / tests (push) Successful in 1m13s
CD Pipeline / build-and-deploy (push) Successful in 3m36s
CD Pipeline / post-deploy-checks (push) Successful in 1m20s
fix(ai): remove 188 ollama provider
2026-05-06 14:34:48 +08:00

269 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
AI Provider 版本探測 — 為每個 Provider 提供 get_version()
每個 probe 函數獨立運作,失敗只影響該 provider不 crash 整批。
Provider:
- ollama : 34.143.170.20 GCP-A Ollama (primary) — 2026-05-03 ogt: ADR-110 GCP-A Primary
- ollama_local : 192.168.0.111 / 110 proxy Ollama (local fallback)
- gemini : Google Gemini API (版本 = model name)
- claude : Anthropic Claude (版本 = model name)
- openclaw_nemo : OpenClaw NemoTron (版本 = OPENCLAW_DEFAULT_MODEL)
# 2026-04-27 P3.2.1 by Claude
"""
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
import structlog
from src.core.config import settings
logger = structlog.get_logger(__name__)
TAIPEI_TZ = timezone(timedelta(hours=8))
@dataclass
class ProviderVersionInfo:
"""AI Provider 版本快照"""
provider: str # "ollama" / "ollama_local" / "gemini" / "claude" / "openclaw_nemo"
model: str
version: str # version string 或 tagOllama 用 modified_at其他用 model name
digest: str | None = None # SHA256 digest僅 Ollama 有)
captured_at: datetime = field(default_factory=lambda: datetime.now(TAIPEI_TZ))
# =============================================================================
# Ollama Probe
# =============================================================================
async def probe_ollama_version(url: str, model: str) -> ProviderVersionInfo:
"""探測 OllamaGCP-A/GCP-B 或本地 111GET /api/tags 取 model digest + modified_at
Args:
url: Ollama base URL例如 "http://34.143.170.20:11434"GCP-A Primary
model: model name例如 "qwen2.5:7b-instruct"
Returns:
ProviderVersionInfo — provider 依 URL 自動判斷GCP-A=ollama, 本地111=ollama_local, 否則=ollama_remote
Raises:
ValueError: model 不在清單
httpx.HTTPError: 連線失敗
"""
import httpx
# 2026-05-06 Codex: 188 不再作為 Ollama providerlocal fallback 一律標示 ollama_local。
_GCP_OLLAMA_IPS = {"34.143.170.20", "34.21.145.224"}
if any(ip in url for ip in _GCP_OLLAMA_IPS):
provider_name = "ollama"
elif "192.168.0.111" in url or "192.168.0.110:11437" in url:
provider_name = "ollama_local"
else:
provider_name = "ollama_remote"
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{url}/api/tags")
resp.raise_for_status()
models = resp.json().get("models", [])
for m in models:
if m.get("name") == model:
return ProviderVersionInfo(
provider=provider_name,
model=model,
version=m.get("modified_at", ""),
digest=m.get("digest"),
)
raise ValueError(f"Model {model!r} not found at {url}; available: {[m.get('name') for m in models]}")
# =============================================================================
# Gemini Probe
# =============================================================================
async def probe_gemini_version() -> ProviderVersionInfo:
"""探測 Gemini以設定的 model name 作為版本字串
Gemini model name 本身即版本識別碼e.g. "gemini-1.5-flash"
不需要額外 API 呼叫。若 GEMINI_API_KEY 存在則視為可用。
Returns:
ProviderVersionInfo — version = model name (e.g. "gemini-1.5-flash")
Raises:
RuntimeError: GEMINI_API_KEY 未設定
"""
api_key = settings.GEMINI_API_KEY
if not api_key:
raise RuntimeError("GEMINI_API_KEY not configured")
# Gemini 以 AI_FALLBACK_ORDER 中 "gemini" 的設定決定 model
# 實際 model name 在 ai_router 層,此處以已知預設值作為版本
# 透過 list models API 取得最新版本資訊
import httpx
async with httpx.AsyncClient(timeout=8.0) as client:
resp = await client.get(
"https://generativelanguage.googleapis.com/v1beta/models",
params={"pageSize": 50},
headers={"x-goog-api-key": api_key},
)
resp.raise_for_status()
data = resp.json()
# 找第一個 GENERATE_CONTENT 功能的 gemini 模型版本
models = data.get("models", [])
gemini_model = None
for m in models:
name = m.get("name", "")
if "gemini" in name and "generateContent" in m.get("supportedGenerationMethods", []):
gemini_model = name.replace("models/", "")
break
if not gemini_model:
gemini_model = "gemini-unknown"
return ProviderVersionInfo(
provider="gemini",
model=gemini_model,
version=gemini_model,
digest=None,
)
# =============================================================================
# Claude Probe
# =============================================================================
async def probe_claude_version() -> ProviderVersionInfo:
"""Claudemodel name 即版本識別(例如 "claude-haiku-4-5-20251001"
Anthropic 沒有 list models endpoint截至 2026-04
以設定中的 claude model name 作為版本字串。
若 CLAUDE_API_KEY 存在則視為可用。
Returns:
ProviderVersionInfo — version = model name來自設定或預設
Raises:
RuntimeError: CLAUDE_API_KEY 未設定
"""
api_key = settings.CLAUDE_API_KEY
if not api_key:
raise RuntimeError("CLAUDE_API_KEY not configured")
model_name = "claude-haiku-4-5-20251001"
return ProviderVersionInfo(
provider="claude",
model=model_name,
version=model_name,
digest=None,
)
# =============================================================================
# OpenClaw NemoTron Probe
# =============================================================================
async def probe_openclaw_nemo_version() -> ProviderVersionInfo:
"""OpenClaw NemoTron版本字串從 settings.OPENCLAW_DEFAULT_MODEL 讀取
NemoTron 運行在 OpenClaw 節點,
透過 OPENCLAW_URL /api/tags 探測,模型名稱即版本識別。
Returns:
ProviderVersionInfo — version = model tag (e.g. "deepseek-r1:14b")
Raises:
RuntimeError: OPENCLAW_DEFAULT_MODEL 未設定
httpx.HTTPError: 連線失敗
"""
model = settings.OPENCLAW_DEFAULT_MODEL
if not model:
raise RuntimeError("OPENCLAW_DEFAULT_MODEL not configured")
# OpenClaw 底層是 Ollama使用 OPENCLAW_URL 的 host:port 加上 Ollama port
# OPENCLAW_URL 是 8088OpenClaw APIOllama 通常在 11434
# OpenClaw 底層 tags 來源優先使用本地 fallback Ollama URL。
ollama_local_url = settings.OLLAMA_FALLBACK_URL
if not ollama_local_url:
# fallback從 OPENCLAW_URL host 構建 Ollama URL
from urllib.parse import urlparse
parsed = urlparse(settings.OPENCLAW_URL)
ollama_local_url = f"{parsed.scheme}://{parsed.hostname}:11434"
import httpx
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{ollama_local_url}/api/tags")
resp.raise_for_status()
models = resp.json().get("models", [])
for m in models:
if m.get("name") == model:
return ProviderVersionInfo(
provider="openclaw_nemo",
model=model,
version=m.get("modified_at", model),
digest=m.get("digest"),
)
# model 不在清單時version 用 model namedigest=None
logger.warning("openclaw_nemo_model_not_in_tags", model=model, url=ollama_local_url)
return ProviderVersionInfo(
provider="openclaw_nemo",
model=model,
version=model,
digest=None,
)
# =============================================================================
# Probe All
# =============================================================================
async def probe_all_providers() -> list[ProviderVersionInfo]:
"""並行探測所有 5 個 AI Provider失敗的 provider 以 exception 跳過
Returns:
成功探測的 ProviderVersionInfo 列表(長度 0~5
Notes:
- 使用 return_exceptions=True 確保任一 provider 失敗不影響其他
- 每個 exception 都有對應的 log warning
"""
tasks = [
probe_ollama_version(settings.OLLAMA_URL, settings.OLLAMA_HEALTH_CHECK_MODEL),
probe_ollama_version(
settings.OLLAMA_FALLBACK_URL or settings.OLLAMA_URL,
settings.OLLAMA_HEALTH_CHECK_MODEL,
),
probe_gemini_version(),
probe_claude_version(),
probe_openclaw_nemo_version(),
]
raw = await asyncio.gather(*tasks, return_exceptions=True)
results: list[ProviderVersionInfo] = []
provider_labels = ["ollama", "ollama_local", "gemini", "claude", "openclaw_nemo"]
for label, outcome in zip(provider_labels, raw, strict=True):
if isinstance(outcome, ProviderVersionInfo):
results.append(outcome)
else:
logger.warning(
"provider_probe_failed",
provider=label,
error=str(outcome),
)
return results