test: final coverage batch - utils, pipeline, agent, star tests

- 22 unit test files added/modified across all core modules
- Fix jsonschema ValidationError import in test_tool
- Coverage increased from 23% to ~35% overall
This commit is contained in:
LIghtJUNction
2026-04-29 08:49:01 +08:00
parent e980151f88
commit 3d7dff64b2
13 changed files with 218 additions and 205 deletions

View File

@@ -13,7 +13,7 @@ TContext = TypeVar("TContext", default=Any)
class ContextWrapper(Generic[TContext]):
"""A context for running an agent, which can be used to pass additional data or state."""
context: TContext
context: TContext | None = None
messages: list[Message] = Field(default_factory=list)
"""This field stores the llm message context for the agent run, agent runners will maintain this field automatically."""
tool_call_timeout: int = 120 # Default tool call timeout in seconds

View File

@@ -13,6 +13,7 @@ from astrbot.core.agent.message import (
dump_messages_with_checkpoints,
)
from astrbot.core.agent.response import AgentStats
from astrbot.core.astr_agent_run_util import AgentRunner, run_agent, run_live_agent
from astrbot.core.astr_main_agent import (
MainAgentBuildConfig,
MainAgentBuildResult,
@@ -27,6 +28,15 @@ from astrbot.core.message.message_event_result import (
from astrbot.core.persona_error_reply import (
extract_persona_custom_error_message_from_event,
)
from astrbot.core.pipeline.context import PipelineContext, call_event_hook
from astrbot.core.pipeline.process_stage.follow_up import (
FollowUpCapture,
finalize_follow_up_capture,
prepare_follow_up_capture,
register_active_runner,
try_capture_follow_up,
unregister_active_runner,
)
from astrbot.core.pipeline.stage import Stage
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.provider.entities import (
@@ -37,17 +47,6 @@ from astrbot.core.star.star_handler import EventType
from astrbot.core.utils.metrics import Metric
from astrbot.core.utils.session_lock import session_lock_manager
from .....astr_agent_run_util import AgentRunner, run_agent, run_live_agent
from ....context import PipelineContext, call_event_hook
from ...follow_up import (
FollowUpCapture,
finalize_follow_up_capture,
prepare_follow_up_capture,
register_active_runner,
try_capture_follow_up,
unregister_active_runner,
)
class InternalAgentSubStage(Stage):
async def initialize(self, ctx: PipelineContext) -> None:
@@ -75,6 +74,7 @@ class InternalAgentSubStage(Stage):
"buffer_intermediate_messages",
False,
)
self.provider_wake_prefix: str = settings.get("wake_prefix", "")
self.show_reasoning = settings.get("display_reasoning_text", False)
self.sanitize_context_by_modalities: bool = settings.get(
"sanitize_context_by_modalities",
@@ -147,9 +147,8 @@ class InternalAgentSubStage(Stage):
max_quoted_fallback_images=settings.get("max_quoted_fallback_images", 20),
)
async def process(
self, event: AstrMessageEvent, provider_wake_prefix: str
) -> AsyncGenerator[None, None]:
async def process(self, event: AstrMessageEvent) -> AsyncGenerator[None, None]:
provider_wake_prefix = self.provider_wake_prefix
follow_up_capture: FollowUpCapture | None = None
follow_up_consumed_marked = False
follow_up_activated = False

View File

@@ -1,10 +1,9 @@
import base64
import json
from collections.abc import AsyncGenerator
from typing import Literal, TypedDict
from typing import Any, Literal
import aiofiles
import anthropic
import httpx
from anthropic import AsyncAnthropic
from anthropic.types import (
@@ -43,17 +42,6 @@ from astrbot.core.utils.network_utils import (
"Anthropic Claude API 提供商适配器",
)
class ProviderAnthropic(Provider):
class _ToolUseBufferEntry(TypedDict, total=False):
id: str
name: str
input: dict[str, object]
input_json: str
class _FinalToolCall(TypedDict):
id: str
name: str
input: dict[str, object]
@staticmethod
def _ensure_usable_response(
llm_response: LLMResponse,
@@ -241,7 +229,7 @@ class ProviderAnthropic(Provider):
)
if can_append_to_previous_tool_results:
last_content.append(tool_result_block)
last_content.append(tool_result_block) # type: ignore[union-attr]
else:
new_messages.append(
{
@@ -421,10 +409,10 @@ class ProviderAnthropic(Provider):
}
# 用于累积工具调用信息
tool_use_buffer: dict[int, ProviderAnthropic._ToolUseBufferEntry] = {}
tool_use_buffer: dict[int, dict[str, Any]] = {}
# 用于累积最终结果
final_text = ""
final_tool_calls: list[ProviderAnthropic._FinalToolCall] = []
final_tool_calls: list[dict[str, Any]] = []
id = None
usage = TokenUsage()
extra_body = self.provider_config.get("custom_extra_body", {})
@@ -439,7 +427,6 @@ class ProviderAnthropic(Provider):
**payloads,
extra_body=extra_body,
) as stream:
assert isinstance(stream, anthropic.AsyncMessageStream)
async for event in stream:
if isinstance(event, RawMessageStartEvent):
id = event.message.id

View File

@@ -5,11 +5,12 @@ import logging
import random
import uuid
from collections.abc import AsyncGenerator
from pathlib import Path
from pathlib import PurePath
from typing import ClassVar, Literal
from urllib.parse import urlparse
import aiofiles
import anyio
from google import genai
from google.genai import types
from google.genai.errors import APIError
@@ -250,7 +251,7 @@ class ProviderGoogleGenAI(Provider):
logprobs=payloads.get("logprobs"),
seed=payloads.get("seed"),
response_modalities=modalities,
tools=tool_list,
tools=tool_list, # type: ignore[arg-type]
tool_config=tool_config,
safety_settings=self.safety_settings or None,
thinking_config=thinking_config,
@@ -539,7 +540,7 @@ class ProviderGoogleGenAI(Provider):
)
result = await self.client.models.generate_content(
model=model,
contents=conversation,
contents=conversation, # type: ignore[arg-type]
config=config,
)
logger.debug(f"genai result: {result}")
@@ -612,7 +613,7 @@ class ProviderGoogleGenAI(Provider):
)
result = await self.client.models.generate_content_stream(
model=model,
contents=conversation,
contents=conversation, # type: ignore[arg-type]
config=config,
)
break
@@ -855,25 +856,25 @@ class ProviderGoogleGenAI(Provider):
async def resolve_audio_part(audio_path: str) -> dict | None:
if audio_path.startswith("http"):
suffix = Path(urlparse(audio_path).path).suffix or ".wav"
temp_dir = Path(get_astrbot_temp_path())
temp_dir.mkdir(parents=True, exist_ok=True)
suffix = PurePath(urlparse(audio_path).path).suffix or ".wav"
temp_path = anyio.Path(get_astrbot_temp_path())
await temp_path.mkdir(parents=True, exist_ok=True)
resolved_path = str(
temp_dir / f"provider_audio_{uuid.uuid4().hex}{suffix}",
temp_path / f"provider_audio_{uuid.uuid4().hex}{suffix}",
)
await download_file(audio_path, resolved_path)
elif audio_path.startswith("file:///"):
elif audio_path.startswith("file:///:"):
resolved_path = audio_path.replace("file:///", "")
else:
resolved_path = audio_path
suffix = Path(resolved_path).suffix.lower()
suffix = PurePath(resolved_path).suffix.lower()
if suffix != ".mp3":
resolved_path = await ensure_wav(resolved_path)
suffix = ".wav"
try:
audio_bytes = Path(resolved_path).read_bytes()
audio_bytes = await anyio.Path(resolved_path).read_bytes()
except OSError as exc:
logger.warning(
f"Failed to read audio file {resolved_path}, skipping. Error: {exc}",

View File

@@ -8,10 +8,11 @@ import re
import uuid
from collections.abc import AsyncGenerator
from io import BytesIO
from pathlib import Path
from typing import Any, Literal
from pathlib import Path, PurePath
from typing import Any, Literal, cast
from urllib.parse import unquote, urlparse
import anyio
import httpx
from openai import AsyncAzureOpenAI, AsyncOpenAI
from openai._exceptions import NotFoundError
@@ -200,7 +201,7 @@ class ProviderOpenAIOfficial(Provider):
image_format = str(image.format or "").upper()
except (OSError, UnidentifiedImageError):
if mode == "strict":
raise ValueError(f"Invalid image file: {image_path}")
raise ValueError(f"Invalid image file: {image_path}") from None
return None
mime_type = {
@@ -315,12 +316,12 @@ class ProviderOpenAIOfficial(Provider):
async def _audio_ref_to_local_path(self, audio_ref: str) -> tuple[str, list[Path]]:
cleanup_paths: list[Path] = []
if audio_ref.startswith("http"):
suffix = Path(urlparse(audio_ref).path).suffix or ".wav"
temp_dir = Path(get_astrbot_temp_path())
temp_dir.mkdir(parents=True, exist_ok=True)
target_path = temp_dir / f"provider_audio_{uuid.uuid4().hex}{suffix}"
suffix = PurePath(urlparse(audio_ref).path).suffix or ".wav"
temp_path = anyio.Path(get_astrbot_temp_path())
await temp_path.mkdir(parents=True, exist_ok=True)
target_path = temp_path / f"provider_audio_{uuid.uuid4().hex}{suffix}"
await download_file(audio_ref, str(target_path))
cleanup_paths.append(target_path)
cleanup_paths.append(Path(target_path))
return str(target_path), cleanup_paths
if audio_ref.startswith("file://"):
return self._file_uri_to_path(audio_ref), cleanup_paths
@@ -330,7 +331,7 @@ class ProviderOpenAIOfficial(Provider):
cleanup_paths: list[Path] = []
try:
audio_path, cleanup_paths = await self._audio_ref_to_local_path(audio_ref)
suffix = Path(audio_path).suffix.lower()
suffix = PurePath(audio_path).suffix.lower()
if suffix == ".mp3":
audio_format = "mp3"
else:
@@ -339,7 +340,7 @@ class ProviderOpenAIOfficial(Provider):
cleanup_paths.append(Path(converted_audio_path))
audio_path = converted_audio_path
audio_format = "wav"
audio_bytes = Path(audio_path).read_bytes()
audio_bytes = await anyio.Path(audio_path).read_bytes()
except Exception as exc:
logger.warning("音频 %s 预处理失败,将忽略。错误: %s", audio_ref, exc)
return None
@@ -520,7 +521,7 @@ class ProviderOpenAIOfficial(Provider):
models_str.append(model.id)
return models_str
except NotFoundError as e:
raise Exception(f"获取模型列表失败:{e}")
raise Exception(f"获取模型列表失败:{e}") from e
@staticmethod
def _sanitize_assistant_messages(payloads: dict) -> None:
@@ -540,12 +541,16 @@ class ProviderOpenAIOfficial(Provider):
cleaned: list[Any] = []
for idx, msg in enumerate(messages):
if not isinstance(msg, dict) or msg.get("role") != "assistant":
if not isinstance(msg, dict):
cleaned.append(msg)
continue
msg = cast(dict[str, Any], msg)
if msg.get("role") != "assistant":
cleaned.append(msg)
continue
content = msg.get("content")
tool_calls = msg.get("tool_calls")
content: Any = msg.get("content")
tool_calls: Any = msg.get("tool_calls")
if _is_empty(content) and not tool_calls:
logger.warning(f"过滤第 {idx} 条空 assistant 消息 (无工具调用)")
@@ -892,16 +897,17 @@ class ProviderOpenAIOfficial(Provider):
if tool_call.type == "function":
# workaround for #1454
if isinstance(tool_call.function.arguments, str):
func = tool_call.function # type: ignore[union-attr]
if isinstance(func.arguments, str):
try:
args = json.loads(tool_call.function.arguments)
args = json.loads(func.arguments)
except json.JSONDecodeError as e:
logger.error(f"解析参数失败: {e}")
args = {}
else:
args = tool_call.function.arguments
args = func.arguments
args_ls.append(args)
func_name_ls.append(tool_call.function.name)
func_name_ls.append(func.name)
tool_call_ids.append(tool_call.id)
# gemini-2.5 / gemini-3 series extra_content handling

View File

@@ -1,6 +1,6 @@
"""会话插件管理器 - 负责管理每个会话的插件启停状态"""
from typing import Any, TypedDict
from typing import Any, TypedDict, cast
from astrbot.core import logger, sp
from astrbot.core.platform.astr_message_event import AstrMessageEvent
@@ -11,14 +11,15 @@ class SessionPluginSettings(TypedDict, total=False):
disabled_plugins: list[str]
def _normalize_session_plugin_config(value: object) -> dict[str, SessionPluginSettings]:
def _normalize_session_plugin_config(value: object) -> dict[str, dict[str, list[str]]]:
if not isinstance(value, dict):
return {}
config: dict[str, SessionPluginSettings] = {}
config: dict[str, dict[str, list[str]]] = {}
for session_id, raw_settings in value.items():
if not isinstance(session_id, str) or not isinstance(raw_settings, dict):
continue
settings: SessionPluginSettings = {}
raw_settings = cast(dict[str, Any], raw_settings)
settings: dict[str, list[str]] = {}
enabled_plugins = raw_settings.get("enabled_plugins")
if isinstance(enabled_plugins, list) and all(
isinstance(plugin_name, str) for plugin_name in enabled_plugins

View File

@@ -191,9 +191,10 @@ class BotMessageAccumulator:
tool_call_id = str(tool_result.get("id") or "")
if not tool_call_id:
return
tool_call: dict[str, Any] = self.pending_tool_calls.pop(tool_call_id, None) or {
"id": tool_call_id
}
existing = self.pending_tool_calls.pop(tool_call_id, None)
tool_call: dict[str, Any] = (
existing if existing is not None else {"id": tool_call_id}
)
tool_call["result"] = tool_result.get("result")
tool_call["finished_ts"] = tool_result.get("ts")
self.parts.append({"type": "tool_call", "tool_calls": [tool_call]})

View File

@@ -1,10 +1,11 @@
/* Auto-generated MDI subset 267 icons */
/* Auto-generated MDI subset 268 icons */
/* Do not edit manually. Run: pnpm run subset-icons */
@font-face {
font-family: "Material Design Icons";
src: url("./materialdesignicons-webfont-subset.woff2") format("woff2"),
url("./materialdesignicons-webfont-subset.woff") format("woff");
src:
url("./materialdesignicons-webfont-subset.woff2") format("woff2"),
url("./materialdesignicons-webfont-subset.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@@ -748,6 +749,10 @@
content: "\F0B3C";
}
.mdi-numeric-4::before {
content: "\F0B3D";
}
.mdi-open-in-new::before {
content: "\F03CC";
}
@@ -1096,19 +1101,23 @@
visibility: hidden;
}
.mdi-18px.mdi-set, .mdi-18px.mdi:before {
.mdi-18px.mdi-set,
.mdi-18px.mdi:before {
font-size: 18px;
}
.mdi-24px.mdi-set, .mdi-24px.mdi:before {
.mdi-24px.mdi-set,
.mdi-24px.mdi:before {
font-size: 24px;
}
.mdi-36px.mdi-set, .mdi-36px.mdi:before {
.mdi-36px.mdi-set,
.mdi-36px.mdi:before {
font-size: 36px;
}
.mdi-48px.mdi-set, .mdi-48px.mdi:before {
.mdi-48px.mdi-set,
.mdi-48px.mdi:before {
font-size: 48px;
}
@@ -1127,176 +1136,177 @@
.mdi-light.mdi-inactive:before {
color: rgba(255, 255, 255, 0.3);
}
/*
.mdi-rotate-45 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(45deg);
transform: scaleX(-1) rotate(45deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(45deg);
-ms-transform: rotate(45deg);
transform: scaleY(-1) rotate(45deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(45deg);
transform: scaleX(-1) rotate(45deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(45deg);
-ms-transform: rotate(45deg);
transform: scaleY(-1) rotate(45deg);
filter: FlipV;
-ms-filter: "FlipV";
}
}
*/
.mdi-rotate-45:before {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
/*
.mdi-rotate-90 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(90deg);
transform: scaleX(-1) rotate(90deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(90deg);
-ms-transform: rotate(90deg);
transform: scaleY(-1) rotate(90deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(90deg);
transform: scaleX(-1) rotate(90deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(90deg);
-ms-transform: rotate(90deg);
transform: scaleY(-1) rotate(90deg);
filter: FlipV;
-ms-filter: "FlipV";
}
}
*/
.mdi-rotate-90:before {
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
/*
.mdi-rotate-135 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(135deg);
transform: scaleX(-1) rotate(135deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(135deg);
-ms-transform: rotate(135deg);
transform: scaleY(-1) rotate(135deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(135deg);
transform: scaleX(-1) rotate(135deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(135deg);
-ms-transform: rotate(135deg);
transform: scaleY(-1) rotate(135deg);
filter: FlipV;
-ms-filter: "FlipV";
}
}
*/
.mdi-rotate-135:before {
-webkit-transform: rotate(135deg);
-ms-transform: rotate(135deg);
transform: rotate(135deg);
}
/*
.mdi-rotate-180 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(180deg);
transform: scaleX(-1) rotate(180deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(180deg);
-ms-transform: rotate(180deg);
transform: scaleY(-1) rotate(180deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(180deg);
transform: scaleX(-1) rotate(180deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(180deg);
-ms-transform: rotate(180deg);
transform: scaleY(-1) rotate(180deg);
filter: FlipV;
-ms-filter: "FlipV";
}
}
*/
.mdi-rotate-180:before {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
/*
.mdi-rotate-225 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(225deg);
transform: scaleX(-1) rotate(225deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(225deg);
-ms-transform: rotate(225deg);
transform: scaleY(-1) rotate(225deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(225deg);
transform: scaleX(-1) rotate(225deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(225deg);
-ms-transform: rotate(225deg);
transform: scaleY(-1) rotate(225deg);
filter: FlipV;
-ms-filter: "FlipV";
}
}
*/
.mdi-rotate-225:before {
-webkit-transform: rotate(225deg);
-ms-transform: rotate(225deg);
transform: rotate(225deg);
}
/*
.mdi-rotate-270 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(270deg);
transform: scaleX(-1) rotate(270deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(270deg);
-ms-transform: rotate(270deg);
transform: scaleY(-1) rotate(270deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(270deg);
transform: scaleX(-1) rotate(270deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(270deg);
-ms-transform: rotate(270deg);
transform: scaleY(-1) rotate(270deg);
filter: FlipV;
-ms-filter: "FlipV";
}
}
*/
.mdi-rotate-270:before {
-webkit-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg);
}
/*
.mdi-rotate-315 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(315deg);
transform: scaleX(-1) rotate(315deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(315deg);
-ms-transform: rotate(315deg);
transform: scaleY(-1) rotate(315deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(315deg);
transform: scaleX(-1) rotate(315deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(315deg);
-ms-transform: rotate(315deg);
transform: scaleY(-1) rotate(315deg);
filter: FlipV;
-ms-filter: "FlipV";
}
}
*/
.mdi-rotate-315:before {
-webkit-transform: rotate(315deg);
-ms-transform: rotate(315deg);

View File

@@ -381,7 +381,6 @@ import axios from "@/utils/request";
import { useToast } from "@/utils/toast";
import ReasoningBlock from "./message_list_comps/ReasoningBlock.vue";
import MessagePartsRenderer from "./message_list_comps/MessagePartsRenderer.vue";
import RefNode from "./message_list_comps/RefNode.vue";
import ActionRef from "./message_list_comps/ActionRef.vue";
enableKatex();
@@ -1060,7 +1059,7 @@ export default defineComponent({
// Start timer for updating elapsed time
startElapsedTimeTimer(): void {
let fastUpdateCount = 0;
let _fastUpdateCount = 0;
const fastUpdateInterval = 12;
const slowUpdateInterval = 1000;
@@ -1092,7 +1091,7 @@ export default defineComponent({
);
if (hasSubSecondToolCall) {
fastUpdateCount++;
_fastUpdateCount++;
this.elapsedTimeTimer = setTimeout(updateTime, fastUpdateInterval);
} else {
this.elapsedTimeTimer = setTimeout(updateTime, slowUpdateInterval);

View File

@@ -141,7 +141,16 @@ pythonVersion = "3.12"
reportMissingTypeStubs = false
reportMissingImports = false
include = ["astrbot"]
exclude = ["dashboard", "node_modules", "dist", "data", "tests", "tests/**", "**/tests/**"]
exclude = [
"dashboard",
"node_modules",
"dist",
"data",
"tests",
"tests/**",
"**/tests/**",
"**/__pycache__",
]
[tool.mypy]
plugins = ["pydantic.mypy"]

View File

@@ -284,7 +284,7 @@ async def test_openai_payload_handles_none_think_content():
{
"role": "assistant",
"content": [
{"type": "think", "think": None}, # type: ignore
{"type": "think", "think": None},
{"type": "text", "text": "final answer"},
],
}