Skip to content

fix: 修复工具调用名重复拼接问题(流式响应chunk重复累积) - #7735

Closed
Blueteemo wants to merge 6 commits into
AstrBotDevs:masterfrom
Blueteemo:fix/issue-7694-tool-call-name-deduplication
Closed

Blueteemo wants to merge 6 commits into
AstrBotDevs:masterfrom
Blueteemo:fix/issue-7694-tool-call-name-deduplication

Conversation

@Blueteemo

@Blueteemo Blueteemo commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

问题描述

使用 MiniMax 模型通过 NVIDIA 代理调用时,工具调用名出现重复拼接:

  • astr_kb_searchastr_kb_searchastr_kb_search
  • call_xxxcall_xxxcall_xxx

这是因为流式响应处理过程中 chunk 被重复累积导致的。

修复方案

openai_source.py_parse_openai_completion 方法中,对 tool call 的 idname 增加去重校验。当检测到字符串是由前半段和后半段相同的“自重复”模式构成时(例如 abcabcabc),取前半段作为正确值。

修改内容

  1. ProviderOpenAIOfficial 类中添加 _deduplicate_self_repeating 静态方法,用于检测并修复自重复字符串。
  2. 在解析 tool_calls 时,对 function.nameidextra_content 的 key 都应用去重处理。

关联 Issue

Fixes #7694

Summary by Sourcery

Deduplicate repeated values in OpenAI-compatible streaming tool calls to restore correct tool invocation behavior.

Bug Fixes:

  • Prevent duplicated tool-call names, IDs, and string arguments caused by repeated accumulation of streaming response chunks.

Enhancements:

  • Normalize self-repeating tool-call values before parsing arguments and associating extra tool-call content.

@auto-assign
auto-assign Bot requested review from Fridemn and LIghtJUNction April 22, 2026 21:12
@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels Apr 22, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The _deduplicate_self_repeating helper only detects exactly-two-part repetitions (abcabc) and skips shorter values, so if other repetition patterns (e.g. abcabcabc or abab) might occur in streaming, consider either documenting this limitation or generalizing the detection logic to handle arbitrary repeated substrings.
  • When deduplicating tool_call.id, None is converted to an empty string; if downstream logic distinguishes between None and a real (even if malformed) id, it may be safer to short-circuit and skip deduplication when id is falsy instead of normalizing to "".
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_deduplicate_self_repeating` helper only detects exactly-two-part repetitions (`abcabc`) and skips shorter values, so if other repetition patterns (e.g. `abcabcabc` or `abab`) might occur in streaming, consider either documenting this limitation or generalizing the detection logic to handle arbitrary repeated substrings.
- When deduplicating `tool_call.id`, `None` is converted to an empty string; if downstream logic distinguishes between `None` and a real (even if malformed) id, it may be safer to short-circuit and skip deduplication when `id` is falsy instead of normalizing to `""`.

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/openai_source.py" line_range="883-884" />
<code_context>
+                    func_name_ls.append(
+                        cls._deduplicate_self_repeating(tool_call.function.name)
+                    )
+                    tool_call_ids.append(
+                        cls._deduplicate_self_repeating(tool_call.id or "")
+                    )

</code_context>
<issue_to_address>
**issue (bug_risk):** Converting `None` tool_call IDs to an empty string may change behavior and cause key collisions.

Previously, `tool_call_ids.append(tool_call.id)` preserved `None`, but `tool_call.id or ""` now turns `None` into `""` and uses that as the key in `tool_call_extra_content_dict`. Multiple `None` IDs will all map to `""`, potentially merging distinct entries and changing behavior. Consider either keeping `None` as-is (e.g., branch on `tool_call.id is None`) or updating `_deduplicate_self_repeating` to accept `Optional[str]` and leave `None` unchanged.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/provider/sources/openai_source.py Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a _deduplicate_self_repeating helper method to address streaming duplication issues in tool call names and IDs. Reviewers identified a critical bug where cls is used instead of self in an instance method, which will lead to a runtime error. Furthermore, the deduplication logic is noted to be overly simplistic, potentially affecting valid strings or failing for triple repetitions, and requires unit tests for validation.

Comment thread astrbot/core/provider/sources/openai_source.py
Comment thread astrbot/core/provider/sources/openai_source.py Outdated
Comment thread astrbot/core/provider/sources/openai_source.py Outdated
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Apr 22, 2026
@Blueteemo

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The _deduplicate_self_repeating heuristic is quite aggressive (e.g., it will collapse aaaa to a or abcabcabc to abc), which risks corrupting legitimately repetitive IDs/names; consider constraining it to simple ABAB double-repeat patterns or adding additional guards (e.g., minimum unit size, character set pattern) to better match the known MiniMax/NVIDIA failure mode.
  • You currently recompute _deduplicate_self_repeating(tool_call.id) multiple times; consider deduplicating once into a local deduped_id and reusing it for tool_call_ids and tool_call_extra_content_dict to avoid divergence and keep the logic clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_deduplicate_self_repeating` heuristic is quite aggressive (e.g., it will collapse `aaaa` to `a` or `abcabcabc` to `abc`), which risks corrupting legitimately repetitive IDs/names; consider constraining it to simple `ABAB` double-repeat patterns or adding additional guards (e.g., minimum unit size, character set pattern) to better match the known MiniMax/NVIDIA failure mode.
