diff --git a/backend/app/api/v1/endpoints/platform.py b/backend/app/api/v1/endpoints/platform.py index 2e81ee6..466d672 100644 --- a/backend/app/api/v1/endpoints/platform.py +++ b/backend/app/api/v1/endpoints/platform.py @@ -741,6 +741,67 @@ async def model_list(current_user: dict = Depends(get_current_user)) -> dict[str return ok(get_platform_store().models()) +@router.post("/model-manage/test-online") +async def test_online_model(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: + """测试在线模型 API 是否可用:发送一个简单的 chat/completions 请求验证连通性。""" + api_url = (payload.get("api_url") or "").rstrip("/") + api_key = payload.get("api_key") or "" + model_name = payload.get("online_model_name") or "" + if not api_url: + raise fail(400, "api_url is required") + if not model_name: + raise fail(400, "online_model_name is required") + import httpx + try: + async with httpx.AsyncClient(timeout=15) as client: + headers = {"Content-Type": "application/json"} + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + # 尝试多种 OpenAI 兼容路径 + chat_paths = [ + f"{api_url}/chat/completions", + f"{api_url}/v1/chat/completions", + f"{api_url}/modelTF/v1/chat/completions", + ] + resp = None + for path in chat_paths: + try: + r = await client.post( + path, + json={ + "model": model_name, + "messages": [{"role": "user", "content": "Hi"}], + "max_tokens": 5, + "temperature": 0, + }, + headers=headers, + ) + if r.status_code in (200, 201): + resp = r + break + except Exception: + continue + if resp is None: + return ok({"success": False, "error": f"无法连接到 {api_url},请检查地址和端口"}) + body = resp.json() + usage = body.get("usage", {}) + return ok({ + "success": True, + "model": body.get("model", model_name), + "provider": body.get("object", ""), + "usage": { + "prompt_tokens": usage.get("prompt_tokens", 0), + "completion_tokens": usage.get("completion_tokens", 0), + "total_tokens": usage.get("total_tokens", 0), + }, + "latency_ms": None, # 由前端计算 + }) + except httpx.TimeoutException: + return ok({"success": False, "error": "连接超时(15s),请检查网络或 API 地址是否正确"}) + except Exception as exc: + return ok({"success": False, "error": str(exc)}) + + @router.post("/model-manage") async def create_model(payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]: payload.setdefault("created_by", current_user.get("id")) diff --git a/frontend/src/api/modules/model.ts b/frontend/src/api/modules/model.ts index 2b063fe..fef7227 100644 --- a/frontend/src/api/modules/model.ts +++ b/frontend/src/api/modules/model.ts @@ -100,3 +100,14 @@ export const mergeModel = (data: { /** 导出已训练模型权重 */ export const exportModelUrl = (modelName: string) => `/modelTF/model-manage/trained-models/${encodeURIComponent(modelName)}/export` + +/** 测试在线模型连通性 */ +export const testOnlineModel = (data: { + api_url: string + api_key: string + online_model_name: string +}) => + post<{ success: boolean; error?: string; model?: string; usage?: object }>( + '/model-manage/test-online', + data, + ) diff --git a/frontend/src/views/model/ModelCreateView.vue b/frontend/src/views/model/ModelCreateView.vue index 4904a7f..85e94ea 100644 --- a/frontend/src/views/model/ModelCreateView.vue +++ b/frontend/src/views/model/ModelCreateView.vue @@ -8,6 +8,7 @@ import { createModel, updateModel, getLocalModels, + testOnlineModel, } from '@/api/modules/model' import { MODEL_TYPE_MAP } from '@/constants' import type { ModelForm, ModelSource } from '@/types' @@ -35,6 +36,10 @@ const form = reactive({ online_model_name: '', }) +// 在线模型测试 +const testLoading = ref(false) +const testResult = ref<{ success: boolean; error?: string; model?: string; usage?: object } | null>(null) + const rules: FormRules = { name: [ { required: true, message: '请输入模型名称', trigger: 'blur' }, @@ -74,7 +79,7 @@ async function loadEditData() { path: model.path || '', api_url: model.api_url || '', api_key: model.api_key || '', - online_model_name: model.model_name || '', + online_model_name: model.online_model_name || '', }) } catch { // ignore @@ -133,6 +138,36 @@ async function handleSubmit() { }) } +async function handleTestOnline() { + if (!form.api_url || !form.online_model_name) { + ElMessage.warning('请先填写 API 地址和模型名称') + return + } + testLoading.value = true + testResult.value = null + try { + const start = Date.now() + const res = await testOnlineModel({ + api_url: form.api_url, + api_key: form.api_key, + online_model_name: form.online_model_name, + }) + const data = res as any + if (data.success) { + data.latency_ms = Date.now() - start + ElMessage.success(`模型 ${data.model || form.online_model_name} 连接成功 (${data.latency_ms}ms)`) + } else { + ElMessage.error(data.error || '连接失败') + } + testResult.value = data + } catch (e: any) { + ElMessage.error(e?.message || e?.response?.data?.message || '测试请求失败') + testResult.value = { success: false, error: String(e) } + } finally { + testLoading.value = false + } +} + function handleCancel() { router.back() } @@ -217,7 +252,25 @@ onMounted(() => { - +
+ + 测试连接 +
+ +
+ + +
@@ -255,4 +308,41 @@ onMounted(() => { color: #64748b; cursor: help; } + +.model-test-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + width: 100%; +} + +.test-result { + margin-top: 8px; + padding: 10px 14px; + border-radius: 6px; + font-size: 13px; + + &.success { + background: #f0fdf4; + color: #166534; + border: 1px solid #bbf7d0; + } + + &.error { + background: #fef2f2; + color: #991b1b; + border: 1px solid #fecaca; + } + + i { + margin-right: 6px; + } + + .usage-info { + margin-left: 12px; + color: #64748b; + font-size: 12px; + } +}