@tanstack/ai-gemini 0.29.4 → 0.30.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.
- package/README.md +42 -16
- package/dist/esm/adapters/text.d.ts +8 -0
- package/dist/esm/adapters/text.js +97 -1
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/experimental/text-interactions/adapter.d.ts +1 -0
- package/dist/esm/experimental/text-interactions/adapter.js +103 -3
- package/dist/esm/experimental/text-interactions/adapter.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/text.ts +130 -3
- package/src/experimental/text-interactions/adapter.ts +163 -5
package/src/adapters/text.ts
CHANGED
|
@@ -417,9 +417,7 @@ export class GeminiTextAdapter<
|
|
|
417
417
|
try {
|
|
418
418
|
parsed = JSON.parse(rawText)
|
|
419
419
|
} catch {
|
|
420
|
-
throw new Error(
|
|
421
|
-
`Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,
|
|
422
|
-
)
|
|
420
|
+
throw new Error(jsonContentParseError(rawText, 'structured output'))
|
|
423
421
|
}
|
|
424
422
|
|
|
425
423
|
return {
|
|
@@ -442,6 +440,113 @@ export class GeminiTextAdapter<
|
|
|
442
440
|
}
|
|
443
441
|
}
|
|
444
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Stream schema-constrained JSON from Gemini natively.
|
|
445
|
+
*
|
|
446
|
+
* `chat({ outputSchema, stream: true })` calls this when the adapter
|
|
447
|
+
* implements it. Without it, the engine buffers `structuredOutput()` and
|
|
448
|
+
* emits one synthetic delta.
|
|
449
|
+
*/
|
|
450
|
+
async *structuredOutputStream(
|
|
451
|
+
options: StructuredOutputOptions<GeminiTextProviderOptions>,
|
|
452
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
453
|
+
const { chatOptions: requestedChatOptions, outputSchema } = options
|
|
454
|
+
const chatOptions = {
|
|
455
|
+
...requestedChatOptions,
|
|
456
|
+
runId: requestedChatOptions.runId ?? generateId(this.name),
|
|
457
|
+
}
|
|
458
|
+
const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
chatOptions.logger.request(
|
|
462
|
+
`activity=structuredOutputStream provider=gemini model=${this.model} messages=${chatOptions.messages.length}`,
|
|
463
|
+
{ provider: 'gemini', model: this.model },
|
|
464
|
+
)
|
|
465
|
+
const result = await this.client.models.generateContentStream({
|
|
466
|
+
...mappedOptions,
|
|
467
|
+
config: {
|
|
468
|
+
...mappedOptions.config,
|
|
469
|
+
responseMimeType: 'application/json',
|
|
470
|
+
responseSchema: outputSchema,
|
|
471
|
+
},
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
let rawText = ''
|
|
475
|
+
let finished:
|
|
476
|
+
| Extract<AdapterYieldChunk, { type: typeof EventType.RUN_FINISHED }>
|
|
477
|
+
| undefined
|
|
478
|
+
let failed = false
|
|
479
|
+
for await (const chunk of this.processStreamChunks(
|
|
480
|
+
result,
|
|
481
|
+
chatOptions,
|
|
482
|
+
chatOptions.logger,
|
|
483
|
+
)) {
|
|
484
|
+
if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
|
|
485
|
+
rawText += chunk.delta
|
|
486
|
+
}
|
|
487
|
+
if (chunk.type === EventType.RUN_ERROR) failed = true
|
|
488
|
+
if (chunk.type === EventType.RUN_FINISHED) {
|
|
489
|
+
finished = chunk
|
|
490
|
+
} else {
|
|
491
|
+
yield chunk
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (failed) return
|
|
496
|
+
if (!finished) {
|
|
497
|
+
yield structuredStreamError(
|
|
498
|
+
chatOptions,
|
|
499
|
+
'Gemini structured-output stream ended without a terminal event',
|
|
500
|
+
'truncated-stream',
|
|
501
|
+
)
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
if (!rawText) {
|
|
505
|
+
yield structuredStreamError(
|
|
506
|
+
chatOptions,
|
|
507
|
+
'Gemini structured-output stream contained no content',
|
|
508
|
+
'empty-response',
|
|
509
|
+
)
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let object: unknown
|
|
514
|
+
try {
|
|
515
|
+
object = JSON.parse(rawText)
|
|
516
|
+
} catch {
|
|
517
|
+
yield structuredStreamError(
|
|
518
|
+
chatOptions,
|
|
519
|
+
jsonContentParseError(rawText, 'Gemini structured-output stream'),
|
|
520
|
+
'parse-error',
|
|
521
|
+
)
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
yield {
|
|
526
|
+
type: EventType.CUSTOM,
|
|
527
|
+
name: 'structured-output.complete',
|
|
528
|
+
value: { object, raw: rawText },
|
|
529
|
+
model: chatOptions.model,
|
|
530
|
+
timestamp: Date.now(),
|
|
531
|
+
}
|
|
532
|
+
yield { ...finished, timestamp: Date.now() }
|
|
533
|
+
} catch (error) {
|
|
534
|
+
const rawEvent = toRunErrorRawEvent(error)
|
|
535
|
+
const message =
|
|
536
|
+
error instanceof Error
|
|
537
|
+
? error.message
|
|
538
|
+
: 'An unknown error occurred during structured output streaming.'
|
|
539
|
+
chatOptions.logger.errors('gemini.structuredOutputStream fatal', {
|
|
540
|
+
error,
|
|
541
|
+
source: 'gemini.structuredOutputStream',
|
|
542
|
+
})
|
|
543
|
+
yield {
|
|
544
|
+
...structuredStreamError(chatOptions, message, 'provider-error'),
|
|
545
|
+
...(rawEvent !== undefined && { rawEvent }),
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
445
550
|
/**
|
|
446
551
|
* Extract text content from a non-streaming response
|
|
447
552
|
*/
|
|
@@ -1131,6 +1236,28 @@ export class GeminiTextAdapter<
|
|
|
1131
1236
|
}
|
|
1132
1237
|
}
|
|
1133
1238
|
|
|
1239
|
+
function jsonContentParseError(rawText: string, label: string) {
|
|
1240
|
+
const snippet = rawText.slice(0, 200)
|
|
1241
|
+
const ellipsis = rawText.length > 200 ? '...' : ''
|
|
1242
|
+
return `Failed to parse ${label} as JSON. Content: ${snippet}${ellipsis}`
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function structuredStreamError(
|
|
1246
|
+
options: TextOptions<GeminiTextProviderOptions>,
|
|
1247
|
+
message: string,
|
|
1248
|
+
code: string,
|
|
1249
|
+
): Extract<AdapterYieldChunk, { type: typeof EventType.RUN_ERROR }> {
|
|
1250
|
+
return {
|
|
1251
|
+
type: EventType.RUN_ERROR,
|
|
1252
|
+
runId: options.runId,
|
|
1253
|
+
model: options.model,
|
|
1254
|
+
timestamp: Date.now(),
|
|
1255
|
+
message,
|
|
1256
|
+
code,
|
|
1257
|
+
error: { message, code },
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1134
1261
|
/**
|
|
1135
1262
|
* Creates a Gemini text adapter with explicit API key.
|
|
1136
1263
|
* Type resolution happens here at the call site.
|
|
@@ -279,7 +279,7 @@ export class GeminiTextInteractionsAdapter<
|
|
|
279
279
|
const stream = (await this.client.interactions.create(
|
|
280
280
|
{ ...request, stream: true } as GeminiInteractionsRequestBody &
|
|
281
281
|
Parameters<typeof this.client.interactions.create>[0],
|
|
282
|
-
{ signal: options
|
|
282
|
+
{ signal: abortSignalFromOptions(options) },
|
|
283
283
|
)) as AsyncIterable<InteractionSSEEvent>
|
|
284
284
|
|
|
285
285
|
for await (const chunk of translateInteractionEvents(
|
|
@@ -427,7 +427,7 @@ export class GeminiTextInteractionsAdapter<
|
|
|
427
427
|
)
|
|
428
428
|
const result = (await this.client.interactions.create(
|
|
429
429
|
request as Parameters<typeof this.client.interactions.create>[0],
|
|
430
|
-
{ signal: chatOptions
|
|
430
|
+
{ signal: abortSignalFromOptions(chatOptions) },
|
|
431
431
|
)) as Interaction
|
|
432
432
|
|
|
433
433
|
const rawText = extractTextFromInteraction(result)
|
|
@@ -442,9 +442,7 @@ export class GeminiTextInteractionsAdapter<
|
|
|
442
442
|
try {
|
|
443
443
|
parsed = JSON.parse(rawText)
|
|
444
444
|
} catch {
|
|
445
|
-
throw new Error(
|
|
446
|
-
`Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,
|
|
447
|
-
)
|
|
445
|
+
throw new Error(jsonContentParseError(rawText, 'structured output'))
|
|
448
446
|
}
|
|
449
447
|
|
|
450
448
|
return { data: parsed, rawText }
|
|
@@ -463,6 +461,166 @@ export class GeminiTextInteractionsAdapter<
|
|
|
463
461
|
)
|
|
464
462
|
}
|
|
465
463
|
}
|
|
464
|
+
|
|
465
|
+
async *structuredOutputStream(
|
|
466
|
+
options: StructuredOutputOptions<GeminiTextInteractionsProviderOptions>,
|
|
467
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
468
|
+
const { chatOptions, outputSchema } = options
|
|
469
|
+
const runId = chatOptions.runId ?? generateId(this.name)
|
|
470
|
+
const threadId = chatOptions.threadId ?? generateId(this.name)
|
|
471
|
+
const timestamp = Date.now()
|
|
472
|
+
const effectivePreviousInteractionId =
|
|
473
|
+
chatOptions.modelOptions?.previous_interaction_id ??
|
|
474
|
+
this.interactionIdByThread.get(threadId)
|
|
475
|
+
const baseRequest = buildInteractionsRequest({
|
|
476
|
+
...chatOptions,
|
|
477
|
+
modelOptions: {
|
|
478
|
+
...chatOptions.modelOptions,
|
|
479
|
+
previous_interaction_id: effectivePreviousInteractionId,
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
const request: GeminiInteractionsRequestBody = {
|
|
483
|
+
...baseRequest,
|
|
484
|
+
stream: true,
|
|
485
|
+
response_format: {
|
|
486
|
+
type: 'text',
|
|
487
|
+
mime_type: 'application/json',
|
|
488
|
+
schema: outputSchema,
|
|
489
|
+
},
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
chatOptions.logger.request(
|
|
494
|
+
`activity=structuredOutputStream provider=gemini-text-interactions model=${this.model} messages=${chatOptions.messages.length}`,
|
|
495
|
+
{ provider: this.name, model: this.model, request },
|
|
496
|
+
)
|
|
497
|
+
const stream = (await this.client.interactions.create(
|
|
498
|
+
request as GeminiInteractionsRequestBody &
|
|
499
|
+
Parameters<typeof this.client.interactions.create>[0],
|
|
500
|
+
{ signal: abortSignalFromOptions(chatOptions) },
|
|
501
|
+
)) as AsyncIterable<InteractionSSEEvent>
|
|
502
|
+
|
|
503
|
+
let rawText = ''
|
|
504
|
+
let finished:
|
|
505
|
+
| Extract<AdapterYieldChunk, { type: typeof EventType.RUN_FINISHED }>
|
|
506
|
+
| undefined
|
|
507
|
+
let failed = false
|
|
508
|
+
for await (const chunk of translateInteractionEvents(
|
|
509
|
+
stream,
|
|
510
|
+
chatOptions.model,
|
|
511
|
+
runId,
|
|
512
|
+
threadId,
|
|
513
|
+
chatOptions.parentRunId,
|
|
514
|
+
timestamp,
|
|
515
|
+
this.name,
|
|
516
|
+
chatOptions.logger,
|
|
517
|
+
)) {
|
|
518
|
+
if (chunk.type === EventType.TEXT_MESSAGE_CONTENT)
|
|
519
|
+
rawText += chunk.delta
|
|
520
|
+
if (
|
|
521
|
+
chunk.type === EventType.CUSTOM &&
|
|
522
|
+
chunk.name === 'gemini.interactionId'
|
|
523
|
+
) {
|
|
524
|
+
const value =
|
|
525
|
+
chunk.value as GeminiInteractionsCustomEventValue<'gemini.interactionId'>
|
|
526
|
+
this.interactionIdByThread.set(threadId, value.interactionId)
|
|
527
|
+
}
|
|
528
|
+
if (chunk.type === EventType.RUN_ERROR) failed = true
|
|
529
|
+
if (chunk.type === EventType.RUN_FINISHED) finished = chunk
|
|
530
|
+
else yield chunk
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (failed) return
|
|
534
|
+
if (!finished) {
|
|
535
|
+
yield interactionsStructuredStreamError(
|
|
536
|
+
chatOptions,
|
|
537
|
+
runId,
|
|
538
|
+
'Gemini Interactions structured-output stream ended without a terminal event',
|
|
539
|
+
'truncated-stream',
|
|
540
|
+
)
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
if (!rawText) {
|
|
544
|
+
yield interactionsStructuredStreamError(
|
|
545
|
+
chatOptions,
|
|
546
|
+
runId,
|
|
547
|
+
'Gemini Interactions structured-output stream contained no content',
|
|
548
|
+
'empty-response',
|
|
549
|
+
)
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
let object: unknown
|
|
553
|
+
try {
|
|
554
|
+
object = JSON.parse(rawText)
|
|
555
|
+
} catch {
|
|
556
|
+
yield interactionsStructuredStreamError(
|
|
557
|
+
chatOptions,
|
|
558
|
+
runId,
|
|
559
|
+
jsonContentParseError(
|
|
560
|
+
rawText,
|
|
561
|
+
'Gemini Interactions structured-output stream',
|
|
562
|
+
),
|
|
563
|
+
'parse-error',
|
|
564
|
+
)
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
yield {
|
|
568
|
+
type: EventType.CUSTOM,
|
|
569
|
+
name: 'structured-output.complete',
|
|
570
|
+
value: { object, raw: rawText },
|
|
571
|
+
model: chatOptions.model,
|
|
572
|
+
timestamp: Date.now(),
|
|
573
|
+
}
|
|
574
|
+
yield { ...finished, timestamp: Date.now() }
|
|
575
|
+
} catch (error) {
|
|
576
|
+
const message =
|
|
577
|
+
error instanceof Error
|
|
578
|
+
? error.message
|
|
579
|
+
: 'An unknown error occurred during structured output streaming.'
|
|
580
|
+
chatOptions.logger.errors(
|
|
581
|
+
'gemini-text-interactions.structuredOutputStream fatal',
|
|
582
|
+
{ error, source: 'gemini-text-interactions.structuredOutputStream' },
|
|
583
|
+
)
|
|
584
|
+
yield interactionsStructuredStreamError(
|
|
585
|
+
chatOptions,
|
|
586
|
+
runId,
|
|
587
|
+
message,
|
|
588
|
+
'provider-error',
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function abortSignalFromOptions(
|
|
595
|
+
options: Pick<
|
|
596
|
+
TextOptions<GeminiTextInteractionsProviderOptions>,
|
|
597
|
+
'request' | 'abortController'
|
|
598
|
+
>,
|
|
599
|
+
) {
|
|
600
|
+
return options.request?.signal ?? options.abortController?.signal
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function jsonContentParseError(rawText: string, label: string) {
|
|
604
|
+
const snippet = rawText.slice(0, 200)
|
|
605
|
+
const ellipsis = rawText.length > 200 ? '...' : ''
|
|
606
|
+
return `Failed to parse ${label} as JSON. Content: ${snippet}${ellipsis}`
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function interactionsStructuredStreamError(
|
|
610
|
+
options: TextOptions<GeminiTextInteractionsProviderOptions>,
|
|
611
|
+
runId: string,
|
|
612
|
+
message: string,
|
|
613
|
+
code: string,
|
|
614
|
+
): Extract<AdapterYieldChunk, { type: typeof EventType.RUN_ERROR }> {
|
|
615
|
+
return {
|
|
616
|
+
type: EventType.RUN_ERROR,
|
|
617
|
+
runId,
|
|
618
|
+
model: options.model,
|
|
619
|
+
timestamp: Date.now(),
|
|
620
|
+
message,
|
|
621
|
+
code,
|
|
622
|
+
error: { message, code },
|
|
623
|
+
}
|
|
466
624
|
}
|
|
467
625
|
|
|
468
626
|
/** @experimental Interactions API is in Beta. */
|