@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.
@@ -0,0 +1,257 @@
1
+ import type { JSONValue, LanguageModelV3Content } from '@ai-sdk/provider';
2
+ import type { GoogleInteractionsContentBlock } from './google-interactions-api';
3
+ import {
4
+ annotationsToSources,
5
+ builtinToolResultToSources,
6
+ } from './extract-google-interactions-sources';
7
+ import type {
8
+ GoogleInteractionsAnnotation,
9
+ GoogleInteractionsBuiltinToolResultContent,
10
+ } from './google-interactions-prompt';
11
+
12
+ export type ParseGoogleInteractionsOutputsResult = {
13
+ content: Array<LanguageModelV3Content>;
14
+ hasFunctionCall: boolean;
15
+ };
16
+
17
+ /*
18
+ * Builds a `providerMetadata.google` payload for an output part so the
19
+ * Interactions converter on the next turn can read both the per-block
20
+ * `signature` (round-trip) and the parent `interactionId` (history compaction
21
+ * under `previousInteractionId`).
22
+ */
23
+ function googleProviderMetadata({
24
+ signature,
25
+ interactionId,
26
+ }: {
27
+ signature?: string | null;
28
+ interactionId?: string;
29
+ }): { providerMetadata: { google: Record<string, string> } } | object {
30
+ const google: Record<string, string> = {};
31
+ if (signature != null) {
32
+ google.signature = signature;
33
+ }
34
+ if (interactionId != null) {
35
+ google.interactionId = interactionId;
36
+ }
37
+ return Object.keys(google).length > 0 ? { providerMetadata: { google } } : {};
38
+ }
39
+
40
+ /**
41
+ * Set of built-in tool *call* discriminators emitted by the Interactions API.
42
+ */
43
+ const BUILTIN_TOOL_CALL_TYPES = new Set([
44
+ 'google_search_call',
45
+ 'code_execution_call',
46
+ 'url_context_call',
47
+ 'file_search_call',
48
+ 'google_maps_call',
49
+ 'mcp_server_tool_call',
50
+ ]);
51
+
52
+ /**
53
+ * Set of built-in tool *result* discriminators.
54
+ */
55
+ const BUILTIN_TOOL_RESULT_TYPES = new Set([
56
+ 'google_search_result',
57
+ 'code_execution_result',
58
+ 'url_context_result',
59
+ 'file_search_result',
60
+ 'google_maps_result',
61
+ 'mcp_server_tool_result',
62
+ ]);
63
+
64
+ function builtinToolNameFromCallType(type: string): string {
65
+ return type.replace(/_call$/, '');
66
+ }
67
+
68
+ function builtinToolNameFromResultType(type: string): string {
69
+ return type.replace(/_result$/, '');
70
+ }
71
+
72
+ /**
73
+ * Walks the `outputs[]` array of an Interaction response and emits AI SDK
74
+ * `LanguageModelV3Content[]`. Surfaces:
75
+ *
76
+ * - `text` blocks (with annotations -> source parts)
77
+ * - `thought` blocks (reasoning)
78
+ * - `function_call` blocks (client-executed tool calls)
79
+ * - Built-in tool `*_call` / `*_result` blocks (Google Search, Code Execution,
80
+ * URL Context, File Search, Google Maps, MCP Server) as
81
+ * `tool-call`/`tool-result` parts with `providerExecuted: true`.
82
+ * - `text_annotation`-derived sources from the URL / file / place citations
83
+ * carried on text blocks.
84
+ */
85
+ export function parseGoogleInteractionsOutputs({
86
+ outputs,
87
+ generateId,
88
+ interactionId,
89
+ }: {
90
+ outputs: Array<GoogleInteractionsContentBlock> | null | undefined;
91
+ generateId: () => string;
92
+ /**
93
+ * Top-level `Interaction.id` on the response. Stamped onto each output
94
+ * part's `providerMetadata.google.interactionId` so the converter can drop
95
+ * matching assistant turns when `previousInteractionId` is used on the
96
+ * next turn (compaction).
97
+ */
98
+ interactionId?: string;
99
+ }): ParseGoogleInteractionsOutputsResult {
100
+ const content: Array<LanguageModelV3Content> = [];
101
+ let hasFunctionCall = false;
102
+
103
+ if (outputs == null) {
104
+ return { content, hasFunctionCall };
105
+ }
106
+
107
+ for (const block of outputs) {
108
+ if (block == null || typeof block !== 'object') continue;
109
+ const type = (block as { type?: string }).type;
110
+ if (typeof type !== 'string') continue;
111
+
112
+ switch (type) {
113
+ case 'text': {
114
+ const text = (block as { text?: string }).text ?? '';
115
+ const annotations = (
116
+ block as { annotations?: Array<GoogleInteractionsAnnotation> }
117
+ ).annotations;
118
+ content.push({
119
+ type: 'text',
120
+ text,
121
+ ...googleProviderMetadata({ interactionId }),
122
+ });
123
+ const sources = annotationsToSources({ annotations, generateId });
124
+ for (const source of sources) {
125
+ content.push(source);
126
+ }
127
+ break;
128
+ }
129
+ case 'thought': {
130
+ const thought = block as {
131
+ signature?: string;
132
+ summary?: Array<{ type: string; text?: string }>;
133
+ };
134
+ const summary = Array.isArray(thought.summary) ? thought.summary : [];
135
+ const text = summary
136
+ .filter(
137
+ item => item?.type === 'text' && typeof item.text === 'string',
138
+ )
139
+ .map(item => item.text as string)
140
+ .join('\n');
141
+ content.push({
142
+ type: 'reasoning',
143
+ text,
144
+ ...googleProviderMetadata({
145
+ signature: thought.signature,
146
+ interactionId,
147
+ }),
148
+ });
149
+ break;
150
+ }
151
+ case 'image': {
152
+ const image = block as {
153
+ data?: string;
154
+ mime_type?: string;
155
+ uri?: string;
156
+ };
157
+ if (image.data != null && image.data.length > 0) {
158
+ content.push({
159
+ type: 'file',
160
+ mediaType: image.mime_type ?? 'image/png',
161
+ data: image.data,
162
+ ...googleProviderMetadata({ interactionId }),
163
+ });
164
+ } else if (image.uri != null && image.uri.length > 0) {
165
+ /*
166
+ * V3 `LanguageModelV3File` only supports inline data (`string` /
167
+ * `Uint8Array`). URL-only image outputs cannot be represented as a
168
+ * file content part on the v3 spec; surface the URI through provider
169
+ * metadata so callers can still recover it.
170
+ */
171
+ content.push({
172
+ type: 'file',
173
+ mediaType: image.mime_type ?? 'image/png',
174
+ data: '',
175
+ providerMetadata: {
176
+ google: {
177
+ ...(interactionId != null ? { interactionId } : {}),
178
+ imageUri: image.uri,
179
+ },
180
+ },
181
+ });
182
+ }
183
+ break;
184
+ }
185
+ case 'function_call': {
186
+ hasFunctionCall = true;
187
+ const call = block as {
188
+ id: string;
189
+ name: string;
190
+ arguments?: Record<string, unknown> | null;
191
+ signature?: string | null;
192
+ };
193
+ content.push({
194
+ type: 'tool-call',
195
+ toolCallId: call.id,
196
+ toolName: call.name,
197
+ input: JSON.stringify(call.arguments ?? {}),
198
+ ...googleProviderMetadata({
199
+ signature: call.signature,
200
+ interactionId,
201
+ }),
202
+ });
203
+ break;
204
+ }
205
+ default: {
206
+ if (BUILTIN_TOOL_CALL_TYPES.has(type)) {
207
+ const call = block as {
208
+ id?: string;
209
+ arguments?: Record<string, unknown>;
210
+ name?: string;
211
+ server_name?: string;
212
+ };
213
+ const toolName =
214
+ type === 'mcp_server_tool_call'
215
+ ? (call.name ?? 'mcp_server_tool')
216
+ : builtinToolNameFromCallType(type);
217
+ const input = JSON.stringify(call.arguments ?? {});
218
+ content.push({
219
+ type: 'tool-call',
220
+ toolCallId: call.id ?? generateId(),
221
+ toolName,
222
+ input,
223
+ providerExecuted: true,
224
+ });
225
+ } else if (BUILTIN_TOOL_RESULT_TYPES.has(type)) {
226
+ const result = block as {
227
+ call_id?: string;
228
+ result?: unknown;
229
+ is_error?: boolean;
230
+ name?: string;
231
+ };
232
+ const toolName =
233
+ type === 'mcp_server_tool_result'
234
+ ? (result.name ?? 'mcp_server_tool')
235
+ : builtinToolNameFromResultType(type);
236
+ content.push({
237
+ type: 'tool-result',
238
+ toolCallId: result.call_id ?? generateId(),
239
+ toolName,
240
+ result: (result.result ?? null) as NonNullable<JSONValue>,
241
+ });
242
+ const sources = builtinToolResultToSources({
243
+ block:
244
+ block as unknown as GoogleInteractionsBuiltinToolResultContent,
245
+ generateId,
246
+ });
247
+ for (const source of sources) {
248
+ content.push(source);
249
+ }
250
+ }
251
+ break;
252
+ }
253
+ }
254
+ }
255
+
256
+ return { content, hasFunctionCall };
257
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ createJsonResponseHandler,
3
+ delay,
4
+ getFromApi,
5
+ type FetchFunction,
6
+ } from '@ai-sdk/provider-utils';
7
+ import { googleFailedResponseHandler } from '../google-error';
8
+ import {
9
+ googleInteractionsResponseSchema,
10
+ type GoogleInteractionsResponse,
11
+ } from './google-interactions-api';
12
+ import type { GoogleInteractionsStatus } from './google-interactions-prompt';
13
+
14
+ const TERMINAL_STATUSES: ReadonlySet<GoogleInteractionsStatus | string> =
15
+ new Set(['completed', 'failed', 'cancelled', 'incomplete']);
16
+
17
+ export function isTerminalStatus(
18
+ status: GoogleInteractionsStatus | string | null | undefined,
19
+ ): boolean {
20
+ return status != null && TERMINAL_STATUSES.has(status);
21
+ }
22
+
23
+ /*
24
+ * Default polling cadence for background interactions. Starts at 1 s, doubles
25
+ * each tick up to a 10 s ceiling, and gives up after 30 minutes -- agent runs
26
+ * such as deep research can take tens of minutes server-side, so we err on
27
+ * the long side rather than truncate a real run. Override per-call via
28
+ * `providerOptions.google.pollingTimeoutMs`.
29
+ */
30
+ const DEFAULT_INITIAL_DELAY_MS = 1000;
31
+ const DEFAULT_MAX_DELAY_MS = 10000;
32
+ const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
33
+
34
+ export type PollGoogleInteractionResult = {
35
+ response: GoogleInteractionsResponse;
36
+ rawResponse: unknown;
37
+ responseHeaders: Record<string, string> | undefined;
38
+ };
39
+
40
+ /**
41
+ * Polls `GET {baseURL}/interactions/{id}` until the response status is
42
+ * terminal (`completed` / `failed` / `cancelled` / `incomplete`). Throws if
43
+ * the polling loop exceeds `timeoutMs`, the response has no `id` to poll on,
44
+ * or the abort signal fires.
45
+ */
46
+ export async function pollGoogleInteractionUntilTerminal({
47
+ baseURL,
48
+ interactionId,
49
+ headers,
50
+ fetch,
51
+ abortSignal,
52
+ initialDelayMs = DEFAULT_INITIAL_DELAY_MS,
53
+ maxDelayMs = DEFAULT_MAX_DELAY_MS,
54
+ timeoutMs = DEFAULT_TIMEOUT_MS,
55
+ }: {
56
+ baseURL: string;
57
+ interactionId: string | null | undefined;
58
+ headers: Record<string, string | undefined>;
59
+ fetch?: FetchFunction;
60
+ abortSignal?: AbortSignal;
61
+ initialDelayMs?: number;
62
+ maxDelayMs?: number;
63
+ timeoutMs?: number;
64
+ }): Promise<PollGoogleInteractionResult> {
65
+ if (interactionId == null || interactionId.length === 0) {
66
+ throw new Error(
67
+ 'google.interactions: cannot poll a background interaction without an id. ' +
68
+ 'The POST response did not include an interaction id.',
69
+ );
70
+ }
71
+
72
+ const startedAt = Date.now();
73
+ let nextDelayMs = initialDelayMs;
74
+ const url = `${baseURL}/interactions/${encodeURIComponent(interactionId)}`;
75
+
76
+ while (true) {
77
+ if (abortSignal?.aborted) {
78
+ throw new DOMException('Polling was aborted', 'AbortError');
79
+ }
80
+
81
+ if (Date.now() - startedAt > timeoutMs) {
82
+ throw new Error(
83
+ `google.interactions: timed out polling interaction ${interactionId} after ${timeoutMs}ms.`,
84
+ );
85
+ }
86
+
87
+ await delay(nextDelayMs, { abortSignal });
88
+
89
+ const {
90
+ value: response,
91
+ rawValue: rawResponse,
92
+ responseHeaders,
93
+ } = await getFromApi({
94
+ url,
95
+ headers,
96
+ failedResponseHandler: googleFailedResponseHandler,
97
+ successfulResponseHandler: createJsonResponseHandler(
98
+ googleInteractionsResponseSchema,
99
+ ),
100
+ abortSignal,
101
+ fetch,
102
+ });
103
+
104
+ if (isTerminalStatus(response.status)) {
105
+ return { response, rawResponse, responseHeaders };
106
+ }
107
+
108
+ nextDelayMs = Math.min(nextDelayMs * 2, maxDelayMs);
109
+ }
110
+ }
@@ -0,0 +1,245 @@
1
+ import type {
2
+ LanguageModelV3CallOptions,
3
+ SharedV3Warning,
4
+ } from '@ai-sdk/provider';
5
+ import type {
6
+ GoogleInteractionsTool,
7
+ GoogleInteractionsToolChoice,
8
+ } from './google-interactions-prompt';
9
+
10
+ export type PrepareGoogleInteractionsToolsResult = {
11
+ tools: Array<GoogleInteractionsTool> | undefined;
12
+ toolChoice: GoogleInteractionsToolChoice | undefined;
13
+ toolWarnings: Array<SharedV3Warning>;
14
+ };
15
+
16
+ /**
17
+ * Maps AI SDK tool definitions and `toolChoice` onto the Gemini Interactions
18
+ * `tools[]` and `tool_choice` request fields.
19
+ *
20
+ * AI SDK function tools (`{ type: 'function', name, description, inputSchema }`)
21
+ * map to Interactions `{ type: 'function', name, description, parameters }`,
22
+ * with `parameters` passed through as plain JSON Schema (per
23
+ * `googleapis/js-genai` `samples/interactions_tool_call_with_functions.ts` and
24
+ * `src/interactions/resources/interactions.ts` `Function.parameters: unknown`).
25
+ *
26
+ * Provider-defined tools (`{ type: 'provider', id: 'google.<name>', args }`)
27
+ * map to the discriminated `Tool` union (TASK-7). The full set of
28
+ * provider-defined tool ids supported here:
29
+ *
30
+ * - `google.google_search` -> `{ type: 'google_search', search_types? }`
31
+ * - `google.code_execution` -> `{ type: 'code_execution' }`
32
+ * - `google.url_context` -> `{ type: 'url_context' }`
33
+ * - `google.file_search` -> `{ type: 'file_search', file_search_store_names?, top_k?, metadata_filter? }`
34
+ * - `google.google_maps` -> `{ type: 'google_maps', latitude?, longitude?, enable_widget? }`
35
+ * - `google.computer_use` -> `{ type: 'computer_use', environment?, excludedPredefinedFunctions? }`
36
+ * - `google.mcp_server` -> `{ type: 'mcp_server', name?, url?, headers?, allowed_tools? }`
37
+ * - `google.retrieval` -> `{ type: 'retrieval', retrieval_types?, vertex_ai_search_config? }`
38
+ *
39
+ * `toolChoice` shapes:
40
+ * - `'auto'` -> `'auto'`
41
+ * - `'required'` -> `'any'`
42
+ * - `'none'` -> `'none'`
43
+ * - `{ type: 'tool', toolName }` -> `{ allowed_tools: { mode: 'validated', tools: [name] } }`
44
+ * (Interactions `AllowedTools.tools` is an `Array<string>` of function
45
+ * names, not tool descriptors -- see `src/interactions/resources/interactions.ts`
46
+ * line ~151).
47
+ */
48
+ export function prepareGoogleInteractionsTools({
49
+ tools,
50
+ toolChoice,
51
+ }: {
52
+ tools: LanguageModelV3CallOptions['tools'];
53
+ toolChoice?: LanguageModelV3CallOptions['toolChoice'];
54
+ }): PrepareGoogleInteractionsToolsResult {
55
+ const toolWarnings: Array<SharedV3Warning> = [];
56
+
57
+ const normalized = tools?.length ? tools : undefined;
58
+
59
+ if (normalized == null) {
60
+ return { tools: undefined, toolChoice: undefined, toolWarnings };
61
+ }
62
+
63
+ const interactionsTools: Array<GoogleInteractionsTool> = [];
64
+
65
+ for (const tool of normalized) {
66
+ if (tool.type === 'function') {
67
+ interactionsTools.push({
68
+ type: 'function',
69
+ name: tool.name,
70
+ description: tool.description ?? '',
71
+ parameters: tool.inputSchema,
72
+ });
73
+ continue;
74
+ }
75
+
76
+ if (tool.type === 'provider') {
77
+ const args = (tool.args ?? {}) as Record<string, unknown>;
78
+ switch (tool.id) {
79
+ case 'google.google_search': {
80
+ const searchTypesArg = args.searchTypes as
81
+ | { webSearch?: unknown; imageSearch?: unknown }
82
+ | undefined;
83
+ let search_types:
84
+ | Array<'web_search' | 'image_search' | 'enterprise_web_search'>
85
+ | undefined;
86
+ if (searchTypesArg != null && typeof searchTypesArg === 'object') {
87
+ const list: Array<
88
+ 'web_search' | 'image_search' | 'enterprise_web_search'
89
+ > = [];
90
+ if (searchTypesArg.webSearch != null) list.push('web_search');
91
+ if (searchTypesArg.imageSearch != null) list.push('image_search');
92
+ if (list.length > 0) {
93
+ search_types = list;
94
+ }
95
+ }
96
+ interactionsTools.push({
97
+ type: 'google_search',
98
+ ...(search_types != null ? { search_types } : {}),
99
+ });
100
+ break;
101
+ }
102
+ case 'google.code_execution': {
103
+ interactionsTools.push({ type: 'code_execution' });
104
+ break;
105
+ }
106
+ case 'google.url_context': {
107
+ interactionsTools.push({ type: 'url_context' });
108
+ break;
109
+ }
110
+ case 'google.file_search': {
111
+ interactionsTools.push({
112
+ type: 'file_search',
113
+ ...(args.fileSearchStoreNames != null
114
+ ? {
115
+ file_search_store_names:
116
+ args.fileSearchStoreNames as Array<string>,
117
+ }
118
+ : {}),
119
+ ...(args.topK != null ? { top_k: args.topK as number } : {}),
120
+ ...(args.metadataFilter != null
121
+ ? { metadata_filter: args.metadataFilter as string }
122
+ : {}),
123
+ });
124
+ break;
125
+ }
126
+ case 'google.google_maps': {
127
+ interactionsTools.push({
128
+ type: 'google_maps',
129
+ ...(args.latitude != null
130
+ ? { latitude: args.latitude as number }
131
+ : {}),
132
+ ...(args.longitude != null
133
+ ? { longitude: args.longitude as number }
134
+ : {}),
135
+ ...(args.enableWidget != null
136
+ ? { enable_widget: args.enableWidget as boolean }
137
+ : {}),
138
+ });
139
+ break;
140
+ }
141
+ case 'google.computer_use': {
142
+ interactionsTools.push({
143
+ type: 'computer_use',
144
+ environment:
145
+ (args.environment as 'browser' | undefined) ?? 'browser',
146
+ ...(args.excludedPredefinedFunctions != null
147
+ ? {
148
+ excludedPredefinedFunctions:
149
+ args.excludedPredefinedFunctions as Array<string>,
150
+ }
151
+ : {}),
152
+ });
153
+ break;
154
+ }
155
+ case 'google.mcp_server': {
156
+ interactionsTools.push({
157
+ type: 'mcp_server',
158
+ ...(args.name != null ? { name: args.name as string } : {}),
159
+ ...(args.url != null ? { url: args.url as string } : {}),
160
+ ...(args.headers != null
161
+ ? { headers: args.headers as Record<string, string> }
162
+ : {}),
163
+ ...(args.allowedTools != null
164
+ ? { allowed_tools: args.allowedTools as Array<unknown> }
165
+ : {}),
166
+ });
167
+ break;
168
+ }
169
+ case 'google.retrieval': {
170
+ const vertexAiSearchConfig =
171
+ (args.vertexAiSearchConfig as
172
+ | { datastores?: Array<string>; engine?: string }
173
+ | undefined) ?? undefined;
174
+ interactionsTools.push({
175
+ type: 'retrieval',
176
+ ...(args.retrievalTypes != null
177
+ ? {
178
+ retrieval_types:
179
+ args.retrievalTypes as Array<'vertex_ai_search'>,
180
+ }
181
+ : { retrieval_types: ['vertex_ai_search'] }),
182
+ ...(vertexAiSearchConfig != null
183
+ ? { vertex_ai_search_config: vertexAiSearchConfig }
184
+ : {}),
185
+ });
186
+ break;
187
+ }
188
+ default: {
189
+ toolWarnings.push({
190
+ type: 'unsupported',
191
+ feature: `provider-defined tool ${tool.id}`,
192
+ details: `provider-defined tool ${tool.id} is not supported by google.interactions; tool dropped.`,
193
+ });
194
+ break;
195
+ }
196
+ }
197
+ continue;
198
+ }
199
+
200
+ toolWarnings.push({
201
+ type: 'unsupported',
202
+ feature: `tool of type ${(tool as { type: string }).type}`,
203
+ details:
204
+ 'Only function tools and google.* provider-defined tools are supported by google.interactions; tool dropped.',
205
+ });
206
+ }
207
+
208
+ /*
209
+ * `tool_choice` on the Interactions API governs function calling only -- the
210
+ * API rejects requests with `tool_choice` set when no `function` tools are
211
+ * present (`{"error":{"message":"Function calling config is set without
212
+ * function_declarations."}}`). Drop `tool_choice` when the resolved tool
213
+ * list is empty or contains no function tools.
214
+ */
215
+ const hasFunctionTool = interactionsTools.some(t => t.type === 'function');
216
+
217
+ let mappedToolChoice: GoogleInteractionsToolChoice | undefined;
218
+ if (toolChoice != null && hasFunctionTool) {
219
+ switch (toolChoice.type) {
220
+ case 'auto':
221
+ mappedToolChoice = 'auto';
222
+ break;
223
+ case 'required':
224
+ mappedToolChoice = 'any';
225
+ break;
226
+ case 'none':
227
+ mappedToolChoice = 'none';
228
+ break;
229
+ case 'tool':
230
+ mappedToolChoice = {
231
+ allowed_tools: {
232
+ mode: 'validated',
233
+ tools: [toolChoice.toolName],
234
+ },
235
+ };
236
+ break;
237
+ }
238
+ }
239
+
240
+ return {
241
+ tools: interactionsTools.length > 0 ? interactionsTools : undefined,
242
+ toolChoice: mappedToolChoice,
243
+ toolWarnings,
244
+ };
245
+ }