@tanstack/ai-gemini 0.10.3 → 0.10.5

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,4 +1,5 @@
1
1
  import { FinishReason } from '@google/genai'
2
+ import { EventType } from '@tanstack/ai'
2
3
  import { BaseTextAdapter } from '@tanstack/ai/adapters'
3
4
  import { convertToolsToProviderFormat } from '../tools/tool-converter'
4
5
  import {
@@ -39,11 +40,6 @@ import type {
39
40
  } from '../message-types'
40
41
  import type { GeminiClientConfig } from '../utils'
41
42
 
42
- /** Cast an event object to StreamChunk. Adapters construct events with string
43
- * literal types which are structurally compatible with the EventType enum. */
44
- const asChunk = (chunk: Record<string, unknown>) =>
45
- chunk as unknown as StreamChunk
46
-
47
43
  /**
48
44
  * Configuration for Gemini text adapter
49
45
  */
@@ -136,15 +132,14 @@ export class GeminiTextAdapter<
136
132
 
137
133
  yield* this.processStreamChunks(result, options, logger)
138
134
  } catch (error) {
139
- const timestamp = Date.now()
140
135
  logger.errors('gemini.chatStream fatal', {
141
136
  error,
142
137
  source: 'gemini.chatStream',
143
138
  })
144
- yield asChunk({
145
- type: 'RUN_ERROR',
139
+ yield {
140
+ type: EventType.RUN_ERROR,
146
141
  model: options.model,
147
- timestamp,
142
+ timestamp: Date.now(),
148
143
  message:
149
144
  error instanceof Error
150
145
  ? error.message
@@ -155,7 +150,7 @@ export class GeminiTextAdapter<
155
150
  ? error.message
156
151
  : 'An unknown error occurred during the chat stream.',
157
152
  },
158
- })
153
+ }
159
154
  }
160
155
  }
161
156
 
@@ -240,7 +235,6 @@ export class GeminiTextAdapter<
240
235
  logger: InternalLogger,
241
236
  ): AsyncIterable<StreamChunk> {
242
237
  const model = options.model
243
- const timestamp = Date.now()
244
238
  let accumulatedContent = ''
245
239
  let accumulatedThinking = ''
246
240
  const toolCallMap = new Map<
@@ -271,13 +265,14 @@ export class GeminiTextAdapter<
271
265
  // Emit RUN_STARTED on first chunk
272
266
  if (!hasEmittedRunStarted) {
273
267
  hasEmittedRunStarted = true
274
- yield asChunk({
275
- type: 'RUN_STARTED',
268
+ yield {
269
+ type: EventType.RUN_STARTED,
276
270
  runId,
277
271
  threadId,
278
272
  model,
279
- timestamp,
280
- })
273
+ timestamp: Date.now(),
274
+ parentRunId: options.parentRunId,
275
+ }
281
276
  }
282
277
 
283
278
  if (chunk.candidates?.[0]?.content?.parts) {
@@ -293,92 +288,92 @@ export class GeminiTextAdapter<
293
288
  reasoningMessageId = generateId(this.name)
294
289
 
295
290
  // Spec REASONING events
296
- yield asChunk({
297
- type: 'REASONING_START',
291
+ yield {
292
+ type: EventType.REASONING_START,
298
293
  messageId: reasoningMessageId,
299
294
  model,
300
- timestamp,
301
- })
302
- yield asChunk({
303
- type: 'REASONING_MESSAGE_START',
295
+ timestamp: Date.now(),
296
+ }
297
+ yield {
298
+ type: EventType.REASONING_MESSAGE_START,
304
299
  messageId: reasoningMessageId,
305
300
  role: 'reasoning' as const,
306
301
  model,
307
- timestamp,
308
- })
302
+ timestamp: Date.now(),
303
+ }
309
304
 
310
305
  // Legacy STEP events (kept during transition)
311
- yield asChunk({
312
- type: 'STEP_STARTED',
306
+ yield {
307
+ type: EventType.STEP_STARTED,
313
308
  stepName: stepId,
314
309
  stepId,
315
310
  model,
316
- timestamp,
311
+ timestamp: Date.now(),
317
312
  stepType: 'thinking',
318
- })
313
+ }
319
314
  }
320
315
 
321
316
  accumulatedThinking += part.text
322
317
 
323
318
  // Spec REASONING content event
324
- yield asChunk({
325
- type: 'REASONING_MESSAGE_CONTENT',
319
+ yield {
320
+ type: EventType.REASONING_MESSAGE_CONTENT,
326
321
  messageId: reasoningMessageId!,
327
322
  delta: part.text,
328
323
  model,
329
- timestamp,
330
- })
324
+ timestamp: Date.now(),
325
+ }
331
326
 
332
327
  // Legacy STEP event
333
- yield asChunk({
334
- type: 'STEP_FINISHED',
328
+ yield {
329
+ type: EventType.STEP_FINISHED,
335
330
  stepName: stepId || generateId(this.name),
336
331
  stepId: stepId || generateId(this.name),
337
332
  model,
338
- timestamp,
333
+ timestamp: Date.now(),
339
334
  delta: part.text,
340
335
  content: accumulatedThinking,
341
- })
336
+ }
342
337
  } else if (part.text.trim()) {
343
338
  // Close reasoning before text starts
344
339
  if (reasoningMessageId && !hasClosedReasoning) {
345
340
  hasClosedReasoning = true
346
- yield asChunk({
347
- type: 'REASONING_MESSAGE_END',
341
+ yield {
342
+ type: EventType.REASONING_MESSAGE_END,
348
343
  messageId: reasoningMessageId,
349
344
  model,
350
- timestamp,
351
- })
352
- yield asChunk({
353
- type: 'REASONING_END',
345
+ timestamp: Date.now(),
346
+ }
347
+ yield {
348
+ type: EventType.REASONING_END,
354
349
  messageId: reasoningMessageId,
355
350
  model,
356
- timestamp,
357
- })
351
+ timestamp: Date.now(),
352
+ }
358
353
  }
359
354
 
360
355
  // Skip whitespace-only text parts (e.g. "\n" during auto-continuation)
361
356
  // Emit TEXT_MESSAGE_START on first text content
362
357
  if (!hasEmittedTextMessageStart) {
363
358
  hasEmittedTextMessageStart = true
364
- yield asChunk({
365
- type: 'TEXT_MESSAGE_START',
359
+ yield {
360
+ type: EventType.TEXT_MESSAGE_START,
366
361
  messageId,
367
362
  model,
368
- timestamp,
363
+ timestamp: Date.now(),
369
364
  role: 'assistant',
370
- })
365
+ }
371
366
  }
372
367
 
373
368
  accumulatedContent += part.text
374
- yield asChunk({
375
- type: 'TEXT_MESSAGE_CONTENT',
369
+ yield {
370
+ type: EventType.TEXT_MESSAGE_CONTENT,
376
371
  messageId,
377
372
  model,
378
- timestamp,
373
+ timestamp: Date.now(),
379
374
  delta: part.text,
380
375
  content: accumulatedContent,
381
- })
376
+ }
382
377
  }
