250 lines
10 KiB
Python
250 lines
10 KiB
Python
import math
|
||
import uuid
|
||
from datetime import datetime, timezone
|
||
|
||
from fastapi import Depends, HTTPException
|
||
from sqlalchemy import select, func, update
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.module.shared.llm import LLMFactory
|
||
from app.core.logging import get_logger
|
||
from app.db.datascope import DataScopeHandle, SYSTEM_USER
|
||
from app.db.models.models import Models
|
||
from app.db.session import get_db
|
||
from app.module.shared.schema import PaginatedData
|
||
from app.module.system.schema.models import (
|
||
CreateModelRequest,
|
||
QueryModelRequest,
|
||
ModelsResponse,
|
||
ProviderItem,
|
||
)
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
# 固定厂商列表
|
||
PROVIDERS = [
|
||
ProviderItem(provider="ModelEngine", baseUrl="http://nginxservice/open/router/v1"),
|
||
ProviderItem(provider="Ollama", baseUrl="http://localhost:11434"),
|
||
ProviderItem(provider="OpenAI", baseUrl="https://api.openai.com/v1"),
|
||
ProviderItem(provider="DeepSeek", baseUrl="https://api.deepseek.com/v1"),
|
||
ProviderItem(provider="火山方舟", baseUrl="https://ark.cn-beijing.volces.com/api/v3"),
|
||
ProviderItem(provider="阿里云百炼", baseUrl="https://dashscope.aliyuncs.com/compatible-mode/v1"),
|
||
ProviderItem(provider="硅基流动", baseUrl="https://api.siliconflow.cn/v1"),
|
||
ProviderItem(provider="智谱AI", baseUrl="https://open.bigmodel.cn/api/paas/v4"),
|
||
ProviderItem(provider="自定义模型", baseUrl=""),
|
||
]
|
||
|
||
|
||
def _orm_to_response(row: Models) -> ModelsResponse:
|
||
return ModelsResponse(
|
||
id=row.id,
|
||
modelName=row.model_name,
|
||
provider=row.provider,
|
||
baseUrl=row.base_url,
|
||
apiKey=row.api_key or "",
|
||
type=row.type or "CHAT",
|
||
isEnabled=bool(row.is_enabled) if row.is_enabled is not None else True,
|
||
isDefault=bool(row.is_default) if row.is_default is not None else False,
|
||
createdAt=row.created_at,
|
||
updatedAt=row.updated_at,
|
||
createdBy=row.created_by,
|
||
updatedBy=row.updated_by,
|
||
)
|
||
|
||
|
||
class ModelsService:
|
||
"""模型配置服务:db 通过 FastAPI Depends(get_db) 注入,不在路由中传递。"""
|
||
|
||
def __init__(self, db: AsyncSession = Depends(get_db)):
|
||
self.db = db
|
||
|
||
async def get_providers(self) -> list[ProviderItem]:
|
||
"""返回固定厂商列表,与 Java getProviders() 一致。"""
|
||
return list(PROVIDERS)
|
||
|
||
async def get_models(self, q: QueryModelRequest) -> PaginatedData[ModelsResponse]:
|
||
"""分页查询,支持 provider/type/isEnabled/isDefault;排除已删除;page 从 0 开始。"""
|
||
query = select(Models).where(
|
||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||
)
|
||
if q.provider is not None and q.provider != "":
|
||
query = query.where(Models.provider == q.provider)
|
||
if q.type is not None:
|
||
query = query.where(Models.type == q.type.value)
|
||
if q.isEnabled is not None:
|
||
query = query.where(Models.is_enabled == q.isEnabled)
|
||
if q.isDefault is not None:
|
||
query = query.where(Models.is_default == q.isDefault)
|
||
|
||
total = (await self.db.execute(select(func.count()).select_from(query.subquery()))).scalar_one()
|
||
size = max(1, min(500, q.size))
|
||
offset = max(0, q.page) * size
|
||
rows = (
|
||
await self.db.execute(
|
||
query.order_by(Models.created_at.desc()).offset(offset).limit(size)
|
||
)
|
||
).scalars().all()
|
||
total_pages = math.ceil(total / size) if total else 0
|
||
current = max(0, q.page) + 1
|
||
return PaginatedData(
|
||
page=current,
|
||
size=size,
|
||
total_elements=total,
|
||
total_pages=total_pages,
|
||
content=[_orm_to_response(r) for r in rows],
|
||
)
|
||
|
||
async def get_model_detail(self, model_id: str) -> ModelsResponse:
|
||
"""获取模型详情,已删除或不存在则 404。"""
|
||
query = select(Models).where(Models.id == model_id).where(
|
||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||
)
|
||
r = (await self.db.execute(query)).scalar_one_or_none()
|
||
if not r:
|
||
raise HTTPException(status_code=404, detail="模型配置不存在")
|
||
return _orm_to_response(r)
|
||
|
||
async def create_model(self, req: CreateModelRequest) -> ModelsResponse:
|
||
"""创建模型:健康检查后 saveAndSetDefault;isEnabled 恒为 True。
|
||
若 uk_model_provider 已存在且 is_deleted=True,则恢复并更新相关字段。"""
|
||
try:
|
||
LLMFactory.check_health(req.modelName, req.baseUrl, req.apiKey, req.type.value)
|
||
except Exception as e:
|
||
logger.error("Model health check failed: model=%s type=%s err=%s", req.modelName, req.type, e)
|
||
raise HTTPException(status_code=400, detail="模型健康检查失败") from e
|
||
|
||
# 检查 uk_model_provider (model_name, provider, created_by) 是否存在且已删除
|
||
effective_user = (DataScopeHandle.get_user_info() or "").strip() or SYSTEM_USER
|
||
created_by_match = (
|
||
(Models.created_by == effective_user)
|
||
| ((Models.created_by.is_(None)) & (effective_user == SYSTEM_USER))
|
||
)
|
||
deleted_row = (
|
||
await self.db.execute(
|
||
select(Models).where(
|
||
Models.model_name == req.modelName,
|
||
Models.provider == req.provider,
|
||
created_by_match,
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
|
||
if deleted_row and deleted_row.is_deleted:
|
||
# 恢复并更新
|
||
entity = deleted_row
|
||
entity.base_url = req.baseUrl
|
||
entity.api_key = req.apiKey or ""
|
||
entity.type = req.type.value
|
||
entity.is_enabled = True
|
||
entity.is_deleted = False
|
||
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||
|
||
want_default = req.isDefault if req.isDefault is not None else False
|
||
if want_default:
|
||
await self.db.execute(
|
||
update(Models)
|
||
.where(Models.type == req.type.value, Models.is_default == True)
|
||
.values(is_default=False)
|
||
)
|
||
entity.is_default = want_default
|
||
|
||
self.db.add(entity)
|
||
await self.db.commit()
|
||
await self.db.refresh(entity)
|
||
return _orm_to_response(entity)
|
||
|
||
# 正常创建
|
||
existing = (
|
||
await self.db.execute(
|
||
select(Models).where(
|
||
(Models.is_deleted == False) | (Models.is_deleted.is_(None)),
|
||
Models.type == req.type.value,
|
||
Models.is_default == True,
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
|
||
is_default: bool
|
||
if existing is None:
|
||
is_default = True
|
||
else:
|
||
await self.db.execute(
|
||
update(Models)
|
||
.where(Models.type == req.type.value, Models.is_default == True)
|
||
.values(is_default=False)
|
||
)
|
||
is_default = req.isDefault if req.isDefault is not None else False
|
||
|
||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||
entity = Models(
|
||
id=str(uuid.uuid4()),
|
||
model_name=req.modelName,
|
||
provider=req.provider,
|
||
base_url=req.baseUrl,
|
||
api_key=req.apiKey or "",
|
||
type=req.type.value,
|
||
is_enabled=True,
|
||
is_default=is_default,
|
||
is_deleted=False,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(entity)
|
||
await self.db.commit()
|
||
await self.db.refresh(entity)
|
||
return _orm_to_response(entity)
|
||
|
||
async def update_model(self, model_id: str, req: CreateModelRequest) -> ModelsResponse:
|
||
"""更新模型:存在性校验、健康检查后 updateAndSetDefault;isEnabled 恒为 True。"""
|
||
res = await self.db.execute(
|
||
select(Models).where(Models.id == model_id).where(
|
||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||
)
|
||
)
|
||
entity = res.scalar_one_or_none()
|
||
if not entity:
|
||
raise HTTPException(status_code=404, detail="模型配置不存在")
|
||
|
||
try:
|
||
LLMFactory.check_health(req.modelName, req.baseUrl, req.apiKey, req.type.value)
|
||
except Exception as e:
|
||
logger.error("Model health check failed: model=%s type=%s err=%s", req.modelName, req.type, e)
|
||
raise HTTPException(status_code=400, detail="模型健康检查失败") from e
|
||
|
||
entity.model_name = req.modelName
|
||
entity.provider = req.provider
|
||
entity.base_url = req.baseUrl
|
||
entity.api_key = req.apiKey or ""
|
||
entity.type = req.type.value
|
||
entity.is_enabled = True
|
||
|
||
want_default = req.isDefault if req.isDefault is not None else False
|
||
if (entity.is_default is not True) and want_default:
|
||
await self.db.execute(
|
||
update(Models)
|
||
.where(Models.type == req.type.value, Models.is_default == True)
|
||
.values(is_default=False)
|
||
)
|
||
entity.is_default = want_default
|
||
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||
|
||
await self.db.commit()
|
||
await self.db.refresh(entity)
|
||
return _orm_to_response(entity)
|
||
|
||
async def delete_model(self, model_id: str) -> None:
|
||
"""软删除模型配置。"""
|
||
entity = (
|
||
await self.db.execute(
|
||
select(Models).where(Models.id == model_id).where(
|
||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
if not entity:
|
||
raise HTTPException(status_code=404, detail="模型配置不存在")
|
||
entity.is_deleted = True
|
||
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||
await self.db.commit()
|
||
await self.db.refresh(entity)
|