Skip to content

Commit 30c63b4

Browse files
wanlin31copybara-github
authored andcommitted
fix: disable AFC when there are AFC incompatible tool presented.
PiperOrigin-RevId: 826531985
1 parent 4b75e5d commit 30c63b4

File tree

4 files changed

+233
-14
lines changed

4 files changed

+233
-14
lines changed

src/_afc.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,31 @@ export function hasCallableTools(
5353
return params.config?.tools?.some((tool) => isCallableTool(tool)) ?? false;
5454
}
5555

56-
// Checks whether the list of tools contains any non-callable tools. Will return
57-
// true if there is at least one non-Callable tool.
58-
export function hasNonCallableTools(
59-
params: types.GenerateContentParameters,
60-
): boolean {
61-
return params.config?.tools?.some((tool) => !isCallableTool(tool)) ?? false;
56+
/**
57+
* Returns the indexes of the tools that are not compatible with AFC.
58+
*/
59+
export function findAfcIncompatibleToolIndexes(
60+
params?: types.GenerateContentParameters,
61+
): number[] {
62+
// Use number[] for an array of numbers in TypeScript
63+
const afcIncompatibleToolIndexes: number[] = [];
64+
if (!params?.config?.tools) {
65+
return afcIncompatibleToolIndexes;
66+
}
67+
params.config.tools.forEach((tool, index) => {
68+
if (isCallableTool(tool)) {
69+
return;
70+
}
71+
const geminiTool = tool as types.Tool;
72+
if (
73+
geminiTool.functionDeclarations &&
74+
geminiTool.functionDeclarations.length > 0
75+
) {
76+
afcIncompatibleToolIndexes.push(index);
77+
}
78+
});
79+
80+
return afcIncompatibleToolIndexes;
6281
}
6382

6483
/**

src/models.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,13 @@ export class Models extends BaseModule {
6969
return await this.generateContentInternal(transformedParams);
7070
}
7171

72-
if (afc.hasNonCallableTools(params)) {
72+
const incompatibleToolIndexes = afc.findAfcIncompatibleToolIndexes(params);
73+
if (incompatibleToolIndexes.length > 0) {
74+
const formattedIndexes = incompatibleToolIndexes
75+
.map((index: number) => `tools[${index}]`)
76+
.join(', ');
7377
throw new Error(
74-
'Automatic function calling with CallableTools and Tools is not yet supported.',
78+
`Automatic function calling with CallableTools (or MCP objects) and basic FunctionDeclarations is not yet supported. Incompatible tools found at ${formattedIndexes}.`,
7579
);
7680
}
7781

@@ -195,9 +199,17 @@ export class Models extends BaseModule {
195199
const transformedParams =
196200
await this.processParamsMaybeAddMcpUsage(params);
197201
return await this.generateContentStreamInternal(transformedParams);
198-
} else {
199-
return await this.processAfcStream(params);
200202
}
203+
const incompatibleToolIndexes = afc.findAfcIncompatibleToolIndexes(params);
204+
if (incompatibleToolIndexes.length > 0) {
205+
const formattedIndexes = incompatibleToolIndexes
206+
.map((index: number) => `tools[${index}]`)
207+
.join(', ');
208+
throw new Error(
209+
`Incompatible tools found at ${formattedIndexes}. Automatic function calling with CallableTools (or MCP objects) and basic FunctionDeclarations" is not yet supported.`,
210+
);
211+
}
212+
return await this.processAfcStream(params);
201213
};
202214

203215
/**

test/unit/afc_test.ts

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
* Copyright 2025 Google LLC
44
* SPDX-License-Identifier: Apache-2.0
55
*/
6-
import {shouldAppendAfcHistory, shouldDisableAfc} from '../../src/_afc.js';
6+
import {
7+
findAfcIncompatibleToolIndexes,
8+
shouldAppendAfcHistory,
9+
shouldDisableAfc,
10+
} from '../../src/_afc.js';
711
import * as types from '../../src/types.js';
812

913
const callableTool: types.CallableTool = {
@@ -155,3 +159,189 @@ describe('afc_test', () => {
155159
expect(shouldAppendAfcHistory(config)).toBeFalse();
156160
});
157161
});
162+
163+
describe('findAfcIncompatibleToolIndexes', () => {
164+
it('should return empty list when there is no config', () => {
165+
expect(findAfcIncompatibleToolIndexes(undefined)).toEqual([]);
166+
});
167+
it('should return empty list when there is no tools', () => {
168+
const params: types.GenerateContentParameters = {
169+
model: 'gemini-2.0-flash',
170+
contents: 'why is the sky blue?',
171+
config: {},
172+
};
173+
expect(findAfcIncompatibleToolIndexes(params)).toEqual([]);
174+
});
175+
it('should return empty list when there are no functiondeclaration filed in the tools', () => {
176+
const params: types.GenerateContentParameters = {
177+
model: 'gemini-2.0-flash',
178+
contents: 'why is the sky blue?',
179+
config: {
180+
tools: [{} as types.Tool],
181+
},
182+
};
183+
expect(findAfcIncompatibleToolIndexes(params)).toEqual([]);
184+
});
185+
it('should return empty list when there are no function declarations', () => {
186+
const params: types.GenerateContentParameters = {
187+
model: 'gemini-2.0-flash',
188+
contents: 'why is the sky blue?',
189+
config: {
190+
tools: [{functionDeclarations: []} as types.Tool],
191+
},
192+
};
193+
expect(findAfcIncompatibleToolIndexes(params)).toEqual([]);
194+
});
195+
it('should return empty list when there function declarations is undefined', () => {
196+
const params: types.GenerateContentParameters = {
197+
model: 'gemini-2.0-flash',
198+
contents: 'why is the sky blue?',
199+
config: {
200+
tools: [{functionDeclarations: undefined} as types.Tool],
201+
},
202+
};
203+
expect(findAfcIncompatibleToolIndexes(params)).toEqual([]);
204+
});
205+
it('should return empty list when there are no incompatible tools', () => {
206+
const params: types.GenerateContentParameters = {
207+
model: 'gemini-2.0-flash',
208+
contents: 'why is the sky blue?',
209+
config: {
210+
tools: [
211+
callableTool,
212+
{
213+
retrieval: {
214+
disableAttribution: true,
215+
},
216+
},
217+
{
218+
googleSearch: {},
219+
},
220+
{
221+
googleSearchRetrieval: {},
222+
},
223+
{
224+
enterpriseWebSearch: {},
225+
},
226+
{
227+
googleMaps: {},
228+
},
229+
{
230+
urlContext: {},
231+
},
232+
{
233+
computerUse: {},
234+
},
235+
{
236+
codeExecution: {},
237+
},
238+
],
239+
},
240+
};
241+
expect(findAfcIncompatibleToolIndexes(params)).toEqual([]);
242+
});
243+
it('should return correct indexes when there are only one incompatible tools', () => {
244+
const params: types.GenerateContentParameters = {
245+
model: 'gemini-2.0-flash',
246+
contents: 'why is the sky blue?',
247+
config: {
248+
tools: [
249+
callableTool,
250+
{
251+
retrieval: {
252+
disableAttribution: true,
253+
},
254+
},
255+
{
256+
googleSearch: {},
257+
},
258+
{
259+
googleSearchRetrieval: {},
260+
},
261+
{
262+
functionDeclarations: [
263+
{
264+
name: 'test_function_1',
265+
},
266+
],
267+
},
268+
{
269+
enterpriseWebSearch: {},
270+
},
271+
{
272+
googleMaps: {},
273+
},
274+
{
275+
urlContext: {},
276+
},
277+
{
278+
computerUse: {},
279+
},
280+
{
281+
codeExecution: {},
282+
},
283+
],
284+
},
285+
};
286+
expect(findAfcIncompatibleToolIndexes(params)).toEqual([4]);
287+
});
288+
it('should return correct indexes when there are incompatible tools', () => {
289+
const params: types.GenerateContentParameters = {
290+
model: 'gemini-2.0-flash',
291+
contents: 'why is the sky blue?',
292+
config: {
293+
tools: [
294+
callableTool,
295+
{
296+
retrieval: {
297+
disableAttribution: true,
298+
},
299+
},
300+
{
301+
googleSearch: {},
302+
},
303+
{
304+
googleSearchRetrieval: {},
305+
},
306+
{
307+
functionDeclarations: [
308+
{
309+
name: 'test_function_1',
310+
},
311+
],
312+
},
313+
{
314+
enterpriseWebSearch: {},
315+
},
316+
{
317+
googleMaps: {},
318+
},
319+
{
320+
urlContext: {},
321+
},
322+
{
323+
computerUse: {},
324+
},
325+
{
326+
codeExecution: {},
327+
},
328+
{
329+
functionDeclarations: [
330+
{
331+
name: 'test_function_2',
332+
},
333+
],
334+
},
335+
{
336+
functionDeclarations: [
337+
{
338+
name: 'test_function_3',
339+
},
340+
],
341+
},
342+
],
343+
},
344+
};
345+
expect(findAfcIncompatibleToolIndexes(params)).toEqual([4, 10, 11]);
346+
});
347+
});

test/unit/models_test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -914,9 +914,7 @@ describe('generateContent', () => {
914914
},
915915
});
916916
} catch (e) {
917-
expect((e as Error).message).toEqual(
918-
'Automatic function calling with CallableTools and Tools is not yet supported.',
919-
);
917+
expect((e as Error).message).toContain('Incompatible tools found');
920918
}
921919
});
922920
it('should handle the error thrown by the underlying tool and wrap it in the response', async () => {

0 commit comments

Comments
 (0)