@ai-sdk/google 3.0.66 → 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 +13 -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 +2 -2
- 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,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
|
+
}
|