@ai-sdk/google 4.0.111 → 4.0.112
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/CHANGELOG.md +28 -0
- package/dist/index.d.ts +17 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +176 -22
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +75 -17
- package/dist/internal/index.js.map +1 -1
- package/docs/15-google.mdx +9 -3
- package/package.json +3 -3
- package/src/convert-to-google-messages.ts +2 -2
- package/src/google-language-model.ts +197 -145
- package/src/google-speech-model.ts +9 -12
- package/src/google-video-model.ts +1 -1
- package/src/realtime/google-realtime-event-mapper.ts +123 -0
- package/src/realtime/google-realtime-model.ts +58 -0
package/docs/15-google.mdx
CHANGED
|
@@ -1358,6 +1358,8 @@ When the serialized batch creation body is under 20 MB, the provider sends the r
|
|
|
1358
1358
|
|
|
1359
1359
|
## Realtime Models
|
|
1360
1360
|
|
|
1361
|
+
For a trusted [server-owned connection](/docs/ai-sdk-core/realtime#server-owned-connections), `getServerWebSocketConfig()` uses the unconstrained Gemini Live endpoint with resolved API-key headers. Use `createServerEventParser()` once per socket. Completion-frame usage is attached to `response-done`; independently delivered usage uses `custom` events with `rawType: 'usageMetadata'` and typed `usage`, without inferring a response association. Gross totals never substitute for missing modality counts.
|
|
1362
|
+
|
|
1361
1363
|
<Note type="warning">Realtime is an experimental feature.</Note>
|
|
1362
1364
|
|
|
1363
1365
|
You can create models that call the [Gemini Live API](https://ai.google.dev/gemini-api/docs/live)
|
|
@@ -2514,6 +2516,11 @@ the provider passes those bytes through unchanged. Older models return raw PCM,
|
|
|
2514
2516
|
which the provider wraps in a WAV container. Write `result.audio.uint8Array`
|
|
2515
2517
|
directly to disk without adding another WAV header.
|
|
2516
2518
|
|
|
2519
|
+
Set `language` to a supported BCP-47 language code, such as `en-US` or `de-DE`.
|
|
2520
|
+
The provider forwards explicit codes unchanged as `speechConfig.languageCode`
|
|
2521
|
+
for both single- and multi-speaker requests. Omit `language` or set it to `'auto'`
|
|
2522
|
+
to let Gemini detect the language from the input text.
|
|
2523
|
+
|
|
2517
2524
|
Gemini 3.8 supports the following `outputFormat` values:
|
|
2518
2525
|
|
|
2519
2526
|
| Format | Values | Result |
|
|
@@ -2614,9 +2621,8 @@ Older models continue to use labelled transcripts, such as
|
|
|
2614
2621
|
`multiSpeakerVoiceConfig` and without `turns`.
|
|
2615
2622
|
|
|
2616
2623
|
<Note>
|
|
2617
|
-
Gemini TTS models do not support the `speed`
|
|
2618
|
-
|
|
2619
|
-
from the input text.
|
|
2624
|
+
Gemini TTS models do not support the `speed` option; passing it adds a warning
|
|
2625
|
+
to `result.warnings`.
|
|
2620
2626
|
</Note>
|
|
2621
2627
|
|
|
2622
2628
|
### Model Capabilities
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/google",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.112",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ai-sdk/provider": "4.0.
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
38
|
+
"@ai-sdk/provider": "4.0.36",
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.75"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@ai-sdk/test-server": "2.0.4",
|
|
@@ -466,7 +466,7 @@ export function convertToGoogleMessages(
|
|
|
466
466
|
|
|
467
467
|
switch (part.type) {
|
|
468
468
|
case 'text': {
|
|
469
|
-
return part.text.length === 0
|
|
469
|
+
return part.text.length === 0 && thoughtSignature == null
|
|
470
470
|
? undefined
|
|
471
471
|
: {
|
|
472
472
|
text: part.text,
|
|
@@ -475,7 +475,7 @@ export function convertToGoogleMessages(
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
case 'reasoning': {
|
|
478
|
-
return part.text.length === 0
|
|
478
|
+
return part.text.length === 0 && thoughtSignature == null
|
|
479
479
|
? undefined
|
|
480
480
|
: {
|
|
481
481
|
text: part.text,
|
|
@@ -978,6 +978,19 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
978
978
|
return;
|
|
979
979
|
}
|
|
980
980
|
|
|
981
|
+
// Finalized partial-argument calls also separate surrounding blocks.
|
|
982
|
+
if (currentTextBlockId !== null) {
|
|
983
|
+
controller.enqueue({ type: 'text-end', id: currentTextBlockId });
|
|
984
|
+
currentTextBlockId = null;
|
|
985
|
+
}
|
|
986
|
+
if (currentReasoningBlockId !== null) {
|
|
987
|
+
controller.enqueue({
|
|
988
|
+
type: 'reasoning-end',
|
|
989
|
+
id: currentReasoningBlockId,
|
|
990
|
+
});
|
|
991
|
+
currentReasoningBlockId = null;
|
|
992
|
+
}
|
|
993
|
+
|
|
981
994
|
const { finalJSON, closingDelta } = active.accumulator.finalize();
|
|
982
995
|
|
|
983
996
|
if (closingDelta.length > 0) {
|
|
@@ -1182,16 +1195,40 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
1182
1195
|
: undefined;
|
|
1183
1196
|
|
|
1184
1197
|
if (part.text.length === 0) {
|
|
1185
|
-
if (
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1198
|
+
if (thoughtSignatureMetadata != null) {
|
|
1199
|
+
if (currentTextBlockId !== null) {
|
|
1200
|
+
controller.enqueue({
|
|
1201
|
+
type: 'text-delta',
|
|
1202
|
+
id: currentTextBlockId,
|
|
1203
|
+
delta: '',
|
|
1204
|
+
providerMetadata: thoughtSignatureMetadata,
|
|
1205
|
+
});
|
|
1206
|
+
} else {
|
|
1207
|
+
if (currentReasoningBlockId !== null) {
|
|
1208
|
+
controller.enqueue({
|
|
1209
|
+
type: 'reasoning-end',
|
|
1210
|
+
id: currentReasoningBlockId,
|
|
1211
|
+
});
|
|
1212
|
+
currentReasoningBlockId = null;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Keep the signature in position without merging later content.
|
|
1216
|
+
const id = String(blockCounter++);
|
|
1217
|
+
const type =
|
|
1218
|
+
part.thought === true ? 'reasoning' : 'text';
|
|
1219
|
+
controller.enqueue({
|
|
1220
|
+
type: `${type}-start`,
|
|
1221
|
+
id,
|
|
1222
|
+
providerMetadata: thoughtSignatureMetadata,
|
|
1223
|
+
});
|
|
1224
|
+
controller.enqueue({
|
|
1225
|
+
type: `${type}-delta`,
|
|
1226
|
+
id,
|
|
1227
|
+
delta: '',
|
|
1228
|
+
providerMetadata: thoughtSignatureMetadata,
|
|
1229
|
+
});
|
|
1230
|
+
controller.enqueue({ type: `${type}-end`, id });
|
|
1231
|
+
}
|
|
1195
1232
|
}
|
|
1196
1233
|
} else if (part.thought === true) {
|
|
1197
1234
|
// End any active text block before starting reasoning
|
|
@@ -1316,67 +1353,108 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
1316
1353
|
providerMetadata: serverMeta,
|
|
1317
1354
|
});
|
|
1318
1355
|
lastServerToolCallId = undefined;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1356
|
+
} else if ('functionCall' in part) {
|
|
1357
|
+
const providerMeta = part.thoughtSignature
|
|
1358
|
+
? wrapProviderMetadata({
|
|
1359
|
+
thoughtSignature: part.thoughtSignature,
|
|
1360
|
+
})
|
|
1361
|
+
: undefined;
|
|
1321
1362
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
});
|
|
1363
|
+
const isStreamingChunk =
|
|
1364
|
+
part.functionCall.partialArgs != null ||
|
|
1365
|
+
(part.functionCall.name != null &&
|
|
1366
|
+
part.functionCall.willContinue === true);
|
|
1367
|
+
const isTerminalChunk =
|
|
1368
|
+
part.functionCall.name == null &&
|
|
1369
|
+
part.functionCall.args == null &&
|
|
1370
|
+
part.functionCall.partialArgs == null &&
|
|
1371
|
+
part.functionCall.willContinue == null;
|
|
1372
|
+
const isCompleteCall =
|
|
1373
|
+
part.functionCall.name != null &&
|
|
1374
|
+
part.functionCall.args != null &&
|
|
1375
|
+
part.functionCall.partialArgs == null;
|
|
1376
|
+
// Single-chunk no-args call: `{ name: 'X' }` with no `args`,
|
|
1377
|
+
// `partialArgs`, or `willContinue`. Carries `thoughtSignature`.
|
|
1378
|
+
const isNoArgsCompleteCall =
|
|
1379
|
+
part.functionCall.name != null &&
|
|
1380
|
+
part.functionCall.args == null &&
|
|
1381
|
+
part.functionCall.partialArgs == null &&
|
|
1382
|
+
part.functionCall.willContinue !== true;
|
|
1383
|
+
|
|
1384
|
+
if (
|
|
1385
|
+
!isStreamingChunk &&
|
|
1386
|
+
(isCompleteCall || isNoArgsCompleteCall)
|
|
1387
|
+
) {
|
|
1388
|
+
if (currentTextBlockId !== null) {
|
|
1389
|
+
controller.enqueue({
|
|
1390
|
+
type: 'text-end',
|
|
1391
|
+
id: currentTextBlockId,
|
|
1392
|
+
});
|
|
1393
|
+
currentTextBlockId = null;
|
|
1394
|
+
}
|
|
1395
|
+
if (currentReasoningBlockId !== null) {
|
|
1396
|
+
controller.enqueue({
|
|
1397
|
+
type: 'reasoning-end',
|
|
1398
|
+
id: currentReasoningBlockId,
|
|
1399
|
+
});
|
|
1400
|
+
currentReasoningBlockId = null;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1363
1403
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
id
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1404
|
+
if (isStreamingChunk) {
|
|
1405
|
+
if (part.functionCall.name != null) {
|
|
1406
|
+
const toolCallId = part.functionCall.id || generateId();
|
|
1407
|
+
const accumulator = new GoogleJSONAccumulator();
|
|
1408
|
+
activeStreamingToolCalls.push({
|
|
1409
|
+
toolCallId,
|
|
1410
|
+
toolName: part.functionCall.name,
|
|
1411
|
+
accumulator,
|
|
1412
|
+
providerMetadata: providerMeta,
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
controller.enqueue({
|
|
1416
|
+
type: 'tool-input-start',
|
|
1417
|
+
id: toolCallId,
|
|
1418
|
+
toolName: part.functionCall.name,
|
|
1419
|
+
providerMetadata: providerMeta,
|
|
1420
|
+
});
|
|
1370
1421
|
|
|
1371
|
-
|
|
1422
|
+
if (part.functionCall.partialArgs != null) {
|
|
1423
|
+
const partialArgs = part.functionCall
|
|
1424
|
+
.partialArgs as PartialArg[];
|
|
1425
|
+
const { textDelta } =
|
|
1426
|
+
accumulator.processPartialArgs(partialArgs);
|
|
1427
|
+
if (textDelta.length > 0) {
|
|
1428
|
+
controller.enqueue({
|
|
1429
|
+
type: 'tool-input-delta',
|
|
1430
|
+
id: toolCallId,
|
|
1431
|
+
delta: textDelta,
|
|
1432
|
+
providerMetadata: providerMeta,
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
if (
|
|
1436
|
+
part.functionCall.willContinue !== true &&
|
|
1437
|
+
partialArgs.every(arg => arg.willContinue !== true)
|
|
1438
|
+
) {
|
|
1439
|
+
finishActiveStreamingToolCall(controller);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
} else if (
|
|
1443
|
+
part.functionCall.partialArgs != null &&
|
|
1444
|
+
activeStreamingToolCalls.length > 0
|
|
1445
|
+
) {
|
|
1446
|
+
const active =
|
|
1447
|
+
activeStreamingToolCalls[
|
|
1448
|
+
activeStreamingToolCalls.length - 1
|
|
1449
|
+
];
|
|
1372
1450
|
const partialArgs = part.functionCall
|
|
1373
1451
|
.partialArgs as PartialArg[];
|
|
1374
1452
|
const { textDelta } =
|
|
1375
|
-
accumulator.processPartialArgs(partialArgs);
|
|
1453
|
+
active.accumulator.processPartialArgs(partialArgs);
|
|
1376
1454
|
if (textDelta.length > 0) {
|
|
1377
1455
|
controller.enqueue({
|
|
1378
1456
|
type: 'tool-input-delta',
|
|
1379
|
-
id: toolCallId,
|
|
1457
|
+
id: active.toolCallId,
|
|
1380
1458
|
delta: textDelta,
|
|
1381
1459
|
providerMetadata: providerMeta,
|
|
1382
1460
|
});
|
|
@@ -1389,100 +1467,74 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
1389
1467
|
}
|
|
1390
1468
|
}
|
|
1391
1469
|
} else if (
|
|
1392
|
-
|
|
1470
|
+
isTerminalChunk &&
|
|
1393
1471
|
activeStreamingToolCalls.length > 0
|
|
1394
1472
|
) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
.
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
if (textDelta.length > 0) {
|
|
1404
|
-
controller.enqueue({
|
|
1405
|
-
type: 'tool-input-delta',
|
|
1406
|
-
id: active.toolCallId,
|
|
1407
|
-
delta: textDelta,
|
|
1408
|
-
providerMetadata: providerMeta,
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
if (
|
|
1412
|
-
part.functionCall.willContinue !== true &&
|
|
1413
|
-
partialArgs.every(arg => arg.willContinue !== true)
|
|
1414
|
-
) {
|
|
1415
|
-
finishActiveStreamingToolCall(controller);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
} else if (
|
|
1419
|
-
isTerminalChunk &&
|
|
1420
|
-
activeStreamingToolCalls.length > 0
|
|
1421
|
-
) {
|
|
1422
|
-
finishActiveStreamingToolCall(controller);
|
|
1423
|
-
} else if (isCompleteCall) {
|
|
1424
|
-
const toolCallId = part.functionCall.id || generateId();
|
|
1425
|
-
const toolName = part.functionCall.name!;
|
|
1426
|
-
const args =
|
|
1427
|
-
typeof part.functionCall.args === 'string'
|
|
1428
|
-
? part.functionCall.args
|
|
1429
|
-
: JSON.stringify(part.functionCall.args ?? {});
|
|
1473
|
+
finishActiveStreamingToolCall(controller);
|
|
1474
|
+
} else if (isCompleteCall) {
|
|
1475
|
+
const toolCallId = part.functionCall.id || generateId();
|
|
1476
|
+
const toolName = part.functionCall.name!;
|
|
1477
|
+
const args =
|
|
1478
|
+
typeof part.functionCall.args === 'string'
|
|
1479
|
+
? part.functionCall.args
|
|
1480
|
+
: JSON.stringify(part.functionCall.args ?? {});
|
|
1430
1481
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1482
|
+
controller.enqueue({
|
|
1483
|
+
type: 'tool-input-start',
|
|
1484
|
+
id: toolCallId,
|
|
1485
|
+
toolName,
|
|
1486
|
+
providerMetadata: providerMeta,
|
|
1487
|
+
});
|
|
1437
1488
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1489
|
+
controller.enqueue({
|
|
1490
|
+
type: 'tool-input-delta',
|
|
1491
|
+
id: toolCallId,
|
|
1492
|
+
delta: args,
|
|
1493
|
+
providerMetadata: providerMeta,
|
|
1494
|
+
});
|
|
1444
1495
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1496
|
+
controller.enqueue({
|
|
1497
|
+
type: 'tool-input-end',
|
|
1498
|
+
id: toolCallId,
|
|
1499
|
+
providerMetadata: providerMeta,
|
|
1500
|
+
});
|
|
1450
1501
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1502
|
+
controller.enqueue({
|
|
1503
|
+
type: 'tool-call',
|
|
1504
|
+
toolCallId,
|
|
1505
|
+
toolName,
|
|
1506
|
+
input: args,
|
|
1507
|
+
providerMetadata: providerMeta,
|
|
1508
|
+
});
|
|
1458
1509
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1510
|
+
hasToolCalls = true;
|
|
1511
|
+
} else if (isNoArgsCompleteCall) {
|
|
1512
|
+
const toolCallId = part.functionCall.id || generateId();
|
|
1513
|
+
const toolName = part.functionCall.name!;
|
|
1463
1514
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1515
|
+
controller.enqueue({
|
|
1516
|
+
type: 'tool-input-start',
|
|
1517
|
+
id: toolCallId,
|
|
1518
|
+
toolName,
|
|
1519
|
+
providerMetadata: providerMeta,
|
|
1520
|
+
});
|
|
1470
1521
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1522
|
+
controller.enqueue({
|
|
1523
|
+
type: 'tool-input-end',
|
|
1524
|
+
id: toolCallId,
|
|
1525
|
+
providerMetadata: providerMeta,
|
|
1526
|
+
});
|
|
1476
1527
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1528
|
+
controller.enqueue({
|
|
1529
|
+
type: 'tool-call',
|
|
1530
|
+
toolCallId,
|
|
1531
|
+
toolName,
|
|
1532
|
+
input: '{}',
|
|
1533
|
+
providerMetadata: providerMeta,
|
|
1534
|
+
});
|
|
1484
1535
|
|
|
1485
|
-
|
|
1536
|
+
hasToolCalls = true;
|
|
1537
|
+
}
|
|
1486
1538
|
}
|
|
1487
1539
|
}
|
|
1488
1540
|
}
|
|
@@ -128,9 +128,16 @@ export class GoogleSpeechModel implements SpeechModelV4 {
|
|
|
128
128
|
|
|
129
129
|
// Multi-speaker (provider option) takes precedence over the single voice.
|
|
130
130
|
const multiSpeakerVoiceConfig = googleOptions?.multiSpeakerVoiceConfig;
|
|
131
|
+
const languageCode = language === 'auto' ? undefined : language;
|
|
131
132
|
const speechConfig = multiSpeakerVoiceConfig
|
|
132
|
-
? {
|
|
133
|
-
|
|
133
|
+
? {
|
|
134
|
+
multiSpeakerVoiceConfig,
|
|
135
|
+
...(languageCode != null && { languageCode }),
|
|
136
|
+
}
|
|
137
|
+
: {
|
|
138
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } },
|
|
139
|
+
...(languageCode != null && { languageCode }),
|
|
140
|
+
};
|
|
134
141
|
|
|
135
142
|
// Older models expect directions in the prompt. Prepending them to a
|
|
136
143
|
// labelled multi-speaker transcript would break speaker parsing.
|
|
@@ -217,16 +224,6 @@ export class GoogleSpeechModel implements SpeechModelV4 {
|
|
|
217
224
|
});
|
|
218
225
|
}
|
|
219
226
|
|
|
220
|
-
if (language != null) {
|
|
221
|
-
warnings.push({
|
|
222
|
-
type: 'unsupported',
|
|
223
|
-
feature: 'language',
|
|
224
|
-
details:
|
|
225
|
-
'Google Gemini TTS models do not support the `language` option. ' +
|
|
226
|
-
'Language is detected automatically from the input text.',
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
227
|
const formats: Record<string, string> = usesStructuredSpeech
|
|
231
228
|
? {
|
|
232
229
|
wav: 'AUDIO_WAV',
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
Experimental_RealtimeModelV4FunctionCallOutput as RealtimeModelV4FunctionCallOutput,
|
|
5
5
|
Experimental_RealtimeModelV4ServerEvent as RealtimeModelV4ServerEvent,
|
|
6
6
|
Experimental_RealtimeModelV4SessionConfig as RealtimeModelV4SessionConfig,
|
|
7
|
+
Experimental_RealtimeModelV4Usage as RealtimeModelV4Usage,
|
|
7
8
|
} from '@ai-sdk/provider';
|
|
8
9
|
import { isRecord, safeParseJSON } from '@ai-sdk/provider-utils';
|
|
9
10
|
import { getModelPath } from '../get-model-path';
|
|
@@ -31,6 +32,22 @@ type GoogleRealtimeServerContent = {
|
|
|
31
32
|
waitingForInput?: boolean;
|
|
32
33
|
};
|
|
33
34
|
|
|
35
|
+
type GoogleRealtimeTokenDetail = {
|
|
36
|
+
modality?: string;
|
|
37
|
+
tokenCount?: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type GoogleRealtimeUsageMetadata = {
|
|
41
|
+
promptTokenCount?: number;
|
|
42
|
+
responseTokenCount?: number;
|
|
43
|
+
totalTokenCount?: number;
|
|
44
|
+
toolUsePromptTokenCount?: number;
|
|
45
|
+
thoughtsTokenCount?: number;
|
|
46
|
+
cachedContentTokenCount?: number;
|
|
47
|
+
promptTokensDetails?: GoogleRealtimeTokenDetail[];
|
|
48
|
+
responseTokensDetails?: GoogleRealtimeTokenDetail[];
|
|
49
|
+
};
|
|
50
|
+
|
|
34
51
|
type GoogleRealtimeWireEvent = {
|
|
35
52
|
setupComplete?: unknown;
|
|
36
53
|
toolCall?: {
|
|
@@ -38,6 +55,7 @@ type GoogleRealtimeWireEvent = {
|
|
|
38
55
|
};
|
|
39
56
|
toolCallCancellation?: unknown;
|
|
40
57
|
serverContent?: GoogleRealtimeServerContent;
|
|
58
|
+
usageMetadata?: GoogleRealtimeUsageMetadata;
|
|
41
59
|
inputTranscription?: { text?: string; finished?: boolean };
|
|
42
60
|
goAway?: { timeLeft?: string };
|
|
43
61
|
sessionResumptionUpdate?: {
|
|
@@ -150,6 +168,37 @@ export class GoogleRealtimeEventMapper {
|
|
|
150
168
|
|
|
151
169
|
parseServerEvent(
|
|
152
170
|
raw: unknown,
|
|
171
|
+
): RealtimeModelV4ServerEvent | RealtimeModelV4ServerEvent[] {
|
|
172
|
+
const events = this.parseEvent(raw);
|
|
173
|
+
const usage = mapGoogleUsage(
|
|
174
|
+
(raw as GoogleRealtimeWireEvent).usageMetadata,
|
|
175
|
+
);
|
|
176
|
+
if (usage == null) return events;
|
|
177
|
+
const list = Array.isArray(events) ? events : [events];
|
|
178
|
+
// Only a shared wire frame establishes an association with a completion.
|
|
179
|
+
if (list.some(event => event.type === 'response-done')) {
|
|
180
|
+
const mapped = list.map(event =>
|
|
181
|
+
event.type === 'response-done' ? { ...event, usage } : event,
|
|
182
|
+
);
|
|
183
|
+
return Array.isArray(events) ? mapped : mapped[0];
|
|
184
|
+
}
|
|
185
|
+
const update: RealtimeModelV4ServerEvent = {
|
|
186
|
+
type: 'custom',
|
|
187
|
+
rawType: 'usageMetadata',
|
|
188
|
+
usage,
|
|
189
|
+
raw,
|
|
190
|
+
};
|
|
191
|
+
if (
|
|
192
|
+
list.length === 1 &&
|
|
193
|
+
list[0].type === 'custom' &&
|
|
194
|
+
list[0].rawType === 'usageMetadata'
|
|
195
|
+
)
|
|
196
|
+
return update;
|
|
197
|
+
return [...list, update];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private parseEvent(
|
|
201
|
+
raw: unknown,
|
|
153
202
|
): RealtimeModelV4ServerEvent | RealtimeModelV4ServerEvent[] {
|
|
154
203
|
const data = raw as GoogleRealtimeWireEvent;
|
|
155
204
|
|
|
@@ -448,6 +497,80 @@ export class GoogleRealtimeEventMapper {
|
|
|
448
497
|
}
|
|
449
498
|
}
|
|
450
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Maps Gemini Live `usageMetadata` to normalized usage. Google reports usage
|
|
502
|
+
* per modality in `promptTokensDetails` / `responseTokensDetails`.
|
|
503
|
+
*
|
|
504
|
+
* Field names are Live-API-specific: the Live `UsageMetadata` uses
|
|
505
|
+
* `responseTokenCount` / `responseTokensDetails` for output, NOT the
|
|
506
|
+
* `candidatesTokenCount` / `candidatesTokensDetails` of the regular
|
|
507
|
+
* `generateContent` response (see google-language-model.ts). Do not "align"
|
|
508
|
+
* these to `candidates*` — they are different wire shapes.
|
|
509
|
+
*
|
|
510
|
+
* Gross totals preserve provider counts even when modality details are absent
|
|
511
|
+
* or contain modalities without a dedicated normalized bucket. Never guess a
|
|
512
|
+
* text/audio split from those totals.
|
|
513
|
+
*/
|
|
514
|
+
function mapGoogleUsage(
|
|
515
|
+
usageMetadata: GoogleRealtimeUsageMetadata | undefined,
|
|
516
|
+
): RealtimeModelV4Usage | undefined {
|
|
517
|
+
if (usageMetadata == null) return undefined;
|
|
518
|
+
|
|
519
|
+
const input = sumByModality(usageMetadata.promptTokensDetails);
|
|
520
|
+
const output = sumByModality(usageMetadata.responseTokensDetails);
|
|
521
|
+
|
|
522
|
+
return compactUsage({
|
|
523
|
+
inputTokens:
|
|
524
|
+
usageMetadata.promptTokenCount != null
|
|
525
|
+
? usageMetadata.promptTokenCount +
|
|
526
|
+
(usageMetadata.toolUsePromptTokenCount ?? 0)
|
|
527
|
+
: undefined,
|
|
528
|
+
outputTokens:
|
|
529
|
+
usageMetadata.responseTokenCount != null
|
|
530
|
+
? usageMetadata.responseTokenCount +
|
|
531
|
+
(usageMetadata.thoughtsTokenCount ?? 0)
|
|
532
|
+
: undefined,
|
|
533
|
+
cachedInputTokens: usageMetadata.cachedContentTokenCount,
|
|
534
|
+
inputTextTokens: input.text,
|
|
535
|
+
inputAudioTokens: input.audio,
|
|
536
|
+
outputTextTokens: output.text,
|
|
537
|
+
outputAudioTokens: output.audio,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Aggregate Gemini per-modality token details into text/audio buckets. */
|
|
542
|
+
function sumByModality(details: GoogleRealtimeTokenDetail[] | undefined): {
|
|
543
|
+
text?: number;
|
|
544
|
+
audio?: number;
|
|
545
|
+
} {
|
|
546
|
+
if (details == null) return {};
|
|
547
|
+
const totals: { text?: number; audio?: number } = {};
|
|
548
|
+
for (const detail of details) {
|
|
549
|
+
const count = detail.tokenCount;
|
|
550
|
+
if (count == null) continue;
|
|
551
|
+
const modality = detail.modality?.toUpperCase();
|
|
552
|
+
if (modality === 'AUDIO') {
|
|
553
|
+
totals.audio = (totals.audio ?? 0) + count;
|
|
554
|
+
} else if (modality === 'TEXT') {
|
|
555
|
+
totals.text = (totals.text ?? 0) + count;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return totals;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Drop `undefined` fields; return `undefined` when nothing is observable. */
|
|
562
|
+
function compactUsage(
|
|
563
|
+
usage: RealtimeModelV4Usage,
|
|
564
|
+
): RealtimeModelV4Usage | undefined {
|
|
565
|
+
const result: RealtimeModelV4Usage = {};
|
|
566
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
567
|
+
if (value != null) {
|
|
568
|
+
result[key as keyof RealtimeModelV4Usage] = value;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
572
|
+
}
|
|
573
|
+
|
|
451
574
|
/**
|
|
452
575
|
* The value Gemini accepts for `functionResponse.response`.
|
|
453
576
|
*
|