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:
fawney19
2026-03-15 22:04:21 +08:00
parent d58c27d22d
commit 60842fbbb5
3 changed files with 30 additions and 15 deletions

View File

@@ -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: