From 3c4d0f5a0f0d7ee6c1f46c25ad364844599dbadd Mon Sep 17 00:00:00 2001 From: Rat0323 <261020116+Rat0323@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:22:06 +0800 Subject: [PATCH 1/5] fix(gemini): automatically disable thinking config when tools are present --- astrbot/core/provider/sources/gemini_source.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 0c58174897..e350485d74 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -290,6 +290,11 @@ async def _prepare_query_config( else: thinking_config.thinking_level = level + # If any tools (native or custom function declarations) are active for this request, + # we must disable thinking because Gemini API does not support them simultaneously. + if tool_list: + thinking_config = None + return types.GenerateContentConfig( system_instruction=system_instruction, temperature=temperature, From 1809eaa9cabcc454649c4a3bae7d38db904aa1e0 Mon Sep 17 00:00:00 2001 From: Rat0323 <261020116+Rat0323@users.noreply.github.com> Date: Fri, 12 Jun 2026 20:37:28 +0800 Subject: [PATCH 2/5] chore(gemini): log warning when thinking is automatically disabled due to active tools --- astrbot/core/provider/sources/gemini_source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index e350485d74..ead95effb9 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -292,7 +292,8 @@ async def _prepare_query_config( # If any tools (native or custom function declarations) are active for this request, # we must disable thinking because Gemini API does not support them simultaneously. - if tool_list: + if tool_list and thinking_config: + logger.warning("[Gemini] Thinking config is automatically disabled because tools are active for this request.") thinking_config = None return types.GenerateContentConfig( From 744acde5b9729409e1ae9d58d223cda66f27e8f1 Mon Sep 17 00:00:00 2001 From: Rat0323 <261020116+Rat0323@users.noreply.github.com> Date: Fri, 12 Jun 2026 21:07:09 +0800 Subject: [PATCH 3/5] chore(gemini): prevent warning log when thinking_budget is 0 --- astrbot/core/provider/sources/gemini_source.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index ead95effb9..31ea9ddcde 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -293,7 +293,10 @@ async def _prepare_query_config( # If any tools (native or custom function declarations) are active for this request, # we must disable thinking because Gemini API does not support them simultaneously. if tool_list and thinking_config: - logger.warning("[Gemini] Thinking config is automatically disabled because tools are active for this request.") + if getattr(thinking_config, "thinking_budget", None) != 0: + logger.warning( + "[Gemini] Thinking config is automatically disabled because tools are active for this request." + ) thinking_config = None return types.GenerateContentConfig( From 7b190c27a9f8426077a75acbc474873cc403a017 Mon Sep 17 00:00:00 2001 From: Rat0323 <261020116+Rat0323@users.noreply.github.com> Date: Sat, 13 Jun 2026 07:39:49 +0800 Subject: [PATCH 4/5] fix(gemini): support parsing both assistant content and tool_calls simultaneously to avoid tool loop --- astrbot/core/provider/sources/gemini_source.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 31ea9ddcde..d843692970 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -386,11 +386,10 @@ def append_or_extend( append_or_extend(gemini_contents, parts, types.UserContent) elif role == "assistant": - if isinstance(content, str): - parts = [types.Part.from_text(text=content)] - append_or_extend(gemini_contents, parts, types.ModelContent) + parts = [] + if isinstance(content, str) and content: + parts.append(types.Part.from_text(text=content)) elif isinstance(content, list): - parts = [] thinking_signature = None text = "" for part in content: @@ -415,10 +414,8 @@ def append_or_extend( thought_signature=thinking_signature, ) ) - append_or_extend(gemini_contents, parts, types.ModelContent) - elif not native_tool_enabled and "tool_calls" in message: - parts = [] + if not native_tool_enabled and "tool_calls" in message: for tool in message["tool_calls"]: part = types.Part.from_function_call( name=tool["function"]["name"], @@ -436,15 +433,16 @@ def append_or_extend( if ts_bs64: part.thought_signature = base64.b64decode(ts_bs64) parts.append(part) - append_or_extend(gemini_contents, parts, types.ModelContent) - else: + + if not parts: logger.warning("assistant 角色的消息内容为空,已添加空格占位") if native_tool_enabled and "tool_calls" in message: logger.warning( "检测到启用Gemini原生工具,且上下文中存在函数调用,建议使用 /reset 重置上下文", ) parts = [types.Part.from_text(text=" ")] - append_or_extend(gemini_contents, parts, types.ModelContent) + + append_or_extend(gemini_contents, parts, types.ModelContent) elif role == "tool" and not native_tool_enabled: func_name = message.get("name", message["tool_call_id"]) From 413d4b9841161458717c71fc924167314142f23b Mon Sep 17 00:00:00 2001 From: Rat0323 <261020116+Rat0323@users.noreply.github.com> Date: Mon, 15 Jun 2026 06:19:33 +0800 Subject: [PATCH 5/5] fix(gemini): preserve assistant tool calls during history conversion --- .../core/provider/sources/gemini_source.py | 14 +-- tests/test_gemini_source.py | 102 ++++++++++++++++++ 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index d843692970..cf43fa2f38 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -290,15 +290,6 @@ async def _prepare_query_config( else: thinking_config.thinking_level = level - # If any tools (native or custom function declarations) are active for this request, - # we must disable thinking because Gemini API does not support them simultaneously. - if tool_list and thinking_config: - if getattr(thinking_config, "thinking_budget", None) != 0: - logger.warning( - "[Gemini] Thinking config is automatically disabled because tools are active for this request." - ) - thinking_config = None - return types.GenerateContentConfig( system_instruction=system_instruction, temperature=temperature, @@ -415,8 +406,9 @@ def append_or_extend( ) ) - if not native_tool_enabled and "tool_calls" in message: - for tool in message["tool_calls"]: + tool_calls = message.get("tool_calls") or [] + if not native_tool_enabled and tool_calls: + for tool in tool_calls: part = types.Part.from_function_call( name=tool["function"]["name"], args=json.loads(tool["function"]["arguments"]), diff --git a/tests/test_gemini_source.py b/tests/test_gemini_source.py index 4db8e92bfe..a9a1ca9c15 100644 --- a/tests/test_gemini_source.py +++ b/tests/test_gemini_source.py @@ -1,3 +1,5 @@ +import base64 + import pytest from astrbot.core.exceptions import EmptyModelOutputError @@ -27,3 +29,103 @@ def test_gemini_reasoning_only_output_is_allowed(): response_id="resp_reasoning", finish_reason="STOP", ) + + +def _make_gemini_provider_for_conversation(): + provider = object.__new__(ProviderGoogleGenAI) + provider.provider_config = { + "gm_native_coderunner": False, + "gm_native_search": False, + } + return provider + + +def _assistant_tool_call_message(content): + return { + "role": "assistant", + "content": content, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "get_pull_request_files", + "arguments": '{"owner":"AstrBotDevs","repo":"AstrBot","pull_number":8742}', + }, + }, + ], + } + + +def _first_model_parts(gemini_contents): + model_content = next( + content + for content in gemini_contents + if content.__class__.__name__ == "ModelContent" + ) + return model_content.parts or [] + + +def test_prepare_conversation_keeps_assistant_text_and_tool_calls(): + provider = _make_gemini_provider_for_conversation() + payloads = { + "messages": [ + {"role": "user", "content": "summarize this PR"}, + _assistant_tool_call_message("I will inspect the changed files first."), + ] + } + + parts = _first_model_parts(provider._prepare_conversation(payloads)) + + assert any(part.text == "I will inspect the changed files first." for part in parts) + assert [ + part.function_call.name + for part in parts + if getattr(part, "function_call", None) + ] == ["get_pull_request_files"] + + +def test_prepare_conversation_keeps_assistant_list_content_and_tool_calls(): + provider = _make_gemini_provider_for_conversation() + payloads = { + "messages": [ + {"role": "user", "content": "summarize this PR"}, + _assistant_tool_call_message( + [ + { + "type": "think", + "encrypted": base64.b64encode(b"signature").decode("utf-8"), + }, + {"type": "text", "text": "I will inspect the changed files first."}, + ] + ), + ] + } + + parts = _first_model_parts(provider._prepare_conversation(payloads)) + + assert any(part.text == "I will inspect the changed files first." for part in parts) + assert [ + part.function_call.name + for part in parts + if getattr(part, "function_call", None) + ] == ["get_pull_request_files"] + + +def test_prepare_conversation_ignores_null_tool_calls(): + provider = _make_gemini_provider_for_conversation() + payloads = { + "messages": [ + {"role": "user", "content": "hello"}, + { + "role": "assistant", + "content": "hello back", + "tool_calls": None, + }, + ] + } + + parts = _first_model_parts(provider._prepare_conversation(payloads)) + + assert [part.text for part in parts] == ["hello back"] + assert not any(getattr(part, "function_call", None) for part in parts)