Skip to content

Commit bdde9d4

Browse files
vercel-ai-sdk[bot]edwardwcfelixarntz
authored
Backport: feat(provider/google): support combining built-in tools with function calling on Gemini 3 (#13993)
This is an automated backport of #13920 to the release-v6.0 branch. FYI @edwardwc ~~This backport has conflicts that need to be resolved manually.~~ Conflicts resolved. ### `git cherry-pick` output ``` Auto-merging packages/google/src/convert-to-google-generative-ai-messages.test.ts Auto-merging packages/google/src/convert-to-google-generative-ai-messages.ts Auto-merging packages/google/src/google-generative-ai-language-model.test.ts Auto-merging packages/google/src/google-generative-ai-language-model.ts CONFLICT (content): Merge conflict in packages/google/src/google-generative-ai-language-model.ts Auto-merging packages/google/src/google-prepare-tools.test.ts Auto-merging packages/google/src/google-prepare-tools.ts error: could not apply 01fa606... feat(provider/google): support combining built-in tools with function calling on Gemini 3 (#13920) hint: After resolving the conflicts, mark them with hint: "git add/rm <pathspec>", then run hint: "git cherry-pick --continue". hint: You can instead skip this commit with "git cherry-pick --skip". hint: To abort and get back to the state before "git cherry-pick", hint: run "git cherry-pick --abort". hint: Disable this message with "git config set advice.mergeConflict false" ``` --------- Co-authored-by: edwardwc <edwardwc@protonmail.com> Co-authored-by: Felix Arntz <felix.arntz@vercel.com>
1 parent ef5aa46 commit bdde9d4

10 files changed

+1069
-3
lines changed

.changeset/heavy-brooms-pretend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ai-sdk/google": patch
3+
---
4+
5+
feat(provider/google): support combining built-in tools with function calling on Gemini 3
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { google } from '@ai-sdk/google';
2+
import { generateText, stepCountIs } from 'ai';
3+
import { weatherTool } from '../../tools/weather-tool';
4+
import { run } from '../../lib/run';
5+
6+
run(async () => {
7+
const result = await generateText({
8+
model: google('gemini-3-flash-preview'),
9+
tools: {
10+
weather: weatherTool,
11+
google_search: google.tools.googleSearch({}),
12+
},
13+
prompt:
14+
'What is the weather in San Francisco and what are the latest news about the city?',
15+
stopWhen: stepCountIs(5),
16+
});
17+
18+
console.log('TEXT:', result.text);
19+
console.log('FINISH REASON:', result.finishReason);
20+
console.log('STEPS:', result.steps.length);
21+
for (const [i, step] of result.steps.entries()) {
22+
console.log(`\nSTEP ${i}:`);
23+
console.log(' finishReason:', step.finishReason);
24+
console.log(' text:', step.text.substring(0, 100));
25+
console.log(
26+
' toolCalls:',
27+
step.toolCalls.map(
28+
tc => `${tc.toolName}(providerExecuted=${tc.providerExecuted})`,
29+
),
30+
);
31+
console.log(
32+
' toolResults:',
33+
step.toolResults.map(tr => tr.toolName),
34+
);
35+
}
36+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { google } from '@ai-sdk/google';
2+
import { stepCountIs, streamText } from 'ai';
3+
import { weatherTool } from '../../tools/weather-tool';
4+
import { run } from '../../lib/run';
5+
6+
run(async () => {
7+
const result = streamText({
8+
model: google('gemini-3-flash-preview'),
9+
tools: {
10+
weather: weatherTool,
11+
google_search: google.tools.googleSearch({}),
12+
},
13+
prompt:
14+
'What is the weather in San Francisco and what are the latest news about the city?',
15+
stopWhen: stepCountIs(5),
16+
});
17+
18+
for await (const part of result.fullStream) {
19+
switch (part.type) {
20+
case 'text-delta':
21+
process.stdout.write(part.text);
22+
break;
23+
case 'tool-call':
24+
console.log(
25+
`\nTool call: ${part.toolName}`,
26+
JSON.stringify(part.input),
27+
);
28+
break;
29+
case 'tool-result':
30+
console.log(
31+
`Tool result: ${part.toolName}`,
32+
JSON.stringify(part.output),
33+
);
34+
break;
35+
case 'source':
36+
if (part.sourceType === 'url') {
37+
console.log(`Source: ${part.title} - ${part.url}`);
38+
}
39+
break;
40+
case 'error':
41+
console.error('Error:', part.error);
42+
break;
43+
}
44+
}
45+
});

packages/google/src/convert-to-google-generative-ai-messages.test.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,3 +1042,184 @@ describe('tool results with thought signatures', () => {
10421042
expect(result.contents[1].parts[0]).not.toHaveProperty('thoughtSignature');
10431043
});
10441044
});
1045+
1046+
describe('server tool combination round-trip', () => {
1047+
it('should convert assistant tool-call with serverToolCallId to toolCall wire format', () => {
1048+
const result = convertToGoogleGenerativeAIMessages([
1049+
{
1050+
role: 'assistant',
1051+
content: [
1052+
{
1053+
type: 'tool-call',
1054+
toolCallId: 'tc-1',
1055+
toolName: 'server:GOOGLE_SEARCH_WEB',
1056+
input: JSON.stringify({ query: 'test' }),
1057+
providerOptions: {
1058+
google: {
1059+
serverToolCallId: 'server-id-1',
1060+
serverToolType: 'GOOGLE_SEARCH_WEB',
1061+
thoughtSignature: 'sig-abc',
1062+
},
1063+
},
1064+
},
1065+
],
1066+
},
1067+
]);
1068+
1069+
expect(result.contents[0].parts[0]).toEqual({
1070+
toolCall: {
1071+
toolType: 'GOOGLE_SEARCH_WEB',
1072+
args: { query: 'test' },
1073+
id: 'server-id-1',
1074+
},
1075+
thoughtSignature: 'sig-abc',
1076+
});
1077+
});
1078+
1079+
it('should convert assistant tool-call without serverToolCallId to functionCall wire format', () => {
1080+
const result = convertToGoogleGenerativeAIMessages([
1081+
{
1082+
role: 'assistant',
1083+
content: [
1084+
{
1085+
type: 'tool-call',
1086+
toolCallId: 'tc-1',
1087+
toolName: 'weather',
1088+
input: { location: 'SF' },
1089+
},
1090+
],
1091+
},
1092+
]);
1093+
1094+
expect(result.contents[0].parts[0]).toEqual({
1095+
functionCall: {
1096+
name: 'weather',
1097+
args: { location: 'SF' },
1098+
},
1099+
thoughtSignature: undefined,
1100+
});
1101+
});
1102+
1103+
it('should convert tool result with serverToolCallId to toolResponse on last model content', () => {
1104+
const result = convertToGoogleGenerativeAIMessages([
1105+
{
1106+
role: 'assistant',
1107+
content: [
1108+
{
1109+
type: 'tool-call',
1110+
toolCallId: 'tc-1',
1111+
toolName: 'server:GOOGLE_SEARCH_WEB',
1112+
input: JSON.stringify({ query: 'test' }),
1113+
providerOptions: {
1114+
google: {
1115+
serverToolCallId: 'server-id-1',
1116+
serverToolType: 'GOOGLE_SEARCH_WEB',
1117+
},
1118+
},
1119+
},
1120+
],
1121+
},
1122+
{
1123+
role: 'tool',
1124+
content: [
1125+
{
1126+
type: 'tool-result',
1127+
toolCallId: 'tc-1',
1128+
toolName: 'server:GOOGLE_SEARCH_WEB',
1129+
output: { type: 'json', value: { results: ['a'] } },
1130+
providerOptions: {
1131+
google: {
1132+
serverToolCallId: 'server-id-1',
1133+
serverToolType: 'GOOGLE_SEARCH_WEB',
1134+
thoughtSignature: 'sig-resp',
1135+
},
1136+
},
1137+
},
1138+
],
1139+
},
1140+
]);
1141+
1142+
expect(result.contents[0].role).toBe('model');
1143+
expect(result.contents[0].parts).toHaveLength(2);
1144+
1145+
expect(result.contents[0].parts[0]).toEqual({
1146+
toolCall: {
1147+
toolType: 'GOOGLE_SEARCH_WEB',
1148+
args: { query: 'test' },
1149+
id: 'server-id-1',
1150+
},
1151+
thoughtSignature: undefined,
1152+
});
1153+
1154+
expect(result.contents[0].parts[1]).toEqual({
1155+
toolResponse: {
1156+
toolType: 'GOOGLE_SEARCH_WEB',
1157+
response: { results: ['a'] },
1158+
id: 'server-id-1',
1159+
},
1160+
thoughtSignature: 'sig-resp',
1161+
});
1162+
});
1163+
1164+
it('should parse string input for server tool call args', () => {
1165+
const result = convertToGoogleGenerativeAIMessages([
1166+
{
1167+
role: 'assistant',
1168+
content: [
1169+
{
1170+
type: 'tool-call',
1171+
toolCallId: 'tc-1',
1172+
toolName: 'server:GOOGLE_SEARCH_WEB',
1173+
input: '{"query":"hello"}',
1174+
providerOptions: {
1175+
google: {
1176+
serverToolCallId: 'sid-1',
1177+
serverToolType: 'GOOGLE_SEARCH_WEB',
1178+
},
1179+
},
1180+
},
1181+
],
1182+
},
1183+
]);
1184+
1185+
expect(result.contents[0].parts[0]).toEqual({
1186+
toolCall: {
1187+
toolType: 'GOOGLE_SEARCH_WEB',
1188+
args: { query: 'hello' },
1189+
id: 'sid-1',
1190+
},
1191+
thoughtSignature: undefined,
1192+
});
1193+
});
1194+
1195+
it('should pass object input directly for server tool call args', () => {
1196+
const result = convertToGoogleGenerativeAIMessages([
1197+
{
1198+
role: 'assistant',
1199+
content: [
1200+
{
1201+
type: 'tool-call',
1202+
toolCallId: 'tc-1',
1203+
toolName: 'server:GOOGLE_SEARCH_WEB',
1204+
input: { query: 'hello' },
1205+
providerOptions: {
1206+
google: {
1207+
serverToolCallId: 'sid-1',
1208+
serverToolType: 'GOOGLE_SEARCH_WEB',
1209+
},
1210+
},
1211+
},
1212+
],
1213+
},
1214+
]);
1215+
1216+
expect(result.contents[0].parts[0]).toEqual({
1217+
toolCall: {
1218+
toolType: 'GOOGLE_SEARCH_WEB',
1219+
args: { query: 'hello' },
1220+
id: 'sid-1',
1221+
},
1222+
thoughtSignature: undefined,
1223+
});
1224+
});
1225+
});

packages/google/src/convert-to-google-generative-ai-messages.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,29 @@ export function convertToGoogleGenerativeAIMessages(
290290
}
291291

292292
case 'tool-call': {
293+
const serverToolCallId =
294+
providerOpts?.serverToolCallId != null
295+
? String(providerOpts.serverToolCallId)
296+
: undefined;
297+
const serverToolType =
298+
providerOpts?.serverToolType != null
299+
? String(providerOpts.serverToolType)
300+
: undefined;
301+
302+
if (serverToolCallId && serverToolType) {
303+
return {
304+
toolCall: {
305+
toolType: serverToolType,
306+
args:
307+
typeof part.input === 'string'
308+
? JSON.parse(part.input)
309+
: part.input,
310+
id: serverToolCallId,
311+
},
312+
thoughtSignature,
313+
};
314+
}
315+
293316
return {
294317
functionCall: {
295318
name: part.toolName,
@@ -298,10 +321,36 @@ export function convertToGoogleGenerativeAIMessages(
298321
thoughtSignature,
299322
};
300323
}
324+
325+
case 'tool-result': {
326+
const serverToolCallId =
327+
providerOpts?.serverToolCallId != null
328+
? String(providerOpts.serverToolCallId)
329+
: undefined;
330+
const serverToolType =
331+
providerOpts?.serverToolType != null
332+
? String(providerOpts.serverToolType)
333+
: undefined;
334+
335+
if (serverToolCallId && serverToolType) {
336+
return {
337+
toolResponse: {
338+
toolType: serverToolType,
339+
response:
340+
part.output.type === 'json' ? part.output.value : {},
341+
id: serverToolCallId,
342+
},
343+
thoughtSignature,
344+
};
345+
}
346+
347+
return undefined;
348+
}
301349
}
302350
})
303351
.filter(part => part !== undefined),
304352
});
353+
305354
break;
306355
}
307356

