diff --git a/app/api/endpoints/llm.py b/app/api/endpoints/llm.py index d9ad4ac5a..33eeb0057 100644 --- a/app/api/endpoints/llm.py +++ b/app/api/endpoints/llm.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Union from fastapi import Depends, Request, Response from fastapi.responses import HTMLResponse @@ -15,7 +15,9 @@ router = ResponseAPIRouter() @router.post( "/manage", summary="LLM提供商统一管理", - response_model=schemas.Response[Dict[str, Any]], + # 各动作 data 形态不一:目录查询返回列表,其余动作返回映射, + # 须用具体联合类型声明,而非单一开放映射 + response_model=schemas.Response[Union[List[Dict[str, Any]], Dict[str, Any]]], ) async def manage_provider( request: Request, diff --git a/tests/test_api_response.py b/tests/test_api_response.py index b88002782..b12f39313 100644 --- a/tests/test_api_response.py +++ b/tests/test_api_response.py @@ -637,6 +637,8 @@ def test_openapi_success_models_have_no_implicit_empty_nested_schemas(): "ManageRequest", # 通用管理响应的 data 为模块自定义结构,按设计不固定字段。 "Response_Dict_str__Any__", + # LLM 提供商管理响应的 data 目录查询为列表、其余动作为映射。 + "Response_Union_List_Dict_str__Any____Dict_str__Any___", } allowed_empty_components = {"McpJsonRpcEmptyResult"} violations = [] diff --git a/tests/test_llm_provider_manage.py b/tests/test_llm_provider_manage.py index 54f24d911..8689a1e6a 100644 --- a/tests/test_llm_provider_manage.py +++ b/tests/test_llm_provider_manage.py @@ -279,3 +279,32 @@ def test_llm_manage_endpoint_accepts_empty_target(monkeypatch): assert resp.success is True assert captured["provider"] == "" assert "callback_url" not in captured["params"] + + +def test_llm_manage_endpoint_response_model_accepts_list_data(): + """目录查询动作 data 为列表,响应模型须同时覆盖列表与映射形态。 + + 回归守护:list_providers 返回 list[dict], + 若响应模型声明为 Dict[str, Any] 会在序列化校验时直接 500; + 同时受响应模型守护测试约束,不得使用 Any/JsonData 弱类型。 + """ + from typing import Any, Dict, List, Union + + from app.api.endpoints import llm as llm_endpoint + + route = next( + r for r in llm_endpoint.router.routes if getattr(r, "path", "").endswith("/manage") + ) + assert route.response_model is schemas.Response[ + Union[List[Dict[str, Any]], Dict[str, Any]] + ] + + list_resp = schemas.Response[Union[List[Dict[str, Any]], Dict[str, Any]]]( + success=True, message="", data=[{"id": "openai", "name": "OpenAI 兼容"}] + ) + assert list_resp.data == [{"id": "openai", "name": "OpenAI 兼容"}] + + dict_resp = schemas.Response[Union[List[Dict[str, Any]], Dict[str, Any]]]( + success=True, message="", data={"models": ["gpt-4o"]} + ) + assert dict_resp.data == {"models": ["gpt-4o"]}