@ai-sdk/google 4.0.0-canary.63 → 4.0.0-canary.65
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 +12 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +510 -335
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.js +43 -28
- package/dist/internal/index.js.map +1 -1
- package/docs/15-google.mdx +72 -16
- package/package.json +1 -1
- package/src/convert-to-google-messages.ts +20 -2
- package/src/google-language-model.ts +5 -4
- package/src/google-prompt.ts +5 -1
- package/src/interactions/build-google-interactions-stream-transform.ts +273 -154
- package/src/interactions/convert-to-google-interactions-input.ts +57 -143
- package/src/interactions/extract-google-interactions-sources.ts +3 -3
- package/src/interactions/google-interactions-api.ts +179 -115
- package/src/interactions/google-interactions-language-model-options.ts +61 -0
- package/src/interactions/google-interactions-language-model.ts +100 -38
- package/src/interactions/google-interactions-prompt.ts +189 -114
- package/src/interactions/map-google-interactions-finish-reason.ts +3 -5
- package/src/interactions/parse-google-interactions-outputs.ts +69 -63
- package/src/interactions/prepare-google-interactions-tools.ts +1 -1
- package/src/interactions/stream-google-interactions.ts +1 -1
- package/src/interactions/synthesize-google-interactions-agent-stream.ts +1 -1
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
} from '@ai-sdk/provider-utils';
|
|
14
14
|
import type {
|
|
15
15
|
GoogleInteractionsContent,
|
|
16
|
+
GoogleInteractionsContentBlock,
|
|
16
17
|
GoogleInteractionsFunctionResultContent,
|
|
17
18
|
GoogleInteractionsImageContent,
|
|
18
19
|
GoogleInteractionsInput,
|
|
20
|
+
GoogleInteractionsStep,
|
|
19
21
|
GoogleInteractionsTextContent,
|
|
20
|
-
GoogleInteractionsTurn,
|
|
21
22
|
} from './google-interactions-prompt';
|
|
22
23
|
|
|
23
24
|
export type GoogleInteractionsMediaResolution =
|
|
@@ -34,22 +35,20 @@ export type ConvertToGoogleInteractionsInputResult = {
|
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Converts an AI SDK `LanguageModelV4Prompt` into the Gemini Interactions
|
|
37
|
-
* request shape (`{ input
|
|
38
|
+
* request shape (`{ input: Array<Step>, system_instruction }`).
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
41
|
-
* `
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* Prior assistant content round-trips as discrete steps:
|
|
41
|
+
* - text / image content → `model_output` step with a single `content` array
|
|
42
|
+
* - reasoning → `thought` step (`signature` + `summary`)
|
|
43
|
+
* - tool-call → `function_call` step
|
|
44
|
+
* User turns (and tool-result turns from the previous round) are sent as
|
|
45
|
+
* `user_input` steps whose `content[]` holds the user's parts (text, files,
|
|
46
|
+
* and — for tool-result turns — `function_result` blocks).
|
|
44
47
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `
|
|
48
|
-
*
|
|
49
|
-
* `src/interactions/resources/interactions.ts` `ThoughtContent` /
|
|
50
|
-
* `FunctionCallContent`). When an input reasoning part has empty text + a
|
|
51
|
-
* signature, the converter emits a `thought` block with `signature` and an
|
|
52
|
-
* omitted `summary` — no synthetic empty-text carrier needed.
|
|
48
|
+
* Handles text parts, file parts (image / audio / document / video, all four
|
|
49
|
+
* `data.type` shapes), tool-call/tool-result round-tripping, per-step
|
|
50
|
+
* `signature` round-tripping, and statefulness compaction (drop assistant/tool
|
|
51
|
+
* turns whose `providerOptions.google.interactionId === previousInteractionId`).
|
|
53
52
|
*/
|
|
54
53
|
export function convertToGoogleInteractionsInput({
|
|
55
54
|
prompt,
|
|
@@ -60,20 +59,12 @@ export function convertToGoogleInteractionsInput({
|
|
|
60
59
|
prompt: LanguageModelV4Prompt;
|
|
61
60
|
previousInteractionId?: string;
|
|
62
61
|
store?: boolean;
|
|
63
|
-
/**
|
|
64
|
-
* Per-block media resolution applied to every image / video input block
|
|
65
|
-
* (the Interactions wire format places `resolution` on the block, not at
|
|
66
|
-
* the top level). See js-genai
|
|
67
|
-
* `src/interactions/resources/interactions.ts` `ImageContent.resolution`
|
|
68
|
-
* and `VideoContent.resolution`.
|
|
69
|
-
*/
|
|
70
62
|
mediaResolution?: GoogleInteractionsMediaResolution;
|
|
71
63
|
}): ConvertToGoogleInteractionsInputResult {
|
|
72
64
|
const warnings: Array<SharedV4Warning> = [];
|
|
73
65
|
|
|
74
66
|
/*
|
|
75
|
-
* Behavior matrix
|
|
76
|
-
* matrix":
|
|
67
|
+
* Behavior matrix for compaction:
|
|
77
68
|
*
|
|
78
69
|
* - `previousInteractionId` set + `store !== false` → compact history (drop
|
|
79
70
|
* assistant/tool turns whose `providerMetadata.google.interactionId`
|
|
@@ -82,10 +73,6 @@ export function convertToGoogleInteractionsInput({
|
|
|
82
73
|
* (incoherent combo), still send full history (NO compaction).
|
|
83
74
|
* - `store === false`, no `previousInteractionId` → no compaction.
|
|
84
75
|
* - Default → no compaction.
|
|
85
|
-
*
|
|
86
|
-
* The actual `previous_interaction_id` / `store` body fields are emitted in
|
|
87
|
-
* the language model's `getArgs`; this converter only handles the history
|
|
88
|
-
* shape and the warning.
|
|
89
76
|
*/
|
|
90
77
|
const incoherentCombo = previousInteractionId != null && store === false;
|
|
91
78
|
const shouldCompact = previousInteractionId != null && store !== false;
|
|
@@ -105,7 +92,7 @@ export function convertToGoogleInteractionsInput({
|
|
|
105
92
|
: prompt;
|
|
106
93
|
|
|
107
94
|
const systemTexts: Array<string> = [];
|
|
108
|
-
const
|
|
95
|
+
const steps: Array<GoogleInteractionsStep> = [];
|
|
109
96
|
|
|
110
97
|
for (const message of compactedPrompt) {
|
|
111
98
|
switch (message.role) {
|
|
@@ -114,14 +101,10 @@ export function convertToGoogleInteractionsInput({
|
|
|
114
101
|
break;
|
|
115
102
|
}
|
|
116
103
|
case 'user': {
|
|
117
|
-
const content: Array<
|
|
104
|
+
const content: Array<GoogleInteractionsContentBlock> = [];
|
|
118
105
|
for (const part of message.content) {
|
|
119
106
|
if (part.type === 'text') {
|
|
120
|
-
|
|
121
|
-
type: 'text',
|
|
122
|
-
text: part.text,
|
|
123
|
-
};
|
|
124
|
-
content.push(block);
|
|
107
|
+
content.push({ type: 'text', text: part.text });
|
|
125
108
|
} else if (part.type === 'file') {
|
|
126
109
|
const fileBlock = convertFilePartToContent({
|
|
127
110
|
part,
|
|
@@ -135,20 +118,34 @@ export function convertToGoogleInteractionsInput({
|
|
|
135
118
|
}
|
|
136
119
|
const merged = mergeAdjacentTextContent(content);
|
|
137
120
|
if (merged.length > 0) {
|
|
138
|
-
|
|
121
|
+
steps.push({ type: 'user_input', content: merged });
|
|
139
122
|
}
|
|
140
123
|
break;
|
|
141
124
|
}
|
|
142
125
|
case 'assistant': {
|
|
143
|
-
|
|
126
|
+
/*
|
|
127
|
+
* Prior assistant content fans out into one step per logical block.
|
|
128
|
+
* Adjacent text/image content blocks are coalesced into a single
|
|
129
|
+
* `model_output` step (matching how the API emits them on output);
|
|
130
|
+
* reasoning and tool-calls each become their own step.
|
|
131
|
+
*/
|
|
132
|
+
let pendingModelOutput: Array<GoogleInteractionsContentBlock> = [];
|
|
133
|
+
const flushModelOutput = () => {
|
|
134
|
+
if (pendingModelOutput.length > 0) {
|
|
135
|
+
steps.push({ type: 'model_output', content: pendingModelOutput });
|
|
136
|
+
pendingModelOutput = [];
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
144
140
|
for (const part of message.content) {
|
|
145
141
|
if (part.type === 'text') {
|
|
146
|
-
|
|
142
|
+
pendingModelOutput.push({ type: 'text', text: part.text });
|
|
147
143
|
} else if (part.type === 'reasoning') {
|
|
144
|
+
flushModelOutput();
|
|
148
145
|
const signature = part.providerOptions?.google?.signature as
|
|
149
146
|
| string
|
|
150
147
|
| undefined;
|
|
151
|
-
|
|
148
|
+
steps.push({
|
|
152
149
|
type: 'thought',
|
|
153
150
|
...(signature != null ? { signature } : {}),
|
|
154
151
|
summary:
|
|
@@ -163,9 +160,10 @@ export function convertToGoogleInteractionsInput({
|
|
|
163
160
|
mediaResolution,
|
|
164
161
|
});
|
|
165
162
|
if (fileBlock != null) {
|
|
166
|
-
|
|
163
|
+
pendingModelOutput.push(fileBlock);
|
|
167
164
|
}
|
|
168
165
|
} else if (part.type === 'tool-call') {
|
|
166
|
+
flushModelOutput();
|
|
169
167
|
const signature = part.providerOptions?.google?.signature as
|
|
170
168
|
| string
|
|
171
169
|
| undefined;
|
|
@@ -173,7 +171,7 @@ export function convertToGoogleInteractionsInput({
|
|
|
173
171
|
typeof part.input === 'string'
|
|
174
172
|
? safeParseToolArgs(part.input)
|
|
175
173
|
: ((part.input ?? {}) as Record<string, unknown>);
|
|
176
|
-
|
|
174
|
+
steps.push({
|
|
177
175
|
type: 'function_call',
|
|
178
176
|
id: part.toolCallId,
|
|
179
177
|
name: part.toolName,
|
|
@@ -187,51 +185,17 @@ export function convertToGoogleInteractionsInput({
|
|
|
187
185
|
});
|
|
188
186
|
}
|
|
189
187
|
}
|
|
190
|
-
|
|
191
|
-
turns.push({ role: 'model', content });
|
|
192
|
-
}
|
|
188
|
+
flushModelOutput();
|
|
193
189
|
break;
|
|
194
190
|
}
|
|
195
191
|
case 'tool': {
|
|
196
192
|
/*
|
|
197
|
-
* Tool-result messages are emitted as a `
|
|
198
|
-
* holds one `function_result` block per tool-result part.
|
|
199
|
-
* (
|
|
200
|
-
*
|
|
201
|
-
* `src/interactions/resources/interactions.ts` `FunctionResultContent`
|
|
202
|
-
* around line 979 — RESOLVES PRD Open Q2):
|
|
203
|
-
*
|
|
204
|
-
* {
|
|
205
|
-
* role: 'user',
|
|
206
|
-
* content: [
|
|
207
|
-
* {
|
|
208
|
-
* type: 'function_result',
|
|
209
|
-
* call_id: <id from the matching function_call block>,
|
|
210
|
-
* name: <tool name>,
|
|
211
|
-
* result: <string | unknown | Array<TextContent|ImageContent>>,
|
|
212
|
-
* is_error?: boolean,
|
|
213
|
-
* signature?: string,
|
|
214
|
-
* },
|
|
215
|
-
* ],
|
|
216
|
-
* }
|
|
217
|
-
*
|
|
218
|
-
* The `result` field is a discriminated union: a plain string for
|
|
219
|
-
* text-only results, or an array of `text` / `image` content blocks
|
|
220
|
-
* for mixed text/image results. Our converter takes the AI SDK
|
|
221
|
-
* canonical `LanguageModelV4ToolResultOutput` and maps:
|
|
222
|
-
* - `{ type: 'text', value }` → `result: <string>`
|
|
223
|
-
* - `{ type: 'json', value }` → `result: <stringified JSON>`
|
|
224
|
-
* - `{ type: 'error-text', value }` → `result: <string>` + `is_error: true`
|
|
225
|
-
* - `{ type: 'error-json', value }` → `result: <stringified JSON>` + `is_error: true`
|
|
226
|
-
* - `{ type: 'execution-denied', reason }` → `result: <reason>` + `is_error: true`
|
|
227
|
-
* - `{ type: 'content', value: [...] }` → `result: Array<text|image>`
|
|
228
|
-
* where each AI SDK `file` part with `mediaType: image/*` becomes
|
|
229
|
-
* an Interactions `image` block (file-data path matches
|
|
230
|
-
* `convertFilePartToContent` for top-level user images), and `text`
|
|
231
|
-
* parts pass through. Non-image file parts fall back to a warning
|
|
232
|
-
* because `FunctionResultContent.result` only accepts text/image.
|
|
193
|
+
* Tool-result messages are emitted as a `user_input` step whose
|
|
194
|
+
* content holds one `function_result` block per tool-result part.
|
|
195
|
+
* `function_result` remains a content-block type (it sits inside
|
|
196
|
+
* a step), not a top-level step type.
|
|
233
197
|
*/
|
|
234
|
-
const content: Array<
|
|
198
|
+
const content: Array<GoogleInteractionsContentBlock> = [];
|
|
235
199
|
for (const part of message.content) {
|
|
236
200
|
if (part.type !== 'tool-result') {
|
|
237
201
|
warnings.push({
|
|
@@ -252,7 +216,7 @@ export function convertToGoogleInteractionsInput({
|
|
|
252
216
|
content.push(block);
|
|
253
217
|
}
|
|
254
218
|
if (content.length > 0) {
|
|
255
|
-
|
|
219
|
+
steps.push({ type: 'user_input', content });
|
|
256
220
|
}
|
|
257
221
|
break;
|
|
258
222
|
}
|
|
@@ -262,39 +226,12 @@ export function convertToGoogleInteractionsInput({
|
|
|
262
226
|
const systemInstruction =
|
|
263
227
|
systemTexts.length > 0 ? systemTexts.join('\n\n') : undefined;
|
|
264
228
|
|
|
265
|
-
|
|
266
|
-
if (turns.length === 0) {
|
|
267
|
-
input = '';
|
|
268
|
-
} else if (
|
|
269
|
-
turns.length === 1 &&
|
|
270
|
-
turns[0].role === 'user' &&
|
|
271
|
-
Array.isArray(turns[0].content)
|
|
272
|
-
) {
|
|
273
|
-
/*
|
|
274
|
-
* Single-turn user prompt: send the bare `Array<Content>` shape per the
|
|
275
|
-
* Interactions API's preferred single-turn format.
|
|
276
|
-
*/
|
|
277
|
-
input = turns[0].content;
|
|
278
|
-
} else {
|
|
279
|
-
input = turns;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return { input, systemInstruction, warnings };
|
|
229
|
+
return { input: steps, systemInstruction, warnings };
|
|
283
230
|
}
|
|
284
231
|
|
|
285
232
|
/**
|
|
286
233
|
* Maps a single AI SDK `LanguageModelV4FilePart` to a Gemini Interactions
|
|
287
234
|
* content block (`image` / `audio` / `document` / `video`).
|
|
288
|
-
*
|
|
289
|
-
* Rules for the four `data.type` cases:
|
|
290
|
-
* - `data` (Uint8Array / base64) → block with inline `data` (base64) +
|
|
291
|
-
* `mime_type`.
|
|
292
|
-
* - `url` → block with `uri` set to the URL string verbatim.
|
|
293
|
-
* - `reference` → block with `uri` set to the resolved `google` provider
|
|
294
|
-
* reference (Files API URI like
|
|
295
|
-
* `https://generativelanguage.googleapis.com/v1beta/files/<id>`).
|
|
296
|
-
* - `text` → collapsed to a `text` block (the wire format has no text-on-file
|
|
297
|
-
* shape; emit an inline text block instead).
|
|
298
235
|
*/
|
|
299
236
|
function convertFilePartToContent({
|
|
300
237
|
part,
|
|
@@ -339,12 +276,6 @@ function convertFilePartToContent({
|
|
|
339
276
|
return undefined;
|
|
340
277
|
}
|
|
341
278
|
|
|
342
|
-
/*
|
|
343
|
-
* `resolution` is per-block on the wire (`ImageContent.resolution`,
|
|
344
|
-
* `VideoContent.resolution`); only image and video carry it (see
|
|
345
|
-
* `googleapis/js-genai` `src/interactions/resources/interactions.ts`).
|
|
346
|
-
* Audio / document blocks ignore the option silently.
|
|
347
|
-
*/
|
|
348
279
|
const resolutionField =
|
|
349
280
|
mediaResolution != null && (kind === 'image' || kind === 'video')
|
|
350
281
|
? { resolution: mediaResolution }
|
|
@@ -388,23 +319,9 @@ function convertFilePartToContent({
|
|
|
388
319
|
}
|
|
389
320
|
|
|
390
321
|
/*
|
|
391
|
-
* Drops assistant
|
|
392
|
-
* (`previousInteractionId`)
|
|
393
|
-
*
|
|
394
|
-
* whose `tool-result.toolCallId` matches a `tool-call.toolCallId` from the
|
|
395
|
-
* dropped assistant turn — server-state already has the matching tool result
|
|
396
|
-
* baked in, and re-sending it without its paired call would be malformed.
|
|
397
|
-
*
|
|
398
|
-
* An assistant message is considered "part of the linked interaction" if any
|
|
399
|
-
* of its content parts carry `providerOptions.google.interactionId ===
|
|
400
|
-
* previousInteractionId`. This is stamped by `parseGoogleInteractionsOutputs`
|
|
401
|
-
* (and the stream transformer) on every output content part.
|
|
402
|
-
*
|
|
403
|
-
* User messages are always kept regardless of where they fell in the prior
|
|
404
|
-
* conversation — only assistant model output and its tool plumbing live on the
|
|
405
|
-
* server. (Note that the AI SDK does not stamp `interactionId` onto user
|
|
406
|
-
* messages, so even if it did, this function would not have a way to identify
|
|
407
|
-
* which user message belongs to which interaction.)
|
|
322
|
+
* Drops assistant messages that were part of the linked interaction
|
|
323
|
+
* (`previousInteractionId`). Tool-result turns whose tool-call counterpart
|
|
324
|
+
* was dropped are also pruned to keep the message stream well-formed.
|
|
408
325
|
*/
|
|
409
326
|
function compactPromptForPreviousInteraction({
|
|
410
327
|
prompt,
|
|
@@ -606,21 +523,18 @@ function filePartToImageBlock({
|
|
|
606
523
|
}
|
|
607
524
|
|
|
608
525
|
/*
|
|
609
|
-
* Collapses runs of adjacent text content blocks within a single user
|
|
610
|
-
* into one combined text block, separated by a blank line.
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* keeps the wire shape compact and preserves intent when an inline text file
|
|
614
|
-
* sits next to a regular text part. Text blocks carrying `annotations` are
|
|
615
|
-
* left untouched (annotations are tied to specific text spans).
|
|
526
|
+
* Collapses runs of adjacent text content blocks within a single user step
|
|
527
|
+
* into one combined text block, separated by a blank line. Text blocks
|
|
528
|
+
* carrying `annotations` are left untouched (annotations are tied to specific
|
|
529
|
+
* text spans).
|
|
616
530
|
*/
|
|
617
531
|
function mergeAdjacentTextContent(
|
|
618
|
-
content: Array<
|
|
619
|
-
): Array<
|
|
532
|
+
content: Array<GoogleInteractionsContentBlock>,
|
|
533
|
+
): Array<GoogleInteractionsContentBlock> {
|
|
620
534
|
if (content.length < 2) {
|
|
621
535
|
return content;
|
|
622
536
|
}
|
|
623
|
-
const result: Array<
|
|
537
|
+
const result: Array<GoogleInteractionsContentBlock> = [];
|
|
624
538
|
for (const block of content) {
|
|
625
539
|
const last = result[result.length - 1];
|
|
626
540
|
if (
|
|
@@ -60,7 +60,7 @@ export function annotationToSource({
|
|
|
60
60
|
}
|
|
61
61
|
case 'file_citation': {
|
|
62
62
|
const a = annotation as GoogleInteractionsFileCitation;
|
|
63
|
-
const uri = a.
|
|
63
|
+
const uri = a.url ?? a.document_uri ?? a.file_name;
|
|
64
64
|
if (uri == null || uri.length === 0) return undefined;
|
|
65
65
|
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
|
66
66
|
return {
|
|
@@ -176,10 +176,10 @@ export function builtinToolResultToSources({
|
|
|
176
176
|
const entry = raw as {
|
|
177
177
|
file_name?: string;
|
|
178
178
|
document_uri?: string;
|
|
179
|
-
|
|
179
|
+
url?: string;
|
|
180
180
|
title?: string;
|
|
181
181
|
};
|
|
182
|
-
const uri = entry.
|
|
182
|
+
const uri = entry.url ?? entry.document_uri ?? entry.file_name;
|
|
183
183
|
if (uri == null || uri.length === 0) continue;
|
|
184
184
|
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
|
185
185
|
sources.push({
|