Skip to content

feat: 让Gemini原生工具和函数工具tools兼容#8418

Open
ZhaiXB wants to merge 11 commits into
AstrBotDevs:masterfrom
ZhaiXB:feat/gemini-native-tools-compatibility
Open

feat: 让Gemini原生工具和函数工具tools兼容#8418
ZhaiXB wants to merge 11 commits into
AstrBotDevs:masterfrom
ZhaiXB:feat/gemini-native-tools-compatibility

Conversation

@ZhaiXB
Copy link
Copy Markdown
Contributor

@ZhaiXB ZhaiXB commented May 29, 2026

#8417 支持多工具混合编排与旧模型兼容,修复工具历史解析死循环


变更背景 (Description)

在当前的 Gemini 提供商适配器中,存在以下几个亟待解决的痛点:

  1. 多工具混合编排支持与老模型兼容冲突:Gemini 3.0+ 引入了对内置工具(如 Google Search)与自定义插件函数并存的多工具混合编排能力,且必须设置 include_server_side_tool_invocations = True。然而,如果在 Gemini 1.x 或 2.x(包括 gemini-2.5-flash 等)等旧模型上启用该参数,API 会直接抛出 400 INVALID_ARGUMENT 错误。
  2. Assistant 历史消息解析 Bug 导致死循环:在 _prepare_conversation 中,解析 assistant 历史消息时采用了 if-elif 链。如果 content 是字符串(例如即使是空字符串 ""),解析器会直接跳过 tool_calls 的处理,导致工具链历史丢失,模型产生无限工具调用死循环
  3. SDK 限制与 ID 绑定报错google-genai Python SDK 中的助手方法 Part.from_function_response 不支持 id 参数(传参会引发 TypeError),而如果不绑定正确的 tool_call_id 会导致历史关联断裂。此外,本地引擎产生的伪 ID(如与函数名相同的占位符)直接传回 API 也会触发 400 错误。

本 PR 对上述问题进行了集中重构,在完美支持 Gemini 3+ 多工具流通的同时,向下兼容所有 legacy 模型,并彻底消除了工具链调用的死循环 Bug。


Modifications / 改动点

1. 引入版本感知的能力检测器 (Capability Detection)

  • 增加了 _supports_multi_tool(self, model_name: str) -> bool 方法。
  • 通过匹配 "gemini-1""gemini-2",将所有 legacy 模型(包括 2.5 系列)安全识别为旧版,从而仅对 Gemini 3.0 及未来更新的模型启用多工具混合编排及 include_server_side_tool_invocations 关联参数。旧模型则优雅降级为原有的互斥逻辑,杜绝 400 报错。

2. 解耦并重构 Assistant 历史解析逻辑

  • 重构 _prepare_conversation 中对 assistant 角色消息的处理逻辑。
  • content 文本/思维链字段的提取与 tool_calls 的解析完全解耦,确保即使存在文本,历史中的工具调用也绝不会被丢弃,彻底根治了工具调用的死循环问题。

3. 精确对齐 Tool ID 并引入健壮性过滤

  • 绕过 SDK 限制:不再使用 Part.from_function_response,而是通过直接实例化 Pydantic 模型types.Part(function_response=types.FunctionResponse(...))types.FunctionCall(...))的方法,安全地传递和还原 id 属性。

  • ID 过滤:增加了 tool_id != func_name 校验,自动过滤掉本地引擎生成的临时占位 ID,仅向 Gemini 服务器传递真实产生的 server-side ID,确保 API 调用百分百合规。

  • This is NOT a breaking change. / 这不是一个破坏性变更。

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

Gemini3系列同时调用grounding google search和astrbot内置的tools未出现问题,.
1780256739292_d

Gemini2系列如果开启原生工具, 也可以正常屏蔽掉tools.
image


Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

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

  • 🤓 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.txtpyproject.toml 文件相应位置。

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

Summary by Sourcery

Add version-aware Gemini tool orchestration to support multi-tool workflows on Gemini 3+ while remaining compatible with legacy models and fixing tool history handling.

New Features:

  • Enable mixed use of Gemini native tools and custom function tools on models that support multi-tool orchestration.
  • Automatically detect Gemini model capabilities to toggle server-side tool invocation behavior per model.

Bug Fixes:

  • Fix loss of assistant tool_call history that could cause infinite tool invocation loops.
  • Avoid invalid or misleading tool_call IDs being sent to the Gemini API that could trigger request errors.

