Skip to content

Commit 2d71f5e

Browse files
committed
Update test_doc.py
1 parent bde642c commit 2d71f5e

File tree

1 file changed

+80
-114
lines changed

1 file changed

+80
-114
lines changed

tests/test_doc.py

Lines changed: 80 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import pytest
22
import json
33
import requests
4-
from typing import Generator, List, Dict, Any
5-
from dataclasses import dataclass
64
import os
5+
from typing import Generator, List, Dict, Any, Optional
6+
from dataclasses import dataclass
77

88
# --- CONSTANTS & CONFIGURATION ---
99

10-
# 修改为动态 URL 的基础路径
11-
GATEWAY_BASE_URL = "http://localhost:8000/siliconflow/models"
10+
# 基础 URL 修改为适配动态路径的前缀
11+
# 最终请求 URL 将拼接为: BASE_URL + "/" + {model_path}
12+
BASE_URL_PREFIX = "http://localhost:8000/siliconflow/models"
1213

1314
API_KEY = os.getenv("SILICONFLOW_API_KEY", "test_api_key")
1415

@@ -43,19 +44,16 @@
4344

4445
# --- HELPERS ---
4546

46-
4747
@dataclass
4848
class SSEFrame:
4949
"""Formal representation of a Server-Sent Event frame for validation."""
50-
5150
id: str
5251
output: Dict[str, Any]
5352
usage: Dict[str, Any]
5453
request_id: str
5554

