Skip to content

fix(provider): support combining Gemini built-in tools with function calling - #11078

Open
DOTBave wants to merge 5 commits into
AstrBotDevs:masterfrom
DOTBave:fix/gemini-tool-combination
Open

DOTBave wants to merge 5 commits into
AstrBotDevs:masterfrom
DOTBave:fix/gemini-tool-combination

Conversation

@DOTBave

@DOTBave DOTBave commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #9937.

Motivation

Since #8833 allowed Gemini built-in tools (gm_native_search, gm_native_coderunner, gm_url_context) and custom function declarations (function_declarations) to coexist in the same generateContent request, enabling any built-in tool alongside function tools (whether from a third-party plugin or built-in AstrBot tools such as web_search_exa and astrbot_execute_python) on Gemini 3/4 models fails with two cascading 400 INVALID_ARGUMENT errors reported in #9937:

  1. First turn (include_server_side_tool_invocations not set):
    400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Please enable tool_config.include_server_side_tool_invocations to use Built-in tools with Function calling.', 'status': 'INVALID_ARGUMENT'}}
    
  2. Second turn (functionResponse.name populated with tool_call_id instead of function name, and server-side tool invocation parts dropped):
    antigravity executor: invalid Gemini function call history: ... functionResponse.name "call_513417" does not match functionCall.name "web_search_exa" ...
    

Root Cause

  1. Missing ToolConfig opt-in & incompatible calling mode: Combining Gemini built-in tools with function_declarations requires tool_config.include_server_side_tool_invocations = True and FunctionCallingConfigMode.VALIDATED (AUTO is rejected when server-side tool invocations are enabled). This capability is only supported on Gemini 3/4 models (Gemini 1.x/2.x reject the flag) and requires google-genai>=1.68.0 for the ToolConfig.include_server_side_tool_invocations field and Part.tool_call / Part.tool_response types.
  2. Dropped server-side tool parts across turns: When include_server_side_tool_invocations=True, the model response includes server-side tool_call / tool_response (and executable_code / code_execution_result) parts with id, tool_type, and thought_signature that must be replayed in subsequent turns of the tool loop. Previously, _process_content_parts and _query_stream discarded these parts, and _prepare_conversation could also duplicate thought_signature onto accompanying narration text parts.
  3. functionResponse.name mismatch on tool results: When server-side tool invocations are enabled, Gemini populates part.function_call.id (e.g., call_513417). AstrBot stores that ID in tool_call_id on role="tool" messages without a "name" field, causing _prepare_conversation to fall back to message.get("name", message["tool_call_id"]) and send the call ID instead of the function name in FunctionResponse.name.

Related Issues & PRs

Modifications / 改动点

  • astrbot/core/provider/sources/gemini_source.py:
    • _prepare_query_config (L263–295): Detect built-in tools (google_search, code_execution, url_context) when function_declarations are present. Enable combines_tools when has_builtin_tool is true and model_name.lower() contains "gemini-3" or "gemini-4" (substring matching supports gateway/relay prefix aliases such as [抗截断]gemini-3.8-flash or gateway/gemini-3.8-flash-high). Set FunctionCallingConfigMode.ANY when tool_choice == "required", VALIDATED when combines_tools is true, and AUTO otherwise; set ToolConfig(include_server_side_tool_invocations=True) only when combines_tools is true.
    • _prepare_conversation — tool_id_to_name mapping (L439–442, L467–469, L548–564): Record tool["id"] -> tool["function"]["name"] while iterating assistant tool_calls, and resolve role="tool" FunctionResponse.name via message.get("name") or tool_id_to_name.get(tool_call_id, tool_call_id).
    • _prepare_conversation — server-side part replay (L462–495, L541): Reconstruct extra_content.google.server_parts via types.Part.model_validate(...) into tool_parts before appending function_call parts and restoring thought_signature.
    • _prepare_conversation — thought_signature deduplication (L519–540): Compare decoded thinking_signature bytes against tool_part.thought_signature across tool_parts (has_duplicate_thought_signature, covering both custom function_call and replayed server_parts) so narration text parts omit duplicate signatures (and skip empty narration parts) while preserving distinct narration signatures.
    • _process_content_parts (L715–735) & _query_stream (L906–971): Capture server-side tool parts (tool_call, tool_response, executable_code, code_execution_result) via part.model_dump(exclude_none=True, mode="json") (accumulating across stream chunks via pending_server_side_parts) and attach them to the first custom tool call's extra_content.google.server_parts.
  • pyproject.toml (L29) & requirements.txt (L18):
    • Bump minimum google-genai version to >=1.68.0 (first release providing ToolConfig.include_server_side_tool_invocations and Part.tool_call / Part.tool_response).
  • tests/test_gemini_source.py:
    • Add 12 unit and regression test functions covering VALIDATED + include_server_side_tool_invocations=True opt-in, model alias / case-insensitive / Gemini 4 matching, tool_choice="required" (ANY mode preservation), function-only (AUTO), built-in-only (tool_config=None), legacy Gemini 2.x exclusion, non-streaming and streaming server-side part capture & replay (with full field-level assertions and malformed-part fallback coverage), functionResponse.name resolution from tool_call_id, and thought_signature deduplication / distinct signature preservation with empty and non-empty narration.

Behavior Matrix

Scenario function_calling_config.mode include_server_side_tool_invocations Changed?
Function tools only AUTO (or ANY if required) None No
Built-in tools only tool_config = None None No
Legacy Gemini 1.x / 2.x + built-in + function tools AUTO (or ANY if required) None No
Gemini 3/4 + built-in + function tools (tool_choice default) VALIDATED True Yes (previously 400)
Gemini 3/4 + built-in + function tools (tool_choice="required") ANY True Yes (previously 400)
  • This is NOT a breaking change. / 这不是一个破坏性变更。
    • Only the Gemini 3/4 built-in + function tool combination path (which previously failed unconditionally with HTTP 400) changes behavior. All other request shapes remain unchanged. Raising the google-genai floor to >=1.68.0 only affects environments with an older SDK installed.

