@ai-sdk/google 3.0.67 → 3.0.68
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 +6 -0
- package/dist/index.d.mts +90 -1
- package/dist/index.d.ts +90 -1
- package/dist/index.js +2383 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2353 -1
- package/dist/index.mjs.map +1 -1
- package/docs/15-google-generative-ai.mdx +396 -0
- package/package.json +3 -3
- package/src/google-provider.ts +34 -0
- package/src/index.ts +6 -0
- package/src/interactions/build-google-interactions-stream-transform.ts +711 -0
- package/src/interactions/convert-google-interactions-usage.ts +47 -0
- package/src/interactions/convert-to-google-interactions-input.ts +630 -0
- package/src/interactions/extract-google-interactions-sources.ts +245 -0
- package/src/interactions/google-interactions-agent.ts +16 -0
- package/src/interactions/google-interactions-api.ts +466 -0
- package/src/interactions/google-interactions-language-model-options.ts +136 -0
- package/src/interactions/google-interactions-language-model.ts +609 -0
- package/src/interactions/google-interactions-prompt.ts +457 -0
- package/src/interactions/google-interactions-provider-metadata.ts +23 -0
- package/src/interactions/map-google-interactions-finish-reason.ts +33 -0
- package/src/interactions/parse-google-interactions-outputs.ts +257 -0
- package/src/interactions/poll-google-interactions.ts +110 -0
- package/src/interactions/prepare-google-interactions-tools.ts +245 -0
- package/src/interactions/synthesize-google-interactions-agent-stream.ts +185 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
JSONValue,
|
|
3
|
+
LanguageModelV3FinishReason,
|
|
4
|
+
LanguageModelV3Source,
|
|
5
|
+
LanguageModelV3StreamPart,
|
|
6
|
+
SharedV3ProviderMetadata,
|
|
7
|
+
SharedV3Warning,
|
|
8
|
+
} from '@ai-sdk/provider';
|
|
9
|
+
import type { ParseResult } from '@ai-sdk/provider-utils';
|
|
10
|
+
import type {
|
|
11
|
+
GoogleInteractionsEvent,
|
|
12
|
+
GoogleInteractionsUsage,
|
|
13
|
+
} from './google-interactions-api';
|
|
14
|
+
import { convertGoogleInteractionsUsage } from './convert-google-interactions-usage';
|
|
15
|
+
import {
|
|
16
|
+
annotationsToSources,
|
|
17
|
+
builtinToolResultToSources,
|
|
18
|
+
} from './extract-google-interactions-sources';
|
|
19
|
+
import { mapGoogleInteractionsFinishReason } from './map-google-interactions-finish-reason';
|
|
20
|
+
import type {
|
|
21
|
+
GoogleInteractionsAnnotation,
|
|
22
|
+
GoogleInteractionsBuiltinToolResultContent,
|
|
23
|
+
GoogleInteractionsStatus,
|
|
24
|
+
} from './google-interactions-prompt';
|
|
25
|
+
|
|
26
|
+
const BUILTIN_TOOL_CALL_TYPES = new Set([
|
|
27
|
+
'google_search_call',
|
|
28
|
+
'code_execution_call',
|
|
29
|
+
'url_context_call',
|
|
30
|
+
'file_search_call',
|
|
31
|
+
'google_maps_call',
|
|
32
|
+
'mcp_server_tool_call',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const BUILTIN_TOOL_RESULT_TYPES = new Set([
|
|
36
|
+
'google_search_result',
|
|
37
|
+
'code_execution_result',
|
|
38
|
+
'url_context_result',
|
|
39
|
+
'file_search_result',
|
|
40
|
+
'google_maps_result',
|
|
41
|
+
'mcp_server_tool_result',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function builtinToolNameFromCallType(type: string): string {
|
|
45
|
+
return type.replace(/_call$/, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function builtinToolNameFromResultType(type: string): string {
|
|
49
|
+
return type.replace(/_result$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type OpenBlockState =
|
|
53
|
+
| { kind: 'text'; id: string; emittedSourceKeys: Set<string> }
|
|
54
|
+
| {
|
|
55
|
+
kind: 'reasoning';
|
|
56
|
+
id: string;
|
|
57
|
+
signature?: string;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
kind: 'image';
|
|
61
|
+
id: string;
|
|
62
|
+
data?: string;
|
|
63
|
+
mimeType?: string;
|
|
64
|
+
uri?: string;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
kind: 'function_call';
|
|
68
|
+
id: string;
|
|
69
|
+
toolCallId: string;
|
|
70
|
+
toolName: string | undefined;
|
|
71
|
+
arguments: Record<string, unknown>;
|
|
72
|
+
signature?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Whether `tool-input-start` has been emitted. Deferred until we know
|
|
75
|
+
* the tool name -- `content.start` for a function_call only carries
|
|
76
|
+
* `type: 'function_call'`; `id`, `name`, and `arguments` arrive on
|
|
77
|
+
* `content.delta`.
|
|
78
|
+
*/
|
|
79
|
+
startEmitted: boolean;
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
kind: 'builtin_tool_call';
|
|
83
|
+
id: string;
|
|
84
|
+
blockType: string;
|
|
85
|
+
toolCallId: string;
|
|
86
|
+
toolName: string;
|
|
87
|
+
arguments: Record<string, unknown>;
|
|
88
|
+
callEmitted: boolean;
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
kind: 'builtin_tool_result';
|
|
92
|
+
id: string;
|
|
93
|
+
blockType: string;
|
|
94
|
+
callId: string;
|
|
95
|
+
toolName: string;
|
|
96
|
+
result: unknown;
|
|
97
|
+
isError?: boolean;
|
|
98
|
+
resultEmitted: boolean;
|
|
99
|
+
}
|
|
100
|
+
| { kind: 'unknown'; id: string };
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds a `TransformStream<ParseResult<GoogleInteractionsEvent>, LanguageModelV3StreamPart>`
|
|
104
|
+
* over the seven Interactions SSE event types.
|
|
105
|
+
*
|
|
106
|
+
* Surfaces text + thought (reasoning), function_call, image, built-in tool
|
|
107
|
+
* call/result blocks, and `text_annotation` -> `source` parts.
|
|
108
|
+
*/
|
|
109
|
+
export function buildGoogleInteractionsStreamTransform({
|
|
110
|
+
warnings,
|
|
111
|
+
generateId,
|
|
112
|
+
includeRawChunks,
|
|
113
|
+
serviceTier: headerServiceTier,
|
|
114
|
+
}: {
|
|
115
|
+
warnings: Array<SharedV3Warning>;
|
|
116
|
+
generateId: () => string;
|
|
117
|
+
includeRawChunks?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Defensive fallback for service tier read from the `x-gemini-service-tier`
|
|
120
|
+
* HTTP response header. The Interactions API surfaces the applied tier in
|
|
121
|
+
* the `interaction.complete` event body (see `service_tier` below); this
|
|
122
|
+
* parameter exists so we still surface a tier if the API later starts
|
|
123
|
+
* sending the header (matching `google-language-model.ts` commit
|
|
124
|
+
* 1adfb76d2d).
|
|
125
|
+
*/
|
|
126
|
+
serviceTier?: string;
|
|
127
|
+
}): TransformStream<
|
|
128
|
+
ParseResult<GoogleInteractionsEvent>,
|
|
129
|
+
LanguageModelV3StreamPart
|
|
130
|
+
> {
|
|
131
|
+
let interactionId: string | undefined;
|
|
132
|
+
let usage: GoogleInteractionsUsage | undefined;
|
|
133
|
+
let serviceTier: string | undefined = headerServiceTier;
|
|
134
|
+
let finishStatus: GoogleInteractionsStatus | string | undefined;
|
|
135
|
+
let hasFunctionCall = false;
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
* Per-index open content slots. The Interactions API frames concurrent
|
|
139
|
+
* content blocks (e.g. text alongside thought) by `index`; we track each
|
|
140
|
+
* open slot independently so a text delta at index N never collides with a
|
|
141
|
+
* thought delta at index M.
|
|
142
|
+
*/
|
|
143
|
+
const openBlocks = new Map<number, OpenBlockState>();
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* De-duplicate sources across the whole stream. Citations often re-appear
|
|
147
|
+
* across multiple `text_annotation` deltas as the model's text grows;
|
|
148
|
+
* surface each unique URL once.
|
|
149
|
+
*/
|
|
150
|
+
const emittedSourceKeys = new Set<string>();
|
|
151
|
+
|
|
152
|
+
function sourceKey(source: LanguageModelV3Source): string {
|
|
153
|
+
return source.sourceType === 'url'
|
|
154
|
+
? `url:${source.url}`
|
|
155
|
+
: `doc:${source.filename ?? source.title}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return new TransformStream<
|
|
159
|
+
ParseResult<GoogleInteractionsEvent>,
|
|
160
|
+
LanguageModelV3StreamPart
|
|
161
|
+
>({
|
|
162
|
+
start(controller) {
|
|
163
|
+
controller.enqueue({ type: 'stream-start', warnings });
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
transform(chunk, controller) {
|
|
167
|
+
if (includeRawChunks) {
|
|
168
|
+
controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!chunk.success) {
|
|
172
|
+
finishStatus = 'failed';
|
|
173
|
+
controller.enqueue({ type: 'error', error: chunk.error });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const value = chunk.value;
|
|
178
|
+
const eventType = (value as { event_type?: string }).event_type;
|
|
179
|
+
|
|
180
|
+
switch (eventType) {
|
|
181
|
+
case 'interaction.start': {
|
|
182
|
+
const event = value as Extract<
|
|
183
|
+
GoogleInteractionsEvent,
|
|
184
|
+
{ event_type: 'interaction.start' }
|
|
185
|
+
>;
|
|
186
|
+
const interaction = event.interaction;
|
|
187
|
+
/*
|
|
188
|
+
* The Interactions API returns `id: ""` (empty string) on streaming
|
|
189
|
+
* `interaction.start` / `interaction.complete` events when running
|
|
190
|
+
* with `store: false` — there is no server-side record. Treat empty
|
|
191
|
+
* string the same as missing so providerMetadata stays clean.
|
|
192
|
+
*/
|
|
193
|
+
interactionId =
|
|
194
|
+
interaction?.id != null && interaction.id.length > 0
|
|
195
|
+
? interaction.id
|
|
196
|
+
: undefined;
|
|
197
|
+
|
|
198
|
+
const created = (interaction as { created?: string } | undefined)
|
|
199
|
+
?.created;
|
|
200
|
+
let timestamp: Date | undefined;
|
|
201
|
+
if (typeof created === 'string') {
|
|
202
|
+
const parsed = new Date(created);
|
|
203
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
204
|
+
timestamp = parsed;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
controller.enqueue({
|
|
209
|
+
type: 'response-metadata',
|
|
210
|
+
...(interactionId != null ? { id: interactionId } : {}),
|
|
211
|
+
modelId: (interaction as { model?: string } | undefined)?.model,
|
|
212
|
+
...(timestamp ? { timestamp } : {}),
|
|
213
|
+
});
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case 'content.start': {
|
|
218
|
+
const event = value as Extract<
|
|
219
|
+
GoogleInteractionsEvent,
|
|
220
|
+
{ event_type: 'content.start' }
|
|
221
|
+
>;
|
|
222
|
+
const block = event.content as
|
|
223
|
+
| {
|
|
224
|
+
type?: string;
|
|
225
|
+
id?: string;
|
|
226
|
+
call_id?: string;
|
|
227
|
+
name?: string;
|
|
228
|
+
arguments?: Record<string, unknown>;
|
|
229
|
+
signature?: string;
|
|
230
|
+
result?: unknown;
|
|
231
|
+
is_error?: boolean;
|
|
232
|
+
annotations?: Array<GoogleInteractionsAnnotation>;
|
|
233
|
+
}
|
|
234
|
+
| undefined;
|
|
235
|
+
const index = event.index;
|
|
236
|
+
const blockId = `${interactionId ?? 'interaction'}:${index}`;
|
|
237
|
+
|
|
238
|
+
if (block?.type === 'text') {
|
|
239
|
+
openBlocks.set(index, {
|
|
240
|
+
kind: 'text',
|
|
241
|
+
id: blockId,
|
|
242
|
+
emittedSourceKeys: new Set<string>(),
|
|
243
|
+
});
|
|
244
|
+
controller.enqueue({ type: 'text-start', id: blockId });
|
|
245
|
+
|
|
246
|
+
// text content blocks may already carry annotations on open.
|
|
247
|
+
const initialSources = annotationsToSources({
|
|
248
|
+
annotations: block.annotations,
|
|
249
|
+
generateId,
|
|
250
|
+
});
|
|
251
|
+
for (const source of initialSources) {
|
|
252
|
+
const key = sourceKey(source);
|
|
253
|
+
if (emittedSourceKeys.has(key)) continue;
|
|
254
|
+
emittedSourceKeys.add(key);
|
|
255
|
+
controller.enqueue(source);
|
|
256
|
+
}
|
|
257
|
+
} else if (block?.type === 'image') {
|
|
258
|
+
const img = block as {
|
|
259
|
+
data?: string;
|
|
260
|
+
mime_type?: string;
|
|
261
|
+
uri?: string;
|
|
262
|
+
};
|
|
263
|
+
openBlocks.set(index, {
|
|
264
|
+
kind: 'image',
|
|
265
|
+
id: blockId,
|
|
266
|
+
...(img.data != null ? { data: img.data } : {}),
|
|
267
|
+
...(img.mime_type != null ? { mimeType: img.mime_type } : {}),
|
|
268
|
+
...(img.uri != null ? { uri: img.uri } : {}),
|
|
269
|
+
});
|
|
270
|
+
} else if (block?.type === 'thought') {
|
|
271
|
+
const signature = (block as { signature?: string }).signature;
|
|
272
|
+
openBlocks.set(index, {
|
|
273
|
+
kind: 'reasoning',
|
|
274
|
+
id: blockId,
|
|
275
|
+
...(signature != null ? { signature } : {}),
|
|
276
|
+
});
|
|
277
|
+
controller.enqueue({ type: 'reasoning-start', id: blockId });
|
|
278
|
+
} else if (block?.type === 'function_call') {
|
|
279
|
+
const fc = block;
|
|
280
|
+
const toolCallId = fc.id ?? blockId;
|
|
281
|
+
hasFunctionCall = true;
|
|
282
|
+
const state: Extract<OpenBlockState, { kind: 'function_call' }> = {
|
|
283
|
+
kind: 'function_call',
|
|
284
|
+
id: blockId,
|
|
285
|
+
toolCallId,
|
|
286
|
+
toolName: fc.name,
|
|
287
|
+
arguments: fc.arguments ?? {},
|
|
288
|
+
...(fc.signature != null ? { signature: fc.signature } : {}),
|
|
289
|
+
startEmitted: false,
|
|
290
|
+
};
|
|
291
|
+
openBlocks.set(index, state);
|
|
292
|
+
if (state.toolName != null) {
|
|
293
|
+
controller.enqueue({
|
|
294
|
+
type: 'tool-input-start',
|
|
295
|
+
id: toolCallId,
|
|
296
|
+
toolName: state.toolName,
|
|
297
|
+
});
|
|
298
|
+
state.startEmitted = true;
|
|
299
|
+
}
|
|
300
|
+
} else if (
|
|
301
|
+
block?.type != null &&
|
|
302
|
+
BUILTIN_TOOL_CALL_TYPES.has(block.type)
|
|
303
|
+
) {
|
|
304
|
+
const toolName =
|
|
305
|
+
block.type === 'mcp_server_tool_call'
|
|
306
|
+
? (block.name ?? 'mcp_server_tool')
|
|
307
|
+
: builtinToolNameFromCallType(block.type);
|
|
308
|
+
const toolCallId = block.id ?? blockId;
|
|
309
|
+
const state: Extract<
|
|
310
|
+
OpenBlockState,
|
|
311
|
+
{ kind: 'builtin_tool_call' }
|
|
312
|
+
> = {
|
|
313
|
+
kind: 'builtin_tool_call',
|
|
314
|
+
id: blockId,
|
|
315
|
+
blockType: block.type,
|
|
316
|
+
toolCallId,
|
|
317
|
+
toolName,
|
|
318
|
+
arguments: block.arguments ?? {},
|
|
319
|
+
callEmitted: false,
|
|
320
|
+
};
|
|
321
|
+
openBlocks.set(index, state);
|
|
322
|
+
} else if (
|
|
323
|
+
block?.type != null &&
|
|
324
|
+
BUILTIN_TOOL_RESULT_TYPES.has(block.type)
|
|
325
|
+
) {
|
|
326
|
+
const toolName =
|
|
327
|
+
block.type === 'mcp_server_tool_result'
|
|
328
|
+
? (block.name ?? 'mcp_server_tool')
|
|
329
|
+
: builtinToolNameFromResultType(block.type);
|
|
330
|
+
const callId = block.call_id ?? blockId;
|
|
331
|
+
const state: Extract<
|
|
332
|
+
OpenBlockState,
|
|
333
|
+
{ kind: 'builtin_tool_result' }
|
|
334
|
+
> = {
|
|
335
|
+
kind: 'builtin_tool_result',
|
|
336
|
+
id: blockId,
|
|
337
|
+
blockType: block.type,
|
|
338
|
+
callId,
|
|
339
|
+
toolName,
|
|
340
|
+
result: block.result ?? null,
|
|
341
|
+
...(block.is_error != null ? { isError: block.is_error } : {}),
|
|
342
|
+
resultEmitted: false,
|
|
343
|
+
};
|
|
344
|
+
openBlocks.set(index, state);
|
|
345
|
+
} else {
|
|
346
|
+
openBlocks.set(index, { kind: 'unknown', id: blockId });
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
case 'content.delta': {
|
|
352
|
+
const event = value as Extract<
|
|
353
|
+
GoogleInteractionsEvent,
|
|
354
|
+
{ event_type: 'content.delta' }
|
|
355
|
+
>;
|
|
356
|
+
const open = openBlocks.get(event.index);
|
|
357
|
+
if (open == null) break;
|
|
358
|
+
|
|
359
|
+
const delta = event.delta as
|
|
360
|
+
| {
|
|
361
|
+
type?: string;
|
|
362
|
+
text?: string;
|
|
363
|
+
signature?: string;
|
|
364
|
+
content?: { type?: string; text?: string };
|
|
365
|
+
id?: string;
|
|
366
|
+
name?: string;
|
|
367
|
+
arguments?: Record<string, unknown>;
|
|
368
|
+
annotations?: Array<GoogleInteractionsAnnotation>;
|
|
369
|
+
call_id?: string;
|
|
370
|
+
result?: unknown;
|
|
371
|
+
is_error?: boolean;
|
|
372
|
+
data?: string;
|
|
373
|
+
mime_type?: string;
|
|
374
|
+
uri?: string;
|
|
375
|
+
}
|
|
376
|
+
| undefined;
|
|
377
|
+
|
|
378
|
+
if (open.kind === 'text' && delta?.type === 'text') {
|
|
379
|
+
const text = delta.text ?? '';
|
|
380
|
+
if (text.length > 0) {
|
|
381
|
+
controller.enqueue({
|
|
382
|
+
type: 'text-delta',
|
|
383
|
+
id: open.id,
|
|
384
|
+
delta: text,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
} else if (
|
|
388
|
+
open.kind === 'text' &&
|
|
389
|
+
delta?.type === 'text_annotation'
|
|
390
|
+
) {
|
|
391
|
+
const sources = annotationsToSources({
|
|
392
|
+
annotations: delta.annotations,
|
|
393
|
+
generateId,
|
|
394
|
+
});
|
|
395
|
+
for (const source of sources) {
|
|
396
|
+
const key = sourceKey(source);
|
|
397
|
+
if (emittedSourceKeys.has(key)) continue;
|
|
398
|
+
emittedSourceKeys.add(key);
|
|
399
|
+
open.emittedSourceKeys.add(key);
|
|
400
|
+
controller.enqueue(source);
|
|
401
|
+
}
|
|
402
|
+
} else if (open.kind === 'image' && delta?.type === 'image') {
|
|
403
|
+
/*
|
|
404
|
+
* `image` ContentDelta carries the entire image payload as a
|
|
405
|
+
* complete object (`data` base64 + `mime_type`, or `uri`) per
|
|
406
|
+
* `googleapis/js-genai`
|
|
407
|
+
* `src/interactions/resources/interactions.ts`
|
|
408
|
+
* `ContentDelta.Image`. Accumulate the latest snapshot; emit the
|
|
409
|
+
* file stream part on `content.stop`.
|
|
410
|
+
*/
|
|
411
|
+
if (delta.data != null) open.data = delta.data;
|
|
412
|
+
if (delta.mime_type != null) open.mimeType = delta.mime_type;
|
|
413
|
+
if (delta.uri != null) open.uri = delta.uri;
|
|
414
|
+
} else if (open.kind === 'reasoning') {
|
|
415
|
+
if (delta?.type === 'thought_summary') {
|
|
416
|
+
const item = delta.content;
|
|
417
|
+
if (item?.type === 'text' && typeof item.text === 'string') {
|
|
418
|
+
controller.enqueue({
|
|
419
|
+
type: 'reasoning-delta',
|
|
420
|
+
id: open.id,
|
|
421
|
+
delta: item.text,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
} else if (delta?.type === 'thought_signature') {
|
|
425
|
+
const signature = delta.signature;
|
|
426
|
+
if (signature != null) {
|
|
427
|
+
open.signature = signature;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} else if (
|
|
431
|
+
open.kind === 'function_call' &&
|
|
432
|
+
delta?.type === 'function_call'
|
|
433
|
+
) {
|
|
434
|
+
/*
|
|
435
|
+
* `function_call` ContentDelta carries the entire call as a
|
|
436
|
+
* complete object (id, name, arguments) per
|
|
437
|
+
* `googleapis/js-genai` `src/interactions/resources/interactions.ts`
|
|
438
|
+
* `ContentDelta.FunctionCall` (line ~458) -- there is no token
|
|
439
|
+
* streaming of the JSON arguments. We accumulate the latest
|
|
440
|
+
* snapshot and emit a single `tool-input-delta` carrying the
|
|
441
|
+
* stringified args at content.stop.
|
|
442
|
+
*
|
|
443
|
+
* The `name` typically arrives here (not on `content.start`), so
|
|
444
|
+
* defer `tool-input-start` emission until we observe it.
|
|
445
|
+
*/
|
|
446
|
+
if (delta.id != null) {
|
|
447
|
+
open.toolCallId = delta.id;
|
|
448
|
+
}
|
|
449
|
+
if (delta.name != null) {
|
|
450
|
+
open.toolName = delta.name;
|
|
451
|
+
}
|
|
452
|
+
if (delta.arguments != null) {
|
|
453
|
+
open.arguments = delta.arguments;
|
|
454
|
+
}
|
|
455
|
+
if (delta.signature != null) {
|
|
456
|
+
open.signature = delta.signature;
|
|
457
|
+
}
|
|
458
|
+
if (!open.startEmitted && open.toolName != null) {
|
|
459
|
+
controller.enqueue({
|
|
460
|
+
type: 'tool-input-start',
|
|
461
|
+
id: open.toolCallId,
|
|
462
|
+
toolName: open.toolName,
|
|
463
|
+
});
|
|
464
|
+
open.startEmitted = true;
|
|
465
|
+
}
|
|
466
|
+
hasFunctionCall = true;
|
|
467
|
+
} else if (
|
|
468
|
+
open.kind === 'builtin_tool_call' &&
|
|
469
|
+
delta?.type === open.blockType
|
|
470
|
+
) {
|
|
471
|
+
if (delta.id != null) open.toolCallId = delta.id;
|
|
472
|
+
if (delta.arguments != null) open.arguments = delta.arguments;
|
|
473
|
+
if (
|
|
474
|
+
delta.name != null &&
|
|
475
|
+
open.blockType === 'mcp_server_tool_call'
|
|
476
|
+
) {
|
|
477
|
+
open.toolName = delta.name;
|
|
478
|
+
}
|
|
479
|
+
} else if (
|
|
480
|
+
open.kind === 'builtin_tool_result' &&
|
|
481
|
+
delta?.type === open.blockType
|
|
482
|
+
) {
|
|
483
|
+
if (delta.call_id != null) open.callId = delta.call_id;
|
|
484
|
+
if (delta.result !== undefined) open.result = delta.result;
|
|
485
|
+
if (delta.is_error != null) open.isError = delta.is_error;
|
|
486
|
+
if (
|
|
487
|
+
delta.name != null &&
|
|
488
|
+
open.blockType === 'mcp_server_tool_result'
|
|
489
|
+
) {
|
|
490
|
+
open.toolName = delta.name;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
case 'content.stop': {
|
|
497
|
+
const event = value as Extract<
|
|
498
|
+
GoogleInteractionsEvent,
|
|
499
|
+
{ event_type: 'content.stop' }
|
|
500
|
+
>;
|
|
501
|
+
const open = openBlocks.get(event.index);
|
|
502
|
+
if (open == null) break;
|
|
503
|
+
|
|
504
|
+
if (open.kind === 'text') {
|
|
505
|
+
const textProviderMetadata =
|
|
506
|
+
interactionId != null ? { google: { interactionId } } : undefined;
|
|
507
|
+
controller.enqueue({
|
|
508
|
+
type: 'text-end',
|
|
509
|
+
id: open.id,
|
|
510
|
+
...(textProviderMetadata
|
|
511
|
+
? { providerMetadata: textProviderMetadata }
|
|
512
|
+
: {}),
|
|
513
|
+
});
|
|
514
|
+
} else if (open.kind === 'reasoning') {
|
|
515
|
+
const google: Record<string, string> = {};
|
|
516
|
+
if (open.signature != null) google.signature = open.signature;
|
|
517
|
+
if (interactionId != null) google.interactionId = interactionId;
|
|
518
|
+
const providerMetadata =
|
|
519
|
+
Object.keys(google).length > 0 ? { google } : undefined;
|
|
520
|
+
controller.enqueue({
|
|
521
|
+
type: 'reasoning-end',
|
|
522
|
+
id: open.id,
|
|
523
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
524
|
+
});
|
|
525
|
+
} else if (open.kind === 'image') {
|
|
526
|
+
const google: Record<string, string> = {};
|
|
527
|
+
if (interactionId != null) google.interactionId = interactionId;
|
|
528
|
+
const providerMetadata =
|
|
529
|
+
Object.keys(google).length > 0 ? { google } : undefined;
|
|
530
|
+
if (open.data != null && open.data.length > 0) {
|
|
531
|
+
controller.enqueue({
|
|
532
|
+
type: 'file',
|
|
533
|
+
mediaType: open.mimeType ?? 'image/png',
|
|
534
|
+
data: open.data,
|
|
535
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
536
|
+
});
|
|
537
|
+
} else if (open.uri != null && open.uri.length > 0) {
|
|
538
|
+
/*
|
|
539
|
+
* V3 `LanguageModelV3File` only supports inline data (`string` /
|
|
540
|
+
* `Uint8Array`). URL-only image outputs cannot be represented as
|
|
541
|
+
* a file stream part on the v3 spec; surface the URI through
|
|
542
|
+
* provider metadata so callers can still recover it.
|
|
543
|
+
*/
|
|
544
|
+
const uriProviderMetadata = {
|
|
545
|
+
google: {
|
|
546
|
+
...(interactionId != null ? { interactionId } : {}),
|
|
547
|
+
imageUri: open.uri,
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
controller.enqueue({
|
|
551
|
+
type: 'file',
|
|
552
|
+
mediaType: open.mimeType ?? 'image/png',
|
|
553
|
+
data: '',
|
|
554
|
+
providerMetadata: uriProviderMetadata,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
} else if (open.kind === 'function_call') {
|
|
558
|
+
const toolName = open.toolName ?? 'unknown';
|
|
559
|
+
const argsJson = JSON.stringify(open.arguments ?? {});
|
|
560
|
+
if (!open.startEmitted) {
|
|
561
|
+
controller.enqueue({
|
|
562
|
+
type: 'tool-input-start',
|
|
563
|
+
id: open.toolCallId,
|
|
564
|
+
toolName,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
controller.enqueue({
|
|
568
|
+
type: 'tool-input-delta',
|
|
569
|
+
id: open.toolCallId,
|
|
570
|
+
delta: argsJson,
|
|
571
|
+
});
|
|
572
|
+
controller.enqueue({
|
|
573
|
+
type: 'tool-input-end',
|
|
574
|
+
id: open.toolCallId,
|
|
575
|
+
});
|
|
576
|
+
const google: Record<string, string> = {};
|
|
577
|
+
if (open.signature != null) google.signature = open.signature;
|
|
578
|
+
if (interactionId != null) google.interactionId = interactionId;
|
|
579
|
+
const providerMetadata =
|
|
580
|
+
Object.keys(google).length > 0 ? { google } : undefined;
|
|
581
|
+
controller.enqueue({
|
|
582
|
+
type: 'tool-call',
|
|
583
|
+
toolCallId: open.toolCallId,
|
|
584
|
+
toolName,
|
|
585
|
+
input: argsJson,
|
|
586
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
587
|
+
});
|
|
588
|
+
} else if (open.kind === 'builtin_tool_call' && !open.callEmitted) {
|
|
589
|
+
controller.enqueue({
|
|
590
|
+
type: 'tool-call',
|
|
591
|
+
toolCallId: open.toolCallId,
|
|
592
|
+
toolName: open.toolName,
|
|
593
|
+
input: JSON.stringify(open.arguments ?? {}),
|
|
594
|
+
providerExecuted: true,
|
|
595
|
+
});
|
|
596
|
+
open.callEmitted = true;
|
|
597
|
+
} else if (
|
|
598
|
+
open.kind === 'builtin_tool_result' &&
|
|
599
|
+
!open.resultEmitted
|
|
600
|
+
) {
|
|
601
|
+
controller.enqueue({
|
|
602
|
+
type: 'tool-result',
|
|
603
|
+
toolCallId: open.callId,
|
|
604
|
+
toolName: open.toolName,
|
|
605
|
+
result: (open.result ?? null) as NonNullable<JSONValue>,
|
|
606
|
+
});
|
|
607
|
+
open.resultEmitted = true;
|
|
608
|
+
|
|
609
|
+
const sources = builtinToolResultToSources({
|
|
610
|
+
block: {
|
|
611
|
+
type: open.blockType,
|
|
612
|
+
call_id: open.callId,
|
|
613
|
+
result: open.result,
|
|
614
|
+
} as unknown as GoogleInteractionsBuiltinToolResultContent,
|
|
615
|
+
generateId,
|
|
616
|
+
});
|
|
617
|
+
for (const source of sources) {
|
|
618
|
+
const key = sourceKey(source);
|
|
619
|
+
if (emittedSourceKeys.has(key)) continue;
|
|
620
|
+
emittedSourceKeys.add(key);
|
|
621
|
+
controller.enqueue(source);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
openBlocks.delete(event.index);
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
case 'interaction.status_update': {
|
|
629
|
+
const event = value as Extract<
|
|
630
|
+
GoogleInteractionsEvent,
|
|
631
|
+
{ event_type: 'interaction.status_update' }
|
|
632
|
+
>;
|
|
633
|
+
finishStatus = event.status;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
case 'interaction.complete': {
|
|
638
|
+
const event = value as Extract<
|
|
639
|
+
GoogleInteractionsEvent,
|
|
640
|
+
{ event_type: 'interaction.complete' }
|
|
641
|
+
>;
|
|
642
|
+
const interaction = event.interaction as {
|
|
643
|
+
id?: string;
|
|
644
|
+
status?: GoogleInteractionsStatus;
|
|
645
|
+
usage?: GoogleInteractionsUsage;
|
|
646
|
+
service_tier?: string;
|
|
647
|
+
};
|
|
648
|
+
if (interaction?.id != null && interaction.id.length > 0) {
|
|
649
|
+
interactionId = interaction.id;
|
|
650
|
+
}
|
|
651
|
+
if (interaction?.status != null) {
|
|
652
|
+
finishStatus = interaction.status;
|
|
653
|
+
}
|
|
654
|
+
if (interaction?.usage != null) {
|
|
655
|
+
usage = interaction.usage;
|
|
656
|
+
}
|
|
657
|
+
/*
|
|
658
|
+
* The Interactions API surfaces the applied service tier on
|
|
659
|
+
* `interaction.complete.interaction.service_tier` (NOT on the
|
|
660
|
+
* `x-gemini-service-tier` HTTP header that `:generateContent`
|
|
661
|
+
* uses). Body wins over header fallback.
|
|
662
|
+
*/
|
|
663
|
+
if (interaction?.service_tier != null) {
|
|
664
|
+
serviceTier = interaction.service_tier;
|
|
665
|
+
}
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
case 'error': {
|
|
670
|
+
const event = value as Extract<
|
|
671
|
+
GoogleInteractionsEvent,
|
|
672
|
+
{ event_type: 'error' }
|
|
673
|
+
>;
|
|
674
|
+
finishStatus = 'failed';
|
|
675
|
+
const errorPayload = event.error ?? {
|
|
676
|
+
message: 'Unknown interaction error',
|
|
677
|
+
};
|
|
678
|
+
controller.enqueue({ type: 'error', error: errorPayload });
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
default:
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
flush(controller) {
|
|
688
|
+
const finishReason: LanguageModelV3FinishReason = {
|
|
689
|
+
unified: mapGoogleInteractionsFinishReason({
|
|
690
|
+
status: finishStatus,
|
|
691
|
+
hasFunctionCall,
|
|
692
|
+
}),
|
|
693
|
+
raw: finishStatus,
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const providerMetadata: SharedV3ProviderMetadata = {
|
|
697
|
+
google: {
|
|
698
|
+
...(interactionId != null ? { interactionId } : {}),
|
|
699
|
+
...(serviceTier != null ? { serviceTier } : {}),
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
controller.enqueue({
|
|
704
|
+
type: 'finish',
|
|
705
|
+
finishReason,
|
|
706
|
+
usage: convertGoogleInteractionsUsage(usage),
|
|
707
|
+
providerMetadata,
|
|
708
|
+
});
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
}
|