56-
5755
def parse_sse_stream(response: requests.Response) -> Generator[SSEFrame, None, None]:
58-
"""Parses the raw SSE stream."""
56+
"""Parses the raw SSE stream line by line."""
5957
for line in response.iter_lines():
6058
if line:
6159
decoded_line = line.decode("utf-8")
@@ -64,41 +62,68 @@ def parse_sse_stream(response: requests.Response) -> Generator[SSEFrame, None, N
6462
try:
6563
data = json.loads(json_str)
6664
yield SSEFrame(
67-
id=data.get("output", {})
68-
.get("choices", [{}])[0]
69-
.get("id", "unknown"),
65+
id=data.get("output", {}).get("choices", [{}])[0].get("id", "unknown"),
7066
output=data.get("output", {}),
7167
usage=data.get("usage", {}),
7268
request_id=data.get("request_id", ""),
7369
)
7470
except json.JSONDecodeError:
7571
continue
7672

77-
78-
def make_request(payload: Dict[str, Any]):
73+
def make_request(payload: Dict[str, Any]) -> requests.Response:
7974
"""
80-
Helper to send POST request using the Dynamic Path URL.
81-
Extracts 'model' from payload to construct the URL:
82-
POST /siliconflow/models/{model_path}
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.
8380
"""
84-
# 提取模型名称用于构建 URL
8581
model_path = payload.get("model")
8682

8783
if not model_path:
88-
raise ValueError(
89-
"Payload must contain 'model' field for dynamic URL construction"
90-
)
84+
raise ValueError("Test payload must contain 'model' field for dynamic URL construction")
9185

92-
# 构建动态 URL
93-
# 例如: http://localhost:8000/siliconflow/models/pre-siliconflow/deepseek-v3
94-
url = f"{GATEWAY_BASE_URL}/{model_path}"
86+
# Construct the dynamic URL, e.g.:
87+
# http://localhost:8000/siliconflow/models/deepseek-ai/DeepSeek-V3
88+
url = f"{BASE_URL_PREFIX}/{model_path}"
9589

96-
# 发送请求 (Payload 中保留 model 字段通常没问题,服务器端代码会再次处理或忽略)
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.
9792
return requests.post(url, headers=HEADERS, json=payload, stream=True)
9893

99-
10094
# --- TEST SUITE ---
10195

96+
class TestDynamicPathRouting:
97+
"""
98+
测试动态 URL 路由本身的正确性
99+
"""
100+
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
113+
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
126+
102127

103128
class TestParameterValidation:
104129
"""
@@ -108,51 +133,40 @@ class TestParameterValidation:
108133
def test_invalid_parameter_type_top_p(self):
109134
"""
110135
Case: parameters.top_p 输入字符串 'a',预期返回 400 InvalidParameter。
111-
URL: /siliconflow/models/pre-siliconflow/deepseek-v3
112136
"""
113137
payload = {
114-
"model": "pre-siliconflow/deepseek-v3",
138+
"model": "deepseek-ai/DeepSeek-V3",
115139
"input": {"messages": [{"role": "user", "content": "你好"}]},
116140
"parameters": {"top_p": "a"}, # Invalid type
117141
}
118142
response = make_request(payload)
119143

120-
# 验证状态码不应为 500
121-
assert (
122-
response.status_code != 500
123-
), "Should not return 500 for invalid parameter type"
124144
assert response.status_code == 400
125-
126145
data = response.json()
127-
assert "InvalidParameter" in data.get(
128-
"code", ""
129-
) or "InvalidParameter" in data.get("message", "")
146+
assert "InvalidParameter" in data.get("code", "") or "InvalidParameter" in data.get("message", "")
130147

131148
@pytest.mark.parametrize("top_p_value", [0, 0.0])
132149
def test_invalid_parameter_range_top_p(self, top_p_value):
133150
"""
134-
Case: pre-siliconflow-deepseek-v3.1 top_p取值范围 (0, 1.0]。
135-
测试边界值 0,预期报错。
136-
URL: /siliconflow/models/pre-siliconflow/deepseek-v3.1
151+
Case: top_p取值范围 (0, 1.0]。测试边界值 0。
137152
"""
138153
payload = {
139-
"model": "pre-siliconflow/deepseek-v3.1",
154+
"model": "deepseek-ai/DeepSeek-V3.1",
140155
"input": {"messages": [{"role": "user", "content": "你好"}]},
141156
"parameters": {"top_p": top_p_value},
142157
}
143158
response = make_request(payload)
144159

145-
assert response.status_code == 400, f"top_p={top_p_value} should be invalid"
160+
assert response.status_code == 400
146161
data = response.json()
147162
assert "Range of top_p should be" in data.get("message", "")
148163

149164
def test_invalid_parameter_range_temperature(self):
150165
"""
151-
Case: pre-siliconflow-deepseek-v3.1 temperature 取值范围 [0, 2]。
152-
测试值 2.1,预期报错。
166+
Case: temperature 取值范围 [0, 2]。测试值 2.1。
153167
"""
154168
payload = {
155-
"model": "pre-siliconflow/deepseek-v3.1",
169+
"model": "deepseek-ai/DeepSeek-V3.1",
156170
"input": {"messages": [{"role": "user", "content": "你好"}]},
157171
"parameters": {"temperature": 2.1},
158172
}
@@ -165,17 +179,16 @@ def test_invalid_parameter_range_temperature(self):
165179

166180
class TestDeepSeekR1Specifics:
167181
"""
168-
针对 R1 模型的特定测试用例
182+
针对 R1 模型的特定测试用例 (Reasoning Models)
169183
"""
170184

171185
def test_r1_usage_structure(self):
172186
"""
173-
Case: .usage.output_tokens_details 该路径下不应该返回 text_tokens 字段。
174-
R1 模型推理侧可能没有 text_tokens。
175-
URL: /siliconflow/models/pre-siliconflow/deepseek-r1
187+
Case: R1 模型不应该返回 text_tokens,应该返回 reasoning_tokens。
188+
URL: .../deepseek-ai/DeepSeek-R1
176189
"""
177190
payload = {
178-
"model": "pre-siliconflow/deepseek-r1",
191+
"model": "deepseek-ai/DeepSeek-R1",
179192
"input": {"messages": [{"role": "user", "content": "你好"}]},
180193
"parameters": {},
181194
}
@@ -188,22 +201,19 @@ def test_r1_usage_structure(self):
188201
final_usage = frames[-1].usage
189202

190203
output_details = final_usage.get("output_tokens_details", {})
191-
# 验证 output_tokens_details 存在
192204
assert output_details, "output_tokens_details missing"
193-
# 验证不包含 text_tokens (根据表格描述这是预期行为)
194-
assert (
195-
"text_tokens" not in output_details
196-
), "R1 usage should not contain text_tokens"
205+
206+
# 验证不包含 text_tokens (R1 特性)
207+
assert "text_tokens" not in output_details, "R1 usage should not contain text_tokens"
197208
# 验证包含 reasoning_tokens
198209
assert "reasoning_tokens" in output_details
199210

200211
def test_r1_enable_thinking_parameter_error(self):
201212
"""
202-
Case: r1传了 enable_thinking 报错。
203-
预期: 400 Value error, current model does not support parameter `enable_thinking`.
213+
Case: R1 原生支持思考,显式传递 enable_thinking=True 应报错。
204214
"""
205215
payload = {
206-
"model": "pre-siliconflow/deepseek-r1",
216+
"model": "deepseek-ai/DeepSeek-R1",
207217
"input": {"messages": [{"role": "user", "content": "你好"}]},
208218
"parameters": {"enable_thinking": True},
209219
}
@@ -216,17 +226,15 @@ def test_r1_enable_thinking_parameter_error(self):
216226

217227
class TestAdvancedFeatures:
218228
"""
219-
复杂场景:前缀续写、历史消息包含 ToolCall
229+
复杂场景:前缀续写、ToolCall 格式校验等
220230
"""
221231

222232
def test_prefix_completion_thinking_conflict(self):
223233
"""
224234
Case: 思考模式下(enable_thinking=true),不支持前缀续写(partial=true)。
225-
预期返回: 400 InvalidParameter.
226-
URL: /siliconflow/models/pre-siliconflow/deepseek-v3.2
227235
"""
228236
payload = {
229-
"model": "pre-siliconflow/deepseek-v3.2",
237+
"model": "deepseek-ai/DeepSeek-V3.2",
230238
"input": {
231239
"messages": [
232240
{"role": "user", "content": "你好"},
@@ -239,69 +247,22 @@ def test_prefix_completion_thinking_conflict(self):
239247

240248
assert response.status_code == 400
241249
data = response.json()
242-
assert "Partial mode is not supported when enable_thinking is true" in data.get(
243-
"message", ""
244-
)
245-
246-
def test_history_with_tool_calls(self):
247-
"""
248-
Case: 3.1和3.2 message中包含历史 tool_call 调用信息曾报 5xx。
249-
预期: 200 OK。
250-
"""
251-
payload = {
252-
"model": "pre-siliconflow/deepseek-v3.2",
253-
"input": {
254-
"messages": [
255-
{
256-
"role": "system",
257-
"content": "你是一个为智能助手。请使用简洁、自然、适合朗读的中文回答",
258-
},
259-
{"role": "user", "content": "外部轴设置"},
260-
{
261-
"role": "assistant",
262-
"tool_calls": [
263-
{
264-
"function": {
265-
"arguments": '{"input_text": "外部轴设置"}',
266-
"name": "KB20250625001",
267-
},
268-
"id": "call_6478091069c2448b83f38e",
269-
"type": "function",
270-
}
271-
],
272-
},
273-
{
274-
"role": "tool",
275-
"content": "界面用于用户进行快速配置。",
276-
"tool_call_id": "call_6478091069c2448b83f38e",
277-
},
278-
]
279-
},
280-
"parameters": {"enable_thinking": True},
281-
}
282-
response = make_request(payload)
250+
assert "Partial mode is not supported when enable_thinking is true" in data.get("message", "")
283251

284-
# 核心验证:不能崩 (500)
285-
assert (
286-
response.status_code != 500
287-
), "Server returned 500 for history with tool calls"
288-
assert response.status_code == 200
289-
290-
def test_r1_tool_call_format_wrapping(self):
252+
def test_r1_tool_choice_conflict(self):
291253
"""
292-
Case: Error code: 400, _sse_http_status: 500 (Wrappping error).
293-
Input should be 'none', 'auto' or 'required'.
294-
验证 R1 对 result_format='message' 和 tool_choice 的组合处理。
254+
Case: R1 模型下,enable_thinking (或原生 R1) 开启时,不支持具体的 tool_choice 字典绑定。
295255
"""
296256
payload = {
297-
"model": "pre-siliconflow/deepseek-r1",
257+
"model": "deepseek-ai/DeepSeek-R1",
298258
"input": {
299259
"messages": [
300260
{"role": "user", "content": "What is the weather like in Boston?"}
301261
]
302262
},
303263
"parameters": {
304264
"result_format": "message",
265+
# R1 logic usually prevents enforcing a specific tool via dict when thinking is active
305266
"tool_choice": {
306267
"type": "function",
307268
"function": {"name": "get_current_weather"},
@@ -312,7 +273,12 @@ def test_r1_tool_call_format_wrapping(self):
312273

313274
response = make_request(payload)
314275

276+
# Expecting 400 because R1 + Specific Tool Choice is often restricted in this proxy logic
315277
if response.status_code != 200:
278+
assert response.status_code == 400
316279
error_data = response.json()
317-
assert response.status_code != 500
318-
assert error_data.get("code") != "InternalError"
280+
assert "DeepSeek R1 does not support specific tool_choice" in error_data.get("message", "")
281+
282+
if __name__ == "__main__":
283+
# 如果直接运行此脚本,可以使用 pytest 调起
284+
pytest.main(["-v", __file__])

0 commit comments

Comments
 (0)