chore: 规范标点
This commit is contained in:
1 parent
15e79887fa
commit
3dd2231c70
81 files changed
+1751
-1751
No files matched your search
+40
-40
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""mock_server.py - 最小的 OpenAI 兼容服务端, 用于端到端测试 ligent.
|
||||
|
||||
不依赖任何第三方包. 行为完全由请求内容决定, 保持确定性:
|
||||
不依赖任何第三方包. 行为完全由请求内容决定, 保持确定性:
|
||||
|
||||
- 用户消息含 ERROR_TEST -> 返回 HTTP 400 + error 对象
|
||||
- 尚未有 tool 结果 -> 流式返回一个 tool_call (run_command echo ...)
|
||||
@@ -25,7 +25,7 @@ def sse(wfile, obj):
|
||||
|
||||
|
||||
def pairing_error(msgs):
|
||||
"""服务端的硬要求:带 tool_calls 的 assistant 后面必须**紧跟**与每个
|
||||
"""服务端的硬要求: 带 tool_calls 的 assistant 后面必须**紧跟**与每个
|
||||
tool_call_id 一一对应的 tool 消息, 中间不能夹别的东西.
|
||||
|
||||
真实服务端就是这么回 400 的, mock 也照做 —— 这样任何会写坏历史的回归
|
||||
@@ -120,7 +120,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
# doctor 的连通性/重定向检查走 GET.
|
||||
# /redirect 开头故意返回 307, 用来验证客户端能把重定向指出来.
|
||||
# doctor 的认证探测:端点要求认证, 且只认 "Bearer good-key".
|
||||
# doctor 的认证探测: 端点要求认证, 且只认 "Bearer good-key".
|
||||
# 带对 key -> 200 (认证过了)
|
||||
# 带错 key -> 401 (密钥被拒)
|
||||
# 不带 key -> 401 (端点要求认证, 而你没配)
|
||||
@@ -140,7 +140,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
# MODEL_LIST=bare 时改回裸数组, MODEL_LIST=empty 时不返回任何模型,
|
||||
# 两种兼容/异常形状都由 e2e 覆盖.
|
||||
if "/models" in self.path:
|
||||
# 模式从**文件**读, 不从环境变量读:mock 是脚本开头起好的,
|
||||
# 模式从**文件**读, 不从环境变量读: mock 是脚本开头起好的,
|
||||
# 给客户端进程设的 env 它根本看不见 (第一版就是这么假通过的).
|
||||
mode = ""
|
||||
mode_file = os.environ.get("MODELS_MODE_FILE", "")
|
||||
@@ -157,7 +157,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
elif mode == "garbage":
|
||||
payload = b"<html>not json at all</html>"
|
||||
else:
|
||||
# 默认列表里**故意包含当前模型** (deepseek-flash):
|
||||
# 默认列表里**故意包含当前模型** (deepseek-flash):
|
||||
# 这样才验得了"光标初始落在当前项上".
|
||||
# 另外塞一个重复项和一个缺 id 的坏条目, 验去重与跳过.
|
||||
payload = json.dumps({"object": "list", "data": [
|
||||
@@ -191,7 +191,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
raw = self.rfile.read(length)
|
||||
|
||||
# ---- UTF-8 严格校验:真服务端就是这么拒的 ----
|
||||
# ---- UTF-8 严格校验: 真服务端就是这么拒的 ----
|
||||
# DeepSeek 对请求体里非法的 UTF-8 回的是
|
||||
# 400 Failed to parse the request body as JSON:
|
||||
# messages[43].content: invalid unicode code point
|
||||
@@ -208,7 +208,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
# ---- 假的 Files API ----
|
||||
# 只做够用的部分:认 multipart 里有没有图片内容, 发一个递增的 file_id.
|
||||
# 只做够用的部分: 认 multipart 里有没有图片内容, 发一个递增的 file_id.
|
||||
try:
|
||||
body = json.loads(raw or b"{}")
|
||||
except Exception:
|
||||
@@ -237,7 +237,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
tool_msgs = [m for m in messages if m.get("role") == "tool"]
|
||||
tools_sent = body.get("tools")
|
||||
|
||||
# ---- 结构校验:和真服务端一样严 ----
|
||||
# ---- 结构校验: 和真服务端一样严 ----
|
||||
perr = pairing_error(messages)
|
||||
if perr:
|
||||
self.reject(400, perr)
|
||||
@@ -246,7 +246,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
# ---- 能力探测序列 ----
|
||||
# ligent 会在第一次真实请求之前发几个试探请求 (基线 / 图片 / tools),
|
||||
# 它们会被真实处理, 所以这里必须排在所有标记分支之前.
|
||||
# 辨认靠形状:刚好一条 user 消息, 内容是 "1", 或者带一张 1x1 的图.
|
||||
# 辨认靠形状: 刚好一条 user 消息, 内容是 "1", 或者带一张 1x1 的图.
|
||||
def looks_like_probe(msgs):
|
||||
if len(msgs) != 1 or msgs[0].get("role") != "user":
|
||||
return False
|
||||
@@ -264,7 +264,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
if looks_like_probe(messages):
|
||||
has_image = isinstance(messages[0].get("content"), list)
|
||||
# 用模型名模拟"不支持某能力"的服务端:400 是明确的拒绝信号
|
||||
# 用模型名模拟"不支持某能力"的服务端: 400 是明确的拒绝信号
|
||||
if "no-vision" in model and has_image:
|
||||
return self.reject(400, "unsupported content type: image_url")
|
||||
if "no-tools" in model and tools_sent:
|
||||
@@ -274,7 +274,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_text("1", stream, model)
|
||||
return
|
||||
|
||||
# 提权:让模型发一个带 elevated 的 run_command
|
||||
# 提权: 让模型发一个带 elevated 的 run_command
|
||||
if "ELEVATED_TEST" in first_user:
|
||||
if not tool_msgs:
|
||||
args = json.dumps({"command": "echo 提权后的输出", "elevated": True})
|
||||
@@ -293,7 +293,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_text("ELEV_SAW[%s]" % got.replace("\n", " | "), stream, model)
|
||||
return
|
||||
|
||||
# /include:把收到的 user 内容块报回来.
|
||||
# /include: 把收到的 user 内容块报回来.
|
||||
# 用 first_user 判断——附带之后最新那条 user 是带块的消息, last_user 会漂.
|
||||
if "INCLUDE_TEST" in first_user:
|
||||
texts, imgs, names = 0, 0, []
|
||||
@@ -330,7 +330,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
stream, model)
|
||||
return
|
||||
|
||||
# 权限/参数校验:客户端必须把 tools 传上来
|
||||
# 权限/参数校验: 客户端必须把 tools 传上来
|
||||
assert tools_sent, "请求里没有 tools 字段"
|
||||
|
||||
if "ERROR_TEST" in first_user:
|
||||
@@ -384,7 +384,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(payload)
|
||||
return
|
||||
|
||||
# 复刻 OpenRouter 对"key 格式不对"的回应:头其实带了,
|
||||
# 复刻 OpenRouter 对"key 格式不对"的回应: 头其实带了,
|
||||
# 但文案说的是 "Missing Authentication header", 容易把人引偏
|
||||
if "AUTHFAIL_TEST" in first_user:
|
||||
payload = json.dumps({"error": {"message": "Missing Authentication header",
|
||||
@@ -396,7 +396,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(payload)
|
||||
return
|
||||
|
||||
# 重定向:用于验证客户端不静默跟随 (跨源跟随会丢掉 Authorization,
|
||||
# 重定向: 用于验证客户端不静默跟随 (跨源跟随会丢掉 Authorization,
|
||||
# 服务端只会返回 401 "Missing Authentication header", 误导用户去查 key)
|
||||
if "REDIRECT_TEST" in first_user:
|
||||
self.send_response(307)
|
||||
@@ -443,7 +443,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(payload)
|
||||
return
|
||||
|
||||
# 超过客户端响应体上限的超大响应:客户端应当主动中止而不是被撑爆
|
||||
# 超过客户端响应体上限的超大响应: 客户端应当主动中止而不是被撑爆
|
||||
if "HUGE_TEST" in first_user:
|
||||
total = 40 * 1024 * 1024
|
||||
self.send_response(200)
|
||||
@@ -459,7 +459,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
pass # 客户端主动断开是预期行为
|
||||
return
|
||||
|
||||
# 畸形 SSE:各种不合法的片段, 客户端必须不崩溃且给出合理结果
|
||||
# 畸形 SSE: 各种不合法的片段, 客户端必须不崩溃且给出合理结果
|
||||
if "GARBAGE_TEST" in first_user:
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
@@ -479,7 +479,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.wfile.flush()
|
||||
return
|
||||
|
||||
# 传输中途断掉:声明了 Content-Length 却提前关闭连接
|
||||
# 传输中途断掉: 声明了 Content-Length 却提前关闭连接
|
||||
if "TRUNCATE_TEST" in first_user:
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
@@ -537,7 +537,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.wfile.flush()
|
||||
return
|
||||
|
||||
# 思考模式:验证 reasoning_content 既能被累积, 也能随 assistant 消息
|
||||
# 思考模式: 验证 reasoning_content 既能被累积, 也能随 assistant 消息
|
||||
# 回传. DeepSeek 要求请求带 tools 时把历史里的 reasoning_content 全带上,
|
||||
# 漏了下一轮就 400, 所以这里第二轮专门检查它有没有回来.
|
||||
if "THINK_TEST" in first_user:
|
||||
@@ -562,7 +562,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.end_headers()
|
||||
# 思考过程分片下发:客户端必须累积, 而不是只留最后一片
|
||||
# 思考过程分片下发: 客户端必须累积, 而不是只留最后一片
|
||||
for part in ("先想一下", "要不要调用工具. "):
|
||||
sse(self.wfile, {"choices": [{"index": 0,
|
||||
"delta": {"reasoning_content": part},
|
||||
@@ -587,8 +587,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
body.get("reasoning_effort") or "none"), stream, model)
|
||||
return
|
||||
|
||||
# 慢速响应:先静默一段 (网络/排队), 再吐思维链, 再静默一段,
|
||||
# 最后给正文. 用来验证等待动画的两段标签:"等待响应" → "思考中".
|
||||
# 慢速响应: 先静默一段 (网络/排队), 再吐思维链, 再静默一段,
|
||||
# 最后给正文. 用来验证等待动画的两段标签: "等待响应" → "思考中".
|
||||
# 名字刻意不含 "THINK_TEST" —— 上面那些标记都是按子串匹配的,
|
||||
# 叫 SLOWTHINK_TEST 会被 THINK_TEST 那个分支先截胡.
|
||||
if "SLOWRESP_TEST" in first_user:
|
||||
@@ -604,7 +604,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_text("答案", stream=True, model=model)
|
||||
return
|
||||
|
||||
# meta 模式:第一轮让 meta 模型调 delete_rounds 删掉第 2 轮,
|
||||
# meta 模式: 第一轮让 meta 模型调 delete_rounds 删掉第 2 轮,
|
||||
# 第二轮收工. 用来端到端验证 /meta 的完整链路.
|
||||
if "META_TEST" in first_user:
|
||||
if not tool_msgs:
|
||||
@@ -626,7 +626,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_text("已删除第 2 轮. ", stream, model)
|
||||
return
|
||||
|
||||
# 按"最后一条用户消息"触发:上面那些模式都看 first_user,
|
||||
# 按"最后一条用户消息"触发: 上面那些模式都看 first_user,
|
||||
# 同一个进程里的第二轮就再也触发不了了, 没法验证"作用域仅限本轮".
|
||||
if "WRITE_LAST_TEST" in last_user:
|
||||
if messages[-1].get("role") == "user": # 新一轮的开头
|
||||
@@ -656,7 +656,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_text("第二轮写完了", stream, model)
|
||||
return
|
||||
|
||||
# 发出一个 write_file 调用:用来验证"在确认提示上按 Ctrl-C"到底
|
||||
# 发出一个 write_file 调用: 用来验证"在确认提示上按 Ctrl-C"到底
|
||||
# 会不会把文件写下去
|
||||
if "WRITE_TEST" in first_user:
|
||||
if not tool_msgs:
|
||||
@@ -686,7 +686,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_text("写完了", stream, model)
|
||||
return
|
||||
|
||||
# 读一个**含非法 UTF-8 字节**的文件. 工具输出里出现坏字节的来源很日常:
|
||||
# 读一个**含非法 UTF-8 字节**的文件. 工具输出里出现坏字节的来源很日常:
|
||||
# 某条命令在非 UTF-8 locale 下打出来的路径、被截断的多字节字符、二进制
|
||||
# 文件里混的一小段文本. 这段字节会进对话历史, 然后被序列化进请求体 ——
|
||||
# 如果序列化器原样吐出, 服务端就会 400, 而且之后每次请求都 400.
|
||||
@@ -715,7 +715,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(b"data: [DONE]\n\n")
|
||||
self.wfile.flush()
|
||||
return
|
||||
# 第二轮:能走到这里就说明请求体过了上面的 UTF-8 校验.
|
||||
# 第二轮: 能走到这里就说明请求体过了上面的 UTF-8 校验.
|
||||
# 把工具消息里有没有替换字符也顺带报出来, 便于断言.
|
||||
got = ""
|
||||
for m in tool_msgs:
|
||||
@@ -725,8 +725,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_text("读完了, 替换字符=%d" % got.count("\ufffd"), stream, model)
|
||||
return
|
||||
|
||||
# 一轮里连写两个文件:用来验证 Always/neVer 的作用域就是"这一轮"
|
||||
# (名字刻意不含 "WRITE_TEST", 免得被上面那个分支先截胡)
|
||||
# 一轮里连写两个文件: 用来验证 Always/neVer 的作用域就是"这一轮"
|
||||
# (名字刻意不含 "WRITE_TEST", 以免被上面那个分支先截胡)
|
||||
if "WRITE2_TEST" in first_user:
|
||||
n = len(tool_msgs)
|
||||
if n < 2:
|
||||
@@ -759,8 +759,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
# 让模型去读一个"扩展名是 .png、内容不是图片"的文件,
|
||||
# 验证 read_image 按内容判断、并明确拒绝
|
||||
# 两种"读不了":内容不是图片、以及大得内联不进去.
|
||||
# 注意:mock 的标记靠子串匹配, 是平铺的命名空间 —— 上面第 249 行的
|
||||
# 两种"读不了": 内容不是图片、以及大得内联不进去.
|
||||
# 注意: mock 的标记靠子串匹配, 是平铺的命名空间 —— 上面第 249 行的
|
||||
# HUGE_TEST 分支会吃掉任何含 "HUGE_TEST" 的标记, 所以这里叫 OVERSIZE.
|
||||
# 两者都该在**本地**被拒 —— 工具返回错误, 之后就没有图片块可发.
|
||||
if "IMAGE_BAD_TEST" in last_user or "IMAGE_OVERSIZE_TEST" in last_user:
|
||||
@@ -780,9 +780,9 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
# 一条 assistant 里发**两个**工具调用, 第一个是 read_image.
|
||||
# 这是"图片附件插在两条 tool 消息中间"那个 bug 的最小复现形状:
|
||||
# 这是"图片附件插在两条 tool 消息中间"那个 bug 的最小复现形状:
|
||||
# 只要附件没被攒到最后一起推, 上面的结构校验就会回 400.
|
||||
# 注意用 first_user:图片附件本身也是一条 user 消息, 而且排在最后,
|
||||
# 注意用 first_user: 图片附件本身也是一条 user 消息, 而且排在最后,
|
||||
# 所以第二轮请求的 last_user 是那个附件 (一个 list), 不是这条标记.
|
||||
if "IMAGE_PAIR_TEST" in first_user:
|
||||
if not tool_msgs:
|
||||
@@ -808,7 +808,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
% (len(found), len(tool_msgs)), stream, model)
|
||||
return
|
||||
|
||||
# 图片:先让模型调一次 read_image, 下一轮把收到的图片内联块报回来.
|
||||
# 图片: 先让模型调一次 read_image, 下一轮把收到的图片内联块报回来.
|
||||
# Files API 已经不用了 —— 请求里应该直接是 image_url + data: base64.
|
||||
if "IMAGE_TEST" in first_user:
|
||||
if not tool_msgs:
|
||||
@@ -835,8 +835,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.wfile.flush()
|
||||
return
|
||||
|
||||
# 第二轮:请求里应该已经带上内联图片了.
|
||||
# 报文形状:{"type":"image_url","image_url":{"url":"data:image/png;base64,..."}}
|
||||
# 第二轮: 请求里应该已经带上内联图片了.
|
||||
# 报文形状: {"type":"image_url","image_url":{"url":"data:image/png;base64,..."}}
|
||||
found, notes = [], []
|
||||
for m in messages:
|
||||
c = m.get("content")
|
||||
@@ -856,7 +856,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
elif part.get("type") == "text" and m.get("role") == "user":
|
||||
notes.append(part.get("text", ""))
|
||||
|
||||
# IMAGE_REJECT_TEST:第一次故意说服务端不收这张图,
|
||||
# IMAGE_REJECT_TEST: 第一次故意说服务端不收这张图,
|
||||
# 客户端应该把图片从历史里摘掉再试 (而不是每轮都失败)
|
||||
if "IMAGE_REJECT_TEST" in last_user and not STATE.get("told_once"):
|
||||
STATE["told_once"] = 1
|
||||
@@ -874,7 +874,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_text(reply, stream, model)
|
||||
return
|
||||
|
||||
# edit_file 的**调用展示**:成功时屏幕上该是"路径 + 行号"加 -/+ 新旧对照,
|
||||
# edit_file 的**调用展示**: 成功时屏幕上该是"路径 + 行号"加 -/+ 新旧对照,
|
||||
# 而不是一整段参数 JSON (old_string/new_string 原样打出来会刷屏).
|
||||
# EDITTEST_OK 成功剧本, EDITTEST_FAIL 行号越界剧本.
|
||||
if "EDITTEST_OK" in first_user or "EDITTEST_FAIL" in first_user:
|
||||
@@ -883,7 +883,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
stream, model)
|
||||
return
|
||||
line = 999 if "EDITTEST_FAIL" in first_user else 2
|
||||
# 12 行的 old_string:既覆盖"多行改动"的显示, 也覆盖"超过 8 行要折叠"
|
||||
# 12 行的 old_string: 既覆盖"多行改动"的显示, 也覆盖"超过 8 行要折叠"
|
||||
old = "\n".join("OLD%d" % i for i in range(1, 13)) if line == 2 else "OLD"
|
||||
args = json.dumps({"path": "build/tmp/edit_probe.txt", "line": line,
|
||||
"old_string": old, "new_string": "NEW"})
|
||||
@@ -909,7 +909,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.wfile.flush()
|
||||
return
|
||||
|
||||
# 多步 (4 次工具往返才收工):用来钉住"往返次数默认不限".
|
||||
# 多步 (4 次工具往返才收工): 用来钉住"往返次数默认不限".
|
||||
# 默认配置下必须跑得完; 显式设了正数上限时要停在那个上限上并如实报出.
|
||||
if "MANYSTEPS_TEST" in first_user:
|
||||
done = len(tool_msgs)
|
||||
|
||||
Reference in new issue
Block a user