383
378
  }
384
379
 
@@ -430,31 +425,31 @@ export class GeminiTextAdapter<
430
425
  // Emit TOOL_CALL_START if not already started
431
426
  if (!toolCallData.started) {
432
427
  toolCallData.started = true
433
- yield asChunk({
434
- type: 'TOOL_CALL_START',
428
+ yield {
429
+ type: EventType.TOOL_CALL_START,
435
430
  toolCallId,
436
431
  toolCallName: toolCallData.name,
437
432
  toolName: toolCallData.name,
438
433
  model,
439
- timestamp,
434
+ timestamp: Date.now(),
440
435
  index: toolCallData.index,
441
436
  ...(toolCallData.thoughtSignature && {
442
437
  metadata: {
443
438
  thoughtSignature: toolCallData.thoughtSignature,
444
439
  } satisfies GeminiToolCallMetadata,
445
440
  }),
446
- })
441
+ }
447
442
  }
448
443
 
449
444
  // Emit TOOL_CALL_ARGS
450
- yield asChunk({
451
- type: 'TOOL_CALL_ARGS',
445
+ yield {
446
+ type: EventType.TOOL_CALL_ARGS,
452
447
  toolCallId,
453
448
  model,
454
- timestamp,
449
+ timestamp: Date.now(),
455
450
  delta: toolCallData.args,
456
451
  args: toolCallData.args,
457
- })
452
+ }
458
453
  }
459
454
  }
460
455
  } else if (chunk.data && chunk.data.trim()) {
@@ -462,24 +457,24 @@ export class GeminiTextAdapter<
462
457
  // Emit TEXT_MESSAGE_START on first text content
463
458
  if (!hasEmittedTextMessageStart) {
464
459
  hasEmittedTextMessageStart = true
465
- yield asChunk({
466
- type: 'TEXT_MESSAGE_START',
460
+ yield {
461
+ type: EventType.TEXT_MESSAGE_START,
467
462
  messageId,
468
463
  model,
469
- timestamp,
464
+ timestamp: Date.now(),
470
465
  role: 'assistant',
471
- })
466
+ }
472
467
  }
473
468
 
474
469
  accumulatedContent += chunk.data
475
- yield asChunk({
476
- type: 'TEXT_MESSAGE_CONTENT',
470
+ yield {
471
+ type: EventType.TEXT_MESSAGE_CONTENT,
477
472
  messageId,
478
473
  model,
479
- timestamp,
474
+ timestamp: Date.now(),
480
475
  delta: chunk.data,
481
476
  content: accumulatedContent,
482
- })
477
+ }
483
478
  }
484
479
 
485
480
  if (chunk.candidates?.[0]?.finishReason) {
@@ -508,15 +503,15 @@ export class GeminiTextAdapter<
508
503
  })
509
504
 
510
505
  // Emit TOOL_CALL_START
511
- yield asChunk({
512
- type: 'TOOL_CALL_START',
506
+ yield {
507
+ type: EventType.TOOL_CALL_START,
513
508
  toolCallId,
514
509
  toolCallName: functionCall.name || '',
515
510
  toolName: functionCall.name || '',
516
511
  model,
517
- timestamp,
512
+ timestamp: Date.now(),
518
513
  index: nextToolIndex - 1,
519
- })
514
+ }
520
515
 
