""" 大模型库管理 API(管理员) """ from fastapi import APIRouter, Depends, HTTPException, Query, Body from sqlalchemy.orm import Session from sqlalchemy import func from typing import List, Optional from pydantic import BaseModel from ...database import get_db, SessionLocal from ...models import AIModel, ModelProvider, SiteSetting from ...auth import get_current_admin from ...middleware.log_service import LogService from ...capability_registry import ( CAPABILITY_TYPES, CAPABILITY_COLORS, TEXT_FAMILY, get_capability_defaults, set_capability_default, resolve_auto_model, ) import uuid import json import logging import httpx from datetime import datetime router = APIRouter() # ============================================================ # 请求体模型(前端以 JSON body 提交,而非 query 参数) # ============================================================ class ModelCreateRequest(BaseModel): provider_id: str name: str display_name: Optional[str] = None api_key: Optional[str] = None endpoint: Optional[str] = None capabilities: Optional[List[str]] = None timeout: int = 60 status: str = "paid" price_per_million_tokens: float = 0 is_enabled: bool = True class ModelUpdateRequest(BaseModel): provider_id: Optional[str] = None name: Optional[str] = None display_name: Optional[str] = None api_key: Optional[str] = None endpoint: Optional[str] = None capabilities: Optional[List[str]] = None timeout: Optional[int] = None status: Optional[str] = None price_per_million_tokens: Optional[float] = None is_enabled: Optional[bool] = None # ============================================================ # 协议预设列表 # ============================================================ PROTOCOLS = [ {"id": "openai_compatible", "name": "OpenAI兼容", "base_path": "/v1", "description": "兼容OpenAI API格式的模型服务"}, {"id": "new_api", "name": "New API", "base_path": "/v1", "description": "新一代API聚合平台"}, {"id": "volcengine", "name": "火山引擎", "base_path": "/v1", "description": "字节跳动火山引擎模型服务"}, {"id": "aliyun_bailian", "name": "阿里云百炼", "base_path": "/v1", "description": "阿里云百炼大模型平台"}, {"id": "ollama", "name": "Ollama", "base_path": "/v1", "description": "本地Ollama模型服务"}, {"id": "xiaoma", "name": "小马算力", "base_path": "/v1", "description": "小马算力模型服务"}, ] # ============================================================ # 模型列表(用于 fetch-models 的回退数据) # ============================================================ FETCHABLE_MODELS = [ {"id": "gpt-4o", "name": "GPT-4o", "capabilities": ["text", "vision", "tool"], "pricing": "paid"}, {"id": "gpt-4o-mini", "name": "GPT-4o Mini", "capabilities": ["text", "vision"], "pricing": "paid"}, {"id": "deepseek-v3", "name": "DeepSeek-V3", "capabilities": ["text", "thinking"], "pricing": "free"}, {"id": "deepseek-r1", "name": "DeepSeek-R1", "capabilities": ["text", "thinking"], "pricing": "paid"}, {"id": "qwen-max", "name": "Qwen-Max", "capabilities": ["text", "tool"], "pricing": "free"}, {"id": "qwen-vl", "name": "Qwen-VL", "capabilities": ["text", "vision"], "pricing": "paid"}, {"id": "whisper", "name": "Whisper", "capabilities": ["audio"], "pricing": "paid"}, {"id": "dalle", "name": "DALL-E", "capabilities": ["image"], "pricing": "paid"}, ] # 支持通过 API 获取模型列表的协议类型 FETCHABLE_PROTOCOLS = {"openai_compatible", "new_api", "volcengine", "aliyun_bailian", "ollama", "xiaoma"} # ============================================================ # 辅助函数 # ============================================================ def provider_to_dict(p: ModelProvider) -> dict: return { "id": p.id, "name": p.name, "protocol": p.protocol, "api_key": p.api_key, "api_base_url": p.api_base_url, "apiKey": p.api_key, "apiBaseUrl": p.api_base_url, "created_at": p.created_at.isoformat() if p.created_at else None, "updated_at": p.updated_at.isoformat() if p.updated_at else None, "createdAt": p.created_at.isoformat() if p.created_at else None, "updatedAt": p.updated_at.isoformat() if p.updated_at else None, } def model_to_dict(m: AIModel) -> dict: return { "id": m.id, "provider_id": m.provider_id, "name": m.name, "display_name": getattr(m, "display_name", None) or m.name, "api_key": m.api_key, "endpoint": m.endpoint, "capabilities": m.capabilities, "timeout": m.timeout, "status": m.status, "price_per_million_tokens": float(m.price_per_million_tokens) if m.price_per_million_tokens else 0, "is_enabled": m.is_enabled, "is_default": bool(m.is_default), "total_calls": m.total_calls or 0, "total_tokens": m.total_tokens or 0, "created_at": m.created_at.isoformat() if m.created_at else None, "updated_at": m.updated_at.isoformat() if m.updated_at else None, } # ============================================================ # 协议列表 # ============================================================ @router.get("/protocols") async def get_protocols( admin_user=Depends(get_current_admin), ): """获取支持的协议列表""" return PROTOCOLS @router.get("/capabilities") async def get_capabilities( admin_user=Depends(get_current_admin), ): """获取平台支持的全部模型能力类型(含标签、测试类型、配色、描述)""" return [ {**c, "color": CAPABILITY_COLORS.get(c["id"], "#6B7280")} for c in CAPABILITY_TYPES ] # ============================================================ # 模型供应商管理 # ============================================================ @router.get("/providers") async def get_providers( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取模型供应商列表""" providers = db.query(ModelProvider).order_by( ModelProvider.created_at.desc() ).all() return [provider_to_dict(p) for p in providers] @router.post("/providers") async def create_provider( name: str = Body(...), protocol: str = Body(...), api_key: str = Body(default=""), api_base_url: str = Body(default=None), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """添加模型供应商""" # 验证协议 valid_protocols = [p["id"] for p in PROTOCOLS] if protocol not in valid_protocols: raise HTTPException(status_code=400, detail=f"无效的协议类型,支持: {', '.join(valid_protocols)}") provider = ModelProvider( id=str(uuid.uuid4()), name=name, protocol=protocol, api_key=api_key, api_base_url=api_base_url, ) db.add(provider) db.commit() db.refresh(provider) LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="create_provider", target_type="model_provider", target_id=provider.id, detail=f"添加模型供应商 {name} (协议: {protocol})", ) return {"message": "供应商已创建", "id": provider.id, "provider": provider_to_dict(provider)} @router.put("/providers/{id}") async def update_provider( id: str, name: str = Body(default=None), protocol: str = Body(default=None), api_key: str = Body(default=None), api_base_url: str = Body(default=None), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """更新模型供应商""" provider = db.query(ModelProvider).filter(ModelProvider.id == id).first() if not provider: raise HTTPException(status_code=404, detail="供应商不存在") if name is not None: provider.name = name if protocol is not None: valid_protocols = [p["id"] for p in PROTOCOLS] if protocol not in valid_protocols: raise HTTPException(status_code=400, detail=f"无效的协议类型") provider.protocol = protocol if api_key is not None: provider.api_key = api_key if api_base_url is not None: provider.api_base_url = api_base_url provider.updated_at = datetime.now() db.commit() db.refresh(provider) LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="update_provider", target_type="model_provider", target_id=id, detail=f"更新模型供应商 {provider.name}", ) return {"message": "供应商已更新", "provider": provider_to_dict(provider)} @router.delete("/providers/{id}") async def delete_provider( id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """删除模型供应商""" provider = db.query(ModelProvider).filter(ModelProvider.id == id).first() if not provider: raise HTTPException(status_code=404, detail="供应商不存在") # 检查是否有模型关联 model_count = db.query(func.count(AIModel.id)).filter( AIModel.provider_id == id ).scalar() if model_count > 0: raise HTTPException(status_code=400, detail=f"该供应商下有 {model_count} 个模型,请先删除模型") db.delete(provider) db.commit() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="delete_provider", target_type="model_provider", target_id=id, detail=f"删除模型供应商 {provider.name}", ) return {"message": "供应商已删除"} @router.post("/providers/{id}/fetch-models") async def fetch_models( id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取供应商可用模型列表(优先从真实API获取)""" provider = db.query(ModelProvider).filter(ModelProvider.id == id).first() if not provider: raise HTTPException(status_code=404, detail="供应商不存在") logger = logging.getLogger(__name__) logger.info(f"Fetching models for provider {provider.name} (protocol: {provider.protocol})") api_base = (provider.api_base_url or "").rstrip("/") api_key = provider.api_key or "" # 尝试从真实API获取模型列表(只要有 api_base 就尝试调用,api_key 可选) if api_base and provider.protocol in FETCHABLE_PROTOCOLS: try: async with httpx.AsyncClient(timeout=15) as client: headers = {} if api_key: headers["Authorization"] = f"Bearer {api_key}" resp = await client.get(f"{api_base}/v1/models", headers=headers) if resp.status_code == 200: data = resp.json() models_list = data.get("data", []) if models_list: result = [] for m in models_list: mid = m.get("id", "") if not mid: continue result.append({ "id": mid, "name": mid, "capabilities": ["text"], "pricing": "paid", }) logger.info(f"Successfully fetched {len(result)} models from provider API") return {"models": result, "source": "api"} else: logger.warning(f"Provider API returned empty model list") return {"models": [], "source": "api", "message": "API返回的模型列表为空"} elif resp.status_code == 401 or resp.status_code == 403: logger.warning(f"Provider API returned {resp.status_code}: auth error") return {"models": [], "source": "error", "message": "API认证失败,请检查API Key是否正确"} else: logger.warning(f"Provider API returned status {resp.status_code}: {resp.text[:200]}") return {"models": [], "source": "error", "message": f"API请求失败(HTTP {resp.status_code}),请检查API地址和密钥"} except httpx.TimeoutException: logger.warning(f"Timeout fetching models from provider API: {api_base}") return {"models": [], "source": "error", "message": "请求API超时,请检查API地址是否正确"} except Exception as e: logger.warning(f"Failed to fetch models from provider API: {e}") return {"models": [], "source": "error", "message": f"请求API失败: {str(e)}"} # 回退:返回固定模型列表(供无 API 的离线环境使用) logger.info(f"Using fallback model list for provider {provider.name}") return {"models": FETCHABLE_MODELS, "source": "fallback"} class AddModelsRequest(BaseModel): model_ids: List[str] @router.post("/providers/{id}/add-models") async def add_models( id: str, req: AddModelsRequest, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """批量添加模型到供应商""" provider = db.query(ModelProvider).filter(ModelProvider.id == id).first() if not provider: raise HTTPException(status_code=404, detail="供应商不存在") # 找到协议对应的 base_path protocol_info = next((p for p in PROTOCOLS if p["id"] == provider.protocol), None) base_path = protocol_info["base_path"] if protocol_info else "/v1" added = [] for model_id in req.model_ids: # 在 FETCHABLE_MODELS 中查找额外信息 model_info = next((m for m in FETCHABLE_MODELS if m["id"] == model_id), None) # 使用 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( AIModel.provider_id == id, AIModel.name == model_name ).first() if existing: continue # 自动生成 endpoint if provider.api_base_url: endpoint = f"{provider.api_base_url.rstrip('/')}{base_path}/chat/completions" else: endpoint = "" model = AIModel( id=str(uuid.uuid4()), provider_id=id, name=model_name, display_name=display_name_val, api_key=provider.api_key or "", endpoint=endpoint, capabilities=model_info.get("capabilities") if model_info else ["text"], timeout=60, status="free" if (model_info and model_info.get("pricing") == "free") else "paid", price_per_million_tokens=0, is_enabled=True, ) db.add(model) added.append(model_name) db.commit() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="add_models", target_type="model_provider", target_id=id, detail=f"为供应商 {provider.name} 批量添加 {len(added)} 个模型", ) return {"message": f"成功添加 {len(added)} 个模型", "added": added} # ============================================================ # 模型管理 # ============================================================ @router.get("/") async def get_models( provider_id: str = Query(None), status: str = Query(None, description="模型状态筛选: free/paid"), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取模型列表(管理员)""" query = db.query(AIModel) if provider_id: query = query.filter(AIModel.provider_id == provider_id) if status: query = query.filter(AIModel.status == status) total = query.count() models = query.order_by(AIModel.created_at.desc()).offset( (page - 1) * page_size ).limit(page_size).all() return { "total": total, "page": page, "page_size": page_size, "items": [model_to_dict(m) for m in models], } @router.post("/") async def create_model( data: ModelCreateRequest, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """添加模型""" # 验证供应商存在 provider = db.query(ModelProvider).filter(ModelProvider.id == data.provider_id).first() if not provider: raise HTTPException(status_code=404, detail="供应商不存在") model = AIModel( id=str(uuid.uuid4()), provider_id=data.provider_id, name=data.name, display_name=data.display_name, api_key=data.api_key, endpoint=data.endpoint, capabilities=data.capabilities, timeout=data.timeout, status=data.status, price_per_million_tokens=data.price_per_million_tokens, is_enabled=data.is_enabled, ) db.add(model) db.commit() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="create_model", target_type="model", target_id=model.id, detail=f"添加模型 {name}", ) return {"message": "模型已创建", "id": model.id} @router.put("/{id}") async def update_model( id: str, data: ModelUpdateRequest, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """更新模型""" model = db.query(AIModel).filter(AIModel.id == id).first() if not model: raise HTTPException(status_code=404, detail="模型不存在") if data.provider_id is not None: provider = db.query(ModelProvider).filter(ModelProvider.id == data.provider_id).first() if not provider: raise HTTPException(status_code=404, detail="供应商不存在") model.provider_id = data.provider_id if data.name is not None: model.name = data.name if data.display_name is not None: model.display_name = data.display_name if data.api_key is not None: model.api_key = data.api_key if data.endpoint is not None: model.endpoint = data.endpoint if data.capabilities is not None: model.capabilities = data.capabilities if data.timeout is not None: model.timeout = data.timeout if data.status is not None: model.status = data.status if data.price_per_million_tokens is not None: model.price_per_million_tokens = data.price_per_million_tokens if data.is_enabled is not None: model.is_enabled = data.is_enabled model.updated_at = datetime.now() db.commit() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="update_model", target_type="model", target_id=id, detail=f"更新模型 {model.name}", ) return {"message": "模型已更新"} @router.delete("/{id}") async def delete_model( id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """删除模型""" model = db.query(AIModel).filter(AIModel.id == id).first() if not model: raise HTTPException(status_code=404, detail="模型不存在") model_name = model.name db.delete(model) db.commit() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="delete_model", target_type="model", target_id=id, detail=f"删除模型 {model_name}", ) return {"message": "模型已删除"} @router.put("/{id}/toggle") async def toggle_model( id: str, is_enabled: bool = Query(...), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """开关模型状态""" model = db.query(AIModel).filter(AIModel.id == id).first() if not model: raise HTTPException(status_code=404, detail="模型不存在") model.is_enabled = is_enabled model.updated_at = datetime.now() db.commit() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="toggle_model", target_type="model", target_id=id, detail=f"将模型 {model.name} {'启用' if is_enabled else '禁用'}", ) return {"message": f"模型已{'启用' if is_enabled else '禁用'}"} @router.put("/{id}/set-default") async def set_default_model( id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """设为全局默认模型(取消其他模型的默认状态)""" model = db.query(AIModel).filter(AIModel.id == id).first() if not model: raise HTTPException(status_code=404, detail="模型不存在") # 取消所有模型的默认状态 db.query(AIModel).filter(AIModel.is_default == True).update({"is_default": False}) # 设置当前模型为默认 model.is_default = True model.updated_at = datetime.now() db.commit() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="set_default_model", target_type="model", target_id=id, detail=f"设置模型 {model.name} 为全局默认", ) return {"message": f"已将 {model.name} 设为全局默认模型"} @router.get("/stats") async def get_model_stats( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取模型调用统计""" total_calls = db.query(func.sum(AIModel.total_calls)).scalar() or 0 total_tokens = db.query(func.sum(AIModel.total_tokens)).scalar() or 0 return { "total_calls": int(total_calls), "total_tokens": int(total_tokens), } # ============================================================ # 按能力指定默认模型(供技能 / 应用 / IM Bot 自动匹配) # ============================================================ @router.get("/capability-defaults") async def get_capability_defaults_api( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取每个能力当前配置的默认模型,以及可选模型列表""" defaults = get_capability_defaults(db) models = db.query(AIModel).filter(AIModel.is_enabled == True).all() model_list = [{ "id": m.id, "name": m.name, "display_name": getattr(m, "display_name", None) or m.name, "provider_name": ( db.query(ModelProvider).filter(ModelProvider.id == m.provider_id).first().name if m.provider_id else "" ), "capabilities": m.capabilities or [], } for m in models] return { "capabilities": [ {**c, "color": CAPABILITY_COLORS.get(c["id"], "#6B7280")} for c in CAPABILITY_TYPES ], "models": model_list, "defaults": defaults, } class CapabilityDefaultRequest(BaseModel): capability: str model_id: Optional[str] = None @router.put("/capability-defaults") async def set_capability_default_api( data: CapabilityDefaultRequest, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """为某个能力设置 / 清除默认模型""" valid = {c["id"] for c in CAPABILITY_TYPES} if data.capability not in valid: raise HTTPException(status_code=400, detail=f"无效的能力类型: {data.capability}") if data.model_id: model = db.query(AIModel).filter(AIModel.id == data.model_id).first() if not model: raise HTTPException(status_code=404, detail="模型不存在") updated = set_capability_default(db, data.capability, data.model_id) LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="set_capability_default", target_type="model_capability", target_id=data.capability, detail=f"设置能力 {data.capability} 的默认模型为 {data.model_id or '(清除)'}", ) return {"message": "已更新能力默认模型", "defaults": updated} # ============================================================ # 模型连接测试 API (Phase 2) # ============================================================ def _detect_model_type(capabilities): """根据 capabilities 判断模型类型,返回测试类型。 优先级(从高到低):image > video > audio > asr > ocr > embedding > rerank > vision > thinking > tool > text """ if not capabilities: return "text" caps = [c.lower() for c in capabilities] if "image" in caps: return "image" if "video" in caps: return "video" if "audio" in caps: return "audio" if "asr" in caps or "speech" in caps: return "asr" if "ocr" in caps: return "ocr" if "embedding" in caps: return "embedding" if "rerank" in caps: return "rerank" if "vision" in caps: return "vision" if "thinking" in caps: return "thinking" if "tool" in caps: return "tool" 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") async def test_model_connection( model_id: str, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """ 测试指定模型的连接是否正常 支持多模态: text/image/audio/vision 自动检测模型类型并使用对应API测试 """ import time logger = logging.getLogger(__name__) logger.info(f"Testing connection for model_id: {model_id}") ai_model = db.query(AIModel).filter(AIModel.id == model_id).first() if not ai_model: raise HTTPException(status_code=404, detail="模型不存在") provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first() if not provider: raise HTTPException(status_code=404, detail="模型供应商不存在") api_key, base_url = _build_api_base(provider, ai_model) if not api_key: return { "success": False, "message": "未配置 API Key,请在模型或供应商设置中添加", "latency_ms": None, "model_type": "unknown", } if not base_url: return { "success": False, "message": "未配置 API 地址,请在模型或供应商设置中添加", "latency_ms": None, "model_type": "unknown", } model_type = _detect_model_type(ai_model.capabilities) timeout = ai_model.timeout or 60 headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } start = time.time() try: async with httpx.AsyncClient(timeout=timeout) as client: # ============================================================ # 文本模型 / 视觉模型 / OCR模型 / 思考模型 → chat/completions # ============================================================ if model_type in ("text", "vision", "ocr", "thinking", "tool"): chat_url = f"{base_url}/v1/chat/completions" payload = { "model": ai_model.name, "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5, } # 视觉 / OCR 模型附带图片测试(用 base64 data-URI,避免远端 URL 不可达) if model_type in ("vision", "ocr"): _test_png_b64 = ( "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAjUlEQVR4nO3PoRHAMADDwMzS/XdsJzAoUnQnYwH/eceesdv6c9uhAPShAPShAPSh3wDL0dUHoPsAdB+A7v0Ay9HVB6D7AHQfgO79AMvR1Qeg+wB0H4Du/QDL0dUHoPsAdB+A7v0Ay9HVB6D7AHQfgO79AMvR1Qeg+wB0H4Du/QDL0dUHoPsAdB+A7vWAD38HGZb2HO9/AAAAAElFTkSuQmCC" ) payload["messages"] = [{ "role": "user", "content": [ {"type": "text", "text": "What is in this image?"}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{_test_png_b64}"}}, ], }] payload["max_tokens"] = 64 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) # ============================================================ # 图像生成模型 → 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 { "success": True, "message": f"图像生成成功 ({model_type}),返回 {img_count} 张图片", "latency_ms": latency, "model_type": model_type, } else: return _build_error_response(resp, latency, model_type) # ============================================================ # 音频模型 → audio/speech (TTS) # ============================================================ elif model_type == "audio": audio_url = f"{base_url}/v1/audio/speech" payload = { "model": ai_model.name, "input": "hello", "voice": "alloy", } resp = await client.post(audio_url, json=payload, headers=headers) latency = int((time.time() - start) * 1000) if resp.status_code == 200: content_type = resp.headers.get("content-type", "") has_audio = "audio" in content_type or len(resp.content) > 100 return { "success": True, "message": f"音频服务连接成功 ({model_type}),响应类型: {content_type}", "latency_ms": latency, "model_type": model_type, } else: return _build_error_response(resp, latency, model_type) # ============================================================ # 嵌入向量模型 → embeddings # ============================================================ elif model_type == "embedding": embed_url = f"{base_url}/v1/embeddings" payload = { "model": ai_model.name, "input": "hello world", } resp = await client.post(embed_url, json=payload, headers=headers) latency = int((time.time() - start) * 1000) if resp.status_code == 200: data = resp.json() emb = data.get("data") or [] dim = len(emb[0].get("embedding", [])) if emb else 0 return { "success": True, "message": f"嵌入服务连接成功 ({model_type}),向量维度: {dim}", "latency_ms": latency, "model_type": model_type, } else: return _build_error_response(resp, latency, model_type) # ============================================================ # 重排序模型 → rerank # ============================================================ elif model_type == "rerank": rerank_url = f"{base_url}/v1/rerank" payload = { "model": ai_model.name, "query": "什么是人工智能", "documents": ["人工智能是计算机科学的一个分支", "今天天气晴朗"], } resp = await client.post(rerank_url, json=payload, headers=headers) latency = int((time.time() - start) * 1000) if resp.status_code == 200: data = resp.json() results = data.get("results") or data.get("data") or [] return { "success": True, "message": f"重排序服务连接成功 ({model_type}),返回 {len(results)} 条结果", "latency_ms": latency, "model_type": model_type, } else: return _build_error_response(resp, latency, model_type) # ============================================================ # 语音识别模型 → audio/transcriptions (ASR) # ============================================================ elif model_type == "asr": asr_url = f"{base_url}/v1/audio/transcriptions" # 一段极短的静音 WAV(base64),仅用于探活端点是否可达 silent_wav_b64 = ( "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQAAAAA=" ) files = { "file": ("test.wav", silent_wav_b64, "audio/wav"), "model": (None, ai_model.name), } asr_headers = {"Authorization": headers["Authorization"]} try: resp = await client.post(asr_url, files=files, headers=asr_headers) except Exception: # 某些实现要求 JSON 而非表单,回退尝试 JSON resp = await client.post( asr_url, json={"model": ai_model.name, "audio": silent_wav_b64}, headers=headers, ) latency = int((time.time() - start) * 1000) if resp.status_code == 200: return { "success": True, "message": f"语音识别服务连接成功 ({model_type})", "latency_ms": latency, "model_type": model_type, } elif resp.status_code in (400, 422): # 端点可达但音频数据无效(探活目的达成) return { "success": True, "message": f"语音识别端点可达 ({model_type}),返回 {resp.status_code}(需真实音频)", "latency_ms": latency, "model_type": model_type, } elif resp.status_code in (404, 405): # OpenAI transcriptions 路径不存在 → 可能是 MiMo 风格 # 的 /v1/chat/completions + input_audio(如 mimo-v2.5-asr) chat_url = f"{base_url}/v1/chat/completions" asr_payload = { "model": ai_model.name, "messages": [{ "role": "user", "content": [{ "type": "input_audio", "input_audio": { "data": f"data:audio/wav;base64,{silent_wav_b64}" }, }], }], "asr_options": {"language": "auto"}, "max_tokens": 5, } try: resp2 = await client.post(chat_url, json=asr_payload, headers=headers) except Exception: resp2 = resp latency2 = int((time.time() - start) * 1000) if resp2.status_code == 200: return { "success": True, "message": f"语音识别服务连接成功 ({model_type})", "latency_ms": latency2, "model_type": model_type, } elif resp2.status_code in (400, 422): return { "success": True, "message": f"语音识别端点可达 ({model_type}),返回 {resp2.status_code}(需真实音频)", "latency_ms": latency2, "model_type": model_type, } else: return _build_error_response(resp, latency, 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: return { "success": False, "message": f"未知模型类型: {model_type}", "latency_ms": int((time.time() - start) * 1000), "model_type": model_type, } except httpx.TimeoutException: latency = int((time.time() - start) * 1000) return { "success": False, "message": f"连接超时({timeout}s):请检查 API 地址是否正确,或服务是否可访问", "latency_ms": latency, "model_type": model_type, } except Exception as e: latency = int((time.time() - start) * 1000) logger.error(f"Model connection test failed: {e}") return { "success": False, "message": f"连接失败: {str(e)}", "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 status: str = "paid" price_per_million_tokens: float = 0.0 is_enabled: bool = True class UpdateModelRequest(BaseModel): name: Optional[str] = None api_key: Optional[str] = None endpoint: Optional[str] = None capabilities: Optional[List[str]] = None timeout: Optional[int] = None status: Optional[str] = None price_per_million_tokens: Optional[float] = None is_enabled: Optional[bool] = None from typing import Optional