@tanstack/ai-gemini 0.19.1 → 0.20.0

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.
Files changed (56) hide show
  1. package/dist/esm/adapters/audio.d.ts +1 -1
  2. package/dist/esm/adapters/audio.js.map +1 -1
  3. package/dist/esm/adapters/image.d.ts +1 -1
  4. package/dist/esm/adapters/image.js +17 -39
  5. package/dist/esm/adapters/image.js.map +1 -1
  6. package/dist/esm/adapters/summarize.d.ts +1 -1
  7. package/dist/esm/adapters/summarize.js.map +1 -1
  8. package/dist/esm/adapters/text.d.ts +1 -1
  9. package/dist/esm/adapters/text.js.map +1 -1
  10. package/dist/esm/adapters/tts.d.ts +1 -1
  11. package/dist/esm/adapters/tts.js.map +1 -1
  12. package/dist/esm/adapters/video.d.ts +60 -11
  13. package/dist/esm/adapters/video.js +205 -6
  14. package/dist/esm/adapters/video.js.map +1 -1
  15. package/dist/esm/experimental/text-interactions/adapter.d.ts +1 -1
  16. package/dist/esm/experimental/text-interactions/adapter.js.map +1 -1
  17. package/dist/esm/index.d.ts +6 -3
  18. package/dist/esm/index.js +9 -3
  19. package/dist/esm/index.js.map +1 -1
  20. package/dist/esm/model-meta.d.ts +11 -3
  21. package/dist/esm/model-meta.js +9 -1
  22. package/dist/esm/model-meta.js.map +1 -1
  23. package/dist/esm/realtime/adapter.d.ts +22 -0
  24. package/dist/esm/realtime/adapter.js +233 -0
  25. package/dist/esm/realtime/adapter.js.map +1 -0
  26. package/dist/esm/realtime/client.d.ts +98 -0
  27. package/dist/esm/realtime/client.js +389 -0
  28. package/dist/esm/realtime/client.js.map +1 -0
  29. package/dist/esm/realtime/index.d.ts +3 -0
  30. package/dist/esm/realtime/token.d.ts +26 -0
  31. package/dist/esm/realtime/token.js +39 -0
  32. package/dist/esm/realtime/token.js.map +1 -0
  33. package/dist/esm/realtime/types.d.ts +51 -0
  34. package/dist/esm/realtime/utils.d.ts +40 -0
  35. package/dist/esm/realtime/utils.js +350 -0
  36. package/dist/esm/realtime/utils.js.map +1 -0
  37. package/dist/esm/video/video-provider-options.d.ts +59 -14
  38. package/dist/esm/video/video-provider-options.js +15 -2
  39. package/dist/esm/video/video-provider-options.js.map +1 -1
  40. package/package.json +4 -4
  41. package/src/adapters/audio.ts +1 -1
  42. package/src/adapters/image.ts +25 -49
  43. package/src/adapters/summarize.ts +1 -1
  44. package/src/adapters/text.ts +1 -1
  45. package/src/adapters/tts.ts +1 -1
  46. package/src/adapters/video.ts +333 -16
  47. package/src/experimental/text-interactions/adapter.ts +2 -2
  48. package/src/index.ts +20 -2
  49. package/src/model-meta.ts +45 -2
  50. package/src/realtime/adapter.ts +311 -0
  51. package/src/realtime/client.ts +547 -0
  52. package/src/realtime/index.ts +14 -0
  53. package/src/realtime/token.ts +70 -0
  54. package/src/realtime/types.ts +94 -0
  55. package/src/realtime/utils.ts +439 -0
  56. package/src/video/video-provider-options.ts +95 -15
package/src/index.ts CHANGED
@@ -61,9 +61,9 @@ export {
61
61
  type GeminiAudioProviderOptions,
62
62
  } from './adapters/audio'
63
63
 
64
- // Video / Veo generation adapter (experimental)
64
+ // Video generation adapter — Veo + Gemini Omni Flash (experimental)
65
65
  /**
66
- * @experimental Veo video generation is an experimental feature and may change.
66
+ * @experimental Video generation is an experimental feature and may change.
67
67
  */