- You currently recompute `_deduplicate_self_repeating(tool_call.id)` multiple times; consider deduplicating once into a local `deduped_id` and reusing it for `tool_call_ids` and `tool_call_extra_content_dict` to avoid divergence and keep the logic clearer.

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/openai_source.py" line_range="891-902" />
<code_context>
+                    func_name_ls.append(
+                        self._deduplicate_self_repeating(tool_call.function.name)
+                    )
+                    tool_call_ids.append(self._deduplicate_self_repeating(tool_call.id))

                     # gemini-2.5 / gemini-3 series extra_content handling
                     extra_content = getattr(tool_call, "extra_content", None)
                     if extra_content is not None:
-                        tool_call_extra_content_dict[tool_call.id] = extra_content
+                        deduped_id = self._deduplicate_self_repeating(tool_call.id)
+                        if deduped_id is not None:
+                            tool_call_extra_content_dict[deduped_id] = extra_content
</code_context>
<issue_to_address>
**suggestion:** Reuse the same deduplicated ID instance instead of recomputing it to keep behavior consistent.

The ID is currently deduplicated twice: once when appending to `tool_call_ids` and again when setting `deduped_id` for `tool_call_extra_content_dict`. This risks subtle divergence if `_deduplicate_self_repeating` changes and adds unnecessary work. Compute `deduped_id` once, append it to `tool_call_ids`, and reuse it as the key in `tool_call_extra_content_dict`.

```suggestion
                    args_ls.append(args)
                    func_name_ls.append(
                        self._deduplicate_self_repeating(tool_call.function.name)
                    )
                    deduped_id = self._deduplicate_self_repeating(tool_call.id)
                    tool_call_ids.append(deduped_id)

                    # gemini-2.5 / gemini-3 series extra_content handling
                    extra_content = getattr(tool_call, "extra_content", None)
                    if extra_content is not None and deduped_id is not None:
                        tool_call_extra_content_dict[deduped_id] = extra_content
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/provider/sources/openai_source.py Outdated
@zouyonghe

Copy link
Copy Markdown
Member

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The _deduplicate_self_repeating helper collapses any fully repeating pattern (e.g. ababab, hahahaha), which might unintentionally alter legitimate tool names/IDs/arguments; consider constraining it to the specific duplication pattern you’ve observed (e.g., exact double repetition with a minimum length) or scoping it to known-affected providers.
  • When deduplicating function.arguments before json.loads, this can change the original payload beyond just fixing chunk duplication; if that’s not desired, consider limiting deduplication to identifiers (id, name) and only applying it to arguments when you can positively detect the streaming concatenation pattern.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_deduplicate_self_repeating` helper collapses any fully repeating pattern (e.g. `abab``ab`, `hahaha``ha`), which might unintentionally alter legitimate tool names/IDs/arguments; consider constraining it to the specific duplication pattern you’ve observed (e.g., exact double repetition with a minimum length) or scoping it to known-affected providers.
