fix: Antigravity 跨格式转换链 tool call ID 完整传递

修复通过 Antigravity 适配器发送请求到 Claude 模型时 tool_use 块缺少必需 id 字段的问题。

根本原因:在跨格式转换链 (Gemini → Internal → Claude/Gemini) 中,
functionCall 和 functionResponse 的 id 字段没有被正确传递。

修复内容:
- internal.py: ToolResultBlock 增加 tool_name 字段,区分工具名称和 call id
- gemini.py: 读取/输出 functionCall 和 functionResponse 时保留 id 字段
- gemini.py: 流式响应中正确传递 tool_id
- envelope.py: 同时支持 camelCase/snake_case 两种命名风格
This commit is contained in:
AAEE86
2026-02-10 02:50:45 +08:00
parent 9f5dd6f658
commit 5d36fd7f99
3 changed files with 60 additions and 28 deletions

View File

@@ -106,7 +106,8 @@ class ToolResultBlock:
"""工具结果内容块"""
type: ContentType = field(default=ContentType.TOOL_RESULT, init=False)
tool_use_id: str = "" # 对应的 ToolUseBlock.tool_id
tool_use_id: str = "" # 对应的 ToolUseBlock.tool_id(用于 Claude/Antigravity id 字段)
tool_name: str | None = None # 工具名称(用于 Gemini function_response.name 字段)
# 工具输出可能是纯文本,也可能是结构化 JSONGemini functionResponse 等)
output: Any = None
content_text: str | None = None

View File

