@ai-sdk/google 4.0.107 → 4.0.109

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.
@@ -97,7 +97,11 @@ You can use the following optional settings to customize the Google provider ins
97
97
  Use a different URL prefix for API calls, e.g. to use proxy servers.
98
98
  The default prefix is `https://generativelanguage.googleapis.com/v1beta`.
99
99
 
100
- - **apiKey** _string_
100
+ - **apiKey** _string | PromiseLike<string> | (() => string | PromiseLike<string>)_
101
+
102
+ Pass a function to resolve credentials before each request, including when keys
103
+ rotate. The function can return a string or a promise. Credential caching and
104
+ refresh are managed by the function.
101
105
 
102
106
  API key that is being sent using the `x-goog-api-key` header.
103
107
  It defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.
@@ -540,7 +544,14 @@ const result = await generateText({
540
544
  });
541
545
  ```
542
546
 
543
- You can also use YouTube URLs directly:
547
+ You can also use YouTube URLs directly. Pass `videoMetadata` through
548
+ `providerOptions.google` on a video file part to clip a segment or control the
549
+ frame rate. This also works with inline video data and uploaded file references.
550
+ The metadata supports these optional fields:
551
+
552
+ - `startOffset` (string): Start time as a duration in seconds, e.g. `'60s'` or `'3.5s'`.
553
+ - `endOffset` (string): End time in the same duration format.
554
+ - `fps` (number): Frames per second in the range `(0, 24]`. The default is `1`.
544
555
 
545
556
  ```ts
546
557
  import { google } from '@ai-sdk/google';
@@ -554,12 +565,21 @@ const result = await generateText({
554
565
  content: [
555
566
  {
556
567
  type: 'text',
557
- text: 'Summarize this video',
568
+ text: 'Summarize this part of the video',
558
569
  },
559
570
  {
560
571
  type: 'file',
561
572
  data: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
562
573
  mediaType: 'video/mp4',
574
+ providerOptions: {
575
+ google: {
576
+ videoMetadata: {
577
+ startOffset: '60s',
578
+ endOffset: '600s',
579
+ fps: 1,
580
+ },
581
+ },
582
+ },
563
583
  },
564
584
  ],
565
585
  },
@@ -1249,6 +1269,8 @@ will still validate the response against the schema in `Output.object`.
1249
1269
 
1250
1270
  ### Model Capabilities
1251
1271
 
1272
+ {/* model-capabilities:google-language:start */}
1273
+
1252
1274
  | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | Google Search | URL Context |
1253
1275
  | ------------------------------------- | ----------- | ----------------- | ---------- | -------------- | ------------- | ----------- |
1254
1276
  | `gemini-nano-banana-2.1` | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Check /> | <Cross /> |
@@ -1270,6 +1292,8 @@ will still validate the response against the schema in `Output.object`.
1270
1292
  | `gemini-2.5-flash-lite-preview-06-17` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
1271
1293
  | `gemini-2.0-flash` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
1272
1294
 
1295
+ {/* model-capabilities:google-language:end */}
1296
+
1273
1297
  <Note>
1274
1298
  The table above lists popular models. Please see the [Google Generative AI
1275
1299
  docs](https://ai.google.dev/gemini-api/docs/models/) for a full list of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "4.0.107",
3
+ "version": "4.0.109",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,8 +35,8 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@ai-sdk/provider": "4.0.34",
39
- "@ai-sdk/provider-utils": "5.0.70"
38
+ "@ai-sdk/provider": "4.0.35",
39
+ "@ai-sdk/provider-utils": "5.0.72"
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
  }
@@ -292,7 +292,6 @@ export class GoogleBatch implements BatchV4<GoogleBatchModelIds> {
292
292
  }
293
293
  }
294
294
 
295
- const headers = await this.getHeaders(options.headers);
296
295
  const createUrl = `${this.batchConfig.baseURL}/${getModelPath(
297
296
  modelId,
298
297
  )}:batchGenerateContent`;