521
516
  // Emit TOOL_CALL_END with parsed input
522
517
  let parsedInput: unknown = {}
@@ -531,15 +526,15 @@ export class GeminiTextAdapter<
531
526
  parsedInput = {}
532
527
  }
533
528
 
534
- yield asChunk({
535
- type: 'TOOL_CALL_END',
529
+ yield {
530
+ type: EventType.TOOL_CALL_END,
536
531
  toolCallId,
537
532
  toolCallName: functionCall.name || '',
538
533
  toolName: functionCall.name || '',
539
534
  model,
540
- timestamp,
535
+ timestamp: Date.now(),
541
536
  input: parsedInput,
542
- })
537
+ }
543
538
  }
544
539
  }
545
540
  }
@@ -555,15 +550,15 @@ export class GeminiTextAdapter<
555
550
  parsedInput = {}
556
551
  }
557
552
 
558
- yield asChunk({
559
- type: 'TOOL_CALL_END',
553
+ yield {
554
+ type: EventType.TOOL_CALL_END,
560
555
  toolCallId,
561
556
  toolCallName: toolCallData.name,
562
557
  toolName: toolCallData.name,
563
558
  model,
564
- timestamp,
559
+ timestamp: Date.now(),
565
560
  input: parsedInput,
566
- })
561
+ }
567
562
  }
568
563
 
569
564
  // Reset so a new TEXT_MESSAGE_START is emitted if text follows tool calls
@@ -572,11 +567,11 @@ export class GeminiTextAdapter<
572
567
  }
573
568
 
574
569
  if (finishReason === FinishReason.MAX_TOKENS) {
575
- yield asChunk({
576
- type: 'RUN_ERROR',
570
+ yield {
571
+ type: EventType.RUN_ERROR,
577
572
  runId,
578
573
  model,
579
- timestamp,
574
+ timestamp: Date.now(),
580
575
  message:
581
576
  'The response was cut off because the maximum token limit was reached.',
582
577
  code: 'max_tokens',
@@ -585,42 +580,42 @@ export class GeminiTextAdapter<
585
580
  'The response was cut off because the maximum token limit was reached.',
586
581
  code: 'max_tokens',
587
582
  },
588
- })
583
+ }
589
584
  }
590
585
 
591
586
  // Close reasoning events if still open
592
587
  if (reasoningMessageId && !hasClosedReasoning) {
593
588
  hasClosedReasoning = true
594
- yield asChunk({
595
- type: 'REASONING_MESSAGE_END',
589
+ yield {
590
+ type: EventType.REASONING_MESSAGE_END,
596
591
  messageId: reasoningMessageId,
597
592
  model,
598
- timestamp,
599
- })
600
- yield asChunk({
601
- type: 'REASONING_END',
593
+ timestamp: Date.now(),
594
+ }
595
+ yield {
596
+ type: EventType.REASONING_END,
602
597
  messageId: reasoningMessageId,
603
598
  model,
604
- timestamp,
605
- })
599
+ timestamp: Date.now(),
600
+ }
606
601
  }
607
602
 
608
603
  // Emit TEXT_MESSAGE_END if we had text content
609
604
  if (hasEmittedTextMessageStart) {
610
- yield asChunk({
611
- type: 'TEXT_MESSAGE_END',
605
+ yield {
606
+ type: EventType.TEXT_MESSAGE_END,
612
607
  messageId,
613
608
  model,
614
- timestamp,
615
- })
609
+ timestamp: Date.now(),
610
+ }
616
611
  }
617
612
 
618
- yield asChunk({
619
- type: 'RUN_FINISHED',
613
+ yield {
614
+ type: EventType.RUN_FINISHED,
620
615
  runId,
621
616
  threadId,
622
617
  model,
623
- timestamp,
618
+ timestamp: Date.now(),
624
619
  finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',
625
620
  usage: chunk.usageMetadata
626
621
  ? {
@@ -629,7 +624,7 @@ export class GeminiTextAdapter<
629
624
  totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,
630
625
  }
631
626
  : undefined,
632
- })
627
+ }
633
628
  }
634
629
  }
635
630
  }
package/src/index.ts CHANGED
@@ -11,15 +11,12 @@ export {
11
11
  type GeminiTextProviderOptions,
12
12
  } from './adapters/text'
13
13
 
14
- // Summarize adapter
14
+ // Summarize - thin factory functions over @tanstack/ai's ChatStreamSummarizeAdapter
15
15
  export {
16
- GeminiSummarizeAdapter,
17
- GeminiSummarizeModels,
18
16
  createGeminiSummarize,
19
17
  geminiSummarize,
20
- type GeminiSummarizeAdapterOptions,
18
+ type GeminiSummarizeConfig,
21
19
  type GeminiSummarizeModel,
22
- type GeminiSummarizeProviderOptions,
23
20
  } from './adapters/summarize'
24
21
 
25
22
  // Image adapter