mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge pull request #174 from hemo94931/dev
fix(conversion): 修复 openai cli tool 调用空 call_id 导致上游失败
This commit is contained in:
@@ -1055,7 +1055,7 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
content_text=content_text,
|
content_text=content_text,
|
||||||
extra={"openai_cli": self._extract_extra(item, {"type", "call_id", "id", "output"})},
|
extra={"openai_cli": self._extract_extra(item, {"type", "call_id", "id", "output"})},
|
||||||
)
|
)
|
||||||
return InternalMessage(role=Role.TOOL, content=[result_block])
|
return InternalMessage(role=Role.USER, content=[result_block])
|
||||||
|
|
||||||
def _parse_reasoning_item(self, item: dict[str, Any]) -> InternalMessage:
|
def _parse_reasoning_item(self, item: dict[str, Any]) -> InternalMessage:
|
||||||
summary_parts: list[str] = []
|
summary_parts: list[str] = []
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from collections.abc import Generator
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from src.core.api_format.conversion.internal import InternalRequest, ToolResultBlock, ToolUseBlock
|
||||||
from src.core.api_format.conversion.exceptions import FormatConversionError
|
from src.core.api_format.conversion.exceptions import FormatConversionError
|
||||||
from src.core.api_format.conversion.normalizer import FormatNormalizer
|
from src.core.api_format.conversion.normalizer import FormatNormalizer
|
||||||
from src.core.api_format.conversion.stream_state import StreamState
|
from src.core.api_format.conversion.stream_state import StreamState
|
||||||
@@ -60,6 +61,40 @@ class FormatConversionRegistry:
|
|||||||
raise FormatConversionError(format_id, format_id, f"未注册 Normalizer: {format_id}")
|
raise FormatConversionError(format_id, format_id, f"未注册 Normalizer: {format_id}")
|
||||||
return normalizer
|
return normalizer
|
||||||
|
|
||||||
|
def _repair_internal_tool_call_ids(self, internal: InternalRequest) -> None:
|
||||||
|
"""修复 InternalRequest 中空的 tool id/tool_use_id,避免上游校验报错。"""
|
||||||
|
|
||||||
|
pending_tool_ids: list[str] = []
|
||||||
|
auto_counter = 0
|
||||||
|
|
||||||
|
def next_tool_id() -> str:
|
||||||
|
nonlocal auto_counter
|
||||||
|
auto_counter += 1
|
||||||
|
return f"call_auto_{auto_counter}"
|
||||||
|
|
||||||
|
for message in internal.messages:
|
||||||
|
for block in message.content:
|
||||||
|
if isinstance(block, ToolUseBlock):
|
||||||
|
tool_id = str(block.tool_id or "").strip()
|
||||||
|
if not tool_id:
|
||||||
|
tool_id = next_tool_id()
|
||||||
|
block.tool_id = tool_id
|
||||||
|
pending_tool_ids.append(tool_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(block, ToolResultBlock):
|
||||||
|
tool_use_id = str(block.tool_use_id or "").strip()
|
||||||
|
if tool_use_id:
|
||||||
|
block.tool_use_id = tool_use_id
|
||||||
|
if tool_use_id in pending_tool_ids:
|
||||||
|
pending_tool_ids.remove(tool_use_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if pending_tool_ids:
|
||||||
|
block.tool_use_id = pending_tool_ids.pop(0)
|
||||||
|
else:
|
||||||
|
block.tool_use_id = next_tool_id()
|
||||||
|
|
||||||
# ==================== 请求/响应转换(严格) ====================
|
# ==================== 请求/响应转换(严格) ====================
|
||||||
|
|
||||||
def convert_request(
|
def convert_request(
|
||||||
@@ -81,6 +116,7 @@ class FormatConversionRegistry:
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
internal = src.request_to_internal(request)
|
internal = src.request_to_internal(request)
|
||||||
|
self._repair_internal_tool_call_ids(internal)
|
||||||
return tgt.request_from_internal(internal, target_variant=target_variant)
|
return tgt.request_from_internal(internal, target_variant=target_variant)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise FormatConversionError(source_format, target_format, str(e)) from e
|
raise FormatConversionError(source_format, target_format, str(e)) from e
|
||||||
|
|||||||
@@ -181,6 +181,89 @@ def test_openai_cli_function_call_to_claude() -> None:
|
|||||||
assert content2[0]["content"] == "file1.txt\nfile2.txt"
|
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:
|
def test_openai_cli_reasoning_preserved_in_roundtrip() -> None:
|
||||||
"""测试 OpenAI CLI 的 reasoning block 在 roundtrip 中被保留"""
|
"""测试 OpenAI CLI 的 reasoning block 在 roundtrip 中被保留"""
|
||||||
reg = _make_registry_with_cli()
|
reg = _make_registry_with_cli()
|
||||||
|
|||||||
Reference in New Issue
Block a user