@ai-sdk/google 4.0.93 → 4.0.95
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 +17 -0
- package/dist/index.d.ts +20 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -23
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +6 -3
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +182 -22
- package/dist/internal/index.js.map +1 -1
- package/docs/15-google.mdx +137 -0
- package/package.json +3 -3
- package/src/continue-google-stream.ts +75 -0
- package/src/convert-google-usage.ts +76 -0
- package/src/google-language-model-options.ts +12 -0
- package/src/google-language-model.ts +163 -31
- package/src/google-model-capabilities.ts +6 -6
- package/src/google-prompt.ts +18 -0
- package/src/interactions/google-interactions-language-model-options.ts +1 -0
- package/src/interactions/google-interactions-language-model.ts +6 -6
- package/src/map-google-finish-reason.ts +3 -0
package/docs/15-google.mdx
CHANGED
|
@@ -253,6 +253,12 @@ The following optional provider options are available for Google models:
|
|
|
253
253
|
[Priority inference](https://ai.google.dev/gemini-api/docs/priority-inference)
|
|
254
254
|
and [Flex inference](https://ai.google.dev/gemini-api/docs/flex-inference).
|
|
255
255
|
|
|
256
|
+
- **continuationToken** _string_
|
|
257
|
+
|
|
258
|
+
Optional. Resumes a response that stopped because it used up its
|
|
259
|
+
`maxOutputTokens` budget (Gemini 4 long decode). Send it with the original
|
|
260
|
+
request and a higher `maxOutputTokens`. See [Long decode](#long-decode).
|
|
261
|
+
|
|
256
262
|
- **threshold** _string_
|
|
257
263
|
|
|
258
264
|
Optional. Standalone threshold setting that can be used independently of `safetySettings`.
|
|
@@ -318,6 +324,136 @@ console.log(text);
|
|
|
318
324
|
console.log(reasoning); // Reasoning summary
|
|
319
325
|
```
|
|
320
326
|
|
|
327
|
+
### Long decode
|
|
328
|
+
|
|
329
|
+
Gemini 4 models such as `gemini-4-argon` can keep thinking and writing past a
|
|
330
|
+
single request's limits, up to the shared 1M token context window (input,
|
|
331
|
+
thinking, and output tokens together). Use it for hard math, science, proofs,
|
|
332
|
+
and long coding or agentic tasks.
|
|
333
|
+
|
|
334
|
+
- A single request generates at most 32,768 tokens without streaming and 65,536
|
|
335
|
+
tokens with streaming (`streamText`). Thinking tokens count towards this cap.
|
|
336
|
+
- `maxOutputTokens` is a **cumulative budget across requests**, not a target
|
|
337
|
+
length. When a request reaches the per-request cap while budget remains, the
|
|
338
|
+
provider automatically sends the same request again with a continuation token
|
|
339
|
+
and combines the results into one response. Set `maxOutputTokens` above the
|
|
340
|
+
per-request cap (up to 1M minus the input tokens) to enable this. If you do
|
|
341
|
+
not set it, the call ends after one request.
|
|
342
|
+
- A lower `maxOutputTokens` does not make the model think less, it ends the
|
|
343
|
+
response early (possibly before any answer) and you are still charged for the
|
|
344
|
+
tokens. Use `thinkingConfig.thinkingLevel` to control how much the model
|
|
345
|
+
thinks. `thinkingBudget` is not supported by these models.
|
|
346
|
+
- Complex requests can run for several minutes. Make sure your HTTP client
|
|
347
|
+
allows read timeouts of at least 15 to 20 minutes.
|
|
348
|
+
|
|
349
|
+
**Usage across requests.** Each continued request re-sends the prompt plus all
|
|
350
|
+
output so far as input, so its prompt token count includes everything
|
|
351
|
+
generated before it. `usage` is the sum of all requests, which matches what
|
|
352
|
+
Google bills (re-sent tokens are billed at a discount through implicit
|
|
353
|
+
caching). Do not use the summed input tokens for context window checks or
|
|
354
|
+
compaction: they can be several times larger than the real context. When a
|
|
355
|
+
response spanned more than one request,
|
|
356
|
+
`providerMetadata.google.initialPromptTokenCount` holds the prompt token count
|
|
357
|
+
of the first request (the google-genai SDKs expose the same value as
|
|
358
|
+
`initial_prompt_token_count`):
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
const result = await generateText({
|
|
362
|
+
model: google('gemini-4-argon'),
|
|
363
|
+
prompt,
|
|
364
|
+
maxOutputTokens: 500_000,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Billing: summed across all requests
|
|
368
|
+
console.log(result.usage.inputTokens.total);
|
|
369
|
+
|
|
370
|
+
// Context window: the first request's prompt plus the generated output
|
|
371
|
+
const promptTokens =
|
|
372
|
+
result.finalStep.providerMetadata?.google?.initialPromptTokenCount ??
|
|
373
|
+
result.usage.inputTokens.total;
|
|
374
|
+
const contextTokens = promptTokens + (result.usage.outputTokens.total ?? 0);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Resuming after the budget is used up.** When the cumulative generated tokens
|
|
378
|
+
reach `maxOutputTokens`, the finish reason is `length` and the provider returns
|
|
379
|
+
a continuation token on `result.providerMetadata.google.continuationToken`. To
|
|
380
|
+
continue, send the **original request unchanged** (same prompt, tools, and
|
|
381
|
+
settings; do not append the partial output as a message) together with the
|
|
382
|
+
token as `providerOptions.google.continuationToken` and a **higher**
|
|
383
|
+
`maxOutputTokens`. Resending with the same `maxOutputTokens` fails because the
|
|
384
|
+
budget is used up. Each response contains only the newly generated content.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { google } from '@ai-sdk/google';
|
|
388
|
+
import { generateText } from 'ai';
|
|
389
|
+
|
|
390
|
+
const prompt = 'Prove that sqrt(2) is irrational, in detail.';
|
|
391
|
+
|
|
392
|
+
const result = await generateText({
|
|
393
|
+
model: google('gemini-4-argon'),
|
|
394
|
+
prompt,
|
|
395
|
+
maxOutputTokens: 128_000,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const continuationToken =
|
|
399
|
+
result.finalStep.providerMetadata?.google?.continuationToken;
|
|
400
|
+
|
|
401
|
+
if (result.finishReason === 'length' && typeof continuationToken === 'string') {
|
|
402
|
+
// The budget was used up. Raise it to keep going.
|
|
403
|
+
const continued = await generateText({
|
|
404
|
+
model: google('gemini-4-argon'),
|
|
405
|
+
prompt,
|
|
406
|
+
maxOutputTokens: 256_000,
|
|
407
|
+
providerOptions: { google: { continuationToken } },
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
console.log(result.text + continued.text);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Things to keep in mind:
|
|
415
|
+
|
|
416
|
+
- **Dropped streams.** Streamed parts are forwarded as they arrive and are not
|
|
417
|
+
buffered between the server's checkpoints, so a stream that drops mid-request
|
|
418
|
+
is not resumed automatically. Retry the request from the start.
|
|
419
|
+
- **Provider metadata is per call.** If you store the conversation history,
|
|
420
|
+
combine the content of all calls into one model message only after the final
|
|
421
|
+
call finishes. Keep thought signatures unchanged, including the ones on empty
|
|
422
|
+
trailing text parts.
|
|
423
|
+
- **Zero Data Retention.** Continuation works with `store: false` because the
|
|
424
|
+
state travels encrypted inside the token.
|
|
425
|
+
|
|
426
|
+
#### Long decode with the Interactions API
|
|
427
|
+
|
|
428
|
+
With the [Interactions API](#interactions-api), long decode requires
|
|
429
|
+
`background: true`. The server runs every request itself, so no client-side
|
|
430
|
+
continuation is needed, and `usage` covers all requests. Set an explicit
|
|
431
|
+
`maxOutputTokens` above 65,536 (without it, you get a single 64K request) and
|
|
432
|
+
keep `store` enabled (the default). The provider polls the interaction (or
|
|
433
|
+
streams it, reconnecting when the connection drops) until it finishes.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
import { google } from '@ai-sdk/google';
|
|
437
|
+
import { generateText } from 'ai';
|
|
438
|
+
|
|
439
|
+
const result = await generateText({
|
|
440
|
+
model: google.interactions('gemini-4-argon'),
|
|
441
|
+
prompt: 'Prove that sqrt(2) is irrational, in detail.',
|
|
442
|
+
maxOutputTokens: 500_000,
|
|
443
|
+
providerOptions: {
|
|
444
|
+
google: {
|
|
445
|
+
background: true,
|
|
446
|
+
pollingTimeoutMs: 60 * 60 * 1000, // default: 30 minutes
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Without `background: true`, an interaction generates at most 65,536 tokens.
|
|
453
|
+
Requests with a higher `maxOutputTokens` are rejected by the API, and an
|
|
454
|
+
interaction that reaches the cap finishes with `length`. Long decode is not
|
|
455
|
+
available with the Vertex AI Interactions API.
|
|
456
|
+
|
|
321
457
|
### File Inputs
|
|
322
458
|
|
|
323
459
|
The Google provider supports file inputs, e.g. PDF files.
|
|
@@ -1059,6 +1195,7 @@ will still validate the response against the schema in `Output.object`.
|
|
|
1059
1195
|
| Model | Image Input | Object Generation | Tool Usage | Tool Streaming | Google Search | URL Context |
|
|
1060
1196
|
| ------------------------------------- | ----------- | ----------------- | ---------- | -------------- | ------------- | ----------- |
|
|
1061
1197
|
| `gemini-nano-banana-2.1` | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Check /> | <Cross /> |
|
|
1198
|
+
| `gemini-4-argon` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
1062
1199
|
| `gemini-3.8-flash` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
1063
1200
|
| `gemini-3.7-flash` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
1064
1201
|
| `gemini-3.6-flash` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/google",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.95",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ai-sdk/provider": "4.0.
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
38
|
+
"@ai-sdk/provider": "4.0.27",
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.59"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@ai-sdk/test-server": "2.0.4",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ParseResult } from '@ai-sdk/provider-utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Concatenates the event streams of a long decode response that spans several
|
|
5
|
+
* requests (Gemini 4 long decode).
|
|
6
|
+
*
|
|
7
|
+
* Events of the current stream are passed through unchanged. When an event
|
|
8
|
+
* signals that the per-request cap was reached while budget remains
|
|
9
|
+
* (`getContinuationToken` returns a token), the next request is sent with that
|
|
10
|
+
* token once the current stream ends, and its events are appended. If the
|
|
11
|
+
* continuation request fails, the error is emitted as a failed parse result
|
|
12
|
+
* and the stream ends.
|
|
13
|
+
*/
|
|
14
|
+
export function continueGoogleStream<T>({
|
|
15
|
+
stream,
|
|
16
|
+
getContinuationToken,
|
|
17
|
+
continueStream,
|
|
18
|
+
}: {
|
|
19
|
+
stream: ReadableStream<ParseResult<T>>;
|
|
20
|
+
getContinuationToken: (value: T) => string | undefined;
|
|
21
|
+
continueStream: (
|
|
22
|
+
continuationToken: string,
|
|
23
|
+
) => Promise<ReadableStream<ParseResult<T>>>;
|
|
24
|
+
}): ReadableStream<ParseResult<T>> {
|
|
25
|
+
let reader = stream.getReader();
|
|
26
|
+
let pendingContinuationToken: string | undefined;
|
|
27
|
+
|
|
28
|
+
return new ReadableStream<ParseResult<T>>({
|
|
29
|
+
async pull(controller) {
|
|
30
|
+
while (true) {
|
|
31
|
+
const { done, value } = await reader.read();
|
|
32
|
+
|
|
33
|
+
if (!done) {
|
|
34
|
+
if (value.success) {
|
|
35
|
+
const token = getContinuationToken(value.value);
|
|
36
|
+
if (token != null) {
|
|
37
|
+
pendingContinuationToken = token;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
controller.enqueue(value);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (pendingContinuationToken == null) {
|
|
45
|
+
controller.close();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const continuationToken = pendingContinuationToken;
|
|
50
|
+
pendingContinuationToken = undefined;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
reader = (await continueStream(continuationToken)).getReader();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Surfaced as an `error` stream part, followed by the `finish` part
|
|
56
|
+
// with the last continuation token so the caller can resume.
|
|
57
|
+
controller.enqueue({
|
|
58
|
+
success: false,
|
|
59
|
+
error: error as Extract<
|
|
60
|
+
ParseResult<T>,
|
|
61
|
+
{ success: false }
|
|
62
|
+
>['error'],
|
|
63
|
+
rawValue: undefined,
|
|
64
|
+
});
|
|
65
|
+
controller.close();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async cancel(reason) {
|
|
72
|
+
await reader.cancel(reason);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -50,3 +50,79 @@ export function convertGoogleUsage(
|
|
|
50
50
|
raw: usage,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sums the usage metadata of two requests, e.g. the hops of a long decode
|
|
56
|
+
* response that was continued automatically. Non-numeric fields are taken
|
|
57
|
+
* from the later request.
|
|
58
|
+
*/
|
|
59
|
+
export function addGoogleUsageMetadata<T extends GoogleUsageMetadata>(
|
|
60
|
+
a: T | undefined,
|
|
61
|
+
b: T | undefined,
|
|
62
|
+
): T | undefined {
|
|
63
|
+
if (a == null || b == null) {
|
|
64
|
+
return b ?? a;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
...a,
|
|
69
|
+
...b,
|
|
70
|
+
promptTokenCount: addCounts(a.promptTokenCount, b.promptTokenCount),
|
|
71
|
+
candidatesTokenCount: addCounts(
|
|
72
|
+
a.candidatesTokenCount,
|
|
73
|
+
b.candidatesTokenCount,
|
|
74
|
+
),
|
|
75
|
+
toolUsePromptTokenCount: addCounts(
|
|
76
|
+
a.toolUsePromptTokenCount,
|
|
77
|
+
b.toolUsePromptTokenCount,
|
|
78
|
+
),
|
|
79
|
+
totalTokenCount: addCounts(a.totalTokenCount, b.totalTokenCount),
|
|
80
|
+
cachedContentTokenCount: addCounts(
|
|
81
|
+
a.cachedContentTokenCount,
|
|
82
|
+
b.cachedContentTokenCount,
|
|
83
|
+
),
|
|
84
|
+
thoughtsTokenCount: addCounts(a.thoughtsTokenCount, b.thoughtsTokenCount),
|
|
85
|
+
promptTokensDetails: addTokenDetails(
|
|
86
|
+
a.promptTokensDetails,
|
|
87
|
+
b.promptTokensDetails,
|
|
88
|
+
),
|
|
89
|
+
cacheTokensDetails: addTokenDetails(
|
|
90
|
+
a.cacheTokensDetails,
|
|
91
|
+
b.cacheTokensDetails,
|
|
92
|
+
),
|
|
93
|
+
candidatesTokensDetails: addTokenDetails(
|
|
94
|
+
a.candidatesTokensDetails,
|
|
95
|
+
b.candidatesTokensDetails,
|
|
96
|
+
),
|
|
97
|
+
toolUsePromptTokensDetails: addTokenDetails(
|
|
98
|
+
a.toolUsePromptTokensDetails,
|
|
99
|
+
b.toolUsePromptTokensDetails,
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function addCounts(
|
|
105
|
+
a: number | null | undefined,
|
|
106
|
+
b: number | null | undefined,
|
|
107
|
+
): number | null | undefined {
|
|
108
|
+
return a == null && b == null ? (b ?? a) : (a ?? 0) + (b ?? 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addTokenDetails(
|
|
112
|
+
a: GoogleTokenDetail[] | null | undefined,
|
|
113
|
+
b: GoogleTokenDetail[] | null | undefined,
|
|
114
|
+
): GoogleTokenDetail[] | null | undefined {
|
|
115
|
+
if (a == null || b == null) {
|
|
116
|
+
return b ?? a;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const tokenCounts = new Map<string, number>();
|
|
120
|
+
for (const { modality, tokenCount } of [...a, ...b]) {
|
|
121
|
+
tokenCounts.set(modality, (tokenCounts.get(modality) ?? 0) + tokenCount);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Array.from(tokenCounts, ([modality, tokenCount]) => ({
|
|
125
|
+
modality,
|
|
126
|
+
tokenCount,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
@@ -35,6 +35,7 @@ export type GoogleModelId =
|
|
|
35
35
|
| 'gemini-3.6-flash'
|
|
36
36
|
| 'gemini-3.7-flash'
|
|
37
37
|
| 'gemini-3.8-flash'
|
|
38
|
+
| 'gemini-4-argon'
|
|
38
39
|
| 'gemini-nano-banana-2.1'
|
|
39
40
|
// latest version
|
|
40
41
|
// https://ai.google.dev/gemini-api/docs/models#latest
|
|
@@ -259,6 +260,17 @@ export const googleLanguageModelOptions = lazySchema(() =>
|
|
|
259
260
|
*/
|
|
260
261
|
serviceTier: z.enum(['standard', 'flex', 'priority']).optional(),
|
|
261
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Optional. Resumes a response that stopped with `MAX_TOKENS` because it
|
|
265
|
+
* used up its `maxOutputTokens` budget (Gemini 4 long decode). Pass the
|
|
266
|
+
* `continuationToken` from the previous response's provider metadata
|
|
267
|
+
* together with the original prompt and a higher `maxOutputTokens` (the
|
|
268
|
+
* budget is cumulative across requests). Requests that reach the
|
|
269
|
+
* per-request cap while budget remains (`CONTINUATION`) are continued
|
|
270
|
+
* automatically. Sent as the `continuationToken` body field.
|
|
271
|
+
*/
|
|
272
|
+
continuationToken: z.string().optional(),
|
|
273
|
+
|
|
262
274
|
/**
|
|
263
275
|
* Optional. Vertex AI only. Sent as the
|
|
264
276
|
* `X-Vertex-AI-LLM-Shared-Request-Type` request header to select a
|
|
@@ -36,7 +36,9 @@ import {
|
|
|
36
36
|
type Resolvable,
|
|
37
37
|
} from '@ai-sdk/provider-utils';
|
|
38
38
|
import { z } from 'zod/v4';
|
|
39
|
+
import { continueGoogleStream } from './continue-google-stream';
|
|
39
40
|
import {
|
|
41
|
+
addGoogleUsageMetadata,
|
|
40
42
|
convertGoogleUsage,
|
|
41
43
|
type GoogleUsageMetadata,
|
|
42
44
|
} from './convert-google-usage';
|
|
@@ -66,7 +68,7 @@ const configurableSafetySettingCategories = [
|
|
|
66
68
|
'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
|
67
69
|
] as const;
|
|
68
70
|
|
|
69
|
-
const gemini25ModelPattern = /(
|
|
71
|
+
const gemini25ModelPattern = /(^|[/.])gemini-2\.5(?:[.-]|$)/i;
|
|
70
72
|
|
|
71
73
|
const googleCloudStorageFunctionResponseUrls = {
|
|
72
74
|
'image/png': [/^gs:\/\/.*$/],
|
|
@@ -350,11 +352,11 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
350
352
|
},
|
|
351
353
|
});
|
|
352
354
|
|
|
353
|
-
const resolvedThinking =
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
const resolvedThinking =
|
|
356
|
+
googleOptions?.thinkingConfig?.thinkingBudget != null ||
|
|
357
|
+
googleOptions?.thinkingConfig?.thinkingLevel != null
|
|
358
|
+
? undefined
|
|
359
|
+
: resolveThinkingConfig({ reasoning, modelId, warnings });
|
|
358
360
|
const thinkingConfig =
|
|
359
361
|
googleOptions?.thinkingConfig || resolvedThinking
|
|
360
362
|
? { ...resolvedThinking, ...googleOptions?.thinkingConfig }
|
|
@@ -442,6 +444,9 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
442
444
|
cachedContent: googleOptions?.cachedContent,
|
|
443
445
|
labels: googleOptions?.labels,
|
|
444
446
|
serviceTier: bodyServiceTier,
|
|
447
|
+
...(googleOptions?.continuationToken != null && {
|
|
448
|
+
continuationToken: googleOptions.continuationToken,
|
|
449
|
+
}),
|
|
445
450
|
},
|
|
446
451
|
warnings: [...warnings, ...toolWarnings],
|
|
447
452
|
providerOptionsNames,
|
|
@@ -510,6 +515,7 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
510
515
|
providerOptionsNames,
|
|
511
516
|
toolNameMapping,
|
|
512
517
|
jsonResponseToolName,
|
|
518
|
+
initialPromptTokenCount,
|
|
513
519
|
}: {
|
|
514
520
|
config: GoogleLanguageModelConfig;
|
|
515
521
|
response: InferSchema<typeof responseSchema>;
|
|
@@ -517,6 +523,7 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
517
523
|
providerOptionsNames: readonly string[];
|
|
518
524
|
toolNameMapping?: ReturnType<typeof createToolNameMapping>;
|
|
519
525
|
jsonResponseToolName?: string;
|
|
526
|
+
initialPromptTokenCount?: number | null;
|
|
520
527
|
}): LanguageModelV4GenerateResult {
|
|
521
528
|
const wrapProviderMetadata = (payload: Record<string, unknown>) =>
|
|
522
529
|
Object.fromEntries(
|
|
@@ -712,6 +719,10 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
712
719
|
usageMetadata: usageMetadata ?? null,
|
|
713
720
|
finishMessage: candidate?.finishMessage ?? null,
|
|
714
721
|
serviceTier: usageMetadata?.serviceTier ?? null,
|
|
722
|
+
...(candidate?.continuationToken != null
|
|
723
|
+
? { continuationToken: candidate.continuationToken }
|
|
724
|
+
: {}),
|
|
725
|
+
...(initialPromptTokenCount != null ? { initialPromptTokenCount } : {}),
|
|
715
726
|
} satisfies GoogleProviderMetadata),
|
|
716
727
|
response: {
|
|
717
728
|
// TODO timestamp, model id
|
|
@@ -738,31 +749,88 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
738
749
|
extraHeaders,
|
|
739
750
|
);
|
|
740
751
|
|
|
741
|
-
const
|
|
752
|
+
const generateContent = (body: typeof args) =>
|
|
753
|
+
postJsonToApi({
|
|
754
|
+
url: `${this.config.baseURL}/${getModelPath(
|
|
755
|
+
this.modelId,
|
|
756
|
+
)}:generateContent`,
|
|
757
|
+
headers: mergedHeaders,
|
|
758
|
+
body,
|
|
759
|
+
failedResponseHandler: googleFailedResponseHandler,
|
|
760
|
+
successfulResponseHandler: createJsonResponseHandler(responseSchema),
|
|
761
|
+
abortSignal: options.abortSignal,
|
|
762
|
+
fetch: this.config.fetch,
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
let {
|
|
742
766
|
responseHeaders,
|
|
743
767
|
value: response,
|
|
744
768
|
rawValue: rawResponse,
|
|
745
|
-
} = await
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
769
|
+
} = await generateContent(args);
|
|
770
|
+
|
|
771
|
+
// Gemini 4 long decode: a request that reaches the per-request cap while
|
|
772
|
+
// `maxOutputTokens` budget remains finishes with `CONTINUATION`. Resend
|
|
773
|
+
// the same request with the continuation token (the server tracks the
|
|
774
|
+
// cumulative tokens) and combine the content of all requests.
|
|
775
|
+
//
|
|
776
|
+
// Usage is summed across requests (matching billing). Each request re-sends
|
|
777
|
+
// the prompt plus all output so far as input, so the first request's
|
|
778
|
+
// prompt token count is kept separately for context window checks.
|
|
779
|
+
const content: LanguageModelV4Content[] = [];
|
|
780
|
+
let usageMetadata: GoogleUsageMetadata | undefined;
|
|
781
|
+
const firstPromptTokenCount = response.usageMetadata?.promptTokenCount;
|
|
782
|
+
let isContinued = false;
|
|
783
|
+
|
|
784
|
+
while (true) {
|
|
785
|
+
const candidate = response.candidates?.[0];
|
|
786
|
+
const continuationToken =
|
|
787
|
+
candidate?.finishReason === 'CONTINUATION'
|
|
788
|
+
? candidate.continuationToken
|
|
789
|
+
: undefined;
|
|
790
|
+
|
|
791
|
+
usageMetadata = addGoogleUsageMetadata(
|
|
792
|
+
usageMetadata,
|
|
793
|
+
response.usageMetadata ?? undefined,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
if (continuationToken == null) {
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
756
799
|
|
|
757
|
-
|
|
800
|
+
content.push(
|
|
801
|
+
...GoogleLanguageModel.convertGenerateContentResponse({
|
|
802
|
+
config: this.config,
|
|
803
|
+
response,
|
|
804
|
+
warnings,
|
|
805
|
+
providerOptionsNames,
|
|
806
|
+
toolNameMapping,
|
|
807
|
+
jsonResponseToolName,
|
|
808
|
+
}).content,
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
isContinued = true;
|
|
812
|
+
({
|
|
813
|
+
responseHeaders,
|
|
814
|
+
value: response,
|
|
815
|
+
rawValue: rawResponse,
|
|
816
|
+
} = await generateContent({ ...args, continuationToken }));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const lastResult = GoogleLanguageModel.convertGenerateContentResponse({
|
|
758
820
|
config: this.config,
|
|
759
|
-
response,
|
|
821
|
+
response: { ...response, usageMetadata },
|
|
760
822
|
warnings,
|
|
761
823
|
providerOptionsNames,
|
|
762
824
|
toolNameMapping,
|
|
763
825
|
jsonResponseToolName,
|
|
826
|
+
initialPromptTokenCount: isContinued ? firstPromptTokenCount : undefined,
|
|
764
827
|
});
|
|
765
828
|
|
|
829
|
+
const result = {
|
|
830
|
+
...lastResult,
|
|
831
|
+
content: [...content, ...lastResult.content],
|
|
832
|
+
};
|
|
833
|
+
|
|
766
834
|
return {
|
|
767
835
|
...result,
|
|
768
836
|
request: { body: args },
|
|
@@ -796,16 +864,37 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
796
864
|
extraHeaders,
|
|
797
865
|
);
|
|
798
866
|
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
this.
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
867
|
+
const streamGenerateContent = (body: typeof args) =>
|
|
868
|
+
postJsonToApi({
|
|
869
|
+
url: `${this.config.baseURL}/${getModelPath(
|
|
870
|
+
this.modelId,
|
|
871
|
+
)}:streamGenerateContent?alt=sse`,
|
|
872
|
+
headers,
|
|
873
|
+
body,
|
|
874
|
+
failedResponseHandler: googleFailedResponseHandler,
|
|
875
|
+
successfulResponseHandler:
|
|
876
|
+
createEventSourceResponseHandler(chunkSchema),
|
|
877
|
+
abortSignal: options.abortSignal,
|
|
878
|
+
fetch: this.config.fetch,
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
const { responseHeaders, value: firstResponse } =
|
|
882
|
+
await streamGenerateContent(args);
|
|
883
|
+
|
|
884
|
+
// Gemini 4 long decode: when a stream reaches the per-request cap while
|
|
885
|
+
// `maxOutputTokens` budget remains, its last chunk finishes with
|
|
886
|
+
// `CONTINUATION`. Resend the same request with the continuation token and
|
|
887
|
+
// append the new stream, which contains only newly generated content.
|
|
888
|
+
const response = continueGoogleStream({
|
|
889
|
+
stream: firstResponse,
|
|
890
|
+
getContinuationToken: chunk => {
|
|
891
|
+
const candidate = chunk.candidates?.[0];
|
|
892
|
+
return candidate?.finishReason === 'CONTINUATION'
|
|
893
|
+
? (candidate.continuationToken ?? undefined)
|
|
894
|
+
: undefined;
|
|
895
|
+
},
|
|
896
|
+
continueStream: async continuationToken =>
|
|
897
|
+
(await streamGenerateContent({ ...args, continuationToken })).value,
|
|
809
898
|
});
|
|
810
899
|
|
|
811
900
|
let finishReason: LanguageModelV4FinishReason = {
|
|
@@ -813,11 +902,16 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
813
902
|
raw: undefined,
|
|
814
903
|
};
|
|
815
904
|
let usage: GoogleUsageMetadata | undefined = undefined;
|
|
905
|
+
// Usage of the completed requests of an automatically continued response.
|
|
906
|
+
let continuedUsage: GoogleUsageMetadata | undefined = undefined;
|
|
907
|
+
// Prompt token count of the first request, for context window checks.
|
|
908
|
+
let initialPromptTokenCount: number | null | undefined = undefined;
|
|
816
909
|
let promptFeedback: PromptFeedbackSchema | null = null;
|
|
817
910
|
let lastGroundingMetadata: GroundingMetadataSchema | null = null;
|
|
818
911
|
let lastUrlContextMetadata: UrlContextMetadataSchema | null = null;
|
|
819
912
|
let lastSafetyRatings: SafetyRatingSchema[] | null = null;
|
|
820
913
|
let lastFinishMessage: string | null = null;
|
|
914
|
+
let lastContinuationToken: string | null = null;
|
|
821
915
|
let confirmedPromptBlockReason: string | undefined;
|
|
822
916
|
|
|
823
917
|
const generateId = this.config.generateId;
|
|
@@ -947,6 +1041,9 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
947
1041
|
if (candidate.finishMessage != null) {
|
|
948
1042
|
lastFinishMessage = candidate.finishMessage;
|
|
949
1043
|
}
|
|
1044
|
+
if (candidate.continuationToken != null) {
|
|
1045
|
+
lastContinuationToken = candidate.continuationToken;
|
|
1046
|
+
}
|
|
950
1047
|
}
|
|
951
1048
|
|
|
952
1049
|
// A confirmed prompt block is terminal for generated content, but
|
|
@@ -1322,6 +1419,16 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
1322
1419
|
}
|
|
1323
1420
|
}
|
|
1324
1421
|
|
|
1422
|
+
if (candidate.finishReason === 'CONTINUATION') {
|
|
1423
|
+
// Usage metadata is per request. The next request reports its
|
|
1424
|
+
// own usage, so bank the usage of this one.
|
|
1425
|
+
if (continuedUsage == null) {
|
|
1426
|
+
initialPromptTokenCount = usage?.promptTokenCount;
|
|
1427
|
+
}
|
|
1428
|
+
continuedUsage = addGoogleUsageMetadata(continuedUsage, usage);
|
|
1429
|
+
usage = undefined;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1325
1432
|
if (candidate.finishReason != null) {
|
|
1326
1433
|
finishReason = {
|
|
1327
1434
|
unified: mapGoogleFinishReason({
|
|
@@ -1334,6 +1441,8 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
1334
1441
|
},
|
|
1335
1442
|
|
|
1336
1443
|
flush(controller) {
|
|
1444
|
+
usage = addGoogleUsageMetadata(continuedUsage, usage);
|
|
1445
|
+
|
|
1337
1446
|
if (currentTextBlockId !== null) {
|
|
1338
1447
|
controller.enqueue({
|
|
1339
1448
|
type: 'text-end',
|
|
@@ -1359,6 +1468,17 @@ export class GoogleLanguageModel implements LanguageModelV4 {
|
|
|
1359
1468
|
usageMetadata: usage ?? null,
|
|
1360
1469
|
finishMessage: lastFinishMessage,
|
|
1361
1470
|
serviceTier: usage?.serviceTier ?? null,
|
|
1471
|
+
// Only surface the token when the response stopped early
|
|
1472
|
+
// (budget used up, or continuing failed), not for the
|
|
1473
|
+
// checkpoint tokens of a stream that finished normally.
|
|
1474
|
+
...(lastContinuationToken != null &&
|
|
1475
|
+
(finishReason.raw === 'MAX_TOKENS' ||
|
|
1476
|
+
finishReason.raw === 'CONTINUATION')
|
|
1477
|
+
? { continuationToken: lastContinuationToken }
|
|
1478
|
+
: {}),
|
|
1479
|
+
...(initialPromptTokenCount != null
|
|
1480
|
+
? { initialPromptTokenCount }
|
|
1481
|
+
: {}),
|
|
1362
1482
|
} satisfies GoogleProviderMetadata),
|
|
1363
1483
|
});
|
|
1364
1484
|
},
|
|
@@ -1460,12 +1580,22 @@ function resolveGemini3ThinkingConfig({
|
|
|
1460
1580
|
function getMinimumThinkingLevelForGemini3Model(
|
|
1461
1581
|
modelId: string,
|
|
1462
1582
|
): 'minimal' | 'low' {
|
|
1463
|
-
const modelName =
|
|
1583
|
+
const modelName = /(?:^|[/.])(gemini-[^/]*)$/i
|
|
1584
|
+
.exec(modelId)?.[1]
|
|
1585
|
+
.toLowerCase();
|
|
1464
1586
|
|
|
1465
1587
|
if (modelName === 'gemini-flash-latest') {
|
|
1466
1588
|
return 'low';
|
|
1467
1589
|
}
|
|
1468
1590
|
|
|
1591
|
+
const proVersionMatch = /^gemini-(\d+)(?:\.\d+)?-pro(?:$|-)/.exec(
|
|
1592
|
+
modelName ?? '',
|
|
1593
|
+
);
|
|
1594
|
+
|
|
1595
|
+
if (Number(proVersionMatch?.[1]) >= 3) {
|
|
1596
|
+
return 'low';
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1469
1599
|
const versionMatch = /^gemini-(\d+)\.(\d+)-flash(?:$|-(?!lite(?:-|$)))/.exec(
|
|
1470
1600
|
modelName ?? '',
|
|
1471
1601
|
);
|
|
@@ -1835,6 +1965,7 @@ export const responseSchema = lazySchema(() =>
|
|
|
1835
1965
|
content: getContentSchema().nullish().or(z.object({}).strict()),
|
|
1836
1966
|
finishReason: z.string().nullish(),
|
|
1837
1967
|
finishMessage: z.string().nullish(),
|
|
1968
|
+
continuationToken: z.string().nullish(),
|
|
1838
1969
|
safetyRatings: z.array(getSafetyRatingSchema()).nullish(),
|
|
1839
1970
|
groundingMetadata: getGroundingMetadataSchema().nullish(),
|
|
1840
1971
|
urlContextMetadata: getUrlContextMetadataSchema().nullish(),
|
|
@@ -1888,6 +2019,7 @@ const chunkSchema = lazySchema(() =>
|
|
|
1888
2019
|
content: getContentSchema().nullish(),
|
|
1889
2020
|
finishReason: z.string().nullish(),
|
|
1890
2021
|
finishMessage: z.string().nullish(),
|
|
2022
|
+
continuationToken: z.string().nullish(),
|
|
1891
2023
|
safetyRatings: z.array(getSafetyRatingSchema()).nullish(),
|
|
1892
2024
|
groundingMetadata: getGroundingMetadataSchema().nullish(),
|
|
1893
2025
|
urlContextMetadata: getUrlContextMetadataSchema().nullish(),
|