mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(openai): tool_call delta 重复携带 function.name,增强严格客户端兼容性
在 ToolCallStart 时记录 block_index 到 tool_name 的映射, ToolCallDelta 时同步输出 function.name,避免严格客户端丢弃无 name 的 delta。 提取 _ss_dict 辅助方法统一 stream state 中 dict 字段的初始化逻辑。
This commit is contained in:
@@ -797,7 +797,9 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
tool_name = event.tool_name or ""
|
||||
tool_index = self._ensure_tool_call_index(ss, tool_id, event.block_index)
|
||||
if tool_id:
|
||||
ss.setdefault("block_to_tool_id", {})[int(event.block_index)] = str(tool_id)
|
||||
self._ss_dict(ss, "block_to_tool_id")[int(event.block_index)] = str(tool_id)
|
||||
if tool_name:
|
||||
self._ss_dict(ss, "block_to_tool_name")[int(event.block_index)] = str(tool_name)
|
||||
out.append(
|
||||
base_chunk(
|
||||
{
|
||||
@@ -828,10 +830,7 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
# 构造 data URL 格式的图片
|
||||
data_url = f"data:{image_media_type};base64,{image_data}"
|
||||
# 存储图片数据,在 ContentBlockStopEvent 时输出
|
||||
image_blocks = ss.get("image_blocks")
|
||||
if not isinstance(image_blocks, dict):
|
||||
image_blocks = {}
|
||||
ss["image_blocks"] = image_blocks
|
||||
image_blocks = self._ss_dict(ss, "image_blocks")
|
||||
image_blocks[int(event.block_index)] = {
|
||||
"url": data_url,
|
||||
"media_type": image_media_type,
|
||||
@@ -865,14 +864,18 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
|
||||
if isinstance(event, ToolCallDeltaEvent):
|
||||
tool_id = str(event.tool_id or "")
|
||||
block_to_tool_id = ss.setdefault("block_to_tool_id", {})
|
||||
block_to_tool_id = self._ss_dict(ss, "block_to_tool_id")
|
||||
if not tool_id:
|
||||
tool_id = str(block_to_tool_id.get(int(event.block_index)) or "")
|
||||
else:
|
||||
block_to_tool_id[int(event.block_index)] = tool_id
|
||||
|
||||
tool_name = str(
|
||||
self._ss_dict(ss, "block_to_tool_name").get(int(event.block_index)) or ""
|
||||
)
|
||||
|
||||
tool_index = self._ensure_tool_call_index(ss, tool_id, event.block_index)
|
||||
# 对严格客户端重复携带 id/type,避免它们把后续 delta 视为无效 tool_call。
|
||||
# 对严格客户端重复携带 id/type/name,避免它们把后续 delta 视为无效 tool_call。
|
||||
tc_delta: dict[str, Any] = {
|
||||
"index": tool_index,
|
||||
"type": "function",
|
||||
@@ -880,6 +883,8 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
}
|
||||
if tool_id:
|
||||
tc_delta["id"] = tool_id
|
||||
if tool_name:
|
||||
tc_delta["function"]["name"] = tool_name
|
||||
out.append(base_chunk({"tool_calls": [tc_delta]}))
|
||||
return out
|
||||
|
||||
@@ -1804,11 +1809,17 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
except ValueError:
|
||||
return ErrorType.UNKNOWN
|
||||
|
||||
@staticmethod
|
||||
def _ss_dict(ss: dict[str, Any], key: str) -> dict:
|
||||
"""Get or create a dict entry in stream state."""
|
||||
val = ss.get(key)
|
||||
if not isinstance(val, dict):
|
||||
val = {}
|
||||
ss[key] = val
|
||||
return val
|
||||
|
||||
def _ensure_tool_block_index(self, ss: dict[str, Any], tool_key: str) -> int:
|
||||
mapping = ss.get("tool_id_to_block_index")
|
||||
if not isinstance(mapping, dict):
|
||||
mapping = {}
|
||||
ss["tool_id_to_block_index"] = mapping
|
||||
mapping = self._ss_dict(ss, "tool_id_to_block_index")
|
||||
|
||||
if tool_key in mapping:
|
||||
return int(mapping[tool_key])
|
||||
@@ -1828,10 +1839,7 @@ class OpenAINormalizer(FormatNormalizer):
|
||||
ss["next_tool_index"] = 0
|
||||
|
||||
# block_index -> tool_index 的辅助映射,用于 tool_id 丢失时回落
|
||||
block_mapping: dict[int, int] = ss.get("block_to_tool_index") # type: ignore[assignment]
|
||||
if not isinstance(block_mapping, dict):
|
||||
block_mapping = {}
|
||||
ss["block_to_tool_index"] = block_mapping
|
||||
block_mapping: dict[int, int] = self._ss_dict(ss, "block_to_tool_index") # type: ignore[assignment]
|
||||
|
||||
# 优先用 tool_id 查找
|
||||
if tool_id and tool_id in mapping:
|
||||
|
||||
@@ -858,6 +858,7 @@ def test_tool_call_stream_index_stable_across_deltas() -> None:
|
||||
# 收集所有 tool_calls chunk
|
||||
tc_indices: list[int] = []
|
||||
tc_ids: list[str] = []
|
||||
tc_names: list[str] = []
|
||||
for event in all_events:
|
||||
for choice in event.get("choices", []):
|
||||
tcs = choice.get("delta", {}).get("tool_calls")
|
||||
@@ -865,6 +866,7 @@ def test_tool_call_stream_index_stable_across_deltas() -> None:
|
||||
for tc in tcs:
|
||||
tc_indices.append(tc["index"])
|
||||
tc_ids.append(str(tc.get("id") or ""))
|
||||
tc_names.append(str((tc.get("function") or {}).get("name") or ""))
|
||||
|
||||
assert len(tc_indices) >= 2, f"expected at least 2 tool_call chunks, got {len(tc_indices)}"
|
||||
# 同一个 tool call 的所有 chunk 必须使用相同的 index
|
||||
@@ -875,3 +877,7 @@ def test_tool_call_stream_index_stable_across_deltas() -> None:
|
||||
"tool_call id should be repeated on every delta for strict OpenAI-compatible clients, "
|
||||
f"got: {tc_ids}"
|
||||
)
|
||||
assert all(tc_name == "get_weather" for tc_name in tc_names), (
|
||||
"tool_call function.name should be repeated on every delta for strict "
|
||||
f"OpenAI-compatible clients, got: {tc_names}"
|
||||
)
|
||||
|
||||
@@ -332,6 +332,7 @@ def test_openai_stream_chunk_and_event_roundtrip_basic() -> None:
|
||||
)
|
||||
assert tool_delta["choices"][0]["delta"]["tool_calls"][0]["id"] == "call_1"
|
||||
assert tool_delta["choices"][0]["delta"]["tool_calls"][0]["index"] == 0
|
||||
assert tool_delta["choices"][0]["delta"]["tool_calls"][0]["function"]["name"] == "get_weather"
|
||||
|
||||
# 最终 stop chunk finish_reason=tool_calls
|
||||
assert out_chunks[-1]["choices"][0]["finish_reason"] == "tool_calls"
|
||||
|
||||
Reference in New Issue
Block a user