- When deduplicating `function.arguments` before `json.loads`, this can change the original payload beyond just fixing chunk duplication; if that’s not desired, consider limiting deduplication to identifiers (`id`, `name`) and only applying it to arguments when you can positively detect the streaming concatenation pattern.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@zouyonghe zouyonghe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我这边补充一个回归风险:

当前 _deduplicate_self_repeating() 会对所有 OpenAI 兼容 provider 的 tool call 名和 id 无条件生效,只要字符串满足“前半段 == 后半段”就会被折叠,例如合法的 pingping 会被改成 pingabab 会被改成 ab

_parse_openai_completion()openai_source.py 的共享解析路径,普通非流式和流式最终都会走这里,所以这个改动不只影响 MiniMax/NVIDIA 的异常场景,也会影响正常 provider 的合法工具名 / id。后续工具查找是按折叠后的名字继续执行的,这会导致原本存在的工具变成 tool not found

建议至少把这个修复收窄到已知受影响的 provider / model,或者增加更强的判定信号,而不是对所有 OpenAI 兼容返回统一改写。

@Blueteemo
Blueteemo force-pushed the fix/issue-7694-tool-call-name-deduplication branch from f23d255 to e329971 Compare April 23, 2026 05:59
@Blueteemo

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The _deduplicate_self_repeating helper only detects exactly two repetitions (e.g. abcabc) with a relatively high min_length default of 20; if streaming ever yields 3+ repeats or shorter tool names/IDs, the bug will still surface—consider making the repetition detection more general and/or lowering or parameterizing the length threshold.
  • By applying _deduplicate_self_repeating to both tool_call.id and the extra_content key, different original IDs that collapse to the same base string will now collide in tool_call_extra_content_dict; it may be worth explicitly deciding how to handle collisions or at least guarding/logging when a key is being overwritten.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_deduplicate_self_repeating` helper only detects exactly two repetitions (e.g. `abcabc`) with a relatively high `min_length` default of 20; if streaming ever yields 3+ repeats or shorter tool names/IDs, the bug will still surface—consider making the repetition detection more general and/or lowering or parameterizing the length threshold.
- By applying `_deduplicate_self_repeating` to both `tool_call.id` and the `extra_content` key, different original IDs that collapse to the same base string will now collide in `tool_call_extra_content_dict`; it may be worth explicitly deciding how to handle collisions or at least guarding/logging when a key is being overwritten.

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/openai_source.py" line_range="878-882" />
<code_context>
                 if tool_call.type == "function":
                     # workaround for #1454
                     if isinstance(tool_call.function.arguments, str):
+                        deduped_args = self._deduplicate_self_repeating(
+                            tool_call.function.arguments
+                        )
                         try:
-                            args = json.loads(tool_call.function.arguments)
+                            args = json.loads(deduped_args)
                         except json.JSONDecodeError as e:
                             logger.error(f"解析参数失败: {e}")
</code_context>
<issue_to_address>
**issue:** Consider guarding json.loads() against unexpected None from the deduplication helper.

`_deduplicate_self_repeating` is typed to allow returning `None`, but `json.loads(None)` would raise a `TypeError` that isn’t caught by the `JSONDecodeError` handler. It’s currently safe only because the caller always passes a `str`. To avoid a future unhandled exception, either make `_deduplicate_self_repeating` return `str` when given `str`, or guard before parsing, e.g. `if deduped_args is None: args = {};` (or another explicit fallback) before calling `json.loads`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/provider/sources/openai_source.py

@Blueteemo Blueteemo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已解决。感谢反馈,min_length=20 参数可以有效保护短名称不被误伤,且只处理正好 2 次重复的情况,平衡了修复效果和回归风险。

@Blueteemo

Copy link
Copy Markdown
Contributor Author

补充:关于 Sourcery 提出的 json.loads(None) 会抛 TypeError 的问题,经分析这是误报。代码中有 isinstance guard:

if isinstance(tool_call.function.arguments, str): deduped_args = self._deduplicate_self_repeating(...) args = json.loads(deduped_args)

只有 arguments 是 str 时才调用 json.loads,不会收到 None。所有问题均已处理。

@x1051445024

Copy link
Copy Markdown
Contributor

[现场报告] 流式 tool_call 的 id/name 重复拼接在 v4.26.7 仍可复现 —— 附线上事故完整链路与流式侧防护方案

大家好,我们是一个长期运行的生产部署,昨天(2026-09-03)在 v4.26.7(openai 2.33.0)上线上实弹踩中了这个 bug,补充一份具体证据和方案建议,希望能帮上忙。

线上事故(litellm 网关,OpenAI 兼容协议)

一轮图片溯源 Agent 完全静默失效:

22:09:37 Agent 使用工具: ['reverse_image_searchreverse_image_search']
22:09:37 WARN 未找到指定的工具: reverse_image_searchreverse_image_search,将跳过。
22:13:03 openai.BadRequestError: 400 - litellm.BadRequestError:
        [StringParam] [input[8].call_id] [string_above_max_length]
        Invalid 'input[8].call_id': string too long. Expected max 64, got 86.
22:14:34 Agent 使用工具: ['brave_web_searchbrave_web_search']
22:14:34 WARN 未找到指定的工具: brave_web_searchbrave_web_search,将跳过。

同一根因引出两种故障形态:

  1. 工具名翻倍 → 工具注册表查不到 → error: Tool ... not found → 整个检索步骤静默降级(bot 只能对用户说"检索链路出了点问题");
  2. call_id 翻倍(86 字符)→ 随对话上下文持久化 → 下一轮请求直接被上游 400 拒收(litellm 执行 Responses API 的 64 字符上限)。这个次生影响比表面更糟:畸形消息会一直毒化会话,直到上下文压缩把它挤出去才自愈。

用 SDK 真实的累加器做过仿真确认机制:3 个 chunk 各自携带完整 id+nameaccumulate_delta 字符串拼接 → len(id) == 87,与线上观测到的 86+后缀 吻合。

现有两个 PR 的覆盖范围

#7735#7765 都是在完成解析时做事后去重,能覆盖常见情况,但事后自重复检测(s+ss)处理不了:

  • 3 次以上重复产生 abcabcabc——除非检测逻辑循环到稳定为止(建议确认两个 PR 是否都做了);
  • 同 index 复用且换了新 id —— 部分网关会复用 index=0 开启第二个 tool_call。SDK 快照按 index 合并,两个不同逻辑调用的 id/args 会被串接成一条(call_AAAcall_BBB)。这种情况下不存在自重复模式,[Bug]在查询的时候提示:未找到指定的工具: astr_kb_searchastr_kb_search,将跳过。 #7694 式的检测不会触发;
  • 字段重复但内容不一致(网关中途改写了 id 等)。

建议:把防护放到流式边界上

我们在 openai_source._query_stream 的流式循环里(紧挨着现有的 #6661 index 修复)打了一个补丁,用一个每次请求独立的 dict[int, tuple[id, name]] 做三件事:

  1. 同 index、相同 id/name 重复出现 → 把 delta 里的这两个字段丢弃,只保留 arguments 的增量拼接;
  2. 同 index、不同 id → 视为新的 tool_call,重映射到一个全新槽位(防止跨调用串接);
  3. 保留原有 1-based index 重映射。

已在真实 _query_stream 代码路径上端到端验证(假客户端重放恶意流,检查 ChatCompletionStreamState.get_final_completion 产出):正常流、双工具并行、与本线上事故完全一致的重复重放模式、同 index 换新 id,四种场景都产出干净的单条 tool_call。

这个方案在 state.handle_chunk 上游生效,SDK 快照完全见不到畸形 delta,不需要任何事后字符串手术。如果维护者倾向流式侧方案,我们乐意整理成 PR;或者它也可以和 #7735/#7765 互为纵深防御(每个 chunk 只是两次字典查询,开销可忽略)。

复现环境

  • AstrBot v4.26.7(Windows 原生部署),openai>=1.78.0 实际装到 2.33.0
  • Provider:openai_chat_completion,经 litellm 网关转发,该网关会间歇性地在 arguments chunk 里重复携带完整的 tool_call 元数据
  • 值得一提:当晚同一网关还有另一次不稳定(37 次 httpx.ReadTimeout),故障出现时 fallback 路由正在生效——fallback 会把网关的偶发怪癖放大成高频问题。

如需完整事故日志片段或最小复现脚本,随时可以提供。这个问题从 4 月(#7694)挂到现在,5 个月后仍在咬生产用户,希望这份 400 报错的实锤证据能帮两个修复 PR 早日合并。🙏

@zouyonghe

Copy link
Copy Markdown
Member

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/openai_source.py" line_range="66-79" />
<code_context>
         return text[: cls._ERROR_TEXT_CANDIDATE_MAX_CHARS]

+    @staticmethod
+    def _deduplicate_self_repeating(
+        value: str | None, min_length: int = 20
+    ) -> str | None:
+        """If string is a self-repeating pattern like 'astr_kb_searchastr_kb_search'
+        (exactly 2 repetitions, min 20 chars), return the base unit.
+        This handles streaming chunk duplication issues for tool names/IDs.
+        Returns None unchanged."""
+        if value is None:
+            return None
+        if not value or len(value) < min_length:
+            return value
+        half = len(value) // 2
+        if value[:half] == value[half:]:
+            return value[:half]
+        return value
+
</code_context>
<issue_to_address>
**issue (bug_risk):** _deduplicate_self_repeating unconditionally halves every string of at least 20 characters whose two halves match, so it changes legitimate values as well as duplicated chunks. For example, a valid JSON numeric argument such as `123456789012345123456789012345` is parsed as a different, shorter number, and a legitimate repeated tool name or ID is changed; if one call has an ID equal to the repeated base of another call, the normalized IDs collide and `tool_call_extra_content_dict` overwrites one call's metadata.

**Triggers:** When a valid tool argument, function name, or opaque tool-call ID happens to consist of two identical halves.

**Suggested fix:** Only deduplicate arguments after parsing the original JSON fails, validate a deduplicated function name against the registered tool set, and avoid rewriting opaque IDs unless the provider-specific duplication can be identified without creating collisions.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and the heuristic changes tool names, IDs, and JSON arguments before they are used, so a legitimate value that happens to consist of two identical halves could be truncated and cause a tool to run with the wrong input or identifier. Reverting stops future incorrect calls, but any external side effect from a call made with the altered value would need separate repair or rerunning.

Blocking findings: astrbot/core/provider/sources/openai_source.py:79


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +66 to +79
def _deduplicate_self_repeating(
value: str | None, min_length: int = 20
) -> str | None:
"""If string is a self-repeating pattern like 'astr_kb_searchastr_kb_search'
(exactly 2 repetitions, min 20 chars), return the base unit.
This handles streaming chunk duplication issues for tool names/IDs.
Returns None unchanged."""
if value is None:
return None
if not value or len(value) < min_length:
return value
half = len(value) // 2
if value[:half] == value[half:]:
return value[:half]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): _deduplicate_self_repeating unconditionally halves every string of at least 20 characters whose two halves match, so it changes legitimate values as well as duplicated chunks. For example, a valid JSON numeric argument such as 123456789012345123456789012345 is parsed as a different, shorter number, and a legitimate repeated tool name or ID is changed; if one call has an ID equal to the repeated base of another call, the normalized IDs collide and tool_call_extra_content_dict overwrites one call's metadata.

Triggers: When a valid tool argument, function name, or opaque tool-call ID happens to consist of two identical halves.

Suggested fix: Only deduplicate arguments after parsing the original JSON fails, validate a deduplicated function name against the registered tool set, and avoid rewriting opaque IDs unless the provider-specific duplication can be identified without creating collisions.

Copy link
Copy Markdown
Contributor Author

感谢大家继续跟进,也感谢 @x1051445024 补充现场日志和流式侧方案。

我重新检查了当前实现,最新 review 指出的风险仍然存在:min_length=20 只能保护较短的值,不能避免合法的长重复名称、ID 或参数被截半,不同 ID 也可能因此发生碰撞。之前 None 被转为空字符串的问题已经修复,但我之前“所有问题均已处理”的表述需要更正,这些回归风险还需要解决。

针对当前提交的去重函数做了独立验证,确认三次重复不会被处理、合法的长重复字符串会被截半,两个不同 ID 也可能归一成同一个值。这还不是完整的流式端到端测试。

@x1051445024 看到你还提交了 #9593,目前也有 #9681 在处理流式 index 和缺失 ID 问题。想确认一下,你在现场报告中提到的重复 id/name 防护,是否已有单独分支或 PR?如果方便,也请提供脱敏的最小复现脚本或原始 chunk 样本,尤其是重复携带完整字段和同 index 换 ID 的情况。请去除密钥、用户内容等敏感信息,保留字段结构、分片顺序及必要的 SDK/网关版本即可。

我倾向于围绕这些样本补充流式回放测试,区分正常分片与异常重复,再确定修复方式,避免继续扩大最终字符串去重的范围,也避免与现有 PR 重复修改。同 index 出现不同 ID 是否应拆成新调用,也需要结合原始分片确认。

维护者如果已有倾向的处理方案或相关修复,也欢迎指出,后续可以据此协调 #7735 的范围。

@x1051445024

x1051445024 commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

@Blueteemo 我重新核对了 #9593#9681,并整理了独立 PR:#10146

先澄清范围:#9593 是我的提交,主要处理 index 归一化和序列化时的缺失 ID 兜底;#9681@LIKIQ 的提交,对缺失 ID、名称及工具结果消息配对的处理更完整。目前两者都未合并,也都没有处理重复完整 id/name 被 SDK 拼接的问题,不能据此认定 #7694 已解决。

进一步回放后,我也需要收窄之前现场报告中的建议:

#10146 采用默认关闭的显式兼容选项 deduplicate_streaming_tool_metadata: true。只有用户确认网关发送完整 metadata pair 时才开启;同槽位同时匹配初始非空 ID/name pair 才抑制重复字段。出现部分字段或变化字段后,该槽位停止去重,保留 SDK 增量语义。arguments 和 extra_content 原样保留,不对最终字符串截半。

下面是最小合成回放输入,每行表示按顺序到达的 choices[0].delta.tool_calls[0],不是生产 SSE 抓包:

{"index":0,"id":"call_abc","type":"function","function":{"name":"reverse_image_search","arguments":"{\"query\":"}}
{"index":0,"id":"call_abc","type":"function","function":{"name":"reverse_image_search","arguments":"\"sample\""}}
{"index":0,"id":"call_abc","type":"function","function":{"name":"reverse_image_search","arguments":"}"}}

可直接运行新 PR 的 tests/test_openai_source.py 中 metadata 回放测试。35 个新增用例覆盖默认模式下合法分片、初始 arguments 为空、三次重复、不同槽位交错、同 provider 连续请求、参数重复片段和 extra_content。相关两套测试本地共 161 passed;替换为基线 _query_stream() 后,3 个重复回放用例失败,另外 32 个用例通过。Sourcery 随后指出的“空字符串字段也应关闭该槽位去重”已在 ef04221d 修复,并补了先失败后通过的回归测试。

当前验证环境:Windows、Python 3.12.12、OpenAI SDK 2.46.0、Pydantic 2.13.4。之前事故记录中的 SDK 是 2.33.0;手头没有当时的原始 SSE 或准确网关构建版本,所以不会把构造样本当作原始证据,也不声称已经解决所有网关变体。高级配置方法、测试边界和与现有 PR 的关系都写在 #10146 描述中。

@Blueteemo

Copy link
Copy Markdown
Contributor Author

继续跟进后,当前结论已经明确:本 PR 在最终字符串上做自重复截半,仍可能误改合法的工具名、调用 ID 和 JSON 参数,也可能造成 ID 碰撞;min_length 不能消除这个风险。

#10146 已基于本 PR 的讨论提供更安全的替代方案:默认关闭、只在明确启用时于流式边界处理重复的完整 metadata pair,不改写 arguments 或 extra_content,并加入 35 个流式回放测试;最新 Sourcery review 已批准。

继续修改本 PR 会与 #10146 重复,而且当前实现不适合合并。因此将本 PR 关闭为 superseded by #10146,保留讨论和事故证据供后续参考。感谢 @zouyonghe@x1051445024 的审查与复现补充。

@Blueteemo Blueteemo closed this Sep 20, 2026
@Blueteemo
Blueteemo deleted the fix/issue-7694-tool-call-name-deduplication branch September 20, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]在查询的时候提示:未找到指定的工具: astr_kb_searchastr_kb_search,将跳过。

3 participants