fix(provider): 修复流式工具调用元数据重复拼接(可选兼容模式) - #10146
Open
x1051445024 wants to merge 2 commits into
Open
x1051445024 wants to merge 2 commits into
x1051445024 wants to merge 2 commits into
Conversation
This was referenced Sep 20, 2026
Open
Contributor
There was a problem hiding this comment.
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="678" />
<code_context>
+ elif metadata and metadata == tool_call_metadata[tc.index]:
+ tc.id = None
+ tc.function.name = None
+ elif raw_id or raw_name:
+ # Partial or changing metadata is not an exact replay.
+ # Keep SDK delta semantics for the rest of this slot.
+ tool_call_metadata[tc.index] = None
# 跳过 delta=None 的 chunk,避免 SDK 内部 _convert_initial_chunk_into_snapshot
# 第 747 行 choice.delta.to_dict() 抛出 NoneType 错误。
</code_context>
<issue_to_address>
**issue (bug_risk):** When compatibility mode is enabled, a partial metadata update whose supplied ID/name value is an empty string does not clear the slot because `raw_id or raw_name` is false. A later complete pair equal to the initial pair is then incorrectly suppressed even though replay suppression should have been disabled for the rest of that slot.
**Triggers:** When a tool-call slot starts with a complete nonempty ID/name pair, then receives an empty-string partial metadata update, followed by the original pair again.
**Suggested fix:** Track whether either metadata field was supplied rather than using truthiness, and disable the slot for any partial or changing metadata update, including empty strings.
```suggestion
elif raw_id is not None or raw_name is not None:
```
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and when enabled, a misclassified repeated metadata fragment could produce the wrong tool-call ID or function name and cause an incorrect tool invocation before the stream is reverted. The parsing behavior is opt-in and future executions stop after reverting, but any side effect from a mistaken tool call would need separate remediation.
Blocking findings: astrbot/core/provider/sources/openai_source.py:678
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题说明
关联 #7694,以及 #7735 中的讨论。本 PR 提供需要显式开启的兼容处理,不是通用字符串去重器。
部分 OpenAI 兼容网关会在每个 arguments 增量片段中重复发送完整的工具调用
id和function.name。SDK 会将这些字符串直接拼接,最终产生不存在的工具名或过长的调用 ID。但相同字符串也可能是合法分片。例如,下面这段流有两种可能的含义:
{"index":0,"id":"id","function":{"name":"ping","arguments":"{"}} {"index":0,"id":"id","function":{"name":"ping","arguments":"}"}}它既可能是重复发送
id/ping,也可能是合法增量,最终应得到idid/pingping。仅凭字段相等、字符串长度或 arguments 已经开始,都无法区分这两种情况。因此,默认行为保持不变,不自动去重。修改内容
true的 provider 配置项deduplicate_streaming_tool_metadata。ChatCompletionStreamState.handle_chunk()前移除重复字段。extra_content。出现部分字段、变化字段或空字符串字段后,该槽位在本次请求剩余阶段停止去重,保留 SDK 增量语义。_query_stream()调用内。不根据 ID 变化自动拆出新调用,不改写最终字符串,不生成兜底 ID,也不重复实现 index 重映射。_query_stream()和 SDK 累加器;仅替换网络返回流。开启方式
只有通过网关 chunk 样本确认其发送的是完整 ID/name 字段对,而非字段增量时,才应开启。
这是高级的本地 provider / 模型配置项。在
data/cmd_config.json的provider数组中,为受影响的现有配置项添加以下字段,并保留其他配置:{ "deduplicate_streaming_tool_metadata": true }手动修改前先停止 AstrBot 并备份配置,修改后重新启动。删除此字段或改为 JSON 布尔值
false即可恢复标准增量行为;字符串"true"不会启用此功能。该字段不应放入
custom_extra_body,也不会发送给上游 API。本 PR 没有新增 WebUI 控件或修改导航。最小合成回放
以下是构造的回归输入,不是生产环境原始 SSE 抓包。每行表示按顺序到达的
choices[0].delta.tool_calls[0]:{"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":"}"}}开启兼容选项后,应得到一个
call_abc/reverse_image_search调用,参数为{"query":"sample"}。测试还覆盖初始 arguments 为空、多个槽位交错、同一 provider 连续请求、合法重复字符串保持不变、重复参数片段不被吞掉,以及
extra_content的键不能发生碰撞等情况。适用范围与相关 PR
master,不依赖这两个 PR,也不重复修改它们负责的逻辑。测试结果
Ruff 使用仓库指定的 0.15.22 版本。pytest 的警告来自已有的 Python
audioop弃用提示。本地未运行整个仓库的 pytest,验证范围为上面的 provider 与 tool-loop 两套测试。作为反向对照,将
_query_stream()替换为基线提交b06550567b25d7ef4404bf526e448c869cc5d4ec中的原始实现,再运行 35 个 metadata 用例,结果为 3 failed、32 passed:两个重复字段对用例及交错重放用例因 ID 被拼接而失败,其他用例保持原有行为。针对 Sourcery 的审查意见,还补充了显式空字符串 ID/name 的测试。修改前 3 个空字符串用例失败;将真值判断改为显式
is not None判断后,全部通过。目前 6 个部分字段或变化字段用例均会验证:该槽位在本次请求后续阶段不会重新开始去重。检查清单
Sourcery 摘要(中文翻译)
为在流式响应中重复发送完整工具调用元数据的网关增加可选兼容处理。