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/cyan-pears-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ai-sdk/google": patch
---

feat(provider/google): add support for service tier parameter
60 changes: 50 additions & 10 deletions content/providers/01-ai-sdk-providers/15-google-generative-ai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,56 @@ The following optional provider options are available for Google Generative AI m

Model defaults to generate 1:1 squares, or to matching the output image size to that of your input image. Can be one of the following:

- 1:1
- 2:3
- 3:2
- 3:4
- 4:3
- 4:5
- 5:4
- 9:16
- 16:9
- 21:9
- 1:1
- 2:3
- 3:2
- 3:4
- 4:3
- 4:5
- 5:4
- 9:16
- 16:9
- 21:9

- **imageSize** _string_

Controls the output image resolution. Defaults to 1K. Can be one of the following:

- 1K
- 2K
- 4K

- **audioTimestamp** _boolean_

Optional. Enables timestamp understanding for audio-only files.
See [Google Cloud audio understanding documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/audio-understanding).

- **mediaResolution** _string_

Optional. If specified, the media resolution specified will be used. Can be one of the following:

- `MEDIA_RESOLUTION_UNSPECIFIED`
- `MEDIA_RESOLUTION_LOW`
- `MEDIA_RESOLUTION_MEDIUM`
- `MEDIA_RESOLUTION_HIGH`