@@ -480,14 +480,14 @@ class GeminiNormalizer(FormatNormalizer):
parts.append({"text": b.text})
continue
if isinstance(b, ToolUseBlock):
parts.append(
{
"functionCall": {
"name": b.tool_name,
"args": b.tool_input or {},
}
}
)
fc: dict[str, Any] = {
"name": b.tool_name,
"args": b.tool_input or {},
}
# 保留 tool_id 用于 Claude/Antigravity 兼容
if b.tool_id:
fc["id"] = b.tool_id
parts.append({"functionCall": fc})
continue
if isinstance(b, ImageBlock):
if b.data and b.media_type:
@@ -672,6 +672,10 @@ class GeminiNormalizer(FormatNormalizer):
if not isinstance(args, dict):
args = {}
# 优先使用 Antigravity/Claude 注入的 id
fc_id = func_call.get("id")
tool_id = fc_id if isinstance(fc_id, str) and fc_id else None
block_index = int(ss.get("next_block_index") or 2)
ss["next_block_index"] = block_index + 1
@@ -679,7 +683,7 @@ class GeminiNormalizer(FormatNormalizer):
ContentBlockStartEvent(
block_index=block_index,
block_type=ContentType.TOOL_USE,
tool_id=None,
tool_id=tool_id,
tool_name=name or None,
)
)
@@ -687,7 +691,7 @@ class GeminiNormalizer(FormatNormalizer):
events.append(
ToolCallDeltaEvent(
block_index=block_index,
tool_id="",
tool_id=tool_id or "",
input_delta=json.dumps(args, ensure_ascii=False),
)
)
@@ -801,6 +805,7 @@ class GeminiNormalizer(FormatNormalizer):
tool_blocks[int(event.block_index)] = {
"name": event.tool_name or "",
"json": "",
"id": event.tool_id or "", # 保留 tool_id 用于 Claude/Antigravity 兼容
}
return out
@@ -842,6 +847,7 @@ class GeminiNormalizer(FormatNormalizer):
name = str(entry.get("name") or "")
raw_json = str(entry.get("json") or "")
tool_id = str(entry.get("id") or "")
args: dict[str, Any] = {}
if raw_json:
try:
@@ -851,7 +857,11 @@ class GeminiNormalizer(FormatNormalizer):
except json.JSONDecodeError:
args = {}
out.append(base_chunk([{"functionCall": {"name": name, "args": args}}]))
fc: dict[str, Any] = {"name": name, "args": args}
# 保留 tool_id 用于 Claude/Antigravity 兼容
if tool_id:
fc["id"] = tool_id
out.append(base_chunk([{"functionCall": fc}]))
return out
if isinstance(event, MessageStopEvent):
@@ -1286,9 +1296,15 @@ class GeminiNormalizer(FormatNormalizer):
args = func_call.get("args")
if not isinstance(args, dict):
args = {}
# 优先使用 Antigravity/Claude 注入的 id回退到生成的 id
fc_id = func_call.get("id")
if isinstance(fc_id, str) and fc_id:
tool_id = fc_id
else:
tool_id = f"toolu_{name}" if name else "toolu_0"
blocks.append(
ToolUseBlock(
tool_id=f"toolu_{name}" if name else "toolu_0",
tool_id=tool_id,
tool_name=name,
tool_input=args,
extra={"gemini": part},
@@ -1314,9 +1330,17 @@ class GeminiNormalizer(FormatNormalizer):
else:
output = response
# 优先使用 Antigravity/Claude 注入的 id回退到 name
fr_id = func_resp.get("id")
if isinstance(fr_id, str) and fr_id:
tool_use_id = fr_id
else:
tool_use_id = name
blocks.append(
ToolResultBlock(
tool_use_id=name,
tool_use_id=tool_use_id,
tool_name=name or None, # 保留工具名称用于输出
output=output,
content_text=content_text,
is_error=False,
@@ -1372,11 +1396,16 @@ class GeminiNormalizer(FormatNormalizer):
continue
if isinstance(b, ToolUseBlock) and role == "model":
parts.append({"function_call": {"name": b.tool_name, "args": b.tool_input or {}}})
fc: dict[str, Any] = {"name": b.tool_name, "args": b.tool_input or {}}
# 保留 tool_id 用于 Claude/Antigravity 兼容
if b.tool_id:
fc["id"] = b.tool_id
parts.append({"function_call": fc})
continue
if isinstance(b, ToolResultBlock) and role == "user":
# 兼容旧转换器:name 直接使用 tool_use_idresponse 固定包一层 result
# name 使用 tool_name工具名称id 使用 tool_use_idcall id
# 如果没有 tool_name回退到 tool_use_id 以保持向后兼容
value: Any
if b.content_text is not None:
value = b.content_text
@@ -1385,14 +1414,16 @@ class GeminiNormalizer(FormatNormalizer):
else:
value = b.output
parts.append(
{
"function_response": {
"name": b.tool_use_id,
"response": {"result": value},
}
}
)
# 优先使用 tool_name 作为 name回退到 tool_use_id
name = b.tool_name if b.tool_name else b.tool_use_id
fr: dict[str, Any] = {
"name": name,
"response": {"result": value},
}
# 保留 tool_use_id 作为 id 用于 Claude/Antigravity 兼容
if b.tool_use_id:
fr["id"] = b.tool_use_id
parts.append({"function_response": fr})
continue
return {"role": role, "parts": parts}

View File

@@ -161,8 +161,8 @@ def _inject_claude_tool_ids_request(inner_request: dict[str, Any], model: str) -
if not isinstance(part, dict):
continue
# 1. functionCallAssistant 请求调用工具)
fc = part.get("functionCall")
# 1. functionCallAssistant 请求调用工具)- 支持 camelCase 和 snake_case
fc = part.get("functionCall") or part.get("function_call")
if isinstance(fc, dict) and fc.get("id") is None:
name = fc.get("name", "unknown")
if not isinstance(name, str):
@@ -171,8 +171,8 @@ def _inject_claude_tool_ids_request(inner_request: dict[str, Any], model: str) -
fc["id"] = f"call_{name}_{count}"
name_counters[name] = count + 1
# 2. functionResponseUser 回复工具结果)
fr = part.get("functionResponse")
# 2. functionResponseUser 回复工具结果)- 支持 camelCase 和 snake_case
fr = part.get("functionResponse") or part.get("function_response")
if isinstance(fr, dict) and fr.get("id") is None:
name = fr.get("name", "unknown")
if not isinstance(name, str):