Repository navigation
fix(provider): merge custom server tools with registered tools in Anthropic requests - #10437
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/core/provider/sources/anthropic_source.py" line_range="534" />
<code_context>
+ # which would silently drop them.
+ custom_tools = extra_body.get("tools")
+ if isinstance(custom_tools, list) and isinstance(payloads.get("tools"), list):
+ payloads["tools"] = [*payloads["tools"], *custom_tools]
+ extra_body = {k: v for k, v in extra_body.items() if k != "tools"}
</code_context>
<issue_to_address>
**Duplicate tool names reject requests**
When a custom server tool shares a name with a registered function tool, or custom tools repeat a name, the merge appends the duplicate definitions to `payloads["tools"]` without checking names. Anthropic rejects the request, so standard and streaming chats fail instead of getting a response.
Check names across registered and custom tools, and reject or resolve collisions before sending the request.
Also at `astrbot/core/provider/sources/anthropic_source.py:644-645`.
</issue_to_address>
### Comment 2
<location path="astrbot/core/provider/sources/anthropic_source.py" line_range="532" />
<code_context>
+ # Anthropic-compatible endpoints) must coexist with the registered
+ # function tools: extra_body replaces same-named top-level keys,
+ # which would silently drop them.
+ custom_tools = extra_body.get("tools")
+ if isinstance(custom_tools, list) and isinstance(payloads.get("tools"), list):
+ payloads["tools"] = [*payloads["tools"], *custom_tools]
</code_context>
<issue_to_address>
**Null extra body breaks requests**
When `custom_extra_body` is explicitly `null`, `extra_body.get("tools")` raises `AttributeError` before the SDK call, so standard and streaming requests fail instead of passing the null body through as before.
Guard access to `extra_body` so a null or non-dict value is handled without calling `.get()`.
Also at `astrbot/core/provider/sources/anthropic_source.py:642-643`.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and when custom server-side tools are configured, the request now exposes both those tools and the registered function tools, so a model could invoke a registered tool that was previously omitted from that request. Reverting prevents future invocations, but any external action, message, or data change caused by an unintended tool call would remain.
Blocking findings: astrbot/core/provider/sources/anthropic_source.py:534, astrbot/core/provider/sources/anthropic_source.py:532
Address review findings on the custom-server-tools merge:
- A custom tool sharing a name with a registered tool (or another
custom tool) produced duplicate definitions that the API rejects.
Merge by name instead; the custom entry wins, matching the override
authority extra_body already had for every other top-level key.
- An explicit null custom_extra_body crashed on .get("tools"). Null
or non-dict values now normalize to an empty body.
|
Both findings addressed in c95a0b5:
Coverage added: a collision case asserting the custom server tool replaces the same-named registered function tool, and a null-body case asserting the request still goes out with the registered tools intact. Applied to both the standard and the streaming call sites. |
zouyonghe
left a comment
There was a problem hiding this comment.
I independently verified this fix, then updated the PR branch with current master to resolve the conflict introduced by #10432. The update is a normal merge commit, 8ffff2fb106e3d108433ec905b011eacdf675a53, not a rebase or force-push.
The only manually resolved file is tests/test_anthropic_source.py: imports are combined and both regression suites are retained. I compared the AST of every test/helper function against both original trees; all are unchanged. The production source merged automatically. Relative to current master, this PR still changes only the two original Anthropic source/test files.
Independent verification uses a real AsyncAnthropic client with an in-memory MockTransport and asserts the actual serialized HTTP body, for both standard and streaming requests. Fourteen cases cover registered/server-tool coexistence, same-name custom override, duplicate custom names, an empty custom list, no registered tools, null custom body and non-list tools pass-through. Repeated requests with the same payload retain the same body and do not mutate the provider configuration.
- On master after #10432, those independent cases reproduce the issue: 6 failed / 8 passed (coexistence, duplicate custom names and empty-list cases fail in both request modes).
- On the original PR head, independent cases plus Anthropic/Kimi tests: 55 passed with Anthropic 1.12.1.
- On the updated combined tree, both independent review suites plus Anthropic/Kimi, agent/context, retry and tool-loop runner checks: 232 passed with Anthropic 1.12.1, and 232 passed with the declared minimum Anthropic 0.51.0.
- Ruff 0.15.22: 514 files unchanged by formatting; lint clean. No conflict markers or unmerged paths remain.
Only existing audioop / get_func_desc_anthropic_style deprecation warnings were observed after installing the project's declared HTTP SOCKS extra. No live endpoint or executable tool was used. I did not run the full local suite, dashboard build or a real DeepSeek/IM deployment.
No blocking finding in the reviewed logic. Approval is bound to updated head 8ffff2fb106e3d108433ec905b011eacdf675a53; the verification above belongs to this combined tree, not the old PR head. Merge remains gated on the specific updated-head checks.
Follow-up offline checks exercise AstrBot's own retry layer with SDK built-in retries disabled: a 429 response followed by success preserves the combined tools in both request modes, with identical HTTP bodies across retries and no configuration mutation. Both cases fail on pre-fix master because registered tools disappear. With these two cases added, the combined local suite is 234 passed with Anthropic 1.12.1 and 234 passed with 0.51.0; 47 existing deprecation warnings. These added review checks remain local and do not change the approved PR head.
|
FYI: All 23 checks on updated head Normal squash merge is blocked by the repository's This PR needs another authorized reviewer to review/approve the updated head before normal merge. My detailed review and verification are above; the manually resolved imports and appended tests preserve every original test/helper function from both PRs. |
|
@sourcery-ai review |
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/anthropic_source.py" line_range="660" />
<code_context>
+ merged = {}
+ for tool in [*payloads["tools"], *custom_tools]:
+ key = tool.get("name") if isinstance(tool, dict) else None
+ merged[key if key else ("_", id(tool))] = tool
+ payloads["tools"] = list(merged.values())
+ extra_body = {k: v for k, v in extra_body.items() if k != "tools"}
</code_context>
<issue_to_address>
**Malformed tools stop streaming requests**
When registered tools are present and the custom tools list contains an entry that is not a dictionary with a truthy name, `_query_stream` has already bound `id` to `None`, so the fallback `id(tool)` raises `TypeError` before the request is sent. The streaming request fails locally instead of reaching the API.
Use a fallback key that does not call the shadowed `id` variable.
</issue_to_address>Sourcery assessment
Approval pending. 1 finding to address first.
Blocking findings: astrbot/core/provider/sources/anthropic_source.py:660
Sourcery withdrew this approval because the latest commits introduced blocking findings.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - I've reviewed your changes and they look great!
Sourcery assessment
Needs a human reviewer. If the merge or name-collision logic is wrong, Anthropic may receive an incorrect tool definition, causing requests to fail or the model to select a different server-side or registered tool. Reverting prevents future affected requests, while any tool invocation or external request that already occurred would not be undone.
zouyonghe
left a comment
There was a problem hiding this comment.
Post-merge verification update: exact head d133beb completed all 23 checks. Linux, macOS and Windows pytest jobs were individually successful; the workflow-level status was not used to mask a continue-on-error lane. An offline combination with the newly merged #10436 passed 330 tests, and the actual master squash commit 6b2e417 now passes the same 330 tests with 75 existing deprecation warnings. The local combination and actual merge have identical Git tree IDs. Full-tree Ruff format/check and dependency check passed. This includes the 14 malformed-tool cases for the shadowed-id fix, the 16 independent serialized tool-combination/retry cases and context/runner regressions. Anthropic 0.51.0 and 1.12.1 had each passed the preceding 248-test provider/context/retry/runner selection. No live model, IM service, or tool side effect was exercised. I did not submit a new merge request after reporting the independent-approval blocker; GitHub subsequently reported this exact head merged at 13:45:05Z under the zouyonghe account. The review listing does not identify a new independent approval, so I am not inferring the mechanism or claiming the earlier blocker was bypassed by this session.
Fixes #10402.
When
custom_extra_bodydeclares atoolskey (for example DeepSeek's server-side web search on an Anthropic-compatible endpoint), the Anthropic SDK appliesextra_bodyas a top-level replacement, so the outgoing request keeps only the custom entries and every function tool AstrBot registered silently disappears from the model. The trace view shows no warning either, which made this painful to diagnose in the issue.This PR folds a list-valued custom
toolsintopayloads["tools"]in both_queryand_query_stream: registered function tools first, custom server tools appended, and the key is removed fromextra_bodyso it can no longer override. Unrelated custom keys pass through unchanged, a non-listtoolsvalue keeps falling through to the API as before, and when nothing is registered the custom body keeps its previous override semantics (covered by a test so it does not regress).Tests
pytest tests/test_anthropic_source.py tests/test_anthropic_kimi_code_provider.pyagainst current master (8831d95): 39 passed.test_query_merges_custom_server_tools_with_registered_toolsdrives_querywith a registeredFunctionToolplus a custom server tool and asserts the merged request: tool order["get_time", "web_search"],toolsremoved fromextra_body, unrelated custom keys preserved.test_query_custom_tools_without_registered_tools_pass_throughlocks the no-registered-tools behavior: the custom body still goes out viaextra_bodyuntouched.messages.stream.ruff formatandruff checkare clean on the touched files.Summary by Sourcery
Merge custom Anthropic server tools with registered function tools without allowing custom request bodies to silently discard registered tools.
Bug Fixes:
Tests: