@ai-sdk/google 4.0.106 → 4.0.108

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.
@@ -540,7 +540,14 @@ const result = await generateText({
540
540
  });
541
541
  ```
542
542
 
543
- You can also use YouTube URLs directly:
543
+ You can also use YouTube URLs directly. Pass `videoMetadata` through
544
+ `providerOptions.google` on a video file part to clip a segment or control the
545
+ frame rate. This also works with inline video data and uploaded file references.
546
+ The metadata supports these optional fields:
547
+
548
+ - `startOffset` (string): Start time as a duration in seconds, e.g. `'60s'` or `'3.5s'`.
549
+ - `endOffset` (string): End time in the same duration format.
550
+ - `fps` (number): Frames per second in the range `(0, 24]`. The default is `1`.
544
551
 
545
552
  ```ts
546
553
  import { google } from '@ai-sdk/google';
@@ -554,12 +561,21 @@ const result = await generateText({
554
561
  content: [
555
562
  {
556
563
  type: 'text',
557
- text: 'Summarize this video',
564
+ text: 'Summarize this part of the video',
558
565
  },
559
566
  {
560
567
  type: 'file',
561
568
  data: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
562
569
  mediaType: 'video/mp4',
570
+ providerOptions: {
571
+ google: {
572
+ videoMetadata: {
573
+ startOffset: '60s',
574
+ endOffset: '600s',
575
+ fps: 1,
576
+ },
577
+ },
578
+ },
563
579
  },
564
580
  ],
565
581
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "4.0.106",
3
+ "version": "4.0.108",
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.34",
39
- "@ai-sdk/provider-utils": "5.0.69"
39
+ "@ai-sdk/provider-utils": "5.0.71"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.4",
@@ -14,11 +14,12 @@ import {
14
14
  resolveProviderReference,
15
15
  secureJsonParse,
16
16
  } from '@ai-sdk/provider-utils';
17
- import type {
18
- GoogleContent,
19
- GoogleContentPart,
20
- GoogleFunctionResponsePart,
21
- GooglePrompt,
17
+ import {
18
+ googleVideoMetadataSchema,
19
+ type GoogleContent,
20
+ type GoogleContentPart,
21
+ type GoogleFunctionResponsePart,
22
+ type GooglePrompt,
22
23
  } from './google-prompt';
23
24
  import {
24
25
  codeExecutionInputSchema,
@@ -330,6 +331,12 @@ export function convertToGoogleMessages(
330
331
  }
331
332
 
332
333
  case 'file': {
334
+ const rawVideoMetadata = readProviderOpts(part)?.videoMetadata;
335
+ const videoMetadata =
336
+ rawVideoMetadata == null
337
+ ? undefined
338
+ : googleVideoMetadataSchema.parse(rawVideoMetadata);
339
+
333
340
  switch (part.data.type) {
334
341
  case 'url': {
335
342
  parts.push({
@@ -341,6 +348,7 @@ export function convertToGoogleMessages(
341
348
  ? part.data.originalUrl
342
349
  : part.data.url.toString(),
343
350
  },
351
+ ...(videoMetadata != null ? { videoMetadata } : {}),
344
352
  });
345
353
  break;
346
354
  }
@@ -359,6 +367,7 @@ export function convertToGoogleMessages(
359
367
  provider: 'google',
360
368
  }),
361
369
  },
370
+ ...(videoMetadata != null ? { videoMetadata } : {}),
362
371
  });
363
372
  break;
364
373
  }
@@ -372,6 +381,7 @@ export function convertToGoogleMessages(
372
381
  new TextEncoder().encode(part.data.text),
373
382
  ),
374
383
  },
384
+ ...(videoMetadata != null ? { videoMetadata } : {}),
375
385
  });
376
386
  break;
377
387
  }
@@ -381,6 +391,7 @@ export function convertToGoogleMessages(
381
391
  mimeType: resolveFullMediaType({ part }),
382
392
  data: convertToBase64(part.data.data),
383
393
  },
394
+ ...(videoMetadata != null ? { videoMetadata } : {}),
384
395
  });
385
396
  break;
386
397
  }
