Files
aiagent/backend/app/api/model_configs.py
renjianbo 769dcc828e feat: add Kimi (moonshot) and full provider support in model configs
- 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>
2026-07-26 18:33:51 +08:00

327 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
模型配置管理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)}"
}