mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +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_name = event.tool_name or ""
|
||||||
tool_index = self._ensure_tool_call_index(ss, tool_id, event.block_index)
|
tool_index = self._ensure_tool_call_index(ss, tool_id, event.block_index)
|
||||||
if tool_id:
|
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(
|
out.append(
|
||||||
base_chunk(
|
base_chunk(
|
||||||
{
|
{
|
||||||
@@ -828,10 +830,7 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
# 构造 data URL 格式的图片
|
# 构造 data URL 格式的图片
|
||||||
data_url = f"data:{image_media_type};base64,{image_data}"
|
data_url = f"data:{image_media_type};base64,{image_data}"
|
||||||
# 存储图片数据,在 ContentBlockStopEvent 时输出
|
# 存储图片数据,在 ContentBlockStopEvent 时输出
|
||||||
image_blocks = ss.get("image_blocks")
|
image_blocks = self._ss_dict(ss, "image_blocks")
|
||||||
if not isinstance(image_blocks, dict):
|
|
||||||
image_blocks = {}
|
|
||||||
ss["image_blocks"] = image_blocks
|
|
||||||
image_blocks[int(event.block_index)] = {
|
image_blocks[int(event.block_index)] = {
|
||||||
"url": data_url,
|
"url": data_url,
|
||||||
"media_type": image_media_type,
|
"media_type": image_media_type,
|
||||||
@@ -865,14 +864,18 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
|
|
||||||
if isinstance(event, ToolCallDeltaEvent):
|
if isinstance(event, ToolCallDeltaEvent):
|
||||||
tool_id = str(event.tool_id or "")
|
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:
|
if not tool_id:
|
||||||
tool_id = str(block_to_tool_id.get(int(event.block_index)) or "")
|
tool_id = str(block_to_tool_id.get(int(event.block_index)) or "")
|
||||||
else:
|
else:
|
||||||
block_to_tool_id[int(event.block_index)] = tool_id
|
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)
|
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] = {
|
tc_delta: dict[str, Any] = {
|
||||||
"index": tool_index,
|
"index": tool_index,
|
||||||
"type": "function",
|
"type": "function",
|
||||||
@@ -880,6 +883,8 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
}
|
}
|
||||||
if tool_id:
|
if tool_id:
|
||||||
tc_delta["id"] = tool_id
|
tc_delta["id"] = tool_id
|
||||||
|
if tool_name:
|
||||||
|
tc_delta["function"]["name"] = tool_name
|
||||||
out.append(base_chunk({"tool_calls": [tc_delta]}))
|
out.append(base_chunk({"tool_calls": [tc_delta]}))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -1804,11 +1809,17 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return ErrorType.UNKNOWN
|
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:
|
def _ensure_tool_block_index(self, ss: dict[str, Any], tool_key: str) -> int:
|
||||||
mapping = ss.get("tool_id_to_block_index")
|
mapping = self._ss_dict(ss, "tool_id_to_block_index")
|
||||||
if not isinstance(mapping, dict):
|
|
||||||
mapping = {}
|
|
||||||
ss["tool_id_to_block_index"] = mapping
|
|
||||||
|
|
||||||
if tool_key in mapping:
|
if tool_key in mapping:
|
||||||
return int(mapping[tool_key])
|
return int(mapping[tool_key])
|
||||||
@@ -1828,10 +1839,7 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
ss["next_tool_index"] = 0
|
ss["next_tool_index"] = 0
|
||||||
|
|
||||||
# block_index -> tool_index 的辅助映射,用于 tool_id 丢失时回落
|
# block_index -> tool_index 的辅助映射,用于 tool_id 丢失时回落
|
||||||
block_mapping: dict[int, int] = ss.get("block_to_tool_index") # type: ignore[assignment]
|
block_mapping: dict[int, int] = self._ss_dict(ss, "block_to_tool_index") # type: ignore[assignment]
|
||||||
if not isinstance(block_mapping, dict):
|
|
||||||
block_mapping = {}
|
|
||||||
ss["block_to_tool_index"] = block_mapping
|
|
||||||
|
|
||||||
# 优先用 tool_id 查找
|
# 优先用 tool_id 查找
|
||||||
if tool_id and tool_id in mapping:
|
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
|
# 收集所有 tool_calls chunk
|
||||||
tc_indices: list[int] = []
|
tc_indices: list[int] = []
|
||||||
tc_ids: list[str] = []
|
tc_ids: list[str] = []
|
||||||
|
tc_names: list[str] = []
|
||||||
for event in all_events:
|
for event in all_events:
|
||||||
for choice in event.get("choices", []):
|
for choice in event.get("choices", []):
|
||||||
tcs = choice.get("delta", {}).get("tool_calls")
|
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:
|
for tc in tcs:
|
||||||
tc_indices.append(tc["index"])
|
tc_indices.append(tc["index"])
|
||||||
tc_ids.append(str(tc.get("id") or ""))
|
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)}"
|
assert len(tc_indices) >= 2, f"expected at least 2 tool_call chunks, got {len(tc_indices)}"
|
||||||
# 同一个 tool call 的所有 chunk 必须使用相同的 index
|
# 同一个 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, "
|
"tool_call id should be repeated on every delta for strict OpenAI-compatible clients, "
|
||||||
f"got: {tc_ids}"
|
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]["id"] == "call_1"
|
||||||
assert tool_delta["choices"][0]["delta"]["tool_calls"][0]["index"] == 0
|
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
|
# 最终 stop chunk finish_reason=tool_calls
|
||||||
assert out_chunks[-1]["choices"][0]["finish_reason"] == "tool_calls"
|
assert out_chunks[-1]["choices"][0]["finish_reason"] == "tool_calls"
|
||||||
|
|||||||
Reference in New Issue
Block a user