fix(anthropic): 修复 Anthropic API tool_choice 格式转换及参数支持#8328
Conversation
- 将 tool_choice 从简单的 auto/required 逻辑改为遵循 Anthropic API 规范,支持 auto/any/none/tool 四种原生值
- 兼容 OpenAI 风格的 tool_choice="required",自动映射为 {"type": "any"}
- 允许直接传入 dict 类型的 tool_choice 以实现指定工具调用
- 更新 text_chat 和 stream_chat 入口的参数类型标注,扩大可接收的 tool_choice 类型
- 新增 tool_choice 格式转换的单元测试,覆盖各类输入场景
Closes AstrBotDevs#8319
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The
tool_choicehandling logic in_queryand_query_streamis duplicated; consider extracting it into a small helper (e.g._normalize_tool_choice(payloads)) to keep behavior consistent and make future updates easier. - The
tool_choiceparameter type hints fortext_chat/text_chat_streamno longer include the legacy'required'value even though it is still supported at runtime; to avoid type-checker friction and better reflect behavior, consider adding'required'back into the Literal or widening the type. - In the
tool_calls_resultiteration you renamed the loop variable totool_choicer, which is misleading since it represents tool call results rather than tool choice; consider keeping a more accurate name liketcrortool_call_resultfor readability.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `tool_choice` handling logic in `_query` and `_query_stream` is duplicated; consider extracting it into a small helper (e.g. `_normalize_tool_choice(payloads)`) to keep behavior consistent and make future updates easier.
- The `tool_choice` parameter type hints for `text_chat`/`text_chat_stream` no longer include the legacy `'required'` value even though it is still supported at runtime; to avoid type-checker friction and better reflect behavior, consider adding `'required'` back into the Literal or widening the type.
- In the `tool_calls_result` iteration you renamed the loop variable to `tool_choicer`, which is misleading since it represents tool call results rather than tool choice; consider keeping a more accurate name like `tcr` or `tool_call_result` for readability.
## Individual Comments
### Comment 1
<location path="astrbot/core/provider/sources/anthropic_source.py" line_range="325-334" />
<code_context>
- if payloads.get("tool_choice") == "required"
- else "auto"
- }
+ # 转换为 Anthropic API 要求的 tool_choice 格式
+ if "tool_choice" in payloads:
+ tool_choice = payloads["tool_choice"]
+ if isinstance(tool_choice, dict):
+ payloads["tool_choice"] = tool_choice
+ elif tool_choice == "required":
+ # 兼容 OpenAI 命名
+ payloads["tool_choice"] = {"type": "any"}
+ elif tool_choice in ("auto", "any", "none", "tool"):
+ payloads["tool_choice"] = {"type": tool_choice}
+ else:
+ payloads["tool_choice"] = {"type": "auto"}
+ else:
+ payloads["tool_choice"] = {"type": "auto"}
</code_context>
<issue_to_address>
**suggestion:** The tool_choice normalization logic is duplicated between _query and _query_stream and could be extracted to a shared helper.
A helper like `_normalize_tool_choice(payloads)` would centralize this Anthropic-specific mapping and reduce the risk that sync and streaming diverge if the API or supported values change.
</issue_to_address>
### Comment 2
<location path="astrbot/core/provider/sources/anthropic_source.py" line_range="334-335" />
<code_context>
+ elif tool_choice == "required":
+ # 兼容 OpenAI 命名
+ payloads["tool_choice"] = {"type": "any"}
+ elif tool_choice in ("auto", "any", "none", "tool"):
+ payloads["tool_choice"] = {"type": tool_choice}
+ else:
+ payloads["tool_choice"] = {"type": "auto"}
</code_context>
<issue_to_address>
**issue (bug_risk):** Using {"type": "tool"} without a name is likely invalid per Anthropic’s tool_choice schema.
Per Anthropic’s schema, `{"type": "tool"}` generally must include a `"name"` (and possibly other fields) to indicate which tool to force. Mapping the bare string "tool" to `{"type": "tool"}` like the other simple variants may generate an invalid request. You could either require a dict for the "tool" case (e.g., `{"type": "tool", "name": ...}`) or remove "tool" from this simple string set and handle it separately so you can enforce presence of a tool name.
</issue_to_address>
### Comment 3
<location path="tests/test_anthropic_kimi_code_provider.py" line_range="424-430" />
<code_context>
+# ---- tool_choice 转换测试 ----
+
+
+class _FakeToolSet:
+ """模拟包含工具的 ToolSet"""
+
+ def get_func_desc_anthropic_style(self):
+ return [{"name": "get_weather", "description": "Get weather"}]
+
+ def empty(self):
+ return False
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for the case where tools are present but `get_func_desc_anthropic_style()` returns an empty list to ensure `tool_choice` is not set in that scenario.
Right now `_FakeToolSet` always returns a non-empty list and `empty()` is always `False`, so the tests only cover the truthy branch of `tool_list := tools.get_func_desc_anthropic_style()`. Please add another fake (or parameterize this one) that returns `[]` (and possibly `True` from `empty()` if the provider uses it), and assert that in this case no `tools` or `tool_choice` keys are added to the payload. This will help prevent regressions where `tool_choice` is sent even when there are no tools.
Suggested implementation:
```python
# ---- tool_choice 转换测试 ----
# ---- tool_choice 转换测试 ----
class _FakeToolSet:
"""模拟包含工具的 ToolSet"""
def get_func_desc_anthropic_style(self):
return [{"name": "get_weather", "description": "Get weather"}]
def empty(self) -> bool:
"""指示该 ToolSet 非空"""
return False
class _EmptyToolSet:
"""模拟包含空工具列表的 ToolSet,用于测试 tool_choice 在无工具时不应被设置"""
def get_func_desc_anthropic_style(self):
# 显式返回空列表,触发代码中对 falsy tool_list 分支的处理
return []
def empty(self) -> bool:
"""对于一些实现会检查 tools.empty(),这里应返回 True。"""
return True
```
要完整实现你的建议,还需要在本文件中新增或扩展测试用例,大致步骤如下:
1. 在现有使用 `_FakeToolSet` 的测试附近,新增一个使用 `_EmptyToolSet` 的测试(或将现有测试参数化),例如:
- 构造 `tools = _EmptyToolSet()`
- 通过当前被测的 provider/构造函数/辅助函数(例如构造请求 payload 的函数)生成 payload。
- `assert "tools" not in payload` 或 `assert not payload.get("tools")`
- `assert "tool_choice" not in payload`
2. 如果生产代码在构造 payload 时会先检查 `tools.empty()` 再检查 `tool_list := tools.get_func_desc_anthropic_style()`,请确保测试同时覆盖两种分支:
- 非空 `_FakeToolSet`:断言 `tools` 和(根据逻辑)`tool_choice` 被正确设置。
- 空 `_EmptyToolSet`:断言既没有 `tools` 也没有 `tool_choice` 字段。
3. 由于当前片段中看不到具体的 provider 函数/类名,你需要在新增测试中调用实际用于构造 Anthropic/Kimi 请求 payload 的 API(例如 `AnthropicKimiCodeProvider._build_payload(...)` 或类似函数),并将上述断言应用到该函数的返回值上。
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| # 转换为 Anthropic API 要求的 tool_choice 格式 | ||
| # 参考: https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#providing-tool-use-examples | ||
| if "tool_choice" in payloads: | ||
| tool_choice = payloads["tool_choice"] | ||
| if isinstance(tool_choice, dict): | ||
| payloads["tool_choice"] = tool_choice | ||
| elif tool_choice == "required": | ||
| # 兼容 OpenAI 命名 | ||
| payloads["tool_choice"] = {"type": "any"} | ||
| elif tool_choice in ("auto", "any", "none", "tool"): |
There was a problem hiding this comment.
suggestion: The tool_choice normalization logic is duplicated between _query and _query_stream and could be extracted to a shared helper.
A helper like _normalize_tool_choice(payloads) would centralize this Anthropic-specific mapping and reduce the risk that sync and streaming diverge if the API or supported values change.
| elif tool_choice in ("auto", "any", "none", "tool"): | ||
| payloads["tool_choice"] = {"type": tool_choice} |
There was a problem hiding this comment.
issue (bug_risk): Using {"type": "tool"} without a name is likely invalid per Anthropic’s tool_choice schema.
Per Anthropic’s schema, {"type": "tool"} generally must include a "name" (and possibly other fields) to indicate which tool to force. Mapping the bare string "tool" to {"type": "tool"} like the other simple variants may generate an invalid request. You could either require a dict for the "tool" case (e.g., {"type": "tool", "name": ...}) or remove "tool" from this simple string set and handle it separately so you can enforce presence of a tool name.
| class _FakeToolSet: | ||
| """模拟包含工具的 ToolSet""" | ||
|
|
||
| def get_func_desc_anthropic_style(self): | ||
| return [{"name": "get_weather", "description": "Get weather"}] | ||
|
|
||
| def empty(self): |
There was a problem hiding this comment.
suggestion (testing): Consider adding a test for the case where tools are present but get_func_desc_anthropic_style() returns an empty list to ensure tool_choice is not set in that scenario.
Right now _FakeToolSet always returns a non-empty list and empty() is always False, so the tests only cover the truthy branch of tool_list := tools.get_func_desc_anthropic_style(). Please add another fake (or parameterize this one) that returns [] (and possibly True from empty() if the provider uses it), and assert that in this case no tools or tool_choice keys are added to the payload. This will help prevent regressions where tool_choice is sent even when there are no tools.
Suggested implementation:
# ---- tool_choice 转换测试 ----
# ---- tool_choice 转换测试 ----
class _FakeToolSet:
"""模拟包含工具的 ToolSet"""
def get_func_desc_anthropic_style(self):
return [{"name": "get_weather", "description": "Get weather"}]
def empty(self) -> bool:
"""指示该 ToolSet 非空"""
return False
class _EmptyToolSet:
"""模拟包含空工具列表的 ToolSet,用于测试 tool_choice 在无工具时不应被设置"""
def get_func_desc_anthropic_style(self):
# 显式返回空列表,触发代码中对 falsy tool_list 分支的处理
return []
def empty(self) -> bool:
"""对于一些实现会检查 tools.empty(),这里应返回 True。"""
return True要完整实现你的建议,还需要在本文件中新增或扩展测试用例,大致步骤如下:
-
在现有使用
_FakeToolSet的测试附近,新增一个使用_EmptyToolSet的测试(或将现有测试参数化),例如:- 构造
tools = _EmptyToolSet() - 通过当前被测的 provider/构造函数/辅助函数(例如构造请求 payload 的函数)生成 payload。
assert "tools" not in payload或assert not payload.get("tools")assert "tool_choice" not in payload
- 构造
-
如果生产代码在构造 payload 时会先检查
tools.empty()再检查tool_list := tools.get_func_desc_anthropic_style(),请确保测试同时覆盖两种分支:- 非空
_FakeToolSet:断言tools和(根据逻辑)tool_choice被正确设置。 - 空
_EmptyToolSet:断言既没有tools也没有tool_choice字段。
- 非空
-
由于当前片段中看不到具体的 provider 函数/类名,你需要在新增测试中调用实际用于构造 Anthropic/Kimi 请求 payload 的 API(例如
AnthropicKimiCodeProvider._build_payload(...)或类似函数),并将上述断言应用到该函数的返回值上。
There was a problem hiding this comment.
Code Review
This pull request enhances the Anthropic provider by implementing tool_choice conversion logic to support various selection modes and maintain compatibility with OpenAI-style naming. It also includes a new test suite to verify these conversions. The reviewer identified critical issues where the tool type was missing the required name parameter and the unsupported none type was included. Feedback also suggested refactoring duplicated logic into a helper function, refining type annotations in method signatures, and renaming loop variables for better clarity.
| elif tool_choice in ("auto", "any", "none", "tool"): | ||
| payloads["tool_choice"] = {"type": tool_choice} |
There was a problem hiding this comment.
这里的逻辑存在两个问题:
tool类型缺失参数:根据 Anthropic 文档,当type为"tool"时,必须提供name字段指定具体的工具名称。仅发送{"type": "tool"}会导致 API 报错。建议在需要指定特定工具时,要求调用者传入dict格式(如{"type": "tool", "name": "my_tool"})。- 不支持
none类型:Anthropic API 的tool_choice并不支持"none"。如果希望不使用工具,通常的做法是不在请求中包含tools字段,或者使用"auto"。发送{"type": "none"}可能会导致 API 返回 400 错误。
| elif tool_choice in ("auto", "any", "none", "tool"): | |
| payloads["tool_choice"] = {"type": tool_choice} | |
| elif tool_choice in ("auto", "any"): | |
| payloads["tool_choice"] = {"type": tool_choice} |
| elif tool_choice in ("auto", "any", "none", "tool"): | ||
| payloads["tool_choice"] = {"type": tool_choice} |
There was a problem hiding this comment.
| # 转换为 Anthropic API 要求的 tool_choice 格式 | ||
| # 参考: https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#providing-tool-use-examples | ||
| if "tool_choice" in payloads: | ||
| tool_choice = payloads["tool_choice"] | ||
| if isinstance(tool_choice, dict): | ||
| payloads["tool_choice"] = tool_choice | ||
| elif tool_choice == "required": | ||
| # 兼容 OpenAI 命名 | ||
| payloads["tool_choice"] = {"type": "any"} | ||
| elif tool_choice in ("auto", "any", "none", "tool"): | ||
| payloads["tool_choice"] = {"type": tool_choice} | ||
| else: | ||
| payloads["tool_choice"] = {"type": "auto"} | ||
| else: | ||
| payloads["tool_choice"] = {"type": "auto"} |
There was a problem hiding this comment.
这里的 tool_choice 转换逻辑在 _query 和 _query_stream 中完全重复。根据通用规则,建议将其提取为一个私有辅助函数(例如 _format_anthropic_tool_choice),以提高代码的可维护性并减少冗余。
此外,根据项目规则,新功能(如工具选择处理)应伴随相应的单元测试。
References
- When implementing similar functionality for different cases (e.g., direct vs. quoted attachments), refactor the logic into a shared helper function to avoid code duplication.
- New functionality, such as handling attachments, should be accompanied by corresponding unit tests.
| model=None, | ||
| extra_user_content_parts=None, | ||
| tool_choice: Literal["auto", "required"] = "auto", | ||
| tool_choice: Literal["auto", "any", "tool", "none"] | dict[str, str] = "auto", |
There was a problem hiding this comment.
建议从 Literal 中移除 "tool" 和 "none"。如前所述,"none" 不是 Anthropic 支持的类型,而 "tool" 需要额外的 name 参数,应该通过 dict 类型传入以确保参数完整性。同时保留 "required" 以维持对 OpenAI 风格命名的兼容性。
| tool_choice: Literal["auto", "any", "tool", "none"] | dict[str, str] = "auto", | |
| tool_choice: Literal["auto", "any", "required"] | dict[str, str] = "auto", |
| for tool_choicer in tool_calls_result: | ||
| context_query.extend(tool_choicer.to_openai_messages()) |
There was a problem hiding this comment.
变量名 tool_choicer 容易引起误解,因为它遍历的是 tool_calls_result(工具调用结果),而不是工具选择器。建议使用更准确的名称,如 tool_call_res。
| for tool_choicer in tool_calls_result: | |
| context_query.extend(tool_choicer.to_openai_messages()) | |
| for tool_call_res in tool_calls_result: | |
| context_query.extend(tool_call_res.to_openai_messages()) |
| model=None, | ||
| extra_user_content_parts=None, | ||
| tool_choice: Literal["auto", "required"] = "auto", | ||
| tool_choice: Literal["auto", "any", "tool", "none"] | dict[str, str] = "auto", |
| for tool_choicer in tool_calls_result: | ||
| context_query.extend(tool_choicer.to_openai_messages()) |
There was a problem hiding this comment.
anthropic api的tool_choice类型错了,还跟openai的搞混了,把他们兼容一下,然后参考anthropic api官方文档修复tool_choice的内容
Modifications / 改动点
Closes #8319
在
anthropic_source.py中修复payload中的tool_choice字段,并且兼容原本的openai的required参数Screenshots or Test Results / 运行截图或测试结果
Checklist / 检查清单
😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in
requirements.txtandpyproject.toml./ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到
requirements.txt和pyproject.toml文件相应位置。😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Align Anthropic provider tool_choice handling with Anthropic API semantics while maintaining backward compatibility with existing usage.
New Features:
Bug Fixes:
Enhancements:
Tests: