@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.
@@ -0,0 +1,609 @@
1
+ import type {
2
+ LanguageModelV3,
3
+ LanguageModelV3CallOptions,
4
+ LanguageModelV3FinishReason,
5
+ LanguageModelV3GenerateResult,
6
+ LanguageModelV3StreamResult,
7
+ SharedV3ProviderMetadata,
8
+ SharedV3Warning,
9
+ } from '@ai-sdk/provider';
10
+ import {
11
+ combineHeaders,
12
+ createEventSourceResponseHandler,
13
+ createJsonResponseHandler,
14
+ generateId as defaultGenerateId,
15
+ parseProviderOptions,
16
+ postJsonToApi,
17
+ resolve,
18
+ type FetchFunction,
19
+ type Resolvable,
20
+ } from '@ai-sdk/provider-utils';
21
+ import { googleFailedResponseHandler } from '../google-error';
22
+ import { buildGoogleInteractionsStreamTransform } from './build-google-interactions-stream-transform';
23
+ import { convertGoogleInteractionsUsage } from './convert-google-interactions-usage';
24
+ import { convertToGoogleInteractionsInput } from './convert-to-google-interactions-input';
25
+ import {
26
+ googleInteractionsEventSchema,
27
+ googleInteractionsResponseSchema,
28
+ } from './google-interactions-api';
29
+ import {
30
+ googleInteractionsLanguageModelOptions,
31
+ type GoogleInteractionsModelId,
32
+ } from './google-interactions-language-model-options';
33
+ import type {
34
+ GoogleInteractionsAgentConfig,
35
+ GoogleInteractionsGenerationConfig,
36
+ GoogleInteractionsRequestBody,
37
+ GoogleInteractionsTool,
38
+ GoogleInteractionsToolChoice,
39
+ } from './google-interactions-prompt';
40
+ import { mapGoogleInteractionsFinishReason } from './map-google-interactions-finish-reason';
41
+ import { parseGoogleInteractionsOutputs } from './parse-google-interactions-outputs';
42
+ import {
43
+ isTerminalStatus,
44
+ pollGoogleInteractionUntilTerminal,
45
+ } from './poll-google-interactions';
46
+ import { prepareGoogleInteractionsTools } from './prepare-google-interactions-tools';
47
+ import { synthesizeGoogleInteractionsAgentStream } from './synthesize-google-interactions-agent-stream';
48
+
49
+ export type GoogleInteractionsConfig = {
50
+ provider: string;
51
+ baseURL: string;
52
+ headers?: Resolvable<Record<string, string | undefined>>;
53
+ fetch?: FetchFunction;
54
+ generateId: () => string;
55
+ supportedUrls?: () => LanguageModelV3['supportedUrls'];
56
+ };
57
+
58
+ export type GoogleInteractionsModelInput =
59
+ | GoogleInteractionsModelId
60
+ | { agent: string };
61
+
62
+ export class GoogleInteractionsLanguageModel implements LanguageModelV3 {
63
+ readonly specificationVersion = 'v3';
64
+
65
+ readonly modelId: string;
66
+
67
+ /**
68
+ * Optional agent name. When provided, the request body sends `agent:` instead
69
+ * of `model:` and rejects `tools` / `generation_config` (warned, not thrown).
70
+ */
71
+ readonly agent: string | undefined;
72
+
73
+ private readonly config: GoogleInteractionsConfig;
74
+
75
+ constructor(
76
+ modelOrAgent: GoogleInteractionsModelInput,
77
+ config: GoogleInteractionsConfig,
78
+ ) {
79
+ if (typeof modelOrAgent === 'string') {
80
+ this.modelId = modelOrAgent;
81
+ this.agent = undefined;
82
+ } else {
83
+ this.modelId = modelOrAgent.agent;
84
+ this.agent = modelOrAgent.agent;
85
+ }
86
+ this.config = config;
87
+ }
88
+
89
+ get provider(): string {
90
+ return this.config.provider;
91
+ }
92
+
93
+ get supportedUrls() {
94
+ if (this.config.supportedUrls) {
95
+ return this.config.supportedUrls();
96
+ }
97
+ return {
98
+ 'image/*': [/^https?:\/\/.+/],
99
+ 'application/pdf': [/^https?:\/\/.+/],
100
+ 'audio/*': [/^https?:\/\/.+/],
101
+ 'video/*': [
102
+ /^https?:\/\/(www\.)?youtube\.com\/watch\?v=.+/,
103
+ /^https?:\/\/youtu\.be\/.+/,
104
+ /^gs:\/\/.+/,
105
+ ],
106
+ };
107
+ }
108
+
109
+ private async getArgs(options: LanguageModelV3CallOptions) {
110
+ const warnings: Array<SharedV3Warning> = [];
111
+
112
+ const opts = await parseProviderOptions({
113
+ provider: 'google',
114
+ providerOptions: options.providerOptions,
115
+ schema: googleInteractionsLanguageModelOptions,
116
+ });
117
+
118
+ const isAgent = this.agent != null;
119
+
120
+ const hasTools = options.tools != null && options.tools.length > 0;
121
+
122
+ let toolsForBody: Array<GoogleInteractionsTool> | undefined;
123
+ let toolChoiceForBody: GoogleInteractionsToolChoice | undefined;
124
+
125
+ if (hasTools && isAgent) {
126
+ warnings.push({
127
+ type: 'other',
128
+ message:
129
+ 'google.interactions: tools are not supported when an agent is set; tools will be omitted from the request body.',
130
+ });
131
+ } else if (hasTools) {
132
+ const prepared = prepareGoogleInteractionsTools({
133
+ tools: options.tools,
134
+ toolChoice: options.toolChoice,
135
+ });
136
+ toolsForBody = prepared.tools;
137
+ toolChoiceForBody = prepared.toolChoice;
138
+ warnings.push(...prepared.toolWarnings);
139
+ }
140
+
141
+ /*
142
+ * Structured output mapping (resolves PRD Open Q1).
143
+ *
144
+ * The Interactions API exposes structured output via two top-level body
145
+ * fields: `response_mime_type` (always `'application/json'` here) and
146
+ * `response_format` (typed as `unknown` in the js-genai SDK). Per the
147
+ * canonical sample at
148
+ * `googleapis/js-genai/sdk-samples/interactions_structured_output_json.ts`,
149
+ * `response_format` accepts a **plain JSON Schema** value directly - no
150
+ * wrapping object, no OpenAPI conversion. The js-genai resource type
151
+ * (`src/interactions/resources/interactions.ts:1399`) confirms the field is
152
+ * passed through verbatim. We therefore send the AI SDK
153
+ * `responseFormat.schema` (a `JSONSchema7`) as-is.
154
+ *
155
+ * If a future API revision rejects plain JSON Schema, fall back to
156
+ * `convertJSONSchemaToOpenAPISchema(...)` (already imported by
157
+ * `google-language-model.ts`); empirically that has not been needed.
158
+ *
159
+ * Agent calls cannot send `generation_config` and (per the API) cannot
160
+ * combine with structured output - emit a warning and drop the field.
161
+ */
162
+ let responseMimeType: string | undefined;
163
+ let responseFormat: unknown | undefined;
164
+ if (options.responseFormat?.type === 'json') {
165
+ if (isAgent) {
166
+ warnings.push({
167
+ type: 'other',
168
+ message:
169
+ 'google.interactions: structured output (responseFormat) is not supported when an agent is set; responseFormat will be ignored.',
170
+ });
171
+ } else {
172
+ responseMimeType = 'application/json';
173
+ if (options.responseFormat.schema != null) {
174
+ responseFormat = options.responseFormat.schema;
175
+ }
176
+ }
177
+ }
178
+
179
+ const {
180
+ input,
181
+ systemInstruction: convertedSystemInstruction,
182
+ warnings: convWarnings,
183
+ } = convertToGoogleInteractionsInput({
184
+ prompt: options.prompt,
185
+ previousInteractionId: opts?.previousInteractionId ?? undefined,
186
+ store: opts?.store ?? undefined,
187
+ mediaResolution: opts?.mediaResolution ?? undefined,
188
+ });
189
+
190
+ warnings.push(...convWarnings);
191
+
192
+ let systemInstruction = convertedSystemInstruction;
193
+ const optionSystemInstruction = opts?.systemInstruction ?? undefined;
194
+ if (systemInstruction != null && optionSystemInstruction != null) {
195
+ warnings.push({
196
+ type: 'other',
197
+ message:
198
+ 'google.interactions: both AI SDK system message and providerOptions.google.systemInstruction were set; using the AI SDK system message.',
199
+ });
200
+ } else if (systemInstruction == null && optionSystemInstruction != null) {
201
+ systemInstruction = optionSystemInstruction;
202
+ }
203
+
204
+ /*
205
+ * The Interactions API splits per-call config into `generation_config`
206
+ * (model branch) and `agent_config` (agent branch); the two are mutually
207
+ * exclusive. We stay minimal here for TASK-1 - only the AI SDK call-level
208
+ * generation params and the thinking/imageConfig provider options flow
209
+ * into `generation_config`. Tool-related fields land here in later tasks.
210
+ *
211
+ * When an agent is set, none of these fields are accepted by the API. Per
212
+ * PRD US 31 we emit a single `LanguageModelV3CallWarning` listing the
213
+ * dropped field names and continue (do not throw); the agent-only
214
+ * `agent_config` field supersedes them.
215
+ */
216
+ let generationConfig: GoogleInteractionsGenerationConfig | undefined;
217
+ if (isAgent) {
218
+ const droppedFields: Array<string> = [];
219
+ if (options.temperature != null) droppedFields.push('temperature');
220
+ if (options.topP != null) droppedFields.push('topP');
221
+ if (options.seed != null) droppedFields.push('seed');
222
+ if (options.stopSequences != null && options.stopSequences.length > 0) {
223
+ droppedFields.push('stopSequences');
224
+ }
225
+ if (options.maxOutputTokens != null)
226
+ droppedFields.push('maxOutputTokens');
227
+ if (opts?.thinkingLevel != null) droppedFields.push('thinkingLevel');
228
+ if (opts?.thinkingSummaries != null) {
229
+ droppedFields.push('thinkingSummaries');
230
+ }
231
+ if (opts?.imageConfig != null) droppedFields.push('imageConfig');
232
+ if (droppedFields.length > 0) {
233
+ warnings.push({
234
+ type: 'other',
235
+ 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.`,
236
+ });
237
+ }
238
+ generationConfig = undefined;
239
+ } else {
240
+ generationConfig = pruneUndefined({
241
+ temperature: options.temperature ?? undefined,
242
+ top_p: options.topP ?? undefined,
243
+ seed: options.seed ?? undefined,
244
+ stop_sequences:
245
+ options.stopSequences != null && options.stopSequences.length > 0
246
+ ? options.stopSequences
247
+ : undefined,
248
+ max_output_tokens: options.maxOutputTokens ?? undefined,
249
+ thinking_level: opts?.thinkingLevel ?? undefined,
250
+ thinking_summaries: opts?.thinkingSummaries ?? undefined,
251
+ image_config:
252
+ opts?.imageConfig != null
253
+ ? pruneUndefined({
254
+ aspect_ratio: opts.imageConfig.aspectRatio ?? undefined,
255
+ image_size: opts.imageConfig.imageSize ?? undefined,
256
+ })
257
+ : undefined,
258
+ tool_choice: toolChoiceForBody,
259
+ });
260
+ }
261
+
262
+ let agentConfig: GoogleInteractionsAgentConfig | undefined;
263
+ if (isAgent && opts?.agentConfig != null) {
264
+ const ac = opts.agentConfig;
265
+ if (ac.type === 'deep-research') {
266
+ agentConfig = pruneUndefined({
267
+ type: 'deep-research',
268
+ thinking_summaries: ac.thinkingSummaries ?? undefined,
269
+ visualization: ac.visualization ?? undefined,
270
+ collaborative_planning: ac.collaborativePlanning ?? undefined,
271
+ }) as GoogleInteractionsAgentConfig;
272
+ } else if (ac.type === 'dynamic') {
273
+ agentConfig = { type: 'dynamic' };
274
+ }
275
+ }
276
+
277
+ /*
278
+ * Agent calls require `background: true` on the wire — otherwise the API
279
+ * rejects them with `background=true is required for agent interactions.`
280
+ * The server returns a non-terminal status (`in_progress`/`requires_action`)
281
+ * and the final outputs must be polled via `GET /interactions/{id}`. This
282
+ * is handled internally in `doGenerate` / `doStream` so the user-facing
283
+ * surface stays identical to model-id calls.
284
+ *
285
+ * Model-id calls retain their original synchronous behavior — no
286
+ * `background` field is sent.
287
+ */
288
+ const args: GoogleInteractionsRequestBody = pruneUndefined({
289
+ ...(isAgent ? { agent: this.agent } : { model: this.modelId }),
290
+ input,
291
+ system_instruction: systemInstruction,
292
+ tools: toolsForBody,
293
+ response_format: responseFormat,
294
+ response_mime_type: responseMimeType,
295
+ response_modalities:
296
+ opts?.responseModalities != null
297
+ ? (opts.responseModalities as Array<
298
+ 'text' | 'image' | 'audio' | 'video' | 'document'
299
+ >)
300
+ : undefined,
301
+ previous_interaction_id: opts?.previousInteractionId ?? undefined,
302
+ service_tier: opts?.serviceTier ?? undefined,
303
+ store: opts?.store ?? undefined,
304
+ generation_config:
305
+ generationConfig != null && Object.keys(generationConfig).length > 0
306
+ ? generationConfig
307
+ : undefined,
308
+ agent_config: agentConfig,
309
+ ...(isAgent ? { background: true } : {}),
310
+ });
311
+
312
+ return {
313
+ args,
314
+ warnings,
315
+ isAgent,
316
+ pollingTimeoutMs: opts?.pollingTimeoutMs ?? undefined,
317
+ };
318
+ }
319
+
320
+ async doGenerate(
321
+ options: LanguageModelV3CallOptions,
322
+ ): Promise<LanguageModelV3GenerateResult> {
323
+ const { args, warnings, isAgent, pollingTimeoutMs } =
324
+ await this.getArgs(options);
325
+
326
+ const url = `${this.config.baseURL}/interactions`;
327
+
328
+ const mergedHeaders = combineHeaders(
329
+ this.config.headers ? await resolve(this.config.headers) : undefined,
330
+ options.headers,
331
+ );
332
+
333
+ const postResult = await postJsonToApi({
334
+ url,
335
+ headers: mergedHeaders,
336
+ body: args,
337
+ failedResponseHandler: googleFailedResponseHandler,
338
+ successfulResponseHandler: createJsonResponseHandler(
339
+ googleInteractionsResponseSchema,
340
+ ),
341
+ abortSignal: options.abortSignal,
342
+ fetch: this.config.fetch,
343
+ });
344
+
345
+ let {
346
+ responseHeaders,
347
+ value: response,
348
+ rawValue: rawResponse,
349
+ } = postResult;
350
+
351
+ /*
352
+ * Agent calls run with `background: true`; the POST returns immediately
353
+ * with a non-terminal status (`in_progress` / `requires_action`). Poll
354
+ * `GET /interactions/{id}` until terminal so the user-facing surface
355
+ * matches a synchronous call.
356
+ */
357
+ if (isAgent && !isTerminalStatus(response.status)) {
358
+ const polled = await pollGoogleInteractionUntilTerminal({
359
+ baseURL: this.config.baseURL,
360
+ interactionId: response.id,
361
+ headers: mergedHeaders,
362
+ fetch: this.config.fetch,
363
+ abortSignal: options.abortSignal,
364
+ timeoutMs: pollingTimeoutMs,
365
+ });
366
+ response = polled.response;
367
+ rawResponse = polled.rawResponse;
368
+ responseHeaders = polled.responseHeaders ?? responseHeaders;
369
+ }
370
+
371
+ /*
372
+ * `response.id` is omitted when `store: false` (fully stateless mode), and
373
+ * the stream surface returns `id: ""` (empty string) for the same case.
374
+ * Normalize both to `undefined` so downstream stamping does not pollute
375
+ * provider metadata with an empty/missing identifier.
376
+ */
377
+ const interactionId =
378
+ typeof response.id === 'string' && response.id.length > 0
379
+ ? response.id
380
+ : undefined;
381
+
382
+ const { content, hasFunctionCall } = parseGoogleInteractionsOutputs({
383
+ outputs: response.outputs ?? null,
384
+ generateId: this.config.generateId ?? defaultGenerateId,
385
+ interactionId,
386
+ });
387
+
388
+ const finishReason: LanguageModelV3FinishReason = {
389
+ unified: mapGoogleInteractionsFinishReason({
390
+ status: response.status,
391
+ hasFunctionCall,
392
+ }),
393
+ raw: response.status,
394
+ };
395
+
396
+ /*
397
+ * Service tier divergence vs. `:generateContent`:
398
+ *
399
+ * `google-language-model.ts` reads the applied service tier from the
400
+ * `x-gemini-service-tier` HTTP response header (see commit 1adfb76d2d).
401
+ * The Interactions API does NOT surface that header; it returns the
402
+ * applied tier in the response body as `service_tier` on the top-level
403
+ * Interaction object (and on `interaction.complete.interaction` for
404
+ * streaming). The `responseHeaders` parameter is also checked as a
405
+ * defensive fallback in case the API later adds the header.
406
+ */
407
+ const serviceTier =
408
+ response.service_tier ??
409
+ responseHeaders?.['x-gemini-service-tier'] ??
410
+ undefined;
411
+
412
+ /*
413
+ * `response.id` is omitted when `store: false` (fully stateless mode), so
414
+ * `interactionId` is only surfaced when the API actually returned one.
415
+ */
416
+ const providerMetadata: SharedV3ProviderMetadata = {
417
+ google: {
418
+ ...(interactionId != null ? { interactionId } : {}),
419
+ ...(serviceTier != null ? { serviceTier } : {}),
420
+ },
421
+ };
422
+
423
+ let timestamp: Date | undefined;
424
+ if (typeof response.created === 'string') {
425
+ const parsed = new Date(response.created);
426
+ if (!Number.isNaN(parsed.getTime())) {
427
+ timestamp = parsed;
428
+ }
429
+ }
430
+
431
+ return {
432
+ content,
433
+ finishReason,
434
+ usage: convertGoogleInteractionsUsage(response.usage),
435
+ warnings,
436
+ providerMetadata,
437
+ request: { body: args },
438
+ response: {
439
+ headers: responseHeaders,
440
+ body: rawResponse,
441
+ ...(interactionId != null ? { id: interactionId } : {}),
442
+ ...(timestamp ? { timestamp } : {}),
443
+ modelId: response.model ?? undefined,
444
+ },
445
+ };
446
+ }
447
+
448
+ async doStream(
449
+ options: LanguageModelV3CallOptions,
450
+ ): Promise<LanguageModelV3StreamResult> {
451
+ const { args, warnings, isAgent, pollingTimeoutMs } =
452
+ await this.getArgs(options);
453
+
454
+ const url = `${this.config.baseURL}/interactions`;
455
+
456
+ const mergedHeaders = combineHeaders(
457
+ this.config.headers ? await resolve(this.config.headers) : undefined,
458
+ options.headers,
459
+ );
460
+
461
+ /*
462
+ * Agent calls require `background: true`, which is incompatible with
463
+ * `stream: true` on POST. We drive the agent flow exactly like
464
+ * `doGenerate` (POST background -> poll GET) and synthesize a stream
465
+ * from the final polled outputs. The user-facing stream surface stays
466
+ * identical -- text-start / text-delta / text-end / finish parts are
467
+ * emitted in the same order as a true SSE response.
468
+ */
469
+ if (isAgent) {
470
+ return this.doStreamAgent({
471
+ args,
472
+ warnings,
473
+ url,
474
+ mergedHeaders,
475
+ options,
476
+ pollingTimeoutMs,
477
+ });
478
+ }
479
+
480
+ const body = { ...args, stream: true };
481
+
482
+ const { responseHeaders, value: response } = await postJsonToApi({
483
+ url,
484
+ headers: mergedHeaders,
485
+ body,
486
+ failedResponseHandler: googleFailedResponseHandler,
487
+ successfulResponseHandler: createEventSourceResponseHandler(
488
+ googleInteractionsEventSchema,
489
+ ),
490
+ abortSignal: options.abortSignal,
491
+ fetch: this.config.fetch,
492
+ });
493
+
494
+ /*
495
+ * Google's API surfaces the applied service tier in the
496
+ * `x-gemini-service-tier` HTTP response header, not in the response body.
497
+ * Mirror the canonical pattern from `google-language-model.ts` (commit
498
+ * 1adfb76d2d) and pipe it through the stream transformer so the `finish`
499
+ * part's `providerMetadata.google.serviceTier` is sourced from the header.
500
+ */
501
+ const headerServiceTier = responseHeaders?.['x-gemini-service-tier'];
502
+
503
+ const transform = buildGoogleInteractionsStreamTransform({
504
+ warnings,
505
+ generateId: this.config.generateId ?? defaultGenerateId,
506
+ includeRawChunks: options.includeRawChunks,
507
+ serviceTier: headerServiceTier,
508
+ });
509
+
510
+ return {
511
+ stream: response.pipeThrough(transform),
512
+ request: { body },
513
+ response: { headers: responseHeaders },
514
+ };
515
+ }
516
+
517
+ /*
518
+ * Drive the streaming surface for agent calls. Agent calls require
519
+ * `background: true`, which is incompatible with `stream: true` on POST.
520
+ *
521
+ * In principle the API also exposes `GET /interactions/{id}?stream=true`
522
+ * to replay events as the agent runs. In practice the connection is
523
+ * idle for long stretches while the agent thinks (deep-research can run
524
+ * for a minute or more between SSE events), and undici's default body
525
+ * timeout terminates the request mid-flight with `UND_ERR_BODY_TIMEOUT`.
526
+ * Tuning the timeout per-call would require the caller to thread an
527
+ * `undici.Agent` through `fetch`, which contradicts the AI SDK's
528
+ * pluggable-fetch contract.
529
+ *
530
+ * We therefore drive `doStream` exactly like `doGenerate` for agents:
531
+ * POST with `background: true`, poll `GET /interactions/{id}` until
532
+ * terminal, then synthesize the stream from the final outputs. The
533
+ * user-facing surface stays identical -- text-start / text-delta /
534
+ * text-end / finish parts arrive in the same order as a true SSE
535
+ * response, just buffered until the agent completes.
536
+ */
537
+ private async doStreamAgent({
538
+ args,
539
+ warnings,
540
+ url,
541
+ mergedHeaders,
542
+ options,
543
+ pollingTimeoutMs,
544
+ }: {
545
+ args: GoogleInteractionsRequestBody;
546
+ warnings: Array<SharedV3Warning>;
547
+ url: string;
548
+ mergedHeaders: Record<string, string | undefined>;
549
+ options: LanguageModelV3CallOptions;
550
+ pollingTimeoutMs: number | undefined;
551
+ }): Promise<LanguageModelV3StreamResult> {
552
+ const postResult = await postJsonToApi({
553
+ url,
554
+ headers: mergedHeaders,
555
+ body: args,
556
+ failedResponseHandler: googleFailedResponseHandler,
557
+ successfulResponseHandler: createJsonResponseHandler(
558
+ googleInteractionsResponseSchema,
559
+ ),
560
+ abortSignal: options.abortSignal,
561
+ fetch: this.config.fetch,
562
+ });
563
+
564
+ let { responseHeaders: postHeaders, value: postResponse } = postResult;
565
+ const interactionId = postResponse.id;
566
+
567
+ if (interactionId == null || interactionId.length === 0) {
568
+ throw new Error(
569
+ 'google.interactions: agent POST response did not include an interaction id; cannot poll for the agent result.',
570
+ );
571
+ }
572
+
573
+ if (!isTerminalStatus(postResponse.status)) {
574
+ const polled = await pollGoogleInteractionUntilTerminal({
575
+ baseURL: this.config.baseURL,
576
+ interactionId,
577
+ headers: mergedHeaders,
578
+ fetch: this.config.fetch,
579
+ abortSignal: options.abortSignal,
580
+ timeoutMs: pollingTimeoutMs,
581
+ });
582
+ postResponse = polled.response;
583
+ postHeaders = polled.responseHeaders ?? postHeaders;
584
+ }
585
+
586
+ const stream = synthesizeGoogleInteractionsAgentStream({
587
+ response: postResponse,
588
+ warnings,
589
+ generateId: this.config.generateId ?? defaultGenerateId,
590
+ includeRawChunks: options.includeRawChunks,
591
+ headerServiceTier: postHeaders?.['x-gemini-service-tier'],
592
+ });
593
+
594
+ return {
595
+ stream,
596
+ request: { body: args },
597
+ response: { headers: postHeaders },
598
+ };
599
+ }
600
+ }
601
+
602
+ function pruneUndefined<T extends Record<string, unknown>>(obj: T): T {
603
+ const result: Record<string, unknown> = {};
604
+ for (const [key, value] of Object.entries(obj)) {
605
+ if (value === undefined) continue;
606
+ result[key] = value;
607
+ }
608
+ return result as T;
609
+ }