@@ -59,7 +59,9 @@ public static LlmRequest prepareGenenerateContentRequest(
5959 * Prepares an {@link LlmRequest} for the GenerateContent API.
6060 *
6161 * <p>This method can optionally sanitize the request and ensures that the last content part is
62- * from the user to prompt a model response. It also strips out any parts marked as "thoughts".
62+ * from the user to prompt a model response. It also strips out any parts marked as "thoughts" and
63+ * removes client-side function call IDs as some LLM APIs reject requests with client-side
64+ * function call IDs.
6365 *
6466 * @param llmRequest The original {@link LlmRequest}.
6567 * @param sanitize Whether to sanitize the request to be compatible with the Gemini API backend.
@@ -70,6 +72,7 @@ public static LlmRequest prepareGenenerateContentRequest(
7072 if (sanitize ) {
7173 llmRequest = sanitizeRequestForGeminiApi (llmRequest );
7274 }
75+ llmRequest = removeClientFunctionCallId (llmRequest );
7376 List <Content > contents = ensureModelResponse (llmRequest .contents ());
7477 if (stripThoughts ) {
7578 contents = stripThoughts (contents );
@@ -136,6 +139,50 @@ public static LlmRequest sanitizeRequestForGeminiApi(LlmRequest llmRequest) {
136139 return requestBuilder .contents (updatedContents ).build ();
137140 }
138141
142+ /**
143+ * Removes client-side function call IDs from the request.
144+ *
145+ * <p>Client-side function call IDs are internal to the ADK and should not be sent to the model.
146+ * This method iterates through the contents and parts, removing the ID from any {@link
147+ * com.google.genai.types.FunctionCall} or {@link com.google.genai.types.FunctionResponse} parts.
148+ *
149+ * @param llmRequest The request to process.
150+ * @return A new {@link LlmRequest} with function call IDs removed.
151+ */
152+ public static LlmRequest removeClientFunctionCallId (LlmRequest llmRequest ) {
153+ if (llmRequest .contents ().isEmpty ()) {
154+ return llmRequest ;
155+ }
156+
157+ ImmutableList <Content > updatedContents =
158+ llmRequest .contents ().stream ()
159+ .map (
160+ content ->
161+ content .toBuilder ()
162+ .parts (
163+ content .parts ().orElse (ImmutableList .of ()).stream ()
164+ .map (GeminiUtil ::removeClientFunctionCallIdFromPart )
165+ .collect (toImmutableList ()))
166+ .build ())
167+ .collect (toImmutableList ());
168+
169+ return llmRequest .toBuilder ().contents (updatedContents ).build ();
170+ }
171+
172+ private static Part removeClientFunctionCallIdFromPart (Part part ) {
173+ if (part .functionCall ().isPresent () && part .functionCall ().get ().id ().isPresent ()) {
174+ return part .toBuilder ()
175+ .functionCall (part .functionCall ().get ().toBuilder ().clearId ().build ())
176+ .build ();
177+ }
178+ if (part .functionResponse ().isPresent () && part .functionResponse ().get ().id ().isPresent ()) {
179+ return part .toBuilder ()
180+ .functionResponse (part .functionResponse ().get ().toBuilder ().clearId ().build ())
181+ .build ();
182+ }
183+ return part ;
184+ }
185+
139186 /**
140187 * Ensures that the content is conducive to prompting a model response by ensuring the last
141188 * content part is from the user.
@@ -213,7 +260,7 @@ public static boolean shouldEmitAccumulatedText(LlmResponse currentLlmResponse)
213260 }
214261
215262 /** Removes any `Part` that contains only a `thought` from the content list. */
216- public static List <Content > stripThoughts (List <Content > originalContents ) {
263+ public static ImmutableList <Content > stripThoughts (List <Content > originalContents ) {
217264 return originalContents .stream ()
218265 .map (
219266 content -> {
0 commit comments