@ai-sdk/google 4.0.51 → 4.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/index.js +67 -78
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +4 -3
- package/dist/internal/index.js +43 -17
- package/dist/internal/index.js.map +1 -1
- package/docs/15-google.mdx +28 -16
- package/package.json +2 -2
- package/src/convert-json-schema-to-openapi-schema.ts +13 -1
- package/src/google-batch.ts +9 -78
- package/src/google-files.ts +27 -2
- package/src/google-prepare-tools.ts +45 -21
- package/src/interactions/build-google-interactions-stream-transform.ts +13 -5
package/docs/15-google.mdx
CHANGED
|
@@ -1067,10 +1067,13 @@ The following Zod features are known to not work with Google:
|
|
|
1067
1067
|
### Text Batches
|
|
1068
1068
|
|
|
1069
1069
|
<Note type="warning">
|
|
1070
|
-
Text batch
|
|
1070
|
+
Text batch support is experimental and the API may change in patch releases.
|
|
1071
1071
|
</Note>
|
|
1072
1072
|
|
|
1073
|
-
The Google provider supports
|
|
1073
|
+
The Google provider supports asynchronous text generation through the
|
|
1074
|
+
[Gemini Batch API](https://ai.google.dev/gemini-api/docs/batch-api). Use the
|
|
1075
|
+
experimental text batch APIs to start a batch, poll its status, and stream its
|
|
1076
|
+
results:
|
|
1074
1077
|
|
|
1075
1078
|
```ts
|
|
1076
1079
|
import { google } from '@ai-sdk/google';
|
|
@@ -1079,32 +1082,39 @@ import {
|
|
|
1079
1082
|
experimental_getBatchStatus as getBatchStatus,
|
|
1080
1083
|
experimental_startTextBatch as startTextBatch,
|
|
1081
1084
|
} from 'ai';
|
|
1085
|
+
import { setTimeout } from 'node:timers/promises';
|
|
1082
1086
|
|
|
1083
1087
|
const model = google('gemini-3.6-flash');
|
|
1084
1088
|
|
|
1085
1089
|
const batch = await startTextBatch({
|
|
1086
1090
|
model,
|
|
1087
1091
|
requests: [
|
|
1088
|
-
{ id: 'france', prompt: 'What is the capital of France?' },
|
|
1089
|
-
{ id: 'germany', prompt: 'What is the capital of Germany?' },
|
|
1092
|
+
{ id: 'capital-france', prompt: 'What is the capital of France?' },
|
|
1093
|
+
{ id: 'capital-germany', prompt: 'What is the capital of Germany?' },
|
|
1090
1094
|
],
|
|
1091
1095
|
});
|
|
1092
1096
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1097
|
+
let status = batch.status;
|
|
1098
|
+
while (status === 'pending') {
|
|
1099
|
+
await setTimeout(60_000);
|
|
1100
|
+
({ status } = await getBatchStatus({ model, batch }));
|
|
1101
|
+
}
|
|
1095
1102
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
console.error(item.id, item.error);
|
|
1102
|
-
}
|
|
1103
|
+
for await (const item of getBatchResults({ model, batch })) {
|
|
1104
|
+
if (item.status === 'succeeded') {
|
|
1105
|
+
console.log(item.id, item.text);
|
|
1106
|
+
} else {
|
|
1107
|
+
console.error(item.id, item.error);
|
|
1103
1108
|
}
|
|
1104
1109
|
}
|
|
1105
1110
|
```
|
|
1106
1111
|
|
|
1107
|
-
|
|
1112
|
+
`startTextBatch` returns a serializable batch reference. Persist this reference
|
|
1113
|
+
to check the batch status or retrieve its results from another process. Results
|
|
1114
|
+
can arrive in a different order from the input requests, so match each result by
|
|
1115
|
+
its `id`.
|
|
1116
|
+
|
|
1117
|
+
#### Webhooks
|
|
1108
1118
|
|
|
1109
1119
|
You can pass a `webhookUrl` to receive a notification when the batch reaches a terminal state:
|
|
1110
1120
|
|
|
@@ -1112,8 +1122,8 @@ You can pass a `webhookUrl` to receive a notification when the batch reaches a t
|
|
|
1112
1122
|
const batch = await startTextBatch({
|
|
1113
1123
|
model,
|
|
1114
1124
|
requests: [
|
|
1115
|
-
{ id: 'france', prompt: 'What is the capital of France?' },
|
|
1116
|
-
{ id: 'germany', prompt: 'What is the capital of Germany?' },
|
|
1125
|
+
{ id: 'capital-france', prompt: 'What is the capital of France?' },
|
|
1126
|
+
{ id: 'capital-germany', prompt: 'What is the capital of Germany?' },
|
|
1117
1127
|
],
|
|
1118
1128
|
webhookUrl: 'https://example.com/api/google-batch-webhook',
|
|
1119
1129
|
});
|
|
@@ -1121,6 +1131,8 @@ const batch = await startTextBatch({
|
|
|
1121
1131
|
|
|
1122
1132
|
Google sends a thin event to the webhook URL. After receiving and verifying the event, use the persisted batch reference with `getBatchStatus` or `getBatchResults`. Your application is responsible for handling the webhook and [verifying Google's dynamic webhook signature](https://ai.google.dev/gemini-api/docs/webhooks#verify_dynamic_signatures_jwks).
|
|
1123
1133
|
|
|
1134
|
+
#### Large Batches
|
|
1135
|
+
|
|
1124
1136
|
When the serialized batch creation body is under 20 MB, the provider sends the requests inline. At 20 MB or more, it uploads a JSONL input file through the Gemini Files API.
|
|
1125
1137
|
|
|
1126
1138
|
## Realtime Models
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/google",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.53",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@ai-sdk/provider": "4.0.8",
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.32"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@ai-sdk/test-server": "2.0.1",
|
|
@@ -14,6 +14,18 @@ type ReferenceContext = {
|
|
|
14
14
|
resolvingReferences: ReadonlySet<string>;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
const recursiveReferenceFunctionalityPrefix =
|
|
18
|
+
'recursive JSON Schema reference:';
|
|
19
|
+
|
|
20
|
+
export function isRecursiveJSONSchemaReferenceError(
|
|
21
|
+
error: unknown,
|
|
22
|
+
): error is UnsupportedFunctionalityError {
|
|
23
|
+
return (
|
|
24
|
+
UnsupportedFunctionalityError.isInstance(error) &&
|
|
25
|
+
error.functionality.startsWith(recursiveReferenceFunctionalityPrefix)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
/**
|
|
18
30
|
* Converts JSON Schema 7 to OpenAPI Schema 3.0
|
|
19
31
|
*/
|
|
@@ -205,7 +217,7 @@ function convertJSONSchemaReference({
|
|
|
205
217
|
|
|
206
218
|
if (referenceContext.resolvingReferences.has(referenceKey)) {
|
|
207
219
|
throw new UnsupportedFunctionalityError({
|
|
208
|
-
functionality:
|
|
220
|
+
functionality: `${recursiveReferenceFunctionalityPrefix} ${reference}`,
|
|
209
221
|
message:
|
|
210
222
|
'Google schema conversion does not support recursive JSON Schema references.',
|
|
211
223
|
});
|
package/src/google-batch.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
EmptyResponseBodyError,
|
|
3
2
|
InvalidArgumentError,
|
|
4
3
|
InvalidResponseDataError,
|
|
5
4
|
type Experimental_BatchLanguageModelV4 as BatchLanguageModelV4,
|
|
@@ -14,11 +13,12 @@ import {
|
|
|
14
13
|
import {
|
|
15
14
|
combineHeaders,
|
|
16
15
|
convertAsyncIteratorToReadableStream,
|
|
16
|
+
createJsonLinesResponseHandler,
|
|
17
17
|
createJsonResponseHandler,
|
|
18
18
|
generateId,
|
|
19
19
|
getFromApi,
|
|
20
20
|
lazySchema,
|
|
21
|
-
|
|
21
|
+
normalizeBatchRequestCounts,
|
|
22
22
|
postJsonToApi,
|
|
23
23
|
postToApi,
|
|
24
24
|
resolve,
|
|
@@ -399,18 +399,20 @@ export class GoogleBatchLanguageModel
|
|
|
399
399
|
.map(segment => encodeURIComponent(segment))
|
|
400
400
|
.join('/');
|
|
401
401
|
|
|
402
|
-
const { value:
|
|
402
|
+
const { value: lines } = await getFromApi({
|
|
403
403
|
url: `${this.getBaseOrigin()}/download/v1beta/${encodedResponsesFile}:download?alt=media`,
|
|
404
404
|
headers: await this.getHeaders(options.headers),
|
|
405
405
|
failedResponseHandler: googleFailedResponseHandler,
|
|
406
|
-
successfulResponseHandler:
|
|
406
|
+
successfulResponseHandler: createJsonLinesResponseHandler(
|
|
407
|
+
googleBatchResultLineSchema,
|
|
408
|
+
),
|
|
407
409
|
abortSignal: options.abortSignal,
|
|
408
410
|
fetch: this.batchConfig.fetch,
|
|
409
411
|
validateUrl: false,
|
|
410
412
|
});
|
|
411
413
|
|
|
412
414
|
return convertAsyncIteratorToReadableStream(
|
|
413
|
-
this.iterateBatchResults(
|
|
415
|
+
this.iterateBatchResults(lines),
|
|
414
416
|
);
|
|
415
417
|
}
|
|
416
418
|
|
|
@@ -626,22 +628,12 @@ function convertGoogleRequestCounts(
|
|
|
626
628
|
const failed = parseCount(counts?.failedRequestCount ?? 0);
|
|
627
629
|
const pending = parseCount(counts?.pendingRequestCount ?? 0);
|
|
628
630
|
|
|
629
|
-
|
|
630
|
-
total == null ||
|
|
631
|
-
completed == null ||
|
|
632
|
-
failed == null ||
|
|
633
|
-
pending == null ||
|
|
634
|
-
completed + failed + pending !== total
|
|
635
|
-
) {
|
|
636
|
-
return undefined;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return {
|
|
631
|
+
return normalizeBatchRequestCounts({
|
|
640
632
|
total,
|
|
641
633
|
pending,
|
|
642
634
|
completed,
|
|
643
635
|
failed,
|
|
644
|
-
};
|
|
636
|
+
});
|
|
645
637
|
}
|
|
646
638
|
|
|
647
639
|
function parseCount(value: string | number | null | undefined) {
|
|
@@ -676,64 +668,3 @@ const googleUploadUrlResponseHandler: ResponseHandler<string> = async ({
|
|
|
676
668
|
|
|
677
669
|
return { value: uploadUrl };
|
|
678
670
|
};
|
|
679
|
-
|
|
680
|
-
const rawStreamResponseHandler: ResponseHandler<
|
|
681
|
-
ReadableStream<Uint8Array>
|
|
682
|
-
> = async ({ response }) => {
|
|
683
|
-
if (response.body == null) {
|
|
684
|
-
throw new EmptyResponseBodyError();
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
return { value: response.body };
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
async function* parseJsonLines(
|
|
691
|
-
stream: ReadableStream<Uint8Array>,
|
|
692
|
-
): AsyncGenerator<GoogleBatchResultLine> {
|
|
693
|
-
const reader = stream.getReader();
|
|
694
|
-
const decoder = new TextDecoder();
|
|
695
|
-
let buffer = '';
|
|
696
|
-
let finished = false;
|
|
697
|
-
|
|
698
|
-
try {
|
|
699
|
-
while (true) {
|
|
700
|
-
const { done, value } = await reader.read();
|
|
701
|
-
|
|
702
|
-
if (done) {
|
|
703
|
-
finished = true;
|
|
704
|
-
buffer += decoder.decode();
|
|
705
|
-
break;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
buffer += decoder.decode(value, { stream: true });
|
|
709
|
-
|
|
710
|
-
let lineEnd = buffer.indexOf('\n');
|
|
711
|
-
while (lineEnd !== -1) {
|
|
712
|
-
const line = buffer.slice(0, lineEnd).replace(/\r$/, '');
|
|
713
|
-
buffer = buffer.slice(lineEnd + 1);
|
|
714
|
-
|
|
715
|
-
if (line.trim().length > 0) {
|
|
716
|
-
yield await parseJSON({
|
|
717
|
-
text: line,
|
|
718
|
-
schema: googleBatchResultLineSchema,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
lineEnd = buffer.indexOf('\n');
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
const finalLine = buffer.replace(/\r$/, '');
|
|
727
|
-
if (finalLine.trim().length > 0) {
|
|
728
|
-
yield await parseJSON({
|
|
729
|
-
text: finalLine,
|
|
730
|
-
schema: googleBatchResultLineSchema,
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
} finally {
|
|
734
|
-
if (!finished) {
|
|
735
|
-
await reader.cancel().catch(() => {});
|
|
736
|
-
}
|
|
737
|
-
reader.releaseLock();
|
|
738
|
-
}
|
|
739
|
-
}
|
package/src/google-files.ts
CHANGED
|
@@ -34,6 +34,17 @@ interface GoogleFilesConfig {
|
|
|
34
34
|
fetch?: FetchFunction;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function encodePathSegment(value: string): string {
|
|
38
|
+
const encodedValue = encodeURIComponent(value);
|
|
39
|
+
|
|
40
|
+
// URL parsing normalizes both literal and percent-encoded dot segments.
|
|
41
|
+
return encodedValue === '.'
|
|
42
|
+
? '%252E'
|
|
43
|
+
: encodedValue === '..'
|
|
44
|
+
? '%252E%252E'
|
|
45
|
+
: encodedValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
export class GoogleFiles implements FilesV4 {
|
|
38
49
|
readonly specificationVersion = 'v4';
|
|
39
50
|
|
|
@@ -106,7 +117,7 @@ export class GoogleFiles implements FilesV4 {
|
|
|
106
117
|
'X-Goog-Upload-Offset': '0',
|
|
107
118
|
'X-Goog-Upload-Command': 'upload, finalize',
|
|
108
119
|
},
|
|
109
|
-
body: fileBytes,
|
|
120
|
+
body: ensureArrayBufferBacked(fileBytes),
|
|
110
121
|
});
|
|
111
122
|
|
|
112
123
|
if (!uploadResponse.ok) {
|
|
@@ -137,8 +148,14 @@ export class GoogleFiles implements FilesV4 {
|
|
|
137
148
|
|
|
138
149
|
await delay(pollIntervalMs);
|
|
139
150
|
|
|
151
|
+
const fileNameMatch = /^files\/([^/]+)$/.exec(file.name);
|
|
152
|
+
const filePath =
|
|
153
|
+
fileNameMatch != null
|
|
154
|
+
? `files/${encodePathSegment(fileNameMatch[1])}`
|
|
155
|
+
: encodePathSegment(file.name);
|
|
156
|
+
|
|
140
157
|
const { value: fileStatus } = await getFromApi({
|
|
141
|
-
url: `${this.config.baseURL}/${
|
|
158
|
+
url: `${this.config.baseURL}/${filePath}`,
|
|
142
159
|
validateUrl: false,
|
|
143
160
|
headers: combineHeaders(resolvedHeaders),
|
|
144
161
|
successfulResponseHandler: createJsonResponseHandler(
|
|
@@ -182,6 +199,14 @@ export class GoogleFiles implements FilesV4 {
|
|
|
182
199
|
}
|
|
183
200
|
}
|
|
184
201
|
|
|
202
|
+
function ensureArrayBufferBacked(data: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
203
|
+
if (data.buffer instanceof ArrayBuffer) {
|
|
204
|
+
return data as Uint8Array<ArrayBuffer>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return new Uint8Array(data);
|
|
208
|
+
}
|
|
209
|
+
|
|
185
210
|
type GoogleFileResource = {
|
|
186
211
|
name: string;
|
|
187
212
|
displayName?: string | null;
|
|
@@ -3,10 +3,25 @@ import {
|
|
|
3
3
|
type LanguageModelV4CallOptions,
|
|
4
4
|
type SharedV4Warning,
|
|
5
5
|
} from '@ai-sdk/provider';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
convertJSONSchemaToOpenAPISchema,
|
|
8
|
+
isRecursiveJSONSchemaReferenceError,
|
|
9
|
+
} from './convert-json-schema-to-openapi-schema';
|
|
7
10
|
import type { GoogleModelId } from './google-language-model-options';
|
|
8
11
|
import { getGoogleModelCapabilities } from './google-model-capabilities';
|
|
9
12
|
|
|
13
|
+
type FunctionTool = Extract<
|
|
14
|
+
NonNullable<LanguageModelV4CallOptions['tools']>[number],
|
|
15
|
+
{ type: 'function' }
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
type GoogleFunctionDeclaration = {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
parameters?: unknown;
|
|
22
|
+
parametersJsonSchema?: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
10
25
|
export function prepareTools({
|
|
11
26
|
tools,
|
|
12
27
|
toolChoice,
|
|
@@ -21,11 +36,7 @@ export function prepareTools({
|
|
|
21
36
|
tools:
|
|
22
37
|
| Array<
|
|
23
38
|
| {
|
|
24
|
-
functionDeclarations:
|
|
25
|
-
name: string;
|
|
26
|
-
description: string;
|
|
27
|
-
parameters: unknown;
|
|
28
|
-
}>;
|
|
39
|
+
functionDeclarations: GoogleFunctionDeclaration[];
|
|
29
40
|
}
|
|
30
41
|
| Record<string, any>
|
|
31
42
|
>
|
|
@@ -172,18 +183,10 @@ export function prepareTools({
|
|
|
172
183
|
});
|
|
173
184
|
|
|
174
185
|
if (hasFunctionTools && usesGemini3Features && googleTools.length > 0) {
|
|
175
|
-
const functionDeclarations:
|
|
176
|
-
name: string;
|
|
177
|
-
description: string;
|
|
178
|
-
parameters: unknown;
|
|
179
|
-
}> = [];
|
|
186
|
+
const functionDeclarations: GoogleFunctionDeclaration[] = [];
|
|
180
187
|
for (const tool of tools) {
|
|
181
188
|
if (tool.type === 'function') {
|
|
182
|
-
functionDeclarations.push(
|
|
183
|
-
name: tool.name,
|
|
184
|
-
description: tool.description ?? '',
|
|
185
|
-
parameters: convertJSONSchemaToOpenAPISchema(tool.inputSchema),
|
|
186
|
-
});
|
|
189
|
+
functionDeclarations.push(prepareFunctionDeclaration(tool));
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
|
|
@@ -238,11 +241,7 @@ export function prepareTools({
|
|
|
238
241
|
for (const tool of tools) {
|
|
239
242
|
switch (tool.type) {
|
|
240
243
|
case 'function':
|
|
241
|
-
functionDeclarations.push(
|
|
242
|
-
name: tool.name,
|
|
243
|
-
description: tool.description ?? '',
|
|
244
|
-
parameters: convertJSONSchemaToOpenAPISchema(tool.inputSchema),
|
|
245
|
-
});
|
|
244
|
+
functionDeclarations.push(prepareFunctionDeclaration(tool));
|
|
246
245
|
if (tool.strict === true) {
|
|
247
246
|
hasStrictTools = true;
|
|
248
247
|
}
|
|
@@ -314,3 +313,28 @@ export function prepareTools({
|
|
|
314
313
|
}
|
|
315
314
|
}
|
|
316
315
|
}
|
|
316
|
+
|
|
317
|
+
function prepareFunctionDeclaration(
|
|
318
|
+
tool: FunctionTool,
|
|
319
|
+
): GoogleFunctionDeclaration {
|
|
320
|
+
const declaration = {
|
|
321
|
+
name: tool.name,
|
|
322
|
+
description: tool.description ?? '',
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
return {
|
|
327
|
+
...declaration,
|
|
328
|
+
parameters: convertJSONSchemaToOpenAPISchema(tool.inputSchema),
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (!isRecursiveJSONSchemaReferenceError(error)) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
...declaration,
|
|
337
|
+
parametersJsonSchema: tool.inputSchema,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -6,7 +6,10 @@ import type {
|
|
|
6
6
|
SharedV4ProviderMetadata,
|
|
7
7
|
SharedV4Warning,
|
|
8
8
|
} from '@ai-sdk/provider';
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
createProviderStreamError,
|
|
11
|
+
type ParseResult,
|
|
12
|
+
} from '@ai-sdk/provider-utils';
|
|
10
13
|
import type {
|
|
11
14
|
GoogleInteractionsEvent,
|
|
12
15
|
GoogleInteractionsUsage,
|
|
@@ -821,10 +824,15 @@ export function buildGoogleInteractionsStreamTransform({
|
|
|
821
824
|
{ event_type: 'error' }
|
|
822
825
|
>;
|
|
823
826
|
finishStatus = 'failed';
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
827
|
+
controller.enqueue({
|
|
828
|
+
type: 'error',
|
|
829
|
+
error: createProviderStreamError({
|
|
830
|
+
message: event.error?.message ?? 'Unknown interaction error',
|
|
831
|
+
type: event.event_type,
|
|
832
|
+
code: event.error?.code ?? undefined,
|
|
833
|
+
data: event,
|
|
834
|
+
}),
|
|
835
|
+
});
|
|
828
836
|
break;
|
|
829
837
|
}
|
|
830
838
|
|