@ai-sdk/google 4.0.0-beta.8 → 4.0.0-beta.82

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +608 -5
  2. package/README.md +6 -4
  3. package/dist/index.d.ts +297 -54
  4. package/dist/index.js +5409 -640
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +97 -26
  7. package/dist/internal/index.js +1653 -453
  8. package/dist/internal/index.js.map +1 -1
  9. package/docs/{15-google-generative-ai.mdx → 15-google.mdx} +784 -69
  10. package/package.json +16 -17
  11. package/src/{convert-google-generative-ai-usage.ts → convert-google-usage.ts} +13 -5
  12. package/src/convert-json-schema-to-openapi-schema.ts +1 -1
  13. package/src/convert-to-google-messages.ts +647 -0
  14. package/src/{google-generative-ai-embedding-options.ts → google-embedding-model-options.ts} +9 -2
  15. package/src/{google-generative-ai-embedding-model.ts → google-embedding-model.ts} +31 -18
  16. package/src/google-error.ts +1 -1
  17. package/src/google-files.ts +225 -0
  18. package/src/google-image-model-options.ts +35 -0
  19. package/src/{google-generative-ai-image-model.ts → google-image-model.ts} +116 -65
  20. package/src/{google-generative-ai-image-settings.ts → google-image-settings.ts} +2 -2
  21. package/src/google-json-accumulator.ts +371 -0
  22. package/src/{google-generative-ai-options.ts → google-language-model-options.ts} +50 -5
  23. package/src/{google-generative-ai-language-model.ts → google-language-model.ts} +691 -217
  24. package/src/google-prepare-tools.ts +72 -12
  25. package/src/google-prompt.ts +86 -0
  26. package/src/google-provider.ts +157 -53
  27. package/src/google-speech-api.ts +36 -0
  28. package/src/google-speech-model-options.ts +48 -0
  29. package/src/google-speech-model.ts +311 -0
  30. package/src/google-video-model-options.ts +43 -0
  31. package/src/{google-generative-ai-video-model.ts → google-video-model.ts} +25 -60
  32. package/src/{google-generative-ai-video-settings.ts → google-video-settings.ts} +2 -1
  33. package/src/index.ts +40 -9
  34. package/src/interactions/build-google-interactions-stream-transform.ts +818 -0
  35. package/src/interactions/cancel-google-interaction.ts +60 -0
  36. package/src/interactions/convert-google-interactions-usage.ts +47 -0
  37. package/src/interactions/convert-to-google-interactions-input.ts +557 -0
  38. package/src/interactions/extract-google-interactions-sources.ts +252 -0
  39. package/src/interactions/google-interactions-agent.ts +15 -0
  40. package/src/interactions/google-interactions-api.ts +530 -0
  41. package/src/interactions/google-interactions-language-model-options.ts +262 -0
  42. package/src/interactions/google-interactions-language-model.ts +776 -0
  43. package/src/interactions/google-interactions-prompt.ts +582 -0
  44. package/src/interactions/google-interactions-provider-metadata.ts +23 -0
  45. package/src/interactions/map-google-interactions-finish-reason.ts +31 -0
  46. package/src/interactions/parse-google-interactions-outputs.ts +252 -0
  47. package/src/interactions/poll-google-interactions.ts +129 -0
  48. package/src/interactions/prepare-google-interactions-tools.ts +245 -0
  49. package/src/interactions/stream-google-interactions.ts +242 -0
  50. package/src/interactions/synthesize-google-interactions-agent-stream.ts +185 -0
  51. package/src/internal/index.ts +3 -2
  52. package/src/{map-google-generative-ai-finish-reason.ts → map-google-finish-reason.ts} +3 -3
  53. package/src/realtime/google-realtime-event-mapper.ts +383 -0
  54. package/src/realtime/google-realtime-model-options.ts +3 -0
  55. package/src/realtime/google-realtime-model.ts +160 -0
  56. package/src/realtime/index.ts +2 -0
  57. package/src/tool/code-execution.ts +2 -2
  58. package/src/tool/enterprise-web-search.ts +9 -3
  59. package/src/tool/file-search.ts +5 -7
  60. package/src/tool/google-maps.ts +3 -2
  61. package/src/tool/google-search.ts +11 -12
  62. package/src/tool/url-context.ts +4 -2
  63. package/src/tool/vertex-rag-store.ts +9 -6
  64. package/dist/index.d.mts +0 -384
  65. package/dist/index.mjs +0 -2519
  66. package/dist/index.mjs.map +0 -1
  67. package/dist/internal/index.d.mts +0 -287
  68. package/dist/internal/index.mjs +0 -1708
  69. package/dist/internal/index.mjs.map +0 -1
  70. package/src/convert-to-google-generative-ai-messages.ts +0 -239
  71. package/src/google-generative-ai-prompt.ts +0 -47
