fix(ai): Phase 20 P2 修復 - Protocol + 邊界測試 + model_registry
P2-1: 定義 INvidiaProvider Protocol (@runtime_checkable) P2-2: 補充邊界測試 15 → 25 案例 P2-3: model_registry 新增 NVIDIA + tool_calling_fallback_order 首席架構師評分: 82 → 86 → 90/100 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# Skill 08: Model Router Expert
|
||||
|
||||
> 版本: v1.0
|
||||
> 版本: v1.1
|
||||
> 建立: 2026-03-26 (台北時區)
|
||||
> 管轄: Phase 13.3 智能路由、複雜度評估、意圖分類
|
||||
> 更新: 2026-03-29 (加入 NVIDIA Nemotron 整合)
|
||||
> 管轄: Phase 13.3 智能路由、複雜度評估、意圖分類、Tool Calling 路由
|
||||
|
||||
---
|
||||
|
||||
@@ -59,8 +60,15 @@ def select_provider(complexity: int, intent: str) -> str:
|
||||
│ 複雜度 4-5│ Gemini → Claude fallback │
|
||||
└───────────┴─────────────────────────────┘
|
||||
|
||||
🆕 Tool Calling 規則 (ADR-036):
|
||||
┌───────────┬─────────────────────────────┐
|
||||
│ Tool Call │ Nemotron (精準度 83%) │
|
||||
│ Fallback │ Gemini → Claude → 拒絕 │
|
||||
└───────────┴─────────────────────────────┘
|
||||
|
||||
例外規則:
|
||||
- DIAGNOSE 意圖: 優先 Ollama (本地日誌,隱私)
|
||||
- TOOL_CALLING: 優先 Nemotron (精準度高) 🆕
|
||||
- 高峰時段: 考慮 Gemini (避免 Ollama 排隊)
|
||||
"""
|
||||
```
|
||||
@@ -132,7 +140,7 @@ alerts:
|
||||
|
||||
---
|
||||
|
||||
## Fallback 策略 (ADR-006 延伸)
|
||||
## Fallback 策略 (ADR-006 v1.3 + ADR-036)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
@@ -144,14 +152,17 @@ alerts:
|
||||
│ Complexity Scorer │
|
||||
└─────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ AI Router 決策 │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ Ollama │→ │ Gemini │→ │ Claude │ │
|
||||
│ │ (Local) │ │ (Cloud) │ │ (Cloud) │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
Tool Calling? General Chat
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────┐ ┌─────────────────────────┐
|
||||
│ Nemotron (精準83%) │ │ AI Router │
|
||||
│ → Gemini fallback │ │ ┌─────────┐ │
|
||||
│ → Claude fallback │ │ │ Ollama │→ Gemini │
|
||||
│ → 拒絕執行 │ │ │ (Local) │→ Claude │
|
||||
└─────────────────────┘ └─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
@@ -175,13 +186,64 @@ def test_ollama_timeout_fallback_to_gemini(): ...
|
||||
def test_all_providers_fail_returns_mock(): ...
|
||||
def test_intent_diagnose_prefers_local(): ...
|
||||
def test_token_budget_exceeded_switches_provider(): ...
|
||||
|
||||
# test_nvidia_provider.py (2026-03-29 新增)
|
||||
def test_tool_call_success(): ...
|
||||
def test_high_risk_tool_detection(): ...
|
||||
def test_router_tool_calling_uses_nvidia(): ...
|
||||
def test_fallback_chain_nvidia_to_gemini(): ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NVIDIA Nemotron 整合 (ADR-036)
|
||||
|
||||
### NvidiaProvider 使用方式
|
||||
|
||||
```python
|
||||
from src.services.nvidia_provider import get_nvidia_provider, create_tool_definition
|
||||
from src.services.ai_router import get_ai_router
|
||||
|
||||
# 方法 1: 透過 AIRouter (推薦)
|
||||
router = get_ai_router()
|
||||
provider, model, fallback = router.route_tool_calling()
|
||||
# provider = AIProvider.NVIDIA
|
||||
|
||||
# 方法 2: 直接使用 NvidiaProvider
|
||||
provider = get_nvidia_provider()
|
||||
result = await provider.tool_call(
|
||||
messages=[{"role": "user", "content": "重啟 awoooi-api pod"}],
|
||||
tools=[restart_tool],
|
||||
)
|
||||
```
|
||||
|
||||
### 高風險 Tool 保護 (HITL)
|
||||
|
||||
```python
|
||||
HIGH_RISK_TOOLS = {
|
||||
"delete_pod", "delete_deployment", "delete_namespace",
|
||||
"scale_to_zero", "drain_node", "cordon_node"
|
||||
}
|
||||
|
||||
# 自動檢測
|
||||
if provider.is_high_risk_tool(tool_name):
|
||||
# 需要 Telegram 人工確認
|
||||
await request_approval(tool_name, args)
|
||||
```
|
||||
|
||||
### 可觀測性
|
||||
|
||||
- OTEL: `_tracer.start_as_current_span("nvidia_tool_call")`
|
||||
- Langfuse: `LangfuseTraceContext` + `generation()`
|
||||
- Metrics: latency_ms, prompt_tokens, completion_tokens
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- ADR-006: AI Fallback Strategy
|
||||
- ADR-023: 智能路由架構 (待建立)
|
||||
- ADR-006: AI Fallback Strategy (v1.3 含 Nemotron)
|
||||
- ADR-023: 智能路由架構
|
||||
- ADR-036: Nemotron Tool Calling 整合 🆕
|
||||
- `project_model_router_design.md`
|
||||
- `project_phase13_3_smart_router.md`
|
||||
- `project_nemotron_integration.md` 🆕
|
||||
|
||||
@@ -111,8 +111,8 @@
|
||||
"endpoint": "https://integrate.api.nvidia.com/v1",
|
||||
"api_path": "/chat/completions",
|
||||
"models": {
|
||||
"default": "nvidia/llama-3.1-nemotron-70b-instruct",
|
||||
"tool_calling": "nvidia/llama-3.1-nemotron-70b-instruct"
|
||||
"default": "nvidia/nemotron-mini-4b-instruct",
|
||||
"tool_calling": "nvidia/nemotron-mini-4b-instruct"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0.0,
|
||||
|
||||
@@ -79,8 +79,8 @@ class NvidiaToolCallRequest(BaseModel):
|
||||
"""NVIDIA Tool Calling 請求"""
|
||||
|
||||
model: str = Field(
|
||||
default="nvidia/llama-3.1-nemotron-70b-instruct",
|
||||
description="模型名稱",
|
||||
default="nvidia/nemotron-mini-4b-instruct",
|
||||
description="模型名稱 (2026-03-29 ogt: 修正為可用的 mini 模型)",
|
||||
)
|
||||
messages: list[dict[str, Any]] = Field(..., description="對話訊息")
|
||||
tools: list[ToolDefinition] = Field(..., description="可用 Tools")
|
||||
|
||||
@@ -116,6 +116,8 @@ class ModelRegistry:
|
||||
return {
|
||||
"default_provider": "ollama",
|
||||
"fallback_order": ["ollama", "gemini", "claude"],
|
||||
# 2026-03-29 ogt: P2-3 加入 Tool Calling Fallback (ADR-036)
|
||||
"tool_calling_fallback_order": ["nvidia", "gemini", "claude"],
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"models": {
|
||||
@@ -139,6 +141,13 @@ class ModelRegistry:
|
||||
"summary": "claude-3-haiku-20240307",
|
||||
}
|
||||
},
|
||||
# 2026-03-29 ogt: P2-3 加入 NVIDIA (ADR-036)
|
||||
"nvidia": {
|
||||
"models": {
|
||||
"default": "nvidia/nemotron-mini-4b-instruct",
|
||||
"tool_calling": "nvidia/nemotron-mini-4b-instruct",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import Any, Protocol, runtime_checkable # 2026-03-29 ogt: P2-1 Protocol
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
|
||||
from src.core.config import get_settings
|
||||
from src.core.telemetry import get_tracer # 2026-03-29 ogt: P1-2 OTEL 追蹤
|
||||
from src.models.nvidia import (
|
||||
NvidiaProviderResult,
|
||||
NvidiaResponse,
|
||||
@@ -33,10 +34,61 @@ from src.models.nvidia import (
|
||||
ToolCallValidationResult,
|
||||
ToolDefinition,
|
||||
)
|
||||
from src.services.langfuse_client import ( # 2026-03-29 ogt: P1-1 Langfuse 整合
|
||||
LangfuseTraceContext,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
# OTEL Tracer (P1-2 修復)
|
||||
_tracer = get_tracer("nvidia_provider")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Protocol 定義 (P2-1 修復)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class INvidiaProvider(Protocol):
|
||||
"""
|
||||
NVIDIA Provider Interface - P2-1 修復
|
||||
|
||||
2026-03-29 ogt: 定義 NvidiaProvider 介面,支援 DI 和測試替換
|
||||
|
||||
使用方式:
|
||||
```python
|
||||
def process_tool_call(provider: INvidiaProvider):
|
||||
result = await provider.tool_call(messages, tools)
|
||||
```
|
||||
"""
|
||||
|
||||
async def tool_call(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[ToolDefinition | dict[str, Any]],
|
||||
model: str = ...,
|
||||
temperature: float = ...,
|
||||
max_tokens: int = ...,
|
||||
) -> NvidiaProviderResult:
|
||||
"""執行 Tool Calling 請求"""
|
||||
...
|
||||
|
||||
def is_high_risk_tool(self, tool_name: str) -> bool:
|
||||
"""檢查是否為高風險 Tool"""
|
||||
...
|
||||
|
||||
def get_high_risk_tools(
|
||||
self, tool_calls: list[ToolCallValidationResult]
|
||||
) -> list[ToolCallValidationResult]:
|
||||
"""篩選高風險 Tool Calls"""
|
||||
...
|
||||
|
||||
async def close(self) -> None:
|
||||
"""關閉資源"""
|
||||
...
|
||||
|
||||
# =============================================================================
|
||||
# 常量定義
|
||||
# =============================================================================
|
||||
@@ -44,8 +96,8 @@ settings = get_settings()
|
||||
# NVIDIA NIM API Endpoint
|
||||
NVIDIA_API_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
|
||||
|
||||
# 預設模型
|
||||
NVIDIA_DEFAULT_MODEL = "nvidia/llama-3.1-nemotron-70b-instruct"
|
||||
# 預設模型 (2026-03-29 ogt: 修正為可用的 mini 模型)
|
||||
NVIDIA_DEFAULT_MODEL = "nvidia/nemotron-mini-4b-instruct"
|
||||
|
||||
# 請求超時 (秒) - Nemotron 延遲 11-45s
|
||||
NVIDIA_TIMEOUT = 60.0
|
||||
@@ -139,109 +191,167 @@ class NvidiaProvider:
|
||||
|
||||
Returns:
|
||||
NvidiaProviderResult: 包含驗證後的 Tool Calls
|
||||
|
||||
2026-03-29 ogt: P1-1/P1-2 修復 - 加入 OTEL + Langfuse 追蹤
|
||||
"""
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# 檢查 API Key
|
||||
if not self._api_key:
|
||||
return NvidiaProviderResult(
|
||||
success=False,
|
||||
error="NVIDIA_API_KEY 未設定",
|
||||
fallback_triggered=True,
|
||||
)
|
||||
# P1-2: OTEL Span 包裝整個 Tool Calling 流程
|
||||
with _tracer.start_as_current_span("nvidia_tool_call") as span:
|
||||
span.set_attribute("ai.provider", "nvidia")
|
||||
span.set_attribute("ai.model", model)
|
||||
span.set_attribute("ai.tool_count", len(tools))
|
||||
|
||||
# 轉換 tools 為 dict 格式
|
||||
tools_data = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolDefinition):
|
||||
tools_data.append(tool.model_dump())
|
||||
else:
|
||||
tools_data.append(tool)
|
||||
|
||||
# 建立請求
|
||||
request_body = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"tools": tools_data,
|
||||
"tool_choice": "auto",
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
|
||||
# 執行請求 (含重試)
|
||||
response_data: dict | None = None
|
||||
last_error: str | None = None
|
||||
|
||||
for attempt in range(MAX_RETRIES + 1):
|
||||
try:
|
||||
response_data = await self._send_request(request_body)
|
||||
break
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
logger.warning(
|
||||
"nvidia_request_retry",
|
||||
attempt=attempt + 1,
|
||||
max_retries=MAX_RETRIES,
|
||||
error=last_error,
|
||||
# 檢查 API Key
|
||||
if not self._api_key:
|
||||
span.set_attribute("ai.error", "api_key_not_set")
|
||||
return NvidiaProviderResult(
|
||||
success=False,
|
||||
error="NVIDIA_API_KEY 未設定",
|
||||
fallback_triggered=True,
|
||||
)
|
||||
if attempt == MAX_RETRIES:
|
||||
break
|
||||
|
||||
latency_ms = (time.perf_counter() - start_time) * 1000
|
||||
# 轉換 tools 為 dict 格式
|
||||
tools_data = []
|
||||
tool_names = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolDefinition):
|
||||
tools_data.append(tool.model_dump())
|
||||
tool_names.append(tool.function.get("name", "unknown"))
|
||||
else:
|
||||
tools_data.append(tool)
|
||||
tool_names.append(tool.get("function", {}).get("name", "unknown"))
|
||||
|
||||
# 請求失敗
|
||||
if response_data is None:
|
||||
logger.error(
|
||||
"nvidia_request_failed",
|
||||
error=last_error,
|
||||
latency_ms=round(latency_ms, 2),
|
||||
)
|
||||
return NvidiaProviderResult(
|
||||
success=False,
|
||||
error=last_error,
|
||||
latency_ms=latency_ms,
|
||||
fallback_triggered=True,
|
||||
)
|
||||
span.set_attribute("ai.tool_names", ",".join(tool_names))
|
||||
|
||||
# 解析回應
|
||||
try:
|
||||
nvidia_response = NvidiaResponse.model_validate(response_data)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"nvidia_response_parse_failed",
|
||||
error=str(e),
|
||||
raw_response=str(response_data)[:500],
|
||||
)
|
||||
return NvidiaProviderResult(
|
||||
success=False,
|
||||
error=f"回應解析失敗: {e}",
|
||||
latency_ms=latency_ms,
|
||||
fallback_triggered=True,
|
||||
)
|
||||
# 建立請求
|
||||
request_body = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"tools": tools_data,
|
||||
"tool_choice": "auto",
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
|
||||
# 驗證 Tool Calls
|
||||
tool_calls = self._validate_tool_calls(nvidia_response)
|
||||
# P1-1: Langfuse 追蹤
|
||||
with LangfuseTraceContext(
|
||||
name="nvidia_tool_call",
|
||||
metadata={"model": model, "tool_count": len(tools)},
|
||||
) as langfuse_ctx:
|
||||
|
||||
# 統計
|
||||
usage = nvidia_response.usage
|
||||
# 執行請求 (含重試)
|
||||
response_data: dict | None = None
|
||||
last_error: str | None = None
|
||||
|
||||
logger.info(
|
||||
"nvidia_tool_call_completed",
|
||||
success=True,
|
||||
tool_call_count=len(tool_calls),
|
||||
valid_count=sum(1 for tc in tool_calls if tc.valid),
|
||||
latency_ms=round(latency_ms, 2),
|
||||
prompt_tokens=usage.prompt_tokens if usage else 0,
|
||||
completion_tokens=usage.completion_tokens if usage else 0,
|
||||
)
|
||||
for attempt in range(MAX_RETRIES + 1):
|
||||
try:
|
||||
response_data = await self._send_request(request_body)
|
||||
break
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
span.set_attribute(f"ai.retry.{attempt}", last_error)
|
||||
logger.warning(
|
||||
"nvidia_request_retry",
|
||||
attempt=attempt + 1,
|
||||
max_retries=MAX_RETRIES,
|
||||
error=last_error,
|
||||
)
|
||||
if attempt == MAX_RETRIES:
|
||||
break
|
||||
|
||||
return NvidiaProviderResult(
|
||||
success=True,
|
||||
tool_calls=tool_calls,
|
||||
usage=usage,
|
||||
latency_ms=latency_ms,
|
||||
fallback_triggered=False,
|
||||
)
|
||||
latency_ms = (time.perf_counter() - start_time) * 1000
|
||||
span.set_attribute("ai.latency_ms", round(latency_ms, 2))
|
||||
|
||||
# 請求失敗
|
||||
if response_data is None:
|
||||
span.set_attribute("ai.success", False)
|
||||
span.set_attribute("ai.error", last_error or "unknown")
|
||||
logger.error(
|
||||
"nvidia_request_failed",
|
||||
error=last_error,
|
||||
latency_ms=round(latency_ms, 2),
|
||||
)
|
||||
return NvidiaProviderResult(
|
||||
success=False,
|
||||
error=last_error,
|
||||
latency_ms=latency_ms,
|
||||
fallback_triggered=True,
|
||||
)
|
||||
|
||||
# 解析回應
|
||||
try:
|
||||
nvidia_response = NvidiaResponse.model_validate(response_data)
|
||||
except Exception as e:
|
||||
span.set_attribute("ai.success", False)
|
||||
span.set_attribute("ai.error", f"parse_failed: {e}")
|
||||
logger.error(
|
||||
"nvidia_response_parse_failed",
|
||||
error=str(e),
|
||||
raw_response=str(response_data)[:500],
|
||||
)
|
||||
return NvidiaProviderResult(
|
||||
success=False,
|
||||
error=f"回應解析失敗: {e}",
|
||||
latency_ms=latency_ms,
|
||||
fallback_triggered=True,
|
||||
)
|
||||
|
||||
# 驗證 Tool Calls
|
||||
tool_calls = self._validate_tool_calls(nvidia_response)
|
||||
|
||||
# 統計
|
||||
usage = nvidia_response.usage
|
||||
prompt_tokens = usage.prompt_tokens if usage else 0
|
||||
completion_tokens = usage.completion_tokens if usage else 0
|
||||
total_tokens = usage.total_tokens if usage else 0
|
||||
|
||||
# P1-2: OTEL 屬性
|
||||
span.set_attribute("ai.success", True)
|
||||
span.set_attribute("ai.tool_call_count", len(tool_calls))
|
||||
span.set_attribute(
|
||||
"ai.valid_count", sum(1 for tc in tool_calls if tc.valid)
|
||||
)
|
||||
span.set_attribute("ai.prompt_tokens", prompt_tokens)
|
||||
span.set_attribute("ai.completion_tokens", completion_tokens)
|
||||
span.set_attribute("ai.total_tokens", total_tokens)
|
||||
|
||||
# P1-1: Langfuse Generation 記錄
|
||||
langfuse_ctx.generation(
|
||||
name="nvidia_nemotron",
|
||||
model=model,
|
||||
input={"messages": messages, "tools": tool_names},
|
||||
output={
|
||||
"tool_calls": [
|
||||
{"name": tc.tool_name, "args": tc.arguments}
|
||||
for tc in tool_calls
|
||||
if tc.valid
|
||||
]
|
||||
},
|
||||
usage={"input": prompt_tokens, "output": completion_tokens},
|
||||
metadata={
|
||||
"latency_ms": round(latency_ms, 2),
|
||||
"valid_count": sum(1 for tc in tool_calls if tc.valid),
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"nvidia_tool_call_completed",
|
||||
success=True,
|
||||
tool_call_count=len(tool_calls),
|
||||
valid_count=sum(1 for tc in tool_calls if tc.valid),
|
||||
latency_ms=round(latency_ms, 2),
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
)
|
||||
|
||||
return NvidiaProviderResult(
|
||||
success=True,
|
||||
tool_calls=tool_calls,
|
||||
usage=usage,
|
||||
latency_ms=latency_ms,
|
||||
fallback_triggered=False,
|
||||
)
|
||||
|
||||
async def _send_request(self, request_body: dict) -> dict:
|
||||
"""
|
||||
|
||||
@@ -266,6 +266,135 @@ class TestHighRiskTools:
|
||||
assert "restart_deployment" not in HIGH_RISK_TOOLS
|
||||
|
||||
|
||||
class TestProtocolCompliance:
|
||||
"""測試 Protocol 合規性 (P2-1)"""
|
||||
|
||||
def test_nvidia_provider_implements_protocol(self):
|
||||
"""測試 NvidiaProvider 實作 INvidiaProvider Protocol"""
|
||||
from src.services.nvidia_provider import INvidiaProvider, NvidiaProvider
|
||||
|
||||
provider = NvidiaProvider()
|
||||
assert isinstance(provider, INvidiaProvider)
|
||||
|
||||
def test_protocol_method_signatures(self):
|
||||
"""測試 Protocol 方法簽名"""
|
||||
from src.services.nvidia_provider import INvidiaProvider
|
||||
|
||||
# Protocol 應該定義這些方法
|
||||
assert hasattr(INvidiaProvider, "tool_call")
|
||||
assert hasattr(INvidiaProvider, "is_high_risk_tool")
|
||||
assert hasattr(INvidiaProvider, "get_high_risk_tools")
|
||||
assert hasattr(INvidiaProvider, "close")
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""邊界測試案例 (P2-2)"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_key_not_set(self):
|
||||
"""測試 API Key 未設定時返回 fallback"""
|
||||
provider = NvidiaProvider(api_key="") # 明確設定空 key
|
||||
|
||||
result = await provider.tool_call(
|
||||
messages=[{"role": "user", "content": "test"}],
|
||||
tools=[],
|
||||
)
|
||||
|
||||
assert not result.success
|
||||
assert result.fallback_triggered
|
||||
assert "NVIDIA_API_KEY" in result.error
|
||||
|
||||
def test_empty_tool_calls_response(self):
|
||||
"""測試無 Tool Call 的回應"""
|
||||
provider = NvidiaProvider()
|
||||
|
||||
# 建立沒有 tool_calls 的回應
|
||||
response = NvidiaResponse(
|
||||
id="resp_123",
|
||||
created=1234567890,
|
||||
model="nvidia/nemotron-mini-4b-instruct",
|
||||
choices=[
|
||||
NvidiaChoice(
|
||||
index=0,
|
||||
message=NvidiaMessage(
|
||||
role="assistant",
|
||||
content="I cannot help with that.",
|
||||
tool_calls=None, # 無 tool_calls
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
results = provider._validate_tool_calls(response)
|
||||
assert len(results) == 0
|
||||
|
||||
def test_empty_choices_response(self):
|
||||
"""測試空 choices 的回應"""
|
||||
provider = NvidiaProvider()
|
||||
|
||||
response = NvidiaResponse(
|
||||
id="resp_123",
|
||||
created=1234567890,
|
||||
model="nvidia/nemotron-mini-4b-instruct",
|
||||
choices=[], # 空 choices
|
||||
)
|
||||
|
||||
results = provider._validate_tool_calls(response)
|
||||
assert len(results) == 0
|
||||
|
||||
def test_provider_result_model(self):
|
||||
"""測試 NvidiaProviderResult 模型各種狀態"""
|
||||
# 成功結果
|
||||
success_result = NvidiaProviderResult(
|
||||
success=True,
|
||||
tool_calls=[
|
||||
ToolCallValidationResult(
|
||||
valid=True,
|
||||
tool_name="restart_pod",
|
||||
arguments={"pod": "api"},
|
||||
)
|
||||
],
|
||||
usage=NvidiaUsage(
|
||||
prompt_tokens=100,
|
||||
completion_tokens=50,
|
||||
total_tokens=150,
|
||||
),
|
||||
latency_ms=1000.0,
|
||||
)
|
||||
assert success_result.success
|
||||
assert len(success_result.tool_calls) == 1
|
||||
assert success_result.usage.total_tokens == 150
|
||||
|
||||
# 失敗結果
|
||||
fail_result = NvidiaProviderResult(
|
||||
success=False,
|
||||
error="Connection timeout",
|
||||
fallback_triggered=True,
|
||||
)
|
||||
assert not fail_result.success
|
||||
assert fail_result.fallback_triggered
|
||||
assert "timeout" in fail_result.error.lower()
|
||||
|
||||
def test_all_high_risk_tools_covered(self):
|
||||
"""確保所有危險操作都被標記為高風險"""
|
||||
dangerous_operations = [
|
||||
"delete_pod",
|
||||
"delete_deployment",
|
||||
"delete_namespace",
|
||||
"delete_service",
|
||||
"delete_configmap",
|
||||
"delete_secret",
|
||||
"scale_to_zero",
|
||||
"drain_node",
|
||||
"cordon_node",
|
||||
"delete_pvc",
|
||||
"delete_pv",
|
||||
]
|
||||
|
||||
for op in dangerous_operations:
|
||||
assert op in HIGH_RISK_TOOLS, f"{op} should be in HIGH_RISK_TOOLS"
|
||||
|
||||
|
||||
class TestAIRouterNvidiaIntegration:
|
||||
"""測試 AIRouter NVIDIA 整合"""
|
||||
|
||||
@@ -314,3 +443,31 @@ class TestAIRouterNvidiaIntegration:
|
||||
assert decision.selected_provider != AIProvider.NVIDIA
|
||||
|
||||
reset_ai_router()
|
||||
|
||||
|
||||
class TestRateLimiterIntegration:
|
||||
"""測試 Rate Limiter 整合 (P2-2)"""
|
||||
|
||||
def test_nvidia_in_rate_limits(self):
|
||||
"""測試 NVIDIA 在 Rate Limits 配置中"""
|
||||
from src.services.ai_rate_limiter import RATE_LIMITS
|
||||
|
||||
assert "nvidia" in RATE_LIMITS
|
||||
assert "rpm" in RATE_LIMITS["nvidia"]
|
||||
assert "daily_requests" in RATE_LIMITS["nvidia"]
|
||||
|
||||
def test_nvidia_rate_limit_values(self):
|
||||
"""測試 NVIDIA Rate Limit 值正確"""
|
||||
from src.services.ai_rate_limiter import RATE_LIMITS
|
||||
|
||||
nvidia_limits = RATE_LIMITS["nvidia"]
|
||||
assert nvidia_limits["rpm"] == 5 # 5 requests per minute
|
||||
assert nvidia_limits["daily_requests"] == 100
|
||||
assert nvidia_limits["daily_tokens"] == 50_000
|
||||
|
||||
def test_nvidia_in_cost_limits(self):
|
||||
"""測試 NVIDIA 在成本限制中 (免費 tier)"""
|
||||
from src.services.ai_rate_limiter import COST_LIMITS
|
||||
|
||||
assert "nvidia" in COST_LIMITS
|
||||
assert COST_LIMITS["nvidia"]["total_cost_usd"] == 0.0 # 免費
|
||||
|
||||
Reference in New Issue
Block a user