@@ -300,7 +299,7 @@ export class GoogleBatch implements BatchV4<GoogleBatchModelIds> {
300
299
  if (fileParts == null) {
301
300
  const { value: operation } = await postJsonToApi({
302
301
  url: createUrl,
303
- headers,
302
+ headers: await this.getHeaders(options.headers),
304
303
  body: inlineBatchBody,
305
304
  failedResponseHandler: googleFailedResponseHandler,
306
305
  successfulResponseHandler: createJsonResponseHandler(
@@ -329,7 +328,7 @@ export class GoogleBatch implements BatchV4<GoogleBatchModelIds> {
329
328
 
330
329
  const { value: uploadUrl } = await postJsonToApi({
331
330
  url: `${this.getBaseOrigin()}/upload/v1beta/files`,
332
- headers: combineHeaders(headers, {
331
+ headers: combineHeaders(await this.getHeaders(options.headers), {
333
332
  'X-Goog-Upload-Protocol': 'resumable',
334
333
  'X-Goog-Upload-Command': 'start',
335
334
  'X-Goog-Upload-Header-Content-Length': String(inputFile.size),
@@ -370,7 +369,7 @@ export class GoogleBatch implements BatchV4<GoogleBatchModelIds> {
370
369
 
371
370
  const { value: operation } = await postJsonToApi({
372
371
  url: createUrl,
373
- headers,
372
+ headers: await this.getHeaders(options.headers),
374
373
  body: {
375
374
  batch: {
376
375
  displayName,
@@ -3,6 +3,7 @@ import {
3
3
  type EmbeddingModelV4,
4
4
  } from '@ai-sdk/provider';
5
5
  import {
6
+ type Resolvable,
6
7
  combineHeaders,
7
8
  createJsonResponseHandler,
8
9
  type EmbeddingModelProviderOptionsTransformer,
@@ -26,7 +27,7 @@ import {
26
27
  type GoogleEmbeddingConfig = {
27
28
  provider: string;
28
29
  baseURL: string;
29
- headers?: () => Record<string, string | undefined>;
30
+ headers?: Resolvable<Record<string, string | undefined>>;
30
31
  fetch?: FetchFunction;
31
32
  };
32
33
 
@@ -6,6 +6,8 @@ import {
6
6
  type SharedV4Warning,
7
7
  } from '@ai-sdk/provider';
8
8
  import {
9
+ resolve,
10
+ type Resolvable,
9
11
  combineHeaders,
10
12
  convertInlineFileDataToUint8Array,
11
13
  createJsonResponseHandler,
@@ -31,7 +33,7 @@ export type GoogleFilesUploadOptions = {
31
33
  interface GoogleFilesConfig {
32
34
  provider: string;
33
35
  baseURL: string;
34
- headers: () => Record<string, string | undefined>;
36
+ headers: Resolvable<Record<string, string | undefined>>;
35
37
  fetch?: FetchFunction;
36
38
  }
37
39
 
@@ -53,10 +55,8 @@ export class GoogleFiles implements FilesV4 {
53
55
  schema: googleFilesUploadOptionsSchema,
54
56
  })) as GoogleFilesUploadOptions | undefined;
55
57
 
56
- const resolvedHeaders = combineHeaders(
57
- this.config.headers(),
58
- options.headers,
59
- );
58
+ const getHeaders = async () =>
59
+ combineHeaders(await resolve(this.config.headers), options.headers);
60
60
  const fetchFn = this.config.fetch ?? globalThis.fetch;
61
61
 
62
62
  const warnings: Array<SharedV4Warning> = [];
@@ -74,7 +74,7 @@ export class GoogleFiles implements FilesV4 {
74
74
  const initResponse = await fetchFn(`${baseOrigin}/upload/v1beta/files`, {
75
75
  method: 'POST',
76
76
  headers: {
77
- ...resolvedHeaders,
77
+ ...(await getHeaders()),
78
78
  'X-Goog-Upload-Protocol': 'resumable',
79
79
  'X-Goog-Upload-Command': 'start',
80
80
  'X-Goog-Upload-Header-Content-Length': String(fileBytes.length),
@@ -152,7 +152,7 @@ export class GoogleFiles implements FilesV4 {
152
152
  const { value: fileStatus } = await getFromApi({
153
153
  url: `${this.config.baseURL}/${filePath}`,
154
154
  validateUrl: false,
155
- headers: combineHeaders(resolvedHeaders),
155
+ headers: await getHeaders(),
156
156
  successfulResponseHandler: createJsonResponseHandler(
157
157
  googleFileResponseSchema,
158
158
  ),
@@ -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
 
@@ -58,7 +58,7 @@ import {
58
58
  type GoogleLanguageModelOptions,
59
59
  type GoogleModelId,
60
60
  } from './google-language-model-options';
61
- import { getGoogleModelCapabilities } from './google-model-capabilities';
61
+ import { getGoogleLanguageModelEntry } from './google-model-catalog';
62
62
  import type { GoogleProviderMetadata } from './google-prompt';
63
63
  import { prepareTools } from './google-prepare-tools';
64
64
  import {
@@ -74,8 +74,6 @@ const configurableSafetySettingCategories = [
74
74
  'HARM_CATEGORY_SEXUALLY_EXPLICIT',
75
75
  ] as const;
76
76
 
77
- const gemini25ModelPattern = /(^|[/.])gemini-2\.5(?:[.-]|$)/i;
78
-
79
77
  const googleCloudStorageFunctionResponseUrls = {
80
78
  'image/png': [/^gs:\/\/.*$/],
81
79
  'image/jpeg': [/^gs:\/\/.*$/],
@@ -296,9 +294,12 @@ export class GoogleLanguageModel implements LanguageModelV4 {
296
294
  }
297
295
  }
298
296
 
299
- const isGemmaModel = modelId.toLowerCase().startsWith('gemma-');
297
+ const entry = getGoogleLanguageModelEntry(modelId);
298
+ const isGemmaModel =
299
+ entry.requestConfig.language.systemInstruction === 'user';
300
300
  const isGemini25DeveloperApiModel =
301
- !isVertexProvider && gemini25ModelPattern.test(modelId);
301
+ !isVertexProvider &&
302
+ entry.capabilities.language.sampling.penalties !== true;
302
303
 
303
304
  if (isGemini25DeveloperApiModel && frequencyPenalty != null) {
304
305
  warnings.push({
@@ -313,7 +314,8 @@ export class GoogleLanguageModel implements LanguageModelV4 {
313
314
  });
314
315
  }
315
316
 
316
- const { usesGemini3Features } = getGoogleModelCapabilities(modelId);
317
+ const usesGemini3Features =
318
+ entry.capabilities.language.tools.functionResponseParts;
317
319
  const supportedFunctionResponseUrls =
318
320
  usesGemini3Features &&
319
321
  config.downloadToolResultFiles?.supportsGoogleCloudStorageUrls
@@ -466,6 +468,8 @@ export class GoogleLanguageModel implements LanguageModelV4 {
466
468
  { isStreaming = false }: { isStreaming?: boolean } = {},
467
469
  ) {
468
470
  const { responseFormat, tools, toolChoice } = options;
471
+ const entry = getGoogleLanguageModelEntry(this.modelId);
472
+ const { structuredOutput } = entry.capabilities.language;
469
473
  let jsonResponseTool: LanguageModelV4FunctionTool | undefined;
470
474
  // Gemini models before Gemini 3 reject JSON response formatting together
471
475
  // with function tools. Gemini 3 supports the combination only with automatic
@@ -474,8 +478,8 @@ export class GoogleLanguageModel implements LanguageModelV4 {
474
478
  responseFormat?.type === 'json' &&
475
479
  responseFormat.schema != null &&
476
480
  tools?.some(tool => tool.type === 'function') &&
477
- /(^|\/)gemini-/i.test(this.modelId) &&
478
- (!getGoogleModelCapabilities(this.modelId).usesGemini3Features ||
481
+ entry.requestConfig.language.jsonResponseTool &&
482
+ (!structuredOutput.withFunctionTools ||
479
483
  toolChoice?.type === 'required' ||
480
484
  toolChoice?.type === 'tool')
481
485
  ) {
@@ -754,18 +758,16 @@ export class GoogleLanguageModel implements LanguageModelV4 {
754
758
  jsonResponseToolName,
755
759
  } = await this.getArgs(options);
756
760
 
757
- const mergedHeaders = combineHeaders(
758
- this.config.headers ? await resolve(this.config.headers) : undefined,
759
- options.headers,
760
- extraHeaders,
761
- );
762
-
763
- const generateContent = (body: typeof args) =>
761
+ const generateContent = async (body: typeof args) =>
764
762
  postJsonToApi({
765
763
  url: `${this.config.baseURL}/${getModelPath(
766
764
  this.modelId,
767
765
  )}:generateContent`,
768
- headers: mergedHeaders,
766
+ headers: combineHeaders(
767
+ await resolve(this.config.headers),
768
+ options.headers,
769
+ extraHeaders,
770
+ ),
769
771
  body,
770
772
  failedResponseHandler: googleFailedResponseHandler,
771
773
  successfulResponseHandler: createJsonResponseHandler(responseSchema),
@@ -875,18 +877,16 @@ export class GoogleLanguageModel implements LanguageModelV4 {
875
877
  providerOptionsNames.map(name => [name, payload]),
876
878
  ) as SharedV4ProviderMetadata;
877
879
 
878
- const headers = combineHeaders(
879
- this.config.headers ? await resolve(this.config.headers) : undefined,
880
- options.headers,
881
- extraHeaders,
882
- );
883
-
884
- const streamGenerateContent = (body: typeof args) =>
880
+ const streamGenerateContent = async (body: typeof args) =>
885
881
  postJsonToApi({
886
882
  url: `${this.config.baseURL}/${getModelPath(
887
883
  this.modelId,
888
884
  )}:streamGenerateContent?alt=sse`,
889
- headers,
885
+ headers: combineHeaders(
886
+ await resolve(this.config.headers),
887
+ options.headers,
888
+ extraHeaders,
889
+ ),
890
890
  body,
891
891
  failedResponseHandler: googleFailedResponseHandler,
892
892
  successfulResponseHandler:
@@ -1571,18 +1571,6 @@ export class GoogleLanguageModel implements LanguageModelV4 {
1571
1571
  }
1572
1572
  }
1573
1573
 
1574
- function getMaxOutputTokensForGemini25Model(): number {
1575
- return 65536;
1576
- }
1577
-
1578
- function getMaxThinkingTokensForGemini25Model(modelId: string): number {
1579
- const id = modelId.toLowerCase();
1580
- if (id.includes('2.5-pro') || id.includes('gemini-3-pro-image')) {
1581
- return 32768;
1582
- }
1583
- return 24576;
1584
- }
1585
-
1586
1574
  type GoogleThinkingConfig = NonNullable<
1587
1575
  InferSchema<typeof googleLanguageModelOptions>['thinkingConfig']
1588
1576
  >;
@@ -1601,8 +1589,8 @@ function resolveThinkingConfig({
1601
1589
  }
1602
1590
 
1603
1591
  if (
1604
- getGoogleModelCapabilities(modelId).usesGemini3Features &&
1605
- !modelId.includes('gemini-3-pro-image')
1592
+ getGoogleLanguageModelEntry(modelId).requestConfig.language
1593
+ .thinkingFormat === 'level'
1606
1594
  ) {
1607
1595
  return resolveGemini3ThinkingConfig({ reasoning, modelId, warnings });
1608
1596
  }
@@ -1622,22 +1610,21 @@ function resolveGemini3ThinkingConfig({
1622
1610
  modelId: string;
1623
1611
  warnings: SharedV4Warning[];
1624
1612
  }): Pick<GoogleThinkingConfig, 'thinkingLevel'> | undefined {
1625
- const minimumThinkingLevel = getMinimumThinkingLevelForGemini3Model(modelId);
1626
-
1627
- if (reasoning === 'none') {
1628
- // It's not possible to fully disable thinking with Gemini 3.
1629
- return { thinkingLevel: minimumThinkingLevel };
1630
- }
1613
+ const minimumThinkingLevel =
1614
+ getGoogleLanguageModelEntry(modelId).capabilities.language.reasoning
1615
+ .efforts?.[0];
1631
1616
 
1617
+ // It's not possible to fully disable thinking with Gemini 3, so reasoning
1618
+ // 'none' maps to the lowest supported thinking level.
1632
1619
  const thinkingLevel = mapReasoningToProviderEffort({
1633
1620
  reasoning,
1634
1621
  effortMap: {
1635
- minimal: minimumThinkingLevel,
1622
+ ...(minimumThinkingLevel === 'minimal' && {
1623
+ minimal: 'minimal' as const,
1624
+ }),
1636
1625
  low: 'low',
1637
1626
  medium: 'medium',
1638
1627
  high: 'high',
1639
- xhigh: 'high',
1640
- max: 'high',
1641
1628
  },
1642
1629
  warnings,
1643
1630
  });
@@ -1649,41 +1636,6 @@ function resolveGemini3ThinkingConfig({
1649
1636
  return { thinkingLevel };
1650
1637
  }
1651
1638
 
1652
- function getMinimumThinkingLevelForGemini3Model(
1653
- modelId: string,
1654
- ): 'minimal' | 'low' {
1655
- const modelName = /(?:^|[/.])(gemini-[^/]*)$/i
1656
- .exec(modelId)?.[1]
1657
- .toLowerCase();
1658
-
1659
- if (modelName === 'gemini-flash-latest') {
1660
- return 'low';
1661
- }
1662
-
1663
- const proVersionMatch = /^gemini-(\d+)(?:\.\d+)?-pro(?:$|-)/.exec(
1664
- modelName ?? '',
1665
- );
1666
-
1667
- if (Number(proVersionMatch?.[1]) >= 3) {
1668
- return 'low';
1669
- }
1670
-
1671
- const versionMatch = /^gemini-(\d+)\.(\d+)-flash(?:$|-(?!lite(?:-|$)))/.exec(
1672
- modelName ?? '',
1673
- );
1674
-
1675
- if (versionMatch == null) {
1676
- return 'minimal';
1677
- }
1678
-
1679
- const majorVersion = Number(versionMatch[1]);
1680
- const minorVersion = Number(versionMatch[2]);
1681
-
1682
- return majorVersion > 3 || (majorVersion === 3 && minorVersion >= 7)
1683
- ? 'low'
1684
- : 'minimal';
1685
- }
1686
-
1687
1639
  function resolveGemini25ThinkingConfig({
1688
1640
  reasoning,
1689
1641
  modelId,
@@ -1695,21 +1647,21 @@ function resolveGemini25ThinkingConfig({
1695
1647
  >;
1696
1648
  modelId: string;
1697
1649
  warnings: SharedV4Warning[];
1698
- }): Pick<GoogleThinkingConfig, 'thinkingBudget'> | undefined {
1699
- if (reasoning === 'none') {
1650
+ }): Pick<GoogleThinkingConfig, 'thinkingBudget'> {
1651
+ const entry = getGoogleLanguageModelEntry(modelId);
1652
+ const minThinkingBudget = entry.capabilities.language.reasoning.minBudget;
1653
+
1654
+ if (reasoning === 'none' && minThinkingBudget === 0) {
1700
1655
  return { thinkingBudget: 0 };
1701
1656
  }
1702
1657
 
1703
1658
  const thinkingBudget = mapReasoningToProviderBudget({
1704
1659
  reasoning,
1705
- maxOutputTokens: getMaxOutputTokensForGemini25Model(),
1706
- maxReasoningBudget: getMaxThinkingTokensForGemini25Model(modelId),
1707
- minReasoningBudget: 0,
1660
+ maxOutputTokens: entry.requestConfig.language.reasoningMaxOutputTokens,
1661
+ maxReasoningBudget: entry.capabilities.language.reasoning.maxBudget,
1662
+ minReasoningBudget: minThinkingBudget,
1708
1663
  warnings,
1709
1664
  });
1710
- if (thinkingBudget == null) {
1711
- return undefined;
1712
- }
1713
1665
  return { thinkingBudget };
1714
1666
  }
1715
1667
 
@@ -1,44 +1,11 @@
1
- type GoogleModelCapabilities = {
2
- supportsGemini2Tools: boolean;
3
- supportsFileSearch: boolean;
4
- usesGemini3Features: boolean;
5
- };
6
-
7
- const gemini1ModelPattern = /(^|[/.])gemini-1(?:[.-]|$)/i;
8
- const gemini2ModelPattern = /(^|[/.])gemini-2(?:[.-]|$)/i;
9
- const gemini25ModelPattern = /(^|[/.])gemini-2\.5(?:[.-]|$)/i;
10
- const geminiModelPattern = /(^|[/.])gemini-/i;
11
-
12
- function isKnownPreGemini2Model(modelId: string): boolean {
13
- return (
14
- gemini1ModelPattern.test(modelId) ||
15
- /(^|[/.])gemini-pro(?:-vision)?$/i.test(modelId) ||
16
- /(^|[/.])gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelId)
17
- );
18
- }
19
-
20
- /**
21
- * Classifies Gemini capabilities by excluding known older generations.
22
- *
23
- * Google model IDs are open-ended, so unrecognized Gemini IDs and aliases
24
- * intentionally inherit the newest supported behavior. Add exceptions here
25
- * only when a model is known to require a legacy request shape.
26
- */
27
- export function getGoogleModelCapabilities(
28
- modelId: string,
29
- ): GoogleModelCapabilities {
30
- const isGeminiModel = geminiModelPattern.test(modelId);
31
- const isGemini2Model = gemini2ModelPattern.test(modelId);
32
- const isKnownPreGemini2 = isKnownPreGemini2Model(modelId);
33
- const isKnownOlderModel = isKnownPreGemini2 || isGemini2Model;
34
- const usesGemini3Features = isGeminiModel && !isKnownOlderModel;
1
+ import { getGoogleLanguageModelEntry } from './google-model-catalog';
35
2
 
3
+ /** Compatibility projection of the resolved catalog for existing request code. */
4
+ export function getGoogleModelCapabilities(modelId: string) {
5
+ const { tools } = getGoogleLanguageModelEntry(modelId).capabilities.language;
36
6
  return {
37
- supportsGemini2Tools:
38
- (isGeminiModel && !isKnownPreGemini2) ||
39
- modelId.toLowerCase().includes('nano-banana'),
40
- supportsFileSearch:
41
- gemini25ModelPattern.test(modelId) || usesGemini3Features,
42
- usesGemini3Features,
7
+ supportsGemini2Tools: tools.gemini2,
8
+ supportsFileSearch: tools.fileSearch,
9
+ usesGemini3Features: tools.mixed,
43
10
  };
44
11
  }