Compare commits

..

6 Commits

Author SHA1 Message Date
Soulter 572689b416 feat: implement retry mechanism for model requests in anthropic and openai providers 2026-02-17 13:18:00 +08:00
Soulter 97c9e95211 chore: ruff format 2026-02-17 02:31:38 +08:00
Soulter a4be369e43 chore: bump version to 4.17.1 2026-02-17 02:30:13 +08:00
Soulter bdaca78750 fix: add support for collecting data from builtin stars in electron pyinstaller build (#5145) 2026-02-17 02:27:07 +08:00
Soulter 6326d7e4ba fix: add MCP tools to function tool set in _plugin_tool_fix (#5144) 2026-02-17 02:19:36 +08:00
Soulter a809a09e55 docs: Added instructions for deploying AstrBot using AstrBot Launcher. (#5136)
Added instructions for deploying AstrBot using AstrBot Launcher.
2026-02-16 17:06:56 +08:00
12 changed files with 115 additions and 118 deletions
+4
View File
@@ -81,6 +81,10 @@ uv tool install astrbot
astrbot
```
#### 启动器一键部署(AstrBot Launcher
进入 [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) 仓库,在 Releases 页最新版本下找到对应的系统安装包安装即可。
#### 宝塔面板部署
AstrBot 与宝塔面板合作,已上架至宝塔面板。
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.17.1"
__version__ = "4.17.2"
+9
View File
@@ -42,6 +42,7 @@ from astrbot.core.message.components import File, Image, Reply
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.provider import Provider
from astrbot.core.provider.entities import ProviderRequest
from astrbot.core.provider.manager import llm_tools
from astrbot.core.skills.skill_manager import SkillManager, build_skills_prompt
from astrbot.core.star.context import Context
from astrbot.core.star.star_handler import star_map
@@ -769,6 +770,14 @@ def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None:
if plugin.name in event.plugins_name or plugin.reserved:
new_tool_set.add_tool(tool)
req.func_tool = new_tool_set
else:
# mcp tools
tool_set = req.func_tool
if not tool_set:
tool_set = ToolSet()
for tool in llm_tools.func_list:
if isinstance(tool, MCPTool):
tool_set.add_tool(tool)
async def _handle_webchat(
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.17.1"
VERSION = "4.17.2"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
WEBHOOK_SUPPORTED_PLATFORMS = [
@@ -22,6 +22,7 @@ from astrbot.core.utils.network_utils import (
)
from ..register import register_provider_adapter
from .default import with_model_request_retry
@register_provider_adapter(
@@ -204,6 +205,7 @@ class ProviderAnthropic(Provider):
if usage.output_tokens is not None:
token_usage.output = usage.output_tokens
@with_model_request_retry()
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
if tools:
if tool_list := tools.get_func_desc_anthropic_style():
@@ -265,6 +267,10 @@ class ProviderAnthropic(Provider):
return llm_response
@with_model_request_retry()
async def _create_message_stream(self, payloads: dict, extra_body: dict):
return self.client.messages.stream(**payloads, extra_body=extra_body)
async def _query_stream(
self,
payloads: dict,
@@ -293,9 +299,8 @@ class ProviderAnthropic(Provider):
"type": "enabled",
}
async with self.client.messages.stream(
**payloads, extra_body=extra_body
) as stream:
stream_ctx = await self._create_message_stream(payloads, extra_body)
async with stream_ctx as stream:
assert isinstance(stream, anthropic.AsyncMessageStream)
async for event in stream:
if event.type == "message_start":
+38
View File
@@ -0,0 +1,38 @@
from tenacity import (
AsyncRetrying,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)
MODEL_REQUEST_RETRY_ATTEMPTS = 5
MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS = 15
MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS = 1
MODEL_REQUEST_RETRY_WAIT_MULTIPLIER = 1
def with_model_request_retry():
return retry(
retry=retry_if_exception_type(Exception),
stop=stop_after_attempt(MODEL_REQUEST_RETRY_ATTEMPTS),
wait=wait_exponential(
multiplier=MODEL_REQUEST_RETRY_WAIT_MULTIPLIER,
min=MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS,
max=MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS,
),
reraise=True,
)
def get_model_request_async_retrying() -> AsyncRetrying:
return AsyncRetrying(
retry=retry_if_exception_type(Exception),
stop=stop_after_attempt(MODEL_REQUEST_RETRY_ATTEMPTS),
wait=wait_exponential(
multiplier=MODEL_REQUEST_RETRY_WAIT_MULTIPLIER,
min=MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS,
max=MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS,
),
reraise=True,
)
+16 -24
View File
@@ -21,6 +21,7 @@ from astrbot.core.utils.io import download_image_by_url
from astrbot.core.utils.network_utils import is_connection_error, log_connection_failure
from ..register import register_provider_adapter
from .default import get_model_request_async_retrying, with_model_request_retry
class SuppressNonTextPartsWarning(logging.Filter):
@@ -513,6 +514,7 @@ class ProviderGoogleGenAI(Provider):
llm_response.reasoning_signature = base64.b64encode(ts).decode("utf-8")
return MessageChain(chain=chain)
@with_model_request_retry()
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
"""非流式请求 Gemini API"""
system_instruction = next(
@@ -601,6 +603,17 @@ class ProviderGoogleGenAI(Provider):
self,
payloads: dict,
tools: ToolSet | None,
) -> AsyncGenerator[LLMResponse, None]:
async for attempt in get_model_request_async_retrying():
with attempt:
async for response in self._query_stream_once(payloads, tools):
yield response
return
async def _query_stream_once(
self,
payloads: dict,
tools: ToolSet | None,
) -> AsyncGenerator[LLMResponse, None]:
"""流式请求 Gemini API"""
system_instruction = next(
@@ -759,18 +772,7 @@ class ProviderGoogleGenAI(Provider):
payloads = {"messages": context_query, "model": model}
retry = 10
keys = self.api_keys.copy()
for _ in range(retry):
try:
return await self._query(payloads, func_tool)
except APIError as e:
if await self._handle_api_error(e, keys):
continue
break
raise Exception("请求失败。")
return await self._query(payloads, func_tool)
async def text_chat_stream(
self,
@@ -814,18 +816,8 @@ class ProviderGoogleGenAI(Provider):
payloads = {"messages": context_query, "model": model}
retry = 10
keys = self.api_keys.copy()
for _ in range(retry):
try:
async for response in self._query_stream(payloads, func_tool):
yield response
break
except APIError as e:
if await self._handle_api_error(e, keys):
continue
break
async for response in self._query_stream(payloads, func_tool):
yield response
async def get_models(self):
try:
+22 -87
View File
@@ -31,6 +31,7 @@ from astrbot.core.utils.network_utils import (
from astrbot.core.utils.string_utils import normalize_and_dedupe_strings
from ..register import register_provider_adapter
from .default import get_model_request_async_retrying, with_model_request_retry
@register_provider_adapter(
@@ -221,6 +222,7 @@ class ProviderOpenAIOfficial(Provider):
except NotFoundError as e:
raise Exception(f"获取模型列表失败:{e}")
@with_model_request_retry()
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
if tools:
model = payloads.get("model", "").lower()
@@ -246,8 +248,6 @@ class ProviderOpenAIOfficial(Provider):
if isinstance(custom_extra_body, dict):
extra_body.update(custom_extra_body)
model = payloads.get("model", "").lower()
completion = await self.client.chat.completions.create(
**payloads,
stream=False,
@@ -269,6 +269,17 @@ class ProviderOpenAIOfficial(Provider):
self,
payloads: dict,
tools: ToolSet | None,
) -> AsyncGenerator[LLMResponse, None]:
async for attempt in get_model_request_async_retrying():
with attempt:
async for response in self._query_stream_once(payloads, tools):
yield response
return
async def _query_stream_once(
self,
payloads: dict,
tools: ToolSet | None,
) -> AsyncGenerator[LLMResponse, None]:
"""流式查询API,逐步返回结果"""
if tools:
@@ -716,7 +727,7 @@ class ProviderOpenAIOfficial(Provider):
extra_user_content_parts=None,
**kwargs,
) -> LLMResponse:
payloads, context_query = await self._prepare_chat_payload(
payloads, _ = await self._prepare_chat_payload(
prompt,
image_urls,
contexts,
@@ -728,47 +739,9 @@ class ProviderOpenAIOfficial(Provider):
)
llm_response = None
max_retries = 10
available_api_keys = self.api_keys.copy()
chosen_key = random.choice(available_api_keys)
image_fallback_used = False
last_exception = None
retry_cnt = 0
for retry_cnt in range(max_retries):
try:
self.client.api_key = chosen_key
llm_response = await self._query(payloads, func_tool)
break
except Exception as e:
last_exception = e
(
success,
chosen_key,
available_api_keys,
payloads,
context_query,
func_tool,
image_fallback_used,
) = await self._handle_api_error(
e,
payloads,
context_query,
func_tool,
chosen_key,
available_api_keys,
retry_cnt,
max_retries,
image_fallback_used=image_fallback_used,
)
if success:
break
if retry_cnt == max_retries - 1 or llm_response is None:
logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。")
if last_exception is None:
raise Exception("未知错误")
raise last_exception
if self.api_keys:
self.client.api_key = random.choice(self.api_keys)
llm_response = await self._query(payloads, func_tool)
return llm_response
async def text_chat_stream(
@@ -784,7 +757,7 @@ class ProviderOpenAIOfficial(Provider):
**kwargs,
) -> AsyncGenerator[LLMResponse, None]:
"""流式对话,与服务商交互并逐步返回结果"""
payloads, context_query = await self._prepare_chat_payload(
payloads, _ = await self._prepare_chat_payload(
prompt,
image_urls,
contexts,
@@ -794,48 +767,10 @@ class ProviderOpenAIOfficial(Provider):
**kwargs,
)
max_retries = 10
available_api_keys = self.api_keys.copy()
chosen_key = random.choice(available_api_keys)
image_fallback_used = False
last_exception = None
retry_cnt = 0
for retry_cnt in range(max_retries):
try:
self.client.api_key = chosen_key
async for response in self._query_stream(payloads, func_tool):
yield response
break
except Exception as e:
last_exception = e
(
success,
chosen_key,
available_api_keys,
payloads,
context_query,
func_tool,
image_fallback_used,
) = await self._handle_api_error(
e,
payloads,
context_query,
func_tool,
chosen_key,
available_api_keys,
retry_cnt,
max_retries,
image_fallback_used=image_fallback_used,
)
if success:
break
if retry_cnt == max_retries - 1:
logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。")
if last_exception is None:
raise Exception("未知错误")
raise last_exception
if self.api_keys:
self.client.api_key = random.choice(self.api_keys)
async for response in self._query_stream(payloads, func_tool):
yield response
async def _remove_image_from_context(self, contexts: list):
"""从上下文中删除所有带有 image 的记录"""
+8
View File
@@ -0,0 +1,8 @@
## What's Changed
hotfix of 4.17.0
- 修复:MCP 服务器的 Tools 没有被正确添加到上下文中。
- 修复:Electron 桌面应用部署时,系统自带插件未被正确加载的问题。
- fix: Tools from MCP server were not properly added to context.
- fix: built-in plugins were not properly loaded in Electron desktop application deployment.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "astrbot-desktop",
"version": "4.17.1",
"version": "4.17.2",
"description": "AstrBot desktop wrapper",
"private": true,
"main": "main.js",
+6
View File
@@ -16,6 +16,8 @@ const kbStopwordsSrc = path.join(
'hit_stopwords.txt',
);
const kbStopwordsDest = 'astrbot/core/knowledge_base/retrieval';
const builtinStarsSrc = path.join(rootDir, 'astrbot', 'builtin_stars');
const builtinStarsDest = 'astrbot/builtin_stars';
const args = [
'run',
@@ -35,9 +37,13 @@ const args = [
'pip',
'--collect-submodules',
'astrbot.api',
'--collect-submodules',
'astrbot.builtin_stars',
'--collect-data',
'certifi',
'--add-data',
`${builtinStarsSrc}${dataSeparator}${builtinStarsDest}`,
'--add-data',
`${kbStopwordsSrc}${dataSeparator}${kbStopwordsDest}`,
'--distpath',
outputDir,
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.17.1"
version = "4.17.2"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
requires-python = ">=3.12"