@@ -0,0 +1,776 @@
1
+ import type {
2
+ LanguageModelV4,
3
+ LanguageModelV4CallOptions,
4
+ LanguageModelV4FinishReason,
5
+ LanguageModelV4GenerateResult,
6
+ LanguageModelV4StreamResult,
7
+ SharedV4ProviderMetadata,
8
+ SharedV4Warning,
9
+ } from '@ai-sdk/provider';
10
+ import {
11
+ combineHeaders,
12
+ createEventSourceResponseHandler,
13
+ createJsonResponseHandler,
14
+ generateId as defaultGenerateId,
15
+ parseProviderOptions,
16
+ postJsonToApi,
17
+ resolve,
18
+ serializeModelOptions,
19
+ WORKFLOW_DESERIALIZE,
20
+ WORKFLOW_SERIALIZE,
21
+ type FetchFunction,
22
+ type Resolvable,
23
+ } from '@ai-sdk/provider-utils';
24
+ import { googleFailedResponseHandler } from '../google-error';
25
+ import { buildGoogleInteractionsStreamTransform } from './build-google-interactions-stream-transform';
26
+ import { convertGoogleInteractionsUsage } from './convert-google-interactions-usage';
27
+ import { convertToGoogleInteractionsInput } from './convert-to-google-interactions-input';
28
+ import {
29
+ googleInteractionsEventSchema,
30
+ googleInteractionsResponseSchema,
31
+ } from './google-interactions-api';
32
+ import {
33
+ googleInteractionsLanguageModelOptions,
34
+ type GoogleInteractionsModelId,
35
+ } from './google-interactions-language-model-options';
36
+ import type {
37
+ GoogleInteractionsAgentConfig,
38
+ GoogleInteractionsEnvironmentSource,
39
+ GoogleInteractionsGenerationConfig,
40
+ GoogleInteractionsNetworkAllowlistEntry,
41
+ GoogleInteractionsNetworkConfig,
42
+ GoogleInteractionsRequestBody,
43
+ GoogleInteractionsResponseFormatEntry,
44
+ GoogleInteractionsTool,
45
+ GoogleInteractionsToolChoice,
46
+ } from './google-interactions-prompt';
47
+ import { mapGoogleInteractionsFinishReason } from './map-google-interactions-finish-reason';
48
+ import { parseGoogleInteractionsOutputs } from './parse-google-interactions-outputs';
49
+ import {
50
+ isTerminalStatus,
51
+ pollGoogleInteractionUntilTerminal,
52
+ } from './poll-google-interactions';
53
+ import { prepareGoogleInteractionsTools } from './prepare-google-interactions-tools';
54
+ import { streamGoogleInteractionEvents } from './stream-google-interactions';
55
+ import { synthesizeGoogleInteractionsAgentStream } from './synthesize-google-interactions-agent-stream';
56
+
57
+ export type GoogleInteractionsConfig = {
58
+ provider: string;
59
+ baseURL: string;
60
+ headers?: Resolvable<Record<string, string | undefined>>;
61
+ fetch?: FetchFunction;
62
+ generateId: () => string;
63
+ supportedUrls?: () => LanguageModelV4['supportedUrls'];
64
+ };
65
+
66
+ export type GoogleInteractionsModelInput =
67
+ | GoogleInteractionsModelId
68
+ | { agent: string }
69
+ | { managedAgent: string };
70
+
71
+ export class GoogleInteractionsLanguageModel implements LanguageModelV4 {
72
+ readonly specificationVersion = 'v4';
73
+
74
+ readonly modelId: string;
75
+
76
+ /**
77
+ * Optional agent name. When provided, the request body sends `agent:` instead
78
+ * of `model:` and rejects `tools` / `generation_config` (warned, not thrown).
79
+ */
80
+ readonly agent: string | undefined;
81
+
82
+ private readonly config: GoogleInteractionsConfig;
83
+
84
+ static [WORKFLOW_SERIALIZE](model: GoogleInteractionsLanguageModel) {
85
+ return {
86
+ ...serializeModelOptions({
87
+ modelId: model.modelId,
88
+ config: model.config,
89
+ }),
90
+ agent: model.agent,
91
+ };
92
+ }
93
+
94
+ static [WORKFLOW_DESERIALIZE](options: {
95
+ modelId: string;
96
+ agent?: string;
97
+ config: GoogleInteractionsConfig;
98
+ }) {
99
+ return new GoogleInteractionsLanguageModel(
100
+ options.agent != null ? { agent: options.agent } : options.modelId,
101
+ options.config,
102
+ );
103
+ }
104
+
105
+ constructor(
106
+ modelOrAgent: GoogleInteractionsModelInput,
107
+ config: GoogleInteractionsConfig,
108
+ ) {
109
+ if (typeof modelOrAgent === 'string') {
110
+ this.modelId = modelOrAgent;
111
+ this.agent = undefined;
112
+ } else if ('managedAgent' in modelOrAgent) {
113
+ this.modelId = modelOrAgent.managedAgent;
114
+ this.agent = modelOrAgent.managedAgent;
115
+ } else {
116
+ this.modelId = modelOrAgent.agent;
117
+ this.agent = modelOrAgent.agent;
118
+ }
119
+ this.config = config;
120
+ }
121
+
122
+ get provider(): string {
123
+ return this.config.provider;
124
+ }
125
+
126
+ get supportedUrls() {
127
+ if (this.config.supportedUrls) {
128
+ return this.config.supportedUrls();
129
+ }
130
+ return {
131
+ 'image/*': [/^https?:\/\/.+/],
132
+ 'application/pdf': [/^https?:\/\/.+/],
133
+ 'audio/*': [/^https?:\/\/.+/],
134
+ 'video/*': [
135
+ /^https?:\/\/(www\.)?youtube\.com\/watch\?v=.+/,
136
+ /^https?:\/\/youtu\.be\/.+/,
137
+ /^gs:\/\/.+/,
138
+ ],
139
+ };
140
+ }
141
+
142
+ private async getArgs(options: LanguageModelV4CallOptions) {
143
+ const warnings: Array<SharedV4Warning> = [];
144
+
145
+ const googleOptions = await parseProviderOptions({
146
+ provider: 'google',
147
+ providerOptions: options.providerOptions,
148
+ schema: googleInteractionsLanguageModelOptions,
149
+ });
150
+
151
+ const isAgent = this.agent != null;
152
+
153
+ const hasTools = options.tools != null && options.tools.length > 0;
154
+
155
+ let toolsForBody: Array<GoogleInteractionsTool> | undefined;
156
+ let toolChoiceForBody: GoogleInteractionsToolChoice | undefined;
157
+
158
+ if (hasTools && isAgent) {
159
+ warnings.push({
160
+ type: 'other',
161
+ message:
162
+ 'google.interactions: tools are not supported when an agent is set; tools will be omitted from the request body.',
163
+ });
164
+ } else if (hasTools) {
165
+ const prepared = prepareGoogleInteractionsTools({
166
+ tools: options.tools,
167
+ toolChoice: options.toolChoice,
168
+ });
169
+ toolsForBody = prepared.tools;
170
+ toolChoiceForBody = prepared.toolChoice;
171
+ warnings.push(...prepared.toolWarnings);
172
+ }
173
+
174
+ /*
175
+ * `response_format` is a polymorphic array of entries. Three sources
176
+ * contribute, in order:
177
+ *
178
+ * 1. AI SDK call-level `responseFormat: { type: 'json', schema }` →
179
+ * `{ type: 'text', mime_type: 'application/json', schema }`.
180
+ * 2. `providerOptions.google.responseFormat` (primary path) — entries
181
+ * are appended verbatim with camelCase → snake_case translation.
182
+ * 3. `providerOptions.google.imageConfig` (deprecated fallback) — only
183
+ * contributes if no `{type:'image'}` entry was already provided via
184
+ * sources 1 or 2; emits a deprecation warning when used.
185
+ *
186
+ * Agent calls cannot send `generation_config` and (per the API) cannot
187
+ * combine with structured output — emit a warning and drop the field.
188
+ */
189
+ const responseFormatEntries: Array<GoogleInteractionsResponseFormatEntry> =
190
+ [];
191
+ if (options.responseFormat?.type === 'json') {
192
+ if (isAgent) {
193
+ warnings.push({
194
+ type: 'other',
195
+ message:
196
+ 'google.interactions: structured output (responseFormat) is not supported when an agent is set; responseFormat will be ignored.',
197
+ });
198
+ } else {
199
+ const entry: GoogleInteractionsResponseFormatEntry = {
200
+ type: 'text',
201
+ mime_type: 'application/json',
202
+ ...(options.responseFormat.schema != null
203
+ ? { schema: options.responseFormat.schema }
204
+ : {}),
205
+ };
206
+ responseFormatEntries.push(entry);
207
+ }
208
+ }
209
+
210
+ if (googleOptions?.responseFormat != null) {
211
+ for (const entry of googleOptions.responseFormat) {
212
+ if (entry.type === 'text') {
213
+ responseFormatEntries.push(
214
+ pruneUndefined({
215
+ type: 'text' as const,
216
+ mime_type: entry.mimeType ?? undefined,
217
+ schema: entry.schema ?? undefined,
218
+ }),
219
+ );
220
+ } else if (entry.type === 'image') {
221
+ responseFormatEntries.push(
222
+ pruneUndefined({
223
+ type: 'image' as const,
224
+ mime_type: entry.mimeType ?? undefined,
225
+ aspect_ratio: entry.aspectRatio ?? undefined,
226
+ image_size: entry.imageSize ?? undefined,
227
+ }),
228
+ );
229
+ } else if (entry.type === 'audio') {
230
+ responseFormatEntries.push(
231
+ pruneUndefined({
232
+ type: 'audio' as const,
233
+ mime_type: entry.mimeType ?? undefined,
234
+ }),
235
+ );
236
+ }
237
+ }
238
+ }
239
+
240
+ const {
241
+ input,
242
+ systemInstruction: convertedSystemInstruction,
243
+ warnings: convWarnings,
244
+ } = convertToGoogleInteractionsInput({
245
+ prompt: options.prompt,
246
+ previousInteractionId: googleOptions?.previousInteractionId ?? undefined,
247
+ store: googleOptions?.store ?? undefined,
248
+ mediaResolution: googleOptions?.mediaResolution ?? undefined,
249
+ });
250
+
251
+ warnings.push(...convWarnings);
252
+
253
+ let systemInstruction = convertedSystemInstruction;
254
+ const optionSystemInstruction =
255
+ googleOptions?.systemInstruction ?? undefined;
256
+ if (systemInstruction != null && optionSystemInstruction != null) {
257
+ warnings.push({
258
+ type: 'other',
259
+ message:
260
+ 'google.interactions: both AI SDK system message and providerOptions.google.systemInstruction were set; using the AI SDK system message.',
261
+ });
262
+ } else if (systemInstruction == null && optionSystemInstruction != null) {
263
+ systemInstruction = optionSystemInstruction;
264
+ }
265
+
266
+ /*
267
+ * The Interactions API splits per-call config into `generation_config`
268
+ * (model branch) and `agent_config` (agent branch); the two are mutually
269
+ * exclusive. The AI SDK call-level generation params and the thinking /
270
+ * imageConfig provider options flow into `generation_config`.
271
+ *
272
+ * When an agent is set, none of these fields are accepted by the API.
273
+ * Emit a single `LanguageModelV4CallWarning` listing the dropped field
274
+ * names and continue (do not throw); the agent-only `agent_config`
275
+ * field supersedes them.
276
+ */
277
+ let generationConfig: GoogleInteractionsGenerationConfig | undefined;
278
+ if (isAgent) {
279
+ const droppedFields: Array<string> = [];
280
+ if (options.temperature != null) droppedFields.push('temperature');
281
+ if (options.topP != null) droppedFields.push('topP');
282
+ if (options.seed != null) droppedFields.push('seed');
283
+ if (options.stopSequences != null && options.stopSequences.length > 0) {
284
+ droppedFields.push('stopSequences');
285
+ }
286
+ if (options.maxOutputTokens != null)
287
+ droppedFields.push('maxOutputTokens');
288
+ if (googleOptions?.thinkingLevel != null)
289
+ droppedFields.push('thinkingLevel');
290
+ if (googleOptions?.thinkingSummaries != null) {
291
+ droppedFields.push('thinkingSummaries');
292
+ }
293
+ if (googleOptions?.imageConfig != null) droppedFields.push('imageConfig');
294
+ if (droppedFields.length > 0) {
295
+ warnings.push({
296
+ type: 'other',
297
+ message: `google.interactions: ${droppedFields.join(', ')} ${droppedFields.length === 1 ? 'is' : 'are'} not supported when an agent is set; use providerOptions.google.agentConfig instead. Dropped from the request body.`,
298
+ });
299
+ }
300
+ generationConfig = undefined;
301
+ } else {
302
+ generationConfig = pruneUndefined({
303
+ temperature: options.temperature ?? undefined,
304
+ top_p: options.topP ?? undefined,
305
+ seed: options.seed ?? undefined,
306
+ stop_sequences:
307
+ options.stopSequences != null && options.stopSequences.length > 0
308
+ ? options.stopSequences
309
+ : undefined,
310
+ max_output_tokens: options.maxOutputTokens ?? undefined,
311
+ thinking_level: googleOptions?.thinkingLevel ?? undefined,
312
+ thinking_summaries: googleOptions?.thinkingSummaries ?? undefined,
313
+ tool_choice: toolChoiceForBody,
314
+ });
315
+
316
+ /*
317
+ * Deprecated fallback path: `imageConfig` contributes an image entry
318
+ * only when none was supplied via `responseFormat`. A warning is
319
+ * always emitted when `imageConfig` is set so callers migrate to the
320
+ * `responseFormat` shape.
321
+ */
322
+ if (googleOptions?.imageConfig != null) {
323
+ const alreadyHasImageEntry = responseFormatEntries.some(
324
+ entry => entry.type === 'image',
325
+ );
326
+ warnings.push({
327
+ type: 'other',
328
+ message: alreadyHasImageEntry
329
+ ? 'google.interactions: providerOptions.google.imageConfig is deprecated and was ignored because providerOptions.google.responseFormat already supplies an image entry. Use responseFormat exclusively.'
330
+ : 'google.interactions: providerOptions.google.imageConfig is deprecated. Use providerOptions.google.responseFormat with a { type: "image", ... } entry instead.',
331
+ });
332
+ if (!alreadyHasImageEntry) {
333
+ responseFormatEntries.push({
334
+ type: 'image',
335
+ mime_type: 'image/png',
336
+ ...(googleOptions.imageConfig.aspectRatio != null
337
+ ? { aspect_ratio: googleOptions.imageConfig.aspectRatio }
338
+ : {}),
339
+ ...(googleOptions.imageConfig.imageSize != null
340
+ ? { image_size: googleOptions.imageConfig.imageSize }
341
+ : {}),
342
+ });
343
+ }
344
+ }
345
+ }
346
+
347
+ let agentConfig: GoogleInteractionsAgentConfig | undefined;
348
+ if (isAgent && googleOptions?.agentConfig != null) {
349
+ const agentConfigOptions = googleOptions.agentConfig;
350
+ if (agentConfigOptions.type === 'deep-research') {
351
+ agentConfig = pruneUndefined({
352
+ type: 'deep-research',
353
+ thinking_summaries: agentConfigOptions.thinkingSummaries ?? undefined,
354
+ visualization: agentConfigOptions.visualization ?? undefined,
355
+ collaborative_planning:
356
+ agentConfigOptions.collaborativePlanning ?? undefined,
357
+ }) as GoogleInteractionsAgentConfig;
358
+ } else if (agentConfigOptions.type === 'dynamic') {
359
+ agentConfig = { type: 'dynamic' };
360
+ }
361
+ }
362
+
363
+ let environment: GoogleInteractionsRequestBody['environment'];
364
+ if (googleOptions?.environment != null) {
365
+ if (!isAgent) {
366
+ warnings.push({
367
+ type: 'other',
368
+ message:
369
+ 'google.interactions: environment is only supported when an agent is set; environment will be omitted from the request body.',
370
+ });
371
+ } else if (typeof googleOptions.environment === 'string') {
372
+ environment = googleOptions.environment;
373
+ } else {
374
+ const environmentOptions = googleOptions.environment;
375
+ const sources: Array<GoogleInteractionsEnvironmentSource> | undefined =
376
+ environmentOptions.sources?.map(source => {
377
+ if (source.type === 'inline') {
378
+ return {
379
+ type: 'inline' as const,
380
+ content: source.content,
381
+ target: source.target,
382
+ };
383
+ }
384
+ return pruneUndefined({
385
+ type: source.type,
386
+ source: source.source,
387
+ target: source.target ?? undefined,
388
+ }) as GoogleInteractionsEnvironmentSource;
389
+ });
390
+ let network: GoogleInteractionsNetworkConfig | undefined;
391
+ if (environmentOptions.network === 'disabled') {
392
+ network = 'disabled';
393
+ } else if (environmentOptions.network != null) {
394
+ network = {
395
+ allowlist: environmentOptions.network.allowlist.map(entry =>
396
+ pruneUndefined({
397
+ domain: entry.domain,
398
+ transform: entry.transform ?? undefined,
399
+ }),
400
+ ) as Array<GoogleInteractionsNetworkAllowlistEntry>,
401
+ };
402
+ }
403
+ environment = pruneUndefined({
404
+ type: 'remote' as const,
405
+ sources: sources != null && sources.length > 0 ? sources : undefined,
406
+ network,
407
+ });
408
+ }
409
+ }
410
+
411
+ /*
412
+ * `background` is opt-in via `providerOptions.google.background`. Some
413
+ * agents require it because their server-side workflow cannot complete
414
+ * within a single request; others reject it. When `background: true`, the
415
+ * POST returns a non-terminal status and the SDK polls
416
+ * `GET /interactions/{id}` until the work completes.
417
+ */
418
+ const args: GoogleInteractionsRequestBody = pruneUndefined({
419
+ ...(isAgent ? { agent: this.agent } : { model: this.modelId }),
420
+ input,
421
+ system_instruction: systemInstruction,
422
+ tools: toolsForBody,
423
+ response_format:
424
+ responseFormatEntries.length > 0 ? responseFormatEntries : undefined,
425
+ response_modalities:
426
+ googleOptions?.responseModalities != null
427
+ ? (googleOptions.responseModalities as Array<
428
+ 'text' | 'image' | 'audio' | 'video' | 'document'
429
+ >)
430
+ : undefined,
431
+ previous_interaction_id:
432
+ googleOptions?.previousInteractionId ?? undefined,
433
+ service_tier: googleOptions?.serviceTier ?? undefined,
434
+ store: googleOptions?.store ?? undefined,
435
+ generation_config:
436
+ generationConfig != null && Object.keys(generationConfig).length > 0
437
+ ? generationConfig
438
+ : undefined,
439
+ agent_config: agentConfig,
440
+ environment,
441
+ background: googleOptions?.background ?? undefined,
442
+ });
443
+
444
+ return {
445
+ args,
446
+ warnings,
447
+ isAgent,
448
+ isBackground: googleOptions?.background === true,
449
+ pollingTimeoutMs: googleOptions?.pollingTimeoutMs ?? undefined,
450
+ };
451
+ }
452
+
453
+ async doGenerate(
454
+ options: LanguageModelV4CallOptions,
455
+ ): Promise<LanguageModelV4GenerateResult> {
456
+ const { args, warnings, isAgent, pollingTimeoutMs } =
457
+ await this.getArgs(options);
458
+
459
+ const url = `${this.config.baseURL}/interactions`;
460
+
461
+ const mergedHeaders = combineHeaders(
462
+ INTERACTIONS_API_REVISION_HEADER,
463
+ this.config.headers ? await resolve(this.config.headers) : undefined,
464
+ options.headers,
465
+ );
466
+
467
+ const postResult = await postJsonToApi({
468
+ url,
469
+ headers: mergedHeaders,
470
+ body: args,
471
+ failedResponseHandler: googleFailedResponseHandler,
472
+ successfulResponseHandler: createJsonResponseHandler(
473
+ googleInteractionsResponseSchema,
474
+ ),
475
+ abortSignal: options.abortSignal,
476
+ fetch: this.config.fetch,
477
+ });
478
+
479
+ let {
480
+ responseHeaders,
481
+ value: response,
482
+ rawValue: rawResponse,
483
+ } = postResult;
484
+
485
+ /*
486
+ * Agent calls may return a non-terminal status (`in_progress` /
487
+ * `requires_action`) when invoked with `background: true`. Poll
488
+ * `GET /interactions/{id}` until terminal so the user-facing surface
489
+ * matches a synchronous call.
490
+ */
491
+ if (isAgent && !isTerminalStatus(response.status)) {
492
+ const polled = await pollGoogleInteractionUntilTerminal({
493
+ baseURL: this.config.baseURL,
494
+ interactionId: response.id,
495
+ headers: mergedHeaders,
496
+ fetch: this.config.fetch,
497
+ abortSignal: options.abortSignal,
498
+ timeoutMs: pollingTimeoutMs,
499
+ });
500
+ response = polled.response;
501
+ rawResponse = polled.rawResponse;
502
+ responseHeaders = polled.responseHeaders ?? responseHeaders;
503
+ }
504
+
505
+ /*
506
+ * `response.id` is omitted when `store: false` (fully stateless mode), and
507
+ * the stream surface returns `id: ""` (empty string) for the same case.
508
+ * Normalize both to `undefined` so downstream stamping does not pollute
509
+ * provider metadata with an empty/missing identifier.
510
+ */
511
+ const interactionId =
512
+ typeof response.id === 'string' && response.id.length > 0
513
+ ? response.id
514
+ : undefined;
515
+
516
+ const { content, hasFunctionCall } = parseGoogleInteractionsOutputs({
517
+ steps: response.steps ?? null,
518
+ generateId: this.config.generateId ?? defaultGenerateId,
519
+ interactionId,
520
+ });
521
+
522
+ const finishReason: LanguageModelV4FinishReason = {
523
+ unified: mapGoogleInteractionsFinishReason({
524
+ status: response.status,
525
+ hasFunctionCall,
526
+ }),
527
+ raw: response.status,
528
+ };
529
+
530
+ /*
531
+ * Service tier divergence vs. `:generateContent`:
532
+ *
533
+ * `google-language-model.ts` reads the applied service tier from the
534
+ * `x-gemini-service-tier` HTTP response header (see commit 1adfb76d2d).
535
+ * The Interactions API does NOT surface that header; it returns the
536
+ * applied tier in the response body as `service_tier` on the top-level
537
+ * Interaction object (and on `interaction.complete.interaction` for
538
+ * streaming). The `responseHeaders` parameter is also checked as a
539
+ * defensive fallback in case the API later adds the header.
540
+ */
541
+ const serviceTier =
542
+ response.service_tier ??
543
+ responseHeaders?.['x-gemini-service-tier'] ??
544
+ undefined;
545
+
546
+ /*
547
+ * `response.id` is omitted when `store: false` (fully stateless mode), so
548
+ * `interactionId` is only surfaced when the API actually returned one.
549
+ */
550
+ const providerMetadata: SharedV4ProviderMetadata = {
551
+ google: {
552
+ ...(interactionId != null ? { interactionId } : {}),
553
+ ...(serviceTier != null ? { serviceTier } : {}),
554
+ },
555
+ };
556
+
557
+ let timestamp: Date | undefined;
558
+ if (typeof response.created === 'string') {
559
+ const parsed = new Date(response.created);
560
+ if (!Number.isNaN(parsed.getTime())) {
561
+ timestamp = parsed;
562
+ }
563
+ }
564
+
565
+ return {
566
+ content,
567
+ finishReason,
568
+ usage: convertGoogleInteractionsUsage(response.usage),
569
+ warnings,
570
+ providerMetadata,
571
+ request: { body: args },
572
+ response: {
573
+ headers: responseHeaders,
574
+ body: rawResponse,
575
+ ...(interactionId != null ? { id: interactionId } : {}),
576
+ ...(timestamp ? { timestamp } : {}),
577
+ modelId: response.model ?? undefined,
578
+ },
579
+ };
580
+ }
581
+
582
+ async doStream(
583
+ options: LanguageModelV4CallOptions,
584
+ ): Promise<LanguageModelV4StreamResult> {
585
+ const { args, warnings, isBackground, pollingTimeoutMs } =
586
+ await this.getArgs(options);
587
+
588
+ const url = `${this.config.baseURL}/interactions`;
589
+
590
+ const mergedHeaders = combineHeaders(
591
+ INTERACTIONS_API_REVISION_HEADER,
592
+ this.config.headers ? await resolve(this.config.headers) : undefined,
593
+ options.headers,
594
+ );
595
+
596
+ /*
597
+ * `background: true` is incompatible with `stream: true` on POST. Drive
598
+ * background calls via POST background -> GET stream (with terminal-status
599
+ * short-circuit). The user-facing stream surface stays identical --
600
+ * text-start / text-delta / text-end / finish parts are emitted in the
601
+ * same order as a true SSE response.
602
+ */
603
+ if (isBackground) {
604
+ return this.doStreamBackground({
605
+ args,
606
+ warnings,
607
+ url,
608
+ mergedHeaders,
609
+ options,
610
+ pollingTimeoutMs,
611
+ });
612
+ }
613
+
614
+ const body = { ...args, stream: true };
615
+
616
+ const { responseHeaders, value: response } = await postJsonToApi({
617
+ url,
618
+ headers: mergedHeaders,
619
+ body,
620
+ failedResponseHandler: googleFailedResponseHandler,
621
+ successfulResponseHandler: createEventSourceResponseHandler(
622
+ googleInteractionsEventSchema,
623
+ ),
624
+ abortSignal: options.abortSignal,
625
+ fetch: this.config.fetch,
626
+ });
627
+
628
+ /*
629
+ * Google's API surfaces the applied service tier in the
630
+ * `x-gemini-service-tier` HTTP response header, not in the response body.
631
+ * Mirror the canonical pattern from `google-language-model.ts` (commit
632
+ * 1adfb76d2d) and pipe it through the stream transformer so the `finish`
633
+ * part's `providerMetadata.google.serviceTier` is sourced from the header.
634
+ */
635
+ const headerServiceTier = responseHeaders?.['x-gemini-service-tier'];
636
+
637
+ const transform = buildGoogleInteractionsStreamTransform({
638
+ warnings,
639
+ generateId: this.config.generateId ?? defaultGenerateId,
640
+ includeRawChunks: options.includeRawChunks,
641
+ serviceTier: headerServiceTier,
642
+ });
643
+
644
+ return {
645
+ stream: response.pipeThrough(transform),
646
+ request: { body },
647
+ response: { headers: responseHeaders },
648
+ };
649
+ }
650
+
651
+ /*
652
+ * Drive the streaming surface for agent calls. Agents require
653
+ * `background: true`, which is incompatible with `stream: true` on POST.
654
+ *
655
+ * Approach:
656
+ * 1. POST `/interactions` with `background: true`. The response includes
657
+ * the interaction id and an initial (usually non-terminal) status.
658
+ * 2. If the POST status is already terminal (rare), synthesize a stream
659
+ * from the polled outputs and we're done.
660
+ * 3. Otherwise open `GET /interactions/{id}?stream=true` and pipe the
661
+ * SSE events through `buildGoogleInteractionsStreamTransform` so the
662
+ * consumer receives text deltas / thinking summaries / tool events as
663
+ * they happen instead of all at once at the end.
664
+ *
665
+ * The SSE connection can drop while the agent idles between events
666
+ * (`UND_ERR_BODY_TIMEOUT`); `streamGoogleInteractionEvents` handles the
667
+ * reconnect-with-`last_event_id` loop transparently.
668
+ */
669
+ private async doStreamBackground({
670
+ args,
671
+ warnings,
672
+ url,
673
+ mergedHeaders,
674
+ options,
675
+ pollingTimeoutMs,
676
+ }: {
677
+ args: GoogleInteractionsRequestBody;
678
+ warnings: Array<SharedV4Warning>;
679
+ url: string;
680
+ mergedHeaders: Record<string, string | undefined>;
681
+ options: LanguageModelV4CallOptions;
682
+ pollingTimeoutMs: number | undefined;
683
+ }): Promise<LanguageModelV4StreamResult> {
684
+ const postResult = await postJsonToApi({
685
+ url,
686
+ headers: mergedHeaders,
687
+ body: args,
688
+ failedResponseHandler: googleFailedResponseHandler,
689
+ successfulResponseHandler: createJsonResponseHandler(
690
+ googleInteractionsResponseSchema,
691
+ ),
692
+ abortSignal: options.abortSignal,
693
+ fetch: this.config.fetch,
694
+ });
695
+
696
+ const { responseHeaders: postHeaders, value: postResponse } = postResult;
697
+ const interactionId = postResponse.id;
698
+
699
+ if (interactionId == null || interactionId.length === 0) {
700
+ throw new Error(
701
+ 'google.interactions: background POST response did not include an interaction id; cannot stream the result.',
702
+ );
703
+ }
704
+
705
+ const headerServiceTier = postHeaders?.['x-gemini-service-tier'];
706
+
707
+ /*
708
+ * If the POST already returned a terminal status (e.g. cached, immediate
709
+ * failure, or `incomplete`), there is nothing to stream from the GET --
710
+ * synthesize directly from the response so the caller still gets a
711
+ * complete stream.
712
+ */
713
+ if (isTerminalStatus(postResponse.status)) {
714
+ const synthesized = synthesizeGoogleInteractionsAgentStream({
715
+ response: postResponse,
716
+ warnings,
717
+ generateId: this.config.generateId ?? defaultGenerateId,
718
+ includeRawChunks: options.includeRawChunks,
719
+ headerServiceTier,
720
+ });
721
+ return {
722
+ stream: synthesized,
723
+ request: { body: args },
724
+ response: { headers: postHeaders },
725
+ };
726
+ }
727
+
728
+ /*
729
+ * `pollingTimeoutMs` is unused on the live-SSE path -- there's no poll
730
+ * loop to time out -- but we surface it as the per-attempt timeout for
731
+ * the AbortSignal-driven cancel that the caller already controls. Future
732
+ * iterations may use it as a backstop if the SSE+resume loop spins
733
+ * indefinitely.
734
+ */
735
+ void pollingTimeoutMs;
736
+
737
+ const events = streamGoogleInteractionEvents({
738
+ baseURL: this.config.baseURL,
739
+ interactionId,
740
+ headers: mergedHeaders,
741
+ fetch: this.config.fetch,
742
+ abortSignal: options.abortSignal,
743
+ });
744
+
745
+ const transform = buildGoogleInteractionsStreamTransform({
746
+ warnings,
747
+ generateId: this.config.generateId ?? defaultGenerateId,
748
+ includeRawChunks: options.includeRawChunks,
749
+ serviceTier: headerServiceTier,
750
+ });
751
+
752
+ return {
753
+ stream: events.pipeThrough(transform),
754
+ request: { body: args },
755
+ response: { headers: postHeaders },
756
+ };
757
+ }
758
+ }
759
+
760
+ /*
761
+ * Pins the Interactions API revision the SDK targets. Sent on every request
762
+ * the model issues so model-id calls, agent calls, polling, SSE reconnects,
763
+ * and cancellation all hit the same schema.
764
+ */
765
+ const INTERACTIONS_API_REVISION_HEADER: Record<string, string> = {
766
+ 'Api-Revision': '2026-05-20',
767
+ };
768
+
769
+ function pruneUndefined<T extends Record<string, unknown>>(obj: T): T {
770
+ const result: Record<string, unknown> = {};
771
+ for (const [key, value] of Object.entries(obj)) {
772
+ if (value === undefined) continue;
773
+ result[key] = value;
774
+ }
775
+ return result as T;
776
+ }