Enhancements:

  • Refine assistant message conversion to always preserve both text and tool call parts when building Gemini contents.
  • Bypass SDK limitations by constructing function call/response parts directly, improving robustness of tool invocation history handling.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels May 29, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • When inserting Comp.Plain(accumulated_text) at result_chain.chain.insert(0, ...), consider whether this can disturb the original chronological order of components (e.g., pushing earlier assistant/system content after the preface); it may be safer to attach this text as a dedicated assistant turn or at a more precise position tied to the current response.
  • In the role == "tool" branch, func_name = message.get("name") or message.get("tool_call_id") can now be None if both keys are missing, which may cause types.Part.from_function_response to behave unexpectedly; you may want to enforce one of these fields or raise a clear error when neither is present.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When inserting `Comp.Plain(accumulated_text)` at `result_chain.chain.insert(0, ...)`, consider whether this can disturb the original chronological order of components (e.g., pushing earlier assistant/system content after the preface); it may be safer to attach this text as a dedicated assistant turn or at a more precise position tied to the current response.
- In the `role == "tool"` branch, `func_name = message.get("name") or message.get("tool_call_id")` can now be `None` if both keys are missing, which may cause `types.Part.from_function_response` to behave unexpectedly; you may want to enforce one of these fields or raise a clear error when neither is present.

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="231-234" />
<code_context>
                     )

+        # 将自定义工具追加进 tool_list
+        if tools and (func_desc := tools.get_func_desc_google_genai_style()):
+            if tool_list is None:
+                tool_list = []
+            tool_list.append(
+                types.Tool(function_declarations=func_desc["function_declarations"])
+            )
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid assuming all `types.Tool` instances have `google_search` and `code_execution` attributes in `has_native_tool`.

Directly accessing `t.google_search` and `t.code_execution` can raise `AttributeError` for tools (including future SDK variants or mocks) that don’t define these attributes. To make this resilient, use `getattr` as you already do for `url_context`, e.g.:

```python
has_native_tool = tool_list and any(
    getattr(t, "google_search", None)
    or getattr(t, "code_execution", None)
    or getattr(t, "url_context", None)
    for t in tool_list
)
```
</issue_to_address>

### Comment 2
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="422-432" />
<code_context>

-                elif not native_tool_enabled and "tool_calls" in message:
+                # 允许在开启搜索时还原工具历史
+                elif "tool_calls" in message:
                     parts = []
                     for tool in message["tool_calls"]:
                         part = types.Part.from_function_call(
                             name=tool["function"]["name"],
                             args=json.loads(tool["function"]["arguments"]),
                         )
+                        # 还原 Assistant 历史消息里工具调用的唯一 ID
+                        if "id" in tool and part.function_call:
+                            part.function_call.id = tool["id"]
+
</code_context>
<issue_to_address>
**suggestion:** Guard against non-JSON `tool["function"]["arguments"]` when reconstructing historical tool calls.

With this branch now running whenever `tool_calls` is present (including older or partially migrated logs), any non‑JSON `function["arguments"]` will cause `json.loads` to raise and break reconstruction. Consider a small guard (e.g., try/except with a fallback to using the raw arguments string) so malformed historical payloads don’t crash this path.