68
68
  export {
69
69
  GeminiVideoAdapter,
@@ -74,8 +74,11 @@ export {
74
74
  export {
75
75
  GEMINI_VIDEO_DURATIONS,
76
76
  getGeminiVideoDurationOptions,
77
+ isInteractionsVideoModel,
77
78
  } from './video/video-provider-options'
78
79
  export type {
80
+ GeminiInteractionsVideoModel,
81
+ GeminiOmniVideoProviderOptions,
79
82
  GeminiVideoModel,
80
83
  GeminiVideoModelDurationByName,
81
84
  GeminiVideoModelInputModalitiesByName,
@@ -96,6 +99,7 @@ export { GEMINI_TTS_MODELS as GeminiTTSModels } from './model-meta'
96
99
  export { GEMINI_TTS_VOICES as GeminiTTSVoices } from './model-meta'
97
100
  export { GEMINI_AUDIO_MODELS as GeminiAudioModels } from './model-meta'
98
101
  export { GEMINI_VIDEO_MODELS as GeminiVideoModels } from './model-meta'
102
+ export { GEMINI_INTERACTIONS_VIDEO_MODELS as GeminiInteractionsVideoModels } from './model-meta'
99
103
  export type { GeminiModels as GeminiTextModel } from './model-meta'
100
104
  export type { GeminiImageModels as GeminiImageModel } from './model-meta'
101
105
  export type { GeminiTTSVoice } from './model-meta'
@@ -125,3 +129,17 @@ export type {
125
129
 
126
130
  // Export provider usage types
127
131
  export type { GeminiProviderUsageDetails } from './usage'
132
+
133
+ // ============================================================================
134
+ // Realtime (Voice) Adapters
135
+ // ============================================================================
136
+
137
+ export { geminiRealtime, geminiRealtimeToken } from './realtime/index'
138
+
139
+ export type {
140
+ GeminiRealtimeModel,
141
+ GeminiRealtimeOptions,
142
+ GeminiRealtimeProviderOptions,
143
+ GeminiRealtimeTokenOptions,
144
+ GeminiRealtimeVoice,
145
+ } from './realtime/index'
package/src/model-meta.ts CHANGED
@@ -712,6 +712,37 @@ const VEO_3_1_LITE_PREVIEW = {
712
712
  GeminiCachedContentOptions
713
713
  >
714
714
 
715
+ /**
716
+ * Gemini Omni Flash — multimodal video generation with conversational
717
+ * editing. Serves only the Interactions API (`generateContent` rejects it),
718
+ * so it routes through the interactions-based path of the video adapter,
719
+ * not Veo's `:predictLongRunning` flow. Pricing is per second of generated
720
+ * video ($0.10/sec). 720p / 24 FPS, 3–10 second clips (default 10s).
721
+ * @experimental Omni video generation is an experimental feature and may change.
722
+ */
723
+ const GEMINI_OMNI_FLASH_PREVIEW = {
724
+ name: 'gemini-omni-flash-preview',
725
+ max_input_tokens: 1_048_576,
726
+ max_output_tokens: 1,
727
+ supports: {
728
+ input: ['text', 'image', 'video'],
729
+ output: ['video', 'audio'],
730
+ },
731
+ pricing: {
732
+ input: {
733
+ normal: 0,
734
+ },
735
+ output: {
736
+ normal: 0.1,
737
+ },
738
+ },
739
+ } as const satisfies ModelMeta<
740
+ GeminiToolConfigOptions &
741
+ GeminiSafetyOptions &
742
+ GeminiCommonConfigOptions &
743
+ GeminiCachedContentOptions
744
+ >
745
+
715
746
  const GEMINI_3_5_FLASH = {
716
747
  name: 'gemini-3.5-flash',
717
748
  max_input_tokens: 1_048_576,
@@ -845,13 +876,25 @@ export const GEMINI_TTS_VOICES = [
845
876
  export type GeminiTTSVoice = (typeof GEMINI_TTS_VOICES)[number]
846
877
 
847
878
  /**
848
- * Veo video generation models.
849
- * @experimental Veo video generation is an experimental feature and may change.
879
+ * Video generation models. Veo models run on the long-running
880
+ * `:predictLongRunning` flow; Gemini Omni Flash runs on the Interactions
881
+ * API — the video adapter routes by model.
882
+ * @experimental Video generation is an experimental feature and may change.
850
883
  */
851
884
  export const GEMINI_VIDEO_MODELS = [
852
885
  VEO_3_1_PREVIEW.name,
853
886
  VEO_3_1_FAST_PREVIEW.name,
854
887
  VEO_3_1_LITE_PREVIEW.name,
888
+ GEMINI_OMNI_FLASH_PREVIEW.name,
889
+ ] as const
890
+
891
+ /**
892
+ * Video models served by the Interactions API rather than Veo's
893
+ * `:predictLongRunning` operations flow.
894
+ * @experimental Omni video generation is an experimental feature and may change.
895
+ */
896
+ export const GEMINI_INTERACTIONS_VIDEO_MODELS = [
897
+ GEMINI_OMNI_FLASH_PREVIEW.name,
855
898
  ] as const
856
899
 
857
900
  // Manual type map for per-model provider options
@@ -0,0 +1,311 @@
1
+ import { createRealtimeEventEmitter } from '@tanstack/ai'
2
+ import { AudioPlayer, AudioStreamer, base64ToArrayBuffer } from './utils'
3
+ import { GeminiLiveClient } from './client'
4
+ import type { LiveResponse } from './client'
5
+ import type {
6
+ AnyClientTool,
7
+ AudioVisualization,
8
+ RealtimeAdapter,
9
+ RealtimeConnection,
10
+ RealtimeMessage,
11
+ RealtimeMode,
12
+ RealtimeToken,
13
+ } from '@tanstack/ai'
14
+ import type { GeminiRealtimeModel, GeminiRealtimeOptions } from './types'
15
+
16
+ /**
17
+ * Creates a Gemini realtime adapter for client-side use.
18
+ *
19
+ * @param options - Optional configuration
20
+ * @returns A RealtimeAdapter for use with RealtimeClient
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { RealtimeClient } from '@tanstack/ai-client'
25
+ * import { geminiRealtime } from '@tanstack/ai-gemini'
26
+ *
27
+ * const client = new RealtimeClient({
28
+ * getToken: () => fetch('/api/realtime-token').then(r => r.json()),
29
+ * adapter: geminiRealtime(),
30
+ * onGoAway: () => client.updateSession({ ... }) // Resume session with new config (available only for Gemini Live adapter)
31
+ * })
32
+ *
33
+ * ```
34
+ */
35
+ export function geminiRealtime(
36
+ options: GeminiRealtimeOptions = {},
37
+ ): RealtimeAdapter {
38
+ return {
39
+ provider: 'gemini',
40
+
41
+ connect(
42
+ token: RealtimeToken,
43
+ clientTools?: ReadonlyArray<AnyClientTool>,
44
+ ): Promise<RealtimeConnection> {
45
+ return createWebSocketConnection(token, options.model, clientTools)
46
+ },
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Creates a WebSocket connection to Gemini's realtime API
52
+ */
53
+ async function createWebSocketConnection(
54
+ token: RealtimeToken,
55
+ model: GeminiRealtimeModel = 'gemini-3.1-flash-live-preview',
56
+ tools?: ReadonlyArray<AnyClientTool>,
57
+ ): Promise<RealtimeConnection> {
58
+ const { emit, on: realtimeEventEmitterOn } = createRealtimeEventEmitter()
59
+
60
+ // Current state
61
+ let currentMode: RealtimeMode = 'idle'
62
+ let currentMessageId: string | null = null
63
+ let messageIdCounter = 0
64
+
65
+ function generateMessageId(): string {
66
+ return `gemini-msg-${Date.now()}-${++messageIdCounter}`
67
+ }
68
+
69
+ const client = new GeminiLiveClient(token.token, model, tools)
70
+
71
+ let message: RealtimeMessage = {
72
+ id: '',
73
+ role: 'assistant',
74
+ timestamp: 0,
75
+ parts: [],
76
+ }
77
+
78
+ let pendingAssistantResponse = ''
79
+
80
+ client.onClose = () => {
81
+ emit('status_change', { status: 'idle' })
82
+ emit('mode_change', { mode: 'idle' })
83
+ }
84
+
85
+ client.onError = (error) => {
86
+ emit('error', { error })
87
+ emit('status_change', { status: 'error' })
88
+ emit('mode_change', { mode: 'idle' })
89
+ }
90
+
91
+ client.onReceiveResponse = (response: LiveResponse) => {
92
+ switch (response.type) {
93
+ case 'text':
94
+ message.parts.push({
95
+ type: 'text',
96
+ content: response.data,
97
+ })
98
+ break
99
+ case 'audio': {
100
+ // Decode once, then reuse for both the stored message part and playback.
101
+ const pcm = base64ToArrayBuffer(response.data.audioData)
102
+ message.parts.push({
103
+ type: 'audio',
104
+ transcript: response.data.transcript,
105
+ audioData: pcm,
106
+ })
107
+ if (currentMode !== 'speaking') {
108
+ currentMode = 'speaking'
109
+ emit('mode_change', { mode: 'speaking' })
110
+ }
111
+ audioPlayer.play(pcm).catch((error) => emit('error', { error }))
112
+ break
113
+ }
114
+ case 'go_away':
115
+ emit('go_away', { timeLeft: response.data.timeLeft })
116
+ break
117
+ case 'usage_metadata':
118
+ emit('usage', {
119
+ completionTokens: response.data.responseTokenCount ?? 0,
120
+ promptTokens: response.data.promptTokenCount ?? 0,
121
+ totalTokens: response.data.totalTokenCount ?? 0,
122
+ })
123
+ break
124
+ case 'input_transcription':
125
+ if (response.data.finished && currentMode !== 'thinking') {
126
+ currentMode = 'thinking'
127
+ emit('mode_change', { mode: 'thinking' })
128
+ }
129
+ emit('transcript', {
130
+ isFinal: response.data.finished,
131
+ transcript: response.data.text,
132
+ role: 'user',
133
+ })
134
+ break
135
+ case 'output_transcription':
136
+ pendingAssistantResponse += response.data.text
137
+ emit('transcript', {
138
+ isFinal: response.data.finished,
139
+ transcript: pendingAssistantResponse,
140
+ role: 'assistant',
141
+ })
142
+ break
143
+ case 'interrupted':
144
+ audioPlayer.interrupt()
145
+ currentMode = 'listening'
146
+ emit('mode_change', { mode: 'listening' })
147
+ emit('interrupted', { messageId: currentMessageId ?? undefined })
148
+ break
149
+ case 'tool_call':
150
+ for (const tool of response.data.functionCalls || []) {
151
+ if (tool.id && tool.name) {
152
+ emit('tool_call', {
153
+ toolCallId: tool.id,
154
+ input: tool.args,
155
+ toolName: tool.name,
156
+ })
157
+ }
158
+ }
159
+ break
160
+ case 'turn_complete':
161
+ currentMessageId = generateMessageId()
162
+ message.id = currentMessageId
163
+ message.timestamp = Date.now()
164
+ message.parts.push({
165
+ type: 'text',
166
+ content: pendingAssistantResponse,
167
+ })
168
+ emit('message_complete', { message })
169
+
170
+ pendingAssistantResponse = ''
171
+ message = {
172
+ id: '',
173
+ role: 'assistant',
174
+ timestamp: 0,
175
+ parts: [],
176
+ }
177
+ currentMode = 'listening'
178
+ emit('mode_change', { mode: 'listening' })
179
+ break
180
+ case 'setup_complete':
181
+ emit('status_change', { status: 'connected' })
182
+ break
183
+ case 'error':
184
+ emit('error', {
185
+ error: new Error(response.data),
186
+ })
187
+ break
188
+ case 'thought':
189
+ case 'session_resumption_update':
190
+ // Handled internally by GeminiLiveClient; not surfaced to the client.
191
+ break
192
+ }
193
+ }
194
+
195
+ await client.connect()
196
+
197
+ const audioStreamer = new AudioStreamer(client)
198
+ const audioPlayer = new AudioPlayer()
199
+ try {
200
+ await audioPlayer.init()
201
+ await audioStreamer.start()
202
+ } catch (error) {
203
+ // Tear down the socket + audio graph if mic/worklet setup fails (e.g. the
204
+ // user denied microphone access) rather than leaking an open connection.
205
+ audioStreamer.stop()
206
+ audioPlayer.destroy()
207
+ client.disconnect()
208
+ throw error
209
+ }
210
+
211
+ const connection: RealtimeConnection = {
212
+ async disconnect() {
213
+ audioStreamer.stop()
214
+ audioPlayer.destroy()
215
+ client.disconnect()
216
+ currentMode = 'idle'
217
+ emit('status_change', { status: 'idle' })
218
+ },
219
+
220
+ async startAudioCapture() {
221
+ // Audio capture is established during connection setup
222
+ audioStreamer.startAudioCapture()
223
+ currentMode = 'listening'
224
+ emit('mode_change', { mode: 'listening' })
225
+ },
226
+
227
+ stopAudioCapture() {
228
+ audioStreamer.stopAudioCapture()
229
+ currentMode = 'idle'
230
+ emit('mode_change', { mode: 'idle' })
231
+ },
232
+
233
+ sendText(text: string) {
234
+ client.sendTextMessage(text)
235
+ currentMode = 'thinking'
236
+ emit('mode_change', { mode: 'thinking' })
237
+ },
238
+
239
+ sendImage(imageData: string, mimeType: string) {
240
+ client.sendImageMessage(imageData, mimeType)
241
+ currentMode = 'thinking'
242
+ emit('mode_change', { mode: 'thinking' })
243
+ },
244
+
245
+ sendToolResult(callId: string, result: string) {
246
+ client.sendToolResponse([
247
+ {
248
+ id: callId,
249
+ response: {
250
+ result,
251
+ },
252
+ },
253
+ ])
254
+ },
255
+
256
+ updateSession(config) {
257
+ client.updateSession(config).catch((error) => emit('error', { error }))
258
+ emit('status_change', { status: 'reconnecting' })
259
+ },
260
+
261
+ updateToken(newToken) {
262
+ client.updateToken(newToken)
263
+ emit('status_change', { status: 'reconnecting' })
264
+ },
265
+
266
+ interrupt() {
267
+ audioPlayer.interrupt()
268
+ currentMode = 'listening'
269
+ emit('mode_change', { mode: 'listening' })
270
+ emit('interrupted', { messageId: currentMessageId ?? undefined })
271
+ },
272
+ on: realtimeEventEmitterOn,
273
+ getAudioVisualization(): AudioVisualization {
274
+ return {
275
+ get inputLevel() {
276
+ return audioStreamer.inputLevel
277
+ },
278
+
279
+ get outputLevel() {
280
+ return audioPlayer.outputLevel
281
+ },
282
+
283
+ getInputFrequencyData() {
284
+ return audioStreamer.inputFrequencyData
285
+ },
286
+
287
+ getOutputFrequencyData() {
288
+ return audioPlayer.outputFrequencyData
289
+ },
290
+
291
+ getInputTimeDomainData() {
292
+ return audioStreamer.inputTimeDomainData
293
+ },
294
+
295
+ getOutputTimeDomainData() {
296
+ return audioPlayer.outputTimeDomainData
297
+ },
298
+
299
+ get inputSampleRate() {
300
+ return audioStreamer.inputSampleRate
301
+ },
302
+
303
+ get outputSampleRate() {
304
+ return audioPlayer.outputSampleRate
305
+ },
306
+ }
307
+ },
308
+ }
309
+
310
+ return connection
311
+ }