From 9da4d6cc8fb1d3ce55dc403b0e2e2283245aadce Mon Sep 17 00:00:00 2001 From: Jaycee Li Date: Wed, 16 Sep 2026 11:24:13 -0700 Subject: [PATCH] fix: do not run functions once the automatic function calling budget is spent PiperOrigin-RevId: 982623260 --- src/main/java/com/google/genai/Models.java | 5 ++ src/test/java/com/google/genai/ChatTest.java | 54 ++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/main/java/com/google/genai/Models.java b/src/main/java/com/google/genai/Models.java index 94bfbba0b6ae..286ed6b52f25 100644 --- a/src/main/java/com/google/genai/Models.java +++ b/src/main/java/com/google/genai/Models.java @@ -7245,7 +7245,12 @@ public GenerateContentResponse generateContent( logger.info(String.format("Automatic function calling remote call %d is done", i)); remainingRemoteCalls--; if (remainingRemoteCalls == 0) { + /* + * No request is left to send a result with, so the functions are not called at all. The + * turn ends on the model's function call, which the caller can run and answer themselves. + */ logger.info("Reached max remote calls for automatic function calling."); + break; } if (!response.candidates().isPresent() || response.candidates().get().isEmpty() diff --git a/src/test/java/com/google/genai/ChatTest.java b/src/test/java/com/google/genai/ChatTest.java index a4ffd837abd6..a22f0e3b2abd 100644 --- a/src/test/java/com/google/genai/ChatTest.java +++ b/src/test/java/com/google/genai/ChatTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; +import com.google.genai.types.AutomaticFunctionCallingConfig; import com.google.genai.types.Candidate; import com.google.genai.types.Content; import com.google.genai.types.FinishReason; @@ -61,7 +62,10 @@ public class ChatTest { private static final String STREAMING_RESPONSE_CHUNK_3 = " far, far away..."; private static final String NON_STREAMING_RESPONSE = "This is a non-streaming response."; + static int findTheatersCallCount = 0; + public static String findTheaters(String movie, String location, String time) { + findTheatersCallCount++; return "AMC Metreon 16, AMC Kabuki 8, AMC Theater 11"; } @@ -245,6 +249,56 @@ public void testGetHistoryWithAfc() throws Exception { assert chatSession.getHistory(true).size() == 4; } + @Test + public void testSpentAfcBudgetLeavesTheFunctionCallUnanswered() throws Exception { + findTheatersCallCount = 0; + String userMessage = "Find theaters for Oppenheimer."; + Content functionCallContent = + Content.fromParts( + Part.fromFunctionCall( + "findTheaters", + ImmutableMap.of( + "movie", "Oppenheimer", "location", "New York, NY", "time", "10:00 PM"))); + + GenerateContentResponse functionResponse = + GenerateContentResponse.builder() + .candidates( + Candidate.builder() + .content(functionCallContent) + .finishReason(FinishReason.Known.STOP)) + .build(); + + when(mockedClient.request(anyString(), anyString(), anyString(), any())) + .thenReturn(mockedResponse1); + ResponseBody functionResponseBody = + ResponseBody.create(functionResponse.toJson(), MediaType.get("application/json")); + when(mockedResponse1.getBody()).thenReturn(functionResponseBody); + + Field apiClientField = Chats.class.getDeclaredField("apiClient"); + apiClientField.setAccessible(true); + apiClientField.set(client.chats, mockedClient); + Method method = + ChatTest.class.getDeclaredMethod("findTheaters", String.class, String.class, String.class); + GenerateContentConfig config = + GenerateContentConfig.builder() + .tools(Tool.builder().functions(method)) + .automaticFunctionCalling( + AutomaticFunctionCallingConfig.builder().maximumRemoteCalls(1)) + .build(); + Chat chatSession = client.chats.create(MODEL_ID, config); + + GenerateContentResponse response = chatSession.sendMessage(userMessage, null); + + // The one request the budget allows is spent being asked, leaving nothing to send a result + // with, so the function is never called. + assert findTheatersCallCount == 0; + // The model's function call is recorded once, not twice, and the turn ends on it so the + // caller can answer it themselves. + assert chatSession.getHistory(false).size() == 2; // user input, function call + assertNotNull(response.functionCalls()); + assert response.functionCalls().size() == 1; + } + @Test public void testMultiTurnChat() throws Exception {