@@ -314,6 +363,44 @@ export function convertToGoogleGenerativeAIMessages(
314363
if (part.type === 'tool-approval-response') {
315364
continue;
316365
}
366+
367+
const partProviderOpts =
368+
part.providerOptions?.[providerOptionsName] ??
369+
(providerOptionsName !== 'google'
370+
? part.providerOptions?.google
371+
: part.providerOptions?.vertex);
372+
const serverToolCallId =
373+
partProviderOpts?.serverToolCallId != null
374+
? String(partProviderOpts.serverToolCallId)
375+
: undefined;
376+
const serverToolType =
377+
partProviderOpts?.serverToolType != null
378+
? String(partProviderOpts.serverToolType)
379+
: undefined;
380+
381+
if (serverToolCallId && serverToolType) {
382+
const serverThoughtSignature =
383+
partProviderOpts?.thoughtSignature != null
384+
? String(partProviderOpts.thoughtSignature)
385+
: undefined;
386+
387+
if (contents.length > 0) {
388+
const lastContent = contents[contents.length - 1];
389+
if (lastContent.role === 'model') {
390+
lastContent.parts.push({
391+
toolResponse: {
392+
toolType: serverToolType,
393+
response:
394+
part.output.type === 'json' ? part.output.value : {},
395+
id: serverToolCallId,
396+
},
397+
thoughtSignature: serverThoughtSignature,
398+
});
399+
continue;
400+
}
401+
}
402+
}
403+
317404
const output = part.output;
318405

319406
if (output.type === 'content') {

0 commit comments

Comments
 (0)