@@ -74,6 +74,9 @@ export class GoogleJSONAccumulator {
74
74
  (existingValue as string) + arg.stringValue,
75
75
  );
76
76
  delta += escaped;
77
+ if (arg.willContinue !== true) {
78
+ delta += this.closeOpenString();
79
+ }
77
80
  continue;
78
81
  }
79
82
 
@@ -81,8 +84,15 @@ export class GoogleJSONAccumulator {
81
84
  if (resolved == null) {
82
85
  // A value-less entry for a path without a value is an empty container
83
86
  // (an empty container has no leaves to stream). Value-less entries for
84
- // existing paths are string or array terminators and are ignored.
85
- if (existingValue !== undefined || arg.willContinue === true) continue;
87
+ // existing paths are string or array terminators. A string terminator
88
+ // closes the open string, an array terminator is ignored.
89
+ if (existingValue !== undefined) {
90
+ if (typeof existingValue === 'string' && arg.willContinue !== true) {
91
+ delta += this.closeOpenString();
92
+ }
93
+ continue;
94
+ }
95
+ if (arg.willContinue === true) continue;
86
96
 
87
97
  setNestedValue(
88
98
  this.accumulatedArgs,
@@ -115,6 +125,18 @@ export class GoogleJSONAccumulator {
115
125
  return { finalJSON: finalArgs, closingDelta };
116
126
  }
117
127
 
128
+ /**
129
+ * Input: stringOpen=true
130
+ * Output: '"' (and stringOpen=false), or '' when no string is open
131
+ */
132
+ private closeOpenString(): string {
133
+ if (!this.stringOpen) {
134
+ return '';
135
+ }
136
+ this.stringOpen = false;
137
+ return '"';
138
+ }
139
+
118
140
  /**
119
141
  * Input: pathStack=[] (first call) or pathStack=[root,...] (subsequent calls)
120
142
  * Output: '{' (first call) or '' (subsequent calls)
@@ -139,12 +161,7 @@ export class GoogleJSONAccumulator {
139
161
  arg: PartialArg,
140
162
  valueJson: string,
141
163
  ): string {
142
- let fragment = '';
143
-
144
- if (this.stringOpen) {
145
- fragment += '"';
146
- this.stringOpen = false;
147
- }
164
+ let fragment = this.closeOpenString();
148
165
 
149
166
  fragment += this.ensureRoot();
150
167
 
@@ -172,12 +189,7 @@ export class GoogleJSONAccumulator {
172
189
  targetSegments: PathSegment[],
173
190
  isArray: boolean,
174
191
  ): string {
175
- let fragment = '';
176
-
177
- if (this.stringOpen) {
178
- fragment += '"';
179
- this.stringOpen = false;
180
- }
192
+ let fragment = this.closeOpenString();
181
193
 
182
194
  fragment += this.ensureRoot();
183
195
 
@@ -1583,6 +1583,15 @@ function getMaxThinkingTokensForGemini25Model(modelId: string): number {
1583
1583
  return 24576;
1584
1584
  }
1585
1585
 
1586
+ /**
1587
+ * Gemini 2.5 Pro cannot disable thinking and requires a thinking budget of at
1588
+ * least 128 tokens. Other Gemini 2.5 models can disable thinking with a budget
1589
+ * of 0.
1590
+ */
1591
+ function getMinThinkingTokensForGemini25Model(modelId: string): number {
1592
+ return modelId.toLowerCase().includes('2.5-pro') ? 128 : 0;
1593
+ }
1594
+
1586
1595
  type GoogleThinkingConfig = NonNullable<
1587
1596
  InferSchema<typeof googleLanguageModelOptions>['thinkingConfig']
1588
1597
  >;
@@ -1624,20 +1633,17 @@ function resolveGemini3ThinkingConfig({
1624
1633
  }): Pick<GoogleThinkingConfig, 'thinkingLevel'> | undefined {
1625
1634
  const minimumThinkingLevel = getMinimumThinkingLevelForGemini3Model(modelId);
1626
1635
 
1627
- if (reasoning === 'none') {
1628
- // It's not possible to fully disable thinking with Gemini 3.
1629
- return { thinkingLevel: minimumThinkingLevel };
1630
- }
1631
-
1636
+ // It's not possible to fully disable thinking with Gemini 3, so reasoning
1637
+ // 'none' maps to the lowest supported thinking level.
1632
1638
  const thinkingLevel = mapReasoningToProviderEffort({
1633
1639
  reasoning,
1634
1640
  effortMap: {
1635
- minimal: minimumThinkingLevel,
1641
+ ...(minimumThinkingLevel === 'minimal' && {
1642
+ minimal: 'minimal' as const,
1643
+ }),
1636
1644
  low: 'low',
1637
1645
  medium: 'medium',
1638
1646
  high: 'high',
1639
- xhigh: 'high',
1640
- max: 'high',
1641
1647
  },
1642
1648
  warnings,
1643
1649
  });
@@ -1695,8 +1701,10 @@ function resolveGemini25ThinkingConfig({
1695
1701
  >;
1696
1702
  modelId: string;
1697
1703
  warnings: SharedV4Warning[];
1698
- }): Pick<GoogleThinkingConfig, 'thinkingBudget'> | undefined {
1699
- if (reasoning === 'none') {
1704
+ }): Pick<GoogleThinkingConfig, 'thinkingBudget'> {
1705
+ const minThinkingBudget = getMinThinkingTokensForGemini25Model(modelId);
1706
+
1707
+ if (reasoning === 'none' && minThinkingBudget === 0) {
1700
1708
  return { thinkingBudget: 0 };
1701
1709
  }
1702
1710
 
@@ -1704,12 +1712,9 @@ function resolveGemini25ThinkingConfig({
1704
1712
  reasoning,
1705
1713
  maxOutputTokens: getMaxOutputTokensForGemini25Model(),
1706
1714
  maxReasoningBudget: getMaxThinkingTokensForGemini25Model(modelId),
1707
- minReasoningBudget: 0,
1715
+ minReasoningBudget: minThinkingBudget,
1708
1716
  warnings,
1709
1717
  });
1710
- if (thinkingBudget == null) {
1711
- return undefined;
1712
- }
1713
1718
  return { thinkingBudget };
1714
1719
  }
1715
1720
 
@@ -1,3 +1,4 @@
1
+ import { z } from 'zod/v4';
1
2
  import type {
2
3
  GroundingMetadataSchema,
3
4
  PromptFeedbackSchema,
@@ -20,10 +21,23 @@ export type GoogleContent = {
20
21
  parts: Array<GoogleContentPart>;
21
22
  };
22
23
 
24
+ export const googleVideoMetadataSchema = z.object({
25
+ /** Start offset as a duration in seconds, e.g. `'3.5s'`. */
26
+ startOffset: z.string().optional(),
27
+ /** End offset as a duration in seconds, e.g. `'10s'`. */
28
+ endOffset: z.string().optional(),
29
+ /** Frames per second in the range (0, 24]. Defaults to 1. */
30
+ fps: z.number().gt(0).max(24).optional(),
31
+ });
32
+
33
+ /** Metadata for preprocessing video file parts. */
34
+ export type GoogleVideoMetadata = z.infer<typeof googleVideoMetadataSchema>;
35
+
23
36
  export type GoogleContentPart =
24
37
  | { text: string; thought?: boolean; thoughtSignature?: string }
25
38
  | {
26
39
  inlineData: { mimeType: string; data: string };
40
+ videoMetadata?: GoogleVideoMetadata;
27
41
  thought?: boolean;
28
42
  thoughtSignature?: string;
29
43
  }
@@ -41,6 +55,7 @@ export type GoogleContentPart =
41
55
  }
42
56
  | {
43
57
  fileData: { mimeType: string; fileUri: string };
58
+ videoMetadata?: GoogleVideoMetadata;
44
59
  thought?: boolean;
45
60
  thoughtSignature?: string;
46
61
  }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export type {
6
6
  } from './google-language-model-options';
7
7
  export type {
8
8
  GoogleProviderMetadata,
9
+ GoogleVideoMetadata,
9
10
  /** @deprecated Use `GoogleProviderMetadata` instead. */
10
11
  GoogleProviderMetadata as GoogleGenerativeAIProviderMetadata,
11
12
  } from './google-prompt';