Skip to content

Commit 42166a0

Browse files
committed
fix: TRAE tool_calls 出站归一为 OpenAI function 形状(Tool not found 根因)
之前转发给客户端的 TRAE tool_calls 保留了 SOLO 格式的 function_call 键,PI 按 OpenAI 协议读 function.name 读到空 → 显示 {} + Tool not found。 - _normalize_solo_tool_call:function_call → function 归一, 无 name 条目丢弃;_named_tool_calls 统一走归一路径 - checkin 测试固定 due()=False,消除真实时钟依赖 重放验证:出站 tool_calls 帧键形状为 OpenAI 标准 function。 后端 650 测试 / 覆盖 100%。
1 parent db8bdbe commit 42166a0

4 files changed

Lines changed: 24 additions & 11 deletions

File tree

‎src/provider/trae/events.py‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,33 @@ def parse_frame(frame: SSEFrame) -> Event | None:
8282
f"unhandled known event {name!r}")
8383

8484

85-
def _solo_tool_call_name(item: Any) -> str:
86-
"""SOLO 增量块里工具名(function_call 或 function 下)。"""
85+
def _normalize_solo_tool_call(item: Any) -> dict | None:
86+
"""SOLO tool_call → OpenAI 形状。
87+
88+
上游流里的 tool_call 用 function_call{name, arguments}(SOLO 协议),
89+
客户端契约是 OpenAI 的 function{name, arguments}。缺失/无 name 的
90+
条目丢弃(客户端无法执行)。
91+
"""
8792
if not isinstance(item, dict):
88-
return ""
93+
return None
8994
fn = item.get("function_call")
9095
if not isinstance(fn, dict):
9196
fn = item.get("function")
92-
if not isinstance(fn, dict):
93-
return ""
94-
return fn.get("name") if isinstance(fn.get("name"), str) else ""
97+
if not isinstance(fn, dict) or not str(fn.get("name") or "").strip():
98+
return None
99+
out = {k: v for k, v in item.items() if k not in ("function", "function_call")}
100+
out["function"] = fn
101+
return out
95102

96103

97104
def _named_tool_calls(tool_calls: list) -> list:
98-
"""剔除 name 为空的 tool_call 增量(上游分片噪声,PI 无法执行)。"""
99-
return [tc for tc in tool_calls if _solo_tool_call_name(tc)]
105+
"""归一为 OpenAI 形状并剔除无 name 的条目。"""
106+
out = []
107+
for tc in tool_calls:
108+
normalized = _normalize_solo_tool_call(tc)
109+
if normalized is not None:
110+
out.append(normalized)
111+
return out
100112

101113

102114
def _parse_output(payload: dict) -> Event | None:

‎tests/test_m1a_coverage.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ def test_parse_all_events_orders_tools_first():
305305
data='{"response":"x","tool_calls":[{"id":"c"},{"id":"d","function_call":{"name":"f"}}]}')
306306
events = trae_events.parse_all_events(noisy)
307307
tools = [e for e in events if e.kind is EventKind.TOOL_CALLS]
308-
assert len(tools) == 1 and tools[0].tool_calls[0]["function_call"]["name"] == "f"
308+
assert len(tools) == 1 and tools[0].tool_calls[0]["function"]["name"] == "f"
309309

310310

311311
def test_parse_all_events_passes_through_non_output_frames():

‎tests/test_m1a_trae.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1041,7 +1041,7 @@ def test_parse_all_events_non_dict_tool_call_entry_skipped():
10411041
data='{"tool_calls":["junk",{"function_call":{"name":"f","arguments":"{}"}}]}')
10421042
events = trae_events.parse_all_events(frame)
10431043
tools = [e for e in events if e.kind is EventKind.TOOL_CALLS]
1044-
assert len(tools) == 1 and tools[0].tool_calls[0]["function_call"]["name"] == "f"
1044+
assert len(tools) == 1 and tools[0].tool_calls[0]["function"]["name"] == "f"
10451045

10461046
empty = trae_events.SSEFrame(
10471047
event="output",

‎tests/test_task_runner.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,10 @@ async def run_once(self, **_kwargs):
140140

141141

142142
async def test_runner_checkin_skips_before_hour(repo):
143-
"""未到签到时刻时 _sync_checkin 是 no-op。"""
143+
"""未到签到时刻(due()=False)时 _sync_checkin 是 no-op。"""
144144
provider = StubProvider()
145145
runner, _collector = _runner(repo, provider)
146+
runner._checkin.due = lambda **kwargs: False # 固定"未到点",避免真实时钟依赖
146147
assert await runner._sync_checkin() is None
147148

148149

0 commit comments

Comments
 (0)