Skip to content
Open
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/brown-hoops-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/google': patch
---

feat (provider/google): add `useResponseJsonSchema` provider option to send structured output schemas as JSON Schema
52 changes: 52 additions & 0 deletions content/providers/01-ai-sdk-providers/15-google.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ The following optional provider options are available for Google models:

See [Troubleshooting: Schema Limitations](#schema-limitations) for more details.

- **useResponseJsonSchema** _boolean_

Optional. Send the response schema as JSON Schema instead of the OpenAPI 3.0
subset that Google uses by default. Default is false.

JSON Schema supports features that the OpenAPI subset does not, e.g. unions
(`z.union`), records (`z.record`) and recursive schemas. Requires Gemini 2.5
or later.

See [Troubleshooting: Schema Limitations](#schema-limitations) for more details.

- **safetySettings** _Array\<\{ category: string; threshold: string \}\>_

Optional. Safety settings for the model.
Expand Down Expand Up @@ -1037,6 +1048,47 @@ The following Zod features are known to not work with Google:
- `z.union`
- `z.record`

Alternatively, you can keep structured outputs enabled and send the schema as
JSON Schema, which Gemini 2.5 and later models support:

```ts highlight="5"
const { output } = await generateText({
model: google('gemini-2.5-flash'),
providerOptions: {
google: {
useResponseJsonSchema: true,
} satisfies GoogleLanguageModelOptions,
},
output: Output.object({
schema: z.object({
name: z.string(),
age: z.number(),
contact: z.union([
z.object({
type: z.literal('email'),
value: z.string(),
}),
z.object({
type: z.literal('phone'),
value: z.string(),
}),
]),
}),
}),
prompt: 'Generate an example person for testing.',
});
```

JSON Schema also only supports a subset of the specification, and it is a
different subset, so switching is a trade-off rather than an upgrade. There is
no `pattern`, `minLength` or `maxLength` in it, and unsupported keywords are
ignored instead of rejected, so a `z.string().min(3)` constraint silently stops
being enforced. In the other direction, the conversion to the OpenAPI subset
drops `minimum`, `maximum`, `minItems` and `maxItems`, which
`responseJsonSchema` keeps. See the
[Gemini structured output documentation](https://ai.google.dev/gemini-api/docs/structured-output)
for the supported schema features.

### Model Capabilities

| Model | Image Input | Object Generation | Tool Usage | Tool Streaming | Google Search | URL Context |
Expand Down
52 changes: 52 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 @@ -294,6 +294,17 @@ The following optional provider options are available for Google Vertex models:

See [Troubleshooting: Schema Limitations](#schema-limitations) for more details.

- **useResponseJsonSchema** _boolean_

Optional. Send the response schema as JSON Schema instead of the OpenAPI 3.0
subset that Google Vertex uses by default. Default is false.

JSON Schema supports features that the OpenAPI subset does not, e.g. unions
(`z.union`), records (`z.record`) and recursive schemas. Requires Gemini 2.5
or later.

See [Troubleshooting: Schema Limitations](#schema-limitations) for more details.

- **safetySettings** _Array\<\{ category: string; threshold: string \}\>_

Optional. Safety settings for the model.
Expand Down Expand Up @@ -904,6 +915,47 @@ The following Zod features are known to not work with Google Vertex:
- `z.union`
- `z.record`

Alternatively, you can keep structured outputs enabled and send the schema as
JSON Schema, which Gemini 2.5 and later models support:

```ts highlight="4"
const result = await generateText({
model: googleVertex('gemini-2.5-pro'),
providerOptions: {
vertex: {
useResponseJsonSchema: true,
} satisfies GoogleLanguageModelOptions,
},
output: Output.object({
schema: z.object({
name: z.string(),
age: z.number(),
contact: z.union([
z.object({
type: z.literal('email'),
value: z.string(),
}),
z.object({
type: z.literal('phone'),
value: z.string(),
}),
]),
}),
}),
prompt: 'Generate an example person for testing.',
});
```

JSON Schema also only supports a subset of the specification, and it is a
different subset, so switching is a trade-off rather than an upgrade. There is
no `pattern`, `minLength` or `maxLength` in it, and unsupported keywords are
ignored instead of rejected, so a `z.string().min(3)` constraint silently stops
being enforced. In the other direction, the conversion to the OpenAPI subset
drops `minimum`, `maximum`, `minItems` and `maxItems`, which
`responseJsonSchema` keeps. See the
[Gemini structured output documentation](https://ai.google.dev/gemini-api/docs/structured-output)
for the supported schema features.

### Model Capabilities

| Model | Image Input | Object Generation | Tool Usage | Tool Streaming |
Expand Down
16 changes: 16 additions & 0 deletions packages/google/src/google-language-model-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ export const googleLanguageModelOptions = lazySchema(() =>
*/
structuredOutputs: z.boolean().optional(),

/**
* Optional. Send the response schema as JSON Schema
* (`generationConfig.responseJsonSchema`) instead of the OpenAPI 3.0
* subset (`generationConfig.responseSchema`). Default is false.
*
* JSON Schema supports features that the OpenAPI subset does not,
* e.g. unions (`anyOf`), records (`additionalProperties`) and
* recursive schemas (`$ref` / `$defs`). Requires Gemini 2.5 or later.
*
* It is a different subset rather than a superset: `pattern`,
* `minLength` and `maxLength` are ignored on this path.
*
* https://ai.google.dev/gemini-api/docs/structured-output
*/
useResponseJsonSchema: z.boolean().optional(),

/**
* Optional. A list of unique safety settings for blocking unsafe content.
*/
Expand Down
198 changes: 198 additions & 0 deletions packages/google/src/google-language-model.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
LanguageModelV4ProviderTool,
type JSONSchema7,
type LanguageModelV4Prompt,
} from '@ai-sdk/provider';
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
Expand All @@ -26,6 +27,15 @@ const TEST_PROMPT: LanguageModelV4Prompt = [
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
];

const CONSTRAINED_TEST_SCHEMA: JSONSchema7 = {
type: 'object',
properties: {
slug: { type: 'string', pattern: '^[a-z-]+$', minLength: 3, maxLength: 50 },
count: { type: 'integer', minimum: 1, maximum: 9 },
},
required: ['slug'],
};

const SAFETY_RATINGS = [
{
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
Expand Down Expand Up @@ -1619,6 +1629,194 @@ describe('doGenerate', () => {
`);
});

it('should pass json schema with responseFormat and useResponseJsonSchema = true', async () => {
prepareJsonFixtureResponse('google-text');

await provider.languageModel('gemini-pro').doGenerate({
providerOptions: {
google: {
useResponseJsonSchema: true,
},
},
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
contact: {
anyOf: [
{ type: 'object', properties: { email: { type: 'string' } } },
{ type: 'object', properties: { phone: { type: 'string' } } },
],
},
},
required: ['contact'],
additionalProperties: false,
},
},
prompt: TEST_PROMPT,
});

expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
{
"contents": [
{
"parts": [
{
"text": "Hello",
},
],
"role": "user",
},
],
"generationConfig": {
"responseJsonSchema": {
"additionalProperties": false,
"properties": {
"contact": {
"anyOf": [
{
"properties": {
"email": {
"type": "string",
},
},
"type": "object",
},
{
"properties": {
"phone": {
"type": "string",
},
},
"type": "object",
},
],
},
},
"required": [
"contact",
],
"type": "object",
},
"responseMimeType": "application/json",
},
}
`);
});

it('should keep string and number constraints with useResponseJsonSchema = true', async () => {
prepareJsonFixtureResponse('google-text');

await provider.languageModel('gemini-pro').doGenerate({
providerOptions: {
google: {
useResponseJsonSchema: true,
},
},
responseFormat: { type: 'json', schema: CONSTRAINED_TEST_SCHEMA },
prompt: TEST_PROMPT,
});

expect(
(await server.calls[0].requestBodyJson).generationConfig,
).toMatchInlineSnapshot(`
{
"responseJsonSchema": {
"properties": {
"count": {
"maximum": 9,
"minimum": 1,
"type": "integer",
},
"slug": {
"maxLength": 50,
"minLength": 3,
"pattern": "^[a-z-]+$",
"type": "string",
},
},
"required": [
"slug",
],
"type": "object",
},
"responseMimeType": "application/json",
}
`);
});

it('should drop most constraints when converting to the OpenAPI subset', async () => {
prepareJsonFixtureResponse('google-text');

await provider.languageModel('gemini-pro').doGenerate({
responseFormat: { type: 'json', schema: CONSTRAINED_TEST_SCHEMA },
prompt: TEST_PROMPT,
});

expect(
(await server.calls[0].requestBodyJson).generationConfig,
).toMatchInlineSnapshot(`
{
"responseMimeType": "application/json",
"responseSchema": {
"properties": {
"count": {
"type": "integer",
},
"slug": {
"minLength": 3,
"type": "string",
},
},
"required": [
"slug",
],
"type": "object",
},
}
`);
});

it('should not pass any schema with useResponseJsonSchema = true and structuredOutputs = false', async () => {
prepareJsonFixtureResponse('google-text');

await provider.languageModel('gemini-pro').doGenerate({
providerOptions: {
google: {
useResponseJsonSchema: true,
structuredOutputs: false,
},
},
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: { property1: { type: 'string' } },
},
},
prompt: TEST_PROMPT,
});

expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
{
"contents": [
{
"parts": [
{
"text": "Hello",
},
],
"role": "user",
},
],
"generationConfig": {
"responseMimeType": "application/json",
},
}
`);
});

it('should pass tools and toolChoice', async () => {
prepareJsonFixtureResponse('google-text');

Expand Down
Loading