@tanstack/ai-gemini 0.29.2 → 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 +99 -2
- 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 +3 -3
- package/src/adapters/text.ts +135 -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
|
*/
|
|
@@ -1109,6 +1214,11 @@ export class GeminiTextAdapter<
|
|
|
1109
1214
|
...(systemInstruction !== undefined && { systemInstruction }),
|
|
1110
1215
|
tools: convertToolsToProviderFormat(options.tools),
|
|
1111
1216
|
...(combinedSchemaConfig ?? {}),
|
|
1217
|
+
// Forward the caller's abort signal so cancellation reaches the SDK
|
|
1218
|
+
// request, matching the OpenAI-compatible adapters (issue #1374).
|
|
1219
|
+
...(options.request?.signal != null && {
|
|
1220
|
+
abortSignal: options.request.signal,
|
|
1221
|
+
}),
|
|
1112
1222
|
},
|
|
1113
1223
|
}
|
|
1114
1224
|
|
|
@@ -1126,6 +1236,28 @@ export class GeminiTextAdapter<
|
|
|
1126
1236
|
}
|
|
1127
1237
|
}
|
|
1128
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
|
+
|
|
1129
1261
|
/**
|
|
1130
1262
|
* Creates a Gemini text adapter with explicit API key.
|
|
1131
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. */
|