fix: handle tool-incompatible models before agent execution#7164
fix: handle tool-incompatible models before agent execution#7164bugkeep wants to merge 4 commits intoAstrBotDevs:masterfrom
Conversation
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- IDE-specific
.vs/*JSON files were added to the repo; these should be removed from the PR and ignored via.gitignoreso they don’t pollute the project with local workspace state.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- IDE-specific `.vs/*` JSON files were added to the repo; these should be removed from the PR and ignored via `.gitignore` so they don’t pollute the project with local workspace state.
## Individual Comments
### Comment 1
<location path="astrbot/core/astr_main_agent.py" line_range="799-802" />
<code_context>
+ if not model_name:
+ return None
+
+ metadata = LLM_METADATAS.get(model_name)
+ if metadata is None:
+ return None
+ return bool(metadata["tool_call"])
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against missing `tool_call` key in `LLM_METADATAS` entries to avoid `KeyError`.
Using `metadata["tool_call"]` will raise a `KeyError` if that key is missing for any model. Prefer `metadata.get("tool_call")` (with a default like `None` or `False`) before casting to `bool`, so the behavior matches the `metadata is None` case instead of breaking the flow.
</issue_to_address>
### Comment 2
<location path="tests/test_astr_main_agent_tool_capability.py" line_range="56-57" />
<code_context>
+ return self.providers.get(provider_id)
+
+
+def _make_metadata(tool_call: bool) -> dict[str, Any]:
+ return {
+ "id": "test-model",
+ "reasoning": False,
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for models with unknown tool capability metadata to cover the `None` branch in `_get_tool_call_support`.
Currently helpers always set an explicit `tool_call` flag in `LLM_METADATAS`, so tests only hit `_get_tool_call_support` for `True`/`False`. We should also cover the case where the model has no metadata entry (or missing metadata despite `modalities` allowing tools), where `_get_tool_call_support` returns `None`.
Please add a test that:
- uses a provider with `modalities=["text", "tool_use"]`,
- omits the model from `LLM_METADATAS` (or deletes its entry),
- calls `_resolve_tool_capability_strategy` with tools.
This will assert that `None` is treated as "unknown but allowed", the provider is kept, and `allow_follow_up=True`, guarding against regressions when metadata is incomplete.
Suggested implementation:
```python
def _make_metadata(
tool_call: bool,
*,
input_modalities: list[str] | None = None,
output_modalities: list[str] | None = None,
model_id: str = "test-model",
) -> dict[str, Any]:
"""
Helper to construct LLM metadata for tests.
Existing callers can keep using the `tool_call` positional argument only.
New tests can override modalities and model id as needed.
"""
if input_modalities is None:
input_modalities = ["text"]
if output_modalities is None:
output_modalities = ["text"]
return {
"id": model_id,
"reasoning": False,
"tool_call": tool_call,
"knowledge": "none",
"release_date": "",
"modalities": {"input": input_modalities, "output": output_modalities},
"open_weights": False,
"limit": {"context": 0, "output": 0},
}
def test_unknown_tool_capability_metadata_allows_follow_up_and_keeps_provider(
monkeypatch: "pytest.MonkeyPatch",
) -> None:
"""
Models with tool-use modality but no explicit tool_call metadata should be treated
as "unknown but allowed": the provider is kept and allow_follow_up=True.
This guards the `_get_tool_call_support` branch that returns `None`.
"""
# Import from the module under test; adjust the import path if different.
from astr.main.agent.tool_capability import (
_resolve_tool_capability_strategy,
LLM_METADATAS,
)
# Use a model id that is *not* present in LLM_METADATAS
model_id = "test-model-unknown-metadata"
# Ensure the metadata entry is absent, even if other tests added it.
LLM_METADATAS.pop(model_id, None)
# Build a provider whose model supports tool_use in its modalities.
# Reuse whatever Provider/Model construction helpers exist in this test module
# or the production code; the important part is the `modalities=["text", "tool_use"]`.
provider_id = "provider-with-tool-modalities"
# NOTE: The following provider construction assumes a simple Provider/Model API.
# If your actual Provider/Model types differ, adapt this block accordingly.
from astr.main.provider import Provider, Model # adjust import if needed
provider = Provider(
id=provider_id,
models=[
Model(
id=model_id,
modalities=["text", "tool_use"],
)
],
)
plugin_context = DummyPluginContext(providers={provider_id: provider})
tools = _make_tool_set()
strategy = _resolve_tool_capability_strategy(
plugin_context=plugin_context,
model_id=model_id,
tools=tools,
)
# When metadata is unknown but tools are allowed by modality:
# - provider should still be considered
# - follow-up tool calls should be allowed
assert provider_id in strategy.providers
assert strategy.allow_follow_up is True
def _make_tool_set() -> ToolSet:
```
To make this compile and pass with your actual codebase, you will likely need to:
1. **Adjust imports in the test file**:
- Ensure `pytest` is imported if not already: `import pytest`.
- Fix the import path for `_resolve_tool_capability_strategy` and `LLM_METADATAS` if they live in a different module than `astr.main.agent.tool_capability`.
- Replace `from astr.main.provider import Provider, Model` with the correct import(s) for your Provider/Model types, or swap in the appropriate factory/helper already used elsewhere in this test file.
2. **Match your Provider/Model API**:
- If `Provider` or `Model` constructors differ (e.g., additional required fields, nested capabilities object instead of a flat `modalities` list), update the `provider = Provider(...)` block to:
- Set `modalities=["text", "tool_use"]` (or the equivalent capabilities structure) for the model.
- Satisfy any other required arguments (e.g., `name`, `metadata`, `pricing`, etc.).
3. **Align `_resolve_tool_capability_strategy` call signature**:
- If the function expects different parameter names or additional arguments (e.g., `provider_filters`, `tool_choice`), add them to the call in the test using values consistent with your existing tests.
4. **Align `strategy` shape**:
- If the object returned by `_resolve_tool_capability_strategy` does not expose `.providers` or `.allow_follow_up` directly, adapt the assertions to match your actual structure (e.g., `strategy.providers_by_id`, `strategy.allow_follow_up_tool_calls`).
5. **Optional: if you don't use `Provider`/`Model` directly in tests**:
- If existing tests construct providers via helpers (e.g., `_make_provider(...)`), prefer reusing those helpers here to ensure consistent setup, while still configuring the model's modalities to include `"tool_use"` and omitting its metadata from `LLM_METADATAS`.
Once these adjustments are made to fit your real types and imports, this test will exercise the `None` branch in `_get_tool_call_support` and verify that unknown but allowed tool capability metadata behaves as intended.
</issue_to_address>
### Comment 3
<location path="tests/test_astr_main_agent_tool_capability.py" line_range="158-178" />
<code_context>
+ ]
+
+
+def test_tool_capability_strategy_hard_fail_raises(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ primary = DummyProvider("primary", "deepseek-r1:7b", ["text", "tool_use"])
+ plugin_context = DummyPluginContext({"primary": primary})
+ req = ProviderRequest(func_tool=_make_tool_set())
+ config = MainAgentBuildConfig(
+ tool_call_timeout=30,
+ tool_capability_strategy="hard_fail",
+ provider_settings={},
+ )
+
+ monkeypatch.setitem(LLM_METADATAS, "deepseek-r1:7b", _make_metadata(False))
+
+ with pytest.raises(UnsupportedToolCapabilityError):
+ _resolve_tool_capability_strategy(
+ provider=primary,
+ req=req,
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the `hard_fail` test to assert that tool state is not mutated when an `UnsupportedToolCapabilityError` is raised.
Right now this test only checks that `UnsupportedToolCapabilityError` is raised. It would be helpful to also assert that `_resolve_tool_capability_strategy` does not mutate `req` before raising—for example, that `req.func_tool` remains set and `req.contexts` is unchanged after the `with pytest.raises(...)` block. That documents that `hard_fail` performs pure validation rather than mutating or downgrading tool state.
```suggestion
def test_tool_capability_strategy_hard_fail_raises(
monkeypatch: pytest.MonkeyPatch,
) -> None:
primary = DummyProvider("primary", "deepseek-r1:7b", ["text", "tool_use"])
plugin_context = DummyPluginContext({"primary": primary})
req = ProviderRequest(func_tool=_make_tool_set())
config = MainAgentBuildConfig(
tool_call_timeout=30,
tool_capability_strategy="hard_fail",
provider_settings={},
)
# Capture initial tool and context state to ensure they are not mutated
original_func_tool = req.func_tool
original_contexts = list(req.contexts)
monkeypatch.setitem(LLM_METADATAS, "deepseek-r1:7b", _make_metadata(False))
with pytest.raises(UnsupportedToolCapabilityError):
_resolve_tool_capability_strategy(
provider=primary,
req=req,
plugin_context=plugin_context,
config=config,
)
# Hard-fail strategy should not mutate tool or context state
assert req.func_tool is original_func_tool
assert req.contexts == original_contexts
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| metadata = LLM_METADATAS.get(model_name) | ||
| if metadata is None: | ||
| return None | ||
| return bool(metadata["tool_call"]) |
There was a problem hiding this comment.
issue (bug_risk): Guard against missing tool_call key in LLM_METADATAS entries to avoid KeyError.
Using metadata["tool_call"] will raise a KeyError if that key is missing for any model. Prefer metadata.get("tool_call") (with a default like None or False) before casting to bool, so the behavior matches the metadata is None case instead of breaking the flow.
| def _make_metadata(tool_call: bool) -> dict[str, Any]: | ||
| return { |
There was a problem hiding this comment.
suggestion (testing): Consider adding a test for models with unknown tool capability metadata to cover the None branch in _get_tool_call_support.
Currently helpers always set an explicit tool_call flag in LLM_METADATAS, so tests only hit _get_tool_call_support for True/False. We should also cover the case where the model has no metadata entry (or missing metadata despite modalities allowing tools), where _get_tool_call_support returns None.
Please add a test that:
- uses a provider with
modalities=["text", "tool_use"], - omits the model from
LLM_METADATAS(or deletes its entry), - calls
_resolve_tool_capability_strategywith tools.
This will assert that None is treated as "unknown but allowed", the provider is kept, and allow_follow_up=True, guarding against regressions when metadata is incomplete.
Suggested implementation:
def _make_metadata(
tool_call: bool,
*,
input_modalities: list[str] | None = None,
output_modalities: list[str] | None = None,
model_id: str = "test-model",
) -> dict[str, Any]:
"""
Helper to construct LLM metadata for tests.
Existing callers can keep using the `tool_call` positional argument only.
New tests can override modalities and model id as needed.
"""
if input_modalities is None:
input_modalities = ["text"]
if output_modalities is None:
output_modalities = ["text"]
return {
"id": model_id,
"reasoning": False,
"tool_call": tool_call,
"knowledge": "none",
"release_date": "",
"modalities": {"input": input_modalities, "output": output_modalities},
"open_weights": False,
"limit": {"context": 0, "output": 0},
}
def test_unknown_tool_capability_metadata_allows_follow_up_and_keeps_provider(
monkeypatch: "pytest.MonkeyPatch",
) -> None:
"""
Models with tool-use modality but no explicit tool_call metadata should be treated
as "unknown but allowed": the provider is kept and allow_follow_up=True.
This guards the `_get_tool_call_support` branch that returns `None`.
"""
# Import from the module under test; adjust the import path if different.
from astr.main.agent.tool_capability import (
_resolve_tool_capability_strategy,
LLM_METADATAS,
)
# Use a model id that is *not* present in LLM_METADATAS
model_id = "test-model-unknown-metadata"
# Ensure the metadata entry is absent, even if other tests added it.
LLM_METADATAS.pop(model_id, None)
# Build a provider whose model supports tool_use in its modalities.
# Reuse whatever Provider/Model construction helpers exist in this test module
# or the production code; the important part is the `modalities=["text", "tool_use"]`.
provider_id = "provider-with-tool-modalities"
# NOTE: The following provider construction assumes a simple Provider/Model API.
# If your actual Provider/Model types differ, adapt this block accordingly.
from astr.main.provider import Provider, Model # adjust import if needed
provider = Provider(
id=provider_id,
models=[
Model(
id=model_id,
modalities=["text", "tool_use"],
)
],
)
plugin_context = DummyPluginContext(providers={provider_id: provider})
tools = _make_tool_set()
strategy = _resolve_tool_capability_strategy(
plugin_context=plugin_context,
model_id=model_id,
tools=tools,
)
# When metadata is unknown but tools are allowed by modality:
# - provider should still be considered
# - follow-up tool calls should be allowed
assert provider_id in strategy.providers
assert strategy.allow_follow_up is True
def _make_tool_set() -> ToolSet:To make this compile and pass with your actual codebase, you will likely need to:
-
Adjust imports in the test file:
- Ensure
pytestis imported if not already:import pytest. - Fix the import path for
_resolve_tool_capability_strategyandLLM_METADATASif they live in a different module thanastr.main.agent.tool_capability. - Replace
from astr.main.provider import Provider, Modelwith the correct import(s) for your Provider/Model types, or swap in the appropriate factory/helper already used elsewhere in this test file.
- Ensure
-
Match your Provider/Model API:
- If
ProviderorModelconstructors differ (e.g., additional required fields, nested capabilities object instead of a flatmodalitieslist), update theprovider = Provider(...)block to:- Set
modalities=["text", "tool_use"](or the equivalent capabilities structure) for the model. - Satisfy any other required arguments (e.g.,
name,metadata,pricing, etc.).
- Set
- If
-
Align
_resolve_tool_capability_strategycall signature:- If the function expects different parameter names or additional arguments (e.g.,
provider_filters,tool_choice), add them to the call in the test using values consistent with your existing tests.
- If the function expects different parameter names or additional arguments (e.g.,
-
Align
strategyshape:- If the object returned by
_resolve_tool_capability_strategydoes not expose.providersor.allow_follow_updirectly, adapt the assertions to match your actual structure (e.g.,strategy.providers_by_id,strategy.allow_follow_up_tool_calls).
- If the object returned by
-
Optional: if you don't use
Provider/Modeldirectly in tests:- If existing tests construct providers via helpers (e.g.,
_make_provider(...)), prefer reusing those helpers here to ensure consistent setup, while still configuring the model's modalities to include"tool_use"and omitting its metadata fromLLM_METADATAS.
- If existing tests construct providers via helpers (e.g.,
Once these adjustments are made to fit your real types and imports, this test will exercise the None branch in _get_tool_call_support and verify that unknown but allowed tool capability metadata behaves as intended.
There was a problem hiding this comment.
Code Review
This pull request introduces a tool capability strategy to handle models that do not support tool calls, allowing for fallback providers, chat-only degradation, or hard failures. The implementation includes configuration updates, request sanitization, and new exception handling. Feedback suggests using an Enum for strategy names to improve maintainability and adding a test case for scenarios where fallback providers also lack tool support.
| if strategy in {"fallback_provider", "chat_only", "hard_fail"}: | ||
| return strategy |
There was a problem hiding this comment.
To improve maintainability, consider defining the valid tool capability strategy names as constants or an Enum in a shared location, rather than hardcoding them as a set here. This would prevent inconsistencies, as these names are also used in astrbot/core/config/default.py for the configuration schema.
For example, you could define an Enum:
from enum import Enum
class ToolCapabilityStrategy(str, Enum):
FALLBACK_PROVIDER = "fallback_provider"
CHAT_ONLY = "chat_only"
HARD_FAIL = "hard_fail"
VALID_TOOL_CAPABILITY_STRATEGIES = {s.value for s in ToolCapabilityStrategy}And then use VALID_TOOL_CAPABILITY_STRATEGIES for validation. This would make the code more robust and easier to update.
| req=req, | ||
| plugin_context=plugin_context, | ||
| config=config, | ||
| ) |
There was a problem hiding this comment.
The current tests cover the main paths for the three strategies. It would be beneficial to also test the edge case for the fallback_provider strategy where no suitable fallback provider (i.e., one that supports tool calls) is found. In this scenario, the logic should gracefully degrade to the chat_only strategy.
Adding a test case for this scenario would improve test coverage and ensure the robustness of the fallback mechanism.
Here is a suggested test case:
def test_tool_capability_strategy_fallback_degrades_to_chat_only(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Tests that when 'fallback_provider' strategy is used but no suitable
fallback is found, it degrades to 'chat_only' behavior.
"""
primary = DummyProvider("primary", "model-no-tools", ["text", "tool_use"])
# A fallback provider that also does not support tool calls
fallback = DummyProvider("fallback", "fallback-no-tools", ["text", "tool_use"])
plugin_context = DummyPluginContext(
{
"primary": primary,
"fallback": fallback,
}
)
req = ProviderRequest(
func_tool=_make_tool_set(),
model="model-no-tools",
contexts=[
{"role": "assistant", "tool_calls": [{"id": "call_1"}], "content": ""},
{"role": "tool", "content": "tool output"},
],
)
config = MainAgentBuildConfig(
tool_call_timeout=30,
tool_capability_strategy="fallback_provider",
provider_settings={"fallback_chat_models": ["fallback"]},
)
monkeypatch.setitem(LLM_METADATAS, "model-no-tools", _make_metadata(False))
monkeypatch.setitem(LLM_METADATAS, "fallback-no-tools", _make_metadata(False))
selected_provider, allow_follow_up = _resolve_tool_capability_strategy(
provider=primary,
req=req,
plugin_context=plugin_context,
config=config,
)
assert selected_provider is primary, "Provider should not change"
assert allow_follow_up is False, "Follow-up should be disabled"
assert req.func_tool is None, "Tool set should be cleared"
assert req.contexts == [], "Tool-related context should be stripped"|
Related Documentation 1 document(s) may need updating based on files changed in this PR: AstrBotTeam's Space pr4697的改动View Suggested Changes@@ -2105,7 +2105,133 @@
---
-### 16. 其他优化
+### 16. 工具不兼容模型处理增强(PR #7164)
+
+#### 功能说明
+[PR #7164](https://github.com/AstrBotDevs/AstrBot/pull/7164) 修复了工具不兼容模型可能进入本地 Agent 执行流程的问题,防止提供商层静默移除工具并重试为纯文本聊天。该修复在 Agent 执行前显式检查模型工具能力,确保对不支持工具调用的模型采取可预测的处理策略。
+
+#### 新增配置选项:tool_capability_strategy
+
+新增 `tool_capability_strategy` 配置项,用于控制当所选模型不支持工具调用时的系统行为:
+
+- **配置路径**:`provider_settings.tool_capability_strategy`
+- **可选值**:
+ - `fallback_provider`(默认):自动切换到支持工具调用的备用提供商
+ - `chat_only`:降级为纯文本聊天模式,清除请求中的工具状态
+ - `hard_fail`:抛出 `UnsupportedToolCapabilityError` 异常
+- **默认行为**:系统默认尝试切换到备用提供商,若无可用的备用提供商,则自动降级为 `chat_only` 模式
+
+#### 核心改进
+
+##### 1. 工具能力检测机制
+新增 `_resolve_tool_capability_strategy()` 函数,在 Agent 执行前检查模型工具能力并应用配置的策略:
+
+- **检测依据**:
+ - 提供商配置的 `modalities` 字段(检查是否包含 `tool_use`)
+ - LLM 元数据(`LLM_METADATAS`)中的 `tool_call` 字段
+- **检测时机**:在主 Agent 构建阶段,工具加载和去重逻辑之后,模型请求发送之前
+- **优先级**:优先检查提供商 `modalities` 配置,再查询 LLM 元数据
+
+##### 2. 工具能力查询(_get_tool_call_support)
+新增 `_get_tool_call_support()` 函数,用于判断模型是否支持工具调用:
+
+- **返回值**:
+ - `True`:明确支持工具调用
+ - `False`:明确不支持工具调用
+ - `None`:无法确定(元数据中无该模型信息)
+- **查询逻辑**:
+ 1. 检查提供商配置的 `modalities` 字段,如果未包含 `tool_use` 则返回 `False`
+ 2. 从 LLM 元数据中查询模型的 `tool_call` 字段
+ 3. 如果元数据中无该模型信息,返回 `None`
+
+##### 3. 工具状态清理(_strip_tool_state_from_request)
+新增 `_strip_tool_state_from_request()` 函数,用于在 `chat_only` 模式下安全清理请求中的工具相关状态:
+
+- **清理内容**:
+ - 移除 `func_tool` 和 `tool_calls_result` 字段
+ - 从上下文中移除 `role="tool"` 的消息
+ - 移除 assistant 消息中的 `tool_calls` 和 `tool_call_id` 字段
+ - 移除内容为空的 assistant 消息
+- **上下文净化**:确保对话历史中不包含工具调用相关的中间状态,避免后续请求出现格式错误
+
+##### 4. 新增异常类型(UnsupportedToolCapabilityError)
+在 `astrbot/core/exceptions.py` 中新增 `UnsupportedToolCapabilityError` 异常类型,用于 `hard_fail` 策略:
+
+```python
+class UnsupportedToolCapabilityError(AstrBotError):
+ """Raised when the selected model cannot satisfy the requested tool mode."""
+```
+
+##### 5. allow_follow_up 标志
+`MainAgentBuildResult` 新增 `allow_follow_up` 标志,用于控制是否允许跟进消息:
+
+- **默认值**:`True`
+- **chat_only 模式**:设置为 `False`,防止在降级为纯文本聊天时继续注册 follow-up runner
+- **用途**:避免在纯文本聊天模式下重复消息或短时追问被放大为 follow-up 处理
+
+#### 行为变更
+
+##### fallback_provider 策略(默认)
+当模型不支持工具调用且配置了备用提供商时:
+
+1. 系统自动查找支持工具调用的备用提供商
+2. 切换到备用提供商并设置对应的模型
+3. 记录信息日志,提示用户已切换提供商
+4. 如果找到的备用提供商工具能力未知(元数据中无信息),记录警告日志并尝试使用
+5. 如果所有备用提供商均不支持工具调用,自动降级为 `chat_only` 模式
+
+##### chat_only 策略
+当策略为 `chat_only` 或 `fallback_provider` 无可用备用提供商时:
+
+1. 调用 `_strip_tool_state_from_request()` 清理请求中的工具状态
+2. 移除上下文中的工具调用相关消息
+3. 设置 `allow_follow_up=False`,避免注册 follow-up runner
+4. 记录信息日志,提示已降级为纯文本聊天模式
+5. 后续请求作为普通文本聊天处理
+
+##### hard_fail 策略
+当策略为 `hard_fail` 时:
+
+1. 检测到模型不支持工具调用后立即抛出 `UnsupportedToolCapabilityError` 异常
+2. 异常消息包含模型名称和提供商 ID,便于用户定位问题
+3. Agent 执行流程中止,不进行任何自动降级或切换
+
+#### 修改文件
+
+- **`astrbot/core/astr_main_agent.py`**:核心 Agent 逻辑,新增工具能力路由
+- **`astrbot/core/config/default.py`**:新增 `tool_capability_strategy` 配置选项
+- **`astrbot/core/exceptions.py`**:新增 `UnsupportedToolCapabilityError` 异常
+- **`astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py`**:集成工具能力策略到 pipeline
+- **`tests/test_astr_main_agent_tool_capability.py`**:全面测试覆盖三种策略
+
+#### 测试覆盖
+
+PR #7164 新增了完整的测试覆盖,验证三种策略的行为:
+
+1. **fallback_provider 策略测试**:
+ - 主模型不支持工具调用时自动切换到备用提供商
+ - 验证提供商和模型是否正确切换
+ - 验证工具状态是否保留
+
+2. **chat_only 策略测试**:
+ - 清理请求中的工具状态(`func_tool`、`tool_calls_result`)
+ - 清理上下文中的工具调用消息(`role="tool"` 和 assistant 的 `tool_calls`)
+ - 验证 `allow_follow_up` 标志设置为 `False`
+
+3. **hard_fail 策略测试**:
+ - 验证抛出 `UnsupportedToolCapabilityError` 异常
+ - 验证异常消息包含模型名称和提供商 ID
+
+#### 用户收益
+
+- **一致性和可预测性**:确保工具不兼容模型的处理策略在 Agent 执行前明确,避免提供商层的静默回退
+- **灵活配置**:用户可根据实际需求选择自动切换、降级或直接报错
+- **增强可靠性**:防止工具不兼容模型进入 Agent 执行流程后引发的错误和异常行为
+- **改善日志可观测性**:通过明确的日志记录工具能力检测和策略执行过程,便于诊断和优化
+
+---
+
+### 17. 其他优化
- JWT 处理和错误处理机制增强,提升系统安全性和稳定性
- UI 细节优化,提升用户体验
- 日志与异常处理增强,便于问题追踪Note: You must be authenticated to accept/decline updates. |
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Motivation / 动机
This PR fixes the local agent flow for models that do not support function/tool calling.
Previously, a tool-incompatible model could still enter the local agent pipeline, and the provider layer would silently remove
toolsand retry as plain text chat. That made the behavior inconsistent and could amplify follow-up handling unexpectedly.This change makes the routing explicit before agent execution, so unsupported models are handled in a predictable way.
Closes #6857
Modifications / 改动点
tools。tool_capability_strategy配置项,用于处理“当前模型不支持函数工具调用”的场景,支持三种策略:fallback_provider:自动切换到fallback_chat_models中支持工具调用的模型chat_only:降级为纯文本聊天模式hard_fail:直接返回错误fallback_provider时,如果主模型不支持工具调用,会优先切换到兼容的回退模型继续执行。chat_only时,会清理请求中的工具状态和工具相关上下文,避免旧的工具调用信息继续污染后续对话。chat_only模式下,不再注册 active follow-up runner,避免重复消息或短时追问被继续放大为 follow-up 处理。UnsupportedToolCapabilityError,用于hard_fail路径下返回更明确的错误类型。tool_capability_strategy,使该策略可以通过配置项直接使用。核心修改文件:
astrbot/core/astr_main_agent.pyastrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.pyastrbot/core/config/default.pyastrbot/core/exceptions.pytests/test_astr_main_agent_tool_capability.pyThis is NOT a breaking change. / 这不是一个破坏性变更。
Screenshots or Test Results / 运行截图或测试结果
Verification Steps:



Checklist / 检查清单
[ x ] 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
[ x ] 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
[ x ] 🤓 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文件相应位置。[ x ] 😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Handle models that do not support tool calls earlier in the local agent pipeline by routing them according to a configurable strategy instead of silently stripping tools at the provider layer.
New Features:
Enhancements:
Tests: