@ai-sdk/google 4.0.94 → 4.0.96

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.
@@ -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';
@@ -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 postJsonToApi({
746
- url: `${this.config.baseURL}/${getModelPath(
747
- this.modelId,
748
- )}:generateContent`,
749
- headers: mergedHeaders,
750
- body: args,
751
- failedResponseHandler: googleFailedResponseHandler,
752
- successfulResponseHandler: createJsonResponseHandler(responseSchema),
753
- abortSignal: options.abortSignal,
754
- fetch: this.config.fetch,
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;
756
790
 
757
- const result = GoogleLanguageModel.convertGenerateContentResponse({
791
+ usageMetadata = addGoogleUsageMetadata(
792
+ usageMetadata,
793
+ response.usageMetadata ?? undefined,
794
+ );
795
+
796
+ if (continuationToken == null) {
797
+ break;
798
+ }
799
+
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 { responseHeaders, value: response } = await postJsonToApi({
800
- url: `${this.config.baseURL}/${getModelPath(
801
- this.modelId,
802
- )}:streamGenerateContent?alt=sse`,
803
- headers,
804
- body: args,
805
- failedResponseHandler: googleFailedResponseHandler,
806
- successfulResponseHandler: createEventSourceResponseHandler(chunkSchema),
807
- abortSignal: options.abortSignal,
808
- fetch: this.config.fetch,
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
  },
@@ -1845,6 +1965,7 @@ export const responseSchema = lazySchema(() =>
1845
1965
  content: getContentSchema().nullish().or(z.object({}).strict()),
1846
1966
  finishReason: z.string().nullish(),
1847
1967
  finishMessage: z.string().nullish(),
1968
+ continuationToken: z.string().nullish(),
1848
1969
  safetyRatings: z.array(getSafetyRatingSchema()).nullish(),
1849
1970
  groundingMetadata: getGroundingMetadataSchema().nullish(),
1850
1971
  urlContextMetadata: getUrlContextMetadataSchema().nullish(),
@@ -1898,6 +2019,7 @@ const chunkSchema = lazySchema(() =>
1898
2019
  content: getContentSchema().nullish(),
1899
2020
  finishReason: z.string().nullish(),
1900
2021
  finishMessage: z.string().nullish(),
2022
+ continuationToken: z.string().nullish(),
1901
2023
  safetyRatings: z.array(getSafetyRatingSchema()).nullish(),
1902
2024
  groundingMetadata: getGroundingMetadataSchema().nullish(),
1903
2025
  urlContextMetadata: getUrlContextMetadataSchema().nullish(),
@@ -100,4 +100,22 @@ export interface GoogleProviderMetadata {
100
100
  finishReason?: string | null;
101
101
  finishMessage: string | null;
102
102
  serviceTier: string | null;
103
+
104
+ /**
105
+ * Opaque token returned when a response stops with `MAX_TOKENS` because it
106
+ * used up its `maxOutputTokens` budget. Pass it back as
107
+ * `providerOptions.google.continuationToken` together with a higher
108
+ * `maxOutputTokens` and the original prompt to resume generation.
109
+ */
110
+ continuationToken?: string | null;
111
+
112
+ /**
113
+ * Prompt token count of the first request of a long decode response that
114
+ * was continued automatically (Gemini 4). Usage is summed across requests
115
+ * to match billing, and each continued request re-sends the prompt plus all
116
+ * output so far as input. Use this value instead of the summed input tokens
117
+ * for context window checks and compaction. Only present when the response
118
+ * spanned more than one request.
119
+ */
120
+ initialPromptTokenCount?: number | null;
103
121
  }
@@ -37,6 +37,7 @@ export type GoogleInteractionsModelId =
37
37
  | 'gemini-3.6-flash'
38
38
  | 'gemini-3.7-flash'
39
39
  | 'gemini-3.8-flash'
40
+ | 'gemini-4-argon'
40
41
  | 'gemini-nano-banana-2.1'
41
42
  | 'lyria-3-clip-preview'
42
43
  | 'lyria-3-pro-preview'
@@ -482,7 +482,7 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV4 {
482
482
  async doGenerate(
483
483
  options: LanguageModelV4CallOptions,
484
484
  ): Promise<LanguageModelV4GenerateResult> {
485
- const { args, warnings, isAgent, pollingTimeoutMs } =
485
+ const { args, warnings, isAgent, isBackground, pollingTimeoutMs } =
486
486
  await this.getArgs(options);
487
487
 
488
488
  const url = `${this.config.baseURL}/interactions`;
@@ -511,12 +511,12 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV4 {
511
511
  } = postResult;
512
512
 
513
513
  /*
514
- * Agent calls may return a non-terminal status (`in_progress` /
515
- * `requires_action`) when invoked with `background: true`. Poll
516
- * `GET /interactions/{id}` until terminal so the user-facing surface
517
- * matches a synchronous call.
514
+ * Agent calls and `background: true` calls (e.g. Gemini 4 long decode,
515
+ * where the server runs every hop) may return a non-terminal status
516
+ * (`in_progress` / `requires_action`). Poll `GET /interactions/{id}` until
517
+ * terminal so the user-facing surface matches a synchronous call.
518
518
  */
519
- if (isAgent && !isTerminalStatus(response.status)) {
519
+ if ((isAgent || isBackground) && !isTerminalStatus(response.status)) {
520
520
  const polled = await pollGoogleInteractionUntilTerminal({
521
521
  baseURL: this.config.baseURL,
522
522
  interactionId: response.id,
@@ -11,6 +11,9 @@ export function mapGoogleFinishReason({
11
11
  case 'STOP':
12
12
  return hasToolCalls ? 'tool-calls' : 'stop';
13
13
  case 'MAX_TOKENS':
14
+ // Gemini 4 long decode: the per-request cap was reached while budget
15
+ // remains. Continued automatically; only surfaced if continuing fails.
16
+ case 'CONTINUATION':
14
17
  return 'length';
15
18
  case 'IMAGE_SAFETY':
16
19
  case 'RECITATION':