From 0c194576c41482b1a574c3d6b18166bae6197248 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Fri, 27 Mar 2026 20:45:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20tool=5Fcall.functio?= =?UTF-8?q?n=20=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF=E5=92=8C=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tool.py: 重构 openai_schema 避免 dict[str, str] 类型推断问题 - openai_source.py: 使用 getattr 安全访问 tool_call.function - 解决 origin/dev 中的合并冲突 --- astrbot/core/agent/tool.py | 15 ++++---- .../core/provider/sources/openai_source.py | 37 ++++++++----------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index 55ec7d600..11f0aa741 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -224,19 +224,18 @@ class ToolSet: """Convert tools to OpenAI API function calling schema format.""" result = [] for tool in self.tools: - func_def: dict[str, Any] = { - "type": "function", - "function": {"name": tool.name}, - } + function_dict: dict[str, Any] = {"name": tool.name} if tool.description: - func_def["function"]["description"] = tool.description - + function_dict["description"] = tool.description if tool.parameters is not None: if ( tool.parameters and tool.parameters.get("properties") ) or not omit_empty_parameter_field: - func_def["function"]["parameters"] = tool.parameters - + function_dict["parameters"] = tool.parameters + func_def: dict[str, Any] = { + "type": "function", + "function": function_dict, + } result.append(func_def) return result diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 826a01548..d75fe236a 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -11,7 +11,6 @@ from pathlib import Path from typing import Any, Literal from urllib.parse import unquote, urlparse -import aiofiles import httpx from openai import AsyncAzureOpenAI, AsyncOpenAI from openai._exceptions import NotFoundError @@ -29,6 +28,7 @@ from astrbot.core.agent.message import ContentPart, ImageURLPart, Message, TextP from astrbot.core.agent.tool import ToolSet from astrbot.core.message.message_event_result import MessageChain from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult +from astrbot.core.provider.register import register_provider_adapter from astrbot.core.utils.io import download_image_by_url from astrbot.core.utils.network_utils import ( create_proxy_client, @@ -37,8 +37,6 @@ from astrbot.core.utils.network_utils import ( ) from astrbot.core.utils.string_utils import normalize_and_dedupe_strings -from ..register import register_provider_adapter - @register_provider_adapter( "openai_chat_completion", @@ -736,23 +734,28 @@ class ProviderOpenAIOfficial(Provider): # Should be unreachable raise Exception("工具集未提供") for tool in tools.list_tools(): + # Safely access function attribute (ChatCompletionMessageCustomToolCall doesn't have it) + tool_call_function = getattr(tool_call, "function", None) + if not tool_call_function: + continue if ( tool_call.type == "function" - and tool.name == tool_call.function.name + and tool_call_function.name == tool.name ): # workaround for #1454 - if isinstance(tool_call.function.arguments, str): - args = json.loads(tool_call.function.arguments) + arguments = tool_call_function.arguments + if isinstance(arguments, str): + args = json.loads(arguments) else: - args = tool_call.function.arguments + args = arguments args_ls.append(args) - func_name_ls.append(tool_call.function.name) + func_name_ls.append(tool_call_function.name) tool_call_ids.append(tool_call.id) - # gemini-2.5 / gemini-3 series extra_content handling - extra_content = getattr(tool_call, "extra_content", None) - if extra_content is not None: - tool_call_extra_content_dict[tool_call.id] = extra_content + # gemini-2.5 / gemini-3 series extra_content handling + extra_content = getattr(tool_call, "extra_content", None) + if extra_content is not None: + tool_call_extra_content_dict[tool_call.id] = extra_content llm_response.role = "tool" llm_response.tools_call_args = args_ls llm_response.tools_call_name = func_name_ls @@ -1127,7 +1130,6 @@ class ProviderOpenAIOfficial(Provider): ) -> dict: """组装成符合 OpenAI 格式的 role 为 user 的消息段""" - # 构建内容块列表 content_blocks = [] @@ -1177,18 +1179,11 @@ class ProviderOpenAIOfficial(Provider): async def encode_image_bs64(self, image_url: str) -> str: """将图片转换为 base64""" -<<<<<<< ours - if image_url.startswith("base64://"): - return image_url.replace("base64://", "data:image/jpeg;base64,") - async with aiofiles.open(image_url, "rb") as f: - image_bs64 = base64.b64encode(await f.read()).decode("utf-8") - return "data:image/jpeg;base64," + image_bs64 -======= + image_data = await self._image_ref_to_data_url(image_url, mode="strict") if image_data is None: raise RuntimeError(f"Failed to encode image data: {image_url}") return image_data ->>>>>>> theirs async def terminate(self): if self.client: