Phase C4: 模型连接测试增强 - 多模态测试+批量测试+暗色主题修复
This commit is contained in:
1 parent
5828728f00
commit
6ccf6d6867
3 files changed
+392
-71
No files matched your search
@@ -75,6 +75,7 @@ def model_to_dict(m: AIModel) -> dict:
|
|||||||
"id": m.id,
|
"id": m.id,
|
||||||
"provider_id": m.provider_id,
|
"provider_id": m.provider_id,
|
||||||
"name": m.name,
|
"name": m.name,
|
||||||
|
"display_name": getattr(m, "display_name", None) or m.name,
|
||||||
"api_key": m.api_key,
|
"api_key": m.api_key,
|
||||||
"endpoint": m.endpoint,
|
"endpoint": m.endpoint,
|
||||||
"capabilities": m.capabilities,
|
"capabilities": m.capabilities,
|
||||||
@@ -320,7 +321,8 @@ async def add_models(
|
|||||||
model_info = next((m for m in FETCHABLE_MODELS if m["id"] == model_id), None)
|
model_info = next((m for m in FETCHABLE_MODELS if m["id"] == model_id), None)
|
||||||
|
|
||||||
# 使用 model_id 作为模型名称
|
# 使用 model_id 作为模型名称
|
||||||
model_name = model_info["name"] if model_info else model_id
|
model_name = model_id # API ID
|
||||||
|
display_name_val = model_info["name"] if model_info else model_id
|
||||||
|
|
||||||
# 检查是否已存在同名模型
|
# 检查是否已存在同名模型
|
||||||
existing = db.query(AIModel).filter(
|
existing = db.query(AIModel).filter(
|
||||||
@@ -340,6 +342,7 @@ async def add_models(
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
provider_id=id,
|
provider_id=id,
|
||||||
name=model_name,
|
name=model_name,
|
||||||
|
display_name=display_name_val,
|
||||||
api_key=provider.api_key or "",
|
api_key=provider.api_key or "",
|
||||||
endpoint=endpoint,
|
endpoint=endpoint,
|
||||||
capabilities=model_info.get("capabilities") if model_info else ["text"],
|
capabilities=model_info.get("capabilities") if model_info else ["text"],
|
||||||
@@ -403,6 +406,7 @@ async def get_models(
|
|||||||
async def create_model(
|
async def create_model(
|
||||||
provider_id: str,
|
provider_id: str,
|
||||||
name: str,
|
name: str,
|
||||||
|
display_name: str = None,
|
||||||
api_key: str = None,
|
api_key: str = None,
|
||||||
endpoint: str = None,
|
endpoint: str = None,
|
||||||
capabilities: List[str] = None,
|
capabilities: List[str] = None,
|
||||||
@@ -423,6 +427,7 @@ async def create_model(
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
name=name,
|
name=name,
|
||||||
|
display_name=display_name,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
endpoint=endpoint,
|
endpoint=endpoint,
|
||||||
capabilities=capabilities,
|
capabilities=capabilities,
|
||||||
@@ -451,6 +456,7 @@ async def update_model(
|
|||||||
id: str,
|
id: str,
|
||||||
provider_id: str = None,
|
provider_id: str = None,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
|
display_name: str = None,
|
||||||
api_key: str = None,
|
api_key: str = None,
|
||||||
endpoint: str = None,
|
endpoint: str = None,
|
||||||
capabilities: List[str] = None,
|
capabilities: List[str] = None,
|
||||||
@@ -473,6 +479,8 @@ async def update_model(
|
|||||||
model.provider_id = provider_id
|
model.provider_id = provider_id
|
||||||
if name is not None:
|
if name is not None:
|
||||||
model.name = name
|
model.name = name
|
||||||
|
if display_name is not None:
|
||||||
|
model.display_name = display_name
|
||||||
if api_key is not None:
|
if api_key is not None:
|
||||||
model.api_key = api_key
|
model.api_key = api_key
|
||||||
if endpoint is not None:
|
if endpoint is not None:
|
||||||
@@ -606,6 +614,41 @@ async def get_model_stats(
|
|||||||
# 模型连接测试 API (Phase 2)
|
# 模型连接测试 API (Phase 2)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
def _detect_model_type(capabilities):
|
||||||
|
"""根据 capabilities 判断模型类型,返回测试类型"""
|
||||||
|
if not capabilities:
|
||||||
|
return "text"
|
||||||
|
caps = [c.lower() for c in capabilities]
|
||||||
|
# 优先级: image > audio > video > vision > text
|
||||||
|
if "image" in caps:
|
||||||
|
return "image"
|
||||||
|
if "audio" in caps:
|
||||||
|
return "audio"
|
||||||
|
if "video" in caps:
|
||||||
|
return "video"
|
||||||
|
if "vision" in caps:
|
||||||
|
return "vision"
|
||||||
|
return "text"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_api_base(provider, ai_model):
|
||||||
|
"""构造 API base URL 和 headers"""
|
||||||
|
api_key = ai_model.api_key or (provider.api_key if provider else None)
|
||||||
|
base_url = ""
|
||||||
|
if provider and provider.api_base_url:
|
||||||
|
base_url = provider.api_base_url.rstrip("/")
|
||||||
|
elif ai_model.endpoint:
|
||||||
|
# 从 endpoint 反推 base
|
||||||
|
endpoint = ai_model.endpoint
|
||||||
|
for suffix in ["/v1/chat/completions", "/v1/images/generations", "/v1/audio/speech", "/v1/audio/transcriptions"]:
|
||||||
|
if endpoint.endswith(suffix):
|
||||||
|
base_url = endpoint[:-len(suffix)]
|
||||||
|
break
|
||||||
|
if not base_url:
|
||||||
|
base_url = endpoint.rsplit("/v1", 1)[0] if "/v1" in endpoint else ""
|
||||||
|
return api_key, base_url
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{model_id}/test-connection")
|
@router.post("/{model_id}/test-connection")
|
||||||
async def test_model_connection(
|
async def test_model_connection(
|
||||||
model_id: str,
|
model_id: str,
|
||||||
@@ -614,12 +657,13 @@ async def test_model_connection(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
测试指定模型的连接是否正常
|
测试指定模型的连接是否正常
|
||||||
支持从模型记录或提供商记录读取配置
|
支持多模态: text/image/audio/vision 自动检测模型类型并使用对应API测试
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.info(f"Testing connection for model_id: {model_id}")
|
logger.info(f"Testing connection for model_id: {model_id}")
|
||||||
|
|
||||||
# 优先从 AIModel 读取
|
|
||||||
ai_model = db.query(AIModel).filter(AIModel.id == model_id).first()
|
ai_model = db.query(AIModel).filter(AIModel.id == model_id).first()
|
||||||
if not ai_model:
|
if not ai_model:
|
||||||
raise HTTPException(status_code=404, detail="模型不存在")
|
raise HTTPException(status_code=404, detail="模型不存在")
|
||||||
@@ -628,101 +672,163 @@ async def test_model_connection(
|
|||||||
if not provider:
|
if not provider:
|
||||||
raise HTTPException(status_code=404, detail="模型供应商不存在")
|
raise HTTPException(status_code=404, detail="模型供应商不存在")
|
||||||
|
|
||||||
# 确定最终配置
|
api_key, base_url = _build_api_base(provider, ai_model)
|
||||||
api_key = ai_model.api_key or (provider.api_key if provider else None)
|
|
||||||
api_url = ai_model.endpoint
|
|
||||||
|
|
||||||
# 如果 endpoint 为空,尝试构造
|
|
||||||
if not api_url and provider and provider.api_base_url:
|
|
||||||
base = provider.api_base_url.rstrip('/')
|
|
||||||
if provider.protocol == 'openai_compatible':
|
|
||||||
api_url = f"{base}/v1/chat/completions"
|
|
||||||
else:
|
|
||||||
api_url = f"{base}/v1/chat/completions"
|
|
||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "未配置 API Key,请在模型或供应商设置中添加",
|
"message": "未配置 API Key,请在模型或供应商设置中添加",
|
||||||
"latency_ms": None,
|
"latency_ms": None,
|
||||||
|
"model_type": "unknown",
|
||||||
}
|
}
|
||||||
if not api_url:
|
if not base_url:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "未配置 API 地址,请在模型或供应商设置中添加",
|
"message": "未配置 API 地址,请在模型或供应商设置中添加",
|
||||||
"latency_ms": None,
|
"latency_ms": None,
|
||||||
|
"model_type": "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model_type = _detect_model_type(ai_model.capabilities)
|
||||||
timeout = ai_model.timeout or 60
|
timeout = ai_model.timeout or 60
|
||||||
import time
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
# 使用 minimal payload 测试连接
|
|
||||||
payload = {
|
|
||||||
"model": ai_model.name,
|
|
||||||
"messages": [{"role": "user", "content": "hi"}],
|
|
||||||
"max_tokens": 5,
|
|
||||||
}
|
|
||||||
resp = await client.post(api_url, json=payload, headers=headers)
|
|
||||||
latency = int((time.time() - start) * 1000)
|
|
||||||
|
|
||||||
if resp.status_code == 200:
|
# ============================================================
|
||||||
try:
|
# 文本模型 / 视觉模型 / 思考模型 → chat/completions
|
||||||
|
# ============================================================
|
||||||
|
if model_type in ("text", "vision", "thinking", "tool"):
|
||||||
|
chat_url = f"{base_url}/v1/chat/completions"
|
||||||
|
payload = {
|
||||||
|
"model": ai_model.name,
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_tokens": 5,
|
||||||
|
}
|
||||||
|
# 视觉模型附带图片URL测试
|
||||||
|
if model_type == "vision":
|
||||||
|
payload["messages"] = [{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "What is this?"},
|
||||||
|
{"type": "image_url", "image_url": {"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png"}},
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
payload["max_tokens"] = 10
|
||||||
|
|
||||||
|
resp = await client.post(chat_url, json=payload, headers=headers)
|
||||||
|
latency = int((time.time() - start) * 1000)
|
||||||
|
|
||||||
|
if resp.status_code == 200:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
model_used = data.get("model", ai_model.name)
|
model_used = data.get("model", ai_model.name)
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"连接成功,使用模型: {model_used}",
|
"message": f"连接成功 ({model_type}),模型: {model_used}",
|
||||||
"latency_ms": latency,
|
"latency_ms": latency,
|
||||||
|
"model_type": model_type,
|
||||||
"model_in_response": model_used,
|
"model_in_response": model_used,
|
||||||
}
|
}
|
||||||
except Exception:
|
else:
|
||||||
|
return _build_error_response(resp, latency, model_type)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 图像生成模型 → images/generations
|
||||||
|
# ============================================================
|
||||||
|
elif model_type == "image":
|
||||||
|
image_url = f"{base_url}/v1/images/generations"
|
||||||
|
payload = {
|
||||||
|
"model": ai_model.name,
|
||||||
|
"prompt": "a red circle",
|
||||||
|
"n": 1,
|
||||||
|
"size": "256x256",
|
||||||
|
}
|
||||||
|
resp = await client.post(image_url, json=payload, headers=headers)
|
||||||
|
latency = int((time.time() - start) * 1000)
|
||||||
|
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
img_count = len(data.get("data", []))
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"连接成功 (HTTP {resp.status_code})",
|
"message": f"图像生成成功 ({model_type}),返回 {img_count} 张图片",
|
||||||
"latency_ms": latency,
|
"latency_ms": latency,
|
||||||
|
"model_type": model_type,
|
||||||
}
|
}
|
||||||
elif resp.status_code == 401:
|
else:
|
||||||
return {
|
return _build_error_response(resp, latency, model_type)
|
||||||
"success": False,
|
|
||||||
"message": "认证失败:API Key 无效或已过期",
|
# ============================================================
|
||||||
"latency_ms": latency,
|
# 音频模型 → audio/speech (TTS)
|
||||||
|
# ============================================================
|
||||||
|
elif model_type == "audio":
|
||||||
|
audio_url = f"{base_url}/v1/audio/speech"
|
||||||
|
payload = {
|
||||||
|
"model": ai_model.name,
|
||||||
|
"input": "hello",
|
||||||
|
"voice": "alloy",
|
||||||
}
|
}
|
||||||
elif resp.status_code == 403:
|
resp = await client.post(audio_url, json=payload, headers=headers)
|
||||||
return {
|
latency = int((time.time() - start) * 1000)
|
||||||
"success": False,
|
|
||||||
"message": "访问被拒绝:API Key 权限不足",
|
if resp.status_code == 200:
|
||||||
"latency_ms": latency,
|
content_type = resp.headers.get("content-type", "")
|
||||||
}
|
has_audio = "audio" in content_type or len(resp.content) > 100
|
||||||
elif resp.status_code == 429:
|
return {
|
||||||
return {
|
"success": True,
|
||||||
"success": False,
|
"message": f"音频服务连接成功 ({model_type}),响应类型: {content_type}",
|
||||||
"message": "触发限流:请稍后重试",
|
"latency_ms": latency,
|
||||||
"latency_ms": latency,
|
"model_type": model_type,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return _build_error_response(resp, latency, model_type)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 视频模型 → 尝试 chat/completions (可能不支持,回退)
|
||||||
|
# ============================================================
|
||||||
|
elif model_type == "video":
|
||||||
|
chat_url = f"{base_url}/v1/chat/completions"
|
||||||
|
payload = {
|
||||||
|
"model": ai_model.name,
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_tokens": 5,
|
||||||
}
|
}
|
||||||
|
resp = await client.post(chat_url, json=payload, headers=headers)
|
||||||
|
latency = int((time.time() - start) * 1000)
|
||||||
|
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
model_used = data.get("model", ai_model.name)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"连接成功 ({model_type}),模型: {model_used}",
|
||||||
|
"latency_ms": latency,
|
||||||
|
"model_type": model_type,
|
||||||
|
"model_in_response": model_used,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return _build_error_response(resp, latency, model_type)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
|
||||||
err_data = resp.json()
|
|
||||||
err_msg = err_data.get("error", {}).get("message", resp.text[:200])
|
|
||||||
except Exception:
|
|
||||||
err_msg = resp.text[:200]
|
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"API 错误 (HTTP {resp.status_code}): {err_msg}",
|
"message": f"未知模型类型: {model_type}",
|
||||||
"latency_ms": latency,
|
"latency_ms": int((time.time() - start) * 1000),
|
||||||
|
"model_type": model_type,
|
||||||
}
|
}
|
||||||
|
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
latency = int((time.time() - start) * 1000)
|
latency = int((time.time() - start) * 1000)
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"连接超时({timeout}s):请检查 API 地址是否正确,或服务是否可访问",
|
"message": f"连接超时({timeout}s):请检查 API 地址是否正确,或服务是否可访问",
|
||||||
"latency_ms": latency,
|
"latency_ms": latency,
|
||||||
|
"model_type": model_type,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
latency = int((time.time() - start) * 1000)
|
latency = int((time.time() - start) * 1000)
|
||||||
@@ -731,9 +837,98 @@ async def test_model_connection(
|
|||||||
"success": False,
|
"success": False,
|
||||||
"message": f"连接失败: {str(e)}",
|
"message": f"连接失败: {str(e)}",
|
||||||
"latency_ms": latency,
|
"latency_ms": latency,
|
||||||
|
"model_type": model_type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_error_response(resp, latency, model_type):
|
||||||
|
"""构造错误响应"""
|
||||||
|
if resp.status_code == 401:
|
||||||
|
msg = "认证失败:API Key 无效或已过期"
|
||||||
|
elif resp.status_code == 403:
|
||||||
|
msg = "访问被拒绝:API Key 权限不足"
|
||||||
|
elif resp.status_code == 429:
|
||||||
|
msg = "触发限流:请稍后重试"
|
||||||
|
elif resp.status_code == 404:
|
||||||
|
msg = f"API 地址不存在 (404):请检查 endpoint 配置是否正确"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
err_data = resp.json()
|
||||||
|
err_msg = err_data.get("error", {}).get("message", resp.text[:200])
|
||||||
|
except Exception:
|
||||||
|
err_msg = resp.text[:200]
|
||||||
|
msg = f"API 错误 (HTTP {resp.status_code}): {err_msg}"
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": msg,
|
||||||
|
"latency_ms": latency,
|
||||||
|
"model_type": model_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/test-all")
|
||||||
|
async def test_all_models(
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""批量测试所有已启用模型的连接状态"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
models = db.query(AIModel).filter(AIModel.is_enabled == True).all()
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for m in models:
|
||||||
|
provider = db.query(ModelProvider).filter(ModelProvider.id == m.provider_id).first()
|
||||||
|
if not provider:
|
||||||
|
results.append({
|
||||||
|
"model_id": m.id,
|
||||||
|
"model_name": m.name,
|
||||||
|
"display_name": m.display_name or m.name,
|
||||||
|
"success": False,
|
||||||
|
"message": "供应商不存在",
|
||||||
|
"latency_ms": None,
|
||||||
|
"model_type": "unknown",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 复用 test_model_connection 逻辑
|
||||||
|
try:
|
||||||
|
result = await test_model_connection(m.id, admin_user, db)
|
||||||
|
if isinstance(result, dict):
|
||||||
|
result["model_id"] = m.id
|
||||||
|
result["model_name"] = m.name
|
||||||
|
result["display_name"] = m.display_name or m.name
|
||||||
|
results.append(result)
|
||||||
|
else:
|
||||||
|
results.append({
|
||||||
|
"model_id": m.id,
|
||||||
|
"model_name": m.name,
|
||||||
|
"display_name": m.display_name or m.name,
|
||||||
|
"success": False,
|
||||||
|
"message": "测试返回异常",
|
||||||
|
"latency_ms": None,
|
||||||
|
"model_type": "unknown",
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
results.append({
|
||||||
|
"model_id": m.id,
|
||||||
|
"model_name": m.name,
|
||||||
|
"display_name": m.display_name or m.name,
|
||||||
|
"success": False,
|
||||||
|
"message": f"测试异常: {str(e)}",
|
||||||
|
"latency_ms": None,
|
||||||
|
"model_type": "unknown",
|
||||||
|
})
|
||||||
|
|
||||||
|
success_count = sum(1 for r in results if r.get("success"))
|
||||||
|
return {
|
||||||
|
"total": len(results),
|
||||||
|
"success": success_count,
|
||||||
|
"failed": len(results) - success_count,
|
||||||
|
"results": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
timeout: int = 60
|
timeout: int = 60
|
||||||
status: str = "paid"
|
status: str = "paid"
|
||||||
price_per_million_tokens: float = 0.0
|
price_per_million_tokens: float = 0.0
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export const adminAPI = {
|
|||||||
setDefaultModel: (id: string) => put<any>(`/admin/models/${id}/set-default`),
|
setDefaultModel: (id: string) => put<any>(`/admin/models/${id}/set-default`),
|
||||||
// Phase 2: Enhanced model management
|
// Phase 2: Enhanced model management
|
||||||
testModelConnection: (id: string) => post<any>(`/admin/models/${id}/test-connection`),
|
testModelConnection: (id: string) => post<any>(`/admin/models/${id}/test-connection`),
|
||||||
|
testAllModels: () => post<any>('/admin/models/test-all'),
|
||||||
fetchProviderModels: (id: string) => post<any>(`/admin/models/providers/${id}/fetch-models`),
|
fetchProviderModels: (id: string) => post<any>(`/admin/models/providers/${id}/fetch-models`),
|
||||||
createAdminModel: (data: any) => post<any>('/admin/models/', data),
|
createAdminModel: (data: any) => post<any>('/admin/models/', data),
|
||||||
updateAdminModel: (id: string, data: any) => put<any>(`/admin/models/${id}`, data),
|
updateAdminModel: (id: string, data: any) => put<any>(`/admin/models/${id}`, data),
|
||||||
|
|||||||
@@ -94,8 +94,10 @@ const Models: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [modelSearch, setModelSearch] = useState('');
|
const [modelSearch, setModelSearch] = useState('');
|
||||||
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
const [testResults, setTestResults] = useState<Record<string, TestResult>>({});
|
||||||
const [testingModelId, setTestingModelId] = useState<string | null>(null);
|
const [testingIds, setTestingIds] = useState<Set<string>>(new Set());
|
||||||
|
const [testingAll, setTestingAll] = useState(false);
|
||||||
|
const [expandedTestResult, setExpandedTestResult] = useState<string | null>(null);
|
||||||
const [showModelEditor, setShowModelEditor] = useState(false);
|
const [showModelEditor, setShowModelEditor] = useState(false);
|
||||||
const [editingModel, setEditingModel] = useState<EditingModel | null>(null);
|
const [editingModel, setEditingModel] = useState<EditingModel | null>(null);
|
||||||
const [modelSaving, setModelSaving] = useState(false);
|
const [modelSaving, setModelSaving] = useState(false);
|
||||||
@@ -147,20 +149,64 @@ const Models: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}, [models, modelSearch]);
|
}, [models, modelSearch]);
|
||||||
|
|
||||||
// Test model connection
|
// Test single model connection
|
||||||
const handleTestConnection = async (modelId: string) => {
|
const handleTestConnection = async (modelId: string) => {
|
||||||
setTestingModelId(modelId);
|
setTestingIds(prev => new Set(prev).add(modelId));
|
||||||
setTestResult(null);
|
|
||||||
try {
|
try {
|
||||||
const result = await adminAPI.testModelConnection(modelId) as any;
|
const result = await adminAPI.testModelConnection(modelId) as any;
|
||||||
setTestResult(result.success !== undefined ? result : (result.data || result as TestResult));
|
const testResult: TestResult = result.success !== undefined ? result : (result.data || result as TestResult);
|
||||||
|
setTestResults(prev => ({ ...prev, [modelId]: testResult }));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setTestResult({ success: false, message: err.message || '测试失败' });
|
setTestResults(prev => ({ ...prev, [modelId]: { success: false, message: err.message || '测试失败' } }));
|
||||||
} finally {
|
} finally {
|
||||||
setTestingModelId(null);
|
setTestingIds(prev => { const next = new Set(prev); next.delete(modelId); return next; });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Test all models
|
||||||
|
const handleTestAll = async () => {
|
||||||
|
setTestingAll(true);
|
||||||
|
setTestResults({});
|
||||||
|
try {
|
||||||
|
const result = await adminAPI.testAllModels() as any;
|
||||||
|
const data = result.data || result;
|
||||||
|
const resultsMap: Record<string, TestResult> = {};
|
||||||
|
if (data.results) {
|
||||||
|
for (const r of data.results) {
|
||||||
|
resultsMap[r.model_id] = {
|
||||||
|
success: r.success,
|
||||||
|
message: r.message,
|
||||||
|
latency_ms: r.latency_ms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTestResults(resultsMap);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError('批量测试失败: ' + (err.message || ''));
|
||||||
|
} finally {
|
||||||
|
setTestingAll(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get model type label
|
||||||
|
const getModelType = (caps: string[] | undefined): string => {
|
||||||
|
if (!caps || caps.length === 0) return 'text';
|
||||||
|
const lower = caps.map(c => c.toLowerCase());
|
||||||
|
if (lower.includes('image')) return 'image';
|
||||||
|
if (lower.includes('audio')) return 'audio';
|
||||||
|
if (lower.includes('video')) return 'video';
|
||||||
|
if (lower.includes('vision')) return 'vision';
|
||||||
|
return 'text';
|
||||||
|
};
|
||||||
|
|
||||||
|
const modelTypeLabels: Record<string, { label: string; color: string }> = {
|
||||||
|
text: { label: '文本', color: '#5B5FED' },
|
||||||
|
image: { label: '图像生成', color: '#D9383A' },
|
||||||
|
audio: { label: '音频', color: '#E8922C' },
|
||||||
|
video: { label: '视频', color: '#EC4899' },
|
||||||
|
vision: { label: '视觉理解', color: '#219B5E' },
|
||||||
|
};
|
||||||
|
|
||||||
const handleSetDefault = async (modelId: string) => {
|
const handleSetDefault = async (modelId: string) => {
|
||||||
try {
|
try {
|
||||||
await adminAPI.setDefaultModel(modelId);
|
await adminAPI.setDefaultModel(modelId);
|
||||||
@@ -319,6 +365,11 @@ const Models: React.FC = () => {
|
|||||||
管理 AI 模型供应商、模型配置和连接测试
|
管理 AI 模型供应商、模型配置和连接测试
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button onClick={handleTestAll}
|
||||||
|
disabled={testingAll}
|
||||||
|
style={btnStyle(testingAll ? '#4B5563' : '#059669')}>
|
||||||
|
{testingAll ? '批量测试中...' : '⚡ 测试全部'}
|
||||||
|
</button>
|
||||||
<button onClick={() => { setShowAddProvider(true); setEditingProvider(null); setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' }); }}
|
<button onClick={() => { setShowAddProvider(true); setEditingProvider(null); setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' }); }}
|
||||||
style={btnStyle('#5B5FED')}>
|
style={btnStyle('#5B5FED')}>
|
||||||
+ 添加供应商
|
+ 添加供应商
|
||||||
@@ -426,7 +477,8 @@ const Models: React.FC = () => {
|
|||||||
<tr style={{ background: '#F3F4F6' }}>
|
<tr style={{ background: '#F3F4F6' }}>
|
||||||
<th style={thStyle}>模型名称</th>
|
<th style={thStyle}>模型名称</th>
|
||||||
<th style={thStyle}>显示名称</th>
|
<th style={thStyle}>显示名称</th>
|
||||||
<th style={thStyle}>能力</th>
|
<th style={thStyle}>类型</th>
|
||||||
|
<th style={thStyle}>能力</th>
|
||||||
<th style={thStyle}>状态</th>
|
<th style={thStyle}>状态</th>
|
||||||
<th style={thStyle}>价格</th>
|
<th style={thStyle}>价格</th>
|
||||||
<th style={thStyle}>调用量</th>
|
<th style={thStyle}>调用量</th>
|
||||||
@@ -446,6 +498,16 @@ const Models: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
{model.isDefault && <span style={{ fontSize: '11px', color: '#16A34A', marginLeft: 6 }}>⭐ 默认</span>}
|
{model.isDefault && <span style={{ fontSize: '11px', color: '#16A34A', marginLeft: 6 }}>⭐ 默认</span>}
|
||||||
</td>
|
</td>
|
||||||
|
<td style={tdStyle}>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '11px', padding: '2px 8px', borderRadius: '3px',
|
||||||
|
background: (modelTypeLabels[getModelType(model.capabilities)].color) + '22',
|
||||||
|
color: modelTypeLabels[getModelType(model.capabilities)].color,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}>
|
||||||
|
{modelTypeLabels[getModelType(model.capabilities)].label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td style={tdStyle}>
|
<td style={tdStyle}>
|
||||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||||
{(model.capabilities || []).map(cap => (
|
{(model.capabilities || []).map(cap => (
|
||||||
@@ -475,18 +537,81 @@ const Models: React.FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td style={tdStyle}>
|
<td style={tdStyle}>
|
||||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||||
<button onClick={() => handleTestConnection(model.id)} disabled={testingModelId === model.id}
|
<button onClick={() => handleTestConnection(model.id)} disabled={testingIds.has(model.id)}
|
||||||
style={{ ...btnStyle(testingModelId === model.id ? '#9CA3AF' : '#059669', 'small') }}>
|
style={{ ...btnStyle(testingIds.has(model.id) ? '#4B5563' : '#059669', 'small') }}>
|
||||||
{testingModelId === model.id ? '测试中...' : '测试'}
|
{testingIds.has(model.id) ? '测试中...' : '测试'}
|
||||||
</button>
|
</button>
|
||||||
{testResult && testingModelId === null && (
|
{testResults[model.id] && (
|
||||||
<span style={{ fontSize: '11px', padding: '4px 8px', background: testResult.success ? '#DCFCE7' : '#FEE2E2', color: testResult.success ? '#16A34A' : '#DC2626', borderRadius: '4px' }}>
|
<span
|
||||||
{testResult.success ? `✓ ${testResult.latency_ms || 0}ms` : '✗ 失败'}
|
onClick={() => setExpandedTestResult(expandedTestResult === model.id ? null : model.id)}
|
||||||
|
style={{
|
||||||
|
fontSize: '11px', padding: '4px 8px', cursor: 'pointer',
|
||||||
|
background: testResults[model.id].success ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)',
|
||||||
|
color: testResults[model.id].success ? '#22C55E' : '#EF4444',
|
||||||
|
borderRadius: '4px', border: `1px solid ${testResults[model.id].success ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
{testResults[model.id].success ? `✓ ${testResults[model.id].latency_ms || 0}ms` : '✗ 失败'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!model.isDefault && model.isEnabled && (
|
{!model.isDefault && model.isEnabled && (
|
||||||
<button onClick={() => handleSetDefault(model.id)} style={btnStyle('#5B5FED', 'small')}>设为默认</button>
|
<button onClick={() => handleSetDefault(model.id)} style={btnStyle('#5B5FED', 'small')}>设为默认</button>
|
||||||
)}
|
)}
|
||||||
|
{expandedTestResult === model.id && testResults[model.id] && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.6)', zIndex: 1000,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}} onClick={() => setExpandedTestResult(null)}>
|
||||||
|
<div style={{
|
||||||
|
background: '#111627', borderRadius: 12, padding: 24,
|
||||||
|
maxWidth: 480, width: '90%',
|
||||||
|
border: `1px solid ${testResults[model.id].success ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
|
||||||
|
}} onClick={e => e.stopPropagation()}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||||
|
<h3 style={{ margin: 0, fontSize: 16, color: '#E2E8F0' }}>连接测试详情</h3>
|
||||||
|
<button onClick={() => setExpandedTestResult(null)} style={{ background: 'none', border: 'none', color: '#6B7280', cursor: 'pointer', fontSize: 18 }}>×</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<span style={{ fontSize: 12, color: '#6B7280' }}>模型</span>
|
||||||
|
<span style={{ fontSize: 14, color: '#E2E8F0', marginLeft: 8 }}>{model.name}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<span style={{ fontSize: 12, color: '#6B7280' }}>类型</span>
|
||||||
|
<span style={{ fontSize: 14, color: modelTypeLabels[getModelType(model.capabilities)].color, marginLeft: 8 }}>
|
||||||
|
{modelTypeLabels[getModelType(model.capabilities)].label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<span style={{ fontSize: 12, color: '#6B7280' }}>状态</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 14, marginLeft: 8, fontWeight: 600,
|
||||||
|
color: testResults[model.id].success ? '#22C55E' : '#EF4444',
|
||||||
|
}}>
|
||||||
|
{testResults[model.id].success ? '✓ 成功' : '✗ 失败'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{testResults[model.id].latency_ms !== undefined && testResults[model.id].latency_ms !== null && (
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<span style={{ fontSize: 12, color: '#6B7280' }}>延迟</span>
|
||||||
|
<span style={{ fontSize: 14, color: '#E2E8F0', marginLeft: 8, fontFamily: 'monospace' }}>
|
||||||
|
{testResults[model.id].latency_ms}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<span style={{ fontSize: 12, color: '#6B7280', display: 'block', marginBottom: 4 }}>详情</span>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 13, color: '#9CA3AF', lineHeight: 1.6,
|
||||||
|
background: 'rgba(0,0,0,0.3)', padding: 12, borderRadius: 8,
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}>
|
||||||
|
{testResults[model.id].message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<button onClick={() => openModelEditor(model as AIModel)} style={btnStyle('#6B7280', 'small')}>编辑</button>
|
<button onClick={() => openModelEditor(model as AIModel)} style={btnStyle('#6B7280', 'small')}>编辑</button>
|
||||||
<button onClick={() => handleDeleteModel(model.id)} style={btnStyle('#EF4444', 'small')}>删除</button>
|
<button onClick={() => handleDeleteModel(model.id)} style={btnStyle('#EF4444', 'small')}>删除</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user