mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-14 17:00:17 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d612c9eed4 | ||
|
|
12039fe7d0 |
@@ -1 +1 @@
|
||||
__version__ = "4.22.3"
|
||||
__version__ = "4.22.2"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user