Implement unified error handling and response format in FastAPI application; add LLM route and update health check response structure.
This commit is contained in:
1 parent
e9edba092b
commit
0e4632efcd
7 files changed
+607
-13
No files matched your search
@@ -2,9 +2,10 @@
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.routes import film, health
|
||||
from app.api.v1.routes import film, health, llm
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(health.router, tags=["health"])
|
||||
router.include_router(film.router, prefix="/film", tags=["film"])
|
||||
router.include_router(llm.router, prefix="/llm", tags=["llm"])
|
||||
@@ -15,6 +15,7 @@ from app.core.skills_runtime import (
|
||||
FilmShotlistResult,
|
||||
)
|
||||
from app.dependencies import get_llm
|
||||
from app.schemas.common import ApiResponse, success_response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -50,14 +51,14 @@ class ShotlistExtractRequest(BaseModel):
|
||||
|
||||
@router.post(
|
||||
"/extract/entities",
|
||||
response_model=FilmEntityExtractionResult,
|
||||
response_model=ApiResponse[FilmEntityExtractionResult],
|
||||
summary="关键信息抽取",
|
||||
description="从小说文本中抽取人物、地点、道具,忠实原文、可追溯证据。",
|
||||
)
|
||||
def extract_entities(
|
||||
body: EntityExtractRequest,
|
||||
llm: Runnable = Depends(get_llm),
|
||||
) -> FilmEntityExtractionResult:
|
||||
) -> ApiResponse[FilmEntityExtractionResult]:
|
||||
"""FilmEntityExtractor:人物/地点/道具抽取。"""
|
||||
extractor = FilmEntityExtractor(llm)
|
||||
extractor.load_skill("film_entity_extractor")
|
||||
@@ -65,25 +66,26 @@ def extract_entities(
|
||||
[{"chunk_id": c.chunk_id, "text": c.text} for c in body.chunks],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
return extractor.extract(
|
||||
result = extractor.extract(
|
||||
{
|
||||
"source_id": body.source_id,
|
||||
"language": body.language or "zh",
|
||||
"chunks_json": chunks_json,
|
||||
}
|
||||
)
|
||||
return success_response(result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/extract/shotlist",
|
||||
response_model=FilmShotlistResult,
|
||||
response_model=ApiResponse[FilmShotlistResult],
|
||||
summary="分镜抽取",
|
||||
description="将小说片段转为可拍镜头表(景别/机位/运镜/转场/VFX)。",
|
||||
)
|
||||
def extract_shotlist(
|
||||
body: ShotlistExtractRequest,
|
||||
llm: Runnable = Depends(get_llm),
|
||||
) -> FilmShotlistResult:
|
||||
) -> ApiResponse[FilmShotlistResult]:
|
||||
"""FilmShotlistStoryboarder:场景/镜头/转场抽取。"""
|
||||
storyboarder = FilmShotlistStoryboarder(llm)
|
||||
storyboarder.load_skill("film_shotlist")
|
||||
@@ -91,7 +93,7 @@ def extract_shotlist(
|
||||
[{"chunk_id": c.chunk_id, "text": c.text} for c in body.chunks],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
return storyboarder.extract(
|
||||
result = storyboarder.extract(
|
||||
{
|
||||
"source_id": body.source_id,
|
||||
"source_title": body.source_title or "",
|
||||
@@ -99,3 +101,4 @@ def extract_shotlist(
|
||||
"chunks_json": chunks_json,
|
||||
}
|
||||
)
|
||||
return success_response(result)
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.schemas.common import ApiResponse, success_response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def v1_health():
|
||||
return {"status": "ok", "version": "v1"}
|
||||
@router.get("/health", response_model=ApiResponse[dict])
|
||||
async def v1_health() -> ApiResponse[dict]:
|
||||
return success_response(data={"status": "ok", "version": "v1"})
|
||||
@@ -0,0 +1,350 @@
|
||||
"""LLM 相关基础配置的 CRUD 接口:Provider / Model / ModelSettings。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db
|
||||
from app.models.llm import Model, ModelCategoryKey, ModelSettings, Provider
|
||||
from app.schemas.common import ApiResponse, PaginatedData, success_response, paginated_response
|
||||
from app.schemas.llm import (
|
||||
ModelCreate,
|
||||
ModelRead,
|
||||
ModelSettingsRead,
|
||||
ModelSettingsUpdate,
|
||||
ModelUpdate,
|
||||
ProviderCreate,
|
||||
ProviderRead,
|
||||
ProviderUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 列表排序允许的字段(避免注入)
|
||||
PROVIDER_ORDER_FIELDS = {"name", "created_at", "updated_at"}
|
||||
MODEL_ORDER_FIELDS = {"name", "category", "created_at", "updated_at"}
|
||||
DEFAULT_PAGE_SIZE = 10
|
||||
MAX_PAGE_SIZE = 100
|
||||
|
||||
|
||||
# ---------- Provider ----------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/providers",
|
||||
response_model=ApiResponse[PaginatedData[ProviderRead]],
|
||||
summary="列出模型供应商(分页)",
|
||||
)
|
||||
async def list_providers(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
q: str | None = Query(None, description="关键字,过滤 name/description"),
|
||||
order: str | None = Query(None, description="排序字段:name, created_at, updated_at"),
|
||||
is_desc: bool = Query(False, description="是否倒序"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE, description="每页条数"),
|
||||
) -> ApiResponse[PaginatedData[ProviderRead]]:
|
||||
stmt = select(Provider)
|
||||
count_stmt = select(func.count()).select_from(Provider)
|
||||
if q and q.strip():
|
||||
q_pattern = f"%{q.strip()}%"
|
||||
stmt = stmt.where(
|
||||
Provider.name.ilike(q_pattern) | Provider.description.ilike(q_pattern)
|
||||
)
|
||||
count_stmt = count_stmt.where(
|
||||
Provider.name.ilike(q_pattern) | Provider.description.ilike(q_pattern)
|
||||
)
|
||||
order_col = order if order and order in PROVIDER_ORDER_FIELDS else "created_at"
|
||||
order_attr = getattr(Provider, order_col)
|
||||
stmt = stmt.order_by(order_attr.desc() if is_desc else order_attr.asc())
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar() or 0
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(stmt)
|
||||
providers: Sequence[Provider] = result.scalars().all()
|
||||
items = [ProviderRead.model_validate(p) for p in providers]
|
||||
return paginated_response(items, page=page, page_size=page_size, total=total)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/providers",
|
||||
response_model=ApiResponse[ProviderRead],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="创建模型供应商",
|
||||
)
|
||||
async def create_provider(
|
||||
body: ProviderCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[ProviderRead]:
|
||||
exists = await db.get(Provider, body.id)
|
||||
if exists is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Provider with id={body.id} already exists",
|
||||
)
|
||||
provider = Provider(
|
||||
id=body.id,
|
||||
name=body.name,
|
||||
base_url=body.base_url,
|
||||
api_key=body.api_key,
|
||||
api_secret=body.api_secret,
|
||||
description=body.description,
|
||||
status=body.status,
|
||||
created_by=body.created_by,
|
||||
)
|
||||
db.add(provider)
|
||||
await db.flush()
|
||||
await db.refresh(provider)
|
||||
return success_response(ProviderRead.model_validate(provider), code=201)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/providers/{provider_id}",
|
||||
response_model=ApiResponse[ProviderRead],
|
||||
summary="获取单个模型供应商",
|
||||
)
|
||||
async def get_provider(
|
||||
provider_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[ProviderRead]:
|
||||
provider = await db.get(Provider, provider_id)
|
||||
if provider is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Provider not found")
|
||||
return success_response(ProviderRead.model_validate(provider))
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/providers/{provider_id}",
|
||||
response_model=ApiResponse[ProviderRead],
|
||||
summary="更新模型供应商",
|
||||
)
|
||||
async def update_provider(
|
||||
provider_id: str,
|
||||
body: ProviderUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[ProviderRead]:
|
||||
provider = await db.get(Provider, provider_id)
|
||||
if provider is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Provider not found")
|
||||
|
||||
update_data = body.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(provider, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(provider)
|
||||
return success_response(ProviderRead.model_validate(provider))
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/providers/{provider_id}",
|
||||
response_model=ApiResponse[None],
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="删除模型供应商",
|
||||
)
|
||||
async def delete_provider(
|
||||
provider_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[None]:
|
||||
provider = await db.get(Provider, provider_id)
|
||||
if provider is None:
|
||||
return success_response(None)
|
||||
await db.delete(provider)
|
||||
await db.flush()
|
||||
return success_response(None)
|
||||
|
||||
|
||||
# ---------- Model ----------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/models",
|
||||
response_model=ApiResponse[PaginatedData[ModelRead]],
|
||||
summary="列出模型(分页)",
|
||||
)
|
||||
async def list_models(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
provider_id: str | None = Query(None, description="按供应商过滤"),
|
||||
category: ModelCategoryKey | None = Query(None, description="按模型类别过滤"),
|
||||
q: str | None = Query(None, description="关键字,过滤 name/description"),
|
||||
order: str | None = Query(None, description="排序字段:name, category, created_at, updated_at"),
|
||||
is_desc: bool = Query(False, description="是否倒序"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE, description="每页条数"),
|
||||
) -> ApiResponse[PaginatedData[ModelRead]]:
|
||||
stmt = select(Model)
|
||||
count_stmt = select(func.count()).select_from(Model)
|
||||
if provider_id is not None:
|
||||
stmt = stmt.where(Model.provider_id == provider_id)
|
||||
count_stmt = count_stmt.where(Model.provider_id == provider_id)
|
||||
if category is not None:
|
||||
stmt = stmt.where(Model.category == category)
|
||||
count_stmt = count_stmt.where(Model.category == category)
|
||||
if q and q.strip():
|
||||
q_pattern = f"%{q.strip()}%"
|
||||
stmt = stmt.where(Model.name.ilike(q_pattern) | Model.description.ilike(q_pattern))
|
||||
count_stmt = count_stmt.where(
|
||||
Model.name.ilike(q_pattern) | Model.description.ilike(q_pattern)
|
||||
)
|
||||
order_col = order if order and order in MODEL_ORDER_FIELDS else "created_at"
|
||||
order_attr = getattr(Model, order_col)
|
||||
stmt = stmt.order_by(order_attr.desc() if is_desc else order_attr.asc())
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar() or 0
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(stmt)
|
||||
models: Sequence[Model] = result.scalars().all()
|
||||
items = [ModelRead.model_validate(m) for m in models]
|
||||
return paginated_response(items, page=page, page_size=page_size, total=total)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/models",
|
||||
response_model=ApiResponse[ModelRead],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="创建模型",
|
||||
)
|
||||
async def create_model(
|
||||
body: ModelCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[ModelRead]:
|
||||
exists = await db.get(Model, body.id)
|
||||
if exists is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Model with id={body.id} already exists",
|
||||
)
|
||||
|
||||
# 确保 provider 存在
|
||||
provider = await db.get(Provider, body.provider_id)
|
||||
if provider is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Provider not found")
|
||||
|
||||
model = Model(
|
||||
id=body.id,
|
||||
name=body.name,
|
||||
category=body.category,
|
||||
provider_id=body.provider_id,
|
||||
params=body.params,
|
||||
description=body.description,
|
||||
is_default=body.is_default,
|
||||
created_by=body.created_by,
|
||||
)
|
||||
db.add(model)
|
||||
await db.flush()
|
||||
await db.refresh(model)
|
||||
return success_response(ModelRead.model_validate(model), code=201)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/models/{model_id}",
|
||||
response_model=ApiResponse[ModelRead],
|
||||
summary="获取单个模型",
|
||||
)
|
||||
async def get_model(
|
||||
model_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[ModelRead]:
|
||||
model = await db.get(Model, model_id)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Model not found")
|
||||
return success_response(ModelRead.model_validate(model))
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/models/{model_id}",
|
||||
response_model=ApiResponse[ModelRead],
|
||||
summary="更新模型",
|
||||
)
|
||||
async def update_model(
|
||||
model_id: str,
|
||||
body: ModelUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[ModelRead]:
|
||||
model = await db.get(Model, model_id)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Model not found")
|
||||
|
||||
update_data = body.model_dump(exclude_unset=True)
|
||||
|
||||
# 如果更新 provider_id,需要校验新 provider 是否存在
|
||||
new_provider_id = update_data.get("provider_id")
|
||||
if new_provider_id is not None:
|
||||
provider = await db.get(Provider, new_provider_id)
|
||||
if provider is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Provider not found")
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(model, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(model)
|
||||
return success_response(ModelRead.model_validate(model))
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/models/{model_id}",
|
||||
response_model=ApiResponse[None],
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="删除模型",
|
||||
)
|
||||
async def delete_model(
|
||||
model_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[None]:
|
||||
model = await db.get(Model, model_id)
|
||||
if model is None:
|
||||
return success_response(None)
|
||||
await db.delete(model)
|
||||
await db.flush()
|
||||
return success_response(None)
|
||||
|
||||
|
||||
# ---------- ModelSettings(单例) ----------
|
||||
|
||||
|
||||
async def _get_or_create_settings(db: AsyncSession) -> ModelSettings:
|
||||
"""内部工具:获取或创建单例设置行(id=1)。"""
|
||||
settings = await db.get(ModelSettings, 1)
|
||||
if settings is None:
|
||||
settings = ModelSettings(id=1)
|
||||
db.add(settings)
|
||||
await db.flush()
|
||||
await db.refresh(settings)
|
||||
return settings
|
||||
|
||||
|
||||
@router.get(
|
||||
"/model-settings",
|
||||
response_model=ApiResponse[ModelSettingsRead],
|
||||
summary="获取模型全局设置(单例)",
|
||||
)
|
||||
async def get_model_settings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[ModelSettingsRead]:
|
||||
settings = await _get_or_create_settings(db)
|
||||
return success_response(ModelSettingsRead.model_validate(settings))
|
||||
|
||||
|
||||
@router.put(
|
||||
"/model-settings",
|
||||
response_model=ApiResponse[ModelSettingsRead],
|
||||
summary="更新模型全局设置(单例)",
|
||||
)
|
||||
async def update_model_settings(
|
||||
body: ModelSettingsUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[ModelSettingsRead]:
|
||||
settings = await _get_or_create_settings(db)
|
||||
|
||||
update_data = body.model_dump()
|
||||
for field, value in update_data.items():
|
||||
setattr(settings, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(settings)
|
||||
return success_response(ModelSettingsRead.model_validate(settings))
|
||||
|
||||
+51
-3
@@ -2,11 +2,53 @@
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.config import settings
|
||||
from app.api.v1 import router as api_v1_router
|
||||
from app.config import settings
|
||||
from app.schemas.common import ApiResponse
|
||||
|
||||
|
||||
def _error_message(detail: object) -> str:
|
||||
"""将异常 detail 转为前端可读的 message。"""
|
||||
if isinstance(detail, str):
|
||||
return detail
|
||||
if isinstance(detail, list):
|
||||
parts = []
|
||||
for item in detail:
|
||||
if isinstance(item, dict) and "msg" in item:
|
||||
loc = item.get("loc", ())
|
||||
loc_str = ".".join(str(x) for x in loc if x != "body")
|
||||
parts.append(f"{loc_str}: {item['msg']}" if loc_str else item["msg"])
|
||||
else:
|
||||
parts.append(str(item))
|
||||
return "; ".join(parts) if parts else "Validation error"
|
||||
return str(detail)
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""HTTP 异常统一为 { code, message, data: null }。"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
if isinstance(exc, HTTPException):
|
||||
code = exc.status_code
|
||||
message = _error_message(exc.detail)
|
||||
else:
|
||||
code = 500
|
||||
message = "Internal server error"
|
||||
body = ApiResponse[None](code=code, message=message, data=None).model_dump()
|
||||
return JSONResponse(status_code=code, content=body)
|
||||
|
||||
|
||||
async def validation_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""422 校验异常统一为 { code: 422, message, data: null }。"""
|
||||
assert isinstance(exc, RequestValidationError)
|
||||
message = _error_message(exc.errors())
|
||||
body = ApiResponse[None](code=422, message=message, data=None).model_dump()
|
||||
return JSONResponse(status_code=422, content=body)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -26,6 +68,11 @@ app = FastAPI(
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
# 统一错误响应格式:{ code, message, data: null }
|
||||
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
||||
app.add_exception_handler(HTTPException, http_exception_handler)
|
||||
app.add_exception_handler(Exception, http_exception_handler)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
@@ -41,4 +88,5 @@ app.include_router(api_v1_router, prefix=settings.api_v1_prefix)
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""健康检查。"""
|
||||
return {"status": "ok"}
|
||||
from app.schemas.common import success_response
|
||||
return success_response({"status": "ok"})
|
||||
@@ -0,0 +1,73 @@
|
||||
"""API 全局响应结构与分页。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ApiResponse(BaseModel, Generic[T]):
|
||||
"""无分页时的统一响应结构。"""
|
||||
|
||||
code: int = Field(200, description="与 HTTP 状态码一致")
|
||||
message: str = Field("success", description="提示信息")
|
||||
data: T | None = Field(None, description="实际数据")
|
||||
|
||||
|
||||
class Pagination(BaseModel):
|
||||
"""分页信息。"""
|
||||
|
||||
page: int = Field(..., description="当前页,从 1 开始")
|
||||
page_size: int = Field(..., description="每页条数")
|
||||
total: int = Field(..., description="总条数")
|
||||
max_page: int = Field(..., description="最大页码")
|
||||
|
||||
|
||||
class PaginatedData(BaseModel, Generic[T]):
|
||||
"""有分页时的 data 结构。"""
|
||||
|
||||
items: list[T] = Field(..., description="当前页数据")
|
||||
pagination: Pagination = Field(..., description="分页信息")
|
||||
|
||||
|
||||
def success_response(
|
||||
data: T,
|
||||
*,
|
||||
code: int = 200,
|
||||
message: str = "success",
|
||||
) -> ApiResponse[T]:
|
||||
"""构造无分页的成功响应。"""
|
||||
return ApiResponse(code=code, message=message, data=data)
|
||||
|
||||
|
||||
def paginated_response(
|
||||
items: list[T],
|
||||
*,
|
||||
page: int,
|
||||
page_size: int,
|
||||
total: int,
|
||||
code: int = 200,
|
||||
message: str = "success",
|
||||
) -> ApiResponse[PaginatedData[T]]:
|
||||
"""构造有分页的成功响应。"""
|
||||
max_page = max(1, (total + page_size - 1) // page_size) if page_size > 0 else 1
|
||||
pagination = Pagination(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
max_page=max_page,
|
||||
)
|
||||
payload = PaginatedData(items=items, pagination=pagination)
|
||||
return ApiResponse(code=code, message=message, data=payload)
|
||||
|
||||
|
||||
def error_response(
|
||||
*,
|
||||
code: int = 500,
|
||||
message: str = "error",
|
||||
) -> ApiResponse[None]:
|
||||
"""构造错误响应(data 为 null)。"""
|
||||
return ApiResponse(code=code, message=message, data=None)
|
||||
@@ -0,0 +1,117 @@
|
||||
"""与 LLM 相关的 Pydantic Schema(Provider/Model/ModelSettings)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.models.llm import AgentTypeKey, LogLevel, ModelCategoryKey, ProviderStatus
|
||||
|
||||
|
||||
class ProviderBase(BaseModel):
|
||||
"""供应商通用字段(不含敏感字段)。"""
|
||||
|
||||
name: str = Field(..., description="供应商名称")
|
||||
base_url: str = Field(..., description="API Base URL")
|
||||
description: str = Field("", description="说明")
|
||||
status: ProviderStatus = Field(
|
||||
ProviderStatus.testing,
|
||||
description="状态:active/testing/disabled",
|
||||
)
|
||||
created_by: str = Field("", description="创建人")
|
||||
|
||||
|
||||
class ProviderCreate(ProviderBase):
|
||||
"""创建供应商时的请求体,允许填写敏感字段。"""
|
||||
|
||||
id: str = Field(..., description="供应商 ID")
|
||||
api_key: str = Field("", description="API Key(敏感,不在响应中回显)")
|
||||
api_secret: str = Field("", description="API Secret(敏感,不在响应中回显)")
|
||||
|
||||
|
||||
class ProviderUpdate(BaseModel):
|
||||
"""更新供应商时的可选字段。"""
|
||||
|
||||
name: str | None = Field(None, description="供应商名称")
|
||||
base_url: str | None = Field(None, description="API Base URL")
|
||||
description: str | None = Field(None, description="说明")
|
||||
status: ProviderStatus | None = Field(
|
||||
None,
|
||||
description="状态:active/testing/disabled",
|
||||
)
|
||||
api_key: str | None = Field(None, description="API Key(敏感,不在响应中回显)")
|
||||
api_secret: str | None = Field(None, description="API Secret(敏感,不在响应中回显)")
|
||||
|
||||
|
||||
class ProviderRead(ProviderBase):
|
||||
"""对外返回的供应商信息(不包含 api_key/api_secret)。"""
|
||||
|
||||
id: str = Field(..., description="供应商 ID")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ModelBase(BaseModel):
|
||||
"""模型通用字段。"""
|
||||
|
||||
name: str = Field(..., description="模型名称")
|
||||
category: ModelCategoryKey = Field(..., description="模型类别:text/image/video")
|
||||
provider_id: str = Field(..., description="所属供应商 ID")
|
||||
params: dict[str, Any] = Field(default_factory=dict, description="模型参数(JSON)")
|
||||
description: str = Field("", description="说明")
|
||||
is_default: bool = Field(False, description="是否默认")
|
||||
created_by: str = Field("", description="创建人")
|
||||
|
||||
|
||||
class ModelCreate(ModelBase):
|
||||
"""创建模型请求体。"""
|
||||
|
||||
id: str = Field(..., description="模型 ID")
|
||||
|
||||
|
||||
class ModelUpdate(BaseModel):
|
||||
"""更新模型请求体(全部可选)。"""
|
||||
|
||||
name: str | None = Field(None, description="模型名称")
|
||||
category: ModelCategoryKey | None = Field(None, description="模型类别")
|
||||
provider_id: str | None = Field(None, description="所属供应商 ID")
|
||||
params: dict[str, Any] | None = Field(None, description="模型参数(JSON)")
|
||||
description: str | None = Field(None, description="说明")
|
||||
is_default: bool | None = Field(None, description="是否默认")
|
||||
|
||||
|
||||
class ModelRead(ModelBase):
|
||||
"""对外返回的模型信息。"""
|
||||
|
||||
id: str = Field(..., description="模型 ID")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ModelSettingsBase(BaseModel):
|
||||
"""模型全局设置通用字段。"""
|
||||
|
||||
default_text_model_id: str | None = Field(None, description="默认文本模型 ID")
|
||||
default_image_model_id: str | None = Field(None, description="默认图片模型 ID")
|
||||
default_video_model_id: str | None = Field(None, description="默认视频模型 ID")
|
||||
api_timeout: int = Field(30, description="API 超时(秒)")
|
||||
log_level: LogLevel = Field(LogLevel.info, description="日志级别")
|
||||
|
||||
|
||||
class ModelSettingsUpdate(ModelSettingsBase):
|
||||
"""更新或保存模型全局设置请求体。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ModelSettingsRead(ModelSettingsBase):
|
||||
"""对外返回的模型全局设置。"""
|
||||
|
||||
id: int = Field(..., description="设置行 ID(通常为 1)")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
Reference in new issue
Block a user