@veryfront/ext-llm-google 0.1.1186 → 0.1.1189
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/README.md +74 -15
- package/esm/_dnt.polyfills.d.ts +12 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/google-content-parts.d.ts +45 -0
- package/esm/google-content-parts.d.ts.map +1 -0
- package/esm/google-content-parts.js +164 -0
- package/esm/google-grounding-metadata.d.ts +15 -0
- package/esm/google-grounding-metadata.d.ts.map +1 -0
- package/esm/google-grounding-metadata.js +87 -0
- package/esm/google-provider.d.ts +4 -3
- package/esm/google-provider.d.ts.map +1 -1
- package/esm/google-provider.js +407 -77
- package/esm/google-request-builder.d.ts +7 -55
- package/esm/google-request-builder.d.ts.map +1 -1
- package/esm/google-request-builder.js +403 -13
- package/esm/google-stream.d.ts +9 -1
- package/esm/google-stream.d.ts.map +1 -1
- package/esm/google-stream.js +590 -65
- package/esm/google-thought-signatures.d.ts +5 -0
- package/esm/google-thought-signatures.d.ts.map +1 -0
- package/esm/google-thought-signatures.js +296 -0
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +10 -1
- package/package.json +3 -3
package/esm/google-stream.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
import { parseSseChunk, readRecord, stringifyJsonValue, } from "veryfront/provider/shared";
|
|
1
|
+
import { mergeUsage, parseFinalSseChunk, parseSseChunk, ProviderRequestError, readRecord, stringifyJsonValue, } from "veryfront/provider/shared";
|
|
2
|
+
import { createGoogleToolCallCorrelationRegistry, GOOGLE_CODE_EXECUTION_TOOL_NAME, googleCodeExecutionInput, googleCodeExecutionOutput, readGoogleCodeExecutionResult, readGoogleExecutableCode, readGooglePartDataField, } from "./google-content-parts.js";
|
|
3
|
+
import { mergeGoogleGroundingMetadata } from "./google-grounding-metadata.js";
|
|
4
|
+
import { createGoogleProviderMetadata, MAX_GOOGLE_PROVIDER_METADATA_BYTES, readGoogleThoughtSignature, } from "./google-thought-signatures.js";
|
|
5
|
+
export const MAX_GOOGLE_SSE_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
export const MAX_GOOGLE_SSE_BUFFER_CODE_UNITS = 8 * 1024 * 1024;
|
|
7
|
+
export const MAX_GOOGLE_RETAINED_STATE_BYTES = MAX_GOOGLE_PROVIDER_METADATA_BYTES;
|
|
8
|
+
export const MAX_GOOGLE_RETAINED_STATE_ITEMS = 8_192;
|
|
9
|
+
const GOOGLE_RETAINED_STATE_ENCODER = new TextEncoder();
|
|
10
|
+
const GOOGLE_CONTENT_FILTER_FINISH_REASONS = new Set([
|
|
11
|
+
"SAFETY",
|
|
12
|
+
"RECITATION",
|
|
13
|
+
"LANGUAGE",
|
|
14
|
+
"BLOCKLIST",
|
|
15
|
+
"PROHIBITED_CONTENT",
|
|
16
|
+
"SPII",
|
|
17
|
+
"IMAGE_SAFETY",
|
|
18
|
+
"IMAGE_PROHIBITED_CONTENT",
|
|
19
|
+
"IMAGE_RECITATION",
|
|
20
|
+
"ESCALATION",
|
|
21
|
+
]);
|
|
2
22
|
export function normalizeGoogleFinishReason(raw) {
|
|
3
23
|
if (typeof raw !== "string") {
|
|
4
24
|
return null;
|
|
@@ -8,10 +28,10 @@ export function normalizeGoogleFinishReason(raw) {
|
|
|
8
28
|
return { unified: "stop", raw };
|
|
9
29
|
case "MAX_TOKENS":
|
|
10
30
|
return { unified: "length", raw };
|
|
11
|
-
case "SAFETY":
|
|
12
|
-
case "RECITATION":
|
|
13
|
-
return { unified: "content-filter", raw };
|
|
14
31
|
default:
|
|
32
|
+
if (GOOGLE_CONTENT_FILTER_FINISH_REASONS.has(raw)) {
|
|
33
|
+
return { unified: "content-filter", raw };
|
|
34
|
+
}
|
|
15
35
|
return raw.toLowerCase();
|
|
16
36
|
}
|
|
17
37
|
}
|
|
@@ -21,20 +41,25 @@ export function extractGoogleUsage(payload) {
|
|
|
21
41
|
if (!usage) {
|
|
22
42
|
return undefined;
|
|
23
43
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
const readTokenCount = (value) => typeof value === "number" &&
|
|
45
|
+
Number.isFinite(value) &&
|
|
46
|
+
Number.isSafeInteger(value) &&
|
|
47
|
+
value >= 0
|
|
48
|
+
? value
|
|
49
|
+
: undefined;
|
|
50
|
+
const inputTokens = readTokenCount(usage.promptTokenCount);
|
|
51
|
+
const outputTokens = readTokenCount(usage.candidatesTokenCount);
|
|
52
|
+
const totalTokens = readTokenCount(usage.totalTokenCount);
|
|
53
|
+
const cacheReadInputTokens = readTokenCount(usage.cachedContentTokenCount);
|
|
54
|
+
const reasoningTokens = readTokenCount(usage.thoughtsTokenCount);
|
|
55
|
+
const normalized = {
|
|
56
|
+
inputTokens,
|
|
57
|
+
outputTokens,
|
|
58
|
+
totalTokens,
|
|
59
|
+
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
60
|
+
...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
|
|
37
61
|
};
|
|
62
|
+
return Object.values(normalized).some((value) => value !== undefined) ? normalized : undefined;
|
|
38
63
|
}
|
|
39
64
|
export function extractFirstGoogleCandidate(payload) {
|
|
40
65
|
const record = readRecord(payload);
|
|
@@ -56,66 +81,320 @@ export function extractGoogleCandidateParts(payload) {
|
|
|
56
81
|
return record ? [record] : [];
|
|
57
82
|
});
|
|
58
83
|
}
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
function invalidGoogleStream(context, issue) {
|
|
85
|
+
return new ProviderRequestError({
|
|
86
|
+
provider: "google",
|
|
87
|
+
status: 200,
|
|
88
|
+
message: `${context.providerLabel ?? "google"} request failed: invalid successful stream (${issue})`,
|
|
89
|
+
retryable: false,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function mergeGoogleUsage(current, payload) {
|
|
93
|
+
const merged = mergeUsage(current, extractGoogleUsage(payload));
|
|
94
|
+
return merged && Object.keys(merged).length > 0 ? merged : undefined;
|
|
95
|
+
}
|
|
96
|
+
function isContentFilterFinishReason(value) {
|
|
97
|
+
return typeof value === "object" && value?.unified === "content-filter";
|
|
98
|
+
}
|
|
99
|
+
export async function* streamGoogleCompatibleParts(stream, context = {}) {
|
|
100
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
61
101
|
let buffer = "";
|
|
62
|
-
const seenToolCalls = new
|
|
102
|
+
const seenToolCalls = new Map();
|
|
103
|
+
const seenCodeExecutions = new Map();
|
|
104
|
+
const seenCodeExecutionResults = new Map();
|
|
105
|
+
const seenAnonymousCodeExecutions = new Map();
|
|
106
|
+
const seenAnonymousCodeExecutionResults = new Map();
|
|
107
|
+
const pendingAnonymousCodeExecutions = [];
|
|
108
|
+
const toolCallRegistry = createGoogleToolCallCorrelationRegistry();
|
|
109
|
+
const rawAssistantParts = [];
|
|
110
|
+
let retainedStateBytes = 0;
|
|
111
|
+
let retainedStateItems = 0;
|
|
63
112
|
let reasoningId = null;
|
|
113
|
+
let reasoningSignature;
|
|
64
114
|
let reasoningIndex = 0;
|
|
65
115
|
let finishReason = null;
|
|
66
116
|
let usage;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
117
|
+
let groundingMetadata;
|
|
118
|
+
let sawCandidateEnvelope = false;
|
|
119
|
+
let sawOutput = false;
|
|
120
|
+
let sawFinishReason = false;
|
|
121
|
+
let sawDone = false;
|
|
122
|
+
const reserveRetainedItem = (issue) => {
|
|
123
|
+
if (retainedStateItems >= MAX_GOOGLE_RETAINED_STATE_ITEMS) {
|
|
124
|
+
throw invalidGoogleStream(context, `retained state exceeded ${MAX_GOOGLE_RETAINED_STATE_ITEMS} items (${issue})`);
|
|
125
|
+
}
|
|
126
|
+
retainedStateItems++;
|
|
127
|
+
};
|
|
128
|
+
const reserveRetainedBytes = (value, issue) => {
|
|
129
|
+
const bytes = GOOGLE_RETAINED_STATE_ENCODER.encode(value).byteLength;
|
|
130
|
+
if (bytes > MAX_GOOGLE_RETAINED_STATE_BYTES - retainedStateBytes) {
|
|
131
|
+
throw invalidGoogleStream(context, `retained state exceeded ${MAX_GOOGLE_RETAINED_STATE_BYTES} UTF-8 bytes (${issue})`);
|
|
132
|
+
}
|
|
133
|
+
retainedStateBytes += bytes;
|
|
134
|
+
};
|
|
135
|
+
const retainRawAssistantPart = (part) => {
|
|
136
|
+
let serialized;
|
|
137
|
+
try {
|
|
138
|
+
serialized = stringifyJsonValue(part);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw invalidGoogleStream(context, "candidate part could not be retained safely");
|
|
142
|
+
}
|
|
143
|
+
reserveRetainedItem("raw candidate part");
|
|
144
|
+
reserveRetainedBytes(serialized, "raw candidate part");
|
|
145
|
+
return rawAssistantParts.push(part) - 1;
|
|
146
|
+
};
|
|
147
|
+
const reserveCorrelation = (issue, ...values) => {
|
|
148
|
+
reserveRetainedItem(issue);
|
|
149
|
+
for (const value of values)
|
|
150
|
+
reserveRetainedBytes(value, issue);
|
|
151
|
+
};
|
|
152
|
+
const mergeReplaySignature = (prior, thoughtSignature, changedIssue) => {
|
|
153
|
+
if (thoughtSignature !== undefined &&
|
|
154
|
+
prior.thoughtSignature !== undefined &&
|
|
155
|
+
thoughtSignature !== prior.thoughtSignature) {
|
|
156
|
+
throw invalidGoogleStream(context, changedIssue);
|
|
157
|
+
}
|
|
158
|
+
if (thoughtSignature !== undefined && prior.thoughtSignature === undefined) {
|
|
159
|
+
const previousRawPart = rawAssistantParts[prior.rawPartIndex];
|
|
160
|
+
if (!previousRawPart) {
|
|
161
|
+
throw invalidGoogleStream(context, "candidate replay history was inconsistent");
|
|
74
162
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
163
|
+
reserveRetainedBytes(thoughtSignature, "replayed thought signature");
|
|
164
|
+
previousRawPart.thoughtSignature = thoughtSignature;
|
|
165
|
+
prior.thoughtSignature = thoughtSignature;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function* processEvent(event) {
|
|
169
|
+
if (event === "[DONE]") {
|
|
170
|
+
if (sawDone) {
|
|
171
|
+
throw invalidGoogleStream(context, "stream contained duplicate [DONE] markers");
|
|
172
|
+
}
|
|
173
|
+
sawDone = true;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (sawDone) {
|
|
177
|
+
throw invalidGoogleStream(context, "event appeared after [DONE]");
|
|
178
|
+
}
|
|
179
|
+
const record = readRecord(event);
|
|
180
|
+
if (!record) {
|
|
181
|
+
throw invalidGoogleStream(context, "event was not an object");
|
|
182
|
+
}
|
|
183
|
+
const usageRecord = readRecord(record.usageMetadata);
|
|
184
|
+
usage = mergeGoogleUsage(usage, record);
|
|
185
|
+
const promptFeedback = readRecord(record.promptFeedback);
|
|
186
|
+
const promptBlockReason = promptFeedback?.blockReason;
|
|
187
|
+
if (promptBlockReason !== undefined &&
|
|
188
|
+
(typeof promptBlockReason !== "string" || promptBlockReason.length === 0)) {
|
|
189
|
+
throw invalidGoogleStream(context, "prompt block reason was malformed");
|
|
190
|
+
}
|
|
191
|
+
if (typeof promptBlockReason === "string") {
|
|
192
|
+
if (sawFinishReason || sawDone) {
|
|
193
|
+
throw invalidGoogleStream(context, "stream contained duplicate terminal reasons");
|
|
194
|
+
}
|
|
195
|
+
finishReason = { unified: "content-filter", raw: promptBlockReason };
|
|
196
|
+
sawCandidateEnvelope = true;
|
|
197
|
+
sawFinishReason = true;
|
|
198
|
+
}
|
|
199
|
+
if (record.candidates === undefined) {
|
|
200
|
+
if (!usageRecord && typeof promptBlockReason !== "string") {
|
|
201
|
+
throw invalidGoogleStream(context, "event had neither candidates nor usage");
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!Array.isArray(record.candidates)) {
|
|
206
|
+
throw invalidGoogleStream(context, "candidates was not an array");
|
|
207
|
+
}
|
|
208
|
+
if (record.candidates.length === 0) {
|
|
209
|
+
if (!usageRecord && typeof promptBlockReason !== "string") {
|
|
210
|
+
throw invalidGoogleStream(context, "empty candidates event had no usage or block reason");
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (record.candidates.length !== 1) {
|
|
215
|
+
throw invalidGoogleStream(context, "event contained multiple candidates");
|
|
216
|
+
}
|
|
217
|
+
if (typeof promptBlockReason === "string") {
|
|
218
|
+
throw invalidGoogleStream(context, "blocked event also contained a candidate");
|
|
219
|
+
}
|
|
220
|
+
const candidate = readRecord(record.candidates[0]);
|
|
221
|
+
if (!candidate) {
|
|
222
|
+
throw invalidGoogleStream(context, "first candidate was not an object");
|
|
223
|
+
}
|
|
224
|
+
if (sawFinishReason || sawDone) {
|
|
225
|
+
const issue = typeof candidate.finishReason === "string"
|
|
226
|
+
? "stream contained duplicate terminal reasons"
|
|
227
|
+
: "candidate appeared after a terminal marker";
|
|
228
|
+
throw invalidGoogleStream(context, issue);
|
|
229
|
+
}
|
|
230
|
+
sawCandidateEnvelope = true;
|
|
231
|
+
if (candidate.groundingMetadata !== undefined) {
|
|
232
|
+
try {
|
|
233
|
+
groundingMetadata = mergeGoogleGroundingMetadata(groundingMetadata, candidate.groundingMetadata);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
throw invalidGoogleStream(context, "candidate grounding metadata was malformed");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (candidate.finishReason !== undefined &&
|
|
240
|
+
(typeof candidate.finishReason !== "string" || candidate.finishReason.length === 0)) {
|
|
241
|
+
throw invalidGoogleStream(context, "candidate finish reason was malformed");
|
|
242
|
+
}
|
|
243
|
+
const candidateFinishReason = typeof candidate.finishReason === "string"
|
|
244
|
+
? candidate.finishReason
|
|
245
|
+
: undefined;
|
|
246
|
+
let parts = [];
|
|
247
|
+
if (candidate.content !== undefined) {
|
|
248
|
+
const content = readRecord(candidate.content);
|
|
249
|
+
if (!content) {
|
|
250
|
+
throw invalidGoogleStream(context, "candidate content was not an object");
|
|
251
|
+
}
|
|
252
|
+
if (content.parts !== undefined && !Array.isArray(content.parts)) {
|
|
253
|
+
throw invalidGoogleStream(context, "candidate content parts was not an array");
|
|
254
|
+
}
|
|
255
|
+
parts = Array.isArray(content.parts) ? content.parts : [];
|
|
256
|
+
}
|
|
257
|
+
else if (candidateFinishReason === undefined) {
|
|
258
|
+
throw invalidGoogleStream(context, "candidate had neither content nor a finish reason");
|
|
259
|
+
}
|
|
260
|
+
for (const [candidatePartIndex, rawPart] of parts.entries()) {
|
|
261
|
+
const part = readRecord(rawPart);
|
|
262
|
+
if (!part) {
|
|
263
|
+
throw invalidGoogleStream(context, "candidate content part was not an object");
|
|
264
|
+
}
|
|
265
|
+
if (part.thought !== undefined && typeof part.thought !== "boolean") {
|
|
266
|
+
throw invalidGoogleStream(context, "candidate thought marker was malformed");
|
|
267
|
+
}
|
|
268
|
+
let thoughtSignature;
|
|
269
|
+
try {
|
|
270
|
+
thoughtSignature = readGoogleThoughtSignature(part);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
throw invalidGoogleStream(context, "candidate thought signature was malformed");
|
|
274
|
+
}
|
|
275
|
+
let dataField;
|
|
276
|
+
try {
|
|
277
|
+
dataField = readGooglePartDataField(part);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const issue = error instanceof Error &&
|
|
281
|
+
(error.message.includes("unsupported") || error.message.includes("unknown"))
|
|
282
|
+
? "candidate part contained an unsupported data field"
|
|
283
|
+
: "candidate part contained multiple data fields";
|
|
284
|
+
throw invalidGoogleStream(context, issue);
|
|
285
|
+
}
|
|
286
|
+
const partText = dataField === "text" && typeof part.text === "string"
|
|
287
|
+
? part.text
|
|
288
|
+
: undefined;
|
|
289
|
+
if (dataField === "text" && partText === undefined) {
|
|
290
|
+
throw invalidGoogleStream(context, "candidate text part was malformed");
|
|
291
|
+
}
|
|
292
|
+
if (dataField === "text") {
|
|
293
|
+
retainRawAssistantPart(part);
|
|
294
|
+
}
|
|
295
|
+
if (partText !== undefined && part.thought === true) {
|
|
296
|
+
if (reasoningId &&
|
|
297
|
+
thoughtSignature !== undefined &&
|
|
298
|
+
reasoningSignature !== undefined &&
|
|
299
|
+
thoughtSignature !== reasoningSignature) {
|
|
99
300
|
yield {
|
|
100
301
|
type: "reasoning-end",
|
|
101
302
|
id: reasoningId,
|
|
303
|
+
signature: reasoningSignature,
|
|
102
304
|
};
|
|
103
305
|
reasoningId = null;
|
|
306
|
+
reasoningSignature = undefined;
|
|
104
307
|
}
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
308
|
+
if (!reasoningId && (partText.length > 0 || thoughtSignature !== undefined)) {
|
|
309
|
+
reasoningId = `reasoning-${reasoningIndex++}`;
|
|
310
|
+
yield { type: "reasoning-start", id: reasoningId };
|
|
311
|
+
}
|
|
312
|
+
if (thoughtSignature !== undefined) {
|
|
313
|
+
reasoningSignature = thoughtSignature;
|
|
314
|
+
}
|
|
315
|
+
if (reasoningId) {
|
|
316
|
+
sawOutput = true;
|
|
317
|
+
if (partText.length > 0) {
|
|
318
|
+
yield {
|
|
319
|
+
type: "reasoning-delta",
|
|
320
|
+
id: reasoningId,
|
|
321
|
+
delta: partText,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (reasoningId) {
|
|
328
|
+
yield {
|
|
329
|
+
type: "reasoning-end",
|
|
330
|
+
id: reasoningId,
|
|
331
|
+
...(reasoningSignature !== undefined ? { signature: reasoningSignature } : {}),
|
|
332
|
+
};
|
|
333
|
+
reasoningId = null;
|
|
334
|
+
reasoningSignature = undefined;
|
|
335
|
+
}
|
|
336
|
+
if (partText !== undefined) {
|
|
337
|
+
if (partText.length > 0) {
|
|
338
|
+
sawOutput = true;
|
|
339
|
+
yield { type: "text-delta", delta: partText };
|
|
108
340
|
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (dataField === "functionCall") {
|
|
109
344
|
const functionCall = readRecord(part.functionCall);
|
|
110
|
-
if (
|
|
111
|
-
|
|
345
|
+
if (!functionCall ||
|
|
346
|
+
typeof functionCall.name !== "string" ||
|
|
347
|
+
functionCall.name.length === 0) {
|
|
348
|
+
throw invalidGoogleStream(context, "candidate function call was malformed");
|
|
349
|
+
}
|
|
350
|
+
if (functionCall.id !== undefined &&
|
|
351
|
+
(typeof functionCall.id !== "string" || functionCall.id.length === 0)) {
|
|
352
|
+
throw invalidGoogleStream(context, "candidate function call id was malformed");
|
|
353
|
+
}
|
|
354
|
+
// Gemini omits `args` entirely for zero-parameter tool calls.
|
|
355
|
+
const functionCallArgs = functionCall.args === undefined
|
|
356
|
+
? {}
|
|
357
|
+
: readRecord(functionCall.args);
|
|
358
|
+
if (!functionCallArgs) {
|
|
359
|
+
throw invalidGoogleStream(context, "candidate function call arguments were not an object");
|
|
360
|
+
}
|
|
361
|
+
let serializedInput;
|
|
362
|
+
try {
|
|
363
|
+
serializedInput = stringifyJsonValue(functionCallArgs);
|
|
112
364
|
}
|
|
113
|
-
|
|
114
|
-
|
|
365
|
+
catch {
|
|
366
|
+
throw invalidGoogleStream(context, "candidate function call arguments were malformed");
|
|
367
|
+
}
|
|
368
|
+
const providerId = typeof functionCall.id === "string" ? functionCall.id : undefined;
|
|
369
|
+
const rawPartIndex = rawAssistantParts.length;
|
|
370
|
+
// Gemini may replay an anonymous function call in later candidate
|
|
371
|
+
// envelopes. Its candidate-part position is stable across those
|
|
372
|
+
// cumulative snapshots; our growing retained-output index is not.
|
|
373
|
+
const deduplicationKey = providerId ?? `anonymous-${candidatePartIndex}`;
|
|
374
|
+
const callShape = `${functionCall.name}\u0000${serializedInput}`;
|
|
375
|
+
const previousToolCall = seenToolCalls.get(deduplicationKey);
|
|
376
|
+
if (previousToolCall !== undefined) {
|
|
377
|
+
if (previousToolCall.callShape !== callShape) {
|
|
378
|
+
throw invalidGoogleStream(context, "candidate function call changed after emission");
|
|
379
|
+
}
|
|
380
|
+
mergeReplaySignature(previousToolCall, thoughtSignature, "candidate function call thought signature changed after emission");
|
|
115
381
|
continue;
|
|
116
382
|
}
|
|
117
|
-
|
|
118
|
-
|
|
383
|
+
let toolCallId;
|
|
384
|
+
try {
|
|
385
|
+
toolCallId = toolCallRegistry.registerFunctionCall(rawPartIndex, providerId);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
throw invalidGoogleStream(context, "candidate tool call id was duplicated");
|
|
389
|
+
}
|
|
390
|
+
retainRawAssistantPart(part);
|
|
391
|
+
reserveCorrelation("function-call correlation", deduplicationKey, callShape, ...(thoughtSignature === undefined ? [] : [thoughtSignature]));
|
|
392
|
+
seenToolCalls.set(deduplicationKey, {
|
|
393
|
+
callShape,
|
|
394
|
+
rawPartIndex,
|
|
395
|
+
...(thoughtSignature !== undefined ? { thoughtSignature } : {}),
|
|
396
|
+
});
|
|
397
|
+
sawOutput = true;
|
|
119
398
|
yield {
|
|
120
399
|
type: "tool-input-start",
|
|
121
400
|
id: toolCallId,
|
|
@@ -132,27 +411,273 @@ export async function* streamGoogleCompatibleParts(stream) {
|
|
|
132
411
|
toolName: functionCall.name,
|
|
133
412
|
input: serializedInput,
|
|
134
413
|
};
|
|
414
|
+
continue;
|
|
135
415
|
}
|
|
416
|
+
if (dataField === "executableCode") {
|
|
417
|
+
let executableCode;
|
|
418
|
+
try {
|
|
419
|
+
executableCode = readGoogleExecutableCode(part.executableCode);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
throw invalidGoogleStream(context, "candidate executable code was malformed");
|
|
423
|
+
}
|
|
424
|
+
const serializedInput = stringifyJsonValue(googleCodeExecutionInput(executableCode));
|
|
425
|
+
const callShape = serializedInput;
|
|
426
|
+
let toolCallId;
|
|
427
|
+
if (executableCode.providerId !== undefined) {
|
|
428
|
+
const prior = seenCodeExecutions.get(executableCode.providerId);
|
|
429
|
+
if (prior) {
|
|
430
|
+
if (prior.callShape !== callShape) {
|
|
431
|
+
throw invalidGoogleStream(context, "candidate executable code changed after emission");
|
|
432
|
+
}
|
|
433
|
+
mergeReplaySignature(prior, thoughtSignature, "candidate executable code thought signature changed after emission");
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
toolCallId = toolCallRegistry.registerCodeExecution(executableCode.providerId);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
throw invalidGoogleStream(context, "candidate executable code id was duplicated");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
const prior = seenAnonymousCodeExecutions.get(candidatePartIndex);
|
|
445
|
+
if (prior) {
|
|
446
|
+
if (prior.callShape === callShape) {
|
|
447
|
+
mergeReplaySignature(prior, thoughtSignature, "candidate executable code thought signature changed after emission");
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (!prior.settled) {
|
|
451
|
+
throw invalidGoogleStream(context, "candidate executable code changed after emission");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
toolCallId = toolCallRegistry.registerCodeExecution(undefined);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
throw invalidGoogleStream(context, "candidate executable code id was duplicated");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const rawPartIndex = retainRawAssistantPart(part);
|
|
462
|
+
reserveCorrelation("code-execution correlation", callShape, ...(executableCode.providerId === undefined ? [] : [executableCode.providerId]), ...(thoughtSignature === undefined ? [] : [thoughtSignature]));
|
|
463
|
+
const pending = {
|
|
464
|
+
callShape,
|
|
465
|
+
rawPartIndex,
|
|
466
|
+
...(thoughtSignature !== undefined ? { thoughtSignature } : {}),
|
|
467
|
+
settled: false,
|
|
468
|
+
};
|
|
469
|
+
if (executableCode.providerId === undefined) {
|
|
470
|
+
seenAnonymousCodeExecutions.set(candidatePartIndex, pending);
|
|
471
|
+
pendingAnonymousCodeExecutions.push(pending);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
seenCodeExecutions.set(executableCode.providerId, pending);
|
|
475
|
+
}
|
|
476
|
+
sawOutput = true;
|
|
477
|
+
yield {
|
|
478
|
+
type: "tool-input-start",
|
|
479
|
+
id: toolCallId,
|
|
480
|
+
toolName: GOOGLE_CODE_EXECUTION_TOOL_NAME,
|
|
481
|
+
providerExecuted: true,
|
|
482
|
+
};
|
|
483
|
+
yield {
|
|
484
|
+
type: "tool-input-delta",
|
|
485
|
+
id: toolCallId,
|
|
486
|
+
delta: serializedInput,
|
|
487
|
+
};
|
|
488
|
+
yield {
|
|
489
|
+
type: "tool-call",
|
|
490
|
+
toolCallId,
|
|
491
|
+
toolName: GOOGLE_CODE_EXECUTION_TOOL_NAME,
|
|
492
|
+
input: serializedInput,
|
|
493
|
+
providerExecuted: true,
|
|
494
|
+
};
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
let result;
|
|
498
|
+
try {
|
|
499
|
+
result = readGoogleCodeExecutionResult(part.codeExecutionResult);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
throw invalidGoogleStream(context, "candidate code execution result was malformed");
|
|
503
|
+
}
|
|
504
|
+
const resultValue = googleCodeExecutionOutput(result);
|
|
505
|
+
const resultShape = stringifyJsonValue(resultValue);
|
|
506
|
+
let toolCallId;
|
|
507
|
+
if (result.providerId !== undefined) {
|
|
508
|
+
const priorResult = seenCodeExecutionResults.get(result.providerId);
|
|
509
|
+
if (priorResult) {
|
|
510
|
+
if (priorResult.resultShape !== resultShape) {
|
|
511
|
+
throw invalidGoogleStream(context, "candidate code execution result changed after emission");
|
|
512
|
+
}
|
|
513
|
+
mergeReplaySignature(priorResult, thoughtSignature, "candidate code execution result thought signature changed after emission");
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
toolCallId = toolCallRegistry.resolveCodeExecutionResult(result.providerId);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
throw invalidGoogleStream(context, "candidate code execution result did not match executable code");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const pendingExecution = pendingAnonymousCodeExecutions[0];
|
|
525
|
+
if (!pendingExecution) {
|
|
526
|
+
const priorResult = seenAnonymousCodeExecutionResults.get(candidatePartIndex);
|
|
527
|
+
if (priorResult) {
|
|
528
|
+
if (priorResult.resultShape !== resultShape) {
|
|
529
|
+
throw invalidGoogleStream(context, "candidate code execution result changed after emission");
|
|
530
|
+
}
|
|
531
|
+
mergeReplaySignature(priorResult, thoughtSignature, "candidate code execution result thought signature changed after emission");
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
throw invalidGoogleStream(context, "candidate code execution result did not match executable code");
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
toolCallId = toolCallRegistry.resolveCodeExecutionResult(undefined);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
throw invalidGoogleStream(context, "candidate code execution result did not match executable code");
|
|
541
|
+
}
|
|
542
|
+
pendingExecution.settled = true;
|
|
543
|
+
pendingAnonymousCodeExecutions.shift();
|
|
544
|
+
}
|
|
545
|
+
const rawPartIndex = retainRawAssistantPart(part);
|
|
546
|
+
if (result.providerId !== undefined) {
|
|
547
|
+
reserveCorrelation("code-result correlation", result.providerId, resultShape, ...(thoughtSignature === undefined ? [] : [thoughtSignature]));
|
|
548
|
+
seenCodeExecutionResults.set(result.providerId, {
|
|
549
|
+
resultShape,
|
|
550
|
+
rawPartIndex,
|
|
551
|
+
...(thoughtSignature !== undefined ? { thoughtSignature } : {}),
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
const deduplicationKey = `anonymous-${candidatePartIndex}`;
|
|
556
|
+
reserveCorrelation("code-result correlation", deduplicationKey, resultShape, ...(thoughtSignature === undefined ? [] : [thoughtSignature]));
|
|
557
|
+
seenAnonymousCodeExecutionResults.set(candidatePartIndex, {
|
|
558
|
+
resultShape,
|
|
559
|
+
rawPartIndex,
|
|
560
|
+
...(thoughtSignature !== undefined ? { thoughtSignature } : {}),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
sawOutput = true;
|
|
564
|
+
yield result.isError
|
|
565
|
+
? {
|
|
566
|
+
type: "tool-error",
|
|
567
|
+
toolCallId,
|
|
568
|
+
toolName: GOOGLE_CODE_EXECUTION_TOOL_NAME,
|
|
569
|
+
error: resultValue,
|
|
570
|
+
isError: true,
|
|
571
|
+
providerExecuted: true,
|
|
572
|
+
}
|
|
573
|
+
: {
|
|
574
|
+
type: "tool-result",
|
|
575
|
+
toolCallId,
|
|
576
|
+
toolName: GOOGLE_CODE_EXECUTION_TOOL_NAME,
|
|
577
|
+
result: resultValue,
|
|
578
|
+
providerExecuted: true,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (candidateFinishReason !== undefined) {
|
|
582
|
+
if (sawFinishReason) {
|
|
583
|
+
throw invalidGoogleStream(context, "stream contained duplicate terminal reasons");
|
|
584
|
+
}
|
|
585
|
+
finishReason = normalizeGoogleFinishReason(candidateFinishReason);
|
|
586
|
+
sawFinishReason = true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
for await (const chunk of stream) {
|
|
590
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
591
|
+
throw invalidGoogleStream(context, "stream chunk was not binary data");
|
|
592
|
+
}
|
|
593
|
+
if (chunk.byteLength > MAX_GOOGLE_SSE_CHUNK_BYTES) {
|
|
594
|
+
throw invalidGoogleStream(context, `SSE chunk exceeded ${MAX_GOOGLE_SSE_CHUNK_BYTES} bytes`);
|
|
595
|
+
}
|
|
596
|
+
let decodedChunk;
|
|
597
|
+
try {
|
|
598
|
+
decodedChunk = decoder.decode(chunk, { stream: true });
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
throw invalidGoogleStream(context, "stream contained invalid UTF-8");
|
|
136
602
|
}
|
|
603
|
+
if (decodedChunk.length > MAX_GOOGLE_SSE_BUFFER_CODE_UNITS - buffer.length) {
|
|
604
|
+
throw invalidGoogleStream(context, `SSE buffer exceeded ${MAX_GOOGLE_SSE_BUFFER_CODE_UNITS} code units`);
|
|
605
|
+
}
|
|
606
|
+
buffer += decodedChunk;
|
|
607
|
+
let parsed;
|
|
608
|
+
try {
|
|
609
|
+
parsed = parseSseChunk(buffer);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
throw invalidGoogleStream(context, "SSE event framing was malformed");
|
|
613
|
+
}
|
|
614
|
+
buffer = parsed.remainder;
|
|
615
|
+
for (const event of parsed.events) {
|
|
616
|
+
yield* processEvent(event);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
let decodedTail;
|
|
620
|
+
try {
|
|
621
|
+
decodedTail = decoder.decode();
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
throw invalidGoogleStream(context, "stream contained invalid UTF-8");
|
|
137
625
|
}
|
|
626
|
+
if (decodedTail.length > MAX_GOOGLE_SSE_BUFFER_CODE_UNITS - buffer.length) {
|
|
627
|
+
throw invalidGoogleStream(context, `SSE buffer exceeded ${MAX_GOOGLE_SSE_BUFFER_CODE_UNITS} code units`);
|
|
628
|
+
}
|
|
629
|
+
buffer += decodedTail;
|
|
138
630
|
if (buffer.trim().length > 0) {
|
|
139
|
-
|
|
631
|
+
let parsed;
|
|
632
|
+
try {
|
|
633
|
+
parsed = parseFinalSseChunk(buffer);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
throw invalidGoogleStream(context, "trailing SSE event was malformed");
|
|
637
|
+
}
|
|
140
638
|
for (const event of parsed.events) {
|
|
141
|
-
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
usage = extractGoogleUsage(event) ?? usage;
|
|
639
|
+
yield* processEvent(event);
|
|
145
640
|
}
|
|
146
641
|
}
|
|
642
|
+
if (!sawCandidateEnvelope) {
|
|
643
|
+
throw invalidGoogleStream(context, "stream contained no candidate envelope");
|
|
644
|
+
}
|
|
645
|
+
if (!sawFinishReason && !sawDone) {
|
|
646
|
+
throw invalidGoogleStream(context, "stream ended before a terminal marker");
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
toolCallRegistry.assertSettled();
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
throw invalidGoogleStream(context, "candidate executable code had no matching execution result");
|
|
653
|
+
}
|
|
654
|
+
const contentFiltered = isContentFilterFinishReason(finishReason);
|
|
655
|
+
if (!sawOutput && !contentFiltered) {
|
|
656
|
+
throw invalidGoogleStream(context, "stream contained no supported output content");
|
|
657
|
+
}
|
|
147
658
|
if (reasoningId) {
|
|
148
659
|
yield {
|
|
149
660
|
type: "reasoning-end",
|
|
150
661
|
id: reasoningId,
|
|
662
|
+
...(reasoningSignature !== undefined ? { signature: reasoningSignature } : {}),
|
|
151
663
|
};
|
|
152
664
|
}
|
|
665
|
+
let providerMetadata;
|
|
666
|
+
try {
|
|
667
|
+
providerMetadata = createGoogleProviderMetadata(rawAssistantParts, groundingMetadata);
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
// The stream accounts for retained raw parts and correlation state under
|
|
671
|
+
// the same byte ceiling as replay metadata. The final owned snapshot also
|
|
672
|
+
// includes JSON container overhead and grounding metadata, so translate
|
|
673
|
+
// any remaining snapshot-boundary failure into the stream's typed error
|
|
674
|
+
// contract instead of leaking an implementation TypeError.
|
|
675
|
+
throw invalidGoogleStream(context, "provider metadata could not be retained safely");
|
|
676
|
+
}
|
|
153
677
|
yield {
|
|
154
678
|
type: "finish",
|
|
155
679
|
finishReason,
|
|
156
680
|
...(usage ? { usage } : {}),
|
|
681
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
157
682
|
};
|
|
158
683
|
}
|