Screenshots or Test Results / 运行截图或测试结果

1. Automated Tests & Linting

$ uv run pytest tests/test_gemini_source.py -q
32 passed, 1 warning in 4.03s

$ uv run pytest tests/ -k gemini -q
36 passed, 5596 deselected, 7 warnings in 11.92s

$ uv run ruff format --check astrbot/core/provider/sources/gemini_source.py tests/test_gemini_source.py
2 files already formatted

$ uv run ruff check astrbot/core/provider/sources/gemini_source.py tests/test_gemini_source.py
All checks passed!

2. End-to-End Runtime Verification (Official Gemini API)

Note on plugin independence: Any registered function tool triggers this path, including AstrBot's built-in web_search_exa and astrbot_execute_python (as seen in #9937). Third-party plugins are not required to reproduce the bug.

  • Setup: google_gemini provider configured with gemini-3.8-flash against the official Gemini API endpoint (generativelanguage.googleapis.com), gm_native_search enabled, and a third-party image generation plugin registering the generate_image function tool.
  • Before fix (immediate 400 INVALID_ARGUMENT on turn 1; if only the flag is enabled, turn 2 fails with 400 INVALID_ARGUMENT due to functionResponse.name mismatch):
[11:17:57.544] [Core] [INFO] [core.event_bus:149]: [DEBUG-gemini] [Gemini(discord)] <user>: 搜索崩坏三的角色,希儿·芙乐艾,并画出她。
[11:17:57.798] [Core] [WARN] [v4.29.4] [runners.tool_loop_agent_runner:636]: Chat Model gemini-3.8-flash/gemini-3.8-flash request error: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Please enable tool_config.include_server_side_tool_invocations to use Built-in tools with Function calling.', 'status': 'INVALID_ARGUMENT'}}
Traceback (most recent call last):
  File "/AstrBot/astrbot/core/agent/runners/tool_loop_agent_runner.py", line 586, in _iter_llm_responses_with_fallback
    async for attempt in retrying:
  File "/usr/local/lib/python3.12/site-packages/tenacity/asyncio/__init__.py", line 170, in __anext__
    do = await self.iter(retry_state=self._retry_state)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/tenacity/asyncio/__init__.py", line 157, in iter
    result = await action(retry_state)
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/tenacity/_utils.py", line 111, in inner
    return call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/tenacity/__init__.py", line 393, in <lambda>
    self._add_action_func(lambda rs: rs.outcome.result())
                                     ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/concurrent/futures/_base.py", line 449, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "/AstrBot/astrbot/core/agent/runners/tool_loop_agent_runner.py", line 592, in _iter_llm_responses_with_fallback
    async for resp in self._iter_llm_responses(
  File "/AstrBot/astrbot/core/agent/runners/tool_loop_agent_runner.py", line 529, in _iter_llm_responses
    resp = await self._await_or_stop(anext(stream))  # type: ignore
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/AstrBot/astrbot/core/agent/runners/tool_loop_agent_runner.py", line 495, in _await_or_stop
    return operation_task.result()
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/AstrBot/astrbot/core/provider/sources/gemini_source.py", line 1081, in text_chat_stream
    if await self._handle_api_error(e, keys):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/AstrBot/astrbot/core/provider/sources/gemini_source.py", line 199, in _handle_api_error
    raise e
  File "/AstrBot/astrbot/core/provider/sources/gemini_source.py", line 1072, in text_chat_stream
    async for response in self._query_stream(
  File "/AstrBot/astrbot/core/provider/sources/gemini_source.py", line 843, in _query_stream
    async for chunk in result:
  File "/usr/local/lib/python3.12/site-packages/google/genai/models.py", line 8832, in stream_generator
    async for chunk in response:  # type: ignore[attr-defined]
  File "/usr/local/lib/python3.12/site-packages/google/genai/models.py", line 8456, in _generate_content_stream_with_continuation
    if not state.end_hop(error):
           ^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/google/genai/_extra_utils.py", line 1011, in end_hop
    raise error
  File "/usr/local/lib/python3.12/site-packages/google/genai/models.py", line 8449, in _generate_content_stream_with_continuation
    response_stream = await self._generate_content_stream(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/google/genai/models.py", line 7134, in _generate_content_stream
    response_stream = await self._api_client.async_request_streamed(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/google/genai/_api_client.py", line 2136, in async_request_streamed
    response = await self._async_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/google/genai/_api_client.py", line 2052, in _async_request
    return await self._async_retry(  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/tenacity/asyncio/__init__.py", line 112, in __call__
    do = await self.iter(retry_state=retry_state)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/tenacity/asyncio/__init__.py", line 157, in iter
    result = await action(retry_state)
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/tenacity/_utils.py", line 111, in inner
    return call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/tenacity/__init__.py", line 413, in exc_check
    raise retry_exc.reraise()
          ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/tenacity/__init__.py", line 184, in reraise
    raise self.last_attempt.result()
          ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/concurrent/futures/_base.py", line 449, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "/usr/local/lib/python3.12/site-packages/tenacity/asyncio/__init__.py", line 116, in __call__
    result = await fn(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/google/genai/_api_client.py", line 1947, in _async_request_once
    await errors.APIError.raise_for_async_response(client_response)
  File "/usr/local/lib/python3.12/site-packages/google/genai/errors.py", line 279, in raise_for_async_response
    await cls.raise_error_async(status_code, response_json, response)
  File "/usr/local/lib/python3.12/site-packages/google/genai/errors.py", line 301, in raise_error_async
    raise ClientError(status_code, response_json, response)
google.genai.errors.ClientError: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Please enable tool_config.include_server_side_tool_invocations to use Built-in tools with Function calling.', 'status': 'INVALID_ARGUMENT'}}
[11:17:57.806] [Core] [INFO] [respond.stage:237]: Prepare to send - <user>: LLM 响应错误: All chat models failed: ClientError: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Please enable tool_config.include_server_side_tool_invocations to use Built-in tools with Function calling.', 'status': 'INVALID_ARGUMENT'}}
  • After fix (full multi-turn tool invocation and tool result round-trip without 400 errors or provider fallback):
[19:51:37.257] [Core] [INFO] [core.event_bus:153]: [DEV-gemini-copy] [Gemini(discord)] <user>: reset
[19:51:37.285] [Core] [INFO] [respond.stage:237]: Prepare to send - <user>: ✅ The current conversation context has been cleared.
[19:52:43.259] [Core] [INFO] [core.event_bus:153]: [DEV-gemini-copy] [Gemini(discord)] <user>: 搜索崩坏三的角色,希儿·芙乐艾,并画出她。
[19:52:49.444] [Core] [INFO] [runners.tool_loop_agent_runner:1416]: Agent 使用工具: ['generate_image']
[19:52:49.444] [Core] [INFO] [runners.tool_loop_agent_runner:1471]: 使用工具:generate_image,参数:{'aspect_ratio': '3:4', 'prompt': '1girl, Seele Vollerei from Honkai Impact 3rd, short dark blue bob hair with inner blue highlights, dark blue eyes, delicate facial features, wearing Swallowtail Phantasm outfit, white and navy blue dress with butterfly motifs, holding a large ornate scythe, glowing translucent blue butterflies floating around, quantum sea particles, dark dreamy fantasy background, anime illustration, highly detailed, masterpieces.'}
[19:52:49.451] [astrbot_plugin_omnidraw] [INFO] [core.chain_manager:92]: 🚀 [Chain] 正在将任务交由节点 [node_1] 处理...
[19:52:49.452] [astrbot_plugin_omnidraw] [INFO] [providers.gemini_official_impl:240]: 📝 [Gemini原生通道] 最终发送给 API 的核心提示词:
1girl, Seele Vollerei from Honkai Impact 3rd, short dark blue bob hair with inner blue highlights, dark blue eyes, delicate facial features, wearing Swallowtail Phantasm outfit, white and navy blue dress with butterfly motifs, holding a large ornate scythe, glowing translucent blue butterflies floating around, quantum sea particles, dark dreamy fantasy background, anime illustration, highly detailed, masterpieces.
[19:52:49.452] [astrbot_plugin_omnidraw] [INFO] [providers.gemini_official_impl:185]: 📤 [Gemini原生通道] 请求路径: https://generativelanguage.googleapis.com/v1beta/models/gemini-nano-banana-2.1:generateContent
[19:52:49.452] [astrbot_plugin_omnidraw] [INFO] [providers.gemini_official_impl:186]: 📤 [Gemini原生通道] 请求体摘要: {"contents": [{"role": "user", "parts": [{"text": "<text chars=417>"}]}], "generationConfig": {"responseModalities": ["TEXT", "IMAGE"], "imageConfig": {"aspectRatio": "3:4"}}}
[19:53:02.910] [astrbot_plugin_omnidraw] [INFO] [core.chain_manager:97]: ✅ [Chain] 节点 [node_1] 创作成功!
[19:53:08.215] [Core] [INFO] [runners.tool_loop_agent_runner:1681]: Tool `generate_image` Result: 系统提示:已成功下发 1 张图。
[19:53:10.408] [Core] [INFO] [respond.stage:237]: Prepare to send - <user>: 已完成《崩坏3》角色「希儿·芙乐艾」(Seele Vollerei)的检索与图像生成,核心特征包括深蓝短发、蓝瞳、量子蝴蝶元素及经典服饰配置。

Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。 (Bug fix only, no new features; tracked in [Bug] gemini API格式工具调用必定报错 #9937, related to fix(provider): opt in server-side tool invocations for gemini built-in tools #9978 / fix: preserve Gemini tool response function names #10174 / fix(dashboard): correct outdated Gemini native tool hints #10875.)

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。

  • 📚 I checked the affected WebUI instructions and screenshots in docs/zh and docs/en against the changed navigation, page structure, and labels, and updated them in this PR (or explained why no documentation update is needed). For renamed, moved, or merged entry points, I included an old entry → new entry mapping in the documentation and changelog.
    / 我已对照变化后的 WebUI 入口、页面结构和术语,核对并在本 PR 中更新 docs/zh 和 docs/en 的相关操作说明与截图(或说明无需更新文档的原因)。入口改名、移动或合并时,已在文档和 changelog 中补充 旧入口 → 新入口 对照。 (No WebUI navigation, page structure, or terminology changes; related config hint text was already updated in fix(dashboard): correct outdated Gemini native tool hints #10875.)

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txt 和 pyproject.toml 文件相应位置。 (No new dependencies added; bumped existing google-genai floor to >=1.68.0 in both pyproject.toml and requirements.txt.)

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Enable reliable Gemini 3/4 built-in tool and function-calling combinations across multi-turn tool interactions.

Bug Fixes:

  • Fix Gemini 3/4 requests that combine built-in tools with custom function calling by enabling the required server-side invocation mode.
  • Preserve server-side tool parts across tool-loop turns and correctly map tool results back to their function names.
  • Prevent duplicate Gemini thought signatures when replaying combined tool responses.

Enhancements:

  • Handle Gemini model aliases and retain existing behavior for function-only, built-in-only, and legacy Gemini requests.

Build:

  • Raise the minimum google-genai version to 1.68.0.

Tests:

  • Add regression coverage for combined tool configuration, server-side part capture and replay, function-name mapping, model compatibility, and thought-signature handling.

…mbination

When native built-in tools and function declarations are sent in the same
request, Gemini 3/4 requires include_server_side_tool_invocations and
rejects AUTO function calling mode once the flag is enabled. Opt in only
for Gemini 3+ models, use VALIDATED mode for automatic tool choice.

Bump google-genai to 1.68.0, the first release exposing the ToolConfig
field.

Refs AstrBotDevs#9937
Replay the server-side tool_call/tool_response parts (including their
thought signatures) returned by Gemini when built-in tools and function
calling are combined. The parts are stored on the custom tool call's
extra_content so later tool-loop steps can send them back unchanged.

Also resolve tool result messages to their function names when rebuilding
history (they only carry tool_call_id), and stop re-attaching thought
signatures that tool calls or server-side parts already carry.

Refs AstrBotDevs#9937
@sourcery-ai

sourcery-ai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Linked issues

Checked at commit f3ab356

#9937 [Bug] gemini API格式工具调用必定报错 · 2 of 2 met

# Requirement Status Notes
1 Use Gemini 3.8 native search together with function tools without the request failing. ✅ Met The request config opts into server-side tool invocations and uses VALIDATED mode for Gemini 3/4 combinations; returned server-side parts are also retained for later tool-loop turns.
2 Avoid the tool-history error where a tool result uses the call ID instead of the function name. ✅ Met Conversation reconstruction maps each assistant tool-call ID to its function name and uses that name for the corresponding tool result.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 8 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="506-507" />
<code_context>
+                        # carry their own thought signatures. Attaching the same
+                        # signature to the narration part would send it twice, so
+                        # leave the narration part unsigned and skip it when empty.
+                        if text:
+                            parts.append(types.Part(text=text))
                     else:
                         parts.append(
</code_context>
<issue_to_address>
**Gemini history loses part order**

When a Gemini response interleaves narration, server-side parts, and custom function calls, including across stream chunks, `_process_content_parts` stores server-side parts separately, and `_prepare_conversation` replays them before function calls; `_query_stream` also returns at a function-call chunk without collecting later chunks. The next request therefore sends reordered or incomplete tool history, which Gemini can reject or mis-handle.

Preserve the original order of narration, server-side parts, and function calls, and collect all relevant parts before returning a streamed response.

Also at `astrbot/core/provider/sources/gemini_source.py:527-538`, `astrbot/core/provider/sources/gemini_source.py:722-743`, `astrbot/core/provider/sources/gemini_source.py:931`, `astrbot/core/provider/sources/gemini_source.py:971`.
</issue_to_address>

### Comment 2
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="735" />
<code_context>
+            or getattr(part, "executable_code", None)
+            or getattr(part, "code_execution_result", None)
+        ]
+        if server_side_parts and llm_response.tools_call_ids:
+            google_extra = llm_response.tools_call_extra_content.setdefault(
+                llm_response.tools_call_ids[0], {}
</code_context>
<issue_to_address>
**Built-in tool history disappears**

When gemini returns server-side tool parts without a custom function call and the conversation continues, `_process_content_parts` and the streaming path attach server-side parts only to custom tool calls, while the runner stores only assistant text when there is no custom call. The next Gemini request omits the built-in tool history and can fail validation or lose tool context.

Persist server-side parts in the response history even when Gemini returns no custom function call.

Also at `astrbot/core/provider/sources/gemini_source.py:940-941`, `astrbot/core/provider/sources/gemini_source.py:971-978`.
</issue_to_address>

### Comment 3
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="501" />
<code_context>
-                        pass
+                        for tool in message.get("tool_calls") or []
+                    )
+                    if thinking_signature and has_tool_thought_signature:
+                        # Tool calls and replayed server-side tool parts already
+                        # carry their own thought signatures. Attaching the same
</code_context>
<issue_to_address>
**Narration signature is omitted**

When the narration signature differs from a signature on a tool or server-side part, `_prepare_conversation` treats any tool or server-side signature as a duplicate without comparing values, so it omits the narration signature. Gemini receives incomplete signed history on the next request.

Suppress the narration signature only when a tool or server-side part carries the same signature.
</issue_to_address>

### Comment 4
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="736-740" />
<code_context>
+            or getattr(part, "code_execution_result", None)
+        ]
+        if server_side_parts and llm_response.tools_call_ids:
+            google_extra = llm_response.tools_call_extra_content.setdefault(
+                llm_response.tools_call_ids[0], {}
+            ).setdefault("google", {})
+            google_extra["server_parts"] = [
+                *server_side_parts,
+                *(google_extra.get("server_parts") or []),
+            ]
</code_context>
<issue_to_address>
**Requery loses server-side context**

When the `skills_like` path re-queries and returns different tool-call IDs, `ToolLoopAgentRunner._step` replaces the tool-call IDs without transferring `tools_call_extra_content`, leaving the saved server-side parts under the original IDs. The next Gemini request omits that context.

Transfer the saved `tools_call_extra_content` to the re-query's tool-call IDs.

Also at `astrbot/core/provider/sources/gemini_source.py:735`.
</issue_to_address>

### Comment 5
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="735-743" />
<code_context>
+            or getattr(part, "executable_code", None)
+            or getattr(part, "code_execution_result", None)
+        ]
+        if server_side_parts and llm_response.tools_call_ids:
+            google_extra = llm_response.tools_call_extra_content.setdefault(
+                llm_response.tools_call_ids[0], {}
+            ).setdefault("google", {})
+            google_extra["server_parts"] = [
+                *server_side_parts,
+                *(google_extra.get("server_parts") or []),
+            ]
         chain_result = MessageChain(chain=chain)
         llm_response.result_chain = chain_result
         if validate_output:
</code_context>
<issue_to_address>
**Skills requery drops server context**

When skills-like tool schema mode is enabled and Gemini returns server-side parts with a custom tool call, `_resolve_tool_exec` builds its parameter requery from `run_context.messages`, which does not yet contain the current response, then replaces or rewrites that response’s tool calls. The requery therefore omits the captured server-side parts, and the later tool-loop request loses them too.

Include the current response’s server-side parts in the parameter requery context and preserve them under the resulting tool-call ID.
</issue_to_address>

### Comment 6
<location path="tests/test_gemini_source.py" line_range="677" />
<code_context>
+    assert parts[0].tool_call.id == "search-1"
+    assert parts[0].thought_signature == b"\x01\x02"
+    assert parts[1].tool_response is not None
+    assert parts[2].function_call is not None
+    assert parts[2].function_call.name == "generate_image"
+    assert parts[2].thought_signature == b"\x03"
</code_context>
<issue_to_address>
**Lost tool payloads go unnoticed**

When replayed server-side parts retain their wrapper but lose payload fields, when replay serialization drops a server-side part’s fields but leaves its wrapper present, `test_gemini_prepare_conversation_replays_server_side_tool_parts` passes because it checks only that `tool_response` is non-`None`; the streaming test checks only key presence and order, so lost IDs or response data go unnoticed.

Assert that replayed and streamed server-side parts preserve their IDs, tool types, arguments, responses, and thought signatures.

Also at `tests/test_gemini_source.py:708`.
</issue_to_address>

### Comment 7
<location path="tests/test_gemini_source.py" line_range="20" />
<code_context>
+def _gemini_config_provider(
+    model: str, provider_config: dict | None = None
+) -> ProviderGoogleGenAI:
+    """Build a provider stub that can build request configs without network I/O."""
+    provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI)
+    provider.provider_config = provider_config or {}
</code_context>
<issue_to_address>
**New docstrings omit required sections**

When an added docstring documents a function with parameters or a return value, the added docstrings for helpers and parameterized tests describe functions with parameters or return values without Google-format `Args:` or `Returns:` sections, contrary to the repository's docstring rule.

Add the appropriate Google-format `Args:` and `Returns:` sections to the affected docstrings.

Also at `tests/test_gemini_source.py:31`, `tests/test_gemini_source.py:95`, `tests/test_gemini_source.py:128`, `tests/test_gemini_source.py:197`, `tests/test_gemini_source.py:572`, `tests/test_gemini_source.py:754`.
</issue_to_address>

### Comment 8
<location path="tests/test_gemini_source.py" line_range="17-46" />
<code_context>
 from astrbot.core.provider.sources.gemini_source import ProviderGoogleGenAI


+def _gemini_config_provider(
+    model: str, provider_config: dict | None = None
+) -> ProviderGoogleGenAI:
+    """Build a provider stub that can build request configs without network I/O."""
+    provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI)
+    provider.provider_config = provider_config or {}
+    provider.provider_settings = {}
+    provider.model_name = model
+    provider.safety_settings = []
+    return provider
+
+
+def _gemini_func_toolset() -> SimpleNamespace:
</code_context>
<issue_to_address>
**Tests added against repository rule**

When the PR adds new test code to `tests/test_gemini_source.py`, the added test helpers and regression tests expand the test suite despite the repository instruction to avoid generating test files.

Remove the added test code to comply with the repository instruction.

Also at `tests/test_gemini_source.py:570-810`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 6 findings to address first, and the change stores Gemini server-side tool parts and signatures in tool-call conversation history so later requests can replay them. If that data is recorded incorrectly, follow-up requests for affected conversations may fail; reverting stops new entries but does not remove existing ones, which can be repaired by clearing those conversations.

Blocking findings: astrbot/core/provider/sources/gemini_source.py:507, astrbot/core/provider/sources/gemini_source.py:735, astrbot/core/provider/sources/gemini_source.py:501, astrbot/core/provider/sources/gemini_source.py:740, astrbot/core/provider/sources/gemini_source.py:743, and 1 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +506 to +507
if text:
parts.append(types.Part(text=text))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Gemini history loses part order

When a Gemini response interleaves narration, server-side parts, and custom function calls, including across stream chunks, _process_content_parts stores server-side parts separately, and _prepare_conversation replays them before function calls; _query_stream also returns at a function-call chunk without collecting later chunks. The next request therefore sends reordered or incomplete tool history, which Gemini can reject or mis-handle.

Preserve the original order of narration, server-side parts, and function calls, and collect all relevant parts before returning a streamed response.

Also at astrbot/core/provider/sources/gemini_source.py:527-538, astrbot/core/provider/sources/gemini_source.py:722-743, astrbot/core/provider/sources/gemini_source.py:931, astrbot/core/provider/sources/gemini_source.py:971.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at lines 506-507:

**Gemini history loses part order**

When a Gemini response interleaves narration, server-side parts, and custom function calls, including across stream chunks, `_process_content_parts` stores server-side parts separately, and `_prepare_conversation` replays them before function calls; `_query_stream` also returns at a function-call chunk without collecting later chunks. The next request therefore sends reordered or incomplete tool history, which Gemini can reject or mis-handle.

Preserve the original order of narration, server-side parts, and function calls, and collect all relevant parts before returning a streamed response.

Also at `astrbot/core/provider/sources/gemini_source.py:527-538`, `astrbot/core/provider/sources/gemini_source.py:722-743`, `astrbot/core/provider/sources/gemini_source.py:931`, `astrbot/core/provider/sources/gemini_source.py:971`.

or getattr(part, "executable_code", None)
or getattr(part, "code_execution_result", None)
]
if server_side_parts and llm_response.tools_call_ids:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Built-in tool history disappears

When gemini returns server-side tool parts without a custom function call and the conversation continues, _process_content_parts and the streaming path attach server-side parts only to custom tool calls, while the runner stores only assistant text when there is no custom call. The next Gemini request omits the built-in tool history and can fail validation or lose tool context.

Persist server-side parts in the response history even when Gemini returns no custom function call.

Also at astrbot/core/provider/sources/gemini_source.py:940-941, astrbot/core/provider/sources/gemini_source.py:971-978.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at line 735:

**Built-in tool history disappears**

When gemini returns server-side tool parts without a custom function call and the conversation continues, `_process_content_parts` and the streaming path attach server-side parts only to custom tool calls, while the runner stores only assistant text when there is no custom call. The next Gemini request omits the built-in tool history and can fail validation or lose tool context.

Persist server-side parts in the response history even when Gemini returns no custom function call.

Also at `astrbot/core/provider/sources/gemini_source.py:940-941`, `astrbot/core/provider/sources/gemini_source.py:971-978`.

Comment on lines +736 to +740
google_extra = llm_response.tools_call_extra_content.setdefault(
llm_response.tools_call_ids[0], {}
).setdefault("google", {})
google_extra["server_parts"] = [
*server_side_parts,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Requery loses server-side context

When the skills_like path re-queries and returns different tool-call IDs, ToolLoopAgentRunner._step replaces the tool-call IDs without transferring tools_call_extra_content, leaving the saved server-side parts under the original IDs. The next Gemini request omits that context.

Transfer the saved tools_call_extra_content to the re-query's tool-call IDs.

Also at astrbot/core/provider/sources/gemini_source.py:735.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at lines 736-740:

**Requery loses server-side context**

When the `skills_like` path re-queries and returns different tool-call IDs, `ToolLoopAgentRunner._step` replaces the tool-call IDs without transferring `tools_call_extra_content`, leaving the saved server-side parts under the original IDs. The next Gemini request omits that context.

Transfer the saved `tools_call_extra_content` to the re-query's tool-call IDs.

Also at `astrbot/core/provider/sources/gemini_source.py:735`.

Comment on lines +735 to 743
if server_side_parts and llm_response.tools_call_ids:
google_extra = llm_response.tools_call_extra_content.setdefault(
llm_response.tools_call_ids[0], {}
).setdefault("google", {})
google_extra["server_parts"] = [
*server_side_parts,
*(google_extra.get("server_parts") or []),
]
chain_result = MessageChain(chain=chain)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Skills requery drops server context

When skills-like tool schema mode is enabled and Gemini returns server-side parts with a custom tool call, _resolve_tool_exec builds its parameter requery from run_context.messages, which does not yet contain the current response, then replaces or rewrites that response’s tool calls. The requery therefore omits the captured server-side parts, and the later tool-loop request loses them too.

Include the current response’s server-side parts in the parameter requery context and preserve them under the resulting tool-call ID.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at lines 735-743:

**Skills requery drops server context**

When skills-like tool schema mode is enabled and Gemini returns server-side parts with a custom tool call, `_resolve_tool_exec` builds its parameter requery from `run_context.messages`, which does not yet contain the current response, then replaces or rewrites that response’s tool calls. The requery therefore omits the captured server-side parts, and the later tool-loop request loses them too.

Include the current response’s server-side parts in the parameter requery context and preserve them under the resulting tool-call ID.

Comment thread tests/test_gemini_source.py Resolved
Comment thread tests/test_gemini_source.py Resolved
Comment on lines +17 to 46
def _gemini_config_provider(
model: str, provider_config: dict | None = None
) -> ProviderGoogleGenAI:
"""Build a provider stub that can build request configs without network I/O."""
provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI)
provider.provider_config = provider_config or {}
provider.provider_settings = {}
provider.model_name = model
provider.safety_settings = []
return provider


def _gemini_func_toolset() -> SimpleNamespace:
"""Minimal ToolSet stand-in exposing one function declaration."""
return SimpleNamespace(
get_func_desc_google_genai_style=lambda: {
"function_declarations": [
{
"name": "generate_image",
"description": "Generate an image",
"parameters": {"type": "object", "properties": {}},
}
]
}
)


@pytest.mark.asyncio
async def test_gemini_thinking_level_is_serialized_on_every_request():
model = "gemini-3.7-flash"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Low · Tests added against repository rule

When the PR adds new test code to tests/test_gemini_source.py, the added test helpers and regression tests expand the test suite despite the repository instruction to avoid generating test files.

Remove the added test code to comply with the repository instruction.

Also at tests/test_gemini_source.py:570-810.

Prompt for AI agents
In `tests/test_gemini_source.py` at lines 17-210:

**Tests added against repository rule**

When the PR adds new test code to `tests/test_gemini_source.py`, the added test helpers and regression tests expand the test suite despite the repository instruction to avoid generating test files.

Remove the added test code to comply with the repository instruction.

Also at `tests/test_gemini_source.py:570-810`.

From .github/copilot-instructions.md L27:

Do not generate test files for now.

@codecov-commenter

codecov-commenter commented Nov 11, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@DOTBave

DOTBave commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review please

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="728" />
<code_context>
+            or getattr(part, "executable_code", None)
+            or getattr(part, "code_execution_result", None)
+        ]
+        if server_side_parts and llm_response.tools_call_ids:
+            google_extra = llm_response.tools_call_extra_content.setdefault(
+                llm_response.tools_call_ids[0], {}
</code_context>
<issue_to_address>
**Built-in tool history disappears**

When gemini uses a built-in tool but returns no custom function call, and the conversation continues, `_process_content_parts` does not save `server_side_parts` without a tool-call ID, and the streaming path likewise leaves `pending_server_side_parts` unattached. The agent records only the assistant text and reasoning, so a later request omits the built-in tool history and Gemini rejects or mishandles the conversation.

Persist and replay server-side tool parts even when Gemini returns no custom function call.

Also at `astrbot/core/provider/sources/gemini_source.py:463`, `astrbot/core/provider/sources/gemini_source.py:934`, `astrbot/core/provider/sources/gemini_source.py:964-971`.
</issue_to_address>

### Comment 2
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="475-499" />
<code_context>
+                        # invocations are enabled, the API requires them to be
+                        # sent back unchanged, including their thought signatures.
+                        for server_side_part in google_extra.get("server_parts") or []:
+                            try:
+                                tool_parts.append(
+                                    types.Part.model_validate(server_side_part)
</code_context>
<issue_to_address>
**Tool history order changes**

When gemini returns server-side parts before or interleaved with narration or custom function calls, `_process_content_parts` separates server-side parts from their original positions, and `_prepare_conversation` replays them before the associated function calls. Gemini receives tool history in a different order and can reject or incorrectly process the follow-up request.

Preserve the original positions of server-side parts and replay them in that order.
</issue_to_address>

### Comment 3
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="925" />
<code_context>
+                chunk_parts = chunk.candidates[0].content.parts or []
+
+                if chunk_parts and any(part.function_call for part in chunk_parts):
                     llm_response = LLMResponse("assistant", is_chunk=False)
                     llm_response.raw_completion = chunk
                     llm_response.result_chain = self._process_content_parts(
</code_context>
<issue_to_address>
**Later tool context is dropped**

When gemini emits a server-side tool part in a stream chunk after the first custom function-call chunk, `_query_stream` returns as soon as it sees a custom function call, so server-side tool parts arriving in subsequent chunks are never added to `pending_server_side_parts` or attached to the tool call. `_prepare_conversation` then replays incomplete history, and the next combined-tool request can fail.

Continue consuming the stream and accumulate server-side parts before yielding the completed tool-call response.
</issue_to_address>

### Comment 4
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="728" />
<code_context>
+            or getattr(part, "executable_code", None)
+            or getattr(part, "code_execution_result", None)
+        ]
+        if server_side_parts and llm_response.tools_call_ids:
+            google_extra = llm_response.tools_call_extra_content.setdefault(
+                llm_response.tools_call_ids[0], {}
</code_context>
<issue_to_address>
**Re-query loses server-side history**

When the agent uses `skills_like` mode and the initial Gemini response includes server-side parts alongside a custom function call, `ToolLoopAgentRunner._resolve_tool_exec` rebuilds the re-query context from pre-call messages and replaces the original response, so the re-query response and later tool-loop history omit the original `server_parts`. Gemini receives a follow-up request without the required tool history and rejects or misprocesses it.

Carry the original response's server-side parts into the `skills_like` re-query context and its replacement response.

Also at `astrbot/core/provider/sources/gemini_source.py:729-735`.
</issue_to_address>

### Comment 5
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="732-734" />
<code_context>
+            google_extra = llm_response.tools_call_extra_content.setdefault(
+                llm_response.tools_call_ids[0], {}
+            ).setdefault("google", {})
+            google_extra["server_parts"] = [
+                *server_side_parts,
+                *(google_extra.get("server_parts") or []),
+            ]
         chain_result = MessageChain(chain=chain)
</code_context>
<issue_to_address>
**Server context disappears after re-query**

When a Gemini request with combined built-in and function tools runs with `tool_schema_mode="skills_like"` and takes the re-query branch, `ToolLoopAgentRunner._step` replaces the tool-call IDs after a `skills_like` re-query without transferring `tools_call_extra_content`. `to_openai_tool_calls_model` then looks up metadata using the new IDs, so `_prepare_conversation` omits the stored server-side parts and the next Gemini request loses required tool context.

Preserve or merge the original and re-query `tools_call_extra_content` under the IDs that will be serialized into history.
</issue_to_address>

Sourcery assessment

Approval pending. 5 findings to address first.

Blocking findings: astrbot/core/provider/sources/gemini_source.py:728, astrbot/core/provider/sources/gemini_source.py:499, astrbot/core/provider/sources/gemini_source.py:925, astrbot/core/provider/sources/gemini_source.py:728, astrbot/core/provider/sources/gemini_source.py:734


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

or getattr(part, "executable_code", None)
or getattr(part, "code_execution_result", None)
]
if server_side_parts and llm_response.tools_call_ids:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Built-in tool history disappears

When gemini uses a built-in tool but returns no custom function call, and the conversation continues, _process_content_parts does not save server_side_parts without a tool-call ID, and the streaming path likewise leaves pending_server_side_parts unattached. The agent records only the assistant text and reasoning, so a later request omits the built-in tool history and Gemini rejects or mishandles the conversation.

Persist and replay server-side tool parts even when Gemini returns no custom function call.

Also at astrbot/core/provider/sources/gemini_source.py:463, astrbot/core/provider/sources/gemini_source.py:934, astrbot/core/provider/sources/gemini_source.py:964-971.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at line 728:

**Built-in tool history disappears**

When gemini uses a built-in tool but returns no custom function call, and the conversation continues, `_process_content_parts` does not save `server_side_parts` without a tool-call ID, and the streaming path likewise leaves `pending_server_side_parts` unattached. The agent records only the assistant text and reasoning, so a later request omits the built-in tool history and Gemini rejects or mishandles the conversation.

Persist and replay server-side tool parts even when Gemini returns no custom function call.

Also at `astrbot/core/provider/sources/gemini_source.py:463`, `astrbot/core/provider/sources/gemini_source.py:934`, `astrbot/core/provider/sources/gemini_source.py:964-971`.

Comment on lines +475 to 499
try:
tool_parts.append(
types.Part.model_validate(server_side_part)
)
except Exception as e:
logger.warning(
"Failed to restore Gemini server-side tool part: %s",
e,
exc_info=True,
)
part = types.Part.from_function_call(
name=function_name,
args=json.loads(tool["function"]["arguments"]),
)
# we should set thought_signature back to part if exists
# for more info about thought_signature, see:
# https://ai.google.dev/gemini-api/docs/thought-signatures
ts_bs64 = google_extra.get("thought_signature")
if ts_bs64:
part.thought_signature = base64.b64decode(ts_bs64)
tool_parts.append(part)

if isinstance(content, str):
parts.append(types.Part.from_text(text=content))
elif isinstance(content, list):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Tool history order changes

When gemini returns server-side parts before or interleaved with narration or custom function calls, _process_content_parts separates server-side parts from their original positions, and _prepare_conversation replays them before the associated function calls. Gemini receives tool history in a different order and can reject or incorrectly process the follow-up request.

Preserve the original positions of server-side parts and replay them in that order.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at lines 475-541:

**Tool history order changes**

When gemini returns server-side parts before or interleaved with narration or custom function calls, `_process_content_parts` separates server-side parts from their original positions, and `_prepare_conversation` replays them before the associated function calls. Gemini receives tool history in a different order and can reject or incorrectly process the follow-up request.

Preserve the original positions of server-side parts and replay them in that order.

chunk_parts = chunk.candidates[0].content.parts or []

if chunk_parts and any(part.function_call for part in chunk_parts):
llm_response = LLMResponse("assistant", is_chunk=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Later tool context is dropped

When gemini emits a server-side tool part in a stream chunk after the first custom function-call chunk, _query_stream returns as soon as it sees a custom function call, so server-side tool parts arriving in subsequent chunks are never added to pending_server_side_parts or attached to the tool call. _prepare_conversation then replays incomplete history, and the next combined-tool request can fail.

Continue consuming the stream and accumulate server-side parts before yielding the completed tool-call response.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at line 925:

**Later tool context is dropped**

When gemini emits a server-side tool part in a stream chunk after the first custom function-call chunk, `_query_stream` returns as soon as it sees a custom function call, so server-side tool parts arriving in subsequent chunks are never added to `pending_server_side_parts` or attached to the tool call. `_prepare_conversation` then replays incomplete history, and the next combined-tool request can fail.

Continue consuming the stream and accumulate server-side parts before yielding the completed tool-call response.

or getattr(part, "executable_code", None)
or getattr(part, "code_execution_result", None)
]
if server_side_parts and llm_response.tools_call_ids:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Re-query loses server-side history

When the agent uses skills_like mode and the initial Gemini response includes server-side parts alongside a custom function call, ToolLoopAgentRunner._resolve_tool_exec rebuilds the re-query context from pre-call messages and replaces the original response, so the re-query response and later tool-loop history omit the original server_parts. Gemini receives a follow-up request without the required tool history and rejects or misprocesses it.

Carry the original response's server-side parts into the skills_like re-query context and its replacement response.

Also at astrbot/core/provider/sources/gemini_source.py:729-735.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at line 728:

**Re-query loses server-side history**

When the agent uses `skills_like` mode and the initial Gemini response includes server-side parts alongside a custom function call, `ToolLoopAgentRunner._resolve_tool_exec` rebuilds the re-query context from pre-call messages and replaces the original response, so the re-query response and later tool-loop history omit the original `server_parts`. Gemini receives a follow-up request without the required tool history and rejects or misprocesses it.

Carry the original response's server-side parts into the `skills_like` re-query context and its replacement response.

Also at `astrbot/core/provider/sources/gemini_source.py:729-735`.

Comment on lines +732 to +734
google_extra["server_parts"] = [
*server_side_parts,
*(google_extra.get("server_parts") or []),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Server context disappears after re-query

When a Gemini request with combined built-in and function tools runs with tool_schema_mode="skills_like" and takes the re-query branch, ToolLoopAgentRunner._step replaces the tool-call IDs after a skills_like re-query without transferring tools_call_extra_content. to_openai_tool_calls_model then looks up metadata using the new IDs, so _prepare_conversation omits the stored server-side parts and the next Gemini request loses required tool context.

Preserve or merge the original and re-query tools_call_extra_content under the IDs that will be serialized into history.

Prompt for AI agents
In `astrbot/core/provider/sources/gemini_source.py` at lines 732-734:

**Server context disappears after re-query**

When a Gemini request with combined built-in and function tools runs with `tool_schema_mode="skills_like"` and takes the re-query branch, `ToolLoopAgentRunner._step` replaces the tool-call IDs after a `skills_like` re-query without transferring `tools_call_extra_content`. `to_openai_tool_calls_model` then looks up metadata using the new IDs, so `_prepare_conversation` omits the stored server-side parts and the next Gemini request loses required tool context.

Preserve or merge the original and re-query `tools_call_extra_content` under the IDs that will be serialized into history.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] gemini API格式工具调用必定报错

2 participants