@tanstack/ai-gemini 0.32.0 → 0.33.1

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.
@@ -1,5 +1,10 @@
1
1
  import { FinishReason } from '@google/genai'
2
- import { EventType, normalizeSystemPrompts } from '@tanstack/ai'
2
+ import {
3
+ EventType,
4
+ fileReferenceFor,
5
+ isFileSource,
6
+ normalizeSystemPrompts,
7
+ } from '@tanstack/ai'
3
8
  import { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'
4
9
  import { BaseTextAdapter } from '@tanstack/ai/adapters'
5
10
  import { convertToolsToProviderFormat } from '../tools/tool-converter'
@@ -107,8 +112,11 @@ function contentPartToInteraction(part: ContentPart): InteractionContent {
107
112
  source.type === 'data'
108
113
  ? source.mimeType
109
114
  : (source.mimeType ?? DEFAULT_MEDIA_MIME_TYPES[part.type])
110
- const base =
111
- source.type === 'data'
115
+ // A Gemini Files API handle maps to the `uri` field, same as a public URL;
116
+ // `fileReferenceFor` throws when another provider issued it.
117
+ const base = isFileSource(source)
118
+ ? { uri: fileReferenceFor(source, 'gemini'), mime_type: mimeType }
119
+ : source.type === 'data'
112
120
  ? { data: source.value, mime_type: mimeType }
113
121
  : { uri: source.value, mime_type: mimeType }
114
122
 
@@ -216,6 +224,8 @@ export class GeminiTextAdapter<
216
224
  > {
217
225
  override readonly kind = 'text' as const
218
226
  readonly name = 'gemini' as const
227
+ // Consumes Gemini Files API references (geminiFiles()) as fileData.fileUri.
228
+ override readonly supportsFileSources = true
219
229
 
220
230
  private readonly client: GoogleGenAI
221
231
 
@@ -920,6 +930,13 @@ export class GeminiTextAdapter<
920
930
  case 'audio':
921
931
  case 'video':
922
932
  case 'document': {
933
+ // File references (Gemini Files API) and public URLs both pass
934
+ // through as `fileData`; Gemini fetches the URI server-side. A
935
+ // file source's handle is the file URI (throws when another provider
936
+ // issued it).
937
+ const fileUri = isFileSource(part.source)
938
+ ? fileReferenceFor(part.source, this.name)
939
+ : part.source.value
923
940
  const geminiPart: Part =
924
941
  part.source.type === 'data'
925
942
  ? {
@@ -930,7 +947,7 @@ export class GeminiTextAdapter<
930
947
  }
931
948
  : {
932
949
  fileData: {
933
- fileUri: part.source.value,
950
+ fileUri,
934
951
  // For URL sources, use provided mimeType or fall back to
935
952
  // reasonable defaults.
936
953
  mimeType:
@@ -1045,6 +1062,9 @@ export class GeminiTextAdapter<
1045
1062
  },
1046
1063
  })
1047
1064
  } else {
1065
+ const fileUri = isFileSource(part.source)
1066
+ ? fileReferenceFor(part.source, this.name)
1067
+ : part.source.value
1048
1068
  const defaultMimeType = {
1049
1069
  image: 'image/jpeg',
1050
1070
  audio: 'audio/mp3',
@@ -1053,7 +1073,7 @@ export class GeminiTextAdapter<
1053
1073
  }[part.type]
1054
1074
  mediaParts.push({
1055
1075
  fileData: {
1056
- fileUri: part.source.value,
1076
+ fileUri,
1057
1077
  mimeType: part.source.mimeType ?? defaultMimeType,
1058
1078
  },
1059
1079
  })
@@ -2,7 +2,12 @@ import {
2
2
  GenerateVideosOperation,
3
3
  VideoGenerationReferenceType,
4
4
  } from '@google/genai'
5
- import { resolveMediaPrompt } from '@tanstack/ai'
5
+ import {
6
+ fileReferenceFor,
7
+ isFileSource,
8
+ resolveMediaPrompt,
9
+ unsupportedFileSourceError,
10
+ } from '@tanstack/ai'
6
11
  import { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'
7
12
  import { arrayBufferToBase64 } from '@tanstack/ai-utils'
8
13
  import { createGeminiClient, getGeminiApiKeyFromEnv } from '../utils'
@@ -93,6 +98,14 @@ async function imagePartToVeoImage(
93
98
  mimeType: part.source.mimeType || 'image/png',
94
99
  }
95
100
  }
101
+ if (isFileSource(part.source)) {
102
+ // Veo's predict API accepts only inline bytes or a gs:// reference — there's
103
+ // no way to reference a Files API handle here.
104
+ throw unsupportedFileSourceError(
105
+ 'gemini',
106
+ 'for Veo video generation, which needs inline image bytes or a gs:// reference — pass a data: URI or gs:// URL',
107
+ )
108
+ }
96
109
  const url = part.source.value
97
110
  if (url.startsWith('gs://')) {
98
111
  return {
@@ -143,15 +156,21 @@ async function imagePartToVeoImage(
143
156
  function mediaPartToInteractionsContent(
144
157
  part: ImagePart<MediaInputMetadata> | VideoPart<MediaInputMetadata>,
145
158
  ): InteractionContent {
159
+ // A Gemini Files API reference maps to the `uri` field, same as a public
160
+ // URL (mirrors the Interactions text adapter). `fileReferenceFor` throws
161
+ // when another provider issued the handle.
162
+ const sourceValue = isFileSource(part.source)
163
+ ? fileReferenceFor(part.source, 'gemini')
164
+ : part.source.value
146
165
  const mimeType = part.source.mimeType
147
166
  if (part.type === 'image') {
148
167
  return part.source.type === 'data'
149
- ? { type: 'image', data: part.source.value, mime_type: mimeType }
150
- : { type: 'image', uri: part.source.value, mime_type: mimeType }
168
+ ? { type: 'image', data: sourceValue, mime_type: mimeType }
169
+ : { type: 'image', uri: sourceValue, mime_type: mimeType }
151
170
  }
152
171
  return part.source.type === 'data'
153
- ? { type: 'video', data: part.source.value, mime_type: mimeType }
154
- : { type: 'video', uri: part.source.value, mime_type: mimeType }
172
+ ? { type: 'video', data: sourceValue, mime_type: mimeType }
173
+ : { type: 'video', uri: sourceValue, mime_type: mimeType }
155
174
  }
156
175
 
157
176
  /**
@@ -255,6 +274,9 @@ export class GeminiVideoAdapter<
255
274
  GeminiVideoModelDurationByName
256
275
  > {
257
276
  readonly name = 'gemini' as const
277
+ // The Interactions path consumes Gemini Files API references as content
278
+ // `uri`s; the Veo path still rejects them (raw bytes / gs:// only).
279
+ override readonly supportsFileSources = true
258
280
 
259
281
  protected client: GoogleGenAI
260
282
  private readonly allowUrlFetch: boolean
@@ -1,4 +1,4 @@
1
- import { EventType } from '@tanstack/ai'
1
+ import { EventType, fileReferenceFor, isFileSource } from '@tanstack/ai'
2
2
  import { BaseTextAdapter } from '@tanstack/ai/adapters'
3
3
  import { parse as parsePartialJSON } from 'partial-json'
4
4
  import {
@@ -200,6 +200,8 @@ export class GeminiTextInteractionsAdapter<
200
200
  > {
201
201
  override readonly kind = 'text' as const
202
202
  override readonly name = 'gemini-text-interactions' as const
203
+ // Consumes Gemini Files API references (geminiFiles()) as content `uri`s.
204
+ override readonly supportsFileSources = true
203
205
 
204
206
  private readonly client: GoogleGenAI
205
207
  // Tracks the most recent server-assigned interaction id per threadId
@@ -911,6 +913,13 @@ function contentPartToBlock(part: ContentPart): ContentBlock {
911
913
  if (part.type === 'text') {
912
914
  return { type: 'text', text: part.content }
913
915
  }
916
+ // A Gemini Files API handle maps to the `uri` field (isData stays false),
917
+ // same as a public URL. `fileReferenceFor` checks the issuer against
918
+ // 'gemini', the name geminiFiles() stamps on its handles, not this
919
+ // adapter's own name.
920
+ const sourceValue = isFileSource(part.source)
921
+ ? fileReferenceFor(part.source, 'gemini')
922
+ : part.source.value
914
923
  const isData = part.source.type === 'data'
915
924
  switch (part.type) {
916
925
  case 'image': {
@@ -920,8 +929,8 @@ function contentPartToBlock(part: ContentPart): ContentBlock {
920
929
  'image',
921
930
  )
922
931
  return isData
923
- ? { type: 'image', data: part.source.value, mime_type }
924
- : { type: 'image', uri: part.source.value, mime_type }
932
+ ? { type: 'image', data: sourceValue, mime_type }
933
+ : { type: 'image', uri: sourceValue, mime_type }
925
934
  }
926
935
  case 'audio': {
927
936
  const mime_type = validateMime(
@@ -930,8 +939,8 @@ function contentPartToBlock(part: ContentPart): ContentBlock {
930
939
  'audio',
931
940
  )
932
941
  return isData
933
- ? { type: 'audio', data: part.source.value, mime_type }
934
- : { type: 'audio', uri: part.source.value, mime_type }
942
+ ? { type: 'audio', data: sourceValue, mime_type }
943
+ : { type: 'audio', uri: sourceValue, mime_type }
935
944
  }
936
945
  case 'video': {
937
946
  const mime_type = validateMime(
@@ -940,8 +949,8 @@ function contentPartToBlock(part: ContentPart): ContentBlock {
940
949
  'video',
941
950
  )
942
951
  return isData
943
- ? { type: 'video', data: part.source.value, mime_type }
944
- : { type: 'video', uri: part.source.value, mime_type }
952
+ ? { type: 'video', data: sourceValue, mime_type }
953
+ : { type: 'video', uri: sourceValue, mime_type }
945
954
  }
946
955
  case 'document': {
947
956
  const mime_type = validateMime(
@@ -950,8 +959,8 @@ function contentPartToBlock(part: ContentPart): ContentBlock {
950
959
  'document',
951
960
  )
952
961
  return isData
953
- ? { type: 'document', data: part.source.value, mime_type }
954
- : { type: 'document', uri: part.source.value, mime_type }
962
+ ? { type: 'document', data: sourceValue, mime_type }
963
+ : { type: 'document', uri: sourceValue, mime_type }
955
964
  }
956
965
  }
957
966
  }
package/src/index.ts CHANGED
@@ -19,6 +19,14 @@ export {
19
19
  type GeminiSummarizeModel,
20
20
  } from './adapters/summarize'
21
21
 
22
+ // Files adapter - upload media to the Gemini Files API and reference by file URI
23
+ export {
24
+ GeminiFilesAdapter,
25
+ createGeminiFiles,
26
+ geminiFiles,
27
+ type GeminiFilesConfig,
28
+ } from './adapters/files'
29
+
22
30
  // Image adapter
23
31
  export {
24
32
  GeminiImageAdapter,