```suggestion
                # 允许在开启搜索时还原工具历史
                elif "tool_calls" in message:
                     parts = []
                     for tool in message["tool_calls"]:
+                        # 兼容历史或异常日志中的非 JSON arguments,避免重放工具历史时报错
+                        raw_args = tool.get("function", {}).get("arguments")
+                        parsed_args = None
+
+                        # 如果本身就是结构化对象则直接使用
+                        if isinstance(raw_args, (dict, list)):
+                            parsed_args = raw_args
+                        else:
+                            try:
+                                parsed_args = json.loads(raw_args) if raw_args is not None else None
+                            except (TypeError, json.JSONDecodeError):
+                                # 回退到原始字符串,保证历史记录可被重建而不中断流程
+                                parsed_args = raw_args
+
                         part = types.Part.from_function_call(
                             name=tool["function"]["name"],
-                            args=json.loads(tool["function"]["arguments"]),
+                            args=parsed_args,
                         )
+                        # 还原 Assistant 历史消息里工具调用的唯一 ID
+                        if "id" in tool and part.function_call:
+                            part.function_call.id = tool["id"]
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request enhances Gemini tool integration by allowing mixed usage of native and custom tools, restoring tool call and response IDs for strict Gemini validation, and preserving accumulated stream text when tool calls interrupt the stream. The review feedback highlights several compatibility improvements, such as using getattr and hasattr to prevent AttributeError on older versions of the google-genai SDK, and suggests also preserving accumulated_reasoning when stream interruption occurs to prevent losing the model's thinking process.

Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
ZhaiXB added 3 commits May 29, 2026 10:07
1. 属性防御性获取
原代码:t.google_search 和 t.code_execution

问题:如果用户本地的 google-genai SDK 版本较老,这两个属性可能根本不存在,直接抛出 AttributeError 导致插件死锁。

改法:全部换成 getattr(t, "xxx", None)。

2. SDK 版本向下兼容
原代码:part.function_call.id = tool["id"]

问题:id 字段是谷歌新版 SDK 强校验模式才引入的。如果用户没升级 SDK,强行给对象赋 .id 属性会直接崩溃。

改法:赋值前加上 hasattr(obj, "id") 的判断。

3. 思考(Reasoning)内容的留存
核心痛点:这个建议太关键了!Gemini 3 在决定调用本地工具前,往往会先进行一长串的“思考(Reasoning)”。如果只保留了 accumulated_text 而把 accumulated_reasoning 给丢了,AI 的思维链在历史记录里就断了。

改法:在流中断返回前,把 accumulated_reasoning 也同步塞进 llm_response.reasoning_content。

4. 历史记录反序列化安全
原代码:json.loads(tool["function"]["arguments"])

问题:AstrBot 的历史数据库里可能残留了 OpenAI 格式的日志,或者某些插件传了已经解析好的 dict。直接 json.loads 如果报错会毁掉整条多轮对话。

改法:加上 try-except 和 isinstance 类型检查。
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels May 29, 2026
@ZhaiXB ZhaiXB closed this May 29, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 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="230-234" />
<code_context>
                         "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
                     )

+        # 将自定义工具追加进 tool_list
+        if tools and (func_desc := tools.get_func_desc_google_genai_style()):
+            if tool_list is None:
+                tool_list = []
+            tool_list.append(
+                types.Tool(function_declarations=func_desc["function_declarations"])
+            )
</code_context>
<issue_to_address>
**issue (bug_risk):** Custom tools are appended twice to tool_list in the same function path.

An earlier block now always appends custom tools to `tool_list` when `tools` is set, and step 3 repeats the same condition and appends them again except for pre‑Gemini‑3 models with native tools. For Gemini‑3+ or when there are no native tools, this causes duplicate function declarations in `tool_list`. To keep a single, centralized append, remove this earlier block and rely on the later guarded one.
</issue_to_address>

### Comment 2
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="471-480" />
<code_context>
                     parts = []
                     for tool in message["tool_calls"]:
+                        # 兼容历史或异常日志中的非 JSON arguments,避免重放工具历史时报错
+                        raw_args = tool.get("function", {}).get("arguments")
+                        parsed_args = None
+                        if isinstance(raw_args, (dict, list)):
+                            parsed_args = raw_args
+                        else:
+                            try:
+                                parsed_args = (
+                                    json.loads(raw_args)
+                                    if raw_args is not None
+                                    else None
+                                )
+                            except (TypeError, json.JSONDecodeError):
+                                parsed_args = raw_args
+
</code_context>
<issue_to_address>
**suggestion:** Silently accepting non-JSON or undecodable arguments may propagate bad data into function calls.

If `raw_args` is malformed JSON, it ends up being passed through as a raw string in `args`. Depending on how `types.Part.from_function_call` handles this, it may cause confusing downstream behavior. Consider either logging a clear warning on JSON decode failure (including tool name/id) or normalizing the value (e.g., defaulting to `{}`) so callers don’t need to handle unexpected types.

Suggested implementation:

```python
                ) and "tool_calls" in message:
                    parts = []
                    for tool in message["tool_calls"]:
                        # 兼容历史或异常日志中的非 JSON arguments,避免重放工具历史时报错
                        raw_args = tool.get("function", {}).get("arguments")
                        parsed_args = None
                        if isinstance(raw_args, (dict, list)):
                            parsed_args = raw_args
                        else:
                            try:
                                parsed_args = (
                                    json.loads(raw_args)
                                    if raw_args is not None
                                    else None
                                )
                            except (TypeError, json.JSONDecodeError):
                                # 当 arguments 不是合法 JSON 时,记录告警并回退为 {}
                                tool_name = tool.get("function", {}).get("name")
                                tool_id = tool.get("id")
                                logger.warning(
                                    "Gemini tool_call arguments JSON decode failed, "
                                    "tool_name=%r, tool_id=%r, raw_args=%r; "
                                    "falling back to empty dict.",
                                    tool_name,
                                    tool_id,
                                    raw_args,
                                )
                                parsed_args = {}

                        # TODO: 使用 parsed_args 构造对应的 Part / function_call,
                        # 例如:
                        # parts.append(
                        #     types.Part.from_function_call(
                        #         name=tool.get("function", {}).get("name"),
                        #         args=parsed_args or {},
                        #     )
                        # )

        # 将自定义工具追加进 tool_list

