- backend: expand valid_providers from 4 to full frontend provider list (module-level VALID_PROVIDERS constant, used by create + update) - backend: test endpoint gains generic OpenAI-compatible branch (moonshot/qwen/zhipu/siliconflow etc. via AsyncOpenAI direct call) - frontend: Kimi preset models add kimi-k2-0905/0711/turbo + kimi-latest alongside legacy moonshot-v1 series Verified: moonshot config create 201 (was 400), test endpoint reaches api.moonshot.cn and returns provider auth error with dummy key. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
327 lines
10 KiB
Python
327 lines
10 KiB
Python
"""
|
||
模型配置管理API
|
||
"""
|
||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||
from sqlalchemy.orm import Session
|
||
from pydantic import BaseModel
|
||
from typing import List, Optional
|
||
from datetime import datetime
|
||
import logging
|
||
from app.core.database import get_db
|
||
from app.models.model_config import ModelConfig
|
||
from app.api.auth import get_current_user
|
||
from app.api.deps import require_workspace_admin, WorkspaceContext, get_current_workspace_id
|
||
from app.models.user import User
|
||
from app.core.exceptions import NotFoundError, ValidationError, ConflictError
|
||
from app.services.encryption_service import EncryptionService
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(
|
||
prefix="/api/v1/model-configs",
|
||
tags=["model-configs"],
|
||
responses={
|
||
401: {"description": "未授权"},
|
||
404: {"description": "资源不存在"},
|
||
400: {"description": "请求参数错误"},
|
||
500: {"description": "服务器内部错误"}
|
||
}
|
||
)
|
||
|
||
|
||
# 与前端 ModelConfigs.vue 的提供商列表保持一致。
|
||
# anthropic/google/cohere 为原生 API;其余均走 OpenAI 兼容端点。
|
||
VALID_PROVIDERS = [
|
||
'openai', 'deepseek', 'anthropic', 'local',
|
||
'qwen', 'zhipu', 'baidu', 'moonshot', 'bytedance', 'minimax',
|
||
'siliconflow', 'xunfei', 'hunyuan', 'yi', 'baichuan', 'openrouter',
|
||
'cohere', 'xai', 'together', 'fireworks', 'perplexity', 'deepinfra',
|
||
'groq', 'google', 'mistral',
|
||
]
|
||
|
||
# 可用 OpenAI 兼容协议直连测试的 provider(anthropic/google/cohere 协议不同,暂除外)
|
||
OPENAI_COMPATIBLE_PROVIDERS = {
|
||
'local', 'qwen', 'zhipu', 'baidu', 'moonshot', 'bytedance', 'minimax',
|
||
'siliconflow', 'xunfei', 'hunyuan', 'yi', 'baichuan', 'openrouter',
|
||
'xai', 'together', 'fireworks', 'perplexity', 'deepinfra', 'groq', 'mistral',
|
||
}
|
||
|
||
|
||
class ModelConfigCreate(BaseModel):
|
||
"""模型配置创建模型"""
|
||
name: str
|
||
provider: str # 见 VALID_PROVIDERS
|
||
model_name: str
|
||
api_key: str
|
||
base_url: Optional[str] = None
|
||
|
||
|
||
class ModelConfigUpdate(BaseModel):
|
||
"""模型配置更新模型"""
|
||
name: Optional[str] = None
|
||
provider: Optional[str] = None
|
||
model_name: Optional[str] = None
|
||
api_key: Optional[str] = None
|
||
base_url: Optional[str] = None
|
||
|
||
|
||
class ModelConfigResponse(BaseModel):
|
||
"""模型配置响应模型"""
|
||
id: str
|
||
name: str
|
||
provider: str
|
||
model_name: str
|
||
base_url: Optional[str]
|
||
user_id: str
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
class Config:
|
||
from_attributes = True
|
||
|
||
|
||
@router.get("", response_model=List[ModelConfigResponse])
|
||
async def get_model_configs(
|
||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||
limit: int = Query(100, ge=1, le=100, description="每页记录数"),
|
||
provider: Optional[str] = Query(None, description="提供商筛选"),
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
workspace_id: str = Depends(get_current_workspace_id),
|
||
):
|
||
"""
|
||
获取模型配置列表
|
||
|
||
支持分页和提供商筛选
|
||
"""
|
||
query = db.query(ModelConfig).filter(
|
||
ModelConfig.user_id == current_user.id,
|
||
ModelConfig.workspace_id == workspace_id,
|
||
)
|
||
|
||
# 筛选:按提供商筛选
|
||
if provider:
|
||
query = query.filter(ModelConfig.provider == provider)
|
||
|
||
# 排序和分页
|
||
configs = query.order_by(ModelConfig.created_at.desc()).offset(skip).limit(limit).all()
|
||
return configs
|
||
|
||
|
||
@router.post("", response_model=ModelConfigResponse, status_code=status.HTTP_201_CREATED)
|
||
async def create_model_config(
|
||
config_data: ModelConfigCreate,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
workspace_id: str = Depends(get_current_workspace_id),
|
||
):
|
||
"""
|
||
创建模型配置
|
||
|
||
注意:API密钥会加密存储
|
||
"""
|
||
# 验证提供商
|
||
if config_data.provider not in VALID_PROVIDERS:
|
||
raise ValidationError(f"不支持的提供商: {config_data.provider}")
|
||
|
||
# 检查名称是否重复
|
||
existing_config = db.query(ModelConfig).filter(
|
||
ModelConfig.name == config_data.name,
|
||
ModelConfig.user_id == current_user.id,
|
||
ModelConfig.workspace_id == workspace_id,
|
||
).first()
|
||
if existing_config:
|
||
raise ConflictError(f"模型配置名称 '{config_data.name}' 已存在")
|
||
|
||
# 创建模型配置
|
||
# API密钥加密存储
|
||
encrypted_api_key = EncryptionService.encrypt(config_data.api_key)
|
||
model_config = ModelConfig(
|
||
name=config_data.name,
|
||
provider=config_data.provider,
|
||
model_name=config_data.model_name,
|
||
api_key=encrypted_api_key,
|
||
base_url=config_data.base_url,
|
||
user_id=current_user.id,
|
||
workspace_id=workspace_id,
|
||
)
|
||
db.add(model_config)
|
||
db.commit()
|
||
db.refresh(model_config)
|
||
|
||
logger.info(f"用户 {current_user.username} 创建了模型配置: {model_config.name} ({model_config.id})")
|
||
return model_config
|
||
|
||
|
||
@router.get("/{config_id}", response_model=ModelConfigResponse)
|
||
async def get_model_config(
|
||
config_id: str,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user)
|
||
):
|
||
"""
|
||
获取模型配置详情
|
||
|
||
注意:API密钥不会返回
|
||
"""
|
||
config = db.query(ModelConfig).filter(
|
||
ModelConfig.id == config_id,
|
||
ModelConfig.user_id == current_user.id
|
||
).first()
|
||
|
||
if not config:
|
||
raise NotFoundError(f"模型配置不存在: {config_id}")
|
||
|
||
return config
|
||
|
||
|
||
@router.put("/{config_id}", response_model=ModelConfigResponse)
|
||
async def update_model_config(
|
||
config_id: str,
|
||
config_data: ModelConfigUpdate,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user)
|
||
):
|
||
"""
|
||
更新模型配置
|
||
"""
|
||
config = db.query(ModelConfig).filter(
|
||
ModelConfig.id == config_id,
|
||
ModelConfig.user_id == current_user.id
|
||
).first()
|
||
|
||
if not config:
|
||
raise NotFoundError(f"模型配置不存在: {config_id}")
|
||
|
||
# 更新字段
|
||
if config_data.name is not None:
|
||
# 检查名称是否重复(排除当前配置)
|
||
existing_config = db.query(ModelConfig).filter(
|
||
ModelConfig.name == config_data.name,
|
||
ModelConfig.user_id == current_user.id,
|
||
ModelConfig.id != config_id
|
||
).first()
|
||
if existing_config:
|
||
raise ConflictError(f"模型配置名称 '{config_data.name}' 已存在")
|
||
config.name = config_data.name
|
||
|
||
if config_data.provider is not None:
|
||
if config_data.provider not in VALID_PROVIDERS:
|
||
raise ValidationError(f"不支持的提供商: {config_data.provider}")
|
||
config.provider = config_data.provider
|
||
|
||
if config_data.model_name is not None:
|
||
config.model_name = config_data.model_name
|
||
|
||
if config_data.api_key is not None:
|
||
# API密钥加密存储
|
||
config.api_key = EncryptionService.encrypt(config_data.api_key)
|
||
|
||
if config_data.base_url is not None:
|
||
config.base_url = config_data.base_url
|
||
|
||
db.commit()
|
||
db.refresh(config)
|
||
|
||
logger.info(f"用户 {current_user.username} 更新了模型配置: {config.name} ({config.id})")
|
||
return config
|
||
|
||
|
||
@router.delete("/{config_id}", status_code=status.HTTP_200_OK)
|
||
async def delete_model_config(
|
||
config_id: str,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
_ws_admin: WorkspaceContext = Depends(require_workspace_admin),
|
||
):
|
||
"""
|
||
删除模型配置(仅工作区管理员和平台管理员可操作)
|
||
"""
|
||
config = db.query(ModelConfig).filter(
|
||
ModelConfig.id == config_id,
|
||
ModelConfig.user_id == current_user.id
|
||
).first()
|
||
|
||
if not config:
|
||
raise NotFoundError(f"模型配置不存在: {config_id}")
|
||
|
||
config_name = config.name
|
||
db.delete(config)
|
||
db.commit()
|
||
|
||
logger.info(f"用户 {current_user.username} 删除了模型配置: {config_name} ({config_id})")
|
||
return {"message": "模型配置已删除"}
|
||
|
||
|
||
@router.post("/{config_id}/test", status_code=status.HTTP_200_OK)
|
||
async def test_model_config(
|
||
config_id: str,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user)
|
||
):
|
||
"""
|
||
测试模型配置连接
|
||
|
||
尝试调用模型API验证配置是否正确
|
||
"""
|
||
config = db.query(ModelConfig).filter(
|
||
ModelConfig.id == config_id,
|
||
ModelConfig.user_id == current_user.id
|
||
).first()
|
||
|
||
if not config:
|
||
raise NotFoundError(f"模型配置不存在: {config_id}")
|
||
|
||
try:
|
||
# 根据提供商测试连接
|
||
from app.services.llm_service import llm_service
|
||
|
||
# 解密API密钥用于测试
|
||
decrypted_api_key = EncryptionService.decrypt(config.api_key)
|
||
|
||
if config.provider == 'openai':
|
||
result = await llm_service.call_openai(
|
||
prompt="test",
|
||
model=config.model_name,
|
||
api_key=decrypted_api_key,
|
||
base_url=config.base_url
|
||
)
|
||
elif config.provider == 'deepseek':
|
||
result = await llm_service.call_deepseek(
|
||
prompt="test",
|
||
model=config.model_name,
|
||
api_key=decrypted_api_key,
|
||
base_url=config.base_url
|
||
)
|
||
elif config.provider in OPENAI_COMPATIBLE_PROVIDERS:
|
||
# OpenAI 兼容协议通用测试(moonshot/qwen/zhipu/siliconflow 等)
|
||
if not config.base_url:
|
||
return {
|
||
"status": "error",
|
||
"message": f"提供商 {config.provider} 需要填写 API 地址(base_url)"
|
||
}
|
||
from openai import AsyncOpenAI
|
||
client = AsyncOpenAI(api_key=decrypted_api_key, base_url=config.base_url)
|
||
await client.chat.completions.create(
|
||
model=config.model_name,
|
||
messages=[{"role": "user", "content": "test"}],
|
||
max_tokens=5,
|
||
timeout=30,
|
||
)
|
||
else:
|
||
return {
|
||
"status": "warning",
|
||
"message": f"提供商 {config.provider} 的测试功能暂未实现"
|
||
}
|
||
|
||
return {
|
||
"status": "success",
|
||
"message": "模型配置测试成功"
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"模型配置测试失败: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": f"模型配置测试失败: {str(e)}"
|
||
}
|