问题描述
gemini_source.py 在把会话历史重建为 Gemini contents 时,对 role == "tool" 的消息使用 message.get("name", message["tool_call_id"]) 来生成 functionResponse.name。当历史中保存的 tool_call_id 是不透明 ID(如 call_xxx)而不是函数名时,重建出的请求会出现:
functionCall.name = "steal_meme_to_smile"(取自 assistant 消息的 tool_calls[].function.name)
functionResponse.name = "call_1073557"(直接沿用了 tool_call_id)
两者不配对。任何校验 functionResponse.name 必须与前一条 functionCall.name 一致的 Gemini 兼容上游(Google 官方 API 及常见兼容网关)都会返回 400:
{"error":{"message":"... functionResponse.name \"call_1073557\" does not match functionCall.name \"steal_meme_to_smile\" ...","type":"invalid_request_error"}}
且由于这条记录会一直留在会话历史里,该会话后续所有请求都会持续失败。
复现条件
满足以下任一情况后,第二轮工具调用往返即触发:
-
会话历史先经 OpenAI 兼容 provider/网关产生(tool_call_id 为 call_xxx 等 opaque ID),之后切换到 Gemini provider 继续使用同一会话;
-
Gemini 兼容网关在响应的 function_call 中携带显式 id。此时 gemini_source.py 中
tool_call_id = part.function_call.id or part.function_call.name
会把该 id 存入会话历史;而重建请求时 Part.from_function_call(name=..., args=...) 并不会把 id 写回 functionCall,tool 分支又把 id 当作函数名写进 functionResponse,两侧必然不配对。
我们在线上(AstrBot + 兼容网关反代 antigravity 后端)实际遇到的即为此场景:首次工具调用成功,紧接着的下一轮请求开始全部 400。
根因
astrbot/core/provider/sources/gemini_source.py(main @ fe3d775,2026-08-29)中 tool 分支:
elif role == "tool":
func_name = message.get("name", message["tool_call_id"])
历史中 role:"tool" 消息通常只有 tool_call_id 而没有 name 字段,因此 opaque ID 被直接当成函数名使用。
修复建议
重建 contents 前先建立 tool_call_id -> function name 映射,tool 分支优先回查真实函数名:
gemini_contents: list[types.Content] = []
tool_call_names: dict[str, str] = {}
for msg in payloads["messages"]:
for tool in msg.get("tool_calls") or []:
if (
isinstance(tool, dict)
and tool.get("id")
and isinstance(tool.get("function"), dict)
and tool["function"].get("name")
):
tool_call_names[tool["id"]] = tool["function"]["name"]
for message in payloads["messages"]:
...
elif role == "tool":
func_name = (
message.get("name")
or tool_call_names.get(message["tool_call_id"])
or message["tool_call_id"]
)
该修复对存量历史同样生效(函数名可从同历史中的 assistant tool_calls 里找回)。可选的增强:重建 functionCall 时把保存的 id 一并写回(types.FunctionCall(id=...)),以兼容按 id 配对的上游。
环境信息
- AstrBot 版本:main 分支 fe3d775(2026-08-29)
- Provider:Gemini(google-genai SDK,兼容网关反代 antigravity 后端)
- 触发模型:gemini-3.6-flash-high / gemini-3.7-flash-high
- 已本地验证上述补丁可消除 400
问题描述
gemini_source.py在把会话历史重建为 Geminicontents时,对role == "tool"的消息使用message.get("name", message["tool_call_id"])来生成functionResponse.name。当历史中保存的tool_call_id是不透明 ID(如call_xxx)而不是函数名时,重建出的请求会出现:functionCall.name = "steal_meme_to_smile"(取自 assistant 消息的tool_calls[].function.name)functionResponse.name = "call_1073557"(直接沿用了tool_call_id)两者不配对。任何校验
functionResponse.name必须与前一条functionCall.name一致的 Gemini 兼容上游(Google 官方 API 及常见兼容网关)都会返回 400:{"error":{"message":"... functionResponse.name \"call_1073557\" does not match functionCall.name \"steal_meme_to_smile\" ...","type":"invalid_request_error"}}且由于这条记录会一直留在会话历史里,该会话后续所有请求都会持续失败。
复现条件
满足以下任一情况后,第二轮工具调用往返即触发:
会话历史先经 OpenAI 兼容 provider/网关产生(
tool_call_id为call_xxx等 opaque ID),之后切换到 Gemini provider 继续使用同一会话;Gemini 兼容网关在响应的
function_call中携带显式id。此时gemini_source.py中会把该 id 存入会话历史;而重建请求时
Part.from_function_call(name=..., args=...)并不会把 id 写回 functionCall,tool 分支又把 id 当作函数名写进 functionResponse,两侧必然不配对。我们在线上(AstrBot + 兼容网关反代 antigravity 后端)实际遇到的即为此场景:首次工具调用成功,紧接着的下一轮请求开始全部 400。
根因
astrbot/core/provider/sources/gemini_source.py(main @ fe3d775,2026-08-29)中 tool 分支:历史中
role:"tool"消息通常只有tool_call_id而没有name字段,因此 opaque ID 被直接当成函数名使用。修复建议
重建 contents 前先建立
tool_call_id -> function name映射,tool 分支优先回查真实函数名:该修复对存量历史同样生效(函数名可从同历史中的 assistant
tool_calls里找回)。可选的增强:重建 functionCall 时把保存的id一并写回(types.FunctionCall(id=...)),以兼容按 id 配对的上游。环境信息