Search before asking
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:
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.
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):
Map<String, Object> call = connection.convertFunctionCall(FunctionCall.builder().name("get_weather").args(Map.of()).build(), null);
→ the map contains neither id nor original_id.
- 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?
Search before asking
Description
The Gemini Developer API frequently returns
functionCallparts without anid. InGeminiChatModelConnection.convertFunctionCall, the id is read viafunctionCall.id().orElse(null),and when absent, neither
idnororiginal_idis put into the tool-call map. Two things break downstream:ToolCallActionreads the id viaString.valueOf(toolCall.get("id")), producing the literalstring
"null". With two or more parallel id-less calls, all results collide under the"null"key in the
success/responses/errormaps, so only the last one survives.original_idis whatToolCallActionpropagates as the TOOL message'sexternalId, which ishow the follow-up turn recovers the function name for Gemini's
functionResponsepart(
buildToolCallIdToNameMap+resolveToolFunctionName). Without it, the second request throwsIllegalArgumentException("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 isnever exercised end-to-end (the one id-less case,
testConvertFunctionCallNoSignature, only assertsthought_signatureabsence).Proposed fix: synthesize a UUID when the native id is absent and set both
idandoriginal_id,marked (e.g.
synthetic_id) soconvertToolCallToPartnever echoes a fabricated id back to theGemini API — on-the-wire behavior for id-less calls stays unchanged, while the runtime round trip
works.
How to reproduce
Unit-level (no network):
Map<String, Object> call = connection.convertFunctionCall(FunctionCall.builder().name("get_weather").args(Map.of()).build(), null);→ the map contains neither
idnororiginal_id.ChatMessagenever gets anexternalId,so
convertToContent(toolMessage, buildToolCallIdToNameMap(messages))throwsIllegalArgumentException("Tool message must carry the function name...").End-to-end: a Java agent with a
GeminiChatModelSetupandtools: [...]against the GeminiDeveloper 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_keymode); verified with unit tests against the SDK types, nonetwork required.
Are you willing to submit a PR?