Compare commits

..

2 Commits

Author SHA1 Message Date
Soulter
d612c9eed4 fix: english logging 2026-04-03 14:01:11 +08:00
Soulter
12039fe7d0 fix: resolve Discord/Misskey hot reload issue by fixing client_self_id misuse
fixes: #7187
closes: #7188
2026-04-03 13:41:56 +08:00
12 changed files with 32 additions and 237 deletions

View File

@@ -1 +1 @@
__version__ = "4.22.3"
__version__ = "4.22.2"

View File

@@ -51,29 +51,18 @@ def _prepare_stdio_env(config: dict) -> dict:
"""Preserve Windows executable resolution for stdio subprocesses."""
if sys.platform != "win32":
return config
pathext = os.environ.get("PATHEXT")
if not pathext:
return config
prepared = config.copy()
env = dict(prepared.get("env") or {})
env = _merge_environment_variables(env)
env.setdefault("PATHEXT", pathext)
prepared["env"] = env
return prepared
def _merge_environment_variables(env: dict) -> dict:
"""合并环境变量处理Windows不区分大小写的情况"""
merged = env.copy()
# 将用户环境变量转换为统一的大小写形式便于比较
user_keys_lower = {k.lower(): k for k in merged.keys()}
for sys_key, sys_value in os.environ.items():
sys_key_lower = sys_key.lower()
if sys_key_lower not in user_keys_lower:
# 使用系统环境变量中的原始大小写
merged[sys_key] = sys_value
return merged
async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
"""Quick test MCP server connectivity"""
import aiohttp

View File

@@ -5,7 +5,7 @@ from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.22.3"
VERSION = "4.22.2"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
PERSONAL_WECHAT_CONFIG_METADATA = {
"weixin_oc_base_url": {
@@ -1761,7 +1761,6 @@ CONFIG_METADATA_2 = {
"enable": True,
"rerank_api_key": "",
"rerank_api_base": "http://127.0.0.1:8000",
"rerank_api_suffix": "/v1/rerank",
"rerank_model": "BAAI/bge-reranker-base",
"timeout": 20,
},
@@ -1827,12 +1826,7 @@ CONFIG_METADATA_2 = {
"rerank_api_base": {
"description": "重排序模型 API Base URL",
"type": "string",
"hint": "最终请求路径由 Base URL 和路径后缀拼接而成(默认为 /v1/rerank",
},
"rerank_api_suffix": {
"description": "API URL 路径后缀",
"type": "string",
"hint": "追加到 base_url 后的路径,如 /v1/rerank。留空则不追加。",
"hint": "AstrBot 会在请求时在末尾加上 /v1/rerank。",
},
"rerank_api_key": {
"description": "API Key",

View File

@@ -241,10 +241,15 @@ class ProviderGoogleGenAI(Provider):
thinking_config = types.ThinkingConfig(
thinking_budget=thinking_budget,
)
elif any(model_name.startswith(p) for p in ("gemini-3-", "gemini-3.")):
# The thinkingLevel parameter, recommended for Gemini 3 models and onwards.
# Use prefix match so new variants (3.1, 3-flash-lite-preview, etc.) are
# covered without needing to keep an exhaustive list up to date.
elif model_name in [
"gemini-3-pro",
"gemini-3-pro-preview",
"gemini-3-flash",
"gemini-3-flash-preview",
"gemini-3-flash-lite",
"gemini-3-flash-lite-preview",
]:
# The thinkingLevel parameter, recommended for Gemini 3 models and onwards
# Gemini 2.5 series models don't support thinkingLevel; use thinkingBudget instead.
thinking_level = self.provider_config.get("gm_thinking_config", {}).get(
"level", "HIGH"
@@ -512,11 +517,7 @@ class ProviderGoogleGenAI(Provider):
):
chain.append(Comp.Plain("这是图片"))
for part in result_parts:
# Skip thinking parts — their text is already captured via
# _extract_reasoning_content above. Including them here would
# leak the model's internal reasoning into the user-facing message,
# which also causes duplicate/triple replies on some platforms.
if part.text and not part.thought:
if part.text:
chain.append(Comp.Plain(part.text))
if (

View File

@@ -532,7 +532,6 @@ class ProviderOpenAIOfficial(Provider):
**payloads,
stream=True,
extra_body=extra_body,
stream_options={"include_usage": True},
)
llm_response = LLMResponse("assistant", is_chunk=True)
@@ -540,10 +539,12 @@ class ProviderOpenAIOfficial(Provider):
state = ChatCompletionStreamState()
async for chunk in stream:
choice = chunk.choices[0] if chunk.choices else None
delta = choice.delta if choice else None
if not chunk.choices:
continue
choice = chunk.choices[0]
delta = choice.delta
if delta and (dtcs := delta.tool_calls):
if dtcs := delta.tool_calls:
for idx, tc in enumerate(dtcs):
# siliconflow workaround
if tc.function and tc.function.arguments:
@@ -573,7 +574,7 @@ class ProviderOpenAIOfficial(Provider):
_y = True
if chunk.usage:
llm_response.usage = self._extract_usage(chunk.usage)
elif choice and (choice_usage := getattr(choice, "usage", None)):
elif choice_usage := getattr(choice, "usage", None):
# Workaround for some providers that only return usage in choices[].usage, e.g. MoonshotAI
# See https://github.com/AstrBotDevs/AstrBot/issues/6614
llm_response.usage = self._extract_usage(choice_usage)
@@ -862,9 +863,6 @@ class ProviderOpenAIOfficial(Provider):
def _finally_convert_payload(self, payloads: dict) -> None:
"""Finally convert the payload. Such as think part conversion, tool inject."""
model = payloads.get("model", "").lower()
is_gemini = "gemini" in model
for message in payloads.get("messages", []):
if message.get("role") == "assistant" and isinstance(
message.get("content"), list
@@ -876,24 +874,11 @@ class ProviderOpenAIOfficial(Provider):
reasoning_content += str(part.get("think"))
else:
new_content.append(part)
# Some providers (Grok, etc.) reject empty content lists.
# When all parts were think blocks, fall back to None.
message["content"] = new_content or None
message["content"] = new_content
# reasoning key is "reasoning_content"
if reasoning_content:
message["reasoning_content"] = reasoning_content
# Gemini 的 function_response 要求 google.protobuf.Struct即 JSON 对象),
# 纯文本会触发 400 Invalid argument需要包一层 JSON。
if is_gemini and message.get("role") == "tool":
content = message.get("content", "")
if isinstance(content, str):
try:
json.loads(content)
except (json.JSONDecodeError, ValueError):
message["content"] = json.dumps(
{"result": content}, ensure_ascii=False
)
async def _handle_api_error(
self,
e: Exception,

View File

@@ -20,11 +20,6 @@ class VLLMRerankProvider(RerankProvider):
self.auth_key = provider_config.get("rerank_api_key", "")
self.base_url = provider_config.get("rerank_api_base", "http://127.0.0.1:8000")
self.base_url = self.base_url.rstrip("/")
self.api_suffix = provider_config.get("rerank_api_suffix", "/v1/rerank")
if self.api_suffix is None:
self.api_suffix = "/v1/rerank"
if self.api_suffix and not self.api_suffix.startswith("/"):
self.api_suffix = "/" + self.api_suffix
self.timeout = provider_config.get("timeout", 20)
self.model = provider_config.get("rerank_model", "BAAI/bge-reranker-base")
@@ -50,9 +45,8 @@ class VLLMRerankProvider(RerankProvider):
if top_n is not None:
payload["top_n"] = top_n
assert self.client is not None
rerank_url = f"{self.base_url}{self.api_suffix}"
async with self.client.post(
rerank_url,
f"{self.base_url}/v1/rerank",
json=payload,
) as response:
response_data = await response.json()

View File

@@ -1,68 +0,0 @@
- [更新日志(简体中文)](#chinese)
- [Changelog(English)](#english)
<a id="chinese"></a>
## What's Changed
### 新增
- 新增 `vllm_rerank` 提供商支持可配置的 `rerank_api_suffix`,可灵活适配不同 API 路径。([#7278](https://github.com/AstrBotDevs/AstrBot/pull/7278)
- 适配 llama.cpp 的 token 用量提取支持,增强 token 统计与计费分析能力。([#7358](https://github.com/AstrBotDevs/AstrBot/pull/7358)
- WebUI 捆绑 Monaco Editor 的 worker 文件,防止从 CDN 下载失败。([#7249](https://github.com/AstrBotDevs/AstrBot/pull/7249)
- 全新的模型/平台总览统计面板。([#7152](https://github.com/AstrBotDevs/AstrBot/pull/7152)
### 优化
- 优化 OpenAI 消息处理,若 `content` 列表为空时将其置为 `None`,并降低空内容下的异常率。([#6551](https://github.com/AstrBotDevs/AstrBot/pull/6551)
- 优化 provider page 布局与样式,提升整体设置页面排版一致性。([#7248](https://github.com/AstrBotDevs/AstrBot/pull/7248)
- 用户已登录时将直接跳转欢迎页,减少再次进入登录页的步骤。([commit](https://github.com/AstrBotDevs/AstrBot/commit/9896b48c)
### 修复
- 修复 Windows 下 MCP runner 未继承完整系统环境变量导致启动异常的问题。([#7054](https://github.com/AstrBotDevs/AstrBot/pull/7054)
- 修复 Gemini 思考片段泄露到用户可见消息链路的问题。([#7196](https://github.com/AstrBotDevs/AstrBot/pull/7196)
- 修复 Gemini(OpenAI 兼容 API) 下,`function_response` 格式要求导致的 `400 Invalid argument` 错误。([#7216](https://github.com/AstrBotDevs/AstrBot/pull/7216)
- 修复知识库初始化失败时的错误提示与恢复问题。([#7243](https://github.com/AstrBotDevs/AstrBot/pull/7243)
- 修复 Bailian Rerank 对新旧响应格式兼容性,避免解析失败。([#7217](https://github.com/AstrBotDevs/AstrBot/pull/7217)
- Docker 镜像补充 AMR 编码器依赖,修复部分部署场景下的音频转码缺失问题。([#7247](https://github.com/AstrBotDevs/AstrBot/pull/7247)
- 修复 pip 安装缺少 `pysocks` 依赖导致的 SOCKS5 代理不可用问题。([#7221](https://github.com/AstrBotDevs/AstrBot/pull/7221)
- 修复 `cached_tokens``_extract_usage` 中的处理逻辑,避免 token 统计错乱。([#6719](https://github.com/AstrBotDevs/AstrBot/pull/6719)
- 增强 SSE 断开保护,压缩传输下主动发送心跳,减少 WebChat 误断连。([#7003](https://github.com/AstrBotDevs/AstrBot/pull/7003)
- 修复严格 API 下空 assistant 消息导致的 400 错误。([#7202](https://github.com/AstrBotDevs/AstrBot/pull/7202)
- 新增二维码功能缺失的 `qrcode` 依赖修复,恢复二维码生成功能。([commit](https://github.com/AstrBotDevs/AstrBot/commit/5886c437)
- 修复配置器数字输入组件的交互体验问题,避免异常输入场景下的处理偏差。([#7153](https://github.com/AstrBotDevs/AstrBot/pull/7153)
- 修复 `Record.path` 在 Pydantic v2 下被错误视为必填字段的问题。([#7048](https://github.com/AstrBotDevs/AstrBot/pull/7048)
<a id="english"></a>
## What's Changed (EN)
### New Features
- Added configurable `rerank_api_suffix` for the `vllm_rerank` provider to support custom API paths.[#7278](https://github.com/AstrBotDevs/AstrBot/pull/7278)
- Added token usage extraction support for llama.cpp to improve token accounting.[#7358](https://github.com/AstrBotDevs/AstrBot/pull/7358)
- Bundled Monaco Editor workers in WebUI so code editing does not depend on external CDN downloads.[#7249](https://github.com/AstrBotDevs/AstrBot/pull/7249)
- Added a new statistics overview for model and platform usage.[#7152](https://github.com/AstrBotDevs/AstrBot/pull/7152)
- Redirected logged-in users to the welcome page instead of the login page.[commit](https://github.com/AstrBotDevs/AstrBot/commit/9896b48c)
### Improvements
- Improved OpenAI message handling by setting message `content` to `None` when content lists are empty, reducing empty-payload edge cases.[#6551](https://github.com/AstrBotDevs/AstrBot/pull/6551)
- Improved provider page layout and styling to keep settings pages consistent.[#7248](https://github.com/AstrBotDevs/AstrBot/pull/7248)
- Refined connection-success handling and messaging when users are already logged in.[commit](https://github.com/AstrBotDevs/AstrBot/commit/2de23184)
### Bug Fixes
- Inherited all system environment variables in Windows MCP runner to fix environment-dependent launch issues.[#7054](https://github.com/AstrBotDevs/AstrBot/pull/7054)
- Filtered Gemini thinking parts from user-facing message chains.[#7196](https://github.com/AstrBotDevs/AstrBot/pull/7196)
- Fixed Gemini `function_response` compatibility to prevent `400 Invalid argument` responses.[#7216](https://github.com/AstrBotDevs/AstrBot/pull/7216)
- Improved knowledge base initialization error handling.[#7243](https://github.com/AstrBotDevs/AstrBot/pull/7243)
- Fixed Bailian rerank parsing for both old and new response formats.[#7217](https://github.com/AstrBotDevs/AstrBot/pull/7217)
- Added AMR encoder support in Docker builds to fix missing audio codec support.[#7247](https://github.com/AstrBotDevs/AstrBot/pull/7247)
- Added the `pysocks` dependency so pip installs can use SOCKS5 proxy correctly.[#7221](https://github.com/AstrBotDevs/AstrBot/pull/7221)
- Fixed `_extract_usage` cached token handling.[#6719](https://github.com/AstrBotDevs/AstrBot/pull/6719)
- Sent SSE heartbeat packets to prevent WebChat disconnection under compression.[#7003](https://github.com/AstrBotDevs/AstrBot/pull/7003)
- Fixed `400` errors caused by empty assistant messages on strict APIs.[#7202](https://github.com/AstrBotDevs/AstrBot/pull/7202)
- Added missing `qrcode` dependency to restore QR code generation.[commit](https://github.com/AstrBotDevs/AstrBot/commit/5886c437)
- Fixed number input UX in config renderer.[#7153](https://github.com/AstrBotDevs/AstrBot/pull/7153)
- Fixed `Record.path` being incorrectly treated as required under Pydantic v2.[#7048](https://github.com/AstrBotDevs/AstrBot/pull/7048)

View File

@@ -1075,11 +1075,7 @@
},
"rerank_api_base": {
"description": "Rerank Model API Base URL",
"hint": "The full request URL is formed by combining the Base URL and a path suffix (defaults to /v1/rerank)."
},
"rerank_api_suffix": {
"description": "API URL path suffix",
"hint": "Path appended to base_url, e.g. /v1/rerank. Leave empty to disable auto-append."
"hint": "AstrBot appends /v1/rerank to the request URL."
},
"rerank_api_key": {
"description": "API Key",

View File

@@ -1076,11 +1076,7 @@
},
"rerank_api_base": {
"description": "Base URL API модели Rerank",
"hint": "Полный URL запроса формируется путём добавления суффикса к Base URL (по умолчанию /v1/rerank)."
},
"rerank_api_suffix": {
"description": "Суффикс пути API",
"hint": "Суффикс пути, добавляемый к base_url, например /v1/rerank. Оставьте пустым, чтобы не добавлять."
"hint": "AstrBot добавляет /v1/rerank к URL запроса."
},
"rerank_api_key": {
"description": "API Key",

View File

@@ -1077,11 +1077,7 @@
},
"rerank_api_base": {
"description": "重排序模型 API Base URL",
"hint": "最终请求路径由 Base URL 和路径后缀拼接而成(默认为 /v1/rerank。"
},
"rerank_api_suffix": {
"description": "API URL 路径后缀",
"hint": "追加到 base_url 后的路径后缀,如 /v1/rerank。留空则不追加。"
"hint": "AstrBot 会在请求时在末尾加上 /v1/rerank。"
},
"rerank_api_key": {
"description": "API Key",

View File

@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.22.3"
version = "4.22.2"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
license = { text = "AGPL-3.0-or-later" }

View File

@@ -2,7 +2,6 @@ from types import SimpleNamespace
import pytest
from openai.types.chat.chat_completion import ChatCompletion
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from PIL import Image as PILImage
from astrbot.core.exceptions import EmptyModelOutputError
@@ -1176,93 +1175,6 @@ async def test_parse_openai_completion_raises_empty_model_output_error():
await provider.terminate()
@pytest.mark.asyncio
async def test_query_stream_extracts_usage_from_empty_choices_chunk(monkeypatch):
provider = _make_provider()
try:
chunks = [
ChatCompletionChunk.model_validate(
{
"id": "chatcmpl-stream",
"object": "chat.completion.chunk",
"created": 0,
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"delta": {
"role": "assistant",
"content": "ok",
},
"finish_reason": None,
}
],
}
),
ChatCompletionChunk.model_validate(
{
"id": "chatcmpl-stream",
"object": "chat.completion.chunk",
"created": 0,
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"delta": {},
"finish_reason": "stop",
}
],
}
),
ChatCompletionChunk.model_validate(
{
"id": "chatcmpl-stream",
"object": "chat.completion.chunk",
"created": 0,
"model": "gpt-4o-mini",
"choices": [],
"usage": {
"prompt_tokens": 2550,
"completion_tokens": 125,
"total_tokens": 2675,
"prompt_tokens_details": {
"cached_tokens": 2488,
},
},
}
),
]
async def fake_stream():
for chunk in chunks:
yield chunk
async def fake_create(**kwargs):
return fake_stream()
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
responses = [
response
async for response in provider._query_stream(
payloads={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "hello"}],
},
tools=None,
)
]
final_response = responses[-1]
assert final_response.completion_text == "ok"
assert final_response.usage is not None
assert final_response.usage.input_other == 62
assert final_response.usage.input_cached == 2488
assert final_response.usage.output == 125
finally:
await provider.terminate()
@pytest.mark.asyncio
async def test_query_filters_empty_assistant_message_without_tool_calls(monkeypatch):
"""Test that empty assistant messages without tool_calls are filtered out."""