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

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 +614 -5
  2. package/README.md +6 -4
  3. package/dist/index.d.ts +297 -54
  4. package/dist/index.js +5404 -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 +765 -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,765 @@
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
+ this.config.headers ? await resolve(this.config.headers) : undefined,
463
+ options.headers,
464
+ );
465
+
466
+ const postResult = await postJsonToApi({
467
+ url,
468
+ headers: mergedHeaders,
469
+ body: args,
470
+ failedResponseHandler: googleFailedResponseHandler,
471
+ successfulResponseHandler: createJsonResponseHandler(
472
+ googleInteractionsResponseSchema,
473
+ ),
474
+ abortSignal: options.abortSignal,
475
+ fetch: this.config.fetch,
476
+ });
477
+
478
+ let {
479
+ responseHeaders,
480
+ value: response,
481
+ rawValue: rawResponse,
482
+ } = postResult;
483
+
484
+ /*
485
+ * Agent calls may return a non-terminal status (`in_progress` /
486
+ * `requires_action`) when invoked with `background: true`. Poll
487
+ * `GET /interactions/{id}` until terminal so the user-facing surface
488
+ * matches a synchronous call.
489
+ */
490
+ if (isAgent && !isTerminalStatus(response.status)) {
491
+ const polled = await pollGoogleInteractionUntilTerminal({
492
+ baseURL: this.config.baseURL,
493
+ interactionId: response.id,
494
+ headers: mergedHeaders,
495
+ fetch: this.config.fetch,
496
+ abortSignal: options.abortSignal,
497
+ timeoutMs: pollingTimeoutMs,
498
+ });
499
+ response = polled.response;
500
+ rawResponse = polled.rawResponse;
501
+ responseHeaders = polled.responseHeaders ?? responseHeaders;
502
+ }
503
+
504
+ /*
505
+ * `response.id` is omitted when `store: false` (fully stateless mode), and
506
+ * the stream surface returns `id: ""` (empty string) for the same case.
507
+ * Normalize both to `undefined` so downstream stamping does not pollute
508
+ * provider metadata with an empty/missing identifier.
509
+ */
510
+ const interactionId =
511
+ typeof response.id === 'string' && response.id.length > 0
512
+ ? response.id
513
+ : undefined;
514
+
515
+ const { content, hasFunctionCall } = parseGoogleInteractionsOutputs({
516
+ steps: response.steps ?? null,
517
+ generateId: this.config.generateId ?? defaultGenerateId,
518
+ interactionId,
519
+ });
520
+
521
+ const finishReason: LanguageModelV4FinishReason = {
522
+ unified: mapGoogleInteractionsFinishReason({
523
+ status: response.status,
524
+ hasFunctionCall,
525
+ }),
526
+ raw: response.status,
527
+ };
528
+
529
+ /*
530
+ * Service tier divergence vs. `:generateContent`:
531
+ *
532
+ * `google-language-model.ts` reads the applied service tier from the
533
+ * `x-gemini-service-tier` HTTP response header (see commit 1adfb76d2d).
534
+ * The Interactions API does NOT surface that header; it returns the
535
+ * applied tier in the response body as `service_tier` on the top-level
536
+ * Interaction object (and on `interaction.complete.interaction` for
537
+ * streaming). The `responseHeaders` parameter is also checked as a
538
+ * defensive fallback in case the API later adds the header.
539
+ */
540
+ const serviceTier =
541
+ response.service_tier ??
542
+ responseHeaders?.['x-gemini-service-tier'] ??
543
+ undefined;
544
+
545
+ /*
546
+ * `response.id` is omitted when `store: false` (fully stateless mode), so
547
+ * `interactionId` is only surfaced when the API actually returned one.
548
+ */
549
+ const providerMetadata: SharedV4ProviderMetadata = {
550
+ google: {
551
+ ...(interactionId != null ? { interactionId } : {}),
552
+ ...(serviceTier != null ? { serviceTier } : {}),
553
+ },
554
+ };
555
+
556
+ let timestamp: Date | undefined;
557
+ if (typeof response.created === 'string') {
558
+ const parsed = new Date(response.created);
559
+ if (!Number.isNaN(parsed.getTime())) {
560
+ timestamp = parsed;
561
+ }
562
+ }
563
+
564
+ return {
565
+ content,
566
+ finishReason,
567
+ usage: convertGoogleInteractionsUsage(response.usage),
568
+ warnings,
569
+ providerMetadata,
570
+ request: { body: args },
571
+ response: {
572
+ headers: responseHeaders,
573
+ body: rawResponse,
574
+ ...(interactionId != null ? { id: interactionId } : {}),
575
+ ...(timestamp ? { timestamp } : {}),
576
+ modelId: response.model ?? undefined,
577
+ },
578
+ };
579
+ }
580
+
581
+ async doStream(
582
+ options: LanguageModelV4CallOptions,
583
+ ): Promise<LanguageModelV4StreamResult> {
584
+ const { args, warnings, isBackground, pollingTimeoutMs } =
585
+ await this.getArgs(options);
586
+
587
+ const url = `${this.config.baseURL}/interactions`;
588
+
589
+ const mergedHeaders = combineHeaders(
590
+ this.config.headers ? await resolve(this.config.headers) : undefined,
591
+ options.headers,
592
+ );
593
+
594
+ /*
595
+ * `background: true` is incompatible with `stream: true` on POST. Drive
596
+ * background calls via POST background -> GET stream (with terminal-status
597
+ * short-circuit). The user-facing stream surface stays identical --
598
+ * text-start / text-delta / text-end / finish parts are emitted in the
599
+ * same order as a true SSE response.
600
+ */
601
+ if (isBackground) {
602
+ return this.doStreamBackground({
603
+ args,
604
+ warnings,
605
+ url,
606
+ mergedHeaders,
607
+ options,
608
+ pollingTimeoutMs,
609
+ });
610
+ }
611
+
612
+ const body = { ...args, stream: true };
613
+
614
+ const { responseHeaders, value: response } = await postJsonToApi({
615
+ url,
616
+ headers: mergedHeaders,
617
+ body,
618
+ failedResponseHandler: googleFailedResponseHandler,
619
+ successfulResponseHandler: createEventSourceResponseHandler(
620
+ googleInteractionsEventSchema,
621
+ ),
622
+ abortSignal: options.abortSignal,
623
+ fetch: this.config.fetch,
624
+ });
625
+
626
+ /*
627
+ * Google's API surfaces the applied service tier in the
628
+ * `x-gemini-service-tier` HTTP response header, not in the response body.
629
+ * Mirror the canonical pattern from `google-language-model.ts` (commit
630
+ * 1adfb76d2d) and pipe it through the stream transformer so the `finish`
631
+ * part's `providerMetadata.google.serviceTier` is sourced from the header.
632
+ */
633
+ const headerServiceTier = responseHeaders?.['x-gemini-service-tier'];
634
+
635
+ const transform = buildGoogleInteractionsStreamTransform({
636
+ warnings,
637
+ generateId: this.config.generateId ?? defaultGenerateId,
638
+ includeRawChunks: options.includeRawChunks,
639
+ serviceTier: headerServiceTier,
640
+ });
641
+
642
+ return {
643
+ stream: response.pipeThrough(transform),
644
+ request: { body },
645
+ response: { headers: responseHeaders },
646
+ };
647
+ }
648
+
649
+ /*
650
+ * Drive the streaming surface for agent calls. Agents require
651
+ * `background: true`, which is incompatible with `stream: true` on POST.
652
+ *
653
+ * Approach:
654
+ * 1. POST `/interactions` with `background: true`. The response includes
655
+ * the interaction id and an initial (usually non-terminal) status.
656
+ * 2. If the POST status is already terminal (rare), synthesize a stream
657
+ * from the polled outputs and we're done.
658
+ * 3. Otherwise open `GET /interactions/{id}?stream=true` and pipe the
659
+ * SSE events through `buildGoogleInteractionsStreamTransform` so the
660
+ * consumer receives text deltas / thinking summaries / tool events as
661
+ * they happen instead of all at once at the end.
662
+ *
663
+ * The SSE connection can drop while the agent idles between events
664
+ * (`UND_ERR_BODY_TIMEOUT`); `streamGoogleInteractionEvents` handles the
665
+ * reconnect-with-`last_event_id` loop transparently.
666
+ */
667
+ private async doStreamBackground({
668
+ args,
669
+ warnings,
670
+ url,
671
+ mergedHeaders,
672
+ options,
673
+ pollingTimeoutMs,
674
+ }: {
675
+ args: GoogleInteractionsRequestBody;
676
+ warnings: Array<SharedV4Warning>;
677
+ url: string;
678
+ mergedHeaders: Record<string, string | undefined>;
679
+ options: LanguageModelV4CallOptions;
680
+ pollingTimeoutMs: number | undefined;
681
+ }): Promise<LanguageModelV4StreamResult> {
682
+ const postResult = await postJsonToApi({
683
+ url,
684
+ headers: mergedHeaders,
685
+ body: args,
686
+ failedResponseHandler: googleFailedResponseHandler,
687
+ successfulResponseHandler: createJsonResponseHandler(
688
+ googleInteractionsResponseSchema,
689
+ ),
690
+ abortSignal: options.abortSignal,
691
+ fetch: this.config.fetch,
692
+ });
693
+
694
+ const { responseHeaders: postHeaders, value: postResponse } = postResult;
695
+ const interactionId = postResponse.id;
696
+
697
+ if (interactionId == null || interactionId.length === 0) {
698
+ throw new Error(
699
+ 'google.interactions: background POST response did not include an interaction id; cannot stream the result.',
700
+ );
701
+ }
702
+
703
+ const headerServiceTier = postHeaders?.['x-gemini-service-tier'];
704
+
705
+ /*
706
+ * If the POST already returned a terminal status (e.g. cached, immediate
707
+ * failure, or `incomplete`), there is nothing to stream from the GET --
708
+ * synthesize directly from the response so the caller still gets a
709
+ * complete stream.
710
+ */
711
+ if (isTerminalStatus(postResponse.status)) {
712
+ const synthesized = synthesizeGoogleInteractionsAgentStream({
713
+ response: postResponse,
714
+ warnings,
715
+ generateId: this.config.generateId ?? defaultGenerateId,
716
+ includeRawChunks: options.includeRawChunks,
717
+ headerServiceTier,
718
+ });
719
+ return {
720
+ stream: synthesized,
721
+ request: { body: args },
722
+ response: { headers: postHeaders },
723
+ };
724
+ }
725
+
726
+ /*
727
+ * `pollingTimeoutMs` is unused on the live-SSE path -- there's no poll
728
+ * loop to time out -- but we surface it as the per-attempt timeout for
729
+ * the AbortSignal-driven cancel that the caller already controls. Future
730
+ * iterations may use it as a backstop if the SSE+resume loop spins
731
+ * indefinitely.
732
+ */
733
+ void pollingTimeoutMs;
734
+
735
+ const events = streamGoogleInteractionEvents({
736
+ baseURL: this.config.baseURL,
737
+ interactionId,
738
+ headers: mergedHeaders,
739
+ fetch: this.config.fetch,
740
+ abortSignal: options.abortSignal,
741
+ });
742
+
743
+ const transform = buildGoogleInteractionsStreamTransform({
744
+ warnings,
745
+ generateId: this.config.generateId ?? defaultGenerateId,
746
+ includeRawChunks: options.includeRawChunks,
747
+ serviceTier: headerServiceTier,
748
+ });
749
+
750
+ return {
751
+ stream: events.pipeThrough(transform),
752
+ request: { body: args },
753
+ response: { headers: postHeaders },
754
+ };
755
+ }
756
+ }
757
+
758
+ function pruneUndefined<T extends Record<string, unknown>>(obj: T): T {
759
+ const result: Record<string, unknown> = {};
760
+ for (const [key, value] of Object.entries(obj)) {
761
+ if (value === undefined) continue;
762
+ result[key] = value;
763
+ }
764
+ return result as T;
765
+ }