fix(conversion): 修复 openai cli tool 调用空 call_id 导致上游失败

- 在 FormatConversionRegistry.convert_request 前统一修复 InternalRequest 中空的 tool_id/tool_use_id

- 为空 ID 自动生成 call_auto_N,并将 tool_result 关联到最近待匹配的 tool_call

- 修正 OpenAICliNormalizer 中 function_call_output 的内部角色为 USER,确保输出 openai:chat 时保留 tool_call_id

- 新增 openai:cli <-> openai:chat 空 call_id 回归测试,覆盖工具调用关联场景
This commit is contained in:
hemo94931
2026-02-13 12:32:29 +08:00
parent 4c83780b5e
commit e80bcabccf
3 changed files with 120 additions and 1 deletions

View File

@@ -181,6 +181,89 @@ def test_openai_cli_function_call_to_claude() -> None:
assert content2[0]["content"] == "file1.txt\nfile2.txt"
def test_openai_cli_empty_call_id_repaired_when_convert_to_openai_chat() -> None:
"""空 call_id 会在转换链路中被自动修复,并保持 tool 调用关联。"""
reg = _make_registry_with_cli()
openai_cli_req = {
"model": "gpt-5",
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "请读取 config"}],
},
{
"type": "function_call",
"name": "read_file",
"arguments": '{"path": "config.yaml"}',
"call_id": "",
},
{
"type": "function_call_output",
"call_id": "",
"output": "ok",
},
],
}
out = reg.convert_request(openai_cli_req, "openai:cli", "openai:chat")
messages = cast(list[dict[str, Any]], out.get("messages") or [])
assistant_msg = next((m for m in messages if m.get("role") == "assistant"), {})
tool_calls = cast(list[dict[str, Any]], assistant_msg.get("tool_calls") or [])
assert len(tool_calls) == 1
generated_id = str(tool_calls[0].get("id") or "")
assert generated_id.startswith("call_auto_")
tool_msg = next((m for m in messages if m.get("role") == "tool"), {})
assert tool_msg.get("tool_call_id") == generated_id
def test_openai_chat_empty_tool_call_id_repaired_when_convert_to_openai_cli() -> None:
"""openai:chat 的空 tool_call_id 转 openai:cli 时应生成有效 call_id。"""
reg = _make_registry_with_cli()
openai_chat_req = {
"model": "gpt-5",
"messages": [
{"role": "user", "content": "帮我读取 README"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "",
"type": "function",
"function": {
"name": "read_file",
"arguments": '{"path":"README.md"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "",
"content": "done",
},
],
}
out = reg.convert_request(openai_chat_req, "openai:chat", "openai:cli")
input_items = cast(list[dict[str, Any]], out.get("input") or [])
function_call = next((i for i in input_items if i.get("type") == "function_call"), {})
function_call_output = next(
(i for i in input_items if i.get("type") == "function_call_output"), {}
)
generated_id = str(function_call.get("call_id") or "")
assert generated_id.startswith("call_auto_")
assert function_call_output.get("call_id") == generated_id
def test_openai_cli_reasoning_preserved_in_roundtrip() -> None:
"""测试 OpenAI CLI 的 reasoning block 在 roundtrip 中被保留"""
reg = _make_registry_with_cli()