Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/itchy-frogs-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ai-sdk/google": patch
---

feat(google-vertex): add support for streaming tool arguments input
63 changes: 63 additions & 0 deletions content/providers/01-ai-sdk-providers/16-google-vertex.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,15 @@ The following optional provider options are available for Google Vertex models:

Consult [Google's Documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/add-labels-to-api-calls) for usage details.

- **streamFunctionCallArguments** _boolean_

Optional. When set to true, function call arguments will be streamed
incrementally in streaming responses. This enables `tool-input-delta` events
to arrive as the model generates function call arguments, reducing perceived
latency for tool calls. Defaults to `true` for Vertex AI providers. Only supported on the Vertex AI API (not the Gemini API).

Consult [Google's Documentation](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#streaming-fc) for details.

You can use Google Vertex language models to generate text with the `generateText` function:

```ts highlight="1,4"
Expand Down Expand Up @@ -454,6 +463,60 @@ const result = await generateText({

The optional `retrievalConfig.latLng` provider option provides location context for queries about nearby places. This configuration applies to any grounding tools that support location context.

#### Streaming Function Call Arguments

For Gemini 3 Pro and later models on Vertex AI, you can stream function call
arguments as they are generated by setting `streamFunctionCallArguments` to
`true`. This reduces perceived latency when functions need to be called, as
`tool-input-delta` events arrive incrementally instead of waiting for the
complete arguments. This option is `true` by default and you can opt out by
setting it to false.

```ts
import { vertex } from '@ai-sdk/google-vertex';
import { type GoogleLanguageModelOptions } from '@ai-sdk/google';
import { streamText } from 'ai';
import { z } from 'zod';

const result = streamText({
model: vertex('gemini-3.1-pro-preview'),
prompt: 'What is the weather in Boston and San Francisco?',
tools: {
getWeather: {
description: 'Get the current weather in a given location',
inputSchema: z.object({
location: z.string().describe('City name'),
}),
},
},
providerOptions: {
vertex: {
streamFunctionCallArguments: false,
} satisfies GoogleLanguageModelOptions,
},
});

for await (const part of result.fullStream) {
switch (part.type) {
case 'tool-input-start':
console.log(`Tool call started: ${part.toolName}`);
break;
case 'tool-input-delta':
process.stdout.write(part.delta);
break;
case 'tool-call':
console.log(`Tool call complete: ${part.toolName}`, part.input);
break;
}
}
```

<Note>
This feature is only available on the Vertex AI API. It is not supported on
the Gemini API. When used with the Google Generative AI provider, a warning
will be emitted and the option will be ignored.
</Note>

#### Reasoning (Thinking Tokens)

Google Vertex AI, through its support for Gemini models, can also emit "thinking" tokens, representing the model's reasoning process. The AI SDK exposes these as reasoning information.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { vertex } from '@ai-sdk/google-vertex';
import { type GoogleLanguageModelOptions } from '@ai-sdk/google';
import { convertToModelMessages, streamText, UIDataTypes, UIMessage } from 'ai';
import { z } from 'zod';

export const maxDuration = 60;

export type VertexStreamingToolCallsMessage = UIMessage<
never,
UIDataTypes,
{
showWeatherInformation: {
input: {
city: string;
weather: string;
temperature: number;
description: string;
};
output: string;
};
}
>;

export async function POST(req: Request) {
const { messages } = await req.json();

const result = streamText({
model: vertex('gemini-3.1-pro-preview'),
messages: await convertToModelMessages(messages),
system:
'You are a helpful weather assistant. ' +
'Use getWeatherInformation to fetch weather data, then use showWeatherInformation to display it to the user. ' +
'Always show the weather using the showWeatherInformation tool.',
tools: {
getWeatherInformation: {
description: 'Get the current weather for a city',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }: { city: string }) => {
const conditions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy'];
return {
city,
weather: conditions[Math.floor(Math.random() * conditions.length)],
temperature: Math.floor(Math.random() * 50 - 10),
};
},
},
showWeatherInformation: {
description:
'Show weather information to the user. Always use this tool to present weather data.',
inputSchema: z.object({
city: z.string(),
weather: z.string(),
temperature: z.number(),
description: z
.string()
.describe('A brief description of the weather conditions.'),
}),
},
},
});

return result.toUIMessageStreamResponse();
}
116 changes: 116 additions & 0 deletions examples/ai-e2e-next/app/chat/vertex-streaming-tool-calls/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use client';

import { useChat } from '@ai-sdk/react';
import ChatInput from '@/components/chat-input';
import {
DefaultChatTransport,
lastAssistantMessageIsCompleteWithToolCalls,
} from 'ai';
import { VertexStreamingToolCallsMessage } from '@/app/api/chat/vertex-streaming-tool-calls/route';

export default function Chat() {
const { messages, status, sendMessage, addToolOutput } =
useChat<VertexStreamingToolCallsMessage>({
transport: new DefaultChatTransport({
api: '/api/chat/vertex-streaming-tool-calls',
}),

sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,

async onToolCall({ toolCall }) {
if (toolCall.toolName === 'showWeatherInformation') {
addToolOutput({
tool: 'showWeatherInformation',
toolCallId: toolCall.toolCallId,
output: 'Weather information displayed to user.',
});
}
},
});

let lastRole: string | undefined = undefined;

return (
<div className="flex flex-col py-24 mx-auto w-full max-w-md stretch">
<h1 className="text-lg font-bold mb-4">
Vertex AI — Streaming Tool Call Arguments
</h1>

{messages?.map(m => {
const isNewRole = m.role !== lastRole;
lastRole = m.role;

return (
<div key={m.id} className="whitespace-pre-wrap mb-2">
{isNewRole && (
<strong className="block mb-1">{`${m.role}: `}</strong>
)}
{m.parts.map((part, i) => {
if (part.type === 'text') {
return <span key={i}>{part.text}</span>;
}

if (part.type === 'tool-showWeatherInformation') {
if (part.state === 'input-streaming') {
return (
<div
key={i}
className="p-3 my-2 rounded border border-blue-300 bg-blue-50"
>
<div className="text-xs font-mono text-blue-600 mb-1">
streaming tool args…
</div>
<pre className="text-sm">
{JSON.stringify(part.input, null, 2)}
</pre>
</div>
);
}

if (part.state === 'input-available') {
return (
<div
key={i}
className="p-3 my-2 rounded border border-yellow-300 bg-yellow-50"
>
<div className="text-xs text-yellow-700 mb-1">
tool call complete — awaiting result
</div>
<pre className="text-sm">
{JSON.stringify(part.input, null, 2)}
</pre>
</div>
);
}

if (part.state === 'output-available') {
return (
<div
key={i}
className="p-4 my-2 rounded border border-green-300 bg-green-50"
>
<h4 className="font-semibold mb-1">{part.input.city}</h4>
<div className="flex gap-3 text-sm">
<span>🌡 {part.input.temperature}°C</span>
<span>☁ {part.input.weather}</span>
</div>
{part.input.description && (
<p className="mt-1 text-sm text-gray-600">
{part.input.description}
</p>
)}
</div>
);
}
}

return null;
})}
</div>
);
})}

<ChatInput status={status} onSubmit={text => sendMessage({ text })} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { vertex } from '@ai-sdk/google-vertex';
import { streamText } from 'ai';
import { z } from 'zod';
import { run } from '../../lib/run';
import { saveRawChunks } from '../../lib/save-raw-chunks';

run(async () => {
const result = streamText({
model: vertex('gemini-3.1-pro-preview'),
prompt: 'What is the weather in Boston and San Francisco?',
tools: {
getWeather: {
description: 'Get the current weather in a given location',
inputSchema: z.object({
location: z.string().describe('City name'),
}),
},
},
includeRawChunks: true,
});

for await (const part of result.fullStream) {
switch (part.type) {
case 'tool-input-start':
console.log(`\n[tool-input-start] ${part.toolName} (${part.id})`);
break;
case 'tool-input-delta':
process.stdout.write(part.delta);
break;
case 'tool-input-end':
console.log(`\n[tool-input-end] (${part.id})`);
break;
case 'tool-call':
console.log(`\n[tool-call] ${part.toolName}:`, part.input);
break;
case 'text-delta':
process.stdout.write(part.text);
break;
case 'finish':
console.log('\nFinish reason:', part.finishReason);
console.log('Usage:', part.totalUsage);
break;
case 'error':
console.error('Error:', part.error);
break;
}
}

await saveRawChunks({
result,
filename: 'google-vertex-stream-function-call-args-default.1',
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { vertex } from '@ai-sdk/google-vertex';
import { streamText } from 'ai';
import { z } from 'zod';
import { run } from '../../lib/run';
import { saveRawChunks } from '../../lib/save-raw-chunks';

run(async () => {
const result = streamText({
model: vertex('gemini-3.1-pro-preview'),
prompt: 'Cook me a lasagna.',
tools: {
cookRecipe: {
description: 'Cook a recipe',
inputSchema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
}),
),
steps: z.array(z.string()),
}),
}),
},
},
includeRawChunks: true,
});

for await (const part of result.fullStream) {
switch (part.type) {
case 'tool-input-start':
console.log(`\n[tool-input-start] ${part.toolName} (${part.id})`);
break;
case 'tool-input-delta':
process.stdout.write(part.delta);
break;
case 'tool-input-end':
console.log(`\n[tool-input-end] (${part.id})`);
break;
case 'tool-call':
console.log(`\n[tool-call] ${part.toolName}:`, part.input);
break;
case 'text-delta':
process.stdout.write(part.text);
break;
case 'finish':
console.log('\nFinish reason:', part.finishReason);
console.log('Usage:', part.totalUsage);
break;
case 'error':
console.error('Error:', part.error);
break;
}
}

await saveRawChunks({
result,
filename: 'google-vertex-stream-function-call-args-default.1',
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"name":"getWeather","willContinue":true},"thoughtSignature":"CiMBjz1rX25KieIB4d4AwFn8/WbsHTRNHBXso88PtemwfsSfRAp6AY89a1/e2FrmrE2HtlOxvV7vy8Kn5Og9n6CepcBKYW9aBT5QdXtTD4PGv9pDDWLEDAfsd86Et6k7HDEV976M3kC0ahLS8FCrwRntUKr+GF5uiUfPvgpzqNl5m8d8EwYykfwo1VwuTStR7q3mr/TzDcNzepafpVLdZloKYQGPPWtfkohmdau0GqGi3DuPbFTUBe5Ac/UbCvUk+P7KFbCGb93vFmnjmmTV7228DcAddqtWfe4iVePtSumw3JptYE6PIeVao2r8q7FXZKazzEiA9c04nmq6Vv9lOtt5NsgKrwEBjz1rX1ajIx3yeG/QI78uO+MsKoqgwrCI7PAEaDRqbyDCW/NX/4Vfuz1wp96oT3h/ttJ5ejzQyGamOrYreUUFvJBR/0TvFBdWOXwwCOWtvJ0Jh7SinV9HNqBDM1WWH8e+tGWY0r7xU/o/M1Fkwd73bvyp3BmSm5orrQL4jUZ1TN6ECqw7T7wHEkIfuBXGMZhQwAPzaapGVCdn0LjFJwd1aVReWmt6pE4ut993/K+0CqsBAY89a1+ts0Nggt7GZxVw/dQhH2Gc6KxRbAZKFMTuYRcNufJ5CyTvq85rMWEhTPWQxlQe1V7NIOlsQzyDhlejiCVbq0chDVrwRY23BmLrhSLi+SfKukEc0M+V9LdFiqxxLgUBrVdCvxA17g11j6HGdmXiNze2WDKm7ZXgMYvEiVmglpRZOb4JeJzxNPFsxhv2k6VkKB+GRtO48XwNz/i2taQ8Nq+LO5tED6HhCp4BAY89a18wBKoSkBT2IRo530FeNq/lpVozY0+k+p+H//TAtKoWZBamKCwra81idaMm1TTaWZGlX85Pq0kBvx1sQLFEhILeQrbH0O3Iuk9JlKLKctAwnBj8BZUGJEGtbigTKaSgWcMBhW+jfKWi1xwSxyJLRkIcXmE8U3FE/XHCLeiBy0NRa0yKn3xh62JU9087Q2nOMClxy5Zy5VsZ0qQ="}]}}],"usageMetadata":{"trafficType":"ON_DEMAND"},"modelVersion":"gemini-3.1-pro-preview","createTime":"2026-04-02T17:03:50.399550Z","responseId":"dqHOab6xGLzWodAPkPuViA4"}
{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"partialArgs":[{"jsonPath":"$.location","stringValue":"Boston","willContinue":true}],"willContinue":true}}]}}],"usageMetadata":{"trafficType":"ON_DEMAND"},"modelVersion":"gemini-3.1-pro-preview","createTime":"2026-04-02T17:03:50.399550Z","responseId":"dqHOab6xGLzWodAPkPuViA4"}
{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"partialArgs":[{"jsonPath":"$.location","stringValue":""}],"willContinue":true}}]}}],"usageMetadata":{"trafficType":"ON_DEMAND"},"modelVersion":"gemini-3.1-pro-preview","createTime":"2026-04-02T17:03:50.399550Z","responseId":"dqHOab6xGLzWodAPkPuViA4"}
{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{}}]}}],"usageMetadata":{"trafficType":"ON_DEMAND"},"modelVersion":"gemini-3.1-pro-preview","createTime":"2026-04-02T17:03:50.399550Z","responseId":"dqHOab6xGLzWodAPkPuViA4"}
{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"name":"getWeather","willContinue":true}}]}}],"usageMetadata":{"trafficType":"ON_DEMAND"},"modelVersion":"gemini-3.1-pro-preview","createTime":"2026-04-02T17:03:50.399550Z","responseId":"dqHOab6xGLzWodAPkPuViA4"}
{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"partialArgs":[{"jsonPath":"$.location","stringValue":"San Francisco","willContinue":true}],"willContinue":true}}]}}],"usageMetadata":{"trafficType":"ON_DEMAND"},"modelVersion":"gemini-3.1-pro-preview","createTime":"2026-04-02T17:03:50.399550Z","responseId":"dqHOab6xGLzWodAPkPuViA4"}
{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"partialArgs":[{"jsonPath":"$.location","stringValue":""}],"willContinue":true}}]}}],"usageMetadata":{"trafficType":"ON_DEMAND"},"modelVersion":"gemini-3.1-pro-preview","createTime":"2026-04-02T17:03:50.399550Z","responseId":"dqHOab6xGLzWodAPkPuViA4"}
{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{}}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":26,"candidatesTokenCount":23,"totalTokenCount":181,"trafficType":"ON_DEMAND","promptTokensDetails":[{"modality":"TEXT","tokenCount":26}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":23}],"thoughtsTokenCount":132},"modelVersion":"gemini-3.1-pro-preview","createTime":"2026-04-02T17:03:50.399550Z","responseId":"dqHOab6xGLzWodAPkPuViA4"}
Loading
Loading