@tanstack/ai-gemini 0.32.1 → 0.34.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.
- package/README.md +1 -0
- package/dist/esm/adapters/files.d.ts +27 -0
- package/dist/esm/adapters/files.js +62 -0
- package/dist/esm/adapters/files.js.map +1 -0
- package/dist/esm/adapters/image.d.ts +1 -0
- package/dist/esm/adapters/image.js +3 -2
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/text.d.ts +1 -0
- package/dist/esm/adapters/text.js +68 -5
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/adapters/video.d.ts +1 -0
- package/dist/esm/adapters/video.js +8 -5
- package/dist/esm/adapters/video.js.map +1 -1
- package/dist/esm/experimental/text-interactions/adapter.d.ts +1 -0
- package/dist/esm/experimental/text-interactions/adapter.js +11 -9
- package/dist/esm/experimental/text-interactions/adapter.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/message-types.d.ts +6 -1
- package/package.json +4 -3
- package/src/adapters/files.ts +84 -0
- package/src/adapters/image.ts +13 -3
- package/src/adapters/text.ts +105 -8
- package/src/adapters/video.ts +27 -5
- package/src/experimental/text-interactions/adapter.ts +18 -9
- package/src/index.ts +8 -0
- package/src/message-types.ts +7 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseFilesAdapter,
|
|
3
|
+
normalizeFileUploadInput,
|
|
4
|
+
} from '@tanstack/ai/adapters'
|
|
5
|
+
import { createGeminiClient, getGeminiApiKeyFromEnv } from '../utils/client'
|
|
6
|
+
import type { File as GeminiFile, GoogleGenAI } from '@google/genai'
|
|
7
|
+
import type { FileHandle, FileUploadInput } from '@tanstack/ai/adapters'
|
|
8
|
+
import type { GeminiClientConfig } from '../utils/client'
|
|
9
|
+
|
|
10
|
+
export interface GeminiFilesConfig extends GeminiClientConfig {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gemini Files adapter — uploads media to the Gemini Files API and references
|
|
14
|
+
* it by its file URI. Pair with `geminiText()` / `geminiImage()`: reference the
|
|
15
|
+
* returned handle via `fileSourceFromHandle(handle)`, which uses the handle URI
|
|
16
|
+
* (Gemini fetches it server-side as `fileData.fileUri`).
|
|
17
|
+
*/
|
|
18
|
+
export class GeminiFilesAdapter extends BaseFilesAdapter<'gemini'> {
|
|
19
|
+
readonly name = 'gemini' as const
|
|
20
|
+
private readonly client: GoogleGenAI
|
|
21
|
+
|
|
22
|
+
constructor(config: GeminiFilesConfig) {
|
|
23
|
+
super()
|
|
24
|
+
this.client = createGeminiClient(config)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async upload(input: FileUploadInput): Promise<FileHandle<'gemini'>> {
|
|
28
|
+
const { blob, mimeType } = normalizeFileUploadInput(input)
|
|
29
|
+
const file = await this.client.files.upload({
|
|
30
|
+
file: blob,
|
|
31
|
+
...(mimeType ? { config: { mimeType } } : {}),
|
|
32
|
+
})
|
|
33
|
+
return toFileHandle(file)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async get(id: string): Promise<FileHandle<'gemini'>> {
|
|
37
|
+
return toFileHandle(await this.client.files.get({ name: id }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async delete(id: string): Promise<void> {
|
|
41
|
+
await this.client.files.delete({ name: id })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toFileHandle(file: GeminiFile): FileHandle<'gemini'> {
|
|
46
|
+
// `name` (e.g. "files/abc-123") is the lifecycle id; `uri` is the URL Gemini
|
|
47
|
+
// fetches when the handle is referenced in a message.
|
|
48
|
+
if (!file.name) {
|
|
49
|
+
throw new Error('gemini: files.upload returned a file without a name')
|
|
50
|
+
}
|
|
51
|
+
const expiresAt = file.expirationTime
|
|
52
|
+
? Date.parse(file.expirationTime)
|
|
53
|
+
: undefined
|
|
54
|
+
return {
|
|
55
|
+
id: file.name,
|
|
56
|
+
provider: 'gemini',
|
|
57
|
+
...(file.uri ? { uri: file.uri } : {}),
|
|
58
|
+
...(file.mimeType ? { mimeType: file.mimeType } : {}),
|
|
59
|
+
...(file.sizeBytes ? { sizeBytes: Number(file.sizeBytes) } : {}),
|
|
60
|
+
...(expiresAt !== undefined && !Number.isNaN(expiresAt)
|
|
61
|
+
? { expiresAt }
|
|
62
|
+
: {}),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a Gemini Files adapter with an explicit API key.
|
|
68
|
+
*/
|
|
69
|
+
export function createGeminiFiles(
|
|
70
|
+
apiKey: string,
|
|
71
|
+
config?: Omit<GeminiFilesConfig, 'apiKey'>,
|
|
72
|
+
): GeminiFilesAdapter {
|
|
73
|
+
return new GeminiFilesAdapter({ ...config, apiKey })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a Gemini Files adapter, reading the API key from `GOOGLE_API_KEY` /
|
|
78
|
+
* `GEMINI_API_KEY`.
|
|
79
|
+
*/
|
|
80
|
+
export function geminiFiles(
|
|
81
|
+
config?: Omit<GeminiFilesConfig, 'apiKey'>,
|
|
82
|
+
): GeminiFilesAdapter {
|
|
83
|
+
return createGeminiFiles(getGeminiApiKeyFromEnv(), config)
|
|
84
|
+
}
|
package/src/adapters/image.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
fileReferenceFor,
|
|
3
|
+
isFileSource,
|
|
4
|
+
resolveMediaPrompt,
|
|
5
|
+
} from '@tanstack/ai'
|
|
2
6
|
import { BaseImageAdapter } from '@tanstack/ai/adapters'
|
|
3
7
|
import {
|
|
4
8
|
createGeminiClient,
|
|
@@ -75,6 +79,8 @@ export class GeminiImageAdapter<
|
|
|
75
79
|
> {
|
|
76
80
|
override readonly kind = 'image' as const
|
|
77
81
|
readonly name = 'gemini' as const
|
|
82
|
+
// Consumes Gemini Files API references (geminiFiles()) as fileData.fileUri.
|
|
83
|
+
override readonly supportsFileSources = true
|
|
78
84
|
|
|
79
85
|
// Type-only property - never assigned at runtime
|
|
80
86
|
declare '~types': {
|
|
@@ -270,10 +276,14 @@ export class GeminiImageAdapter<
|
|
|
270
276
|
// `fileData` and Gemini fetches them server-side — same as the chat
|
|
271
277
|
// adapter. Fetching locally and inlining as base64 double-buffers the
|
|
272
278
|
// image and OOMs on memory-constrained runtimes (e.g. Cloudflare
|
|
273
|
-
// Workers).
|
|
279
|
+
// Workers). A file source's handle is the file URI (throws when another
|
|
280
|
+
// provider issued it).
|
|
281
|
+
const fileUri = isFileSource(part.source)
|
|
282
|
+
? fileReferenceFor(part.source, this.name)
|
|
283
|
+
: part.source.value
|
|
274
284
|
return {
|
|
275
285
|
fileData: {
|
|
276
|
-
fileUri
|
|
286
|
+
fileUri,
|
|
277
287
|
mimeType: part.source.mimeType ?? 'image/jpeg',
|
|
278
288
|
},
|
|
279
289
|
}
|
package/src/adapters/text.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { FinishReason } from '@google/genai'
|
|
2
|
-
import {
|
|
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'
|
|
@@ -26,6 +31,7 @@ import type {
|
|
|
26
31
|
GenerateContentParameters,
|
|
27
32
|
GenerateContentResponse,
|
|
28
33
|
GoogleGenAI,
|
|
34
|
+
GroundingMetadata,
|
|
29
35
|
Part,
|
|
30
36
|
ThinkingLevel,
|
|
31
37
|
VideoMetadata,
|
|
@@ -35,6 +41,7 @@ import type {
|
|
|
35
41
|
Modality,
|
|
36
42
|
ModelMessage,
|
|
37
43
|
AdapterYieldChunk,
|
|
44
|
+
ProviderExecutedToolSource,
|
|
38
45
|
TextOptions,
|
|
39
46
|
} from '@tanstack/ai'
|
|
40
47
|
import type { ExternalTextProviderOptions } from '../text/text-provider-options'
|
|
@@ -56,6 +63,26 @@ const DEFAULT_MEDIA_MIME_TYPES = {
|
|
|
56
63
|
document: 'application/pdf',
|
|
57
64
|
} as const
|
|
58
65
|
|
|
66
|
+
function getGroundingSources(
|
|
67
|
+
metadata: GroundingMetadata,
|
|
68
|
+
): Array<ProviderExecutedToolSource> {
|
|
69
|
+
const sources = new Map<string, ProviderExecutedToolSource>()
|
|
70
|
+
for (const chunk of metadata.groundingChunks ?? []) {
|
|
71
|
+
const url = chunk.web?.uri
|
|
72
|
+
if (!url) continue
|
|
73
|
+
const existing = sources.get(url)
|
|
74
|
+
if (existing) {
|
|
75
|
+
if (!existing.title && chunk.web?.title) existing.title = chunk.web.title
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
sources.set(url, {
|
|
79
|
+
url,
|
|
80
|
+
...(chunk.web?.title ? { title: chunk.web.title } : {}),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
return [...sources.values()]
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
/**
|
|
60
87
|
* Content block shape for an Interactions API `input` step. The installed
|
|
61
88
|
* @google/genai types predate the video `processing` field, so we model the
|
|
@@ -107,8 +134,11 @@ function contentPartToInteraction(part: ContentPart): InteractionContent {
|
|
|
107
134
|
source.type === 'data'
|
|
108
135
|
? source.mimeType
|
|
109
136
|
: (source.mimeType ?? DEFAULT_MEDIA_MIME_TYPES[part.type])
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
// A Gemini Files API handle maps to the `uri` field, same as a public URL;
|
|
138
|
+
// `fileReferenceFor` throws when another provider issued it.
|
|
139
|
+
const base = isFileSource(source)
|
|
140
|
+
? { uri: fileReferenceFor(source, 'gemini'), mime_type: mimeType }
|
|
141
|
+
: source.type === 'data'
|
|
112
142
|
? { data: source.value, mime_type: mimeType }
|
|
113
143
|
: { uri: source.value, mime_type: mimeType }
|
|
114
144
|
|
|
@@ -216,6 +246,8 @@ export class GeminiTextAdapter<
|
|
|
216
246
|
> {
|
|
217
247
|
override readonly kind = 'text' as const
|
|
218
248
|
readonly name = 'gemini' as const
|
|
249
|
+
// Consumes Gemini Files API references (geminiFiles()) as fileData.fileUri.
|
|
250
|
+
override readonly supportsFileSources = true
|
|
219
251
|
|
|
220
252
|
private readonly client: GoogleGenAI
|
|
221
253
|
|
|
@@ -594,6 +626,51 @@ export class GeminiTextAdapter<
|
|
|
594
626
|
let hasEmittedRunStarted = false
|
|
595
627
|
let hasEmittedTextMessageStart = false
|
|
596
628
|
let hasEmittedStepStarted = false
|
|
629
|
+
let groundingMetadata: GroundingMetadata | undefined
|
|
630
|
+
let groundingCallEmitted = false
|
|
631
|
+
const adapterName = this.name
|
|
632
|
+
|
|
633
|
+
const emitGroundingToolCall = function* (): Generator<AdapterYieldChunk> {
|
|
634
|
+
if (!groundingMetadata || groundingCallEmitted) return
|
|
635
|
+
const sources = getGroundingSources(groundingMetadata)
|
|
636
|
+
const hasGoogleSearchEvidence =
|
|
637
|
+
sources.length > 0 ||
|
|
638
|
+
(groundingMetadata.webSearchQueries?.length ?? 0) > 0 ||
|
|
639
|
+
groundingMetadata.searchEntryPoint !== undefined
|
|
640
|
+
if (!hasGoogleSearchEvidence) return
|
|
641
|
+
|
|
642
|
+
groundingCallEmitted = true
|
|
643
|
+
const toolCallId = generateId(adapterName)
|
|
644
|
+
const metadata: GeminiToolCallMetadata = {
|
|
645
|
+
providerExecuted: true,
|
|
646
|
+
sources,
|
|
647
|
+
gemini: { groundingMetadata },
|
|
648
|
+
}
|
|
649
|
+
yield {
|
|
650
|
+
type: EventType.TOOL_CALL_START,
|
|
651
|
+
toolCallId,
|
|
652
|
+
toolCallName: 'google_search',
|
|
653
|
+
toolName: 'google_search',
|
|
654
|
+
parentMessageId: messageId,
|
|
655
|
+
model,
|
|
656
|
+
timestamp: Date.now(),
|
|
657
|
+
index: toolCallMap.size,
|
|
658
|
+
metadata,
|
|
659
|
+
}
|
|
660
|
+
yield {
|
|
661
|
+
type: EventType.TOOL_CALL_END,
|
|
662
|
+
toolCallId,
|
|
663
|
+
toolCallName: 'google_search',
|
|
664
|
+
toolName: 'google_search',
|
|
665
|
+
model,
|
|
666
|
+
timestamp: Date.now(),
|
|
667
|
+
input: {
|
|
668
|
+
...(groundingMetadata.webSearchQueries && {
|
|
669
|
+
queries: groundingMetadata.webSearchQueries,
|
|
670
|
+
}),
|
|
671
|
+
},
|
|
672
|
+
}
|
|
673
|
+
}
|
|
597
674
|
|
|
598
675
|
for await (const chunk of result) {
|
|
599
676
|
logger.provider(`provider=gemini`, { chunk })
|
|
@@ -610,6 +687,10 @@ export class GeminiTextAdapter<
|
|
|
610
687
|
}
|
|
611
688
|
}
|
|
612
689
|
|
|
690
|
+
if (chunk.candidates?.[0]?.groundingMetadata) {
|
|
691
|
+
groundingMetadata = chunk.candidates[0].groundingMetadata
|
|
692
|
+
}
|
|
693
|
+
|
|
613
694
|
if (chunk.candidates?.[0]?.content?.parts) {
|
|
614
695
|
const parts = chunk.candidates[0].content.parts
|
|
615
696
|
|
|
@@ -820,6 +901,7 @@ export class GeminiTextAdapter<
|
|
|
820
901
|
}
|
|
821
902
|
|
|
822
903
|
if (chunk.candidates?.[0]?.finishReason) {
|
|
904
|
+
yield* emitGroundingToolCall()
|
|
823
905
|
const finishReason = chunk.candidates[0].finishReason
|
|
824
906
|
|
|
825
907
|
// Emit TOOL_CALL_END for all tracked tool calls. functionCall parts on
|
|
@@ -910,6 +992,8 @@ export class GeminiTextAdapter<
|
|
|
910
992
|
}
|
|
911
993
|
}
|
|
912
994
|
}
|
|
995
|
+
|
|
996
|
+
yield* emitGroundingToolCall()
|
|
913
997
|
}
|
|
914
998
|
|
|
915
999
|
private convertContentPartToGemini(part: ContentPart): Part {
|
|
@@ -920,6 +1004,13 @@ export class GeminiTextAdapter<
|
|
|
920
1004
|
case 'audio':
|
|
921
1005
|
case 'video':
|
|
922
1006
|
case 'document': {
|
|
1007
|
+
// File references (Gemini Files API) and public URLs both pass
|
|
1008
|
+
// through as `fileData`; Gemini fetches the URI server-side. A
|
|
1009
|
+
// file source's handle is the file URI (throws when another provider
|
|
1010
|
+
// issued it).
|
|
1011
|
+
const fileUri = isFileSource(part.source)
|
|
1012
|
+
? fileReferenceFor(part.source, this.name)
|
|
1013
|
+
: part.source.value
|
|
923
1014
|
const geminiPart: Part =
|
|
924
1015
|
part.source.type === 'data'
|
|
925
1016
|
? {
|
|
@@ -930,7 +1021,7 @@ export class GeminiTextAdapter<
|
|
|
930
1021
|
}
|
|
931
1022
|
: {
|
|
932
1023
|
fileData: {
|
|
933
|
-
fileUri
|
|
1024
|
+
fileUri,
|
|
934
1025
|
// For URL sources, use provided mimeType or fall back to
|
|
935
1026
|
// reasonable defaults.
|
|
936
1027
|
mimeType:
|
|
@@ -994,6 +1085,11 @@ export class GeminiTextAdapter<
|
|
|
994
1085
|
|
|
995
1086
|
if (msg.role === 'assistant' && msg.toolCalls?.length) {
|
|
996
1087
|
for (const toolCall of msg.toolCalls) {
|
|
1088
|
+
const metadata = toolCall.metadata as
|
|
1089
|
+
| GeminiToolCallMetadata
|
|
1090
|
+
| undefined
|
|
1091
|
+
if (metadata?.providerExecuted) continue
|
|
1092
|
+
|
|
997
1093
|
let parsedArgs: Record<string, unknown> = {}
|
|
998
1094
|
try {
|
|
999
1095
|
parsedArgs = toolCall.function.arguments
|
|
@@ -1006,9 +1102,7 @@ export class GeminiTextAdapter<
|
|
|
1006
1102
|
parsedArgs = {}
|
|
1007
1103
|
}
|
|
1008
1104
|
|
|
1009
|
-
const thoughtSignature =
|
|
1010
|
-
toolCall.metadata as GeminiToolCallMetadata | undefined
|
|
1011
|
-
)?.thoughtSignature
|
|
1105
|
+
const thoughtSignature = metadata?.thoughtSignature
|
|
1012
1106
|
// Gemini requires thoughtSignature at the Part level (sibling of
|
|
1013
1107
|
// functionCall), not nested inside functionCall. Nesting it causes
|
|
1014
1108
|
// the API to reject the next turn with
|
|
@@ -1045,6 +1139,9 @@ export class GeminiTextAdapter<
|
|
|
1045
1139
|
},
|
|
1046
1140
|
})
|
|
1047
1141
|
} else {
|
|
1142
|
+
const fileUri = isFileSource(part.source)
|
|
1143
|
+
? fileReferenceFor(part.source, this.name)
|
|
1144
|
+
: part.source.value
|
|
1048
1145
|
const defaultMimeType = {
|
|
1049
1146
|
image: 'image/jpeg',
|
|
1050
1147
|
audio: 'audio/mp3',
|
|
@@ -1053,7 +1150,7 @@ export class GeminiTextAdapter<
|
|
|
1053
1150
|
}[part.type]
|
|
1054
1151
|
mediaParts.push({
|
|
1055
1152
|
fileData: {
|
|
1056
|
-
fileUri
|
|
1153
|
+
fileUri,
|
|
1057
1154
|
mimeType: part.source.mimeType ?? defaultMimeType,
|
|
1058
1155
|
},
|
|
1059
1156
|
})
|
package/src/adapters/video.ts
CHANGED
|
@@ -2,7 +2,12 @@ import {
|
|
|
2
2
|
GenerateVideosOperation,
|
|
3
3
|
VideoGenerationReferenceType,
|
|
4
4
|
} from '@google/genai'
|
|
5
|
-
import {
|
|
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:
|
|
150
|
-
: { type: 'image', uri:
|
|
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:
|
|
154
|
-
: { type: 'video', uri:
|
|
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:
|
|
924
|
-
: { type: 'image', uri:
|
|
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:
|
|
934
|
-
: { type: 'audio', uri:
|
|
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:
|
|
944
|
-
: { type: 'video', uri:
|
|
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:
|
|
954
|
-
: { type: 'document', uri:
|
|
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,
|
package/src/message-types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ProviderExecutedToolMetadata } from '@tanstack/ai'
|
|
2
|
+
import type { GroundingMetadata } from '@google/genai'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Gemini-specific metadata types for multimodal content parts.
|
|
3
6
|
* These types extend the base ContentPart metadata with Gemini-specific options.
|
|
@@ -178,6 +181,9 @@ export interface GeminiMessageMetadataByModality {
|
|
|
178
181
|
*
|
|
179
182
|
* @see https://ai.google.dev/gemini-api/docs/thinking
|
|
180
183
|
*/
|
|
181
|
-
export interface GeminiToolCallMetadata {
|
|
184
|
+
export interface GeminiToolCallMetadata extends ProviderExecutedToolMetadata {
|
|
182
185
|
thoughtSignature?: string
|
|
186
|
+
gemini?: {
|
|
187
|
+
groundingMetadata: GroundingMetadata
|
|
188
|
+
}
|
|
183
189
|
}
|