Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions astrbot/core/provider/sources/gemini_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ def append_or_extend(
contents.append(content_cls(parts=part))

gemini_contents: list[types.Content] = []
tool_call_names: dict[str, str] = {}
for message in payloads["messages"]:
role, content = message["role"], message.get("content")

Expand Down Expand Up @@ -406,8 +407,12 @@ def append_or_extend(

if "tool_calls" in message:
for tool in message["tool_calls"]:
tool_call_id = tool.get("id")
function_name = tool["function"]["name"]
if tool_call_id:
tool_call_names[tool_call_id] = function_name
part = types.Part.from_function_call(
name=tool["function"]["name"],
name=function_name,
args=json.loads(tool["function"]["arguments"]),
)
# we should set thought_signature back to part if exists
Expand All @@ -429,7 +434,12 @@ def append_or_extend(
append_or_extend(gemini_contents, parts, types.ModelContent)

elif role == "tool":
func_name = message.get("name", message["tool_call_id"])
tool_call_id = message["tool_call_id"]
func_name = (
message.get("name")
or tool_call_names.get(tool_call_id)
or tool_call_id
)
part = types.Part.from_function_response(
name=func_name,
response={
Expand Down
71 changes: 71 additions & 0 deletions tests/test_gemini_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,77 @@ async def test_gemini_prepare_conversation_preserves_user_model_history():
assert contents[-1].parts[-1].text == "assistant turn"


@pytest.mark.asyncio
async def test_gemini_prepare_conversation_maps_tool_call_id_to_function_name():
provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI)

contents = await provider._prepare_conversation(
{
"messages": [
{"role": "user", "content": "check the weather"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_opaque_123",
"function": {
"name": "get_weather",
"arguments": '{"city": "Shenyang"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_opaque_123",
"content": "sunny",
},
]
}
)

assert contents[-1].parts is not None
function_response = contents[-1].parts[0].function_response
assert function_response is not None
assert function_response.name == "get_weather"
assert function_response.response == {
"name": "get_weather",
"content": "sunny",
}


@pytest.mark.asyncio
async def test_gemini_prepare_conversation_keeps_tool_name_fallbacks():
provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI)

contents = await provider._prepare_conversation(
{
"messages": [
{"role": "user", "content": "run tools"},
{
"role": "tool",
"name": "explicit_name",
"tool_call_id": "call_explicit",
"content": "first result",
},
{
"role": "tool",
"tool_call_id": "legacy_function_name",
"content": "second result",
},
]
}
)

assert contents[-1].parts is not None
function_responses = [part.function_response for part in contents[-1].parts]
assert [response.name for response in function_responses if response] == [
"explicit_name",
"legacy_function_name",
]


@pytest.mark.asyncio
async def test_gemini_prepare_conversation_resolves_local_history_image(tmp_path):
image_path = tmp_path / "history.webp"
Expand Down
Loading