Compare commits

..

8 Commits

Author SHA1 Message Date
Soulter
7ab750c088 chore: bump version to 4.22.3 2026-04-05 01:50:12 +08:00
Rico0919x
70872cd44b feat(provider/vllm_rerank): add configurable rerank_api_suffix option (#7278)
* feat(provider/vllm_rerank): add configurable rerank_api_suffix option

Add rerank_api_suffix config option to the VLLM Rerank provider so
users can control the API URL path suffix instead of having /v1/rerank
hardcoded.

- Default value is /v1/rerank (preserves existing behavior)
- Users can set it to empty string to disable auto-append
- Handles suffix without leading slash by auto-adding one
- Schema, default config, and i18n metadata all updated

Issue: Fixes #7238

* fix(provider/vllm_rerank): handle null suffix and improve hint descriptions

- Add explicit None check for rerank_api_suffix (fixes HIGH from Gemini)
- Update rerank_api_base hint to describe actual behavior without
  mentioning specific provider options (fixes 3x MEDIUM from Gemini)
- Add ru-RU i18n for rerank_api_suffix (fixes P2 from Codex)

Co-authored-by: gemini-code-assist[bot]
Co-authored-by: chatgpt-codex-connector[bot]

---------

Co-authored-by: LehaoLin <linlehao@cuhk.edu.cn>
2026-04-05 00:15:27 +08:00
Soulter
dc9c17c195 feat: support token usage extraction for llama.cpp (#7358)
* feat: support token usage extraction for llama.cpp

* chore: ruff format
2026-04-04 23:49:18 +08:00
SoloLevelingAI
77d5d5cc6a fix(windows): inherit all system environment variables into MCP runner for Windows (#7054)
- Merge environment variables case‑insensitively
- Auto‑configure dotnet PATH and suppress console window
- Keep PATHEXT inheritance (from old implementation)

Replaces the minimal PATHEXT fix with a robust solution that resolves
common Windows subprocess issues, especially for .NET servers.

* fix(stdio): improve Windows environment setup for subprocesses

- Merge environment variables case‑insensitively
- Auto‑configure dotnet PATH and suppress console window
- Keep PATHEXT inheritance (from old implementation)
-Add `show_console` option to control `CREATE_NO_WINDOW` flag.Default remains hidden (backward compatible) but can be overridden.

Replaces the minimal PATHEXT fix with a robust solution that resolves
common Windows subprocess issues, especially for .NET servers.

* fix(stdio): improve Windows environment setup for subprocesses 修复(标准输入输出):改进 Windows 环境设置以支持子进程

- Merge environment variables case‑insensitively
- Auto‑configure dotnet PATH and suppress console window
- Keep PATHEXT inheritance (from old implementation)
-Add `show_console` option to control `CREATE_NO_WINDOW` flag.Default remains hidden (backward compatible) but can be overridden.

Replaces the minimal PATHEXT fix with a robust solution that resolves
common Windows subprocess issues, especially for .NET servers.

* fix(stdio): improve Windows environment setup for subprocesses 修复(标准输入输出):改进 Windows 环境设置以支持子进程

- Merge environment variables case‑insensitively
- Auto‑configure dotnet PATH and suppress console window
- Keep PATHEXT inheritance (from old implementation)
-Add `no_console` option to control `CREATE_NO_WINDOW` flag.Default remains hidden (backward compatible) but can be overridden.

Replaces the minimal PATHEXT fix with a robust solution that resolves
common Windows subprocess issues, especially for .NET servers.

* fix(stdio): improve Windows environment setup for subprocesses 修复(标准输入输出):改进 Windows 环境设置以支持子进程

- Merge environment variables case‑insensitively
- Auto‑configure dotnet PATH and suppress console window
- Keep PATHEXT inheritance (from old implementation)
-Add `no_console` option to control `CREATE_NO_WINDOW` flag.Default remains hidden (backward compatible) but can be overridden.

Replaces the minimal PATHEXT fix with a robust solution that resolves
common Windows subprocess issues, especially for .NET servers.

* fix(windows): inherit all system environment variables for subprocesses

- Merge environment variables case‑insensitively
- Auto‑configure dotnet PATH and suppress console window
- Keep PATHEXT inheritance (from old implementation)
- No longer handle as a special case for C#.

Replaces the minimal PATHEXT fix with a robust solution that resolves
common Windows subprocess issues, especially for .NET servers.

* chore: ruff format

---------

Co-authored-by: Soulter <905617992@qq.com>
2026-04-03 16:51:20 +08:00
Yufeng He
1408a8449e perf: Set content to None when the OpenAI message content list is empty (#6551)
_finally_convert_payload 提取 think 部分后,如果 assistant 消息的
所有 content 都是 think 类型,new_content 会变成空列表 []。
Grok 等 provider 不接受空 content list,直接报 400。

改为 new_content or None,空列表时回退到 None(OpenAI 兼容 API
普遍接受 null content 的 assistant 消息)。

Fixes #6447

Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>
2026-04-03 16:38:22 +08:00
Yufeng He
8f95ca9d98 fix: filter Gemini thinking parts from user-facing message chain (#7196)
Gemini 3 models return thinking parts (part.thought=True) alongside the
actual response text.  _process_content_parts was including these thinking
parts in the message chain sent to the user, effectively leaking internal
reasoning into the output.  On platforms that split long messages (e.g.
aiocqhttp with realtime segmenting), this caused duplicate or triple
replies since the thinking text often mirrors the actual response.

The streaming path already handled this correctly via chunk.text which
skips thinking parts, but the non-streaming path and the final-chunk
processing in streaming both went through _process_content_parts.

Also switch the Gemini 3 model name matching from an exhaustive list to
prefix matching (gemini-3- / gemini-3.) so new variants like gemini-3.1
get proper thinkingLevel config without code changes.

Fixes #7183
2026-04-03 16:35:25 +08:00
Yufeng He
5e78a24d63 fix: satisfy Google Gemini's function_response requirements to avoid 400 Invalid argument errors (#7216)
Gemini API requires function_response to be a google.protobuf.Struct
(JSON object). When tool results are plain text strings, the API
returns 400 Invalid argument. Detect non-JSON tool content for Gemini
models and wrap it in {"result": content} before sending.

Fixes #7134
2026-04-03 16:32:49 +08:00
Soulter
6a7b622c48 fix: resolve Discord/Misskey hot reload issue by fixing client_self_id misuse (#7331)
* fix: resolve Discord/Misskey hot reload issue by fixing client_self_id misuse

fixes: #7187
closes: #7188

* fix: english logging
2026-04-03 14:03:23 +08:00
12 changed files with 237 additions and 32 deletions

View File

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

View File

@@ -51,18 +51,29 @@ 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.setdefault("PATHEXT", pathext)
env = _merge_environment_variables(env)
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.2"
VERSION = "4.22.3"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
PERSONAL_WECHAT_CONFIG_METADATA = {
"weixin_oc_base_url": {
@@ -1761,6 +1761,7 @@ 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,
},
@@ -1826,7 +1827,12 @@ CONFIG_METADATA_2 = {
"rerank_api_base": {
"description": "重排序模型 API Base URL",
"type": "string",
"hint": "AstrBot 会在请求时在末尾加上 /v1/rerank。",
"hint": "最终请求路径由 Base URL 和路径后缀拼接而成(默认为 /v1/rerank",
},
"rerank_api_suffix": {
"description": "API URL 路径后缀",
"type": "string",
"hint": "追加到 base_url 后的路径,如 /v1/rerank。留空则不追加。",
},
"rerank_api_key": {
"description": "API Key",

View File

@@ -241,15 +241,10 @@ class ProviderGoogleGenAI(Provider):
thinking_config = types.ThinkingConfig(
thinking_budget=thinking_budget,
)
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
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.
# Gemini 2.5 series models don't support thinkingLevel; use thinkingBudget instead.
thinking_level = self.provider_config.get("gm_thinking_config", {}).get(
"level", "HIGH"
@@ -517,7 +512,11 @@ class ProviderGoogleGenAI(Provider):
):
chain.append(Comp.Plain("这是图片"))
for part in result_parts:
if part.text:
# 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:
chain.append(Comp.Plain(part.text))
if (

View File

@@ -532,6 +532,7 @@ class ProviderOpenAIOfficial(Provider):
**payloads,
stream=True,
extra_body=extra_body,
stream_options={"include_usage": True},
)
llm_response = LLMResponse("assistant", is_chunk=True)
@@ -539,12 +540,10 @@ class ProviderOpenAIOfficial(Provider):
state = ChatCompletionStreamState()
async for chunk in stream:
if not chunk.choices:
continue
choice = chunk.choices[0]
delta = choice.delta
choice = chunk.choices[0] if chunk.choices else None
delta = choice.delta if choice else None
if dtcs := delta.tool_calls:
if delta and (dtcs := delta.tool_calls):
for idx, tc in enumerate(dtcs):
# siliconflow workaround
if tc.function and tc.function.arguments:
@@ -574,7 +573,7 @@ class ProviderOpenAIOfficial(Provider):
_y = True
if chunk.usage:
llm_response.usage = self._extract_usage(chunk.usage)
elif choice_usage := getattr(choice, "usage", None):
elif choice and (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)
@@ -863,6 +862,9 @@ 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
@@ -874,11 +876,24 @@ class ProviderOpenAIOfficial(Provider):
reasoning_content += str(part.get("think"))
else:
new_content.append(part)
message["content"] = new_content
# reasoning key is "reasoning_content"
# Some providers (Grok, etc.) reject empty content lists.
# When all parts were think blocks, fall back to None.
message["content"] = new_content or None
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,6 +20,11 @@ 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")
@@ -45,8 +50,9 @@ 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(
f"{self.base_url}/v1/rerank",
rerank_url,
json=payload,
) as response:
response_data = await response.json()

68
changelogs/v4.22.3.md Normal file
View File

@@ -0,0 +1,68 @@
- [更新日志(简体中文)](#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,7 +1075,11 @@
},
"rerank_api_base": {
"description": "Rerank Model API Base URL",
"hint": "AstrBot appends /v1/rerank to the request 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."
},
"rerank_api_key": {
"description": "API Key",

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ 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
@@ -1175,6 +1176,93 @@ 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."""