```

1. Ensure `json` and a module-level `logger` (e.g. from `logging.getLogger(__name__)` or the project's logging utility) are already imported/defined in `gemini_source.py`. If not, add:
   - `import json`
   - `import logging` and `logger = logging.getLogger(__name__)`, or adapt to your existing logging helper.
2. Replace the commented `TODO` block with the actual logic currently used in this file to build `types.Part` / function calls from `tool` data, wiring `parsed_args` into that call (likely replacing the previous usage of `raw_args`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

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="230-234" />
<code_context>
                         "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
                     )

+        # 将自定义工具追加进 tool_list
+        if tools and (func_desc := tools.get_func_desc_google_genai_style()):
+            if tool_list is None:
+                tool_list = []
+            tool_list.append(
+                types.Tool(function_declarations=func_desc["function_declarations"])
+            )
</code_context>
<issue_to_address>
**issue (bug_risk):** Gemini 3+ 自定义工具逻辑在前后两个代码块中重复执行,可能导致函数工具被追加两次。

当前在 `_prepare_query_config` 中,你先在这里根据 `tools.get_func_desc_google_genai_style()` 追加了一次自定义工具,后面“3. 追加自定义工具的逻辑(全文件仅保留这一处)”中在相同条件下又追加了一次。如果这两处都被执行,会产生重复的 `types.Tool`,导致函数重复暴露或工具选择异常。建议删除这里的追加逻辑,仅保留后面带 `is_gemini_3_or_later`/`has_native_before` 判定的那段,确保函数工具只在一个路径下追加。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/provider/sources/gemini_source.py Outdated
@ZhaiXB ZhaiXB reopened this May 29, 2026
ZhaiXB added 4 commits May 29, 2026 12:35
当 arguments 不是合法 JSON 时,记录详细告警并安全回退为空字典 {},防止 downstream 校验崩溃
@ZhaiXB
Copy link
Copy Markdown
Contributor Author

ZhaiXB commented May 31, 2026

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The _supports_multi_tool check relies on 'gemini-1' / 'gemini-2' substrings, which may miss legacy models like models/gemini-pro; consider normalizing the model name and matching against all known 1.x/2.x identifiers (or using a configurable allowlist) to avoid accidentally enabling multi-tool on unsupported models.
  • When filtering tool_id / tool_call_id by comparing them to func_name, you assume all locally generated IDs equal the function name; if the backend ever legitimately uses the function name as an ID this will strip real IDs, so it may be safer to tag local IDs at creation time (e.g., a prefix) and filter on that marker instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_supports_multi_tool` check relies on `'gemini-1'` / `'gemini-2'` substrings, which may miss legacy models like `models/gemini-pro`; consider normalizing the model name and matching against all known 1.x/2.x identifiers (or using a configurable allowlist) to avoid accidentally enabling multi-tool on unsupported models.
- When filtering `tool_id` / `tool_call_id` by comparing them to `func_name`, you assume all locally generated IDs equal the function name; if the backend ever legitimately uses the function name as an ID this will strip real IDs, so it may be safer to tag local IDs at creation time (e.g., a prefix) and filter on that marker instead.

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="238" />
<code_context>
                         "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
                     )

+        supports_multi_tool = self._supports_multi_tool(model_name)
+
+        if tools and (func_desc := tools.get_func_desc_google_genai_style()):
</code_context>
<issue_to_address>
**issue (complexity):** Consider flattening the multi-tool handling logic, making tool_config construction more explicit, and simplifying native_tool_enabled computation to reduce branching and mental overhead.

You can keep the new behavior but simplify some of the branching to reduce mental overhead.

### 1. Flatten multi-tool + plugin tool list construction

You can separate capability checks from list construction to avoid nested `if`/`else` and repeated `tool_list` fiddling:

```python
supports_multi_tool = self._supports_multi_tool(model_name)
func_desc = tools.get_func_desc_google_genai_style() if tools else None

has_native_tools = bool(tool_list)
has_plugin_tools = bool(func_desc)

if has_plugin_tools:
    if has_native_tools and not supports_multi_tool:
        logger.warning(
            f"模型 {model_name} 不支持多工具混合编排。已启用原生工具,函数工具(本地插件)将被忽略。"
        )
    else:
        if tool_list is None:
            tool_list = []
        tool_list.append(
            types.Tool(function_declarations=func_desc["function_declarations"])
        )
```