See [Google API MediaResolution documentation](https://ai.google.dev/api/generate-content#MediaResolution).

- **labels** _Record<string, string>_

Optional. Defines labels used in billing reports. Available on Vertex AI only.
See [Google Cloud labels documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/add-labels-to-api-calls).

- **serviceTier** _'SERVICE_TIER_STANDARD' | 'SERVICE_TIER_FLEX' | 'SERVICE_TIER_PRIORITY'_

Optional. The service tier to use for the request.
Set to 'SERVICE_TIER_FLEX' for 50% cheaper processing at the cost of increased latency.
Set to 'SERVICE_TIER_PRIORITY' for ultra-low latency at a 75-100% price premium over 'SERVICE_TIER_STANDARD'.

- **threshold** _string_

Optional. Standalone threshold setting that can be used independently of `safetySettings`.
Uses the same values as the `safetySettings` threshold.

### Thinking

Expand Down
18 changes: 18 additions & 0 deletions examples/ai-core/src/generate-text/google-service-tier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { google, type GoogleGenerativeAIProviderOptions } from '@ai-sdk/google';
import { generateText } from 'ai';
import { run } from '../lib/run';

run(async () => {
const result = await generateText({
model: google('gemini-3.1-pro-preview'),
prompt: 'What color is the sky in one word?',
providerOptions: {
google: {
serviceTier: 'SERVICE_TIER_FLEX',
} satisfies GoogleGenerativeAIProviderOptions,
},
});

console.log(result.text);
console.log('serviceTier:', result.providerMetadata?.google?.serviceTier);
});
23 changes: 23 additions & 0 deletions examples/ai-core/src/stream-text/google-service-tier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { google, type GoogleGenerativeAIProviderOptions } from '@ai-sdk/google';
import { streamText } from 'ai';
import { run } from '../lib/run';

run(async () => {
const result = streamText({
model: google('gemini-3.1-pro-preview'),
prompt: 'What color is the sky in one word?',
providerOptions: {
google: {
serviceTier: 'SERVICE_TIER_FLEX',
} satisfies GoogleGenerativeAIProviderOptions,
},
});

await result.consumeStream();

console.log(await result.text);
console.log(
'serviceTier:',
(await result.providerMetadata)?.google?.serviceTier,
);
});
176 changes: 135 additions & 41 deletions packages/google/src/google-generative-ai-language-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,101 @@ describe('doGenerate', () => {
expect(finishReason).toStrictEqual('error');
});

it('should extract tool calls', async () => {
server.urls[TEST_URL_GEMINI_PRO].response = {
type: 'json-value',
body: {
candidates: [
{
content: {
parts: [
{
functionCall: {
name: 'test-tool',
args: { value: 'example value' },
},
},
],
role: 'model',
},
finishReason: 'STOP',
index: 0,
safetyRatings: SAFETY_RATINGS,
},
],
promptFeedback: { safetyRatings: SAFETY_RATINGS },
},
};
});

it('should send serviceTier in request body when specified', async () => {
prepareJsonResponse({ content: 'test response' });

await model.doGenerate({
prompt: TEST_PROMPT,
providerOptions: {
google: {
serviceTier: 'SERVICE_TIER_FLEX',
},
},
});

expect(await server.calls[0].requestBodyJson).toMatchObject({
serviceTier: 'SERVICE_TIER_FLEX',
});
});

it('should not send serviceTier in request body when not specified', async () => {
prepareJsonResponse({ content: 'test response' });

await model.doGenerate({
prompt: TEST_PROMPT,
});

const body = await server.calls[0].requestBodyJson;
expect(body).not.toHaveProperty('serviceTier');
});

it('should expose serviceTier in provider metadata', async () => {
server.urls[TEST_URL_GEMINI_PRO].response = {
type: 'json-value',
body: {
candidates: [
{
content: {
parts: [{ text: 'test response' }],
role: 'model',
},
finishReason: 'STOP',
safetyRatings: SAFETY_RATINGS,
},
],
usageMetadata: {
promptTokenCount: 1,
candidatesTokenCount: 2,
totalTokenCount: 3,
},
serviceTier: 'SERVICE_TIER_FLEX',
},
};

const { providerMetadata } = await model.doGenerate({
prompt: TEST_PROMPT,
});

expect(providerMetadata?.google.serviceTier).toBe('SERVICE_TIER_FLEX');
});

it('should expose null serviceTier in provider metadata when not present', async () => {
prepareJsonResponse({ content: 'test response' });

const { providerMetadata } = await model.doGenerate({
prompt: TEST_PROMPT,
});

expect(providerMetadata?.google.serviceTier).toBeNull();
});

it('should extract tool calls', async () => {
server.urls[TEST_URL_GEMINI_PRO].response = {
type: 'json-value',
Expand Down Expand Up @@ -1435,35 +1530,6 @@ describe('doGenerate', () => {
]);
});

it('should expose PromptFeedback in provider metadata', async () => {
server.urls[TEST_URL_GEMINI_PRO].response = {
type: 'json-value',
body: {
candidates: [
{
content: { parts: [{ text: 'No' }], role: 'model' },
finishReason: 'SAFETY',
index: 0,
safetyRatings: SAFETY_RATINGS,
},
],
promptFeedback: {
blockReason: 'SAFETY',
safetyRatings: SAFETY_RATINGS,
},
},
};

const { providerMetadata } = await model.doGenerate({
prompt: TEST_PROMPT,
});

expect(providerMetadata?.google.promptFeedback).toStrictEqual({
blockReason: 'SAFETY',
safetyRatings: SAFETY_RATINGS,
});
});

it('should expose grounding metadata in provider metadata', async () => {
prepareJsonResponse({
content: 'test response',
Expand Down Expand Up @@ -2666,6 +2732,7 @@ describe('doStream', () => {
"probability": "NEGLIGIBLE",
},
],
"serviceTier": null,
"urlContextMetadata": null,
"usageMetadata": {
"candidatesTokenCount": 233,
Expand Down Expand Up @@ -2721,17 +2788,28 @@ describe('doStream', () => {
]);
});

it('should expose PromptFeedback in provider metadata on finish', async () => {
it('should expose serviceTier in provider metadata on finish', async () => {
server.urls[TEST_URL_GEMINI_PRO].response = {
type: 'stream-chunks',
chunks: [
`data: {"candidates": [{"content": {"parts": [{"text": "No"}],"role": "model"},` +
`"finishReason": "PROHIBITED_CONTENT","index": 0}],` +
`"promptFeedback": {"blockReason": "PROHIBITED_CONTENT","safetyRatings": [` +
`{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},` +
`{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},` +
`{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},` +
`{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}}\n\n`,
`data: ${JSON.stringify({
candidates: [
{
content: {
parts: [{ text: 'test response' }],
role: 'model',
},
finishReason: 'STOP',
safetyRatings: SAFETY_RATINGS,
},
],
usageMetadata: {
promptTokenCount: 1,
candidatesTokenCount: 2,
totalTokenCount: 3,
},
serviceTier: 'SERVICE_TIER_FLEX',
})}\n\n`,
],
};

Expand All @@ -2744,11 +2822,24 @@ describe('doStream', () => {

expect(
finishEvent?.type === 'finish' &&
finishEvent.providerMetadata?.google.promptFeedback,
).toStrictEqual({
blockReason: 'PROHIBITED_CONTENT',
safetyRatings: SAFETY_RATINGS,
finishEvent.providerMetadata?.google.serviceTier,
).toBe('SERVICE_TIER_FLEX');
});

it('should expose null serviceTier in provider metadata on finish when not present', async () => {
prepareStreamResponse({ content: ['test'] });

const { stream } = await model.doStream({
prompt: TEST_PROMPT,
});

const events = await convertReadableStreamToArray(stream);
const finishEvent = events.find(event => event.type === 'finish');

expect(
finishEvent?.type === 'finish' &&
finishEvent.providerMetadata?.google.serviceTier,
).toBeNull();
});

it('should stream code execution tool calls and results', async () => {
Expand Down Expand Up @@ -3202,6 +3293,7 @@ describe('doStream', () => {
"probability": "NEGLIGIBLE",
},
],
"serviceTier": null,
"urlContextMetadata": null,
},
},
Expand Down Expand Up @@ -3556,6 +3648,7 @@ describe('doStream', () => {
"groundingMetadata": null,
"promptFeedback": null,
"safetyRatings": null,
"serviceTier": null,
"urlContextMetadata": null,
"usageMetadata": {
"candidatesTokenCount": 18,
Expand Down Expand Up @@ -3702,6 +3795,7 @@ describe('doStream', () => {
"probability": "NEGLIGIBLE",
},
],
"serviceTier": null,
"urlContextMetadata": null,
},
},
Expand Down
Loading
Loading