Skip to content

Commit 063e82d

Browse files
committed
fix: TRAE body 归一 developer 角色 + tool_calls 转 function_call
用 DUMP_REQUEST_BODIES 抓到的 PI 真实请求体逐项排查 TRAE 失败: 1. tool_calls 历史格式:OpenAI 是 tool_calls[].function{name,arguments}, TRAE SOLO 要求 tool_calls[].function_call——原样透传导致上游 protobuf 解析错(code=2001 required field Name is not set) → prepare_body 转换 function→function_call,无 name 剔除,全空删除 2. developer 角色在 TRAE 同样不被接受,但不报错而是静默空流 (code=3003 llm response empty stream)→ 同 CB 归一为 system 试验矩阵定位:function_call 转换修掉 2001 后仍 3003,逐层裁剪 (tool 历史/消息数/内容长度)最终二分到 developer 角色—— 实测 developer=3003、system=ok,同一 body 其余不变。 端到端验证:PI 真实请求体(developer+33消息+7工具+tool历史) → 200 正常生成 reasoning 与 tool_calls。后端 643 测试 / 覆盖 100%。
1 parent 5f2a317 commit 063e82d

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

‎src/provider/trae/client.py‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ def prepare_body(payload: dict[str, Any], model: str) -> dict[str, Any]:
8383
body["model"] = model
8484

8585
messages = body.get("messages")
86+
if isinstance(messages, list):
87+
for message in messages:
88+
# OpenAI developer 角色(PI 等客户端对 reasoning 模型使用)
89+
# TRAE 上游不认:静默返回空流(3003)→ 归一为 system
90+
if isinstance(message, dict) and message.get("role") == "developer":
91+
message["role"] = "system"
8692
if isinstance(messages, list):
8793
rewritten: list[Any] = []
8894
for message in messages:
@@ -97,12 +103,25 @@ def prepare_body(payload: dict[str, Any], model: str) -> dict[str, Any]:
97103
pass # 已是数组 → 透传
98104
tool_calls = item.get("tool_calls")
99105
if isinstance(tool_calls, list):
100-
kept = [tc for tc in tool_calls if isinstance(tc, dict)]
106+
kept = []
107+
for tc in tool_calls:
108+
if not isinstance(tc, dict):
109+
continue
110+
fn = tc.get("function")
111+
if not isinstance(fn, dict):
112+
continue
113+
# OpenAI function{name,arguments} → SOLO function_call;
114+
# 上游要求 FunctionCall.Name 必填,无 name 的剔除
115+
if not str(fn.get("name") or "").strip():
116+
continue
117+
tc = dict(tc)
118+
tc["function_call"] = fn
119+
del tc["function"]
120+
kept.append(tc)
101121
if kept:
102122
item["tool_calls"] = kept
103123
else:
104124
item.pop("tool_calls", None)
105-
item.pop("content", None) if not content else None
106125
rewritten.append(item)
107126
body["messages"] = rewritten
108127

‎tests/test_m1a_trae.py‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,31 @@ def test_prepare_body_drops_empty_tool_calls_and_content():
252252
body = prepare_body({"messages": [{"role": "assistant", "content": None,
253253
"tool_calls": []}]}, "m")
254254
assert "tool_calls" not in body["messages"][0]
255-
assert "content" not in body["messages"][0]
255+
256+
257+
def test_prepare_body_skips_non_dict_tool_call_entries():
258+
"""非对象 tool_call 条目跳过(102-103 分支)。"""
259+
body = prepare_body({"messages": [{"role": "assistant", "content": None,
260+
"tool_calls": ["junk", {"id": "c", "type": "function",
261+
"function": {"name": "bash", "arguments": "{}"}}]}]}, "m")
262+
tcs = body["messages"][0]["tool_calls"]
263+
assert len(tcs) == 1 and tcs[0]["function_call"]["name"] == "bash"
264+
assert body["messages"][0]["content"] is None # 原实现保留 nil content
256265

257266

258267
def test_prepare_body_keeps_valid_tool_calls():
268+
"""有 name 的 function → function_call;无 function/name 的剔除。"""
259269
body = prepare_body({"messages": [{"role": "assistant", "content": "x",
260-
"tool_calls": [{"id": "c"}]}]}, "m")
261-
assert body["messages"][0]["tool_calls"] == [{"id": "c"}]
270+
"tool_calls": [
271+
{"id": "c1", "type": "function",
272+
"function": {"name": "bash", "arguments": "{\"a\":1}"}},
273+
{"id": "c2", "type": "function", "function": {"arguments": "{}"}},
274+
{"id": "c3"},
275+
]}]}, "m")
276+
tcs = body["messages"][0]["tool_calls"]
277+
assert len(tcs) == 1 and tcs[0]["id"] == "c1"
278+
assert tcs[0]["function_call"] == {"name": "bash", "arguments": "{\"a\":1}"}
279+
assert "function" not in tcs[0]
262280

263281

264282
# ------------------------------------------------------------ 模型解析
@@ -955,3 +973,24 @@ async def stream_chat(self, cred, payload, model):
955973
waited.clear()
956974
_ = [e async for e in provider2.stream_chat({"accessToken": "a"}, {}, "m")]
957975
assert not waited
976+
977+
978+
def test_prepare_body_drops_unnamed_function_call():
979+
"""function_call 无 name 剔除后若全空 → 整个 tool_calls 删除(102-103)。"""
980+
body = prepare_body({"messages": [
981+
{"role": "assistant", "content": None,
982+
"tool_calls": [{"id": "c", "type": "function",
983+
"function": {"arguments": "{}"}}]},
984+
]}, "m")
985+
assert "tool_calls" not in body["messages"][0]
986+
987+
988+
989+
def test_prepare_body_normalizes_developer_role():
990+
"""TRAE 不认 developer 角色(静默空流 3003)→ 归一 system。"""
991+
body = prepare_body({"messages": [
992+
{"role": "developer", "content": "You are PI."},
993+
{"role": "user", "content": "hi"},
994+
]}, "m")
995+
roles = [m["role"] for m in body["messages"]]
996+
assert roles == ["system", "user"]

0 commit comments

Comments
 (0)