This keeps all behavior but flattens the conditions and makes the combinations “native vs plugin vs capability” easier to read.

### 2. Make `tool_config` construction explicit

The `kwargs_tool_config` dict is only used to add one optional field. You can keep the same behavior with clearer, positional construction:

```python
has_func_decl = tool_list and any(t.function_declarations for t in tool_list)
tool_config = None

if has_func_decl:
    has_builtin_tools = any(
        getattr(t, "google_search", None)
        or getattr(t, "code_execution", None)
        or getattr(t, "url_context", None)
        for t in tool_list
    )

    fc_config = types.FunctionCallingConfig(
        mode=(
            types.FunctionCallingConfigMode.ANY
            if tool_choice == "required"
            else types.FunctionCallingConfigMode.AUTO
        )
    )

    if supports_multi_tool and has_builtin_tools:
        tool_config = types.ToolConfig(
            function_calling_config=fc_config,
            include_server_side_tool_invocations=True,
        )
    else:
        tool_config = types.ToolConfig(function_calling_config=fc_config)
```

This removes the indirection of `kwargs_tool_config` while keeping all the new flags.

### 3. Simplify `native_tool_enabled` derivation

The relationship between `supports_multi_tool` and `native_tool_enabled` can be expressed as a single expression:

```python
model_name = cast(str, payloads.get("model", self.get_model()))
supports_multi_tool = self._supports_multi_tool(model_name)

native_tool_enabled = (
    not supports_multi_tool
    and (
        self.provider_config.get("gm_native_coderunner", False)
        or self.provider_config.get("gm_native_search", False)
    )
)
```

This avoids the temporary `False` assignment and the subsequent `if` block, while preserving behavior and making later `if native_tool_enabled` checks easier to reason about.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

"当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
)

supports_multi_tool = self._supports_multi_tool(model_name)
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.

issue (complexity): Consider flattening the multi-tool handling logic, making tool_config construction more explicit, and simplifying native_tool_enabled computation to reduce branching and mental overhead.

You can keep the new behavior but simplify some of the branching to reduce mental overhead.

1. Flatten multi-tool + plugin tool list construction

You can separate capability checks from list construction to avoid nested if/else and repeated tool_list fiddling:

supports_multi_tool = self._supports_multi_tool(model_name)
func_desc = tools.get_func_desc_google_genai_style() if tools else None

has_native_tools = bool(tool_list)
has_plugin_tools = bool(func_desc)

if has_plugin_tools:
    if has_native_tools and not supports_multi_tool:
        logger.warning(
            f"模型 {model_name} 不支持多工具混合编排。已启用原生工具,函数工具(本地插件)将被忽略。"
        )
    else:
        if tool_list is None:
            tool_list = []
        tool_list.append(
            types.Tool(function_declarations=func_desc["function_declarations"])
        )

This keeps all behavior but flattens the conditions and makes the combinations “native vs plugin vs capability” easier to read.

2. Make tool_config construction explicit

The kwargs_tool_config dict is only used to add one optional field. You can keep the same behavior with clearer, positional construction:

has_func_decl = tool_list and any(t.function_declarations for t in tool_list)
tool_config = None

if has_func_decl:
    has_builtin_tools = any(
        getattr(t, "google_search", None)
        or getattr(t, "code_execution", None)
        or getattr(t, "url_context", None)
        for t in tool_list
    )

    fc_config = types.FunctionCallingConfig(
        mode=(
            types.FunctionCallingConfigMode.ANY
            if tool_choice == "required"
            else types.FunctionCallingConfigMode.AUTO
        )
    )

    if supports_multi_tool and has_builtin_tools:
        tool_config = types.ToolConfig(
            function_calling_config=fc_config,
            include_server_side_tool_invocations=True,
        )
    else:
        tool_config = types.ToolConfig(function_calling_config=fc_config)

This removes the indirection of kwargs_tool_config while keeping all the new flags.

3. Simplify native_tool_enabled derivation

The relationship between supports_multi_tool and native_tool_enabled can be expressed as a single expression:

model_name = cast(str, payloads.get("model", self.get_model()))
supports_multi_tool = self._supports_multi_tool(model_name)

native_tool_enabled = (
    not supports_multi_tool
    and (
        self.provider_config.get("gm_native_coderunner", False)
        or self.provider_config.get("gm_native_search", False)
    )
)

This avoids the temporary False assignment and the subsequent if block, while preserving behavior and making later if native_tool_enabled checks easier to reason about.

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

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant