Skip to content

Commit d301919

Browse files
committed
fix
1 parent 2d71f5e commit d301919

File tree

2 files changed

+91
-212
lines changed

2 files changed

+91
-212
lines changed

tests/mock_server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ async def generate(
337337
status_code=400,
338338
content={
339339
"code": "InvalidParameter",
340-
"message": f"<400> InternalError.Algo.InvalidParameter: Range of top_p should be (0.0, 1.0], but got {params.top_p}",
340+
"message": "<400> InternalError.Algo.InvalidParameter: Range of top_p should be (0.0, 1.0]",
341341
},
342342
)
343343

@@ -347,7 +347,7 @@ async def generate(
347347
status_code=400,
348348
content={
349349
"code": "InvalidParameter",
350-
"message": f"<400> InternalError.Algo.InvalidParameter: Temperature should be in [0, 2], but got {params.temperature}",
350+
"message": "<400> InternalError.Algo.InvalidParameter: Temperature should be in [0.0, 2.0]",
351351
},
352352
)
353353

@@ -455,8 +455,8 @@ async def generate(
455455
return JSONResponse(
456456
status_code=400,
457457
content={
458-
"code": "InvalidParameter",
459-
"message": "Value error, current model does not support parameter `enable_thinking`.",
458+
"code": "InternalError",
459+
"message": "Error code: 400 - {'code': 20015, 'message': 'Value error, current model does not support parameter `enable_thinking`.', 'data': None}",
460460
},
461461
)
462462

tests/test_doc.py

Lines changed: 87 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import pytest
2-
import json
32
import requests
43
import os
5-
from typing import Generator, List, Dict, Any, Optional
6-
from dataclasses import dataclass
7-
8-
# --- CONSTANTS & CONFIGURATION ---
4+
import json
5+
from typing import Dict, Any
96

10-
# 基础 URL 修改为适配动态路径的前缀
11-
# 最终请求 URL 将拼接为: BASE_URL + "/" + {model_path}
7+
# --- CONFIGURATION ---
128
BASE_URL_PREFIX = "http://localhost:8000/siliconflow/models"
13-
149
API_KEY = os.getenv("SILICONFLOW_API_KEY", "test_api_key")
1510

1611
HEADERS = {
@@ -20,265 +15,149 @@
2015
"X-DashScope-SSE": "enable",
2116
}
2217

23-
# --- TOOL DEFINITIONS ---
24-
TOOL_VECTOR_WEATHER = [
25-
{
26-
"type": "function",
27-
"function": {
28-
"name": "get_current_weather",
29-
"description": "Get the current weather in a given location",
30-
"parameters": {
31-
"type": "object",
32-
"properties": {
33-
"location": {
34-
"type": "string",
35-
"description": "The city and state, e.g. San Francisco, CA",
36-
},
37-
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
38-
},
39-
"required": ["location"],
40-
},
41-
},
42-
}
43-
]
18+
# --- EXPECTED ERROR MESSAGES (COPIED FROM TABLE) ---
19+
# 定义预期错误常量,确保逐字对齐
20+
ERR_MSG_TOP_P_TYPE = "<400> InternalError.Algo.InvalidParameter: Input should be a valid number, unable to parse string as a number: parameters.top_p"
21+
ERR_MSG_TOP_P_RANGE = "<400> InternalError.Algo.InvalidParameter: Range of top_p should be (0.0, 1.0]"
22+
ERR_MSG_TEMP_RANGE = "<400> InternalError.Algo.InvalidParameter: Temperature should be in [0.0, 2.0]"
23+
ERR_MSG_PARTIAL_THINKING_CONFLICT = "<400> InternalError.Algo.InvalidParameter: Partial mode is not supported when enable_thinking is true"
24+
# R1 不支持 enable_thinking 的报错 (注意:表格中该报错包含 Python 字典的字符串表示,需严格匹配引号)
25+
ERR_MSG_R1_THINKING = "Error code: 400 - {'code': 20015, 'message': 'Value error, current model does not support parameter `enable_thinking`.', 'data': None}"
4426

4527
# --- HELPERS ---
4628

47-
@dataclass
48-
class SSEFrame:
49-
"""Formal representation of a Server-Sent Event frame for validation."""
50-
id: str
51-
output: Dict[str, Any]
52-
usage: Dict[str, Any]
53-
request_id: str
54-
55-
def parse_sse_stream(response: requests.Response) -> Generator[SSEFrame, None, None]:
56-
"""Parses the raw SSE stream line by line."""
57-
for line in response.iter_lines():
58-
if line:
59-
decoded_line = line.decode("utf-8")
60-
if decoded_line.startswith("data:"):
61-
json_str = decoded_line[5:].strip()
62-
try:
63-
data = json.loads(json_str)
64-
yield SSEFrame(
65-
id=data.get("output", {}).get("choices", [{}])[0].get("id", "unknown"),
66-
output=data.get("output", {}),
67-
usage=data.get("usage", {}),
68-
request_id=data.get("request_id", ""),
69-
)
70-
except json.JSONDecodeError:
71-
continue
72-
7329
def make_request(payload: Dict[str, Any]) -> requests.Response:
74-
"""
75-
Helper to send POST request using the Dynamic Path URL structure.
76-
77-
Format: POST /siliconflow/models/{model_path}
78-
79-
It extracts the 'model' from the payload to construct the URL.
80-
"""
30+
"""Helper to send POST request using the Dynamic Path URL structure."""
8131
model_path = payload.get("model")
82-
83-
if not model_path:
84-
raise ValueError("Test payload must contain 'model' field for dynamic URL construction")
85-
86-
# Construct the dynamic URL, e.g.:
87-
# http://localhost:8000/siliconflow/models/deepseek-ai/DeepSeek-V3
8832
url = f"{BASE_URL_PREFIX}/{model_path}"
89-
90-
# Send the request. Note: We keep 'model' in the json body as well,
91-
# though the server mainly relies on the path parameter now.
9233
return requests.post(url, headers=HEADERS, json=payload, stream=True)
9334

94-
# --- TEST SUITE ---
95-
96-
class TestDynamicPathRouting:
35+
def assert_exact_error(response: requests.Response, expected_code_str: str, expected_message: str):
9736
"""
98-
测试动态 URL 路由本身的正确性
37+
严格校验错误返回:
38+
1. HTTP 状态码通常为 4xx 或 500 (根据表格,部分 4xx 业务错误可能返回 200 或 400,此处以解析 body 为主)
39+
2. JSON body 中的 code 字段
40+
3. JSON body 中的 message 字段 (逐字匹配)
9941
"""
42+
try:
43+
data = response.json()
44+
except Exception:
45+
pytest.fail(f"Response is not valid JSON: {response.text}")
10046

101-
def test_routing_basic_success(self):
102-
"""
103-
测试标准的 URL 格式是否能通
104-
URL: .../deepseek-ai/DeepSeek-V3
105-
"""
106-
payload = {
107-
"model": "deepseek-ai/DeepSeek-V3",
108-
"input": {"messages": [{"role": "user", "content": "Hello"}]},
109-
"parameters": {"max_tokens": 10}
110-
}
111-
response = make_request(payload)
112-
assert response.status_code == 200
47+
# 1. Check Error Code (e.g., 'InvalidParameter' or 'InternalError')
48+
actual_code = data.get("code")
49+
assert actual_code == expected_code_str, f"Error Code mismatch.\nExpected: {expected_code_str}\nActual: {actual_code}"
11350

114-
def test_routing_with_mapping(self):
115-
"""
116-
测试服务端 ModelResolver 是否依然工作
117-
URL: .../pre-siliconflow/deepseek-v3 (会被映射到 upstream 的 deepseek-ai/DeepSeek-V3)
118-
"""
119-
payload = {
120-
"model": "pre-siliconflow/deepseek-v3",
121-
"input": {"messages": [{"role": "user", "content": "Test"}]},
122-
"parameters": {"max_tokens": 10}
123-
}
124-
response = make_request(payload)
125-
assert response.status_code == 200
51+
# 2. Check Error Message (Exact String Match)
52+
actual_message = data.get("message")
53+
assert actual_message == expected_message, f"Error Message mismatch.\nExpected: {expected_message}\nActual: {actual_message}"
12654

55+
# --- TEST SUITE ---
12756

128-
class TestParameterValidation:
129-
"""
130-
对应表格中参数校验相关的错误用例 (4xx Error Codes)
131-
"""
57+
class TestStrictErrorValidation:
13258

13359
def test_invalid_parameter_type_top_p(self):
13460
"""
135-
Case: parameters.top_p 输入字符串 'a',预期返回 400 InvalidParameter。
61+
表格行: 4xx的报错请求
62+
Input: top_p = "a" (string)
63+
Expected: InvalidParameter, <400> ... unable to parse string as a number
13664
"""
13765
payload = {
138-
"model": "deepseek-ai/DeepSeek-V3",
66+
"model": "pre-siliconflow/deepseek-v3",
13967
"input": {"messages": [{"role": "user", "content": "你好"}]},
140-
"parameters": {"top_p": "a"}, # Invalid type
68+
"parameters": {"top_p": "a"}
14169
}
14270
response = make_request(payload)
14371

144-
assert response.status_code == 400
145-
data = response.json()
146-
assert "InvalidParameter" in data.get("code", "") or "InvalidParameter" in data.get("message", "")
72+
# 根据表格预期,HTTP Code 可能是 400 或 500,但我们主要校验 Body 内容
73+
# 表格预期返回: code="InvalidParameter"
74+
assert_exact_error(
75+
response,
76+
expected_code_str="InvalidParameter",
77+
expected_message=ERR_MSG_TOP_P_TYPE
78+
)
14779

148-
@pytest.mark.parametrize("top_p_value", [0, 0.0])
149-
def test_invalid_parameter_range_top_p(self, top_p_value):
80+
def test_invalid_parameter_range_top_p(self):
15081
"""
151-
Case: top_p取值范围 (0, 1.0]。测试边界值 0。
82+
表格行: pre-siliconflow-deepseek-v3.1 top_p取值范围(0,1.0]
83+
Input: top_p = 0
84+
Expected: InvalidParameter, Range of top_p should be (0.0, 1.0]
15285
"""
15386
payload = {
154-
"model": "deepseek-ai/DeepSeek-V3.1",
87+
"model": "pre-siliconflow/deepseek-v3.1",
15588
"input": {"messages": [{"role": "user", "content": "你好"}]},
156-
"parameters": {"top_p": top_p_value},
89+
"parameters": {"top_p": 0}
15790
}
15891
response = make_request(payload)
15992

160-
assert response.status_code == 400
161-
data = response.json()
162-
assert "Range of top_p should be" in data.get("message", "")
93+
assert_exact_error(
94+
response,
95+
expected_code_str="InvalidParameter",
96+
expected_message=ERR_MSG_TOP_P_RANGE
97+
)
16398

16499
def test_invalid_parameter_range_temperature(self):
165100
"""
166-
Case: temperature 取值范围 [0, 2]。测试值 2.1。
167-
"""
168-
payload = {
169-
"model": "deepseek-ai/DeepSeek-V3.1",
170-
"input": {"messages": [{"role": "user", "content": "你好"}]},
171-
"parameters": {"temperature": 2.1},
172-
}
173-
response = make_request(payload)
174-
175-
assert response.status_code == 400
176-
data = response.json()
177-
assert "Temperature should be in" in data.get("message", "")
178-
179-
180-
class TestDeepSeekR1Specifics:
181-
"""
182-
针对 R1 模型的特定测试用例 (Reasoning Models)
183-
"""
184-
185-
def test_r1_usage_structure(self):
186-
"""
187-
Case: R1 模型不应该返回 text_tokens,应该返回 reasoning_tokens。
188-
URL: .../deepseek-ai/DeepSeek-R1
101+
表格行: pre-siliconflow-deepseek-v3.1 取值范围 [0, 2)
102+
Input: temperature = 2.1
103+
Expected: InvalidParameter, Temperature should be in [0.0, 2.0]
189104
"""
190105
payload = {
191-
"model": "deepseek-ai/DeepSeek-R1",
106+
"model": "pre-siliconflow/deepseek-v3.1",
192107
"input": {"messages": [{"role": "user", "content": "你好"}]},
193-
"parameters": {},
108+
"parameters": {"temperature": 2.1}
194109
}
195110
response = make_request(payload)
196-
assert response.status_code == 200
197-
198-
# 检查流式返回的最后一帧 Usage
199-
frames = list(parse_sse_stream(response))
200-
assert len(frames) > 0
201-
final_usage = frames[-1].usage
202-
203-
output_details = final_usage.get("output_tokens_details", {})
204-
assert output_details, "output_tokens_details missing"
205111

206-
# 验证不包含 text_tokens (R1 特性)
207-
assert "text_tokens" not in output_details, "R1 usage should not contain text_tokens"
208-
# 验证包含 reasoning_tokens
209-
assert "reasoning_tokens" in output_details
112+
assert_exact_error(
113+
response,
114+
expected_code_str="InvalidParameter",
115+
expected_message=ERR_MSG_TEMP_RANGE
116+
)
210117

211-
def test_r1_enable_thinking_parameter_error(self):
118+
def test_conflict_prefix_and_thinking(self):
212119
"""
213-
Case: R1 原生支持思考,显式传递 enable_thinking=True 应报错。
120+
表格行: 前缀续写...思考模式下...会报4xx
121+
Input: partial=True AND enable_thinking=True
122+
Expected: InvalidParameter, Partial mode is not supported when enable_thinking is true
214123
"""
215124
payload = {
216-
"model": "deepseek-ai/DeepSeek-R1",
217-
"input": {"messages": [{"role": "user", "content": "你好"}]},
218-
"parameters": {"enable_thinking": True},
219-
}
220-
response = make_request(payload)
221-
222-
assert response.status_code == 400
223-
data = response.json()
224-
assert "does not support parameter" in data.get("message", "")
225-
226-
227-
class TestAdvancedFeatures:
228-
"""
229-
复杂场景:前缀续写、ToolCall 格式校验等
230-
"""
231-
232-
def test_prefix_completion_thinking_conflict(self):
233-
"""
234-
Case: 思考模式下(enable_thinking=true),不支持前缀续写(partial=true)。
235-
"""
236-
payload = {
237-
"model": "deepseek-ai/DeepSeek-V3.2",
125+
"model": "pre-siliconflow/deepseek-v3.2",
238126
"input": {
239127
"messages": [
240128
{"role": "user", "content": "你好"},
241-
{"role": "assistant", "partial": True, "content": "你好,我是"},
129+
{"role": "assistant", "partial": True, "content": "你好,我是"}
242130
]
243131
},
244-
"parameters": {"enable_thinking": True},
132+
"parameters": {"enable_thinking": True}
245133
}
246134
response = make_request(payload)
247135

248-
assert response.status_code == 400
249-
data = response.json()
250-
assert "Partial mode is not supported when enable_thinking is true" in data.get("message", "")
136+
assert_exact_error(
137+
response,
138+
expected_code_str="InvalidParameter",
139+
expected_message=ERR_MSG_PARTIAL_THINKING_CONFLICT
140+
)
251141

252-
def test_r1_tool_choice_conflict(self):
142+
def test_r1_enable_thinking_unsupported(self):
253143
"""
254-
Case: R1 模型下,enable_thinking (或原生 R1) 开启时,不支持具体的 tool_choice 字典绑定。
144+
表格行: r1传了enable_thinking报错
145+
Input: model=deepseek-r1, enable_thinking=True
146+
Expected: InternalError, Error code: 400 - {'code': 20015...}
255147
"""
256148
payload = {
257-
"model": "deepseek-ai/DeepSeek-R1",
258-
"input": {
259-
"messages": [
260-
{"role": "user", "content": "What is the weather like in Boston?"}
261-
]
262-
},
263-
"parameters": {
264-
"result_format": "message",
265-
# R1 logic usually prevents enforcing a specific tool via dict when thinking is active
266-
"tool_choice": {
267-
"type": "function",
268-
"function": {"name": "get_current_weather"},
269-
},
270-
"tools": TOOL_VECTOR_WEATHER,
271-
},
149+
"model": "pre-siliconflow/deepseek-r1",
150+
"input": {"messages": [{"role": "user", "content": "你好"}]},
151+
"parameters": {"enable_thinking": True}
272152
}
273-
274153
response = make_request(payload)
275154

276-
# Expecting 400 because R1 + Specific Tool Choice is often restricted in this proxy logic
277-
if response.status_code != 200:
278-
assert response.status_code == 400
279-
error_data = response.json()
280-
assert "DeepSeek R1 does not support specific tool_choice" in error_data.get("message", "")
155+
# 表格显示此处返回的是 InternalError,且 message 是上游透传回来的原始错误
156+
assert_exact_error(
157+
response,
158+
expected_code_str="InternalError",
159+
expected_message=ERR_MSG_R1_THINKING
160+
)
281161

282162
if __name__ == "__main__":
283-
# 如果直接运行此脚本,可以使用 pytest 调起
284163
pytest.main(["-v", __file__])

0 commit comments

Comments
 (0)