Skip to content

[Bug] Gemini tool calling fails on the second turn when functionCall carries no id #947

Description

@Zhuoxi2000

Search before asking

  • I searched in the issues and found nothing similar.

Description

The Gemini Developer API frequently returns functionCall parts without an id. In
GeminiChatModelConnection.convertFunctionCall, the id is read via functionCall.id().orElse(null),
and when absent, neither id nor original_id is put into the tool-call map. Two things break downstream:

  1. ToolCallAction reads the id via String.valueOf(toolCall.get("id")), producing the literal
    string "null". With two or more parallel id-less calls, all results collide under the "null"
    key in the success / responses / error maps, so only the last one survives.
  2. original_id is what ToolCallAction propagates as the TOOL message's externalId, which is
    how the follow-up turn recovers the function name for Gemini's functionResponse part
    (buildToolCallIdToNameMap + resolveToolFunctionName). Without it, the second request throws
    IllegalArgumentException("Tool message must carry the function name...").

Expected: a documented, supported flow — Gemini setup with tools — completes the tool round trip.

Actual: turn 1 executes the tool, turn 2 crashes. The class javadoc assumes a native id is always
present ("Tool calls are returned as functionCall parts carrying a native id"), but the Developer API
does not guarantee one.

Existing unit tests always build FunctionCall.builder().id("call_1")..., so the id-less path is
never exercised end-to-end (the one id-less case, testConvertFunctionCallNoSignature, only asserts
thought_signature absence).

Proposed fix: synthesize a UUID when the native id is absent and set both id and original_id,
marked (e.g. synthetic_id) so convertToolCallToPart never echoes a fabricated id back to the
Gemini API — on-the-wire behavior for id-less calls stays unchanged, while the runtime round trip
works.

How to reproduce

Unit-level (no network):

  1. Map<String, Object> call = connection.convertFunctionCall(FunctionCall.builder().name("get_weather").args(Map.of()).build(), null);
    → the map contains neither id nor original_id.
  2. Feed that tool call through the runtime shape: the TOOL ChatMessage never gets an externalId,
    so convertToContent(toolMessage, buildToolCallIdToNameMap(messages)) throws
    IllegalArgumentException("Tool message must carry the function name...").

End-to-end: a Java agent with a GeminiChatModelSetup and tools: [...] against the Gemini
Developer API. First model turn calls the tool successfully; the follow-up chat request throws the
exception above. With two tools called in parallel, additionally observe that both results key on
"null" and one overwrites the other.

Version and environment

Current main (4d99567, 2026-07-25). Java 17, google-genai SDK as pinned by the repo. Affects the
Gemini Developer API path (api_key mode); verified with unit tests against the SDK types, no
network required.

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Metadata

Metadata

Assignees

Labels

bug[Issue Type] Something isn't working as expected.priority/majorDefault priority of the PR or issue.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions