Skip to content

Commit 9d52d21

Browse files
committed
add
1 parent 172d2e8 commit 9d52d21

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

tests/test_doc.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import pytest
2+
import json
3+
import requests
4+
from typing import Generator, List, Dict, Any
5+
from dataclasses import dataclass
6+
import os
7+
8+
# --- CONSTANTS & CONFIGURATION ---
9+
GATEWAY_URL = "http://localhost:8000/api/v1/services/aigc/text-generation/generation"
10+
# 如果需要测试硅基流动的真实环境,请切换 URL 并设置 API KEY
11+
# GATEWAY_URL = "https://api-bailian.siliconflow.cn/api/v1/services/aigc/text-generation/generation"
12+
13+
API_KEY = os.getenv("SILICONFLOW_API_KEY", "test_api_key")
14+
15+
HEADERS = {
16+
"Authorization": f"Bearer {API_KEY}",
17+
"Content-Type": "application/json",
18+
"Accept": "text/event-stream",
19+
"X-DashScope-SSE": "enable" # 模拟 DashScope 协议头
20+
}
21+
22+
# --- TOOL DEFINITIONS ---
23+
TOOL_VECTOR_WEATHER = [
24+
{
25+
"type": "function",
26+
"function": {
27+
"name": "get_current_weather",
28+
"description": "Get the current weather in a given location",
29+
"parameters": {
30+
"type": "object",
31+
"properties": {
32+
"location": {
33+
"type": "string",
34+
"description": "The city and state, e.g. San Francisco, CA",
35+
},
36+
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
37+
},
38+
"required": ["location"],
39+
},
40+
},
41+
}
42+
]
43+
44+
# --- HELPERS ---
45+
46+
@dataclass
47+
class SSEFrame:
48+
"""Formal representation of a Server-Sent Event frame for validation."""
49+
id: str
50+
output: Dict[str, Any]
51+
usage: Dict[str, Any]
52+
request_id: str
53+
54+
def parse_sse_stream(response: requests.Response) -> Generator[SSEFrame, None, None]:
55+
"""Parses the raw SSE stream."""
56+
for line in response.iter_lines():
57+
if line:
58+
decoded_line = line.decode("utf-8")
59+
if decoded_line.startswith("data:"):
60+
json_str = decoded_line[5:].strip()
61+
try:
62+
data = json.loads(json_str)
63+
yield SSEFrame(
64+
id=data.get("output", {}).get("choices", [{}])[0].get("id", "unknown"),
65+
output=data.get("output", {}),
66+
usage=data.get("usage", {}),
67+
request_id=data.get("request_id", ""),
68+
)
69+
except json.JSONDecodeError:
70+
continue
71+
72+
def make_request(payload):
73+
"""Helper to send POST request."""
74+
return requests.post(GATEWAY_URL, headers=HEADERS, json=payload, stream=True)
75+
76+
# --- TEST SUITE ---
77+
78+
class TestParameterValidation:
79+
"""
80+
对应表格中参数校验相关的错误用例 (4xx Error Codes)
81+
"""
82+
83+
def test_invalid_parameter_type_top_p(self):
84+
"""
85+
Case: parameters.top_p 输入字符串 'a',预期返回 400 InvalidParameter。
86+
Bug描述: 曾返回 InternalError 500。
87+
"""
88+
payload = {
89+
"model": "pre-siliconflow/deepseek-v3",
90+
"input": {"messages": [{"role": "user", "content": "你好"}]},
91+
"parameters": {"top_p": "a"} # Invalid type
92+
}
93+
response = make_request(payload)
94+
95+
# 验证状态码不应为 500
96+
assert response.status_code != 500, "Should not return 500 for invalid parameter type"
97+
assert response.status_code == 400
98+
99+
data = response.json()
100+
assert "InvalidParameter" in data.get("code", "") or "InvalidParameter" in data.get("message", "")
101+
102+
@pytest.mark.parametrize("top_p_value", [0, 0.0])
103+
def test_invalid_parameter_range_top_p(self, top_p_value):
104+
"""
105+
Case: pre-siliconflow-deepseek-v3.1 top_p取值范围 (0, 1.0]。
106+
测试边界值 0,预期报错。
107+
"""
108+
payload = {
109+
"model": "pre-siliconflow/deepseek-v3.1",
110+
"input": {"messages": [{"role": "user", "content": "你好"}]},
111+
"parameters": {"top_p": top_p_value}
112+
}
113+
response = make_request(payload)
114+
115+
assert response.status_code == 400, f"top_p={top_p_value} should be invalid"
116+
data = response.json()
117+
assert "Range of top_p should be" in data.get("message", "")
118+
119+
def test_invalid_parameter_range_temperature(self):
120+
"""
121+
Case: pre-siliconflow-deepseek-v3.1 temperature 取值范围 [0, 2]。
122+
测试值 2.1,预期报错。
123+
"""
124+
payload = {
125+
"model": "pre-siliconflow/deepseek-v3.1",
126+
"input": {"messages": [{"role": "user", "content": "你好"}]},
127+
"parameters": {"temperature": 2.1}
128+
}
129+
response = make_request(payload)
130+
131+
assert response.status_code == 400
132+
data = response.json()
133+
assert "Temperature should be in" in data.get("message", "")
134+
135+
136+
class TestDeepSeekR1Specifics:
137+
"""
138+
针对 R1 模型的特定测试用例
139+
"""
140+
141+
def test_r1_usage_structure(self):
142+
"""
143+
Case: .usage.output_tokens_details 该路径下不应该返回 text_tokens 字段。
144+
R1 模型推理侧可能没有 text_tokens。
145+
"""
146+
payload = {
147+
"model": "pre-siliconflow/deepseek-r1",
148+
"input": {"messages": [{"role": "user", "content": "你好"}]},
149+
"parameters": {}
150+
}
151+
response = make_request(payload)
152+
assert response.status_code == 200
153+
154+
# 检查流式返回的最后一帧 Usage
155+
frames = list(parse_sse_stream(response))
156+
assert len(frames) > 0
157+
final_usage = frames[-1].usage
158+
159+
output_details = final_usage.get("output_tokens_details", {})
160+
# 验证 output_tokens_details 存在
161+
assert output_details, "output_tokens_details missing"
162+
# 验证不包含 text_tokens (根据表格描述这是预期行为)
163+
assert "text_tokens" not in output_details, "R1 usage should not contain text_tokens"
164+
# 验证包含 reasoning_tokens
165+
assert "reasoning_tokens" in output_details
166+
167+
def test_r1_enable_thinking_parameter_error(self):
168+
"""
169+
Case: r1传了 enable_thinking 报错。
170+
预期: 400 Value error, current model does not support parameter `enable_thinking`.
171+
"""
172+
payload = {
173+
"model": "pre-siliconflow/deepseek-r1",
174+
"input": {"messages": [{"role": "user", "content": "你好"}]},
175+
"parameters": {"enable_thinking": True}
176+
}
177+
response = make_request(payload)
178+
179+
assert response.status_code == 400
180+
data = response.json()
181+
assert "does not support parameter" in data.get("message", "")
182+
183+
184+
class TestAdvancedFeatures:
185+
"""
186+
复杂场景:前缀续写、历史消息包含 ToolCall 等
187+
"""
188+
189+
def test_prefix_completion_thinking_conflict(self):
190+
"""
191+
Case: 思考模式下(enable_thinking=true),不支持前缀续写(partial=true)。
192+
预期返回: 400 InvalidParameter.
193+
"""
194+
payload = {
195+
"model": "pre-siliconflow/deepseek-v3.2",
196+
"input": {
197+
"messages": [
198+
{"role": "user", "content": "你好"},
199+
{"role": "assistant", "partial": True, "content": "你好,我是"}
200+
]
201+
},
202+
"parameters": {
203+
"enable_thinking": True
204+
}
205+
}
206+
response = make_request(payload)
207+
208+
assert response.status_code == 400
209+
data = response.json()
210+
assert "Partial mode is not supported when enable_thinking is true" in data.get("message", "")
211+
212+
def test_history_with_tool_calls(self):
213+
"""
214+
Case: 3.1和3.2 message中包含历史 tool_call 调用信息曾报 5xx。
215+
预期: 200 OK。
216+
"""
217+
payload = {
218+
"model": "pre-siliconflow/deepseek-v3.2",
219+
"input": {
220+
"messages": [
221+
{
222+
"role": "system",
223+
"content": "你是一个为智能助手。请使用简洁、自然、适合朗读的中文回答"
224+
},
225+
{
226+
"role": "user",
227+
"content": "外部轴设置"
228+
},
229+
{
230+
"role": "assistant",
231+
"tool_calls": [
232+
{
233+
"function": {
234+
"arguments": "{\"input_text\": \"外部轴设置\"}",
235+
"name": "KB20250625001"
236+
},
237+
"id": "call_6478091069c2448b83f38e",
238+
"type": "function"
239+
}
240+
]
241+
},
242+
{
243+
"role": "tool",
244+
"content": "界面用于用户进行快速配置。",
245+
"tool_call_id": "call_6478091069c2448b83f38e"
246+
}
247+
]
248+
},
249+
"parameters": {
250+
"enable_thinking": True
251+
}
252+
}
253+
response = make_request(payload)
254+
255+
# 核心验证:不能崩 (500)
256+
assert response.status_code != 500, "Server returned 500 for history with tool calls"
257+
assert response.status_code == 200
258+
259+
def test_r1_tool_call_format_wrapping(self):
260+
"""
261+
Case: Error code: 400, _sse_http_status: 500 (Wrappping error).
262+
Input should be 'none', 'auto' or 'required'.
263+
验证 R1 对 result_format='message' 和 tool_choice 的组合处理。
264+
"""
265+
payload = {
266+
"model": "pre-siliconflow/deepseek-r1",
267+
"input": {
268+
"messages": [{"role": "user", "content": "What is the weather like in Boston?"}]
269+
},
270+
"parameters": {
271+
"result_format": "message",
272+
"tool_choice": {"type": "function", "function": {"name": "get_current_weather"}}, # 修正后的 tool_choice 格式
273+
"tools": TOOL_VECTOR_WEATHER
274+
}
275+
}
276+
277+
# 注意:CSV中提到的错误是 `tool_choice` 格式问题导致的 400 被包了一层 500
278+
# 这里我们发送请求并检查状态码
279+
response = make_request(payload)
280+
281+
# 即使失败,也应该返回标准的 400 而不是 InternalError
282+
if response.status_code != 200:
283+
error_data = response.json()
284+
# 确保不是 500 或者 InternalError
285+
assert response.status_code != 500
286+
assert error_data.get("code") != "InternalError"

0 commit comments

Comments
 (0)